package coordinator import ( "context" "fmt" "strings" "time" "chat-agent-system/internal/contracts/vocabulary" "chat-agent-system/internal/intent" ) // resolveTargetDocGate runs the DocResolver mistouch gate. // Returns (doc, nil) on unique hit; otherwise a clarification/reject CoordinationResult. func (e *CoordinatorEngine) resolveTargetDocGate( ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext, ) (*ResolvedDoc, *CoordinationResult) { ref := "" if cand != nil && cand.FilledSlots != nil { ref = strings.TrimSpace(cand.FilledSlots[intent.SlotTargetDoc]) } active := "" if userCtx != nil && userCtx.InteractiveCard != nil && userCtx.InteractiveCard.CurrentStatus == "active" { active = userCtx.InteractiveCard.TargetDocID } if active == "" && userCtx != nil { snap := userCtx.ToSnapshot() if snap != nil { active = snap.ActiveDocID } } projectID := int64(0) if userCtx != nil { projectID = parseDecimalID(userCtx.Project.ID) } if e == nil || e.docResolver == nil { // Soft fallback for unit tests without resolver: treat ref as DocID. if ref == "" || ref == "当前受控文档" { return nil, &CoordinationResult{ Status: StatusClarificationRequired, Intent: cand.Intent, Branch: cand.Branch, ClarificationPrompt: "请明确要操作的受控文档名称或文档 ID(不会默认选择第一份文档)。", NaturalReply: "需要您点名目标文档后才能继续,请回复文档标题或文档 ID。", UpdatedContext: userCtx, } } return &ResolvedDoc{DocID: ref, Title: ref}, nil } doc, ok, candidates, err := e.docResolver.ResolveDoc(ctx, projectID, ref, active) if err != nil { return nil, &CoordinationResult{ Status: StatusRejected, Intent: cand.Intent, Branch: cand.Branch, RejectReason: "DOC_RESOLVE_FAILED", NaturalReply: fmt.Sprintf("文档解析失败:%v", err), UpdatedContext: userCtx, } } if ok && doc != nil { return doc, nil } prompt := "未唯一匹配到受控文档。请从下列候选中点名一份(回复标题或文档 ID),系统不会默认选择第一份:" if len(candidates) == 0 { prompt = "当前项目没有可匹配的受控文档。请先确认工作流文档已创建,并点名文档标题或 ID。" } else { prompt += "\n- " + strings.Join(candidates, "\n- ") } return nil, &CoordinationResult{ Status: StatusClarificationRequired, Intent: cand.Intent, Branch: cand.Branch, ClarificationPrompt: prompt, NaturalReply: prompt, UpdatedContext: userCtx, } } func buildConfirmCard( cardTypeTitle, docID, docTitle, section, proposed, impact string, confirmActionID, confirmLabel string, extraPayload map[string]string, ) (*intent.InteractiveCardPayload, *intent.PendingExpectation) { payload := map[string]string{ "target_doc": docID, "target_action": confirmActionID, } for k, v := range extraPayload { if v != "" { payload[k] = v } } if section != "" { payload["target_section"] = section } cardID := fmt.Sprintf("card_%s_%d", confirmActionID, time.Now().UnixNano()) card := &intent.InteractiveCardPayload{ CardID: cardID, CardType: "CONFIRMATION", Title: cardTypeTitle, TargetDocID: docID, TargetDocTitle: fmt.Sprintf("《%s》", docTitle), TargetSectionID: section, SectionTitle: section, ProposedContent: proposed, ImpactRisk: impact, CurrentStatus: "active", TTLTurns: 3, CreatedAt: time.Now(), Actions: []intent.CardAction{ { ActionID: confirmActionID, Label: confirmLabel, Style: "primary", IsAffirmativeDefault: true, Payload: payload, }, { ActionID: "cancel", Label: "取消", Style: "subtle", Payload: map[string]string{"action": "cancel"}, }, }, } exp := &intent.PendingExpectation{ ExpectationID: "exp_" + card.CardID, ExpectedIntent: intent.IntentDocGovProposalDecide, TargetAction: confirmActionID, Prompt: "请确认是否执行该操作", AffirmativeWord: "确认", DeclineWord: "取消", ExpiresAt: time.Now().Add(5 * time.Minute), Payload: payload, AllowAffirmative: true, CreatedAt: time.Now(), DefaultOption: &intent.ExpectationOption{ OptionID: confirmActionID, Title: confirmLabel, Intent: intent.IntentDocGovProposalDecide, Slots: payload, }, } return card, exp } func displayDocTitle(doc *ResolvedDoc) string { if doc == nil { return "当前受控文档" } if doc.Title != "" { return doc.Title } return doc.DocID } func commentRoleAllowed(role string, state vocabulary.DocumentState) bool { r := strings.ToLower(strings.TrimSpace(role)) switch state { case vocabulary.StateDrafting: return r == "author" || r == "reviewer" || r == "system" || r == "composer" case vocabulary.StateReviewing: return r == "reviewer" || r == "author" || r == "system" || r == "composer" case vocabulary.StateApproving: return r == "approver" || r == "reviewer" || r == "system" case vocabulary.StateApproved: return false default: return false } }