update
This commit is contained in:
@@ -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"
|
||||
@@ -125,20 +126,21 @@ func (h *Handler) Start(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
derivedRaw, _ := json.Marshal(derived)
|
||||
knowledgeRaw, err := snapshotKnowledge(h.db, p.TenantID, sopItem.ScenarioID)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "KNOWLEDGE_SNAPSHOT_FAILED", "生成知识快照失败")
|
||||
return
|
||||
}
|
||||
externalRef := input.ExternalRef
|
||||
if externalRef == "" {
|
||||
externalRef = "run-" + uuid.NewString()
|
||||
}
|
||||
run := model.SOPRun{TenantID: p.TenantID, SOPID: input.SOPID, OperatorID: p.UserID, ExternalRef: externalRef, CurrentNodeKey: sop.StartNodeKey, Status: "running", Answers: datatypes.JSON(initialAnswers), Input: datatypes.JSON(initialAnswers), Derived: datatypes.JSON(derivedRaw), Outputs: datatypes.JSON([]byte(`[]`)), KnowledgeSnapshot: datatypes.JSON(knowledgeRaw), Result: "", StartedAt: time.Now()}
|
||||
scriptStateRaw, _ := json.Marshal(scriptkit.ScriptState{ScriptAnswers: map[string][]string{}, DimensionSelects: map[string]bool{}})
|
||||
run := model.SOPRun{TenantID: p.TenantID, SOPID: input.SOPID, OperatorID: p.UserID, ExternalRef: externalRef, CurrentNodeKey: sop.StartNodeKey, Status: "running", Answers: datatypes.JSON(initialAnswers), Input: datatypes.JSON(initialAnswers), Derived: datatypes.JSON(derivedRaw), Outputs: datatypes.JSON([]byte(`[]`)), ScriptState: datatypes.JSON(scriptStateRaw), Result: "", StartedAt: time.Now()}
|
||||
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
|
||||
}
|
||||
}
|
||||
payload, err := json.Marshal(gin.H{"source": "scenario_input", "mapped_field_keys": sortedKeys(normalizedInput), "matched_rule_keys": matchedRules})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -235,16 +237,27 @@ func (h *Handler) Options(c *gin.Context) {
|
||||
response.OK(c, gin.H{"sops": sops, "operators": operators})
|
||||
}
|
||||
|
||||
// answerInput accepts both legacy node answers and stage script answers.
|
||||
type answerInput struct {
|
||||
NodeKey string `json:"node_key"`
|
||||
Answers map[string]interface{} `json:"answers"`
|
||||
// stage-specific fields
|
||||
ScriptKey string `json:"script_key"`
|
||||
OptionKeys []string `json:"option_keys"`
|
||||
Value string `json:"value"`
|
||||
ScriptAnswers []stageScriptAnswerInput `json:"script_answers"`
|
||||
DimensionSelects map[string]bool `json:"dimension_selects"`
|
||||
DimensionValueKey string `json:"dimension_value_key"`
|
||||
Selected *bool `json:"selected"`
|
||||
}
|
||||
|
||||
func (h *Handler) Answer(c *gin.Context) {
|
||||
p, _ := auth.PrincipalFromContext(c)
|
||||
id, ok := runID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var input struct {
|
||||
NodeKey string `json:"node_key"`
|
||||
Answers map[string]interface{} `json:"answers"`
|
||||
}
|
||||
var input answerInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "回答格式不正确")
|
||||
return
|
||||
@@ -267,6 +280,9 @@ func (h *Handler) Answer(c *gin.Context) {
|
||||
if err := tx.Where("sop_id = ? AND node_key = ? AND tenant_id = ?", updated.SOPID, updated.CurrentNodeKey, p.TenantID).First(¤tNode).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if currentNode.Type == "stage" {
|
||||
return applyStageAnswer(tx, &updated, currentNode, stageAnswerInput{NodeKey: input.NodeKey, ScriptKey: input.ScriptKey, OptionKeys: input.OptionKeys, Value: input.Value, ScriptAnswers: input.ScriptAnswers, DimensionSelects: input.DimensionSelects, Answers: input.Answers, DimensionValueKey: input.DimensionValueKey, Selected: input.Selected})
|
||||
}
|
||||
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 = ?", updated.SOPID, p.TenantID).Order("sf.sort_order, sf.id").Find(&fields).Error; err != nil {
|
||||
return err
|
||||
@@ -281,53 +297,16 @@ func (h *Handler) Answer(c *gin.Context) {
|
||||
for key, value := range input.Answers {
|
||||
answers[key] = value
|
||||
}
|
||||
if err := validateKnowledgeSelections(currentNode, updated, answers); err != nil {
|
||||
return err
|
||||
}
|
||||
var edges []model.SOPEdge
|
||||
if err := tx.Where("sop_id = ? AND source_node_key = ? AND tenant_id = ?", updated.SOPID, updated.CurrentNodeKey, p.TenantID).Order("priority, id").Find(&edges).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
sortEdges(edges)
|
||||
nextKey := ""
|
||||
for _, edge := range edges {
|
||||
matched, matchErr := matchCondition(json.RawMessage(edge.Condition), answers)
|
||||
if matchErr != nil {
|
||||
return matchErr
|
||||
}
|
||||
if matched {
|
||||
nextKey = edge.TargetNodeKey
|
||||
break
|
||||
}
|
||||
}
|
||||
if nextKey == "" {
|
||||
return errors.New("没有满足条件的下一节点")
|
||||
}
|
||||
answerBytes, _ := json.Marshal(answers)
|
||||
payload, _ := json.Marshal(input)
|
||||
if err := tx.Create(&model.SOPRunEvent{TenantID: p.TenantID, RunID: updated.ID, NodeKey: updated.CurrentNodeKey, Action: "answer", Payload: datatypes.JSON(payload)}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var next model.SOPNode
|
||||
if err := tx.Where("sop_id = ? AND node_key = ? AND tenant_id = ?", updated.SOPID, nextKey, p.TenantID).First(&next).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
updates := map[string]interface{}{"current_node_key": nextKey, "answers": datatypes.JSON(answerBytes)}
|
||||
if next.Type == "finish" || next.Type == "escalate" {
|
||||
now := time.Now()
|
||||
updates["status"] = "completed"
|
||||
updates["completed_at"] = &now
|
||||
updates["result"] = next.Type
|
||||
updated.Status = "completed"
|
||||
updated.CompletedAt = &now
|
||||
updated.Result = next.Type
|
||||
}
|
||||
if err := tx.Model(&updated).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
updated.CurrentNodeKey = nextKey
|
||||
updated.Answers = datatypes.JSON(answerBytes)
|
||||
return tx.Create(&model.SOPRunEvent{TenantID: p.TenantID, RunID: updated.ID, NodeKey: nextKey, Action: "enter", Payload: datatypes.JSON([]byte(`{}`))}).Error
|
||||
if err := tx.Model(&updated).Update("answers", updated.Answers).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return advanceRunNode(tx, &updated)
|
||||
})
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusUnprocessableEntity, "ADVANCE_FAILED", err.Error())
|
||||
@@ -336,6 +315,127 @@ func (h *Handler) Answer(c *gin.Context) {
|
||||
h.respondRun(c, updated)
|
||||
}
|
||||
|
||||
// Next advances a stage run to the next node after checking stage completion.
|
||||
func (h *Handler) Next(c *gin.Context) {
|
||||
p, _ := auth.PrincipalFromContext(c)
|
||||
id, ok := runID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var updated model.SOPRun
|
||||
err := h.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND tenant_id = ?", id, p.TenantID).First(&updated).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if updated.Status != "running" {
|
||||
return errors.New("run is not active")
|
||||
}
|
||||
if !canOperateRun(p, updated) {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
var currentNode model.SOPNode
|
||||
if err := tx.Where("sop_id = ? AND node_key = ? AND tenant_id = ?", updated.SOPID, updated.CurrentNodeKey, p.TenantID).First(¤tNode).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if currentNode.Type == "stage" {
|
||||
if err := advanceFromStage(tx, &updated, currentNode); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if currentNode.Type == "question" || currentNode.Type == "choice" || currentNode.Type == "form" {
|
||||
return errors.New("当前节点需要先提交表单")
|
||||
} else {
|
||||
if err := advanceRunNode(tx, &updated); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Create(&model.SOPRunEvent{TenantID: p.TenantID, RunID: updated.ID, NodeKey: updated.CurrentNodeKey, Action: "next", Payload: datatypes.JSON([]byte(`{}`))}).Error
|
||||
})
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusUnprocessableEntity, "ADVANCE_FAILED", err.Error())
|
||||
return
|
||||
}
|
||||
h.respondRun(c, updated)
|
||||
}
|
||||
|
||||
// Back moves the run to the previous node.
|
||||
func (h *Handler) Back(c *gin.Context) {
|
||||
p, _ := auth.PrincipalFromContext(c)
|
||||
id, ok := runID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var updated model.SOPRun
|
||||
err := h.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND tenant_id = ?", id, p.TenantID).First(&updated).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if updated.Status != "running" {
|
||||
return errors.New("run is not active")
|
||||
}
|
||||
if !canOperateRun(p, updated) {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
if err := backRunNode(tx, &updated); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&model.SOPRunEvent{TenantID: p.TenantID, RunID: updated.ID, NodeKey: updated.CurrentNodeKey, Action: "back", Payload: datatypes.JSON([]byte(`{}`))}).Error
|
||||
})
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusUnprocessableEntity, "ADVANCE_FAILED", err.Error())
|
||||
return
|
||||
}
|
||||
h.respondRun(c, updated)
|
||||
}
|
||||
|
||||
// ScriptFeedback records like/report/unreasonable feedback on a script.
|
||||
func (h *Handler) ScriptFeedback(c *gin.Context) {
|
||||
p, _ := auth.PrincipalFromContext(c)
|
||||
id, ok := runID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var input struct {
|
||||
NodeKey string `json:"node_key"`
|
||||
ScriptKey string `json:"script_key" binding:"required"`
|
||||
FeedbackType string `json:"feedback_type" binding:"required"`
|
||||
Note string `json:"note" binding:"max=2000"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "话术反馈格式不正确")
|
||||
return
|
||||
}
|
||||
if input.FeedbackType != "like" && input.FeedbackType != "report" && input.FeedbackType != "unreasonable" {
|
||||
response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "话术反馈类型不正确")
|
||||
return
|
||||
}
|
||||
var run model.SOPRun
|
||||
if err := h.db.Where("id = ? AND tenant_id = ?", id, p.TenantID).First(&run).Error; err != nil || !canViewRun(p, run) {
|
||||
response.Error(c, http.StatusNotFound, "NOT_FOUND", "执行记录不存在")
|
||||
return
|
||||
}
|
||||
pkg, err := loadRunPackage(h.db, run)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusNotFound, "NOT_FOUND", "场景没有配置话术包")
|
||||
return
|
||||
}
|
||||
script, ok := pkg.ScriptByKey(input.ScriptKey)
|
||||
if !ok {
|
||||
response.Error(c, http.StatusNotFound, "NOT_FOUND", "话术不存在")
|
||||
return
|
||||
}
|
||||
item := model.ScriptFeedback{TenantID: p.TenantID, RunID: run.ID, StageID: script.StageID, ScriptID: script.ID, FeedbackType: input.FeedbackType, OperatorID: p.UserID, Note: input.Note}
|
||||
if err := h.db.Create(&item).Error; err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "SAVE_FAILED", "保存话术反馈失败")
|
||||
return
|
||||
}
|
||||
payload, _ := json.Marshal(input)
|
||||
if err := h.db.Create(&model.SOPRunEvent{TenantID: p.TenantID, RunID: run.ID, NodeKey: input.NodeKey, Action: "script_feedback", Payload: datatypes.JSON(payload)}).Error; err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "SAVE_FAILED", "保存话术反馈失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"recorded": true})
|
||||
}
|
||||
|
||||
func sortEdges(edges []model.SOPEdge) {
|
||||
sort.SliceStable(edges, func(i, j int) bool {
|
||||
leftDefault := defaultCondition(edges[i].Condition)
|
||||
@@ -368,6 +468,40 @@ func defaultCondition(raw []byte) bool {
|
||||
return ok && len(object) == 0
|
||||
}
|
||||
|
||||
func (h *Handler) RecordScriptUsage(c *gin.Context) {
|
||||
p, _ := auth.PrincipalFromContext(c)
|
||||
id, ok := runID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var input struct {
|
||||
NodeKey string `json:"node_key"`
|
||||
ScriptKey string `json:"script_key"`
|
||||
ScriptTitle string `json:"script_title"`
|
||||
Template string `json:"template"`
|
||||
Scene string `json:"scene"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "话术使用记录格式不正确")
|
||||
return
|
||||
}
|
||||
var run model.SOPRun
|
||||
if err := h.db.Where("id = ? AND tenant_id = ?", id, p.TenantID).First(&run).Error; err != nil {
|
||||
response.Error(c, http.StatusNotFound, "NOT_FOUND", "执行记录不存在")
|
||||
return
|
||||
}
|
||||
if !canOperateRun(p, run) {
|
||||
response.Error(c, http.StatusNotFound, "NOT_FOUND", "执行记录不存在")
|
||||
return
|
||||
}
|
||||
payload, _ := json.Marshal(input)
|
||||
if err := h.db.Create(&model.SOPRunEvent{TenantID: p.TenantID, RunID: run.ID, NodeKey: input.NodeKey, Action: "script_used", Payload: datatypes.JSON(payload)}).Error; err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "SAVE_FAILED", "保存话术使用记录失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"recorded": true})
|
||||
}
|
||||
|
||||
func (h *Handler) Finish(c *gin.Context) {
|
||||
p, _ := auth.PrincipalFromContext(c)
|
||||
id, ok := runID(c)
|
||||
@@ -492,20 +626,12 @@ func (h *Handler) respondRun(c *gin.Context, item model.SOPRun) {
|
||||
response.Error(c, http.StatusInternalServerError, "NODE_NOT_FOUND", "当前流程节点不存在")
|
||||
return
|
||||
}
|
||||
answers := map[string]interface{}{}
|
||||
_ = json.Unmarshal(item.Answers, &answers)
|
||||
derived := map[string]interface{}{}
|
||||
_ = json.Unmarshal(item.Derived, &derived)
|
||||
input := map[string]interface{}{}
|
||||
_ = json.Unmarshal(item.Input, &input)
|
||||
context := runtimeContext(input, derived, answers)
|
||||
context["__knowledge_snapshot"] = json.RawMessage(item.KnowledgeSnapshot)
|
||||
nodeView, err := h.nodeView(h.db, node, item.TenantID, context)
|
||||
nodeView, err := nodeView(h.db, item, node)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "KNOWLEDGE_NOT_FOUND", "当前节点关联的历史知识内容不存在")
|
||||
response.Error(c, http.StatusInternalServerError, "STAGE_FAILED", "生成当前阶段内容失败")
|
||||
return
|
||||
}
|
||||
outputs, err := buildScenarioOutputs(h.db, item, nodeView.Outputs)
|
||||
outputs, err := buildScenarioOutputs(h.db, item)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "OUTPUT_FAILED", "生成场景输出失败")
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user