Files
Agent-System/internal/coordinator/engine_docgov.go
T

399 lines
13 KiB
Go

package coordinator
import (
"context"
"fmt"
"strings"
"time"
"chat-agent-system/internal/docgov"
"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 &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 处理受控文档认领
func (e *CoordinatorEngine) handleDocGovClaim(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
docName := cand.FilledSlots[intent.SlotTargetDoc]
if docName == "" {
docName = "当前受控文档"
}
// 记录在用户的已认领清单中
found := false
for _, d := range userCtx.UserDocs.Claimed {
if d == docName {
found = true
break
}
}
if !found {
userCtx.UserDocs.Claimed = append(userCtx.UserDocs.Claimed, docName)
}
reply := fmt.Sprintf("您已成功认领《%s》。系统已锁定您作为该文档的法定责任人,您可以开始在线就地修订正文或提交审核。", docName)
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: reply,
UpdatedContext: userCtx,
}, nil
}
// handleDocGovSubmitReview 处理受控文档提交审核
func (e *CoordinatorEngine) handleDocGovSubmitReview(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
docName := cand.FilledSlots[intent.SlotTargetDoc]
if docName == "" {
docName = "当前受控文档"
}
reply := fmt.Sprintf("《%s》已成功提交审核!文档正文已进入受控锁定状态,并已通知审核责任人进行合规审查与批注。", docName)
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: reply,
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 := docgov.StateDrafting
if userCtx.InteractiveCard != nil && strings.Contains(userCtx.InteractiveCard.ImpactRisk, "reviewing") {
rollback = docgov.StateDrafting
}
return &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 &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 == "cancel" {
if userCtx.InteractiveCard != nil {
userCtx.dismissCard("cancelled")
}
userCtx.applyPending(nil)
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: fmt.Sprintf("已取消针对《%s》的操作,文档状态保持不变。", docName),
UpdatedContext: userCtx,
ClearExpectation: true,
}, nil
}
action := cand.FilledSlots[intent.SlotProposalAction]
var reply string
if action == "reject" {
reply = "已拒绝该修改变更提案。文档正文保持原样,提案记录已归档。"
} else {
reply = "已采纳该修改变更提案,补充内容与参数已合并入受控文档。"
}
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: reply,
UpdatedContext: userCtx,
}, nil
}