package run import ( "crypto/sha256" "encoding/hex" "encoding/json" "errors" "net/http" "strings" "time" "git.iwork-ai.com/xdc/iqudo-top1/internal/audit" "git.iwork-ai.com/xdc/iqudo-top1/internal/auth" "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" "github.com/gin-gonic/gin" "github.com/google/uuid" "gorm.io/datatypes" "gorm.io/gorm" "gorm.io/gorm/clause" ) var ( errPublicRunCompleted = errors.New("执行记录已经结束") errPublicNodeChanged = errors.New("当前步骤已经变化") errPublicInputNeeded = errors.New("当前节点需要提交表单") ) func (h *Handler) PublicStart(c *gin.Context) { var body struct { PublicKey string `json:"public_key" binding:"required"` SOPID uint64 `json:"sop_id"` Input map[string]interface{} `json:"input"` InitialValues map[string]interface{} `json:"initial_values"` ExternalRef string `json:"external_ref"` } if err := c.ShouldBindJSON(&body); err != nil { response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "公开场景参数不完整") return } var scenario model.Scenario if err := h.db.Where("scenario_key = ? AND public_key = ? AND status <> ?", c.Param("scenarioKey"), body.PublicKey, "archived").First(&scenario).Error; err != nil { response.Error(c, http.StatusNotFound, "NOT_FOUND", "公开场景不存在") return } query := h.db.Where("scenario_id = ? AND tenant_id = ? AND status = ?", scenario.ID, scenario.TenantID, "published") if body.SOPID != 0 { query = query.Where("id = ?", body.SOPID) } var sop model.SOP if err := query.Order("updated_at DESC").First(&sop).Error; err != nil { response.Error(c, http.StatusNotFound, "SOP_NOT_FOUND", "场景没有可执行的 SOP") return } if body.ExternalRef != "" { var existing model.SOPRun if err := h.db.Where("tenant_id = ? AND sop_id = ? AND external_ref = ?", scenario.TenantID, sop.ID, body.ExternalRef).First(&existing).Error; err == nil { token := uuid.NewString() + uuid.NewString() if err := h.db.Create(&model.PublicRunSession{TenantID: scenario.TenantID, RunID: existing.ID, TokenHash: publicTokenHash(token), ExpiresAt: time.Now().Add(24 * time.Hour)}).Error; err != nil { response.Error(c, http.StatusInternalServerError, "SESSION_FAILED", "创建 SDK 会话失败") return } h.respondPublicRun(c, existing, token) return } } var version model.SOPVersion if err := h.db.Where("sop_id = ? AND tenant_id = ? AND status = ?", sop.ID, scenario.TenantID, "published").Order("version DESC").First(&version).Error; err != nil { response.Error(c, http.StatusNotFound, "SOP_NOT_FOUND", "场景没有可执行的 SOP") return } fields := make([]model.ScenarioField, 0) if err := h.db.Where("scenario_id = ? AND tenant_id = ?", scenario.ID, scenario.TenantID).Order("sort_order, id").Find(&fields).Error; err != nil { response.Error(c, http.StatusInternalServerError, "QUERY_FAILED", "查询场景契约失败") return } normalized := mergeValues(mapScenarioInput(fields, body.Input), body.InitialValues) if err := validateInitialAnswers(fields, normalized); err != nil { response.Error(c, http.StatusUnprocessableEntity, "INVALID_INPUT", err.Error()) return } raw, _ := json.Marshal(normalized) derived, matchedRules, err := deriveForScenario(h.db, scenario.TenantID, scenario.ID, normalized) if err != nil { response.Error(c, http.StatusUnprocessableEntity, "RULE_EVALUATION_FAILED", err.Error()) 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, SOPVersionID: version.ID, OperatorID: scenario.CreatedBy, ExternalRef: externalRef, CurrentNodeKey: version.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()} 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 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 } payload, _ := json.Marshal(gin.H{"source": "public_sdk", "mapped_field_keys": sortedKeys(normalized), "matched_rule_keys": matchedRules}) if err := tx.Create(&model.SOPRunEvent{TenantID: scenario.TenantID, RunID: run.ID, NodeKey: run.CurrentNodeKey, Action: "start", Payload: datatypes.JSON(payload)}).Error; err != nil { return err } return audit.RecordTx(tx, auth.Principal{TenantID: scenario.TenantID, UserID: scenario.CreatedBy}, "start", "sop_run", run.ID, gin.H{"source": "public_sdk"}) }) if err != nil { if body.ExternalRef != "" { var existing model.SOPRun if findErr := h.db.Where("tenant_id = ? AND sop_id = ? AND external_ref = ?", scenario.TenantID, sop.ID, body.ExternalRef).First(&existing).Error; findErr == nil { token = uuid.NewString() + uuid.NewString() if sessionErr := h.db.Create(&model.PublicRunSession{TenantID: scenario.TenantID, RunID: existing.ID, TokenHash: publicTokenHash(token), ExpiresAt: time.Now().Add(24 * time.Hour)}).Error; sessionErr == nil { h.respondPublicRun(c, existing, token) return } } } response.Error(c, http.StatusInternalServerError, "START_FAILED", "启动公开场景失败") return } h.respondPublicRun(c, run, token) } func (h *Handler) PublicCurrent(c *gin.Context) { session, run, ok := h.publicSession(c) if !ok { return } _ = session h.respondPublicRun(c, run, "") } func (h *Handler) PublicSubmit(c *gin.Context) { session, _, ok := h.publicSession(c) if !ok { return } var body struct { NodeKey string `json:"node_key"` Answers map[string]interface{} `json:"answers"` } 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_version_id = ? AND node_key = ? AND tenant_id = ?", run.SOPVersionID, run.CurrentNodeKey, run.TenantID).First(&node).Error; err != nil { return err } 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 } if err := validateNodeAnswers(node, fields, body.Answers); err != nil { return err } answers := map[string]interface{}{} _ = json.Unmarshal(run.Answers, &answers) for key, value := range body.Answers { answers[key] = value } answerRaw, _ := json.Marshal(answers) run.Answers = datatypes.JSON(answerRaw) 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 { return err } if run.Status == "completed" { return audit.RecordTx(tx, auth.Principal{TenantID: run.TenantID, UserID: run.OperatorID}, "finish", "sop_run", run.ID, gin.H{"source": "public_sdk", "result": run.Result}) } return nil }) 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 { 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 } var node model.SOPNode if err := tx.Where("sop_version_id = ? AND node_key = ? AND tenant_id = ?", run.SOPVersionID, run.CurrentNodeKey, run.TenantID).First(&node).Error; err != nil { return err } 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 { return err } return advancePublicRun(tx, &run) }) if err != nil { h.respondPublicMutationError(c, err, "推进节点失败") return } h.respondPublicRun(c, run, "") } func (h *Handler) PublicFinish(c *gin.Context) { session, _, ok := h.publicSession(c) if !ok { return } var body struct { Result string `json:"result"` FinalResult json.RawMessage `json:"final_result"` } 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" && run.Status != "completed" { return errPublicRunCompleted } if body.Result == "" { body.Result = run.Result if body.Result == "" { body.Result = "completed" } } finalResult, err := parseFinalResult(body.FinalResult) if err != nil { return err } var scenario model.Scenario if err := tx.Table("scenarios sc").Select("sc.*").Joins("JOIN sops s ON s.scenario_id = sc.id").Where("s.id = ? AND sc.tenant_id = ?", run.SOPID, run.TenantID).First(&scenario).Error; err != nil { return err } schema, err := resultcontract.ParseAndValidate(scenario.ResultSchema) if err != nil { return err } if err := resultcontract.ValidateResult(schema, finalResult); err != nil { return err } finalRaw, _ := json.Marshal(finalResult) now := time.Now() completedAt := run.CompletedAt if completedAt == nil { completedAt = &now } payload, _ := json.Marshal(gin.H{"result": body.Result, "final_result": finalResult}) if err := tx.Create(&model.SOPRunEvent{TenantID: run.TenantID, RunID: run.ID, NodeKey: run.CurrentNodeKey, Action: "finish", Payload: datatypes.JSON(payload)}).Error; err != nil { return err } if err := tx.Model(&run).Updates(map[string]interface{}{"status": "completed", "result": body.Result, "final_result": datatypes.JSON(finalRaw), "completed_at": completedAt}).Error; err != nil { return err } run.Status, run.Result, run.FinalResult, run.CompletedAt = "completed", body.Result, datatypes.JSON(finalRaw), completedAt return audit.RecordTx(tx, auth.Principal{TenantID: run.TenantID, UserID: run.OperatorID}, "finish", "sop_run", run.ID, gin.H{"source": "public_sdk", "result": body.Result}) }) if err != nil { h.respondPublicMutationError(c, err, "结束执行失败") return } h.respondPublicRun(c, run, "") } func (h *Handler) PublicReset(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 } var version model.SOPVersion if err := tx.Where("id = ? AND tenant_id = ?", run.SOPVersionID, run.TenantID).First(&version).Error; err != nil { return err } run.CurrentNodeKey, run.Status, run.Result, run.FinalResult, run.CompletedAt, run.Answers = version.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 { return err } 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 } return tx.Create(&model.SOPRunEvent{TenantID: run.TenantID, RunID: run.ID, NodeKey: run.CurrentNodeKey, Action: "enter", Payload: datatypes.JSON([]byte(`{}`))}).Error }) if err != nil { h.respondPublicMutationError(c, err, "重置执行失败") return } h.respondPublicRun(c, run, "") } func advancePublicRun(tx *gorm.DB, run *model.SOPRun) error { var edges []model.SOPEdge if err := tx.Where("sop_version_id = ? AND source_node_key = ? AND tenant_id = ?", run.SOPVersionID, 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_version_id = ? AND node_key = ? AND tenant_id = ?", run.SOPVersionID, 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 } func (h *Handler) respondPublicMutationError(c *gin.Context, err error, fallback string) { switch { case errors.Is(err, errPublicRunCompleted): response.Error(c, http.StatusConflict, "RUN_COMPLETED", err.Error()) case errors.Is(err, errPublicNodeChanged): response.Error(c, http.StatusConflict, "NODE_CHANGED", err.Error()) case errors.Is(err, errPublicInputNeeded): response.Error(c, http.StatusUnprocessableEntity, "INPUT_REQUIRED", err.Error()) case errors.Is(err, gorm.ErrRecordNotFound): response.Error(c, http.StatusNotFound, "NOT_FOUND", "执行记录或流程节点不存在") default: response.Error(c, http.StatusUnprocessableEntity, "ADVANCE_FAILED", fallback+": "+err.Error()) } } func (h *Handler) respondPublicRun(c *gin.Context, run model.SOPRun, token string) { var node model.SOPNode if err := h.db.Where("sop_version_id = ? AND node_key = ? AND tenant_id = ?", run.SOPVersionID, run.CurrentNodeKey, run.TenantID).First(&node).Error; err != nil { 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) if err != nil { response.Error(c, http.StatusInternalServerError, "OUTPUT_FAILED", "生成节点输出失败") return } view.Config = nil outputs, err := buildScenarioOutputs(h.db, run, view.Outputs) if err != nil { response.Error(c, http.StatusInternalServerError, "OUTPUT_FAILED", "生成场景输出失败") return } if raw, marshalErr := json.Marshal(outputs); marshalErr == nil { run.Outputs = datatypes.JSON(raw) _ = h.db.Model(&model.SOPRun{}).Where("id = ?", run.ID).Update("outputs", run.Outputs).Error } var scenario model.Scenario if err := h.db.Table("scenarios sc").Select("sc.*").Joins("JOIN sops s ON s.scenario_id = sc.id").Where("s.id = ? AND sc.tenant_id = ?", run.SOPID, run.TenantID).First(&scenario).Error; err != nil { response.Error(c, http.StatusInternalServerError, "OUTPUT_FAILED", "读取场景结果格式失败") return } data := gin.H{"run_id": run.ID, "external_ref": run.ExternalRef, "status": run.Status, "final_result": json.RawMessage(run.FinalResult), "result_schema": json.RawMessage(scenario.ResultSchema), "node": view, "outputs": outputs} if token != "" { data["session_token"] = token } response.OK(c, data) } func parseFinalResult(raw json.RawMessage) (map[string]interface{}, error) { if len(raw) == 0 || string(raw) == "null" { return nil, nil } var result map[string]interface{} if err := json.Unmarshal(raw, &result); err != nil || result == nil { return nil, errors.New("final_result 必须是对象或 null") } return result, nil } func (h *Handler) publicSession(c *gin.Context) (model.PublicRunSession, model.SOPRun, bool) { raw := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ") var session model.PublicRunSession if raw == "" || h.db.Where("token_hash = ? AND expires_at > ?", publicTokenHash(raw), time.Now()).First(&session).Error != nil { response.Error(c, http.StatusUnauthorized, "INVALID_SESSION", "SDK 会话无效或已过期") return session, model.SOPRun{}, false } var run model.SOPRun if err := h.db.Where("id = ? AND tenant_id = ?", c.Param("id"), session.TenantID).First(&run).Error; err != nil || run.ID != session.RunID { response.Error(c, http.StatusNotFound, "NOT_FOUND", "执行记录不存在") return session, run, false } return session, run, true } func publicTokenHash(value string) string { sum := sha256.Sum256([]byte(value)) return hex.EncodeToString(sum[:]) }