ppxans-harness 2.4.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.
- package/LICENSE +201 -0
- package/README.md +265 -0
- package/bin/ppx-channels.js +3 -0
- package/bin/ppx-serve.js +6 -0
- package/bin/ppx.js +3 -0
- package/config/identity.md +6 -0
- package/config/ishiki.md +16 -0
- package/config/ppx.json +151 -0
- package/package.json +69 -0
- package/src/agent/context.js +179 -0
- package/src/agent/index.js +717 -0
- package/src/agent/prompts.js +107 -0
- package/src/aml-server.js +151 -0
- package/src/ans/eviction.js +144 -0
- package/src/ans/guard.js +120 -0
- package/src/ans/lifecycle.js +93 -0
- package/src/ans/proactive.js +129 -0
- package/src/ans/reward.js +112 -0
- package/src/ans/values.js +15 -0
- package/src/audit/audit-chain.js +167 -0
- package/src/audit/verifier.js +120 -0
- package/src/bus/circuit-breaker.js +115 -0
- package/src/bus/runtime-bus.js +94 -0
- package/src/channels/base.js +35 -0
- package/src/channels/feishu.js +127 -0
- package/src/channels/http.js +592 -0
- package/src/channels/index.js +110 -0
- package/src/channels/log.js +29 -0
- package/src/channels/wechat-crypto.js +74 -0
- package/src/channels/wechat.js +197 -0
- package/src/channels-cli.js +124 -0
- package/src/cli.js +120 -0
- package/src/config/channels.js +170 -0
- package/src/config/index.js +224 -0
- package/src/config/providers.js +189 -0
- package/src/config/settings.js +182 -0
- package/src/core/policy.js +272 -0
- package/src/core/trace.js +89 -0
- package/src/evolve/playbook.js +194 -0
- package/src/llm/client.js +446 -0
- package/src/llm/dsml.js +74 -0
- package/src/llm/embedder.js +35 -0
- package/src/llm/fence.js +105 -0
- package/src/llm/index.js +4 -0
- package/src/llm/retry.js +73 -0
- package/src/llm/router.js +98 -0
- package/src/mcp/client.js +375 -0
- package/src/mcp/index.js +116 -0
- package/src/memory/asset-hub.js +131 -0
- package/src/memory/canvas.js +131 -0
- package/src/memory/compaction.js +28 -0
- package/src/memory/experience.js +122 -0
- package/src/memory/fact-store.js +699 -0
- package/src/memory/failure-episode.js +99 -0
- package/src/memory/fork.js +83 -0
- package/src/memory/index.js +7 -0
- package/src/memory/l0.js +52 -0
- package/src/memory/l2.js +131 -0
- package/src/memory/l3.js +112 -0
- package/src/memory/memory-ticker.js +240 -0
- package/src/memory/session.js +398 -0
- package/src/mode/blackboard.js +49 -0
- package/src/mode/graph.js +41 -0
- package/src/mode/index.js +64 -0
- package/src/mode/legion.js +51 -0
- package/src/mode/plan-exec.js +50 -0
- package/src/mode/router.js +40 -0
- package/src/orchestrator/agent-worker.js +70 -0
- package/src/orchestrator/dag.js +83 -0
- package/src/orchestrator/index.js +2 -0
- package/src/orchestrator/legion.js +188 -0
- package/src/orchestrator/supervisor.js +177 -0
- package/src/persona/index.js +29 -0
- package/src/plugin/builtin.js +212 -0
- package/src/plugin/context.js +79 -0
- package/src/plugin/index.js +62 -0
- package/src/seam/registry.js +98 -0
- package/src/seam/shell.js +55 -0
- package/src/selfheal/evolve.js +68 -0
- package/src/selfheal/healer.js +167 -0
- package/src/selfheal/run.js +9 -0
- package/src/server.js +60 -0
- package/src/services/learning-service.js +177 -0
- package/src/services/memory-health.js +99 -0
- package/src/services/memory-service.js +160 -0
- package/src/skills/loader.js +150 -0
- package/src/skills/verify.js +100 -0
- package/src/tools/advanced.js +353 -0
- package/src/tools/builtin.js +298 -0
- package/src/tools/catalog.js +159 -0
- package/src/tools/command-guard.js +112 -0
- package/src/tools/custom.js +47 -0
- package/src/tools/delegate.js +297 -0
- package/src/tools/document.js +253 -0
- package/src/tools/governance.js +260 -0
- package/src/tools/index.js +11 -0
- package/src/tools/methods.js +178 -0
- package/src/tools/ocr.js +59 -0
- package/src/tools/seam.js +125 -0
- package/src/tools/selfmod.js +176 -0
- package/src/utils/logger.js +17 -0
- package/src/utils/pii.js +42 -0
- package/src/utils/store.js +108 -0
- package/src/utils/text.js +16 -0
- package/src/utils/trace.js +153 -0
- package/src/utils/winutf8.js +15 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// src/tools/document.js - 文档加载器 (对标 LangChain Document Loaders, 零依赖)
|
|
2
|
+
// 支持: txt / md / json / csv / html / pdf (文字型, 扫描件需 OCR)
|
|
3
|
+
// PDF 零依赖提取: 解压 FlateDecode 流 (zlib) + 提取 Tj/TJ 文本操作符
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import zlib from "node:zlib";
|
|
8
|
+
import { ocrImage } from "./ocr.js";
|
|
9
|
+
|
|
10
|
+
const MAX_CHARS = 20000; // 单文档返回上限
|
|
11
|
+
|
|
12
|
+
// 安全路径: 阻止逃出工作目录 (防路径穿越, 与 builtin.js 同策略)
|
|
13
|
+
function safePath(root, p) {
|
|
14
|
+
const resolved = path.resolve(root, p);
|
|
15
|
+
if (resolved !== root && !resolved.startsWith(root + path.sep)) {
|
|
16
|
+
throw new Error(`路径越界拒绝: ${p}`);
|
|
17
|
+
}
|
|
18
|
+
return resolved;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 解码 PDF 文本字符串: 处理 UTF-16BE (FE FF BOM) 与 UTF-8 与转义字符
|
|
22
|
+
function decodePdfString(latin1Str) {
|
|
23
|
+
let s = String(latin1Str || "").replace(/\\(\r?\n)/g, ""); // 续行
|
|
24
|
+
const buf = Buffer.from(s, "latin1");
|
|
25
|
+
// UTF-16BE BOM
|
|
26
|
+
if (buf.length >= 2 && buf[0] === 0xfe && buf[1] === 0xff) {
|
|
27
|
+
return buf.slice(2).toString("utf16le");
|
|
28
|
+
}
|
|
29
|
+
// 字节级反转义 \( \) \\
|
|
30
|
+
const out = [];
|
|
31
|
+
for (let i = 0; i < buf.length; i++) {
|
|
32
|
+
if (buf[i] === 0x5c && i + 1 < buf.length) {
|
|
33
|
+
const n = buf[i + 1];
|
|
34
|
+
if (n === 0x28 || n === 0x29 || n === 0x5c) { out.push(n); i++; continue; }
|
|
35
|
+
}
|
|
36
|
+
out.push(buf[i]);
|
|
37
|
+
}
|
|
38
|
+
const clean = Buffer.from(out);
|
|
39
|
+
const utf8 = clean.toString("utf8");
|
|
40
|
+
return utf8.includes("\uFFFD") ? clean.toString("latin1") : utf8;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 零依赖 PDF 文本提取: 遍历 stream, FlateDecode 解压, 提取 (text) Tj 与 [(a)(b)] TJ
|
|
44
|
+
export function extractPdfText(buf) {
|
|
45
|
+
const raw = buf.toString("latin1");
|
|
46
|
+
const texts = [];
|
|
47
|
+
const streamRe = /stream\r?\n([\s\S]*?)endstream/g;
|
|
48
|
+
let m;
|
|
49
|
+
while ((m = streamRe.exec(raw)) !== null) {
|
|
50
|
+
let data = m[1];
|
|
51
|
+
// 尝试 FlateDecode 解压 (内容流通常是压缩的)
|
|
52
|
+
try {
|
|
53
|
+
const inf = zlib.inflateSync(Buffer.from(data, "latin1")).toString("latin1");
|
|
54
|
+
if (inf.length > 0) data = inf;
|
|
55
|
+
} catch { /* 未压缩则用原文 */ }
|
|
56
|
+
// (text) Tj
|
|
57
|
+
for (const tm of data.matchAll(/\(([^)]*)\)\s*Tj/g)) {
|
|
58
|
+
const t = decodePdfString(tm[1]).trim();
|
|
59
|
+
if (t) texts.push(t);
|
|
60
|
+
}
|
|
61
|
+
// [(a)(b)] TJ (数组形式)
|
|
62
|
+
for (const tm of data.matchAll(/\[((?:\([^)]*\)[\s<>0-9.-]*)+)\]\s*TJ/g)) {
|
|
63
|
+
for (const pm of tm[1].matchAll(/\(([^)]*)\)/g)) {
|
|
64
|
+
const t = decodePdfString(pm[1]).trim();
|
|
65
|
+
if (t) texts.push(t);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return texts.join(" ");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 提取 PDF 内嵌 JPEG 图片 (DCTDecode 流, 扫描件 PDF 的页面图), 供 OCR
|
|
73
|
+
export function extractPdfJpegs(buf) {
|
|
74
|
+
const raw = buf.toString("latin1");
|
|
75
|
+
const jpegs = [];
|
|
76
|
+
const re = /\/DCTDecode[\s\S]{0,200}?stream\r?\n([\s\S]*?)endstream/g;
|
|
77
|
+
let m;
|
|
78
|
+
while ((m = re.exec(raw)) !== null) {
|
|
79
|
+
const data = Buffer.from(m[1], "latin1");
|
|
80
|
+
// JPEG 魔数 FFD8
|
|
81
|
+
if (data.length > 2 && data[0] === 0xff && data[1] === 0xd8) jpegs.push(data);
|
|
82
|
+
}
|
|
83
|
+
return jpegs;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// HTML 转纯文本 (基础 strip, 与 advanced.js fetch_page 同思路)
|
|
87
|
+
function htmlToText(html) {
|
|
88
|
+
return String(html || "")
|
|
89
|
+
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
90
|
+
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
91
|
+
.replace(/<br\s*\/?>|<\/p>|<\/div>|<\/li>|<\/h[1-6]>|<\/tr>/gi, "\n")
|
|
92
|
+
.replace(/<[^>]+>/g, " ")
|
|
93
|
+
.replace(/ /gi, " ").replace(/&/gi, "&").replace(/</gi, "<").replace(/>/gi, ">")
|
|
94
|
+
.replace(/\s+/g, " ")
|
|
95
|
+
.trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 按扩展名提取文档文本 (纯函数, 供 read_document 与 ingest_document 复用)
|
|
99
|
+
export function extractDocumentText(filePath) {
|
|
100
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
101
|
+
if (!fs.existsSync(filePath)) throw new Error(`文件不存在: ${filePath}`);
|
|
102
|
+
const buf = fs.readFileSync(filePath);
|
|
103
|
+
switch (ext) {
|
|
104
|
+
case ".txt": case ".md": case ".csv": case ".json": case ".log":
|
|
105
|
+
return buf.toString("utf8");
|
|
106
|
+
case ".html": case ".htm":
|
|
107
|
+
return htmlToText(buf.toString("utf8"));
|
|
108
|
+
case ".pdf":
|
|
109
|
+
return extractPdfText(buf);
|
|
110
|
+
default:
|
|
111
|
+
throw new Error(`不支持的文档类型: ${ext} (支持 txt/md/csv/json/html/pdf)`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 分块: 按段落切, 每块约 chunkSize 字 (供 ingest 向量化)
|
|
116
|
+
export function splitChunks(text, chunkSize = 500) {
|
|
117
|
+
const clean = String(text || "").replace(/\r/g, "");
|
|
118
|
+
const paras = clean.split(/\n{2,}/).map((p) => p.trim()).filter(Boolean);
|
|
119
|
+
const chunks = [];
|
|
120
|
+
let cur = "";
|
|
121
|
+
for (const p of paras) {
|
|
122
|
+
if ((cur + p).length > chunkSize && cur) {
|
|
123
|
+
chunks.push(cur.trim());
|
|
124
|
+
cur = p;
|
|
125
|
+
} else {
|
|
126
|
+
cur = cur ? cur + "\n" + p : p;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (cur.trim()) chunks.push(cur.trim());
|
|
130
|
+
return chunks.filter((c) => c.length >= 10);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 从 config 构造 OCR 选项 (未显式关闭时默认启用本地 tesseract 自动 OCR)
|
|
134
|
+
function ocrOptsFromConfig(cfg) {
|
|
135
|
+
const c = (cfg && cfg.ocr) || {};
|
|
136
|
+
if (c.auto === false) return null; // 显式关闭自动 OCR
|
|
137
|
+
return { tesseract: c.tesseract || "tesseract", lang: c.lang || "chi_sim", cloud: c.cloud || null };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 读文档文本, PDF 无文本层(扫描件)时自动提取内嵌图片 OCR (_ocrFn 供测试注入)
|
|
141
|
+
export async function readDocumentText(filePath, ocrOpts, _ocrFn = ocrImage) {
|
|
142
|
+
let text = extractDocumentText(filePath);
|
|
143
|
+
if (path.extname(filePath).toLowerCase() === ".pdf" && !text.trim() && ocrOpts) {
|
|
144
|
+
const jpegs = extractPdfJpegs(fs.readFileSync(filePath));
|
|
145
|
+
if (jpegs.length) {
|
|
146
|
+
const parts = [];
|
|
147
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ppx-ocr-pdf-"));
|
|
148
|
+
try {
|
|
149
|
+
for (let i = 0; i < jpegs.length; i++) {
|
|
150
|
+
const tmp = path.join(tmpDir, `page-${i + 1}.jpg`);
|
|
151
|
+
fs.writeFileSync(tmp, jpegs[i]);
|
|
152
|
+
try { parts.push(await _ocrFn(tmp, ocrOpts)); } catch {}
|
|
153
|
+
}
|
|
154
|
+
} finally {
|
|
155
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
text = parts.filter(Boolean).join("\n").trim();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return text;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 注册文档工具
|
|
164
|
+
export function registerDocumentTools(catalog, { rootDir }) {
|
|
165
|
+
// 1. 读文档 (加载器, PDF 扫描件自动 OCR)
|
|
166
|
+
catalog.register({
|
|
167
|
+
name: "read_document",
|
|
168
|
+
description: "读取本地文档并转纯文本。支持 .txt/.md/.json/.csv/.html/.pdf (文字型 PDF 直接提取, 扫描件 PDF 自动 OCR)。用于读文档/报告/数据文件后回答问题。",
|
|
169
|
+
parameters: {
|
|
170
|
+
type: "object",
|
|
171
|
+
properties: {
|
|
172
|
+
path: { type: "string", description: "文档路径 (相对工作目录)" },
|
|
173
|
+
maxChars: { type: "number", description: "返回最大字符数, 默认 20000" },
|
|
174
|
+
},
|
|
175
|
+
required: ["path"],
|
|
176
|
+
},
|
|
177
|
+
execute: async (args, ctx) => {
|
|
178
|
+
try {
|
|
179
|
+
const p = safePath(rootDir, args.path);
|
|
180
|
+
const cfg = (ctx && ctx.agent && ctx.agent.config) || {};
|
|
181
|
+
const text = await readDocumentText(p, ocrOptsFromConfig(cfg));
|
|
182
|
+
const max = Math.min(args.maxChars || MAX_CHARS, 40000);
|
|
183
|
+
const truncated = text.length > max ? text.slice(0, max) + `\n...[已截断, 共 ${text.length} 字符]` : text;
|
|
184
|
+
return truncated || "(文档无文本内容, 且未识别出扫描件文字)";
|
|
185
|
+
} catch (e) {
|
|
186
|
+
return JSON.stringify({ error: "read_document 失败: " + e.message });
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// 2. OCR 识别图片/扫描件文字
|
|
192
|
+
catalog.register({
|
|
193
|
+
name: "ocr_image",
|
|
194
|
+
description: "识别图片或扫描件里的文字 (OCR)。需系统安装 tesseract (含中文语言包) 或配置 config.ocr 云 key。用于 read_image/read_document 读到图片却无法理解文字时。",
|
|
195
|
+
parameters: {
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: {
|
|
198
|
+
path: { type: "string", description: "图片或扫描件路径 (相对工作目录)" },
|
|
199
|
+
lang: { type: "string", description: "识别语言, 默认 chi_sim (中文)" },
|
|
200
|
+
},
|
|
201
|
+
required: ["path"],
|
|
202
|
+
},
|
|
203
|
+
execute: async (args, ctx) => {
|
|
204
|
+
const agent = ctx && ctx.agent;
|
|
205
|
+
const cfg = (agent && agent.config && agent.config.ocr) || {};
|
|
206
|
+
try {
|
|
207
|
+
const p = safePath(rootDir, args.path);
|
|
208
|
+
const text = await ocrImage(p, {
|
|
209
|
+
tesseract: cfg.tesseract || "tesseract",
|
|
210
|
+
lang: args.lang || cfg.lang || "chi_sim",
|
|
211
|
+
cloud: cfg.cloud || null,
|
|
212
|
+
});
|
|
213
|
+
return text || "(未识别出文字)";
|
|
214
|
+
} catch (e) {
|
|
215
|
+
return JSON.stringify({ error: "ocr_image 失败: " + e.message });
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// 3. 文档入库 (RAG): 读文档 → 分块 → 存记忆 (带 scope 隔离)
|
|
221
|
+
catalog.register({
|
|
222
|
+
name: "ingest_document",
|
|
223
|
+
description: "读取文档, 分块后写入长期记忆 (RAG 入库), 之后可被语义检索命中。scope 用于隔离文档来源 (如 '公司制度'/'项目文档'), 避免与其他记忆混淆。",
|
|
224
|
+
parameters: {
|
|
225
|
+
type: "object",
|
|
226
|
+
properties: {
|
|
227
|
+
path: { type: "string", description: "文档路径 (相对工作目录)" },
|
|
228
|
+
scope: { type: "string", description: "文档来源标签 (可选, 便于按来源检索)" },
|
|
229
|
+
},
|
|
230
|
+
required: ["path"],
|
|
231
|
+
},
|
|
232
|
+
execute: async (args, ctx) => {
|
|
233
|
+
const agent = ctx && ctx.agent;
|
|
234
|
+
if (!agent || !agent.facts) return JSON.stringify({ error: "ingest_document: 缺少 agent 上下文" });
|
|
235
|
+
try {
|
|
236
|
+
const p = safePath(rootDir, args.path);
|
|
237
|
+
const text = await readDocumentText(p, ocrOptsFromConfig(agent.config));
|
|
238
|
+
const chunks = splitChunks(text, 500);
|
|
239
|
+
if (!chunks.length) return JSON.stringify({ error: "文档无可入库的文本 (可能是扫描件 PDF, 且 OCR 不可用)" });
|
|
240
|
+
let added = 0;
|
|
241
|
+
for (const c of chunks) {
|
|
242
|
+
const f = agent.facts.add(c, { source: "document", scope: args.scope || null, dedupe: false });
|
|
243
|
+
if (f) added++;
|
|
244
|
+
}
|
|
245
|
+
return JSON.stringify({ ok: true, chunks: chunks.length, added, scope: args.scope || null });
|
|
246
|
+
} catch (e) {
|
|
247
|
+
return JSON.stringify({ error: "ingest_document 失败: " + e.message });
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return catalog;
|
|
253
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// src/tools/governance.js - 记忆治理 + 审计 + 运维工具集 (吸收自 ppx-v2)
|
|
2
|
+
// 来源: ppx-v2 v0.4.0 bundles/ppx-tools/index.js 的 5 个治理工具 (memory_forget/restore/export/import/clear_layer)
|
|
3
|
+
// + audit_verify, 以及 ppx-memory/ppx-selfheal 暴露的 persona_build/persona_read/selfheal_run。
|
|
4
|
+
// 改写: ppx-v2 原版用 cordis 的 defineTool + @deepseek-ai/schemastery (z.object) 声明,
|
|
5
|
+
// 这里改写为 ppx-agent 原生的 ToolCatalog.register 风格, 剥离全部外部依赖。
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { safePath } from "./builtin.js";
|
|
9
|
+
import { ensureDir, nowISO } from "../utils/store.js";
|
|
10
|
+
|
|
11
|
+
// 记忆治理 + 审计 + 运维工具注册
|
|
12
|
+
// deps: { rootDir, facts, audit, personaStore, healer, experience, dataDir }
|
|
13
|
+
export function registerGovernanceTools(catalog, { rootDir, facts, audit, personaStore, healer, experience, dataDir } = {}) {
|
|
14
|
+
const exportsDir = path.join(rootDir || dataDir || ".", "exports");
|
|
15
|
+
|
|
16
|
+
// 1. 遗忘 (软删, 可回滚) —— ppx-agent 原版只有不可逆硬删, 这是最大的治理缺口
|
|
17
|
+
catalog.register({
|
|
18
|
+
name: "memory_forget",
|
|
19
|
+
description: "遗忘一条记忆 (软删, 数据保留可用 memory_restore 回滚)。传 id 或内容关键字定位。",
|
|
20
|
+
parameters: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
id_or_content: { type: "string", description: "记忆 id 或内容 (按内容精确匹配, 去记忆动词前缀后比对)" },
|
|
24
|
+
reason: { type: "string", description: "遗忘原因 (记入审计, 便于后续复核)" },
|
|
25
|
+
},
|
|
26
|
+
required: ["id_or_content"],
|
|
27
|
+
},
|
|
28
|
+
category: "memory",
|
|
29
|
+
power: "user",
|
|
30
|
+
idempotent: true,
|
|
31
|
+
execute: async (args) => {
|
|
32
|
+
if (!facts) return JSON.stringify({ error: "记忆未初始化" });
|
|
33
|
+
const f = facts.forget(args.id_or_content, { reason: args.reason || null });
|
|
34
|
+
if (!f) return JSON.stringify({ error: "未找到匹配记忆", target: args.id_or_content });
|
|
35
|
+
return JSON.stringify({ ok: true, id: f.id, content: f.content, status: f.status, note: "已软删, 可用 memory_restore 回滚" });
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// 2. 回滚遗忘
|
|
40
|
+
catalog.register({
|
|
41
|
+
name: "memory_restore",
|
|
42
|
+
description: "恢复一条被遗忘 (软删) 的记忆。",
|
|
43
|
+
parameters: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: { id: { type: "string", description: "记忆 id" } },
|
|
46
|
+
required: ["id"],
|
|
47
|
+
},
|
|
48
|
+
category: "memory",
|
|
49
|
+
power: "user",
|
|
50
|
+
idempotent: true,
|
|
51
|
+
execute: async (args) => {
|
|
52
|
+
if (!facts) return JSON.stringify({ error: "记忆未初始化" });
|
|
53
|
+
const f = facts.restore(args.id);
|
|
54
|
+
if (!f) return JSON.stringify({ error: "未找到该记忆", id: args.id });
|
|
55
|
+
return JSON.stringify({ ok: true, id: f.id, content: f.content, status: f.status });
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// 3. 查看已遗忘的记忆 (复核入口)
|
|
60
|
+
catalog.register({
|
|
61
|
+
name: "memory_list_deleted",
|
|
62
|
+
description: "列出已遗忘 (软删) 的记忆, 供人工/审计复核, 避免误删无法发现。",
|
|
63
|
+
parameters: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: { limit: { type: "number", description: "最多返回条数, 默认 20" } },
|
|
66
|
+
required: [],
|
|
67
|
+
},
|
|
68
|
+
category: "memory",
|
|
69
|
+
power: "user",
|
|
70
|
+
idempotent: true,
|
|
71
|
+
execute: async (args) => {
|
|
72
|
+
if (!facts) return JSON.stringify({ error: "记忆未初始化" });
|
|
73
|
+
const list = facts.deletedList().slice(0, args.limit || 20);
|
|
74
|
+
if (!list.length) return "(无已遗忘的记忆)";
|
|
75
|
+
return list.map((f) => `- ${f.id} | ${f.deletedAt || "?"} | ${f.deleteReason || "无原因"} | ${f.content}`).join("\n");
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// 4. 导出记忆 (备份/迁移)
|
|
80
|
+
catalog.register({
|
|
81
|
+
name: "memory_export",
|
|
82
|
+
description: "导出全量记忆到 JSON 文件 (含软删/归档条目), 用于备份与跨机迁移。",
|
|
83
|
+
parameters: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
file: { type: "string", description: "相对工作区的输出路径, 省略则写到 exports/memory-<时间戳>.json" },
|
|
87
|
+
include_deleted: { type: "boolean", description: "是否包含已软删/归档条目, 默认 true" },
|
|
88
|
+
},
|
|
89
|
+
required: [],
|
|
90
|
+
},
|
|
91
|
+
category: "memory",
|
|
92
|
+
power: "user",
|
|
93
|
+
idempotent: true,
|
|
94
|
+
execute: async (args) => {
|
|
95
|
+
if (!facts) return JSON.stringify({ error: "记忆未初始化" });
|
|
96
|
+
const dump = facts.exportAll({ includeDeleted: args.include_deleted !== false });
|
|
97
|
+
let out;
|
|
98
|
+
if (args.file) {
|
|
99
|
+
out = safePath(rootDir || ".", args.file);
|
|
100
|
+
} else {
|
|
101
|
+
ensureDir(exportsDir);
|
|
102
|
+
out = path.join(exportsDir, `memory-${Date.now()}.json`);
|
|
103
|
+
}
|
|
104
|
+
ensureDir(path.dirname(out));
|
|
105
|
+
fs.writeFileSync(out, JSON.stringify(dump, null, 2), "utf8");
|
|
106
|
+
return JSON.stringify({ ok: true, file: out, count: dump.count, exportedAt: dump.exportedAt });
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// 5. 导入记忆 (merge 去重 / replace 整体替换)
|
|
111
|
+
catalog.register({
|
|
112
|
+
name: "memory_import",
|
|
113
|
+
description: "从 JSON 文件导入记忆。mode=merge 按内容去重跳过重复 (默认), mode=replace 整体替换。",
|
|
114
|
+
parameters: {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: {
|
|
117
|
+
file: { type: "string", description: "相对工作区的 JSON 文件路径 (memory_export 的产物)" },
|
|
118
|
+
mode: { type: "string", description: "merge (默认) 或 replace" },
|
|
119
|
+
},
|
|
120
|
+
required: ["file"],
|
|
121
|
+
},
|
|
122
|
+
category: "memory",
|
|
123
|
+
power: "user",
|
|
124
|
+
idempotent: false,
|
|
125
|
+
execute: async (args) => {
|
|
126
|
+
if (!facts) return JSON.stringify({ error: "记忆未初始化" });
|
|
127
|
+
const fp = safePath(rootDir || ".", args.file);
|
|
128
|
+
if (!fs.existsSync(fp)) return JSON.stringify({ error: "文件不存在", file: args.file });
|
|
129
|
+
let payload;
|
|
130
|
+
try { payload = JSON.parse(fs.readFileSync(fp, "utf8")); } catch (e) { return JSON.stringify({ error: "JSON 解析失败: " + e.message }); }
|
|
131
|
+
const r = facts.importAll(payload, { mode: args.mode === "replace" ? "replace" : "merge" });
|
|
132
|
+
return JSON.stringify(r);
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// 6. 按层清空 (L1 事实 / L4 程序性记忆)
|
|
137
|
+
catalog.register({
|
|
138
|
+
name: "memory_clear_layer",
|
|
139
|
+
description: "按记忆层级批量清空。layer=1 是事实/用户记忆, layer=4 是程序性记忆(技能/流程)。默认软删可回滚, hard=true 才物理删除。",
|
|
140
|
+
parameters: {
|
|
141
|
+
type: "object",
|
|
142
|
+
properties: {
|
|
143
|
+
layer: { type: "number", description: "记忆层级: 1 (事实) 或 4 (程序性)" },
|
|
144
|
+
hard: { type: "boolean", description: "true 则物理删除不可回滚, 默认 false (软删)" },
|
|
145
|
+
},
|
|
146
|
+
required: ["layer"],
|
|
147
|
+
},
|
|
148
|
+
category: "memory",
|
|
149
|
+
power: "user",
|
|
150
|
+
idempotent: false,
|
|
151
|
+
execute: async (args) => {
|
|
152
|
+
if (!facts) return JSON.stringify({ error: "记忆未初始化" });
|
|
153
|
+
const r = facts.clearLayer(Number(args.layer), { hard: args.hard === true });
|
|
154
|
+
return JSON.stringify({ ok: true, ...r, note: r.hard ? "已物理删除" : "已软删, 可用 memory_restore 逐条回滚" });
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// 7. 审计链校验 (吸收自 ppx-v2 audit.js: SHA-256 哈希链防篡改验证)
|
|
159
|
+
catalog.register({
|
|
160
|
+
name: "audit_verify",
|
|
161
|
+
description: "校验工具调用审计日志的 SHA-256 哈希链完整性, 定位首个被篡改/截断的位置。quarantine=true 时隔离损坏段并重建空链。",
|
|
162
|
+
parameters: {
|
|
163
|
+
type: "object",
|
|
164
|
+
properties: {
|
|
165
|
+
quarantine: { type: "boolean", description: "校验失败时是否自动隔离损坏日志并重建, 默认 false" },
|
|
166
|
+
tail: { type: "number", description: "附带返回最近 n 条审计记录, 默认 0 (不返回)" },
|
|
167
|
+
},
|
|
168
|
+
required: [],
|
|
169
|
+
},
|
|
170
|
+
category: "system",
|
|
171
|
+
power: "user",
|
|
172
|
+
idempotent: true,
|
|
173
|
+
execute: async (args) => {
|
|
174
|
+
if (!audit) return JSON.stringify({ error: "审计未启用 (config.audit.enabled = false)" });
|
|
175
|
+
let v = audit.verify();
|
|
176
|
+
let quarantined = false;
|
|
177
|
+
let backup = null;
|
|
178
|
+
if (!v.ok && args.quarantine) {
|
|
179
|
+
const { quarantineBroken } = await import("../audit/audit-chain.js");
|
|
180
|
+
const q = quarantineBroken(audit.dataDir);
|
|
181
|
+
quarantined = !!q.quarantined;
|
|
182
|
+
backup = q.backup || null;
|
|
183
|
+
v = audit.verify();
|
|
184
|
+
}
|
|
185
|
+
const out = { ok: v.ok, total: v.total, brokenAt: v.brokenAt, detail: v.detail, quarantined, backup };
|
|
186
|
+
if (args.tail) out.recent = audit.tail(Number(args.tail) || 10);
|
|
187
|
+
return JSON.stringify(out);
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// 8. 画像重建 (ppx-v2 ppx-memory 的 persona_build, 这里薄包装 l3 PersonaStore)
|
|
192
|
+
catalog.register({
|
|
193
|
+
name: "persona_build",
|
|
194
|
+
description: "从当前记忆与经验重新提炼用户画像 / agent 人格, 写入 L3 层。",
|
|
195
|
+
parameters: {
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: {
|
|
198
|
+
target: { type: "string", description: "user (默认) 或 agent" },
|
|
199
|
+
force: { type: "boolean", description: "true 则忽略频率限制强制重建" },
|
|
200
|
+
},
|
|
201
|
+
required: [],
|
|
202
|
+
},
|
|
203
|
+
category: "memory",
|
|
204
|
+
power: "user",
|
|
205
|
+
idempotent: false,
|
|
206
|
+
execute: async (args) => {
|
|
207
|
+
if (!personaStore) return JSON.stringify({ error: "画像存储未初始化" });
|
|
208
|
+
const target = args.target === "agent" ? "agent" : "user";
|
|
209
|
+
if (target === "agent") {
|
|
210
|
+
const lessons = experience ? (experience.list?.() || []) : [];
|
|
211
|
+
const r = personaStore.buildAgentPersona(lessons, { force: args.force === true });
|
|
212
|
+
return JSON.stringify({ ok: true, target, ...(typeof r === "object" ? r : { built: true }) });
|
|
213
|
+
}
|
|
214
|
+
const factsList = facts ? facts.list() : [];
|
|
215
|
+
const r = personaStore.buildUserPersona(factsList, { force: args.force === true });
|
|
216
|
+
return JSON.stringify({ ok: true, target, ...(typeof r === "object" ? r : { built: true }) });
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// 9. 画像读取
|
|
221
|
+
catalog.register({
|
|
222
|
+
name: "persona_read",
|
|
223
|
+
description: "读取 L3 层沉淀的用户画像 / agent 人格全文。",
|
|
224
|
+
parameters: {
|
|
225
|
+
type: "object",
|
|
226
|
+
properties: { target: { type: "string", description: "user (默认) 或 agent" } },
|
|
227
|
+
required: [],
|
|
228
|
+
},
|
|
229
|
+
category: "memory",
|
|
230
|
+
power: "user",
|
|
231
|
+
idempotent: true,
|
|
232
|
+
execute: async (args) => {
|
|
233
|
+
if (!personaStore) return JSON.stringify({ error: "画像存储未初始化" });
|
|
234
|
+
const target = args.target === "agent" ? "agent" : "user";
|
|
235
|
+
const text = target === "agent" ? personaStore.agentPersona() : personaStore.userPersona();
|
|
236
|
+
return text || "(画像尚未生成, 可用 persona_build 提炼)";
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// 10. 自愈体检 (ppx-v2 selfheal_run, 薄包装 Healer)
|
|
241
|
+
catalog.register({
|
|
242
|
+
name: "selfheal_run",
|
|
243
|
+
description: "手动跑一次自愈体检: 补建缺失目录、修复损坏 JSON、清理崩溃残留与过期备份。",
|
|
244
|
+
parameters: {
|
|
245
|
+
type: "object",
|
|
246
|
+
properties: {},
|
|
247
|
+
required: [],
|
|
248
|
+
},
|
|
249
|
+
category: "system",
|
|
250
|
+
power: "user",
|
|
251
|
+
idempotent: true,
|
|
252
|
+
execute: async () => {
|
|
253
|
+
if (!healer) return JSON.stringify({ error: "自愈引擎未初始化" });
|
|
254
|
+
const health = healer.heal();
|
|
255
|
+
return JSON.stringify({ ok: true, healedAt: nowISO(), health });
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return catalog;
|
|
260
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/tools/index.js - 工具系统统一出口
|
|
2
|
+
export { ToolCatalog, TOOL_ERROR_PREFIX } from "./catalog.js";
|
|
3
|
+
export { normalizeMeta, runWithPolicy, toDescriptor } from "./seam.js";
|
|
4
|
+
export { registerBuiltinTools } from "./builtin.js";
|
|
5
|
+
export { registerAdvancedTools, Scheduler } from "./advanced.js";
|
|
6
|
+
export { registerMethodTools } from "./methods.js";
|
|
7
|
+
export { registerSelfmodTools } from "./selfmod.js";
|
|
8
|
+
export { registerCustomTools } from "./custom.js";
|
|
9
|
+
export { registerDocumentTools } from "./document.js";
|
|
10
|
+
// 记忆治理 + 审计 + 运维 (吸收自 ppx-v2)
|
|
11
|
+
export { registerGovernanceTools } from "./governance.js";
|