add-coder 0.3.33 → 0.3.35
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/dist/index.js +21 -3
- package/package.json +3 -3
- package/templates/.add-coder-src-hash.json +95 -41
- package/templates/adapters/claude/hooks/doc-format-guard.mjs +172 -84
- package/templates/adapters/claude/hooks/post-tool-use.mjs +59 -1
- package/templates/adapters/claude/hooks/prompt-submit.mjs +72 -0
- package/templates/adapters/claude/hooks/session-start.mjs +65 -1
- package/templates/adapters/codex/hooks/doc-format-guard.mjs +172 -84
- package/templates/adapters/codex/hooks/post-tool-use.mjs +59 -1
- package/templates/adapters/codex/hooks/prompt-submit.mjs +72 -0
- package/templates/adapters/codex/hooks/session-start.mjs +65 -1
- package/templates/adapters/qoder/hooks/doc-format-guard.mjs +172 -84
- package/templates/adapters/qoder/hooks/post-tool-use.mjs +59 -1
- package/templates/adapters/qoder/hooks/prompt-submit.mjs +72 -0
- package/templates/adapters/qoder/hooks/session-start.mjs +67 -1
- package/templates/adapters/trae/hooks/doc-format-guard.mjs +172 -84
- package/templates/adapters/trae/hooks/post-tool-use.mjs +59 -1
- package/templates/adapters/trae/hooks/prompt-submit.mjs +72 -0
- package/templates/adapters/trae/hooks/session-start.mjs +65 -1
- package/templates/adapters/vscode/hooks/doc-format-guard.mjs +172 -84
- package/templates/adapters/vscode/hooks/post-tool-use.mjs +59 -1
- package/templates/adapters/vscode/hooks/prompt-submit.mjs +72 -0
- package/templates/adapters/vscode/hooks/session-start.mjs +65 -1
- package/templates/core/governance/doc-format-guard.ts +29 -112
- package/templates/core/governance/post-tool-router.ts +33 -1
- package/templates/core/governance/prompt-router.ts +47 -0
- package/templates/core/governance/session-start-guard.ts +48 -1
- package/templates/core/prisma/add.prisma +203 -0
- package/templates/core/scripts/db-ensure.sh +61 -2
- package/templates/core/scripts/mcp-server/shared/db-types.ts +119 -0
- package/templates/core/scripts/mcp-server/shared/hitl-create-policy.ts +27 -0
- package/templates/core/scripts/mcp-server/shared/hitl-proposal-content.ts +110 -0
- package/templates/core/scripts/mcp-server/shared/hitl-widget-instance.ts +85 -0
- package/templates/core/scripts/mcp-server/shared/memory/calibration/batch-fit.ts +250 -0
- package/templates/core/scripts/mcp-server/shared/memory/calibration/feedback-stats.ts +101 -0
- package/templates/core/scripts/mcp-server/shared/memory/calibration/unit-state.ts +224 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/conflicts.ts +59 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/dedup.ts +45 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/errors.ts +33 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/handoff-digest.ts +92 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/metric-candidate.ts +79 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/scope.ts +94 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/secrets.ts +50 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/state-machine.ts +90 -0
- package/templates/core/scripts/mcp-server/shared/memory/embedding/index.ts +117 -0
- package/templates/core/scripts/mcp-server/shared/memory/embedding/local-onnx.ts +105 -0
- package/templates/core/scripts/mcp-server/shared/memory/embedding/openai-compatible.ts +87 -0
- package/templates/core/scripts/mcp-server/shared/memory/jobs/consolidation.ts +226 -0
- package/templates/core/scripts/mcp-server/shared/memory/jobs/evidence-collector.ts +153 -0
- package/templates/core/scripts/mcp-server/shared/memory/jobs/snapshot.ts +114 -0
- package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-recall.ts +134 -0
- package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-writer.ts +217 -0
- package/templates/core/scripts/mcp-server/shared/memory/metrics/stage-words.ts +69 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/context-builder.ts +89 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/pg.ts +139 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql +29 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite.ts +106 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fusion.ts +43 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/pipeline.ts +285 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/query-terms.ts +31 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/recall-writer.ts +87 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/reranker.ts +116 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/types.ts +52 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/pgvector.ts +143 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/sqlite-vec.ts +118 -0
- package/templates/core/scripts/mcp-server/shared/memory/switches.ts +39 -0
- package/templates/core/scripts/mcp-server/shared/review-files.ts +22 -0
- package/templates/core/scripts/mcp-server/shared/runtime-freshness.ts +235 -0
- package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +41 -3
- package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +40 -1
- package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +2 -2
- package/templates/core/scripts/mcp-server/tools/hitl.ts +108 -42
- package/templates/core/scripts/mcp-server/tools/index.ts +7 -1
- package/templates/core/scripts/mcp-server/tools/memory-compat.ts +258 -0
- package/templates/core/scripts/mcp-server/tools/memory.ts +654 -0
- package/templates/core/scripts/mcp-server/tools/plan.ts +8 -3
- package/templates/core/scripts/mcp-server/tools/review.ts +10 -7
- package/templates/core/scripts/mcp-server.ts +36 -0
- package/templates/core/templates/checklist-template.md +13 -0
- package/templates/core/templates/review-implementation-template.md +24 -0
- package/templates/core/templates/review-template.md +16 -0
- package/templates/core/validation/index.ts +136 -0
- package/templates/core/validation/policy.ts +91 -0
- package/templates/core/validation/registry.ts +61 -0
- package/templates/core/validation/schema-validator.ts +277 -0
- package/templates/core/validation/validators/add-route.ts +32 -0
- package/templates/core/validation/validators/checklist.ts +48 -0
- package/templates/core/validation/validators/handoff.ts +46 -0
- package/templates/core/validation/validators/hitl.ts +22 -0
- package/templates/core/validation/validators/index.ts +52 -0
- package/templates/core/validation/validators/plan.ts +20 -0
- package/templates/core/validation/validators/report.ts +16 -0
- package/templates/core/validation/validators/review.ts +30 -0
- package/templates/core/validation/validators/spec.ts +25 -0
- package/templates/core/validation/validators/tasks.ts +40 -0
- package/templates/core/validation/validators/types.ts +32 -0
- package/templates/core/vocabulary/add-governance-vocabulary.md +18 -0
- package/templates/core/scripts/mcp-server/shared/dps-spec-ref.ts +0 -17
|
@@ -20,6 +20,14 @@ import {
|
|
|
20
20
|
import { prisma } from "../../shared/prisma.js";
|
|
21
21
|
import { runCommand } from "../../shared/run-command.js";
|
|
22
22
|
import { getRuntimeContext } from "../../shared/env.js";
|
|
23
|
+
import {
|
|
24
|
+
buildGateCaptureDetail,
|
|
25
|
+
createGateWriterDeps,
|
|
26
|
+
deriveGateRunId,
|
|
27
|
+
formatGateCaptureSummary,
|
|
28
|
+
writeGateMetric,
|
|
29
|
+
GATE_METRIC_TYPE,
|
|
30
|
+
} from "../../shared/memory/metrics/gate-writer.js";
|
|
23
31
|
|
|
24
32
|
export function registerCheckRahs(server: ToolRegistrar) {
|
|
25
33
|
const runtimeContext = getRuntimeContext();
|
|
@@ -50,6 +58,8 @@ export function registerCheckRahs(server: ToolRegistrar) {
|
|
|
50
58
|
);
|
|
51
59
|
if (!pm)
|
|
52
60
|
return errorResponse(`未找到匹配的 Plan 文件(关键词: ${pp})`);
|
|
61
|
+
// Plan 正文在多个维度(Spec 引用解析、采证 seed)共用,提到此处只读一次
|
|
62
|
+
const planContent = (await readFileSafe(join(plansDir, pm))) || "";
|
|
53
63
|
let scopeScore = 80,
|
|
54
64
|
typeScore = 80,
|
|
55
65
|
auditScore = 80,
|
|
@@ -115,7 +125,6 @@ export function registerCheckRahs(server: ToolRegistrar) {
|
|
|
115
125
|
const rahsCandidates = rahsVersionSuffix
|
|
116
126
|
? [`${rahsPlanBase}-${rahsVersionSuffix}`, rahsPlanBase]
|
|
117
127
|
: [rahsPlanBase];
|
|
118
|
-
const planContent = (await readFileSafe(join(plansDir, pm))) || "";
|
|
119
128
|
const rahsSpecRef = planContent.match(/specs\/([^/`\s]+)/);
|
|
120
129
|
if (rahsSpecRef) rahsCandidates.push(rahsSpecRef[1]);
|
|
121
130
|
const sn =
|
|
@@ -181,6 +190,36 @@ export function registerCheckRahs(server: ToolRegistrar) {
|
|
|
181
190
|
`=== RAHS = ${rahs} ${rahs >= 90 ? "🟢 PASS" : rahs >= 70 ? "🟡 WARN" : "🔴 BLOCKED"} ===`,
|
|
182
191
|
);
|
|
183
192
|
if (rahs < 70) parts.push(" 动作: 注意力漂移严重,返工回退");
|
|
193
|
+
|
|
194
|
+
// ── Gate 采证(Spec §GateMetric):与评分解耦,失败旁路不阻塞返回 ──
|
|
195
|
+
const rahsSeed = `${planContent}\n${scopeScore}|${typeScore}|${auditScore}|${specScore}|${symScore}`;
|
|
196
|
+
const rahsCaptureInput = {
|
|
197
|
+
gate: "check_rahs" as const,
|
|
198
|
+
planKeyword: pp,
|
|
199
|
+
runId: deriveGateRunId("check_rahs", pp, rahsSeed),
|
|
200
|
+
metricType: GATE_METRIC_TYPE.check_rahs,
|
|
201
|
+
score: rahs,
|
|
202
|
+
repository: runtimeContext.projectKey,
|
|
203
|
+
dimensionScores: {
|
|
204
|
+
scope: scopeScore,
|
|
205
|
+
type: typeScore,
|
|
206
|
+
audit: auditScore,
|
|
207
|
+
spec: specScore,
|
|
208
|
+
symmetry: symScore,
|
|
209
|
+
},
|
|
210
|
+
baseline: 90,
|
|
211
|
+
unit: "score",
|
|
212
|
+
};
|
|
213
|
+
const rahsCapture = await writeGateMetric(
|
|
214
|
+
rahsCaptureInput,
|
|
215
|
+
createGateWriterDeps((prisma as Record<string, unknown>).addMetricSnapshot),
|
|
216
|
+
);
|
|
217
|
+
parts.push(
|
|
218
|
+
"",
|
|
219
|
+
"=== Gate 采证(MetricSnapshot)===",
|
|
220
|
+
` ${formatGateCaptureSummary(buildGateCaptureDetail(rahsCaptureInput, rahsCapture))}`,
|
|
221
|
+
);
|
|
222
|
+
|
|
184
223
|
return textResponse(parts.join("\n"));
|
|
185
224
|
} catch (e) {
|
|
186
225
|
return errorResponse(
|
|
@@ -76,8 +76,8 @@ export function registerCheckSpecSync(server: ToolRegistrar) {
|
|
|
76
76
|
} else {
|
|
77
77
|
lines.push(`add-route: ${arFile}`);
|
|
78
78
|
const arContent = (await readFileSafe(join(plansDir, arFile))) || "";
|
|
79
|
-
// 提取 add-route
|
|
80
|
-
const appendixFiles = (arContent.match(/`[^`]+\.(ts|js|sh|md|tsx|json|yml|yaml
|
|
79
|
+
// 提取 add-route 附录文件清单
|
|
80
|
+
const appendixFiles = (arContent.match(/`[^`]+\.(ts|js|sh|md|tsx|json|yml|yaml)`/g) || [])
|
|
81
81
|
.map((f: string) => f.replace(/`/g, ""));
|
|
82
82
|
lines.push(`附录文件: ${appendixFiles.length} 个`);
|
|
83
83
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as z from "zod/v4"
|
|
2
|
-
import { inputRequired, acceptedContent } from "@modelcontextprotocol/server"
|
|
2
|
+
import { inputRequired, acceptedContent, type ElicitInputParams } from "@modelcontextprotocol/server"
|
|
3
3
|
import type { ToolRegistrar } from "./registrar.js"
|
|
4
4
|
import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs"
|
|
5
5
|
import { join, basename, relative } from "path"
|
|
@@ -7,6 +7,7 @@ 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 { shouldSkipHitlCreateDialog } from "../shared/hitl-create-policy.js"
|
|
10
11
|
import type { HitlRow } from "../shared/db-types.js"
|
|
11
12
|
import { HitlRowSchema, validatedDelegate } from "../shared/db-types.js"
|
|
12
13
|
import { getRuntimeContext } from "../shared/env.js"
|
|
@@ -18,6 +19,10 @@ import {
|
|
|
18
19
|
createHitlProposalAndPublish,
|
|
19
20
|
type HitlProposalDatabase,
|
|
20
21
|
} from "../shared/hitl-proposal-mutation.js"
|
|
22
|
+
import {
|
|
23
|
+
buildHitlProposalMarkdown,
|
|
24
|
+
applyHitlDecisionToProposal,
|
|
25
|
+
} from "../shared/hitl-proposal-content.js"
|
|
21
26
|
import { HITL_APPROVAL_WIDGET_URI } from "../shared/hitl-ui.js"
|
|
22
27
|
|
|
23
28
|
// 无类型边界单点(zod 托管):动态加载的 prisma client 在此一次性转为运行期校验的泛型委托
|
|
@@ -25,6 +30,23 @@ const db = {
|
|
|
25
30
|
get hitl() { return validatedDelegate<HitlRow>(prisma.hitlRecord, HitlRowSchema, "HitlRecord") },
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
/**
|
|
34
|
+
* 表单式 elicitation 的 `requestedSchema`(2026-09-14:替换 4 处 `as any`)。
|
|
35
|
+
*
|
|
36
|
+
* 为什么需要一个函数:属性表是**运行时按维度动态构造**的(`dim_0_content`/`dim_0_decision`…),
|
|
37
|
+
* 而 SDK 的 `ElicitInputParams["requestedSchema"]` 是静态 JSON Schema 形状 —— 字面量直接赋值
|
|
38
|
+
* 会因索引签名不匹配而报错。这里把**唯一的窄化点**收在一处,类型取自 SDK 而非 `any`。
|
|
39
|
+
*/
|
|
40
|
+
/** 表单里单个属性的形状(本仓库只用字符串型 + 可选枚举) */
|
|
41
|
+
type ElicitFormProperty = { type: "string"; enum?: string[]; description?: string }
|
|
42
|
+
|
|
43
|
+
function elicitFormSchema(
|
|
44
|
+
properties: Record<string, ElicitFormProperty>,
|
|
45
|
+
required: string[],
|
|
46
|
+
): ElicitInputParams["requestedSchema"] {
|
|
47
|
+
return { type: "object", properties, required } as ElicitInputParams["requestedSchema"]
|
|
48
|
+
}
|
|
49
|
+
|
|
28
50
|
export function registerHitlTools(server: ToolRegistrar) {
|
|
29
51
|
const runtimeContext = getRuntimeContext()
|
|
30
52
|
|
|
@@ -123,9 +145,15 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
123
145
|
filePath: string,
|
|
124
146
|
status: HitlRow["status"],
|
|
125
147
|
dimensions?: { name: string; content?: string }[],
|
|
148
|
+
reason?: string,
|
|
126
149
|
): void {
|
|
127
150
|
let proposal = readFileSync(filePath, "utf-8")
|
|
128
|
-
|
|
151
|
+
// 状态行 + 「审批结论」行(2026-09-14:原实现只改状态行,裁决结论不回写文档)
|
|
152
|
+
proposal = applyHitlDecisionToProposal(proposal, {
|
|
153
|
+
status,
|
|
154
|
+
at: new Date().toISOString(),
|
|
155
|
+
reason,
|
|
156
|
+
})
|
|
129
157
|
|
|
130
158
|
if (dimensions?.length) {
|
|
131
159
|
const byName = new Map(dimensions.map((d) => [d.name.trim(), d.content ?? ""]))
|
|
@@ -187,16 +215,60 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
187
215
|
if (!dimensions.length) {
|
|
188
216
|
return errorResponse(`HITL 提案缺少可渲染维度: ${proposalPath ?? planName}`)
|
|
189
217
|
}
|
|
218
|
+
// 产物-进程新鲜度(覆盖全 adapter;判定不确定 → "unknown",不误报"需重启")
|
|
219
|
+
const { computeFreshness } = await import("../shared/runtime-freshness.js")
|
|
220
|
+
const { writeHitlInstanceHtml } = await import("../shared/hitl-widget-instance.js")
|
|
221
|
+
const stale = computeFreshness(MAGIC_DIR, PROJECT_ROOT)
|
|
222
|
+
// 确定性降级入口:markdown 提案 + 已落盘实例 HTML(无 UI 能力/进程陈旧时仍可完成审批)
|
|
223
|
+
const toRel = (abs: string): string => abs.replace(`${PROJECT_ROOT}/`, "")
|
|
224
|
+
const markdownPath = proposalPath ? toRel(proposalPath) : ""
|
|
225
|
+
let htmlPath = ""
|
|
226
|
+
try {
|
|
227
|
+
htmlPath = toRel(
|
|
228
|
+
writeHitlInstanceHtml({
|
|
229
|
+
projectRoot: PROJECT_ROOT,
|
|
230
|
+
magicDir: MAGIC_DIR,
|
|
231
|
+
planName,
|
|
232
|
+
type,
|
|
233
|
+
round: current.round,
|
|
234
|
+
status: current.status,
|
|
235
|
+
dimensions,
|
|
236
|
+
}).htmlPath,
|
|
237
|
+
)
|
|
238
|
+
} catch {
|
|
239
|
+
htmlPath = "" // 模板缺失等 → 仍保留 markdown 降级路径(fail-open)
|
|
240
|
+
}
|
|
190
241
|
const output = {
|
|
191
242
|
planName,
|
|
192
243
|
type,
|
|
193
244
|
round: current.round,
|
|
194
245
|
status: current.status,
|
|
195
246
|
dimensions,
|
|
247
|
+
stale,
|
|
248
|
+
// 服务端无法得知客户端是否真的渲染了 widget —— 如实标注 unknown,不谎报
|
|
249
|
+
ui: { resourceUri: HITL_APPROVAL_WIDGET_URI, rendered: "unknown" as const },
|
|
250
|
+
fallback: { markdownPath, htmlPath },
|
|
196
251
|
}
|
|
252
|
+
const fallbackHint = markdownPath || htmlPath
|
|
253
|
+
const text =
|
|
254
|
+
stale.stale === true
|
|
255
|
+
? `已加载 ${planName} round ${current.round} 的 ${dimensions.length} 个审批维度。⚠️ 当前 server 进程早于产物更新(需重启);widget 若未显示,降级入口:${fallbackHint}`
|
|
256
|
+
: `已加载 ${planName} round ${current.round} 的 ${dimensions.length} 个审批维度。widget 若未显示,降级入口:${fallbackHint}`
|
|
197
257
|
return {
|
|
198
|
-
content: [{ type: "text" as const, text
|
|
258
|
+
content: [{ type: "text" as const, text }],
|
|
199
259
|
structuredContent: output,
|
|
260
|
+
/*
|
|
261
|
+
* 结果侧 `_meta`(Apps SDK 兼容补位,2026-09-14):
|
|
262
|
+
* 宿主实际读的是**工具定义**的 `_meta.ui.resourceUri`(`tools/list` 时解析,
|
|
263
|
+
* 实测 `thread_items` 里已带 `mcpAppResourceUri`,而 `result._meta` 仍为 null 也照常绑定);
|
|
264
|
+
* 这里补一份结果侧 `_meta` 不改变行为,只为兼容同时读结果位的客户端。
|
|
265
|
+
* 真正的渲染前提是:客户端 MCP Apps 能力开关(Codex: `/experimental` 的 enable_mcp_apps)
|
|
266
|
+
* + 资源 mimeType 为 `text/html;profile=mcp-app` + 资源存在 + dimensions 非空。
|
|
267
|
+
*/
|
|
268
|
+
_meta: {
|
|
269
|
+
ui: { resourceUri: HITL_APPROVAL_WIDGET_URI },
|
|
270
|
+
"openai/outputTemplate": HITL_APPROVAL_WIDGET_URI,
|
|
271
|
+
},
|
|
200
272
|
}
|
|
201
273
|
} catch (e) {
|
|
202
274
|
return errorResponse(`render_hitl_approval 失败: ${e instanceof Error ? e.message : String(e)}`)
|
|
@@ -208,9 +280,11 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
208
280
|
description: "HITL 审批:创建提案。写入 HitlRecord(status=DRAFT,自动递增 round),并生成 hitl.md 提案文件供人工审核。\n" +
|
|
209
281
|
"交互模式按安装环境自动裁决(caijuehub: hitl-interaction-rules.toml):\n" +
|
|
210
282
|
"- genui 模式环境(如 Qoder,客户端未声明 elicitation capability):MUST 直接走 genui 流程——先用 genui show_widget 渲染逐项决策表单,用户拍板后以 _use_genui=true + 最终 dimensions 调用本工具。不带模式参数调用会返回 genui 引导而非弹框。\n" +
|
|
283
|
+
"- MCP Apps 模式环境(如 Codex):MUST NOT 展开高维 inputRequired——创建阶段直接落 DRAFT,审批走 render_hitl_approval 打开的 core widget(其回调以 update_hitl(_use_widget=true) 落库)。不带模式参数时按环境自动走该路径;也可用 _mcp_apps=true 强制。\n" +
|
|
211
284
|
"- 支持 elicitation 的客户端(2026-07-28+ 协议):inputRequired 弹框,含逐项决策(LLM 传 dimensions)或简单弹框两种模式。\n" +
|
|
212
285
|
"降级模式(_fallback=true):genui 与弹框均不可用时兜底,跳过弹框直接以传入的 dimensions 创建,人工审核 hitl.md。\n" +
|
|
213
286
|
"_use_genui=true:genui widget 回调后使用,跳过所有弹框,直接以传入的 dimensions 创建 DB+文件。\n" +
|
|
287
|
+
"_mcp_apps=true:强制 MCP Apps 流程(落 DRAFT 不弹框,审批走 core widget)。\n" +
|
|
214
288
|
"planName 示例: add-coder-hitl-mcp-hook-plan-v1\n" +
|
|
215
289
|
"type: PLAN=计划审批, PLAN_REVIEW=方案评审",
|
|
216
290
|
inputSchema: z.object({
|
|
@@ -222,10 +296,11 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
222
296
|
})).optional().describe("决策维度列表(LLM 根据对话生成)。不传则按模板默认 8 维度"),
|
|
223
297
|
_fallback: z.boolean().optional().default(false).describe("降级模式:跳过 inputRequired,按原始代码行为直接创建 DB 记录+hitl.md"),
|
|
224
298
|
_use_genui: z.boolean().optional().default(false).describe("genui 模式:genui widget 回调后使用,跳过所有弹框直接创建(dimensions 需传最终确认值)"),
|
|
299
|
+
_mcp_apps: z.boolean().optional().default(false).describe("Codex MCP Apps 强制模式:落 DRAFT 不弹 inputRequired,审批走 render_hitl_approval core widget(update_hitl(_use_widget=true))"),
|
|
225
300
|
}),
|
|
226
301
|
}, async (args: Record<string, unknown>, ctx: Record<string, unknown>) => {
|
|
227
302
|
try {
|
|
228
|
-
const { planName, type, dimensions, _fallback, _use_genui } = args as { planName: string; type: string; dimensions?: { name: string; content?: string }[]; _fallback?: boolean; _use_genui?: boolean }
|
|
303
|
+
const { planName, type, dimensions, _fallback, _use_genui, _mcp_apps } = args as { planName: string; type: string; dimensions?: { name: string; content?: string }[]; _fallback?: boolean; _use_genui?: boolean; _mcp_apps?: boolean }
|
|
229
304
|
|
|
230
305
|
// ── planName 入口强校验(弱模型友好:不合规返回可照抄修正调用,而非让错误漂移) ──
|
|
231
306
|
const _pnValid = /-(plan|collab-contract)-v\d+$/.test(planName)
|
|
@@ -243,13 +318,14 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
243
318
|
// ── 最终维度内容(从弹框结果合并) ──
|
|
244
319
|
let finalDims: { name: string; content: string }[] = []
|
|
245
320
|
|
|
246
|
-
// ── genui
|
|
247
|
-
|
|
321
|
+
// ── genui/降级/mcpApps 模式:无弹框环节,直接采用传入的 dimensions(降级丢维度会退化成默认空模板) ──
|
|
322
|
+
const skipDialog = shouldSkipHitlCreateDialog(_interaction.mode, { fallback: _fallback, useGenui: _use_genui, mcpApps: _mcp_apps })
|
|
323
|
+
if (skipDialog) {
|
|
248
324
|
finalDims = (dimensions || []).map(d => ({ name: d.name, content: d.content || "" }))
|
|
249
325
|
}
|
|
250
326
|
|
|
251
327
|
// ── 交互式确认(非降级模式且非 genui 模式) ──
|
|
252
|
-
if (!
|
|
328
|
+
if (!skipDialog) {
|
|
253
329
|
// 环境裁决:genui 模式下不发起注定失败的 elicitation,引导 LLM 走 widget 流程
|
|
254
330
|
if (_interaction.mode === "genui") {
|
|
255
331
|
const dimDesc = (dimensions || []).map((d, i) => `${i + 1}. ${d.name}: ${d.content || ""}`).join("\n")
|
|
@@ -266,7 +342,7 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
266
342
|
|
|
267
343
|
if (!decResp) {
|
|
268
344
|
// 首次调用 — 构建扁平逐项弹框
|
|
269
|
-
const props: Record<string,
|
|
345
|
+
const props: Record<string, ElicitFormProperty> = {
|
|
270
346
|
globalAction: {
|
|
271
347
|
type: "string", enum: ["", "同意全部", "驳回全部"],
|
|
272
348
|
description: "同意全部=通过所有(含已调整行); 驳回全部=驳回提案; 留空=逐项"
|
|
@@ -290,7 +366,7 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
290
366
|
inputRequests: {
|
|
291
367
|
confirm: inputRequired.elicit({
|
|
292
368
|
message: `确认创建 HITL 提案\n\nplan: ${planName}\ntype: ${type}\n\n共 ${dimensions.length} 个决策维度:\n${dimDesc}`,
|
|
293
|
-
requestedSchema:
|
|
369
|
+
requestedSchema: elicitFormSchema(props, requiredKeys),
|
|
294
370
|
})
|
|
295
371
|
}
|
|
296
372
|
})
|
|
@@ -322,7 +398,10 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
322
398
|
inputRequests: {
|
|
323
399
|
confirm: inputRequired.elicit({
|
|
324
400
|
message: `确认创建 HITL 提案?\n\nplan: ${planName}\ntype: ${type}`,
|
|
325
|
-
requestedSchema:
|
|
401
|
+
requestedSchema: elicitFormSchema(
|
|
402
|
+
{ action: { type: "string", enum: ["同意", "取消"], description: "同意=确认创建提案, 取消=取消操作" } },
|
|
403
|
+
["action"],
|
|
404
|
+
),
|
|
326
405
|
})
|
|
327
406
|
}
|
|
328
407
|
})
|
|
@@ -361,34 +440,14 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
361
440
|
// 生成 hitl.md
|
|
362
441
|
const isoNow = now.toISOString()
|
|
363
442
|
|
|
364
|
-
//
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
"| 5 | 模板 + schema | | 同意/驳回 |",
|
|
373
|
-
"| 6 | 新增依赖 | | 同意/驳回 |",
|
|
374
|
-
"| 7 | 预计文件数 | | 同意/驳回 |",
|
|
375
|
-
"| 8 | 预计轮次 | | 同意/驳回 |",
|
|
376
|
-
].join("\n")
|
|
377
|
-
|
|
378
|
-
const tpl = [
|
|
379
|
-
`# ${planName} — HITL 提案 (round ${round})`,
|
|
380
|
-
"",
|
|
381
|
-
`> 创建: ${isoNow} | 类型: ${type} | 状态: DRAFT`,
|
|
382
|
-
"",
|
|
383
|
-
"## HITL 计划总览",
|
|
384
|
-
"",
|
|
385
|
-
"请填写以下决策维度,人工审核后点击 update_hitl 弹框选择「同意/驳回」完成审批:",
|
|
386
|
-
"",
|
|
387
|
-
"| # | 维度 | 方案内容 | 决策 |",
|
|
388
|
-
"|---|------|----------|:----:|",
|
|
389
|
-
tableRows,
|
|
390
|
-
"",
|
|
391
|
-
].join("\n")
|
|
443
|
+
// 生成提案文档(真源 hitl-template.md:HITL 计划总览 + 审批结论 两章节齐备)
|
|
444
|
+
const tpl = buildHitlProposalMarkdown({
|
|
445
|
+
planName: String(planName),
|
|
446
|
+
round,
|
|
447
|
+
type: String(type),
|
|
448
|
+
createdAt: isoNow,
|
|
449
|
+
dimensions: finalDims,
|
|
450
|
+
})
|
|
392
451
|
mkdirSync(plansDir, { recursive: true })
|
|
393
452
|
writeFileSync(proposalPath, tpl, "utf-8")
|
|
394
453
|
|
|
@@ -406,6 +465,10 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
406
465
|
if (finalDims.length > 0) lines.push(`dimensions: ${finalDims.length} 项`)
|
|
407
466
|
if (proposal.planProvisioned) lines.push(`PlanRecord: 自动预置(占位行,Plan 文件写出后 plan_track 回刷真实路径)`)
|
|
408
467
|
if (_fallback) lines.push(`mode: _fallback (跳过 dialog,原始代码降级)`)
|
|
468
|
+
if ((_interaction.mode === "mcpApps" || _mcp_apps) && !_fallback) {
|
|
469
|
+
lines.push(`mode: mcpApps (Codex:不展开 inputRequired${_mcp_apps ? ",_mcp_apps 显式强制" : ""})`)
|
|
470
|
+
lines.push(``, `➡️ 下一步:调用 render_hitl_approval({ planName: "${planName}", type: "${type}" }) 打开审批 widget,由用户拍板。`)
|
|
471
|
+
}
|
|
409
472
|
return textResponse(lines.join("\n"))
|
|
410
473
|
} catch (e) {
|
|
411
474
|
const msg = e instanceof Error ? e.message : String(e)
|
|
@@ -489,7 +552,7 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
489
552
|
const decResp = _parseDecisions(ctx)
|
|
490
553
|
|
|
491
554
|
if (!decResp) {
|
|
492
|
-
const props: Record<string,
|
|
555
|
+
const props: Record<string, ElicitFormProperty> = {
|
|
493
556
|
globalAction: {
|
|
494
557
|
type: "string", enum: ["", "同意全部", "驳回全部"],
|
|
495
558
|
description: "同意全部=通过所有维度; 驳回全部=驳回提案; 留空=逐项"
|
|
@@ -512,7 +575,7 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
512
575
|
inputRequests: {
|
|
513
576
|
confirm: inputRequired.elicit({
|
|
514
577
|
message: `HITL 审批决策\n\nplan: ${planName}\ntype: ${type}\n\n逐项决策以下 ${dims.length} 个维度:\n${dimDesc}`,
|
|
515
|
-
requestedSchema:
|
|
578
|
+
requestedSchema: elicitFormSchema(props, requiredKeys),
|
|
516
579
|
})
|
|
517
580
|
}
|
|
518
581
|
})
|
|
@@ -536,7 +599,10 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
536
599
|
inputRequests: {
|
|
537
600
|
confirm: inputRequired.elicit({
|
|
538
601
|
message: `HITL 审批决策\n\nplan: ${planName}\ntype: ${type}`,
|
|
539
|
-
requestedSchema:
|
|
602
|
+
requestedSchema: elicitFormSchema(
|
|
603
|
+
{ action: { type: "string", enum: ["同意", "驳回", "取消"], description: "同意=通过审批, 驳回=驳回重做, 取消=暂不操作" } },
|
|
604
|
+
["action"],
|
|
605
|
+
),
|
|
540
606
|
})
|
|
541
607
|
}
|
|
542
608
|
})
|
|
@@ -585,7 +651,7 @@ export function registerHitlTools(server: ToolRegistrar) {
|
|
|
585
651
|
// P3 #6:回写 .hitl.md 提案文件状态(DRAFT → TONGYI/BOHUI),保证双通道校验一致
|
|
586
652
|
const proposalPath = findHitlFile(String(planName))
|
|
587
653
|
if (proposalPath) {
|
|
588
|
-
updateHitlProposal(proposalPath, s, (_use_widget || _use_genui) ? dimensions : undefined)
|
|
654
|
+
updateHitlProposal(proposalPath, s, (_use_widget || _use_genui) ? dimensions : undefined, reason)
|
|
589
655
|
}
|
|
590
656
|
// 响应
|
|
591
657
|
const lines = [
|
|
@@ -9,6 +9,8 @@ import { registerHookEventTools } from "./hook-event-report.js"
|
|
|
9
9
|
import { registerHitlTools } from "./hitl.js"
|
|
10
10
|
import { registerPlanTools } from "./plan.js"
|
|
11
11
|
import { registerReviewTools } from "./review.js"
|
|
12
|
+
import { registerMemoryTools } from "./memory.js"
|
|
13
|
+
import { registerMemoryCompatTools } from "./memory-compat.js"
|
|
12
14
|
import { PROJECT_ID } from "../shared/env.js"
|
|
13
15
|
import type { ToolRegistrar } from "./registrar.js"
|
|
14
16
|
|
|
@@ -24,6 +26,8 @@ const READ_TOOLS = new Set([
|
|
|
24
26
|
"check_dps", "check_rahs", "check_add_route_status", "check_spec_sync",
|
|
25
27
|
"check_add_route_completeness", "check_phase_symmetry", "check_failure_path",
|
|
26
28
|
"check_add_compliance", "get_hook_events",
|
|
29
|
+
"recall_memory", "get_memory", "list_memories", "review_memory", "get_memory_health",
|
|
30
|
+
"search_memory", "read_memory", "memory_stats",
|
|
27
31
|
])
|
|
28
32
|
|
|
29
33
|
function createSemaphore(limit: number) {
|
|
@@ -104,5 +108,7 @@ export function registerAllTools(server: McpServer) {
|
|
|
104
108
|
registerHitlTools(registrar) // 4 tools: create_hitl / update_hitl / status_hitl / render_hitl_approval
|
|
105
109
|
registerPlanTools(registrar) // 3 tools: plan_track / plan_status / plan_sync
|
|
106
110
|
registerReviewTools(registrar) // 3 tools: review_track / review_status / review_sync
|
|
107
|
-
//
|
|
111
|
+
registerMemoryTools(registrar) // 8 tools: propose_memory / recall_memory / get_memory / list_memories / review_memory / resolve_memory / feedback_memory / get_memory_health
|
|
112
|
+
registerMemoryCompatTools(registrar) // 6 tools: 5 个 v1 弃用门面(append_memory/search_memory/read_memory/link_memory/memory_stats)+ forget_memory(合规清除)
|
|
113
|
+
// Total: 44 tools
|
|
108
114
|
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Memory 兼容面与合规清除(Plan §3.4 轮 2 / Spec §4 §CompatAndPurge)
|
|
3
|
+
*
|
|
4
|
+
* 两部分:
|
|
5
|
+
* A. v1 shim —— 5 个旧工具名注册为**弃用门面**:正常受理调用、把参数映射到 v2 语义、
|
|
6
|
+
* 返回 `deprecated: true` + 目标工具 + 迁移指引,不重复实现 v2 逻辑(避免与 memory.ts 双源漂移)。
|
|
7
|
+
* ⚠️ 溯源声明:v1 工具名在仓库内**零命中**(CHANGELOG / docs / 五个 magic 目录均无),
|
|
8
|
+
* 名字集合按 Plan §3.4 的「append/search/read/link/stats」字面构造。真实 v1 名单若不同,
|
|
9
|
+
* 只需改下方 SHIM_TABLE 一行。
|
|
10
|
+
* 执行转发(真正跑 v2 逻辑)需要把 memory.ts 的操作层抽成共享 ops 模块,按轮次边界
|
|
11
|
+
* (memory.ts 仅轮 3 装配)不在本轮改动,已登记为待补项。
|
|
12
|
+
* B. forget_memory —— 合规清除:物理删除记忆行,并清理三类外键占用(证据关联 / 召回记录 /
|
|
13
|
+
* 被 supersede 指向),最后写 AuditLog 留痕(ADD-5:不落库的清除视为未发生)。
|
|
14
|
+
* 普通废弃仍走状态机(resolve_memory 的 archive/reject/supersede),不物理删除。
|
|
15
|
+
*/
|
|
16
|
+
import * as z from "zod/v4"
|
|
17
|
+
import type { ToolRegistrar } from "./registrar.js"
|
|
18
|
+
import { textResponse, errorResponse } from "../shared/response.js"
|
|
19
|
+
import { prisma } from "../shared/prisma.js"
|
|
20
|
+
import { getRuntimeContext } from "../shared/env.js"
|
|
21
|
+
import {
|
|
22
|
+
validatedDelegate,
|
|
23
|
+
AddMemoryRowSchema,
|
|
24
|
+
AddMemoryEvidenceLinkRowSchema,
|
|
25
|
+
AddMemoryRecallItemRowSchema,
|
|
26
|
+
AuditLogRowSchema,
|
|
27
|
+
AddUserRowSchema,
|
|
28
|
+
type AddMemoryRow,
|
|
29
|
+
type AddMemoryEvidenceLinkRow,
|
|
30
|
+
type AddMemoryRecallItemRow,
|
|
31
|
+
type AuditLogRow,
|
|
32
|
+
type AddUserRow,
|
|
33
|
+
type TableDelegate,
|
|
34
|
+
} from "../shared/db-types.js"
|
|
35
|
+
import { MemoryError, isMemoryError } from "../shared/memory/domain/errors.js"
|
|
36
|
+
|
|
37
|
+
/** v1 → v2 映射表(表驱动:真实名单变化时只改这里) */
|
|
38
|
+
export interface ShimEntry {
|
|
39
|
+
v1: string
|
|
40
|
+
mappedTo: string
|
|
41
|
+
note: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const SHIM_TABLE: readonly ShimEntry[] = [
|
|
45
|
+
{ v1: "append_memory", mappedTo: "propose_memory", note: "候选写入语义:落 CANDIDATE,绝不直接 ACTIVE" },
|
|
46
|
+
{ v1: "search_memory", mappedTo: "recall_memory", note: "受治理混合召回:RRF 融合 + 治理重排 + token 预算,返回 degradedMode" },
|
|
47
|
+
{ v1: "read_memory", mappedTo: "get_memory", note: "单条详情 + 证据链 + supersession 链" },
|
|
48
|
+
{ v1: "link_memory", mappedTo: "get_memory", note: "关联改由证据链承载(AddMemoryEvidenceLink),无独立链接工具" },
|
|
49
|
+
{ v1: "memory_stats", mappedTo: "get_memory_health", note: "backlog / leakage / provider / FTS 健康度" },
|
|
50
|
+
] as const
|
|
51
|
+
|
|
52
|
+
/** 弃用响应体(纯函数,便于测试) */
|
|
53
|
+
export function buildShimPayload(v1: string, args: Record<string, unknown>) {
|
|
54
|
+
const entry = SHIM_TABLE.find((s) => s.v1 === v1)
|
|
55
|
+
return {
|
|
56
|
+
deprecated: true,
|
|
57
|
+
v1,
|
|
58
|
+
mappedTo: entry?.mappedTo ?? null,
|
|
59
|
+
mappedArgs: args,
|
|
60
|
+
note: entry?.note ?? null,
|
|
61
|
+
executed: false,
|
|
62
|
+
hint: entry
|
|
63
|
+
? `v1 工具 ${v1} 已弃用:请改用 ${entry.mappedTo}(参数同上表映射)。`
|
|
64
|
+
: `未知的 v1 工具名 ${v1}。`,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type PurgeReason = "secret" | "privacy" | "compliance"
|
|
69
|
+
|
|
70
|
+
export interface PurgeResult {
|
|
71
|
+
deleted: true
|
|
72
|
+
memoryId: string
|
|
73
|
+
reason: PurgeReason
|
|
74
|
+
ftsCleared: true
|
|
75
|
+
vectorCleared: boolean
|
|
76
|
+
clearedLinks: number
|
|
77
|
+
clearedRecallItems: number
|
|
78
|
+
clearedSupersedePointers: number
|
|
79
|
+
auditId: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface PurgeDeps {
|
|
83
|
+
/** 目标仓库(= 运行时 projectKey,越库直接拒绝) */
|
|
84
|
+
repositoryRef: string
|
|
85
|
+
context: { projectKey: string; adapterKey: string; contextId: string }
|
|
86
|
+
memoryDb: Pick<TableDelegate<AddMemoryRow>, "findUnique" | "delete" | "updateMany">
|
|
87
|
+
linkDb: Pick<TableDelegate<AddMemoryEvidenceLinkRow>, "deleteMany">
|
|
88
|
+
recallItemDb: Pick<TableDelegate<AddMemoryRecallItemRow>, "deleteMany">
|
|
89
|
+
auditDb: Pick<TableDelegate<AuditLogRow>, "create">
|
|
90
|
+
ensureUserId: () => Promise<string>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 合规清除(物理删除)。仅限 secret / privacy / compliance 三类原因;
|
|
95
|
+
* FTS 索引随行删除自动维护(PG GIN 索引与 SQLite FTS5 触发器均为行级同步),故 ftsCleared 恒为 true;
|
|
96
|
+
* 向量列由 embeddingState 决定:首版 provider=none → 无向量可清(vectorCleared=false)。
|
|
97
|
+
*/
|
|
98
|
+
export async function purgeMemory(
|
|
99
|
+
deps: PurgeDeps,
|
|
100
|
+
input: { memoryId: string; repositoryRef: string; reason: PurgeReason; actor?: string },
|
|
101
|
+
): Promise<PurgeResult> {
|
|
102
|
+
if (input.repositoryRef !== deps.context.projectKey) {
|
|
103
|
+
throw new MemoryError("ERR_REPOSITORY_MISMATCH", `越库清除被拒绝:${input.repositoryRef}`)
|
|
104
|
+
}
|
|
105
|
+
const row = await deps.memoryDb.findUnique({ where: { id: input.memoryId } })
|
|
106
|
+
if (!row) throw new MemoryError("ERR_NOT_FOUND", `memoryId=${input.memoryId}`)
|
|
107
|
+
if (row.repositoryRef !== deps.context.projectKey) {
|
|
108
|
+
throw new MemoryError("ERR_REPOSITORY_MISMATCH", `memoryId=${input.memoryId} 属于 ${row.repositoryRef}`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 外键占用清理顺序:证据关联 → 召回记录 → 被指向的 supersede 指针 → 行本身
|
|
112
|
+
const links = await deps.linkDb.deleteMany({ where: { memoryId: row.id } })
|
|
113
|
+
const recItems = await deps.recallItemDb.deleteMany({ where: { memoryId: row.id } })
|
|
114
|
+
const pointers = await deps.memoryDb.updateMany({
|
|
115
|
+
where: { supersededById: row.id },
|
|
116
|
+
data: { supersededById: null },
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const vectorCleared = String(row.embeddingState ?? "DISABLED") !== "DISABLED"
|
|
120
|
+
const userId = await deps.ensureUserId()
|
|
121
|
+
const log = await deps.auditDb.create({
|
|
122
|
+
data: {
|
|
123
|
+
userId,
|
|
124
|
+
projectKey: deps.context.projectKey,
|
|
125
|
+
producerAdapterKey: deps.context.adapterKey,
|
|
126
|
+
contextId: deps.context.contextId,
|
|
127
|
+
action: "MEMORY_PURGE",
|
|
128
|
+
targetType: "AddMemory",
|
|
129
|
+
targetId: row.id,
|
|
130
|
+
beforeState: {
|
|
131
|
+
status: row.status,
|
|
132
|
+
kind: row.kind,
|
|
133
|
+
topic: row.topic,
|
|
134
|
+
contentHash: row.contentHash,
|
|
135
|
+
scopeType: row.scopeType,
|
|
136
|
+
scopeValue: row.scopeValue,
|
|
137
|
+
embeddingState: row.embeddingState ?? null,
|
|
138
|
+
evidenceLinks: links.count ?? 0,
|
|
139
|
+
recallItems: recItems.count ?? 0,
|
|
140
|
+
supersedePointers: pointers.count ?? 0,
|
|
141
|
+
},
|
|
142
|
+
afterState: {
|
|
143
|
+
deleted: true,
|
|
144
|
+
ftsCleared: true,
|
|
145
|
+
vectorCleared,
|
|
146
|
+
reason: input.reason,
|
|
147
|
+
actor: input.actor ?? null,
|
|
148
|
+
},
|
|
149
|
+
reason: input.reason,
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
await deps.memoryDb.delete({ where: { id: row.id } })
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
deleted: true,
|
|
157
|
+
memoryId: row.id,
|
|
158
|
+
reason: input.reason,
|
|
159
|
+
ftsCleared: true,
|
|
160
|
+
vectorCleared,
|
|
161
|
+
clearedLinks: links.count ?? 0,
|
|
162
|
+
clearedRecallItems: recItems.count ?? 0,
|
|
163
|
+
clearedSupersedePointers: pointers.count ?? 0,
|
|
164
|
+
auditId: log.id,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function registerMemoryCompatTools(server: ToolRegistrar) {
|
|
169
|
+
const runtimeContext = getRuntimeContext()
|
|
170
|
+
|
|
171
|
+
const memoryDb = validatedDelegate<AddMemoryRow>(prisma.addMemory, AddMemoryRowSchema, "AddMemory")
|
|
172
|
+
const linkDb = validatedDelegate<AddMemoryEvidenceLinkRow>(
|
|
173
|
+
prisma.addMemoryEvidenceLink, AddMemoryEvidenceLinkRowSchema, "AddMemoryEvidenceLink",
|
|
174
|
+
)
|
|
175
|
+
const recallItemDb = validatedDelegate<AddMemoryRecallItemRow>(
|
|
176
|
+
prisma.addMemoryRecallItem, AddMemoryRecallItemRowSchema, "AddMemoryRecallItem",
|
|
177
|
+
)
|
|
178
|
+
const auditDb = validatedDelegate<AuditLogRow>(prisma.auditLog, AuditLogRowSchema, "AuditLog")
|
|
179
|
+
const userDb = validatedDelegate<AddUserRow>(prisma.addUser, AddUserRowSchema, "AddUser")
|
|
180
|
+
|
|
181
|
+
function fail(e: unknown) {
|
|
182
|
+
if (isMemoryError(e)) return errorResponse(e.message)
|
|
183
|
+
return errorResponse(`ERR_INTERNAL: ${e instanceof Error ? e.message : String(e)}`)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function ensureSystemUser(): Promise<string> {
|
|
187
|
+
let u = await userDb.findUnique({ where: { username: "ai-assistant" } })
|
|
188
|
+
if (!u) {
|
|
189
|
+
u = await userDb.create({
|
|
190
|
+
data: { id: "ai-assistant", username: "ai-assistant", email: "ai-assistant@internal" },
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
return u.id
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ===== A. v1 shim(5 个弃用门面) =====
|
|
197
|
+
for (const entry of SHIM_TABLE) {
|
|
198
|
+
server.registerTool(
|
|
199
|
+
entry.v1,
|
|
200
|
+
{
|
|
201
|
+
description: `[已弃用] v1 兼容门面 → 请改用 ${entry.mappedTo}。${entry.note}`,
|
|
202
|
+
inputSchema: z.looseObject({}),
|
|
203
|
+
},
|
|
204
|
+
(args: Record<string, unknown>) => {
|
|
205
|
+
try {
|
|
206
|
+
return Promise.resolve(textResponse(JSON.stringify(buildShimPayload(entry.v1, args))))
|
|
207
|
+
} catch (e) {
|
|
208
|
+
return Promise.resolve(fail(e))
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ===== B. forget_memory(合规清除,非 shim:v2 新增运维工具) =====
|
|
215
|
+
server.registerTool(
|
|
216
|
+
"forget_memory",
|
|
217
|
+
{
|
|
218
|
+
description:
|
|
219
|
+
"合规清除(物理删除 + 清证据/召回/supersede 占用 + AuditLog 留痕)。仅限 reason=secret|privacy|compliance;" +
|
|
220
|
+
"普通废弃请用 resolve_memory 的 archive/reject/supersede(状态机,不物理删除)。必须显式 confirm=true。",
|
|
221
|
+
inputSchema: z.object({
|
|
222
|
+
repositoryRef: z.string().describe("仓库标识(必须等于运行时 projectKey)"),
|
|
223
|
+
memoryId: z.string(),
|
|
224
|
+
reason: z.enum(["secret", "privacy", "compliance"]).describe("清除原因(决定审计口径)"),
|
|
225
|
+
actor: z.string().optional().describe("操作者标识"),
|
|
226
|
+
confirm: z.boolean().describe("必须为 true:物理删除不可恢复"),
|
|
227
|
+
}),
|
|
228
|
+
},
|
|
229
|
+
async (args: Record<string, unknown>, _ctx: unknown) => {
|
|
230
|
+
try {
|
|
231
|
+
if (args.confirm !== true) {
|
|
232
|
+
return errorResponse("ERR_INVARIANT: 物理清除必须显式 confirm=true")
|
|
233
|
+
}
|
|
234
|
+
const result = await purgeMemory(
|
|
235
|
+
{
|
|
236
|
+
repositoryRef: runtimeContext.projectKey,
|
|
237
|
+
context: {
|
|
238
|
+
projectKey: runtimeContext.projectKey,
|
|
239
|
+
adapterKey: runtimeContext.adapterKey,
|
|
240
|
+
contextId: runtimeContext.contextId,
|
|
241
|
+
},
|
|
242
|
+
memoryDb, linkDb, recallItemDb, auditDb,
|
|
243
|
+
ensureUserId: ensureSystemUser,
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
memoryId: args.memoryId as string,
|
|
247
|
+
repositoryRef: args.repositoryRef as string,
|
|
248
|
+
reason: args.reason as PurgeReason,
|
|
249
|
+
actor: args.actor as string | undefined,
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
return textResponse(JSON.stringify(result))
|
|
253
|
+
} catch (e) {
|
|
254
|
+
return fail(e)
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
)
|
|
258
|
+
}
|