claim/submit 经 DocResolver+确认卡后 PendingAction 真写 docgov;proposal 默认分支改澄清;workflow.switch 诚实拒绝(UI专属);workflow.run 确认后走 CreateWorkflowRun(ActionServices.Workflow 由合入时接线)。
177 lines
5.2 KiB
Go
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
|
|
}
|
|
}
|