535 lines
21 KiB
Go
535 lines
21 KiB
Go
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"
|
|
"git.iwork-ai.com/xdc/iqudo-top1/internal/scriptkit"
|
|
"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
|
|
}
|
|
}
|
|
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)
|
|
externalRef := body.ExternalRef
|
|
if externalRef == "" {
|
|
externalRef = "run-" + uuid.NewString()
|
|
}
|
|
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
|
|
}
|
|
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_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
|
|
}
|
|
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)
|
|
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 := advanceRunNode(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, "")
|
|
}
|
|
|
|
// 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 {
|
|
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_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 err := advanceFromStage(tx, &run, node); err != nil {
|
|
return err
|
|
}
|
|
} else if node.Type == "question" || node.Type == "choice" || node.Type == "form" {
|
|
return errPublicInputNeeded
|
|
} else if err := advanceRunNode(tx, &run); err != nil {
|
|
return err
|
|
}
|
|
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, "推进节点失败")
|
|
return
|
|
}
|
|
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 {
|
|
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 sop model.SOP
|
|
if err := tx.Where("id = ? AND tenant_id = ?", run.SOPID, run.TenantID).First(&sop).Error; err != nil {
|
|
return err
|
|
}
|
|
run.CurrentNodeKey, run.Status, run.Result, run.FinalResult, run.CompletedAt, run.Answers = sop.StartNodeKey, "running", "", nil, nil, run.Input
|
|
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
|
|
}
|
|
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 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_id = ? AND node_key = ? AND tenant_id = ?", run.SOPID, run.CurrentNodeKey, run.TenantID).First(&node).Error; err != nil {
|
|
response.Error(c, http.StatusInternalServerError, "NODE_NOT_FOUND", "当前流程节点不存在")
|
|
return
|
|
}
|
|
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)
|
|
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[:])
|
|
}
|