Files
Agent-System/internal/coordinator/doc_resolve_gate.go
T
weichen ce694baef1 feat(coordinator): P0 消除对话主链路假成功
claim/submit 经 DocResolver+确认卡后 PendingAction 真写 docgov;proposal 默认分支改澄清;workflow.switch 诚实拒绝(UI专属);workflow.run 确认后走 CreateWorkflowRun(ActionServices.Workflow 由合入时接线)。
2026-09-25 13:19:36 +00:00

177 lines
5.2 KiB
Go

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
}
}