ppxans-harness 2.4.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 -265
- 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 +15 -151
- package/config/ppx.json.example +143 -0
- package/package.json +17 -10
- 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 +47 -3
- 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 -592
- 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 -188
- 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 +188 -446
- package/src/llm/dsml.js +74 -74
- package/src/llm/embedder.js +41 -35
- package/src/llm/fence.js +52 -105
- 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 -97
- package/src/mcp/admin.js +326 -0
- package/src/mcp/client.js +487 -375
- package/src/mcp/http.js +203 -0
- package/src/mcp/index.js +116 -116
- package/src/mcp/server.js +392 -0
- package/src/mcp/tasks.js +133 -0
- 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 -212
- 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/tools/advanced.js
CHANGED
|
@@ -1,353 +1,389 @@
|
|
|
1
|
-
// src/tools/advanced.js - 进阶工具集 (搜索 / HTTP / 定时任务)
|
|
2
|
-
// 全部零依赖: 用 Node 原生 fetch + timers
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
// ---------- 网页搜索 (零依赖, 多引擎兜底: tavily/brave[有key] -> DDG) ----------
|
|
10
|
-
// 有 TAVILY_API_KEY / BRAVE_API_KEY 时优先用官方 API, 否则回退加固后的 DDG 解析
|
|
11
|
-
function _stripTags(h) { return String(h || "").replace(/<[^>]+>/g, "").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").trim(); }
|
|
12
|
-
function _decodeDDGUrl(u) {
|
|
13
|
-
// DDG 结果链接是 /duckduckgo.html?uddg=<encoded>&rut=...
|
|
14
|
-
const m = String(u || "").match(/[?&]uddg=([^&]+)/);
|
|
15
|
-
if (m) { try { return decodeURIComponent(m[1]); } catch {} }
|
|
16
|
-
return u;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async function searchWeb(query) {
|
|
20
|
-
const q = encodeURIComponent(query);
|
|
21
|
-
|
|
22
|
-
// 1. Tavily (官方 API, 需 TAVILY_API_KEY)
|
|
23
|
-
const tavilyKey = process.env.TAVILY_API_KEY;
|
|
24
|
-
if (tavilyKey) {
|
|
25
|
-
try {
|
|
26
|
-
const r = await fetch("https://api.tavily.com/search", {
|
|
27
|
-
method: "POST",
|
|
28
|
-
headers: { "Content-Type": "application/json" },
|
|
29
|
-
body: JSON.stringify({ api_key: tavilyKey, query, max_results: 5 }),
|
|
30
|
-
signal: AbortSignal.timeout(15000),
|
|
31
|
-
});
|
|
32
|
-
if (r.ok) {
|
|
33
|
-
const j = await r.json();
|
|
34
|
-
const results = (j.results || []).map(x => ({ title: x.title, url: x.url, snippet: x.content }));
|
|
35
|
-
if (results.length) return results;
|
|
36
|
-
}
|
|
37
|
-
} catch {}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// 2. Brave (官方 API, 需 BRAVE_API_KEY)
|
|
41
|
-
const braveKey = process.env.BRAVE_API_KEY;
|
|
42
|
-
if (braveKey) {
|
|
43
|
-
try {
|
|
44
|
-
const r = await fetch(`https://api.search.brave.com/res/v1/web/search?q=${q}&count=5`, {
|
|
45
|
-
headers: { "X-Subscription-Token": braveKey, "Accept": "application/json" },
|
|
46
|
-
signal: AbortSignal.timeout(15000),
|
|
47
|
-
});
|
|
48
|
-
if (r.ok) {
|
|
49
|
-
const j = await r.json();
|
|
50
|
-
const results = (j.web?.results || []).map(x => ({ title: x.title, url: x.url, snippet: x.description }));
|
|
51
|
-
if (results.length) return results;
|
|
52
|
-
}
|
|
53
|
-
} catch {}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// 3. DuckDuckGo HTML (免key兜底, 加固解析)
|
|
57
|
-
try {
|
|
58
|
-
const r = await fetch(`https://html.duckduckgo.com/html/?q=${q}`, {
|
|
59
|
-
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" },
|
|
60
|
-
signal: AbortSignal.timeout(15000),
|
|
61
|
-
});
|
|
62
|
-
const html = await r.text();
|
|
63
|
-
const results = [];
|
|
64
|
-
// 每次抓一个 result 块: <a class="result__a" href="...">title<\/a> ... <a class="result__snippet"...>snippet<\/a>
|
|
65
|
-
const re = /<a[^>]*class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<a[^>]*class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>/g;
|
|
66
|
-
let m;
|
|
67
|
-
while ((m = re.exec(html)) && results.length < 5) {
|
|
68
|
-
const title = _stripTags(m[2]);
|
|
69
|
-
const snippet = _stripTags(m[3]);
|
|
70
|
-
if (title) results.push({ title, url: _decodeDDGUrl(m[1]), snippet });
|
|
71
|
-
}
|
|
72
|
-
if (results.length) return results;
|
|
73
|
-
} catch {}
|
|
74
|
-
|
|
75
|
-
// 4. DuckDuckGo lite (最终兜底)
|
|
76
|
-
try {
|
|
77
|
-
const r = await fetch(`https://lite.duckduckgo.com/lite/?q=${q}`, {
|
|
78
|
-
headers: { "User-Agent": "Mozilla/5.0" },
|
|
79
|
-
signal: AbortSignal.timeout(15000),
|
|
80
|
-
});
|
|
81
|
-
const html = await r.text();
|
|
82
|
-
const results = [];
|
|
83
|
-
const re = /<a[^>]+class="result-link"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
|
|
84
|
-
let m;
|
|
85
|
-
while ((m = re.exec(html)) && results.length < 5) {
|
|
86
|
-
const title = _stripTags(m[2]);
|
|
87
|
-
if (title) results.push({ title, url: _decodeDDGUrl(m[1]), snippet: "" });
|
|
88
|
-
}
|
|
89
|
-
if (results.length) return results;
|
|
90
|
-
} catch {}
|
|
91
|
-
|
|
92
|
-
throw new Error("所有搜索源失败: 无结果");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ---------- HTTP 请求 (含 SSRF 防护) ----------
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
body
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
this.
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
},
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
catalog.register({
|
|
301
|
-
name: "
|
|
302
|
-
description: "
|
|
303
|
-
parameters: {
|
|
304
|
-
type: "object",
|
|
305
|
-
properties: {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
1
|
+
// src/tools/advanced.js - 进阶工具集 (搜索 / HTTP / 定时任务)
|
|
2
|
+
// 全部零依赖: 用 Node 原生 fetch + timers
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import dns from "node:dns/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { ensureDir, readJson, writeJson } from "../utils/store.js";
|
|
7
|
+
import { debug } from "../utils/logger.js";
|
|
8
|
+
|
|
9
|
+
// ---------- 网页搜索 (零依赖, 多引擎兜底: tavily/brave[有key] -> DDG) ----------
|
|
10
|
+
// 有 TAVILY_API_KEY / BRAVE_API_KEY 时优先用官方 API, 否则回退加固后的 DDG 解析
|
|
11
|
+
function _stripTags(h) { return String(h || "").replace(/<[^>]+>/g, "").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").trim(); }
|
|
12
|
+
function _decodeDDGUrl(u) {
|
|
13
|
+
// DDG 结果链接是 /duckduckgo.html?uddg=<encoded>&rut=...
|
|
14
|
+
const m = String(u || "").match(/[?&]uddg=([^&]+)/);
|
|
15
|
+
if (m) { try { return decodeURIComponent(m[1]); } catch (e) { debug(`[tools/advanced] 已忽略异常: ${e && e.message ? e.message : e}`); } }
|
|
16
|
+
return u;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function searchWeb(query) {
|
|
20
|
+
const q = encodeURIComponent(query);
|
|
21
|
+
|
|
22
|
+
// 1. Tavily (官方 API, 需 TAVILY_API_KEY)
|
|
23
|
+
const tavilyKey = process.env.TAVILY_API_KEY;
|
|
24
|
+
if (tavilyKey) {
|
|
25
|
+
try {
|
|
26
|
+
const r = await fetch("https://api.tavily.com/search", {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "Content-Type": "application/json" },
|
|
29
|
+
body: JSON.stringify({ api_key: tavilyKey, query, max_results: 5 }),
|
|
30
|
+
signal: AbortSignal.timeout(15000),
|
|
31
|
+
});
|
|
32
|
+
if (r.ok) {
|
|
33
|
+
const j = await r.json();
|
|
34
|
+
const results = (j.results || []).map(x => ({ title: x.title, url: x.url, snippet: x.content }));
|
|
35
|
+
if (results.length) return results;
|
|
36
|
+
}
|
|
37
|
+
} catch (e) { debug(`[tools/advanced] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. Brave (官方 API, 需 BRAVE_API_KEY)
|
|
41
|
+
const braveKey = process.env.BRAVE_API_KEY;
|
|
42
|
+
if (braveKey) {
|
|
43
|
+
try {
|
|
44
|
+
const r = await fetch(`https://api.search.brave.com/res/v1/web/search?q=${q}&count=5`, {
|
|
45
|
+
headers: { "X-Subscription-Token": braveKey, "Accept": "application/json" },
|
|
46
|
+
signal: AbortSignal.timeout(15000),
|
|
47
|
+
});
|
|
48
|
+
if (r.ok) {
|
|
49
|
+
const j = await r.json();
|
|
50
|
+
const results = (j.web?.results || []).map(x => ({ title: x.title, url: x.url, snippet: x.description }));
|
|
51
|
+
if (results.length) return results;
|
|
52
|
+
}
|
|
53
|
+
} catch (e) { debug(`[tools/advanced] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 3. DuckDuckGo HTML (免key兜底, 加固解析)
|
|
57
|
+
try {
|
|
58
|
+
const r = await fetch(`https://html.duckduckgo.com/html/?q=${q}`, {
|
|
59
|
+
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" },
|
|
60
|
+
signal: AbortSignal.timeout(15000),
|
|
61
|
+
});
|
|
62
|
+
const html = await r.text();
|
|
63
|
+
const results = [];
|
|
64
|
+
// 每次抓一个 result 块: <a class="result__a" href="...">title<\/a> ... <a class="result__snippet"...>snippet<\/a>
|
|
65
|
+
const re = /<a[^>]*class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<a[^>]*class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>/g;
|
|
66
|
+
let m;
|
|
67
|
+
while ((m = re.exec(html)) && results.length < 5) {
|
|
68
|
+
const title = _stripTags(m[2]);
|
|
69
|
+
const snippet = _stripTags(m[3]);
|
|
70
|
+
if (title) results.push({ title, url: _decodeDDGUrl(m[1]), snippet });
|
|
71
|
+
}
|
|
72
|
+
if (results.length) return results;
|
|
73
|
+
} catch (e) { debug(`[tools/advanced] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
74
|
+
|
|
75
|
+
// 4. DuckDuckGo lite (最终兜底)
|
|
76
|
+
try {
|
|
77
|
+
const r = await fetch(`https://lite.duckduckgo.com/lite/?q=${q}`, {
|
|
78
|
+
headers: { "User-Agent": "Mozilla/5.0" },
|
|
79
|
+
signal: AbortSignal.timeout(15000),
|
|
80
|
+
});
|
|
81
|
+
const html = await r.text();
|
|
82
|
+
const results = [];
|
|
83
|
+
const re = /<a[^>]+class="result-link"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
|
|
84
|
+
let m;
|
|
85
|
+
while ((m = re.exec(html)) && results.length < 5) {
|
|
86
|
+
const title = _stripTags(m[2]);
|
|
87
|
+
if (title) results.push({ title, url: _decodeDDGUrl(m[1]), snippet: "" });
|
|
88
|
+
}
|
|
89
|
+
if (results.length) return results;
|
|
90
|
+
} catch (e) { debug(`[tools/advanced] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
91
|
+
|
|
92
|
+
throw new Error("所有搜索源失败: 无结果");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------- HTTP 请求 (含 SSRF 防护) ----------
|
|
96
|
+
// 2026-09-18 修复 (P1): 原实现只按 IPv4 点分十进制判断, IPv6 字面量 (parts.length !== 4)
|
|
97
|
+
// 直接返回 false → http://[::1]/ http://[fe80::x]/ http://[fc00::x]/ 全部被当"公网"放行,
|
|
98
|
+
// 可打本机回环与内网服务。现补齐 IPv6 判定 (回环/未指定/链路本地/唯一本地 fc00::/7),
|
|
99
|
+
// 并处理 IPv4-mapped IPv6 (::ffff:127.0.0.1 等)。导出供回归测试使用。
|
|
100
|
+
export function isPrivateIP(ip) {
|
|
101
|
+
if (!ip) return false;
|
|
102
|
+
let s = String(ip).trim().replace(/^\[|\]$/g, "");
|
|
103
|
+
// IPv4-mapped IPv6 (::ffff:a.b.c.d) → 取回 IPv4 部分走 v4 逻辑
|
|
104
|
+
const mapped = s.match(/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/i);
|
|
105
|
+
if (mapped) s = mapped[1];
|
|
106
|
+
if (s.includes(":")) {
|
|
107
|
+
const low = s.toLowerCase();
|
|
108
|
+
if (low === "::" || low === "::1") return true; // 未指定 / 回环
|
|
109
|
+
if (low.startsWith("fe80")) return true; // 链路本地 fe80::/10
|
|
110
|
+
if (/^f[cd][0-9a-f]{2}:/.test(low)) return true; // 唯一本地 fc00::/7
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
const parts = s.split(".").map(Number);
|
|
114
|
+
if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return false;
|
|
115
|
+
const [a, b] = parts;
|
|
116
|
+
if (a === 127) return true;
|
|
117
|
+
if (a === 10) return true;
|
|
118
|
+
if (a === 169 && b === 254) return true;
|
|
119
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
120
|
+
if (a === 192 && b === 168) return true;
|
|
121
|
+
if (a === 0) return true;
|
|
122
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function assertPublicUrl(url) {
|
|
127
|
+
const u = new URL(url);
|
|
128
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error("仅允许 http/https");
|
|
129
|
+
const hostname = u.hostname.replace(/^\[|\]$/g, "");
|
|
130
|
+
if (net.isIP(hostname)) {
|
|
131
|
+
if (isPrivateIP(hostname)) throw new Error("SSRF 拒绝: 内网地址 " + hostname);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const addrs = await dns.lookup(hostname, { all: true, verbatim: true });
|
|
135
|
+
for (const { address } of addrs) {
|
|
136
|
+
if (isPrivateIP(address)) throw new Error("SSRF 拒绝: " + hostname + " 解析到内网 " + address);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 带 SSRF 校验的安全 fetch: 手动跟随重定向, 并对每一跳(含首发)都做 assertPublicUrl 校验。
|
|
141
|
+
// fetch 默认 redirect:"follow" 会在每次跳转时重新解析 DNS —— 攻击者用一个公网 URL 302→内网
|
|
142
|
+
// (如 http://127.0.0.1:x 或 http://169.254.169.254/)即可绕过单次 assertPublicUrl。
|
|
143
|
+
// 这里改用 redirect:"manual" 逐跳校验后再继续, 堵住"302 到内网/云元数据"的绕过。
|
|
144
|
+
async function _fetchWithSsrSafe(url, { method = "GET", headers = {}, body, signal, maxRedirects = 5 } = {}) {
|
|
145
|
+
let current = url;
|
|
146
|
+
for (let hop = 0; hop <= maxRedirects; hop++) {
|
|
147
|
+
await assertPublicUrl(current);
|
|
148
|
+
const resp = await fetch(current, {
|
|
149
|
+
method,
|
|
150
|
+
headers,
|
|
151
|
+
body: body !== undefined ? (typeof body === "string" ? body : JSON.stringify(body)) : undefined,
|
|
152
|
+
redirect: "manual",
|
|
153
|
+
signal,
|
|
154
|
+
});
|
|
155
|
+
if (resp.status >= 300 && resp.status < 400 && resp.headers.has("location")) {
|
|
156
|
+
const loc = resp.headers.get("location");
|
|
157
|
+
current = new URL(loc, current).toString(); // 相对 Location 基于 current 解析成绝对 URL
|
|
158
|
+
method = "GET"; // 重定向后不再携带 body, 并退回 GET (302/303 语义)
|
|
159
|
+
body = undefined;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
return resp;
|
|
163
|
+
}
|
|
164
|
+
throw new Error("SSRF 拒绝: 重定向次数超过 " + maxRedirects);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function httpRequest({ url, method = "GET", headers = {}, body = null, timeout = 15000 }) {
|
|
168
|
+
const ctrl = new AbortController();
|
|
169
|
+
const timer = setTimeout(() => ctrl.abort(), timeout);
|
|
170
|
+
try {
|
|
171
|
+
const resp = await _fetchWithSsrSafe(url, {
|
|
172
|
+
method,
|
|
173
|
+
headers: { "User-Agent": "PPX-Agent/0.2", ...headers },
|
|
174
|
+
body,
|
|
175
|
+
signal: ctrl.signal,
|
|
176
|
+
});
|
|
177
|
+
const text = await resp.text();
|
|
178
|
+
return { status: resp.status, ok: resp.ok, body: text.slice(0, 20000) };
|
|
179
|
+
} finally {
|
|
180
|
+
clearTimeout(timer);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
// ---------- 网页正文提取 (fetch_page: 抓正文转纯文本, 配合 web_search 读网页) ----------
|
|
186
|
+
function _htmlToText(html) {
|
|
187
|
+
const s0 = String(html || "");
|
|
188
|
+
// 去掉 script/style/nav/footer/header 噪音
|
|
189
|
+
let out = s0.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
190
|
+
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
191
|
+
.replace(/<(script|style|nav|footer|header|iframe|form|button|svg)[^>]*>[\s\S]*?<\/\1>/gi, " ");
|
|
192
|
+
out = out.replace(/<br\s*\/?>|<\/p>|<\/div>|<\/li>|<\/h[1-6]>|<\/tr>/gi, "\n");
|
|
193
|
+
out = out.replace(/<[^>]+>/g, " "); // 去掉所有标签
|
|
194
|
+
out = out.replace(/ /gi, " ").replace(/&/gi, "&")
|
|
195
|
+
.replace(/</gi, "<").replace(/>/gi, ">")
|
|
196
|
+
.replace(/"/gi, '"').replace(/'|'/gi, "'");
|
|
197
|
+
out = out.replace(/\s+/g, " "); // 压缩空白
|
|
198
|
+
return out.trim();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---------- 定时任务 ----------
|
|
202
|
+
// 2026-09-18 修复 (P1): 原构造函数只从 jobs.json 恢复 jobs 数组、从不 _schedule(),
|
|
203
|
+
// 且 add() 传入的 action 是运行时闭包, JSON.stringify 落盘时被静默丢弃 ——
|
|
204
|
+
// 进程重启后所有持久化任务没有任何定时器在跑, 触发了也因无 action 空转。
|
|
205
|
+
// 现: ① 构造时重排每日 (HH:MM) 任务; 陈旧的 once (after:Ns) 任务跨重启丢弃不补触发;
|
|
206
|
+
// ② _fire 对无 action 的恢复任务走 onFire 回调 (由注册方按 job.name 还原行为)。
|
|
207
|
+
export class Scheduler {
|
|
208
|
+
constructor(dataDir, { onFire = null } = {}) {
|
|
209
|
+
this.dir = path.join(dataDir, "scheduler");
|
|
210
|
+
ensureDir(this.dir);
|
|
211
|
+
this.file = path.join(this.dir, "jobs.json");
|
|
212
|
+
this.onFire = onFire;
|
|
213
|
+
this.timers = new Map(); // 必须先于恢复重排初始化 (2026-09-18 回归测试抓出)
|
|
214
|
+
const loaded = readJson(this.file, []);
|
|
215
|
+
// 重启恢复: 重排每日任务; once (after:Ns) 任务视为陈旧丢弃 (语义 = "N秒后一次", 跨重启已失义)
|
|
216
|
+
this.jobs = [];
|
|
217
|
+
for (const job of loaded) {
|
|
218
|
+
if (job && job.enabled === false) { this.jobs.push(job); continue; }
|
|
219
|
+
if (typeof job?.cron === "string" && /^\d{2}:\d{2}$/.test(job.cron)) {
|
|
220
|
+
this.jobs.push(job);
|
|
221
|
+
this._schedule(job);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (this.jobs.length !== loaded.length) writeJson(this.file, this.jobs);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
add({ name, cron, action, type = "once" }) {
|
|
228
|
+
const job = {
|
|
229
|
+
id: "j_" + Math.random().toString(36).slice(2, 8),
|
|
230
|
+
name, cron, action, type, enabled: true, createdAt: new Date().toISOString(),
|
|
231
|
+
};
|
|
232
|
+
this.jobs.push(job);
|
|
233
|
+
writeJson(this.file, this.jobs);
|
|
234
|
+
this._schedule(job);
|
|
235
|
+
return job;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
_schedule(job) {
|
|
239
|
+
if (this.timers.has(job.id)) clearTimeout(this.timers.get(job.id));
|
|
240
|
+
// 简化 cron: 支持 "HH:MM" (每日) 或 "after:Ns" (N秒后)
|
|
241
|
+
let delayMs = null;
|
|
242
|
+
if (typeof job.cron === "string" && job.cron.startsWith("after:")) {
|
|
243
|
+
delayMs = parseInt(job.cron.split(":")[1], 10) * 1000;
|
|
244
|
+
} else if (typeof job.cron === "string" && /^\d{2}:\d{2}$/.test(job.cron)) {
|
|
245
|
+
const [h, m] = job.cron.split(":").map(Number);
|
|
246
|
+
const now = new Date();
|
|
247
|
+
const target = new Date(now); target.setHours(h, m, 0, 0);
|
|
248
|
+
if (target <= now) target.setDate(target.getDate() + 1);
|
|
249
|
+
delayMs = target - now;
|
|
250
|
+
} else if (typeof job.cron === "number") {
|
|
251
|
+
delayMs = job.cron * 1000;
|
|
252
|
+
}
|
|
253
|
+
if (delayMs === null) return;
|
|
254
|
+
const timer = setTimeout(() => this._fire(job), delayMs);
|
|
255
|
+
this.timers.set(job.id, timer);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async _fire(job) {
|
|
259
|
+
if (job.type === "once") { this.remove(job.id); }
|
|
260
|
+
else { this._schedule(job); } // 每日任务重新排
|
|
261
|
+
try {
|
|
262
|
+
if (typeof job.action === "function") await job.action();
|
|
263
|
+
else if (this.onFire) await this.onFire(job); // 重启恢复的任务: action 闭包已丢, 走注册方回调
|
|
264
|
+
} catch (e) { console.error("定时任务失败:", e); }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
remove(id) {
|
|
268
|
+
if (this.timers.has(id)) { clearTimeout(this.timers.get(id)); this.timers.delete(id); }
|
|
269
|
+
this.jobs = this.jobs.filter((j) => j.id !== id);
|
|
270
|
+
writeJson(this.file, this.jobs);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
list() { return this.jobs.map(({ id, name, cron, enabled }) => ({ id, name, cron, enabled })); }
|
|
274
|
+
|
|
275
|
+
// 清理所有定时器 (进程关停时调用, 防 daily/repeating 任务把事件循环挂住不退出)
|
|
276
|
+
shutdown() {
|
|
277
|
+
for (const [id, t] of this.timers) { try { clearTimeout(t); } catch (e) { debug(`[tools/advanced] 已忽略异常: ${e && e.message ? e.message : e}`); } }
|
|
278
|
+
this.timers.clear();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 注册进阶工具
|
|
283
|
+
export function registerAdvancedTools(catalog, { dataDir, scheduler, onMemoryNote }) {
|
|
284
|
+
catalog.register({
|
|
285
|
+
name: "web_search",
|
|
286
|
+
description: "搜索互联网, 返回网页标题+链接+摘要。",
|
|
287
|
+
parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] },
|
|
288
|
+
execute: async (args) => {
|
|
289
|
+
try {
|
|
290
|
+
const results = await searchWeb(args.query);
|
|
291
|
+
return results.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet || ""}`).join("\n");
|
|
292
|
+
} catch (e) {
|
|
293
|
+
// 2026-10-03 修复 (P2): 网络级失败原返回优雅 JSON (ok=true), 模型可判读性差,
|
|
294
|
+
// 且故障病历闭环只认 [工具错误] 前缀 → 学习闭环对网络类失败完全失明。对齐项目错误纪律。
|
|
295
|
+
return `[工具错误] web_search: ${e.message}`;
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
catalog.register({
|
|
301
|
+
name: "http_request",
|
|
302
|
+
description: "发送 HTTP 请求 (GET/POST/PUT/DELETE), 返回状态码和响应体。",
|
|
303
|
+
parameters: {
|
|
304
|
+
type: "object",
|
|
305
|
+
properties: {
|
|
306
|
+
url: { type: "string" },
|
|
307
|
+
method: { type: "string", enum: ["GET", "POST", "PUT", "DELETE"] },
|
|
308
|
+
headers: { type: "object" },
|
|
309
|
+
body: { type: "string" },
|
|
310
|
+
},
|
|
311
|
+
required: ["url"],
|
|
312
|
+
},
|
|
313
|
+
execute: async (args) => {
|
|
314
|
+
try {
|
|
315
|
+
const r = await httpRequest(args);
|
|
316
|
+
return JSON.stringify({ status: r.status, ok: r.ok, body: r.body.slice(0, 5000) });
|
|
317
|
+
} catch (e) {
|
|
318
|
+
// 2026-10-03 修复 (P2): 同 web_search —— 网络/DNS/超时失败必须以 [工具错误] 前缀返回,
|
|
319
|
+
// 否则故障病历 (failures.record) 永远不记录这类最常见的执行失败。
|
|
320
|
+
return `[工具错误] http_request: ${e.message}`;
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
catalog.register({
|
|
326
|
+
name: "notify",
|
|
327
|
+
description: "主动向用户通道发送通知消息 (用于长任务或异步完成提醒)。",
|
|
328
|
+
parameters: { type: "object", properties: { message: { type: "string", description: "要发送的通知内容" } }, required: ["message"] },
|
|
329
|
+
execute: async (args, ctx) => {
|
|
330
|
+
const agent = ctx && ctx.agent;
|
|
331
|
+
if (agent && agent.notify) { agent.notify(args && args.message); return "notified"; }
|
|
332
|
+
return "no notify sink registered";
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
catalog.register({
|
|
337
|
+
name: "add_schedule",
|
|
338
|
+
description: "添加定时任务。cron 支持 'HH:MM'(每日) 或 'after:秒数'(N秒后执行一次)。",
|
|
339
|
+
parameters: {
|
|
340
|
+
type: "object",
|
|
341
|
+
properties: {
|
|
342
|
+
name: { type: "string" },
|
|
343
|
+
cron: { type: "string", description: "'HH:MM' 或 'after:60'" },
|
|
344
|
+
},
|
|
345
|
+
required: ["name", "cron"],
|
|
346
|
+
},
|
|
347
|
+
execute: async (args) => {
|
|
348
|
+
if (!scheduler) return JSON.stringify({ error: "调度器未初始化" });
|
|
349
|
+
const job = scheduler.add({ name: args.name, cron: args.cron, type: /^\d{2}:\d{2}$/.test(args.cron) ? "daily" : "once", action: () => onMemoryNote?.(`定时任务触发: ${args.name}`) });
|
|
350
|
+
return JSON.stringify({ ok: true, id: job.id, next: job.cron });
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
catalog.register({
|
|
355
|
+
name: "list_schedules",
|
|
356
|
+
description: "列出所有定时任务。",
|
|
357
|
+
parameters: { type: "object", properties: {} },
|
|
358
|
+
execute: async () => {
|
|
359
|
+
if (!scheduler) return "调度器未初始化";
|
|
360
|
+
return JSON.stringify(scheduler.list());
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
catalog.register({
|
|
366
|
+
name: "fetch_page",
|
|
367
|
+
description: "抓取一个网页的正文并转成纯文本 (截断到 20000 字符)。用于读文章/文档/新闻内容后回答问题。",
|
|
368
|
+
parameters: {
|
|
369
|
+
type: "object",
|
|
370
|
+
properties: { url: { type: "string", description: "要抓取的网页 URL" }, maxChars: { type: "number", description: "返回最大字符数, 默认 20000" } },
|
|
371
|
+
required: ["url"],
|
|
372
|
+
},
|
|
373
|
+
execute: async (args) => {
|
|
374
|
+
try {
|
|
375
|
+
const timeout = 15000;
|
|
376
|
+
const r = await httpRequest({ url: args.url, timeout });
|
|
377
|
+
if (!r.ok) return JSON.stringify({ error: "抓取失败 HTTP " + r.status });
|
|
378
|
+
const text = _htmlToText(r.body);
|
|
379
|
+
const max = Math.min(args.maxChars || 20000, 40000);
|
|
380
|
+
const truncated = text.length > max ? text.slice(0, max) + "\n...[已截断, 共 " + text.length + " 字符]" : text;
|
|
381
|
+
return JSON.stringify({ url: args.url, status: r.status, chars: text.length, content: truncated });
|
|
382
|
+
} catch (e) {
|
|
383
|
+
return JSON.stringify({ error: "fetch_page 失败: " + e.message });
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
return catalog;
|
|
353
389
|
}
|