@bolloon/bolloon-agent 0.2.9 → 0.2.10

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 (268) hide show
  1. package/README.md +467 -467
  2. package/bin/bolloon-cli.cjs +183 -183
  3. package/bin/bolloon.cjs +0 -0
  4. package/bin/bolloon.js +157 -0
  5. package/dist/agents/agent-manifest-protocol.js +0 -52
  6. package/dist/agents/constraint-layer.js +19 -19
  7. package/dist/agents/pi-sdk.js +287 -1213
  8. package/dist/agents/shell-guard.js +2 -2
  9. package/dist/agents/workflow-pivot-loop.js +2 -81
  10. package/dist/bollharness/src/scripts/deploy-guard.js +8 -8
  11. package/dist/bollharness-integration/context-router-judgment.js +4 -4
  12. package/dist/bollharness-integration/context-router.js +292 -292
  13. package/dist/bollharness-integration/gate-state-machine.js +13 -26
  14. package/dist/bollharness-integration/integration.js +0 -71
  15. package/dist/bollharness-integration/llm-judgment-engine.js +134 -134
  16. package/dist/bollharness-integration/skill-adapter.js +127 -52
  17. package/dist/bootstrap/context-collector.js +3 -6
  18. package/dist/bootstrap/lifecycle-hooks.js +1 -15
  19. package/dist/cli-entry.js +35 -39
  20. package/dist/constraint-runtime/src/_archive_helper.js +9 -0
  21. package/dist/constraint-runtime/src/agent/coordinator.js +48 -0
  22. package/dist/constraint-runtime/src/agent/index.js +1 -0
  23. package/dist/constraint-runtime/src/assistant/index.js +12 -0
  24. package/dist/constraint-runtime/src/bootstrap/index.js +12 -0
  25. package/dist/constraint-runtime/src/bootstrap_graph.js +13 -0
  26. package/dist/constraint-runtime/src/bridge/index.js +12 -0
  27. package/dist/constraint-runtime/src/buddy/index.js +12 -0
  28. package/dist/constraint-runtime/src/cli/index.js +12 -0
  29. package/dist/constraint-runtime/src/command_graph.js +10 -0
  30. package/dist/constraint-runtime/src/commands.js +60 -0
  31. package/dist/constraint-runtime/src/components/index.js +12 -0
  32. package/dist/constraint-runtime/src/constants/index.js +12 -0
  33. package/dist/constraint-runtime/src/constraint/budget.js +22 -0
  34. package/dist/constraint-runtime/src/constraint/index.js +2 -0
  35. package/dist/constraint-runtime/src/constraint/permission.js +20 -0
  36. package/dist/constraint-runtime/src/context.js +30 -0
  37. package/dist/constraint-runtime/src/coordinator/index.js +12 -0
  38. package/dist/constraint-runtime/src/cost_hook.js +4 -0
  39. package/dist/constraint-runtime/src/cost_tracker.js +8 -0
  40. package/dist/constraint-runtime/src/deferred_init.js +10 -0
  41. package/dist/constraint-runtime/src/direct_modes.js +6 -0
  42. package/dist/constraint-runtime/src/dynamic-tool-loader.js +85 -0
  43. package/dist/constraint-runtime/src/entrypoints/index.js +12 -0
  44. package/dist/constraint-runtime/src/execution_registry.js +44 -0
  45. package/dist/constraint-runtime/src/history.js +9 -0
  46. package/dist/constraint-runtime/src/hooks/index.js +12 -0
  47. package/dist/constraint-runtime/src/index.js +26 -0
  48. package/dist/constraint-runtime/src/ink.js +4 -0
  49. package/dist/constraint-runtime/src/keybindings/index.js +12 -0
  50. package/dist/constraint-runtime/src/memdir/index.js +12 -0
  51. package/dist/constraint-runtime/src/migrations/index.js +12 -0
  52. package/dist/constraint-runtime/src/models.js +1 -0
  53. package/dist/constraint-runtime/src/moreright/index.js +12 -0
  54. package/dist/constraint-runtime/src/native_ts/index.js +12 -0
  55. package/dist/constraint-runtime/src/output_styles/index.js +12 -0
  56. package/dist/constraint-runtime/src/parity_audit.js +12 -0
  57. package/dist/constraint-runtime/src/plugins/index.js +12 -0
  58. package/dist/constraint-runtime/src/port_manifest.js +11 -0
  59. package/dist/constraint-runtime/src/prefetch.js +9 -0
  60. package/dist/constraint-runtime/src/query.js +1 -0
  61. package/dist/constraint-runtime/src/remote/index.js +12 -0
  62. package/dist/constraint-runtime/src/remote_runtime.js +9 -0
  63. package/dist/constraint-runtime/src/runtime/index.js +1 -0
  64. package/dist/constraint-runtime/src/runtime/session.js +35 -0
  65. package/dist/constraint-runtime/src/schemas/index.js +12 -0
  66. package/dist/constraint-runtime/src/screens/index.js +12 -0
  67. package/dist/constraint-runtime/src/server/index.js +12 -0
  68. package/dist/constraint-runtime/src/services/index.js +12 -0
  69. package/dist/constraint-runtime/src/session_store.js +22 -0
  70. package/dist/constraint-runtime/src/setup.js +30 -0
  71. package/dist/constraint-runtime/src/skills/index.js +1 -0
  72. package/dist/constraint-runtime/src/skills/skill-registry.js +28 -0
  73. package/dist/constraint-runtime/src/state/index.js +12 -0
  74. package/dist/constraint-runtime/src/system_init.js +20 -0
  75. package/dist/constraint-runtime/src/thinking/engine.js +42 -0
  76. package/dist/constraint-runtime/src/thinking/index.js +1 -0
  77. package/dist/constraint-runtime/src/tool_pool.js +8 -0
  78. package/dist/constraint-runtime/src/tools/OpenCLI/execAdapter.js +7 -0
  79. package/dist/constraint-runtime/src/tools/OpenCLI/listAdapters.js +7 -0
  80. package/dist/constraint-runtime/src/tools/OpenCLI/runCommand.js +7 -0
  81. package/dist/constraint-runtime/src/tools/SafeSDK/confirmTransaction.js +6 -0
  82. package/dist/constraint-runtime/src/tools/SafeSDK/createTransaction.js +8 -0
  83. package/dist/constraint-runtime/src/tools/SafeSDK/executeTransaction.js +6 -0
  84. package/dist/constraint-runtime/src/tools/SafeSDK/getBalance.js +6 -0
  85. package/dist/constraint-runtime/src/tools/SafeSDK/getPendingTransactions.js +6 -0
  86. package/dist/constraint-runtime/src/tools/SafeSDK/proposeTransaction.js +6 -0
  87. package/dist/constraint-runtime/src/tools.js +80 -0
  88. package/dist/constraint-runtime/src/transcript.js +19 -0
  89. package/dist/constraint-runtime/src/types/index.js +12 -0
  90. package/dist/constraint-runtime/src/upstream_proxy/index.js +12 -0
  91. package/dist/constraint-runtime/src/utils/index.js +12 -0
  92. package/dist/constraint-runtime/src/vim/index.js +12 -0
  93. package/dist/constraint-runtime/src/voice/index.js +12 -0
  94. package/dist/constraint-runtime/tests/agent.test.js +16 -0
  95. package/dist/constraint-runtime/tests/constraint.test.js +41 -0
  96. package/dist/constraint-runtime/tests/skill.test.js +19 -0
  97. package/dist/constraint-runtime/tests/thinking.test.js +22 -0
  98. package/dist/context-compaction/auto-compact.js +7 -7
  99. package/dist/electron-preload.js +1 -18
  100. package/dist/electron-preload.js.map +1 -1
  101. package/dist/electron.js +168 -3
  102. package/dist/electron.js.map +1 -1
  103. package/dist/index.js +116 -136
  104. package/dist/llm/config-store.js +8 -24
  105. package/dist/llm/llm-judgment-client.js +102 -102
  106. package/dist/llm/pi-ai.js +85 -126
  107. package/dist/llm/system-prompt/layers/channel/local.md +14 -14
  108. package/dist/llm/system-prompt/layers/channel/p2p-agent.md +18 -18
  109. package/dist/llm/system-prompt/layers/channel/p2p-visitor.md +19 -19
  110. package/dist/llm/system-prompt/layers/core/artifacts_storage.md +89 -89
  111. package/dist/llm/system-prompt/layers/core/evenhandedness.md +21 -21
  112. package/dist/llm/system-prompt/layers/core/hibs_reminders.md +15 -15
  113. package/dist/llm/system-prompt/layers/core/identity.md +37 -37
  114. package/dist/llm/system-prompt/layers/core/knowledge.md +17 -17
  115. package/dist/llm/system-prompt/layers/core/memory_system.md +12 -12
  116. package/dist/llm/system-prompt/layers/core/network_filesystem.md +28 -28
  117. package/dist/llm/system-prompt/layers/core/refusal.md +37 -37
  118. package/dist/llm/system-prompt/layers/core/tone.md +31 -31
  119. package/dist/llm/system-prompt/layers/core/tools.thin.md +13 -13
  120. package/dist/llm/system-prompt/layers/core/wellbeing.md +41 -41
  121. package/dist/llm/system-prompt/layers/role/architect.md +20 -20
  122. package/dist/llm/system-prompt/layers/role/expert.md +19 -19
  123. package/dist/llm/system-prompt/layers/role/implementer.md +15 -15
  124. package/dist/llm/system-prompt/layers/role/security.md +15 -15
  125. package/dist/llm/system-prompt/layers/tool/artifacts.md +72 -72
  126. package/dist/llm/system-prompt/layers/tool/bash.md +25 -25
  127. package/dist/llm/system-prompt/layers/tool/hibs_api.md +171 -171
  128. package/dist/llm/system-prompt/layers/tool/image_search.md +70 -70
  129. package/dist/llm/system-prompt/layers/tool/manifest.md +89 -89
  130. package/dist/llm/system-prompt/layers/tool/mcp_apps.md +53 -53
  131. package/dist/llm/system-prompt/layers/tool/web_search.md +83 -83
  132. package/dist/llm/tool-manifest/ask_user_input.js +1 -1
  133. package/dist/llm/tool-manifest/bash.js +3 -3
  134. package/dist/llm/tool-manifest/create_file.js +4 -4
  135. package/dist/llm/tool-manifest/fetch_sports_data.js +3 -3
  136. package/dist/llm/tool-manifest/image_search.js +2 -2
  137. package/dist/llm/tool-manifest/mcp.js +2 -2
  138. package/dist/llm/tool-manifest/message_compose.js +3 -3
  139. package/dist/llm/tool-manifest/places.js +2 -2
  140. package/dist/llm/tool-manifest/present_files.js +1 -1
  141. package/dist/llm/tool-manifest/recipe.js +2 -2
  142. package/dist/llm/tool-manifest/str_replace.js +5 -5
  143. package/dist/llm/tool-manifest/view.js +3 -3
  144. package/dist/llm/tool-manifest/weather.js +4 -4
  145. package/dist/llm/tool-manifest/web.js +4 -4
  146. package/dist/pi-ecosystem-colony/index.js +365 -0
  147. package/dist/pi-ecosystem-judgment/causal-judge.js +51 -51
  148. package/dist/pi-ecosystem-judgment/distill-prompt.js +42 -42
  149. package/dist/pi-ecosystem-judgment/distillation.js +14 -14
  150. package/dist/pi-ecosystem-judgment/monitor-gate.js +28 -28
  151. package/dist/pi-ecosystem-judgment/value-injection.js +102 -102
  152. package/dist/security/context-router-tool.js +15 -15
  153. package/dist/security/tool-gate.js +0 -11
  154. package/dist/social/ant-colony/AdaptiveHeartbeat.js +101 -0
  155. package/dist/social/ant-colony/PheromoneEngine.js +227 -0
  156. package/dist/social/ant-colony/index.js +6 -0
  157. package/dist/social/ant-colony/types.js +24 -0
  158. package/dist/test/ai-judgment-test.js +80 -0
  159. package/dist/test/bollharness-integration.test.js +318 -0
  160. package/dist/test/channel-agent-multi-dialogue.js +205 -0
  161. package/dist/test/channel-heartbeat-agent-test.js +201 -0
  162. package/dist/test/constraint-layer.test.js +164 -0
  163. package/dist/test/diap-identity-test.js +172 -0
  164. package/dist/test/diap-quick-test.js +62 -0
  165. package/dist/test/global-shared-context.test.js +315 -0
  166. package/dist/test/harness-judgment-injection.test.js +246 -0
  167. package/dist/test/harness-workflow-integrator-test.js +228 -0
  168. package/dist/test/human-value-store.test.js +243 -0
  169. package/dist/test/hybrid-integration-test.js +118 -0
  170. package/dist/test/hybrid-messenger-verify.js +55 -0
  171. package/dist/test/iroh-bistream-debug.js +38 -0
  172. package/dist/test/iroh-communication.test.js +66 -0
  173. package/dist/test/iroh-debug-test.js +57 -0
  174. package/dist/test/iroh-diap-test.js +71 -0
  175. package/dist/test/iroh-direct-connect.js +55 -0
  176. package/dist/test/iroh-e2e-fixed.js +89 -0
  177. package/dist/test/iroh-e2e-same-process.js +63 -0
  178. package/dist/test/iroh-e2e.js +66 -0
  179. package/dist/test/iroh-final-e2e.js +72 -0
  180. package/dist/test/iroh-relay-test.js +37 -0
  181. package/dist/test/iroh-simple-test.js +41 -0
  182. package/dist/test/iroh-transport-verify.js +54 -0
  183. package/dist/test/iroh-transport.test.js +37 -0
  184. package/dist/test/iroh-two-nodes.js +70 -0
  185. package/dist/test/iroh-verify.js +44 -0
  186. package/dist/test/judgment-decision.test.js +219 -0
  187. package/dist/test/llm-judgment-integration.test.js +220 -0
  188. package/dist/test/p2p-agent-complex-dialogue.js +385 -0
  189. package/dist/test/p2p-agent-dialogue.js +341 -0
  190. package/dist/test/p2p-agent-full-bidirectional.js +510 -0
  191. package/dist/test/p2p-agent-harness-flow.js +437 -0
  192. package/dist/test/p2p-agent-harness-single.js +143 -0
  193. package/dist/test/p2p-ai-dialogue-test.js +318 -0
  194. package/dist/test/p2p-cid-connect-test.js +195 -0
  195. package/dist/test/p2p-connect-receiver.js +69 -0
  196. package/dist/test/p2p-doc-transfer.js +110 -0
  197. package/dist/test/p2p-identity-page-test.js +77 -0
  198. package/dist/test/p2p-iroh-test.js +171 -0
  199. package/dist/test/p2p-minimal-test.js +241 -0
  200. package/dist/test/p2p-node-1.js +148 -0
  201. package/dist/test/p2p-node-2.js +148 -0
  202. package/dist/test/p2p-server.js +281 -0
  203. package/dist/test/p2p-two-nodes-test.js +438 -0
  204. package/dist/test/pi-sdk.test.js +44 -0
  205. package/dist/test/set-persona.js +40 -0
  206. package/dist/test/simple.test.js +9 -0
  207. package/dist/test/storage-integration.test.js +150 -0
  208. package/dist/test/subagent-manager.test.js +276 -0
  209. package/dist/test/test-gate-flow.test.js +81 -0
  210. package/dist/test/workflow-engine.test.js +87 -0
  211. package/dist/test/workflow-pivot-loop.test.js +246 -0
  212. package/dist/web/api-config.html +520 -520
  213. package/dist/web/client.js +2917 -3791
  214. package/dist/web/components/p2p/index.js +234 -276
  215. package/dist/web/components/wallet-viem.mjs +118 -118
  216. package/dist/web/index.html +374 -373
  217. package/dist/web/manifest.json +20 -20
  218. package/dist/web/server.js +23 -775
  219. package/dist/web/style.css +4737 -4737
  220. package/dist/web/ui/message-renderer.js +373 -451
  221. package/dist/web/ui/step-timeline.js +255 -351
  222. package/package.json +160 -160
  223. package/scripts/build-cli.js +215 -215
  224. package/scripts/build-web.ts +125 -125
  225. package/scripts/postinstall.js +152 -152
  226. package/bin/bolloon-daemon.sh +0 -207
  227. package/bin/ipfs +0 -0
  228. package/dist/agents/chat-segmenter.js +0 -298
  229. package/dist/agents/judgment-protocol.js +0 -479
  230. package/dist/agents/parse-tool-call.js +0 -304
  231. package/dist/agents/peer-manifest-loader.js +0 -210
  232. package/dist/agents/react-loop.js +0 -120
  233. package/dist/agents/session-store.js +0 -171
  234. package/dist/agents/tool-registry.js +0 -136
  235. package/dist/bootstrap/chat-archiver.js +0 -276
  236. package/dist/bootstrap/memory-compressor.js +0 -170
  237. package/dist/bootstrap/persona-loader.js +0 -94
  238. package/dist/electron/config.js +0 -16
  239. package/dist/electron/config.js.map +0 -1
  240. package/dist/electron/dialogs.js +0 -71
  241. package/dist/electron/dialogs.js.map +0 -1
  242. package/dist/electron/first-run.js +0 -129
  243. package/dist/electron/first-run.js.map +0 -1
  244. package/dist/electron/ipc.js +0 -16
  245. package/dist/electron/ipc.js.map +0 -1
  246. package/dist/electron/logger.js +0 -77
  247. package/dist/electron/logger.js.map +0 -1
  248. package/dist/electron/main.js +0 -60
  249. package/dist/electron/main.js.map +0 -1
  250. package/dist/electron/menu.js +0 -140
  251. package/dist/electron/menu.js.map +0 -1
  252. package/dist/electron/paths.js +0 -33
  253. package/dist/electron/paths.js.map +0 -1
  254. package/dist/electron/server.js +0 -74
  255. package/dist/electron/server.js.map +0 -1
  256. package/dist/electron/tray.js +0 -73
  257. package/dist/electron/tray.js.map +0 -1
  258. package/dist/electron/window.js +0 -69
  259. package/dist/electron/window.js.map +0 -1
  260. package/dist/network/local-inbox-bus.js +0 -73
  261. package/dist/network/p2p-outbox.js +0 -161
  262. package/dist/network/peer-fs.js +0 -420
  263. package/dist/network/peer-resource-bridge.js +0 -216
  264. package/dist/web/components/p2p/P2PModal.js +0 -188
  265. package/dist/web/components/p2p/p2p-modal.js +0 -664
  266. package/dist/web/components/p2p/p2p-tools.js +0 -248
  267. package/dist/web/input-validator.js +0 -103
  268. package/dist/web/util/safe-name.js +0 -32
@@ -6,33 +6,6 @@ import * as fs from 'fs/promises';
6
6
  import * as fsSync from 'fs';
7
7
  import * as path from 'path';
8
8
  import * as os from 'os';
9
- import { validateMessageInput, validateChannelInput, healthCheck, } from './input-validator.js';
10
- import { segmentChatReply } from '../agents/chat-segmenter.js';
11
- // 读自身 package.json 拿 version (health endpoint 用)
12
- // 路径: src/web/server.ts → ../../package.json (编译后 dist/web/server.js)
13
- let cachedVersion = null;
14
- function getPackageVersion() {
15
- if (cachedVersion)
16
- return cachedVersion;
17
- try {
18
- const here = fileURLToPath(import.meta.url);
19
- const hereDir = path.dirname(here);
20
- // 试 ../package.json (相对 dist/web/) 或 ../../package.json (相对 src/web/)
21
- let raw = null;
22
- for (const rel of ['../package.json', '../../package.json']) {
23
- try {
24
- raw = fsSync.readFileSync(path.join(hereDir, rel), 'utf-8');
25
- break;
26
- }
27
- catch { }
28
- }
29
- cachedVersion = raw ? (JSON.parse(raw).version ?? '0.0.0') : 'unknown';
30
- }
31
- catch {
32
- cachedVersion = 'unknown';
33
- }
34
- return cachedVersion;
35
- }
36
9
  // 2026-06-17: 终端静默 — server.ts 高频 spam (LLM 流式每个 token 一次 broadcast,
37
10
  // 每次 P2P 收发 [v3]/[v3-meta]/[v3-cross]/[v3-friend] 各一次) 全部走 console.log proxy,
38
11
  // 默认 (BOLLOON_VERBOSE != '1') 完全不打;VERBOSE=1 时恢复.
@@ -74,9 +47,6 @@ import { irohTransport } from '../network/iroh-transport.js';
74
47
  import { createAgentDelegateApp } from './agent-delegate-server.js';
75
48
  import { createIrohDelegateTransport } from './iroh-delegate-transport.js';
76
49
  import { verifyMessage, isAddress, getAddress } from 'viem';
77
- // 2026-07-05: peer 目录管理 + manifest 协议
78
- import * as peerFs from '../network/peer-fs.js';
79
- import { loadLocalResources, writeRemoteResources } from '../network/peer-resource-bridge.js';
80
50
  // 前端资源路径: 兼容 src 运行 + dist 运行 + npm 全局安装
81
51
  // - src 跑 (tsx): __dirname = .../src/web → .../dist/web
82
52
  // - dist 跑 (npm): __dirname = .../dist/web → 自身就是 web 根
@@ -288,29 +258,6 @@ let sseClients = new Set();
288
258
  // v3: 远端 channel UI 元数据缓存 — key: peerId, value: sanitize 过的 channel 列表
289
259
  // in-memory only, 进程重启清空 (judgment 内容永远不在这里)
290
260
  let remoteChannelCache = new Map();
291
- // 2026-07-05: 一次性 prompt 附加块 — key: channelId, value: 下一次 LLM prompt 时 prepend 的内容
292
- // 用于 manifest-loader 加载对方能力后, 仅影响本次对话, 不污染主 prompt
293
- const nextPromptHints = new Map();
294
- // 2026-07-05: 直接读 ~/.bolloon/agents/agents.json, 跳过 subagent-manager 的 lazy init
295
- // 原因: getSubAgentManager().getAllAgents() 只在 SubAgentManager.initialize() 后才加载,
296
- // 但没人调 initialize(), 永远返回 []. 这里用 raw 文件 IO 直接拿数据.
297
- async function loadLocalSubAgents() {
298
- try {
299
- const fsPromises = await import('fs/promises');
300
- const path = await import('path');
301
- const home = process.env.BOLLOON_HOME || process.env.HOME || '/tmp';
302
- const file = path.join(home, '.bolloon', 'agents', 'agents.json');
303
- const raw = await fsPromises.readFile(file, 'utf-8');
304
- const parsed = JSON.parse(raw);
305
- const arr = Array.isArray(parsed) ? parsed : [];
306
- process.stderr.write(`[LOADED] ${arr.length} agents from ${file}\n`);
307
- return arr;
308
- }
309
- catch (e) {
310
- process.stderr.write(`[LOAD FAIL] ${e?.message}\n`);
311
- return [];
312
- }
313
- }
314
261
  // 2026-06-10: 持久化 remote channel cache 到 ~/.bolloon/remote-channels-cache.json
315
262
  // 之前是纯内存 Map, nodeA 重启后所有对端 channel 列表丢失, 需要等对面再推一次
316
263
  const REMOTE_CACHE_FILE = `${process.env.HOME || '/tmp'}/.bolloon/remote-channels-cache.json`;
@@ -480,25 +427,22 @@ async function routeMentionsInReply(originChannelId, replyText, localChannels, r
480
427
  continue;
481
428
  }
482
429
  try {
483
- const rpcPayload = {
484
- targetChannelId: remoteTarget.id,
485
- targetChannelName: remoteTarget.name,
486
- originChannelId,
487
- originChannelName,
488
- text,
489
- fromPublicKey: v3P2PRef.getPublicKey()
490
- };
491
- // 2026-07-05: 用 outbox.sendOrQueue 兜底 — 对方不在线时自动入队, 上线后批量重发
492
- const { sendOrQueue } = await import('../network/p2p-outbox.js');
493
- const r = await sendOrQueue(ownerPk, 'agent.cross.post', rpcPayload, v3P2PRef);
494
- if (r === 'SENT') {
430
+ const rpc = JSON.stringify({
431
+ v: 3, op: 'agent.cross.post',
432
+ payload: {
433
+ targetChannelId: remoteTarget.id,
434
+ targetChannelName: remoteTarget.name,
435
+ originChannelId,
436
+ originChannelName,
437
+ text,
438
+ fromPublicKey: v3P2PRef.getPublicKey()
439
+ }
440
+ });
441
+ const ok = v3P2PRef.sendTo(ownerPk, rpc);
442
+ if (ok) {
495
443
  console.log(`[v3-cross] (${originChannelName}) @${targetName} → 远端 peer ${ownerPk.substring(0, 12)}... (channelId=${remoteTarget.id})`);
496
444
  results.push({ targetName, targetId: remoteTarget.id, source: 'remote', text, status: 'sent' });
497
445
  }
498
- else if (r === 'QUEUED') {
499
- console.log(`[v3-cross] (${originChannelName}) @${targetName} → 远端 peer ${ownerPk.substring(0, 12)}... 已入队 (对方不在线)`);
500
- results.push({ targetName, targetId: remoteTarget.id, source: 'remote', text, status: 'queued' });
501
- }
502
446
  else {
503
447
  results.push({ targetName, targetId: remoteTarget.id, source: 'remote', text, status: 'failed' });
504
448
  }
@@ -634,28 +578,6 @@ async function handleV3P2PMessage(parsed, conn, comm) {
634
578
  catch (saveErr) {
635
579
  console.warn(`[v3] 存 user 消息失败 (不影响 chat):`, saveErr.message);
636
580
  }
637
- // 2026-07-05: 同时追加到 sender 的 peer 月度归档 — 让 A 本地的"与 sender 对话历史"也能离线查看
638
- try {
639
- const { appendChatArchive } = await import('../bootstrap/chat-archiver.js');
640
- const chanObj = (await loadChannels()).find(c => c.id === channelId);
641
- await appendChatArchive({
642
- publicKey: senderKey,
643
- entry: {
644
- ts: new Date().toISOString(),
645
- source: 'remote',
646
- channelId, channelName: chanObj?.name,
647
- text,
648
- fromPublicKey: senderKey,
649
- msgType: 'user',
650
- }
651
- });
652
- // 顺手把 sender 加入 known peers (如果没有)
653
- const { addOrUpdatePeer } = await import('../network/known-peers.js');
654
- await addOrUpdatePeer(undefined, senderKey);
655
- }
656
- catch (archiveErr) {
657
- console.warn('[chat-archive] remote user 归档失败 (non-fatal):', archiveErr?.message?.slice(0, 200));
658
- }
659
581
  // v3 修复: 同步给 A 自己的 UI — broadcast SSE 事件让 A 的 owner 实时看到 B 的消息
660
582
  broadcast({
661
583
  type: 'user',
@@ -756,25 +678,6 @@ async function handleV3P2PMessage(parsed, conn, comm) {
756
678
  catch (saveErr) {
757
679
  console.warn(`[v3] 存 assistant 消息失败 (不影响):`, saveErr.message);
758
680
  }
759
- // 2026-07-05: A 的 assistant 回复也归档到 sender 的月度 — 这样 sender 本地也有一份"与 A 的对话"
760
- try {
761
- const { appendChatArchive } = await import('../bootstrap/chat-archiver.js');
762
- const chanObj = (await loadChannels()).find(c => c.id === channelId);
763
- await appendChatArchive({
764
- publicKey: senderKey,
765
- entry: {
766
- ts: new Date().toISOString(),
767
- source: 'ai-mention-remote',
768
- channelId, channelName: chanObj?.name,
769
- text: `[${v3P2PRef?.getPublicKey()?.slice(0, 12)}…] ${fullResponse.slice(0, 1500)}`,
770
- fromPublicKey: v3P2PRef?.getPublicKey(),
771
- msgType: 'ai',
772
- }
773
- });
774
- }
775
- catch (archiveErr) {
776
- console.warn('[chat-archive] remote ai 归档失败 (non-fatal):', archiveErr?.message?.slice(0, 200));
777
- }
778
681
  // v3 修复: 同步给 A 自己的 UI — broadcast AI 回复给 A 的 owner 实时看到
779
682
  broadcast({
780
683
  type: 'ai',
@@ -943,26 +846,6 @@ async function handleV3P2PMessage(parsed, conn, comm) {
943
846
  session.lastUpdated = new Date().toISOString();
944
847
  await saveSession(session);
945
848
  console.log(`[v3-cross] 收到远端 @-mention: ${originChannelName} → 本地 ${targetChannelName} (${text.length} chars)`);
946
- // 2026-07-05: 跨渠道 @-mention 也归档到 fromPublicKey 的月度 — 本节点 owner 看月度历史能还原跨节点对话
947
- if (fromPublicKey) {
948
- try {
949
- const { appendChatArchive } = await import('../bootstrap/chat-archiver.js');
950
- await appendChatArchive({
951
- publicKey: fromPublicKey,
952
- entry: {
953
- ts: new Date().toISOString(),
954
- source: 'ai-mention-remote',
955
- channelId: targetChannelId, channelName: targetChannelName,
956
- text: `[跨渠道] from ${originChannelName}: ${text.slice(0, 1500)}`,
957
- fromPublicKey,
958
- msgType: 'ai',
959
- }
960
- });
961
- }
962
- catch (archiveErr) {
963
- console.warn('[chat-archive] cross.post 归档失败 (non-fatal):', archiveErr?.message?.slice(0, 200));
964
- }
965
- }
966
849
  // 推 SSE 让本地 UI 知道有跨渠道消息到达
967
850
  broadcast({
968
851
  type: 'cross-mention-received',
@@ -977,201 +860,6 @@ async function handleV3P2PMessage(parsed, conn, comm) {
977
860
  }
978
861
  return;
979
862
  }
980
- // ============== 2026-07-05: agent.manifest.exchange ==============
981
- // 对方问: "给我你的 agent 清单 + capabilities"
982
- // 我们回: agent.manifest.exchange.reply, 含本地 SubAgent + persona 描述
983
- if (op === 'agent.manifest.exchange') {
984
- try {
985
- // 直接读 agents.json, 跳过 subagent-manager 的 lazy initialize
986
- const localAgents = await loadLocalSubAgents();
987
- // 转成 AgentManifestEntry
988
- const entries = localAgents.map((a) => ({
989
- id: a.id,
990
- name: a.name,
991
- capabilities: a.capabilities || [],
992
- status: a.status || 'active',
993
- peerId: a.peerId,
994
- irohNodeId: a.irohNodeId,
995
- sessionId: a.sessionId,
996
- cid: a.cid,
997
- ipnsName: a.ipnsName,
998
- }));
999
- // 读本地 persona 拿 owner 名字 + 简介
1000
- let ownerName = '';
1001
- let ownerDescription = '';
1002
- try {
1003
- const { readFileSync, existsSync } = await import('fs');
1004
- const p = path.join(process.env.HOME || '/tmp', '.bolloon', 'persona.json');
1005
- if (existsSync(p)) {
1006
- const pj = JSON.parse(readFileSync(p, 'utf-8'));
1007
- ownerName = pj.name || '';
1008
- ownerDescription = pj.description || '';
1009
- }
1010
- }
1011
- catch { }
1012
- const manifest = {
1013
- ownerName,
1014
- ownerDescription,
1015
- ownerPublicKey: v3P2PRef?.getPublicKey() || '',
1016
- agents: entries,
1017
- publishedAt: Date.now(),
1018
- ...(await loadLocalResources()),
1019
- };
1020
- const reply = JSON.stringify({
1021
- v: 3, op: 'agent.manifest.exchange.reply',
1022
- payload: {
1023
- manifest,
1024
- since: parsed.payload?.since || 0,
1025
- }
1026
- });
1027
- await comm.sendToConnection(conn.id, reply);
1028
- // 2026-07-05: P2PDirect.sendToWithWait 双发 (兼容同机 P2PDirect 互不相连场景)
1029
- if (v3P2PRef && peerKey) {
1030
- try {
1031
- const r = await v3P2PRef.sendToWithWait(peerKey, reply, 2000);
1032
- console.log(`[v3-manifest] 回 ${peerKey.substring(0, 12)}... manifest (comm=${await comm.sendToConnection.length}, p2p=${r}, ${entries.length} agents)`);
1033
- }
1034
- catch { }
1035
- }
1036
- else {
1037
- console.log(`[v3-manifest] 回 ${peerKey.substring(0, 12)}... manifest (${entries.length} agents, owner=${ownerName || '?'})`);
1038
- }
1039
- }
1040
- catch (err) {
1041
- console.error('[v3-manifest] 处理 agent.manifest.exchange 失败:', err.message);
1042
- }
1043
- return;
1044
- }
1045
- // 对方推 manifest 过来 (我们这边是接收方)
1046
- if (op === 'agent.manifest.exchange.reply') {
1047
- try {
1048
- const manifest = parsed.payload?.manifest;
1049
- if (!manifest || !manifest.ownerPublicKey) {
1050
- console.warn('[v3-manifest] manifest.exchange.reply 缺 manifest/ownerPublicKey');
1051
- return;
1052
- }
1053
- // 2026-07-05 修复: 跳过自己的 manifest (Hyperswarm 内部 peer discovery 会推送自己)
1054
- const peerKey2 = manifest.ownerPublicKey;
1055
- if (v3P2PRef && peerKey2 === v3P2PRef.getPublicKey()) {
1056
- return;
1057
- }
1058
- // 增量: 如果 since >= 本地最新 ts, 跳过
1059
- const existing = await peerFs.readPeerIndex(peerKey2);
1060
- if (existing?.manifestTs && existing.manifestTs >= manifest.publishedAt && parsed.payload?.since) {
1061
- console.log(`[v3-manifest] ${peerKey2.substring(0, 12)}... manifest 已是最新 (ts=${manifest.publishedAt}), 跳过`);
1062
- return;
1063
- }
1064
- // 写 peer.json + _index.json + 每个 agent 的 md
1065
- await peerFs.upsertPeer({
1066
- publicKey: peerKey2,
1067
- name: manifest.ownerName,
1068
- lastSeenAt: new Date().toISOString(),
1069
- lastManifestTs: manifest.publishedAt,
1070
- manifestCount: (await peerFs.getPeer(peerKey2))?.manifestCount ?? 0 + 1,
1071
- });
1072
- // 拼 _index.json
1073
- const ownerDescription = manifest.ownerDescription || '';
1074
- const idx = {
1075
- version: 1,
1076
- publicKey: peerKey2,
1077
- ownerName: manifest.ownerName,
1078
- ownerDescription,
1079
- agents: manifest.agents,
1080
- groups: manifest.groups,
1081
- functions: manifest.functions,
1082
- exportments: manifest.exportments,
1083
- updatedAt: new Date().toISOString(),
1084
- manifestTs: manifest.publishedAt,
1085
- };
1086
- await peerFs.writePeerIndex(peerKey2, idx);
1087
- // 每个 agent 写一份 markdown
1088
- for (const a of manifest.agents) {
1089
- await peerFs.writeAgentDescription(peerKey2, a, ownerDescription);
1090
- }
1091
- // 2026-07-05: 4 类资源 (groups/function/exportment/science) 也落盘
1092
- const counts = await writeRemoteResources(peerKey2, manifest);
1093
- // capability-index.md (≤500 字, 进 prompt)
1094
- await peerFs.writeCapabilityIndex(peerKey2, idx);
1095
- console.log(`[v3-manifest] 收到 ${peerKey2.substring(0, 12)}... manifest (${manifest.agents.length} agents, owner=${manifest.ownerName || '?'}, +g${counts.groups}/f${counts.functions}/e${counts.exportments}/s${counts.sciences}) → 落盘`);
1096
- // 推 SSE 让前端知道"对方能力刷新了"
1097
- broadcast({
1098
- type: 'peer-manifest-updated',
1099
- fromPublicKey: peerKey2,
1100
- ownerName: manifest.ownerName,
1101
- agentCount: manifest.agents.length,
1102
- capabilityIndex: await peerFs.readCapabilityIndex(peerKey2),
1103
- }, 'p2p-global');
1104
- }
1105
- catch (err) {
1106
- console.error('[v3-manifest] 处理 manifest.exchange.reply 失败:', err.message);
1107
- }
1108
- return;
1109
- }
1110
- // 对方问: "给我某个 agent 的详细描述" (markdown 全文)
1111
- if (op === 'agent.resource.get') {
1112
- try {
1113
- const agentId = parsed.payload?.agentId;
1114
- if (!agentId)
1115
- return;
1116
- // 2026-07-05: 4 类资源 (group/function/exportment/science) 也走同一个 RPC
1117
- // 识别方式: id 前缀 group:/fn:/game:/exp: → 读 ~/.bolloon/local-resources/<cat>/<id>.md
1118
- let body = '';
1119
- const prefixMatch = String(agentId).match(/^(group|fn|game|exp):(.+)$/);
1120
- if (prefixMatch) {
1121
- const cat = prefixMatch[1] === 'fn' ? 'functions'
1122
- : prefixMatch[1] === 'game' ? 'exportments'
1123
- : prefixMatch[1] === 'exp' ? 'sciences'
1124
- : 'groups';
1125
- try {
1126
- const fsPromises = await import('fs/promises');
1127
- const safe = String(prefixMatch[2]).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
1128
- const file = path.join(process.env.HOME || '/tmp', '.bolloon', 'local-resources', cat, `${safe}.md`);
1129
- body = await fsPromises.readFile(file, 'utf-8');
1130
- }
1131
- catch {
1132
- body = '';
1133
- }
1134
- const reply = JSON.stringify({
1135
- v: 3, op: 'agent.resource.get.reply',
1136
- payload: { agentId, body: body || '(空)', fromPublicKey: v3P2PRef?.getPublicKey() || '' }
1137
- });
1138
- await comm.sendToConnection(conn.id, reply);
1139
- return;
1140
- }
1141
- // 默认: 读本地 persona/<agentId>/agent.md (6 段 persona 之一)
1142
- try {
1143
- const fsPromises = await import('fs/promises');
1144
- const safeId = agentId.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
1145
- const file = path.join(process.env.HOME || '/tmp', '.bolloon', 'persona', safeId, 'agent.md');
1146
- body = await fsPromises.readFile(file, 'utf-8');
1147
- }
1148
- catch {
1149
- body = '';
1150
- }
1151
- // fallback: 用 subagent-manager 的 description
1152
- if (!body) {
1153
- const all = await loadLocalSubAgents();
1154
- const a = all.find((x) => x.id === agentId);
1155
- if (a?.persona?.description)
1156
- body = a.persona.description;
1157
- }
1158
- const reply = JSON.stringify({
1159
- v: 3, op: 'agent.resource.get.reply',
1160
- payload: { agentId, body: body || '(空)', fromPublicKey: v3P2PRef?.getPublicKey() || '' }
1161
- });
1162
- await comm.sendToConnection(conn.id, reply);
1163
- }
1164
- catch (err) {
1165
- console.error('[v3-manifest] resource.get 失败:', err.message);
1166
- }
1167
- return;
1168
- }
1169
- // 对方回某 agent 的详细描述
1170
- if (op === 'agent.resource.get.reply') {
1171
- // 由 caller 端通过 rpcId 解析 (这里只 log)
1172
- console.log(`[v3-manifest] 收到 agent.resource.get.reply (agentId=${parsed.payload?.agentId}, body=${(parsed.payload?.body || '').length} chars)`);
1173
- return;
1174
- }
1175
863
  console.log(`[v3] 收到未知 op: ${op}`);
1176
864
  }
1177
865
  async function buildJudgmentHint(channel, channelIdForLog) {
@@ -1293,8 +981,6 @@ async function getAgentForChannel(channelId, channelDid, channelName, channelDid
1293
981
  cwd: process.cwd(),
1294
982
  peerId: `channel-${channelId}:${currentSessionId}`,
1295
983
  identityDoc,
1296
- // 2026-07-04: 透传 agentId 让 onSessionStart 加载 persona docs
1297
- agentId: channel?.agentId,
1298
984
  // M2.3 (2026-06-17): 构造时从 session JSON 回灌历史 — 服务重启后 LLM 仍记得前面对话
1299
985
  // key 跟 server.ts:240 写入路径保持一致: ~/.bolloon/sessions/cache/<key>.json
1300
986
  loadSessionKey: sessionKey,
@@ -1542,66 +1228,6 @@ export async function createWebServer(port = 3000, options = {}) {
1542
1228
  }, 'p2p-global');
1543
1229
  return;
1544
1230
  }
1545
- // 2026-07-05: v3 P2PDirect 主路径也要独立处理 manifest.exchange.reply — 否则只走老通道就拿不到
1546
- if (parsed.op === 'agent.manifest.exchange.reply') {
1547
- const manifest = parsed.payload?.manifest;
1548
- if (!manifest || !manifest.ownerPublicKey) {
1549
- console.warn('[v3-manifest] manifest.exchange.reply 缺 manifest/ownerPublicKey');
1550
- return;
1551
- }
1552
- const peerKey2 = manifest.ownerPublicKey;
1553
- // 2026-07-05 修复: 跳过自己的 manifest
1554
- if (v3P2PRef && peerKey2 === v3P2PRef.getPublicKey()) {
1555
- return;
1556
- }
1557
- // 异步落盘, 不阻塞 data handler
1558
- (async () => {
1559
- try {
1560
- const existing = await peerFs.readPeerIndex(peerKey2);
1561
- if (existing?.manifestTs && existing.manifestTs >= manifest.publishedAt && parsed.payload?.since) {
1562
- console.log(`[v3-manifest] ${peerKey2.substring(0, 12)}... manifest 已是最新, 跳过`);
1563
- return;
1564
- }
1565
- await peerFs.upsertPeer({
1566
- publicKey: peerKey2,
1567
- name: manifest.ownerName,
1568
- lastSeenAt: new Date().toISOString(),
1569
- lastManifestTs: manifest.publishedAt,
1570
- });
1571
- const ownerDescription = manifest.ownerDescription || '';
1572
- const idx = {
1573
- version: 1,
1574
- publicKey: peerKey2,
1575
- ownerName: manifest.ownerName,
1576
- ownerDescription,
1577
- agents: manifest.agents,
1578
- groups: manifest.groups,
1579
- functions: manifest.functions,
1580
- exportments: manifest.exportments,
1581
- updatedAt: new Date().toISOString(),
1582
- manifestTs: manifest.publishedAt,
1583
- };
1584
- await peerFs.writePeerIndex(peerKey2, idx);
1585
- for (const a of manifest.agents) {
1586
- await peerFs.writeAgentDescription(peerKey2, a, ownerDescription);
1587
- }
1588
- const counts = await writeRemoteResources(peerKey2, manifest);
1589
- await peerFs.writeCapabilityIndex(peerKey2, idx);
1590
- console.log(`[v3-manifest] (P2PDirect) 收到 ${peerKey2.substring(0, 12)}... manifest (${manifest.agents.length} agents, owner=${manifest.ownerName || '?'}, +g${counts.groups}/f${counts.functions}/e${counts.exportments}/s${counts.sciences}) → 落盘`);
1591
- broadcast({
1592
- type: 'peer-manifest-updated',
1593
- fromPublicKey: peerKey2,
1594
- ownerName: manifest.ownerName,
1595
- agentCount: manifest.agents.length,
1596
- capabilityIndex: await peerFs.readCapabilityIndex(peerKey2),
1597
- }, 'p2p-global');
1598
- }
1599
- catch (err) {
1600
- console.error('[v3-manifest] (P2PDirect) manifest.exchange.reply 失败:', err.message);
1601
- }
1602
- })();
1603
- return;
1604
- }
1605
1231
  // 2026-06-10: 收到对方请求本机的 channel 列表 (启动时主动发请求, 加速 cache 填充)
1606
1232
  if (parsed.op === 'agent.meta.list.request') {
1607
1233
  console.log(`[v3-meta] 收到 ${evt.fromPublicKey.substring(0, 12)}... 的 channel 列表请求 → 立刻回包`);
@@ -1643,47 +1269,6 @@ export async function createWebServer(port = 3000, options = {}) {
1643
1269
  console.error('[v3] 新连接发 list.reply 失败:', err.message);
1644
1270
  }
1645
1271
  }, 500);
1646
- // 2026-07-05: 新连接到来立刻推自己的 manifest — 让对方秒知道我有哪些 agent
1647
- setTimeout(async () => {
1648
- try {
1649
- // 跳过自己的连接 (Hyperswarm 内部 peer discovery 会推自己过来, 形成循环)
1650
- if (v3P2PRef && evt.remotePublicKey === v3P2PRef.getPublicKey()) {
1651
- return;
1652
- }
1653
- const localAgents = await loadLocalSubAgents();
1654
- const entries = localAgents.map((a) => ({
1655
- id: a.id, name: a.name,
1656
- capabilities: a.capabilities || [],
1657
- status: a.status || 'active',
1658
- peerId: a.peerId, irohNodeId: a.irohNodeId,
1659
- sessionId: a.sessionId, cid: a.cid, ipnsName: a.ipnsName,
1660
- }));
1661
- let ownerName = '';
1662
- try {
1663
- const fsPromises = await import('fs/promises');
1664
- const p = (process.env.HOME || '/tmp') + '/.bolloon/persona.json';
1665
- const raw = await fsPromises.readFile(p, 'utf-8');
1666
- ownerName = JSON.parse(raw).name || '';
1667
- }
1668
- catch { }
1669
- const manifest = {
1670
- ownerName,
1671
- ownerPublicKey: v3P2PRef?.getPublicKey() || '',
1672
- agents: entries,
1673
- publishedAt: Date.now(),
1674
- ...(await loadLocalResources()),
1675
- };
1676
- const msg = JSON.stringify({
1677
- v: 3, op: 'agent.manifest.exchange.reply',
1678
- payload: { manifest, since: 0 }
1679
- });
1680
- const r = await v3P2PRef.sendToWithWait(evt.remotePublicKey, msg, 3000);
1681
- console.log(`[v3-manifest] 新连接 ${evt.remotePublicKey.substring(0, 12)}... → 主动推 manifest (${entries.length} agents, p2p=${r})`);
1682
- }
1683
- catch (err) {
1684
- console.error('[v3-manifest] 新连接推 manifest 失败:', err.message);
1685
- }
1686
- }, 800);
1687
1272
  });
1688
1273
  console.log(`[v3] P2PDirect 已启动, role=${v3P2PRef.getRole()}, publicKey=${v3P2PRef.getPublicKey().substring(0, 12)}...`);
1689
1274
  // v3: 启动后自动重连 known peers — 让"启动就互联"成为现实
@@ -1713,10 +1298,6 @@ export async function createWebServer(port = 3000, options = {}) {
1713
1298
  // 2026-06-10: 同时主动请求每个 known peer 把 ta 的 channel 列表推过来
1714
1299
  // 避免对面 publicKey 没变但 cache 丢了(本机重启) → 一直空
1715
1300
  setTimeout(() => requestChannelsFromAllPeers(), 3500);
1716
- // 2026-07-05: 拉每个 known peer 的 manifest (agent 能力清单)
1717
- setTimeout(() => requestManifestsFromAllPeers(), 4500);
1718
- // 2026-07-05: 对方刚连上来, 把本地的 outbox 重发出去
1719
- setTimeout(() => flushAllOutboxes(), 6000);
1720
1301
  }
1721
1302
  catch (err) {
1722
1303
  console.error('[v3] 自动重连失败:', err.message);
@@ -1749,78 +1330,6 @@ export async function createWebServer(port = 3000, options = {}) {
1749
1330
  // 立即跑一次 + 每 30s 兜底 (跟 v3BroadcastOwn 一样的节奏)
1750
1331
  setTimeout(requestChannelsFromAllPeers, 4000);
1751
1332
  setInterval(requestChannelsFromAllPeers, 30000);
1752
- // 2026-07-05 新增: 主动向所有 known peer 发起 manifest 拉取
1753
- async function requestManifestsFromAllPeers() {
1754
- if (!v3P2PRef)
1755
- return;
1756
- try {
1757
- const { listPeers } = await import('../network/known-peers.js');
1758
- const peers = await listPeers();
1759
- const myPk = v3P2PRef.getPublicKey();
1760
- let sent = 0;
1761
- for (const peer of peers) {
1762
- if (peer.publicKey === myPk)
1763
- continue;
1764
- // 增量: since = 本地已有的 lastManifestTs, 对端 manifestTs <= since 则跳过
1765
- const peerRec = await peerFs.getPeer(peer.publicKey);
1766
- const since = peerRec?.lastManifestTs || 0;
1767
- const req = JSON.stringify({
1768
- v: 3, op: 'agent.manifest.exchange',
1769
- payload: { since, fromPublicKey: myPk }
1770
- });
1771
- const r = await v3P2PRef.sendToWithWait(peer.publicKey, req, 3000);
1772
- if (r === 'SENT')
1773
- sent++;
1774
- }
1775
- console.log(`[v3-manifest] requestManifestsFromAllPeers → sent=${sent}/${peers.length - 1}`);
1776
- }
1777
- catch (err) {
1778
- console.warn('[v3-manifest] requestManifestsFromAllPeers failed:', err.message);
1779
- }
1780
- }
1781
- // 每 5 分钟兜底 (manifest 增量拉取)
1782
- setInterval(requestManifestsFromAllPeers, 5 * 60 * 1000);
1783
- // 2026-07-05 新增: 把每个 peer 的 outbox 队列重发出去 (对方上线后)
1784
- async function flushAllOutboxes() {
1785
- if (!v3P2PRef)
1786
- return;
1787
- try {
1788
- const { listPeers } = await import('../network/known-peers.js');
1789
- const peers = await listPeers();
1790
- const myPk = v3P2PRef.getPublicKey();
1791
- let totalFlushed = 0;
1792
- for (const peer of peers) {
1793
- if (peer.publicKey === myPk)
1794
- continue;
1795
- const outbox = await peerFs.readOutbox(peer.publicKey);
1796
- if (outbox.length === 0)
1797
- continue;
1798
- let sent = 0;
1799
- for (const entry of outbox) {
1800
- const rpc = JSON.stringify({ v: 3, op: entry.op, payload: entry.payload });
1801
- const r = await v3P2PRef.sendToWithWait(peer.publicKey, rpc, 3000);
1802
- if (r === 'SENT')
1803
- sent++;
1804
- }
1805
- if (sent > 0) {
1806
- // 全部成功 → 清空; 部分成功 → 写回剩余 (简单起见, 全成功才清)
1807
- if (sent === outbox.length) {
1808
- await peerFs.clearOutbox(peer.publicKey);
1809
- totalFlushed += sent;
1810
- console.log(`[v3-outbox] flush → ${peer.publicKey.substring(0, 12)}... 重发 ${sent} 条 ✓`);
1811
- }
1812
- }
1813
- }
1814
- if (totalFlushed > 0) {
1815
- console.log(`[v3-outbox] 累计重发 ${totalFlushed} 条离线消息`);
1816
- }
1817
- }
1818
- catch (err) {
1819
- console.warn('[v3-outbox] flushAllOutboxes failed:', err.message);
1820
- }
1821
- }
1822
- // 每 2 分钟兜底 flush
1823
- setInterval(flushAllOutboxes, 2 * 60 * 1000);
1824
1333
  }
1825
1334
  catch (err) {
1826
1335
  console.error('[v3] P2PDirect 启动失败:', err.message);
@@ -1831,9 +1340,6 @@ export async function createWebServer(port = 3000, options = {}) {
1831
1340
  // v3 修复: 用 setInterval 替代一次性 setTimeout, 确保分享变更后能持续推送给 peer
1832
1341
  setInterval(v3BroadcastOwn, 30000);
1833
1342
  // 保留 @diap/sdk 的旧实例 (它的 Hyperswarm 实例能帮 P2PDirect 做 DHT bootstrap)
1834
- // 2026-07-04: hyperswarm 4.x 删除了 Discovery.update() (被 .flushed() 替代).
1835
- // @diap/sdk 0.1.10 还调 .update(), 是上游 bug (已记录 docs/plans/2026-06-17-supervisor-iter-1.md).
1836
- // 这里静默 joinTopic 失败, P2PDirect (v3 主路径) 不依赖 @diap/sdk, 不影响功能.
1837
1343
  try {
1838
1344
  const rawSeed = crypto.getRandomValues(new Uint8Array(32));
1839
1345
  p2pCommunicator = createHyperswarmCommunicator({
@@ -1847,38 +1353,13 @@ export async function createWebServer(port = 3000, options = {}) {
1847
1353
  // 旧 p2p_message 路径 (非 v3)
1848
1354
  const content = new TextDecoder().decode(msg.content);
1849
1355
  broadcast({ type: 'p2p_message', from: conn.publicKey.substring(0, 8), content }, undefined);
1850
- // 2026-07-05: 老通道也要走 v3 RPC handler — 兼容同机/跨机 P2PDirect 互不相连的场景
1851
- // 实测: P2PDirect 用 hyperswarm 4.x, 同机 2 role 互不相连; 但 @diap/sdk 0.1.10 的 HyperswarmCommunicator 能互通
1852
- // 所以 manifest/exchange/chat.send 都可能从老通道来, 必须进 handleV3P2PMessage
1853
- try {
1854
- const parsed = JSON.parse(content);
1855
- if (parsed && parsed.v === 3 && parsed.op && p2pCommunicator) {
1856
- await handleV3P2PMessage(parsed, conn, p2pCommunicator);
1857
- }
1858
- }
1859
- catch (err) {
1860
- // 非 v3 JSON 帧 (老 p2p_message), 静默忽略
1861
- }
1862
1356
  });
1863
1357
  await p2pCommunicator.start();
1864
1358
  // @diap/sdk 也 join topic — 它的 Hyperswarm 实例帮 P2PDirect 做 DHT 引导
1865
- // joinTopic 失败已知 (hyperswarm 4.x + @diap/sdk 0.1.10), catch 静默
1359
+ // @diap/sdk 收到的数据是 mock (不真发), DHT 发现 + 节点连接是 OK
1866
1360
  const oldTopic = createTopic('bolloon-agent-harness');
1867
- try {
1868
- await p2pCommunicator.joinTopic(oldTopic);
1869
- console.log(`P2P 老通道已就绪 (DHT bootstrap 帮 P2PDirect, 实际数据走 P2PDirect)`);
1870
- }
1871
- catch (joinErr) {
1872
- // 已知: @diap/sdk 0.1.10 + hyperswarm 4.x → discovery.update is not a function
1873
- // v3 P2PDirect 是主路径, 此处不阻断
1874
- const msg = String(joinErr?.message || joinErr);
1875
- if (msg.includes('discovery.update') || msg.includes('is not a function')) {
1876
- console.warn(`[v3-legacy] @diap/sdk joinTopic 失败 (已知 hyperswarm 4.x 兼容问题, 已忽略): ${msg}`);
1877
- }
1878
- else {
1879
- throw joinErr;
1880
- }
1881
- }
1361
+ await p2pCommunicator.joinTopic(oldTopic);
1362
+ console.log(`P2P 老通道已就绪 (DHT bootstrap 帮 P2PDirect, 实际数据走 P2PDirect)`);
1882
1363
  }
1883
1364
  catch (e) {
1884
1365
  console.log(`P2P 老通道初始化失败: ${e.message}`);
@@ -1901,12 +1382,6 @@ export async function createWebServer(port = 3000, options = {}) {
1901
1382
  next();
1902
1383
  });
1903
1384
  app.use(express.static(webRoot));
1904
- // 2026-07-01 (v0.2.6 修复): 前端 message-renderer import 了 src/agents/chat-segmenter.js,
1905
- // 浏览器加载时按相对路径解析成 /agents/chat-segmenter.js. 这个路径在 webRoot (dist/web) 之外.
1906
- // 解决: 把 dist/agents 也 mount 到 /agents 静态路径, 让前端 import 解析成功.
1907
- // 这条不破坏 segmenter 内部 import (Node 走文件系统, 浏览器走 HTTP).
1908
- const agentsRoot = path.join(webRoot, '..', 'agents');
1909
- app.use('/agents', express.static(agentsRoot));
1910
1385
  app.get('/', (req, res) => {
1911
1386
  res.sendFile(join(webRoot, 'index.html'));
1912
1387
  });
@@ -1935,61 +1410,6 @@ export async function createWebServer(port = 3000, options = {}) {
1935
1410
  }
1936
1411
  app.get('/', serveStaticHtml('index.html', 'index.html not found; please run `npm run build:web`', 'index'));
1937
1412
  app.get('/api-config', serveStaticHtml('api-config.html', 'api-config.html not found; please run `npm run build:web`', 'api-config'));
1938
- // 2026-07-01 (v0.2.5): 输入验证 + 健康检查 API
1939
- // - POST /api/validate-input: 前端发送前预校验, 避免后端 reject
1940
- // - GET /api/health: liveness probe + validators 清单
1941
- // 这些端点不依赖 LLM / P2P, 永远可以响应.
1942
- app.post('/api/validate-input', async (req, res) => {
1943
- try {
1944
- const body = req.body ?? {};
1945
- const kind = body.kind || 'message';
1946
- let result;
1947
- if (kind === 'channel') {
1948
- result = validateChannelInput({ name: body.name, agentId: body.agentId });
1949
- }
1950
- else {
1951
- result = validateMessageInput({ text: body.text, channelId: body.channelId });
1952
- }
1953
- res.json({
1954
- ok: result.ok,
1955
- severity: result.severity ?? (result.ok ? 'info' : 'block'),
1956
- reason: result.reason,
1957
- cleaned: result.cleaned,
1958
- kind,
1959
- });
1960
- }
1961
- catch (e) {
1962
- res.status(400).json({ ok: false, severity: 'block', reason: e?.message ?? 'parse error' });
1963
- }
1964
- });
1965
- app.get('/api/health', (_req, res) => {
1966
- res.json(healthCheck(getPackageVersion()));
1967
- });
1968
- // 2026-07-01 (v0.2.6): 前后端分离核心 — 后端切 LLM 输出为结构化 segments
1969
- // - POST /api/segment-reply { reply, knownTools }
1970
- // - 返回 ChatSegment[] (think / text / env_details / tool_call / final)
1971
- // 前端拿到 segments 后只渲染, 不知道任何 LLM 格式 (minimax <invoke>, Hermes <tool_call>, Qwen function_calls, JSON 形式)
1972
- app.post('/api/segment-reply', async (req, res) => {
1973
- try {
1974
- const body = req.body ?? {};
1975
- const reply = typeof body.reply === 'string' ? body.reply : '';
1976
- const knownToolsArr = Array.isArray(body.knownTools) ? body.knownTools : [];
1977
- const knownToolNames = new Set(knownToolsArr.filter((s) => typeof s === 'string'));
1978
- const segments = segmentChatReply(reply, { knownToolNames });
1979
- res.json({
1980
- ok: true,
1981
- input_length: reply.length,
1982
- segments,
1983
- segments_count: segments.length,
1984
- has_think: segments.some(s => s.type === 'think'),
1985
- has_tool_call: segments.some(s => s.type === 'tool_call'),
1986
- has_final: segments.some(s => s.type === 'final'),
1987
- });
1988
- }
1989
- catch (e) {
1990
- res.status(400).json({ ok: false, reason: e?.message ?? 'parse error' });
1991
- }
1992
- });
1993
1413
  // 全局兜底: 任何 next(err) 走到这里, 给出结构化 4xx/5xx 而不是默认 HTML
1994
1414
  app.use((err, req, res, _next) => {
1995
1415
  console.error('[server] unhandled error on', req.method, req.path, '-', err?.message || err);
@@ -2059,20 +1479,9 @@ export async function createWebServer(port = 3000, options = {}) {
2059
1479
  }
2060
1480
  runState.running = true;
2061
1481
  runState.abortController = new AbortController();
2062
- // 2026-07-04: pivot loop safety net — 60s 强制 timeout 防止 LLM hang (minimax M3
2063
- // 偶尔反复 think 不输出 <final gen>, pivot 连 5 次无进展时会 hang 在
2064
- // quality 评估). setTimeout 让 LLM 客户端收 signal 主动 break.
2065
- const PIVOT_FORCE_TIMEOUT_MS = 30_000;
2066
- const forceTimeout = setTimeout(() => {
2067
- console.warn(`[server] /message pivot 强制 timeout (${PIVOT_FORCE_TIMEOUT_MS}ms), aborting`);
2068
- runState.abortController?.abort();
2069
- }, PIVOT_FORCE_TIMEOUT_MS);
2070
1482
  broadcastQueueUpdate(channelId);
2071
- // 2026-07-01 (v0.2.5): hoist sessionKey 到 try 外, 让 finally 块的 saveCurrentSession 能用
2072
- const sessionKey = `${channelId}:${currentSessionId}`;
2073
- let agent = null;
2074
1483
  try {
2075
- agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
1484
+ const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
2076
1485
  let fullResponse = '';
2077
1486
  // P0.5: 注入门回传的 usedIds, 落 session message metadata, UI 可查
2078
1487
  let usedJudgmentIds = [];
@@ -2298,14 +1707,7 @@ export async function createWebServer(port = 3000, options = {}) {
2298
1707
  // 修法: contextHint 当 "背景信息", text 当 "本轮用户请求" — 显式 marker 让 LLM 区分
2299
1708
  // 2026-06-15 二次修: 把 text 放在最前 (LLM 看到 input 第一眼是 user text, 不会被 judgmentHint 末尾
2300
1709
  // 的 "..." 误判为整个 input 截断)
2301
- // 2026-07-05: prepend nextPromptHints (manifest-loader 加载的对方能力, 仅本次生效)
2302
- let extraHint = '';
2303
- const hint = nextPromptHints.get(channelId);
2304
- if (hint) {
2305
- extraHint = hint + '\n\n';
2306
- nextPromptHints.delete(channelId);
2307
- }
2308
- const markedPrompt = `${extraHint}【本轮用户请求】\n${text}\n【请求结束】\n\n${contextHint}`;
1710
+ const markedPrompt = `【本轮用户请求】\n${text}\n【请求结束】\n\n${contextHint}`;
2309
1711
  fullResponse = await agent.promptStream(markedPrompt, streamCallback, runState.abortController?.signal, channelId);
2310
1712
  }
2311
1713
  catch (err) {
@@ -2321,16 +1723,10 @@ export async function createWebServer(port = 3000, options = {}) {
2321
1723
  broadcast({ type: 'error', content: fullResponse }, channelId);
2322
1724
  }
2323
1725
  }
2324
- // abort 模式: 给 partial 拼后缀 — 只在 LLM 真有输出时加中断标记,
2325
- // 跳过空响应 + 跳过 [AI 服务调用失败] fallback (这些本身就是错误占位, 拼
2326
- // "[生成已中断]" 会误导用户)
2327
- const hasRealLlmOutput = fullResponse.trim().length > 0 &&
2328
- !fullResponse.trimStart().startsWith('[AI 服务调用失败]');
2329
- if (runState.abortController?.signal.aborted && hasRealLlmOutput) {
1726
+ // abort 模式: 给 partial 拼后缀
1727
+ if (runState.abortController?.signal.aborted && fullResponse.trim().length > 0) {
2330
1728
  fullResponse = fullResponse + '\n\n_[生成已中断]_';
2331
1729
  }
2332
- // 2026-06-18: 修 lastFinalReply 没设 → /api/loop/inspect 永远返回空
2333
- runState.lastFinalReply = fullResponse;
2334
1730
  // v3 新增: 解析 LLM 回复里的 @-mentions, 转发到目标 channel
2335
1731
  await routeMentionsInReply(channelId, fullResponse, localChannels, remoteChannels);
2336
1732
  broadcast({ type: 'ai', content: fullResponse }, channelId);
@@ -2350,117 +1746,6 @@ export async function createWebServer(port = 3000, options = {}) {
2350
1746
  });
2351
1747
  session.lastUpdated = new Date().toISOString();
2352
1748
  await saveSession(session);
2353
- // 2026-07-04: 写 session 后, 调 memory-compressor 把消息历史压缩到
2354
- // ~/.bolloon/memory/<channel.agentId>/sessions/<safe-channel>__<safe-session>.summary.md
2355
- // 失败静默, 不阻塞 /message 主路径.
2356
- try {
2357
- const channelsForMemory = await loadChannels();
2358
- const channelForMemory = channelsForMemory.find(c => c.id === channelId);
2359
- if (channelForMemory?.agentId) {
2360
- const { compressSessionToMemory } = await import('../bootstrap/memory-compressor.js');
2361
- const compressRes = await compressSessionToMemory({
2362
- agentId: channelForMemory.agentId,
2363
- channelId,
2364
- sessionId: currentSessionId,
2365
- });
2366
- if (!compressRes.skipped && compressRes.bytesWritten > 0) {
2367
- console.log(`[memory] compressed ${compressRes.messagesCount} new messages for ${channelForMemory.agentId}/${channelId} → ${compressRes.summaryPath} (${compressRes.bytesWritten}B)`);
2368
- }
2369
- }
2370
- }
2371
- catch (memErr) {
2372
- console.warn('[memory] compressSessionToMemory failed (non-fatal):', memErr?.message?.slice(0, 200));
2373
- }
2374
- // 2026-07-05: 把 session 里的 user/ai 消息按涉及到的远端 peer 归档到月度 markdown
2375
- // 触发条件: 消息里出现过 @远端 channel, 或者 session 历史上与该 peer 通信过
2376
- // 失败静默, 不阻塞主对话
2377
- try {
2378
- const { appendChatArchive } = await import('../bootstrap/chat-archiver.js');
2379
- // 找到这条消息涉及到的远端 peer (从 routeMentionsInReply 的结果推断 — 简单起见直接遍历 remoteChannels)
2380
- const localChannels2 = (await loadChannels());
2381
- const remoteChannels2 = [];
2382
- for (const [peerPk, list] of remoteChannelCache.entries()) {
2383
- for (const ch of list) {
2384
- remoteChannels2.push({ ...ch, _ownerPublicKey: peerPk });
2385
- }
2386
- }
2387
- // 找出本次 text/fullResponse 里出现 @ 的 channel 对应的 owner pk
2388
- const peerSet = new Set();
2389
- const mentionRe = /@([一-龥A-Za-z0-9_\-]{1,30})/g;
2390
- const mentionedNames = new Set();
2391
- for (const m of text.matchAll(mentionRe))
2392
- mentionedNames.add(m[1]);
2393
- for (const m of fullResponse.matchAll(mentionRe))
2394
- mentionedNames.add(m[1]);
2395
- for (const rc of remoteChannels2) {
2396
- if (mentionedNames.has(rc.name))
2397
- peerSet.add(rc._ownerPublicKey);
2398
- }
2399
- // 追加到每个相关 peer 的月度归档
2400
- for (const pk of peerSet) {
2401
- await appendChatArchive({
2402
- publicKey: pk,
2403
- entry: {
2404
- ts: new Date().toISOString(),
2405
- source: 'local',
2406
- channelId, channelName: (await loadChannels()).find(c => c.id === channelId)?.name,
2407
- text: `[本节点] user: ${text.slice(0, 500)}\n[本节点] ai: ${fullResponse.slice(0, 800)}`,
2408
- fromPublicKey: v3P2PRef?.getPublicKey(),
2409
- msgType: 'user',
2410
- }
2411
- });
2412
- }
2413
- }
2414
- catch (archiveErr) {
2415
- console.warn('[chat-archive] append failed (non-fatal):', archiveErr?.message?.slice(0, 200));
2416
- }
2417
- // 2026-07-05: 懒加载触发器 — 探测到 @-mention 远端 / 连续失败 / 关键词, 拉对方 manifest
2418
- // 把 capability-index 拼到下一次 prompt 上下文 (只生效 1 次, 不污染主循环)
2419
- try {
2420
- const { detectLoadTrigger, loadPeerManifest, clearFailure } = await import('../agents/peer-manifest-loader.js');
2421
- const remoteChannelsForDetect = [];
2422
- for (const [peerPk, list] of remoteChannelCache.entries()) {
2423
- for (const ch of list) {
2424
- remoteChannelsForDetect.push({ ...ch, _ownerPublicKey: peerPk });
2425
- }
2426
- }
2427
- // 检测 + 加载
2428
- const detection = detectLoadTrigger({
2429
- text,
2430
- channelId,
2431
- remoteChannels: remoteChannelsForDetect,
2432
- });
2433
- if (detection.shouldLoad && detection.remotePublicKey && v3P2PRef) {
2434
- const result = await loadPeerManifest({
2435
- channelId,
2436
- channelName: (await loadChannels()).find(c => c.id === channelId)?.name,
2437
- reason: detection.reason,
2438
- triggerValue: detection.triggerValue,
2439
- remotePublicKey: detection.remotePublicKey,
2440
- }, { p2p: v3P2PRef });
2441
- if (result && result.promptBlock) {
2442
- console.log(`[manifest-loader] ${detection.reason} → ${detection.remotePublicKey.slice(0, 12)}... (${result.durationMs}ms, rpc=${result.rpcTriggered}, agents=${result.agentDescriptions.length})`);
2443
- // 把 promptBlock 推给前端 + 写入一次性的 prompt 附加 (nextPromptHint 全局变量, 在下次 agent.promptStream 前 prepend)
2444
- broadcast({
2445
- type: 'peer-manifest-loaded',
2446
- fromPublicKey: detection.remotePublicKey,
2447
- reason: detection.reason,
2448
- promptBlock: result.promptBlock,
2449
- capabilityIndex: result.capabilityIndex,
2450
- }, channelId);
2451
- // 缓存到 channel 维度的 nextPromptHint (下一次 LLM prompt 前 inject)
2452
- nextPromptHints.set(channelId, (nextPromptHints.get(channelId) || '') + '\n\n' + result.promptBlock);
2453
- clearFailure(channelId);
2454
- }
2455
- }
2456
- else {
2457
- // 没触发, 也清掉连续失败计数 (用户消息说明对话还在正常进行)
2458
- clearFailure(channelId);
2459
- }
2460
- }
2461
- catch (loaderErr) {
2462
- console.warn('[manifest-loader] failed (non-fatal):', loaderErr?.message?.slice(0, 200));
2463
- }
2464
1749
  const channels = await loadChannels();
2465
1750
  const channel = channels.find(c => c.id === channelId);
2466
1751
  // 2026-06-11: 移除 suggestRename 的二次 LLM 调用 — 之前每次用户发消息, 智能体 channel 都会再调一次 LLM (5-8s) 自动改名
@@ -2522,10 +1807,6 @@ export async function createWebServer(port = 3000, options = {}) {
2522
1807
  res.status(500).json({ error: err.message });
2523
1808
  }
2524
1809
  finally {
2525
- // 2026-07-04: 清掉 pivot 强制 timeout timer, 避免 hang 情况下 timer 在
2526
- // process 里一直挂着. forceTimeout 在 try 外创建, finally 在闭包里
2527
- // 能直接拿到引用.
2528
- clearTimeout(forceTimeout);
2529
1810
  // queue dequeue: 跑完或失败都要清状态
2530
1811
  // 当前实现: 自动接下一条需要把 ~200 行 try 块抽函数, 暂不抽.
2531
1812
  // 替代: 用户点 [队列 +N] 按钮时, 客户端发起一个特殊的 HTTP 请求触发下一条
@@ -2533,18 +1814,6 @@ export async function createWebServer(port = 3000, options = {}) {
2533
1814
  runState.running = false;
2534
1815
  runState.abortController = null;
2535
1816
  broadcastQueueUpdate(channelId);
2536
- // 2026-07-01 (v0.2.5): 持久化当前 messageHistory — 让 web 用户跨刷新保留对话.
2537
- // saveCurrentSession 失败静默, 不阻塞 channel 状态清理.
2538
- // saveCurrentSession 内部走 SessionStore (默认 ~/.bolloon/sessions/cache/<sessionKey>.json).
2539
- // agent 可能在 try 抛错后仍为 null (getAgentForChannel 失败), null-guard 跳过 save.
2540
- if (agent) {
2541
- try {
2542
- await agent.saveCurrentSession(sessionKey);
2543
- }
2544
- catch (saveErr) {
2545
- console.warn(`[web] saveCurrentSession failed (non-fatal): ${saveErr?.message?.slice(0, 100)}`);
2546
- }
2547
- }
2548
1817
  }
2549
1818
  });
2550
1819
  // ---------- 频道元数据后台修复队列 ----------
@@ -4289,25 +3558,11 @@ export async function createWebServer(port = 3000, options = {}) {
4289
3558
  res.json({ initialized: false });
4290
3559
  return;
4291
3560
  }
4292
- // 2026-07-04: irohTransport.getNodeId() 在某些环境下返回空字符串 (@rayhanadev/iroh + Windows
4293
- // native binding 启动延迟). fallback: 用 v3 P2PDirect (hyperswarm) 的 publicKey 兜底,
4294
- // 保证客户端拿到一个可用的 peer id (v3 是实际数据通道, 不是 mock).
4295
- let effectiveNodeId = irohNodeInfo.irohNodeId;
4296
- if (!effectiveNodeId && v3P2PRef) {
4297
- try {
4298
- effectiveNodeId = v3P2PRef.getPublicKey() || '';
4299
- if (effectiveNodeId) {
4300
- console.log('[iroh API] irohNodeId 为空, fallback 到 v3 P2PDirect publicKey:', effectiveNodeId.substring(0, 16) + '...');
4301
- }
4302
- }
4303
- catch { /* ignore */ }
4304
- }
4305
3561
  res.json({
4306
3562
  initialized: true,
4307
3563
  did: irohNodeInfo.did,
4308
3564
  cid: irohNodeInfo.cid,
4309
- irohNodeId: effectiveNodeId,
4310
- irohNodeIdSource: effectiveNodeId === irohNodeInfo.irohNodeId ? 'iroh' : (effectiveNodeId ? 'v3-p2p-fallback' : 'unavailable'),
3565
+ irohNodeId: irohNodeInfo.irohNodeId,
4311
3566
  name: irohNodeInfo.name
4312
3567
  });
4313
3568
  });
@@ -5952,23 +5207,16 @@ export async function createWebServer(port = 3000, options = {}) {
5952
5207
  const startPort = port;
5953
5208
  let currentPort = startPort;
5954
5209
  let attempt = 0;
5955
- // 2026-06-24: 默认 loopback bind,避免在 LAN 上暴露 (CORS 已经 * 了)。
5956
- // Electron 包装里强制 127.0.0.1; CLI 用户想 LAN 访问可显式 BOLLOON_HOST=0.0.0.0
5957
- const bindHost = options.host ?? process.env.BOLLOON_HOST ?? '127.0.0.1';
5958
5210
  // 局部可变 server 引用 — listen 失败后必须重新 createServer 再 listen
5959
5211
  let currentServer = server;
5960
5212
  const tryListen = () => {
5961
5213
  currentServer.removeAllListeners('error');
5962
5214
  currentServer.once('error', onError);
5963
- currentServer.listen(currentPort, bindHost, () => {
5215
+ currentServer.listen(currentPort, () => {
5964
5216
  if (currentPort !== startPort) {
5965
5217
  console.warn(`⚠ 端口 ${startPort} 被占用,已自动切换到 ${currentPort}`);
5966
5218
  }
5967
- // 2026-06-24: BOLLOON_PORT=NNNN 是 Electron 主进程解析的契约行 (parseable),
5968
- // 旧 marker ('服务器已监听') 也保留给日志/调试看。
5969
- console.log(`BOLLOON_PORT=${currentPort}`);
5970
- console.log(`BOLLOON_HOST=${bindHost}`);
5971
- console.log(`Web 服务器启动完成: http://${bindHost}:${currentPort}`);
5219
+ console.log(`Web 服务器启动完成: http://localhost:${currentPort}`);
5972
5220
  console.log('服务器已监听');
5973
5221
  // 安装 chat bus -> SSE 桥 (供前端 inbox UI 实时刷新)
5974
5222
  void installChatBusHook();