@bolloon/bolloon-agent 0.5.4 → 0.5.5

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 (126) hide show
  1. package/dist/agents/agent-identity-store.js +41 -5
  2. package/dist/agents/background-notices.js +124 -0
  3. package/dist/agents/channel-identity.js +54 -0
  4. package/dist/agents/code-write-gate.js +47 -0
  5. package/dist/agents/deny-pipeline.js +11 -2
  6. package/dist/agents/egress-sanitize.js +69 -0
  7. package/dist/agents/error-classifier.js +67 -5
  8. package/dist/agents/experience-review.js +226 -0
  9. package/dist/agents/gateway-group.js +153 -18
  10. package/dist/agents/iteration-budget.js +64 -0
  11. package/dist/agents/lesson-to-skill.js +211 -0
  12. package/dist/agents/managed-services.js +64 -0
  13. package/dist/agents/parse-tool-call.js +38 -32
  14. package/dist/agents/pi-sdk-session-manager.js +72 -3
  15. package/dist/agents/pi-sdk-tools.js +201 -32
  16. package/dist/agents/pi-sdk.js +463 -21
  17. package/dist/agents/plan-store.js +36 -0
  18. package/dist/agents/process-runner.js +64 -2
  19. package/dist/agents/shell-guard.js +109 -13
  20. package/dist/agents/skill-health.js +90 -0
  21. package/dist/agents/skill-ledger.js +129 -0
  22. package/dist/agents/tool-loop-guard.js +303 -0
  23. package/dist/agents/tool-path-args.js +53 -0
  24. package/dist/agents/tool-result-gate.js +50 -0
  25. package/dist/agents/tool-subset.js +109 -0
  26. package/dist/agents/tool-telemetry.js +97 -0
  27. package/dist/bootstrap/context-collector.js +37 -7
  28. package/dist/bootstrap/lifecycle-hooks.js +12 -3
  29. package/dist/bootstrap/persona-init.js +174 -0
  30. package/dist/bootstrap/persona-loader.js +5 -1
  31. package/dist/cli/ink-app.js +153 -6
  32. package/dist/cli/input-paste.js +218 -0
  33. package/dist/cli/loading-tui.js +3 -1
  34. package/dist/cli/log-gate.js +7 -0
  35. package/dist/cli/mention-data.js +30 -0
  36. package/dist/cli/reasoning-view.js +103 -0
  37. package/dist/cli/startup-notice.js +25 -0
  38. package/dist/cli/status-segments.js +87 -2
  39. package/dist/cli/trace-line.js +54 -0
  40. package/dist/context-compaction/auto-compact.js +37 -2
  41. package/dist/electron-build/agents/agent-identity-store.js +43 -5
  42. package/dist/electron-build/agents/agent-identity-store.js.map +1 -1
  43. package/dist/electron-build/agents/background-notices.js +138 -0
  44. package/dist/electron-build/agents/background-notices.js.map +1 -0
  45. package/dist/electron-build/agents/channel-identity.js +64 -0
  46. package/dist/electron-build/agents/channel-identity.js.map +1 -0
  47. package/dist/electron-build/agents/code-write-gate.js +55 -0
  48. package/dist/electron-build/agents/code-write-gate.js.map +1 -0
  49. package/dist/electron-build/agents/deny-pipeline.js +11 -2
  50. package/dist/electron-build/agents/deny-pipeline.js.map +1 -1
  51. package/dist/electron-build/agents/egress-sanitize.js +76 -0
  52. package/dist/electron-build/agents/egress-sanitize.js.map +1 -0
  53. package/dist/electron-build/agents/error-classifier.js +68 -5
  54. package/dist/electron-build/agents/error-classifier.js.map +1 -1
  55. package/dist/electron-build/agents/experience-review.js +243 -0
  56. package/dist/electron-build/agents/experience-review.js.map +1 -0
  57. package/dist/electron-build/agents/iteration-budget.js +73 -0
  58. package/dist/electron-build/agents/iteration-budget.js.map +1 -0
  59. package/dist/electron-build/agents/lesson-to-skill.js +228 -0
  60. package/dist/electron-build/agents/lesson-to-skill.js.map +1 -0
  61. package/dist/electron-build/agents/managed-services.js +69 -0
  62. package/dist/electron-build/agents/managed-services.js.map +1 -0
  63. package/dist/electron-build/agents/parse-tool-call.js +38 -32
  64. package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
  65. package/dist/electron-build/agents/pi-sdk-session-manager.js +75 -3
  66. package/dist/electron-build/agents/pi-sdk-session-manager.js.map +1 -1
  67. package/dist/electron-build/agents/pi-sdk-tools.js +200 -31
  68. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  69. package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
  70. package/dist/electron-build/agents/pi-sdk.js +464 -21
  71. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  72. package/dist/electron-build/agents/plan-store.js +37 -0
  73. package/dist/electron-build/agents/plan-store.js.map +1 -1
  74. package/dist/electron-build/agents/process-runner.js +71 -2
  75. package/dist/electron-build/agents/process-runner.js.map +1 -1
  76. package/dist/electron-build/agents/shell-guard.js +110 -14
  77. package/dist/electron-build/agents/shell-guard.js.map +1 -1
  78. package/dist/electron-build/agents/skill-health.js +103 -0
  79. package/dist/electron-build/agents/skill-health.js.map +1 -0
  80. package/dist/electron-build/agents/skill-ledger.js +146 -0
  81. package/dist/electron-build/agents/skill-ledger.js.map +1 -0
  82. package/dist/electron-build/agents/tool-loop-guard.js +319 -0
  83. package/dist/electron-build/agents/tool-loop-guard.js.map +1 -0
  84. package/dist/electron-build/agents/tool-path-args.js +63 -0
  85. package/dist/electron-build/agents/tool-path-args.js.map +1 -0
  86. package/dist/electron-build/agents/tool-result-gate.js +59 -0
  87. package/dist/electron-build/agents/tool-result-gate.js.map +1 -0
  88. package/dist/electron-build/agents/tool-subset.js +116 -0
  89. package/dist/electron-build/agents/tool-subset.js.map +1 -0
  90. package/dist/electron-build/agents/tool-telemetry.js +108 -0
  91. package/dist/electron-build/agents/tool-telemetry.js.map +1 -0
  92. package/dist/electron-build/bootstrap/context-collector.js +38 -7
  93. package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
  94. package/dist/electron-build/bootstrap/lifecycle-hooks.js +12 -3
  95. package/dist/electron-build/bootstrap/lifecycle-hooks.js.map +1 -1
  96. package/dist/electron-build/bootstrap/persona-init.js +216 -0
  97. package/dist/electron-build/bootstrap/persona-init.js.map +1 -0
  98. package/dist/electron-build/bootstrap/persona-loader.js +5 -1
  99. package/dist/electron-build/bootstrap/persona-loader.js.map +1 -1
  100. package/dist/electron-build/context-compaction/auto-compact.js +38 -2
  101. package/dist/electron-build/context-compaction/auto-compact.js.map +1 -1
  102. package/dist/electron-build/external-engines/delegate.js +22 -0
  103. package/dist/electron-build/external-engines/delegate.js.map +1 -1
  104. package/dist/electron-build/orbitdb/cid-database.js +55 -0
  105. package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
  106. package/dist/electron-build/orbitdb/ipfs-node.js +36 -11
  107. package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
  108. package/dist/electron-build/web/server-storage.js +26 -5
  109. package/dist/electron-build/web/server-storage.js.map +1 -1
  110. package/dist/external-engines/delegate.js +21 -0
  111. package/dist/index.js +503 -77
  112. package/dist/orbitdb/cid-database.js +55 -0
  113. package/dist/orbitdb/event-index.js +370 -0
  114. package/dist/orbitdb/event-query.js +131 -0
  115. package/dist/orbitdb/event-shell.js +510 -0
  116. package/dist/orbitdb/group-access.js +381 -0
  117. package/dist/orbitdb/group-shards.js +140 -0
  118. package/dist/orbitdb/ipfs-node.js +36 -11
  119. package/dist/web/bolloon-web.json +3 -3
  120. package/dist/web/client.js +3 -2
  121. package/dist/web/mobile-core.js +7762 -6289
  122. package/dist/web/routes-mobile-tasks.js +2 -1
  123. package/dist/web/server-storage.js +26 -5
  124. package/dist/web/server.js +2 -0
  125. package/dist/web/sw.js +1 -1
  126. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -2,7 +2,9 @@ import { createHyperswarmCommunicator, createTopic, KeyManager, AgentAuthManager
2
2
  import { irohTransport } from './network/iroh-transport.js';
3
3
  import { getLastAiTiming, getLastAiUsage } from './llm/pi-ai.js';
4
4
  import { setHistoryScope } from './cli/input-history.js';
5
- import { statusSegments, fitSegments, rightAlignPad } from './cli/status-segments.js';
5
+ import { statusSegments, fitSegments, rightAlignPad, dispWidthSafe, statusLineBudget } from './cli/status-segments.js';
6
+ import { renderReasoning } from './cli/reasoning-view.js';
7
+ import { traceLabel } from './cli/trace-line.js';
6
8
  import { loadWalletTool } from './agents/wallet-tools.js';
7
9
  import { HybridMessenger } from './network/hybrid-messenger.js';
8
10
  import * as ed25519 from '@noble/ed25519';
@@ -29,10 +31,83 @@ import { installStartupLogGate, isStartupVerbose, startupLogPath, VERBOSE_ENV, c
29
31
  // 2026-09-27: 回复流卫生 — 内部运行日志 (循环推进/运行登记/收尾计数…) 不进对话回复流, 改落日志文件
30
32
  import { isInternalRunLog, appendInternalRunLog } from './cli/reply-hygiene.js';
31
33
  // 2026-09-27: 启动**前言 / 就绪度报告**闸门 (默认不上屏 → 面板一行 + 显式查询命令; 失败折成面板提示)
32
- import { noticeLine as noticePreamble, flushStartupNotices, setStartupReadiness, alertsFromSetup, readinessLine, preambleHintLine, startupPreambleVisible, pushStartupAlert, takeStartupPanelNotes, clearBufferedNotices, } from './cli/startup-notice.js';
34
+ import { onlyConnectivityRetest, SETUP_STATUS_CMD, noticeLine as noticePreamble, flushStartupNotices, setStartupReadiness, alertsFromSetup, readinessLine, preambleHintLine, startupPreambleVisible, pushStartupAlert, takeStartupPanelNotes, clearBufferedNotices, } from './cli/startup-notice.js';
33
35
  import { clipboardWrite } from './cli/clipboard.js';
34
36
  // 启动自动检查更新:后台、节流、检测到新版本自动安装(可被 --no-update / BOLLOON_SKIP_UPDATE 关闭)
35
37
  import { createRequire } from 'module';
38
+ export function parseGroupSub(input) {
39
+ const rest = String(input || '').replace(/^\/group\b/i, '').trim();
40
+ if (!rest)
41
+ return { sub: 'list', arg: '' };
42
+ const sp = rest.indexOf(' ');
43
+ const head = (sp === -1 ? rest : rest.slice(0, sp)).toLowerCase();
44
+ const arg = sp === -1 ? '' : rest.slice(sp + 1).trim();
45
+ const aliases = { ls: 'list', l: 'list', n: 'new', create: 'new', add: 'join', msg: 'send', history: 'log', use_: 'use' };
46
+ const sub = aliases[head] || head;
47
+ // 2026-10-01 (用户: 「group 命令无法选中进群, 要可以选中」): 裸序号 ⇒ 直接选中该群
48
+ // 与 `/channel <序号>` 同一个习惯 ✓ (列表里就带序号)
49
+ if (/^\d+$/.test(head))
50
+ return { sub: 'use', arg: head };
51
+ const known = ['list', 'new', 'join', 'leave', 'use', 'send', 'log', 'members', 'help'];
52
+ return { sub: known.includes(sub) ? sub : 'help', arg: known.includes(sub) ? arg : rest };
53
+ }
54
+ export const CLI_KNOWN_COMMAND_HEADS = new Set([
55
+ '/group',
56
+ '/agent',
57
+ '/answer',
58
+ '/approve',
59
+ '/contacts',
60
+ '/copy',
61
+ '/criteria',
62
+ '/cron',
63
+ '/dequeue',
64
+ '/did',
65
+ '/dq',
66
+ '/dream',
67
+ '/email',
68
+ '/fork',
69
+ '/goal',
70
+ '/goals',
71
+ '/help',
72
+ '/insight',
73
+ '/ipfs',
74
+ '/ipns',
75
+ '/judgement',
76
+ '/judgments',
77
+ '/login',
78
+ '/logout',
79
+ '/loop',
80
+ '/mcp',
81
+ '/memory',
82
+ '/model',
83
+ '/net',
84
+ '/now',
85
+ '/p2p',
86
+ '/pause',
87
+ '/payments',
88
+ '/plan',
89
+ '/q',
90
+ '/questions',
91
+ '/queue',
92
+ '/reject',
93
+ '/resume',
94
+ '/runs',
95
+ '/session',
96
+ '/sessions',
97
+ '/setup',
98
+ '/skill',
99
+ '/skills',
100
+ '/suggestions',
101
+ '/supervise',
102
+ '/todo',
103
+ '/tools',
104
+ '/trace',
105
+ '/tx',
106
+ '/wake',
107
+ '/wallet',
108
+ '/wiki',
109
+ '/x402',
110
+ ]);
36
111
  const _require = createRequire(import.meta.url);
37
112
  const _BOLLOON_VERSION = (() => {
38
113
  try {
@@ -68,6 +143,75 @@ const MOVE_UP = '\x1b[A';
68
143
  const CLEAR_LINE = '\x1b[2K';
69
144
  const HIDE_CURSOR = '\x1b[?25l';
70
145
  const SHOW_CURSOR = '\x1b[?25h';
146
+ /**
147
+ * 统一出口 (2026-10-01, 用户: 「还是UI有污染」)。
148
+ * 为什么: Ink 被显式设成 `patchConsole: false` ⇒ **不接管 console** ✗ ⇒ 运行期任何 `console.log`
149
+ * 都写在 Ink 托管区**之外** ⇒ 每次直写都把正在渲染的 footer(提示行 + 分界线)往下推一份 ✗
150
+ * ⇒ 屏幕上重复出现「· 回车发送 · ↑↓ 历史 · PgUp 回看 · Esc 双击退出」✓(真机实测就是这个)。
151
+ * 判据复用已有的 `startupPanelReady`(启动面板画好了 = Ink 已在跑 ✓):
152
+ * Ink 在跑 ⇒ **一律走它自己的消息流**; 没跑(boot 期)⇒ 才直写 ✓。
153
+ */
154
+ function writeOut(line) {
155
+ // 2026-10-01 (用户: 「时间好像是面板后面启动的」+「继续,还没去掉」):
156
+ // **关键**: 启动那几步**不是 await 的** ✗(fire-and-forget)⇒ 它们常在 `startupPanelReady = true`
157
+ // **之后**才打印 ✓ ⇒ 用"面板好了没"当判据必然漏 ✗(我上一版就栽在这 ✓)。
158
+ // 改成**独立的启动期标志** `bootPhase` ✓: 启动期内的**进度类**输出一律只落盘 ✓, 与面板状态无关 ✓。
159
+ if (bootPhase) {
160
+ bootLogOnly(line);
161
+ return;
162
+ }
163
+ if (startupPanelReady) {
164
+ try {
165
+ appendLine(line);
166
+ return;
167
+ }
168
+ catch { /* 落到落盘 */ }
169
+ }
170
+ // 2026-10-01 (用户: 「启动那几行……继续,还没去掉」): **启动期的"进度类"输出直接不进屏** ✓
171
+ // 上一版我把它们**搬进对话流**了 ✗ —— 但用户要的是**去掉** ✓(面板已经把版本/模型/技能这些要点画了 ✓,
172
+ // 而 `复用 DID` / `iroh:` / `主题:` / `[N/5]` 这类属于**过程细节** ✓)。
173
+ // 现在: 启动期 ⇒ **只落日志**(~/.bolloon/logs/startup.log ✓ 事后照样能查 ✓), 不上屏 ✓。
174
+ bootLogOnly(line);
175
+ }
176
+ /**
177
+ * 启动期**警告/错误**仍然要上屏 ✓ (不许因为"想干净"把问题藏起来 ✗):
178
+ * Ink 没起 ⇒ 先缓冲, `flushBootBuffer()` 在 Ink 起来后灌进对话流 ✓; Ink 已在跑 ⇒ 直接进 ✓。
179
+ */
180
+ function writeOutWarn(line) {
181
+ if (startupPanelReady) {
182
+ try {
183
+ appendLine(line);
184
+ return;
185
+ }
186
+ catch { /* 落到缓冲 */ }
187
+ }
188
+ if (bootBuffer.length < 500) {
189
+ bootBuffer.push(line);
190
+ return;
191
+ }
192
+ console.log(line);
193
+ }
194
+ /** 启动期进度类输出: 只落盘(不上屏 ✓) */
195
+ function bootLogOnly(line) {
196
+ try {
197
+ const clean = String(line).replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
198
+ fsSync.appendFileSync(startupLogPath(), `[boot] ${clean}\n`);
199
+ }
200
+ catch { /* 落盘失败也不上屏(它本来就是过程细节 ✓) */ }
201
+ }
202
+ /** 启动期"警告/错误"缓冲: Ink 起来之前攒着, `flushBootBuffer()` 一次性灌进对话流 ✓ */
203
+ const bootBuffer = [];
204
+ export function flushBootBuffer() {
205
+ while (bootBuffer.length) {
206
+ const line = bootBuffer.shift();
207
+ try {
208
+ appendLine(line);
209
+ }
210
+ catch {
211
+ console.log(line);
212
+ }
213
+ }
214
+ }
71
215
  const s = {
72
216
  banner: () => {
73
217
  printBanner(_BOLLOON_VERSION);
@@ -78,52 +222,48 @@ const s = {
78
222
  status === 'warn' ? `${YELLOW}⚠` :
79
223
  status === 'error' ? `${MAGENTA}✗` :
80
224
  `${CYAN}●`;
81
- console.log(` ${check} ${WHITE}[${num}/${total}]${GRAY} ${text}${RESET}`);
225
+ writeOut(` ${check} ${WHITE}[${num}/${total}]${GRAY} ${text}${RESET}`);
82
226
  },
83
- success: (text) => console.log(` ${GREEN}✓${RESET} ${text}`),
84
- warn: (text) => console.log(` ${YELLOW}⚠${RESET} ${text}`),
85
- error: (text) => console.log(` ${MAGENTA}✗${RESET} ${text}`),
86
- info: (text) => console.log(` ${CYAN}●${RESET} ${text}`),
227
+ success: (text) => writeOut(` ${GREEN}✓${RESET} ${text}`),
228
+ warn: (text) => writeOutWarn(` ${YELLOW}⚠${RESET} ${text}`),
229
+ error: (text) => writeOutWarn(` ${MAGENTA}✗${RESET} ${text}`),
230
+ info: (text) => writeOut(` ${CYAN}●${RESET} ${text}`),
87
231
  section: (title) => {
88
- console.log(`\n${BLUE}━━━ ${WHITE}${BOLD}${title}${RESET} ${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
232
+ writeOut(`\n${BLUE}━━━ ${WHITE}${BOLD}${title}${RESET} ${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
89
233
  },
90
- divider: () => console.log(`\n${GRAY}${'─'.repeat(50)}${RESET}\n`),
91
- prompt: (text) => console.log(`\n${CYAN}❯ ${WHITE}${text}${RESET}`),
234
+ divider: () => writeOut(`\n${GRAY}${'─'.repeat(50)}${RESET}\n`),
235
+ prompt: (text) => writeOut(`\n${CYAN}❯ ${WHITE}${text}${RESET}`),
92
236
  response: (label, content) => {
93
- console.log(`\n${GREEN}${label}${RESET}\n${content}\n`);
237
+ writeOut(`\n${GREEN}${label}${RESET}\n${content}\n`);
94
238
  },
95
239
  agentCard: (agent) => {
96
240
  const statusColor = agent.status === 'active' ? GREEN :
97
241
  agent.status === 'idle' ? YELLOW :
98
242
  agent.status === 'busy' ? MAGENTA : GRAY;
99
- console.log(` ${WHITE}${BOLD}${agent.name}${RESET}`);
100
- console.log(` ${GRAY}ID:${RESET} ${agent.id}`);
101
- console.log(` ${GRAY}状态:${RESET} ${statusColor}${agent.status}${RESET}`);
102
- console.log(` ${GRAY}能力:${RESET} ${agent.capabilities.join(', ')}`);
243
+ writeOut(` ${WHITE}${BOLD}${agent.name}${RESET}`);
244
+ writeOut(` ${GRAY}ID:${RESET} ${agent.id}`);
245
+ writeOut(` ${GRAY}状态:${RESET} ${statusColor}${agent.status}${RESET}`);
246
+ writeOut(` ${GRAY}能力:${RESET} ${agent.capabilities.join(', ')}`);
103
247
  if (agent.did)
104
- console.log(` ${GRAY}DID:${RESET} ${agent.did}`);
105
- console.log();
248
+ writeOut(` ${GRAY}DID:${RESET} ${agent.did}`);
249
+ writeOut('');
106
250
  },
107
251
  Thinking: () => {
108
- const frames = ['(`・ω・´)', '(´・_・`)', '(。•́︿•̀。)', 'ᕙ(▀̿̿Ĺ̯̿̿▀̿ ̿)ᕗ', '(◕‿◕)'];
109
- let i = 0;
110
- let dots = 0;
111
- const frame = frames[0];
112
- appendLine(` ${frame} 思考...`);
113
- return setInterval(() => {
114
- i = (i + 1) % frames.length;
115
- dots = (dots + 1) % 4;
116
- const dotStr = '.'.repeat(dots || 1);
117
- appendLine(`\r ${frames[i]} 思考${dotStr} `);
118
- }, 600);
252
+ // 2026-10-01 (用户: 「颜文字被加载进去了」/「可能是 ink 渲染的问题」): **真凶在这里** ✗ ——
253
+ // 原来每一帧都走 `appendLine` ⇒ 而对话流是 Ink 的 `<Static>`(**写一次永不重绘** ✓)
254
+ // ⇒ 于是每 600ms 就有一条「思考中...」被**永久烙进滚动区** ✗, 带 `\r` 也擦不掉 ✗,
255
+ // 滚出来的残影还能被框选/拖进输入框 ⇒ 混进上下文 ✓。
256
+ // 现在改成走 **Ink 的瞬时行**(footer 自己帧进动画 ✓, 只重绘一行 ✓, 不落进 Static ✓)。
257
+ inkSetThinking(true);
258
+ return setInterval(() => { }, 600);
119
259
  },
120
260
  clearThinking: (interval) => {
121
261
  clearInterval(interval);
122
- process.stdout.write('\r' + ' '.repeat(40) + '\r');
262
+ inkSetThinking(false);
123
263
  },
124
264
  dialog: async (title, promptText) => {
125
265
  return new Promise((resolve) => {
126
- console.log(renderDialog({ title, prompt: promptText }));
266
+ writeOut(renderDialog({ title, prompt: promptText }));
127
267
  const rl = readline.createInterface({
128
268
  input: process.stdin,
129
269
  output: process.stdout
@@ -149,6 +289,14 @@ ed25519.hashes.sha512 = sha512;
149
289
  let startupPanelFirst = false;
150
290
  /** 面板第一帧已经画出来了 (startCLI 取走面板提示之后): 晚到的降级提示直接追加进面板 */
151
291
  let startupPanelReady = false;
292
+ /**
293
+ * **启动期**标志 (2026-10-01): 与 `startupPanelReady` **不是一回事** ✗ ——
294
+ * 启动那几步是 fire-and-forget, 打印时间常在面板之后 ✓ ⇒ 只有用独立标志才拦得住 ✓。
295
+ * 结束条件: **用户第一次发消息** ✓(他真正接管了)或启动后 **20s 兜底** ✓。
296
+ */
297
+ let bootPhase = true;
298
+ function endBootPhase() { bootPhase = false; }
299
+ setTimeout(() => { bootPhase = false; }, 20_000).unref?.();
152
300
  /**
153
301
  * 2026-09-30: 启动面板 / 就绪行**各只许追加一次**.
154
302
  * 防任何重入路径 (会话切换 / 重复 boot / 恢复) 把面板再叠一条 —— leo 报的「顶框重复 14 次」那一类
@@ -235,8 +383,8 @@ async function bootstrapIdentity() {
235
383
  const username = getUserName();
236
384
  const suffix = did.split(':').pop()?.substring(0, 4);
237
385
  const name = `blln-${username}-${suffix}`;
238
- console.log(` ${reused ? GRAY + '复用 ' : ''}${GRAY}DID:${RESET} ${did}`);
239
- console.log(` ${GRAY}名称:${RESET} ${name}`);
386
+ writeOut(` ${reused ? GRAY + '复用 ' : ''}${GRAY}DID:${RESET} ${did}`);
387
+ writeOut(` ${GRAY}名称:${RESET} ${name}`);
240
388
  s.step(1, 5, reused ? '复用 DIAP 身份' : '生成 DIAP 身份', 'ok');
241
389
  return { keypair: kp, did, name };
242
390
  }
@@ -297,7 +445,7 @@ async function bootstrapP2P(verifier) {
297
445
  await comm.start();
298
446
  const topic = createTopic('bolloon-agent-harness');
299
447
  await comm.joinTopic(topic);
300
- console.log(` ${GRAY}主题:${RESET} ${topic.slice(0, 8).toString('hex')}...`);
448
+ writeOut(` ${GRAY}主题:${RESET} ${topic.slice(0, 8).toString('hex')}...`);
301
449
  s.step(3, 5, '启动 P2P 网络', 'ok');
302
450
  return comm;
303
451
  }
@@ -315,20 +463,20 @@ async function bootstrapIroh(keypair, name) {
315
463
  s.step(4, 5, '启动 iroh P2P', 'loading');
316
464
  try {
317
465
  const node = await irohTransport.start();
318
- console.log(` ${GRAY}iroh:${RESET} ${node.nodeId.substring(0, 16)}...`);
466
+ writeOut(` ${GRAY}iroh:${RESET} ${node.nodeId.substring(0, 16)}...`);
319
467
  hybridMessenger = new HybridMessenger({
320
468
  preferIrohForLarge: true,
321
469
  largeThresholdBytes: 64 * 1024,
322
470
  enableRelay: true,
323
471
  });
324
472
  hybridMessenger.onMessage('task', async (msg) => {
325
- console.log(`[iroh] Task from ${msg.from.substring(0, 12)}...: ${new TextDecoder().decode(msg.payload).substring(0, 50)}...`);
473
+ writeOut(`[iroh] Task from ${msg.from.substring(0, 12)}...: ${new TextDecoder().decode(msg.payload).substring(0, 50)}...`);
326
474
  });
327
475
  hybridMessenger.onMessage('blob', async (msg) => {
328
- console.log(`[iroh] Blob from ${msg.from.substring(0, 12)}...: ${msg.payload.length} bytes`);
476
+ writeOut(`[iroh] Blob from ${msg.from.substring(0, 12)}...: ${msg.payload.length} bytes`);
329
477
  });
330
478
  hybridMessenger.onMessage('response', async (msg) => {
331
- console.log(`[iroh] Response from ${msg.from.substring(0, 12)}...`);
479
+ writeOut(`[iroh] Response from ${msg.from.substring(0, 12)}...`);
332
480
  });
333
481
  if (agentIdentity) {
334
482
  agentIdentity.irohNodeId = node.nodeId;
@@ -337,13 +485,15 @@ async function bootstrapIroh(keypair, name) {
337
485
  }
338
486
  catch (e) {
339
487
  s.step(4, 5, '启动 iroh P2P', 'warn');
340
- console.log(` ${YELLOW}iroh 启动失败: ${e.message}${RESET}`);
341
- console.log(` ${GRAY}继续使用 Hyperswarm P2P${RESET}`);
488
+ writeOut(` ${YELLOW}iroh 启动失败: ${e.message}${RESET}`);
489
+ writeOut(` ${GRAY}继续使用 Hyperswarm P2P${RESET}`);
342
490
  }
343
491
  }
344
492
  // ---------------------------------------------------------------------------
345
493
  // Agent 懒加载
346
494
  // ---------------------------------------------------------------------------
495
+ // 2026-10-01: /group 的"当前群"(进程内; 重启后需重新 /group use)
496
+ let currentGroupId;
347
497
  let agent = null;
348
498
  /**
349
499
  * 退出存档 (2026-09-30, leo: 「按 ctrl+c / esc 退出的时候, 自动存档 session 进程, 最后显示再见👋」)
@@ -488,15 +638,34 @@ async function getAgent() {
488
638
  catch { /* 读不到就退默认 */ }
489
639
  }
490
640
  let identityDoc;
491
- if (chIdentity?.did && chIdentity.publicKey) {
492
- // channel 已有持久 DID → 用 channel 身份
641
+ // 2026-10-01 (用户实测: 两个不同 channel 都答同一个名字, get_identity 的 DID 为空):
642
+ // 旧逻辑要求 channel 记录**同时**有 did + publicKey, 否则落到 `agentIdentity`(进程级**共享**) ⇒
643
+ // 多个 channel 共用一个身份 ✗; 且 channel 里可能存着假 DID (did:local:…) 或干脆没有 did。
644
+ // 现在: 归属以 channel 的 agentId 为准 —— 现取现造**该 agent 的**真 did:key, 名字用**它自己的** persona。
645
+ const { channelIdentityUsable, nameForChannel } = await import('./agents/channel-identity.js');
646
+ let perAgentIdentity;
647
+ if (chIdentity?.agentId) {
648
+ try {
649
+ const { loadOrCreateAgentIdentity } = await import('./agents/agent-identity.js');
650
+ const mine = loadOrCreateAgentIdentity(chIdentity.agentId);
651
+ if (mine?.did)
652
+ perAgentIdentity = { did: mine.did, publicKey: mine.publicKey, name: nameForChannel(chIdentity, chIdentity.agentId) };
653
+ }
654
+ catch { /* 退回落 */ }
655
+ }
656
+ if (channelIdentityUsable(chIdentity)) {
657
+ // channel 自带**可用**的持久 DID (真 did:key) → 用它, 名字仍按该 agent 取 (不再用共享名)
493
658
  identityDoc = {
494
659
  did: chIdentity.did,
495
- name: chIdentity.persona?.name || chIdentity.name || 'agent',
660
+ name: nameForChannel(chIdentity, chIdentity.agentId),
496
661
  publicKey: chIdentity.publicKey,
497
662
  createdAt: Date.now(),
498
663
  };
499
664
  }
665
+ else if (perAgentIdentity) {
666
+ // channel 缺身份 / 只有假 DID ⇒ 用**该 agent 自己的**真身份 (按 agentId 落盘密钥, 稳定)
667
+ identityDoc = { ...perAgentIdentity, createdAt: Date.now() };
668
+ }
500
669
  else if (agentIdentity) {
501
670
  identityDoc = {
502
671
  did: agentIdentity.did,
@@ -512,15 +681,60 @@ async function getAgent() {
512
681
  else {
513
682
  identityDoc = undefined;
514
683
  }
684
+ // 2026-10-01: **身份取值自证** —— 只在"可疑"时打一行事实 (不是刷屏):
685
+ // 缺 did / 假 did / channel 有 agentId 却没被用上 ⇒ 打出来, 免得靠猜。
686
+ // 用户实测过 DID 为空、名字串台、以及一个数据里根本不存在的 did ⇒ 这行能直接指认哪条分支被走了。
687
+ try {
688
+ const d = String(identityDoc?.did ?? '');
689
+ const suspicious = !d || /^did:(local|pi):/i.test(d) || (!chIdentity?.agentId && !!targetChannelId);
690
+ if (suspicious) {
691
+ console.warn(`[identity] 身份可疑: channel=${targetChannelId ?? '(无)'} · channel.agentId=${chIdentity?.agentId ?? '(无)'} · ` +
692
+ `分支=${channelIdentityUsable(chIdentity) ? 'channel 自带' : perAgentIdentity ? 'per-agent' : agentIdentity ? '**共享 agentIdentity**' : '默认'} · ` +
693
+ `did=${d ? d.slice(0, 30) : '(空)'} · name=${String(identityDoc?.name ?? '(空)')}`);
694
+ }
695
+ }
696
+ catch { /* 自证失败不影响主流程 */ }
697
+ // 2026-10-01: 把当前 agent 告诉进程 —— 项目上下文(context-collector)据此取**该 agent 的** persona,
698
+ // 否则它会读全局 persona.json, 于是 get_identity 已是「小龙」而上下文里还写着「小宝」✗。
699
+ if (chIdentity?.agentId)
700
+ process.env.BOLLOON_ACTIVE_AGENT_ID = chIdentity.agentId;
701
+ // 2026-10-01: **身份取值一行事实** —— 每次**建/切 agent** 打一行(不是每回合, 不刷屏)。
702
+ // 用户报「xiaomi 频道却拿到 233 的 DID + 小龙」而可疑判据没触发 ⇒ 猜不动了, 让代码直说。
703
+ try {
704
+ console.warn(`[identity] channel=${targetChannelId ?? '(无)'} · 查到=${chIdentity ? 'yes' : '**no**'} · ` +
705
+ `channel.agentId=${chIdentity?.agentId ?? '(无)'} · channel.did=${String(chIdentity?.did ?? '(无)').slice(0, 26)} · ` +
706
+ `分支=${channelIdentityUsable(chIdentity) ? 'channel自带' : perAgentIdentity ? 'per-agent' : agentIdentity ? '**共享' : '默认'} · ` +
707
+ `结果 did=${String(identityDoc?.did ?? '(空)').slice(0, 26)} name=${String(identityDoc?.name ?? '(空)')}`);
708
+ }
709
+ catch { /* 自证失败不影响主流程 */ }
515
710
  const loadSessionKey = targetChannelId
516
711
  ? `${targetChannelId}:${chIdentity?.currentSessionId || 'default'}`
517
712
  : undefined;
713
+ // 2026-10-01: 确保该 agent 名下有身份文档 (此前仓里没有任何"建文档"的路径 ⇒
714
+ // 用户四个 agent 的 persona/<agentId>/ 全空 ⇒ loadPersonaDocs 读回空 ⇒ 所有智能体一个样)。
715
+ // 挂在这里 = 建 agent / 切 channel 两条路都会走到; 幂等, 已存在的不覆盖。
716
+ if (chIdentity?.agentId) {
717
+ try {
718
+ const { ensurePersonaDocs } = await import('./bootstrap/persona-init.js');
719
+ // 2026-10-01: 名字必须和该 agent 的 persona.json 一致 —— 之前传的是**渠道名**,
720
+ // 于是 soul.md 写「我是 233」而 persona.json 写「小龙」⇒ 用户看到"身份没匹配上" ✗。
721
+ const r = await ensurePersonaDocs(chIdentity.agentId, { name: nameForChannel(chIdentity, chIdentity.agentId) });
722
+ if (r.created.length)
723
+ console.warn(`[persona] 已为 ${r.agentId} 生成 ${r.created.length} 份起步身份文档: ${r.created.join(' · ')} → ${r.dir}`);
724
+ }
725
+ catch { /* 非致命 */ }
726
+ }
518
727
  agent = await createAgentSession({
519
728
  cwd: process.cwd(),
520
729
  peerId: targetChannelId ?? 'harness',
521
- identityDoc,
522
730
  // 2026-08-09: 透传 channel.agentId → persona docs 按 agent 加载 (身份真正变化)
523
731
  agentId: chIdentity?.agentId || (targetChannelId ? undefined : agentIdentity?.name),
732
+ // 2026-10-01 ★ **必须传 identityDoc** —— 之前漏了 ✗, 后果有两层:
733
+ // ① 上面按频道算好的身份(名字 + 该 agent 自己的 did)被**丢掉**, session 用自己造/复用的;
734
+ // ② session 工厂的更新路径 (pi-sdk-session-factory.updateIdentity) 以 `config.identityDoc?.did`
735
+ // 为条件 ⇒ 缺它 **永不触发** ⇒ 切频道时复用同一个 session 实例, 身份却**不换** ✗✗
736
+ // (实测: 233→智能体 后答 233 的身份; 智能体→233 后答 智能体的身份 = 身份"差一格" ✓ 根因在此)
737
+ identityDoc,
524
738
  loadSessionKey,
525
739
  });
526
740
  agentBoundChannelId = targetChannelId;
@@ -548,7 +762,7 @@ async function dispatchTask(raw) {
548
762
  const task = safeParse(body);
549
763
  if (!task)
550
764
  return `ERR|${JSON.stringify({ code: 'bad_format' })}`;
551
- console.log(`\n📥 [${task.type}] from=${task.from?.substring(0, 18)}... id=${task.id}`);
765
+ writeOut(`\n📥 [${task.type}] from=${task.from?.substring(0, 18)}... id=${task.id}`);
552
766
  try {
553
767
  switch (task.type) {
554
768
  case 'summarize':
@@ -568,7 +782,7 @@ async function handleSummarize(task) {
568
782
  return `ERR|${JSON.stringify({ code: 'no_path' })}`;
569
783
  const a = await getAgent();
570
784
  const { summary, qualityScore } = await a.summarizeDocument(task.documentPath);
571
- console.log(` ✅ 质量=${(qualityScore * 10).toFixed(1)}/10`);
785
+ writeOut(` ✅ 质量=${(qualityScore * 10).toFixed(1)}/10`);
572
786
  return `OK|${JSON.stringify({ id: task.id, type: 'summarize', qualityScore, summary })}`;
573
787
  }
574
788
  async function handleImprove(task) {
@@ -582,7 +796,7 @@ async function handleImprove(task) {
582
796
  context: `来自节点: ${task.from}`,
583
797
  });
584
798
  const ok = res.improved ?? false;
585
- console.log(` ✅ 改进${ok ? '成功' : '失败'} 质量=${(res.qualityScore * 10).toFixed(1)}/10 自动发送=${res.shouldAutoSend}`);
799
+ writeOut(` ✅ 改进${ok ? '成功' : '失败'} 质量=${(res.qualityScore * 10).toFixed(1)}/10 自动发送=${res.shouldAutoSend}`);
586
800
  return `OK|${JSON.stringify({
587
801
  id: task.id, type: 'improve', improved: ok,
588
802
  qualityScore: res.qualityScore, shouldAutoSend: res.shouldAutoSend,
@@ -731,7 +945,12 @@ function getStatus() {
731
945
  return `${C_DIM}${icon}${RESET} ${color}${val}${RESET}`;
732
946
  });
733
947
  // 按剩余宽度取舍 (真机 112 列时右段被截 ⇒ 看着像没出现)
734
- const __segsFit = fitSegments(__segs.map(x => x.replace(/\x1b\[[0-9;]*m/g, '')), Math.max(20, termWidth() - 2 - __leftPlainLen - 6));
948
+ // 2026-10-01: 底栏一律用**保守测宽** (Ambiguous 字符按 2 列) —— 超 1 列就会让终端折行、
949
+ // 进而打破 Ink 光标数学, 表现是整块底栏被重复打印 (见 status-segments.dispWidthSafe 注释)
950
+ // 2026-10-01: 预算按**保守宽度**给 (left + ≥1 格 + title ≤ width-1) ⇒ 任何字体下都不折行
951
+ const __notePlain = (cliSessionPreview && cliSessionTitleKey === cliSessionKey) ? `▸ ${cliSessionPreview}` : '';
952
+ const __budget = statusLineBudget(termWidth(), __leftPlainLen, String(__notePlain || ''));
953
+ const __segsFit = fitSegments(__segs.map(x => x.replace(/\x1b\[[0-9;]*m/g, '')), __budget, dispWidthSafe);
735
954
  const __live = __segsFit.length
736
955
  ? ` ${C_DIM}│${RESET} ` + __segsFit.map(x => {
737
956
  const m = /^(\S+)\s(.*)$/.exec(x);
@@ -742,13 +961,13 @@ function getStatus() {
742
961
  : '';
743
962
  // 2026-09-30 (leo: 「这个 title 需要顶格右侧」): 会话 Title **右对齐**贴右边缘 ——
744
963
  // 先算左半边+活数据段的显示宽, 再补空格把 `▸ 标题` 推到 width-1 处; 放不下就不显示 (不挤坏这一行)。
745
- const __notePlain = (cliSessionPreview && cliSessionTitleKey === cliSessionKey) ? `▸ ${cliSessionPreview}` : '';
746
964
  let __noteStr = '';
747
965
  if (__notePlain) {
748
966
  // 宽度必须用 dispWidth (East Asian Width 表): 行内的 `│ ░ ◷ ▸ ◎ ✓ ↑ ⚙` 都是**单宽**,
749
967
  // 用 "charCode > 255 ⇒ 2" 的启发式会每行多算十几列 ⇒ 右对齐留一大截 (leo: 「没有完全右对齐」)。
750
- const used = dispWidth(`${__left}${__live}`);
751
- const pad = rightAlignPad(used, dispWidth(__notePlain), termWidth());
968
+ const used = dispWidthSafe(`${__left}${__live}`);
969
+ // 保守测宽 (同上): 右对齐后整行最多到 width-1, 在任何终端字体下都不会折行
970
+ const pad = rightAlignPad(used, dispWidthSafe(__notePlain), termWidth());
752
971
  if (pad !== null)
753
972
  __noteStr = ' '.repeat(pad) + `${C_DIM}${__notePlain}${RESET}`;
754
973
  }
@@ -1150,6 +1369,8 @@ async function startCLI(commReady) {
1150
1369
  startupPanelReady = true;
1151
1370
  // 2026-09-30: 面板在起帧前已进 store ⇒ 首帧里与输入框一起出现
1152
1371
  startInk((text) => { processInput(text, comm); }, initialStatus, getStatus);
1372
+ // 2026-10-01: Ink 起来了 ⇒ 把启动期缓冲的日志一次性灌进对话流(不丢, 也不跟面板交错 ✓)
1373
+ flushBootBuffer();
1153
1374
  // 2026-08-10: 自动整理心跳 (CLI 侧, 与社交心跳并列) — 启动后立即"固定看一下 skills view"
1154
1375
  // (扫描遗留 skills), 之后按周期 (默认 30min, env BOLLOON_ORGANIZE_HEARTBEAT_MS) 完整进化经验.
1155
1376
  // 显示走 transient 颜文字行: 触发时显示, 结束后清空 (显示为空).
@@ -1303,6 +1524,7 @@ async function startCLI(commReady) {
1303
1524
  * scheduler 的每个 tick 会先查 resolveDnd(), 处于勿扰时把 due job 记 deferred 并留到下一轮.
1304
1525
  */
1305
1526
  async function processInput(input, comm) {
1527
+ endBootPhase(); // 用户已接管 ⇒ 启动期结束(之后的输出照常上屏 ✓)
1306
1528
  const gate = globalThis.__bolloonMainTask;
1307
1529
  if (gate?.enterMainTask) {
1308
1530
  try {
@@ -1440,7 +1662,31 @@ async function processInputInner(input, comm) {
1440
1662
  const extra = prev && prev.name !== r.identity.name ? ` (从 ${prev.name} 切换)` : '';
1441
1663
  appendLine(`${C_ACCENT}→ 当前智能体: ${r.identity.name}${RESET}${extra}`);
1442
1664
  appendLine(`${C_DIM} channel: ${r.channel.id} [${r.match}]${RESET}`);
1443
- appendLine(`${C_DIM} persona: ${r.channel.persona?.description || r.channel.persona?.personality || '无'}${RESET}`);
1665
+ // 2026-10-01 (用户报「切换身份还没有成功」): 把**CLI 认为的身份**打进对话流 —— 之前用 console.warn,
1666
+ // 在这套 TUI 里被吞掉, 用户从来看不到 ✗。切频道才一行, 不刷屏。
1667
+ try {
1668
+ const live = agent?.getIdentity?.() ?? null;
1669
+ appendLine(`${C_DIM} [identity] 频道身份=${r.identity.name} (did=${String(r.identity?.did ?? '(无)').slice(0, 26)})` +
1670
+ ` · agent 当前身份=${live?.name ?? '(读不到)'} (did=${String(live?.did ?? '(无)').slice(0, 26)})` +
1671
+ ` · agentId=${r.channel.agentId ?? '(无)'}${RESET}`);
1672
+ }
1673
+ catch { /* 自证失败不影响切换 */ }
1674
+ // 2026-10-01: 这行原先读 channels.json 里**内联的 metadata** (你四条 channel 都空 ⇒ 恒打"无"),
1675
+ // 而身份真源是 ~/.bolloon/persona/<agentId>/ 的 6 份文档 ⇒ 改读真源, 如实报"加载到几份"。
1676
+ try {
1677
+ const { loadPersonaDocs, PERSONA_DOC_FILES_HINT } = await import('./bootstrap/persona-loader.js').then(async (m) => ({ loadPersonaDocs: m.loadPersonaDocs, PERSONA_DOC_FILES_HINT: ['soul', 'identity', 'project', 'user', 'agent', 'wiki'] }));
1678
+ const agentId = r.channel.agentId || '';
1679
+ const docs = agentId ? await loadPersonaDocs(agentId) : null;
1680
+ const loaded = docs ? PERSONA_DOC_FILES_HINT.filter((k) => String(docs[k] || '').trim().length > 0) : [];
1681
+ appendLine(agentId
1682
+ ? (loaded.length
1683
+ ? `${C_DIM} persona: ${loaded.length}/6 份身份文档 (${loaded.join(' · ')}) · agentId=${agentId}${RESET}`
1684
+ : `${C_DIM} persona: 未初始化 (agentId=${agentId}, 目录 ${'~/.bolloon/persona/' + agentId}/ 为空 — 下次加载会自动生成起步模板)${RESET}`)
1685
+ : `${C_DIM} persona: 该 channel 没有 agentId ⇒ 无法按 agent 加载身份文档${RESET}`);
1686
+ }
1687
+ catch {
1688
+ appendLine(`${C_DIM} persona: (读身份文档失败, 不影响本次切换)${RESET}`);
1689
+ }
1444
1690
  }
1445
1691
  catch (e) {
1446
1692
  appendLine(`${C_ERROR}/channel 失败: ${String(e.message || e).slice(0, 200)}${RESET}`);
@@ -1522,7 +1768,7 @@ async function processInputInner(input, comm) {
1522
1768
  });
1523
1769
  }
1524
1770
  await fs.writeFile(agentsPath, JSON.stringify(arr, null, 2), 'utf-8');
1525
- console.log(`[创建频道] agent 同步进 agents.json: ${agentId} → channel ${id}`);
1771
+ writeOut(`[创建频道] agent 同步进 agents.json: ${agentId} → channel ${id}`);
1526
1772
  }
1527
1773
  catch { /* agents.json 写失败不阻塞创建 */ }
1528
1774
  // 刷新 store 缓存 (updateChannels 走了 server-storage, store 内存还是旧的)
@@ -1619,6 +1865,14 @@ async function processInputInner(input, comm) {
1619
1865
  // 不能全局改成"首词" —— 同文件里 `/email clear` 这类多词命令就是靠整行匹配的。
1620
1866
  // 所以加一个**首词**给"带参数的命令"用 (cmd 保持原样给多词命令用)。
1621
1867
  const cmdHead = cmd.split(/\s+/)[0];
1868
+ // 2026-10-01: **未知命令兜底** —— 以 / 开头但不在白名单里的输入, 不当消息发下去 ✗
1869
+ // (用户实测: /group 不是命令 ⇒ 却发给模型, 白烧一轮工具调用)
1870
+ if (cmd.startsWith('/') && !CLI_KNOWN_COMMAND_HEADS.has(cmdHead)) {
1871
+ const near = [...CLI_KNOWN_COMMAND_HEADS].filter((h) => h.includes(cmdHead.slice(1)) || cmdHead.includes(h.slice(1))).slice(0, 6);
1872
+ appendLine(`${C_WARN}未知命令 ${cmdHead}${RESET}${C_DIM} —— 输入${RESET} / ${C_DIM}看命令列表, 或 /help 看用法${RESET}`
1873
+ + (near.length ? `${C_DIM} · 你是不是想用: ${near.join(' / ')}${RESET}` : ''));
1874
+ return;
1875
+ }
1622
1876
  // /net — Agent 网络快捷命令 (join/status/ctx, 2026-09-08)
1623
1877
  if (cmd === '/net' || cmd.startsWith('/net ')) {
1624
1878
  const arg = trimmed.slice(4).trim();
@@ -3576,6 +3830,153 @@ async function processInputInner(input, comm) {
3576
3830
  }
3577
3831
  return;
3578
3832
  }
3833
+ // 「当前群」的取用: 没选过时, 只有恰好一个群就自动用它; 否则提示先 /group use
3834
+ const ensureCurrentGroup = async (gg) => {
3835
+ if (currentGroupId)
3836
+ return currentGroupId;
3837
+ const gs = await gg.listGroups();
3838
+ if (gs.length === 1) {
3839
+ currentGroupId = gs[0].id;
3840
+ return currentGroupId;
3841
+ }
3842
+ if (!gs.length) {
3843
+ appendLine(`${C_DIM}本地还没有群 —— 用 /group new <名字> 建一个${RESET}`);
3844
+ return undefined;
3845
+ }
3846
+ appendLine(`${C_DIM}本地有 ${gs.length} 个群, 先 /group use <名字|id> 选一个${RESET}`);
3847
+ return undefined;
3848
+ };
3849
+ // /group — OrbitDB 群聊 (2026-10-01; 接 gateway-group 那套: 建群/加入/收发/成员/日志)
3850
+ if (cmd === '/group' || cmd.startsWith('/group ')) {
3851
+ const { sub, arg } = parseGroupSub(trimmed);
3852
+ const gg = await import('./agents/gateway-group.js');
3853
+ const dim = (t) => appendLine(`${C_DIM}${t}${RESET}`);
3854
+ try {
3855
+ if (sub === 'help') {
3856
+ appendLine(`${C_ACCENT}/group${RESET} —— OrbitDB 群聊`);
3857
+ dim(' /group 列出本地群');
3858
+ dim(' /group new <名字> 建群 (默认创建者独占写; 会打印可分享地址)');
3859
+ dim(' /group join <链接|地址> 加入');
3860
+ dim(' /group leave <名字|id> 退出');
3861
+ dim(' /group use <名字|id> 选定当前群 (之后 send/log/members 都指它)');
3862
+ dim(' /group send <文本> 往当前群发一条');
3863
+ dim(' /group log [N] 看当前群最近 N 条 (默认 10)');
3864
+ dim(' /group members 列当前群成员');
3865
+ return;
3866
+ }
3867
+ if (sub === 'list') {
3868
+ const gs = await gg.listGroups();
3869
+ if (!gs.length) {
3870
+ dim('本地还没有群 —— 用 /group new <名字> 建一个');
3871
+ return;
3872
+ }
3873
+ const rows = [];
3874
+ gs.forEach((g, i) => {
3875
+ const cur = currentGroupId === g.id ? '●' : '○';
3876
+ const n = String(i + 1).padStart(2, ' ');
3877
+ rows.push(`${n}. ${cur} ${g.name} ${String(g.id).slice(0, 18)}…`);
3878
+ });
3879
+ rows.push('');
3880
+ rows.push('选中进群: /group <序号> (或 /group use <名字|id>)');
3881
+ appendLine(renderMessageBox({ title: 'OrbitDB 群', body: rows.join('\n'), color: C_ACCENT, maxLines: 24 }));
3882
+ return;
3883
+ }
3884
+ if (sub === 'new') {
3885
+ const r = await gg.createGroup(arg || '');
3886
+ if (!r.ok) {
3887
+ appendLine(`${C_WARN}建群失败: ${r.error}${RESET}`);
3888
+ return;
3889
+ }
3890
+ currentGroupId = r.group?.id || currentGroupId;
3891
+ appendLine(renderMessageBox({ title: '✅ 群已建立', body: `名字: ${r.group?.name || arg}\nid: ${r.group?.id || '-'}\n地址: ${r.group?.link || r.group?.address || '(未返回地址)'}\n\n把地址发给同伴, 用 /group join <地址> 加入`, color: C_ACCENT, maxLines: 12 }));
3892
+ return;
3893
+ }
3894
+ if (sub === 'join') {
3895
+ if (!arg) {
3896
+ dim('用法: /group join <链接|地址>');
3897
+ return;
3898
+ }
3899
+ const r = await gg.joinGroup(arg);
3900
+ if (!r.ok) {
3901
+ appendLine(`${C_WARN}加入失败: ${r.error}${RESET}`);
3902
+ return;
3903
+ }
3904
+ currentGroupId = r.group?.id || currentGroupId;
3905
+ appendLine(`${C_ACCENT}✓ 已加入 ${r.group?.name || arg}${RESET}`);
3906
+ return;
3907
+ }
3908
+ if (sub === 'leave') {
3909
+ if (!arg) {
3910
+ dim('用法: /group leave <名字|id>');
3911
+ return;
3912
+ }
3913
+ const r = await gg.leaveGroup(arg);
3914
+ appendLine(r.ok ? `${C_ACCENT}✓ 已退出 ${r.removed || arg}${RESET}` : `${C_WARN}退出失败: ${r.error}${RESET}`);
3915
+ if (r.ok && currentGroupId === arg)
3916
+ currentGroupId = undefined;
3917
+ return;
3918
+ }
3919
+ if (sub === 'use') {
3920
+ if (!arg) {
3921
+ dim('用法: /group use <名字|id>');
3922
+ return;
3923
+ }
3924
+ const gs = await gg.listGroups();
3925
+ // 序号 (与 /group 列表里的编号一致) → 名字 → id → 名字子串
3926
+ let hit = /^\d+$/.test(arg) ? gs[parseInt(arg, 10) - 1] : undefined;
3927
+ if (!hit)
3928
+ hit = gs.find((g) => g.id === arg || g.name === arg) || gs.find((g) => String(g.name).includes(arg));
3929
+ if (!hit) {
3930
+ appendLine(`${C_WARN}没找到群: ${arg}${RESET}${C_DIM} —— 先 /group 看编号${RESET}`);
3931
+ return;
3932
+ }
3933
+ currentGroupId = hit.id;
3934
+ appendLine(`${C_ACCENT}✓ 当前群: ${hit.name} (${String(hit.id).slice(0, 18)}…)${RESET}${C_DIM} 现在可以 /group send <文本> 或 /group log${RESET}`);
3935
+ return;
3936
+ }
3937
+ if (sub === 'send') {
3938
+ if (!arg) {
3939
+ dim('用法: /group send <文本>');
3940
+ return;
3941
+ }
3942
+ const gid = await ensureCurrentGroup(gg);
3943
+ if (!gid)
3944
+ return;
3945
+ const r = await gg.groupSend(gid, arg, String(agentIdentity?.did || 'cli')).catch((e) => ({ ok: false, error: String(e?.message || e) }));
3946
+ appendLine(r.ok === false ? `${C_WARN}发送失败: ${r.error}${RESET}` : `${C_ACCENT}✓ 已发到群${RESET}`);
3947
+ return;
3948
+ }
3949
+ if (sub === 'log') {
3950
+ const gid = await ensureCurrentGroup(gg);
3951
+ if (!gid)
3952
+ return;
3953
+ const n = Math.max(1, Math.min(100, parseInt(arg || '10', 10) || 10));
3954
+ const msgs = await gg.groupMessages(gid, n);
3955
+ if (!msgs.length) {
3956
+ dim('这个群还没有消息');
3957
+ return;
3958
+ }
3959
+ appendLine(renderMessageBox({ title: `群消息 (最近 ${msgs.length})`, body: msgs.map((m) => `${m.from ? String(m.from).slice(0, 10) : '?'}: ${String(m.text || '').slice(0, 200)}`).join('\n'), color: C_ACCENT, maxLines: 24 }));
3960
+ return;
3961
+ }
3962
+ if (sub === 'members') {
3963
+ const gid = await ensureCurrentGroup(gg);
3964
+ if (!gid)
3965
+ return;
3966
+ const ms = await gg.groupMembers(gid);
3967
+ dim(ms.length ? ms.join('\n') : '(暂无成员记录)');
3968
+ return;
3969
+ }
3970
+ dim('用法: /group help');
3971
+ }
3972
+ catch (e) {
3973
+ // GroupStoreUnreachableError 等: 说清"本地群存储没起来", 不要笼统报错
3974
+ const msg = String(e?.message || e);
3975
+ const unreachable = e?.name === 'GroupStoreUnreachableError' || /unreachable|not open|未就绪/i.test(msg);
3976
+ appendLine(`${C_WARN}${unreachable ? '本地群存储未就绪 —— 需要本机服务/OrbitDB 起来后才行' : '群操作失败'}: ${msg}${RESET}`);
3977
+ }
3978
+ return;
3979
+ }
3579
3980
  if (trimmed.toLowerCase() === '/help' || trimmed === 'help') {
3580
3981
  appendLine(`${C_DIM}命令:${RESET}`);
3581
3982
  appendLine(` ${C_ACCENT}!<cmd>${RESET} 执行终端命令 ${C_DIM}如 !ls -la${RESET}`);
@@ -3716,6 +4117,18 @@ async function processInputInner(input, comm) {
3716
4117
  // (thinking / status / phase / Reflection) 全被丢弃 → 用户只能看到输入和最终输出
3717
4118
  // 用户偏好 (2026-08-07): 思考过程 = 圆角框渲染 (和回复同路径 renderMessageBox),
3718
4119
  // 颜文字动画 (inkSetThinking) 只表示"正在运行", 不承载思考内容
4120
+ // 2026-10-01: 思考记录 (模型的思维链) —— 暗色块 + 明确标注"未验证", 与正式回答分开 ✓
4121
+ if (e.type === 'reasoning' && e.content) {
4122
+ // 2026-10-01 (用户: 「我要的是那种**短的**思考, 长思维链可以不显示」): 默认只显示**一句**"在想什么";
4123
+ // 想看全链 ⇒ BOLLOON_SHOW_THINKING=full; 不想看 ⇒ =0。
4124
+ const r = renderReasoning(String(e.content));
4125
+ if (r.text) {
4126
+ if (r.mode === 'short')
4127
+ appendLine(`${C_DIM}💭 ${r.text}${RESET}`);
4128
+ else
4129
+ appendLine(renderMessageBox({ title: '💭 思考 (未验证)', body: r.text, color: C_DIM, maxLines: 16 }));
4130
+ }
4131
+ }
3719
4132
  if (e.type === 'thinking' && e.content) {
3720
4133
  // thinking 事件只有 "🤔 开始思考..." 占位 → 不 appendLine, 运行过程由动画表示;
3721
4134
  // 真正思考内容在 status 的 Reflection/💡 事件 → 下方框渲染
@@ -3742,7 +4155,10 @@ async function processInputInner(input, comm) {
3742
4155
  // Reflection / 反思 / 💡 → 圆角思考框 (和回复一样走 renderMessageBox, 白字+亮边框)
3743
4156
  else if (content.includes('Reflection') || content.includes('反思') || content.includes('💡')) {
3744
4157
  const body = content.replace(/^💡\s*/, '').slice(0, 1500);
3745
- if (body.trim())
4158
+ // 2026-10-01: 只有标记词 (Reflection: / 反思 / 💡) 而无正文 ⇒ **不渲染**, 免得出空框 ✗
4159
+ const plain = body.replace(/\x1b\[[0-9;]*m/g, '').trim(); // 2026-10-01: 先剥 ANSI —— 只有彩色控制符的正文看着就是**空框** ✗
4160
+ const onlyMarker = /^(?:reflection|反思)\s*[::\-—]?\s*$/i.test(plain);
4161
+ if (plain && !onlyMarker)
3746
4162
  appendLine(renderMessageBox({ title: '💡 反思', body, color: C_WARN }));
3747
4163
  }
3748
4164
  else if (!content.includes('🔄 循环') && !content.includes('📋 参数')
@@ -3763,7 +4179,9 @@ async function processInputInner(input, comm) {
3763
4179
  // 改用 transient 行显示"正在执行"(消息流只在 done 时出现一次完成行).
3764
4180
  if (toolName !== 'system' && toolName !== 'loop' && toolName !== '?') {
3765
4181
  const activeNames = tuiToolCalls.map(c => c.tool).filter(t => t !== 'system' && t !== 'loop' && t !== '?');
3766
- const label = activeNames.length > 1 ? `执行 ${activeNames.length} 个工具: ${activeNames.join(', ')}` : `🔧 ${toolName}`;
4182
+ const label = activeNames.length > 1
4183
+ ? `执行 ${activeNames.length} 个工具: ${activeNames.join(', ')}`
4184
+ : `🔧 ${traceLabel(toolName)}`; // trace 执行描述: 写清这一步在做什么
3767
4185
  inkSetTransient(`${C_DIM}${label} 运行中...${RESET}`);
3768
4186
  }
3769
4187
  }
@@ -3793,7 +4211,7 @@ async function processInputInner(input, comm) {
3793
4211
  const remaining = tuiToolCalls.filter(c => c.tool !== 'system' && c.tool !== 'loop' && c.tool !== '?');
3794
4212
  cliTurnToolCount = remaining.length; // ⚙ 状态栏用 (模块级)
3795
4213
  if (remaining.length > 0) {
3796
- const label = remaining.length > 1 ? `执行 ${remaining.length} 个工具: ${remaining.map(c => c.tool).join(', ')}` : `🔧 ${remaining[0].tool}`;
4214
+ const label = remaining.length > 1 ? `执行 ${remaining.length} 个工具: ${remaining.map(c => c.tool).join(', ')}` : `🔧 ${traceLabel(remaining[0].tool)}`; // trace 执行描述
3797
4215
  inkSetTransient(`${C_DIM}${label} 运行中...${RESET}`);
3798
4216
  }
3799
4217
  else {
@@ -4895,18 +5313,18 @@ goalFlag) {
4895
5313
  error,
4896
5314
  metadata
4897
5315
  };
4898
- console.log(JSON.stringify(result, null, 2));
5316
+ writeOut(JSON.stringify(result, null, 2));
4899
5317
  }
4900
5318
  else {
4901
5319
  if (error) {
4902
5320
  s.divider();
4903
- console.log(`${MAGENTA}${error}${RESET}\n`);
5321
+ writeOut(`${MAGENTA}${error}${RESET}\n`);
4904
5322
  }
4905
5323
  else {
4906
5324
  s.divider();
4907
- console.log(`${response}\n`);
5325
+ writeOut(`${response}\n`);
4908
5326
  }
4909
- console.log(`${GRAY}耗时: ${metadata.duration}ms${RESET}`);
5327
+ writeOut(`${GRAY}耗时: ${metadata.duration}ms${RESET}`);
4910
5328
  }
4911
5329
  }
4912
5330
  async function runNonInteractive(args, comm) {
@@ -4922,11 +5340,11 @@ async function runNonInteractive(args, comm) {
4922
5340
  }
4923
5341
  else if (prompt) {
4924
5342
  const a = await getAgent();
4925
- console.log(await a.prompt(prompt));
5343
+ writeOut(await a.prompt(prompt));
4926
5344
  }
4927
5345
  console.log = originalLog;
4928
5346
  await fs.writeFile(output, outputBuffer.trim(), 'utf-8');
4929
- console.log(`✅ 结果已保存到: ${output}`);
5347
+ writeOut(`✅ 结果已保存到: ${output}`);
4930
5348
  return;
4931
5349
  }
4932
5350
  if (tool === 'prompt' && prompt) {
@@ -4938,20 +5356,20 @@ async function runNonInteractive(args, comm) {
4938
5356
  const response = await a.prompt(prompt);
4939
5357
  const elapsed = Date.now() - startTime;
4940
5358
  if (json) {
4941
- console.log(JSON.stringify({ success: true, response, elapsedMs: elapsed }, null, 2));
5359
+ writeOut(JSON.stringify({ success: true, response, elapsedMs: elapsed }, null, 2));
4942
5360
  }
4943
5361
  else {
4944
- console.log(response);
4945
- console.log(`\n耗时: ${elapsed}ms`);
5362
+ writeOut(response);
5363
+ writeOut(`\n耗时: ${elapsed}ms`);
4946
5364
  }
4947
5365
  }
4948
5366
  catch (e) {
4949
5367
  const error = e?.message || String(e);
4950
5368
  if (json) {
4951
- console.log(JSON.stringify({ success: false, error }, null, 2));
5369
+ writeOut(JSON.stringify({ success: false, error }, null, 2));
4952
5370
  }
4953
5371
  else {
4954
- console.log(`\n错误: ${error}`);
5372
+ writeOut(`\n错误: ${error}`);
4955
5373
  }
4956
5374
  process.exit(1);
4957
5375
  }
@@ -4979,10 +5397,10 @@ async function runNonInteractive(args, comm) {
4979
5397
  error: response.startsWith('错误:') ? response : undefined,
4980
5398
  metadata: { duration, peers }
4981
5399
  };
4982
- console.log(JSON.stringify(result, null, 2));
5400
+ writeOut(JSON.stringify(result, null, 2));
4983
5401
  }
4984
5402
  else {
4985
- console.log(response);
5403
+ writeOut(response);
4986
5404
  }
4987
5405
  }
4988
5406
  }
@@ -5300,7 +5718,7 @@ function parseArgs() {
5300
5718
  return result;
5301
5719
  }
5302
5720
  function printHelp() {
5303
- console.log(`
5721
+ writeOut(`
5304
5722
  🤖 Bolloon Agent - AI 可调用文档处理智能体
5305
5723
 
5306
5724
  用法:
@@ -5528,21 +5946,21 @@ async function main() {
5528
5946
  const res = await runStandaloneSupervisorHost({
5529
5947
  once: !!args.superviseOnce,
5530
5948
  dryRun: !!args.superviseDryRun,
5531
- log: (m) => console.log(m),
5949
+ log: (m) => writeOut(m),
5532
5950
  });
5533
5951
  if (args.superviseOnce) {
5534
5952
  const rep = res.lastReport;
5535
5953
  if (rep) {
5536
- console.log(`调度周期 #${rep.tick}: 认领 ${rep.claimed.length} · 执行 ${rep.executed.length} · 跳过 ${rep.skipped.length}${rep.errors.length ? ` · 错误 ${rep.errors.length}` : ''}`);
5954
+ writeOut(`调度周期 #${rep.tick}: 认领 ${rep.claimed.length} · 执行 ${rep.executed.length} · 跳过 ${rep.skipped.length}${rep.errors.length ? ` · 错误 ${rep.errors.length}` : ''}`);
5537
5955
  for (const s of rep.skipped.slice(0, 8))
5538
- console.log(` 跳过 ${s.goalId}: ${s.reason}`);
5956
+ writeOut(` 跳过 ${s.goalId}: ${s.reason}`);
5539
5957
  for (const e of rep.executed)
5540
- console.log(` ▶ ${e.goalId} → run=${e.runId || '-'} ${e.status || ''}${e.error ? ` (${e.error})` : ''}`);
5958
+ writeOut(` ▶ ${e.goalId} → run=${e.runId || '-'} ${e.status || ''}${e.error ? ` (${e.error})` : ''}`);
5541
5959
  }
5542
- console.log(`supervisor 宿主: owner=${res.state.owner} worker=${res.state.workerId} ticks=${res.ticks} 状态文件=~/.bolloon/supervisor.json`);
5960
+ writeOut(`supervisor 宿主: owner=${res.state.owner} worker=${res.state.workerId} ticks=${res.ticks} 状态文件=~/.bolloon/supervisor.json`);
5543
5961
  }
5544
5962
  else {
5545
- console.log(`[supervisor] 常驻宿主已启动 (owner=${res.state.owner}, worker=${res.state.workerId}); Ctrl-C 优雅停止`);
5963
+ writeOut(`[supervisor] 常驻宿主已启动 (owner=${res.state.owner}, worker=${res.state.workerId}); Ctrl-C 优雅停止`);
5546
5964
  }
5547
5965
  return;
5548
5966
  }
@@ -5593,6 +6011,14 @@ async function main() {
5593
6011
  if (process.env.BOLLOON_SKIP_SETUP === '1') {
5594
6012
  noticePreamble('⚠ BOLLOON_SKIP_SETUP=1 → 诊断模式: 可以看状态/修配置, 但 agent 执行被门禁拦住 (不能绕过)');
5595
6013
  }
6014
+ else if (onlyConnectivityRetest(gateEv)) {
6015
+ // 2026-10-01 (用户报「启动时候的日志也没去掉」):
6016
+ // 只差"连通性结果过期(需重测)"这一类 ⇒ **不在启动时跑 setup 向导** (跑一次要刷约 30 行前言)。
6017
+ // 改为: 面板内一行告警 + 让用户按需跑 `bolloon setup --test`; 门禁照旧拦住 agent 执行 (不绕过)。
6018
+ const a = `⚠ 连通性结果过期 → 跑 \`bolloon setup --test\` 复测 (启动时不自动跑), 全量: ${SETUP_STATUS_CMD}`;
6019
+ pushStartupAlert(a);
6020
+ noticePreamble(a);
6021
+ }
5596
6022
  else {
5597
6023
  const { runSetupWizard } = await import('./cli/setup-wizard.js');
5598
6024
  // quietStartup: 向导的续跑前言默认不上屏 (它自己会在"要人回答/有失败"时先 flush)
@@ -5793,7 +6219,7 @@ async function main() {
5793
6219
  // 默认用户模式: 不自迭代, 自改卡片不自动出现, 仍可 POST /api/self-improve/trigger 手动触发
5794
6220
  const selfImprove = process.env.BOLLOON_DEV_MODE === '1' || process.env.BOLLOON_DEV_MODE === 'true';
5795
6221
  if (selfImprove) {
5796
- console.log('[startup] BOLLOON_DEV_MODE=1, 开发者模式: 自迭代已启用');
6222
+ writeOut('[startup] BOLLOON_DEV_MODE=1, 开发者模式: 自迭代已启用');
5797
6223
  }
5798
6224
  const { createWebServer, openBrowser } = await import('./web/server.js');
5799
6225
  // 2026-06-24: CLI 默认 loopback bind (安全), LAN 访问需 BOLLOON_HOST=0.0.0.0
@@ -5808,7 +6234,7 @@ async function main() {
5808
6234
  console.info = originalInfo;
5809
6235
  process.stdout.write = originalStdoutWrite;
5810
6236
  s.info('执行命令...');
5811
- console.log();
6237
+ writeOut('');
5812
6238
  await runNonInteractive(args, comm);
5813
6239
  comm?.stop();
5814
6240
  // chat-watch / chat-p2p-listen 是长循环, 不会自然 return, 走 SIGINT 自然退出