add-coder 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +66 -27
  2. package/dist/index.js +158 -11
  3. package/package.json +9 -12
  4. package/templates/.add-coder-src-hash.json +256 -0
  5. package/templates/adapters/claude/hooks/doc-format-guard.sh +17 -0
  6. package/templates/adapters/claude/hooks/lib/notify.sh +34 -0
  7. package/templates/adapters/claude/hooks/pre-tool-use.sh +26 -7
  8. package/templates/adapters/claude/hooks/prompt-submit.sh +16 -0
  9. package/templates/adapters/codex/hooks/doc-format-guard.sh +17 -0
  10. package/templates/adapters/codex/hooks/lib/notify.sh +34 -0
  11. package/templates/adapters/codex/hooks/pre-tool-use.sh +48 -18
  12. package/templates/adapters/codex/hooks/prompt-submit.sh +16 -0
  13. package/templates/adapters/qoder/hooks/doc-format-guard.sh +17 -0
  14. package/templates/adapters/qoder/hooks/lib/notify.sh +34 -0
  15. package/templates/adapters/qoder/hooks/pre-tool-use.sh +28 -9
  16. package/templates/adapters/qoder/hooks/prompt-submit.sh +18 -1
  17. package/templates/adapters/trae/hooks/doc-format-guard.sh +17 -0
  18. package/templates/adapters/trae/hooks/lib/notify.sh +34 -0
  19. package/templates/adapters/trae/hooks/pre-tool-use.sh +48 -18
  20. package/templates/adapters/trae/hooks/prompt-submit.sh +16 -0
  21. package/templates/adapters/vscode/hooks/doc-format-guard.sh +16 -0
  22. package/templates/adapters/vscode/hooks/lib/notify.sh +34 -0
  23. package/templates/adapters/vscode/hooks/pre-tool-use.sh +26 -7
  24. package/templates/adapters/vscode/hooks/prompt-submit.sh +16 -0
  25. package/templates/core/hooks/doc-format-guard.sh +17 -0
  26. package/templates/core/hooks/lib/notify.sh +34 -0
  27. package/templates/core/hooks/pre-tool-use.sh +48 -18
  28. package/templates/core/hooks/prompt-submit.sh +16 -0
  29. package/templates/core/reviews/.gitkeep +0 -0
  30. package/templates/core/scripts/mcp-server/elicitation/confirm.ts +30 -0
  31. package/templates/core/scripts/mcp-server/elicitation/index.ts +1 -0
  32. package/templates/core/scripts/mcp-server/index.ts +15 -0
  33. package/templates/core/scripts/mcp-server/notifications/hitl.ts +49 -0
  34. package/templates/core/scripts/mcp-server/notifications/hook.ts +268 -0
  35. package/templates/core/scripts/mcp-server/notifications/index.ts +8 -0
  36. package/templates/core/scripts/mcp-server/resources/add-coder-version.ts +25 -0
  37. package/templates/core/scripts/mcp-server/resources/add-state.ts +44 -0
  38. package/templates/core/scripts/mcp-server/resources/hook-events-report.ts +104 -0
  39. package/templates/core/scripts/mcp-server/resources/index.ts +12 -0
  40. package/templates/core/scripts/mcp-server/resources/round-task.ts +22 -0
  41. package/templates/core/scripts/mcp-server/sampling/index.ts +1 -0
  42. package/templates/core/scripts/mcp-server/sampling/review.ts +47 -0
  43. package/templates/core/scripts/mcp-server/shared/env.ts +26 -0
  44. package/templates/core/scripts/mcp-server/shared/fs.ts +40 -0
  45. package/templates/core/scripts/mcp-server/shared/prisma.ts +35 -0
  46. package/templates/core/scripts/mcp-server/shared/response.ts +9 -0
  47. package/templates/core/scripts/mcp-server/tasks/index.ts +2 -0
  48. package/templates/core/scripts/mcp-server/tasks/runner.ts +20 -0
  49. package/templates/core/scripts/mcp-server/tasks/store.ts +20 -0
  50. package/templates/core/scripts/mcp-server/tools/audit.ts +73 -0
  51. package/templates/core/scripts/mcp-server/tools/context.ts +337 -0
  52. package/templates/core/scripts/mcp-server/tools/docs.ts +42 -0
  53. package/templates/core/scripts/mcp-server/tools/gateway.ts +135 -0
  54. package/templates/core/scripts/mcp-server/tools/hook-event-report.ts +92 -0
  55. package/templates/core/scripts/mcp-server/tools/index.ts +17 -0
  56. package/templates/core/scripts/mcp-server/tools/quality.ts +93 -0
  57. package/templates/core/scripts/mcp-server/types.ts +7 -0
  58. package/templates/core/scripts/mcp-server.ts +8 -3455
@@ -0,0 +1,20 @@
1
+ type TaskStatus = "pending" | "running" | "done" | "failed"
2
+ export interface Task { id: string; type: string; status: TaskStatus; progress: number; result?: string; createdAt: Date }
3
+
4
+ const taskQueue: Task[] = []
5
+
6
+ export function enqueueTask(type: string): string {
7
+ const id = `task-${Date.now()}-${Math.random().toString(36).slice(2,6)}`
8
+ taskQueue.push({ id, type, status: "pending", progress: 0, createdAt: new Date() })
9
+ return id
10
+ }
11
+
12
+ export function getTaskStatus(id: string): Task | undefined { return taskQueue.find(t => t.id === id) }
13
+
14
+ export function getAllTasks(): Task[] { return [...taskQueue] }
15
+
16
+ export async function runTask(id: string, handler: () => Promise<string>) {
17
+ const task = taskQueue.find(t => t.id === id); if (!task) return
18
+ task.status = "running"; task.progress = 10
19
+ try { task.result = await handler(); task.status = "done"; task.progress = 100 } catch (e) { task.status = "failed"; task.result = e instanceof Error ? e.message : String(e) }
20
+ }
@@ -0,0 +1,20 @@
1
+ import { prisma } from "../shared/prisma.js"
2
+ import type { Task } from "./runner.js"
3
+
4
+ export async function persistTask(task: Task): Promise<void> {
5
+ try {
6
+ const typedPrisma = prisma as Record<string, Record<string, (...a: unknown[]) => unknown>>
7
+ await typedPrisma.auditLog.create({
8
+ data: {
9
+ userId: "system",
10
+ action: `TASK_${task.status.toUpperCase()}`,
11
+ targetType: "TASK",
12
+ targetId: task.id,
13
+ reason: task.type,
14
+ afterState: { progress: task.progress, result: task.result ?? null }
15
+ }
16
+ })
17
+ } catch { /* intentionally empty */ }
18
+ }
19
+
20
+ export type { Task }
@@ -0,0 +1,73 @@
1
+ import * as z from "zod/v4"
2
+ import type { McpServer } from "@modelcontextprotocol/server"
3
+ import { textResponse, errorResponse } from "../shared/response.js"
4
+ import { prisma } from "../shared/prisma.js"
5
+
6
+ export function registerAuditTools(server: McpServer) {
7
+
8
+ // ===== query_audit_logs (L992-1109) =====
9
+ server.registerTool("query_audit_logs", {
10
+ description: "稀疏查询开发操作审计日志(DevOperation 表,MCP-5 稀疏推理恢复)。支持多维度检索,AI 可在不同对话会话中通过任意维度组合查询之前的开发操作记录,实现跨会话的上下文恢复。\n\n典型用法:\n- query_audit_logs({ targetType: \"API_ROUTE\" }) — 查所有 API 路由改动\n- query_audit_logs({ targetId: \"src/app/api/knowledge/route.ts\" }) — 查特定文件改动\n- query_audit_logs({ keyword: \"pagination\" }) — 按关键词搜索\n- query_audit_logs({ planKeyword: \"add-coder\" }) — 按 Plan 关键词查该 Plan 下所有 devlog\n- query_audit_logs({}) — 查最近的记录(session-init 会话恢复)",
11
+ inputSchema: z.object({
12
+ targetType: z.string().optional().describe("按目标类型精确过滤"),
13
+ action: z.string().optional().describe("按操作类型精确过滤"),
14
+ targetId: z.string().optional().describe("按目标标识精确过滤"),
15
+ planKeyword: z.string().optional().describe("按 Plan 关键词过滤"),
16
+ keyword: z.string().optional().describe("关键词搜索"),
17
+ sinceMinutes: z.number().optional().describe("时间窗口起始(分钟前)"),
18
+ limit: z.number().optional().default(20).describe("返回最大条数,默认 20,最大 100"),
19
+ }),
20
+ }, async (args: Record<string, string | number | undefined>, _ctx: unknown) => {
21
+ try {
22
+ const { targetType, action, targetId, planKeyword, keyword, sinceMinutes, limit = 20 } = args
23
+ const where: Record<string, unknown> = {}
24
+ if (sinceMinutes !== undefined) where.createdAt = { gte: new Date(Date.now() - sinceMinutes * 60 * 1000) }
25
+ if (targetType) where.targetType = targetType; if (action) where.action = action; if (targetId) where.targetId = targetId; if (planKeyword) where.planKeyword = planKeyword
26
+ if (keyword) where.OR = [{ action: { contains: keyword, mode: "insensitive" } },{ targetType: { contains: keyword, mode: "insensitive" } },{ targetId: { contains: keyword, mode: "insensitive" } },{ reason: { contains: keyword, mode: "insensitive" } },{ planKeyword: { contains: keyword, mode: "insensitive" } }]
27
+ const logs = await prisma.devOperation.findMany({ where, orderBy: { createdAt: "desc" }, take: Math.min(limit, 100), include: { user: { select: { username: true } } } })
28
+ if (logs.length === 0) {
29
+ const f: string[] = []; if (targetType) f.push(`targetType=${targetType}`); if (action) f.push(`action=${action}`); if (targetId) f.push(`targetId=${targetId}`); if (planKeyword) f.push(`planKeyword=${planKeyword}`); if (keyword) f.push(`keyword="${keyword}"`); if (sinceMinutes !== undefined) f.push(`sinceMinutes=${sinceMinutes}`)
30
+ return textResponse(`=== 开发操作审计日志 ===\n\n未找到匹配的审计记录(条件: ${f.length > 0 ? f.join(", ") : "无条件"})。\n\n可能原因: 数据库未运行、尚无相关开发操作记录、或 filter 过于严格。`)
31
+ }
32
+ const fl: string[] = []; if (targetType) fl.push(`targetType=${targetType}`); if (action) fl.push(`action=${action}`); if (targetId) fl.push(`targetId=${targetId}`); if (planKeyword) fl.push(`planKeyword=${planKeyword}`); if (keyword) fl.push(`keyword="${keyword}"`); if (sinceMinutes !== undefined) fl.push(`最近${sinceMinutes}分钟`)
33
+ const lines = [`=== 开发操作审计日志 (条件: ${fl.length > 0 ? fl.join(", ") : "无过滤条件(最近全部)"}) ===`, `共 ${logs.length} 条记录`]
34
+ for (let i = 0; i < logs.length; i++) {
35
+ const l = logs[i]; lines.push(`[${i+1}] ${l.createdAt.toISOString()}`, ` action: ${l.action} | targetType: ${l.targetType} | targetId: ${l.targetId || "(无)"}`)
36
+ if (l.reason) lines.push(` reason: ${l.reason}`); if (l.planKeyword) lines.push(` planKeyword: ${l.planKeyword}`)
37
+ }
38
+ if (planKeyword) { lines.push("=== Plan 分组 ===", `planKeyword=${planKeyword} 的操作链:`); for (const l of logs) lines.push(` ${l.createdAt.toISOString().slice(11,19)} ${l.action}`) }
39
+ lines.push("", "=== 稀疏推理建议 ===", "基于以上审计日志,可以恢复开发上下文。")
40
+ return textResponse(lines.join("\n"))
41
+ } catch (error) { return errorResponse(`查询审计日志失败: ${error instanceof Error ? error.message : String(error)}\n可能原因: 数据库未运行或 AuditLog 表不存在。`) }
42
+ })
43
+
44
+ // ===== record_dev_operation (L1111-1212) =====
45
+ server.registerTool("record_dev_operation", {
46
+ description: "记录一次开发操作到 DevOperation 表(ADD-7)。AI 助手在对代码进行任何修改/创建/删除操作后,必须调用此工具记录操作审计。\n\n**targetId 路径格式(强制)**:必须使用相对于 workspace 根目录的路径,禁止绝对路径。\n- ✅ src/middleware.ts\n- ✅ ${MAGIC_DIR}/plans/xxx.md\n- ❌ /absolute/path/to/src/middleware.ts",
47
+ inputSchema: z.object({
48
+ action: z.string().describe("操作类型: 'MODIFY', 'CREATE', 'DELETE', 'DOC_UPDATED', 'DOC_CREATED' 等"),
49
+ targetType: z.string().describe("目标类型: 'API_ROUTE', 'COMPONENT', 'SCHEMA', 'RULE', 'DOC', 'PLAN', 'SPEC', 'SKILL', 'AGENT' 等"),
50
+ targetId: z.string().optional().describe("目标标识(相对路径)"),
51
+ planKeyword: z.string().optional().describe("关联 Plan 的关键词"),
52
+ beforeState: z.string().optional().describe("操作前的状态(JSON 字符串)"),
53
+ afterState: z.string().optional().describe("操作后的状态(JSON 字符串)"),
54
+ reason: z.string().optional().describe("操作原因"),
55
+ }),
56
+ }, async (args: Record<string, string | number | undefined>, _ctx: unknown) => {
57
+ try {
58
+ const { action, targetType, targetId, planKeyword, beforeState, afterState, reason } = args
59
+ const pathWarnings: string[] = []
60
+ if (targetId && (targetId.startsWith("/") || /^[A-Z]:\\/.test(targetId))) pathWarnings.push(`⚠️ targetId 使用了绝对路径: "${targetId}"。请使用相对路径。`)
61
+ let parsedBefore: unknown, parsedAfter: unknown
62
+ try { if (beforeState) parsedBefore = JSON.parse(beforeState); if (afterState) parsedAfter = JSON.parse(afterState) } catch { return errorResponse("beforeState/afterState 必须是有效的 JSON 字符串。") }
63
+ let systemUser = await (prisma.addUser as Record<string, (...a: unknown[]) => unknown>).findUnique({ where: { username: "ai-assistant" }, select: { id: true } })
64
+ if (!systemUser) systemUser = await (prisma.addUser as Record<string, (...a: unknown[]) => unknown>).create({ data: { id: "ai-assistant", username: "ai-assistant", email: "ai-assistant@internal" }, select: { id: true } })
65
+ const log = await (prisma.devOperation as Record<string, (...a: unknown[]) => unknown>).create({ data: { userId: systemUser.id, planKeyword: planKeyword || "unknown", action, targetType, targetId: targetId || "unknown", beforeState: parsedBefore ?? null, afterState: parsedAfter ?? null, reason: reason || null } })
66
+ const lines = [`✅ 开发操作已记录`, ` ID: ${log.id}`, ` action: ${action}`, ` targetType: ${targetType}`, ` targetId: ${targetId || "unknown"}`, ` planKeyword: ${planKeyword || "unknown"}`, ` createdAt: ${log.createdAt.toISOString()}`]
67
+ if (pathWarnings.length > 0) { lines.push(""); lines.push(...pathWarnings) }
68
+ lines.push("", `📋 落库回查(必须执行):`, targetId ? ` query_audit_logs({ targetId: "${targetId}" }) — 确认本条记录已写入` : "")
69
+ return textResponse(lines.join("\n"))
70
+ } catch (error) { return errorResponse(`记录开发操作失败: ${error instanceof Error ? error.message : String(error)}`) }
71
+ })
72
+
73
+ }
@@ -0,0 +1,337 @@
1
+ import * as z from "zod/v4"
2
+ import type { McpServer } from "@modelcontextprotocol/server"
3
+ import { readFile } from "fs/promises"
4
+ import { readdir } from "fs/promises"
5
+ import { stat } from "fs/promises"
6
+ import { existsSync } from "fs"
7
+ import { join, relative } from "path"
8
+ import { textResponse, errorResponse } from "../shared/response.js"
9
+ import { readFileSafe, readdirRecursive } from "../shared/fs.js"
10
+ import { PROJECT_ROOT, MAGIC_DIR } from "../shared/fs.js"
11
+
12
+ export function registerContextTools(server: McpServer) {
13
+
14
+ // ===== get_project_context (L166-430) =====
15
+ server.registerTool(
16
+ "get_project_context",
17
+ {
18
+ description: `获取项目的完整上下文信息:目录结构、技术栈、包管理信息、项目规则(ADD 范式约束)、${MAGIC_DIR}/ 工作流产物(templates/specs/reviews/plans)。AI 助手在 MCP-1(上下文优先)中应在生成代码前调用此工具获取项目真实信息,避免幻觉。\n\nscope='add-state' 返回 ADD 工作流状态快照:当前活跃 Plan、Review 未闭环问题、DPS 门禁状态、待执行操作清单。用于空白对话开局时快速介入 ADD 范式。`,
19
+ inputSchema: z.object({
20
+ scope: z.string().optional().describe("获取信息的范围: 'structure' 仅目录结构, 'rules' 仅项目规则, 'package' 仅包信息, 'add-state' ADD 工作流状态, 'all' 全部"),
21
+ }),
22
+ },
23
+ async (args: { scope?: string }) => {
24
+ try {
25
+ const scope = args?.scope || "all"
26
+ const parts: string[] = []
27
+
28
+ if (scope === "all" || scope === "package") {
29
+ const pkg = await readFileSafe(join(PROJECT_ROOT, "package.json"))
30
+ if (pkg) {
31
+ const parsed = JSON.parse(pkg)
32
+ parts.push("=== 项目信息 ===")
33
+ parts.push(`名称: ${parsed.name}`)
34
+ parts.push(`版本: ${parsed.version}`)
35
+ parts.push(`技术栈: Next.js ${parsed.dependencies?.next || "unknown"} + TypeScript + Prisma + LangGraph`)
36
+ parts.push("")
37
+ parts.push("核心依赖:")
38
+ const keyDeps = [
39
+ "next", "@langchain/langgraph", "@prisma/client", "prisma",
40
+ "zustand", "@tanstack/react-query", "chromadb", "zod",
41
+ ]
42
+ for (const dep of keyDeps) {
43
+ const ver = parsed.dependencies?.[dep] || parsed.devDependencies?.[dep]
44
+ if (ver) parts.push(` ${dep}: ${ver}`)
45
+ }
46
+ parts.push("")
47
+ parts.push("可用脚本:")
48
+ for (const [name, script] of Object.entries(parsed.scripts || {})) {
49
+ parts.push(` ${name}: ${String(script)}`)
50
+ }
51
+ parts.push("")
52
+ }
53
+ }
54
+
55
+ if (scope === "all" || scope === "rules") {
56
+ const rules = await readFileSafe(join(PROJECT_ROOT, MAGIC_DIR, "rules", "project_rules.md"))
57
+ if (rules) {
58
+ parts.push("=== 项目规则 (ADD 范式强制约束) ===")
59
+ const lines = rules.split("\n")
60
+ let inCodeBlock = false
61
+ for (const line of lines) {
62
+ if (line.startsWith("```")) { inCodeBlock = !inCodeBlock; continue }
63
+ if (inCodeBlock) continue
64
+ if (line.startsWith("## ADD-") || line.startsWith("## 项目")) { parts.push(""); parts.push(line) }
65
+ else if (line.startsWith("###") || line.startsWith("####")) { parts.push(line) }
66
+ }
67
+ parts.push("")
68
+ }
69
+ }
70
+
71
+ if (scope === "all" || scope === "structure" || scope === "add-state") {
72
+ parts.push("=== ADD 工作流状态 ===")
73
+ const plansDir = join(PROJECT_ROOT, MAGIC_DIR, "plans")
74
+ const reviewsDir = join(PROJECT_ROOT, MAGIC_DIR, "reviews")
75
+ const specsDir = join(PROJECT_ROOT, MAGIC_DIR, "specs")
76
+
77
+ let activePlan = ""
78
+ let activePlanPath = ""
79
+ if (existsSync(plansDir)) {
80
+ const planFiles = (await readdirRecursive(plansDir))
81
+ .filter(f => f.endsWith(".md") && !f.includes("add-route") && !f.includes("handoff"))
82
+ .sort()
83
+ if (planFiles.length > 0) {
84
+ activePlan = planFiles[planFiles.length - 1]
85
+ activePlanPath = join(plansDir, activePlan)
86
+ parts.push(`最近 Plan: ${activePlan}`)
87
+ const allPlanFiles = await readdirRecursive(plansDir)
88
+ const addRouteFile = allPlanFiles.find(f => f.includes("add-route"))
89
+ const planKeyword = activePlan.replace(/-plan-v\d+\.md$/, "")
90
+ const handoffFiles = allPlanFiles.filter(f => f.includes("handoff"))
91
+ const hasHandoff = handoffFiles.some(f => f.toLowerCase().includes(planKeyword.toLowerCase()))
92
+ parts.push(` add-route: ${addRouteFile ? "✅ " + addRouteFile : "❌ 缺失(需回退 Step 0.5)"}`)
93
+ parts.push(` handoff: ${hasHandoff ? "✅ 已生成" : "❌ 缺失(需回退 Step 0.5)"}`)
94
+ } else { parts.push("最近 Plan: 无") }
95
+ } else { parts.push("最近 Plan: 无") }
96
+
97
+ let reviewFileName = ""; let reviewP0Count = 0; let reviewP1Count = 0; let reviewBackflowRate = 0
98
+ if (existsSync(reviewsDir) && activePlan) {
99
+ const reviewFiles = await readdir(reviewsDir)
100
+ const planKeyword = activePlan.replace(/-plan-v\d+\.md$/, "")
101
+ const matchingReview = reviewFiles.find(f => f.toLowerCase().includes(planKeyword.toLowerCase()) && f.includes("-review-v"))
102
+ || reviewFiles.find(f => f.toLowerCase().includes(planKeyword.toLowerCase()))
103
+ if (matchingReview) {
104
+ reviewFileName = matchingReview
105
+ const reviewContent = await readFileSafe(join(reviewsDir, matchingReview)) || ""
106
+ const reviewLines = reviewContent.split("\n")
107
+ let inP0 = false, inP1 = false
108
+ for (const line of reviewLines) {
109
+ if (line.match(/P0|ADD.*合规|阻断/)) { inP0 = true; inP1 = false; continue }
110
+ if (line.match(/P1|架构设计.*缺口/)) { inP0 = false; inP1 = true; continue }
111
+ if (line.match(/P2|中等|影响评估|决策结论|方案对比/)) { inP0 = false; inP1 = false; continue }
112
+ if ((inP0 || inP1) && line.trim().startsWith("|") && !line.includes("---")) {
113
+ const cols = line.split("|").map(c => c.trim()).filter(Boolean)
114
+ if (cols.length >= 4 && cols[0].match(/^\d+$/)) {
115
+ if (inP0) reviewP0Count++; else if (inP1) reviewP1Count++
116
+ }
117
+ }
118
+ }
119
+ const planContent = await readFileSafe(activePlanPath) || ""
120
+ if ((reviewP0Count + reviewP1Count) > 0 && planContent) {
121
+ const indicators = ["add-route", "specs/", "handoff", "Task Group", "perExpertTopK", "agri_tech", "ChromaCollectionManager", "迁移路线图", "8 个 Expert", "冗余策略"]
122
+ let hit = 0
123
+ for (const ind of indicators) { if (planContent.toLowerCase().includes(ind.toLowerCase())) hit++ }
124
+ reviewBackflowRate = Math.min(100, Math.round((hit / Math.max(reviewP0Count + reviewP1Count, 1)) * 100))
125
+ }
126
+ parts.push(""); parts.push(`关联 Review: ${matchingReview}`)
127
+ parts.push(` P0 问题: ${reviewP0Count} 个, P1 问题: ${reviewP1Count} 个`)
128
+ parts.push(` 回流状态: 约 ${reviewBackflowRate}%`)
129
+ parts.push(reviewBackflowRate < 70 ? " ⚠️ Review 结论未充分回流至 Plan — 需执行 0.6.5 卡位" : " ✅ Review 结论基本回流至 Plan")
130
+ } else { parts.push(""); parts.push("关联 Review: 无(该 Plan 尚未生成方案评审)") }
131
+ }
132
+
133
+ if (activePlan) {
134
+ const planKeyword = activePlan.replace(/-plan-v\d+\.md$/, "")
135
+ const specDirs = existsSync(specsDir) ? await readdir(specsDir) : []
136
+ const matchingSpec = specDirs.find(d => d.toLowerCase().includes(planKeyword.toLowerCase()))
137
+ parts.push("")
138
+ if (matchingSpec) {
139
+ const hasSpec = existsSync(join(specsDir, matchingSpec, "spec.md"))
140
+ const hasTasks = existsSync(join(specsDir, matchingSpec, "tasks.md"))
141
+ const hasChecklist = existsSync(join(specsDir, matchingSpec, "checklist.md"))
142
+ parts.push(`Specs: ${matchingSpec}/`)
143
+ parts.push(` spec.md: ${hasSpec ? "✅" : "❌"}`)
144
+ parts.push(` tasks.md: ${hasTasks ? "✅" : "❌"}`)
145
+ parts.push(` checklist.md: ${hasChecklist ? "✅" : "❌"}`)
146
+ } else { parts.push("Specs: ❌ 缺失") }
147
+ }
148
+
149
+ parts.push(""); parts.push("=== 待执行 ADD 操作(按流程顺序) ===")
150
+ const todoItems: string[] = []
151
+ if (!activePlan) { todoItems.push("1. [未开始] 用户提出需求 → 生成 Plan") }
152
+ else {
153
+ if (!reviewFileName) { todoItems.push("1. [Step 0] 生成 Plan Review(ADD-9 方案评审)") }
154
+ else if (reviewP0Count + reviewP1Count > 0 && reviewBackflowRate < 70) {
155
+ todoItems.push("1. [0.6.5] Review 结论回流至 Plan — P0/P1 未写入 Plan 体")
156
+ todoItems.push("2. [check_dps] DPS 门禁预计不通过(回流完整度 < 70%)")
157
+ }
158
+ const allPlanDir = await readdirRecursive(plansDir)
159
+ const hasAddRoute = allPlanDir.some(f => f.includes("add-route"))
160
+ if (!hasAddRoute && reviewBackflowRate >= 70) { todoItems.push("2. [Step 0.5] 生成 add-route") }
161
+ const planKw = activePlan.replace(/-plan-v\d+\.md$/, "")
162
+ const sd = existsSync(specsDir) ? await readdir(specsDir) : []
163
+ if (!sd.some(d => d.toLowerCase().includes(planKw.toLowerCase())) && reviewBackflowRate >= 70) { todoItems.push("3. [Step 0] 生成 Specs 三元组") }
164
+ if (todoItems.length === 0) { todoItems.push("✅ ADD 就绪 — check_dps ≥ 85 后可进入 Step 1") }
165
+ }
166
+ for (const item of todoItems) parts.push(item)
167
+ parts.push(""); parts.push("快速指令: 说「执行 add-paradigm Step 0」进入文档先行流程")
168
+ parts.push(" 说「将 Review 结论回流至 Plan」触发 0.6.5 卡位")
169
+ }
170
+
171
+ if (scope === "all" || scope === "structure") {
172
+ parts.push("=== 项目目录结构(顶层) ===")
173
+ const topDirs = [MAGIC_DIR, "src", "prisma", "scripts", "docs", "data", "public"]
174
+ for (const dir of topDirs) {
175
+ const fullPath = join(PROJECT_ROOT, dir)
176
+ if (existsSync(fullPath)) { const entries = await readdir(fullPath); parts.push(` ${dir}/ (${entries.length} 项)`) }
177
+ }
178
+ parts.push(""); parts.push("=== src/ 子目录结构 ===")
179
+ const srcDirs = ["agents", "app", "components", "lib", "services", "stores", "types"]
180
+ for (const dir of srcDirs) {
181
+ const fullPath = join(PROJECT_ROOT, "src", dir)
182
+ if (existsSync(fullPath)) {
183
+ const entries = await readdir(fullPath)
184
+ const items = (await Promise.all(entries.slice(0, 15).map(async (e) => { const s = await stat(join(fullPath, e)); return s.isDirectory() ? `${e}/` : e }))).join(", ")
185
+ parts.push(` src/${dir}/ (${entries.length} 项): ${items})`)
186
+ }
187
+ }
188
+ parts.push(""); parts.push(`=== ${MAGIC_DIR}/ ADD 工作流产物 ===`)
189
+ const magicDirs = [
190
+ { dir: "templates", desc: "ADD 文档模板(11 个)" }, { dir: "specs", desc: "specs 三元组(spec+tasks+checklist)" },
191
+ { dir: "reviews", desc: "方案审查 + 实现审查 + 运行时审查" }, { dir: "plans", desc: "Plan + handoff 交接手册" },
192
+ { dir: "rules", desc: "项目规则 + 理论→实践映射" }, { dir: "skills", desc: "SKILL 行为定义(add-paradigm / session-init)" },
193
+ { dir: "scripts", desc: "工具脚本 + MCP 服务器" },
194
+ ]
195
+ for (const { dir, desc } of magicDirs) {
196
+ const fullPath = join(PROJECT_ROOT, MAGIC_DIR, dir)
197
+ if (existsSync(fullPath)) { const entries = await readdir(fullPath); parts.push(` ${MAGIC_DIR}/${dir}/ (${entries.length} 项) — ${desc}`) }
198
+ }
199
+ }
200
+ return textResponse(parts.join("\n"))
201
+ } catch (error) { return errorResponse(`获取项目上下文失败: ${error instanceof Error ? error.message : String(error)}`) }
202
+ }
203
+ )
204
+
205
+ // ===== get_db_schema (L433-522) =====
206
+ server.registerTool(
207
+ "get_db_schema",
208
+ {
209
+ description: "获取 Prisma 数据库 Schema 定义(MCP-1 上下文优先)。返回指定模型的结构、字段、关系。AI 助手在编写数据库查询代码时应调用此工具获取真实的 Schema 信息,避免凭记忆假设。",
210
+ inputSchema: z.object({ model: z.string().optional().describe("可选的模型名称(不区分大小写)。不指定则返回所有模型概况。指定则返回该模型的完整字段定义。") }),
211
+ },
212
+ async (args: { model?: string }) => {
213
+ try {
214
+ const schemaPath = join(PROJECT_ROOT, "prisma", "schema.prisma")
215
+ const schema = await readFileSafe(schemaPath)
216
+ if (!schema) return errorResponse("未找到 prisma/schema.prisma 文件")
217
+ const modelName = args?.model?.toLowerCase()
218
+ if (modelName) {
219
+ const modelRegex = new RegExp(`model\\s+${modelName}\\s*\\{`, "i")
220
+ const match = schema.match(modelRegex)
221
+ if (match) {
222
+ const startIdx = match.index ?? 0; const braceIdx = schema.indexOf("{", startIdx)
223
+ if (braceIdx !== -1) { let depth = 1; let endIdx = braceIdx + 1; while (depth > 0 && endIdx < schema.length) { if (schema[endIdx] === "{") depth++; else if (schema[endIdx] === "}") depth--; endIdx++ }; return textResponse(`=== Model: ${args.model} ===\n\nmodel ${args.model} ${schema.slice(braceIdx, endIdx)}`) }
224
+ }
225
+ const enumMatch = schema.match(new RegExp(`enum\\s+${modelName}\\s*\\{`, "i"))
226
+ if (enumMatch) return textResponse(`=== Enum: ${args.model} ===\n\n可用的枚举值见不传参调用结果。`)
227
+ return errorResponse(`未找到模型或枚举: ${args.model}。可用模型见不传参调用结果。`)
228
+ }
229
+ const models: Array<{ name: string; fieldCount: number }> = []
230
+ const modelRegex = /model\s+(\w+)\s*\{/g; let m
231
+ while ((m = modelRegex.exec(schema)) !== null) {
232
+ const startIdx = m.index; const braceIdx = schema.indexOf("{", startIdx)
233
+ if (braceIdx !== -1) { let depth = 1; let endIdx = braceIdx + 1; while (depth > 0 && endIdx < schema.length) { if (schema[endIdx] === "{") depth++; else if (schema[endIdx] === "}") depth--; endIdx++ }; const body = schema.slice(braceIdx, endIdx); const fieldCount = body.split("\n").filter((l: string) => l.trim() && !l.trim().startsWith("//") && !l.trim().startsWith("@@")).length; models.push({ name: m[1], fieldCount }) }
234
+ }
235
+ const enums: string[] = []; const enumRegex = /enum\s+(\w+)\s*\{/g
236
+ while ((m = enumRegex.exec(schema)) !== null) { enums.push(m[1]) }
237
+ const parts = ["=== Prisma Schema 概况 ===", "", `模型 (${models.length} 个):`]
238
+ for (const model of models) parts.push(` ${model.name} (${model.fieldCount} 字段)`)
239
+ if (enums.length > 0) { parts.push(""); parts.push(`枚举 (${enums.length} 个): ${enums.join(", ")}`) }
240
+ parts.push("", '提示: 指定 model 参数获取完整字段定义,例如: get_db_schema({ model: "User" })')
241
+ return textResponse(parts.join("\n"))
242
+ } catch (error) { return errorResponse(`获取 Schema 失败: ${error instanceof Error ? error.message : String(error)}`) }
243
+ }
244
+ )
245
+
246
+ // ===== get_audit_logger_pattern (L524-612) =====
247
+ server.registerTool(
248
+ "get_audit_logger_pattern",
249
+ {
250
+ description: "获取指定域的审计日志器完整代码或模式(MCP-1 上下文优先)。历史域('knowledge-base', 'agent')返回已有的混合式日志器代码;新域返回三层分离式模板(ADD-4 三层可插拔架构):\n- Layer 1 开发审计(dev-logger): console + file + DB metadata,可插拔\n- Layer 2 运行时审计(audit): console + AuditLog 表,始终开启\n- Layer 3 调试日志: console only,LOG_LEVEL 控制\n新业务域必须使用三层分离模式。",
251
+ inputSchema: z.object({ domain: z.string().describe("审计日志器域: 'knowledge-base', 'agent'(历史混合式),或新域如 'personnel'(三层分离式)") }),
252
+ },
253
+ async (args: { domain: string }) => {
254
+ try {
255
+ const domain = args?.domain; if (!domain) return errorResponse("domain 参数不能为空")
256
+ const filePath = (() => { switch(domain) { case "knowledge-base": return join(PROJECT_ROOT, "src", "lib", "audit-logger.ts"); case "agent": return join(PROJECT_ROOT, "src", "lib", "agent-audit-logger.ts"); default: return "" } })()
257
+ if (filePath) {
258
+ const content = await readFileSafe(filePath); if (!content) return errorResponse(`未找到 ${domain} 审计日志器文件: ${filePath}`)
259
+ const meta: Record<string, { prefix: string; logDir: string; logFile: string }> = { "knowledge-base": { prefix: "[KB-AUDIT]", logDir: "logs/knowledge-base/", logFile: "kb-audit.log" }, "agent": { prefix: "[AGENT-AUDIT]", logDir: "logs/agent/", logFile: "agent-audit.log" } }
260
+ const m = meta[domain]
261
+ const parts = [`=== ${domain} 审计日志器(历史混合式,Layer 1 + Layer 2 未分离) ===`, `前缀: ${m.prefix}`, `日志目录: ${m.logDir}`, `日志文件: ${m.logFile}`, `文件路径: ${relative(PROJECT_ROOT, filePath)}`, "", "=== 完整代码 ===", content, "", "=== 模式要点 ===", "1. PREFIX 常量: [DOMAIN-AUDIT] 格式", `2. LOG_DIR: logs/domain/ 目录 (当前: ${m.logDir})`, "3. AuditPhase 类型: 枚举所有业务阶段", "4. audit() / auditPhaseStart() / auditPhaseEnd() 三函数", "5. readRecentLogs() / clearLogs() 读写函数", "6. ENABLE_FILE_LOG 环境变量控制,开发环境默认启用", "7. 三通道输出: console.log + fs.appendFile + 数据库回写", "", "⚠️ 注意: 这是历史混合式模式。新建业务域应使用三层分离模式(调用 generate_audit_logger 生成)。"]
262
+ return textResponse(parts.join("\n"))
263
+ }
264
+ const featureCap = domain.split("-").map((s: string) => s.charAt(0).toUpperCase() + s.slice(1)).join("")
265
+ const fnPrefix = featureCap.charAt(0).toLowerCase() + featureCap.slice(1)
266
+ return textResponse(`=== ${domain} 三层分离式审计日志器(新模式,ADD-4) ===\n\n新建 ${domain} 业务域应调用 generate_audit_logger 生成两个文件:\n\n--- 文件1: src/lib/${domain}-dev-logger.ts (Layer 1 开发审计,可插拔) ---\n仅在 NODE_ENV=development 时生效,用于 AI 合规检查。\nADD-4 三通道输出:\n 1. console.log — ${fnPrefix}Audit / ${fnPrefix}AuditPhaseStart / ${fnPrefix}AuditPhaseEnd\n 2. fs.appendFile — writeToFile(自动)\n 3. DB metadata — build${featureCap}DevAudit() 构建记录 → 业务服务层 saveAuditData 写入\n包含函数: auditPhaseStart / auditPhaseEnd / audit / buildXxxDevAudit / readRecentLogs / clearLogs\n\n--- 文件2: src/lib/${domain}-audit.ts (Layer 2 运行时业务审计,不可插拔) ---\n始终开启,用于业务记录和前端查询。\nADD-4 三通道输出:\n 1. console.log — record${featureCap}Audit\n 2. fs.appendFile — (AuditLog 表替代文件写入)\n 3. DB AuditLog 表 — prisma.auditLog.create\n包含函数: record${featureCap}Audit()\n\n新建业务域必须使用三层分离模式(而非历史混合式)。详见项目规则 ADD-4「三层可插拔架构」。`)
267
+ } catch (error) { return errorResponse(`获取审计日志器失败: ${error instanceof Error ? error.message : String(error)}`) }
268
+ }
269
+ )
270
+
271
+ // ===== get_add_template (L1718-1780) =====
272
+ server.registerTool(
273
+ "get_add_template",
274
+ {
275
+ description: "获取 ADD 工作流的文档模板(ADD-0.1 广义文档先行)。ADD 范式要求每次生成文档产物前必须先读取对应模板,禁止凭记忆生成——模板可能在迭代中已更新。\n\n可用模板(12 个):\n- plan-template.md — 需求方案\n- add-route-template.md — Plan→ADD 九阶段执行映射(轻量模式,适合前端/小项目)\n- add-route-template-heavyweight.md — Plan→ADD 九阶段执行映射(重型模式,适合后端/管线/合规场景,强制 check_spec_sync 文档-代码交叉校验)\n- spec-template.md — 功能规格\n- tasks-template.md — 任务拆分\n- checklist-template.md — 验收清单\n- review-template.md — 方案审查(ADD-9)\n- review-implementation-template.md — 实现审查(ADD-10)\n- review-runtime-template.md — 运行时纠偏(ADD-11)\n- handoff-template.md — 交接总览索引\n- handoff-single-round-template.md — 单轮交接(9 章节)\n- handoff-multi-round-template.md — 多轮交接(13 子章节/轮)\n\n本项目默认使用重型 add-route 模板。",
276
+ inputSchema: z.object({ template: z.string().describe("模板名称(不含 .md 后缀),如 'plan-template', 'add-route-template', 'spec-template', 'review-template'。传 'list' 获取所有模板列表。") }),
277
+ },
278
+ async (args: { template: string }) => {
279
+ try {
280
+ const { template } = args; if (!template) return errorResponse("template 参数不能为空")
281
+ const templatesDir = join(PROJECT_ROOT, MAGIC_DIR, "templates")
282
+ if (template === "list") {
283
+ const entries = await readdir(templatesDir); const mdFiles = entries.filter(f => f.endsWith(".md"))
284
+ const parts = [`=== ADD 模板列表(${mdFiles.length} 个) ===`, ""]
285
+ for (const f of mdFiles) { const content = await readFileSafe(join(templatesDir, f)); const firstLine = content?.split("\n")[0] || ""; const title = firstLine.replace(/^#+\s*/, "").trim() || f; parts.push(` ${f.replace(".md", "")} — ${title}`) }
286
+ parts.push("", '用法: get_add_template({ template: "plan-template" })'); return textResponse(parts.join("\n"))
287
+ }
288
+ const fileName = template.endsWith(".md") ? template : `${template}.md`
289
+ const filePath = join(templatesDir, fileName)
290
+ if (!existsSync(filePath)) return errorResponse(`未找到模板: ${fileName}\n可用模板请调用: get_add_template({ template: "list" })`)
291
+ const content = await readFile(filePath, "utf-8")
292
+ return textResponse([`=== ADD 模板: ${fileName} ===`, `路径: ${MAGIC_DIR}/templates/${fileName}`, "", content, "", "=== 使用提示 ===", "1. 复制模板到目标路径,替换 {占位符} 为实际内容", "2. 禁止凭记忆生成——模板可能在迭代中已更新", "3. 生成文档后调用 record_dev_operation 记录(targetType 视产物类型而定)"].join("\n"))
293
+ } catch (error) { return errorResponse(`获取 ADD 模板失败: ${error instanceof Error ? error.message : String(error)}`) }
294
+ }
295
+ )
296
+
297
+ // ===== get_spec_context (L1782-1873) =====
298
+ server.registerTool(
299
+ "get_spec_context",
300
+ {
301
+ description: `获取 ${MAGIC_DIR}/specs/ 下指定任务的 specs 三元组上下文(spec.md + tasks.md + checklist.md)。ADD 工作流中每个原子事务对应一个 specs 三元组目录,形成「需求→执行→验收」闭环。\n\nAI 助手在 ADD 范式 Step 3 执行业务逻辑时,应调用此工具获取当前任务的 spec/tasks/checklist 上下文,确保实现与规格一致。`,
302
+ inputSchema: z.object({
303
+ task: z.string().describe(`任务名(即 ${MAGIC_DIR}/specs/ 下的目录名),如 'co-agent-response-strategy'。传 'list' 获取所有任务列表。`),
304
+ file: z.string().optional().describe("指定读取三元组中的某个文件: 'spec', 'tasks', 'checklist'。不传则返回全部三个文件。"),
305
+ }),
306
+ },
307
+ async (args: { task: string; file?: string }) => {
308
+ try {
309
+ const { task, file } = args; if (!task) return errorResponse("task 参数不能为空")
310
+ const specsDir = join(PROJECT_ROOT, MAGIC_DIR, "specs")
311
+ if (task === "list") {
312
+ const entries = await readdir(specsDir, { withFileTypes: true }); const dirs = entries.filter(e => e.isDirectory()).map(e => e.name)
313
+ const parts = [`=== Specs 任务列表(${dirs.length} 个) ===`, ""]
314
+ for (const dir of dirs) {
315
+ const specPath = join(specsDir, dir, "spec.md"); const content = await readFileSafe(specPath)
316
+ const firstLine = content?.split("\n")[0] || ""; const title = firstLine.replace(/^#+\s*/, "").trim() || dir
317
+ const hasTasks = existsSync(join(specsDir, dir, "tasks.md")); const hasChecklist = existsSync(join(specsDir, dir, "checklist.md"))
318
+ parts.push(` ${(hasTasks && hasChecklist) ? "✅" : "⚠️ 不完整"} ${dir} — ${title}`)
319
+ }
320
+ parts.push("", '用法: get_spec_context({ task: "co-agent-response-strategy" })'); return textResponse(parts.join("\n"))
321
+ }
322
+ const taskDir = join(specsDir, task)
323
+ if (!existsSync(taskDir)) return errorResponse(`未找到 specs 任务: ${task}\n可用任务请调用: get_spec_context({ task: "list" })`)
324
+ const trinity: Record<string, string> = { spec: "spec.md", tasks: "tasks.md", checklist: "checklist.md" }
325
+ const parts = [`=== Specs 三元组: ${task} ===`, `路径: ${MAGIC_DIR}/specs/${task}/`, ""]
326
+ if (file && trinity[file]) {
327
+ const fp = join(taskDir, trinity[file]); const content = await readFileSafe(fp)
328
+ if (content) { parts.push(`=== ${trinity[file]} ===`); parts.push(content) } else { parts.push(`⚠️ ${trinity[file]} 不存在`) }
329
+ } else {
330
+ for (const [_key, fn] of Object.entries(trinity)) { const fp = join(taskDir, fn); const content = await readFileSafe(fp); if (content) { parts.push(`=== ${fn} ===`); parts.push(content); parts.push("") } else { parts.push(`⚠️ ${fn} 不存在(三元组不完整)`); parts.push("") } }
331
+ }
332
+ return textResponse(parts.join("\n"))
333
+ } catch (error) { return errorResponse(`获取 Spec 上下文失败: ${error instanceof Error ? error.message : String(error)}`) }
334
+ }
335
+ )
336
+
337
+ }
@@ -0,0 +1,42 @@
1
+ import * as z from "zod/v4"
2
+ import type { McpServer } from "@modelcontextprotocol/server"
3
+ import { readdir } from "fs/promises"
4
+ import { existsSync } from "fs"
5
+ import { join } from "path"
6
+ import { textResponse, errorResponse } from "../shared/response.js"
7
+ import { readFileSafe } from "../shared/fs.js"
8
+ import { PROJECT_ROOT, MAGIC_DIR } from "../shared/fs.js"
9
+
10
+ export function registerDocsTools(server: McpServer) {
11
+
12
+ server.registerTool("find_related_docs", {
13
+ description: `搜索与当前变更相关的项目文档(ADD-0.1 广义文档先行)。搜索范围:\n1. docs/ 目录 — 需求文档、架构文档、规范文档\n2. ${MAGIC_DIR}/ 产物 — plans/(Plan+add-route+handoff)、specs/(三元组)、reviews/(方案审查+实现审查+运行时审查)\n\nAI 助手在 ADD 范式 Step 0 中应调用此工具查找需要更新的 docs/ 文档。`,
14
+ inputSchema: z.object({
15
+ query: z.string().describe("搜索关键词"),
16
+ category: z.string().optional().describe("文档类别过滤: 'requirement', 'architecture', 'standard', 'plan', 'add-route', 'spec', 'review', 'handoff'"),
17
+ }),
18
+ }, async (args, _ctx) => {
19
+ try {
20
+ const { query, category } = args
21
+ const docsDir = join(PROJECT_ROOT, "docs")
22
+ const catPrefixes: Record<string, string[]> = { requirement: ["00-需求"], architecture: ["01-架构","02-架构"], standard: ["02-规范","03-规范"] }
23
+ const magicCat: Record<string, string> = { plans: "plan", specs: "spec", reviews: "review" }
24
+ const allFiles: Array<{ path: string; relativePath: string; category: string }> = []
25
+ const walk = async (dir: string, rel: string, src: "docs" | "magic" = "docs") => {
26
+ try { for (const e of await readdir(dir, { withFileTypes: true })) { const fp = join(dir, e.name); const rp = rel ? `${rel}/${e.name}` : e.name; if (e.isDirectory()) await walk(fp, rp, src); else if (e.name.endsWith(".md") || e.name.endsWith(".html")) { let c = "unknown"; if (src === "magic") { const td = rp.split("/")[0]; c = magicCat[td] || "unknown"; if (td === "plans" && e.name.includes("handoff")) c = "handoff"; if (td === "plans" && e.name.includes("add-route")) c = "add-route" } else { for (const [cat, pf] of Object.entries(catPrefixes)) if (pf.some(p => rp.includes(p))) { c = cat; break } }; allFiles.push({ path: fp, relativePath: rp, category: c }) } } } catch { /* fallthrough */ }
27
+ }
28
+ await walk(docsDir, "", "docs")
29
+ for (const md of ["plans","specs","reviews"]) { const p = join(PROJECT_ROOT, MAGIC_DIR, md); if (existsSync(p)) await walk(p, md, "magic") }
30
+ let filtered = allFiles
31
+ if (category) { const ac: Record<string, string[]> = { ...catPrefixes, plan: ["plan"], "add-route": ["add-route"], spec: ["spec"], review: ["review"], handoff: ["handoff"] }; if (ac[category]) filtered = allFiles.filter(f => f.category === category) }
32
+ const ql = query.toLowerCase(); const matches: Array<{ path: string; relativePath: string; category: string; title: string; relevance: number }> = []
33
+ for (const d of filtered) { let r = 0; if (d.relativePath.toLowerCase().includes(ql)) r += 3; let t = ""; try { const c = await readFileSafe(d.path); if (c) { const m = c.match(/^#\s+(.+)/m); t = m ? m[1].trim() : c.split("\n")[0].replace(/^#+\s*/,"").replace(/[#*]/g,"").trim(); if (c.toLowerCase().includes(ql)) r += 2 } } catch { /* fallthrough */ }; if (r > 0) matches.push({ ...d, title: t || d.relativePath.split("/").pop()!, relevance: r }) }
34
+ matches.sort((a,b) => b.relevance - a.relevance)
35
+ const p: string[] = [`=== 项目文档搜索: "${query}" ===`, `匹配文档数: ${matches.length}`, ""]
36
+ if (!matches.length) { p.push("未找到匹配的文档。"); p.push("=== 可用文档列表 ==="); for (const d of allFiles) p.push(` [${d.category}] ${d.relativePath}`) }
37
+ else { for (const d of matches) p.push(` [相关度 ${d.relevance}] ${d.relativePath}\n 标题: ${d.title}\n`) }
38
+ return textResponse(p.join("\n"))
39
+ } catch (error) { return errorResponse(`搜索文档失败: ${error instanceof Error ? error.message : String(error)}`) }
40
+ })
41
+
42
+ }