package squad import ( "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "time" "chat-agent-system/internal/agent" "chat-agent-system/internal/config" "chat-agent-system/internal/quote" "chat-agent-system/internal/stream" "chat-agent-system/internal/workflowdef" ) // TaskMeta 任务元信息 type TaskMeta struct { Key string `json:"key"` Title string `json:"title"` ArtifactTitle string `json:"artifact_title"` Description string `json:"description"` } // Attachment 附加产物文件 (如 Excel) type Attachment struct { Filename string `json:"filename"` ContentType string `json:"content_type"` Data []byte `json:"data"` } // SquadRunRequest 运行请求 type SquadRunRequest struct { TaskKey string `json:"task_key"` Title string `json:"title,omitempty"` // 自由任务/非标任务的人类可读标题(缺省回落 TaskArtifactTitle) ProjectName string `json:"project_name"` CustomerName string `json:"customer_name"` DeviceType string `json:"device_type"` IntendedUse string `json:"intended_use"` TargetMarket string `json:"target_market"` Fields map[string]string `json:"fields"` Materials []MaterialItem `json:"materials"` Backend string `json:"backend"` Timeout time.Duration `json:"timeout"` WorkDir string `json:"work_dir"` CustomSteps []StepSpec `json:"custom_steps"` AutoRepair bool `json:"auto_repair"` KnowledgeHits []KnowledgeHit `json:"knowledge_hits"` PriorDocs []PriorDocItem `json:"prior_docs"` UserContext string `json:"user_context,omitempty"` MemoryBankFiles map[string]string `json:"memory_bank_files,omitempty"` DialoguePackage *DialoguePackage `json:"-"` SessionID string `json:"session_id,omitempty"` UserID int64 `json:"user_id,omitempty"` ProjectID int64 `json:"project_id,omitempty"` RunID string `json:"run_id,omitempty"` StepNotify func(StepEvent) `json:"-"` } // SquadRunResult 运行结果 type SquadRunResult struct { TaskKey string `json:"task_key"` ArtifactTitle string `json:"artifact_title"` MarkdownBody string `json:"markdown_body"` ExtractedData map[string]string `json:"extracted_data"` Attachments []Attachment `json:"attachments"` WorkDir string `json:"work_dir"` BackendUsed string `json:"backend_used"` Duration time.Duration `json:"duration"` Success bool `json:"success"` ErrorReason string `json:"error_reason,omitempty"` QualityReport *QualityReport `json:"quality_report,omitempty"` Manifest *Manifest `json:"manifest,omitempty"` AdvisorBrief *AdvisorBriefing `json:"advisor_brief,omitempty"` } // WorkspaceState 用于观察工作区当前完整状态 type WorkspaceState struct { WorkDir string `json:"work_dir"` Metadata WorkspaceMetadata `json:"metadata"` Squad SquadConfig `json:"squad"` Workflow WorkflowPlan `json:"workflow"` Blackboard BlackboardState `json:"blackboard"` DeliverableContent string `json:"deliverable_content"` Manifest *Manifest `json:"manifest,omitempty"` } // ModifyWorkflowRequest 动态调整现有工作区任务 type ModifyWorkflowRequest struct { WorkDir string `json:"work_dir"` AddSteps []StepSpec `json:"add_steps,omitempty"` RemoveStepIDs []string `json:"remove_step_ids,omitempty"` UpdateSteps []StepSpec `json:"update_steps,omitempty"` Backend string `json:"backend,omitempty"` Timeout time.Duration `json:"timeout,omitempty"` AutoRepair bool `json:"auto_repair,omitempty"` StepNotify func(StepEvent) `json:"-"` } // SquadService 小队领域服务核心接口 type SquadService interface { ListTasks() []TaskMeta PlanWorkflow(ctx context.Context, req SquadRunRequest) (*WorkflowPlan, error) ExecuteWorkflow(ctx context.Context, wsDir string, backend string, opts ExecuteOptions) (*SquadRunResult, error) ExecuteTask(ctx context.Context, req SquadRunRequest) (*SquadRunResult, error) InspectWorkspace(ctx context.Context, workDir string) (*WorkspaceState, error) ResumeOrModifyWorkspace(ctx context.Context, req ModifyWorkflowRequest) (*SquadRunResult, error) } // Service SquadService 实现 type Service struct { cfg *config.Config engine *WorkspaceEngine driver *WorkflowDriver gate *QualityGate } func NewService(cfg *config.Config) *Service { engine := NewWorkspaceEngine() return &Service{ cfg: cfg, engine: engine, driver: NewWorkflowDriver(engine), gate: NewQualityGate(), } } var baseSupportedTasks = []TaskMeta{ {Key: "bd_intake", Title: "用户初始需求收集", ArtifactTitle: "用户初始需求收集表", Description: "梳理客户原始需求、提炼事实清单与缺口"}, {Key: "bd_research", Title: "调研", ArtifactTitle: "调研报告", Description: "市场竞品同维度对比、法规要点与技术方案初筛"}, {Key: "bd_feasibility_dev", Title: "设计开发可行性评估", ArtifactTitle: "设计开发可行性评估报告", Description: "工程化差距、技术路线与系统架构可行性推导"}, {Key: "bd_feasibility_reg", Title: "注册路径可行性评估", ArtifactTitle: "注册路径可行性评估报告", Description: "分类建议、临床评价策略与主要标准检测规划"}, {Key: "bd_user_requirements", Title: "用户需求表", ArtifactTitle: "用户需求表", Description: "用户角色画像与可追溯需求条目清单"}, {Key: "pm_system_reqs", Title: "系统需求规格书", ArtifactTitle: "系统需求规格书", Description: "智能产品经理深度提炼,输出系统需求规格书及工程追溯"}, {Key: "tm_system_verification", Title: "系统验证方案及计划", ArtifactTitle: "系统验证方案及计划", Description: "智能测试经理基于系统需求生成系统测试方案与测试用例"}, {Key: "bd_prelim_design", Title: "系统初步设计方案", ArtifactTitle: "系统初步设计方案", Description: "子系统架构边界、数据接口与Mermaid上下文流向"}, {Key: "bd_cdo_quote", Title: "CDO报价单", ArtifactTitle: "CDO报价单(含定价核算 Excel)", Description: "基于公司定价模型的工时拆解、成本核算与法定说明"}, } // SupportedTasks 标准小队任务白名单(商务九键 + rdev 六十键)。 var SupportedTasks []TaskMeta func init() { SupportedTasks = append(append([]TaskMeta{}, baseSupportedTasks...), rdevSupportedTasks()...) } func (s *Service) ListTasks() []TaskMeta { return SupportedTasks } func taskKnown(taskKey string) bool { canon := workflowdef.CanonicalTaskKey(taskKey) for _, t := range SupportedTasks { if t.Key == canon { return true } } return false } func TaskArtifactTitle(taskKey string) string { canon := workflowdef.CanonicalTaskKey(taskKey) for _, t := range SupportedTasks { if t.Key == canon { return t.ArtifactTitle } } return taskKey } // taskDisplayTitle 返回任务展示标题(自由任务 Title 优先,回落标准任务产物名) func taskDisplayTitle(req SquadRunRequest) string { if req.Title != "" { return req.Title } return TaskArtifactTitle(req.TaskKey) } // PlanWorkflow 动态规划生成任务工作流 func (s *Service) PlanWorkflow(ctx context.Context, req SquadRunRequest) (*WorkflowPlan, error) { planner := NewAdaptivePlanner(nil) res, err := planner.Plan(PlannerRequest{ TaskKey: req.TaskKey, Title: taskDisplayTitle(req), ProjectName: req.ProjectName, CustomerName: req.CustomerName, DeviceType: req.DeviceType, IntendedUse: req.IntendedUse, TargetMarket: req.TargetMarket, Materials: req.Materials, KnowledgeHits: req.KnowledgeHits, Fields: req.Fields, CustomSteps: req.CustomSteps, }) if err != nil { return nil, err } return &res.Workflow, nil } // ExecuteTask 便捷门面:一步构建工作区、调度动态工作流、进行CDO核算与终审质量门禁 func (s *Service) ExecuteTask(ctx context.Context, req SquadRunRequest) (*SquadRunResult, error) { start := time.Now() workDir := req.WorkDir if workDir == "" { ts := time.Now().UnixNano() workDir = filepath.Join(s.cfg.DataDir, "workspaces", "bizdev", fmt.Sprintf("%s_%d", req.TaskKey, ts)) } backendName := req.Backend if backendName == "" { backendName = s.cfg.AgentBackend } // 1. 初始化工作区目录体系 wsReq := WorkspacePrepareRequest{ WorkDir: workDir, SessionID: FlexSessionID(req.SessionID), ProjectID: req.ProjectID, TaskKey: req.TaskKey, StageTitle: taskDisplayTitle(req), ProjectName: req.ProjectName, CustomerName: req.CustomerName, DeviceType: req.DeviceType, IntendedUse: req.IntendedUse, TargetMarket: req.TargetMarket, Backend: backendName, Materials: req.Materials, KnowledgeHits: req.KnowledgeHits, PriorDocs: req.PriorDocs, WorkflowSteps: req.CustomSteps, InitialFields: req.Fields, UserContext: req.UserContext, MemoryBankFiles: req.MemoryBankFiles, DialoguePackage: req.DialoguePackage, } _, err := s.engine.PrepareWorkspace(wsReq) if err != nil { return nil, fmt.Errorf("prepare workspace: %w", err) } // 2. 若为 CDO 报价任务,先执行 Excel 报价核算引擎并回填 var attachments []Attachment if workflowdef.SameTaskKey(req.TaskKey, "cdo_quote") { att, err := s.handleCDOQuotePreparation(workDir, req) if err == nil && att != nil { attachments = append(attachments, *att) } } // 3. 执行动态工作流调度 streamer := stream.FromContext(ctx) if streamer != nil { streamer.EmitPhase("squad_assembly", fmt.Sprintf("正在组建并启动【%s】智能体小队(后端: %s)...", TaskArtifactTitle(req.TaskKey), backendName), "leader", 0, 0, 5) } backend := agent.NewBackend(backendName, s.cfg) timeout := req.Timeout if timeout <= 0 { timeout = s.cfg.AgentTimeout } execOpts := ExecuteOptions{ Timeout: timeout, StepNotify: req.StepNotify, } execRes, err := s.driver.Execute(ctx, workDir, backend, execOpts) if err != nil { if streamer != nil { streamer.EmitPhase("task_failed", fmt.Sprintf("任务执行异常: %v", err), "leader", 0, 0, 0) } return &SquadRunResult{ TaskKey: req.TaskKey, ArtifactTitle: TaskArtifactTitle(req.TaskKey), WorkDir: workDir, BackendUsed: backendName, Duration: time.Since(start), Success: false, ErrorReason: err.Error(), }, err } // 4. 自动化质量门禁与修复 (QualityGate) if streamer != nil { streamer.EmitPhase("quality_gate", "审核主管正在核验交付物规范并执行质量门禁把关...", "leader", 0, 0, 85) } maxRetries := repairBudgetForSquadTask(req) report, err := s.gate.EnsureQualityOrRepair(ctx, workDir, req.TaskKey, s.driver, backend, maxRetries, execOpts) if err != nil { // 记录门禁错误但容错返回当前正文 report = &QualityReport{Passed: false, Violations: []string{err.Error()}} if streamer != nil { streamer.EmitPhase("quality_gate", fmt.Sprintf("质量门禁审查告警: %v", err), "leader", 0, 0, 90) } } else if streamer != nil { if report.Passed { streamer.EmitPhase("quality_gate", fmt.Sprintf("质量门禁审核通过(评分: %.1f / 100)", report.Score), "leader", 0, 0, 95) } else { violations := strings.Join(report.Violations, ";") if violations == "" { violations = "未满足质量门禁要求" } streamer.EmitPhase("quality_gate", fmt.Sprintf("质量门禁未通过(评分: %.1f / 100):%s", report.Score, violations), "leader", 0, 0, 95) } } manifest, _ := s.gate.GenerateManifest(workDir, req.TaskKey, *report) // 5. 读取最终交付正文与黑板抽取数据 finalOutPath := filepath.Join(workDir, "output.md") finalBytes, _ := os.ReadFile(finalOutPath) if backendName == "stub" { finalBytes, _ = os.ReadFile(filepath.Join(workDir, "template.md")) _ = os.WriteFile(finalOutPath, finalBytes, 0o644) // stub 终稿以 template 为准:QualityReport 对齐最终交付物(Success 仍只表示执行完成) if reReport, reErr := s.gate.Validate(workDir, req.TaskKey, string(finalBytes)); reErr == nil { report = reReport manifest, _ = s.gate.GenerateManifest(workDir, req.TaskKey, *report) } } bb, _ := s.driver.LoadBlackboard(workDir) var extractedData map[string]string if bb != nil { extractedData = bb.Fields } advisorBrief := ExtractAdvisorBriefing(req.TaskKey, string(finalBytes), extractedData) if bb != nil && len(bb.Assumptions) > 0 { for _, a := range bb.Assumptions { stmt := fmt.Sprintf("[%s] %s (推导依据: %s)", a.Topic, a.Statement, a.Rationale) advisorBrief.BaselineAssumptions = append(advisorBrief.BaselineAssumptions, stmt) } // 若交付正文中尚未包含显式基准假设附录,自动挂载 if !strings.Contains(string(finalBytes), "基准假设与待确认清单") && !strings.Contains(string(finalBytes), "Baseline Assumptions") { var appendix strings.Builder appendix.WriteString("\n\n---\n\n## 附录:智能体小队基准假设与待确认清单 (Baseline Assumptions & Checklist)\n\n") appendix.WriteString("> 提示:针对项目初期未明确的信息,智能体小队已依据医疗器械行业先验预设基准假设并完成初步推演成文;用户可随时确认或修正:\n\n") for idx, a := range bb.Assumptions { appendix.WriteString(fmt.Sprintf("%d. **[%s]** %s\n - *推导依据*:%s\n", idx+1, a.Topic, a.Statement, a.Rationale)) } if len(bb.Gaps) > 0 { appendix.WriteString("\n### 待补充与确认缺口 (Pending Gaps)\n") for idx, g := range bb.Gaps { appendix.WriteString(fmt.Sprintf("- [%d] %s (阻塞性: %v, 责任方: %s)\n", idx+1, g.Item, g.Blocking, g.Owner)) } } finalBytes = append(finalBytes, []byte(appendix.String())...) _ = os.WriteFile(finalOutPath, finalBytes, 0o644) } } if streamer != nil { score := 0.0 if report != nil { score = report.Score } streamer.EmitDeliverable(req.TaskKey, TaskArtifactTitle(req.TaskKey), finalOutPath, len(strings.Fields(string(finalBytes))), score) if advisorBrief != nil { streamer.EmitPhase("advisor_briefing", "贴身业务参谋正在提炼关键决策摘要与风险雷达...", "coordinator", 0, 0, 98) streamer.EmitSummary(TaskArtifactTitle(req.TaskKey)+"已生成", advisorBrief.MaturityHeadline, advisorBrief.MaturityScore, advisorBrief.MaturityLevel) streamer.EmitBriefing(advisorBrief) } } return &SquadRunResult{ TaskKey: req.TaskKey, ArtifactTitle: TaskArtifactTitle(req.TaskKey), MarkdownBody: string(finalBytes), ExtractedData: extractedData, Attachments: attachments, WorkDir: workDir, BackendUsed: backendName, Duration: time.Since(start), Success: execRes.Success, QualityReport: report, Manifest: manifest, AdvisorBrief: advisorBrief, }, nil } // repairBudgetForSquadTask 仅尊重请求方 AutoRepair,不因 TaskKey 非空而改写 CLI/桩/非受控语义。 // 受控文档任务的修复预算由 bizdev/HTTP 在调用前强制 AutoRepair=true。 func repairBudgetForSquadTask(req SquadRunRequest) int { if req.AutoRepair { return DefaultMaxRepairRounds } return 0 } // ExecuteWorkflow 执行既有工作区工作流 func (s *Service) ExecuteWorkflow(ctx context.Context, wsDir string, backendName string, opts ExecuteOptions) (*SquadRunResult, error) { start := time.Now() if backendName == "" { backendName = s.cfg.AgentBackend } backend := agent.NewBackend(backendName, s.cfg) execRes, err := s.driver.Execute(ctx, wsDir, backend, opts) if err != nil { return nil, err } wsMeta, _ := s.loadWorkspaceMeta(wsDir) finalBytes, _ := os.ReadFile(filepath.Join(wsDir, "output.md")) bb, _ := s.driver.LoadBlackboard(wsDir) var extractedData map[string]string if bb != nil { extractedData = bb.Fields } advisorBrief := ExtractAdvisorBriefing(wsMeta.TaskKey, string(finalBytes), extractedData) if bb != nil && len(bb.Assumptions) > 0 { for _, a := range bb.Assumptions { stmt := fmt.Sprintf("[%s] %s (推导依据: %s)", a.Topic, a.Statement, a.Rationale) advisorBrief.BaselineAssumptions = append(advisorBrief.BaselineAssumptions, stmt) } } return &SquadRunResult{ TaskKey: wsMeta.TaskKey, ArtifactTitle: TaskArtifactTitle(wsMeta.TaskKey), MarkdownBody: string(finalBytes), ExtractedData: extractedData, WorkDir: wsDir, BackendUsed: backendName, Duration: time.Since(start), Success: execRes.Success, AdvisorBrief: advisorBrief, }, nil } // InspectWorkspace 查看指定工作区的当前执行状态、黑板数据与各专员产出 func (s *Service) InspectWorkspace(ctx context.Context, workDir string) (*WorkspaceState, error) { meta, err := s.loadWorkspaceMeta(workDir) if err != nil { return nil, fmt.Errorf("load workspace meta: %w", err) } squadCfg, err := s.driver.loadSquadConfig(workDir) if err != nil { return nil, fmt.Errorf("load squad config: %w", err) } wf, err := s.driver.LoadWorkflow(workDir) if err != nil { return nil, fmt.Errorf("load workflow: %w", err) } bb, err := s.driver.LoadBlackboard(workDir) if err != nil { return nil, fmt.Errorf("load blackboard: %w", err) } outPath := filepath.Join(workDir, "output.md") data, _ := os.ReadFile(outPath) return &WorkspaceState{ WorkDir: workDir, Metadata: meta, Squad: squadCfg, Workflow: *wf, Blackboard: *bb, DeliverableContent: string(data), }, nil } // ResumeOrModifyWorkspace 在现有工作区基础上,动态添加/重跑/修改任务并继续执行 func (s *Service) ResumeOrModifyWorkspace(ctx context.Context, req ModifyWorkflowRequest) (*SquadRunResult, error) { for _, add := range req.AddSteps { if err := s.driver.AddStep(req.WorkDir, add); err != nil { return nil, fmt.Errorf("add step %s: %w", add.ID, err) } } for _, removeID := range req.RemoveStepIDs { if err := s.driver.RemoveStep(req.WorkDir, removeID); err != nil { return nil, fmt.Errorf("remove step %s: %w", removeID, err) } } for _, update := range req.UpdateSteps { if err := s.driver.UpdateStep(req.WorkDir, update); err != nil { return nil, fmt.Errorf("update step %s: %w", update.ID, err) } } backendName := req.Backend if backendName == "" { backendName = s.cfg.AgentBackend } timeout := req.Timeout if timeout <= 0 { timeout = s.cfg.AgentTimeout } opts := ExecuteOptions{ Timeout: timeout, StepNotify: req.StepNotify, } return s.ExecuteWorkflow(ctx, req.WorkDir, backendName, opts) } func (s *Service) handleCDOQuotePreparation(workDir string, req SquadRunRequest) (*Attachment, error) { xlsxPath := filepath.Join(workDir, "CDO报价核算-V01.xlsx") artifactXlsxPath := filepath.Join(workDir, "artifacts", "CDO报价核算-V01.xlsx") in := quote.Input{ CustomerName: req.CustomerName, ProductName: req.ProjectName, Hours: map[string]float64{ "结构工程师": 320, "硬件工程师": 320, "软件工程师": 480, "项目经理": 160, }, } summary, err := quote.CalculateAndFill(in, "templates/xlsx/cdo_quote.xlsx", xlsxPath) if err != nil { return nil, err } data, err := os.ReadFile(xlsxPath) if err != nil { return nil, err } _ = os.WriteFile(artifactXlsxPath, data, 0o644) // 回填 template.md 中的报价数据 tmplPath := filepath.Join(workDir, "template.md") if tmplBytes, err := os.ReadFile(tmplPath); err == nil { filled := BackfillCDOQuoteMarkdown(string(tmplBytes), summary) _ = os.WriteFile(tmplPath, []byte(filled), 0o644) _ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte(filled), 0o644) } return &Attachment{ Filename: "CDO报价核算-V01.xlsx", ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", Data: data, }, nil } func (s *Service) loadWorkspaceMeta(wsDir string) (WorkspaceMetadata, error) { p := filepath.Join(wsDir, "workspace.json") var m WorkspaceMetadata data, err := os.ReadFile(p) if err != nil { return m, err } err = json.Unmarshal(data, &m) return m, err }