package squad import ( "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "time" "chat-agent-system/internal/agent" "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) } // Options 是 Squad 依赖注入的唯一配置面;不反向依赖主仓 config.Config, // 为独立模块做准备(见多仓方案 KTD1)。 type Options struct { // BackendName 为执行后端名(空 = 由调用方默认)。 BackendName string // BackendTimeout 为单次执行超时;零值由调用方默认。 BackendTimeout time.Duration // DataDir 为工作区根(任务目录挂在其下)。 DataDir string // NewBackend 由调用方注入的后端工厂(Core 装配:凭证、环境、并发上限 // 留在产品侧)。nil 时返回错误,不静默降级 Stub。 NewBackend func(name string) agent.Backend // TaskPreparer 为可选的任务准备钩子(KTD1:报价领域规则、核算与 xlsx // 资产归 Core;Core 只做计算并返回产物,工作区文件由 squad 统一写入)。 // nil 或返回 nil 表示无预处理。 TaskPreparer func(workDir string, req SquadRunRequest) (*TaskPreparation, error) // Assets 提供文档模板与成员指令等内容资产(KTD1:与业务绑定的模板、 // 提示词及版本选择归 Core)。nil 时相关能力失败关闭,不降级猜测。 Assets AssetProvider } // AssetProvider 是小队执行所需内容资产的唯一来源;磁盘/内嵌来源、 // 版本与业务绑定由产品侧装配决定。 type AssetProvider interface { // DocTemplate 返回任务对应的文档模板正文。 DocTemplate(taskKey string) (string, error) // RenderDoc 以变量渲染模板。 RenderDoc(tmpl string, vars map[string]string) string // AgentInstruction 返回角色指令。 AgentInstruction(role string) (string, error) // AgentInstructionFile 返回指定指令文件内容。 AgentInstructionFile(file string) (string, error) } // TaskPreparation 是领域准备钩子的产物:附件与工作区内回写内容。 // 路径均为工作区相对路径;物理写入由 squad 执行(工作区单写者)。 type TaskPreparation struct { Attachment *Attachment Backfills map[string][]byte // MirrorTemplateToOutput 为 true 时,template.md 回填内容同时镜像到 // output.md(工作区产物镜像语义归 squad,Core 不直接命名 output.md)。 MirrorTemplateToOutput bool } func (o Options) withDefaults() Options { if o.BackendName == "" { o.BackendName = "pi-agent" } if o.BackendTimeout <= 0 { o.BackendTimeout = 10 * time.Minute } return o } // Service SquadService 实现 type Service struct { opts Options engine *WorkspaceEngine driver *WorkflowDriver gate *QualityGate } func NewService(opts Options) *Service { engine := NewWorkspaceEngine(opts.Assets) return &Service{ opts: opts.withDefaults(), 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, s.opts.Assets) 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.opts.DataDir, "workspaces", "bizdev", fmt.Sprintf("%s_%d", req.TaskKey, ts)) } backendName := req.Backend if backendName == "" { backendName = s.opts.BackendName } // 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 报价核算+xlsx 回填):Core 计算,squad 写入 var attachments []Attachment if s.opts.TaskPreparer != nil { prep, err := s.opts.TaskPreparer(workDir, req) if err == nil && prep != nil { for rel, content := range prep.Backfills { _ = os.MkdirAll(filepath.Dir(filepath.Join(workDir, rel)), 0o755) _ = os.WriteFile(filepath.Join(workDir, rel), content, 0o644) } if prep.MirrorTemplateToOutput { if filled, ok := prep.Backfills["template.md"]; ok { _ = os.WriteFile(filepath.Join(workDir, "output.md"), filled, 0o644) } } if prep.Attachment != nil { attachments = append(attachments, *prep.Attachment) } } } // 3. 执行动态工作流调度 sink := ProgressFromContext(ctx) if sink != nil { sink.EmitPhase("squad_assembly", fmt.Sprintf("正在组建并启动【%s】智能体小队(后端: %s)...", TaskArtifactTitle(req.TaskKey), backendName), "leader", 0, 0, 5) } if s.opts.NewBackend == nil { return nil, fmt.Errorf("squad: NewBackend factory not injected") } backend := s.opts.NewBackend(backendName) timeout := req.Timeout if timeout <= 0 { timeout = s.opts.BackendTimeout } execOpts := ExecuteOptions{ Timeout: timeout, StepNotify: req.StepNotify, } execRes, err := s.driver.Execute(ctx, workDir, backend, execOpts) if err != nil { if sink != nil { sink.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 sink != nil { sink.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 sink != nil { sink.EmitPhase("quality_gate", fmt.Sprintf("质量门禁审查告警: %v", err), "leader", 0, 0, 90) } } else if sink != nil { if report.Passed { sink.EmitPhase("quality_gate", fmt.Sprintf("质量门禁审核通过(评分: %.1f / 100)", report.Score), "leader", 0, 0, 95) } else { violations := strings.Join(report.Violations, ";") if violations == "" { violations = "未满足质量门禁要求" } sink.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 sink != nil { score := 0.0 if report != nil { score = report.Score } sink.EmitDeliverable(req.TaskKey, TaskArtifactTitle(req.TaskKey), finalOutPath, len(strings.Fields(string(finalBytes))), score) if advisorBrief != nil { sink.EmitPhase("advisor_briefing", "贴身业务参谋正在提炼关键决策摘要与风险雷达...", "coordinator", 0, 0, 98) sink.EmitSummary(TaskArtifactTitle(req.TaskKey)+"已生成", advisorBrief.MaturityHeadline, advisorBrief.MaturityScore, advisorBrief.MaturityLevel) sink.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.opts.BackendName } if s.opts.NewBackend == nil { return nil, fmt.Errorf("squad: NewBackend factory not injected") } backend := s.opts.NewBackend(backendName) 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.opts.BackendName } timeout := req.Timeout if timeout <= 0 { timeout = s.opts.BackendTimeout } opts := ExecuteOptions{ Timeout: timeout, StepNotify: req.StepNotify, } return s.ExecuteWorkflow(ctx, req.WorkDir, backendName, opts) } 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 }