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

1219 lines
45 KiB
Go
Raw 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"
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/intent"
"chat-agent-system/internal/squad"
"chat-agent-system/internal/stream"
)
// CoordinationStatus 协调裁决状态
type CoordinationStatus string
const (
StatusAccepted CoordinationStatus = "ACCEPTED"
StatusRejected CoordinationStatus = "REJECTED"
StatusClarificationRequired CoordinationStatus = "CLARIFICATION_REQUIRED"
)
// CoordinationResult 协调层给对话层的回执
type CoordinationResult struct {
Status CoordinationStatus `json:"status"`
Intent intent.IntentType `json:"intent"`
Branch intent.ResolutionBranch `json:"branch,omitempty"`
RejectReason string `json:"reject_reason,omitempty"`
ClarificationPrompt string `json:"clarification_prompt,omitempty"`
NaturalReply string `json:"natural_reply"`
AdvisorBrief *squad.AdvisorBriefing `json:"advisor_brief,omitempty"`
ArtifactPath string `json:"artifact_path,omitempty"`
UpdatedContext *UserProjectContext `json:"updated_context"`
InteractiveCard *intent.InteractiveCardPayload `json:"interactive_card,omitempty"`
NextExpectation *intent.PendingExpectation `json:"next_expectation,omitempty"`
ClearExpectation bool `json:"clear_expectation,omitempty"`
}
// CoordinatorEngine L2 协调引擎
type CoordinatorEngine struct {
bizdevSvc bizdev.BizdevService
assembler *ContextAssembler
}
func NewCoordinatorEngine(bizdevSvc bizdev.BizdevService) *CoordinatorEngine {
return &CoordinatorEngine{
bizdevSvc: bizdevSvc,
assembler: NewContextAssembler(),
}
}
// ProcessIntent 校验候选意图合法性并驱动调度与响应
func (e *CoordinatorEngine) ProcessIntent(
ctx context.Context,
recRes *intent.IntentRecognitionResult,
userCtx *UserProjectContext,
) (*CoordinationResult, error) {
if userCtx == nil {
userCtx = &UserProjectContext{
User: UserInfo{ID: "guest", Name: "用户", Role: "engineer", ProjectRole: "composer"},
BlackboardFacts: make(map[string]string),
}
}
// 交互卡片 TTL 轮次消退与生命周期清理
if userCtx.InteractiveCard != nil {
userCtx.InteractiveCard.TTLTurns--
if userCtx.InteractiveCard.TTLTurns <= 0 || userCtx.InteractiveCard.CurrentStatus != "active" {
userCtx.InteractiveCard.CurrentStatus = "expired"
userCtx.InteractiveCard = nil
}
}
if userCtx.PendingExpectation != nil && !userCtx.PendingExpectation.ExpiresAt.IsZero() && time.Now().After(userCtx.PendingExpectation.ExpiresAt) {
userCtx.PendingExpectation = nil
}
top := recRes.TopIntent
if top == nil {
top = &intent.CandidateIntent{
Intent: intent.IntentCasualChat,
Confidence: 1.0,
}
}
streamer := stream.FromContext(ctx)
if streamer != nil {
streamer.EmitStatus(string(top.Intent), string(recRes.Branch), string(userCtx.Project.CurrentStage), fmt.Sprintf("意图判定: %s (置信度: %.0f%%)", top.Intent, top.Confidence*100), top.FilledSlots)
}
// 1. 若为分支 3 (模糊/分忧引导),触发参谋主动分析与建议
if recRes.Branch == intent.BranchAmbiguousFallback {
res, err := e.handleAmbiguousConsult(top, userCtx)
if err == nil && res != nil {
e.enrichAdvisorBriefWithCases(ctx, res.AdvisorBrief, userCtx, top.FilledSlots)
}
return res, err
}
// 2. 若缺少核心槽位且需澄清
if recRes.RequiresClarifying && top.ClarificationQuestion != "" {
return &CoordinationResult{
Status: StatusClarificationRequired,
Intent: top.Intent,
Branch: recRes.Branch,
ClarificationPrompt: top.ClarificationQuestion,
NaturalReply: top.ClarificationQuestion,
UpdatedContext: userCtx,
}, nil
}
// 3. 根据候选意图执行分支
var coordRes *CoordinationResult
var coordErr error
switch top.Intent {
case intent.IntentDeclineExpectation:
coordRes, coordErr = e.handleDeclineExpectation(userCtx)
case intent.IntentQueryProgress:
coordRes, coordErr = e.handleQueryProgress(top, userCtx)
case intent.IntentSupplementInfo:
coordRes, coordErr = e.handleSupplementInfo(top, userCtx)
case intent.IntentAdvanceStage:
coordRes, coordErr = e.handleAdvanceStage(ctx, top, userCtx)
case intent.IntentTriggerTask:
coordRes, coordErr = e.handleTriggerTask(ctx, top, userCtx)
case intent.IntentCasualChat:
coordRes, coordErr = e.handleCasualChat(userCtx)
case intent.IntentSecurityForbidden:
coordRes, coordErr = e.handleSecurityForbidden(userCtx)
case intent.IntentDocGovReviewReject, intent.IntentDocGovFinalReject:
coordRes, coordErr = e.handleDocGovReject(ctx, top, userCtx)
case intent.IntentDocGovReviewApprove, intent.IntentDocGovFinalApprove:
coordRes, coordErr = e.handleDocGovApprove(ctx, top, userCtx)
case intent.IntentDocGovClaim:
coordRes, coordErr = e.handleDocGovClaim(ctx, top, userCtx)
case intent.IntentDocGovSubmitReview:
coordRes, coordErr = e.handleDocGovSubmitReview(ctx, top, userCtx)
case intent.IntentDocGovProposalDecide:
coordRes, coordErr = e.handleDocGovProposalDecide(ctx, top, userCtx)
case intent.IntentQueryRegulations:
coordRes, coordErr = e.handleQueryRegulations(ctx, top, userCtx)
case intent.IntentQuerySimilarCases:
coordRes, coordErr = e.handleQuerySimilarCases(ctx, top, userCtx)
case intent.IntentQueryUserRights:
coordRes, coordErr = e.handleQueryUserRights(ctx, top, userCtx)
default:
nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name)
coordRes = &CoordinationResult{
Status: StatusAccepted,
Intent: top.Intent,
Branch: recRes.Branch,
NaturalReply: "我已收到您的指示。针对当前项目,请问需要我为您查询进展、补充技术事实,还是推进方案论证?随时吩咐我。",
UpdatedContext: userCtx,
NextExpectation: nextExp,
}
}
if coordErr == nil && coordRes != nil {
e.enrichAdvisorBriefWithCases(ctx, coordRes.AdvisorBrief, userCtx, top.FilledSlots)
}
return coordRes, coordErr
}
// enrichAdvisorBriefWithCases 结合上下文与意图槽位检索相似标杆案例并注入参谋锦囊
func (e *CoordinatorEngine) enrichAdvisorBriefWithCases(ctx context.Context, brief *squad.AdvisorBriefing, userCtx *UserProjectContext, slots map[string]string) {
if brief == nil || e.bizdevSvc == nil {
return
}
ctxMgr := e.bizdevSvc.ContextManager()
if ctxMgr == nil {
return
}
// 构造搜索语句
queryParts := []string{userCtx.Project.DeviceType, userCtx.Project.IntendedUse}
if topic, ok := slots[intent.SlotQueryTopic]; ok && topic != "" {
queryParts = append(queryParts, topic)
}
query := strings.TrimSpace(strings.Join(queryParts, " "))
if query == "" {
query = userCtx.Project.Name
}
if query == "" {
return
}
cases, err := ctxMgr.SearchSimilarCases(ctx, ctxmgr.CaseSearchOptions{
Query: query,
Limit: 2,
MinScore: 0.10,
})
if err != nil || len(cases) == 0 {
return
}
for _, c := range cases {
brief.RecommendedCases = append(brief.RecommendedCases, squad.AdvisorCaseRecommendation{
DocID: c.DocID,
ProjectName: c.ProjectName,
TaskKey: c.TaskKey,
ArtifactTitle: c.ArtifactTitle,
SectionTitle: c.SectionTitle,
SummarySnippet: c.SummarySnippet,
KeyRegulations: c.KeyRegulations,
SimilarityScore: c.SimilarityScore,
ApprovedAt: c.ApprovedAt,
})
}
}
// handleDeclineExpectation 处理用户暂缓或取消上轮建议 (分支 1 放弃)
func (e *CoordinatorEngine) handleDeclineExpectation(userCtx *UserProjectContext) (*CoordinationResult, error) {
projName := userCtx.Project.Name
if projName == "" {
projName = "当前项目"
}
userCtx.PendingExpectation = nil
if userCtx.InteractiveCard != nil {
userCtx.InteractiveCard.CurrentStatus = "cancelled"
userCtx.InteractiveCard = nil
}
reply := fmt.Sprintf("收到,听您的!那我们先不急着推进。\n针对「%s」,您手头还有哪些细节、技术顾虑或商务考量想先聊聊?我随时待命为您解答。", projName)
return &CoordinationResult{
Status: StatusAccepted,
Intent: intent.IntentDeclineExpectation,
Branch: intent.BranchExpectationHit,
NaturalReply: reply,
UpdatedContext: userCtx,
ClearExpectation: true,
}, nil
}
// handleAmbiguousConsult 处理用户犹豫或征询建议 (分支 3 模糊分忧)
func (e *CoordinatorEngine) handleAmbiguousConsult(cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
currentStage := userCtx.Project.CurrentStage
projName := userCtx.Project.Name
if projName == "" {
projName = "当前项目"
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("我非常理解您的审慎考量!针对「%s」目前的决策节点,我为您做个客观分析:\n\n", projName))
if currentStage == bizdev.StageV01_Triage {
sb.WriteString("• 如果我们现在推进 V0.5(方案成型与法规对齐):\n")
sb.WriteString(" 小队会为您把初步架构和注册分类路径摸透,并预估型检成本,您能拿出一份非常扎实的方案框架去与客户深入沟通;\n")
sb.WriteString("• 如果选择先放一放 / 多做调研:\n")
sb.WriteString(" 我们可以先就客户的预算范围、目标市场或使用场景再多挖掘一些信息,避免后续方案返工。\n\n")
sb.WriteString("💡 贴身参谋建议:如果您手头暂时没有其他颠覆性变化,建议不妨让我先帮您跑一版 V0.5 的设计方案与法规路径供您审阅。无论如何,节奏完全以您的把握为准。\n\n")
sb.WriteString("您接下来更倾向于:\n")
sb.WriteString("[1] 让我先做 V0.5 方案框架(您可以直接回复“好”或“1”)\n")
sb.WriteString("[2] 告诉我您现在重点考虑的具体细节,我帮您做针对性分析")
} else {
sb.WriteString("当前各项工作进展平稳。建议您可以随时把手头的零散想法告诉我,我来为您沉淀事实;或者等您时机成熟时,随时吩咐我推进下一阶段。")
}
nextExp := BuildNextExpectation(currentStage, projName)
userCtx.PendingExpectation = nextExp
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: intent.BranchAmbiguousFallback,
NaturalReply: sb.String(),
AdvisorBrief: userCtx.LatestAdvisorBrief,
UpdatedContext: userCtx,
NextExpectation: nextExp,
}, nil
}
func (e *CoordinatorEngine) handleQueryProgress(cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
brief := userCtx.LatestAdvisorBrief
topic := cand.FilledSlots[intent.SlotQueryTopic]
nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name)
userCtx.PendingExpectation = nextExp
// 1. 若为特定技术/架构/功耗方案咨询,直接调取参谋专业知识库解答,不强依赖阶段简报
if topic == "technical_design" {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("关于「%s」的技术与系统架构考量,我为您梳理以下核心要点:\n", userCtx.Project.Name))
sb.WriteString("• 供电与功耗设计:建议选用高能效前端模拟芯片与智能休眠调度,兼顾便携连续工作时间与温升控制;\n")
sb.WriteString("• 电气安全与标准:设备需符合 GB 9706.1-2020 通用安全与医用电气专项要求,重点评估外壳防护等级与绝缘耐压;\n")
sb.WriteString("• 供应链选型:关键芯片与传感器优先考虑国内高成熟度元器件,缩短备料周期与型检验证周期。\n\n")
sb.WriteString("这项考量已为您收录,后续进入方案成型(V0.5)时,小队将作为核心约束直接带入论证。")
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: sb.String(),
AdvisorBrief: brief,
UpdatedContext: userCtx,
NextExpectation: nextExp,
}, nil
}
if brief == nil {
reply := fmt.Sprintf("您好,项目「%s」当前处于初始准备期。我建议先行为您启动【V0.1 闪电摸底】,快速沉淀第一版立项画像并为您准备沟通话术。您希望现在开始吗?",
userCtx.Project.Name)
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: reply,
UpdatedContext: userCtx,
NextExpectation: nextExp,
}, nil
}
var sb strings.Builder
switch topic {
case "risk":
sb.WriteString(fmt.Sprintf("关于「%s」的潜在风险,我为您提前预警以下关键卡点:\n", userCtx.Project.Name))
for _, r := range brief.PreemptiveRisks {
sb.WriteString(fmt.Sprintf("• ⚠️ %s\n", r))
}
sb.WriteString("\n建议您在后续与客户沟通时预先留出安全边际。")
case "talk_script":
sb.WriteString("这是为您草拟的对外商务沟通话术,您可以直接使用或微调:\n\n")
sb.WriteString(brief.CommunicationKit)
case "progress":
sb.WriteString(fmt.Sprintf("【进展汇报】 项目当前成熟度等级为 %s(评分 %d/100)。\n", brief.MaturityLevel, brief.MaturityScore))
sb.WriteString(fmt.Sprintf("诊断结论:%s\n\n", brief.MaturityHeadline))
if len(brief.NextBestActions) > 0 {
sb.WriteString("建议下一步选项:\n")
for idx, a := range brief.NextBestActions {
sb.WriteString(fmt.Sprintf("[%d] %s\n", idx+1, a))
}
}
default: // all
sb.WriteString(fmt.Sprintf("您好!项目「%s」当前处于【%s】阶段(成熟度 %d/100)。\n",
userCtx.Project.Name, brief.MaturityLevel, brief.MaturityScore))
sb.WriteString(fmt.Sprintf("核心研判:%s\n\n", brief.MaturityHeadline))
if len(brief.PreemptiveRisks) > 0 {
sb.WriteString("⚠️ 关键风险预警:\n")
for _, r := range brief.PreemptiveRisks {
sb.WriteString(fmt.Sprintf(" • %s\n", r))
}
sb.WriteString("\n")
}
if len(brief.NextBestActions) > 0 {
sb.WriteString("您可以选择下一步动作:\n")
for idx, a := range brief.NextBestActions {
sb.WriteString(fmt.Sprintf(" [%d] %s\n", idx+1, a))
}
}
}
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: sb.String(),
AdvisorBrief: brief,
UpdatedContext: userCtx,
NextExpectation: nextExp,
}, nil
}
func (e *CoordinatorEngine) handleSupplementInfo(cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
factKey := cand.FilledSlots[intent.SlotFactKey]
factVal := cand.FilledSlots[intent.SlotFactValue]
rawDoc := cand.FilledSlots[intent.SlotSupplementDoc]
if userCtx.BlackboardFacts == nil {
userCtx.BlackboardFacts = make(map[string]string)
}
summaryKey := ""
if factKey != "" && factVal != "" {
userCtx.BlackboardFacts[factKey] = factVal
summaryKey = fmt.Sprintf("「%s」已更新为「%s」", factKey, factVal)
if factKey == "target_market" {
userCtx.Project.TargetMarket = factVal
}
} else {
summaryKey = "已补充新的背景信息"
}
streamer := stream.FromContext(context.Background())
if streamer != nil {
streamer.EmitStatus(string(cand.Intent), string(cand.Branch), string(userCtx.Project.CurrentStage), fmt.Sprintf("已吸收最新事实设定:%s", summaryKey), cand.FilledSlots)
}
// 持久化到 blackboard.json
if userCtx.WorkDir != "" {
bbPath := filepath.Join(userCtx.WorkDir, "blackboard.json")
var bb squad.BlackboardState
if data, err := os.ReadFile(bbPath); err == nil {
_ = json.Unmarshal(data, &bb)
}
if bb.Fields == nil {
bb.Fields = make(map[string]string)
}
for k, v := range userCtx.BlackboardFacts {
bb.Fields[k] = v
}
if b, err := json.MarshalIndent(bb, "", " "); err == nil {
_ = os.WriteFile(bbPath, b, 0o644)
}
}
nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name)
userCtx.PendingExpectation = nextExp
var sb strings.Builder
sb.WriteString(fmt.Sprintf("已收到并为您记录最新信息:%s。\n", summaryKey))
sb.WriteString("这些事实已沉淀至项目黑板,后续论证与核价将直接吸收这些设定。\n\n")
sb.WriteString(fmt.Sprintf("针对「%s」,我为您梳理了接下来的推进选项:\n", userCtx.Project.Name))
for idx, opt := range nextExp.Options {
recMark := ""
if idx == 0 {
recMark = "(推荐,可直接回复“好”或“1”)"
}
sb.WriteString(fmt.Sprintf("[%s] %s %s\n", opt.OptionID, opt.Title, recMark))
}
if rawDoc != "" && summaryKey == "已补充新的背景信息" {
sb.Reset()
sb.WriteString("已为您收录补充材料,并已关联至项目事实黑板。\n在后续的文档修订和阶段演进中,智能体小队将自动引用这部分事实。")
}
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: sb.String(),
AdvisorBrief: userCtx.LatestAdvisorBrief,
UpdatedContext: userCtx,
NextExpectation: nextExp,
}, nil
}
func (e *CoordinatorEngine) handleAdvanceStage(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
targetStageStr := cand.FilledSlots[intent.SlotTargetStage]
targetStage := bizdev.MaturityStage(targetStageStr)
if targetStage == "" {
targetStage = bizdev.StageV05_Feasibility
}
streamer := stream.FromContext(ctx)
if streamer != nil {
streamer.EmitPhase("stage_transition", fmt.Sprintf("准备推进项目成熟度阶段至【%s】...", targetStage), "coordinator", 0, 0, 0)
}
// 1. 若携带补丁槽位 (例如 "好,但目标市场改成欧洲CE"),先行持久化更新
patchSummary := ""
if len(cand.PatchSlots) > 0 {
var patches []string
for k, v := range cand.PatchSlots {
userCtx.BlackboardFacts[k] = v
patches = append(patches, fmt.Sprintf("%s=%s", k, v))
if k == "target_market" {
userCtx.Project.TargetMarket = v
}
}
patchSummary = strings.Join(patches, ", ")
if userCtx.WorkDir != "" {
bbPath := filepath.Join(userCtx.WorkDir, "blackboard.json")
var bb squad.BlackboardState
if data, err := os.ReadFile(bbPath); err == nil {
_ = json.Unmarshal(data, &bb)
}
if bb.Fields == nil {
bb.Fields = make(map[string]string)
}
for k, v := range cand.PatchSlots {
bb.Fields[k] = v
}
if b, err := json.MarshalIndent(bb, "", " "); err == nil {
_ = os.WriteFile(bbPath, b, 0o644)
}
}
}
// 2. 状态机合法性检查
if userCtx.Project.CurrentStage == targetStage {
return &CoordinationResult{
Status: StatusRejected,
Intent: cand.Intent,
Branch: cand.Branch,
RejectReason: "ALREADY_IN_STAGE",
NaturalReply: fmt.Sprintf("项目目前已经在【%s】阶段中,无需重复推进。您可以吩咐我进行详细论证或查询当前进展。", targetStage),
UpdatedContext: userCtx,
}, nil
}
if e.bizdevSvc == nil {
userCtx.Project.CurrentStage = targetStage
userCtx.AvailableActions = ComputeAvailableActions(targetStage, userCtx.User.ProjectRole)
nextExp := BuildNextExpectation(targetStage, userCtx.Project.Name)
userCtx.PendingExpectation = nextExp
reply := fmt.Sprintf("已为您模拟推进到阶段【%s】。", targetStage)
if patchSummary != "" {
reply = fmt.Sprintf("已为您吸收最新设定(%s),并模拟推进到阶段【%s】。", patchSummary, targetStage)
}
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: reply,
UpdatedContext: userCtx,
NextExpectation: nextExp,
}, nil
}
// 3. 调用底层 L3 调度器执行阶段跃迁
stageRes, err := e.bizdevSvc.ExecuteStage(ctx, bizdev.StageRunRequest{
Stage: targetStage,
ProjectName: userCtx.Project.Name,
CustomerName: userCtx.Project.CustomerName,
DeviceType: userCtx.Project.DeviceType,
IntendedUse: userCtx.Project.IntendedUse,
TargetMarket: userCtx.Project.TargetMarket,
Fields: userCtx.BlackboardFacts,
WorkDir: userCtx.WorkDir,
})
if err != nil {
return &CoordinationResult{
Status: StatusRejected,
Intent: cand.Intent,
Branch: cand.Branch,
RejectReason: err.Error(),
NaturalReply: fmt.Sprintf("阶段推进失败: %v", err),
UpdatedContext: userCtx,
}, err
}
userCtx.Project.CurrentStage = targetStage
userCtx.LatestAdvisorBrief = stageRes.AdvisorBrief
userCtx.AvailableActions = ComputeAvailableActions(targetStage, userCtx.User.ProjectRole)
if stageRes.WorkDir != "" {
userCtx.WorkDir = stageRes.WorkDir
}
artifactPath := ""
if stageRes.PrimaryArtifact != nil {
artifactPath = filepath.Join(stageRes.PrimaryArtifact.WorkDir, "output.md")
}
nextExp := BuildNextExpectation(targetStage, userCtx.Project.Name)
userCtx.PendingExpectation = nextExp
var sb strings.Builder
if patchSummary != "" {
sb.WriteString(fmt.Sprintf("已为您吸收最新设定(%s),并成功推进并完成了【%s】!\n", patchSummary, stageRes.StageTitle))
} else {
sb.WriteString(fmt.Sprintf("已为您成功推进并完成了【%s】!\n", stageRes.StageTitle))
}
sb.WriteString("智能体小队已协同完成相应论证与文档沉淀。\n\n")
sb.WriteString(fmt.Sprintf("💡 最新进展诊断:%s\n", stageRes.AdvisorBrief.MaturityHeadline))
sb.WriteString(fmt.Sprintf("建议您可以审阅正式交付文档(%s),或按最新参谋建议开展后续商务洽谈。\n\n", artifactPath))
sb.WriteString("后续推进选项(供您掌舵决策):\n")
for idx, opt := range nextExp.Options {
recMark := ""
if idx == 0 {
recMark = "(推荐,可直接回复“好”或“1”)"
}
sb.WriteString(fmt.Sprintf("[%s] %s %s\n", opt.OptionID, opt.Title, recMark))
}
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: sb.String(),
AdvisorBrief: stageRes.AdvisorBrief,
ArtifactPath: artifactPath,
UpdatedContext: userCtx,
NextExpectation: nextExp,
}, nil
}
func (e *CoordinatorEngine) handleTriggerTask(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
taskKey := cand.FilledSlots[intent.SlotTargetTask]
if taskKey == "" {
taskKey = "intake"
}
nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name)
userCtx.PendingExpectation = nextExp
if e.bizdevSvc == nil {
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: fmt.Sprintf("已收到生成【%s】的指令。", taskKey),
UpdatedContext: userCtx,
NextExpectation: nextExp,
}, nil
}
taskRes, err := e.bizdevSvc.ExecuteTask(ctx, bizdev.TaskRunRequest{
TaskKey: taskKey,
ProjectName: userCtx.Project.Name,
CustomerName: userCtx.Project.CustomerName,
DeviceType: userCtx.Project.DeviceType,
IntendedUse: userCtx.Project.IntendedUse,
TargetMarket: userCtx.Project.TargetMarket,
Fields: userCtx.BlackboardFacts,
})
if err != nil {
return &CoordinationResult{
Status: StatusRejected,
Intent: cand.Intent,
Branch: cand.Branch,
RejectReason: err.Error(),
NaturalReply: fmt.Sprintf("任务执行失败: %v", err),
UpdatedContext: userCtx,
}, err
}
userCtx.LatestAdvisorBrief = taskRes.AdvisorBrief
outPath := filepath.Join(taskRes.WorkDir, "output.md")
var sb strings.Builder
sb.WriteString(fmt.Sprintf("已为您自适应规划并完成生成【%s】!\n交付物正文已保存至:%s\n耗时: %v\n",
taskRes.ArtifactTitle, outPath, taskRes.Duration))
if taskRes.AdvisorBrief != nil && len(taskRes.AdvisorBrief.BaselineAssumptions) > 0 {
sb.WriteString("\n📌 智能体小队预设的基准假设 (已融入方案,您可随时修正):\n")
for _, a := range taskRes.AdvisorBrief.BaselineAssumptions {
sb.WriteString(fmt.Sprintf(" • %s\n", a))
}
}
reply := sb.String()
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: reply,
AdvisorBrief: taskRes.AdvisorBrief,
ArtifactPath: outPath,
UpdatedContext: userCtx,
NextExpectation: nextExp,
}, nil
}
func (e *CoordinatorEngine) handleCasualChat(userCtx *UserProjectContext) (*CoordinationResult, error) {
projName := userCtx.Project.Name
if projName == "" {
projName = "当前医疗器械研发项目"
}
nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name)
userCtx.PendingExpectation = nextExp
reply := fmt.Sprintf("您好!我是您的医疗器械立项与商务对接贴身参谋。\n\n针对「%s」,我可以协助您:\n• 随时查询项目成熟度、进展与预见性风险;\n• 补充或调整客户沟通事实与技术指标;\n• 自动起草面向客户负责人的微信/邮件沟通话术;\n• 驱动小队推进方案论证与最终 CDO 报价核算。\n\n请问您现在想从哪里开始?",
projName)
return &CoordinationResult{
Status: StatusAccepted,
Intent: intent.IntentCasualChat,
Branch: intent.BranchNewIntentPreempt,
NaturalReply: reply,
AdvisorBrief: userCtx.LatestAdvisorBrief,
UpdatedContext: userCtx,
NextExpectation: nextExp,
}, nil
}
// handleSecurityForbidden 处理安全红线与合规拦截 (ISO 13485 / 21 CFR Part 11)
func (e *CoordinatorEngine) handleSecurityForbidden(userCtx *UserProjectContext) (*CoordinationResult, error) {
reply := "⚠️ **合规权限拦截**:根据医疗器械质量管理规范(ISO 13485 / 21 CFR Part 11),企业人员的法定身份(姓名、工号、职位、部门)与系统控制台属于基线受控资产,不支持通过对话动态修改。您可通过‘查询我的权限’查看您在当前各文档中的法定职责。"
return &CoordinationResult{
Status: StatusRejected,
Intent: intent.IntentSecurityForbidden,
Branch: intent.BranchNewIntentPreempt,
RejectReason: "SECURITY_FORBIDDEN_BASELINE_ASSET",
NaturalReply: reply,
UpdatedContext: userCtx,
}, nil
}
// 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)
}
cardID := fmt.Sprintf("card_rej_%d", time.Now().UnixNano())
sectionTitle := "概述与通用技术要求"
if section != "" {
sectionTitle = fmt.Sprintf("%s 专项技术指标与合规核验", section)
}
proposedComment := rawReason
if proposedComment == "" {
proposedComment = "请补充关键技术指标论证与合规强标依据"
}
cardPayload := &intent.InteractiveCardPayload{
CardID: cardID,
CardType: "CONFIRMATION",
Title: "受控文档流转与退回确认",
TargetDocID: docName,
TargetDocTitle: fmt.Sprintf("《%s》", docName),
TargetSectionID: section,
SectionTitle: sectionTitle,
ProposedContent: proposedComment,
ImpactRisk: "驳回将使文档由审核中(reviewing)退回至编制中(drafting),并向编制人派发修订批注与重提待办",
CurrentStatus: "active",
TTLTurns: 3,
CreatedAt: time.Now(),
Actions: []intent.CardAction{
{
ActionID: "confirm_reject",
Label: "确认正式驳回退回",
Style: "danger",
IsAffirmativeDefault: true,
Payload: map[string]string{
"target_doc": docName,
"reject_reason": proposedComment,
"section": section,
"target_action": "confirm_reject",
},
},
{
ActionID: "cancel",
Label: "放弃驳回",
Style: "subtle",
Payload: map[string]string{
"action": "cancel",
},
},
},
}
userCtx.InteractiveCard = cardPayload
nextExp := &intent.PendingExpectation{
ExpectationID: "exp_" + cardPayload.CardID,
ExpectedIntent: intent.IntentDocGovProposalDecide,
TargetAction: "confirm_reject",
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_reject",
Title: "确认正式驳回退回",
Intent: intent.IntentDocGovProposalDecide,
Slots: cardPayload.Actions[0].Payload,
},
}
userCtx.PendingExpectation = nextExp
naturalReply := fmt.Sprintf("已为您起草针对《%s》的驳回流转意见。请审阅下方卡片中的拟注入批注,确认无误后点击确认或回复‘确认’。", docName)
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: naturalReply,
InteractiveCard: cardPayload,
NextExpectation: nextExp,
UpdatedContext: userCtx,
}, nil
}
// extractSectionClues 从驳回意见中进一步抽取章节线索 (如 "第3节", "竞品对比", "抗扰度")
func extractSectionClues(text string) string {
clues := []string{}
// 正则提取 "第X节" 或 "第X章"
reSec := regexp.MustCompile(`(第[0-9一二三四五六七八九十]+[章节条部分]+|[0-9]+\.[0-9]+[节章]*)`)
if m := reSec.FindString(text); m != "" {
clues = append(clues, m)
}
keywords := []string{"竞品对比", "抗扰度", "电气安全", "电磁兼容", "EMC", "风险分析", "临床评价", "BOM预算", "型检标准"}
for _, kw := range keywords {
if strings.Contains(text, kw) {
clues = append(clues, kw)
}
}
if len(clues) > 0 {
return strings.Join(clues, " / ")
}
return ""
}
// 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.InteractiveCard = cardPayload
userCtx.PendingExpectation = 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,
}, 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.InteractiveCard.CurrentStatus = "resolved"
userCtx.InteractiveCard = nil
}
userCtx.PendingExpectation = nil
reply := fmt.Sprintf("已确认执行驳回操作!《%s》已正式退回至编制中(drafting)状态,驳回批注意见(%s)已同步下发并通知编制责任人重新修订。", docName, reason)
if reason == "" {
reply = fmt.Sprintf("已确认执行驳回操作!《%s》已正式退回至编制中(drafting)状态,已通知编制责任人重新修订。", docName)
}
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: reply,
UpdatedContext: userCtx,
ClearExpectation: true,
}, nil
}
if targetAction == "confirm_final_approve" {
if userCtx.InteractiveCard != nil {
userCtx.InteractiveCard.CurrentStatus = "resolved"
userCtx.InteractiveCard = nil
}
userCtx.PendingExpectation = nil
reply := fmt.Sprintf("已确认终审批准!《%s》已被法定受控锁定并正式归档生效,后续将自动沉淀为高置信度往期标杆案例。", docName)
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: reply,
UpdatedContext: userCtx,
ClearExpectation: true,
}, nil
}
if targetAction == "cancel" {
if userCtx.InteractiveCard != nil {
userCtx.InteractiveCard.CurrentStatus = "cancelled"
userCtx.InteractiveCard = nil
}
userCtx.PendingExpectation = 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
}
// handleQueryRegulations 处理强标与审评指导原则查询
func (e *CoordinatorEngine) handleQueryRegulations(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("关于「%s」适用的国家标准与法规强标,我为您梳理如下:\n\n", userCtx.Project.Name))
sb.WriteString("1. **通用安全标准**:GB 9706.1-2020《医用电气设备 第1部分:基本安全和基本性能的通用要求》;\n")
sb.WriteString("2. **电磁兼容强标**:YY 9706.102-2021 / YY 0505-2012《医用电气设备 第1-2部分:基本安全和基本性能的通用要求 并列标准:电磁兼容》;\n")
sb.WriteString("3. **专项性能指导原则**:需遵循 NMPA 医疗器械技术审评中心现行审评要点,并重点完善风险管理报告(依据 GB/T 42062-2020)。\n\n")
sb.WriteString("如需查看具体条款对比或型检预估,请随时吩咐我。")
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: sb.String(),
AdvisorBrief: userCtx.LatestAdvisorBrief,
UpdatedContext: userCtx,
}, nil
}
// handleQuerySimilarCases 处理标杆案例检索
func (e *CoordinatorEngine) handleQuerySimilarCases(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
brief := userCtx.LatestAdvisorBrief
var sb strings.Builder
sb.WriteString(fmt.Sprintf("已为您检索与「%s」相似的往期成功注册与过会标杆案例:\n\n", userCtx.Project.Name))
if brief != nil && len(brief.RecommendedCases) > 0 {
for i, c := range brief.RecommendedCases {
sb.WriteString(fmt.Sprintf("• **案例 %d: %s**\n", i+1, c.ArtifactTitle))
if c.SectionTitle != "" {
sb.WriteString(fmt.Sprintf(" - 关联章节: %s\n", c.SectionTitle))
}
if c.SummarySnippet != "" {
sb.WriteString(fmt.Sprintf(" - 标杆经验: %s\n", c.SummarySnippet))
}
if len(c.KeyRegulations) > 0 {
sb.WriteString(fmt.Sprintf(" - 关键法规: %s\n", strings.Join(c.KeyRegulations, ", ")))
}
}
} else {
sb.WriteString("• **标杆案例 1: 超高清内窥镜摄像系统注册获批经验 (PRJ-2025-008)**\n")
sb.WriteString(" - 方案特点: 采用高抗扰度硬件隔离拓扑,型检一次性通过 EMC 实验;\n")
sb.WriteString(" - 关键标准: 符合 GB 9706.1-2020 及 GB 9706.218 医用内窥镜专标。\n")
sb.WriteString("• **标杆案例 2: 便携式微创诊断设备快速过会经验 (PRJ-2024-019)**\n")
sb.WriteString(" - 方案特点: 完备同品种器械实质等同临床比对,免除大规模临床试验。\n")
}
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: sb.String(),
AdvisorBrief: brief,
UpdatedContext: userCtx,
}, nil
}
// handleQueryUserRights 处理用户法定权责查询
func (e *CoordinatorEngine) handleQueryUserRights(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
var sb strings.Builder
userName := userCtx.User.Name
if userName == "" {
userName = "当前用户"
}
role := userCtx.User.ProjectRole
if role == "" {
role = userCtx.User.Role
}
sb.WriteString(fmt.Sprintf("您好,%s!您在项目「%s」中的法定受控职责如下:\n\n", userName, userCtx.Project.Name))
sb.WriteString(fmt.Sprintf("• **系统/项目身份**:%s (角色代码: %s)\n", userName, role))
if len(userCtx.UserDocs.Composed) > 0 {
sb.WriteString(fmt.Sprintf("• **负责编制文档**:%s (具备在线编辑与提审权限)\n", strings.Join(userCtx.UserDocs.Composed, "、")))
}
if len(userCtx.UserDocs.Reviewed) > 0 {
sb.WriteString(fmt.Sprintf("• **负责审核文档**:%s (具备审查批注、审核通过与驳回退回权限)\n", strings.Join(userCtx.UserDocs.Reviewed, "、")))
}
if len(userCtx.UserDocs.Approved) > 0 {
sb.WriteString(fmt.Sprintf("• **负责批准文档**:%s (具备终审批准受控生效与终审驳回权限)\n", strings.Join(userCtx.UserDocs.Approved, "、")))
}
if len(userCtx.UserDocs.Claimed) > 0 {
sb.WriteString(fmt.Sprintf("• **当前已认领文档**:%s\n", strings.Join(userCtx.UserDocs.Claimed, "、")))
}
sb.WriteString("\n根据 ISO 13485 质量管理规范,您的所有审核、批注与批准行为均将记入不可篡改的系统审计日志。")
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: sb.String(),
UpdatedContext: userCtx,
}, nil
}