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.
Files changed (186) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +218 -265
  3. package/bin/ppx-channels.js +2 -2
  4. package/bin/ppx-serve.js +5 -5
  5. package/bin/ppx-setup.js +124 -0
  6. package/bin/ppx-web.js +140 -0
  7. package/bin/ppx.js +2 -2
  8. package/config/identity.md +6 -6
  9. package/config/ishiki.md +16 -16
  10. package/config/ppx.json +15 -151
  11. package/config/ppx.json.example +143 -0
  12. package/package.json +17 -10
  13. package/skills/.usage.json +6 -0
  14. package/skills/agent-professional-training/SKILL.md +94 -0
  15. package/skills/brainstorm/SKILL.md +24 -0
  16. package/skills/cupid-lover-comms/SKILL.md +37 -0
  17. package/skills/debug/SKILL.md +26 -0
  18. package/skills/plan/SKILL.md +25 -0
  19. package/skills/ponytail/SKILL.md +25 -0
  20. package/skills/ppx-memory/SKILL.md +91 -0
  21. package/skills/ppx-memory/scripts/cli.js +192 -0
  22. package/skills/ppx-memory/scripts/experience.js +133 -0
  23. package/skills/ppx-memory/scripts/fact-store.js +842 -0
  24. package/skills/ppx-memory/scripts/l0.js +52 -0
  25. package/skills/ppx-memory/scripts/l2.js +146 -0
  26. package/skills/ppx-memory/scripts/l3.js +112 -0
  27. package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
  28. package/skills/ppx-memory/scripts/pii.js +42 -0
  29. package/skills/ppx-memory/scripts/schema.js +80 -0
  30. package/skills/ppx-memory/scripts/session.js +398 -0
  31. package/skills/ppx-memory/scripts/similarity.js +43 -0
  32. package/skills/ppx-memory/scripts/store.js +116 -0
  33. package/skills/ppx-memory/scripts/wal.js +38 -0
  34. package/skills/ppx-selfheal/SKILL.md +24 -0
  35. package/skills/ppx-selfheal/scripts/cli.js +80 -0
  36. package/skills/ppx-selfheal/scripts/healer.js +184 -0
  37. package/skills/ppx-selfheal/scripts/logger.js +17 -0
  38. package/skills/ppx-selfheal/scripts/store.js +116 -0
  39. package/skills/prompt-depth-kit/SKILL.md +28 -0
  40. package/skills/session-naming/SKILL.md +36 -0
  41. package/skills/verify/SKILL.md +25 -0
  42. package/src/agent/index.js +1340 -717
  43. package/src/agent/prompts.js +47 -3
  44. package/src/aml-server.js +197 -151
  45. package/src/ans/eviction.js +123 -143
  46. package/src/ans/guard.js +159 -120
  47. package/src/ans/lifecycle.js +96 -93
  48. package/src/ans/proactive.js +112 -129
  49. package/src/ans/reward.js +95 -111
  50. package/src/ans/values.js +15 -15
  51. package/src/audit/audit-chain.js +43 -7
  52. package/src/audit/verifier.js +157 -120
  53. package/src/bus/circuit-breaker.js +9 -1
  54. package/src/bus/runtime-bus.js +107 -93
  55. package/src/channels/base.js +57 -34
  56. package/src/channels/feishu.js +118 -126
  57. package/src/channels/http.js +1090 -592
  58. package/src/channels/index.js +111 -110
  59. package/src/channels/log.js +29 -29
  60. package/src/channels/wechat-crypto.js +73 -74
  61. package/src/channels/wechat.js +191 -197
  62. package/src/channels/workspace.js +94 -0
  63. package/src/channels-cli.js +126 -124
  64. package/src/cli.js +129 -120
  65. package/src/commands/index.js +142 -0
  66. package/src/config/channels.js +137 -170
  67. package/src/config/index.js +277 -224
  68. package/src/config/placeholder.js +31 -0
  69. package/src/config/providers.js +148 -188
  70. package/src/config/settings.js +156 -182
  71. package/src/core/policy.js +69 -18
  72. package/src/core/trace.js +8 -6
  73. package/src/edit/editblock.js +266 -0
  74. package/src/edit/snapshot.js +67 -0
  75. package/src/evidence/index.js +153 -0
  76. package/src/evolve/playbook.js +9 -10
  77. package/src/hooks/index.js +113 -0
  78. package/src/llm/client.js +188 -446
  79. package/src/llm/dsml.js +74 -74
  80. package/src/llm/embedder.js +41 -35
  81. package/src/llm/fence.js +52 -105
  82. package/src/llm/index.js +4 -4
  83. package/src/llm/local-embedder.js +94 -0
  84. package/src/llm/presets.js +113 -0
  85. package/src/llm/pricing.js +93 -0
  86. package/src/llm/retry.js +73 -73
  87. package/src/llm/router.js +92 -97
  88. package/src/mcp/admin.js +326 -0
  89. package/src/mcp/client.js +487 -375
  90. package/src/mcp/http.js +203 -0
  91. package/src/mcp/index.js +116 -116
  92. package/src/mcp/server.js +392 -0
  93. package/src/mcp/tasks.js +133 -0
  94. package/src/memory/asset-hub.js +6 -11
  95. package/src/memory/canvas.js +2 -5
  96. package/src/memory/compaction.js +28 -28
  97. package/src/memory/experience.js +133 -122
  98. package/src/memory/fact-store.js +914 -698
  99. package/src/memory/failure-episode.js +20 -11
  100. package/src/memory/fork.js +17 -8
  101. package/src/memory/index.js +8 -6
  102. package/src/memory/l0.js +53 -52
  103. package/src/memory/l2.js +145 -130
  104. package/src/memory/l3.js +111 -111
  105. package/src/memory/legion-board.js +71 -0
  106. package/src/memory/memory-ticker.js +239 -240
  107. package/src/memory/session.js +398 -397
  108. package/src/memory/sqlite-store.js +581 -0
  109. package/src/mode/blackboard.js +49 -49
  110. package/src/mode/graph.js +42 -41
  111. package/src/mode/index.js +64 -64
  112. package/src/mode/legion.js +54 -51
  113. package/src/mode/plan-exec.js +50 -50
  114. package/src/mode/router.js +28 -40
  115. package/src/orchestrator/agent-worker.js +69 -69
  116. package/src/orchestrator/dag.js +90 -83
  117. package/src/orchestrator/experts.js +76 -0
  118. package/src/orchestrator/index.js +1 -1
  119. package/src/orchestrator/legion.js +179 -187
  120. package/src/orchestrator/supervisor.js +6 -8
  121. package/src/permissions/index.js +378 -0
  122. package/src/persona/index.js +28 -29
  123. package/src/plugin/builtin.js +313 -212
  124. package/src/plugin/context.js +80 -79
  125. package/src/plugin/index.js +62 -62
  126. package/src/plugin/v3.js +73 -0
  127. package/src/protocol/index.js +148 -0
  128. package/src/repomap/index.js +309 -0
  129. package/src/review/index.js +393 -0
  130. package/src/seam/registry.js +3 -0
  131. package/src/seam/shell.js +55 -55
  132. package/src/security/injection.js +79 -0
  133. package/src/selfheal/evolve.js +67 -67
  134. package/src/selfheal/healer.js +184 -167
  135. package/src/selfheal/run.js +9 -9
  136. package/src/server.js +63 -60
  137. package/src/services/diagnose.js +180 -0
  138. package/src/services/learning-service.js +9 -0
  139. package/src/services/memory-health.js +34 -6
  140. package/src/services/memory-service.js +42 -9
  141. package/src/services/triage.js +138 -0
  142. package/src/session/parts.js +76 -0
  143. package/src/session/projection.js +73 -0
  144. package/src/session/rollout.js +54 -0
  145. package/src/session/turn.js +137 -0
  146. package/src/skills/lint.js +72 -0
  147. package/src/skills/loader.js +231 -150
  148. package/src/skills/search.js +58 -0
  149. package/src/skills/verify.js +95 -100
  150. package/src/tools/advanced.js +388 -352
  151. package/src/tools/builtin.js +384 -297
  152. package/src/tools/catalog.js +283 -159
  153. package/src/tools/command-guard.js +112 -112
  154. package/src/tools/custom.js +47 -47
  155. package/src/tools/delegate.js +383 -297
  156. package/src/tools/document.js +254 -253
  157. package/src/tools/git.js +151 -0
  158. package/src/tools/governance.js +47 -20
  159. package/src/tools/index.js +16 -11
  160. package/src/tools/methods.js +178 -178
  161. package/src/tools/ocr.js +59 -59
  162. package/src/tools/sandbox-worker.js +40 -0
  163. package/src/tools/sandbox.js +92 -0
  164. package/src/tools/seam.js +162 -125
  165. package/src/tools/selfmod.js +196 -176
  166. package/src/tools/v3.js +225 -0
  167. package/src/tools/vad.js +176 -0
  168. package/src/tools/voice.js +238 -0
  169. package/src/utils/async.js +14 -0
  170. package/src/utils/config-file.js +53 -0
  171. package/src/utils/crashguard.js +88 -0
  172. package/src/utils/http.js +53 -0
  173. package/src/utils/id.js +8 -0
  174. package/src/utils/json-state.js +33 -0
  175. package/src/utils/logger.js +17 -17
  176. package/src/utils/ndjson.js +25 -0
  177. package/src/utils/pii.js +42 -42
  178. package/src/utils/rate-limit.js +50 -0
  179. package/src/utils/schema.js +80 -0
  180. package/src/utils/similarity.js +43 -0
  181. package/src/utils/store.js +170 -108
  182. package/src/utils/text.js +15 -15
  183. package/src/utils/trace.js +153 -153
  184. package/src/utils/wal.js +39 -0
  185. package/src/utils/winutf8.js +16 -15
  186. package/src/wiki/index.js +170 -0
@@ -1,167 +1,184 @@
1
- // src/selfheal/healer.js - 自愈引擎
2
- // 1. 启动检查: 修复缺失目录/损坏JSON/权限
3
- // 2. 崩溃恢复: 检测上次异常退出, 清理残留
4
- // 3. 数据一致性: 校验记忆文件
5
- import fs from "node:fs";
6
- import path from "node:path";
7
- import { ensureDir, readJson } from "../utils/store.js";
8
- import { info, warn, error } from "../utils/logger.js";
9
-
10
- export class Healer {
11
- constructor(rootDir) {
12
- this.root = rootDir;
13
- this.dataDir = path.join(rootDir, "data");
14
- this.integrity = path.join(this.dataDir, "integrity.json");
15
- }
16
-
17
- // 启动体检: 返回修复清单
18
- runStartupChecks() {
19
- const fixes = [];
20
- ensureDir(this.dataDir);
21
- for (const sub of ["memory", "memory/daily", "experience", "sessions", "logs"]) {
22
- const d = path.join(this.dataDir, sub);
23
- if (!fs.existsSync(d)) { fs.mkdirSync(d, { recursive: true }); fixes.push(`created dir: ${sub}`); }
24
- }
25
- // 校验关键 JSON 可解析
26
- const facts = path.join(this.dataDir, "memory", "facts.json");
27
- if (fs.existsSync(facts)) {
28
- const parsed = readJson(facts, null);
29
- if (parsed === null) {
30
- // 损坏: 备份后重建
31
- const bak = facts + ".corrupt-" + Date.now();
32
- fs.renameSync(facts, bak);
33
- fs.writeFileSync(facts, "[]", "utf8");
34
- fixes.push(`facts.json corrupt -> backed up to ${path.basename(bak)}, reset`);
35
- }
36
- }
37
- return fixes;
38
- }
39
-
40
- // 崩溃恢复: 检查上次是否干净退出
41
- checkCrash() {
42
- const state = readJson(this.integrity, { clean: true, pid: null });
43
- const result = { crashed: false, detail: null };
44
- if (state.clean === false) {
45
- result.crashed = true;
46
- result.detail = `上次进程 (pid=${state.pid}) 未干净退出, 可能残留临时文件`;
47
- this._cleanupTmp();
48
- }
49
- return result;
50
- }
51
-
52
- _cleanupTmp() {
53
- const walk = (dir) => {
54
- if (!fs.existsSync(dir)) return;
55
- for (const f of fs.readdirSync(dir)) {
56
- const p = path.join(dir, f);
57
- let st;
58
- try { st = fs.statSync(p); } catch { continue; } // 并发删除竞态容错
59
- if (st.isDirectory()) walk(p);
60
- else if (f.endsWith(".tmp")) { try { fs.unlinkSync(p); } catch {} info(`cleaned tmp: ${f}`); }
61
- }
62
- };
63
- walk(this.dataDir);
64
- }
65
-
66
- markDirty() {
67
- ensureDir(this.dataDir);
68
- fs.writeFileSync(this.integrity, JSON.stringify({ clean: false, pid: process.pid, ts: Date.now() }), "utf8");
69
- }
70
-
71
- markClean() {
72
- ensureDir(this.dataDir);
73
- fs.writeFileSync(this.integrity, JSON.stringify({ clean: true, pid: process.pid, ts: Date.now() }), "utf8");
74
- }
75
-
76
- // 清理历史 corrupt 备份: 保留最近 N 个, 更早自动删除 (默认保留 2)
77
- cleanupCorruptBackups(keep = 2) {
78
- const files = [];
79
- const walk = (d) => {
80
- if (!fs.existsSync(d)) return;
81
- for (const f of fs.readdirSync(d)) {
82
- const p = path.join(d, f);
83
- let st;
84
- try { st = fs.statSync(p); } catch { continue; }
85
- if (st.isDirectory()) walk(p);
86
- else if (f.includes(".corrupt-")) files.push({ p, mtime: st.mtimeMs });
87
- }
88
- };
89
- walk(this.dataDir);
90
- files.sort((a, b) => b.mtime - a.mtime);
91
- const removed = [];
92
- for (const f of files.slice(keep)) {
93
- try { fs.unlinkSync(f.p); removed.push(path.basename(f.p)); }
94
- catch (e) { warn("清理损坏备份失败: " + f.p + ": " + e.message); }
95
- }
96
- if (removed.length) info("selfheal: 清理旧 corrupt 备份 " + removed.length + " 个: " + removed.join(", "));
97
- return removed;
98
- }
99
-
100
- // 清理历史手动备份目录 (memory-backup-*): 保留最近 N 个, 更早自动删除 (默认保留 2)
101
- // 手动全量备份目录不在 corrupt 备份机制内, 若无清理会持续累积磁盘占用
102
- cleanupStaleBackupDirs(keep = 2) {
103
- if (!fs.existsSync(this.dataDir)) return [];
104
- const dirs = [];
105
- for (const f of fs.readdirSync(this.dataDir)) {
106
- if (!f.startsWith("memory-backup-")) continue;
107
- const p = path.join(this.dataDir, f);
108
- let st;
109
- try { st = fs.statSync(p); } catch { continue; }
110
- if (!st.isDirectory()) continue;
111
- dirs.push({ p, mtime: st.mtimeMs });
112
- }
113
- dirs.sort((a, b) => b.mtime - a.mtime);
114
- const removed = [];
115
- for (const d of dirs.slice(keep)) {
116
- try { fs.rmSync(d.p, { recursive: true, force: true }); removed.push(path.basename(d.p)); }
117
- catch (e) { warn("清理过期备份目录失败: " + d.p + ": " + e.message); }
118
- }
119
- if (removed.length) info("selfheal: 清理旧手动备份目录 " + removed.length + " 个: " + removed.join(", "));
120
- return removed;
121
- }
122
-
123
- // 清理历史 .bak-* 文件 (去重/迁移等一次性工具留下的手动备份): 保留最近 N 个, 更早自动删除 (默认保留 2)
124
- // 覆盖 facts.json.bak-* / lessons.json.bak-* 等, 防手动备份文件在 data/ 内无限累积
125
- cleanupStaleBakFiles(keep = 2) {
126
- if (!fs.existsSync(this.dataDir)) return [];
127
- const files = [];
128
- const walk = (d) => {
129
- if (!fs.existsSync(d)) return;
130
- for (const f of fs.readdirSync(d)) {
131
- const p = path.join(d, f);
132
- let st;
133
- try { st = fs.statSync(p); } catch { continue; }
134
- if (st.isDirectory()) walk(p);
135
- else if (f.includes(".bak-") && !f.endsWith(".lock")) files.push({ p, mtime: st.mtimeMs });
136
- }
137
- };
138
- walk(this.dataDir);
139
- files.sort((a, b) => b.mtime - a.mtime);
140
- const removed = [];
141
- for (const f of files.slice(keep)) {
142
- try { fs.unlinkSync(f.p); removed.push(path.basename(f.p)); }
143
- catch (e) { warn("清理过期备份文件失败: " + f.p + ": " + e.message); }
144
- }
145
- if (removed.length) info("selfheal: 清理旧 .bak-* 备份文件 " + removed.length + " 个: " + removed.join(", "));
146
- return removed;
147
- }
148
-
149
- // 完整自愈入口
150
- heal() {
151
- const fixes = this.runStartupChecks();
152
- const crash = this.checkCrash();
153
- // 清理历史 corrupt 备份 (保留最近 2 个, 更早自动删除) — 之前漏调用导致 corrupt 持续累积
154
- const cleanedCorrupt = this.cleanupCorruptBackups(2);
155
- // 清理历史手动备份目录 (保留最近 2 个)
156
- const cleanedBackupDirs = this.cleanupStaleBackupDirs(2);
157
- // 清理历史 .bak-* 文件 (保留最近 2 个)
158
- const cleanedBakFiles = this.cleanupStaleBakFiles(2);
159
- // 崩溃残留清理后主动翻回 clean: 自愈闭环 - 清了毒就要"宣布痊愈", 否则下次启动还误报崩溃
160
- if (crash.crashed) { this.markClean(); info("selfheal: 崩溃残留已清理, 状态置回 clean"); }
161
- const report = { fixes, crashed: crash.crashed, crashDetail: crash.detail, cleanedCorrupt, cleanedBackupDirs, cleanedBakFiles };
162
- if (fixes.length) info(`selfheal: 修复 ${fixes.length} 项: ${fixes.join("; ")}`);
163
- else info("selfheal: 无异常");
164
- if (crash.crashed) warn(`selfheal: 检测到崩溃残留 -> ${crash.detail}`);
165
- return report;
166
- }
167
- }
1
+ // src/selfheal/healer.js - 自愈引擎
2
+ // 1. 启动检查: 修复缺失目录/损坏JSON/权限
3
+ // 2. 崩溃恢复: 检测上次异常退出, 清理残留
4
+ // 3. 数据一致性: 校验记忆文件
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { ensureDir, readJson } from "../utils/store.js";
8
+ import { info, warn, debug } from "../utils/logger.js";
9
+
10
+ export class Healer {
11
+ // dataDir 可选: 显式传入真实数据目录 (PPX_DATA_DIR 可能指向非默认位置)。
12
+ // v1.0.8 修复 (P1-2): 原实现把 dataDir 硬编码为 path.join(rootDir, "data"),
13
+ // 而调用方 builtin.js 传进来的是 root 而非数据目录 —— 一旦 PPX_DATA_DIR 改到别处,
14
+ // 自愈就会在错误的目录里创建 memory/experience/logs、写 integrity.json、清理 .tmp,
15
+ // 形成"数据目录 / 自愈目录"分叉: 真实数据目录永不被体检, 空目录反而被反复重建。
16
+ // 默认值保留 rootDir/data, 让既有 15 处 `new Healer(root)` 调用点行为不变 (向后兼容)。
17
+ constructor(rootDir, dataDir = null) {
18
+ this.root = rootDir;
19
+ this.dataDir = dataDir || path.join(rootDir, "data");
20
+ this.integrity = path.join(this.dataDir, "integrity.json");
21
+ }
22
+
23
+ // 启动体检: 返回修复清单
24
+ runStartupChecks() {
25
+ const fixes = [];
26
+ ensureDir(this.dataDir);
27
+ for (const sub of ["memory", "memory/daily", "experience", "sessions", "logs"]) {
28
+ const d = path.join(this.dataDir, sub);
29
+ if (!fs.existsSync(d)) { fs.mkdirSync(d, { recursive: true }); fixes.push(`created dir: ${sub}`); }
30
+ }
31
+ // 校验关键 JSON 可解析
32
+ const facts = path.join(this.dataDir, "memory", "facts.json");
33
+ if (fs.existsSync(facts)) {
34
+ const parsed = readJson(facts, null);
35
+ if (parsed === null) {
36
+ // 损坏: 备份后重建
37
+ const bak = facts + ".corrupt-" + Date.now();
38
+ fs.renameSync(facts, bak);
39
+ fs.writeFileSync(facts, "[]", "utf8");
40
+ fixes.push(`facts.json corrupt -> backed up to ${path.basename(bak)}, reset`);
41
+ }
42
+ }
43
+ return fixes;
44
+ }
45
+
46
+ // 崩溃恢复: 检查上次是否干净退出
47
+ checkCrash() {
48
+ const state = readJson(this.integrity, { clean: true, pid: null });
49
+ const result = { crashed: false, detail: null };
50
+ if (state.clean === false) {
51
+ result.crashed = true;
52
+ result.detail = `上次进程 (pid=${state.pid}) 未干净退出, 可能残留临时文件`;
53
+ this._cleanupTmp();
54
+ }
55
+ return result;
56
+ }
57
+
58
+ _cleanupTmp() {
59
+ const walk = (dir) => {
60
+ if (!fs.existsSync(dir)) return;
61
+ for (const f of fs.readdirSync(dir)) {
62
+ const p = path.join(dir, f);
63
+ let st;
64
+ try { st = fs.statSync(p); } catch { continue; } // 并发删除竞态容错
65
+ if (st.isDirectory()) walk(p);
66
+ else if (f.endsWith(".tmp")) { try { fs.unlinkSync(p); } catch (e) { debug(`[selfheal/healer] 已忽略异常: ${e && e.message ? e.message : e}`); } info(`cleaned tmp: ${f}`); }
67
+ }
68
+ };
69
+ walk(this.dataDir);
70
+ }
71
+
72
+ markDirty() {
73
+ ensureDir(this.dataDir);
74
+ fs.writeFileSync(this.integrity, JSON.stringify({ clean: false, pid: process.pid, ts: Date.now() }), "utf8");
75
+ }
76
+
77
+ markClean() {
78
+ ensureDir(this.dataDir);
79
+ fs.writeFileSync(this.integrity, JSON.stringify({ clean: true, pid: process.pid, ts: Date.now() }), "utf8");
80
+ }
81
+
82
+ // ---- 备份清理 (2026-09-18 重构: 原先三个方法各抄一份「收集→按 mtime 排序→保留最近 N→删其余」) ----
83
+
84
+ // 通用清理器: collect() 给出候选 [{p, mtime}] → 按 mtime 新→旧排序 → 保留最近 keep 个, 其余逐个 remove
85
+ // 单个删除失败只告警不中断 (与原来逐方法 try/catch 语义一致)。
86
+ _pruneByMtime({ keep, collect, remove, logLabel, warnLabel }) {
87
+ const all = collect();
88
+ all.sort((a, b) => b.mtime - a.mtime);
89
+ const removed = [];
90
+ for (const item of all.slice(keep)) {
91
+ try { remove(item.p); removed.push(path.basename(item.p)); }
92
+ catch (e) { warn(`清理${warnLabel}失败: ${item.p}: ${e.message}`); }
93
+ }
94
+ if (removed.length) info(`selfheal: 清理旧${logLabel} ${removed.length} 个: ${removed.join(", ")}`);
95
+ return removed;
96
+ }
97
+
98
+ // 递归收集 dataDir 下满足 pred(fileName) 的文件 (带 mtime)
99
+ _collectFilesBy(pred) {
100
+ const found = [];
101
+ const walk = (d) => {
102
+ if (!fs.existsSync(d)) return;
103
+ for (const f of fs.readdirSync(d)) {
104
+ const p = path.join(d, f);
105
+ let st;
106
+ try { st = fs.statSync(p); } catch { continue; }
107
+ if (st.isDirectory()) walk(p);
108
+ else if (pred(f)) found.push({ p, mtime: st.mtimeMs });
109
+ }
110
+ };
111
+ walk(this.dataDir);
112
+ return found;
113
+ }
114
+
115
+ // 清理历史 corrupt 备份: 保留最近 N 个, 更早自动删除 (默认保留 2)
116
+ cleanupCorruptBackups(keep = 2) {
117
+ return this._pruneByMtime({
118
+ keep,
119
+ collect: () => this._collectFilesBy((f) => f.includes(".corrupt-")),
120
+ remove: (p) => fs.unlinkSync(p),
121
+ logLabel: "corrupt 备份",
122
+ warnLabel: "损坏备份",
123
+ });
124
+ }
125
+
126
+ // 清理历史手动备份目录 (memory-backup-*): 保留最近 N 个, 更早自动删除 (默认保留 2)
127
+ // 手动全量备份目录不在 corrupt 备份机制内, 若无清理会持续累积磁盘占用
128
+ cleanupStaleBackupDirs(keep = 2) {
129
+ return this._pruneByMtime({
130
+ keep,
131
+ collect: () => {
132
+ if (!fs.existsSync(this.dataDir)) return [];
133
+ const dirs = [];
134
+ for (const f of fs.readdirSync(this.dataDir)) {
135
+ if (!f.startsWith("memory-backup-")) continue;
136
+ const p = path.join(this.dataDir, f);
137
+ let st;
138
+ try { st = fs.statSync(p); } catch { continue; }
139
+ if (st.isDirectory()) dirs.push({ p, mtime: st.mtimeMs });
140
+ }
141
+ return dirs;
142
+ },
143
+ remove: (p) => fs.rmSync(p, { recursive: true, force: true }),
144
+ logLabel: "手动备份目录",
145
+ warnLabel: "过期备份目录",
146
+ });
147
+ }
148
+
149
+ // 清理历史 .bak-* 文件 (去重/迁移等一次性工具留下的手动备份): 保留最近 N 个, 更早自动删除 (默认保留 2)
150
+ // 覆盖 facts.json.bak-* / lessons.json.bak-* 等, 防手动备份文件在 data/ 内无限累积
151
+ cleanupStaleBakFiles(keep = 2) {
152
+ return this._pruneByMtime({
153
+ keep,
154
+ collect: () => this._collectFilesBy((f) => f.includes(".bak-") && !f.endsWith(".lock")),
155
+ remove: (p) => fs.unlinkSync(p),
156
+ logLabel: ".bak-* 备份文件",
157
+ warnLabel: "过期备份文件",
158
+ });
159
+ }
160
+
161
+ // 完整自愈入口
162
+ // 2026-09-17 体检修复: 原实现把"崩溃残留已清理, 状态置回 clean"打在"修复 N 项"与
163
+ // "检测到崩溃残留 -> ..."之前, 日志里表现为「先说痊愈、再说发现崩溃」的因果颠倒,
164
+ // 使用者会怀疑自愈到底有没有生效。现按"先报问题 → 再报处置 → 最后报结果"的顺序输出。
165
+ heal() {
166
+ // 1) 先判定上次是否崩溃 (必须在 runStartupChecks 之前判定, 否则重建的目录会掩盖证据)
167
+ const crash = this.checkCrash();
168
+ if (crash.crashed) warn(`selfheal: 检测到崩溃残留 -> ${crash.detail}`);
169
+ // 2) 启动体检 + 备份清理
170
+ const fixes = this.runStartupChecks();
171
+ // 清理历史 corrupt 备份 (保留最近 2 个, 更早自动删除) — 之前漏调用导致 corrupt 持续累积
172
+ const cleanedCorrupt = this.cleanupCorruptBackups(2);
173
+ // 清理历史手动备份目录 (保留最近 2 个)
174
+ const cleanedBackupDirs = this.cleanupStaleBackupDirs(2);
175
+ // 清理历史 .bak-* 文件 (保留最近 2 个)
176
+ const cleanedBakFiles = this.cleanupStaleBakFiles(2);
177
+ // 3) 处置回执: 崩溃残留清理后主动翻回 clean —— 自愈闭环, 清了毒就要"宣布痊愈",
178
+ // 否则下次启动还误报崩溃
179
+ if (crash.crashed) { this.markClean(); info("selfheal: 崩溃残留已清理, 状态置回 clean"); }
180
+ if (fixes.length) info(`selfheal: 修复 ${fixes.length} 项: ${fixes.join("; ")}`);
181
+ else info("selfheal: 无异常");
182
+ return { fixes, crashed: crash.crashed, crashDetail: crash.detail, cleanedCorrupt, cleanedBackupDirs, cleanedBakFiles };
183
+ }
184
+ }
@@ -1,9 +1,9 @@
1
- // src/selfheal/run.js - 自愈命令行入口
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { Healer } from "./healer.js";
5
-
6
- const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
7
- const healer = new Healer(root);
8
- const report = healer.heal();
9
- console.log(JSON.stringify(report, null, 2));
1
+ // src/selfheal/run.js - 自愈命令行入口
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Healer } from "./healer.js";
5
+
6
+ const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
7
+ const healer = new Healer(root);
8
+ const report = healer.heal();
9
+ console.log(JSON.stringify(report, null, 2));
package/src/server.js CHANGED
@@ -1,60 +1,63 @@
1
- #!/usr/bin/env node
2
- // src/server.js - 皮皮虾 HTTP 服务入口
3
- // 提供: /health, /message (HTTP通道), /feishu/webhook, /wechat/webhook
4
- // 通道统一走 ChannelManager 注册表: connect + webhook 挂载由各通道 mount() 完成
5
- import { ensureUTF8Console } from "./utils/winutf8.js";
6
- import { PPXAgent } from "./agent/index.js";
7
- import { ChannelManager } from "./channels/index.js";
8
-
9
- ensureUTF8Console();
10
- export async function startServer({ root = process.cwd(), port = 8899, host = "127.0.0.1", config = {}, llm = null } = {}) {
11
- const agent = new PPXAgent({ root });
12
- if (llm) {
13
- // 测试注入 stub LLM, 避免依赖真实网络; 同步把 allProviders 替换为同源 stub,
14
- // 让 /api/providers/test 等"按 id 取客户端"的路径也能命中 stub
15
- agent.llm = llm;
16
- if (Array.isArray(agent.allProviders)) agent.allProviders = [llm];
17
- }
18
-
19
- // 统一走注册表: HTTP 通道始终启动 (port/host 参数覆盖配置), 其余按 config.channels
20
- // 基础配置来自 agent.config (config/ppx.json 加载的), 调用方传入的 config.channels 只做覆盖
21
- const baseChannels = agent.config.channels || {};
22
- const channelsCfg = { ...baseChannels, ...((config.channels) || {}) };
23
- channelsCfg.http = { ...(channelsCfg.http || {}), enabled: true, port, host }; // port/host 参数最高优先级
24
- const manager = new ChannelManager(agent, channelsCfg);
25
- await manager.start();
26
-
27
- // 主动提醒 → 广播到所有已启用通道 (agent.proactive.enabled 才接; 默认关不打扰)
28
- if (agent.config.agent?.proactive?.enabled) {
29
- agent.startProactiveTicker((payload) => manager.broadcast(payload.text));
30
- }
31
-
32
- const server = manager.httpServer;
33
- return {
34
- agent,
35
- server,
36
- http: manager.get("http"),
37
- feishu: manager.get("feishu"),
38
- wechat: manager.get("wechat"),
39
- manager,
40
- };
41
- }
42
-
43
- // CLI 启动公共逻辑: 供 bin/ppx-serve 与 src/server.js 直接运行共用
44
- export async function runServer({ root = process.cwd(), port = Number(process.env.PPX_PORT || 8899) } = {}) {
45
- const { agent, server, manager } = await startServer({ root, port });
46
- console.log(`皮皮虾 服务已启动: http://127.0.0.1:${port}`);
47
- console.log(` /health 健康检查`);
48
- console.log(` /message HTTP 对话通道 (POST {message})`);
49
- console.log(` 通道: ${manager.list().filter((c) => c.enabled).map((c) => c.name).join(", ") || "(无)"}`);
50
- console.log(` 工具: ${agent.tools.list().join(", ")}`);
51
- console.log(" Ctrl+C 退出");
52
- process.on("SIGINT", () => { agent.shutdown(); process.exit(0); });
53
- return { agent, server, manager };
54
- }
55
-
56
- // 直接运行 (node src/server.js, scripts/start-web.js)
57
- const _entry = (process.argv[1] || "").replace(/\\/g, "/");
58
- if (_entry.endsWith("src/server.js")) {
59
- await runServer({ root: process.cwd() });
60
- }
1
+ #!/usr/bin/env node
2
+ // src/server.js - 皮皮虾 HTTP 服务入口
3
+ // 提供: /health, /message (HTTP通道), /feishu/webhook, /wechat/webhook
4
+ // 通道统一走 ChannelManager 注册表: connect + webhook 挂载由各通道 mount() 完成
5
+ import { ensureUTF8Console } from "./utils/winutf8.js";
6
+ import { installCrashGuard } from "./utils/crashguard.js";
7
+ import { PPXAgent } from "./agent/index.js";
8
+ import { ChannelManager } from "./channels/index.js";
9
+
10
+ ensureUTF8Console();
11
+ export async function startServer({ root = process.cwd(), port = 8899, host = "127.0.0.1", config = {}, llm = null } = {}) {
12
+ const agent = new PPXAgent({ root });
13
+ if (llm) {
14
+ // 测试注入 stub LLM, 避免依赖真实网络; 同步把 allProviders 替换为同源 stub,
15
+ // 让 /api/providers/test 等"按 id 取客户端"的路径也能命中 stub
16
+ agent.llm = llm;
17
+ if (Array.isArray(agent.allProviders)) agent.allProviders = [llm];
18
+ }
19
+
20
+ // 统一走注册表: HTTP 通道始终启动 (port/host 参数覆盖配置), 其余按 config.channels
21
+ // 基础配置来自 agent.config (config/ppx.json 加载的), 调用方传入的 config.channels 只做覆盖
22
+ const baseChannels = agent.config.channels || {};
23
+ const channelsCfg = { ...baseChannels, ...((config.channels) || {}) };
24
+ channelsCfg.http = { ...(channelsCfg.http || {}), enabled: true, port, host }; // port/host 参数最高优先级
25
+ const manager = new ChannelManager(agent, channelsCfg);
26
+ await manager.start();
27
+
28
+ // 主动提醒 → 广播到所有已启用通道 (agent.proactive.enabled 才接; 默认关不打扰)
29
+ if (agent.config.agent?.proactive?.enabled) {
30
+ agent.startProactiveTicker((payload) => manager.broadcast(payload.text));
31
+ }
32
+
33
+ const server = manager.httpServer;
34
+ return {
35
+ agent,
36
+ server,
37
+ http: manager.get("http"),
38
+ feishu: manager.get("feishu"),
39
+ wechat: manager.get("wechat"),
40
+ manager,
41
+ };
42
+ }
43
+
44
+ // CLI 启动公共逻辑: 供 bin/ppx-serve 与 src/server.js 直接运行共用
45
+ export async function runServer({ root = process.cwd(), port = Number(process.env.PPX_PORT || 8899) } = {}) {
46
+ // 全局异常兜底 (P1): 工具/通道里的意外异常不应让整个服务消失
47
+ installCrashGuard({ tag: "ppx-serve" });
48
+ const { agent, server, manager } = await startServer({ root, port });
49
+ console.log(`皮皮虾 服务已启动: http://127.0.0.1:${port}`);
50
+ console.log(` /health 健康检查`);
51
+ console.log(` /message HTTP 对话通道 (POST {message})`);
52
+ console.log(` 通道: ${manager.list().filter((c) => c.enabled).map((c) => c.name).join(", ") || "(无)"}`);
53
+ console.log(` 工具: ${agent.tools.list().join(", ")}`);
54
+ console.log(" Ctrl+C 退出");
55
+ process.on("SIGINT", () => { agent.shutdown(); process.exit(0); });
56
+ return { agent, server, manager };
57
+ }
58
+
59
+ // 直接运行 (node src/server.js, scripts/start-web.js)
60
+ const _entry = (process.argv[1] || "").replace(/\\/g, "/");
61
+ if (_entry.endsWith("src/server.js")) {
62
+ await runServer({ root: process.cwd() });
63
+ }