package coordinator import ( "context" "encoding/json" "errors" "fmt" "os" "path/filepath" "strconv" "strings" "sync" "time" "chat-agent-system/internal/ctxmgr" "chat-agent-system/internal/intent" "chat-agent-system/internal/kb" "chat-agent-system/internal/maturity" "chat-agent-system/internal/squad" "chat-agent-system/internal/workspace" ) // UserInfo 用户在系统与项目内的身份 type UserInfo struct { ID string `json:"id"` Name string `json:"name"` Role string `json:"role"` // 系统角色 (如 engineer, pm, director) ProjectRole string `json:"project_role"` // 项目内角色 (如 编制人 composer, 审核人 reviewer, 批准人 approver) } // ProjectInfo 项目与工作流当前基准 type ProjectInfo struct { ID string `json:"id"` Name string `json:"name"` CustomerName string `json:"customer_name"` DeviceType string `json:"device_type"` IntendedUse string `json:"intended_use"` TargetMarket string `json:"target_market"` CreatorUserID int64 `json:"creator_user_id,omitempty"` ActiveWorkflowKey string `json:"active_workflow_key,omitempty"` WorkflowSelectionVersion int `json:"workflow_selection_version,omitempty"` CurrentWorkflow string `json:"current_workflow"` CurrentStage maturity.MaturityStage `json:"current_stage"` StageRules []string `json:"stage_rules,omitempty"` } // UserDocsInfo 用户相关文档清单与认领状态 type UserDocsInfo struct { Composed []string `json:"composed"` // 负责编制的文档 Reviewed []string `json:"reviewed"` // 负责审核的文档 Approved []string `json:"approved"` // 负责批准的文档 Claimed []string `json:"claimed"` // 已认领/已打开查阅的文档 } // PendingItems 待用户处理或决策的待办项 type PendingItems struct { ConfirmModifications []string `json:"confirm_modifications,omitempty"` PendingReviews []string `json:"pending_reviews,omitempty"` PendingApprovals []string `json:"pending_approvals,omitempty"` MyDraftsInProgress []string `json:"my_drafts_in_progress,omitempty"` UnreadSupplements []string `json:"unread_supplements,omitempty"` } // ReadableDocument 是已通过项目与阶段权限过滤后的只读文档快照。 // Markdown 只用于当轮总结,不写入上下文快照或对话元数据。 type ReadableDocument struct { DocID string Title string TaskKey string LifecycleState string Version int Markdown string IsTemplateOnly bool // WorkflowKey 是文档归属工作流。CrossWorkflow 为真时正文来自非激活流,仅只读。 WorkflowKey string ActiveWorkflow string CrossWorkflow bool } // DocumentSummaryReader 由 HTTP 服务端按当前登录用户和项目绑定。 // 协调层不得直接枚举文档,以免绕过阶段可见性规则。 type DocumentSummaryReader func(ctx context.Context, target string) (*ReadableDocument, error) // ErrWrongWorkflow 表示目标文档存在但不属于当前激活工作流。 // 服务端文档读取器按激活流收窄匹配池后,用该错误携带「请先切换工作流」引导话术, // 协调层识别后原样回复,不执行任何读/写动作。 type ErrWrongWorkflow struct { Message string } func (e *ErrWrongWorkflow) Error() string { return e.Message } // WrongWorkflowGuidance 若错误链上是跨工作流拦截,返回引导话术;否则空串。 func WrongWorkflowGuidance(err error) string { var target *ErrWrongWorkflow if errors.As(err, &target) && target != nil && strings.TrimSpace(target.Message) != "" { return target.Message } return "" } // ErrDocViewDenied 表示跨工作流文档存在,但当前用户无权查看。 type ErrDocViewDenied struct { Message string } func (e *ErrDocViewDenied) Error() string { if e == nil { return "document view denied" } return e.Message } // DocViewDeniedGuidance 若错误链是跨流查看拒绝,返回话术。 func DocViewDeniedGuidance(err error) string { var target *ErrDocViewDenied if errors.As(err, &target) && target != nil && strings.TrimSpace(target.Message) != "" { return target.Message } return "" } // ErrNoUploadAsset 表示项目内尚无可总结的上传附件。 type ErrNoUploadAsset struct { Message string } func (e *ErrNoUploadAsset) Error() string { if e == nil || strings.TrimSpace(e.Message) == "" { return "no upload asset in project" } return e.Message } // NoUploadAssetGuidance 返回无上传附件时的话术。 func NoUploadAssetGuidance(err error) string { var target *ErrNoUploadAsset if errors.As(err, &target) && target != nil { if msg := strings.TrimSpace(target.Message); msg != "" { return msg } return "当前项目还没有上传过附件。请先在对话中上传文件后再试总结。" } return "" } // ErrUploadParseState 表示最近上传附件仍在解析中或解析失败,尚无可总结正文。 type ErrUploadParseState struct { State string Filename string Message string } func (e *ErrUploadParseState) Error() string { if e == nil { return "upload parse state unavailable" } if msg := strings.TrimSpace(e.Message); msg != "" { return msg } name := strings.TrimSpace(e.Filename) if name == "" { name = "该附件" } switch strings.TrimSpace(e.State) { case "pending", "running": return fmt.Sprintf("《%s》仍在解析中,请稍后再试总结。", name) case "failed": return fmt.Sprintf("《%s》解析失败。可在附件卡片点击重新解析后再试总结。", name) case "slow": return fmt.Sprintf("《%s》解析耗时较长(仍在后台进行),请稍后再让我总结。", name) default: return fmt.Sprintf("《%s》暂时无法读取正文,请稍后再试。", name) } } // UploadParseStateGuidance 返回上传解析状态提示话术。 func UploadParseStateGuidance(err error) string { var target *ErrUploadParseState if errors.As(err, &target) && target != nil { return target.Error() } return "" } // UserProjectContext 用户项目级全量统一上下文 (对话系统与执行系统共同消费) type UserProjectContext struct { mu sync.Mutex Version int64 `json:"version"` SnapshotID string `json:"snapshot_id"` AssembledAt time.Time `json:"assembled_at"` User UserInfo `json:"user"` Project ProjectInfo `json:"project"` UserDocs UserDocsInfo `json:"user_docs"` Pending PendingItems `json:"pending"` BlackboardFacts map[string]string `json:"blackboard_facts"` LatestAdvisorBrief *squad.AdvisorBriefing `json:"latest_advisor_brief,omitempty"` PendingExpectation *intent.PendingExpectation `json:"pending_expectation,omitempty"` InteractiveCard *intent.InteractiveCardPayload `json:"interactive_card,omitempty"` AvailableActions []string `json:"available_actions"` RecentEvents []string `json:"recent_events,omitempty"` WorkDir string `json:"work_dir,omitempty"` // RecentDialogue 近轮对话(供 Phase A 对话 LLM 使用,不序列化到快照) RecentDialogue []DialogueTurn `json:"-"` // DialogueSummary 早期对话滚动压缩摘要(由 ctxmgr 维护生成,不序列化到快照) DialogueSummary string `json:"-"` // ProjectDialogueSnippet 项目成员协同近况(跨会话共享对话摘要,权威度低于黑板) ProjectDialogueSnippet string `json:"-"` // TaskState 会话级任务状态(DST,记录 active_task, topic_anchor, collected_slots 等) TaskState *ctxmgr.ConversationTaskState `json:"task_state,omitempty"` // PersonalizedGuidance 用户工作习惯/偏好注入语(由 ctxmgr 编译,供对话 LLM 使用) PersonalizedGuidance string `json:"-"` // RecalledMemories 按需语义召回的相关历史片段(TopK ≤ 3) RecalledMemories []ctxmgr.RecalledMemoryItem `json:"-"` // RecalledMemorySnippet 格式化后的相关历史片段 Prompt 文本 RecalledMemorySnippet string `json:"-"` // RewrittenQuery 规则改写补全后的完整独立问句(用于检索与 Prompt 双呈现,不进意图识别) RewrittenQuery string `json:"-"` // RewriteRules 本轮命中的改写规则列表(如 ellipsis_completion, pronoun_resolution) RewriteRules []string `json:"-"` // RegulationHits 知识库法规与强标检索命中项(带 CiteKey 引用键,供对话层精准引用) RegulationHits []kb.HitItem `json:"-"` // UpgradeOffered 路由升级建议冷却标记(每会话至多主动建议一次,防打扰——对齐"参谋不强推"人设) UpgradeOffered bool `json:"-"` // DocumentSummaryReader 仅用于服务端鉴权后的只读正文访问,不序列化。 DocumentSummaryReader DocumentSummaryReader `json:"-"` // RealtimeSnippet 本轮实时检索(天气/新闻等)成功结果,供 Prompt 注入;不序列化。 RealtimeSnippet string `json:"-"` // RealtimeUnavailable 本轮命中实时查询但检索失败/空结果,强制拒答约束;不序列化。 RealtimeUnavailable bool `json:"-"` } // ContextAssembler 上下文组装服务 type ContextAssembler struct{} func NewContextAssembler() *ContextAssembler { return &ContextAssembler{} } // ComputeAvailableActions 根据当前成熟度阶段与角色计算允许执行的操作集合 func ComputeAvailableActions(stage maturity.MaturityStage, pRole string) []string { actions := []string{"QUERY_PROGRESS", "SUPPLEMENT_INFO", "VIEW_DOC"} switch stage { case maturity.StageV01_Triage: actions = append(actions, "TRIGGER_TASK:intake", "ADVANCE_STAGE:V0.5_feasibility") case maturity.StageV05_Feasibility: actions = append(actions, "TRIGGER_TASK:research", "TRIGGER_TASK:feasibility_reg", "TRIGGER_TASK:prelim_design", "ADVANCE_STAGE:V1.0_quoting", ) case maturity.StageV10_Quoting: actions = append(actions, "TRIGGER_TASK:cdo_quote", "LOCK_COMMERCIAL") default: actions = append(actions, "ADVANCE_STAGE:V0.1_triage") } return actions } // ComputeAvailableActionsWithWorkflow 结合激活工作流、成熟度阶段与角色权限计算可用操作集合 func ComputeAvailableActionsWithWorkflow(stage maturity.MaturityStage, pRole string, activeWorkflow string, isCreator bool) []string { actions := ComputeAvailableActions(stage, pRole) actions = appendUniqueCapability(actions, "QUERY_WORKFLOW") if isCreator { actions = appendUniqueCapability(actions, "SWITCH_WORKFLOW") } switch activeWorkflow { case "product_manager": actions = appendUniqueCapability(actions, "TRIGGER_TASK:system_reqs", "TRIGGER_WORKFLOW:product_manager") case "test_manager": actions = appendUniqueCapability(actions, "TRIGGER_TASK:test_manager", "TRIGGER_WORKFLOW:test_manager") default: if activeWorkflow != "" && activeWorkflow != "main" { actions = appendUniqueCapability(actions, "TRIGGER_WORKFLOW:"+activeWorkflow) } } return actions } func appendUniqueCapability(actions []string, keys ...string) []string { seen := make(map[string]struct{}, len(actions)) for _, a := range actions { seen[a] = struct{}{} } for _, k := range keys { if _, ok := seen[k]; ok { continue } seen[k] = struct{}{} actions = append(actions, k) } return actions } // ToSnapshot 转换为 L1 意图识别器所需的轻量上下文快照 func (ctx *UserProjectContext) ToSnapshot() *intent.ContextSnapshot { if ctx == nil { return nil } snap := &intent.ContextSnapshot{ SnapshotID: ctx.SnapshotID, ProjectName: ctx.Project.Name, CurrentStage: string(ctx.Project.CurrentStage), ActiveWorkflowKey: ctx.Project.ActiveWorkflowKey, AvailableActions: ctx.AvailableActions, PendingExpectation: ctx.PendingExpectation, } if ctx.User.ProjectRole != "" { snap.UserDocRole = ctx.User.ProjectRole } else if ctx.User.Role != "" { snap.UserDocRole = ctx.User.Role } if ctx.InteractiveCard != nil && ctx.InteractiveCard.CurrentStatus == "active" { snap.ActiveDocID = ctx.InteractiveCard.TargetDocID if ctx.InteractiveCard.TargetDocID != "" { snap.DocLifecycleState = "reviewing" } } return snap } // BuildNextExpectation 根据当前成熟度阶段与项目名构造参谋下一步建议选项草稿 func BuildNextExpectation(stage maturity.MaturityStage, projectName string) *intent.PendingExpectation { if projectName == "" { projectName = "本项目" } var options []intent.ExpectationOption var snippet string switch stage { case maturity.StageV01_Triage: snippet = fmt.Sprintf("项目「%s」当前处于初始摸底期,基准产品画像已梳理完成。", projectName) options = []intent.ExpectationOption{ { OptionID: "1", Title: "进入 V0.5 方案成型与法规对齐", Description: "自动比选技术路线,并梳理分类路径与型检标准", Intent: intent.IntentAdvanceStage, Slots: map[string]string{ intent.SlotTargetStage: string(maturity.StageV05_Feasibility), }, }, { OptionID: "2", Title: "补充或微调事实信息", Description: "随时告知我客户的最新的预算、目标市场或使用场景", Intent: intent.IntentSupplementInfo, }, { OptionID: "3", Title: "查阅当前立项成熟度与对外沟通建议", Description: "获取进展研判与对外商务沟通话术", Intent: intent.IntentQueryProgress, Slots: map[string]string{ intent.SlotQueryTopic: "all", }, }, } case maturity.StageV05_Feasibility: snippet = fmt.Sprintf("项目「%s」已完成方案成型与法规对齐,具备进入商务核算的条件。", projectName) options = []intent.ExpectationOption{ { OptionID: "1", Title: "推进到 V1.0 商业锁定阶段", Description: "生成精细化 CDO 工时与成本核算表,锁定立项商务方案", Intent: intent.IntentAdvanceStage, Slots: map[string]string{ intent.SlotTargetStage: string(maturity.StageV10_Quoting), }, }, { OptionID: "2", Title: "微调设计方案或技术参数", Description: "进一步指定元器件品牌、特殊工艺或交付节点", Intent: intent.IntentSupplementInfo, }, { OptionID: "3", Title: "审阅初步系统架构图与法规分类论证说明", Description: "查看技术路线比选报告与检验标准分析", Intent: intent.IntentQueryProgress, Slots: map[string]string{ intent.SlotQueryTopic: "progress", }, }, } case maturity.StageV10_Quoting: snippet = fmt.Sprintf("项目「%s」已完成商务锁定,报价核算表已就绪。", projectName) options = []intent.ExpectationOption{ { OptionID: "1", Title: "重新核算或调整报价单参数", Description: "调整费率、工时模型或元器件采购预算", Intent: intent.IntentTriggerTask, Slots: map[string]string{ intent.SlotTargetTask: "cdo_quote", }, }, { OptionID: "2", Title: "生成面向客户负责人的正式沟通函件", Description: "草拟发给客户的商务方案总揽与下一步推进排期", Intent: intent.IntentQueryProgress, Slots: map[string]string{ intent.SlotQueryTopic: "talk_script", }, }, } default: snippet = fmt.Sprintf("项目「%s」当前处于初始准备阶段。", projectName) options = []intent.ExpectationOption{ { OptionID: "1", Title: "启动 V0.1 闪电摸底", Description: "快速沉淀第一版立项画像并为您准备沟通话术", Intent: intent.IntentAdvanceStage, Slots: map[string]string{ intent.SlotTargetStage: string(maturity.StageV01_Triage), }, }, } } var defaultOpt *intent.ExpectationOption if len(options) > 0 { defaultOpt = &options[0] } return &intent.PendingExpectation{ ContextSnippet: snippet, DefaultOption: defaultOpt, Options: options, AllowAffirmative: true, CreatedAt: time.Now(), } } // AssembleFromWorkspace 基于现有工作区快速组装 UserProjectContext func (a *ContextAssembler) AssembleFromWorkspace(workDir string, user UserInfo) (*UserProjectContext, error) { snapID := fmt.Sprintf("ctx_%d", time.Now().UnixNano()) ctx := &UserProjectContext{ SnapshotID: snapID, AssembledAt: time.Now(), User: user, WorkDir: workDir, BlackboardFacts: make(map[string]string), AvailableActions: []string{"QUERY_PROGRESS", "SUPPLEMENT_INFO", "VIEW_DOC"}, } // 1. 尝试读取 workspace.json wsMetaPath := filepath.Join(workDir, "workspace.json") if data, err := os.ReadFile(wsMetaPath); err == nil { var wsMeta squad.WorkspaceMetadata if json.Unmarshal(data, &wsMeta) == nil { ctx.Project.Name = wsMeta.ProjectName ctx.Project.CustomerName = wsMeta.CustomerName ctx.Project.DeviceType = wsMeta.DeviceType ctx.Project.IntendedUse = wsMeta.IntendedUse ctx.Project.TargetMarket = wsMeta.TargetMarket ctx.Project.CurrentWorkflow = wsMeta.TaskKey } } else { // 备选:从目录名推断 baseName := filepath.Base(workDir) if strings.HasPrefix(baseName, "intake_") { ctx.Project.CurrentWorkflow = "intake" } } // 2. 尝试读取 blackboard.json bbPath := filepath.Join(workDir, "blackboard.json") if data, err := os.ReadFile(bbPath); err == nil { var bb squad.BlackboardState if json.Unmarshal(data, &bb) == nil { for k, v := range bb.Fields { ctx.BlackboardFacts[k] = v } for _, g := range bb.Gaps { if g.Blocking && !g.Resolved { ctx.Pending.ConfirmModifications = append(ctx.Pending.ConfirmModifications, g.Item) } } } } // 3. 尝试提取阶段与文档 if ctx.Project.ActiveWorkflowKey != "" { ctx.Project.CurrentWorkflow = ctx.Project.ActiveWorkflowKey } if strings.HasPrefix(ctx.Project.CurrentWorkflow, "V0.1") || ctx.Project.CurrentWorkflow == "intake" { ctx.Project.CurrentStage = maturity.StageV01_Triage } else if strings.HasPrefix(ctx.Project.CurrentWorkflow, "V0.5") { ctx.Project.CurrentStage = maturity.StageV05_Feasibility } else if strings.HasPrefix(ctx.Project.CurrentWorkflow, "V1.0") { ctx.Project.CurrentStage = maturity.StageV10_Quoting } if ctx.Project.CurrentStage == "" { ctx.Project.CurrentStage = maturity.StageV01_Triage } isCreator := false if user.ID != "" && ctx.Project.CreatorUserID > 0 { uid, _ := strconv.ParseInt(user.ID, 10, 64) isCreator = (uid == ctx.Project.CreatorUserID) } ctx.AvailableActions = ComputeAvailableActionsWithWorkflow(ctx.Project.CurrentStage, user.ProjectRole, ctx.Project.ActiveWorkflowKey, isCreator) // 4. 优先尝试直接读取持久化的 advisor_brief.json briefPath := filepath.Join(workDir, "advisor_brief.json") if data, err := os.ReadFile(briefPath); err == nil { var brief squad.AdvisorBriefing if json.Unmarshal(data, &brief) == nil { ctx.LatestAdvisorBrief = &brief } } // 5. 若无 advisor_brief.json 但有交付正文,尝试从文本抽取 if ctx.LatestAdvisorBrief == nil { outPath := filepath.Join(workDir, workspace.DeliverableKey) if outBytes, err := os.ReadFile(outPath); err == nil && len(outBytes) > 0 { ctx.LatestAdvisorBrief = squad.ExtractAdvisorBriefing(ctx.Project.CurrentWorkflow, string(outBytes), ctx.BlackboardFacts) ctx.UserDocs.Composed = append(ctx.UserDocs.Composed, ctx.Project.CurrentWorkflow) } } return ctx, nil }