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
package/src/memory/l3.js CHANGED
@@ -1,112 +1,112 @@
1
- // src/memory/l3.js - L3 核心画像 (腾讯风格 persona generation)
2
- // 从记忆提炼: 用户画像 (user.persona.md) + agent 人格 (agent.persona.md)
3
- // 零依赖: 高频词统计 + 主题聚合, 输出结构化画像
4
- import fs from "node:fs";
5
- import path from "node:path";
6
- import { ensureDir, readText, writeText, logicalDay } from "../utils/store.js";
7
-
8
- const STOP = new Set(["这个","那个","我们","你们","他们","什么","怎么","可以","一个","就是","知道","没有","如果","因为","所以","但是","然后","现在","今天","昨天","明天","已经","还有","所有","这样","那样","自己","的东西","的事情","一下","一点","一些","这些","那些","东西","事情","问题","觉得","应该","需要","开始","继续","大家","真的","只是","可能","不是","都是","一直","非常","其实","最后","主要","联系","关系","喜欢","讨厌","不要","想要","认为"]);
9
-
10
- export class PersonaStore {
11
- constructor(dataDir, { userName = "兄弟" } = {}) {
12
- this.dir = path.join(dataDir, "memory", "l3");
13
- ensureDir(this.dir);
14
- this.userFile = path.join(this.dir, "user.persona.md");
15
- this.agentFile = path.join(this.dir, "agent.persona.md");
16
- this.userName = userName;
17
- }
18
-
19
- // 从一批事实提炼用户画像
20
- buildUserPersona(facts, { force = false } = {}) {
21
- if (!force && this._exists(this.userFile)) return this._read(this.userFile);
22
- // 聚焦用户相关的记忆 (来源: 对话 / LLM提炼 / 主动记 / 手动 / 用户分享)
23
- // 注: 事实的来源标记在 source 字段 (type 恒为 general), 之前按 type 过滤永远为空 -> 修正为按 source
24
- const USER_SOURCES = ["conversation", "extract", "agent-self", "manual", "user-shared"];
25
- const userFacts = facts.filter((f) => USER_SOURCES.includes(f.source) || USER_SOURCES.includes(f.type));
26
- const interests = this._topTopics(userFacts);
27
- // 记忆概要: 内容去重后取最近 10 条 (防 LLM 提炼变体/重复记忆稀释画像)
28
- const uniq = this._uniqByContent(userFacts).slice(-10);
29
- const md = `# ${this.userName} 的用户画像
30
-
31
- > 由皮皮虾 L3 画像引擎生成 | 更新: ${logicalDay()}
32
-
33
- ## 关注主题
34
- ${interests.length ? interests.map(([w, n]) => `- ${w} (出现${n}次)`).join("\n") : "- 暂无足够数据"}
35
-
36
- ## 记忆概要
37
- ${uniq.map((f) => `- ${f.content}`).join("\n") || "- 暂无"}
38
-
39
- ## 画像版本
40
- - 生成时间: ${logicalDay()}
41
- - 数据来源: 对话记忆 + 用户主动分享
42
- `;
43
- writeText(this.userFile, md);
44
- return md;
45
- }
46
-
47
- // 提炼 agent 自身人格 (从经验/工具使用学习)
48
- buildAgentPersona(lessons, { force = false } = {}) {
49
- if (!force && this._exists(this.agentFile)) return this._read(this.agentFile);
50
- // 学到的经验: 内容去重后取最近 10 条 (防重复经验污染自我画像)
51
- const uniq = this._uniqByContent(lessons, (l) => l.lesson).slice(-10);
52
- const md = `# 皮皮虾 自我画像
53
-
54
- > 从经验库自动学习 | 更新: ${logicalDay()}
55
-
56
- ## 学到的经验
57
- ${uniq.map((l) => `- ${l.lesson}`).join("\n") || "- 暂无"}
58
-
59
- ## 能力画像
60
- - 工具: 文件操作 / 命令执行 / 搜索 / HTTP / 定时任务
61
- - 记忆: 四层架构 (L0对话→L1原子→L2场景→L3画像)
62
- - 自愈: 崩溃恢复 / 数据修复
63
- - 军团: 多进程并行协作
64
- `;
65
- writeText(this.agentFile, md);
66
- return md;
67
- }
68
-
69
- // 内容去重: 按归一化内容 (去空白折叠) 过滤, 保留首次出现的条目
70
- _uniqByContent(items, pick = (x) => x.content) {
71
- const seen = new Set();
72
- const out = [];
73
- for (const it of items || []) {
74
- const key = String(pick(it) || "").trim().replace(/\s+/g, " ");
75
- if (!key || seen.has(key)) continue;
76
- seen.add(key);
77
- out.push(it);
78
- }
79
- return out;
80
- }
81
-
82
- // 读取已生成的画像 (供 agent._context 注入; 未生成返回 "")
83
- userPersona() { return this._read(this.userFile); }
84
- agentPersona() { return this._read(this.agentFile); }
85
-
86
- // 可观测: L3 画像更新时间 (无文件返回 null), 供 agent.stats() 聚合
87
- stats() {
88
- return {
89
- user_updated: this._mtime(this.userFile),
90
- agent_updated: this._mtime(this.agentFile),
91
- };
92
- }
93
-
94
- _mtime(f) {
95
- try { return new Date(fs.statSync(f).mtime).toISOString().slice(0, 10); } catch { return null; }
96
- }
97
-
98
- _topTopics(facts) {
99
- const freq = new Map();
100
- for (const f of facts) {
101
- const words = String(f.content).match(/[\u4e00-\u9fa5]{2,4}/g) || [];
102
- for (const w of words) {
103
- if (STOP.has(w)) continue;
104
- freq.set(w, (freq.get(w) || 0) + 1);
105
- }
106
- }
107
- return [...freq.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8);
108
- }
109
-
110
- _exists(f) { try { return fs.existsSync(f); } catch { return false; } }
111
- _read(f) { return readText(f, ""); }
1
+ // src/memory/l3.js - L3 核心画像 (腾讯风格 persona generation)
2
+ // 从记忆提炼: 用户画像 (user.persona.md) + agent 人格 (agent.persona.md)
3
+ // 零依赖: 高频词统计 + 主题聚合, 输出结构化画像
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { ensureDir, readText, writeText, logicalDay } from "../utils/store.js";
7
+
8
+ const STOP = new Set(["这个","那个","我们","你们","他们","什么","怎么","可以","一个","就是","知道","没有","如果","因为","所以","但是","然后","现在","今天","昨天","明天","已经","还有","所有","这样","那样","自己","的东西","的事情","一下","一点","一些","这些","那些","东西","事情","问题","觉得","应该","需要","开始","继续","大家","真的","只是","可能","不是","都是","一直","非常","其实","最后","主要","联系","关系","喜欢","讨厌","不要","想要","认为"]);
9
+
10
+ export class PersonaStore {
11
+ constructor(dataDir, { userName = "兄弟" } = {}) {
12
+ this.dir = path.join(dataDir, "memory", "l3");
13
+ ensureDir(this.dir);
14
+ this.userFile = path.join(this.dir, "user.persona.md");
15
+ this.agentFile = path.join(this.dir, "agent.persona.md");
16
+ this.userName = userName;
17
+ }
18
+
19
+ // 从一批事实提炼用户画像
20
+ buildUserPersona(facts, { force = false } = {}) {
21
+ if (!force && this._exists(this.userFile)) return this._read(this.userFile);
22
+ // 聚焦用户相关的记忆 (来源: 对话 / LLM提炼 / 主动记 / 手动 / 用户分享)
23
+ // 注: 事实的来源标记在 source 字段 (type 恒为 general), 之前按 type 过滤永远为空 -> 修正为按 source
24
+ const USER_SOURCES = ["conversation", "extract", "agent-self", "manual", "user-shared"];
25
+ const userFacts = facts.filter((f) => USER_SOURCES.includes(f.source) || USER_SOURCES.includes(f.type));
26
+ const interests = this._topTopics(userFacts);
27
+ // 记忆概要: 内容去重后取最近 10 条 (防 LLM 提炼变体/重复记忆稀释画像)
28
+ const uniq = this._uniqByContent(userFacts).slice(-10);
29
+ const md = `# ${this.userName} 的用户画像
30
+
31
+ > 由皮皮虾 L3 画像引擎生成 | 更新: ${logicalDay()}
32
+
33
+ ## 关注主题
34
+ ${interests.length ? interests.map(([w, n]) => `- ${w} (出现${n}次)`).join("\n") : "- 暂无足够数据"}
35
+
36
+ ## 记忆概要
37
+ ${uniq.map((f) => `- ${f.content}`).join("\n") || "- 暂无"}
38
+
39
+ ## 画像版本
40
+ - 生成时间: ${logicalDay()}
41
+ - 数据来源: 对话记忆 + 用户主动分享
42
+ `;
43
+ writeText(this.userFile, md);
44
+ return md;
45
+ }
46
+
47
+ // 提炼 agent 自身人格 (从经验/工具使用学习)
48
+ buildAgentPersona(lessons, { force = false } = {}) {
49
+ if (!force && this._exists(this.agentFile)) return this._read(this.agentFile);
50
+ // 学到的经验: 内容去重后取最近 10 条 (防重复经验污染自我画像)
51
+ const uniq = this._uniqByContent(lessons, (l) => l.lesson).slice(-10);
52
+ const md = `# 皮皮虾 自我画像
53
+
54
+ > 从经验库自动学习 | 更新: ${logicalDay()}
55
+
56
+ ## 学到的经验
57
+ ${uniq.map((l) => `- ${l.lesson}`).join("\n") || "- 暂无"}
58
+
59
+ ## 能力画像
60
+ - 工具: 文件操作 / 命令执行 / 搜索 / HTTP / 定时任务
61
+ - 记忆: 四层架构 (L0对话→L1原子→L2场景→L3画像)
62
+ - 自愈: 崩溃恢复 / 数据修复
63
+ - 军团: 多进程并行协作
64
+ `;
65
+ writeText(this.agentFile, md);
66
+ return md;
67
+ }
68
+
69
+ // 内容去重: 按归一化内容 (去空白折叠) 过滤, 保留首次出现的条目
70
+ _uniqByContent(items, pick = (x) => x.content) {
71
+ const seen = new Set();
72
+ const out = [];
73
+ for (const it of items || []) {
74
+ const key = String(pick(it) || "").trim().replace(/\s+/g, " ");
75
+ if (!key || seen.has(key)) continue;
76
+ seen.add(key);
77
+ out.push(it);
78
+ }
79
+ return out;
80
+ }
81
+
82
+ // 读取已生成的画像 (供 agent._context 注入; 未生成返回 "")
83
+ userPersona() { return this._read(this.userFile); }
84
+ agentPersona() { return this._read(this.agentFile); }
85
+
86
+ // 可观测: L3 画像更新时间 (无文件返回 null), 供 agent.stats() 聚合
87
+ stats() {
88
+ return {
89
+ user_updated: this._mtime(this.userFile),
90
+ agent_updated: this._mtime(this.agentFile),
91
+ };
92
+ }
93
+
94
+ _mtime(f) {
95
+ try { return new Date(fs.statSync(f).mtime).toISOString().slice(0, 10); } catch { return null; }
96
+ }
97
+
98
+ _topTopics(facts) {
99
+ const freq = new Map();
100
+ for (const f of facts) {
101
+ const words = String(f.content).match(/[\u4e00-\u9fa5]{2,4}/g) || [];
102
+ for (const w of words) {
103
+ if (STOP.has(w)) continue;
104
+ freq.set(w, (freq.get(w) || 0) + 1);
105
+ }
106
+ }
107
+ return [...freq.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8);
108
+ }
109
+
110
+ _exists(f) { try { return fs.existsSync(f); } catch { return false; } }
111
+ _read(f) { return readText(f, ""); }
112
112
  }
@@ -0,0 +1,71 @@
1
+ // src/memory/legion-board.js - 军团共享记忆板 (2026-10-02)
2
+ // 跨 agent 实时共享知识: 主 agent 与所有 worker 子 agent 通过同一全局文件互通。
3
+ // 与 FactStore (内存快照, 私有) 不同: 每次读/写都走盘 + 文件锁, 保证多进程实时一致性。
4
+ // 并发模式对齐 Experience: 锁内「重读 → 改 → 写」, 防陈旧覆盖。
5
+ import path from "node:path";
6
+ import { ensureDir, readJson, writeJson, nowISO, withFileLock } from "../utils/store.js";
7
+
8
+ export class LegionBoard {
9
+ constructor(globalDataDir, opts = {}) {
10
+ this.dir = path.join(globalDataDir, "legion");
11
+ ensureDir(this.dir);
12
+ this.file = path.join(this.dir, "board.json");
13
+ this.opts = {
14
+ maxEntries: 500, // 容量上限: 超限裁剪最旧 (FIFO, 记忆板是临时协作区不是档案库)
15
+ ttlDays: 7, // 条目存活期: 0/负 = 永不过期
16
+ ...opts,
17
+ };
18
+ }
19
+
20
+ // 发布一条共享知识 (topic 可选作频道, tags 可选作检索辅助)
21
+ publish({ from, topic, content, tags = [] }) {
22
+ if (!content || !String(content).trim()) throw new Error("board_publish: content 不能为空");
23
+ return withFileLock(this.file, () => {
24
+ const entries = readJson(this.file, []);
25
+ const entry = {
26
+ id: `bd_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`,
27
+ from: String(from || "unknown").slice(0, 64),
28
+ topic: String(topic || "general").slice(0, 64),
29
+ content: String(content).slice(0, 2000),
30
+ tags: (Array.isArray(tags) ? tags : []).map((t) => String(t).slice(0, 32)).slice(0, 8),
31
+ ts: nowISO(),
32
+ };
33
+ entries.push(entry);
34
+ // 写入时统一裁剪: TTL 过滤 + 容量 FIFO
35
+ const cutoff = this.opts.ttlDays > 0 ? Date.now() - this.opts.ttlDays * 86400000 : 0;
36
+ const kept = entries
37
+ .filter((x) => !cutoff || new Date(x.ts).getTime() >= cutoff)
38
+ .slice(-this.opts.maxEntries);
39
+ writeJson(this.file, kept);
40
+ return entry;
41
+ });
42
+ }
43
+
44
+ // 查询: 读时重读磁盘 → 实时可见其他 agent 刚发布的内容 (跨进程)
45
+ query({ q, topic, from, limit = 10 } = {}) {
46
+ let entries = readJson(this.file, []);
47
+ if (topic) entries = entries.filter((x) => x.topic === topic);
48
+ if (from) entries = entries.filter((x) => x.from === from);
49
+ if (q) {
50
+ const kw = String(q).toLowerCase();
51
+ entries = entries.filter((x) =>
52
+ x.content.toLowerCase().includes(kw) ||
53
+ x.topic.toLowerCase().includes(kw) ||
54
+ (x.tags || []).some((t) => t.toLowerCase().includes(kw))
55
+ );
56
+ }
57
+ // 新的在前
58
+ entries.sort((a, b) => (a.ts < b.ts ? 1 : -1));
59
+ return entries.slice(0, Math.max(1, Math.min(50, Number(limit) || 10)));
60
+ }
61
+
62
+ // 概况: 当前板上条数 + 参与过的 agent (可观测性)
63
+ stats() {
64
+ const entries = readJson(this.file, []);
65
+ return {
66
+ count: entries.length,
67
+ agents: [...new Set(entries.map((x) => x.from))],
68
+ topics: [...new Set(entries.map((x) => x.topic))],
69
+ };
70
+ }
71
+ }