package squad import ( "chat-agent-system/internal/textextract" "fmt" "os" "path/filepath" "strings" "time" ) // 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" } // 自动提取纯文本并注入页码锚点 textContent, err := textextract.ExtractTextWithAnchors(name, m.Content) if err != nil || strings.TrimSpace(textContent) == "" { textContent = string(m.Content) } textBytes := []byte(textContent) p := filepath.Join(ctxDir, "materials_text", name) _ = os.WriteFile(p, textBytes, 0o644) // 若原文件扩展名不是 .txt / .md,同时生成一份 .txt 方便智能体直接阅读 if !strings.HasSuffix(name, ".txt") && !strings.HasSuffix(name, ".md") { _ = os.WriteFile(filepath.Join(ctxDir, "materials_text", name+".txt"), textBytes, 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), textBytes, 0o644) if !strings.HasSuffix(name, ".txt") && !strings.HasSuffix(name, ".md") { _ = os.WriteFile(filepath.Join(rootMatDir, name+".txt"), textBytes, 0o644) } allBuilder.WriteString(fmt.Sprintf("\n\n=== 文件: %s ===\n\n", name)) allBuilder.WriteString(textContent) 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) _ = os.WriteFile(filepath.Join(ctxDir, "materials_text", "README.md"), []byte("# 原始材料清单\n\n已汇总在 `context/materials_text/_all.txt`,亦可单独查阅各文件。\n"), 0o644) } else { _ = os.WriteFile(filepath.Join(ctxDir, "materials_text", "README.md"), []byte("# 原始材料说明\n\n本项目当前未提供独立的附加文本材料,所有事实依据以 `context/project.md`(项目全局基线)、`context/user.md`(用户要求与偏好)以及 `context/knowledge/hits.md`(法规标准知识库)为准。\n"), 0o644) } // 5. knowledge/ _ = os.WriteFile(filepath.Join(ctxDir, "knowledge", "WEB_CROSSCHECK.md"), []byte("# 法规检索与交叉核对原则\n\n1. 知识库条目(hits.md)为首要法规事实依据。\n2. 如需查询外部信息,以国家药监局官方分类目录为主。\n"), 0o644) if len(req.KnowledgeHits) > 0 { var kbBuilder strings.Builder kbBuilder.WriteString("# 权威法规与技术标准引用库 (Regulatory Knowledge Hits)\n\n") kbBuilder.WriteString("以下为针对本项目研发与注册需求命中的权威法规、国家标准(GB)与行业标准(YY)条款。\n") kbBuilder.WriteString("在输出交付物时,请结合上下文使用,并显式标注【引用标记 (CiteKey)】标明出处。\n\n") for _, hit := range req.KnowledgeHits { kbBuilder.WriteString(fmt.Sprintf("### [%s] %s %s\n\n", strings.ToUpper(hit.Category), hit.DocCode, hit.Title)) if hit.HeadingPath != "" { kbBuilder.WriteString(fmt.Sprintf("- **章节路径**: %s\n", hit.HeadingPath)) } if hit.CiteKey != "" { kbBuilder.WriteString(fmt.Sprintf("- **引用标记 (CiteKey)**: `%s`\n", hit.CiteKey)) } kbBuilder.WriteString(fmt.Sprintf("- **来源**: %s\n\n", hit.Source)) kbBuilder.WriteString(hit.Content + "\n\n---\n\n") } _ = 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: "合规与技术论据源。引用必须标注标准号与引用标记 (CiteKey);涉及审批结论需标注'需向药监局审评中心确认'", }) } // 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) } else { _ = os.WriteFile(filepath.Join(ctxDir, "prior_docs", "README.md"), []byte("# 前序交付物说明\n\n本项目当前处于启动阶段,暂无已归档的前序阶段交付物。\n"), 0o644) } if req.TemplateContent != "" { _ = os.WriteFile(filepath.Join(ctxDir, "template.md"), []byte(req.TemplateContent), 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. dialogue/ — project member dialogue (digest + turns + facts) if err := e.writeDialogueContext(ctxDir, req.ProjectID, req.DialoguePackage, &contextItems); err != nil { return err } // 9. 写入 context/index.json ctxIndex := ContextIndex{Items: contextItems} if err := writeJSON(filepath.Join(ctxDir, "index.json"), ctxIndex); err != nil { return err } // 10. 写入 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`" + ` | **标准与法规依据**。必须指明法规/标准代码;涉及监管裁定标注需窗口核实 | | **collab_dialogue** | ` + "`dialogue/*`" + ` | **项目成员对话**。采信级低于黑板事实与已批准文档;与高权威来源冲突时须并列呈现矛盾,不得静默采信对话版本 | | **supplementary** | ` + "`memory/*`" + ` | **补充记忆背景**。仅供背景了解,不作为唯一支撑证据 | --- ` return os.WriteFile(filepath.Join(ctxDir, "README.md"), []byte(readmeContent), 0o644) } func (e *WorkspaceEngine) writeDialogueContext(ctxDir string, projectID int64, pkg *DialoguePackage, items *[]ContextItem) error { dlgDir := filepath.Join(ctxDir, "dialogue") if err := os.MkdirAll(dlgDir, 0o755); err != nil { return err } if reason := dialogueOmitReason(projectID, pkg); reason != "" { _ = os.WriteFile(filepath.Join(dlgDir, "README.md"), []byte(reason), 0o644) return nil } digest := pkg.DigestMD if strings.TrimSpace(digest) == "" { digest = "# 项目对话纪要\n\n暂无可用共享对话纪要。\n" } turns := pkg.TurnsMD if strings.TrimSpace(turns) == "" { turns = "# 项目成员对话近窗\n\n本窗口内无授权可见的共享对话。\n" } facts := pkg.FactsJSON if len(facts) == 0 { facts = []byte("[]\n") } confirmations := pkg.ConfirmationsMD if strings.TrimSpace(confirmations) == "" { confirmations = "# 上下文冲突裁决记录\n\n本轮未检出冲突,或裁决尚未执行。\n" } _ = os.WriteFile(filepath.Join(dlgDir, "digest.md"), []byte(digest), 0o644) _ = os.WriteFile(filepath.Join(dlgDir, "turns.md"), []byte(turns), 0o644) _ = os.WriteFile(filepath.Join(dlgDir, "facts.json"), facts, 0o644) _ = os.WriteFile(filepath.Join(dlgDir, "CONFIRMATIONS.md"), []byte(confirmations), 0o644) readme := "# 项目成员对话上下文\n\n- `digest.md`:按日纪要\n- `turns.md`:近窗带元数据原文\n- `facts.json`:对话沉淀事实候选\n- `CONFIRMATIONS.md`:冲突裁决审计\n\n权威度 `collab_dialogue`:低于黑板与已批准文档。\n" readme += dialogueCollectionStats(pkg) _ = os.WriteFile(filepath.Join(dlgDir, "README.md"), []byte(readme), 0o644) *items = append(*items, ContextItem{ File: "context/dialogue/", Description: "项目成员共享对话(纪要/近窗原文/事实候选/冲突裁决)", AuthorityLevel: "collab_dialogue", Guidance: "项目成员对话记录。采信级低于黑板事实与已批准文档;与高权威来源冲突时须并列呈现矛盾,不得静默采信对话版本", }) return nil } func dialogueInjectionEnabled() bool { switch strings.TrimSpace(strings.ToLower(os.Getenv("DIALOGUE_CTX_INJECTION"))) { case "off", "false", "0", "no": return false default: return true } } // dialogueOmitReason 区分未注入的三种原因。已有对话包时返回空,调用方写入正文。 func dialogueOmitReason(projectID int64, pkg *DialoguePackage) string { if pkg != nil { return "" } if !dialogueInjectionEnabled() { return "# 项目对话说明\n\n未注入项目成员对话:对话注入开关已关闭(DIALOGUE_CTX_INJECTION)。\n" } if projectID <= 0 { return "# 项目对话说明\n\n未注入项目成员对话:任务未绑定项目,无法收集个人会话与项目成员共享对话。\n" } return "# 项目对话说明\n\n未注入项目成员对话:项目已绑定,但本轮没有对话包(采集失败或调用方未传入)。这不表示对话已被判定为无用信息。\n" } func dialogueCollectionStats(pkg *DialoguePackage) string { if pkg == nil { return "" } if pkg.TurnCount == 0 && pkg.ExcludedPrivate == 0 && pkg.ExcludedNoAccess == 0 && pkg.ExcludedNoise == 0 { return "" } return fmt.Sprintf("\n采集统计:保留 %d 轮;滤除私密 %d、阶段不可见 %d、噪声 %d。仅当这些过滤后仍有内容时才写入 turns.md。\n", pkg.TurnCount, pkg.ExcludedPrivate, pkg.ExcludedNoAccess, pkg.ExcludedNoise) }