add-coder 0.3.25-0 → 0.3.25
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.
- package/README.en.md +10 -10
- package/README.md +20 -46
- package/dist/index.js +6 -69
- package/package.json +1 -1
- package/templates/.add-coder-src-hash.json +18 -22
- package/templates/core/scripts/db-ensure.sh +0 -8
- package/templates/core/scripts/mcp-server/shared/db-types.ts +208 -0
- package/templates/core/scripts/mcp-server/shared/response.ts +2 -3
- package/templates/core/scripts/mcp-server/tools/audit.ts +10 -7
- package/templates/core/scripts/mcp-server/tools/context.ts +19 -0
- package/templates/core/scripts/mcp-server/tools/contract.ts +6 -4
- package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +2 -69
- package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +1 -1
- package/templates/core/scripts/mcp-server/tools/hitl.ts +116 -34
- package/templates/core/scripts/mcp-server/tools/index.ts +1 -66
- package/templates/core/scripts/mcp-server/tools/plan.ts +33 -16
- package/templates/core/scripts/mcp-server/tools/review.ts +12 -9
- package/templates/core/scripts/mcp-server.ts +1 -2
- package/templates/core/templates/simple-plan-template.md +13 -0
- package/templates/core/templates/simple-plan-template.schema.json +10 -0
- package/templates/core/templates/standard-plan-template.md +13 -0
- package/templates/core/templates/standard-plan-template.schema.json +10 -0
- package/templates/adapters/codex/config.toml.example +0 -20
- package/templates/core/plans/2026-07/08/add-coder-npm-package-add-route-v1.md +0 -331
- package/templates/core/plans/2026-07/08/add-coder-npm-package-handoff-v1.md +0 -686
- package/templates/core/plans/2026-07/08/add-coder-npm-package-plan-v1.md +0 -895
- package/templates/core/scripts/mcp-server/shared/redact.ts +0 -28
|
@@ -3,9 +3,15 @@ import type { ToolRegistrar } from "./registrar.js"
|
|
|
3
3
|
import { textResponse, errorResponse } from "../shared/response.js"
|
|
4
4
|
import { prisma } from "../shared/prisma.js"
|
|
5
5
|
import { PROJECT_ID, PROJECT_ROOT } from "../shared/env.js"
|
|
6
|
+
import type { AddUserRow, DevOperationRow } from "../shared/db-types.js"
|
|
7
|
+
import { AddUserRowSchema, DevOperationRowSchema, validatedDelegate } from "../shared/db-types.js"
|
|
6
8
|
|
|
7
9
|
export function registerAuditTools(server: ToolRegistrar) {
|
|
8
10
|
|
|
11
|
+
// 无类型边界单点(zod 托管):动态 client → 运行期校验的泛型委托
|
|
12
|
+
const devDb = validatedDelegate<DevOperationRow>(prisma.devOperation, DevOperationRowSchema, "DevOperation")
|
|
13
|
+
const userDb = validatedDelegate<AddUserRow>(prisma.addUser, AddUserRowSchema, "AddUser")
|
|
14
|
+
|
|
9
15
|
// args 窄化辅助:MCP 工具参数可能是 string | number | undefined,统一转 string
|
|
10
16
|
const s = (v: string | number | undefined): string => (typeof v === "number" ? String(v) : (v ?? ""))
|
|
11
17
|
|
|
@@ -29,8 +35,7 @@ export function registerAuditTools(server: ToolRegistrar) {
|
|
|
29
35
|
if (targetType) where.targetType = s(targetType); if (action) where.action = s(action); if (targetId) where.targetId = s(targetId); if (planKeyword) where.planKeyword = s(planKeyword)
|
|
30
36
|
const kw = s(keyword)
|
|
31
37
|
if (kw) where.OR = [{ action: { contains: kw, mode: "insensitive" } },{ targetType: { contains: kw, mode: "insensitive" } },{ targetId: { contains: kw, mode: "insensitive" } },{ reason: { contains: kw, mode: "insensitive" } },{ planKeyword: { contains: kw, mode: "insensitive" } }]
|
|
32
|
-
|
|
33
|
-
const logs = await (prisma.devOperation as Record<string, (...a: unknown[]) => unknown>).findMany({ where, orderBy: { createdAt: "desc" }, take: Math.min(Number(limit) || 20, 100), include: { user: { select: { username: true } } } }) as unknown as AuditLogRow[]
|
|
38
|
+
const logs = await devDb.findMany({ where, orderBy: { createdAt: "desc" }, take: Math.min(Number(limit) || 20, 100), include: { user: { select: { username: true } } } })
|
|
34
39
|
if (logs.length === 0) {
|
|
35
40
|
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}`)
|
|
36
41
|
return textResponse(`=== 开发操作审计日志 ===\n\n未找到匹配的审计记录(条件: ${f.length > 0 ? f.join(", ") : "无条件"})。\n\n可能原因: 数据库未运行、尚无相关开发操作记录、或 filter 过于严格。`)
|
|
@@ -67,11 +72,9 @@ export function registerAuditTools(server: ToolRegistrar) {
|
|
|
67
72
|
if (tId && (tId.startsWith("/") || /^[A-Z]:\\/.test(tId))) pathWarnings.push(`⚠️ targetId 使用了绝对路径: "${tId}"。请使用相对路径。`)
|
|
68
73
|
let parsedBefore: unknown, parsedAfter: unknown
|
|
69
74
|
try { if (beforeState) parsedBefore = JSON.parse(s(beforeState)); if (afterState) parsedAfter = JSON.parse(s(afterState)) } catch { return errorResponse("beforeState/afterState 必须是有效的 JSON 字符串。") }
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
type DevLogRow = { id: string; createdAt: Date }
|
|
74
|
-
const log = await (prisma.devOperation as Record<string, (...a: unknown[]) => unknown>).create({ data: { userId: systemUser.id, planKeyword: s(planKeyword) || "unknown", action: s(action), targetType: s(targetType), targetId: tId || "unknown", beforeState: parsedBefore ?? null, afterState: parsedAfter ?? null, reason: s(reason) || null } }) as unknown as DevLogRow
|
|
75
|
+
let systemUser = await userDb.findUnique({ where: { username: "ai-assistant" }, select: { id: true } })
|
|
76
|
+
if (!systemUser) systemUser = await userDb.create({ data: { id: "ai-assistant", username: "ai-assistant", email: "ai-assistant@internal" } })
|
|
77
|
+
const log = await devDb.create({ data: { userId: systemUser.id, planKeyword: s(planKeyword) || "unknown", action: s(action), targetType: s(targetType), targetId: tId || "unknown", beforeState: parsedBefore ?? null, afterState: parsedAfter ?? null, reason: s(reason) || null } })
|
|
75
78
|
const lines = [`✅ 开发操作已记录`, ` 落库项目: ${PROJECT_ID} (${PROJECT_ROOT})`, ` ID: ${log.id}`, ` action: ${s(action)}`, ` targetType: ${s(targetType)}`, ` targetId: ${tId || "unknown"}`, ` planKeyword: ${s(planKeyword) || "unknown"}`, ` createdAt: ${log.createdAt.toISOString()}`]
|
|
76
79
|
if (pathWarnings.length > 0) { lines.push(""); lines.push(...pathWarnings) }
|
|
77
80
|
lines.push("", `📋 落库回查(必须执行):`, tId ? ` query_audit_logs({ targetId: "${tId}" }) — 确认本条记录已写入` : "")
|
|
@@ -8,6 +8,9 @@ import { join, relative } from "path"
|
|
|
8
8
|
import { textResponse, errorResponse } from "../shared/response.js"
|
|
9
9
|
import { readFileSafe, readdirRecursive } from "../shared/fs.js"
|
|
10
10
|
import { PROJECT_ROOT, MAGIC_DIR } from "../shared/fs.js"
|
|
11
|
+
import { prisma } from "../shared/prisma.js"
|
|
12
|
+
import type { PlanRow } from "../shared/db-types.js"
|
|
13
|
+
import { PlanRowSchema, validatedDelegate } from "../shared/db-types.js"
|
|
11
14
|
|
|
12
15
|
export function registerContextTools(server: ToolRegistrar) {
|
|
13
16
|
|
|
@@ -164,6 +167,22 @@ export function registerContextTools(server: ToolRegistrar) {
|
|
|
164
167
|
} else { parts.push("Specs: ❌ 缺失") }
|
|
165
168
|
}
|
|
166
169
|
|
|
170
|
+
parts.push(""); parts.push("=== 悬空 Plan 对账(记录在、文件缺失) ===")
|
|
171
|
+
// 孤儿可见性:create_hitl 预置行被中断 / 放弃未清理 → 开局即提示,不静默累积
|
|
172
|
+
try {
|
|
173
|
+
// 无类型边界单点(zod 托管):动态 client → 运行期校验的泛型委托
|
|
174
|
+
const planDb = validatedDelegate<PlanRow>(prisma.planRecord, PlanRowSchema, "PlanRecord")
|
|
175
|
+
const allPlans = await planDb.findMany({ orderBy: { createdAt: "desc" } })
|
|
176
|
+
const orphans = allPlans.filter(p => !existsSync(p.planPath))
|
|
177
|
+
if (orphans.length > 0) {
|
|
178
|
+
const cutoff = Date.now() - 14 * 86400000
|
|
179
|
+
for (const o of orphans) {
|
|
180
|
+
const stale = o.createdAt.getTime() < cutoff
|
|
181
|
+
parts.push(`${stale ? "🔴" : "·"} ${o.planName} → ${o.planPath}${stale ? "(超 14 天,建议清理)" : ""}`)
|
|
182
|
+
}
|
|
183
|
+
} else { parts.push("✅ 无悬空记录") }
|
|
184
|
+
} catch { parts.push("⚠️ 悬空对账跳过(数据库不可达)") }
|
|
185
|
+
|
|
167
186
|
parts.push(""); parts.push("=== 待执行 ADD 操作(按流程顺序) ===")
|
|
168
187
|
const todoItems: string[] = []
|
|
169
188
|
if (!activePlan) { todoItems.push("1. [未开始] 用户提出需求 → 生成 Plan") }
|
|
@@ -5,10 +5,12 @@ import { join, basename } from "path"
|
|
|
5
5
|
import { textResponse, errorResponse } from "../shared/response.js"
|
|
6
6
|
import { readdirRecursive, PROJECT_ROOT, MAGIC_DIR } from "../shared/fs.js"
|
|
7
7
|
import { prisma } from "../shared/prisma.js"
|
|
8
|
+
import type { CollabContractRow, PlanRow } from "../shared/db-types.js"
|
|
9
|
+
import { CollabContractRowSchema, PlanRowSchema, validatedDelegate } from "../shared/db-types.js"
|
|
8
10
|
|
|
9
11
|
const db = {
|
|
10
|
-
get collabContract() { return prisma.collabContract
|
|
11
|
-
get plan() { return prisma.planRecord
|
|
12
|
+
get collabContract() { return validatedDelegate<CollabContractRow>(prisma.collabContract, CollabContractRowSchema, "CollabContract") },
|
|
13
|
+
get plan() { return validatedDelegate<PlanRow>(prisma.planRecord, PlanRowSchema, "PlanRecord") },
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
/** 契约文档结构化解析结果 */
|
|
@@ -122,7 +124,7 @@ export function registerContractTools(server: ToolRegistrar) {
|
|
|
122
124
|
results.push(`⚠️ ${name} 缺少「总控 Plan:」声明,跳过`)
|
|
123
125
|
continue
|
|
124
126
|
}
|
|
125
|
-
const existing = await db.collabContract.findFirst({ where: { contractName: name } })
|
|
127
|
+
const existing = await db.collabContract.findFirst({ where: { contractName: name } })
|
|
126
128
|
const version = (existing?.version as number ?? 0) + 1
|
|
127
129
|
|
|
128
130
|
const data = {
|
|
@@ -171,7 +173,7 @@ export function registerContractTools(server: ToolRegistrar) {
|
|
|
171
173
|
}, async (args: Record<string, unknown>) => {
|
|
172
174
|
try {
|
|
173
175
|
const { contractName } = args as { contractName: string }
|
|
174
|
-
const contract = await db.collabContract.findFirst({ where: { contractName } })
|
|
176
|
+
const contract = await db.collabContract.findFirst({ where: { contractName } })
|
|
175
177
|
if (!contract) {
|
|
176
178
|
return textResponse(`📋 契约: 未跟踪\ncontractName: ${contractName}\n\n操作: contract_track({ contractName: "${contractName}" })`)
|
|
177
179
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import * as z from "zod/v4";
|
|
10
10
|
import type { ToolRegistrar } from "../registrar.js";
|
|
11
11
|
import { existsSync } from "fs";
|
|
12
|
-
import { join
|
|
12
|
+
import { join } from "path";
|
|
13
13
|
import { textResponse, errorResponse } from "../../shared/response.js";
|
|
14
14
|
import {
|
|
15
15
|
readFileSafe,
|
|
@@ -42,9 +42,7 @@ export function registerCheckRahs(server: ToolRegistrar) {
|
|
|
42
42
|
);
|
|
43
43
|
const pm = apf.find(
|
|
44
44
|
(f) =>
|
|
45
|
-
f.toLowerCase().includes(pp.toLowerCase()) &&
|
|
46
|
-
f.includes("-plan-v") &&
|
|
47
|
-
!f.includes(".hitl"),
|
|
45
|
+
f.toLowerCase().includes(pp.toLowerCase()) && f.includes("-plan-v"),
|
|
48
46
|
);
|
|
49
47
|
if (!pm)
|
|
50
48
|
return errorResponse(`未找到匹配的 Plan 文件(关键词: ${pp})`);
|
|
@@ -82,71 +80,6 @@ export function registerCheckRahs(server: ToolRegistrar) {
|
|
|
82
80
|
})) as Array<{ id: string }>;
|
|
83
81
|
auditScore = Math.min(100, logs.length * 10);
|
|
84
82
|
} catch {}
|
|
85
|
-
// ── 范围保真度 / Spec 合规 / 阶段对称性:从静态基线改为动态计算(修复:
|
|
86
|
-
// 三个维度原为固定 80,任何 Plan 的 RAHS 上限 88 永远无法通过)──
|
|
87
|
-
try {
|
|
88
|
-
const arFile = apf.find(
|
|
89
|
-
(f) =>
|
|
90
|
-
f.toLowerCase().includes(pp.toLowerCase()) &&
|
|
91
|
-
f.toLowerCase().includes("add-route"),
|
|
92
|
-
);
|
|
93
|
-
if (arFile) {
|
|
94
|
-
const arContent =
|
|
95
|
-
(await readFileSafe(join(plansDir, arFile))) || "";
|
|
96
|
-
const checks = arContent.match(/^- \[[ x]\]/gm) || [];
|
|
97
|
-
const done = checks.filter((c) => c.includes("[x]")).length;
|
|
98
|
-
// 阶段对称性:add-route Step 产出项勾选率(Handoff/Report Closure 等
|
|
99
|
-
// 条件项未勾选时按比例折算,不阻塞)
|
|
100
|
-
symScore = checks.length > 0
|
|
101
|
-
? Math.max(70, Math.round((done / checks.length) * 100))
|
|
102
|
-
: 70;
|
|
103
|
-
}
|
|
104
|
-
} catch {}
|
|
105
|
-
try {
|
|
106
|
-
// Spec 合规:checklist [T] 项勾选率([R] 运行时项不计入)
|
|
107
|
-
const specDir = join(PROJECT_ROOT, MAGIC_DIR, "specs");
|
|
108
|
-
const sn = basename(pm, ".md").replace(/-plan-v\d+$/i, "");
|
|
109
|
-
const clPath = join(specDir, sn, "checklist.md");
|
|
110
|
-
if (existsSync(clPath)) {
|
|
111
|
-
const cl = (await readFileSafe(clPath)) || "";
|
|
112
|
-
const tItems = cl.match(/^- \[[ x]\] \[T\]/gm) || [];
|
|
113
|
-
const tDone = tItems.filter((c) => c.includes("[x]")).length;
|
|
114
|
-
specScore = tItems.length > 0
|
|
115
|
-
? Math.max(70, Math.round((tDone / tItems.length) * 100))
|
|
116
|
-
: 70;
|
|
117
|
-
}
|
|
118
|
-
} catch {}
|
|
119
|
-
try {
|
|
120
|
-
// 范围保真度:git diff 变更文件与 add-route 附录清单匹配率
|
|
121
|
-
const arFile = apf.find(
|
|
122
|
-
(f) =>
|
|
123
|
-
f.toLowerCase().includes(pp.toLowerCase()) &&
|
|
124
|
-
f.toLowerCase().includes("add-route"),
|
|
125
|
-
);
|
|
126
|
-
if (arFile) {
|
|
127
|
-
const arContent =
|
|
128
|
-
(await readFileSafe(join(plansDir, arFile))) || "";
|
|
129
|
-
const appendix = (
|
|
130
|
-
arContent.match(/`[^`]+\.(ts|js|sh|md|tsx|json|yml|yaml)`/g) ||
|
|
131
|
-
[]
|
|
132
|
-
).map((f: string) => f.replace(/`/g, "").toLowerCase());
|
|
133
|
-
const diff = runCommand("git", ["diff", "--name-only"], {
|
|
134
|
-
cwd: PROJECT_ROOT,
|
|
135
|
-
timeout: 5000,
|
|
136
|
-
});
|
|
137
|
-
const diffFiles = diff.stdout
|
|
138
|
-
.trim()
|
|
139
|
-
.split("\n")
|
|
140
|
-
.filter(Boolean)
|
|
141
|
-
.map((f: string) => f.toLowerCase());
|
|
142
|
-
if (appendix.length > 0 && diffFiles.length > 0) {
|
|
143
|
-
const matched = diffFiles.filter((f) =>
|
|
144
|
-
appendix.some((a) => f === a || f.endsWith(`/${a}`)),
|
|
145
|
-
).length;
|
|
146
|
-
scopeScore = Math.max(70, Math.round((matched / diffFiles.length) * 100));
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
} catch {}
|
|
150
83
|
const parts = [
|
|
151
84
|
"=== RAHS:Runtime Architecture Health Score ===",
|
|
152
85
|
`Plan 关键词: "${pp}"`,
|
|
@@ -69,7 +69,7 @@ export function registerCheckSpecSync(server: ToolRegistrar) {
|
|
|
69
69
|
lines.push(`add-route: ${arFile}`);
|
|
70
70
|
const arContent = (await readFileSafe(join(plansDir, arFile))) || "";
|
|
71
71
|
// 提取 add-route 附录文件清单
|
|
72
|
-
const appendixFiles = (arContent.match(/`[^`]+\.(ts|js|sh|md|tsx|json
|
|
72
|
+
const appendixFiles = (arContent.match(/`[^`]+\.(ts|js|sh|md|tsx|json)`/g) || [])
|
|
73
73
|
.map((f: string) => f.replace(/`/g, ""));
|
|
74
74
|
lines.push(`附录文件: ${appendixFiles.length} 个`);
|
|
75
75
|
|
|
@@ -2,14 +2,18 @@ import * as z from "zod/v4"
|
|
|
2
2
|
import { inputRequired, acceptedContent } from "@modelcontextprotocol/server"
|
|
3
3
|
import type { ToolRegistrar } from "./registrar.js"
|
|
4
4
|
import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs"
|
|
5
|
-
import { join, basename } from "path"
|
|
5
|
+
import { join, basename, relative } from "path"
|
|
6
6
|
import { textResponse, errorResponse } from "../shared/response.js"
|
|
7
7
|
import { PROJECT_ROOT, MAGIC_DIR } from "../shared/fs.js"
|
|
8
8
|
import { prisma } from "../shared/prisma.js"
|
|
9
9
|
import { HITL_INTERACTION_CONFIG } from "../shared/hitl-interaction.strategy.js"
|
|
10
|
+
import type { HitlRow, PlanRow } from "../shared/db-types.js"
|
|
11
|
+
import { HitlRowSchema, PlanRowSchema, validatedDelegate } from "../shared/db-types.js"
|
|
10
12
|
|
|
13
|
+
// 无类型边界单点(zod 托管):动态加载的 prisma client 在此一次性转为运行期校验的泛型委托
|
|
11
14
|
const db = {
|
|
12
|
-
get hitl() { return prisma.hitlRecord
|
|
15
|
+
get hitl() { return validatedDelegate<HitlRow>(prisma.hitlRecord, HitlRowSchema, "HitlRecord") },
|
|
16
|
+
get plan() { return validatedDelegate<PlanRow>(prisma.planRecord, PlanRowSchema, "PlanRecord") },
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
export function registerHitlTools(server: ToolRegistrar) {
|
|
@@ -22,18 +26,32 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
22
26
|
|
|
23
27
|
// genui 模式客户端(如 Qoder)未声明 elicitation capability,弹框必然失败 → 返回 genui 流程引导
|
|
24
28
|
function _genuiGuide(recallStep: string): string {
|
|
29
|
+
// D2(Review 回流):widget 模板多候选探测——sync 实际落脚含 core/ 层级,
|
|
30
|
+
// toml 原值为源仓路径(消费方不存在),逐候选回退,全 miss 显式告警。
|
|
31
|
+
// 输出必须为 workspace 相对路径:genui show_widget 实测拒绝绝对路径(2026-08-11 验证)。
|
|
25
32
|
let widgetPath = ""
|
|
26
33
|
if (_interaction.widget_path) {
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
34
|
+
const base = basename(_interaction.widget_path)
|
|
35
|
+
const candidates = [
|
|
36
|
+
join(PROJECT_ROOT, MAGIC_DIR, "templates", "core", "templates", base),
|
|
37
|
+
join(PROJECT_ROOT, MAGIC_DIR, "templates", base),
|
|
38
|
+
join(PROJECT_ROOT, _interaction.widget_path),
|
|
39
|
+
]
|
|
40
|
+
const abs = candidates.find((p) => existsSync(p)) ?? ""
|
|
41
|
+
widgetPath = abs ? relative(PROJECT_ROOT, abs) : ""
|
|
30
42
|
}
|
|
31
|
-
|
|
43
|
+
const lines = [
|
|
32
44
|
`⚠️ 当前环境(${MAGIC_DIR})客户端不支持 elicitation 弹框,交互模式已裁决为 genui。请按以下流程完成:`,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
`
|
|
36
|
-
|
|
45
|
+
]
|
|
46
|
+
if (widgetPath) {
|
|
47
|
+
lines.push(`1. 调用 genui show_widget 渲染审批表单(widget_path: ${widgetPath},workspace 相对路径,维度列表经 data 注入)`)
|
|
48
|
+
} else {
|
|
49
|
+
lines.push(`1. ⚠️ widget 模板缺失(hitl-approval-widget.html 未同步到 ${MAGIC_DIR}/templates/),请先执行 add-coder sync 补齐,再走 genui show_widget 流程`)
|
|
50
|
+
lines.push(` - 兜底可用 inline widget_code 渲染;数据契约: window.__WIDGET_DATA__ = { planName, type, dimensions: [{name, content}] }`)
|
|
51
|
+
}
|
|
52
|
+
lines.push(`2. 用户在 widget 中逐项拍板`)
|
|
53
|
+
lines.push(`3. ${recallStep}`)
|
|
54
|
+
return lines.join("\n")
|
|
37
55
|
}
|
|
38
56
|
|
|
39
57
|
// ═══════════════ 辅助:解析 inputRequired 响应 ═══════════════
|
|
@@ -115,6 +133,19 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
115
133
|
try {
|
|
116
134
|
const { planName, type, dimensions, _fallback, _use_genui } = args as { planName: string; type: string; dimensions?: { name: string; content?: string }[]; _fallback?: boolean; _use_genui?: boolean }
|
|
117
135
|
|
|
136
|
+
// ── planName 入口强校验(弱模型友好:不合规返回可照抄修正调用,而非让错误漂移) ──
|
|
137
|
+
const _pnValid = /-(plan|collab-contract)-v\d+$/.test(planName)
|
|
138
|
+
|| /-review(-v\d+|-implementation(-v\d+)?|-runtime(-v\d+)?)?$/.test(planName)
|
|
139
|
+
|| planName.endsWith(".hitl")
|
|
140
|
+
if (!_pnValid) {
|
|
141
|
+
const suggested = type === "COLLAB_CONTRACT" ? `${planName}-collab-contract-v1` : `${planName}-plan-v1`
|
|
142
|
+
return errorResponse(
|
|
143
|
+
`planName 格式不合规: "${planName}"\n` +
|
|
144
|
+
`要求: 以 -plan-v{n} / -collab-contract-v{n} / -review-* 结尾(哨兵命名依赖此后缀剥离)\n` +
|
|
145
|
+
`可照抄修正: create_hitl({ planName: "${suggested}", type: "${type}" })`
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
118
149
|
// ── 最终维度内容(从弹框结果合并) ──
|
|
119
150
|
let finalDims: { name: string; content: string }[] = []
|
|
120
151
|
|
|
@@ -210,21 +241,44 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
210
241
|
}
|
|
211
242
|
|
|
212
243
|
// ── 原始业务逻辑(创建 DB + 生成 hitl.md。降级模式也走此路) ──
|
|
244
|
+
const now = new Date()
|
|
245
|
+
const yyyyMM = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,"0")}`
|
|
246
|
+
const dd = String(now.getDate()).padStart(2,"0")
|
|
247
|
+
const plansDir = join(PROJECT_ROOT, MAGIC_DIR, "plans", yyyyMM, dd)
|
|
248
|
+
const proposalPath = join(plansDir, `${planName}.hitl.md`)
|
|
249
|
+
|
|
250
|
+
// ── FK 断环修复:HitlRecord.planName 外键要求 PlanRecord 先行。
|
|
251
|
+
// 全新 Plan 首次审批时自动预置占位行(约定式 planPath,文件存在性即指纹),
|
|
252
|
+
// Plan 文件写出后由 plan_track 回刷真实路径——机器占位机器回刷,不消耗 LLM 认知。
|
|
253
|
+
// D5(Review 回流):占位路径按 type 分流——PLAN_REVIEW 指向 reviews/ 目录,
|
|
254
|
+
// review 文档写出后由 review_track/plan_track 回刷消解(避免永久悬空)。 ──
|
|
255
|
+
let planProvisioned = false
|
|
256
|
+
const existingPlan = await db.plan.findFirst({ where: { planName } })
|
|
257
|
+
if (!existingPlan) {
|
|
258
|
+
const placeholderPath = type === "PLAN_REVIEW"
|
|
259
|
+
? join(PROJECT_ROOT, MAGIC_DIR, "reviews", yyyyMM, dd, `${planName}.md`)
|
|
260
|
+
: join(plansDir, `${planName}.md`)
|
|
261
|
+
await db.plan.create({
|
|
262
|
+
data: {
|
|
263
|
+
planName,
|
|
264
|
+
planPath: placeholderPath,
|
|
265
|
+
planKeyword: String(planName).replace(/-(plan|collab-contract|review)-v\d+$/, ""),
|
|
266
|
+
totalTasks: 0, doneTasks: 0, checklistT: 0, checklistTDone: 0, checklistR: 0,
|
|
267
|
+
},
|
|
268
|
+
})
|
|
269
|
+
planProvisioned = true
|
|
270
|
+
}
|
|
271
|
+
|
|
213
272
|
const rows = await db.hitl.findMany({
|
|
214
273
|
where: { planName },
|
|
215
274
|
orderBy: { round: "desc" },
|
|
216
275
|
take: 1,
|
|
217
|
-
})
|
|
276
|
+
})
|
|
218
277
|
const round = (rows[0]?.round ?? 0) + 1
|
|
219
278
|
const record = await db.hitl.create({
|
|
220
|
-
data: { planName, round, type, status: "DRAFT" },
|
|
221
|
-
})
|
|
279
|
+
data: { planName, round, type: type as HitlRow["type"], status: "DRAFT" },
|
|
280
|
+
})
|
|
222
281
|
// 生成 hitl.md
|
|
223
|
-
const now = new Date()
|
|
224
|
-
const yyyyMM = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,"0")}`
|
|
225
|
-
const dd = String(now.getDate()).padStart(2,"0")
|
|
226
|
-
const plansDir = join(PROJECT_ROOT, MAGIC_DIR, "plans", yyyyMM, dd)
|
|
227
|
-
const proposalPath = join(plansDir, `${planName}.hitl.md`)
|
|
228
282
|
const isoNow = now.toISOString()
|
|
229
283
|
|
|
230
284
|
// 动态生成维度表格行
|
|
@@ -270,10 +324,18 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
270
324
|
`recordId: ${record.id}`,
|
|
271
325
|
]
|
|
272
326
|
if (finalDims.length > 0) lines.push(`dimensions: ${finalDims.length} 项`)
|
|
327
|
+
if (planProvisioned) lines.push(`PlanRecord: 自动预置(占位行,Plan 文件写出后 plan_track 回刷真实路径)`)
|
|
273
328
|
if (_fallback) lines.push(`mode: _fallback (跳过 dialog,原始代码降级)`)
|
|
274
329
|
return textResponse(lines.join("\n"))
|
|
275
330
|
} catch (e) {
|
|
276
|
-
|
|
331
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
332
|
+
if (msg.includes("Foreign key")) {
|
|
333
|
+
return errorResponse(
|
|
334
|
+
`create_hitl 外键异常(PlanRecord 自动预置可能失败): ${msg}\n` +
|
|
335
|
+
`处方: 检查数据库连接与 PlanRecord 表结构,或改用 plan_track({ planName }) 先建立追踪记录后重试`
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
return errorResponse(`create_hitl 失败: ${msg}`)
|
|
277
339
|
}
|
|
278
340
|
})
|
|
279
341
|
|
|
@@ -296,7 +358,13 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
296
358
|
}),
|
|
297
359
|
}, async (args: Record<string, unknown>, ctx: Record<string, unknown>) => {
|
|
298
360
|
try {
|
|
299
|
-
const
|
|
361
|
+
const _raw = args as Record<string, string | boolean | undefined>
|
|
362
|
+
const planName = String(_raw.planName ?? "")
|
|
363
|
+
const type = String(_raw.type ?? "")
|
|
364
|
+
const status = typeof _raw.status === "string" ? _raw.status : undefined
|
|
365
|
+
const reason = typeof _raw.reason === "string" ? _raw.reason : undefined
|
|
366
|
+
const _fallback = _raw._fallback === true
|
|
367
|
+
const _use_genui = _raw._use_genui === true
|
|
300
368
|
|
|
301
369
|
// ── 交互式确认(非降级模式且非 genui 模式) ──
|
|
302
370
|
let effectiveStatus = status as string | undefined
|
|
@@ -384,26 +452,38 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
384
452
|
// 降级模式必须传 status
|
|
385
453
|
|
|
386
454
|
// ── 原始业务逻辑(更新 DB + 写哨兵文件。降级模式也走此路) ──
|
|
387
|
-
const s = effectiveStatus
|
|
455
|
+
const s = effectiveStatus as HitlRow["status"]
|
|
456
|
+
const qType = type as HitlRow["type"]
|
|
388
457
|
const rows = await db.hitl.findMany({
|
|
389
|
-
where: { planName, type },
|
|
458
|
+
where: { planName, type: qType },
|
|
390
459
|
orderBy: { round: "desc" },
|
|
391
460
|
take: 1,
|
|
392
|
-
})
|
|
461
|
+
})
|
|
393
462
|
if (!rows.length) return errorResponse(`未找到 HITL 记录: planName=${planName}, type=${type}`)
|
|
394
463
|
const r = rows[0]
|
|
395
|
-
const prevStatus = r.status
|
|
464
|
+
const prevStatus = r.status
|
|
396
465
|
if (prevStatus === "TONGYI" || prevStatus === "BOHUI") {
|
|
397
466
|
return errorResponse(`HITL 已终态(${prevStatus}),不可再更新。BOHUI 后请用 create_hitl 新建 round。`)
|
|
398
467
|
}
|
|
399
468
|
// 更新
|
|
400
|
-
const data:
|
|
469
|
+
const data: Partial<HitlRow> = { status: s }
|
|
401
470
|
if (s === "TONGYI") data.approvedAt = new Date()
|
|
402
471
|
if (s === "BOHUI") {
|
|
403
472
|
data.rejectedAt = new Date()
|
|
404
|
-
if (reason) data.rejectReason = reason
|
|
473
|
+
if (reason) data.rejectReason = String(reason)
|
|
405
474
|
}
|
|
406
475
|
await db.hitl.update({ where: { id: r.id }, data })
|
|
476
|
+
// ── BOHUI 清算:Plan 文件不存在 → 预置 PlanRecord 无存续理由,连同本 plan 全部 HitlRecord 删除(孤儿即时归零)。
|
|
477
|
+
// 已写出文件的驳回 Plan 保留记录(历史价值)。 ──
|
|
478
|
+
let orphanCleaned = ""
|
|
479
|
+
if (s === "BOHUI") {
|
|
480
|
+
const planRow = await db.plan.findFirst({ where: { planName } })
|
|
481
|
+
if (planRow && !existsSync(planRow.planPath)) {
|
|
482
|
+
await db.hitl.deleteMany({ where: { planName } })
|
|
483
|
+
await db.plan.delete({ where: { id: planRow.id } })
|
|
484
|
+
orphanCleaned = `预置数据已清算(Plan 文件不存在: ${planRow.planPath})`
|
|
485
|
+
}
|
|
486
|
+
}
|
|
407
487
|
// 写哨兵文件(双命名:原始 planName + pre-tool-use hook §C 剥后缀推导名,保持闸门一致)
|
|
408
488
|
const markerDir = join(PROJECT_ROOT, MAGIC_DIR, "hitl")
|
|
409
489
|
mkdirSync(markerDir, { recursive: true })
|
|
@@ -440,7 +520,8 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
440
520
|
if (s === "BOHUI") {
|
|
441
521
|
lines.push(`marker: ${marker}`)
|
|
442
522
|
lines.push(`reason: ${reason || "—"}`)
|
|
443
|
-
lines.push(
|
|
523
|
+
if (orphanCleaned) lines.push(`cleanup: ${orphanCleaned}`)
|
|
524
|
+
lines.push(``, `💡 驳回后请用 create_hitl 新建 round ${r.round + 1} 重新发起审批。`)
|
|
444
525
|
}
|
|
445
526
|
return textResponse(lines.join("\n"))
|
|
446
527
|
} catch (e) {
|
|
@@ -458,11 +539,12 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
458
539
|
}, async (args: Record<string, unknown>) => {
|
|
459
540
|
try {
|
|
460
541
|
const { planName, type } = args as { planName: string; type?: string }
|
|
542
|
+
const qType = (type || "PLAN") as HitlRow["type"]
|
|
461
543
|
const rows = await db.hitl.findMany({
|
|
462
|
-
where: { planName, type },
|
|
544
|
+
where: { planName, type: qType },
|
|
463
545
|
orderBy: { round: "desc" },
|
|
464
546
|
take: 1,
|
|
465
|
-
})
|
|
547
|
+
})
|
|
466
548
|
if (!rows.length) {
|
|
467
549
|
return textResponse(
|
|
468
550
|
`📋 HITL: 未发起\n` +
|
|
@@ -472,7 +554,7 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
472
554
|
)
|
|
473
555
|
}
|
|
474
556
|
const r = rows[0]
|
|
475
|
-
const status = r.status
|
|
557
|
+
const status = r.status
|
|
476
558
|
const icons: Record<string, string> = { DRAFT: "⏳", SUBMITTED: "📤", TONGYI: "✅", BOHUI: "❌" }
|
|
477
559
|
const icon = icons[status] || "❓"
|
|
478
560
|
const lines = [
|
|
@@ -481,13 +563,13 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
481
563
|
`planName: ${planName}`,
|
|
482
564
|
`type: ${r.type}`,
|
|
483
565
|
`round: ${r.round}`,
|
|
484
|
-
r.createdAt ? `createdAt: ${
|
|
485
|
-
r.approvedAt ? `approvedAt: ${
|
|
486
|
-
r.rejectedAt ? `rejectedAt: ${
|
|
566
|
+
r.createdAt ? `createdAt: ${r.createdAt.toISOString()}` : null,
|
|
567
|
+
r.approvedAt ? `approvedAt: ${r.approvedAt.toISOString()}` : null,
|
|
568
|
+
r.rejectedAt ? `rejectedAt: ${r.rejectedAt.toISOString()}` : null,
|
|
487
569
|
r.rejectReason ? `reason: ${r.rejectReason}` : null,
|
|
488
570
|
].filter(Boolean)
|
|
489
571
|
if (status === "BOHUI") {
|
|
490
|
-
lines.push(``, `💡 驳回后可 create_hitl 新建 round ${
|
|
572
|
+
lines.push(``, `💡 驳回后可 create_hitl 新建 round ${r.round + 1}。`)
|
|
491
573
|
}
|
|
492
574
|
if (status === "DRAFT" || status === "SUBMITTED") {
|
|
493
575
|
lines.push(``, `操作: update_hitl({ planName: "${planName}", type: "${r.type}", status: "TONGYI|BOHUI" })`)
|
|
@@ -12,61 +12,6 @@ import { registerReviewTools } from "./review.js"
|
|
|
12
12
|
import { PROJECT_ID } from "../shared/env.js"
|
|
13
13
|
import type { ToolRegistrar } from "./registrar.js"
|
|
14
14
|
|
|
15
|
-
// ── 读写分级信号量节流(进程层并发契约 v2 §7:Codex Parallel MCP 真并行兑底) ──
|
|
16
|
-
// 读工具共享 8 并发;写工具共享 4 并发;超限请求排队(MCP 协议允许延迟响应,排队即天然反压)
|
|
17
|
-
const READ_MAX = 8
|
|
18
|
-
const WRITE_MAX = 4
|
|
19
|
-
|
|
20
|
-
const READ_TOOLS = new Set([
|
|
21
|
-
"get_project_context", "find_related_docs", "get_db_schema", "query_audit_logs",
|
|
22
|
-
"plan_status", "review_status", "status_hitl", "contract_status",
|
|
23
|
-
"check_dps", "check_rahs", "check_add_route_status", "check_spec_sync",
|
|
24
|
-
"check_add_route_completeness", "check_phase_symmetry", "check_failure_path",
|
|
25
|
-
"check_add_compliance", "get_hook_events",
|
|
26
|
-
])
|
|
27
|
-
|
|
28
|
-
function createSemaphore(limit: number) {
|
|
29
|
-
let active = 0
|
|
30
|
-
const queue: (() => void)[] = []
|
|
31
|
-
const acquire = () =>
|
|
32
|
-
new Promise<void>((resolve) => {
|
|
33
|
-
if (active < limit) {
|
|
34
|
-
active++
|
|
35
|
-
resolve()
|
|
36
|
-
} else {
|
|
37
|
-
queue.push(resolve)
|
|
38
|
-
}
|
|
39
|
-
})
|
|
40
|
-
const release = () => {
|
|
41
|
-
active--
|
|
42
|
-
const next = queue.shift()
|
|
43
|
-
if (next) {
|
|
44
|
-
active++
|
|
45
|
-
next()
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return { acquire, release }
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// 429 指数退避重试(3 次,250ms 起步,倍率 2x):上游限流不直接透传
|
|
52
|
-
async function withRetry<T>(fn: () => Promise<T>): Promise<T> {
|
|
53
|
-
let lastErr: unknown
|
|
54
|
-
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
55
|
-
try {
|
|
56
|
-
return await fn()
|
|
57
|
-
} catch (e) {
|
|
58
|
-
lastErr = e
|
|
59
|
-
const msg = e instanceof Error ? e.message : String(e)
|
|
60
|
-
if (!/429|rate.?limit|限流/i.test(msg)) throw e
|
|
61
|
-
await new Promise((r) => setTimeout(r, 250 * 2 ** (attempt - 1)))
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
throw lastErr
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const readSem = createSemaphore(READ_MAX)
|
|
68
|
-
const writeSem = createSemaphore(WRITE_MAX)
|
|
69
|
-
|
|
70
15
|
export function registerAllTools(server: McpServer) {
|
|
71
16
|
// D9 多 MCP 路由安全:派生装饰版 registrar,为所有工具 description 注入项目身份前缀
|
|
72
17
|
const origRegister = server.registerTool.bind(server)
|
|
@@ -75,20 +20,10 @@ export function registerAllTools(server: McpServer) {
|
|
|
75
20
|
type ToolCb = Parameters<RegisterTool>[2]
|
|
76
21
|
const registerTool = ((name: string, config: ToolConfig, cb: ToolCb) => {
|
|
77
22
|
const c = config as { description?: string }
|
|
78
|
-
// 读写分级节流 + 429 退避(进程层契约 v2:Codex 并行调用兜底)
|
|
79
|
-
const sem = READ_TOOLS.has(name) ? readSem : writeSem
|
|
80
|
-
const throttledCb = (async (...args: unknown[]) => {
|
|
81
|
-
await sem.acquire()
|
|
82
|
-
try {
|
|
83
|
-
return await withRetry(async () => cb(...(args as Parameters<ToolCb>)))
|
|
84
|
-
} finally {
|
|
85
|
-
sem.release()
|
|
86
|
-
}
|
|
87
|
-
}) as ToolCb
|
|
88
23
|
return origRegister(
|
|
89
24
|
name,
|
|
90
25
|
{ ...config, description: `[项目: ${PROJECT_ID}] ${c.description ?? ""}` },
|
|
91
|
-
|
|
26
|
+
cb,
|
|
92
27
|
)
|
|
93
28
|
}) as RegisterTool
|
|
94
29
|
const registrar: ToolRegistrar = { registerTool }
|