Files
weichen d55488ae37 feat(coordinator): 用智能体叙述查询、帮助与操作回执
确定性事实仍由代码生成;PI Agent 只组织语言,失败或关闭开关时回退原模板。安全拦截与确认提问保持模板。
2026-09-28 03:25:06 +00:00

202 lines
7.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}