@bolloon/bolloon-agent 0.6.5 → 0.6.6

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 (169) hide show
  1. package/dist/agents/chain/chain-settlement.js +0 -1
  2. package/dist/agents/chat-segmenter.js +4 -16
  3. package/dist/agents/contacts/human-directory.js +155 -0
  4. package/dist/agents/contacts/tools.js +79 -0
  5. package/dist/agents/experience-review.js +0 -3
  6. package/dist/agents/goal-flywheel/goal-change.js +3 -4
  7. package/dist/agents/goal-resume.js +0 -4
  8. package/dist/agents/intent-classifier.js +1 -1
  9. package/dist/agents/iteration-budget.js +12 -0
  10. package/dist/agents/knowledge-organizer.js +0 -2
  11. package/dist/agents/lesson-to-skill.js +0 -1
  12. package/dist/agents/loop-review.js +2 -2
  13. package/dist/agents/memory-store.js +359 -0
  14. package/dist/agents/opportunity-web-search.js +39 -0
  15. package/dist/agents/p2p-chat-tools.js +1 -2
  16. package/dist/agents/parse-tool-call.js +9 -22
  17. package/dist/agents/pi-harness.js +1 -2
  18. package/dist/agents/pi-sdk-tools.js +180 -5
  19. package/dist/agents/pi-sdk.js +143 -9
  20. package/dist/agents/process-runner.js +28 -9
  21. package/dist/agents/react-loop.js +9 -17
  22. package/dist/agents/runner-resolver.js +4 -3
  23. package/dist/agents/skill-loader.js +22 -15
  24. package/dist/agents/skills-manager.js +1 -25
  25. package/dist/agents/workflow-pivot-loop.js +152 -61
  26. package/dist/bootstrap/context-collector.js +2 -4
  27. package/dist/bootstrap/context-hierarchy.js +13 -1
  28. package/dist/cli/ink-app.js +44 -17
  29. package/dist/cli/intent-command.js +22 -12
  30. package/dist/cli/loading-tui.js +8 -2
  31. package/dist/cli/log-gate.js +40 -8
  32. package/dist/cli/mention-data.js +1 -0
  33. package/dist/cli/reply-hygiene.js +22 -0
  34. package/dist/cli/trace-line.js +61 -2
  35. package/dist/electron-build/agents/agent-gateway.js.map +1 -1
  36. package/dist/electron-build/agents/chain/chain-indexer.js.map +1 -1
  37. package/dist/electron-build/agents/chain/chain-settlement.js +0 -1
  38. package/dist/electron-build/agents/chain/chain-settlement.js.map +1 -1
  39. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  40. package/dist/electron-build/agents/contacts/human-directory.js +204 -0
  41. package/dist/electron-build/agents/contacts/human-directory.js.map +1 -0
  42. package/dist/electron-build/agents/contacts/policy.js.map +1 -1
  43. package/dist/electron-build/agents/contacts/tools.js +112 -0
  44. package/dist/electron-build/agents/contacts/tools.js.map +1 -1
  45. package/dist/electron-build/agents/experience-review.js +0 -3
  46. package/dist/electron-build/agents/experience-review.js.map +1 -1
  47. package/dist/electron-build/agents/external-events.js.map +1 -1
  48. package/dist/electron-build/agents/goal-flywheel/goal-change.js +3 -4
  49. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -1
  50. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -1
  51. package/dist/electron-build/agents/goal-resume.js +0 -4
  52. package/dist/electron-build/agents/goal-resume.js.map +1 -1
  53. package/dist/electron-build/agents/intent-classifier.js +1 -1
  54. package/dist/electron-build/agents/lesson-to-skill.js +0 -1
  55. package/dist/electron-build/agents/lesson-to-skill.js.map +1 -1
  56. package/dist/electron-build/agents/loop-review.js +2 -2
  57. package/dist/electron-build/agents/loop-review.js.map +1 -1
  58. package/dist/electron-build/agents/memory-recall.js.map +1 -1
  59. package/dist/electron-build/agents/memory-store.js +404 -0
  60. package/dist/electron-build/agents/memory-store.js.map +1 -0
  61. package/dist/electron-build/agents/parse-tool-call.js +9 -22
  62. package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
  63. package/dist/electron-build/agents/pi-harness.js +1 -2
  64. package/dist/electron-build/agents/pi-harness.js.map +1 -1
  65. package/dist/electron-build/agents/pi-sdk-tools.js +180 -5
  66. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  67. package/dist/electron-build/agents/pi-sdk.js +143 -9
  68. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  69. package/dist/electron-build/agents/pre-tool-validator.js.map +1 -1
  70. package/dist/electron-build/agents/process-runner.js +28 -9
  71. package/dist/electron-build/agents/process-runner.js.map +1 -1
  72. package/dist/electron-build/agents/runner-resolver.js +4 -3
  73. package/dist/electron-build/agents/runner-resolver.js.map +1 -1
  74. package/dist/electron-build/agents/skill-loader.js +22 -16
  75. package/dist/electron-build/agents/skill-loader.js.map +1 -1
  76. package/dist/electron-build/agents/skill-supervisor-link.js.map +1 -1
  77. package/dist/electron-build/agents/skills-manager.js +0 -24
  78. package/dist/electron-build/agents/skills-manager.js.map +1 -1
  79. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  80. package/dist/electron-build/agents/workflow-engine.js.map +1 -1
  81. package/dist/electron-build/agents/workflow-pivot-loop.js +155 -62
  82. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  83. package/dist/electron-build/agents/x402/goal-run-bridge.js.map +1 -1
  84. package/dist/electron-build/bollharness-integration/gate-state-machine.js.map +1 -1
  85. package/dist/electron-build/bootstrap/context-collector.js +2 -4
  86. package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
  87. package/dist/electron-build/bootstrap/context-hierarchy.js +13 -1
  88. package/dist/electron-build/bootstrap/context-hierarchy.js.map +1 -1
  89. package/dist/electron-build/electron-preload.js.map +1 -1
  90. package/dist/electron-build/external-engines/delegate.js.map +1 -1
  91. package/dist/electron-build/kernel/channel-actor.js +31 -2
  92. package/dist/electron-build/kernel/channel-actor.js.map +1 -1
  93. package/dist/electron-build/kernel/model-runtime.js +8 -1
  94. package/dist/electron-build/kernel/model-runtime.js.map +1 -1
  95. package/dist/electron-build/llm/config-store.js +20 -0
  96. package/dist/electron-build/llm/config-store.js.map +1 -1
  97. package/dist/electron-build/llm/embedding-adapter.js +75 -0
  98. package/dist/electron-build/llm/embedding-adapter.js.map +1 -0
  99. package/dist/electron-build/llm/pi-ai.js +96 -35
  100. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  101. package/dist/electron-build/network/agent-network.js.map +1 -1
  102. package/dist/electron-build/orbitdb/memory-sync.js +76 -0
  103. package/dist/electron-build/orbitdb/memory-sync.js.map +1 -0
  104. package/dist/electron-build/pi-ecosystem-judgment/adaptive-scan.js.map +1 -1
  105. package/dist/electron-build/security/tool-gate.js +2 -0
  106. package/dist/electron-build/security/tool-gate.js.map +1 -1
  107. package/dist/electron-build/setup/setup-store.js +34 -6
  108. package/dist/electron-build/setup/setup-store.js.map +1 -1
  109. package/dist/electron-build/social/heartbeat.js +18 -5
  110. package/dist/electron-build/social/heartbeat.js.map +1 -1
  111. package/dist/electron-build/utils/update-manager.js.map +1 -1
  112. package/dist/index.js +250 -12
  113. package/dist/ios/bolloon-web.json +2 -2
  114. package/dist/ios/client.js +74 -35
  115. package/dist/ios/icons/icon-1400x1400.png +0 -0
  116. package/dist/ios/icons/icon-1400x1400.webp +0 -0
  117. package/dist/ios/index.html +106 -107
  118. package/dist/ios/mobile-agent.js +346 -45
  119. package/dist/ios/mobile-core.js +7303 -4590
  120. package/dist/ios/mobile-harness.js +1004 -0
  121. package/dist/ios/mobile-orbitdb.js +194 -0
  122. package/dist/ios/mobile-persona.js +80 -0
  123. package/dist/ios/mobile-privacy.js +2 -3
  124. package/dist/ios/mobile.css +286 -21
  125. package/dist/ios/mobile.html +106 -107
  126. package/dist/ios/mobile.js +300 -132
  127. package/dist/ios/routes-world.js +4 -1
  128. package/dist/ios/server.js +3 -1
  129. package/dist/ios/style.css +13 -5
  130. package/dist/ios/ui/message-renderer.js +4 -18
  131. package/dist/ios/ui/step-timeline.js +14 -1
  132. package/dist/kernel/channel-actor.js +30 -2
  133. package/dist/kernel/gate-scan.js +1 -1
  134. package/dist/kernel/index.js +33 -0
  135. package/dist/kernel/model-runtime.js +8 -1
  136. package/dist/kernel/plan.js +3 -3
  137. package/dist/kernel/roster.js +3 -3
  138. package/dist/llm/config-store.js +20 -0
  139. package/dist/llm/embedding-adapter.js +69 -0
  140. package/dist/llm/pi-ai.js +94 -34
  141. package/dist/orbitdb/memory-sync.js +70 -0
  142. package/dist/pi-ecosystem-judgment/adaptive-scan.js +1 -1
  143. package/dist/security/tool-gate.js +2 -0
  144. package/dist/setup/setup-store.js +34 -6
  145. package/dist/social/heartbeat.js +18 -5
  146. package/dist/web/bolloon-web.json +3 -3
  147. package/dist/web/client.js +74 -35
  148. package/dist/web/icons/icon-1400x1400.png +0 -0
  149. package/dist/web/icons/icon-1400x1400.webp +0 -0
  150. package/dist/web/index.html +37 -25
  151. package/dist/web/mobile-agent.js +351 -45
  152. package/dist/web/mobile-contacts.js +0 -4
  153. package/dist/web/mobile-core.js +7430 -4590
  154. package/dist/web/mobile-harness.js +1004 -0
  155. package/dist/web/mobile-orbitdb.js +194 -0
  156. package/dist/web/mobile-persona.js +80 -0
  157. package/dist/web/mobile-privacy.js +2 -3
  158. package/dist/web/mobile.css +286 -21
  159. package/dist/web/mobile.html +121 -107
  160. package/dist/web/mobile.js +409 -136
  161. package/dist/web/routes-contacts.js +94 -1
  162. package/dist/web/routes-world.js +4 -1
  163. package/dist/web/server.js +4 -2
  164. package/dist/web/style.css +13 -5
  165. package/dist/web/sw.js +1 -1
  166. package/dist/web/ui/message-renderer.js +4 -18
  167. package/dist/web/ui/step-timeline.js +14 -1
  168. package/package.json +3 -2
  169. package/scripts/build-web.ts +16 -1
@@ -70,6 +70,8 @@ const TOOL_WHITELIST = new Set([
70
70
  'ui_save_component', 'ui_load_component',
71
71
  // 2026-08-12: Kanban 看板工具 (Hermes kanban_db → OrbitDB, src/orbitdb/kanban-store.ts)
72
72
  'kanban_create', 'kanban_list', 'kanban_get', 'kanban_claim', 'kanban_complete', 'kanban_status',
73
+ // 2026-10-14: 结构化记忆工具 (src/agents/memory-store.ts, P0)
74
+ 'memory_save', 'memory_recall',
73
75
  // 2026-08-07: Bolloon 自身配置读写 (bolloon-config.json, agent 有修改自身配置权限)
74
76
  'bolloon_config_get', 'bolloon_config_set',
75
77
  // 2026-09-13: 下一代工具集 (人机问答 / 精确补丁 / 代码执行 / 桌面操作 / 技能分享 / 浏览器)
@@ -257,18 +257,36 @@ export async function evaluateSetup(opts = {}) {
257
257
  checks.credentialPresent = hasKey;
258
258
  checks.providerUsable = providerSelected && credentialOk;
259
259
  checks.modelPresent = modelOk;
260
- // 3) 连通性 (24h 有效期)
260
+ // 3) 连通性 (2026-10-13 重构: 结论有效期 = 「测过 + 配置没变」, 不再只看墙钟 24h)
261
+ // - 旧逻辑: 24h 墙钟一到就过期, 即使 provider/key/baseUrl/model 一个没动 → 用户隔天启动必见
262
+ // 「未就绪」, 被迫重测 (体验债: 配置没变重测无意义, 且评估会把 connectivityOk 覆盖成 false 落盘,
263
+ // 于是「测过」这个事实被销毁, 更修不回来)。
264
+ // - 新语义: connectivityAt 存在(测过) 且 配置指纹一致(配置没变) ⇒ 结论仍有效; 只有配置真变了才重测。
261
265
  const connAt = checks.connectivityAt ? Date.parse(checks.connectivityAt) : 0;
262
- const connFresh = !!connAt && now - connAt < ttl;
266
+ const hadConnTest = !!connAt;
267
+ // 配置指纹: 当前配置 vs 上次通过时的配置 —— 相同则连通性结论仍有效 (provider/key/baseUrl/model 都没变)
268
+ let configUnchanged = false;
269
+ try {
270
+ const { hash: curHash } = await hashConfigFiles(bolloonHome);
271
+ configUnchanged = !!prev.configHash && prev.configHash === curHash;
272
+ }
273
+ catch {
274
+ configUnchanged = false;
275
+ }
263
276
  let connectivityOk = false;
264
- if (checks.connectivityOk && connFresh)
277
+ if (hadConnTest && configUnchanged) {
278
+ // 测过 + 配置没变 ⇒ 结论有效 (即使墙钟过了 24h)
265
279
  connectivityOk = true;
266
- else if (checks.connectivityOk && !connFresh) {
267
- reasons.push('连通性结果已过期 (>24h) → 需要重新测试');
280
+ checks.connectivityOk = true; // 救回被旧逻辑覆盖的 false
281
+ }
282
+ else if (hadConnTest && !configUnchanged) {
283
+ // 测过但配置变了 (或 configHash 缺失) ⇒ 需要重测
284
+ connectivityOk = false;
268
285
  checks.connectivityOk = false;
286
+ reasons.push('LLM 配置已变化 → 需要重新测试连通性');
269
287
  }
270
288
  if (!connectivityOk)
271
- why.basic.push(connAt && !connFresh ? '连通性结果过期 (需重测)' : '还没做过连通性测试');
289
+ why.basic.push(hadConnTest && !configUnchanged ? '配置变化, 连通性需重测' : '还没做过连通性测试');
272
290
  // 4) 运行时 (真实: initMinimax + session; 不是"singleton 存在")
273
291
  let runtimeOk = checks.runtimeInitialized === true;
274
292
  if (!opts.light) {
@@ -545,6 +563,16 @@ export async function recordSetupStage(stage, patch = {}, bolloonHome = resolveB
545
563
  lastError: undefined,
546
564
  updatedAt: new Date().toISOString(),
547
565
  };
566
+ // 2026-10-13 重构: 补算并落盘配置指纹 —— 与 evaluateSetup (413-414) 同一逻辑。
567
+ // 之前只有 evaluateSetup 落 configHash; 直写路径 (onboard 连通性成功 / 测试模拟) 缺 hash ⇒
568
+ // 连通性判定 (「测过且配置没变 → 结论有效」) 在非 evaluateSetup 路径下永远 configUnchanged=false。
569
+ // 行为不变 (configHash 仍是同一文件的 sha256), 只是所有写 state 的路径都带上指纹。
570
+ try {
571
+ const fp = await hashConfigFiles(bolloonHome);
572
+ state.configHash = fp.hash;
573
+ state.configVersion = fp.version;
574
+ }
575
+ catch { /* hash 计算失败不阻塞记录 */ }
548
576
  await writeSetupState(state, bolloonHome);
549
577
  return state;
550
578
  }
@@ -260,6 +260,9 @@ export class SocialHeartbeat {
260
260
  currentPeerDids = new Set();
261
261
  greetingMessage;
262
262
  ownIpfsCid;
263
+ // 2026-10-12: IPFS/IPNS 发布失败只提示一次 (本地没跑节点时每次心跳刷 warn)
264
+ _ipfsPublishWarned = false;
265
+ _ipnsPublishWarned = false;
263
266
  // 蚁群组件
264
267
  pheromoneEngine = null;
265
268
  adaptiveHeartbeat = null;
@@ -393,7 +396,7 @@ export class SocialHeartbeat {
393
396
  return;
394
397
  try {
395
398
  const { IpfsClient } = await import('@diap/sdk');
396
- const ipfs = new IpfsClient('http://127.0.0.1:5001', null);
399
+ const ipfs = new IpfsClient('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
397
400
  console.log(`[Heartbeat] 从 IPNS 加载 persona: ${this.config.ownIpnsName}`);
398
401
  const cid = await ipfs.resolveIpns(this.config.ownIpnsName);
399
402
  if (cid) {
@@ -524,7 +527,7 @@ export class SocialHeartbeat {
524
527
  return;
525
528
  try {
526
529
  const { IpfsClient } = await import('@diap/sdk');
527
- const ipfs = new IpfsClient('http://127.0.0.1:5001', null);
530
+ const ipfs = new IpfsClient('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
528
531
  const doc = {
529
532
  name: persona.name,
530
533
  description: persona.description,
@@ -541,6 +544,9 @@ export class SocialHeartbeat {
541
544
  const content = JSON.stringify(doc);
542
545
  const result = await ipfs.upload(content);
543
546
  const cid = typeof result === 'string' ? result : result?.cid;
547
+ // 2026-10-12 (leo: 「IPFS 发布失败刷屏 27 行」): 本地没跑 IPFS 节点时每次心跳都 warn。
548
+ // 成功一次就重置提示标志 —— 节点恢复后重新可提示。
549
+ this._ipfsPublishWarned = false;
544
550
  if (cid && cid !== this.ownIpfsCid) {
545
551
  this.ownIpfsCid = cid;
546
552
  console.log(`[Heartbeat] Persona 发布到 IPFS: ${cid}`);
@@ -550,13 +556,20 @@ export class SocialHeartbeat {
550
556
  console.log(`[Heartbeat] Persona 发布到 IPNS: ${this.config.ownIpnsName}`);
551
557
  }
552
558
  catch (err) {
553
- console.warn(`[Heartbeat] IPNS 发布失败:`, err);
559
+ if (!this._ipnsPublishWarned) {
560
+ this._ipnsPublishWarned = true;
561
+ console.warn('[Heartbeat] IPNS 发布失败:', err);
562
+ }
554
563
  }
555
564
  }
556
565
  }
557
566
  }
558
567
  catch (err) {
559
- console.warn('[Heartbeat] IPFS 发布失败:', err);
568
+ // 只提示第一次 (配置缺失 / 节点未跑是常态, 不该每个心跳刷一条)
569
+ if (!this._ipfsPublishWarned) {
570
+ this._ipfsPublishWarned = true;
571
+ console.warn('[Heartbeat] IPFS 发布失败:', err);
572
+ }
560
573
  }
561
574
  }
562
575
  async discoverPeers() {
@@ -652,7 +665,7 @@ export class SocialHeartbeat {
652
665
  return;
653
666
  try {
654
667
  const { IpfsClient } = await import('@diap/sdk');
655
- const ipfs = new IpfsClient('http://127.0.0.1:5001', null);
668
+ const ipfs = new IpfsClient('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
656
669
  const cid = await ipfs.resolveIpns(agent.ipnsName);
657
670
  if (cid) {
658
671
  agent.cid = cid;
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema": "bolloon-mobile-web/1",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "channel": "stable",
5
5
  "build": "stable",
6
- "commit": "0d876c1b48ea86d5a390bd21b62468c8d2487fe0",
7
- "builtAt": "2026-10-06T07:04:38.761Z",
6
+ "commit": "f2668fa9e6cc17c9868356b581a15d8cef867a3b",
7
+ "builtAt": "2026-10-15T10:28:08.907Z",
8
8
  "layer": "web",
9
9
  "note": "web 资源层构建戳 (手机端 OTA 的身份第 ③ 级回退; 装机后以 OTA state.json 为准)"
10
10
  }
@@ -33,6 +33,15 @@
33
33
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
34
34
 
35
35
  // src/web/ui/step-timeline.ts
36
+ function formatDuration(ms) {
37
+ if (!Number.isFinite(ms) || ms < 0) return "(\u8017\u65F6\u672A\u77E5)";
38
+ if (ms < 1e3) return `${ms}ms`;
39
+ const s = ms / 1e3;
40
+ if (s < 60) return `${s.toFixed(1)}s`;
41
+ const m = Math.floor(s / 60);
42
+ const rem = Math.round(s % 60);
43
+ return `${m}m${rem}s`;
44
+ }
36
45
  function readExpanded(channelId) {
37
46
  if (!channelId) return false;
38
47
  try {
@@ -152,7 +161,10 @@
152
161
  }
153
162
  node.setAttribute("data-status", step.status);
154
163
  const label = node.querySelector(".step-timeline-label");
155
- if (label) label.textContent = step.name;
164
+ if (label) {
165
+ const dur = step.durationMs !== void 0 && step.status !== "active" ? ` \xB7 ${formatDuration(step.durationMs)}` : "";
166
+ label.textContent = step.name + dur;
167
+ }
156
168
  const argsEl = node.querySelector(".step-timeline-args");
157
169
  if (argsEl) {
158
170
  const argStr = step.args && Object.keys(step.args).length > 0 ? JSON.stringify(step.args).slice(0, 60) : "";
@@ -296,6 +308,7 @@
296
308
  steps[targetIdx].status = data.success === false ? "error" : "done";
297
309
  if (data.output) steps[targetIdx].output = data.output;
298
310
  if (data.error) steps[targetIdx].error = data.error;
311
+ if (data.durationMs !== void 0) steps[targetIdx].durationMs = data.durationMs;
299
312
  } else {
300
313
  steps[targetIdx].status = "error";
301
314
  if (data.error) steps[targetIdx].error = data.error;
@@ -595,21 +608,10 @@
595
608
  if (remaining.trim()) {
596
609
  remaining = filterFillerText(remaining);
597
610
  }
598
- const finalIdx = remaining.indexOf("<final gen>");
599
- let beforeFinalText = remaining;
600
- if (finalIdx >= 0) {
601
- beforeFinalText = remaining.substring(0, finalIdx);
602
- }
603
- const textContent = beforeFinalText.trim();
611
+ const textContent = remaining.trim();
604
612
  if (textContent) {
605
613
  segments.push({ type: "text", content: textContent });
606
614
  }
607
- if (finalIdx >= 0) {
608
- const afterFinal = remaining.substring(finalIdx + "<final gen>".length).trim();
609
- if (afterFinal) {
610
- segments.push({ type: "final", content: afterFinal });
611
- }
612
- }
613
615
  return segments;
614
616
  }
615
617
  function extractAndPush(type, text, out, re) {
@@ -879,11 +881,7 @@
879
881
  div.className = `message message-${type}`;
880
882
  let cleanContent = content;
881
883
  if (type === "ai") {
882
- cleanContent = cleanContent.replace(/<think>[\s\S]*?<\/think>/g, "");
883
- const finalGenIdx = cleanContent.indexOf("<final gen>");
884
- if (finalGenIdx >= 0) {
885
- cleanContent = cleanContent.substring(0, finalGenIdx).trim();
886
- }
884
+ cleanContent = cleanContent.replace(/ thinking[\s\S]*?<\/think>/g, "");
887
885
  }
888
886
  const knownToolNames2 = ctx && ctx.knownToolNames || /* @__PURE__ */ new Set();
889
887
  const segments = segmentChatReply(cleanContent, { knownToolNames: knownToolNames2 });
@@ -1207,7 +1205,8 @@
1207
1205
  args: data.args,
1208
1206
  success: data.success,
1209
1207
  output: data.output,
1210
- error: data.error
1208
+ error: data.error,
1209
+ durationMs: data.durationMs
1211
1210
  });
1212
1211
  }
1213
1212
  function flushStepEventBuffer(channelId, ctx) {
@@ -1547,6 +1546,8 @@
1547
1546
  var renderLoopStatusBar2 = (...args) => _getLS().renderLoopStatusBar?.(...args);
1548
1547
  var hideLoopStatusBar2 = (...args) => _getLS().hideLoopStatusBar?.(...args);
1549
1548
  var sidebarToggle = document.getElementById("sidebar-toggle");
1549
+ var sidebarCollapsedBar = document.getElementById("sidebar-collapsed-bar");
1550
+ var sidebarExpandBtn = document.getElementById("sidebar-expand-btn");
1550
1551
  var themeToggle = document.getElementById("theme-toggle");
1551
1552
  var channelList = document.getElementById("channel-list");
1552
1553
  var newChannelBtn = document.getElementById("new-channel-btn");
@@ -1720,13 +1721,16 @@
1720
1721
  isSidebarCollapsed = !isSidebarCollapsed;
1721
1722
  if (isSidebarCollapsed) {
1722
1723
  sidebar.classList.add("collapsed");
1724
+ if (sidebarCollapsedBar) sidebarCollapsedBar.classList.remove("hidden");
1723
1725
  } else {
1724
1726
  sidebar.classList.remove("collapsed");
1727
+ if (sidebarCollapsedBar) sidebarCollapsedBar.classList.add("hidden");
1725
1728
  }
1726
1729
  }
1727
1730
  function expandSidebar() {
1728
1731
  isSidebarCollapsed = false;
1729
1732
  sidebar.classList.remove("collapsed");
1733
+ if (sidebarCollapsedBar) sidebarCollapsedBar.classList.add("hidden");
1730
1734
  }
1731
1735
  async function loadChannels() {
1732
1736
  try {
@@ -1736,7 +1740,6 @@
1736
1740
  const text = await res.text().catch(() => "");
1737
1741
  if (!text.trim().startsWith("[") && !text.trim().startsWith("{")) {
1738
1742
  console.warn("[\u52A0\u8F7D\u9891\u9053] \u68C0\u6D4B\u5230 IPFS \u9759\u6001\u6A21\u5F0F (\u65E0\u540E\u7AEF server), \u529F\u80FD\u53D7\u9650");
1739
- showStaticModeNotice();
1740
1743
  return;
1741
1744
  }
1742
1745
  }
@@ -1762,17 +1765,6 @@
1762
1765
  console.error("[\u52A0\u8F7D\u9891\u9053] \u5931\u8D25:", err);
1763
1766
  }
1764
1767
  }
1765
- function showStaticModeNotice() {
1766
- if (document.getElementById("ipfs-static-notice")) return;
1767
- const notice = document.createElement("div");
1768
- notice.id = "ipfs-static-notice";
1769
- notice.style.cssText = "position:fixed;bottom:60px;left:50%;transform:translateX(-50%);z-index:9999;background:#1a1a18;border:1px solid #c4d640;color:#d8d8c8;padding:10px 16px;border-radius:8px;font-size:12px;box-shadow:0 4px 20px rgba(0,0,0,.5);max-width:560px;text-align:center;";
1770
- notice.innerHTML = `\u{1F4E1} <b style="color:#c4d640">IPFS \u9759\u6001\u6A21\u5F0F</b> \u2014 \u6B64\u9875\u9762\u901A\u8FC7 IPNS \u4ECE\u53BB\u4E2D\u5FC3\u5316\u7F51\u7EDC\u52A0\u8F7D.<br>\u5B8C\u6574\u529F\u80FD (\u5BF9\u8BDD/\u5DE5\u5177/\u5224\u65AD\u529B) \u9700\u8FDE\u63A5\u672C\u5730 Bolloon server: <code style="color:#c4d640">bolloon --web</code>`;
1771
- document.body.appendChild(notice);
1772
- setTimeout(() => {
1773
- notice.remove();
1774
- }, 15e3);
1775
- }
1776
1768
  var gwGroups = [];
1777
1769
  var gwNetworks = [];
1778
1770
  var gwActiveChat = null;
@@ -3232,6 +3224,13 @@ ${data.error || "channel not found"}`, "error");
3232
3224
  async function sendMessage() {
3233
3225
  const text = input.value.trim();
3234
3226
  if (!text) return;
3227
+ if (text === "/world" || text === "/chat") {
3228
+ const panel = text === "/world" ? "world" : "chat";
3229
+ if (typeof switchPanel === "function") switchPanel(panel);
3230
+ input.value = "";
3231
+ if (typeof showSimpleToast === "function") showSimpleToast(panel === "world" ? "\u{1F30D} \u5DF2\u5207\u6362\u5230\u4E16\u754C\u6A21\u5F0F" : "\u{1F4AC} \u5DF2\u5207\u6362\u5230\u804A\u5929\u6A21\u5F0F");
3232
+ return;
3233
+ }
3235
3234
  setSendMode("abort");
3236
3235
  const container = messagesContainers.get(currentChannelId) || messagesEl;
3237
3236
  addMessage2(text, "user", true, container);
@@ -3528,6 +3527,8 @@ ${data.error || "channel not found"}`, "error");
3528
3527
  { cmd: "goal", desc: "\u6682\u505C\u76EE\u6807 (park_goal)", args: "\u76EE\u6807ID; \u539F\u56E0" },
3529
3528
  { cmd: "skill", desc: "\u6C89\u6DC0\u6280\u80FD (create_skill)", args: "\u6280\u80FD\u540D; \u63CF\u8FF0; \u6B65\u9AA4" },
3530
3529
  { cmd: "add-friend", desc: "\u6DFB\u52A0 P2P \u597D\u53CB", args: "\u516C\u94A5; \u5907\u6CE8" },
3530
+ { cmd: "world", desc: "\u5207\u6362\u5230\u4E16\u754C\u6A21\u5F0F (\u673A\u4F1A\u6D41)", args: "" },
3531
+ { cmd: "chat", desc: "\u5207\u6362\u5230\u804A\u5929\u6A21\u5F0F (\u5BF9\u8BDD)", args: "" },
3531
3532
  { cmd: "help", desc: "\u663E\u793A\u53EF\u7528\u547D\u4EE4", args: "" }
3532
3533
  ];
3533
3534
  var slashDropdownEl = null;
@@ -3550,6 +3551,14 @@ ${data.error || "channel not found"}`, "error");
3550
3551
  slashBlockEnd = -1;
3551
3552
  }
3552
3553
  function applySlashCommand(cmdObj) {
3554
+ if (cmdObj.cmd === "world" || cmdObj.cmd === "chat") {
3555
+ const panel = cmdObj.cmd === "world" ? "world" : "chat";
3556
+ if (typeof switchPanel === "function") switchPanel(panel);
3557
+ input.value = "";
3558
+ closeSlashDropdown();
3559
+ if (typeof showSimpleToast === "function") showSimpleToast(panel === "world" ? "\u{1F30D} \u5DF2\u5207\u6362\u5230\u4E16\u754C\u6A21\u5F0F" : "\u{1F4AC} \u5DF2\u5207\u6362\u5230\u804A\u5929\u6A21\u5F0F");
3560
+ return;
3561
+ }
3553
3562
  const anchor = slashAnchor;
3554
3563
  const blockEnd = slashBlockEnd >= 0 ? slashBlockEnd : anchor + 1 + (getCurrentSlashQuery()?.query || "").length;
3555
3564
  if (anchor < 0 || anchor > input.value.length || input.value[anchor] !== "/") {
@@ -4084,6 +4093,9 @@ ${data.error || "channel not found"}`, "error");
4084
4093
  if (sidebarToggle) {
4085
4094
  sidebarToggle.addEventListener("click", toggleSidebar);
4086
4095
  }
4096
+ if (sidebarExpandBtn) {
4097
+ sidebarExpandBtn.addEventListener("click", expandSidebar);
4098
+ }
4087
4099
  if (newChannelBtn) {
4088
4100
  newChannelBtn.addEventListener("click", () => {
4089
4101
  createChannel("\u667A\u80FD\u4F53");
@@ -5002,7 +5014,9 @@ ${data.error || "channel not found"}`, "error");
5002
5014
  }
5003
5015
  var worldView = document.getElementById("world-view");
5004
5016
  var worldBtn = document.getElementById("world-btn");
5005
- var worldRefreshBtn = document.getElementById("world-refresh-btn");
5017
+ var worldIntentCol = document.getElementById("world-intent-col");
5018
+ var worldColToggle = document.getElementById("world-col-toggle");
5019
+ var worldColClose = document.getElementById("world-col-close");
5006
5020
  var worldIntentsList = document.getElementById("world-intents-list");
5007
5021
  var worldOpportunities = document.getElementById("world-opportunities");
5008
5022
  var worldStats = document.getElementById("world-stats");
@@ -5190,16 +5204,36 @@ ${data.error || "channel not found"}`, "error");
5190
5204
  if (tabWorld) tabWorld.addEventListener("click", () => switchPanel("world"));
5191
5205
  if (tabChat) tabChat.addEventListener("click", () => switchPanel("chat"));
5192
5206
  if (worldBtn) worldBtn.addEventListener("click", () => switchPanel("world"));
5193
- if (worldRefreshBtn) worldRefreshBtn.addEventListener("click", () => {
5194
- fetchWorldIntents();
5195
- refreshWorldOpportunities();
5207
+ function toggleWorldIntentCol(show) {
5208
+ if (!worldIntentCol) return;
5209
+ worldIntentCol.style.display = show ? "" : "none";
5210
+ if (worldColToggle) worldColToggle.textContent = show ? "\u25BE \u6211\u7684\u610F\u56FE" : "\u25B8 \u6211\u7684\u610F\u56FE";
5211
+ }
5212
+ if (worldColToggle) worldColToggle.addEventListener("click", () => {
5213
+ toggleWorldIntentCol(worldIntentCol ? worldIntentCol.style.display === "none" : false);
5196
5214
  });
5215
+ if (worldColClose) worldColClose.addEventListener("click", () => toggleWorldIntentCol(false));
5197
5216
  var worldInput = document.getElementById("world-input");
5198
5217
  var worldInputSend = document.getElementById("world-input-send");
5199
5218
  async function tellWorld() {
5200
5219
  const text = (worldInput && worldInput.value || "").trim();
5201
5220
  if (!text) return;
5221
+ if (text === "/chat" || text === "/world") {
5222
+ const panel = text === "/chat" ? "chat" : "world";
5223
+ if (typeof switchPanel === "function") switchPanel(panel);
5224
+ worldInput.value = "";
5225
+ if (typeof showSimpleToast === "function") showSimpleToast(panel === "world" ? "\u{1F30D} \u5DF2\u5207\u6362\u5230\u4E16\u754C\u6A21\u5F0F" : "\u{1F4AC} \u5DF2\u5207\u6362\u5230\u804A\u5929\u6A21\u5F0F");
5226
+ return;
5227
+ }
5202
5228
  worldInput.value = "";
5229
+ if (worldOpportunities) {
5230
+ const echo = document.createElement("div");
5231
+ echo.className = "world-opp-card";
5232
+ echo.style.cssText = "border:1px dashed var(--accent,#c4d640);border-radius:12px;padding:10px 14px;margin-bottom:10px;background:var(--bg-card,#222220);font-size:13px;color:var(--text,#f0f0ea);";
5233
+ echo.innerHTML = `<div style="font-weight:700;margin-bottom:4px;">\u270D \u4F60\u544A\u8BC9\u4E16\u754C</div><div>${String(text).replace(/</g, "&lt;")}</div><div style="color:#909088;font-size:11px;margin-top:6px;">\u5DF2\u58F0\u660E\u610F\u56FE \xB7 \u6B63\u5728\u4E3A\u4F60\u5339\u914D\u673A\u4F1A\u2026</div>`;
5234
+ worldOpportunities.prepend(echo);
5235
+ }
5236
+ if (typeof showSimpleToast === "function") showSimpleToast("\u{1F30D} \u5DF2\u544A\u8BC9\u4E16\u754C\uFF1A" + text, "success");
5203
5237
  try {
5204
5238
  const res = await fetch("/api/intents", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text, priority: 3 }) });
5205
5239
  if (!res.ok) {
@@ -5207,6 +5241,11 @@ ${data.error || "channel not found"}`, "error");
5207
5241
  return;
5208
5242
  }
5209
5243
  await fetchWorldIntents();
5244
+ try {
5245
+ await fetch("/api/world/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ topic: text }) });
5246
+ } catch (e) {
5247
+ console.info("[world] \u4E3B\u52A8\u641C\u7D22\u5931\u8D25", e);
5248
+ }
5210
5249
  await refreshWorldOpportunities();
5211
5250
  } catch (e) {
5212
5251
  console.info("[world] \u58F0\u660E\u5931\u8D25", e);
@@ -125,10 +125,19 @@
125
125
  <span class="status-text">已连接</span>
126
126
  </div>
127
127
  </div>
128
- </aside>
128
+ </aside>
129
+
130
+ <!-- 2026-10-06 (用户): 左侧导航收起的恢复条 — 收起后滑出的方式不留下展开按钮就打不开, 补窄条 + 展开按钮 -->
131
+ <div class="sidebar-collapsed hidden" id="sidebar-collapsed-bar">
132
+ <button class="expand-btn" id="sidebar-expand-btn" title="展开侧边栏">
133
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
134
+ <polyline points="9 18 15 12 9 6"></polyline>
135
+ </svg>
136
+ </button>
137
+ </div>
129
138
 
130
- <main class="main">
131
- <header class="header">
139
+ <main class="main">
140
+ <header class="header">
132
141
  <div class="header-left">
133
142
  <span class="channel-indicator" id="channel-indicator"></span>
134
143
  <h1 id="channel-name">Bolloon Agent</h1>
@@ -421,28 +430,31 @@
421
430
  <!-- World View (2026-10-05, leo Intent Network) — 主页内嵌面板 (不再全屏跳转):
422
431
  顶部 tab 切换 世界/聊天, 默认世界; 无嵌套无跳转 -->
423
432
  <div id="world-view" style="position:relative;background:var(--bg,#1a1a18);display:flex;flex-direction:column;flex:1;min-height:0;">
424
- <!-- 面板头 (只剩刷新; 关闭按钮已由顶部 tab 取代) -->
425
- <div style="display:flex;align-items:center;justify-content:space-between;padding:8px 18px;border-bottom:1px solid var(--border,#3a3a36);flex-shrink:0;">
426
- <div style="font-weight:700;font-size:14px;">🌍 World <span style="color:#606058;font-weight:400;font-size:11px;">世界在给你送东西 · 自动流入</span></div>
427
- <button id="world-refresh-btn" class="btn-secondary btn-sm" style="font-size:11px;padding:2px 10px;">⟳ 刷新</button>
428
- </div>
429
- <!-- Twitter 式两栏 (2026-10-05: 右侧输入栏删除 — 匹配是内置自动算法, 不需要用户填写) -->
430
- <div style="display:flex;flex:1;overflow:hidden;">
431
- <!-- 左栏: 我的意图 (nav 风格, 只读展示) -->
432
- <aside style="width:220px;border-right:1px solid var(--border,#3a3a36);overflow-y:auto;padding:12px;flex-shrink:0;display:none;" id="world-left-col">
433
- <div style="font-size:12px;color:#909088;font-weight:600;margin-bottom:8px;">我的意图 (Intent)</div>
434
- <div id="world-intents-list" style="margin-bottom:12px;"><div class="form-hint">(还没有意图)</div></div>
435
- <div style="font-size:12px;color:#909088;font-weight:600;margin-bottom:8px;">世界变化</div>
436
- <div id="world-stats" style="font-size:12px;color:#606058;"></div>
437
- </aside>
438
- <!-- 中栏: 机会时间线 (Twitter feed 自动流入, 全宽) -->
439
- <main style="flex:1;overflow-y:auto;padding:16px 24px;max-width:760px;margin:0 auto;width:100%;">
440
- <div style="font-size:12px;color:#909088;margin-bottom:12px;">✦ 世界在给你送东西 (自动匹配, 持续流入)</div>
441
- <div id="world-opportunities" style="min-height:120px;">
442
- <div class="form-hint">世界正在观察…</div>
443
- </div>
444
- </main>
445
- </div>
433
+ <!-- 面板头 (刷新按钮已按用户要求去掉; 内容自动流入) -->
434
+ <div style="display:flex;align-items:center;justify-content:space-between;padding:8px 18px;border-bottom:1px solid var(--border,#3a3a36);flex-shrink:0;">
435
+ <div style="font-weight:700;font-size:14px;">🌍 World <span style="color:#606058;font-weight:400;font-size:11px;">世界在给你送东西 · 自动流入</span></div>
436
+ <button id="world-col-toggle" class="btn-secondary btn-sm" style="font-size:11px;padding:3px 12px;" title="展开/折叠我的意图栏">▸ 我的意图</button>
437
+ </div>
438
+ <!-- Twitter 式两栏: 左 = 机会时间线, 右 = 我的意图 (可折叠, 2026-10-06 用户要求右置 + 折叠) -->
439
+ <div style="display:flex;flex:1;overflow:hidden;">
440
+ <!-- 左栏: 机会时间线 (Twitter feed 自动流入, 全宽) -->
441
+ <main style="flex:1;overflow-y:auto;padding:16px 24px;max-width:760px;margin:0 auto;width:100%;">
442
+ <div style="font-size:12px;color:#909088;margin-bottom:12px;">✦ 世界在给你送东西 (自动匹配, 持续流入)</div>
443
+ <div id="world-opportunities" style="min-height:120px;">
444
+ <div class="form-hint">世界正在观察…</div>
445
+ </div>
446
+ </main>
447
+ <!-- 右栏: 我的意图 (nav 风格, 可折叠隐藏) -->
448
+ <aside style="width:240px;border-left:1px solid var(--border,#3a3a36);overflow-y:auto;padding:12px;flex-shrink:0;transition:width .2s;" id="world-intent-col">
449
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
450
+ <div style="font-size:12px;color:#909088;font-weight:600;">我的意图 (Intent)</div>
451
+ <button id="world-col-close" class="btn-secondary btn-sm" style="font-size:10px;padding:1px 7px;" title="折叠意图栏">✕</button>
452
+ </div>
453
+ <div id="world-intents-list" style="margin-bottom:12px;"><div class="form-hint">(还没有意图)</div></div>
454
+ <div style="font-size:12px;color:#909088;font-weight:600;margin-bottom:8px;">世界变化</div>
455
+ <div id="world-stats" style="font-size:12px;color:#606058;"></div>
456
+ </aside>
457
+ </div>
446
458
  <!-- 2026-10-05 (leo): 世界底部输入框 — 想做什么直接说, 自动触发匹配 (不是聊天输入框) -->
447
459
  <div class="world-input-bar" style="display:flex;gap:8px;align-items:center;padding:10px 18px;border-top:1px solid var(--border,#3a3a36);flex-shrink:0;background:var(--bg-sidebar,#222220);">
448
460
  <input type="text" id="world-input" placeholder="告诉世界你在做什么 / 需要什么… (Enter 自动匹配)" autocomplete="off"