@bolloon/bolloon-agent 0.2.11 → 0.2.13

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 (284) hide show
  1. package/README.md +495 -467
  2. package/bin/bolloon-cli.cjs +183 -183
  3. package/bin/bolloon-daemon.sh +207 -0
  4. package/bin/bolloon.cjs +0 -0
  5. package/bin/ipfs +0 -0
  6. package/dist/agents/agent-manifest-protocol.js +52 -0
  7. package/dist/agents/chat-segmenter.js +298 -0
  8. package/dist/agents/constraint-layer.js +19 -19
  9. package/dist/agents/judgment-protocol.js +479 -0
  10. package/dist/agents/parse-tool-call.js +304 -0
  11. package/dist/agents/peer-manifest-loader.js +210 -0
  12. package/dist/agents/pi-sdk-session-factory.js +110 -0
  13. package/dist/agents/pi-sdk-session-manager.js +297 -0
  14. package/dist/agents/pi-sdk-tools.js +1288 -0
  15. package/dist/agents/pi-sdk-types.js +13 -0
  16. package/dist/agents/pi-sdk.js +553 -1306
  17. package/dist/agents/react-loop.js +120 -0
  18. package/dist/agents/session-store.js +171 -0
  19. package/dist/agents/shell-guard.js +2 -2
  20. package/dist/agents/tool-registry.js +136 -0
  21. package/dist/agents/workflow-pivot-loop.js +200 -22
  22. package/dist/bollharness/src/scripts/context_router.js +7 -0
  23. package/dist/bollharness/src/scripts/deploy-guard.js +8 -8
  24. package/dist/bollharness-integration/context-router-judgment.js +4 -4
  25. package/dist/bollharness-integration/context-router.js +292 -292
  26. package/dist/bollharness-integration/gate-state-machine.js +26 -13
  27. package/dist/bollharness-integration/integration.js +71 -0
  28. package/dist/bollharness-integration/llm-judgment-engine.js +134 -134
  29. package/dist/bollharness-integration/skill-adapter.js +57 -127
  30. package/dist/bootstrap/chat-archiver.js +276 -0
  31. package/dist/bootstrap/context-collector.js +6 -3
  32. package/dist/bootstrap/event-log.js +160 -0
  33. package/dist/bootstrap/lifecycle-hooks.js +15 -1
  34. package/dist/bootstrap/memory-compressor.js +170 -0
  35. package/dist/bootstrap/persona-loader.js +94 -0
  36. package/dist/bootstrap/project-state.js +159 -0
  37. package/dist/bootstrap/remote-mirror.js +97 -0
  38. package/dist/bootstrap/session-window.js +121 -0
  39. package/dist/bootstrap/vector-index.js +248 -0
  40. package/dist/cli/loading-tui.js +40 -0
  41. package/dist/cli-entry.js +41 -37
  42. package/dist/context-compaction/auto-compact.js +7 -7
  43. package/dist/electron/config.js +21 -0
  44. package/dist/electron/config.js.map +1 -0
  45. package/dist/electron/dialogs.js +108 -0
  46. package/dist/electron/dialogs.js.map +1 -0
  47. package/dist/electron/first-run.js +170 -0
  48. package/dist/electron/first-run.js.map +1 -0
  49. package/dist/electron/ipc.js +20 -0
  50. package/dist/electron/ipc.js.map +1 -0
  51. package/dist/electron/logger.js +114 -0
  52. package/dist/electron/logger.js.map +1 -0
  53. package/dist/electron/main.js +63 -0
  54. package/dist/electron/main.js.map +1 -0
  55. package/dist/electron/menu.js +145 -0
  56. package/dist/electron/menu.js.map +1 -0
  57. package/dist/electron/paths.js +75 -0
  58. package/dist/electron/paths.js.map +1 -0
  59. package/dist/electron/server.js +119 -0
  60. package/dist/electron/server.js.map +1 -0
  61. package/dist/electron/tray.js +111 -0
  62. package/dist/electron/tray.js.map +1 -0
  63. package/dist/electron/window.js +111 -0
  64. package/dist/electron/window.js.map +1 -0
  65. package/dist/electron-preload.js +26 -6
  66. package/dist/electron-preload.js.map +1 -1
  67. package/dist/electron.js +6 -168
  68. package/dist/electron.js.map +1 -1
  69. package/dist/index.js +323 -266
  70. package/dist/llm/config-store.js +24 -8
  71. package/dist/llm/llm-judgment-client.js +102 -102
  72. package/dist/llm/pi-ai.js +137 -85
  73. package/dist/llm/system-prompt/layers/channel/local.md +14 -14
  74. package/dist/llm/system-prompt/layers/channel/p2p-agent.md +18 -18
  75. package/dist/llm/system-prompt/layers/channel/p2p-visitor.md +19 -19
  76. package/dist/llm/system-prompt/layers/core/artifacts_storage.md +89 -89
  77. package/dist/llm/system-prompt/layers/core/evenhandedness.md +21 -21
  78. package/dist/llm/system-prompt/layers/core/hibs_reminders.md +15 -15
  79. package/dist/llm/system-prompt/layers/core/identity.md +37 -37
  80. package/dist/llm/system-prompt/layers/core/knowledge.md +17 -17
  81. package/dist/llm/system-prompt/layers/core/memory_system.md +12 -12
  82. package/dist/llm/system-prompt/layers/core/network_filesystem.md +28 -28
  83. package/dist/llm/system-prompt/layers/core/refusal.md +37 -37
  84. package/dist/llm/system-prompt/layers/core/tone.md +31 -31
  85. package/dist/llm/system-prompt/layers/core/tools.thin.md +13 -13
  86. package/dist/llm/system-prompt/layers/core/wellbeing.md +41 -41
  87. package/dist/llm/system-prompt/layers/role/architect.md +20 -20
  88. package/dist/llm/system-prompt/layers/role/expert.md +19 -19
  89. package/dist/llm/system-prompt/layers/role/implementer.md +15 -15
  90. package/dist/llm/system-prompt/layers/role/security.md +15 -15
  91. package/dist/llm/system-prompt/layers/tool/artifacts.md +72 -72
  92. package/dist/llm/system-prompt/layers/tool/bash.md +25 -25
  93. package/dist/llm/system-prompt/layers/tool/hibs_api.md +171 -171
  94. package/dist/llm/system-prompt/layers/tool/image_search.md +70 -70
  95. package/dist/llm/system-prompt/layers/tool/manifest.md +89 -89
  96. package/dist/llm/system-prompt/layers/tool/mcp_apps.md +53 -53
  97. package/dist/llm/system-prompt/layers/tool/web_search.md +83 -83
  98. package/dist/llm/tool-manifest/ask_user_input.js +1 -1
  99. package/dist/llm/tool-manifest/bash.js +3 -3
  100. package/dist/llm/tool-manifest/create_file.js +4 -4
  101. package/dist/llm/tool-manifest/fetch_sports_data.js +3 -3
  102. package/dist/llm/tool-manifest/image_search.js +2 -2
  103. package/dist/llm/tool-manifest/mcp.js +2 -2
  104. package/dist/llm/tool-manifest/message_compose.js +3 -3
  105. package/dist/llm/tool-manifest/places.js +2 -2
  106. package/dist/llm/tool-manifest/present_files.js +1 -1
  107. package/dist/llm/tool-manifest/recipe.js +2 -2
  108. package/dist/llm/tool-manifest/str_replace.js +5 -5
  109. package/dist/llm/tool-manifest/view.js +3 -3
  110. package/dist/llm/tool-manifest/weather.js +4 -4
  111. package/dist/llm/tool-manifest/web.js +4 -4
  112. package/dist/network/local-inbox-bus.js +73 -0
  113. package/dist/network/p2p-outbox.js +161 -0
  114. package/dist/network/peer-fs.js +420 -0
  115. package/dist/network/peer-resource-bridge.js +216 -0
  116. package/dist/pi-ecosystem-judgment/causal-judge.js +51 -51
  117. package/dist/pi-ecosystem-judgment/cleanup.js +126 -0
  118. package/dist/pi-ecosystem-judgment/distill-prompt.js +42 -42
  119. package/dist/pi-ecosystem-judgment/distillation.js +14 -14
  120. package/dist/pi-ecosystem-judgment/human-value-store.js +45 -11
  121. package/dist/pi-ecosystem-judgment/monitor-gate.js +28 -28
  122. package/dist/pi-ecosystem-judgment/value-injection.js +102 -102
  123. package/dist/security/context-router-tool.js +15 -15
  124. package/dist/security/tool-gate.js +11 -0
  125. package/dist/web/api-config.html +520 -520
  126. package/dist/web/client-loop-status.js +220 -0
  127. package/dist/web/client.js +1677 -244
  128. package/dist/web/client.js.map +7 -0
  129. package/dist/web/components/wallet-viem.mjs +118 -118
  130. package/dist/web/index.html +375 -374
  131. package/dist/web/input-validator.js +103 -0
  132. package/dist/web/manifest.json +20 -20
  133. package/dist/web/routes-judgments.js +738 -0
  134. package/dist/web/routes-llm-config.js +279 -0
  135. package/dist/web/routes-tasks.js +222 -0
  136. package/dist/web/server-sse.js +121 -0
  137. package/dist/web/server-storage.js +154 -0
  138. package/dist/web/server-types.js +15 -0
  139. package/dist/web/server-v3-p2p.js +224 -0
  140. package/dist/web/server.js +1070 -1307
  141. package/dist/web/style.css +4742 -4737
  142. package/dist/web/util/safe-name.js +32 -0
  143. package/package.json +162 -160
  144. package/scripts/build-cli.js +215 -215
  145. package/scripts/build-web.ts +130 -125
  146. package/scripts/postinstall.js +152 -152
  147. package/bin/bolloon.js +0 -157
  148. package/dist/constraint-runtime/src/_archive_helper.js +0 -9
  149. package/dist/constraint-runtime/src/agent/coordinator.js +0 -48
  150. package/dist/constraint-runtime/src/agent/index.js +0 -1
  151. package/dist/constraint-runtime/src/assistant/index.js +0 -12
  152. package/dist/constraint-runtime/src/bootstrap/index.js +0 -12
  153. package/dist/constraint-runtime/src/bootstrap_graph.js +0 -13
  154. package/dist/constraint-runtime/src/bridge/index.js +0 -12
  155. package/dist/constraint-runtime/src/buddy/index.js +0 -12
  156. package/dist/constraint-runtime/src/cli/index.js +0 -12
  157. package/dist/constraint-runtime/src/command_graph.js +0 -10
  158. package/dist/constraint-runtime/src/commands.js +0 -60
  159. package/dist/constraint-runtime/src/components/index.js +0 -12
  160. package/dist/constraint-runtime/src/constants/index.js +0 -12
  161. package/dist/constraint-runtime/src/constraint/budget.js +0 -22
  162. package/dist/constraint-runtime/src/constraint/index.js +0 -2
  163. package/dist/constraint-runtime/src/constraint/permission.js +0 -20
  164. package/dist/constraint-runtime/src/context.js +0 -30
  165. package/dist/constraint-runtime/src/coordinator/index.js +0 -12
  166. package/dist/constraint-runtime/src/cost_hook.js +0 -4
  167. package/dist/constraint-runtime/src/cost_tracker.js +0 -8
  168. package/dist/constraint-runtime/src/deferred_init.js +0 -10
  169. package/dist/constraint-runtime/src/direct_modes.js +0 -6
  170. package/dist/constraint-runtime/src/dynamic-tool-loader.js +0 -85
  171. package/dist/constraint-runtime/src/entrypoints/index.js +0 -12
  172. package/dist/constraint-runtime/src/execution_registry.js +0 -44
  173. package/dist/constraint-runtime/src/history.js +0 -9
  174. package/dist/constraint-runtime/src/hooks/index.js +0 -12
  175. package/dist/constraint-runtime/src/index.js +0 -26
  176. package/dist/constraint-runtime/src/ink.js +0 -4
  177. package/dist/constraint-runtime/src/keybindings/index.js +0 -12
  178. package/dist/constraint-runtime/src/memdir/index.js +0 -12
  179. package/dist/constraint-runtime/src/migrations/index.js +0 -12
  180. package/dist/constraint-runtime/src/models.js +0 -1
  181. package/dist/constraint-runtime/src/moreright/index.js +0 -12
  182. package/dist/constraint-runtime/src/native_ts/index.js +0 -12
  183. package/dist/constraint-runtime/src/output_styles/index.js +0 -12
  184. package/dist/constraint-runtime/src/parity_audit.js +0 -12
  185. package/dist/constraint-runtime/src/plugins/index.js +0 -12
  186. package/dist/constraint-runtime/src/port_manifest.js +0 -11
  187. package/dist/constraint-runtime/src/prefetch.js +0 -9
  188. package/dist/constraint-runtime/src/query.js +0 -1
  189. package/dist/constraint-runtime/src/remote/index.js +0 -12
  190. package/dist/constraint-runtime/src/remote_runtime.js +0 -9
  191. package/dist/constraint-runtime/src/runtime/index.js +0 -1
  192. package/dist/constraint-runtime/src/runtime/session.js +0 -35
  193. package/dist/constraint-runtime/src/schemas/index.js +0 -12
  194. package/dist/constraint-runtime/src/screens/index.js +0 -12
  195. package/dist/constraint-runtime/src/server/index.js +0 -12
  196. package/dist/constraint-runtime/src/services/index.js +0 -12
  197. package/dist/constraint-runtime/src/session_store.js +0 -22
  198. package/dist/constraint-runtime/src/setup.js +0 -30
  199. package/dist/constraint-runtime/src/skills/index.js +0 -1
  200. package/dist/constraint-runtime/src/skills/skill-registry.js +0 -28
  201. package/dist/constraint-runtime/src/state/index.js +0 -12
  202. package/dist/constraint-runtime/src/system_init.js +0 -20
  203. package/dist/constraint-runtime/src/thinking/engine.js +0 -42
  204. package/dist/constraint-runtime/src/thinking/index.js +0 -1
  205. package/dist/constraint-runtime/src/tool_pool.js +0 -8
  206. package/dist/constraint-runtime/src/tools/OpenCLI/execAdapter.js +0 -7
  207. package/dist/constraint-runtime/src/tools/OpenCLI/listAdapters.js +0 -7
  208. package/dist/constraint-runtime/src/tools/OpenCLI/runCommand.js +0 -7
  209. package/dist/constraint-runtime/src/tools/SafeSDK/confirmTransaction.js +0 -6
  210. package/dist/constraint-runtime/src/tools/SafeSDK/createTransaction.js +0 -8
  211. package/dist/constraint-runtime/src/tools/SafeSDK/executeTransaction.js +0 -6
  212. package/dist/constraint-runtime/src/tools/SafeSDK/getBalance.js +0 -6
  213. package/dist/constraint-runtime/src/tools/SafeSDK/getPendingTransactions.js +0 -6
  214. package/dist/constraint-runtime/src/tools/SafeSDK/proposeTransaction.js +0 -6
  215. package/dist/constraint-runtime/src/tools.js +0 -80
  216. package/dist/constraint-runtime/src/transcript.js +0 -19
  217. package/dist/constraint-runtime/src/types/index.js +0 -12
  218. package/dist/constraint-runtime/src/upstream_proxy/index.js +0 -12
  219. package/dist/constraint-runtime/src/utils/index.js +0 -12
  220. package/dist/constraint-runtime/src/vim/index.js +0 -12
  221. package/dist/constraint-runtime/src/voice/index.js +0 -12
  222. package/dist/constraint-runtime/tests/agent.test.js +0 -16
  223. package/dist/constraint-runtime/tests/constraint.test.js +0 -41
  224. package/dist/constraint-runtime/tests/skill.test.js +0 -19
  225. package/dist/constraint-runtime/tests/thinking.test.js +0 -22
  226. package/dist/pi-ecosystem-colony/index.js +0 -365
  227. package/dist/social/ant-colony/AdaptiveHeartbeat.js +0 -101
  228. package/dist/social/ant-colony/PheromoneEngine.js +0 -227
  229. package/dist/social/ant-colony/index.js +0 -6
  230. package/dist/social/ant-colony/types.js +0 -24
  231. package/dist/test/ai-judgment-test.js +0 -80
  232. package/dist/test/bollharness-integration.test.js +0 -318
  233. package/dist/test/channel-agent-multi-dialogue.js +0 -205
  234. package/dist/test/channel-heartbeat-agent-test.js +0 -201
  235. package/dist/test/constraint-layer.test.js +0 -164
  236. package/dist/test/diap-identity-test.js +0 -172
  237. package/dist/test/diap-quick-test.js +0 -62
  238. package/dist/test/global-shared-context.test.js +0 -315
  239. package/dist/test/harness-judgment-injection.test.js +0 -246
  240. package/dist/test/harness-workflow-integrator-test.js +0 -228
  241. package/dist/test/human-value-store.test.js +0 -243
  242. package/dist/test/hybrid-integration-test.js +0 -118
  243. package/dist/test/hybrid-messenger-verify.js +0 -55
  244. package/dist/test/iroh-bistream-debug.js +0 -38
  245. package/dist/test/iroh-communication.test.js +0 -66
  246. package/dist/test/iroh-debug-test.js +0 -57
  247. package/dist/test/iroh-diap-test.js +0 -71
  248. package/dist/test/iroh-direct-connect.js +0 -55
  249. package/dist/test/iroh-e2e-fixed.js +0 -89
  250. package/dist/test/iroh-e2e-same-process.js +0 -63
  251. package/dist/test/iroh-e2e.js +0 -66
  252. package/dist/test/iroh-final-e2e.js +0 -72
  253. package/dist/test/iroh-relay-test.js +0 -37
  254. package/dist/test/iroh-simple-test.js +0 -41
  255. package/dist/test/iroh-transport-verify.js +0 -54
  256. package/dist/test/iroh-transport.test.js +0 -37
  257. package/dist/test/iroh-two-nodes.js +0 -70
  258. package/dist/test/iroh-verify.js +0 -44
  259. package/dist/test/judgment-decision.test.js +0 -219
  260. package/dist/test/llm-judgment-integration.test.js +0 -220
  261. package/dist/test/p2p-agent-complex-dialogue.js +0 -385
  262. package/dist/test/p2p-agent-dialogue.js +0 -341
  263. package/dist/test/p2p-agent-full-bidirectional.js +0 -510
  264. package/dist/test/p2p-agent-harness-flow.js +0 -437
  265. package/dist/test/p2p-agent-harness-single.js +0 -143
  266. package/dist/test/p2p-ai-dialogue-test.js +0 -318
  267. package/dist/test/p2p-cid-connect-test.js +0 -195
  268. package/dist/test/p2p-connect-receiver.js +0 -69
  269. package/dist/test/p2p-doc-transfer.js +0 -110
  270. package/dist/test/p2p-identity-page-test.js +0 -77
  271. package/dist/test/p2p-iroh-test.js +0 -171
  272. package/dist/test/p2p-minimal-test.js +0 -241
  273. package/dist/test/p2p-node-1.js +0 -148
  274. package/dist/test/p2p-node-2.js +0 -148
  275. package/dist/test/p2p-server.js +0 -281
  276. package/dist/test/p2p-two-nodes-test.js +0 -438
  277. package/dist/test/pi-sdk.test.js +0 -44
  278. package/dist/test/set-persona.js +0 -40
  279. package/dist/test/simple.test.js +0 -9
  280. package/dist/test/storage-integration.test.js +0 -150
  281. package/dist/test/subagent-manager.test.js +0 -276
  282. package/dist/test/test-gate-flow.test.js +0 -81
  283. package/dist/test/workflow-engine.test.js +0 -87
  284. package/dist/test/workflow-pivot-loop.test.js +0 -246
@@ -15,8 +15,8 @@ export const web_search = {
15
15
  parameters: [
16
16
  { name: 'query', type: 'string', required: true, description: '搜索查询 (1-6 词最佳)' },
17
17
  ],
18
- callExample: `[TOOL:web_search]
19
- [P:query]S&P 500 current price
18
+ callExample: `[TOOL:web_search]
19
+ [P:query]S&P 500 current price
20
20
  [ENDTOOL]`,
21
21
  layerId: 'tool.web_search',
22
22
  };
@@ -44,8 +44,8 @@ export const web_fetch = {
44
44
  { name: 'web_fetch_rate_limit_key', type: 'string', required: false, description: '速率限制键' },
45
45
  { name: 'web_fetch_rate_limit_dark_launch', type: 'boolean', required: false, description: '速率限制暗启动模式' },
46
46
  ],
47
- callExample: `[TOOL:web_fetch]
48
- [P:url]https://example.com/article
47
+ callExample: `[TOOL:web_fetch]
48
+ [P:url]https://example.com/article
49
49
  [ENDTOOL]`,
50
50
  layerId: 'tool.manifest',
51
51
  };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * local-inbox-bus.ts — 进程内 Agent 通信总线 (2026-06-19)
3
+ *
4
+ * 解决 bolloon "和另一个智能体发消息" 的本地场景:
5
+ * - 同进程内多个 PiAgentSession 实例 (例如 nodeA / nodeB 测试) 互发消息
6
+ * - 走 in-process EventEmitter, 不依赖 P2P 网络/Hyperswarm
7
+ * - 远端通信仍走 p2pNetwork.sendMessage + onMessage
8
+ *
9
+ * 用法:
10
+ * const bus = LocalInboxBus.getInstance();
11
+ * bus.subscribe('nodeA', (msg) => console.log('nodeA got:', msg));
12
+ * bus.deliver('nodeB', { from: 'nodeA', type: 'agent-message', payload: 'hi' });
13
+ *
14
+ * 角色标识: 默认从 process.env.BOLLOON_ROLE 读, 也可显式传
15
+ */
16
+ import { EventEmitter } from 'events';
17
+ class LocalInboxBus {
18
+ static _instance = null;
19
+ emitter;
20
+ /** 记录已注册的角色, deliver 时找不到会返回 false (避免 silent drop) */
21
+ registeredRoles = new Set();
22
+ constructor() {
23
+ this.emitter = new EventEmitter();
24
+ // 防止 listener 太多导致 memory leak
25
+ this.emitter.setMaxListeners(1000);
26
+ }
27
+ static getInstance() {
28
+ if (!LocalInboxBus._instance) {
29
+ LocalInboxBus._instance = new LocalInboxBus();
30
+ }
31
+ return LocalInboxBus._instance;
32
+ }
33
+ /**
34
+ * 订阅角色 inbox — 投递到该角色的消息会回调 fn
35
+ * 返回 unsubscribe 函数
36
+ */
37
+ subscribe(role, fn) {
38
+ this.registeredRoles.add(role);
39
+ this.emitter.on(`role:${role}`, fn);
40
+ return () => {
41
+ this.emitter.off(`role:${role}`, fn);
42
+ // 如果没人订阅, 移除 role (避免泄漏)
43
+ if (this.emitter.listenerCount(`role:${role}`) === 0) {
44
+ this.registeredRoles.delete(role);
45
+ }
46
+ };
47
+ }
48
+ /**
49
+ * 投递消息到指定角色
50
+ * 返回 true=已投递, false=角色未注册
51
+ */
52
+ deliver(role, msg) {
53
+ if (!this.registeredRoles.has(role)) {
54
+ return false;
55
+ }
56
+ this.emitter.emit(`role:${role}`, msg);
57
+ return true;
58
+ }
59
+ /**
60
+ * 列出当前进程所有已注册的角色
61
+ */
62
+ listRoles() {
63
+ return Array.from(this.registeredRoles);
64
+ }
65
+ /**
66
+ * 检查某角色是否已订阅
67
+ */
68
+ hasRole(role) {
69
+ return this.registeredRoles.has(role);
70
+ }
71
+ }
72
+ export { LocalInboxBus };
73
+ export default LocalInboxBus;
@@ -0,0 +1,161 @@
1
+ /**
2
+ * p2p-outbox.ts — 离线消息队列管理 (基于 peer-fs.ts 落盘)
3
+ *
4
+ * 设计目的 (2026-07-05):
5
+ * v3 P2PDirect 通道 sendTo 找不到 conn 就返回 false (消息扔进虚空).
6
+ * 这个模块给所有"发往远端"的 RPC 提供 outbox 兜底:
7
+ * 1) 发送前先 try sendToWithWait (3s 超时)
8
+ * 2) 失败 → enqueueOutbox 到 ~/.bolloon/peers/<pk>/outbox.jsonl
9
+ * 3) flushAllOutboxes() 周期跑 + 启动后跑, 检测到 peer 上线就批量重发
10
+ *
11
+ * 用法:
12
+ * import { sendOrQueue } from '../network/p2p-outbox.js';
13
+ * const r = await sendOrQueue(peerPk, 'agent.chat.send', { channelId, text }, v3P2PRef);
14
+ * // r: 'SENT' | 'QUEUED' | 'FAILED'
15
+ */
16
+ import * as crypto from 'crypto';
17
+ import * as peerFs from './peer-fs.js';
18
+ /**
19
+ * 发送或入队: 优先 sendToWithWait (3s), 失败 → 写 outbox.jsonl
20
+ */
21
+ export async function sendOrQueue(publicKey, op, payload, p2p, opts = {}) {
22
+ if (!publicKey)
23
+ return 'FAILED';
24
+ const rpc = JSON.stringify({ v: 3, op, payload });
25
+ // 先尝试直发
26
+ if (!opts.forceQueue && p2p) {
27
+ try {
28
+ const r = await p2p.sendToWithWait(publicKey, rpc, 3000);
29
+ if (r === 'SENT')
30
+ return 'SENT';
31
+ }
32
+ catch (err) {
33
+ console.warn(`[outbox] sendToWithWait 抛错 (${publicKey.slice(0, 12)}...): ${err?.message?.slice(0, 100)}`);
34
+ }
35
+ }
36
+ // 直发失败 → 入队
37
+ try {
38
+ await peerFs.enqueueOutbox(publicKey, {
39
+ id: crypto.randomUUID(),
40
+ ts: new Date().toISOString(),
41
+ op,
42
+ payload,
43
+ attempts: 0,
44
+ });
45
+ console.log(`[outbox] ${op} → ${publicKey.slice(0, 12)}... 入队 (peer 当前不在线, 上线后自动重发)`);
46
+ return 'QUEUED';
47
+ }
48
+ catch (err) {
49
+ console.error(`[outbox] 入队失败 (${publicKey.slice(0, 12)}...): ${err?.message?.slice(0, 200)}`);
50
+ return 'FAILED';
51
+ }
52
+ }
53
+ /**
54
+ * 批量 flush 所有 peer 的 outbox — 对方上线后调用.
55
+ * 返回 { totalQueued, totalSent, totalFailed }.
56
+ */
57
+ export async function flushAllOutboxes(p2p) {
58
+ if (!p2p)
59
+ return { peers: 0, totalQueued: 0, totalSent: 0, totalFailed: 0 };
60
+ let peers = 0, totalQueued = 0, totalSent = 0, totalFailed = 0;
61
+ try {
62
+ const all = await peerFs.listPeersFromDisk();
63
+ const myPk = p2p.getPublicKey();
64
+ for (const peer of all) {
65
+ if (peer.publicKey === myPk)
66
+ continue;
67
+ const outbox = await peerFs.readOutbox(peer.publicKey);
68
+ if (outbox.length === 0)
69
+ continue;
70
+ peers++;
71
+ totalQueued += outbox.length;
72
+ let sent = 0;
73
+ let failedEntries = [];
74
+ for (const entry of outbox) {
75
+ const rpc = JSON.stringify({ v: 3, op: entry.op, payload: entry.payload });
76
+ try {
77
+ const r = await p2p.sendToWithWait(peer.publicKey, rpc, 3000);
78
+ if (r === 'SENT') {
79
+ sent++;
80
+ }
81
+ else {
82
+ failedEntries.push({ ...entry, attempts: entry.attempts + 1, lastError: r });
83
+ }
84
+ }
85
+ catch (err) {
86
+ failedEntries.push({ ...entry, attempts: entry.attempts + 1, lastError: err?.message?.slice(0, 200) });
87
+ }
88
+ }
89
+ totalSent += sent;
90
+ totalFailed += failedEntries.length;
91
+ // 写回未发送的 (覆盖原文件)
92
+ if (failedEntries.length === 0) {
93
+ await peerFs.clearOutbox(peer.publicKey);
94
+ }
95
+ else {
96
+ // 重新写一份
97
+ const { writeFile, mkdir } = await import('fs/promises');
98
+ const path = await import('path');
99
+ const file = peerFs.getPeerOutboxPath(peer.publicKey);
100
+ await mkdir(path.dirname(file), { recursive: true });
101
+ const lines = failedEntries.map(e => JSON.stringify(e)).join('\n') + '\n';
102
+ await writeFile(file, lines, 'utf-8');
103
+ }
104
+ if (sent > 0) {
105
+ console.log(`[outbox] flush → ${peer.publicKey.slice(0, 12)}... 重发 ${sent}/${outbox.length} 条 ✓`);
106
+ }
107
+ }
108
+ if (totalQueued > 0) {
109
+ console.log(`[outbox] 累计处理: queued=${totalQueued}, sent=${totalSent}, failed=${totalFailed}`);
110
+ }
111
+ }
112
+ catch (err) {
113
+ console.warn('[outbox] flushAllOutboxes 失败:', err?.message?.slice(0, 200));
114
+ }
115
+ return { peers, totalQueued, totalSent, totalFailed };
116
+ }
117
+ /**
118
+ * 给单个 peer 入队一条 — 用于明确"对方一定不在线"的场景
119
+ */
120
+ export async function queueOnly(publicKey, op, payload) {
121
+ await peerFs.enqueueOutbox(publicKey, {
122
+ id: crypto.randomUUID(),
123
+ ts: new Date().toISOString(),
124
+ op,
125
+ payload,
126
+ attempts: 0,
127
+ });
128
+ }
129
+ /**
130
+ * 查 outbox 状态
131
+ * 直接扫 ~/.bolloon/peers/ 子目录, 看每个目录里 outbox.jsonl 是否有内容 (不依赖 peer.json)
132
+ */
133
+ export async function outboxStats() {
134
+ const fsPromises = await import('fs/promises');
135
+ const path = await import('path');
136
+ const byPeer = [];
137
+ let totalQueued = 0;
138
+ try {
139
+ const entries = await fsPromises.readdir(peerFs._debug.PEERS_ROOT);
140
+ for (const e of entries) {
141
+ const outboxFile = path.join(peerFs._debug.PEERS_ROOT, e, 'outbox.jsonl');
142
+ try {
143
+ const raw = await fsPromises.readFile(outboxFile, 'utf-8');
144
+ const cnt = raw.split('\n').filter(Boolean).length;
145
+ if (cnt > 0) {
146
+ // 从目录名反推 publicKey (peerDirName 是 hash__prefix)
147
+ const prefix = e.split('__').slice(1).join('__') || e;
148
+ byPeer.push({ publicKey: prefix, count: cnt });
149
+ totalQueued += cnt;
150
+ }
151
+ }
152
+ catch {
153
+ // 没 outbox.jsonl → 跳过
154
+ }
155
+ }
156
+ }
157
+ catch {
158
+ // PEERS_ROOT 不存在 → 空
159
+ }
160
+ return { totalPeers: byPeer.length, totalQueued, byPeer };
161
+ }
@@ -0,0 +1,420 @@
1
+ /**
2
+ * peer-fs.ts — peer 目录读写 + agents 索引 + manifest 落盘
3
+ *
4
+ * 设计目的 (2026-07-05):
5
+ * 把"远程 P2P 节点"在本地物化成一份文件夹, 让本地所有 agent 都能"知道对方有什么":
6
+ * ~/.bolloon/peers/<publicKey>/
7
+ * peer.json ← 对方基础信息 (name / publicKey / lastSeen / lastManifestTs)
8
+ * user.md ← 对方用户画像 (本地维护)
9
+ * _index.json ← 对方 agent 列表 (轻量, 懒加载时优先读)
10
+ * capability-index.md ← ≤500 字摘要, 拼进 prompt
11
+ * agents/<agentId>.md ← 每个 agent 详细描述
12
+ * groups/<groupId>.md ← 群组
13
+ * function/<cap>.md ← 视频/音乐/图片
14
+ * exportment/<game>.md ← 游戏
15
+ * science/<exp>.md ← 实验记录
16
+ * chat-<YYYY-MM>.md ← 月度聊天记录归档
17
+ * outbox.jsonl ← 离线发送队列 (对方不在线时缓存)
18
+ *
19
+ * 写盘策略:
20
+ * - 纯文本 md 文件, 人类可读, 便于调试
21
+ * - JSON 仅用于 peer.json / _index.json / outbox.jsonl
22
+ * - 全部操作 atomic (write tmp + rename), 避免半截文件
23
+ */
24
+ import * as fs from 'fs/promises';
25
+ import * as path from 'path';
26
+ import * as os from 'os';
27
+ import * as crypto from 'crypto';
28
+ // ============== 路径常量 ==============
29
+ const HOME = process.env.BOLLOON_HOME || path.join(os.homedir(), '.bolloon');
30
+ const PEERS_ROOT = path.join(HOME, 'peers');
31
+ /** publicKey hex → 短哈希, 用于目录名防超长 */
32
+ export function peerDirName(publicKey) {
33
+ // 用 sha256 前 16 位 hex, 防 publicKey 改名冲突 + 文件名长度可控
34
+ const h = crypto.createHash('sha256').update(publicKey).digest('hex').slice(0, 16);
35
+ return `${h}__${publicKey.slice(0, 8)}`;
36
+ }
37
+ export function getPeerDir(publicKey) {
38
+ return path.join(PEERS_ROOT, peerDirName(publicKey));
39
+ }
40
+ export function getPeerIndexPath(publicKey) {
41
+ return path.join(getPeerDir(publicKey), '_index.json');
42
+ }
43
+ export function getPeerJsonPath(publicKey) {
44
+ return path.join(getPeerDir(publicKey), 'peer.json');
45
+ }
46
+ export function getPeerCapabilityIndexPath(publicKey) {
47
+ return path.join(getPeerDir(publicKey), 'capability-index.md');
48
+ }
49
+ export function getPeerChatPath(publicKey, yearMonth) {
50
+ const ym = yearMonth || currentYearMonth();
51
+ return path.join(getPeerDir(publicKey), `chat-${ym}.md`);
52
+ }
53
+ export function getPeerOutboxPath(publicKey) {
54
+ return path.join(getPeerDir(publicKey), 'outbox.jsonl');
55
+ }
56
+ export function getPeerUserMdPath(publicKey) {
57
+ return path.join(getPeerDir(publicKey), 'user.md');
58
+ }
59
+ export function getPeerAgentMdPath(publicKey, agentId) {
60
+ return path.join(getPeerDir(publicKey), 'agents', `${safeName(agentId)}.md`);
61
+ }
62
+ export function getPeerGroupMdPath(publicKey, groupId) {
63
+ return path.join(getPeerDir(publicKey), 'groups', `${safeName(groupId)}.md`);
64
+ }
65
+ export function getPeerFunctionMdPath(publicKey, capability) {
66
+ return path.join(getPeerDir(publicKey), 'function', `${safeName(capability)}.md`);
67
+ }
68
+ export function getPeerExportmentMdPath(publicKey, game) {
69
+ return path.join(getPeerDir(publicKey), 'exportment', `${safeName(game)}.md`);
70
+ }
71
+ export function getPeerScienceMdPath(publicKey, expId) {
72
+ return path.join(getPeerDir(publicKey), 'science', `${safeName(expId)}.md`);
73
+ }
74
+ export function currentYearMonth() {
75
+ const d = new Date();
76
+ return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`;
77
+ }
78
+ /** 文件名安全化: 去掉 / \ : * ? " < > | 控制符 */
79
+ function safeName(s) {
80
+ return s.replace(/[\/\\:\*\?"<>\|\x00-\x1f]/g, '_').slice(0, 64);
81
+ }
82
+ // ============== Atomic write helper ==============
83
+ async function atomicWrite(filePath, content) {
84
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
85
+ const tmp = filePath + '.tmp.' + Math.random().toString(36).slice(2, 8);
86
+ await fs.writeFile(tmp, content);
87
+ await fs.rename(tmp, filePath);
88
+ }
89
+ async function atomicWriteJson(filePath, obj) {
90
+ await atomicWrite(filePath, JSON.stringify(obj, null, 2) + '\n');
91
+ }
92
+ async function readJsonSafe(filePath) {
93
+ try {
94
+ const raw = await fs.readFile(filePath, 'utf-8');
95
+ return JSON.parse(raw);
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
101
+ // ============== peer.json ==============
102
+ export async function upsertPeer(rec) {
103
+ const file = getPeerJsonPath(rec.publicKey);
104
+ const existing = await readJsonSafe(file);
105
+ const merged = {
106
+ publicKey: rec.publicKey,
107
+ name: rec.name ?? existing?.name,
108
+ addedAt: existing?.addedAt || rec.addedAt || new Date().toISOString(),
109
+ lastSeenAt: rec.lastSeenAt ?? existing?.lastSeenAt,
110
+ lastManifestTs: rec.lastManifestTs ?? existing?.lastManifestTs,
111
+ manifestCount: rec.manifestCount ?? existing?.manifestCount,
112
+ notes: rec.notes ?? existing?.notes,
113
+ };
114
+ await atomicWriteJson(file, merged);
115
+ return merged;
116
+ }
117
+ export async function getPeer(publicKey) {
118
+ return readJsonSafe(getPeerJsonPath(publicKey));
119
+ }
120
+ export async function listPeersFromDisk() {
121
+ try {
122
+ const entries = await fs.readdir(PEERS_ROOT);
123
+ const out = [];
124
+ for (const e of entries) {
125
+ const peerFile = path.join(PEERS_ROOT, e, 'peer.json');
126
+ const rec = await readJsonSafe(peerFile);
127
+ if (rec)
128
+ out.push(rec);
129
+ }
130
+ return out;
131
+ }
132
+ catch {
133
+ return [];
134
+ }
135
+ }
136
+ // ============== _index.json (agent 清单) ==============
137
+ export async function writePeerIndex(publicKey, idx) {
138
+ await atomicWriteJson(getPeerIndexPath(publicKey), idx);
139
+ }
140
+ export async function readPeerIndex(publicKey) {
141
+ return readJsonSafe(getPeerIndexPath(publicKey));
142
+ }
143
+ // ============== 单个 agent 详细描述 (markdown) ==============
144
+ export async function writeAgentDescription(publicKey, agent, ownerDescription) {
145
+ const lines = [];
146
+ lines.push(`# ${agent.name || agent.id}`);
147
+ lines.push('');
148
+ lines.push(`- **ID**: ${agent.id}`);
149
+ lines.push(`- **状态**: ${agent.status}`);
150
+ if (agent.capabilities.length) {
151
+ lines.push(`- **能力**: ${agent.capabilities.join(', ')}`);
152
+ }
153
+ if (agent.description) {
154
+ lines.push('');
155
+ lines.push('## 描述');
156
+ lines.push('');
157
+ lines.push(agent.description);
158
+ }
159
+ if (ownerDescription) {
160
+ lines.push('');
161
+ lines.push('## Owner 备注');
162
+ lines.push('');
163
+ lines.push(ownerDescription);
164
+ }
165
+ if (agent.peerId || agent.irohNodeId || agent.sessionId) {
166
+ lines.push('');
167
+ lines.push('## 寻址');
168
+ if (agent.peerId)
169
+ lines.push(`- peerId: ${agent.peerId}`);
170
+ if (agent.irohNodeId)
171
+ lines.push(`- irohNodeId: ${agent.irohNodeId}`);
172
+ if (agent.sessionId)
173
+ lines.push(`- sessionId: ${agent.sessionId}`);
174
+ }
175
+ lines.push('');
176
+ lines.push(`> 自动生成于 ${new Date().toISOString()} (peer=${publicKey.slice(0, 12)}...)`);
177
+ lines.push('');
178
+ await atomicWrite(getPeerAgentMdPath(publicKey, agent.id), lines.join('\n'));
179
+ }
180
+ // ============== capability-index.md (≤500 字摘要, 进 prompt) ==============
181
+ export function buildCapabilityIndex(idx) {
182
+ const lines = [];
183
+ lines.push(`# 远端节点能力索引 — ${idx.ownerName || idx.publicKey.slice(0, 12)}`);
184
+ lines.push('');
185
+ if (idx.ownerDescription) {
186
+ lines.push(idx.ownerDescription.slice(0, 200));
187
+ lines.push('');
188
+ }
189
+ lines.push(`## Agent (${idx.agents.length} 个)`);
190
+ for (const a of idx.agents.slice(0, 20)) {
191
+ const caps = a.capabilities.length ? ` [${a.capabilities.join('/')}]` : '';
192
+ const desc = a.description ? ` — ${a.description.slice(0, 60)}` : '';
193
+ lines.push(`- ${a.name}${caps}${desc}`);
194
+ }
195
+ if (idx.agents.length > 20)
196
+ lines.push(`- … 还有 ${idx.agents.length - 20} 个`);
197
+ if (idx.groups?.length) {
198
+ lines.push('');
199
+ lines.push(`## 群组 (${idx.groups.length})`);
200
+ for (const g of idx.groups.slice(0, 10))
201
+ lines.push(`- ${g.name}${g.description ? ' — ' + g.description.slice(0, 50) : ''}`);
202
+ }
203
+ if (idx.functions?.length) {
204
+ lines.push('');
205
+ lines.push(`## 可用功能 (${idx.functions.length})`);
206
+ for (const f of idx.functions.slice(0, 10))
207
+ lines.push(`- ${f.capability}${f.description ? ' — ' + f.description.slice(0, 50) : ''}`);
208
+ }
209
+ if (idx.exportments?.length) {
210
+ lines.push('');
211
+ lines.push(`## 娱乐 (${idx.exportments.length})`);
212
+ for (const e of idx.exportments.slice(0, 10))
213
+ lines.push(`- ${e.name}${e.description ? ' — ' + e.description.slice(0, 50) : ''}`);
214
+ }
215
+ const text = lines.join('\n');
216
+ // 截到 500 字
217
+ return text.length > 500 ? text.slice(0, 497) + '…' : text;
218
+ }
219
+ export async function writeCapabilityIndex(publicKey, idx) {
220
+ const text = buildCapabilityIndex(idx);
221
+ await atomicWrite(getPeerCapabilityIndexPath(publicKey), text);
222
+ }
223
+ export async function readCapabilityIndex(publicKey) {
224
+ try {
225
+ return await fs.readFile(getPeerCapabilityIndexPath(publicKey), 'utf-8');
226
+ }
227
+ catch {
228
+ return null;
229
+ }
230
+ }
231
+ // ============== chat-<YYYY-MM>.md 月度归档 ==============
232
+ function formatChatEntry(e) {
233
+ const ts = e.ts.replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
234
+ const src = e.source;
235
+ const chan = e.channelName ? ` [${e.channelName}]` : '';
236
+ const from = e.fromPublicKey ? ` (${e.fromPublicKey.slice(0, 12)}…)` : '';
237
+ return `### ${ts}${chan} — ${src}${from}\n\n${e.text}\n`;
238
+ }
239
+ export async function appendChat(publicKey, entry) {
240
+ const file = getPeerChatPath(publicKey);
241
+ // 如果文件不存在, 先写头
242
+ try {
243
+ await fs.access(file);
244
+ }
245
+ catch {
246
+ await atomicWrite(file, `# 与 ${publicKey.slice(0, 12)}… 的聊天记录\n\n> 自动归档; 详细消息存于 sessions/cache/, 这里按月滚动.\n\n`);
247
+ }
248
+ // append
249
+ await fs.appendFile(file, '\n---\n\n' + formatChatEntry(entry), 'utf-8');
250
+ }
251
+ export async function readChatArchive(publicKey, yearMonth) {
252
+ try {
253
+ return await fs.readFile(getPeerChatPath(publicKey, yearMonth), 'utf-8');
254
+ }
255
+ catch {
256
+ return null;
257
+ }
258
+ }
259
+ export async function listChatMonths(publicKey) {
260
+ try {
261
+ const dir = getPeerDir(publicKey);
262
+ const entries = await fs.readdir(dir);
263
+ return entries
264
+ .filter(e => /^chat-\d{4}-\d{2}\.md$/.test(e))
265
+ .map(e => e.replace(/^chat-/, '').replace(/\.md$/, ''))
266
+ .sort();
267
+ }
268
+ catch {
269
+ return [];
270
+ }
271
+ }
272
+ // ============== outbox.jsonl 离线队列 ==============
273
+ export async function enqueueOutbox(publicKey, entry) {
274
+ const file = getPeerOutboxPath(publicKey);
275
+ await fs.mkdir(path.dirname(file), { recursive: true });
276
+ await fs.appendFile(file, JSON.stringify(entry) + '\n', 'utf-8');
277
+ }
278
+ export async function readOutbox(publicKey) {
279
+ try {
280
+ const raw = await fs.readFile(getPeerOutboxPath(publicKey), 'utf-8');
281
+ return raw.split('\n').filter(Boolean).map(line => JSON.parse(line));
282
+ }
283
+ catch {
284
+ return [];
285
+ }
286
+ }
287
+ export async function clearOutbox(publicKey) {
288
+ try {
289
+ await fs.unlink(getPeerOutboxPath(publicKey));
290
+ }
291
+ catch { }
292
+ }
293
+ export async function countOutbox(publicKey) {
294
+ return (await readOutbox(publicKey)).length;
295
+ }
296
+ // ============== user.md 本地维护的对方用户画像 ==============
297
+ export async function readUserPortrait(publicKey) {
298
+ try {
299
+ return await fs.readFile(getPeerUserMdPath(publicKey), 'utf-8');
300
+ }
301
+ catch {
302
+ return null;
303
+ }
304
+ }
305
+ export async function writeUserPortrait(publicKey, markdown) {
306
+ await atomicWrite(getPeerUserMdPath(publicKey), markdown);
307
+ }
308
+ // ============== 单个 group/function/exportment/science writer ==============
309
+ // (peer-fs.ts 之前只有 reader — 现在补齐 4 类资源的 writer, 让 manifest.exchange.reply
310
+ // 能落盘对方的群组/功能/娱乐/实验清单, 跟 agents/*.md 一样被本地 agent 看见.)
311
+ function formatFrontmatter(fields) {
312
+ const lines = ['---'];
313
+ for (const [k, v] of Object.entries(fields)) {
314
+ if (v === undefined || v === null)
315
+ continue;
316
+ if (Array.isArray(v)) {
317
+ lines.push(`${k}: [${v.map((x) => JSON.stringify(x)).join(', ')}]`);
318
+ }
319
+ else if (typeof v === 'string') {
320
+ lines.push(`${k}: ${JSON.stringify(v)}`);
321
+ }
322
+ else {
323
+ lines.push(`${k}: ${JSON.stringify(v)}`);
324
+ }
325
+ }
326
+ lines.push('---');
327
+ return lines.join('\n');
328
+ }
329
+ function mdBody(title, fields, description, extra) {
330
+ const parts = [];
331
+ parts.push(`# ${title}`);
332
+ parts.push('');
333
+ parts.push(formatFrontmatter(fields));
334
+ parts.push('');
335
+ if (description) {
336
+ parts.push(description.trim());
337
+ parts.push('');
338
+ }
339
+ if (extra?.length) {
340
+ parts.push(...extra);
341
+ parts.push('');
342
+ }
343
+ parts.push(`> 自动写入于 ${new Date().toISOString()} (peer 来源)`);
344
+ parts.push('');
345
+ return parts.join('\n');
346
+ }
347
+ export async function writeGroup(publicKey, g) {
348
+ const body = mdBody(g.name, {
349
+ id: g.id,
350
+ name: g.name,
351
+ visibility: g.visibility || 'invite',
352
+ memberCount: g.memberCount,
353
+ }, g.description);
354
+ await atomicWrite(getPeerGroupMdPath(publicKey, g.id), body);
355
+ }
356
+ export async function writeFunction(publicKey, f) {
357
+ const body = mdBody(f.capability, {
358
+ capability: f.capability,
359
+ mediaType: f.mediaType || 'text',
360
+ endpoint: f.endpoint,
361
+ }, f.description, f.examples?.length ? ['## 示例', '', ...f.examples.map((e) => `- ${e}`)] : undefined);
362
+ await atomicWrite(getPeerFunctionMdPath(publicKey, f.capability), body);
363
+ }
364
+ export async function writeExportment(publicKey, e) {
365
+ const body = mdBody(e.name, {
366
+ name: e.name,
367
+ genre: e.genre,
368
+ minPlayers: e.minPlayers,
369
+ maxPlayers: e.maxPlayers,
370
+ }, e.description);
371
+ await atomicWrite(getPeerExportmentMdPath(publicKey, e.name), body);
372
+ }
373
+ export async function writeScience(publicKey, s) {
374
+ const recordsBlock = s.records?.length
375
+ ? ['## 记录', '', ...s.records.map((r) => `- **${r.ts}**: ${r.note}`)]
376
+ : undefined;
377
+ const body = mdBody(s.title, {
378
+ id: s.id,
379
+ title: s.title,
380
+ status: s.status || 'planned',
381
+ tags: s.tags,
382
+ }, s.description, recordsBlock);
383
+ await atomicWrite(getPeerScienceMdPath(publicKey, s.id), body);
384
+ }
385
+ export async function listPeerResources(publicKey) {
386
+ const dir = getPeerDir(publicKey);
387
+ const out = { groups: [], functions: [], exportments: [], sciences: [] };
388
+ for (const [subdir, key] of [
389
+ ['groups', 'groups'],
390
+ ['function', 'functions'],
391
+ ['exportment', 'exportments'],
392
+ ['science', 'sciences'],
393
+ ]) {
394
+ try {
395
+ const entries = await fs.readdir(path.join(dir, subdir));
396
+ for (const e of entries) {
397
+ if (!e.endsWith('.md'))
398
+ continue;
399
+ const content = await fs.readFile(path.join(dir, subdir, e), 'utf-8');
400
+ const id = e.replace(/\.md$/, '');
401
+ if (key === 'groups')
402
+ out.groups.push({ id, content });
403
+ else if (key === 'functions')
404
+ out.functions.push({ capability: id, content });
405
+ else if (key === 'exportments')
406
+ out.exportments.push({ name: id, content });
407
+ else
408
+ out.sciences.push({ id, content });
409
+ }
410
+ }
411
+ catch { }
412
+ }
413
+ return out;
414
+ }
415
+ // ============== 测试 / 调试 helper ==============
416
+ export const _debug = {
417
+ PEERS_ROOT,
418
+ peerDirName,
419
+ getPeerDir,
420
+ };