package coordinator import ( "chat-agent-system/internal/contracts/vocabulary" "context" "fmt" "strconv" "strings" "time" "chat-agent-system/internal/intent" ) // handleDocGovReject 处理受控文档审核/批准驳回与二次确认交互卡片挂载(角色硬门禁) func (e *CoordinatorEngine) handleDocGovReject(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) { docName := cand.FilledSlots[intent.SlotTargetDoc] if docName == "" { if userCtx.InteractiveCard != nil && userCtx.InteractiveCard.TargetDocID != "" { docName = userCtx.InteractiveCard.TargetDocID } else { docName = "当前受控文档" } } rawReason := cand.FilledSlots[intent.SlotRejectReason] section := cand.FilledSlots[intent.SlotTargetSection] if section == "" { section = extractSectionClues(rawReason) } role := effectiveDocRole(userCtx) isFinal := cand.Intent == intent.IntentDocGovFinalReject // 与批准对称的硬门禁 if isFinal { if !isApproverAuthorized(userCtx.User) && role != "approver" { return &CoordinationResult{ Status: StatusRejected, Intent: cand.Intent, Branch: cand.Branch, RejectReason: "UNAUTHORIZED_REJECT_ROLE", NaturalReply: fmt.Sprintf("权限不足:终审驳回需批准人角色。当前您的身份为「%s」。若需了解驳回原因,可直接询问。", roleLabelCN(role)), UpdatedContext: userCtx, }, nil } } else { if !isReviewerAuthorized(userCtx.User) && role != "reviewer" { return &CoordinationResult{ Status: StatusRejected, Intent: cand.Intent, Branch: cand.Branch, RejectReason: "UNAUTHORIZED_REJECT_ROLE", NaturalReply: fmt.Sprintf("权限不足:审核驳回需审核人角色。当前您的身份为「%s」。若需了解驳回原因,可直接询问「被驳回的原因是什么」。", roleLabelCN(role)), UpdatedContext: userCtx, }, nil } } // 规范化策略角色(授权通过后按意图选审核/批准确认卡) policyRole := role if isFinal { policyRole = "approver" } else if policyRole != "reviewer" && isReviewerAuthorized(userCtx.User) { policyRole = "reviewer" } lifecycle := "reviewing" if isFinal { lifecycle = "approving" } resolved := ResolveRejectCard(policyRole, lifecycle, docName, docName, section, rawReason, RejectUtteranceExecute) if resolved.Kind == "deny_text" || resolved.Card == nil { return &CoordinationResult{ Status: StatusRejected, Intent: cand.Intent, Branch: cand.Branch, RejectReason: resolved.DenyReason, NaturalReply: resolved.Reply, UpdatedContext: userCtx, }, nil } userCtx.applyCard(resolved.Card) userCtx.applyPending(resolved.Expectation) return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: resolved.Reply, InteractiveCard: resolved.Card, NextExpectation: resolved.Expectation, UpdatedContext: userCtx, }, nil } // isReviewerAuthorized 校验审核人或主导工程师权限 func isReviewerAuthorized(u UserInfo) bool { r := strings.ToLower(strings.TrimSpace(u.Role)) pr := strings.ToLower(strings.TrimSpace(u.ProjectRole)) return r == "reviewer" || pr == "reviewer" || r == "lead_engineer" || pr == "lead_engineer" || strings.Contains(pr, "审核") || strings.Contains(r, "审核") || strings.Contains(pr, "主导") || strings.Contains(r, "主导") } // isApproverAuthorized 校验批准人或项目经理权限 func isApproverAuthorized(u UserInfo) bool { r := strings.ToLower(strings.TrimSpace(u.Role)) pr := strings.ToLower(strings.TrimSpace(u.ProjectRole)) return r == "approver" || pr == "approver" || r == "project_manager" || pr == "project_manager" || strings.Contains(pr, "批准") || strings.Contains(r, "批准") || strings.Contains(pr, "总监") || strings.Contains(r, "总监") || r == "director" } // handleDocGovApprove 处理受控文档审核/批准通过与终审批准二次确认 func (e *CoordinatorEngine) handleDocGovApprove(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) { docName := cand.FilledSlots[intent.SlotTargetDoc] if docName == "" { if userCtx.InteractiveCard != nil && userCtx.InteractiveCard.TargetDocID != "" { docName = userCtx.InteractiveCard.TargetDocID } else { docName = "当前受控文档" } } currentRole := userCtx.User.ProjectRole if currentRole == "" { currentRole = userCtx.User.Role } if currentRole == "" { currentRole = "未知角色" } isFinal := cand.Intent == intent.IntentDocGovFinalApprove if isFinal { // 校验终审批准权限: approver 或 project_manager if !isApproverAuthorized(userCtx.User) { return &CoordinationResult{ Status: StatusRejected, Intent: cand.Intent, Branch: cand.Branch, RejectReason: "UNAUTHORIZED_APPROVAL_ROLE", NaturalReply: fmt.Sprintf("权限不足:受控文档终审批准需要批准人 (approver) 或项目经理 (project_manager) 权限。当前您的身份为「%s」,无权执行终审批准操作。", currentRole), UpdatedContext: userCtx, }, nil } cardID := fmt.Sprintf("card_final_appr_%d", time.Now().UnixNano()) cardPayload := &intent.InteractiveCardPayload{ CardID: cardID, CardType: "CONFIRMATION", Title: "受控文档终审批准生效确认", TargetDocID: docName, TargetDocTitle: fmt.Sprintf("《%s》", docName), ProposedContent: "正式终审批准受控文档,签署电子签名并受控归档生效", ImpactRisk: "终审批准后文档正文将永久锁定为基线版本,后续非受控修改将被严格拦截,任何变更必须走变更审批流程", CurrentStatus: "active", TTLTurns: 3, CreatedAt: time.Now(), Actions: []intent.CardAction{ { ActionID: "confirm_final_approve", Label: "确认终审批准生效", Style: "primary", IsAffirmativeDefault: true, Payload: map[string]string{ "target_doc": docName, "target_action": "confirm_final_approve", }, }, { ActionID: "cancel", Label: "取消", Style: "subtle", Payload: map[string]string{ "action": "cancel", }, }, }, } nextExp := &intent.PendingExpectation{ ExpectationID: "exp_" + cardPayload.CardID, ExpectedIntent: intent.IntentDocGovProposalDecide, TargetAction: "confirm_final_approve", Prompt: "请确认是否执行终审批准操作", AffirmativeWord: "确认", DeclineWord: "取消", ExpiresAt: time.Now().Add(5 * time.Minute), Payload: cardPayload.Actions[0].Payload, AllowAffirmative: true, CreatedAt: time.Now(), DefaultOption: &intent.ExpectationOption{ OptionID: "confirm_final_approve", Title: "确认终审批准生效", Intent: intent.IntentDocGovProposalDecide, Slots: cardPayload.Actions[0].Payload, }, } userCtx.applyCard(cardPayload) userCtx.applyPending(nextExp) reply := fmt.Sprintf("已为您起草针对《%s》的终审批准确认单。终审批准将使文档正式归档并永久锁定,请审阅下方卡片并在确认无误后点击确认或回复‘确认’。", docName) return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: reply, InteractiveCard: cardPayload, NextExpectation: nextExp, UpdatedContext: userCtx, }, nil } // 校验审核通过权限: reviewer 或 lead_engineer if !isReviewerAuthorized(userCtx.User) { return &CoordinationResult{ Status: StatusRejected, Intent: cand.Intent, Branch: cand.Branch, RejectReason: "UNAUTHORIZED_REVIEW_ROLE", NaturalReply: fmt.Sprintf("权限不足:受控文档审核通过需要审核人 (reviewer) 或主导工程师 (lead_engineer) 权限。当前您的身份为「%s」,无权执行审核通过操作。", currentRole), UpdatedContext: userCtx, }, nil } reply := fmt.Sprintf("正在处理您对《%s》的审核通过请求…", docName) return e.applyNarrative(ctx, userCtx, "handleDocGovApprove", "", "这是审核通过请求的受理通报。必须保留文档名,并说明正在处理,不得宣称已经批准归档。", &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: reply, UpdatedContext: userCtx, PendingAction: &PendingAction{ Kind: "docgov.review_approve", DocID: docName, Comment: cand.FilledSlots[intent.SlotRejectReason], }, }), nil } // handleDocGovClaim 解析门 → 确认卡 → 拍板后 PendingAction(docgov.claim) func (e *CoordinatorEngine) handleDocGovClaim(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) { doc, gate := e.resolveTargetDocGate(ctx, cand, userCtx) if gate != nil { return gate, nil } title := displayDocTitle(doc) card, exp := buildConfirmCard( "受控文档认领确认", doc.DocID, title, "", "确认认领该文档并开始编制责任", "认领将记录您已查看并承接该文档的编制责任;不会静默选择其他文档。", "confirm_claim", "确认认领", map[string]string{"task_key": doc.TaskKey}, ) userCtx.applyCard(card) userCtx.applyPending(exp) reply := fmt.Sprintf("已为《%s》起草认领确认单。请确认文档无误后点击确认或回复「确认」。", title) return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: reply, InteractiveCard: card, NextExpectation: exp, UpdatedContext: userCtx, }, nil } // handleDocGovSubmitReview 解析门 → drafting 校验 → CONFIRMATION 卡(含锁定后果)→ 拍板后 PendingAction func (e *CoordinatorEngine) handleDocGovSubmitReview(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) { doc, gate := e.resolveTargetDocGate(ctx, cand, userCtx) if gate != nil { return gate, nil } state := doc.State // ContextSnapshot hardcodes DocLifecycleState="reviewing" whenever an interactive // card has ActiveDocID — do not treat that as authoritative unless the resolver // already supplied a real lifecycle state. if state != "" && state != vocabulary.StateDrafting { return &CoordinationResult{ Status: StatusRejected, Intent: cand.Intent, Branch: cand.Branch, RejectReason: "SUBMIT_STATE_DENIED", NaturalReply: fmt.Sprintf("当前文档处于「%s」阶段,仅编制中(drafting)可送审。", state), UpdatedContext: userCtx, }, nil } title := displayDocTitle(doc) card, exp := buildConfirmCard( "受控文档送审确认", doc.DocID, title, "", "编制完成,提交审核并锁定正文", "送审后文档进入审核锁定:非审核职责人不可改正文;锁定期间补充将进入积压台账,待退回编制后再消化。", "confirm_submit_review", "确认送审", map[string]string{"task_key": doc.TaskKey}, ) userCtx.applyCard(card) userCtx.applyPending(exp) reply := fmt.Sprintf("已为《%s》起草送审确认单。送审将锁定正文并通知审核人,请确认无误后点击确认或回复「确认」。", title) return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: reply, InteractiveCard: card, NextExpectation: exp, UpdatedContext: userCtx, }, nil } // handleDocGovProposalDecide 处理智能体修改建议与提案裁决,以及驳回/终审批准确认执行 func (e *CoordinatorEngine) handleDocGovProposalDecide(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) { targetAction := cand.FilledSlots["target_action"] if targetAction == "" { targetAction = cand.FilledSlots["action"] } if targetAction == "" && cand.MatchedOption != nil { targetAction = cand.MatchedOption.OptionID } docName := cand.FilledSlots[intent.SlotTargetDoc] if docName == "" && userCtx.InteractiveCard != nil && userCtx.InteractiveCard.TargetDocID != "" { docName = userCtx.InteractiveCard.TargetDocID } if docName == "" { docName = "当前受控文档" } if targetAction == "confirm_reject" { reason := cand.FilledSlots[intent.SlotRejectReason] if reason == "" && userCtx.InteractiveCard != nil { reason = userCtx.InteractiveCard.ProposedContent } if userCtx.InteractiveCard != nil { userCtx.dismissCard("resolved") } userCtx.applyPending(nil) rollback := vocabulary.StateDrafting if userCtx.InteractiveCard != nil && strings.Contains(userCtx.InteractiveCard.ImpactRisk, "reviewing") { rollback = vocabulary.StateDrafting } return e.applyNarrative(ctx, userCtx, "handleDocGovProposalDecide", "", "这是已确认或已取消后的受理通报。必须保留文档名与动作边界,不得把受理说成已经归档生效。", &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: fmt.Sprintf("正在处理您对《%s》的驳回确认…", docName), UpdatedContext: userCtx, ClearExpectation: true, PendingAction: &PendingAction{ Kind: "docgov.reject", DocID: docName, Reason: reason, Rollback: rollback, }, }), nil } if targetAction == "confirm_final_approve" { if userCtx.InteractiveCard != nil { userCtx.dismissCard("resolved") } userCtx.applyPending(nil) return e.applyNarrative(ctx, userCtx, "handleDocGovProposalDecide", "", "这是已确认或已取消后的受理通报。必须保留文档名与动作边界,不得把受理说成已经归档生效。", &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: fmt.Sprintf("正在处理您对《%s》的终审批准…", docName), UpdatedContext: userCtx, ClearExpectation: true, PendingAction: &PendingAction{ Kind: "docgov.approver_approve", DocID: docName, }, }), nil } if targetAction == "confirm_claim" { payload := mergeDecidePayload(cand, userCtx) if userCtx.InteractiveCard != nil { userCtx.dismissCard("resolved") } userCtx.applyPending(nil) return e.applyNarrative(ctx, userCtx, "handleDocGovProposalDecide", "", "这是已确认或已取消后的受理通报。必须保留文档名与动作边界,不得把受理说成已经归档生效。", &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: fmt.Sprintf("正在处理《%s》的认领…", docName), UpdatedContext: userCtx, ClearExpectation: true, PendingAction: &PendingAction{ Kind: "docgov.claim", DocID: docName, TaskKey: payload["task_key"], }, }), nil } if targetAction == "confirm_submit_review" { payload := mergeDecidePayload(cand, userCtx) if userCtx.InteractiveCard != nil { userCtx.dismissCard("resolved") } userCtx.applyPending(nil) taskKey := payload["task_key"] return e.applyNarrative(ctx, userCtx, "handleDocGovProposalDecide", "", "这是已确认或已取消后的受理通报。必须保留文档名与动作边界,不得把受理说成已经归档生效。", &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: fmt.Sprintf("正在处理《%s》的送审…", docName), UpdatedContext: userCtx, ClearExpectation: true, PendingAction: &PendingAction{Kind: "docgov.submit_review", DocID: docName, TaskKey: taskKey}, }), nil } if targetAction == "confirm_workflow_run" { payload := mergeDecidePayload(cand, userCtx) if userCtx.InteractiveCard != nil { userCtx.dismissCard("resolved") } userCtx.applyPending(nil) wfKey := payload[intent.SlotTargetWorkflow] if wfKey == "" { wfKey = payload["workflow_key"] } if wfKey == "" && userCtx != nil { wfKey = userCtx.Project.ActiveWorkflowKey } if wfKey == "" { wfKey = "main" } return e.applyNarrative(ctx, userCtx, "handleDocGovProposalDecide", "", "这是已确认或已取消后的受理通报。必须保留文档名与动作边界,不得把受理说成已经归档生效。", &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: fmt.Sprintf("正在启动工作流「%s」…", workflowDisplayName(wfKey)), UpdatedContext: userCtx, ClearExpectation: true, PendingAction: &PendingAction{ Kind: "workflow.run", WorkflowKey: wfKey, }, }), nil } if targetAction == "confirm_comment_add" { payload := mergeDecidePayload(cand, userCtx) if userCtx.InteractiveCard != nil { userCtx.dismissCard("resolved") } userCtx.applyPending(nil) return e.applyNarrative(ctx, userCtx, "handleDocGovProposalDecide", "", "这是已确认或已取消后的受理通报。必须保留文档名与动作边界,不得把受理说成已经归档生效。", &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: fmt.Sprintf("正在为《%s》添加批注…", docName), UpdatedContext: userCtx, ClearExpectation: true, PendingAction: &PendingAction{ Kind: "docgov.comment_add", DocID: docName, Role: payload["role"], Section: payload["target_section"], Comment: payload["comment_content"], Reason: payload["comment_type"], }, }), nil } if targetAction == "confirm_comment_resolve" { payload := mergeDecidePayload(cand, userCtx) if userCtx.InteractiveCard != nil { userCtx.dismissCard("resolved") } userCtx.applyPending(nil) return e.applyNarrative(ctx, userCtx, "handleDocGovProposalDecide", "", "这是已确认或已取消后的受理通报。必须保留文档名与动作边界,不得把受理说成已经归档生效。", &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: fmt.Sprintf("正在解决《%s》的批注…", docName), UpdatedContext: userCtx, ClearExpectation: true, PendingAction: &PendingAction{ Kind: "docgov.comment_resolve", DocID: docName, Role: payload["role"], Section: payload["target_section"], }, }), nil } if targetAction == "confirm_reopen_request" { payload := mergeDecidePayload(cand, userCtx) if userCtx.InteractiveCard != nil { userCtx.dismissCard("resolved") } userCtx.applyPending(nil) return e.applyNarrative(ctx, userCtx, "handleDocGovProposalDecide", "", "这是已确认或已取消后的受理通报。必须保留文档名与动作边界,不得把受理说成已经归档生效。", &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: fmt.Sprintf("正在提交《%s》的重开申请…", docName), UpdatedContext: userCtx, ClearExpectation: true, PendingAction: &PendingAction{ Kind: "docgov.reopen_request", DocID: docName, TaskKey: payload["task_key"], Reason: payload["reopen_reason"], Role: payload["role"], }, }), nil } if targetAction == "confirm_reopen_approve" { payload := mergeDecidePayload(cand, userCtx) if userCtx.InteractiveCard != nil { userCtx.dismissCard("resolved") } userCtx.applyPending(nil) reqID, _ := strconv.ParseInt(payload["request_id"], 10, 64) return e.applyNarrative(ctx, userCtx, "handleDocGovProposalDecide", "", "这是已确认或已取消后的受理通报。必须保留文档名与动作边界,不得把受理说成已经归档生效。", &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: "正在批准重开申请…", UpdatedContext: userCtx, ClearExpectation: true, PendingAction: &PendingAction{ Kind: "docgov.reopen_approve", DocID: docName, RequestID: reqID, }, }), nil } if targetAction == "cancel" { if userCtx.InteractiveCard != nil { userCtx.dismissCard("cancelled") } userCtx.applyPending(nil) return e.applyNarrative(ctx, userCtx, "handleDocGovProposalDecide", "", "这是已确认或已取消后的受理通报。必须保留文档名与动作边界,不得把受理说成已经归档生效。", &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: fmt.Sprintf("已取消针对《%s》的操作,文档状态保持不变。", docName), UpdatedContext: userCtx, ClearExpectation: true, }), nil } // 默认分支:禁止假成功——缺确认动作时诚实澄清 clarify := "请明确您要执行的文档操作(例如确认驳回、确认终审批准、确认认领或确认送审)。未确认前不会修改文档状态。" return &CoordinationResult{ Status: StatusClarificationRequired, Intent: cand.Intent, Branch: cand.Branch, ClarificationPrompt: clarify, NaturalReply: clarify, UpdatedContext: userCtx, }, nil } func mergeDecidePayload(cand *intent.CandidateIntent, userCtx *UserProjectContext) map[string]string { out := map[string]string{} if userCtx != nil && userCtx.InteractiveCard != nil { for _, a := range userCtx.InteractiveCard.Actions { if a.IsAffirmativeDefault && a.Payload != nil { for k, v := range a.Payload { out[k] = v } } } } if userCtx != nil && userCtx.PendingExpectation != nil && userCtx.PendingExpectation.Payload != nil { for k, v := range userCtx.PendingExpectation.Payload { out[k] = v } } if cand != nil && cand.FilledSlots != nil { for k, v := range cand.FilledSlots { if v != "" { out[k] = v } } } return out }