ppxans-harness 2.6.0 → 3.2.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 -201
- package/README.md +218 -262
- package/bin/ppx-channels.js +2 -2
- package/bin/ppx-serve.js +5 -5
- package/bin/ppx-setup.js +124 -0
- package/bin/ppx-web.js +140 -0
- package/bin/ppx.js +2 -2
- package/config/identity.md +6 -6
- package/config/ishiki.md +16 -16
- package/config/ppx.json +6 -124
- package/config/ppx.json.example +143 -0
- package/package.json +16 -6
- package/skills/.usage.json +6 -0
- package/skills/agent-professional-training/SKILL.md +94 -0
- package/skills/brainstorm/SKILL.md +24 -0
- package/skills/cupid-lover-comms/SKILL.md +37 -0
- package/skills/debug/SKILL.md +26 -0
- package/skills/plan/SKILL.md +25 -0
- package/skills/ponytail/SKILL.md +25 -0
- package/skills/ppx-memory/SKILL.md +91 -0
- package/skills/ppx-memory/scripts/cli.js +192 -0
- package/skills/ppx-memory/scripts/experience.js +133 -0
- package/skills/ppx-memory/scripts/fact-store.js +842 -0
- package/skills/ppx-memory/scripts/l0.js +52 -0
- package/skills/ppx-memory/scripts/l2.js +146 -0
- package/skills/ppx-memory/scripts/l3.js +112 -0
- package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
- package/skills/ppx-memory/scripts/pii.js +42 -0
- package/skills/ppx-memory/scripts/schema.js +80 -0
- package/skills/ppx-memory/scripts/session.js +398 -0
- package/skills/ppx-memory/scripts/similarity.js +43 -0
- package/skills/ppx-memory/scripts/store.js +116 -0
- package/skills/ppx-memory/scripts/wal.js +38 -0
- package/skills/ppx-selfheal/SKILL.md +24 -0
- package/skills/ppx-selfheal/scripts/cli.js +80 -0
- package/skills/ppx-selfheal/scripts/healer.js +184 -0
- package/skills/ppx-selfheal/scripts/logger.js +17 -0
- package/skills/ppx-selfheal/scripts/store.js +116 -0
- package/skills/prompt-depth-kit/SKILL.md +28 -0
- package/skills/session-naming/SKILL.md +36 -0
- package/skills/verify/SKILL.md +25 -0
- package/src/agent/index.js +1340 -717
- package/src/agent/prompts.js +46 -2
- package/src/aml-server.js +197 -151
- package/src/ans/eviction.js +123 -143
- package/src/ans/guard.js +159 -120
- package/src/ans/lifecycle.js +96 -93
- package/src/ans/proactive.js +112 -129
- package/src/ans/reward.js +95 -111
- package/src/ans/values.js +15 -15
- package/src/audit/audit-chain.js +43 -7
- package/src/audit/verifier.js +157 -120
- package/src/bus/circuit-breaker.js +9 -1
- package/src/bus/runtime-bus.js +107 -93
- package/src/channels/base.js +57 -34
- package/src/channels/feishu.js +118 -126
- package/src/channels/http.js +1090 -651
- package/src/channels/index.js +111 -110
- package/src/channels/log.js +29 -29
- package/src/channels/wechat-crypto.js +73 -74
- package/src/channels/wechat.js +191 -197
- package/src/channels/workspace.js +94 -0
- package/src/channels-cli.js +126 -124
- package/src/cli.js +129 -120
- package/src/commands/index.js +142 -0
- package/src/config/channels.js +137 -170
- package/src/config/index.js +277 -224
- package/src/config/placeholder.js +31 -0
- package/src/config/providers.js +148 -182
- package/src/config/settings.js +156 -182
- package/src/core/policy.js +69 -18
- package/src/core/trace.js +8 -6
- package/src/edit/editblock.js +266 -0
- package/src/edit/snapshot.js +67 -0
- package/src/evidence/index.js +153 -0
- package/src/evolve/playbook.js +9 -10
- package/src/hooks/index.js +113 -0
- package/src/llm/client.js +4 -3
- package/src/llm/dsml.js +74 -74
- package/src/llm/embedder.js +41 -35
- package/src/llm/fence.js +52 -52
- package/src/llm/index.js +4 -4
- package/src/llm/local-embedder.js +94 -0
- package/src/llm/presets.js +113 -0
- package/src/llm/pricing.js +93 -0
- package/src/llm/retry.js +73 -73
- package/src/llm/router.js +92 -89
- package/src/mcp/admin.js +44 -10
- package/src/mcp/client.js +487 -477
- package/src/mcp/http.js +23 -37
- package/src/mcp/index.js +116 -116
- package/src/mcp/server.js +23 -18
- package/src/mcp/tasks.js +9 -14
- package/src/memory/asset-hub.js +6 -11
- package/src/memory/canvas.js +2 -5
- package/src/memory/compaction.js +28 -28
- package/src/memory/experience.js +133 -122
- package/src/memory/fact-store.js +914 -698
- package/src/memory/failure-episode.js +20 -11
- package/src/memory/fork.js +17 -8
- package/src/memory/index.js +8 -6
- package/src/memory/l0.js +53 -52
- package/src/memory/l2.js +145 -130
- package/src/memory/l3.js +111 -111
- package/src/memory/legion-board.js +71 -0
- package/src/memory/memory-ticker.js +239 -240
- package/src/memory/session.js +398 -397
- package/src/memory/sqlite-store.js +581 -0
- package/src/mode/blackboard.js +49 -49
- package/src/mode/graph.js +42 -41
- package/src/mode/index.js +64 -64
- package/src/mode/legion.js +54 -51
- package/src/mode/plan-exec.js +50 -50
- package/src/mode/router.js +28 -40
- package/src/orchestrator/agent-worker.js +69 -69
- package/src/orchestrator/dag.js +90 -83
- package/src/orchestrator/experts.js +76 -0
- package/src/orchestrator/index.js +1 -1
- package/src/orchestrator/legion.js +179 -187
- package/src/orchestrator/supervisor.js +6 -8
- package/src/permissions/index.js +378 -0
- package/src/persona/index.js +28 -29
- package/src/plugin/builtin.js +313 -215
- package/src/plugin/context.js +80 -79
- package/src/plugin/index.js +62 -62
- package/src/plugin/v3.js +73 -0
- package/src/protocol/index.js +148 -0
- package/src/repomap/index.js +309 -0
- package/src/review/index.js +393 -0
- package/src/seam/registry.js +3 -0
- package/src/seam/shell.js +55 -55
- package/src/security/injection.js +79 -0
- package/src/selfheal/evolve.js +67 -67
- package/src/selfheal/healer.js +184 -167
- package/src/selfheal/run.js +9 -9
- package/src/server.js +63 -60
- package/src/services/diagnose.js +180 -0
- package/src/services/learning-service.js +9 -0
- package/src/services/memory-health.js +34 -6
- package/src/services/memory-service.js +42 -9
- package/src/services/triage.js +138 -0
- package/src/session/parts.js +76 -0
- package/src/session/projection.js +73 -0
- package/src/session/rollout.js +54 -0
- package/src/session/turn.js +137 -0
- package/src/skills/lint.js +72 -0
- package/src/skills/loader.js +231 -150
- package/src/skills/search.js +58 -0
- package/src/skills/verify.js +95 -100
- package/src/tools/advanced.js +388 -352
- package/src/tools/builtin.js +384 -297
- package/src/tools/catalog.js +283 -159
- package/src/tools/command-guard.js +112 -112
- package/src/tools/custom.js +47 -47
- package/src/tools/delegate.js +383 -297
- package/src/tools/document.js +254 -253
- package/src/tools/git.js +151 -0
- package/src/tools/governance.js +47 -20
- package/src/tools/index.js +16 -11
- package/src/tools/methods.js +178 -178
- package/src/tools/ocr.js +59 -59
- package/src/tools/sandbox-worker.js +40 -0
- package/src/tools/sandbox.js +92 -0
- package/src/tools/seam.js +162 -125
- package/src/tools/selfmod.js +196 -176
- package/src/tools/v3.js +225 -0
- package/src/tools/vad.js +176 -0
- package/src/tools/voice.js +238 -0
- package/src/utils/async.js +14 -0
- package/src/utils/config-file.js +53 -0
- package/src/utils/crashguard.js +88 -0
- package/src/utils/http.js +53 -0
- package/src/utils/id.js +8 -0
- package/src/utils/json-state.js +33 -0
- package/src/utils/logger.js +17 -17
- package/src/utils/ndjson.js +25 -0
- package/src/utils/pii.js +42 -42
- package/src/utils/rate-limit.js +50 -0
- package/src/utils/schema.js +80 -0
- package/src/utils/similarity.js +43 -0
- package/src/utils/store.js +170 -108
- package/src/utils/text.js +15 -15
- package/src/utils/trace.js +153 -153
- package/src/utils/wal.js +39 -0
- package/src/utils/winutf8.js +16 -15
- package/src/wiki/index.js +170 -0
package/src/mcp/http.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// - 取消 = 关闭 SSE 流 (无需 notifications/cancelled)
|
|
8
8
|
// - Origin 校验防 DNS rebinding; 本地默认只绑 127.0.0.1
|
|
9
9
|
import { McpServer, McpError, MCP_ERROR, MODERN_PROTOCOL_VERSION, SERVER_INFO_META_KEY } from "./server.js";
|
|
10
|
+
import { readBody, sendJson, SSE_HEADERS } from "../utils/http.js";
|
|
11
|
+
import { debug } from "../utils/logger.js";
|
|
10
12
|
|
|
11
13
|
const MAX_BODY = 1024 * 1024; // 1MB
|
|
12
14
|
|
|
@@ -15,15 +17,7 @@ function sseMessage(obj) {
|
|
|
15
17
|
return `event: message\ndata: ${JSON.stringify(obj)}\n\n`;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
//
|
|
19
|
-
async function readBody(req) {
|
|
20
|
-
let body = "";
|
|
21
|
-
for await (const chunk of req) {
|
|
22
|
-
body += chunk;
|
|
23
|
-
if (body.length > MAX_BODY) throw new McpError(MCP_ERROR.INVALID_REQUEST, "request too large");
|
|
24
|
-
}
|
|
25
|
-
return body;
|
|
26
|
-
}
|
|
20
|
+
// (readBody 收敛到 utils/http.js: 超限返回 null, 由下方改为抛 McpError 保持原语义)
|
|
27
21
|
|
|
28
22
|
// ---- 传输错误 -> HTTP 状态码映射 (2026-07-28) ----
|
|
29
23
|
function statusFor(err) {
|
|
@@ -54,8 +48,7 @@ export function createMcpHttpHandler(server, opts = {}) {
|
|
|
54
48
|
try {
|
|
55
49
|
// 1. 方法限制: 只收 POST
|
|
56
50
|
if (req.method !== "POST") {
|
|
57
|
-
res
|
|
58
|
-
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
51
|
+
sendJson(res, 405, { error: "method not allowed" }, { headers: { "Allow": "POST" } });
|
|
59
52
|
return;
|
|
60
53
|
}
|
|
61
54
|
|
|
@@ -66,8 +59,7 @@ export function createMcpHttpHandler(server, opts = {}) {
|
|
|
66
59
|
? opts.allowedOrigins.includes(origin)
|
|
67
60
|
: /^https?:\/\/(127\.0\.0\.1|localhost)(:\d+)?$/.test(origin);
|
|
68
61
|
if (!allowed) {
|
|
69
|
-
res
|
|
70
|
-
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32600, message: "origin not allowed" } }));
|
|
62
|
+
sendJson(res, 403, { jsonrpc: "2.0", error: { code: -32600, message: "origin not allowed" } });
|
|
71
63
|
return;
|
|
72
64
|
}
|
|
73
65
|
}
|
|
@@ -79,15 +71,15 @@ export function createMcpHttpHandler(server, opts = {}) {
|
|
|
79
71
|
// 4. 读 body + 解析
|
|
80
72
|
let msg;
|
|
81
73
|
try {
|
|
82
|
-
const body = await readBody(req);
|
|
74
|
+
const body = await readBody(req, { maxBytes: MAX_BODY });
|
|
75
|
+
if (body === null) throw new McpError(MCP_ERROR.INVALID_REQUEST, "request too large");
|
|
83
76
|
msg = body ? JSON.parse(body) : {};
|
|
84
77
|
} catch (e) {
|
|
85
|
-
res
|
|
86
|
-
res.end(JSON.stringify({
|
|
78
|
+
sendJson(res, 400, {
|
|
87
79
|
jsonrpc: "2.0",
|
|
88
80
|
id: null,
|
|
89
81
|
error: { code: MCP_ERROR.PARSE_ERROR, message: `Parse error: ${e.message}` },
|
|
90
|
-
})
|
|
82
|
+
});
|
|
91
83
|
return;
|
|
92
84
|
}
|
|
93
85
|
|
|
@@ -95,12 +87,11 @@ export function createMcpHttpHandler(server, opts = {}) {
|
|
|
95
87
|
const headerVersion = req.headers["mcp-protocol-version"];
|
|
96
88
|
const bodyVersion = msg && msg.params && msg.params._meta && msg.params._meta["io.modelcontextprotocol/protocolVersion"];
|
|
97
89
|
if (bodyVersion && headerVersion && headerVersion !== bodyVersion) {
|
|
98
|
-
res
|
|
99
|
-
res.end(JSON.stringify({
|
|
90
|
+
sendJson(res, 400, {
|
|
100
91
|
jsonrpc: "2.0",
|
|
101
92
|
id: msg.id ?? null,
|
|
102
93
|
error: { code: MCP_ERROR.HEADER_MISMATCH, message: "Header MCP-Protocol-Version does not match body _meta", data: { header: headerVersion, body: bodyVersion } },
|
|
103
|
-
})
|
|
94
|
+
});
|
|
104
95
|
return;
|
|
105
96
|
}
|
|
106
97
|
// 现代请求 (有 bodyVersion) 但缺 header: 允许 (兼容未带头的老客户端), 记录即可
|
|
@@ -115,8 +106,7 @@ export function createMcpHttpHandler(server, opts = {}) {
|
|
|
115
106
|
// 通知失败仅记日志, 不影响 202 语义
|
|
116
107
|
if (opts.onError) opts.onError(e);
|
|
117
108
|
});
|
|
118
|
-
res
|
|
119
|
-
res.end();
|
|
109
|
+
sendJson(res, 202);
|
|
120
110
|
return;
|
|
121
111
|
}
|
|
122
112
|
|
|
@@ -127,12 +117,7 @@ export function createMcpHttpHandler(server, opts = {}) {
|
|
|
127
117
|
|
|
128
118
|
if (wantsSSE) {
|
|
129
119
|
// SSE 响应流: 进度通知 + 最终响应, 关闭流 = 取消
|
|
130
|
-
res.writeHead(200,
|
|
131
|
-
"Content-Type": "text/event-stream",
|
|
132
|
-
"Cache-Control": "no-cache",
|
|
133
|
-
"Connection": "keep-alive",
|
|
134
|
-
"X-Accel-Buffering": "no",
|
|
135
|
-
});
|
|
120
|
+
res.writeHead(200, SSE_HEADERS);
|
|
136
121
|
res.write(": keep-alive\n\n");
|
|
137
122
|
const progressToken = msg.params && msg.params._meta && msg.params._meta.progressToken;
|
|
138
123
|
const streamCtx = {
|
|
@@ -167,7 +152,7 @@ export function createMcpHttpHandler(server, opts = {}) {
|
|
|
167
152
|
const err = e instanceof McpError ? e : new McpError(MCP_ERROR.INTERNAL_ERROR, e.message || "internal error");
|
|
168
153
|
res.write(sseMessage({ jsonrpc: "2.0", id: msg.id, error: { code: err.code, message: err.message, ...(err.data ? { data: err.data } : {}) } }));
|
|
169
154
|
} finally {
|
|
170
|
-
try { res.end(); } catch {}
|
|
155
|
+
try { res.end(); } catch (e) { debug(`[mcp/http] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
171
156
|
}
|
|
172
157
|
return;
|
|
173
158
|
}
|
|
@@ -175,23 +160,20 @@ export function createMcpHttpHandler(server, opts = {}) {
|
|
|
175
160
|
// 8. 普通 JSON 响应
|
|
176
161
|
try {
|
|
177
162
|
const { result } = await server.handle(msg, {});
|
|
178
|
-
res
|
|
179
|
-
res.end(JSON.stringify({ jsonrpc: "2.0", id: msg.id, result: result ?? {} }));
|
|
163
|
+
sendJson(res, 200, { jsonrpc: "2.0", id: msg.id, result: result ?? {} });
|
|
180
164
|
} catch (e) {
|
|
181
165
|
const err = e instanceof McpError ? e : new McpError(MCP_ERROR.INTERNAL_ERROR, e.message || "internal error");
|
|
182
|
-
res
|
|
183
|
-
res.end(JSON.stringify({
|
|
166
|
+
sendJson(res, statusFor(err), {
|
|
184
167
|
jsonrpc: "2.0",
|
|
185
168
|
id: msg.id ?? null,
|
|
186
169
|
error: { code: err.code, message: err.message, ...(err.data ? { data: err.data } : {}) },
|
|
187
|
-
})
|
|
170
|
+
});
|
|
188
171
|
}
|
|
189
172
|
} catch (e) {
|
|
190
173
|
// 外层兜底 (不应发生)
|
|
191
174
|
try {
|
|
192
|
-
res
|
|
193
|
-
|
|
194
|
-
} catch {}
|
|
175
|
+
sendJson(res, 500, { jsonrpc: "2.0", error: { code: -32603, message: e.message || "internal error" } });
|
|
176
|
+
} catch (e) { debug(`[mcp/http] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
195
177
|
}
|
|
196
178
|
};
|
|
197
179
|
}
|
|
@@ -209,6 +191,10 @@ export function createMcpEndpoint(agent, opts = {}) {
|
|
|
209
191
|
authenticated: opts.authenticated,
|
|
210
192
|
rateLimit: opts.rateLimit,
|
|
211
193
|
allowedOrigins: opts.allowedOrigins,
|
|
194
|
+
// 2026-10-03 修复 (P2): 原工厂吞掉 skipOriginCheck —— channels/http.js 显式传
|
|
195
|
+
// skipOriginCheck: true (宿主 CORS 白名单已统一处理 Origin), 但工厂不转发 →
|
|
196
|
+
// handler 内部仍用默认回环正则二次校验, 契约失真 (宿主白名单形同虚设)。
|
|
197
|
+
skipOriginCheck: opts.skipOriginCheck,
|
|
212
198
|
onError: opts.onError,
|
|
213
199
|
});
|
|
214
200
|
return { server, handler };
|
package/src/mcp/index.js
CHANGED
|
@@ -1,116 +1,116 @@
|
|
|
1
|
-
// src/mcp/index.js - MCP 工具注册 (接入 MCP 工具生态)
|
|
2
|
-
// 把 MCP 服务器的工具转换为皮皮虾 ToolCatalog 工具, 与内置工具同权。
|
|
3
|
-
// servers: [
|
|
4
|
-
// { command, args, env, prefix } // stdio (本机)
|
|
5
|
-
// { url, headers, prefix, timeout } // HTTP Streamable (远程)
|
|
6
|
-
// ]
|
|
7
|
-
import { McpClient, extractToolResult, extractResourceText } from "./client.js";
|
|
8
|
-
import { TOOL_ERROR_PREFIX } from "../tools/catalog.js";
|
|
9
|
-
import { warn, info } from "../utils/logger.js";
|
|
10
|
-
|
|
11
|
-
// v1.0.8: MCP 工具名清洗 (只留 \w.-, 截断) — 防恶意服务器注册非法工具名/注入
|
|
12
|
-
export function sanitizeMcpName(name) {
|
|
13
|
-
return String(name || "").replace(/[^\w.-]/g, "_").slice(0, 64);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// P1⑦ (2026-09-15): MCP 命名空间隔离 —— 防工具名碰撞 (Hermes 生产数据: 51 工具服务器上
|
|
17
|
-
// 幻觉工具名命中真实工具; cron job 本想调 kanban_list 结果执行了 list_goals)。
|
|
18
|
-
// 强制 serverName__toolName 前缀 + 精确匹配短路。
|
|
19
|
-
export function serverLabel(s) {
|
|
20
|
-
// 取服务器可读标识: 配置 name > 包名 (args 里 @scope/pkg 或 *.py) > command 首段 > url host
|
|
21
|
-
if (s && s.name) return sanitizeMcpName(s.name);
|
|
22
|
-
const fromArgs = (s && s.args || []).find((a) => /(?:^|\/)[@\w.-]+(?:@[\w.]+)?$/.test(a) && !a.startsWith("-"));
|
|
23
|
-
if (fromArgs) return sanitizeMcpName(fromArgs.split("/").pop());
|
|
24
|
-
if (s && s.command) return sanitizeMcpName(String(s.command).split(/[\\/\s]/).filter(Boolean).pop() || "mcp");
|
|
25
|
-
if (s && s.url) {
|
|
26
|
-
try {
|
|
27
|
-
const host = new URL(s.url).hostname.split(".").filter(Boolean);
|
|
28
|
-
// 取二级域: mcp.example.com → example; www.google.com → google
|
|
29
|
-
return sanitizeMcpName(host.length >= 2 ? host[host.length - 2] : host[0] || "mcp");
|
|
30
|
-
} catch {}
|
|
31
|
-
}
|
|
32
|
-
return "mcp";
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function namespacedMcpName(label, raw) {
|
|
36
|
-
const clean = sanitizeMcpName(raw);
|
|
37
|
-
return clean ? `${label}__${clean}` : label;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// v1.0.8: MCP 工具描述清洗 (单行化 + 截断) — 服务器描述直接进 LLM prompt, 防换行/长文本注入指令
|
|
41
|
-
// P1⑦: 额外剔除危险 flag (--system/--dangerously 等命令行注入惯用手法)
|
|
42
|
-
export function sanitizeMcpDescription(desc, name) {
|
|
43
|
-
let s = String(desc || "").replace(/\s+/g, " ").trim();
|
|
44
|
-
// 危险 flag 清洗: 描述里出现命令行注入惯用 flag 视为可疑, 剔除该片段
|
|
45
|
-
s = s.replace(/--(?:system|dangerously[-\w]*|yes|force|no[-\w]+|y)\b/g, "[flag]");
|
|
46
|
-
return s.slice(0, 200) || `MCP 工具: ${name}`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// 连接所有 MCP 服务器, 列出工具并注册到 catalog。
|
|
50
|
-
// 除 tools 外, 服务器声明 resources/prompts 能力时, 额外注册对应读写工具 (P2⑥)。
|
|
51
|
-
// 返回 { count, clients, close } — close() 关闭全部连接 (调用方负责生命周期)
|
|
52
|
-
export async function registerMcpTools(catalog, servers = []) {
|
|
53
|
-
const clients = [];
|
|
54
|
-
let count = 0;
|
|
55
|
-
for (const s of servers) {
|
|
56
|
-
if (!s) { warn("[mcp] 跳过无效服务器配置"); continue; }
|
|
57
|
-
if (!s.command && !s.url) { warn("[mcp] 跳过无效服务器配置 (缺 command/url)"); continue; }
|
|
58
|
-
const client = new McpClient(s);
|
|
59
|
-
try {
|
|
60
|
-
await client.connect();
|
|
61
|
-
const tools = await client.listTools();
|
|
62
|
-
// P1⑦: 命名空间强制前缀 (serverLabel) —— 默认开启, 可显式 prefix 覆盖/关闭
|
|
63
|
-
const label = serverLabel(s);
|
|
64
|
-
const ns = s.prefix != null ? s.prefix : `${label}__`;
|
|
65
|
-
for (const t of tools) {
|
|
66
|
-
if (!t || !t.name) continue;
|
|
67
|
-
const name = ns + sanitizeMcpName(t.name);
|
|
68
|
-
// P1⑦: 精确匹配短路 —— 注册时登记原始名 (供幻觉工具名防御: 无前缀名永不执行)
|
|
69
|
-
catalog.register({
|
|
70
|
-
name,
|
|
71
|
-
description: sanitizeMcpDescription(t.description, t.name),
|
|
72
|
-
parameters: t.inputSchema || { type: "object", properties: {} },
|
|
73
|
-
category: "mcp",
|
|
74
|
-
// isError 时加错误前缀, 让皮皮虾的自愈重试语义对 MCP 工具同样生效
|
|
75
|
-
execute: async (args) => {
|
|
76
|
-
const raw = await client.callToolRaw(t.name, args || {});
|
|
77
|
-
const { text, isError } = extractToolResult(raw);
|
|
78
|
-
return isError ? `${TOOL_ERROR_PREFIX} MCP 工具 ${t.name}: ${text}` : text;
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
count += 1;
|
|
82
|
-
}
|
|
83
|
-
count += registerResourceTools(catalog, client, s, ns);
|
|
84
|
-
clients.push(client);
|
|
85
|
-
} catch (e) {
|
|
86
|
-
warn(`[mcp] 服务器 ${s.command || s.url} 连接失败: ${e.message}`);
|
|
87
|
-
client.close();
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
if (count) info(`[mcp] 已注册 ${count} 个 MCP 工具`);
|
|
91
|
-
return { count, clients, close: () => clients.forEach((c) => c.close()) };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// 服务器声明 resources 能力时, 注册 list_resources / read_resource 两个工具 (让 LLM 可读远程资源)
|
|
95
|
-
function registerResourceTools(catalog, client, s, ns = "") {
|
|
96
|
-
if (!client.capabilities?.resources) return 0;
|
|
97
|
-
const label = s.command || s.url || "MCP";
|
|
98
|
-
catalog.register({
|
|
99
|
-
name: ns + "list_resources",
|
|
100
|
-
description: `列出 MCP 服务器 ${label} 提供的资源 (URI 列表)`,
|
|
101
|
-
parameters: { type: "object", properties: {}, required: [] },
|
|
102
|
-
category: "mcp",
|
|
103
|
-
execute: async () => {
|
|
104
|
-
const res = await client.listResources();
|
|
105
|
-
return res.map((r) => `${r.uri || "?"}${r.name ? ` — ${r.name}` : ""}`).join("\n") || "(无资源)";
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
catalog.register({
|
|
109
|
-
name: ns + "read_resource",
|
|
110
|
-
description: `读取 MCP 服务器 ${label} 的资源内容 (按 uri)`,
|
|
111
|
-
parameters: { type: "object", properties: { uri: { type: "string", description: "资源 URI" } }, required: ["uri"] },
|
|
112
|
-
category: "mcp",
|
|
113
|
-
execute: async (args) => extractResourceText(await client.readResource((args || {}).uri)),
|
|
114
|
-
});
|
|
115
|
-
return 2;
|
|
116
|
-
}
|
|
1
|
+
// src/mcp/index.js - MCP 工具注册 (接入 MCP 工具生态)
|
|
2
|
+
// 把 MCP 服务器的工具转换为皮皮虾 ToolCatalog 工具, 与内置工具同权。
|
|
3
|
+
// servers: [
|
|
4
|
+
// { command, args, env, prefix } // stdio (本机)
|
|
5
|
+
// { url, headers, prefix, timeout } // HTTP Streamable (远程)
|
|
6
|
+
// ]
|
|
7
|
+
import { McpClient, extractToolResult, extractResourceText } from "./client.js";
|
|
8
|
+
import { TOOL_ERROR_PREFIX } from "../tools/catalog.js";
|
|
9
|
+
import { warn, info, debug } from "../utils/logger.js";
|
|
10
|
+
|
|
11
|
+
// v1.0.8: MCP 工具名清洗 (只留 \w.-, 截断) — 防恶意服务器注册非法工具名/注入
|
|
12
|
+
export function sanitizeMcpName(name) {
|
|
13
|
+
return String(name || "").replace(/[^\w.-]/g, "_").slice(0, 64);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// P1⑦ (2026-09-15): MCP 命名空间隔离 —— 防工具名碰撞 (Hermes 生产数据: 51 工具服务器上
|
|
17
|
+
// 幻觉工具名命中真实工具; cron job 本想调 kanban_list 结果执行了 list_goals)。
|
|
18
|
+
// 强制 serverName__toolName 前缀 + 精确匹配短路。
|
|
19
|
+
export function serverLabel(s) {
|
|
20
|
+
// 取服务器可读标识: 配置 name > 包名 (args 里 @scope/pkg 或 *.py) > command 首段 > url host
|
|
21
|
+
if (s && s.name) return sanitizeMcpName(s.name);
|
|
22
|
+
const fromArgs = (s && s.args || []).find((a) => /(?:^|\/)[@\w.-]+(?:@[\w.]+)?$/.test(a) && !a.startsWith("-"));
|
|
23
|
+
if (fromArgs) return sanitizeMcpName(fromArgs.split("/").pop());
|
|
24
|
+
if (s && s.command) return sanitizeMcpName(String(s.command).split(/[\\/\s]/).filter(Boolean).pop() || "mcp");
|
|
25
|
+
if (s && s.url) {
|
|
26
|
+
try {
|
|
27
|
+
const host = new URL(s.url).hostname.split(".").filter(Boolean);
|
|
28
|
+
// 取二级域: mcp.example.com → example; www.google.com → google
|
|
29
|
+
return sanitizeMcpName(host.length >= 2 ? host[host.length - 2] : host[0] || "mcp");
|
|
30
|
+
} catch (e) { debug(`[mcp/index] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
31
|
+
}
|
|
32
|
+
return "mcp";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function namespacedMcpName(label, raw) {
|
|
36
|
+
const clean = sanitizeMcpName(raw);
|
|
37
|
+
return clean ? `${label}__${clean}` : label;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// v1.0.8: MCP 工具描述清洗 (单行化 + 截断) — 服务器描述直接进 LLM prompt, 防换行/长文本注入指令
|
|
41
|
+
// P1⑦: 额外剔除危险 flag (--system/--dangerously 等命令行注入惯用手法)
|
|
42
|
+
export function sanitizeMcpDescription(desc, name) {
|
|
43
|
+
let s = String(desc || "").replace(/\s+/g, " ").trim();
|
|
44
|
+
// 危险 flag 清洗: 描述里出现命令行注入惯用 flag 视为可疑, 剔除该片段
|
|
45
|
+
s = s.replace(/--(?:system|dangerously[-\w]*|yes|force|no[-\w]+|y)\b/g, "[flag]");
|
|
46
|
+
return s.slice(0, 200) || `MCP 工具: ${name}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 连接所有 MCP 服务器, 列出工具并注册到 catalog。
|
|
50
|
+
// 除 tools 外, 服务器声明 resources/prompts 能力时, 额外注册对应读写工具 (P2⑥)。
|
|
51
|
+
// 返回 { count, clients, close } — close() 关闭全部连接 (调用方负责生命周期)
|
|
52
|
+
export async function registerMcpTools(catalog, servers = []) {
|
|
53
|
+
const clients = [];
|
|
54
|
+
let count = 0;
|
|
55
|
+
for (const s of servers) {
|
|
56
|
+
if (!s) { warn("[mcp] 跳过无效服务器配置"); continue; }
|
|
57
|
+
if (!s.command && !s.url) { warn("[mcp] 跳过无效服务器配置 (缺 command/url)"); continue; }
|
|
58
|
+
const client = new McpClient(s);
|
|
59
|
+
try {
|
|
60
|
+
await client.connect();
|
|
61
|
+
const tools = await client.listTools();
|
|
62
|
+
// P1⑦: 命名空间强制前缀 (serverLabel) —— 默认开启, 可显式 prefix 覆盖/关闭
|
|
63
|
+
const label = serverLabel(s);
|
|
64
|
+
const ns = s.prefix != null ? s.prefix : `${label}__`;
|
|
65
|
+
for (const t of tools) {
|
|
66
|
+
if (!t || !t.name) continue;
|
|
67
|
+
const name = ns + sanitizeMcpName(t.name);
|
|
68
|
+
// P1⑦: 精确匹配短路 —— 注册时登记原始名 (供幻觉工具名防御: 无前缀名永不执行)
|
|
69
|
+
catalog.register({
|
|
70
|
+
name,
|
|
71
|
+
description: sanitizeMcpDescription(t.description, t.name),
|
|
72
|
+
parameters: t.inputSchema || { type: "object", properties: {} },
|
|
73
|
+
category: "mcp",
|
|
74
|
+
// isError 时加错误前缀, 让皮皮虾的自愈重试语义对 MCP 工具同样生效
|
|
75
|
+
execute: async (args) => {
|
|
76
|
+
const raw = await client.callToolRaw(t.name, args || {});
|
|
77
|
+
const { text, isError } = extractToolResult(raw);
|
|
78
|
+
return isError ? `${TOOL_ERROR_PREFIX} MCP 工具 ${t.name}: ${text}` : text;
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
count += 1;
|
|
82
|
+
}
|
|
83
|
+
count += registerResourceTools(catalog, client, s, ns);
|
|
84
|
+
clients.push(client);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
warn(`[mcp] 服务器 ${s.command || s.url} 连接失败: ${e.message}`);
|
|
87
|
+
client.close();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (count) info(`[mcp] 已注册 ${count} 个 MCP 工具`);
|
|
91
|
+
return { count, clients, close: () => clients.forEach((c) => c.close()) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 服务器声明 resources 能力时, 注册 list_resources / read_resource 两个工具 (让 LLM 可读远程资源)
|
|
95
|
+
function registerResourceTools(catalog, client, s, ns = "") {
|
|
96
|
+
if (!client.capabilities?.resources) return 0;
|
|
97
|
+
const label = s.command || s.url || "MCP";
|
|
98
|
+
catalog.register({
|
|
99
|
+
name: ns + "list_resources",
|
|
100
|
+
description: `列出 MCP 服务器 ${label} 提供的资源 (URI 列表)`,
|
|
101
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
102
|
+
category: "mcp",
|
|
103
|
+
execute: async () => {
|
|
104
|
+
const res = await client.listResources();
|
|
105
|
+
return res.map((r) => `${r.uri || "?"}${r.name ? ` — ${r.name}` : ""}`).join("\n") || "(无资源)";
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
catalog.register({
|
|
109
|
+
name: ns + "read_resource",
|
|
110
|
+
description: `读取 MCP 服务器 ${label} 的资源内容 (按 uri)`,
|
|
111
|
+
parameters: { type: "object", properties: { uri: { type: "string", description: "资源 URI" } }, required: ["uri"] },
|
|
112
|
+
category: "mcp",
|
|
113
|
+
execute: async (args) => extractResourceText(await client.readResource((args || {}).uri)),
|
|
114
|
+
});
|
|
115
|
+
return 2;
|
|
116
|
+
}
|
package/src/mcp/server.js
CHANGED
|
@@ -6,9 +6,18 @@
|
|
|
6
6
|
// - legacy 2025-06-18 及更早: initialize 握手 + capabilities 协商 (兼容存量 MCP 客户端)。
|
|
7
7
|
// 传输无关: 仅做 JSON-RPC 分发, stdio/HTTP 传输由上层 (src/mcp/http.js 等) 接入。
|
|
8
8
|
import { TOOL_ERROR_PREFIX } from "../tools/catalog.js";
|
|
9
|
-
import { warn } from "../utils/logger.js";
|
|
9
|
+
import { warn, debug } from "../utils/logger.js";
|
|
10
10
|
import { sanitizeMcpName } from "./index.js";
|
|
11
11
|
|
|
12
|
+
// 版本号统一读 package.json, 避免与发布版本漂移 (外部体检: 硬编码 2.5.0 导致 serverInfo 落后真实版本)
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
let PKG_VERSION = "0.0.0";
|
|
17
|
+
try {
|
|
18
|
+
PKG_VERSION = JSON.parse(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json"), "utf8")).version || "0.0.0";
|
|
19
|
+
} catch (e) { debug(`[mcp/server] 已忽略异常: ${e && e.message ? e.message : e}`); } // 非标准安装位置时降级, 不影响启动
|
|
20
|
+
|
|
12
21
|
// 协议版本常量 (与官方 schema/2026-07-28 对齐)
|
|
13
22
|
export const MODERN_PROTOCOL_VERSION = "2026-07-28"; // 现代 era (每请求 _meta)
|
|
14
23
|
export const LEGACY_PROTOCOL_VERSION = "2025-06-18"; // legacy era (initialize 握手) 最高支持版本
|
|
@@ -70,23 +79,26 @@ export class McpServer {
|
|
|
70
79
|
this.agent = agent;
|
|
71
80
|
this.serverInfo = {
|
|
72
81
|
name: opts.name || agent?.config?.agent?.name || "ppxans-harness",
|
|
73
|
-
version: opts.version ||
|
|
82
|
+
version: opts.version || PKG_VERSION,
|
|
74
83
|
};
|
|
75
84
|
this.supportedVersions = opts.supportedVersions || SUPPORTED_VERSIONS;
|
|
76
85
|
// 对话虚拟工具 (MCP 客户端驱动 agent 对话的入口, 不进 catalog 避免污染 LLM 工具列表)
|
|
86
|
+
// 两个对话工具入参完全一致 (message + sessionId), 共用一个 schema 工厂, 避免改一处漏一处;
|
|
87
|
+
// 每次调用返回新对象, 不共享引用 (防止下游按需改写 schema 时互相污染)。
|
|
88
|
+
const chatInputSchema = () => ({
|
|
89
|
+
type: "object",
|
|
90
|
+
properties: {
|
|
91
|
+
message: { type: "string", description: "用户消息" },
|
|
92
|
+
sessionId: { type: "string", description: "会话标识 (默认 default)" },
|
|
93
|
+
},
|
|
94
|
+
required: ["message"],
|
|
95
|
+
});
|
|
77
96
|
const chatTools = [
|
|
78
97
|
{
|
|
79
98
|
name: "ppx.chat.send",
|
|
80
99
|
title: "对话 (非流式)",
|
|
81
100
|
description: "发送消息给皮皮虾 agent, 返回完整回复。内部会执行完整工具调用循环 (记忆/搜索/命令等)。sessionId 用于区分会话上下文, 默认 default。",
|
|
82
|
-
inputSchema:
|
|
83
|
-
type: "object",
|
|
84
|
-
properties: {
|
|
85
|
-
message: { type: "string", description: "用户消息" },
|
|
86
|
-
sessionId: { type: "string", description: "会话标识 (默认 default)" },
|
|
87
|
-
},
|
|
88
|
-
required: ["message"],
|
|
89
|
-
},
|
|
101
|
+
inputSchema: chatInputSchema(),
|
|
90
102
|
execute: async (args, ctx, agent) => {
|
|
91
103
|
const reply = await agent.chat(String(args.message || ""), { sessionKey: args.sessionId || "default" });
|
|
92
104
|
return { content: [{ type: "text", text: String(reply) }] };
|
|
@@ -96,14 +108,7 @@ export class McpServer {
|
|
|
96
108
|
name: "ppx.chat.stream",
|
|
97
109
|
title: "对话 (流式)",
|
|
98
110
|
description: "发送消息给皮皮虾 agent, 流式返回回复 (SSE 进度通知)。内部会执行完整工具调用循环。sessionId 用于区分会话上下文, 默认 default。",
|
|
99
|
-
inputSchema:
|
|
100
|
-
type: "object",
|
|
101
|
-
properties: {
|
|
102
|
-
message: { type: "string", description: "用户消息" },
|
|
103
|
-
sessionId: { type: "string", description: "会话标识 (默认 default)" },
|
|
104
|
-
},
|
|
105
|
-
required: ["message"],
|
|
106
|
-
},
|
|
111
|
+
inputSchema: chatInputSchema(),
|
|
107
112
|
execute: async (args, ctx, agent) => {
|
|
108
113
|
let full = "";
|
|
109
114
|
const reply = await agent.chatStream(String(args.message || ""), {
|
package/src/mcp/tasks.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// 轻量任务工作区: 任务 = { id, title, description, status, steps[], result, createdAt, updatedAt }
|
|
3
3
|
// 持久化到 <root>/data/tasks.json (原子写), 支持 create/list/get/update/step/delete/complete。
|
|
4
4
|
// 供 MCP 工具 ppx.task.* 与 web 前端任务面板使用。
|
|
5
|
-
import fs from "node:fs";
|
|
6
5
|
import path from "node:path";
|
|
7
|
-
import {
|
|
6
|
+
import { readJson, writeJson } from "../utils/store.js";
|
|
7
|
+
import { shortId } from "../utils/id.js";
|
|
8
8
|
|
|
9
9
|
const VALID_TASK_STATUS = new Set(["todo", "running", "done", "failed"]);
|
|
10
10
|
const VALID_STEP_STATUS = new Set(["pending", "running", "done", "failed"]);
|
|
@@ -18,22 +18,17 @@ export class TaskBoard {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
_load() {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (
|
|
25
|
-
for (const t of raw.tasks) {
|
|
26
|
-
if (t && t.id) this.tasks.set(t.id, this._normalize(t));
|
|
27
|
-
}
|
|
28
|
-
}
|
|
21
|
+
const raw = readJson(this.file, null); // 损坏则从空开始, 下次写入覆盖
|
|
22
|
+
if (raw && Array.isArray(raw.tasks)) {
|
|
23
|
+
for (const t of raw.tasks) {
|
|
24
|
+
if (t && t.id) this.tasks.set(t.id, this._normalize(t));
|
|
29
25
|
}
|
|
30
|
-
}
|
|
26
|
+
}
|
|
31
27
|
}
|
|
32
28
|
|
|
33
29
|
_save() {
|
|
34
30
|
try {
|
|
35
|
-
|
|
36
|
-
atomicWrite(this.file, JSON.stringify({ tasks: [...this.tasks.values()] }, null, 2));
|
|
31
|
+
writeJson(this.file, { tasks: [...this.tasks.values()] });
|
|
37
32
|
} catch (e) { /* 持久化失败不阻断内存操作 */ }
|
|
38
33
|
}
|
|
39
34
|
|
|
@@ -58,7 +53,7 @@ export class TaskBoard {
|
|
|
58
53
|
|
|
59
54
|
create({ title, description = "", steps = [] } = {}) {
|
|
60
55
|
if (!title) throw new Error("任务需 title");
|
|
61
|
-
const id = "t_"
|
|
56
|
+
const id = shortId("t_", 5);
|
|
62
57
|
const t = this._normalize({
|
|
63
58
|
id, title, description,
|
|
64
59
|
steps: steps.map((s) => ({ title: String(s) })).filter((s) => s.title),
|
package/src/memory/asset-hub.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
// - 检索隔离: 私有资产仅 owner 可检索; 团队资产共享 (简化: 全局可见)
|
|
6
6
|
// 皮皮虾自研实现, 构建在现有 facts (带 scope) 之上: 资产 = 一组带 scope 的 facts + 元数据登记。
|
|
7
7
|
// 纯代码可测, 无 LLM。
|
|
8
|
-
import fs from "node:fs";
|
|
9
8
|
import path from "node:path";
|
|
10
|
-
import { ensureDir,
|
|
9
|
+
import { ensureDir, readJson, writeJson } from "../utils/store.js";
|
|
10
|
+
import { shortId } from "../utils/id.js";
|
|
11
11
|
|
|
12
12
|
export const VISIBILITY = { PRIVATE: "private", TEAM: "team" };
|
|
13
13
|
|
|
@@ -26,22 +26,17 @@ export class AssetHub {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
_load() {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const d = JSON.parse(fs.readFileSync(this.file, "utf8"));
|
|
32
|
-
if (Array.isArray(d)) return d;
|
|
33
|
-
}
|
|
34
|
-
} catch {}
|
|
35
|
-
return [];
|
|
29
|
+
const d = readJson(this.file, null);
|
|
30
|
+
return Array.isArray(d) ? d : [];
|
|
36
31
|
}
|
|
37
32
|
|
|
38
|
-
_save() {
|
|
33
|
+
_save() { writeJson(this.file, this._assets); }
|
|
39
34
|
|
|
40
35
|
// 登记一个资产 (关联到 facts scope)
|
|
41
36
|
// { name, kind: 'document'|'skill'|'experience', scope, owner, visibility, source, description }
|
|
42
37
|
register(asset) {
|
|
43
38
|
if (!asset || !asset.name) throw new Error("资产需 name");
|
|
44
|
-
const id = "as"
|
|
39
|
+
const id = shortId("as", 5);
|
|
45
40
|
const a = {
|
|
46
41
|
id,
|
|
47
42
|
name: asset.name,
|
package/src/memory/canvas.js
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
// 细节按 node_id 从事件日志取 —— 上层存结构, 下层存证据, 完整 drill-down 路径。
|
|
5
5
|
// 皮皮虾自研实现: 从 trace.js 的事件流 (turn/step 边界事件) 归纳画布, 非 LLM、纯代码、可测。
|
|
6
6
|
// 触发条件: steps >= minSteps (默认 8) 或事件数超阈值时才开启, 防过度设计。
|
|
7
|
-
import fs from "node:fs";
|
|
8
7
|
import path from "node:path";
|
|
9
|
-
import { ensureDir, writeText, logicalDay } from "../utils/store.js";
|
|
8
|
+
import { ensureDir, writeText, readJson, logicalDay } from "../utils/store.js";
|
|
10
9
|
|
|
11
10
|
export const CANVAS_DEFAULTS = { minSteps: 8, maxNodes: 30, maxEdges: 40 };
|
|
12
11
|
|
|
@@ -101,9 +100,7 @@ export class CanvasStore {
|
|
|
101
100
|
}
|
|
102
101
|
|
|
103
102
|
read(day = logicalDay()) {
|
|
104
|
-
|
|
105
|
-
if (!fs.existsSync(f)) return null;
|
|
106
|
-
try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch { return null; }
|
|
103
|
+
return readJson(this._file(day), null);
|
|
107
104
|
}
|
|
108
105
|
|
|
109
106
|
// 从 trace 事件自动生成并保存 (若步骤数达标); 返回 null 表示未触发
|
package/src/memory/compaction.js
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
// src/memory/compaction.js - 会话压缩层 (吸收 OpenClaw compaction 精华)
|
|
2
|
-
// 设计要点:
|
|
3
|
-
// - 超阈值时用 LLM 把旧对话压缩成「结构化摘要」(固定格式), 替换旧历史, 防上下文失控
|
|
4
|
-
// - 摘要可迭代: 摘要作为 system 消息进入下一轮, 下次压缩时摘要+新对话一起再压
|
|
5
|
-
// - 固定格式: 目标/进展/关键决策/待办/关键上下文, 保证可预测、可续写
|
|
6
|
-
|
|
7
|
-
export const COMPACTION_SYSTEM_PROMPT = `你是对话压缩器。把下面的对话历史压缩成一段结构化摘要,严格按以下格式输出(无内容的小节填「无」):
|
|
8
|
-
- 目标:用户想达成什么
|
|
9
|
-
- 进展:已完成的关键步骤
|
|
10
|
-
- 关键决策:已确定的重要决定/约定
|
|
11
|
-
- 待办:尚未完成的事项
|
|
12
|
-
- 关键上下文:后续对话必须知道的细节(数字/路径/偏好/约束)
|
|
13
|
-
要求:简洁,≤300字,保留所有关键事实,不要客套,直接输出摘要。`;
|
|
14
|
-
|
|
15
|
-
// 组装压缩请求消息 (system + 待压缩对话)
|
|
16
|
-
export function buildCompactionMessages(transcript) {
|
|
17
|
-
return [
|
|
18
|
-
{ role: "system", content: COMPACTION_SYSTEM_PROMPT },
|
|
19
|
-
{ role: "user", content: String(transcript).slice(0, 8000) },
|
|
20
|
-
];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// 把投影消息列表转成压缩用文本 (role → 中文标签)
|
|
24
|
-
export function transcriptToText(messages) {
|
|
25
|
-
return messages
|
|
26
|
-
.map((m) => (m.role === "user" ? "用户" : m.role === "assistant" ? "助手" : m.role) + ": " + String(m.content ?? ""))
|
|
27
|
-
.join("\n");
|
|
28
|
-
}
|
|
1
|
+
// src/memory/compaction.js - 会话压缩层 (吸收 OpenClaw compaction 精华)
|
|
2
|
+
// 设计要点:
|
|
3
|
+
// - 超阈值时用 LLM 把旧对话压缩成「结构化摘要」(固定格式), 替换旧历史, 防上下文失控
|
|
4
|
+
// - 摘要可迭代: 摘要作为 system 消息进入下一轮, 下次压缩时摘要+新对话一起再压
|
|
5
|
+
// - 固定格式: 目标/进展/关键决策/待办/关键上下文, 保证可预测、可续写
|
|
6
|
+
|
|
7
|
+
export const COMPACTION_SYSTEM_PROMPT = `你是对话压缩器。把下面的对话历史压缩成一段结构化摘要,严格按以下格式输出(无内容的小节填「无」):
|
|
8
|
+
- 目标:用户想达成什么
|
|
9
|
+
- 进展:已完成的关键步骤
|
|
10
|
+
- 关键决策:已确定的重要决定/约定
|
|
11
|
+
- 待办:尚未完成的事项
|
|
12
|
+
- 关键上下文:后续对话必须知道的细节(数字/路径/偏好/约束)
|
|
13
|
+
要求:简洁,≤300字,保留所有关键事实,不要客套,直接输出摘要。`;
|
|
14
|
+
|
|
15
|
+
// 组装压缩请求消息 (system + 待压缩对话)
|
|
16
|
+
export function buildCompactionMessages(transcript) {
|
|
17
|
+
return [
|
|
18
|
+
{ role: "system", content: COMPACTION_SYSTEM_PROMPT },
|
|
19
|
+
{ role: "user", content: String(transcript).slice(0, 8000) },
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 把投影消息列表转成压缩用文本 (role → 中文标签)
|
|
24
|
+
export function transcriptToText(messages) {
|
|
25
|
+
return messages
|
|
26
|
+
.map((m) => (m.role === "user" ? "用户" : m.role === "assistant" ? "助手" : m.role) + ": " + String(m.content ?? ""))
|
|
27
|
+
.join("\n");
|
|
28
|
+
}
|