Files
Agent-System/internal/coordinator/engine.go
T
weichenandCursor 5e05d966b4 feat(intent): bridge chat into impact analysis and revision proposals
Analysis stays read-only, and a revoked project member cannot confirm an old proposal.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-20 15:35:07 +00:00

1860 lines
71 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"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"chat-agent-system/internal/agent"
"chat-agent-system/internal/app/stageflow"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/intent"
"chat-agent-system/internal/kb"
"chat-agent-system/internal/maturity"
"chat-agent-system/internal/projectinstance"
"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"
)
// PendingAction 声明对话层需执行的真实动作(由 Session 持有服务后执行)。
type PendingAction struct {
Kind string // docgov.review_approve | docgov.approver_approve | docgov.reject | taskrun.resume | taskrun.restart
DocID string
RunID string
WorkDir string
Reason string
Comment string
Rollback docgov.DocumentState
TaskKey string
}
// 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"`
ReplySource string `json:"reply_source,omitempty"` // pi-agent | template
PendingAction *PendingAction `json:"-"`
// Task projection for clickable document entry (sync TRIGGER_TASK / adhoc).
TaskStatus string `json:"task_status,omitempty"`
DocID string `json:"doc_id,omitempty"`
ArtifactTitle string `json:"artifact_title,omitempty"`
TaskKey string `json:"task_key,omitempty"`
PendingAgentCandidateID int64 `json:"pending_agent_candidate_id,omitempty"`
PendingChangeReportID int64 `json:"pending_change_report_id,omitempty"`
PendingRiskLevel string `json:"pending_risk_level,omitempty"`
}
// CoordinatorEngine L2 协调引擎
type CoordinatorEngine struct {
stageTasks StageTaskRunner
instSvc *projectinstance.Service
assembler *ContextAssembler
dialogueBackend agent.Backend
dataDir string
ctxMgr *ctxmgr.ContextManager
kbClient *kb.Client
impact ImpactActions
projectAccess ProjectAccess
}
// StageTaskRunner is the application owner for ExecuteTask / ExecuteStage.
type StageTaskRunner interface {
ExecuteTask(ctx context.Context, req stageflow.TaskRunRequest) (*stageflow.TaskRunResult, error)
ExecuteStage(ctx context.Context, req stageflow.StageRunRequest) (*stageflow.StageRunResult, error)
}
func (e *CoordinatorEngine) SetStageTaskRunner(r StageTaskRunner) {
if e != nil {
e.stageTasks = r
}
}
func (e *CoordinatorEngine) tasks() StageTaskRunner {
if e == nil {
return nil
}
return e.stageTasks
}
func (e *CoordinatorEngine) SetContextServices(ctxMgr *ctxmgr.ContextManager, kbClient *kb.Client) {
if e != nil {
e.ctxMgr = ctxMgr
e.kbClient = kbClient
}
}
// SetInstanceService 注入项目工作流实例服务(对话推进与实例门禁合流)。
func (e *CoordinatorEngine) SetInstanceService(svc *projectinstance.Service) {
if e != nil {
e.instSvc = svc
}
}
// SetImpactActions wires read-only impact analysis and confirmed revision proposals.
func (e *CoordinatorEngine) SetImpactActions(a ImpactActions) {
if e != nil {
e.impact = a
}
}
func NewCoordinatorEngine(runner StageTaskRunner) *CoordinatorEngine {
return &CoordinatorEngine{
stageTasks: runner,
assembler: NewContextAssembler(),
}
}
// NewCoordinatorEngineWithDialogue 注入对话 LLM 后端(Phase A:CASUAL / AMBIGUOUS)
func NewCoordinatorEngineWithDialogue(runner StageTaskRunner, dialogueBackend agent.Backend, dataDir string) *CoordinatorEngine {
return &CoordinatorEngine{
stageTasks: runner,
assembler: NewContextAssembler(),
dialogueBackend: dialogueBackend,
dataDir: dataDir,
}
}
// 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 {
_ = ApplyContextUpdate(userCtx, ContextUpdate{ExpectedVersion: userCtx.Version, TickInteractive: true})
}
if userCtx.PendingExpectation != nil && !userCtx.PendingExpectation.ExpiresAt.IsZero() && time.Now().After(userCtx.PendingExpectation.ExpiresAt) {
userCtx.applyPending(nil)
}
top := recRes.TopIntent
if top == nil {
top = &intent.CandidateIntent{
Intent: intent.IntentCasualChat,
Confidence: 1.0,
}
}
// Phase 5:RankedFrames 写/读冲突二次门禁
if intent.RankedFrameConflictRequiresClarification(recRes) {
msg := "检测到查询与写操作意图接近,为避免误执行,请明确您是要查询状态,还是要执行变更操作?"
return &CoordinationResult{
Status: StatusClarificationRequired,
Intent: top.Intent,
Branch: recRes.Branch,
ClarificationPrompt: msg,
NaturalReply: msg,
UpdatedContext: userCtx,
}, nil
}
// Phase 5:有限多意图计划(query+query / expectation_plus_new)
if recRes.IntentPlan != nil && intent.CurrentFeatureFlags().MultiIntentEnabled {
if planRes, planErr := e.executeIntentPlan(ctx, recRes, userCtx); planRes != nil {
return planRes, planErr
}
}
// Phase 5:V2 灰度路由(intent_v2_decision 开启且档位允许时对齐 V2 帧)
if effective := intent.ResolveEffectiveIntent(recRes); effective != top.Intent {
top = &intent.CandidateIntent{
Intent: effective,
Confidence: top.Confidence,
Branch: top.Branch,
FilledSlots: top.FilledSlots,
MissingSlots: top.MissingSlots,
Reasoning: top.Reasoning + " (v2_decision)",
ClarificationQuestion: top.ClarificationQuestion,
MatchedOption: top.MatchedOption,
PatchSlots: top.PatchSlots,
IsDecline: top.IsDecline,
}
recRes.TopIntent = top
}
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)
}
logRouteDecision(ctx, recRes, RouteIntent(top.Intent, TaskProfile{}))
// 1. 若缺少核心槽位且需定向澄清(优先于模糊咨询,避免缺槽追问被参谋兜底截断)
if recRes.RequiresClarifying && top.ClarificationQuestion != "" {
return &CoordinationResult{
Status: StatusClarificationRequired,
Intent: top.Intent,
Branch: recRes.Branch,
ClarificationPrompt: top.ClarificationQuestion,
NaturalReply: top.ClarificationQuestion,
UpdatedContext: userCtx,
}, nil
}
// 2. 若为分支 3 (模糊/分忧引导),触发参谋主动分析与建议(Phase A:优先真实 LLM)
if recRes.Branch == intent.BranchAmbiguousFallback {
res, err := e.handleAmbiguousConsult(ctx, top, userCtx, recRes.RawInput)
if err == nil && res != nil {
e.enrichAdvisorBriefWithCases(ctx, res.AdvisorBrief, userCtx, top.FilledSlots)
}
return res, err
}
// 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(ctx, top, userCtx, recRes.RawInput)
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.IntentTriggerAdhocTask:
coordRes, coordErr = e.handleTriggerAdhocTask(ctx, top, userCtx, recRes.RawInput)
case intent.IntentCasualChat:
coordRes, coordErr = e.handleCasualChat(ctx, userCtx, recRes.RawInput)
case intent.IntentSecurityForbidden:
coordRes, coordErr = e.handleSecurityForbidden(userCtx)
case intent.IntentDocGovReviewReject, intent.IntentDocGovFinalReject:
coordRes, coordErr = e.handleDocGovReject(ctx, top, userCtx)
case intent.IntentQueryRejectReason:
coordRes, coordErr = e.handleQueryRejectReason(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)
case intent.IntentSwitchWorkflow:
coordRes, coordErr = e.handleSwitchWorkflow(ctx, top, userCtx)
case intent.IntentTriggerWorkflow:
coordRes, coordErr = e.handleTriggerWorkflow(ctx, top, userCtx)
case intent.IntentQueryWorkflowStatus:
coordRes, coordErr = e.handleQueryWorkflowStatus(ctx, top, userCtx)
case intent.IntentQueryDocStatus:
coordRes, coordErr = e.handleQueryDocStatus(ctx, top, userCtx)
case intent.IntentHelpCapabilities:
coordRes, coordErr = e.handleHelpCapabilities(ctx, top, userCtx)
case intent.IntentUnsupportedAction:
coordRes, coordErr = e.handleUnsupportedAction(ctx, top, userCtx)
case intent.IntentRunStatusQuery:
coordRes, coordErr = e.handleRunStatusQuery(ctx, top, userCtx)
case intent.IntentRunResume:
coordRes, coordErr = e.handleRunResume(ctx, top, userCtx)
case intent.IntentRunRestart:
coordRes, coordErr = e.handleRunRestart(ctx, top, userCtx)
case intent.IntentImpactAnalyze:
coordRes, coordErr = e.handleImpactAnalyze(ctx, top, userCtx, recRes.RawInput)
case intent.IntentRevisePropose:
coordRes, coordErr = e.handleRevisePropose(ctx, top, userCtx, recRes.RawInput)
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.ctxMgr == nil {
return
}
ctxMgr := e.ctxMgr
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.applyPending(nil)
if userCtx.InteractiveCard != nil {
userCtx.dismissCard("cancelled")
}
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(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext, rawInput string) (*CoordinationResult, error) {
currentStage := userCtx.Project.CurrentStage
projName := userCtx.Project.Name
if projName == "" {
projName = "当前项目"
}
// 只在低置信度的模糊咨询里,才用关键词改走测试分析。高置信度保持原意图。
if cand.Confidence < 0.6 && (strings.Contains(rawInput, "测试") || strings.Contains(rawInput, "验证") || strings.Contains(rawInput, "型检") ||
strings.Contains(rawInput, "难点") || strings.Contains(rawInput, "注意") || strings.Contains(rawInput, "比较难") ||
strings.Contains(rawInput, "方案")) {
if cand.FilledSlots == nil {
cand.FilledSlots = make(map[string]string)
}
cand.FilledSlots[intent.SlotQueryTopic] = "testing_analysis"
return e.handleQueryProgress(ctx, cand, userCtx, rawInput)
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("完全理解您想再掂量一下。针对「%s」目前的节点,我帮您快速对照一下:\n\n", projName))
if currentStage == maturity.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("当前各项工作进展平稳。您可以把零散想法随时告诉我,我来帮您沉淀;时机成熟时,再一起推进下一阶段也完全可以。")
}
reply := sb.String()
source := ReplySourceTemplate
if llmReply, llmSource := e.generateDialogueReply(ctx, rawInput, userCtx); llmReply != "" {
reply = llmReply
source = llmSource
}
nextExp := BuildNextExpectation(currentStage, projName)
// 路由层:复杂自由任务画像命中时,先给即时初答,再附升级建议(复用挂起期望机制,含每会话冷却)
reply, nextExp = maybeAttachUpgradeHint(rawInput, userCtx, reply, nextExp)
userCtx.applyPending(nextExp)
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: intent.BranchAmbiguousFallback,
NaturalReply: reply,
AdvisorBrief: userCtx.LatestAdvisorBrief,
UpdatedContext: userCtx,
NextExpectation: nextExp,
ReplySource: source,
}, nil
}
func (e *CoordinatorEngine) handleQueryProgress(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext, rawInput string) (*CoordinationResult, error) {
brief := userCtx.LatestAdvisorBrief
topic := cand.FilledSlots[intent.SlotQueryTopic]
nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name)
userCtx.applyPending(nextExp)
// 1. 若为测试方案/难点/验证咨询,直接调取测试经理与参谋专业知识库解答
if topic == "testing_analysis" {
if llmReply, llmSource := e.generateDialogueReply(ctx, rawInput, userCtx); llmReply != "" {
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: llmReply,
AdvisorBrief: brief,
UpdatedContext: userCtx,
NextExpectation: nextExp,
ReplySource: llmSource,
}, nil
}
projName := userCtx.Project.Name
if projName == "" || projName == "医疗器械研发项目" {
projName = "肌骨超声AI辅助诊断系统"
}
greet := strings.TrimSpace(userCtx.User.Name)
if greet == "" {
greet = "您好"
} else {
greet += ",您好"
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("%s!针对「%s」的系统验证方案(SVP)与型式检验要求,我为您梳理以下核心难点项目与特别注意事项:\n\n", greet, projName))
sb.WriteString("### 一、 核心测试难点项目\n\n")
sb.WriteString("1. **声学输出安全与探头表面温升极限(GB 9706.237-2020)**\n")
sb.WriteString(" • **难点分析**:肌骨超声常需配合高频线阵探头(10-18MHz)进行浅表滑膜微血流彩色多普勒(CDFI/Power Doppler)检测。连续高占空比发射脉冲会导致探头声透镜声吸收发热。根据强标要求,探头施加于人体的表面温升严禁超过 **41℃**,同时机械指数(MI)需严格低于 1.9,热指数(TIS/TIB)需低于 1.0。\n")
sb.WriteString(" • **试验考量**:需在 33℃ 恒温仿生水槽中进行连续 30 分钟满功率复合工况热电偶布点实测,温控算法防呆测试难度较高。\n\n")
sb.WriteString("2. **高频浅表微小解剖量测公差与体模标定(GB 10188 / KS107BG 仿组织体模)**\n")
sb.WriteString(" • **难点分析**:肌腱撕裂缝隙及滑膜增厚量测精度要求达到 **±0.3mm 或 ±3%**。在 1~3cm 浅表盲区内,探头晶片近场声束干涉容易造成线靶伪影或横向展宽。\n")
sb.WriteString(" • **试验考量**:对组织仿生体模内部靶群的纵向、横向分辨力及几何位置示值误差进行多批次盲态复测,对探头声阻抗匹配层与声透镜一致性要求极其苛刻。\n\n")
sb.WriteString("3. **AI 算法病灶识别与量化分割的泛化鲁棒性(YY/T 0664 / 软件性能测试)**\n")
sb.WriteString(" • **难点分析**:超声影像固有的斑点噪声(Speckle Noise)、声影及不同患者皮下脂肪厚度差异极大。滑膜边界 Dice 系数(≥0.85)在骨皮质高回声边缘极易出现伪阳性粘连;血流微信号半定量分级在探头晃动时易出现运动伪影误判。\n")
sb.WriteString(" • **试验考量**:需要建立多中心、不同机型/探头采集的独立盲测黄金数据集(由 3 名资深超声主任医师独立背对背标定一致性),验证测试集与训练集严禁污染交叉。\n\n")
sb.WriteString("### 二、 需要特别注意的关键地方\n\n")
sb.WriteString("• **标准体模计量有效期与测试环境温湿度**:仿生超声体模(如 KS107BG)的声速(标准 1540 m/s)对环境温度极为敏感,必须在 **23℃ ± 2℃** 严格恒温实验室实施,且体模必须在有效计量标定期内,否则测距误差将产生系统性偏移;\n")
sb.WriteString("• **电磁兼容(EMC GB 9706.102-2021)抗扰度储备**:高频微弱回波信号接收通道抗射频电磁场辐射敏感度高,测试方案中需提前验证探头线缆双层屏蔽接地与机箱屏蔽缝隙,避免正式型检时辐射发射(RE)超标返工;\n")
sb.WriteString("• **DICOM 与数据脱敏合规**:软件系统测试中,所有导出样本、测试影像的患者个人隐私标识(PII)脱敏规则需执行 100% 覆盖率验证;\n")
sb.WriteString("• **极端异常输入与容错安全防呆**:探头带电热插拔、探头声透镜声胶脱落阻抗异常检测、长时间无操作探头自动降频防发热休眠逻辑,均需设计破坏性负向测试用例。\n\n")
sb.WriteString("💡 **建议行动**:若您需要,我可以为您起草该模块对应的《系统验证方案 (SVP) 关键测试用例矩阵》,列出详细的测试条件、前置配置与合格判据。")
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: sb.String(),
AdvisorBrief: brief,
UpdatedContext: userCtx,
NextExpectation: nextExp,
ReplySource: ReplySourceTemplate,
}, nil
}
// 2. 若为特定技术/架构/功耗方案咨询,直接调取参谋专业知识库解答,不强依赖阶段简报
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
}
// 2. 无参谋简报的降级应答(修复:曾直接返回 V0.1 推销模板导致答非所问)
// 2a. 优先真实对话 LLM 结合项目上下文直接回答本轮问题(Phase A 机制延伸至 QUERY_PROGRESS 降级路径);
// 2b. LLM 不可用时,基于黑板事实合成 intake 基准简报,仍按查询主题给出实质研判,末尾附轻量摸底引导。
synthesized := false
replySource := ""
if brief == nil {
if llmReply, llmSource := e.generateDialogueReply(ctx, rawInput, userCtx); llmReply != "" {
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: llmReply,
UpdatedContext: userCtx,
NextExpectation: nextExp,
ReplySource: llmSource,
}, nil
}
brief = synthesizeBaselineBrief(userCtx)
synthesized = true
replySource = ReplySourceTemplate
}
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))
}
}
}
// 3. 合成简报场景:先给出实质回答,再轻量附上摸底引导(保留引导、不再答非所问、不强推)
if synthesized {
sb.WriteString("\n\n💡 说明:项目尚未启动过任何交付任务,以上研判基于行业基准假设。")
sb.WriteString("若希望沉淀更精准的第一版立项画像与沟通话术,您可以吩咐我「生成一版 V0.1 需求收集表(立项摸底)」,或直接补充器械类型、目标市场、预算等关键细节。")
}
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: sb.String(),
AdvisorBrief: brief,
UpdatedContext: userCtx,
NextExpectation: nextExp,
ReplySource: replySource,
}, nil
}
// synthesizeBaselineBrief 在用户尚未执行过任何阶段/任务(无持久化参谋简报)时,
// 基于黑板事实与项目信息合成 intake 级基准参谋简报,确保查询类意图始终有实质研判可用。
func synthesizeBaselineBrief(userCtx *UserProjectContext) *squad.AdvisorBriefing {
fields := make(map[string]string)
for k, v := range userCtx.BlackboardFacts {
fields[k] = v
}
fields["project_name"] = userCtx.Project.Name
fields["customer_name"] = userCtx.Project.CustomerName
if userCtx.Project.DeviceType != "" {
fields["device_type"] = userCtx.Project.DeviceType
}
if userCtx.Project.IntendedUse != "" {
fields["intended_use"] = userCtx.Project.IntendedUse
}
if userCtx.Project.TargetMarket != "" {
fields["target_market"] = userCtx.Project.TargetMarket
}
return squad.ExtractAdvisorBriefing("intake", "", fields)
}
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.applyBlackboard(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.applyPending(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) {
if e.instSvc != nil && e.instSvc.Available() && userCtx != nil && userCtx.Project.ID != "" {
projectID, parseErr := strconv.ParseInt(userCtx.Project.ID, 10, 64)
userID, userErr := strconv.ParseInt(userCtx.User.ID, 10, 64)
if parseErr == nil && userErr == nil && projectID > 0 && userID > 0 {
isCreator := userCtx.Project.CreatorUserID > 0 && userID == userCtx.Project.CreatorUserID
settings, settingsErr := e.instSvc.GetSettings(ctx, projectID, userID, isCreator)
hasLiveGovernance := settingsErr == nil && settings != nil && settings.LiveInstance != nil &&
settings.LiveInstance.Status != projectinstance.InstanceCompleted &&
settings.LiveInstance.Status != "terminated"
if hasLiveGovernance {
if !isCreator {
return &CoordinationResult{
Status: StatusRejected,
Intent: cand.Intent,
Branch: cand.Branch,
RejectReason: projectinstance.ErrForbiddenNotCreator.Error(),
NaturalReply: "只有项目创建者可以确认推进工作流阶段。",
UpdatedContext: userCtx,
}, projectinstance.ErrForbiddenNotCreator
}
res, _, advErr := e.tryAdvanceWorkflowInstance(ctx, projectID, userID)
if advErr != nil {
return &CoordinationResult{
Status: StatusRejected,
Intent: cand.Intent,
Branch: cand.Branch,
RejectReason: advErr.Error(),
NaturalReply: fmt.Sprintf("阶段推进未通过门禁:%v", advErr),
UpdatedContext: userCtx,
}, advErr
}
return res, nil
}
if isCreator {
if res, handled, advErr := e.tryAdvanceWorkflowInstance(ctx, projectID, userID); handled {
if advErr != nil {
return &CoordinationResult{
Status: StatusRejected,
Intent: cand.Intent,
Branch: cand.Branch,
RejectReason: advErr.Error(),
NaturalReply: fmt.Sprintf("阶段推进未通过门禁:%v", advErr),
UpdatedContext: userCtx,
}, advErr
}
return res, nil
}
}
}
}
targetStageStr := cand.FilledSlots[intent.SlotTargetStage]
targetStage := maturity.MaturityStage(targetStageStr)
if targetStage == "" {
targetStage = maturity.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.applyBlackboard(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.tasks() == nil {
userCtx.Project.CurrentStage = targetStage
userCtx.AvailableActions = ComputeAvailableActions(targetStage, userCtx.User.ProjectRole)
nextExp := BuildNextExpectation(targetStage, userCtx.Project.Name)
userCtx.applyPending(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.tasks().ExecuteStage(ctx, stageflow.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.applyPending(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.applyPending(nextExp)
if e.tasks() == nil {
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: fmt.Sprintf("已收到生成【%s】的指令。", taskKey),
UpdatedContext: userCtx,
NextExpectation: nextExp,
}, nil
}
var projectID, userID int64
if userCtx.Project.ID != "" {
projectID = parseDecimalID(userCtx.Project.ID)
}
if userCtx.User.ID != "" {
userID = parseDecimalID(userCtx.User.ID)
}
taskRes, err := e.tasks().ExecuteTask(ctx, stageflow.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,
ProjectID: projectID,
UserID: userID,
})
if err != nil {
if errors.Is(err, docgov.ErrDocumentBusy) {
msg := "该文档正在由项目助手生成或他人编辑中,请稍后再发起更新。您可以先继续对话或处理其他文档。"
if taskRes != nil && taskRes.ErrorReason != "" {
msg = taskRes.ErrorReason + "。您可以先继续对话或处理其他文档。"
}
return &CoordinationResult{
Status: StatusRejected,
Intent: cand.Intent,
Branch: cand.Branch,
RejectReason: err.Error(),
NaturalReply: msg,
UpdatedContext: userCtx,
}, nil
}
if errors.Is(err, docgov.ErrDocumentLocked) {
return &CoordinationResult{
Status: StatusRejected,
Intent: cand.Intent,
Branch: cand.Branch,
RejectReason: err.Error(),
NaturalReply: "该文档已进入审核/批准锁定状态,无法自动覆写正文。如需修订请走驳回解锁或变更提案流程。",
UpdatedContext: userCtx,
}, 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")
reply, taskStatus := formatTriggerTaskReply(taskRes, outPath)
res := &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: reply,
AdvisorBrief: taskRes.AdvisorBrief,
ArtifactPath: outPath,
UpdatedContext: userCtx,
NextExpectation: nextExp,
TaskStatus: taskStatus,
TaskKey: taskRes.TaskKey,
ArtifactTitle: taskRes.ArtifactTitle,
}
attachTaskRunProjection(res, taskRes)
return res, nil
}
// formatTriggerTaskReply builds sync coordinator copy for a TaskRunResult.
// Pending high-risk candidates must not claim "正文已保存 / 已完成".
func formatTriggerTaskReply(taskRes *stageflow.TaskRunResult, outPath string) (reply string, taskStatus string) {
if taskRes == nil {
return "任务执行失败。", stageflow.TaskStatusFailed
}
taskStatus = taskRes.TerminalStatus()
if taskRes.AwaitsAgentConfirmation() {
return taskRes.CompletionSummary(), taskStatus
}
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))
}
}
return sb.String(), taskStatus
}
func attachTaskRunProjection(res *CoordinationResult, taskRes *stageflow.TaskRunResult) {
if res == nil || taskRes == nil {
return
}
if taskRes.DocRecord != nil {
res.DocID = taskRes.DocRecord.DocID
}
if taskRes.AwaitsAgentConfirmation() {
res.PendingAgentCandidateID = taskRes.PendingAgentCandidateID
res.PendingChangeReportID = taskRes.PendingChangeReportID
res.PendingRiskLevel = taskRes.PendingRiskLevel
}
}
// handleTriggerAdhocTask 处理自由任务升级确认(路由层 R1 升级建议的兑现路径):
// 基于任务简报组装特设小队,走 planAutonomousAdhoc 自主规划 + TaskGov 全量治理
// (预算账本 / 执行轨迹 / 重复识别 / 归档锁定 / 质量门禁修复闭环)。
func (e *CoordinatorEngine) handleTriggerAdhocTask(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext, rawInput string) (*CoordinationResult, error) {
brief := cand.FilledSlots[intent.SlotTaskBrief]
if brief == "" {
brief = rawInput
}
title := deriveAdhocTitle(brief)
taskKey := sanitizeAdhocTaskKey(title)
nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name)
userCtx.applyPending(nextExp)
if e.tasks() == nil {
return &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: fmt.Sprintf("已收到自由任务《%s》的升级指令(当前环境未接入小队服务,简报已登记)。\n任务简报: %s", title, brief),
UpdatedContext: userCtx,
NextExpectation: nextExp,
}, nil
}
var projectID, userID int64
if userCtx.Project.ID != "" {
projectID = parseDecimalID(userCtx.Project.ID)
}
if userCtx.User.ID != "" {
userID = parseDecimalID(userCtx.User.ID)
}
if st := stream.FromContext(ctx); st != nil {
st.EmitPhase("squad_assembly", fmt.Sprintf("正在为自由任务《%s》组建特设智能体小队(动态规划 + 预算管控 + 归档锁定)...", title), "leader", 0, 0, 5)
}
// 任务简报材料化:作为首要输入材料注入工作区(context/materials_text/task_brief.md),
// 供 organizer 专员在容器内直接读到用户原始诉求(否则首步将报"无材料可整理")
briefMaterial := stageflow.MaterialInput{
Filename: "task_brief.md",
Content: []byte(fmt.Sprintf("# 任务简报(用户原始诉求,权威等级:primary_evidence)\n\n%s\n", brief)),
}
taskRes, err := e.tasks().ExecuteTask(ctx, stageflow.TaskRunRequest{
TaskKey: taskKey,
Title: title,
ProjectName: userCtx.Project.Name,
CustomerName: userCtx.Project.CustomerName,
DeviceType: userCtx.Project.DeviceType,
IntendedUse: userCtx.Project.IntendedUse,
TargetMarket: userCtx.Project.TargetMarket,
Fields: userCtx.BlackboardFacts,
Materials: []stageflow.MaterialInput{briefMaterial},
AutoRepair: true,
ProjectID: projectID,
UserID: userID,
})
if err != nil {
if taskRes != nil && taskRes.WorkDir != "" {
userCtx.WorkDir = taskRes.WorkDir
}
return &CoordinationResult{
Status: StatusRejected,
Intent: cand.Intent,
Branch: cand.Branch,
RejectReason: err.Error(),
NaturalReply: fmt.Sprintf("自由任务《%s》执行失败: %v", title, err),
UpdatedContext: userCtx,
}, err
}
userCtx.LatestAdvisorBrief = taskRes.AdvisorBrief
if taskRes.WorkDir != "" {
userCtx.WorkDir = taskRes.WorkDir
}
outPath := filepath.Join(taskRes.WorkDir, "output.md")
var sb strings.Builder
taskStatus := taskRes.TerminalStatus()
if taskRes.AwaitsAgentConfirmation() {
sb.WriteString(taskRes.CompletionSummary())
} else {
sb.WriteString(fmt.Sprintf("已为您组建特设小队并完成自由任务《%s》!\n任务简报: %s\n交付物正文已保存至:%s\n耗时: %v\n",
title, brief, outPath, taskRes.Duration))
if taskRes.QualityReport != nil {
verdict := "⚠️ 未完全通过"
if taskRes.QualityReport.Passed {
verdict = "✅ 通过"
}
sb.WriteString(fmt.Sprintf("质量门禁: %s(评分 %.0f)\n", verdict, taskRes.QualityReport.Score))
}
if gov := governanceSummary(taskRes.WorkDir); gov != "" {
sb.WriteString("治理审计: " + gov + "\n")
}
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))
}
}
}
res := &CoordinationResult{
Status: StatusAccepted,
Intent: cand.Intent,
Branch: cand.Branch,
NaturalReply: sb.String(),
AdvisorBrief: taskRes.AdvisorBrief,
ArtifactPath: outPath,
UpdatedContext: userCtx,
NextExpectation: nextExp,
TaskStatus: taskStatus,
TaskKey: taskRes.TaskKey,
ArtifactTitle: taskRes.ArtifactTitle,
}
attachTaskRunProjection(res, taskRes)
return res, nil
}
func (e *CoordinatorEngine) handleCasualChat(ctx context.Context, userCtx *UserProjectContext, rawInput string) (*CoordinationResult, error) {
projName := userCtx.Project.Name
if projName == "" {
projName = "当前医疗器械研发项目"
}
nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name)
userCtx.applyPending(nextExp)
nameSuffix := ""
if userCtx.User.Name != "" {
nameSuffix = "," + userCtx.User.Name
}
reply := fmt.Sprintf("您好%s!我是您的项目助手,很高兴协助您推进医疗器械立项与研发协同。\n\n针对「%s」,我可以帮您:\n• 查询进展、成熟度与风险;\n• 沉淀客户需求与技术指标;\n• 起草沟通话术与方案要点;\n• 推进方案论证与报价核算。\n\n如果您愿意,我们可以先从您当前最关心的一块开始——您想先看进展、补事实,还是直接聊方案?",
nameSuffix, projName)
source := ReplySourceTemplate
if llmReply, llmSource := e.generateDialogueReply(ctx, rawInput, userCtx); llmReply != "" {
reply = llmReply
source = llmSource
}
// 路由层:复杂自由任务画像命中时,先给即时初答,再附升级建议(复用挂起期望机制,含每会话冷却)
reply, nextExp = maybeAttachUpgradeHint(rawInput, userCtx, reply, nextExp)
userCtx.applyPending(nextExp)
return &CoordinationResult{
Status: StatusAccepted,
Intent: intent.IntentCasualChat,
Branch: intent.BranchNewIntentPreempt,
NaturalReply: reply,
AdvisorBrief: userCtx.LatestAdvisorBrief,
UpdatedContext: userCtx,
NextExpectation: nextExp,
ReplySource: source,
}, 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)
}
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
}
// handleQueryRejectReason 处理编制人等对驳回原因的查询(信息卡,无危险按钮)
func (e *CoordinatorEngine) handleQueryRejectReason(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
docName := cand.FilledSlots[intent.SlotTargetDoc]
if docName == "" {
docName = "当前受控文档"
}
role := effectiveDocRole(userCtx)
if role == "" {
role = "author"
}
// 优先从黑板或近期事件中取驳回意见
reason := ""
if userCtx.BlackboardFacts != nil {
reason = userCtx.BlackboardFacts["last_reject_reason"]
if reason == "" {
reason = userCtx.BlackboardFacts["reject_reason"]
}
}
if reason == "" && len(userCtx.RecentEvents) > 0 {
for i := len(userCtx.RecentEvents) - 1; i >= 0; i-- {
ev := userCtx.RecentEvents[i]
if strings.Contains(ev, "驳回") || strings.Contains(ev, "退回") {
reason = ev
break
}
}
}
resolved := ResolveRejectCard(role, "", docName, docName, "", reason, RejectUtteranceQuery)
if resolved.Card != nil {
userCtx.applyCard(resolved.Card)
}
return &CoordinationResult{
Status: StatusAccepted,
Intent: intent.IntentQueryRejectReason,
Branch: cand.Branch,
NaturalReply: resolved.Reply,
InteractiveCard: resolved.Card,
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.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
}
// handleQueryRegulations 处理强标与审评指导原则查询
func (e *CoordinatorEngine) handleQueryRegulations(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
searchQuery := ""
if userCtx != nil && userCtx.RewrittenQuery != "" {
searchQuery = userCtx.RewrittenQuery
} else if userCtx != nil && userCtx.TaskState != nil && userCtx.TaskState.TopicAnchor != "" {
searchQuery = userCtx.TaskState.TopicAnchor
} else if userCtx != nil {
searchQuery = userCtx.Project.Name
}
if userCtx != nil && userCtx.Project.DeviceType != "" && !strings.Contains(searchQuery, userCtx.Project.DeviceType) {
searchQuery += " " + userCtx.Project.DeviceType
}
var hits []kb.HitItem
if e != nil && e.kbClient != nil && strings.TrimSpace(searchQuery) != "" {
cacheKey := ctxmgr.HashKey("regulatory_search", searchQuery)
cache := ctxmgr.GetGlobalCache()
if cached, ok := cache.Get(ctxmgr.LayerC1Search, cacheKey); ok {
if ch, ok := cached.([]kb.HitItem); ok {
hits = ch
}
} else {
kbHits, err := e.kbClient.Search(ctx, searchQuery, kb.SearchOptions{
Domains: []kb.KnowledgeDomain{kb.DomainRegulatory},
Limit: 5,
})
if err == nil && len(kbHits) > 0 {
hits = kbHits
cache.Set(ctxmgr.LayerC1Search, cacheKey, hits, 30)
}
}
}
var sb strings.Builder
projectName := "医疗器械项目"
if userCtx != nil && userCtx.Project.Name != "" {
projectName = userCtx.Project.Name
}
if len(hits) > 0 {
if userCtx != nil {
userCtx.RegulationHits = hits
}
sb.WriteString(fmt.Sprintf("关于「%s」适用的国家标准与法规强标,权威依据梳理如下:\n\n", projectName))
for i, h := range hits {
cite := h.CiteKey
if cite == "" {
cite = fmt.Sprintf("〔%s〕", h.DocCode)
}
title := h.Title
if title == "" {
title = h.DocCode
}
sb.WriteString(fmt.Sprintf("%d. **%s** %s\n", i+1, title, cite))
if h.HeadingPath != "" {
sb.WriteString(fmt.Sprintf(" - 条款章节: %s\n", h.HeadingPath))
}
if h.Content != "" {
cText := strings.TrimSpace(h.Content)
if len([]rune(cText)) > 150 {
cText = string([]rune(cText)[:150]) + "..."
}
sb.WriteString(fmt.Sprintf(" - 条款要点: %s\n", cText))
}
}
sb.WriteString("\n上述标准与条款可作为型检与注册申报的权威技术支撑。如需深入比对专标或预估测试周期,请随时吩咐我。")
} else {
// 降级基准模板
sb.WriteString(fmt.Sprintf("关于「%s」适用的国家标准与法规强标,我为您梳理如下:\n\n", projectName))
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
}
// parseDecimalID accepts only a non-negative decimal integer.
// Values like u_sess_* must not become UserID 0 via a partial Sscanf match.
func parseDecimalID(raw string) int64 {
raw = strings.TrimSpace(raw)
if raw == "" {
return 0
}
n, err := strconv.ParseInt(raw, 10, 64)
if err != nil || n < 0 {
return 0
}
return n
}