package coordinator import ( "context" "fmt" "regexp" "strings" "chat-agent-system/internal/ctxmgr" "chat-agent-system/internal/intent" "chat-agent-system/internal/kb" "chat-agent-system/internal/squad" ) // 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, }) } } 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" { 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 e.applyNarrative(ctx, userCtx, "handleQueryProgress", rawInput, "", &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: sb.String(), AdvisorBrief: brief, UpdatedContext: userCtx, NextExpectation: nextExp, }), 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 e.applyNarrative(ctx, userCtx, "handleQueryProgress", rawInput, "", &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: sb.String(), AdvisorBrief: brief, UpdatedContext: userCtx, NextExpectation: nextExp, }), nil } // 2. 无参谋简报时基于黑板事实合成 intake 基准简报,再交由智能体组织叙述。 // LLM 不可用时仍按查询主题给出实质研判,末尾附轻量摸底引导。 synthesized := false if brief == nil { brief = synthesizeBaselineBrief(userCtx) synthesized = true } 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": if snap := e.latestSessionRun(ctx, userCtx); snap != nil { sb.WriteString(formatSessionRunReply(snap)) if brief != nil { sb.WriteString(fmt.Sprintf("\n项目成熟度仍为 %s(%d/100)。", brief.MaturityLevel, brief.MaturityScore)) } break } 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 e.applyNarrative(ctx, userCtx, "handleQueryProgress", rawInput, "", &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: sb.String(), AdvisorBrief: brief, UpdatedContext: userCtx, NextExpectation: nextExp, }), nil } func (e *CoordinatorEngine) latestSessionRun(ctx context.Context, userCtx *UserProjectContext) *SessionRunSnapshot { if e == nil || e.sessionRuns == nil || userCtx == nil || strings.TrimSpace(userCtx.ChatSessionID) == "" { return nil } snap, err := e.sessionRuns.LatestSessionRun(ctx, userCtx.ChatSessionID) if err != nil || snap == nil || snap.RunID == "" { return nil } return snap } func formatSessionRunReply(snap *SessionRunSnapshot) string { label := map[string]string{ "pending": "排队中", "running": "正在编写", "completed": "已完成", "failed": "执行失败", "finalizing_failed": "收尾失败", "interrupted": "已中断,可以继续", "cancelled": "已取消", "pending_author_confirmation": "待编制人确认", "quality_failed": "质量未通过", "blocked_pending_governance": "待激活工作流", } status := label[snap.Status] if status == "" { status = snap.Status } taskName := snap.TaskKey if taskName == "" { taskName = "当前任务" } var b strings.Builder b.WriteString(fmt.Sprintf("「%s」当前状态:%s。", taskName, status)) if snap.WorkspaceID != "" { b.WriteString(fmt.Sprintf("工作区标识:%s。", snap.WorkspaceID)) } if snap.ErrorMessage != "" && (snap.Status == "failed" || snap.Status == "interrupted" || snap.Status == "finalizing_failed") { b.WriteString("原因:" + snap.ErrorMessage + "。") } return b.String() } // 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) } // 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 e.applyNarrative(ctx, userCtx, "handleQueryRejectReason", "", "", &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 "" } // 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 e.applyNarrative(ctx, userCtx, "handleQueryRegulations", "", "", &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: sb.String(), AdvisorBrief: userCtx.LatestAdvisorBrief, UpdatedContext: userCtx, }), nil } // handleQueryMaterials 检索项目资产与法规库中的说明书、视频、竞品资料。 // 有命中则直接引用;没有命中则交给对话模型;模型也失败时给出中性追问。 func (e *CoordinatorEngine) handleQueryMaterials(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext, rawInput string) (*CoordinationResult, error) { projectName := "当前项目" if userCtx != nil && userCtx.Project.Name != "" { projectName = userCtx.Project.Name } searchQuery := strings.TrimSpace(rawInput) if projectName != "当前项目" && !strings.Contains(searchQuery, projectName) { searchQuery = projectName + " " + searchQuery } var hits []kb.HitItem if e != nil && e.kbClient != nil && searchQuery != "" { kbHits, err := e.kbClient.Search(ctx, searchQuery, kb.SearchOptions{ Domains: []kb.KnowledgeDomain{kb.DomainProjectAsset, kb.DomainRegulatory}, Limit: 5, }) if err == nil && len(kbHits) > 0 { hits = kbHits } } if len(hits) > 0 { return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: formatMaterialHits(projectName, rawInput, hits), AdvisorBrief: userCtx.LatestAdvisorBrief, UpdatedContext: userCtx, ReplySource: ReplySourceKB, }, nil } reply := neutralUnmatchedReply(rawInput, projectName) source := ReplySourceTemplate fallbackReason := "" if llmReply, llmSource, llmReason := e.generateDialogueReply(ctx, rawInput, userCtx); llmReply != "" { reply = llmReply source = llmSource } else { fallbackReason = llmReason } return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: reply, AdvisorBrief: userCtx.LatestAdvisorBrief, UpdatedContext: userCtx, ReplySource: source, ReplyFallbackReason: fallbackReason, }, nil } func formatMaterialHits(projectName, rawInput string, hits []kb.HitItem) string { var sb strings.Builder sb.WriteString(fmt.Sprintf("关于「%s」里您要的「%s」,检索到这些材料:\n\n", projectName, strings.TrimSpace(rawInput))) for i, h := range hits { cite := h.CiteKey if cite == "" && h.DocCode != "" { cite = fmt.Sprintf("〔%s〕", h.DocCode) } title := h.Title if title == "" { title = h.DocCode } if title == "" { title = "未命名材料" } 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以上内容来自已索引资料。若还要某一份的操作步骤或和当前项目的差异,直接告诉我。") return sb.String() } // 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 e.applyNarrative(ctx, userCtx, "handleQuerySimilarCases", "", "", &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 e.applyNarrative(ctx, userCtx, "handleQueryUserRights", "", "", &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: sb.String(), UpdatedContext: userCtx, }), nil } // parseDecimalID accepts only a non-negative decimal integer.