1540 lines
57 KiB
Go
1540 lines
57 KiB
Go
package coordinator
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"regexp"
|
||
"strings"
|
||
"time"
|
||
|
||
"chat-agent-system/internal/agent"
|
||
"chat-agent-system/internal/bizdev"
|
||
"chat-agent-system/internal/ctxmgr"
|
||
"chat-agent-system/internal/docgov"
|
||
"chat-agent-system/internal/intent"
|
||
"chat-agent-system/internal/kb"
|
||
"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"`
|
||
ReplySource string `json:"reply_source,omitempty"` // pi-agent | template
|
||
}
|
||
|
||
// CoordinatorEngine L2 协调引擎
|
||
type CoordinatorEngine struct {
|
||
bizdevSvc bizdev.BizdevService
|
||
assembler *ContextAssembler
|
||
dialogueBackend agent.Backend
|
||
dataDir string
|
||
}
|
||
|
||
func NewCoordinatorEngine(bizdevSvc bizdev.BizdevService) *CoordinatorEngine {
|
||
return &CoordinatorEngine{
|
||
bizdevSvc: bizdevSvc,
|
||
assembler: NewContextAssembler(),
|
||
}
|
||
}
|
||
|
||
// NewCoordinatorEngineWithDialogue 注入对话 LLM 后端(Phase A:CASUAL / AMBIGUOUS)
|
||
func NewCoordinatorEngineWithDialogue(bizdevSvc bizdev.BizdevService, dialogueBackend agent.Backend, dataDir string) *CoordinatorEngine {
|
||
return &CoordinatorEngine{
|
||
bizdevSvc: bizdevSvc,
|
||
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 {
|
||
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 (模糊/分忧引导),触发参谋主动分析与建议(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
|
||
}
|
||
|
||
// 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(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)
|
||
|
||
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(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext, rawInput string) (*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("当前各项工作进展平稳。您可以把零散想法随时告诉我,我来帮您沉淀;时机成熟时,再一起推进下一阶段也完全可以。")
|
||
}
|
||
|
||
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.PendingExpectation = 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.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
|
||
}
|
||
|
||
// 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.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
|
||
}
|
||
|
||
var projectID, userID int64
|
||
if userCtx.Project.ID != "" {
|
||
fmt.Sscanf(userCtx.Project.ID, "%d", &projectID)
|
||
}
|
||
if userCtx.User.ID != "" {
|
||
fmt.Sscanf(userCtx.User.ID, "%d", &userID)
|
||
}
|
||
|
||
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,
|
||
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")
|
||
|
||
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
|
||
}
|
||
|
||
// 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.PendingExpectation = nextExp
|
||
|
||
if e.bizdevSvc == 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 != "" {
|
||
fmt.Sscanf(userCtx.Project.ID, "%d", &projectID)
|
||
}
|
||
if userCtx.User.ID != "" {
|
||
fmt.Sscanf(userCtx.User.ID, "%d", &userID)
|
||
}
|
||
|
||
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 := bizdev.MaterialInput{
|
||
Filename: "task_brief.md",
|
||
Content: []byte(fmt.Sprintf("# 任务简报(用户原始诉求,权威等级:primary_evidence)\n\n%s\n", brief)),
|
||
}
|
||
|
||
taskRes, err := e.bizdevSvc.ExecuteTask(ctx, bizdev.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: []bizdev.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
|
||
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))
|
||
}
|
||
}
|
||
|
||
return &CoordinationResult{
|
||
Status: StatusAccepted,
|
||
Intent: cand.Intent,
|
||
Branch: cand.Branch,
|
||
NaturalReply: sb.String(),
|
||
AdvisorBrief: taskRes.AdvisorBrief,
|
||
ArtifactPath: outPath,
|
||
UpdatedContext: userCtx,
|
||
NextExpectation: nextExp,
|
||
}, 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.PendingExpectation = 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.PendingExpectation = 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.InteractiveCard = resolved.Card
|
||
userCtx.PendingExpectation = 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.InteractiveCard = 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.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) {
|
||
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.bizdevSvc != nil && e.bizdevSvc.KB() != 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.bizdevSvc.KB().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
|
||
}
|