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.
Files changed (106) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +265 -0
  3. package/bin/ppx-channels.js +3 -0
  4. package/bin/ppx-serve.js +6 -0
  5. package/bin/ppx.js +3 -0
  6. package/config/identity.md +6 -0
  7. package/config/ishiki.md +16 -0
  8. package/config/ppx.json +151 -0
  9. package/package.json +69 -0
  10. package/src/agent/context.js +179 -0
  11. package/src/agent/index.js +717 -0
  12. package/src/agent/prompts.js +107 -0
  13. package/src/aml-server.js +151 -0
  14. package/src/ans/eviction.js +144 -0
  15. package/src/ans/guard.js +120 -0
  16. package/src/ans/lifecycle.js +93 -0
  17. package/src/ans/proactive.js +129 -0
  18. package/src/ans/reward.js +112 -0
  19. package/src/ans/values.js +15 -0
  20. package/src/audit/audit-chain.js +167 -0
  21. package/src/audit/verifier.js +120 -0
  22. package/src/bus/circuit-breaker.js +115 -0
  23. package/src/bus/runtime-bus.js +94 -0
  24. package/src/channels/base.js +35 -0
  25. package/src/channels/feishu.js +127 -0
  26. package/src/channels/http.js +592 -0
  27. package/src/channels/index.js +110 -0
  28. package/src/channels/log.js +29 -0
  29. package/src/channels/wechat-crypto.js +74 -0
  30. package/src/channels/wechat.js +197 -0
  31. package/src/channels-cli.js +124 -0
  32. package/src/cli.js +120 -0
  33. package/src/config/channels.js +170 -0
  34. package/src/config/index.js +224 -0
  35. package/src/config/providers.js +189 -0
  36. package/src/config/settings.js +182 -0
  37. package/src/core/policy.js +272 -0
  38. package/src/core/trace.js +89 -0
  39. package/src/evolve/playbook.js +194 -0
  40. package/src/llm/client.js +446 -0
  41. package/src/llm/dsml.js +74 -0
  42. package/src/llm/embedder.js +35 -0
  43. package/src/llm/fence.js +105 -0
  44. package/src/llm/index.js +4 -0
  45. package/src/llm/retry.js +73 -0
  46. package/src/llm/router.js +98 -0
  47. package/src/mcp/client.js +375 -0
  48. package/src/mcp/index.js +116 -0
  49. package/src/memory/asset-hub.js +131 -0
  50. package/src/memory/canvas.js +131 -0
  51. package/src/memory/compaction.js +28 -0
  52. package/src/memory/experience.js +122 -0
  53. package/src/memory/fact-store.js +699 -0
  54. package/src/memory/failure-episode.js +99 -0
  55. package/src/memory/fork.js +83 -0
  56. package/src/memory/index.js +7 -0
  57. package/src/memory/l0.js +52 -0
  58. package/src/memory/l2.js +131 -0
  59. package/src/memory/l3.js +112 -0
  60. package/src/memory/memory-ticker.js +240 -0
  61. package/src/memory/session.js +398 -0
  62. package/src/mode/blackboard.js +49 -0
  63. package/src/mode/graph.js +41 -0
  64. package/src/mode/index.js +64 -0
  65. package/src/mode/legion.js +51 -0
  66. package/src/mode/plan-exec.js +50 -0
  67. package/src/mode/router.js +40 -0
  68. package/src/orchestrator/agent-worker.js +70 -0
  69. package/src/orchestrator/dag.js +83 -0
  70. package/src/orchestrator/index.js +2 -0
  71. package/src/orchestrator/legion.js +188 -0
  72. package/src/orchestrator/supervisor.js +177 -0
  73. package/src/persona/index.js +29 -0
  74. package/src/plugin/builtin.js +212 -0
  75. package/src/plugin/context.js +79 -0
  76. package/src/plugin/index.js +62 -0
  77. package/src/seam/registry.js +98 -0
  78. package/src/seam/shell.js +55 -0
  79. package/src/selfheal/evolve.js +68 -0
  80. package/src/selfheal/healer.js +167 -0
  81. package/src/selfheal/run.js +9 -0
  82. package/src/server.js +60 -0
  83. package/src/services/learning-service.js +177 -0
  84. package/src/services/memory-health.js +99 -0
  85. package/src/services/memory-service.js +160 -0
  86. package/src/skills/loader.js +150 -0
  87. package/src/skills/verify.js +100 -0
  88. package/src/tools/advanced.js +353 -0
  89. package/src/tools/builtin.js +298 -0
  90. package/src/tools/catalog.js +159 -0
  91. package/src/tools/command-guard.js +112 -0
  92. package/src/tools/custom.js +47 -0
  93. package/src/tools/delegate.js +297 -0
  94. package/src/tools/document.js +253 -0
  95. package/src/tools/governance.js +260 -0
  96. package/src/tools/index.js +11 -0
  97. package/src/tools/methods.js +178 -0
  98. package/src/tools/ocr.js +59 -0
  99. package/src/tools/seam.js +125 -0
  100. package/src/tools/selfmod.js +176 -0
  101. package/src/utils/logger.js +17 -0
  102. package/src/utils/pii.js +42 -0
  103. package/src/utils/store.js +108 -0
  104. package/src/utils/text.js +16 -0
  105. package/src/utils/trace.js +153 -0
  106. package/src/utils/winutf8.js +15 -0
@@ -0,0 +1,592 @@
1
+ // src/channels/http.js - HTTP 通道 (零依赖)
2
+ // 起一个本地 HTTP server, 接收 POST /message 消息, 调 agent 回复
3
+ import http from "node:http";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import crypto from "node:crypto";
7
+ import { Channel } from "./base.js";
8
+ import {
9
+ listProviders, addProvider, updateProvider, removeProvider, reorderProviders,
10
+ } from "../config/providers.js";
11
+ import { getSettings, updateSettings } from "../config/settings.js";
12
+ import { suggestProactive } from "../ans/proactive.js";
13
+ import { ensureDir, atomicWrite, readText } from "../utils/store.js";
14
+
15
+ const MAX_BODY = 1024 * 1024; // 请求体上限 1MB
16
+ const RATE_PER_MIN = 60; // 每 IP 每分钟最大请求数 (令牌桶)
17
+ const RATE_WINDOW_MS = 60_000;
18
+ const MAX_INFLIGHT = 4; // 同时处理的最大对话请求数 (超出立即 429, 防单 agent 被并发压垮)
19
+
20
+ // HTTP 鉴权 token 解析 / 生成 (v1.0.9 起持久化, 重启复用, 免去 Web 前端每次重贴 token)
21
+ // 优先级: 1) 显式配置 (env/ppx.json 的 channels.http.auth_token) > 2) 数据目录持久化文件复用 > 3) 新生成并原子落盘
22
+ // @param {object} opts
23
+ // @param {string} opts.configured 显式配置的 token (PPX_AUTH_TOKEN 或 config 里的 auth_token; 空串视为未配置)
24
+ // @param {string|null} opts.persistedFile 自动生成 token 的持久化文件路径 (无则仅返回生成, 不落盘)
25
+ // @returns {{ token:string, source:"configured"|"persisted"|"generated", generated:boolean }}
26
+ // 纯函数, 便于单测: 不依赖 agent/实例状态, 全部副作用 (读/写文件) 走参数传入的路径
27
+ export function resolveAuthToken({ configured = "", persistedFile = null } = {}) {
28
+ const explicit = String(configured || "").trim();
29
+ // 优先级 1: 显式配置 (env/config) — 以身作则, 不读写持久化文件
30
+ if (explicit) return { token: explicit, source: "configured", generated: false };
31
+
32
+ // 优先级 2: 复用持久化文件里的旧 token (非空)
33
+ if (persistedFile) {
34
+ const old = String(readText(persistedFile, "") || "").trim();
35
+ if (old) return { token: old, source: "persisted", generated: false };
36
+ }
37
+
38
+ // 优先级 3: 新生成随机 token; 若有持久化路径则原子落盘 (写 .tmp 再 rename, 全程目录存在)
39
+ const token = crypto.randomBytes(24).toString("hex");
40
+ if (persistedFile) {
41
+ try {
42
+ ensureDir(path.dirname(persistedFile));
43
+ atomicWrite(persistedFile, token); // .tmp + rename: 原子替换, 并发/崩溃不损坏
44
+ } catch {
45
+ // token 已生成但在内存可用; 落盘失败仅丢失"跨重启复用", 不阻断启动
46
+ }
47
+ }
48
+ return { token, source: "generated", generated: true };
49
+ }
50
+
51
+ export class HttpChannel extends Channel {
52
+ constructor(agent, { port = 8899, host = "127.0.0.1" } = {}) {
53
+ super("http", agent);
54
+ this.port = port;
55
+ this.host = host;
56
+ this.server = null;
57
+ this.agent = agent;
58
+ this.publicDir = path.join(this.agent.root, "public");
59
+ // v1.0.9: token 解析延迟到 connect() 的 _ensureToken, 以便持久化文件与 agent.dataDir 就绪
60
+ // 这里只缓存显式配置 (env/config), 交给 _ensureToken 走"显式 > 持久化 > 新生成"的优先级
61
+ this.authToken = process.env.PPX_AUTH_TOKEN || this._tokenFromConfig();
62
+ this._persistedTokenFile = agent.dataDir ? path.join(agent.dataDir, "http-token") : null;
63
+ this._buckets = new Map(); // ip -> {tokens, last}
64
+ // v1.0.8: webhook 路由注册表 (feishu/wechat 通道挂载), 单一 request handler 分发, 无多 listener 竞态
65
+ this.webhookRoutes = new Map(); // path -> async (req, res) => void
66
+ // CORS 来源白名单 (v1.0.7): channels.http.cors_origin 数组; 未配置默认 * (向后兼容)
67
+ // 配置后仅放行白名单 origin, 其余跨域请求 403 (token 泄露时降低任意跨站读取风险)
68
+ this.corsOrigins = this._corsFromConfig();
69
+ this._inFlight = 0; // 当前处理中的对话请求数
70
+ }
71
+
72
+ // 简单并发护栏: 用计数信号量限制同时处理的对话请求, 超出立即 429
73
+ // 防止多个慢请求无限叠加占用单线程主 agent (可选的背压层, 区别于每 IP 令牌桶限流)
74
+ _acquire(res) {
75
+ if (this._inFlight >= MAX_INFLIGHT) {
76
+ res.writeHead(429, { "Content-Type": "application/json", "Retry-After": "5" });
77
+ res.end(JSON.stringify({ error: "too many concurrent requests, retry shortly" }));
78
+ return false;
79
+ }
80
+ this._inFlight += 1;
81
+ return true;
82
+ }
83
+ _release() { if (this._inFlight > 0) this._inFlight -= 1; }
84
+
85
+ // v1.0.8: webhook 通道注册路由 (路径匹配即由该通道处理, 不经过主逻辑)
86
+ registerWebhook(path, handler) {
87
+ if (typeof handler === "function") this.webhookRoutes.set(path, handler);
88
+ return () => this.webhookRoutes.delete(path);
89
+ }
90
+
91
+ _corsFromConfig() {
92
+ try {
93
+ const c = this.agent?.config?.channels?.http?.cors_origin;
94
+ if (c === undefined || c === null || c === "*") return [];
95
+ if (Array.isArray(c)) return c.filter(Boolean);
96
+ if (typeof c === "string") return [c];
97
+ } catch { /* 配置异常回退默认 */ }
98
+ return [];
99
+ }
100
+
101
+ _tokenFromConfig() {
102
+ try {
103
+ const p = path.join(this.agent.root, "config", "ppx.json");
104
+ if (fs.existsSync(p)) {
105
+ const cfg = JSON.parse(fs.readFileSync(p, "utf8"));
106
+ return (cfg.channels && cfg.channels.http && cfg.channels.http.auth_token) || "";
107
+ }
108
+ } catch {}
109
+ return "";
110
+ }
111
+
112
+ // 若未配置 token: 复用持久化文件(重启不变)或自动生成并原子落盘 (类似 Jupyter token)
113
+ _ensureToken() {
114
+ if (this.authToken) return this.authToken;
115
+ const r = resolveAuthToken({ configured: process.env.PPX_AUTH_TOKEN || "", persistedFile: this._persistedTokenFile });
116
+ this.authToken = r.token;
117
+ if (r.source === "persisted") {
118
+ console.log(" ℹ HTTP 认证 token 复用自持久化文件 (重启不更换, 前端无需重贴)");
119
+ } else {
120
+ console.log("");
121
+ console.log(" ⚠️ HTTP 认证 token 未配置, 已自动生成并持久化 (重启复用):");
122
+ console.log(` PPX_AUTH_TOKEN=${this.authToken}`);
123
+ console.log(` 用法: Authorization: Bearer ${this.authToken}`);
124
+ console.log(` token 已保存到 ${this._persistedTokenFile}, 重启不再更换`);
125
+ console.log(" (或设置 config/ppx.json 的 channels.http.auth_token)");
126
+ console.log("");
127
+ }
128
+ return this.authToken;
129
+ }
130
+
131
+ _authed(req, res) {
132
+ if (!this.authToken) return true; // 兼容旧调用: _ensureToken 在 connect 时已执行
133
+ const h = req.headers["authorization"] || "";
134
+ return h === "Bearer " + this.authToken;
135
+ }
136
+
137
+ // 简单令牌桶限流: 每 IP 60 req/min
138
+ _rateLimit(req, res) {
139
+ const ip = req.socket?.remoteAddress || "unknown";
140
+ const now = Date.now();
141
+ let b = this._buckets.get(ip);
142
+ if (!b) {
143
+ b = { tokens: RATE_PER_MIN, last: now };
144
+ this._buckets.set(ip, b);
145
+ }
146
+ const refill = Math.floor((now - b.last) / RATE_WINDOW_MS);
147
+ if (refill > 0) {
148
+ b.tokens = Math.min(RATE_PER_MIN, b.tokens + refill * RATE_PER_MIN);
149
+ b.last = now;
150
+ }
151
+ if (b.tokens <= 0) {
152
+ res.writeHead(429, { "Content-Type": "application/json", "Retry-After": "60" });
153
+ res.end(JSON.stringify({ error: "rate limited" }));
154
+ return false;
155
+ }
156
+ b.tokens -= 1;
157
+ return true;
158
+ }
159
+
160
+ // 读取请求体, 限制大小
161
+ async _readBody(req, res) {
162
+ let body = "";
163
+ for await (const chunk of req) {
164
+ body += chunk;
165
+ if (body.length > MAX_BODY) {
166
+ res.writeHead(413, { "Content-Type": "application/json" });
167
+ res.end(JSON.stringify({ error: "request too large" }));
168
+ return null;
169
+ }
170
+ }
171
+ return body;
172
+ }
173
+
174
+ async connect() {
175
+ this._ensureToken(); // 启动时确保有 token
176
+ this.server = http.createServer(async (req, res) => {
177
+ // v1.0.8: webhook 路由分发 (feishu/wechat 等): 匹配路径交给对应通道, 不走主逻辑
178
+ const reqPath0 = (req.url || "/").split("?")[0];
179
+ const wh = this.webhookRoutes.get(reqPath0);
180
+ if (wh) {
181
+ try { await wh(req, res); } catch (e) {
182
+ try { if (!res.writableEnded) { res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: e.message })); } } catch {}
183
+ }
184
+ return;
185
+ }
186
+ // CORS (v1.0.7): 默认 * (兼容); 配置 cors_origin 白名单时校验浏览器来源 (无 Origin 的非浏览器请求不受 CORS 约束)
187
+ const origin = req.headers.origin;
188
+ let allowOrigin = "*";
189
+ if (this.corsOrigins.length) {
190
+ allowOrigin = !origin ? "*" : (this.corsOrigins.includes(origin) ? origin : null);
191
+ }
192
+ if (!allowOrigin) {
193
+ res.writeHead(403, { "Content-Type": "application/json" });
194
+ res.end(JSON.stringify({ error: "origin not allowed" }));
195
+ return;
196
+ }
197
+ res.setHeader("Access-Control-Allow-Origin", allowOrigin);
198
+ if (allowOrigin !== "*") res.setHeader("Vary", "Origin");
199
+ res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
200
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
201
+ if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
202
+
203
+ // 静态页面与 /health 不设限, 其余 API 限流
204
+ const reqPath = (req.url || "/").split("?")[0];
205
+ if (!(req.method === "GET" && (reqPath === "/" || reqPath === "/index.html" || reqPath === "/health"))) {
206
+ if (!this._rateLimit(req, res)) return;
207
+ }
208
+
209
+ if (req.method === "GET" && req.url === "/health") {
210
+ res.writeHead(200, { "Content-Type": "application/json" });
211
+ res.end(JSON.stringify({ status: "ok", agent: this.agent.config.agent?.name || "ppx" }));
212
+ return;
213
+ }
214
+
215
+ if (req.method === "POST" && req.url === "/message") {
216
+ if (!this._authed(req, res)) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "unauthorized" })); return; }
217
+ const body = await this._readBody(req, res);
218
+ if (body === null) return;
219
+ try {
220
+ const data = JSON.parse(body);
221
+ const text = data.message || data.text || "";
222
+ if (!text) { res.writeHead(400); res.end(JSON.stringify({ error: "missing message" })); return; }
223
+ const sessionKey = data.sessionId || "default";
224
+ if (!this._acquire(res)) return;
225
+ try {
226
+ const reply = await this.agent.chat(String(text), { sessionKey });
227
+ res.writeHead(200, { "Content-Type": "application/json" });
228
+ res.end(JSON.stringify({ reply, sessionId: sessionKey, agent: this.agent.config.agent?.name || "ppx" }));
229
+ } finally { this._release(); }
230
+ } catch (e) {
231
+ res.writeHead(500, { "Content-Type": "application/json" });
232
+ res.end(JSON.stringify({ error: e.message }));
233
+ }
234
+ return;
235
+ }
236
+
237
+ // SSE 流式对话
238
+ if (req.method === "POST" && (req.url === "/message/stream" || req.url === "/chat")) {
239
+ if (!this._authed(req, res)) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "unauthorized" })); return; }
240
+ const body = await this._readBody(req, res);
241
+ if (body === null) return;
242
+ try {
243
+ const data = JSON.parse(body);
244
+ const text = data.message || data.text || "";
245
+ if (!text) { res.writeHead(400); res.end(JSON.stringify({ error: "missing message" })); return; }
246
+ const sessionKey = data.sessionId || "default";
247
+ res.writeHead(200, {
248
+ "Content-Type": "text/event-stream",
249
+ "Cache-Control": "no-cache",
250
+ "Connection": "keep-alive",
251
+ "X-Accel-Buffering": "no",
252
+ });
253
+ if (!this._acquire(res)) { try { res.end(); } catch {} return; }
254
+ try {
255
+ const send = (obj) => res.write("data: " + JSON.stringify(obj) + "\n\n");
256
+ let full = "";
257
+ const reply = await this.agent.chatStream(String(text), {
258
+ sessionKey,
259
+ onDelta: (d) => { full += d; try { send({ type: "delta", content: d }); } catch {} },
260
+ onTool: (ev) => { try { send({ type: "tool", tool: ev.tool, status: ev.type, args: ev.args, ok: ev.ok, durationMs: ev.durationMs }); } catch {} }, // 工具调用可视化
261
+ onStep: (ev) => { try { send({ type: "step", round: ev.round, maxRounds: ev.maxRounds }); } catch {} }, // turn/step 推理轮次进度
262
+ });
263
+ const finalContent = full || reply;
264
+ send({ type: "done", content: finalContent, sessionId: sessionKey });
265
+ res.end();
266
+ } finally { this._release(); }
267
+ } catch (e) {
268
+ try { res.write("data: " + JSON.stringify({ type: "error", error: e.message }) + "\n\n"); } catch {}
269
+ try { res.end(); } catch {}
270
+ }
271
+ return;
272
+ }
273
+
274
+ // 会话列表 (Web UI 多会话管理) [P1#6]
275
+ if (req.method === "GET" && req.url === "/sessions") {
276
+ if (!this._authed(req, res)) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "unauthorized" })); return; }
277
+ const list = (this.agent.sessionStore && this.agent.sessionStore.list) ? this.agent.sessionStore.list() : [];
278
+ res.writeHead(200, { "Content-Type": "application/json" });
279
+ res.end(JSON.stringify({ sessions: list }));
280
+ return;
281
+ }
282
+
283
+ // 会话历史 (Web UI 切换会话时恢复消息显示): GET /sessions/:key/history
284
+ if (req.method === "GET" && /^\/sessions\/[^/]+\/history$/.test(reqPath)) {
285
+ if (!this._authed(req, res)) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "unauthorized" })); return; }
286
+ const key = decodeURIComponent(reqPath.split("/")[2]);
287
+ const msgs = this.agent.sessionStore && typeof this.agent.sessionStore.deriveMessages === "function"
288
+ ? this.agent.sessionStore.deriveMessages(key)
289
+ : [];
290
+ res.writeHead(200, { "Content-Type": "application/json" });
291
+ res.end(JSON.stringify({ sessionId: key, messages: msgs }));
292
+ return;
293
+ }
294
+
295
+ // 会话重命名 [P1#6]
296
+ if (req.method === "POST" && req.url === "/sessions/rename") {
297
+ if (!this._authed(req, res)) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "unauthorized" })); return; }
298
+ const body = await this._readBody(req, res);
299
+ if (body === null) return;
300
+ const ok = this.agent.sessionStore && typeof this.agent.sessionStore.rename === "function"
301
+ ? this.agent.sessionStore.rename(body.from, body.to) : false;
302
+ res.writeHead(ok ? 200 : 404, { "Content-Type": "application/json" });
303
+ res.end(JSON.stringify({ ok }));
304
+ return;
305
+ }
306
+ // 会话删除 [P1#6]
307
+ if (req.method === "POST" && req.url === "/sessions/delete") {
308
+ if (!this._authed(req, res)) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "unauthorized" })); return; }
309
+ const body = await this._readBody(req, res);
310
+ if (body === null) return;
311
+ if (this.agent.sessionStore) this.agent.sessionStore.delete(body.key || "default");
312
+ res.writeHead(200, { "Content-Type": "application/json" });
313
+ res.end(JSON.stringify({ ok: true }));
314
+ return;
315
+ }
316
+
317
+ // 会话重置
318
+ if (req.method === "POST" && req.url === "/reset") {
319
+ if (!this._authed(req, res)) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "unauthorized" })); return; }
320
+ const body = await this._readBody(req, res);
321
+ if (body === null) return;
322
+ try {
323
+ const data = JSON.parse(body || "{}");
324
+ this.agent.resetSession(data.sessionId || "default");
325
+ } catch {}
326
+ res.writeHead(200, { "Content-Type": "application/json" });
327
+ res.end(JSON.stringify({ ok: true }));
328
+ return;
329
+ }
330
+
331
+ // ---- 静态界面 + 可观测 API ----
332
+ // 通用静态文件服务 (public/ 下任意文件, 含 vendor/ 资源, 防路径穿越)
333
+ if (req.method === "GET" && !reqPath.startsWith("/api/") && !["/message","/message/stream","/chat","/reset"].some(p=>reqPath===p)) {
334
+ const rel = reqPath === "/" ? "index.html" : reqPath.replace(/^\//, "");
335
+ const file = path.resolve(this.publicDir, rel);
336
+ if (file.startsWith(this.publicDir + path.sep) && fs.existsSync(file) && fs.statSync(file).isFile()) {
337
+ const ext = path.extname(file).toLowerCase();
338
+ const mime = { ".html":"text/html; charset=utf-8", ".js":"text/javascript", ".css":"text/css", ".json":"application/json", ".png":"image/png", ".jpg":"image/jpeg", ".svg":"image/svg+xml", ".ico":"image/x-icon" }[ext] || "application/octet-stream";
339
+ res.writeHead(200, { "Content-Type": mime });
340
+ res.end(fs.readFileSync(file));
341
+ return;
342
+ }
343
+ if (reqPath !== "/") { res.writeHead(404); res.end("not found"); return; }
344
+ }
345
+ // 静态页面
346
+ if (req.method === "GET" && (reqPath === "/" || reqPath === "/index.html")) {
347
+ const html = path.join(this.publicDir, "index.html");
348
+ if (fs.existsSync(html)) {
349
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
350
+ res.end(fs.readFileSync(html, "utf8"));
351
+ } else {
352
+ res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
353
+ res.end("皮皮虾服务已启动。public/index.html 未找到。");
354
+ }
355
+ return;
356
+ }
357
+ // API 端点认证
358
+ const protectedApi = reqPath.startsWith("/api/");
359
+ if (protectedApi && !this._authed(req, res)) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "unauthorized" })); return; }
360
+ // 轨迹 API
361
+ if (req.method === "GET" && reqPath === "/api/traces") {
362
+ const limit = Number((req.url.split("limit=")[1] || "").split("&")[0] || 50);
363
+ res.writeHead(200, { "Content-Type": "application/json" });
364
+ res.end(JSON.stringify(this.agent.traces.read(undefined, limit)));
365
+ return;
366
+ }
367
+ // 统计 API (聚合记忆/轨迹/工具/经验/自愈)
368
+ if (req.method === "GET" && reqPath === "/api/stats") {
369
+ res.writeHead(200, { "Content-Type": "application/json" });
370
+ res.end(JSON.stringify(this.agent.stats ? this.agent.stats() : this.agent.traces.stats()));
371
+ return;
372
+ }
373
+ // 记忆 API
374
+ if (req.method === "GET" && reqPath === "/api/memory") {
375
+ res.writeHead(200, { "Content-Type": "application/json" });
376
+ const facts = this.agent.facts ? this.agent.facts.list().slice(0, 20).map((f) => ({ content: f.content, score: f.score, type: f.type })) : [];
377
+ const scenes = this.agent.scenes ? this.agent.scenes.listWithDesc().slice(-10) : [];
378
+ res.end(JSON.stringify({ facts, scenes }));
379
+ return;
380
+ }
381
+ // 主动任务生成 API (ANS 自主性): 扫描记忆待办/偏好, 返回主动提醒 + 结构化 items (含 id)
382
+ if (req.method === "GET" && reqPath === "/api/proactive") {
383
+ res.writeHead(200, { "Content-Type": "application/json" });
384
+ try {
385
+ const out = await suggestProactive(this.agent);
386
+ res.end(JSON.stringify({ message: out ? out.text : null, items: out ? out.items : [] }));
387
+ } catch (e) {
388
+ res.end(JSON.stringify({ message: null, items: [], error: e.message }));
389
+ }
390
+ return;
391
+ }
392
+ // 标记待办完成 (ANS): POST /api/proactive/done { id } → 之后不再主动提醒
393
+ if (req.method === "POST" && reqPath === "/api/proactive/done") {
394
+ if (!this._authed(req, res)) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "unauthorized" })); return; }
395
+ const body = await this._readBody(req, res);
396
+ if (body === null) return;
397
+ try {
398
+ const data = JSON.parse(body || "{}");
399
+ const ok = this.agent.proactiveMarkDone(data.id);
400
+ res.writeHead(ok ? 200 : 404, { "Content-Type": "application/json" });
401
+ res.end(JSON.stringify({ ok }));
402
+ } catch (e) {
403
+ res.writeHead(400, { "Content-Type": "application/json" });
404
+ res.end(JSON.stringify({ error: e.message }));
405
+ }
406
+ return;
407
+ }
408
+ // 生命周期状态 API (ANS): 阶段/年龄/进化/繁衍
409
+ if (req.method === "GET" && reqPath === "/api/lifecycle") {
410
+ res.writeHead(200, { "Content-Type": "application/json" });
411
+ res.end(JSON.stringify(this.agent.lifecycleStatus ? this.agent.lifecycleStatus() : {}));
412
+ return;
413
+ }
414
+
415
+ // 提供方 API [本轮新增] - 模型配置 Web UI 后端
416
+ // GET /api/providers 列出全部提供方 (key 抹掉, 只返 api_key_set 标志)
417
+ // POST /api/providers 新增 (body: { provider: {...} })
418
+ // PUT /api/providers 更新 (body: { id, patch: {...} })
419
+ // DELETE /api/providers 删除 (body: { id })
420
+ // POST /api/providers/test 健康探测 (body: { id }), 复用 LLMClient.health()
421
+ // POST /api/providers/reorder 重排 (body: { order: [id1, id2, ...] })
422
+ if (req.method === "GET" && reqPath === "/api/providers") {
423
+ if (!this._authed(req, res)) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "unauthorized" })); return; }
424
+ const providers = listProviders(this.agent.root);
425
+ const defaultId = providers[0] ? providers[0].id : null;
426
+ res.writeHead(200, { "Content-Type": "application/json" });
427
+ res.end(JSON.stringify({ providers, default_id: defaultId }));
428
+ return;
429
+ }
430
+ if (req.method === "POST" && reqPath === "/api/providers") {
431
+ if (!this._authed(req, res)) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "unauthorized" })); return; }
432
+ const body = await this._readBody(req, res);
433
+ if (body === null) return;
434
+ try {
435
+ const data = JSON.parse(body || "{}");
436
+ const raw = data.provider || data;
437
+ const created = addProvider(this.agent.root, raw);
438
+ // 热重载 (新增默认时让 agent 立即可用)
439
+ this.agent.reloadProviders();
440
+ res.writeHead(200, { "Content-Type": "application/json" });
441
+ res.end(JSON.stringify({ ok: true, provider: created }));
442
+ } catch (e) {
443
+ res.writeHead(400, { "Content-Type": "application/json" });
444
+ res.end(JSON.stringify({ error: e.message }));
445
+ }
446
+ return;
447
+ }
448
+ if (req.method === "PUT" && reqPath === "/api/providers") {
449
+ if (!this._authed(req, res)) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "unauthorized" })); return; }
450
+ const body = await this._readBody(req, res);
451
+ if (body === null) return;
452
+ try {
453
+ const data = JSON.parse(body || "{}");
454
+ if (!data.id) throw new Error("缺少 id");
455
+ const updated = updateProvider(this.agent.root, data.id, data.patch || {});
456
+ this.agent.reloadProviders();
457
+ res.writeHead(200, { "Content-Type": "application/json" });
458
+ res.end(JSON.stringify({ ok: true, provider: updated }));
459
+ } catch (e) {
460
+ const code = /不存在|缺少/.test(e.message) ? 404 : 400;
461
+ res.writeHead(code, { "Content-Type": "application/json" });
462
+ res.end(JSON.stringify({ error: e.message }));
463
+ }
464
+ return;
465
+ }
466
+ if (req.method === "DELETE" && reqPath === "/api/providers") {
467
+ if (!this._authed(req, res)) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "unauthorized" })); return; }
468
+ const body = await this._readBody(req, res);
469
+ if (body === null) return;
470
+ try {
471
+ const data = JSON.parse(body || "{}");
472
+ if (!data.id) throw new Error("缺少 id");
473
+ const removed = removeProvider(this.agent.root, data.id);
474
+ this.agent.reloadProviders();
475
+ res.writeHead(200, { "Content-Type": "application/json" });
476
+ res.end(JSON.stringify({ ok: true, provider: removed }));
477
+ } catch (e) {
478
+ const code = /不存在|缺少/.test(e.message) ? 404 : 400;
479
+ res.writeHead(code, { "Content-Type": "application/json" });
480
+ res.end(JSON.stringify({ error: e.message }));
481
+ }
482
+ return;
483
+ }
484
+ if (req.method === "POST" && reqPath === "/api/providers/test") {
485
+ if (!this._authed(req, res)) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "unauthorized" })); return; }
486
+ const body = await this._readBody(req, res);
487
+ if (body === null) return;
488
+ try {
489
+ const data = JSON.parse(body || "{}");
490
+ if (!data.id) throw new Error("缺少 id");
491
+ // 优先复用 agent 已有的 LLM 客户端 (含测试注入的 stub), 避免对磁盘配置的真实网络探测
492
+ // 兜底顺序: allProviders → agent.llm → 磁盘临时构造
493
+ let client = (this.agent.allProviders || []).find((c) => c.providerId === data.id);
494
+ let fromCache = !!client;
495
+ if (!client && this.agent.llm && this.agent.llm.providerId === data.id) {
496
+ client = this.agent.llm;
497
+ fromCache = true;
498
+ }
499
+ if (!client) {
500
+ const { readConfig } = await import("../config/providers.js");
501
+ const { providers } = readConfig(this.agent.root);
502
+ const p = providers.find((x) => x.id === data.id);
503
+ if (!p) throw new Error("提供方不存在");
504
+ const { LLMClient } = await import("../llm/client.js");
505
+ client = new LLMClient(p);
506
+ }
507
+ const healthy = await client.health();
508
+ const detail = healthy
509
+ ? `${client.backend === "openclaw" ? "openclaw 后端: Node 版本满足" : client.backend === "deepseek" ? "dsh 源码就绪" : fromCache ? "复用 agent 客户端, 探测通过" : "API 端点可达"}`
510
+ : "探测失败, 请检查 key/base_url/engine 路径/Node 版本";
511
+ res.writeHead(200, { "Content-Type": "application/json" });
512
+ res.end(JSON.stringify({ ok: true, healthy, detail, source: fromCache ? "agent-cache" : "disk-config" }));
513
+ } catch (e) {
514
+ res.writeHead(400, { "Content-Type": "application/json" });
515
+ res.end(JSON.stringify({ error: e.message }));
516
+ }
517
+ return;
518
+ }
519
+ if (req.method === "POST" && reqPath === "/api/providers/reorder") {
520
+ if (!this._authed(req, res)) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "unauthorized" })); return; }
521
+ const body = await this._readBody(req, res);
522
+ if (body === null) return;
523
+ try {
524
+ const data = JSON.parse(body || "{}");
525
+ const providers = reorderProviders(this.agent.root, data.order || []);
526
+ this.agent.reloadProviders();
527
+ res.writeHead(200, { "Content-Type": "application/json" });
528
+ res.end(JSON.stringify({ ok: true, providers }));
529
+ } catch (e) {
530
+ res.writeHead(400, { "Content-Type": "application/json" });
531
+ res.end(JSON.stringify({ error: e.message }));
532
+ }
533
+ return;
534
+ }
535
+
536
+ // 通用设置 API: 用户名 / HTTP 端口 / 安全 / agent 预设
537
+ // GET /api/settings 返回可编辑设置 (敏感字段只回 set 标志)
538
+ // PUT /api/settings 更新 (body: { patch: { user?, http?, security?, agent? } })
539
+ if (req.method === "GET" && reqPath === "/api/settings") {
540
+ if (!this._authed(req, res)) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "unauthorized" })); return; }
541
+ res.writeHead(200, { "Content-Type": "application/json" });
542
+ res.end(JSON.stringify({ settings: getSettings(this.agent.root) }));
543
+ return;
544
+ }
545
+ if (req.method === "PUT" && reqPath === "/api/settings") {
546
+ if (!this._authed(req, res)) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "unauthorized" })); return; }
547
+ const body = await this._readBody(req, res);
548
+ if (body === null) return;
549
+ try {
550
+ const data = JSON.parse(body || "{}");
551
+ const settings = updateSettings(this.agent.root, data.patch || {});
552
+ // 热重载: 用户名/安全/agent 预设立即生效
553
+ this.agent.reloadSettings();
554
+ res.writeHead(200, { "Content-Type": "application/json" });
555
+ res.end(JSON.stringify({ ok: true, settings }));
556
+ } catch (e) {
557
+ res.writeHead(400, { "Content-Type": "application/json" });
558
+ res.end(JSON.stringify({ error: e.message }));
559
+ }
560
+ return;
561
+ }
562
+
563
+ res.writeHead(404); res.end("not found");
564
+
565
+ });
566
+
567
+ await new Promise((resolve, reject) => {
568
+ this.server.once("error", reject);
569
+ this.server.listen(this.port, this.host, resolve);
570
+ });
571
+ this.connected = true;
572
+ return this;
573
+ }
574
+
575
+ async send(to, text) { return text; } // HTTP 是请求-响应, 直接返回
576
+
577
+ // 连通性测试: 用独立实例起临时 server 验证端口可绑定
578
+ async test() {
579
+ try {
580
+ await this.connect();
581
+ await this.disconnect();
582
+ return { ok: true, detail: `HTTP 通道可启动: http://${this.host}:${this.port}` };
583
+ } catch (e) {
584
+ return { ok: false, detail: e.message };
585
+ }
586
+ }
587
+
588
+ async disconnect() {
589
+ if (this.server) { await new Promise((r) => this.server.close(r)); this.server = null; }
590
+ this.connected = false;
591
+ }
592
+ }