package coordinator import ( "context" "fmt" "strconv" "strings" "chat-agent-system/internal/contracts/vocabulary" "chat-agent-system/internal/intent" ) // handleDocGovReopenRequest 须 approved;卡片含原因 → PendingAction reopen_request func (e *CoordinatorEngine) handleDocGovReopenRequest(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) { doc, gate := e.resolveTargetDocGate(ctx, cand, userCtx) if gate != nil { return gate, nil } reason := strings.TrimSpace(cand.FilledSlots[intent.SlotReopenReason]) if reason == "" { return &CoordinationResult{ Status: StatusClarificationRequired, Intent: cand.Intent, Branch: cand.Branch, ClarificationPrompt: "请补充重开原因后再申请。", NaturalReply: "已定位文档,但缺少重开原因。请说明为何需要再修订。", UpdatedContext: userCtx, }, nil } state := doc.State if state == "" && userCtx != nil && userCtx.ToSnapshot() != nil { state = vocabulary.DocumentState(userCtx.ToSnapshot().DocLifecycleState) } if state != "" && state != vocabulary.StateApproved { return &CoordinationResult{ Status: StatusRejected, Intent: cand.Intent, Branch: cand.Branch, RejectReason: "REOPEN_STATE_DENIED", NaturalReply: fmt.Sprintf("当前文档处于「%s」阶段,仅已批准(approved)文档可申请重开。", state), UpdatedContext: userCtx, }, nil } role := effectiveDocRole(userCtx) title := displayDocTitle(doc) card, exp := buildConfirmCard( "申请受控文档重开确认", doc.DocID, title, "", reason, "确认后将提交重开申请;批准人优先审批(创建人兜底),服务端二次校验职责权限。", "confirm_reopen_request", "确认申请重开", map[string]string{ "role": role, "reopen_reason": reason, "task_key": doc.TaskKey, }, ) userCtx.applyCard(card) userCtx.applyPending(exp) return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: fmt.Sprintf("已为《%s》起草重开申请确认单。原因:%s。请确认或回复「确认」。", title, reason), InteractiveCard: card, NextExpectation: exp, UpdatedContext: userCtx, }, nil } // handleDocGovReopenApprove 列 pending 重开申请 → 卡片 → PendingAction func (e *CoordinatorEngine) handleDocGovReopenApprove(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) { projectID := int64(0) if userCtx != nil { projectID = parseDecimalID(userCtx.Project.ID) } var items []ReopenListItem if e != nil && e.reopenLister != nil { var err error items, err = e.reopenLister.ListPendingReopens(ctx, projectID) if err != nil { return &CoordinationResult{ Status: StatusRejected, Intent: cand.Intent, Branch: cand.Branch, RejectReason: "REOPEN_LIST_FAILED", NaturalReply: fmt.Sprintf("无法列出待批重开申请:%v", err), UpdatedContext: userCtx, }, nil } } reqID := int64(0) if cand.FilledSlots != nil { if raw := strings.TrimSpace(cand.FilledSlots["request_id"]); raw != "" { reqID, _ = strconv.ParseInt(raw, 10, 64) } } if reqID == 0 && len(items) == 1 { reqID = items[0].RequestID } if reqID == 0 { if len(items) == 0 { return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: "当前没有待批准的重开申请。", UpdatedContext: userCtx, }, nil } var b strings.Builder b.WriteString("请指定要批准的重开申请编号(回复 request_id=N):\n") for _, it := range items { b.WriteString(fmt.Sprintf("- #%d 文档任务=%v 原因=%s\n", it.RequestID, it.DocTasks, it.Reason)) } return &CoordinationResult{ Status: StatusClarificationRequired, Intent: cand.Intent, Branch: cand.Branch, ClarificationPrompt: b.String(), NaturalReply: b.String(), UpdatedContext: userCtx, }, nil } reason := "" docID := "" for _, it := range items { if it.RequestID == reqID { reason = it.Reason if len(it.DocTasks) > 0 { docID = it.DocTasks[0] } break } } if docID == "" { doc, gate := e.resolveTargetDocGate(ctx, cand, userCtx) if gate != nil && doc == nil { // still allow approve by request id alone docID = fmt.Sprintf("reopen#%d", reqID) } else if doc != nil { docID = doc.DocID } else { docID = fmt.Sprintf("reopen#%d", reqID) } } title := docID proposed := fmt.Sprintf("批准重开申请 #%d", reqID) if reason != "" { proposed += ":" + reason } card, exp := buildConfirmCard( "批准受控文档重开确认", docID, title, "", proposed, "确认后将解锁文档至编制态;服务端按批准人优先/创建人兜底二次校验。", "confirm_reopen_approve", "确认批准重开", map[string]string{"request_id": strconv.FormatInt(reqID, 10)}, ) userCtx.applyCard(card) userCtx.applyPending(exp) return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: fmt.Sprintf("已起草批准重开确认单(申请 #%d)。请确认或回复「确认」。", reqID), InteractiveCard: card, NextExpectation: exp, UpdatedContext: userCtx, }, nil } // handleQueryPendingSupplements 只读直答积压补充 func (e *CoordinatorEngine) handleQueryPendingSupplements(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) { projectID := int64(0) if userCtx != nil { projectID = parseDecimalID(userCtx.Project.ID) } docID := "" if cand != nil && cand.FilledSlots != nil { docID = strings.TrimSpace(cand.FilledSlots[intent.SlotTargetDoc]) } if e == nil || e.pendingSupps == nil { return e.applyNarrative(ctx, userCtx, "handleQueryPendingSupplements", "", "", &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: "积压补充查询尚未接到当前环境。", UpdatedContext: userCtx, }), nil } rows, err := e.pendingSupps.ListPendingSupplements(ctx, projectID, docID) if err != nil { return &CoordinationResult{ Status: StatusRejected, Intent: cand.Intent, Branch: cand.Branch, RejectReason: "PENDING_SUPPLEMENT_QUERY_FAILED", NaturalReply: fmt.Sprintf("查询积压补充失败:%v", err), UpdatedContext: userCtx, }, nil } if len(rows) == 0 { return e.applyNarrative(ctx, userCtx, "handleQueryPendingSupplements", "", "", &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: "当前没有未消化的锁定期间积压补充。", UpdatedContext: userCtx, }), nil } var b strings.Builder b.WriteString(fmt.Sprintf("共有 %d 条待消化积压补充:\n", len(rows))) for i, r := range rows { title := r.DocTitle if title == "" { title = r.DocID } b.WriteString(fmt.Sprintf("%d) 《%s》来源:%s", i+1, title, r.SourceSummary)) if r.ImpactReason != "" { b.WriteString(";影响:" + r.ImpactReason) } b.WriteString("\n") } return e.applyNarrative(ctx, userCtx, "handleQueryPendingSupplements", "", "", &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: b.String(), UpdatedContext: userCtx, }), nil }