From 6bbbb16a1c762645e63017a5df25ea967a88c2ad Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Mon, 7 Sep 2026 18:23:35 +0800 Subject: [PATCH] feat(squad): define workspace directory metadata models and engine Co-authored-by: Cursor --- internal/squad/models.go | 160 ++++++++ internal/squad/workspace.go | 741 ++++++++++++++++++++++++++++++++++++ 2 files changed, 901 insertions(+) create mode 100644 internal/squad/models.go create mode 100644 internal/squad/workspace.go diff --git a/internal/squad/models.go b/internal/squad/models.go new file mode 100644 index 00000000..2922ab33 --- /dev/null +++ b/internal/squad/models.go @@ -0,0 +1,160 @@ +package squad + +import "time" + +// WorkspaceMetadata 记录工作区全局元数据 +type WorkspaceMetadata struct { + ID string `json:"id"` + SessionID int64 `json:"session_id"` + ProjectID int64 `json:"project_id"` + TaskKey string `json:"task_key"` + StageTitle string `json:"stage_title"` + ProjectName string `json:"project_name"` + CustomerName string `json:"customer_name"` + DeviceType string `json:"device_type,omitempty"` + IntendedUse string `json:"intended_use,omitempty"` + TargetMarket string `json:"target_market,omitempty"` + Status string `json:"status"` // created, running, completed, failed + Backend string `json:"backend"` + WorkDir string `json:"work_dir"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + CompletedAt *time.Time `json:"completed_at,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// AgentSpec 声明单个专员角色在小队中的设定 +type AgentSpec struct { + Role string `json:"role"` + Name string `json:"name"` + Model string `json:"model"` + Tools []string `json:"tools"` + BashPermission string `json:"bash_permission"` // allow, deny + Responsibilities []string `json:"responsibilities"` + ForbiddenActions []string `json:"forbidden_actions"` + InstructionsFile string `json:"instructions_file,omitempty"` +} + +// SquadConfig 激活的智能体小队编制 +type SquadConfig struct { + SquadName string `json:"squad_name"` + Version string `json:"version"` + Agents []AgentSpec `json:"agents"` +} + +// StepSpec 单个任务步骤规格定义 +type StepSpec struct { + ID string `json:"id"` + Role string `json:"role"` + Title string `json:"title"` + SubWorkspace string `json:"sub_workspace"` // 相对子路径,如 tasks/01_organize + Mode string `json:"mode"` // execute, plan, review_plan, replan + Parallel bool `json:"parallel"` + Needs []string `json:"needs,omitempty"` + Accept []string `json:"accept,omitempty"` + Status string `json:"status"` // pending, running, completed, failed, skipped + DurationMs int64 `json:"duration_ms,omitempty"` + Error string `json:"error,omitempty"` +} + +// WorkflowPlan 动态任务编排清单 +type WorkflowPlan struct { + WorkflowID string `json:"workflow_id"` + Status string `json:"status"` // pending, in_progress, completed, failed + CurrentStepIndex int `json:"current_step_index"` + Steps []StepSpec `json:"steps"` +} + +// ContextItem 上下文项目元数据 +type ContextItem struct { + File string `json:"file"` // 相对路径,如 context/user.md + Description string `json:"description"` // 用途说明 + AuthorityLevel string `json:"authority_level"` // authoritative, primary_evidence, reference_standard, supplementary, inferred + Guidance string `json:"guidance"` // 使用建议与优先级说明 +} + +// ContextIndex 上下文索引表 +type ContextIndex struct { + Items []ContextItem `json:"context_items"` +} + +// MaterialItem 输入材料 +type MaterialItem struct { + Filename string `json:"filename"` + Content []byte `json:"content"` +} + +// KnowledgeHit 法规知识库检索命中条目 +type KnowledgeHit struct { + DocCode string `json:"doc_code"` + Category string `json:"category,omitempty"` // standards, guidance, laws, catalog + Title string `json:"title"` + Content string `json:"content"` + Source string `json:"source"` +} + +// PriorDocItem 前序阶段归档文档 +type PriorDocItem struct { + DocKey string `json:"doc_key"` + Title string `json:"title"` + Filename string `json:"filename"` + Content string `json:"content"` + Stage string `json:"stage"` +} + +// FactItem 黑板事实项 +type FactItem struct { + ID string `json:"id"` + Claim string `json:"claim"` + Source string `json:"source"` + Verified bool `json:"verified"` +} + +// GapItem 黑板缺口项 +type GapItem struct { + ID string `json:"id"` + Item string `json:"item"` + Owner string `json:"owner"` // user, agent + Blocking bool `json:"blocking"` // 是否阻塞后续推进 + Resolved bool `json:"resolved"` +} + +// ImplicationItem 黑板含义项 +type ImplicationItem struct { + ID string `json:"id"` + Topic string `json:"topic"` + Statement string `json:"statement"` +} + +// BlackboardState 结构化共享黑板 +type BlackboardState struct { + Facts []FactItem `json:"facts"` + Gaps []GapItem `json:"gaps"` + Implications []ImplicationItem `json:"implications"` + Fields map[string]string `json:"fields"` + UpdatedAt time.Time `json:"updated_at"` +} + +// QualityReport 自动化质量门禁报告 +type QualityReport struct { + Passed bool `json:"passed"` + Score float64 `json:"score"` + TitleCheck bool `json:"title_check"` + MissingSections []string `json:"missing_sections,omitempty"` + ProhibitedPhrases []string `json:"prohibited_phrases,omitempty"` + QuoteMatches bool `json:"quote_matches"` + Violations []string `json:"violations,omitempty"` + CheckedAt time.Time `json:"checked_at"` +} + +// Manifest 执行清单与防篡改审计 +type Manifest struct { + ManifestVersion string `json:"manifest_version"` + WorkspaceID string `json:"workspace_id"` + DocKey string `json:"doc_key"` + GeneratedAt time.Time `json:"generated_at"` + Deliverable string `json:"deliverable"` + Files map[string]string `json:"files"` // relPath -> sha256 + QualityReport QualityReport `json:"quality_report"` + DurationMs int64 `json:"duration_ms"` +} diff --git a/internal/squad/workspace.go b/internal/squad/workspace.go new file mode 100644 index 00000000..c0436a3b --- /dev/null +++ b/internal/squad/workspace.go @@ -0,0 +1,741 @@ +package squad + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "chat-agent-system/internal/template" +) + +// WorkspacePrepareRequest 工作区准备请求 +type WorkspacePrepareRequest struct { + WorkDir string + SessionID int64 + ProjectID int64 + TaskKey string + StageTitle string + ProjectName string + CustomerName string + DeviceType string + IntendedUse string + TargetMarket string + Backend string + TemplateContent string + Materials []MaterialItem + KnowledgeHits []KnowledgeHit + PriorDocs []PriorDocItem + UserContext string + ProjectContext string + AgentContext string + MemoryBankFiles map[string]string + CustomAgents []AgentSpec + WorkflowSteps []StepSpec + InitialFields map[string]string + Metadata map[string]string +} + +// Workspace 表示已初始化的工作区句柄 +type Workspace struct { + WorkDir string + Meta WorkspaceMetadata + Squad SquadConfig + Workflow WorkflowPlan +} + +// WorkspaceEngine 工作区生命周期与沙箱构建引擎 +type WorkspaceEngine struct{} + +func NewWorkspaceEngine() *WorkspaceEngine { + return &WorkspaceEngine{} +} + +// PrepareWorkspace 初始化自包含工作区目录 +func (e *WorkspaceEngine) PrepareWorkspace(req WorkspacePrepareRequest) (*Workspace, error) { + workDir := req.WorkDir + if workDir == "" { + ts := time.Now().UnixNano() + workDir = filepath.Join("data", "dev", "workspaces", "bizdev", fmt.Sprintf("task_%s_%d", req.TaskKey, ts)) + } + if err := os.MkdirAll(workDir, 0o755); err != nil { + return nil, fmt.Errorf("create workspace root dir: %w", err) + } + + now := time.Now() + wsID := fmt.Sprintf("ws_%s_%d", req.TaskKey, now.UnixNano()) + if req.Metadata != nil && req.Metadata["id"] != "" { + wsID = req.Metadata["id"] + } + + // 1. 初始化 workspace.json + meta := WorkspaceMetadata{ + ID: wsID, + SessionID: req.SessionID, + ProjectID: req.ProjectID, + TaskKey: req.TaskKey, + StageTitle: req.StageTitle, + ProjectName: req.ProjectName, + CustomerName: req.CustomerName, + DeviceType: req.DeviceType, + IntendedUse: req.IntendedUse, + TargetMarket: req.TargetMarket, + Status: "created", + Backend: req.Backend, + WorkDir: workDir, + CreatedAt: now, + UpdatedAt: now, + Metadata: req.Metadata, + } + if err := writeJSON(filepath.Join(workDir, "workspace.json"), meta); err != nil { + return nil, fmt.Errorf("write workspace.json: %w", err) + } + + // 2. 初始化 squad.json + agents := req.CustomAgents + if len(agents) == 0 { + agents = DefaultSquadAgents(req.TaskKey) + } + squadCfg := SquadConfig{ + SquadName: fmt.Sprintf("%s_squad", req.TaskKey), + Version: "1.0.0", + Agents: agents, + } + if err := writeJSON(filepath.Join(workDir, "squad.json"), squadCfg); err != nil { + return nil, fmt.Errorf("write squad.json: %w", err) + } + + // 3. 初始化 workflow.json + steps := req.WorkflowSteps + if len(steps) == 0 { + steps = DefaultWorkflowSteps(req.TaskKey) + } + wf := WorkflowPlan{ + WorkflowID: fmt.Sprintf("wf_%s_%d", req.TaskKey, now.UnixNano()), + Status: "pending", + CurrentStepIndex: 0, + Steps: steps, + } + if err := writeJSON(filepath.Join(workDir, "workflow.json"), wf); err != nil { + return nil, fmt.Errorf("write workflow.json: %w", err) + } + + // 4. 写入自描述模板 template.md + tmplContent := req.TemplateContent + if strings.TrimSpace(tmplContent) == "" { + var err error + tmplContent, err = template.GetDocTemplate(req.TaskKey) + if err != nil { + tmplContent = fmt.Sprintf("# %s\n\n## 1. 结论\n\n待确认\n\n## 2. 正文\n\n待确认\n", req.StageTitle) + } + } + vars := map[string]string{ + "project_name": req.ProjectName, + "product_name": req.ProjectName, + "customer_name": req.CustomerName, + "device_type": req.DeviceType, + "intended_use": req.IntendedUse, + "target_market": req.TargetMarket, + "generated_at": now.Format("2006-01-02 15:04:05"), + "snapshot_id": wsID, + } + for k, v := range req.InitialFields { + vars[k] = v + } + renderedTmpl := template.RenderDoc(tmplContent, vars) + if err := os.WriteFile(filepath.Join(workDir, "template.md"), []byte(renderedTmpl), 0o644); err != nil { + return nil, fmt.Errorf("write template.md: %w", err) + } + + // 5. 初始化 blackboard.json + bb := BlackboardState{ + Facts: make([]FactItem, 0), + Gaps: make([]GapItem, 0), + Implications: make([]ImplicationItem, 0), + Fields: make(map[string]string), + UpdatedAt: now, + } + if req.CustomerName != "" { + bb.Fields["customer_name"] = req.CustomerName + bb.Facts = append(bb.Facts, FactItem{ + ID: "fact_01", + Claim: "客户名称为 " + req.CustomerName, + Source: "用户输入", + Verified: true, + }) + } + if req.ProjectName != "" { + bb.Fields["project_name"] = req.ProjectName + } + if req.DeviceType != "" { + bb.Fields["device_type"] = req.DeviceType + } + if req.IntendedUse != "" { + bb.Fields["intended_use"] = req.IntendedUse + } + if req.TargetMarket != "" { + bb.Fields["target_market"] = req.TargetMarket + } + for k, v := range req.InitialFields { + bb.Fields[k] = v + } + if err := writeJSON(filepath.Join(workDir, "blackboard.json"), bb); err != nil { + return nil, fmt.Errorf("write blackboard.json: %w", err) + } + + // 6. 构造自描述上下文体系 context/ + if err := e.buildContextSystem(workDir, req); err != nil { + return nil, fmt.Errorf("build context system: %w", err) + } + + // 7. 创建子工作区 tasks/ 并注入 TASK.md 与 AGENTS.md + if err := e.buildSubWorkspaces(workDir, wf, squadCfg, renderedTmpl); err != nil { + return nil, fmt.Errorf("build sub workspaces: %w", err) + } + + // 8. 创建 artifacts/ 与基础日志 + _ = os.MkdirAll(filepath.Join(workDir, "artifacts"), 0o755) + logEntry := fmt.Sprintf("[%s] 工作区创建完成: ID=%s, Task=%s\n", now.Format(time.RFC3339), wsID, req.TaskKey) + _ = os.WriteFile(filepath.Join(workDir, "execution.log"), []byte(logEntry), 0o644) + + return &Workspace{ + WorkDir: workDir, + Meta: meta, + Squad: squadCfg, + Workflow: wf, + }, nil +} + +// buildContextSystem 构建具有权威度说明的 context/ 体系 +func (e *WorkspaceEngine) buildContextSystem(workDir string, req WorkspacePrepareRequest) error { + ctxDir := filepath.Join(workDir, "context") + _ = os.MkdirAll(filepath.Join(ctxDir, "materials_text"), 0o755) + _ = os.MkdirAll(filepath.Join(ctxDir, "knowledge"), 0o755) + _ = os.MkdirAll(filepath.Join(ctxDir, "prior_docs"), 0o755) + _ = os.MkdirAll(filepath.Join(ctxDir, "memory"), 0o755) + + var contextItems []ContextItem + + // 1. user.md + userContent := req.UserContext + if strings.TrimSpace(userContent) == "" { + userContent = fmt.Sprintf("# 用户角色与偏好\n\n- 用户角色: 商务与技术负责人\n- 决策风格: 严谨可追溯,注重四段式论据\n- 授权边界: 预算与商业承诺必须经由用户拍板\n") + } + _ = os.WriteFile(filepath.Join(ctxDir, "user.md"), []byte(userContent), 0o644) + contextItems = append(contextItems, ContextItem{ + File: "context/user.md", + Description: "操作用户的业务角色、决策授权边界与协同偏好", + AuthorityLevel: "authoritative", + Guidance: "必须尊重用户设定的目标市场与边界,用户未决事项一律列入待确认清单,严禁越权替用户拍板", + }) + + // 2. project.md + projContent := req.ProjectContext + if strings.TrimSpace(projContent) == "" { + projContent = fmt.Sprintf("# 项目全局基线\n\n- 项目名称: %s\n- 客户名称: %s\n- 产品类型: %s\n- 预期用途: %s\n- 目标市场: %s\n", + req.ProjectName, req.CustomerName, req.DeviceType, req.IntendedUse, req.TargetMarket) + } + _ = os.WriteFile(filepath.Join(ctxDir, "project.md"), []byte(projContent), 0o644) + contextItems = append(contextItems, ContextItem{ + File: "context/project.md", + Description: "项目立项章程与全局事实基线", + AuthorityLevel: "authoritative", + Guidance: "本项目核心前提。所有后续分析论证不得推翻已锁定的项目名称与核心预期用途", + }) + + // 3. agent.md + agentContent := req.AgentContext + if strings.TrimSpace(agentContent) == "" { + agentContent = "# 智能体小队协同通用规范\n\n- 事实闭环: 严禁凭空捏造未提供之参数或法规\n- 引用规范: 引用必须标注出处〔材料:...〕或〔法规:...〕\n- 负向红线: 禁止敷衍写作、禁止'不做展开'、'从略'\n" + } + _ = os.WriteFile(filepath.Join(ctxDir, "agent.md"), []byte(agentContent), 0o644) + contextItems = append(contextItems, ContextItem{ + File: "context/agent.md", + Description: "智能体通用协同纪律与引用要求", + AuthorityLevel: "authoritative", + Guidance: "所有小队角色必须严格执行的专业守则与写作禁令", + }) + + // 4. materials_text/ + if len(req.Materials) > 0 { + var allBuilder strings.Builder + for _, m := range req.Materials { + name := filepath.Base(m.Filename) + if name == "" || name == "." { + name = "material.txt" + } + p := filepath.Join(ctxDir, "materials_text", name) + _ = os.WriteFile(p, m.Content, 0o644) + + // Also write to root materials_text/ for backward compatibility + rootMatDir := filepath.Join(workDir, "materials_text") + _ = os.MkdirAll(rootMatDir, 0o755) + _ = os.WriteFile(filepath.Join(rootMatDir, name), m.Content, 0o644) + + allBuilder.WriteString(fmt.Sprintf("\n\n=== 文件: %s ===\n\n", name)) + allBuilder.Write(m.Content) + + contextItems = append(contextItems, ContextItem{ + File: filepath.ToSlash(filepath.Join("context", "materials_text", name)), + Description: fmt.Sprintf("用户上传的输入材料文本: %s", name), + AuthorityLevel: "primary_evidence", + Guidance: "核心原始证据。提取参数需标明出处;材料内部矛盾需在矛盾表中并列呈现", + }) + } + _ = os.WriteFile(filepath.Join(ctxDir, "materials_text", "_all.txt"), []byte(allBuilder.String()), 0o644) + } + + // 5. knowledge/ + if len(req.KnowledgeHits) > 0 { + var kbBuilder strings.Builder + kbBuilder.WriteString("# 法规与标准检索条文摘录\n\n") + for _, hit := range req.KnowledgeHits { + kbBuilder.WriteString(fmt.Sprintf("### [%s] %s %s\n\n%s\n\n> 来源: %s\n\n---\n\n", + strings.ToUpper(hit.Category), hit.DocCode, hit.Title, hit.Content, hit.Source)) + } + _ = os.WriteFile(filepath.Join(ctxDir, "knowledge", "hits.md"), []byte(kbBuilder.String()), 0o644) + contextItems = append(contextItems, ContextItem{ + File: "context/knowledge/hits.md", + Description: "法规知识库中命中的医疗器械分类、国家标准与指导原则", + AuthorityLevel: "reference_standard", + Guidance: "合规与技术论据源。引用必须带标准号;涉及审批结论需标注'需向药监局审评中心确认'", + }) + } + + // 6. prior_docs/ + if len(req.PriorDocs) > 0 { + var indexBuilder strings.Builder + indexBuilder.WriteString("# 前序阶段交付物索引\n\n") + for _, doc := range req.PriorDocs { + filename := doc.Filename + if filename == "" { + filename = doc.DocKey + ".md" + } + p := filepath.Join(ctxDir, "prior_docs", filename) + _ = os.WriteFile(p, []byte(doc.Content), 0o644) + indexBuilder.WriteString(fmt.Sprintf("- [%s] `%s`: %s (阶段: %s)\n", doc.DocKey, filename, doc.Title, doc.Stage)) + + contextItems = append(contextItems, ContextItem{ + File: filepath.ToSlash(filepath.Join("context", "prior_docs", filename)), + Description: fmt.Sprintf("前序已归档文档: %s (%s)", doc.Title, doc.DocKey), + AuthorityLevel: "primary_evidence", + Guidance: "前序阶段已批准的结论基准。本阶段内容应与前序结论保持自洽", + }) + } + _ = os.WriteFile(filepath.Join(ctxDir, "prior_docs", "index.md"), []byte(indexBuilder.String()), 0o644) + } + + // 7. memory/ + if len(req.MemoryBankFiles) > 0 { + for k, v := range req.MemoryBankFiles { + p := filepath.Join(ctxDir, "memory", k) + _ = os.WriteFile(p, []byte(v), 0o644) + } + } else { + // 默认四大核心资产骨架 + _ = os.WriteFile(filepath.Join(ctxDir, "memory", "projectbrief.md"), []byte(fmt.Sprintf("# 项目章程\n- 项目: %s\n- 客户: %s\n", req.ProjectName, req.CustomerName)), 0o644) + _ = os.WriteFile(filepath.Join(ctxDir, "memory", "activeContext.md"), []byte(fmt.Sprintf("# 当前焦点\n- 阶段: %s\n- 时间: %s\n", req.TaskKey, time.Now().Format("2006-01-02 15:04"))), 0o644) + _ = os.WriteFile(filepath.Join(ctxDir, "memory", "systemPatterns.md"), []byte("# 规范与模式\n- 遵循四段式结构与严格角色边界\n- 单一真实源原则\n"), 0o644) + _ = os.WriteFile(filepath.Join(ctxDir, "memory", "progress.md"), []byte(fmt.Sprintf("# 进度\n- 当前正在进行: %s\n", req.StageTitle)), 0o644) + } + contextItems = append(contextItems, ContextItem{ + File: "context/memory/", + Description: "长期项目记忆银行 (ActiveContext, Progress, ProjectBrief, SystemPatterns)", + AuthorityLevel: "supplementary", + Guidance: "协同上下文背景参考,防止上下文遗忘", + }) + + // 8. 写入 context/index.json + ctxIndex := ContextIndex{Items: contextItems} + if err := writeJSON(filepath.Join(ctxDir, "index.json"), ctxIndex); err != nil { + return err + } + + // 9. 写入 context/README.md 指引 + readmeContent := `# 上下文使用指引 (Context Reading Guide) + +本目录为当前智能体任务的自包含上下文沙箱。所有智能体专员在执行任务时,必须按照以下权威度等级(Authority Level)采纳信息: + +| 权威度级别 | 适用文件类别 | 采信与处理规则 | +|---|---|---| +| **authoritative** | ` + "`user.md`" + `, ` + "`project.md`" + `, ` + "`agent.md`" + ` | **最高权威**。必须绝对遵循;严禁越权替用户拍板,未决项列入待确认清单 | +| **primary_evidence** | ` + "`materials_text/*`" + `, ` + "`prior_docs/*`" + ` | **原始事实证据**。引用必须标明出处条文;若材料间存在矛盾,并列呈现 | +| **reference_standard** | ` + "`knowledge/hits.md`" + ` | **标准与法规依据**。必须指明法规/标准代码;涉及监管裁定标注需窗口核实 | +| **supplementary** | ` + "`memory/*`" + ` | **补充记忆背景**。仅供背景了解,不作为唯一支撑证据 | + +--- +` + return os.WriteFile(filepath.Join(ctxDir, "README.md"), []byte(readmeContent), 0o644) +} + +// buildSubWorkspaces 为 workflow 中的各个子任务构建独立沙箱 +func (e *WorkspaceEngine) buildSubWorkspaces(workDir string, wf WorkflowPlan, squadCfg SquadConfig, renderedTmpl string) error { + agentMap := make(map[string]AgentSpec) + for _, a := range squadCfg.Agents { + agentMap[a.Role] = a + } + + for _, step := range wf.Steps { + subDir := filepath.Join(workDir, filepath.FromSlash(step.SubWorkspace)) + if err := os.MkdirAll(subDir, 0o755); err != nil { + return err + } + + // 1. 生成 AGENTS.md + var agentsBuilder strings.Builder + inst, err := template.GetAgentInstruction(step.Role) + if err == nil { + agentsBuilder.WriteString(inst) + } else { + spec := agentMap[step.Role] + agentsBuilder.WriteString(fmt.Sprintf("# 角色: %s (%s)\n\n", spec.Name, spec.Role)) + if len(spec.Responsibilities) > 0 { + agentsBuilder.WriteString("## 核心职责\n") + for _, r := range spec.Responsibilities { + agentsBuilder.WriteString(fmt.Sprintf("- %s\n", r)) + } + agentsBuilder.WriteString("\n") + } + if len(spec.ForbiddenActions) > 0 { + agentsBuilder.WriteString("## 严禁行为(红线)\n") + for _, f := range spec.ForbiddenActions { + agentsBuilder.WriteString(fmt.Sprintf("- ❌ %s\n", f)) + } + agentsBuilder.WriteString("\n") + } + } + _ = os.WriteFile(filepath.Join(subDir, "AGENTS.md"), []byte(agentsBuilder.String()), 0o644) + _ = os.WriteFile(filepath.Join(subDir, "INSTRUCTIONS.md"), []byte(agentsBuilder.String()), 0o644) + + // 2. 生成 TASK.md + deliverableName := "output.md" + if step.Mode == "plan" || step.Mode == "replan" { + deliverableName = "output_plan.md" + } else if step.Mode == "review_plan" { + deliverableName = "review_notes.md" + } + + var taskBuilder strings.Builder + taskBuilder.WriteString(fmt.Sprintf("# 任务执行指令: %s\n\n", step.Title)) + taskBuilder.WriteString(fmt.Sprintf("- **步骤 ID**: `%s`\n", step.ID)) + taskBuilder.WriteString(fmt.Sprintf("- **分配角色**: `%s`\n", step.Role)) + taskBuilder.WriteString(fmt.Sprintf("- **执行模式**: `%s`\n", step.Mode)) + taskBuilder.WriteString(fmt.Sprintf("- **交付产物**: `%s`\n\n", deliverableName)) + + taskBuilder.WriteString("## 环境与输入导航\n") + taskBuilder.WriteString("请仔细查阅上级目录资源:\n") + taskBuilder.WriteString("- `../../context/index.json` 及 `../../context/README.md`:阅读上下文指引\n") + taskBuilder.WriteString("- `../../template.md`:自描述交付模板(嵌入四段式与格式约束)\n") + taskBuilder.WriteString("- `../../blackboard.json`:前序专员抽取的结构化事实与缺口\n") + taskBuilder.WriteString("- `../../context/materials_text/`:原始材料全文\n") + taskBuilder.WriteString("- `../../context/knowledge/`:法规与标准命中快照\n\n") + + if len(step.Needs) > 0 { + taskBuilder.WriteString("## 前序依赖产出\n") + for _, n := range step.Needs { + taskBuilder.WriteString(fmt.Sprintf("- `../../%s`\n", filepath.ToSlash(n))) + } + taskBuilder.WriteString("\n") + } + + if len(step.Accept) > 0 { + taskBuilder.WriteString("## 验收标准(Checklist)\n") + for _, a := range step.Accept { + taskBuilder.WriteString(fmt.Sprintf("- [ ] %s\n", a)) + } + taskBuilder.WriteString("\n") + } + + taskBuilder.WriteString("## 交付契约与写作纪律\n") + taskBuilder.WriteString(fmt.Sprintf("请在当前目录下生成完整交付物 `%s`:\n", deliverableName)) + taskBuilder.WriteString("1. 严格四段式展开(结论句 → 表格/列表 → 解读 → 支撑出处)。\n") + taskBuilder.WriteString("2. 禁止使用'不做展开'、'从略'、'篇幅有限'等省略敷衍套话。\n") + taskBuilder.WriteString("3. 关键事实必须标注出处,未知项列入待确认清单。\n") + if step.Role == "organizer" { + taskBuilder.WriteString("4. 从材料提取的客观字段,请在当前工作目录下输出 `fields.json`。\n") + } + + _ = os.WriteFile(filepath.Join(subDir, "TASK.md"), []byte(taskBuilder.String()), 0o644) + + // 3. 写入 input_refs.json + inputRefs := map[string]any{ + "step_id": step.ID, + "role": step.Role, + "needs": step.Needs, + "target_file": deliverableName, + } + _ = writeJSON(filepath.Join(subDir, "input_refs.json"), inputRefs) + } + + return nil +} + +// DefaultSquadAgents 根据任务类型返回默认小队成员编制 +func DefaultSquadAgents(taskKey string) []AgentSpec { + switch taskKey { + case "intake": + return []AgentSpec{ + { + Role: "organizer", + Name: "材料整理专员", + Model: "claude-3-7-sonnet", + Tools: []string{"read", "write", "edit"}, + BashPermission: "deny", + Responsibilities: []string{"材料事实抽取", "缺口识别", "字段结构化回填"}, + ForbiddenActions: []string{"严禁测算价格", "严禁出具法规终结裁定", "严禁撰写正文终稿"}, + }, + { + Role: "writer", + Name: "文档撰写专员", + Model: "claude-3-7-sonnet", + Tools: []string{"read", "write", "edit"}, + BashPermission: "deny", + Responsibilities: []string{"严格四段式成文", "依据材料出处引用", "模板各章节写满"}, + ForbiddenActions: []string{"严禁写不做展开或从略", "严禁随意篡改前序事实"}, + }, + { + Role: "leader", + Name: "商务对接负责人", + Model: "claude-3-7-sonnet", + Tools: []string{"read", "write", "edit"}, + BashPermission: "deny", + Responsibilities: []string{"计划审核", "正文终审", "待确认清单汇总", "对外汇报"}, + ForbiddenActions: []string{"严禁代用户做商业拍板", "严禁直接交付未审核初稿"}, + }, + } + case "research": + return []AgentSpec{ + { + Role: "searcher", + Name: "资料检索专员", + Model: "claude-3-7-sonnet", + Tools: []string{"read", "write", "edit"}, + BashPermission: "deny", + Responsibilities: []string{"公开资料检索", "法规标准核对", "出处与日期标注"}, + ForbiddenActions: []string{"严禁编造URL与标准号", "未检索到须标明尝试路径"}, + }, + { + Role: "market", + Name: "市场与竞品分析师", + Model: "claude-3-7-sonnet", + Tools: []string{"read", "write", "edit"}, + BashPermission: "deny", + Responsibilities: []string{"竞品同维度对比", "市场规模测算", "机会与风险分析"}, + ForbiddenActions: []string{"严禁编造市场份额与价格", "必须采用统一对比维度"}, + }, + { + Role: "regulatory", + Name: "医疗器械法规专员", + Model: "claude-3-7-sonnet", + Tools: []string{"read", "write", "edit"}, + BashPermission: "deny", + Responsibilities: []string{"器械分类初筛", "注册路径比对", "关键标准提炼"}, + ForbiddenActions: []string{"严禁代做行政终局裁定", "必须标注需向窗口确认"}, + }, + { + Role: "writer", + Name: "文档撰写专员", + Model: "claude-3-7-sonnet", + Tools: []string{"read", "write", "edit"}, + BashPermission: "deny", + Responsibilities: []string{"按计划合成调研报告", "全面四段式展开"}, + ForbiddenActions: []string{"严禁写不做展开或从略"}, + }, + { + Role: "leader", + Name: "商务对接负责人", + Model: "claude-3-7-sonnet", + Tools: []string{"read", "write", "edit"}, + BashPermission: "deny", + Responsibilities: []string{"审核撰写计划", "终审报告", "质量把关"}, + ForbiddenActions: []string{"严禁代用户做商业拍板"}, + }, + } + default: + return []AgentSpec{ + { + Role: "organizer", + Name: "材料整理专员", + Model: "claude-3-7-sonnet", + Tools: []string{"read", "write", "edit"}, + BashPermission: "deny", + Responsibilities: []string{"事实整理与基线对齐"}, + }, + { + Role: "writer", + Name: "文档撰写专员", + Model: "claude-3-7-sonnet", + Tools: []string{"read", "write", "edit"}, + BashPermission: "deny", + Responsibilities: []string{"正文撰写"}, + }, + { + Role: "leader", + Name: "商务对接负责人", + Model: "claude-3-7-sonnet", + Tools: []string{"read", "write", "edit"}, + BashPermission: "deny", + Responsibilities: []string{"终审与把关"}, + }, + } + } +} + +// DefaultWorkflowSteps 根据任务类型返回最佳实践编排步骤 +func DefaultWorkflowSteps(taskKey string) []StepSpec { + switch taskKey { + case "intake": + return []StepSpec{ + { + ID: "step_01_organize", + Role: "organizer", + Title: "抽取材料事实并回填字段", + SubWorkspace: "tasks/01_organize", + Mode: "execute", + Accept: []string{ + "抽取材料中的客户身份与产品定义", + "回填 fields.json 结构化数据", + "向 blackboard 写入 facts 与 gaps", + }, + }, + { + ID: "step_02_draft", + Role: "writer", + Title: "撰写用户初始需求收集表初稿", + SubWorkspace: "tasks/02_draft", + Mode: "execute", + Needs: []string{"tasks/01_organize/output.md"}, + Accept: []string{ + "按 template.md 各章节严格四段式展开", + "正文翔实,禁止写'不做展开'", + "文末集中附录字段快照与待确认清单", + }, + }, + { + ID: "step_03_review", + Role: "leader", + Title: "审核并定稿交付", + SubWorkspace: "tasks/03_review", + Mode: "execute", + Needs: []string{"tasks/02_draft/output.md"}, + Accept: []string{ + "核验全文逻辑一致性与出处标注", + "汇编待确认清单并给出下一步建议", + "交付终稿 output.md", + }, + }, + } + case "research": + return []StepSpec{ + { + ID: "step_01_search", + Role: "searcher", + Title: "公开资料与标准检索", + SubWorkspace: "tasks/01_search", + Mode: "execute", + Parallel: true, + Accept: []string{ + "检索纪要附带明确出处或未检索到说明", + "对本项目含义逐条阐述", + }, + }, + { + ID: "step_02_market", + Role: "market", + Title: "市场格局与竞品同维度对比", + SubWorkspace: "tasks/02_market", + Mode: "execute", + Parallel: true, + Accept: []string{ + "多维度竞品对比表", + "机会与风险对称分析", + }, + }, + { + ID: "step_03_regulatory", + Role: "regulatory", + Title: "医疗器械法规与指导原则初筛", + SubWorkspace: "tasks/03_regulatory", + Mode: "execute", + Parallel: true, + Accept: []string{ + "分类依据条目与建议类别", + "明确标注需向审评窗口确认", + }, + }, + { + ID: "step_04_plan", + Role: "writer", + Title: "撰写调研报告段落计划", + SubWorkspace: "tasks/04_plan", + Mode: "plan", + Needs: []string{ + "tasks/01_search/output.md", + "tasks/02_market/output.md", + "tasks/03_regulatory/output.md", + }, + Accept: []string{"各章节论点与引用规划"}, + }, + { + ID: "step_05_review_plan", + Role: "leader", + Title: "审核调研报告撰写计划", + SubWorkspace: "tasks/05_review_plan", + Mode: "review_plan", + Needs: []string{"tasks/04_plan/output_plan.md"}, + Accept: []string{"出具审核意见 review_notes.md"}, + }, + { + ID: "step_06_draft", + Role: "writer", + Title: "按计划合成调研报告正文", + SubWorkspace: "tasks/06_draft", + Mode: "execute", + Needs: []string{ + "tasks/04_plan/output_plan.md", + "tasks/05_review_plan/review_notes.md", + }, + Accept: []string{"四段式完整正文,无省略套话"}, + }, + { + ID: "step_07_review", + Role: "leader", + Title: "终审并定稿", + SubWorkspace: "tasks/07_review", + Mode: "execute", + Needs: []string{"tasks/06_draft/output.md"}, + Accept: []string{"质量把关并通过交付"}, + }, + } + default: + return []StepSpec{ + { + ID: "step_01_draft", + Role: "writer", + Title: "撰写初稿", + SubWorkspace: "tasks/01_draft", + Mode: "execute", + Accept: []string{"按模板四段式展开"}, + }, + { + ID: "step_02_review", + Role: "leader", + Title: "审核定稿", + SubWorkspace: "tasks/02_review", + Mode: "execute", + Needs: []string{"tasks/01_draft/output.md"}, + Accept: []string{"质量门禁通过"}, + }, + } + } +} + +func writeJSON(path string, val any) error { + b, err := json.MarshalIndent(val, "", " ") + if err != nil { + return err + } + return os.WriteFile(path, b, 0o644) +}