package coordinator import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "time" "chat-agent-system/internal/bizdev" "chat-agent-system/internal/intent" "chat-agent-system/internal/squad" ) // 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"` CurrentWorkflow string `json:"current_workflow"` CurrentStage bizdev.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"` } // UserProjectContext 用户项目级全量统一上下文 (对话系统与执行系统共同消费) type UserProjectContext struct { 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"` } // ContextAssembler 上下文组装服务 type ContextAssembler struct{} func NewContextAssembler() *ContextAssembler { return &ContextAssembler{} } // ComputeAvailableActions 根据当前成熟度阶段与角色计算允许执行的操作集合 func ComputeAvailableActions(stage bizdev.MaturityStage, pRole string) []string { actions := []string{"QUERY_PROGRESS", "SUPPLEMENT_INFO", "VIEW_DOC"} switch stage { case bizdev.StageV01_Triage: actions = append(actions, "TRIGGER_TASK:intake", "ADVANCE_STAGE:V0.5_feasibility") case bizdev.StageV05_Feasibility: actions = append(actions, "TRIGGER_TASK:research", "TRIGGER_TASK:feasibility_reg", "TRIGGER_TASK:prelim_design", "ADVANCE_STAGE:V1.0_quoting", ) case bizdev.StageV10_Quoting: actions = append(actions, "TRIGGER_TASK:cdo_quote", "LOCK_COMMERCIAL") default: actions = append(actions, "ADVANCE_STAGE:V0.1_triage") } 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), 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 bizdev.MaturityStage, projectName string) *intent.PendingExpectation { if projectName == "" { projectName = "本项目" } var options []intent.ExpectationOption var snippet string switch stage { case bizdev.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(bizdev.StageV05_Feasibility), }, }, { OptionID: "2", Title: "补充或微调事实信息", Description: "随时告知我客户的最新的预算、目标市场或使用场景", Intent: intent.IntentSupplementInfo, }, { OptionID: "3", Title: "查阅当前立项成熟度与对外沟通建议", Description: "获取参谋深度研判与对外商务沟通话术", Intent: intent.IntentQueryProgress, Slots: map[string]string{ intent.SlotQueryTopic: "all", }, }, } case bizdev.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(bizdev.StageV10_Quoting), }, }, { OptionID: "2", Title: "微调设计方案或技术参数", Description: "进一步指定元器件品牌、特殊工艺或交付节点", Intent: intent.IntentSupplementInfo, }, { OptionID: "3", Title: "审阅初步系统架构图与法规分类论证说明", Description: "查看技术路线比选报告与检验标准分析", Intent: intent.IntentQueryProgress, Slots: map[string]string{ intent.SlotQueryTopic: "progress", }, }, } case bizdev.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(bizdev.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 strings.HasPrefix(ctx.Project.CurrentWorkflow, "V0.1") || ctx.Project.CurrentWorkflow == "intake" { ctx.Project.CurrentStage = bizdev.StageV01_Triage } else if strings.HasPrefix(ctx.Project.CurrentWorkflow, "V0.5") { ctx.Project.CurrentStage = bizdev.StageV05_Feasibility } else if strings.HasPrefix(ctx.Project.CurrentWorkflow, "V1.0") { ctx.Project.CurrentStage = bizdev.StageV10_Quoting } if ctx.Project.CurrentStage == "" { ctx.Project.CurrentStage = bizdev.StageV01_Triage } ctx.AvailableActions = ComputeAvailableActions(ctx.Project.CurrentStage, user.ProjectRole) // 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 但有 output.md,尝试从文本抽取 if ctx.LatestAdvisorBrief == nil { outPath := filepath.Join(workDir, "output.md") 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 }