This commit is contained in:
Eric 1549169735@qq.com
2026-09-13 20:08:13 +08:00
parent 7cc06976ab
commit da3f16e6db
65 changed files with 3855 additions and 2153 deletions

View File

@@ -14,6 +14,7 @@ import (
"git.iwork-ai.com/xdc/iqudo-top1/internal/model"
"git.iwork-ai.com/xdc/iqudo-top1/internal/response"
"git.iwork-ai.com/xdc/iqudo-top1/internal/resultcontract"
"git.iwork-ai.com/xdc/iqudo-top1/internal/scriptkit"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/datatypes"
@@ -82,21 +83,22 @@ func (h *Handler) PublicStart(c *gin.Context) {
return
}
derivedRaw, _ := json.Marshal(derived)
knowledgeRaw, err := snapshotKnowledge(h.db, scenario.TenantID, scenario.ID)
if err != nil {
response.Error(c, http.StatusInternalServerError, "KNOWLEDGE_SNAPSHOT_FAILED", "生成知识快照失败")
return
}
externalRef := body.ExternalRef
if externalRef == "" {
externalRef = "run-" + uuid.NewString()
}
run := model.SOPRun{TenantID: scenario.TenantID, SOPID: sop.ID, OperatorID: scenario.CreatedBy, ExternalRef: externalRef, CurrentNodeKey: sop.StartNodeKey, Status: "running", Answers: datatypes.JSON(raw), Input: datatypes.JSON(raw), Derived: datatypes.JSON(derivedRaw), Outputs: datatypes.JSON([]byte(`[]`)), KnowledgeSnapshot: datatypes.JSON(knowledgeRaw), StartedAt: time.Now()}
scriptStateRaw, _ := json.Marshal(scriptkit.ScriptState{ScriptAnswers: map[string][]string{}, DimensionSelects: map[string]bool{}})
run := model.SOPRun{TenantID: scenario.TenantID, SOPID: sop.ID, OperatorID: scenario.CreatedBy, ExternalRef: externalRef, CurrentNodeKey: sop.StartNodeKey, Status: "running", Answers: datatypes.JSON(raw), Input: datatypes.JSON(raw), Derived: datatypes.JSON(derivedRaw), Outputs: datatypes.JSON([]byte(`[]`)), ScriptState: datatypes.JSON(scriptStateRaw), StartedAt: time.Now()}
token := uuid.NewString() + uuid.NewString()
err = h.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&run).Error; err != nil {
return err
}
if pkg, loadErr := loadRunPackage(tx, run); loadErr == nil {
if persistErr := persistRunWeights(tx, run, pkg, runWeights(run, pkg)); persistErr != nil {
return persistErr
}
}
if err := tx.Create(&model.PublicRunSession{TenantID: scenario.TenantID, RunID: run.ID, TokenHash: publicTokenHash(token), ExpiresAt: time.Now().Add(24 * time.Hour)}).Error; err != nil {
return err
}
@@ -160,6 +162,14 @@ func (h *Handler) PublicSubmit(c *gin.Context) {
if err := tx.Where("sop_id = ? AND node_key = ? AND tenant_id = ?", run.SOPID, run.CurrentNodeKey, run.TenantID).First(&node).Error; err != nil {
return err
}
if node.Type == "stage" {
if len(body.Answers) > 0 {
if err := applyStageAnswer(tx, &run, node, stageAnswerInput{NodeKey: body.NodeKey, Answers: body.Answers}); err != nil {
return err
}
}
return advanceFromStage(tx, &run, node)
}
fields := make([]model.ScenarioField, 0)
if err := tx.Table("scenario_fields sf").Joins("JOIN sops s ON s.scenario_id = sf.scenario_id").Where("s.id = ? AND sf.tenant_id = ?", run.SOPID, run.TenantID).Find(&fields).Error; err != nil {
return err
@@ -174,11 +184,14 @@ func (h *Handler) PublicSubmit(c *gin.Context) {
}
answerRaw, _ := json.Marshal(answers)
run.Answers = datatypes.JSON(answerRaw)
if err := tx.Model(&run).Update("answers", run.Answers).Error; err != nil {
return err
}
payload, _ := json.Marshal(body.Answers)
if err := tx.Create(&model.SOPRunEvent{TenantID: run.TenantID, RunID: run.ID, NodeKey: run.CurrentNodeKey, Action: "answer", Payload: datatypes.JSON(payload)}).Error; err != nil {
return err
}
if err := advancePublicRun(tx, &run); err != nil {
if err := advanceRunNode(tx, &run); err != nil {
return err
}
if run.Status == "completed" {
@@ -193,6 +206,44 @@ func (h *Handler) PublicSubmit(c *gin.Context) {
h.respondPublicRun(c, run, "")
}
// PublicAnswer records one stage script/dimension answer without advancing.
func (h *Handler) PublicAnswer(c *gin.Context) {
session, _, ok := h.publicSession(c)
if !ok {
return
}
var body stageAnswerInput
if err := c.ShouldBindJSON(&body); err != nil {
response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "回答格式不正确")
return
}
var run model.SOPRun
err := h.db.Transaction(func(tx *gorm.DB) error {
if err := lockPublicRun(tx, session, &run); err != nil {
return err
}
if run.Status != "running" {
return errPublicRunCompleted
}
if body.NodeKey != "" && body.NodeKey != run.CurrentNodeKey {
return errPublicNodeChanged
}
var node model.SOPNode
if err := tx.Where("sop_id = ? AND node_key = ? AND tenant_id = ?", run.SOPID, run.CurrentNodeKey, run.TenantID).First(&node).Error; err != nil {
return err
}
if node.Type != "stage" {
return errors.New("当前节点不是话术阶段")
}
return applyStageAnswer(tx, &run, node, body)
})
if err != nil {
h.respondPublicMutationError(c, err, "保存回答失败")
return
}
h.respondPublicRun(c, run, "")
}
func (h *Handler) PublicNext(c *gin.Context) {
session, _, ok := h.publicSession(c)
if !ok {
@@ -210,13 +261,16 @@ func (h *Handler) PublicNext(c *gin.Context) {
if err := tx.Where("sop_id = ? AND node_key = ? AND tenant_id = ?", run.SOPID, run.CurrentNodeKey, run.TenantID).First(&node).Error; err != nil {
return err
}
if node.Type == "question" || node.Type == "choice" || node.Type == "form" {
if node.Type == "stage" {
if err := advanceFromStage(tx, &run, node); err != nil {
return err
}
} else if node.Type == "question" || node.Type == "choice" || node.Type == "form" {
return errPublicInputNeeded
}
if err := tx.Create(&model.SOPRunEvent{TenantID: run.TenantID, RunID: run.ID, NodeKey: run.CurrentNodeKey, Action: "next", Payload: datatypes.JSON([]byte(`{}`))}).Error; err != nil {
} else if err := advanceRunNode(tx, &run); err != nil {
return err
}
return advancePublicRun(tx, &run)
return tx.Create(&model.SOPRunEvent{TenantID: run.TenantID, RunID: run.ID, NodeKey: run.CurrentNodeKey, Action: "next", Payload: datatypes.JSON([]byte(`{}`))}).Error
})
if err != nil {
h.respondPublicMutationError(c, err, "推进节点失败")
@@ -225,6 +279,76 @@ func (h *Handler) PublicNext(c *gin.Context) {
h.respondPublicRun(c, run, "")
}
// PublicBack moves the run to the previous node.
func (h *Handler) PublicBack(c *gin.Context) {
session, _, ok := h.publicSession(c)
if !ok {
return
}
var run model.SOPRun
err := h.db.Transaction(func(tx *gorm.DB) error {
if err := lockPublicRun(tx, session, &run); err != nil {
return err
}
if run.Status != "running" {
return errPublicRunCompleted
}
if err := backRunNode(tx, &run); err != nil {
return err
}
return tx.Create(&model.SOPRunEvent{TenantID: run.TenantID, RunID: run.ID, NodeKey: run.CurrentNodeKey, Action: "back", Payload: datatypes.JSON([]byte(`{}`))}).Error
})
if err != nil {
h.respondPublicMutationError(c, err, "返回上一步失败")
return
}
h.respondPublicRun(c, run, "")
}
// PublicFeedback records like/report/unreasonable feedback on a script.
func (h *Handler) PublicFeedback(c *gin.Context) {
session, run, ok := h.publicSession(c)
if !ok {
return
}
var body struct {
NodeKey string `json:"node_key"`
ScriptKey string `json:"script_key" binding:"required"`
FeedbackType string `json:"feedback_type" binding:"required"`
Note string `json:"note"`
}
if err := c.ShouldBindJSON(&body); err != nil {
response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "话术反馈格式不正确")
return
}
if body.FeedbackType != "like" && body.FeedbackType != "report" && body.FeedbackType != "unreasonable" {
response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "话术反馈类型不正确")
return
}
pkg, err := loadRunPackage(h.db, run)
if err != nil {
response.Error(c, http.StatusNotFound, "NOT_FOUND", "场景没有配置话术包")
return
}
script, found := pkg.ScriptByKey(body.ScriptKey)
if !found {
response.Error(c, http.StatusNotFound, "NOT_FOUND", "话术不存在")
return
}
item := model.ScriptFeedback{TenantID: run.TenantID, RunID: run.ID, StageID: script.StageID, ScriptID: script.ID, FeedbackType: body.FeedbackType, OperatorID: run.OperatorID, Note: body.Note}
if err := h.db.Create(&item).Error; err != nil {
response.Error(c, http.StatusInternalServerError, "SAVE_FAILED", "保存话术反馈失败")
return
}
payload, _ := json.Marshal(body)
if err := h.db.Create(&model.SOPRunEvent{TenantID: run.TenantID, RunID: run.ID, NodeKey: body.NodeKey, Action: "script_feedback", Payload: datatypes.JSON(payload)}).Error; err != nil {
response.Error(c, http.StatusInternalServerError, "SAVE_FAILED", "保存话术反馈失败")
return
}
_ = session
response.OK(c, gin.H{"recorded": true})
}
func (h *Handler) PublicFinish(c *gin.Context) {
session, _, ok := h.publicSession(c)
if !ok {
@@ -305,9 +429,15 @@ func (h *Handler) PublicReset(c *gin.Context) {
return err
}
run.CurrentNodeKey, run.Status, run.Result, run.FinalResult, run.CompletedAt, run.Answers = sop.StartNodeKey, "running", "", nil, nil, run.Input
if err := tx.Model(&run).Updates(map[string]interface{}{"current_node_key": run.CurrentNodeKey, "status": run.Status, "result": run.Result, "final_result": nil, "completed_at": nil, "answers": run.Input, "outputs": datatypes.JSON([]byte(`[]`))}).Error; err != nil {
run.ScriptState = datatypes.JSON([]byte(`{"script_answers":{},"dimension_selects":{}}`))
if err := tx.Model(&run).Updates(map[string]interface{}{"current_node_key": run.CurrentNodeKey, "status": run.Status, "result": run.Result, "final_result": nil, "completed_at": nil, "answers": run.Input, "outputs": datatypes.JSON([]byte(`[]`)), "script_state": run.ScriptState}).Error; err != nil {
return err
}
if pkg, loadErr := loadRunPackage(tx, run); loadErr == nil {
if persistErr := persistRunWeights(tx, run, pkg, runWeights(run, pkg)); persistErr != nil {
return persistErr
}
}
if err := tx.Create(&model.SOPRunEvent{TenantID: run.TenantID, RunID: run.ID, NodeKey: run.CurrentNodeKey, Action: "reset", Payload: datatypes.JSON([]byte(`{}`))}).Error; err != nil {
return err
}
@@ -320,58 +450,6 @@ func (h *Handler) PublicReset(c *gin.Context) {
h.respondPublicRun(c, run, "")
}
func advancePublicRun(tx *gorm.DB, run *model.SOPRun) error {
var edges []model.SOPEdge
if err := tx.Where("sop_id = ? AND source_node_key = ? AND tenant_id = ?", run.SOPID, run.CurrentNodeKey, run.TenantID).Order("priority,id").Find(&edges).Error; err != nil {
return err
}
answers := map[string]interface{}{}
_ = json.Unmarshal(run.Answers, &answers)
derived := map[string]interface{}{}
_ = json.Unmarshal(run.Derived, &derived)
input := map[string]interface{}{}
_ = json.Unmarshal(run.Input, &input)
context := runtimeContext(input, derived, answers)
sortEdges(edges)
nextKey := ""
for _, edge := range edges {
matched, err := matchCondition(json.RawMessage(edge.Condition), context)
if err != nil {
return err
}
if matched {
nextKey = edge.TargetNodeKey
break
}
}
if nextKey == "" {
return errors.New("没有满足条件的下一节点")
}
var next model.SOPNode
if err := tx.Where("sop_id = ? AND node_key = ? AND tenant_id = ?", run.SOPID, nextKey, run.TenantID).First(&next).Error; err != nil {
return err
}
updates := map[string]interface{}{"current_node_key": nextKey}
if next.Type == "finish" || next.Type == "escalate" {
now := time.Now()
updates["status"] = "completed"
updates["completed_at"] = &now
updates["result"] = next.Type
run.Status, run.CompletedAt, run.Result = "completed", &now, next.Type
}
if err := tx.Model(run).Updates(updates).Error; err != nil {
return err
}
run.CurrentNodeKey = nextKey
if err := tx.Create(&model.SOPRunEvent{TenantID: run.TenantID, RunID: run.ID, NodeKey: nextKey, Action: "enter", Payload: datatypes.JSON([]byte(`{}`))}).Error; err != nil {
return err
}
if run.Status == "completed" {
return tx.Create(&model.SOPRunEvent{TenantID: run.TenantID, RunID: run.ID, NodeKey: nextKey, Action: "finish", Payload: datatypes.JSON([]byte(`{"source":"terminal_node"}`))}).Error
}
return nil
}
func lockPublicRun(tx *gorm.DB, session model.PublicRunSession, run *model.SOPRun) error {
return tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND tenant_id = ?", session.RunID, session.TenantID).First(run).Error
}
@@ -397,21 +475,13 @@ func (h *Handler) respondPublicRun(c *gin.Context, run model.SOPRun, token strin
response.Error(c, http.StatusInternalServerError, "NODE_NOT_FOUND", "当前流程节点不存在")
return
}
answers := map[string]interface{}{}
_ = json.Unmarshal(run.Answers, &answers)
derived := map[string]interface{}{}
_ = json.Unmarshal(run.Derived, &derived)
input := map[string]interface{}{}
_ = json.Unmarshal(run.Input, &input)
context := runtimeContext(input, derived, answers)
context["__knowledge_snapshot"] = json.RawMessage(run.KnowledgeSnapshot)
view, err := h.nodeView(h.db, node, run.TenantID, context)
view, err := nodeView(h.db, run, node)
if err != nil {
response.Error(c, http.StatusInternalServerError, "OUTPUT_FAILED", "生成节点输出失败")
return
}
view.Config = nil
outputs, err := buildScenarioOutputs(h.db, run, view.Outputs)
outputs, err := buildScenarioOutputs(h.db, run)
if err != nil {
response.Error(c, http.StatusInternalServerError, "OUTPUT_FAILED", "生成场景输出失败")
return