@bolloon/bolloon-agent 0.5.3 → 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.
- package/dist/agents/agent-identity-store.js +41 -5
- package/dist/agents/background-notices.js +124 -0
- package/dist/agents/channel-identity.js +54 -0
- package/dist/agents/code-write-gate.js +47 -0
- package/dist/agents/deny-pipeline.js +11 -2
- package/dist/agents/egress-sanitize.js +69 -0
- package/dist/agents/error-classifier.js +67 -5
- package/dist/agents/experience-review.js +226 -0
- package/dist/agents/gateway-group.js +153 -18
- package/dist/agents/iteration-budget.js +64 -0
- package/dist/agents/lesson-to-skill.js +211 -0
- package/dist/agents/managed-services.js +64 -0
- package/dist/agents/parse-tool-call.js +38 -32
- package/dist/agents/pi-sdk-session-manager.js +72 -3
- package/dist/agents/pi-sdk-tools.js +268 -47
- package/dist/agents/pi-sdk.js +470 -22
- package/dist/agents/plan-store.js +36 -0
- package/dist/agents/process-runner.js +64 -2
- package/dist/agents/session-store.js +144 -3
- package/dist/agents/shell-guard.js +109 -13
- package/dist/agents/skill-health.js +90 -0
- package/dist/agents/skill-ledger.js +129 -0
- package/dist/agents/tool-loop-guard.js +303 -0
- package/dist/agents/tool-path-args.js +53 -0
- package/dist/agents/tool-result-gate.js +50 -0
- package/dist/agents/tool-subset.js +109 -0
- package/dist/agents/tool-telemetry.js +97 -0
- package/dist/agents/wallet-tools.js +63 -0
- package/dist/bootstrap/context-collector.js +37 -7
- package/dist/bootstrap/lifecycle-hooks.js +12 -3
- package/dist/bootstrap/persona-init.js +174 -0
- package/dist/bootstrap/persona-loader.js +5 -1
- package/dist/cli/ink-app.js +231 -32
- package/dist/cli/input-history.js +119 -0
- package/dist/cli/input-paste.js +218 -0
- package/dist/cli/loading-tui.js +69 -7
- package/dist/cli/log-gate.js +46 -1
- package/dist/cli/mention-data.js +30 -0
- package/dist/cli/reasoning-view.js +103 -0
- package/dist/cli/session-summary.js +129 -0
- package/dist/cli/session-tree.js +141 -0
- package/dist/cli/startup-notice.js +25 -0
- package/dist/cli/status-segments.js +155 -0
- package/dist/cli/stores.js +16 -0
- package/dist/cli/trace-line.js +54 -0
- package/dist/cli/wallet-store.js +159 -0
- package/dist/cli/wallet-table.js +46 -0
- package/dist/cli-entry.js +22 -0
- package/dist/context-compaction/auto-compact.js +37 -2
- package/dist/electron-build/agents/agent-identity-store.js +43 -5
- package/dist/electron-build/agents/agent-identity-store.js.map +1 -1
- package/dist/electron-build/agents/background-notices.js +138 -0
- package/dist/electron-build/agents/background-notices.js.map +1 -0
- package/dist/electron-build/agents/channel-identity.js +64 -0
- package/dist/electron-build/agents/channel-identity.js.map +1 -0
- package/dist/electron-build/agents/code-write-gate.js +55 -0
- package/dist/electron-build/agents/code-write-gate.js.map +1 -0
- package/dist/electron-build/agents/deny-pipeline.js +11 -2
- package/dist/electron-build/agents/deny-pipeline.js.map +1 -1
- package/dist/electron-build/agents/egress-sanitize.js +76 -0
- package/dist/electron-build/agents/egress-sanitize.js.map +1 -0
- package/dist/electron-build/agents/error-classifier.js +68 -5
- package/dist/electron-build/agents/error-classifier.js.map +1 -1
- package/dist/electron-build/agents/experience-review.js +243 -0
- package/dist/electron-build/agents/experience-review.js.map +1 -0
- package/dist/electron-build/agents/iteration-budget.js +73 -0
- package/dist/electron-build/agents/iteration-budget.js.map +1 -0
- package/dist/electron-build/agents/lesson-to-skill.js +228 -0
- package/dist/electron-build/agents/lesson-to-skill.js.map +1 -0
- package/dist/electron-build/agents/managed-services.js +69 -0
- package/dist/electron-build/agents/managed-services.js.map +1 -0
- package/dist/electron-build/agents/parse-tool-call.js +38 -32
- package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-session-manager.js +75 -3
- package/dist/electron-build/agents/pi-sdk-session-manager.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-tools.js +268 -46
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +471 -22
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/plan-store.js +37 -0
- package/dist/electron-build/agents/plan-store.js.map +1 -1
- package/dist/electron-build/agents/process-runner.js +71 -2
- package/dist/electron-build/agents/process-runner.js.map +1 -1
- package/dist/electron-build/agents/session-store.js +144 -3
- package/dist/electron-build/agents/session-store.js.map +1 -1
- package/dist/electron-build/agents/shell-guard.js +110 -14
- package/dist/electron-build/agents/shell-guard.js.map +1 -1
- package/dist/electron-build/agents/skill-health.js +103 -0
- package/dist/electron-build/agents/skill-health.js.map +1 -0
- package/dist/electron-build/agents/skill-ledger.js +146 -0
- package/dist/electron-build/agents/skill-ledger.js.map +1 -0
- package/dist/electron-build/agents/tool-loop-guard.js +319 -0
- package/dist/electron-build/agents/tool-loop-guard.js.map +1 -0
- package/dist/electron-build/agents/tool-path-args.js +63 -0
- package/dist/electron-build/agents/tool-path-args.js.map +1 -0
- package/dist/electron-build/agents/tool-result-gate.js +59 -0
- package/dist/electron-build/agents/tool-result-gate.js.map +1 -0
- package/dist/electron-build/agents/tool-subset.js +116 -0
- package/dist/electron-build/agents/tool-subset.js.map +1 -0
- package/dist/electron-build/agents/tool-telemetry.js +108 -0
- package/dist/electron-build/agents/tool-telemetry.js.map +1 -0
- package/dist/electron-build/agents/wallet-tools.js +102 -0
- package/dist/electron-build/agents/wallet-tools.js.map +1 -0
- package/dist/electron-build/bootstrap/context-collector.js +38 -7
- package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
- package/dist/electron-build/bootstrap/lifecycle-hooks.js +12 -3
- package/dist/electron-build/bootstrap/lifecycle-hooks.js.map +1 -1
- package/dist/electron-build/bootstrap/persona-init.js +216 -0
- package/dist/electron-build/bootstrap/persona-init.js.map +1 -0
- package/dist/electron-build/bootstrap/persona-loader.js +5 -1
- package/dist/electron-build/bootstrap/persona-loader.js.map +1 -1
- package/dist/electron-build/cli/wallet-store.js +200 -0
- package/dist/electron-build/cli/wallet-store.js.map +1 -0
- package/dist/electron-build/context-compaction/auto-compact.js +38 -2
- package/dist/electron-build/context-compaction/auto-compact.js.map +1 -1
- package/dist/electron-build/external-engines/delegate.js +22 -0
- package/dist/electron-build/external-engines/delegate.js.map +1 -1
- package/dist/electron-build/llm/pi-ai.js +25 -0
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/electron-build/orbitdb/cid-database.js +55 -0
- package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
- package/dist/electron-build/orbitdb/ipfs-node.js +36 -11
- package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
- package/dist/electron-build/web/server-storage.js +26 -5
- package/dist/electron-build/web/server-storage.js.map +1 -1
- package/dist/external-engines/delegate.js +21 -0
- package/dist/index.js +1324 -173
- package/dist/llm/pi-ai.js +23 -0
- package/dist/orbitdb/cid-database.js +55 -0
- package/dist/orbitdb/event-index.js +370 -0
- package/dist/orbitdb/event-query.js +131 -0
- package/dist/orbitdb/event-shell.js +510 -0
- package/dist/orbitdb/group-access.js +381 -0
- package/dist/orbitdb/group-shards.js +140 -0
- package/dist/orbitdb/ipfs-node.js +36 -11
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/client.js +3 -2
- package/dist/web/mobile-core.js +7762 -6289
- package/dist/web/routes-mobile-tasks.js +2 -1
- package/dist/web/server-storage.js +26 -5
- package/dist/web/server.js +2 -0
- package/dist/web/sw.js +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { createHyperswarmCommunicator, createTopic, KeyManager, AgentAuthManager, createVerificationManager, } from '@diap/sdk';
|
|
2
2
|
import { irohTransport } from './network/iroh-transport.js';
|
|
3
|
+
import { getLastAiTiming, getLastAiUsage } from './llm/pi-ai.js';
|
|
4
|
+
import { setHistoryScope } from './cli/input-history.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';
|
|
8
|
+
import { loadWalletTool } from './agents/wallet-tools.js';
|
|
3
9
|
import { HybridMessenger } from './network/hybrid-messenger.js';
|
|
4
10
|
import * as ed25519 from '@noble/ed25519';
|
|
5
11
|
import { sha512 } from '@noble/hashes/sha2.js';
|
|
6
12
|
import * as fs from 'fs/promises';
|
|
13
|
+
import * as fsSync from 'fs'; // 2026-09-30: 启动面板读技能快照(同步; 渲染路径上不能 await)
|
|
7
14
|
import { existsSync, mkdirSync } from 'fs';
|
|
8
15
|
import * as path from 'path';
|
|
9
16
|
import { spawn, execSync } from 'child_process';
|
|
@@ -11,21 +18,96 @@ import * as os from 'os';
|
|
|
11
18
|
import { documentReader } from './documents/reader.js';
|
|
12
19
|
import { initMinimax } from './constraints/index.js';
|
|
13
20
|
import { createAgentSession } from './agents/pi-sdk.js';
|
|
21
|
+
import { SessionStore } from './agents/session-store.js'; // 2026-09-30: 退出存档 —— 只为把落盘路径如实打出来
|
|
22
|
+
import { kuboAlreadyRunning } from './agents/pi-sdk-tools.js';
|
|
14
23
|
import { createSubAgentManager } from './agents/subagent-manager.js';
|
|
15
24
|
import { getGlobalSharedContext } from './social/global-shared-context.js';
|
|
16
25
|
import { createBollharnessIntegration } from './bollharness-integration/index.js';
|
|
17
26
|
import * as readline from 'readline';
|
|
18
|
-
import { printBanner, renderDialog, renderUserMessage, renderAgentMessage, renderMessageBox, renderToolCallListItem, termWidth, ROBOT_HEAD, BOLLOON_BANNER, dispWidth } from './cli/loading-tui.js';
|
|
19
|
-
import { startInk, stopInk, suspendInk, resumeInk, inkAppendLine as appendLine,
|
|
27
|
+
import { printBanner, termHeight, truncate, renderDialog, renderUserMessage, renderAgentMessage, renderMessageBox, renderToolCallListItem, termWidth, ROBOT_HEAD, BOLLOON_BANNER, BOLLOON_BANNER_SMALL, dispWidth } from './cli/loading-tui.js';
|
|
28
|
+
import { startInk, stopInk, suspendInk, resumeInk, inkAppendLine as appendLine, inkSetStatus, inkSetThinking, inkSetTransient, bootPanelMaxLines } from './cli/ink-app.js';
|
|
20
29
|
// 2026-09-26: 启动期日志闸门 (默认静默加载日志 + 写文件 + verbose 全量回流 + 信号行不吞)
|
|
21
30
|
import { installStartupLogGate, isStartupVerbose, startupLogPath, VERBOSE_ENV, carriesHumanSignal, logStartupLine } from './cli/log-gate.js';
|
|
22
31
|
// 2026-09-27: 回复流卫生 — 内部运行日志 (循环推进/运行登记/收尾计数…) 不进对话回复流, 改落日志文件
|
|
23
32
|
import { isInternalRunLog, appendInternalRunLog } from './cli/reply-hygiene.js';
|
|
24
33
|
// 2026-09-27: 启动**前言 / 就绪度报告**闸门 (默认不上屏 → 面板一行 + 显式查询命令; 失败折成面板提示)
|
|
25
|
-
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';
|
|
26
35
|
import { clipboardWrite } from './cli/clipboard.js';
|
|
27
36
|
// 启动自动检查更新:后台、节流、检测到新版本自动安装(可被 --no-update / BOLLOON_SKIP_UPDATE 关闭)
|
|
28
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
|
+
]);
|
|
29
111
|
const _require = createRequire(import.meta.url);
|
|
30
112
|
const _BOLLOON_VERSION = (() => {
|
|
31
113
|
try {
|
|
@@ -61,6 +143,75 @@ const MOVE_UP = '\x1b[A';
|
|
|
61
143
|
const CLEAR_LINE = '\x1b[2K';
|
|
62
144
|
const HIDE_CURSOR = '\x1b[?25l';
|
|
63
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
|
+
}
|
|
64
215
|
const s = {
|
|
65
216
|
banner: () => {
|
|
66
217
|
printBanner(_BOLLOON_VERSION);
|
|
@@ -71,52 +222,48 @@ const s = {
|
|
|
71
222
|
status === 'warn' ? `${YELLOW}⚠` :
|
|
72
223
|
status === 'error' ? `${MAGENTA}✗` :
|
|
73
224
|
`${CYAN}●`;
|
|
74
|
-
|
|
225
|
+
writeOut(` ${check} ${WHITE}[${num}/${total}]${GRAY} ${text}${RESET}`);
|
|
75
226
|
},
|
|
76
|
-
success: (text) =>
|
|
77
|
-
warn: (text) =>
|
|
78
|
-
error: (text) =>
|
|
79
|
-
info: (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}`),
|
|
80
231
|
section: (title) => {
|
|
81
|
-
|
|
232
|
+
writeOut(`\n${BLUE}━━━ ${WHITE}${BOLD}${title}${RESET} ${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
|
|
82
233
|
},
|
|
83
|
-
divider: () =>
|
|
84
|
-
prompt: (text) =>
|
|
234
|
+
divider: () => writeOut(`\n${GRAY}${'─'.repeat(50)}${RESET}\n`),
|
|
235
|
+
prompt: (text) => writeOut(`\n${CYAN}❯ ${WHITE}${text}${RESET}`),
|
|
85
236
|
response: (label, content) => {
|
|
86
|
-
|
|
237
|
+
writeOut(`\n${GREEN}${label}${RESET}\n${content}\n`);
|
|
87
238
|
},
|
|
88
239
|
agentCard: (agent) => {
|
|
89
240
|
const statusColor = agent.status === 'active' ? GREEN :
|
|
90
241
|
agent.status === 'idle' ? YELLOW :
|
|
91
242
|
agent.status === 'busy' ? MAGENTA : GRAY;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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(', ')}`);
|
|
96
247
|
if (agent.did)
|
|
97
|
-
|
|
98
|
-
|
|
248
|
+
writeOut(` ${GRAY}DID:${RESET} ${agent.did}`);
|
|
249
|
+
writeOut('');
|
|
99
250
|
},
|
|
100
251
|
Thinking: () => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
dots = (dots + 1) % 4;
|
|
109
|
-
const dotStr = '.'.repeat(dots || 1);
|
|
110
|
-
appendLine(`\r ${frames[i]} 思考${dotStr} `);
|
|
111
|
-
}, 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);
|
|
112
259
|
},
|
|
113
260
|
clearThinking: (interval) => {
|
|
114
261
|
clearInterval(interval);
|
|
115
|
-
|
|
262
|
+
inkSetThinking(false);
|
|
116
263
|
},
|
|
117
264
|
dialog: async (title, promptText) => {
|
|
118
265
|
return new Promise((resolve) => {
|
|
119
|
-
|
|
266
|
+
writeOut(renderDialog({ title, prompt: promptText }));
|
|
120
267
|
const rl = readline.createInterface({
|
|
121
268
|
input: process.stdin,
|
|
122
269
|
output: process.stdout
|
|
@@ -142,6 +289,21 @@ ed25519.hashes.sha512 = sha512;
|
|
|
142
289
|
let startupPanelFirst = false;
|
|
143
290
|
/** 面板第一帧已经画出来了 (startCLI 取走面板提示之后): 晚到的降级提示直接追加进面板 */
|
|
144
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?.();
|
|
300
|
+
/**
|
|
301
|
+
* 2026-09-30: 启动面板 / 就绪行**各只许追加一次**.
|
|
302
|
+
* 防任何重入路径 (会话切换 / 重复 boot / 恢复) 把面板再叠一条 —— leo 报的「顶框重复 14 次」那一类
|
|
303
|
+
* 都落在这个类里; 叠了就再也说不清屏上哪一条是真的.
|
|
304
|
+
*/
|
|
305
|
+
let bootPanelPrinted = false;
|
|
306
|
+
let bootReadinessPrinted = false;
|
|
145
307
|
/**
|
|
146
308
|
* 启动期(面板之前)一行进度 / 降级输出 —— 交互默认口径下的**唯一出路**。
|
|
147
309
|
*
|
|
@@ -221,8 +383,8 @@ async function bootstrapIdentity() {
|
|
|
221
383
|
const username = getUserName();
|
|
222
384
|
const suffix = did.split(':').pop()?.substring(0, 4);
|
|
223
385
|
const name = `blln-${username}-${suffix}`;
|
|
224
|
-
|
|
225
|
-
|
|
386
|
+
writeOut(` ${reused ? GRAY + '复用 ' : ''}${GRAY}DID:${RESET} ${did}`);
|
|
387
|
+
writeOut(` ${GRAY}名称:${RESET} ${name}`);
|
|
226
388
|
s.step(1, 5, reused ? '复用 DIAP 身份' : '生成 DIAP 身份', 'ok');
|
|
227
389
|
return { keypair: kp, did, name };
|
|
228
390
|
}
|
|
@@ -283,7 +445,7 @@ async function bootstrapP2P(verifier) {
|
|
|
283
445
|
await comm.start();
|
|
284
446
|
const topic = createTopic('bolloon-agent-harness');
|
|
285
447
|
await comm.joinTopic(topic);
|
|
286
|
-
|
|
448
|
+
writeOut(` ${GRAY}主题:${RESET} ${topic.slice(0, 8).toString('hex')}...`);
|
|
287
449
|
s.step(3, 5, '启动 P2P 网络', 'ok');
|
|
288
450
|
return comm;
|
|
289
451
|
}
|
|
@@ -301,20 +463,20 @@ async function bootstrapIroh(keypair, name) {
|
|
|
301
463
|
s.step(4, 5, '启动 iroh P2P', 'loading');
|
|
302
464
|
try {
|
|
303
465
|
const node = await irohTransport.start();
|
|
304
|
-
|
|
466
|
+
writeOut(` ${GRAY}iroh:${RESET} ${node.nodeId.substring(0, 16)}...`);
|
|
305
467
|
hybridMessenger = new HybridMessenger({
|
|
306
468
|
preferIrohForLarge: true,
|
|
307
469
|
largeThresholdBytes: 64 * 1024,
|
|
308
470
|
enableRelay: true,
|
|
309
471
|
});
|
|
310
472
|
hybridMessenger.onMessage('task', async (msg) => {
|
|
311
|
-
|
|
473
|
+
writeOut(`[iroh] Task from ${msg.from.substring(0, 12)}...: ${new TextDecoder().decode(msg.payload).substring(0, 50)}...`);
|
|
312
474
|
});
|
|
313
475
|
hybridMessenger.onMessage('blob', async (msg) => {
|
|
314
|
-
|
|
476
|
+
writeOut(`[iroh] Blob from ${msg.from.substring(0, 12)}...: ${msg.payload.length} bytes`);
|
|
315
477
|
});
|
|
316
478
|
hybridMessenger.onMessage('response', async (msg) => {
|
|
317
|
-
|
|
479
|
+
writeOut(`[iroh] Response from ${msg.from.substring(0, 12)}...`);
|
|
318
480
|
});
|
|
319
481
|
if (agentIdentity) {
|
|
320
482
|
agentIdentity.irohNodeId = node.nodeId;
|
|
@@ -323,14 +485,132 @@ async function bootstrapIroh(keypair, name) {
|
|
|
323
485
|
}
|
|
324
486
|
catch (e) {
|
|
325
487
|
s.step(4, 5, '启动 iroh P2P', 'warn');
|
|
326
|
-
|
|
327
|
-
|
|
488
|
+
writeOut(` ${YELLOW}iroh 启动失败: ${e.message}${RESET}`);
|
|
489
|
+
writeOut(` ${GRAY}继续使用 Hyperswarm P2P${RESET}`);
|
|
328
490
|
}
|
|
329
491
|
}
|
|
330
492
|
// ---------------------------------------------------------------------------
|
|
331
493
|
// Agent 懒加载
|
|
332
494
|
// ---------------------------------------------------------------------------
|
|
495
|
+
// 2026-10-01: /group 的"当前群"(进程内; 重启后需重新 /group use)
|
|
496
|
+
let currentGroupId;
|
|
333
497
|
let agent = null;
|
|
498
|
+
/**
|
|
499
|
+
* 退出存档 (2026-09-30, leo: 「按 ctrl+c / esc 退出的时候, 自动存档 session 进程, 最后显示再见👋」)
|
|
500
|
+
*
|
|
501
|
+
* 三条纪律:
|
|
502
|
+
* 1. **幂等** —— 两条退出路径 (Ctrl+C/双击 Esc 与输入 exit) 都调它, 只存一次 (sessionArchivedOnExit).
|
|
503
|
+
* 2. **如实** —— 存成功给 key 与落盘路径; 没建过会话就直说"无需存档"; 失败给原因。**不许静默**。
|
|
504
|
+
* 3. **不阻塞退出** —— 存档失败也照常走完退出流程 (存不下不是不退出的理由)。
|
|
505
|
+
*/
|
|
506
|
+
let sessionArchivedOnExit = false;
|
|
507
|
+
/**
|
|
508
|
+
* 退出期上屏 (2026-09-30): `stopInk()` 之后 Ink 已卸载, 此时 `appendLine()` 只进 store、**打不到终端**
|
|
509
|
+
* —— 既有代码就是这样, 所以「👋 再见!」用户**从来没真看见过** (这次真机日志坐实)。
|
|
510
|
+
* 退出期的最后几句话必须用 `process.stdout.write` 直写。
|
|
511
|
+
*/
|
|
512
|
+
function writeExitLine(line) {
|
|
513
|
+
try {
|
|
514
|
+
process.stdout.write(line + '\n');
|
|
515
|
+
}
|
|
516
|
+
catch { /* 写不出去也不能挡住退出 */ }
|
|
517
|
+
}
|
|
518
|
+
/** 本次 CLI 会话的 key (bootSessionId 算好后写进来; 退出存档两条路径都读它) */
|
|
519
|
+
let cliSessionKey = null;
|
|
520
|
+
async function archiveSessionOnExit(key) {
|
|
521
|
+
if (sessionArchivedOnExit)
|
|
522
|
+
return '';
|
|
523
|
+
sessionArchivedOnExit = true;
|
|
524
|
+
const t0 = Date.now();
|
|
525
|
+
try {
|
|
526
|
+
if (!agent || !key || typeof agent.saveCurrentSession !== 'function') {
|
|
527
|
+
return `${C_DIM}(本次没建会话, 无需存档)${RESET}`;
|
|
528
|
+
}
|
|
529
|
+
await agent.saveCurrentSession(key);
|
|
530
|
+
// 2026-09-30 (leo: 「结束进程的时候没有加载 title 和总结,以至于打开 session 后没有内容」):
|
|
531
|
+
// 退出存档时**顺手把 AI 标题/摘要算好写回** —— 否则 /sessions 里那条只有会话 ID (数据在, 只是没标题)。
|
|
532
|
+
// 纪律: 12s 内不回来就放行 (存档已完成, 标题是附加品); 失败**如实标注**, 不挡退出, 不编标题。
|
|
533
|
+
let titleNote = '';
|
|
534
|
+
try {
|
|
535
|
+
const { summarizeSession } = await import('./cli/session-summary.js');
|
|
536
|
+
const d = await summarizeSession(key, { store: new SessionStore() });
|
|
537
|
+
if (d.source === 'ai' && d.title)
|
|
538
|
+
titleNote = ` · ${C_TEXT}${d.title}${RESET}${C_DIM}`;
|
|
539
|
+
else if (d.source === 'raw' && d.title)
|
|
540
|
+
titleNote = ` · ${C_DIM}(AI 不可用, 标题用了首条消息)`;
|
|
541
|
+
else
|
|
542
|
+
titleNote = ` · ${C_DIM}(没有可总结的用户内容)`;
|
|
543
|
+
}
|
|
544
|
+
catch (e) {
|
|
545
|
+
titleNote = ` · ${C_WARN}(标题没生成: ${String(e?.message || e).slice(0, 40)})${RESET}${C_DIM}`;
|
|
546
|
+
}
|
|
547
|
+
// 2026-09-30: 退出也落一次 commit —— 会话树 (git) 靠 fork/退出两个时点建起来, 不做消息级提交
|
|
548
|
+
let treeNote = '';
|
|
549
|
+
try {
|
|
550
|
+
const { commitTree } = await import('./cli/session-tree.js');
|
|
551
|
+
const tr = await commitTree(new SessionStore(), `session: ${key} · exit save`);
|
|
552
|
+
if (!tr.ok)
|
|
553
|
+
treeNote = ` · ${C_WARN}树未记上: ${String(tr.error).slice(0, 60)}${RESET}${C_DIM}`;
|
|
554
|
+
}
|
|
555
|
+
catch { /* 树失败不挡退出 */ }
|
|
556
|
+
let where = '';
|
|
557
|
+
try {
|
|
558
|
+
where = new SessionStore().pathFor(key);
|
|
559
|
+
}
|
|
560
|
+
catch { /* 路径拿不到不影响存档本身 */ }
|
|
561
|
+
// 2026-09-30 (leo: 「没有结束时间,没有总花费时间和日期记录」): 退出的那一行要能回答
|
|
562
|
+
// 「这次会话从几点到几点、一共多久」—— 起点取会话真起点 (cliStartTime, 与状态栏 ⏱ 同一个钟)。
|
|
563
|
+
const p2 = (n) => String(n).padStart(2, '0');
|
|
564
|
+
const stamp = (d) => `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())} ${p2(d.getHours())}:${p2(d.getMinutes())}:${p2(d.getSeconds())}`;
|
|
565
|
+
const ended = new Date();
|
|
566
|
+
const timeNote = cliStartTime
|
|
567
|
+
? ` · ${stamp(new Date(cliStartTime))} → ${stamp(ended)} · 用时 ${fmtDuration(Date.now() - cliStartTime)}`
|
|
568
|
+
: ` · ${stamp(ended)}`;
|
|
569
|
+
return `${C_DIM}📦 session 已存档: ${key}${timeNote}${RESET}\n ${C_DIM}${where ? where : ''} · ${Date.now() - t0}ms${titleNote}${treeNote}${RESET}`;
|
|
570
|
+
}
|
|
571
|
+
catch (e) {
|
|
572
|
+
const msg = String(e?.message || e);
|
|
573
|
+
// 2026-09-30: "没内容可存档" 与 "存档失败" 是两件事 —— 前者不该报成 ⚠ 吓人。
|
|
574
|
+
// (历史事故: 写路径误用读语义 ⇒ 新会话一律抛 not found ⇒ 用户看到红字却什么也没坏)
|
|
575
|
+
if (/session not found/i.test(msg)) {
|
|
576
|
+
return `${C_DIM}(本次没有可存档的会话内容 — 会话文件还没落盘)${RESET}`;
|
|
577
|
+
}
|
|
578
|
+
return `${C_WARN}⚠ session 存档失败: ${msg.slice(0, 90)}${RESET}`;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* 读一条会话的 metadata.title 并刷新工具栏右侧 (没有就清空)。
|
|
583
|
+
* 2026-09-30 (leo: 「切换 session 后也要有 title 加载」): 启动 / 切会话 / 回合结束三处共用同一套口径 ——
|
|
584
|
+
* 否则切完会话右侧还挂上一条的标题 (或空着), 看着像没生效。
|
|
585
|
+
*/
|
|
586
|
+
async function refreshSessionTitle(key) {
|
|
587
|
+
// 2026-09-30 (leo: 「切换后没有立刻刷新」): **先立刻清空并重绘** —— 否则等待异步读盘那几十毫秒里,
|
|
588
|
+
// 工具栏右侧还挂着上一条会话的标题, 看着就是"没刷新"。读完再填。
|
|
589
|
+
cliSessionPreview = '';
|
|
590
|
+
cliSessionTitleKey = key || null;
|
|
591
|
+
try {
|
|
592
|
+
inkSetStatus(getStatus());
|
|
593
|
+
}
|
|
594
|
+
catch { /* Ink 未起 */ }
|
|
595
|
+
if (!key)
|
|
596
|
+
return;
|
|
597
|
+
try {
|
|
598
|
+
const fs = await import('fs/promises');
|
|
599
|
+
const store = new SessionStore();
|
|
600
|
+
const raw = JSON.parse(await fs.readFile(store.pathFor(key), 'utf-8'));
|
|
601
|
+
const md = (Array.isArray(raw) ? {} : raw?.metadata) || {};
|
|
602
|
+
const t = String(md.title || '').trim();
|
|
603
|
+
// 只认"这条读的确实是当前会话" —— 读盘期间可能又切了一次
|
|
604
|
+
cliSessionPreview = (t && t !== key && cliSessionTitleKey === key) ? t.slice(0, 24) : '';
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
cliSessionPreview = ''; // 读不到/没标题 ⇒ 空着 (不拿旧标题冒充)
|
|
608
|
+
}
|
|
609
|
+
try {
|
|
610
|
+
inkSetStatus(getStatus());
|
|
611
|
+
}
|
|
612
|
+
catch { /* Ink 未起 */ }
|
|
613
|
+
}
|
|
334
614
|
/** 2026-08-09: agent 当前绑定的 channel id (null = 默认 harness 身份) — 切换时据此重建 */
|
|
335
615
|
let agentBoundChannelId = null;
|
|
336
616
|
let harness = null;
|
|
@@ -358,15 +638,34 @@ async function getAgent() {
|
|
|
358
638
|
catch { /* 读不到就退默认 */ }
|
|
359
639
|
}
|
|
360
640
|
let identityDoc;
|
|
361
|
-
|
|
362
|
-
|
|
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 取 (不再用共享名)
|
|
363
658
|
identityDoc = {
|
|
364
659
|
did: chIdentity.did,
|
|
365
|
-
name: chIdentity
|
|
660
|
+
name: nameForChannel(chIdentity, chIdentity.agentId),
|
|
366
661
|
publicKey: chIdentity.publicKey,
|
|
367
662
|
createdAt: Date.now(),
|
|
368
663
|
};
|
|
369
664
|
}
|
|
665
|
+
else if (perAgentIdentity) {
|
|
666
|
+
// channel 缺身份 / 只有假 DID ⇒ 用**该 agent 自己的**真身份 (按 agentId 落盘密钥, 稳定)
|
|
667
|
+
identityDoc = { ...perAgentIdentity, createdAt: Date.now() };
|
|
668
|
+
}
|
|
370
669
|
else if (agentIdentity) {
|
|
371
670
|
identityDoc = {
|
|
372
671
|
did: agentIdentity.did,
|
|
@@ -382,15 +681,60 @@ async function getAgent() {
|
|
|
382
681
|
else {
|
|
383
682
|
identityDoc = undefined;
|
|
384
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 { /* 自证失败不影响主流程 */ }
|
|
385
710
|
const loadSessionKey = targetChannelId
|
|
386
711
|
? `${targetChannelId}:${chIdentity?.currentSessionId || 'default'}`
|
|
387
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
|
+
}
|
|
388
727
|
agent = await createAgentSession({
|
|
389
728
|
cwd: process.cwd(),
|
|
390
729
|
peerId: targetChannelId ?? 'harness',
|
|
391
|
-
identityDoc,
|
|
392
730
|
// 2026-08-09: 透传 channel.agentId → persona docs 按 agent 加载 (身份真正变化)
|
|
393
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,
|
|
394
738
|
loadSessionKey,
|
|
395
739
|
});
|
|
396
740
|
agentBoundChannelId = targetChannelId;
|
|
@@ -418,7 +762,7 @@ async function dispatchTask(raw) {
|
|
|
418
762
|
const task = safeParse(body);
|
|
419
763
|
if (!task)
|
|
420
764
|
return `ERR|${JSON.stringify({ code: 'bad_format' })}`;
|
|
421
|
-
|
|
765
|
+
writeOut(`\n📥 [${task.type}] from=${task.from?.substring(0, 18)}... id=${task.id}`);
|
|
422
766
|
try {
|
|
423
767
|
switch (task.type) {
|
|
424
768
|
case 'summarize':
|
|
@@ -438,7 +782,7 @@ async function handleSummarize(task) {
|
|
|
438
782
|
return `ERR|${JSON.stringify({ code: 'no_path' })}`;
|
|
439
783
|
const a = await getAgent();
|
|
440
784
|
const { summary, qualityScore } = await a.summarizeDocument(task.documentPath);
|
|
441
|
-
|
|
785
|
+
writeOut(` ✅ 质量=${(qualityScore * 10).toFixed(1)}/10`);
|
|
442
786
|
return `OK|${JSON.stringify({ id: task.id, type: 'summarize', qualityScore, summary })}`;
|
|
443
787
|
}
|
|
444
788
|
async function handleImprove(task) {
|
|
@@ -452,7 +796,7 @@ async function handleImprove(task) {
|
|
|
452
796
|
context: `来自节点: ${task.from}`,
|
|
453
797
|
});
|
|
454
798
|
const ok = res.improved ?? false;
|
|
455
|
-
|
|
799
|
+
writeOut(` ✅ 改进${ok ? '成功' : '失败'} 质量=${(res.qualityScore * 10).toFixed(1)}/10 自动发送=${res.shouldAutoSend}`);
|
|
456
800
|
return `OK|${JSON.stringify({
|
|
457
801
|
id: task.id, type: 'improve', improved: ok,
|
|
458
802
|
qualityScore: res.qualityScore, shouldAutoSend: res.shouldAutoSend,
|
|
@@ -483,8 +827,23 @@ let _startupLogGate = null;
|
|
|
483
827
|
const pendingQueue = [];
|
|
484
828
|
let cliStartTime = 0;
|
|
485
829
|
let cliModelName = '…';
|
|
486
|
-
let cliAgentName = '…'
|
|
830
|
+
let cliAgentName = 'bolloon'; // 2026-09-30: 别用 '…' —— 身份没解析出来时状态栏显示个省略号, 看着像"没有 bolloon 标识"
|
|
487
831
|
let cliActiveChannelId = null;
|
|
832
|
+
/**
|
|
833
|
+
* 状态栏"活数据" (leo 2026-09-30: 参考别家 CLI 的 ◎ / ◷ / ↑ t/s / ⚙ / ⏲ / ✓ 那一行)
|
|
834
|
+
* · cliTurnStartedAt / cliLastTurnMs —— 本轮用时(◷) 与 上一步用时(✓)
|
|
835
|
+
* · cliTurnReplyBytes —— 本轮回复字节数, 折 ≈t/s (按 ~3.5B/token 估, 所以标"≈")
|
|
836
|
+
* · 在跑工具数不另存 —— 直接用已有的 tuiToolCalls (它就是"还没结束的工具"清单)
|
|
837
|
+
* 测不到的段 (比如"限流窗口还剩 20m") 一律不显示 —— 不编数字。
|
|
838
|
+
*/
|
|
839
|
+
let cliTurnStartedAt = 0;
|
|
840
|
+
let cliLastTurnMs = 0;
|
|
841
|
+
let cliTurnReplyBytes = 0;
|
|
842
|
+
let cliTurnToolCount = 0; // 在跑工具数 (回合内由工具事件更新)
|
|
843
|
+
/** 当前会话的 Title/Preview (工具栏右侧显示用; 回合结束后后台生成一次, 生成过就不再跑) */
|
|
844
|
+
let cliSessionPreview = '';
|
|
845
|
+
let cliSessionTitleKey = null; // 这条标题属于哪个会话 —— 不是当前会话一律不显示 (防串台)
|
|
846
|
+
let cliSessionSummarizing = false;
|
|
488
847
|
// 2026-08-12: 当前 active channel 的 agentId (如 agent-alice). memory 落盘按 agentId 存,
|
|
489
848
|
// /memory /resume /did 读路径必须用 agentId 而非 display name (cliAgentName), 否则路径不一致读不到.
|
|
490
849
|
let cliAgentId = null;
|
|
@@ -548,16 +907,76 @@ function buildContextBar(usage) {
|
|
|
548
907
|
suffix = ` ${C_OK}✓ 已压缩${RESET}`;
|
|
549
908
|
return `${usageTxt} ${C_DIM}│${RESET} ${bar} ${barColor}${pctTxt}${RESET}${suffix}`;
|
|
550
909
|
}
|
|
910
|
+
/**
|
|
911
|
+
* 状态栏里的模型名: **只留型号**。
|
|
912
|
+
* 2026-09-30 leo: 「deepseek · deepseek-flash v0.5.3,这里有重复冗余,保留模型型号就行,provider 可以不要」
|
|
913
|
+
* (`cliModelName` 仍是 'deepseek · deepseek-flash' —— 启动面板那边保持原样, 只有状态栏缩。)
|
|
914
|
+
*/
|
|
915
|
+
function statusModelName() {
|
|
916
|
+
const parts = String(cliModelName || '').split(' · ');
|
|
917
|
+
return parts.length > 1 ? parts.slice(1).join(' · ') : String(cliModelName || '');
|
|
918
|
+
}
|
|
551
919
|
/** 状态栏: 模型 │ 当前智能体 (含 channel) │ ⏱ 时间 │ 320k/1M │ [██████░░░░] 32% (bolloon 色系) */
|
|
552
920
|
function getStatus() {
|
|
553
921
|
const usage = getCliCtxUsage();
|
|
554
|
-
|
|
555
|
-
|
|
922
|
+
// 名字兜底: 空/'…' 一律显示 bolloon —— 标识必须一直在 (leo: 「bolloon 标识没出现」)
|
|
923
|
+
const __name = (!cliAgentName || cliAgentName === '…') ? 'bolloon' : cliAgentName;
|
|
924
|
+
// 2026-09-30: 左半边按宽压扁 —— 窄终端里 `(ch:xxx)` 与 token 计数让位给右边的活数据段
|
|
925
|
+
const __w = termWidth();
|
|
926
|
+
const agentPart = (cliActiveChannelId && __w >= 120) ? `${__name} ${C_DIM}(ch:${cliActiveChannelId.slice(0, 10)})${RESET}` : __name;
|
|
927
|
+
// 左半边先建好 —— 后面对右侧活数据段做宽度取舍要用它的纯文本长度
|
|
928
|
+
const __left = `${C_ACCENT}${statusModelName()}${RESET} ${C_DIM}v${_BOLLOON_VERSION}${RESET}${C_DIM} │${RESET} ${agentPart} ${C_DIM}│${RESET} ⏱ ${C_TEXT}${fmtDuration(Date.now() - cliStartTime)}${RESET}${C_DIM} │${RESET} ${buildContextBar(usage)}`;
|
|
929
|
+
const __leftPlainLen = __left.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
930
|
+
// 2026-09-30 (leo): 四段抽到 src/cli/status-segments.ts (纯函数 ⇒ "什么时候显示什么"可确定性验证)
|
|
931
|
+
const __segs = statusSegments({
|
|
932
|
+
running: cliTurnStartedAt > 0,
|
|
933
|
+
turnElapsedMs: cliTurnStartedAt > 0 ? Date.now() - cliTurnStartedAt : 0,
|
|
934
|
+
lastTurnMs: cliLastTurnMs,
|
|
935
|
+
lastTurnReplyBytes: cliTurnReplyBytes,
|
|
936
|
+
toolCount: cliTurnToolCount,
|
|
937
|
+
aiTiming: getLastAiTiming(),
|
|
938
|
+
aiUsage: getLastAiUsage(),
|
|
939
|
+
turnStartedAt: cliTurnStartedAt,
|
|
940
|
+
}).map(s => {
|
|
941
|
+
const m = /^(\S+)\s(.*)$/.exec(s);
|
|
942
|
+
const icon = m ? m[1] : '';
|
|
943
|
+
const val = m ? m[2] : s;
|
|
944
|
+
const color = icon.startsWith('✓') ? C_OK : C_TEXT;
|
|
945
|
+
return `${C_DIM}${icon}${RESET} ${color}${val}${RESET}`;
|
|
946
|
+
});
|
|
947
|
+
// 按剩余宽度取舍 (真机 112 列时右段被截 ⇒ 看着像没出现)
|
|
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);
|
|
954
|
+
const __live = __segsFit.length
|
|
955
|
+
? ` ${C_DIM}│${RESET} ` + __segsFit.map(x => {
|
|
956
|
+
const m = /^(\S+)\s(.*)$/.exec(x);
|
|
957
|
+
const icon = m ? m[1] : '';
|
|
958
|
+
const val = m ? m[2] : x;
|
|
959
|
+
return `${C_DIM}${icon}${RESET} ${icon.startsWith('✓') ? C_OK : C_TEXT}${val}${RESET}`;
|
|
960
|
+
}).join(` ${C_DIM}│${RESET} `)
|
|
961
|
+
: '';
|
|
962
|
+
// 2026-09-30 (leo: 「这个 title 需要顶格右侧」): 会话 Title **右对齐**贴右边缘 ——
|
|
963
|
+
// 先算左半边+活数据段的显示宽, 再补空格把 `▸ 标题` 推到 width-1 处; 放不下就不显示 (不挤坏这一行)。
|
|
964
|
+
let __noteStr = '';
|
|
965
|
+
if (__notePlain) {
|
|
966
|
+
// 宽度必须用 dispWidth (East Asian Width 表): 行内的 `│ ░ ◷ ▸ ◎ ✓ ↑ ⚙` 都是**单宽**,
|
|
967
|
+
// 用 "charCode > 255 ⇒ 2" 的启发式会每行多算十几列 ⇒ 右对齐留一大截 (leo: 「没有完全右对齐」)。
|
|
968
|
+
const used = dispWidthSafe(`${__left}${__live}`);
|
|
969
|
+
// 保守测宽 (同上): 右对齐后整行最多到 width-1, 在任何终端字体下都不会折行
|
|
970
|
+
const pad = rightAlignPad(used, dispWidthSafe(__notePlain), termWidth());
|
|
971
|
+
if (pad !== null)
|
|
972
|
+
__noteStr = ' '.repeat(pad) + `${C_DIM}${__notePlain}${RESET}`;
|
|
973
|
+
}
|
|
974
|
+
return `${__left}${__live}${__noteStr}`;
|
|
556
975
|
}
|
|
557
976
|
function statusBarLine() {
|
|
558
977
|
const dur = cliStartTime ? fmtDuration(Date.now() - cliStartTime) : '0s';
|
|
559
978
|
const usage = getCliCtxUsage();
|
|
560
|
-
return `${C_ACCENT}${
|
|
979
|
+
return `${C_ACCENT}${statusModelName()}${RESET}${C_DIM} │${RESET} ${cliAgentName} ${C_DIM}│${RESET} ⏱ ${C_ACCENT}${dur}${RESET} ${C_DIM}│${RESET} ${buildContextBar(usage)}`;
|
|
561
980
|
}
|
|
562
981
|
/**
|
|
563
982
|
* 启动会话面板 — 按类别展示启动加载的 skills (真实类别 = 目录名去掉 frontmatter.name 后缀),
|
|
@@ -565,31 +984,91 @@ function statusBarLine() {
|
|
|
565
984
|
*/
|
|
566
985
|
/** 启动面板框: BOLLOON 字标 logo 顶部居中 → 下方两栏 = face 艺术字(左) + 加载内容(右) */
|
|
567
986
|
function buildBootBox(face, banner, rest) {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
987
|
+
/**
|
|
988
|
+
* 2026-09-30 修复 (leo 报「TUI 渲染出错 + 回复被截断」):
|
|
989
|
+
* 盒子外宽 = min(终端宽-2, 内容宽+4, 调用方上限) ⇒ **内容宽一旦超过 终端宽-6, 每行都被终端折行**,
|
|
990
|
+
* 艺术字因此裂成两列错位 (实测 110 列终端下 BOLLOON 艺术字宽 108 > 内宽 104).
|
|
991
|
+
* 另外原来 maxLines: 0 = 不限高 ⇒ 面板长过屏幕, 把输入行顶出视野 (30 行终端下实测输入行消失).
|
|
992
|
+
* 所以这里先把内容**裁进真实可用宽高**, 再画盒子:
|
|
993
|
+
* 宽度: 艺术字太宽 ⇒ 整块丢掉(宁可不显示) · 两栏装不下 ⇒ 先丢 face 列 · 右侧行超宽 ⇒ 逐行截断
|
|
994
|
+
* 高度: 面板 ≤ 终端行数 - 10 (给输入行/状态栏留位置), 砍掉的尾部如实标注行数与自查入口
|
|
995
|
+
*/
|
|
571
996
|
const gap = 4;
|
|
572
|
-
const
|
|
573
|
-
const
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
997
|
+
const maxInner = Math.max(24, termWidth() - 6); // 外框 = │ + 空 + 内容 + 空 + │
|
|
998
|
+
const rows = termHeight();
|
|
999
|
+
const bannerW = banner.reduce((m, l) => Math.max(m, dispWidth(l)), 0);
|
|
1000
|
+
// 装不下(极窄终端)仍退回一行文字 —— 保证任何宽度都有字标, 不再是空白
|
|
1001
|
+
const useBanner = bannerW > 0 && bannerW + 4 <= maxInner ? banner : [`${C_ACCENT}${BOLD}BOLLOON${RESET}`];
|
|
1002
|
+
let useFace = face;
|
|
1003
|
+
const faceW0 = face.reduce((m, l) => Math.max(m, dispWidth(l)), 0);
|
|
1004
|
+
const restMax0 = rest.reduce((m, l) => Math.max(m, dispWidth(l)), 0);
|
|
1005
|
+
/**
|
|
1006
|
+
* 2026-09-30 (leo: 「我说的 logo 是面板里面的小吉祥物, 它看不到了」):
|
|
1007
|
+
* 原来的判据是「吉祥物 + 最宽内容行 > 内宽 ⇒ **把吉祥物整块丢掉**」——
|
|
1008
|
+
* 而技能类目行天生很长 (openclaw-imports: 3d-web-experience, …), 于是**每次都触发、吉祥物永远不显示**。
|
|
1009
|
+
* 优先级反了: 吉祥物是面板的标识, 内容行可以裁。现在只在
|
|
1010
|
+
* 「连最小内容宽度都放不下」(真的窄) 时才丢吉祥物; 否则**裁窄右侧内容**让它俩共存。
|
|
1011
|
+
*/
|
|
1012
|
+
const MIN_REST_W = 30;
|
|
1013
|
+
if (useFace.length > 0 && faceW0 + gap + MIN_REST_W > maxInner)
|
|
1014
|
+
useFace = [];
|
|
1015
|
+
const faceW = useFace.reduce((m, l) => Math.max(m, dispWidth(l)), 0);
|
|
1016
|
+
const room = Math.max(16, maxInner - (useFace.length > 0 ? faceW + gap : 0));
|
|
1017
|
+
const restFit = rest.map((l) => (dispWidth(l) > room ? truncate(l, room) : l));
|
|
1018
|
+
const bannerRows = useBanner.length > 0 ? useBanner.length + 1 : 0; // +1 = banner 与两栏之间的空行
|
|
1019
|
+
// 2026-09-30 (leo: 「还是启动后会出现顶部的刷新」): 面板高过**消息窗口**时, Ink 清不掉上一帧
|
|
1020
|
+
// ⇒ 退化成追加模式, 顶框和艺术字被一条条重复写到屏上. 所以高度按窗口真高算, 不拍脑袋 (原来 rows-10).
|
|
1021
|
+
const bootWinH = bootPanelMaxLines(rows, termWidth());
|
|
1022
|
+
// 2026-09-30 (leo: 「面板右侧的 skills 内容不见了」): **内容优先** —— 面板内容按终端高度留出
|
|
1023
|
+
// 输入栏/状态栏的余量来给 (不按"面板区"的高度砍); 装不下的部分由**面板区滚动**看全.
|
|
1024
|
+
// 上一版按区域高算 ⇒ 小终端下面板只剩 3 行内容, skills 直接没了 (回归, 已改回).
|
|
1025
|
+
// 2026-09-30 (leo: 「内容出现了, logo 消失了」): 面板进 <Static>(无限高度) 后, 一旦
|
|
1026
|
+
// "面板 + 底部块" 超过可视行数, **被顶出视野的正是顶部的 logo** (内容在下面反而留着)。
|
|
1027
|
+
// 原来 rows-10 正好卡满(0 余量) ⇒ 最后那个换行就把顶行(logo)滚掉。
|
|
1028
|
+
// 底部块实测 6 行 + 就绪行 1 行 ⇒ 这里留 3 行余量, 启动这一刻 logo 一定看得见。
|
|
1029
|
+
const bodyCap = Math.max(4, rows - 13 - bannerRows); // 下限 4: 宁可少列类目, 也要保住整只 logo
|
|
1030
|
+
void bootWinH;
|
|
1031
|
+
let restLines = restFit;
|
|
1032
|
+
if (restLines.length > bodyCap) {
|
|
1033
|
+
const dropped = restLines.length - (bodyCap - 1);
|
|
1034
|
+
restLines = [...restLines.slice(0, bodyCap - 1), `${C_DIM}… 还有 ${dropped} 行 (PgUp 滚面板 / bolloon status 看全量)${RESET}`];
|
|
1035
|
+
}
|
|
1036
|
+
const restMax = restLines.reduce((m, l) => Math.max(m, dispWidth(l)), 0);
|
|
1037
|
+
const twoColW = useFace.length > 0 ? faceW + gap + restMax : restMax;
|
|
1038
|
+
const contentW = Math.max(useBanner.length > 0 ? bannerW + 8 : 0, twoColW);
|
|
1039
|
+
const center = (rs) => rs.map((l) => ' '.repeat(Math.max(0, Math.floor((contentW - dispWidth(l)) / 2))) + l);
|
|
1040
|
+
const n = restLines.length;
|
|
1041
|
+
const fStart = Math.max(0, Math.floor((n - useFace.length) / 2)); // face 列对内容垂直居中 = 等高
|
|
1042
|
+
const twoCol = restLines.map((r, i) => {
|
|
1043
|
+
const f = (useFace.length > 0 && i >= fStart && i < fStart + useFace.length) ? useFace[i - fStart] : null;
|
|
1044
|
+
const fpart = f
|
|
1045
|
+
? `${f}${' '.repeat(Math.max(0, faceW - dispWidth(f)) + gap)}`
|
|
1046
|
+
: (useFace.length > 0 ? ' '.repeat(faceW + gap) : '');
|
|
580
1047
|
return fpart + r;
|
|
581
1048
|
});
|
|
582
|
-
const body = [...center(
|
|
583
|
-
|
|
1049
|
+
const body = [...(useBanner.length > 0 ? [...center(useBanner), ''] : []), ...twoCol];
|
|
1050
|
+
const outer = Math.min(termWidth() - 2, Math.max(contentW + 4, 40));
|
|
1051
|
+
return renderMessageBox({ title: '🚀 Bolloon · 启动面板', body: body.join('\n'), color: C_ACCENT, maxLines: 0, width: outer });
|
|
1052
|
+
}
|
|
1053
|
+
/** 启动面板的技能快照 (成功扫描后写; 下次超时/读失败时兜底 —— 跟启动日志同目录) */
|
|
1054
|
+
function skillsCachePath() {
|
|
1055
|
+
return path.join(path.dirname(startupLogPath()), 'panel-skills.json');
|
|
584
1056
|
}
|
|
585
1057
|
async function bootPanel(boot) {
|
|
586
1058
|
const sub = []; // tools / MCP (Promise.all 里填充, 最后统一排到类别下方)
|
|
587
1059
|
const catNames = new Map();
|
|
1060
|
+
let skillsScanTimedOut = false; // 技能扫描到点没收完 ⇒ 面板里如实标出来, 不假装"就是这些"
|
|
1061
|
+
const skillsScanErrors = []; // 2026-09-30: 读失败**必须计数写出来** —— 静默吞掉 = 屏上"0 skills"(假 0, 比不显示更坏)
|
|
1062
|
+
let skillsFromCache = false; // 本次没扫到 ⇒ 用上次成功扫描的快照, 并如实标注来源
|
|
1063
|
+
let skillsCacheAt = '';
|
|
588
1064
|
await Promise.all([
|
|
589
1065
|
(async () => {
|
|
590
1066
|
try {
|
|
591
1067
|
// 真实类别 = 目录名前缀去掉技能名后缀 (SKILL.md 无 category 字段, 但 frontmatter.name 是真名:
|
|
592
1068
|
// software-development-bolloon-development / name=bolloon-development → software-development)
|
|
1069
|
+
// 2026-09-30: **先把模块导进来, 再开始计时** —— 原写法把 4s 预算从 import 之前就开始算,
|
|
1070
|
+
// 而冷启动那一刻 import 技能模块(带 yaml 等)本身要好几秒 ⇒ 预算被 import 吃掉, 面板报
|
|
1071
|
+
// "扫描超时 / 0 个技能"(实测: 扫描真身只 702ms/1300 个, 却显示 0).
|
|
593
1072
|
const { loadSkillsDir, defaultSkillPaths } = await import('./agents/skill-loader.js');
|
|
594
1073
|
const pushCat = (cat, name) => {
|
|
595
1074
|
const arr = catNames.get(cat) || [];
|
|
@@ -597,32 +1076,63 @@ async function bootPanel(boot) {
|
|
|
597
1076
|
arr.push(name);
|
|
598
1077
|
catNames.set(cat, arr);
|
|
599
1078
|
};
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
1079
|
+
// 2026-09-30: 多个技能根**并行**扫 (原来顺序 await ⇒ 实测 4399ms, 并行 297ms, 1300 个技能);
|
|
1080
|
+
// 并给一个上限: 超时就如实标出来, 不让"出面板"被一次磁盘抖动无限期拖住.
|
|
1081
|
+
const roots = defaultSkillPaths();
|
|
1082
|
+
const all = await Promise.race([
|
|
1083
|
+
Promise.all(roots.map((r) => loadSkillsDir(r).catch((e) => {
|
|
1084
|
+
// 2026-09-30: 原来这里 `.catch(() => [])` **静默**吞错 ⇒ 屏上"0 skills · 0 类",
|
|
1085
|
+
// 谁也看不出是"真 0"还是"读不动" (leo 报「面板内容变少了」)。现在计数 + 写出来。
|
|
1086
|
+
skillsScanErrors.push(`${path.basename(r)}: ${String(e?.message || e).slice(0, 70)}`);
|
|
1087
|
+
return [];
|
|
1088
|
+
}))),
|
|
1089
|
+
new Promise((res) => setTimeout(() => res(null), 6000)), // 冷 FS 上留足余量
|
|
1090
|
+
]);
|
|
1091
|
+
if (all === null) {
|
|
1092
|
+
skillsScanTimedOut = true;
|
|
1093
|
+
}
|
|
1094
|
+
else {
|
|
1095
|
+
for (const metas of all) {
|
|
1096
|
+
for (const m of metas) {
|
|
1097
|
+
if (m.status === 'archived')
|
|
1098
|
+
continue;
|
|
1099
|
+
const dir = m.sourcePath ? path.basename(path.dirname(m.sourcePath)) : '';
|
|
1100
|
+
const nm = m.name || '';
|
|
1101
|
+
let cat = dir;
|
|
1102
|
+
if (dir && nm && dir.endsWith(nm)) {
|
|
1103
|
+
const pre = dir.slice(0, dir.length - nm.length).replace(/-+$/, '');
|
|
1104
|
+
if (pre)
|
|
1105
|
+
cat = pre;
|
|
1106
|
+
}
|
|
1107
|
+
pushCat(cat || 'other', nm || dir);
|
|
612
1108
|
}
|
|
613
|
-
pushCat(cat || 'other', nm || dir);
|
|
614
1109
|
}
|
|
615
1110
|
}
|
|
1111
|
+
// 扫到东西就落一份快照 —— 下次超时/读失败时用它兜底(并标注来源), 不再"内容突然变少"
|
|
1112
|
+
if (catNames.size > 0) {
|
|
1113
|
+
try {
|
|
1114
|
+
const totalNow = [...catNames.values()].reduce((s2, a) => s2 + a.length, 0);
|
|
1115
|
+
await fs.mkdir(path.dirname(skillsCachePath()), { recursive: true });
|
|
1116
|
+
await fs.writeFile(skillsCachePath(), JSON.stringify({
|
|
1117
|
+
at: new Date().toLocaleString('zh-CN', { hour12: false }), total: totalNow, cats: [...catNames.entries()],
|
|
1118
|
+
}), 'utf8');
|
|
1119
|
+
}
|
|
1120
|
+
catch { /* 快照写不进去不影响面板 */ }
|
|
1121
|
+
}
|
|
616
1122
|
}
|
|
617
1123
|
catch { /* 省略 */ }
|
|
618
1124
|
})(),
|
|
619
1125
|
(async () => {
|
|
1126
|
+
/**
|
|
1127
|
+
* 2026-09-30 (leo: 「面板的加载没有和输入框一起出现, 有时间差, 且面板内部内容有缺失」):
|
|
1128
|
+
* 这里原来是 `getAgent()` —— 建**整个 agent 会话**(模型/工具/身份). 插桩实测它把事件循环
|
|
1129
|
+
* **同步阻塞**几十秒 (调用后 20s 连 end 都没打出来, 2.5s/4s 上限的定时器都点不着) ⇒ 面板被拖到
|
|
1130
|
+
* 十几秒后才出, 技能那一段也没跑完(=内容缺失). 面板只为一行 `🔧 N tools` 付这个代价不值得
|
|
1131
|
+
* ⇒ **不再等它**: 已建好的 agent 才取工具数; 全量清单看 `bolloon status`.
|
|
1132
|
+
*/
|
|
620
1133
|
try {
|
|
621
|
-
const a =
|
|
622
|
-
|
|
623
|
-
new Promise((res) => setTimeout(() => res(null), 2500)),
|
|
624
|
-
]);
|
|
625
|
-
const tools = a && typeof a.getToolList === 'function' ? a.getToolList() : null;
|
|
1134
|
+
const a = agent && typeof agent.getToolList === 'function' ? agent : null;
|
|
1135
|
+
const tools = a ? a.getToolList() : null;
|
|
626
1136
|
if (tools && tools.length > 0)
|
|
627
1137
|
sub.push(`🔧 ${tools.length} tools`);
|
|
628
1138
|
}
|
|
@@ -640,7 +1150,20 @@ async function bootPanel(boot) {
|
|
|
640
1150
|
]);
|
|
641
1151
|
// 栈式布局: face 艺术字居中 → BOLLOON 字标 logo 在其下 → 内容左对齐
|
|
642
1152
|
const art = ROBOT_HEAD;
|
|
643
|
-
|
|
1153
|
+
// 2026-09-30 (leo: 「我看不到我的 logo」): 按宽度**挑档**, 不再"窄就丢".
|
|
1154
|
+
// 原来只有一档 87 列艺术字, 终端宽 < ~97 列时整块被丢、只剩一行文字.
|
|
1155
|
+
// 挑"装得下的最大档": 大(87 列) → 小(37 列) → 都装不下才在框里退回一行文字.
|
|
1156
|
+
const candInner = Math.max(24, termWidth() - 6);
|
|
1157
|
+
// 2026-09-30 (leo: 「内容出现了, logo 消失了」): 挑档**宽和高都要装得下** ——
|
|
1158
|
+
// 面板进 <Static>(无限高度) 后, 一旦"面板 + 底部块"超过可视行数, 被顶出视野的正是顶部 logo。
|
|
1159
|
+
// 所以: 整块艺术字(87 列)装不下高就换窄版(37 列), 都不行才退回一行文字 —— 任何行数都有 logo。
|
|
1160
|
+
const candRows = termHeight();
|
|
1161
|
+
const bannerFits = (b) => {
|
|
1162
|
+
const w = Math.max(...b.map(dispWidth));
|
|
1163
|
+
const panelRows = b.length + 4 /* 最少内容行 */ + 2 /* 上下边框 */;
|
|
1164
|
+
return w + 4 <= candInner && panelRows + 7 /* 底部块+就绪行 */ <= candRows - 2;
|
|
1165
|
+
};
|
|
1166
|
+
const banner = [BOLLOON_BANNER, BOLLOON_BANNER_SMALL].map(b => b.split('\n')).find(bannerFits) || [];
|
|
644
1167
|
// 头: 目录 / 模型 / Session (预先加载信息)
|
|
645
1168
|
const rest = [];
|
|
646
1169
|
if (boot.dir)
|
|
@@ -650,6 +1173,20 @@ async function bootPanel(boot) {
|
|
|
650
1173
|
if (boot.session)
|
|
651
1174
|
rest.push(`Session: ${boot.session}`);
|
|
652
1175
|
rest.push('');
|
|
1176
|
+
// 2026-09-30 (leo 的屏面: 提示写了"快照"但数字还是 0): 回落必须在 normalized/sorted **之前** ——
|
|
1177
|
+
// 先把快照填进 catNames, 再统计, 否则出现"有提示没数字"的假面板。
|
|
1178
|
+
if (catNames.size === 0) {
|
|
1179
|
+
try {
|
|
1180
|
+
const c = JSON.parse(fsSync.readFileSync(skillsCachePath(), 'utf8'));
|
|
1181
|
+
for (const [k, arr] of (c.cats || []))
|
|
1182
|
+
catNames.set(k, arr);
|
|
1183
|
+
if (catNames.size > 0) {
|
|
1184
|
+
skillsFromCache = true;
|
|
1185
|
+
skillsCacheAt = String(c.at || '');
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
catch { /* 没快照就照实显示 0 */ }
|
|
1189
|
+
}
|
|
653
1190
|
// 类别行 (全部展开, 不截断类别; 每类列前 8 名 + '+N more') — 单一实例归 other
|
|
654
1191
|
const normalized = new Map();
|
|
655
1192
|
for (const [cat, arr] of catNames) {
|
|
@@ -664,12 +1201,20 @@ async function bootPanel(boot) {
|
|
|
664
1201
|
}
|
|
665
1202
|
const sorted = [...normalized.entries()].sort((a, b) => b[1].length - a[1].length);
|
|
666
1203
|
const total = sorted.reduce((s, [, arr]) => s + arr.length, 0);
|
|
1204
|
+
// 2026-09-30 (leo 报「回复渲染时被截断」): 面板**压矮** —— 技能类目只列前 4 类(其余归到计数行),
|
|
1205
|
+
// 否则 20+ 行面板把消息区占满, 后面的回复没地方站 (= 挤成窄列/看起来被截断).
|
|
1206
|
+
// 2026-09-30 (真机日志: 计数行被面板裁掉): **计数行放类目之前** —— 面板装不下时裁的是尾部,
|
|
1207
|
+
// 原来的顺序 (类目 → 计数) 会让"⚡ N skills · M 类"这个最要紧的数先被丢掉。
|
|
1208
|
+
rest.push(`⚡ ${total} skills · ${sorted.length} 类${skillsFromCache ? ` (上次成功扫描的快照 · ${skillsCacheAt})` : ''}`, '');
|
|
667
1209
|
for (const [cat, arr] of sorted) {
|
|
668
1210
|
const shown = arr.slice(0, 8);
|
|
669
1211
|
const more = arr.length > shown.length ? `, +${arr.length - shown.length} more` : '';
|
|
670
1212
|
rest.push(`${cat}: ${shown.join(', ')}${more}`);
|
|
671
1213
|
}
|
|
672
|
-
|
|
1214
|
+
if (skillsScanTimedOut)
|
|
1215
|
+
rest.push(`${C_DIM}(本次技能扫描超时, 上面是${skillsFromCache ? '快照' : '已扫到的部分'} —— bolloon status 看全量)${RESET}`, '');
|
|
1216
|
+
if (skillsScanErrors.length)
|
|
1217
|
+
rest.push(`${C_WARN}(技能根读取失败 ${skillsScanErrors.length} 个: ${skillsScanErrors[0]})${RESET}`, '');
|
|
673
1218
|
if (sub.length)
|
|
674
1219
|
rest.push(...sub, '');
|
|
675
1220
|
// 自动整理 (经验/技能整理心跳) 模式并入面板
|
|
@@ -744,6 +1289,10 @@ async function startCLI(commReady) {
|
|
|
744
1289
|
if (process.env.BOLLOON_SKIP_KUBO !== '1') {
|
|
745
1290
|
void (async () => {
|
|
746
1291
|
try {
|
|
1292
|
+
// 2026-09-30: 已经在跑就别让 SDK 再走装配 —— 那步会跑 `ipfs init` 并在启动面板前打一行
|
|
1293
|
+
// [warn](含"失败"⇒ 日志门按信号放行), 就是 leo 报的「开启过程的日志没去掉」.
|
|
1294
|
+
if (await kuboAlreadyRunning())
|
|
1295
|
+
return;
|
|
747
1296
|
const sdk = await import('@diap/sdk');
|
|
748
1297
|
const checkKuboSetup = sdk.checkKuboSetup;
|
|
749
1298
|
if (typeof checkKuboSetup === 'function') {
|
|
@@ -762,6 +1311,7 @@ async function startCLI(commReady) {
|
|
|
762
1311
|
if (active) {
|
|
763
1312
|
cliAgentName = active.name;
|
|
764
1313
|
cliActiveChannelId = active.channelId ?? null;
|
|
1314
|
+
setHistoryScope(active.channelId ?? null);
|
|
765
1315
|
// 2026-08-12: 同步 agentId (memory 路径一致) — 从 rawChannels 按 channelId 取 agentId
|
|
766
1316
|
const raw = active.channelId ? store.rawChannels.find((c) => c.id === active.channelId) : undefined;
|
|
767
1317
|
cliAgentId = raw?.agentId || null;
|
|
@@ -777,7 +1327,7 @@ async function startCLI(commReady) {
|
|
|
777
1327
|
_ctxManagerRef = await import('./bootstrap/context-manager.js');
|
|
778
1328
|
}
|
|
779
1329
|
catch { /* 降级: getCliCtxUsage 返回 0/1M */ }
|
|
780
|
-
const initialStatus = `${C_ACCENT}${
|
|
1330
|
+
const initialStatus = `${C_ACCENT}${statusModelName()}${RESET} ${C_DIM}v${_BOLLOON_VERSION}${RESET}${C_DIM} │${RESET} ${cliAgentName} ${C_DIM}│${RESET} ⏱ 0s${C_DIM} │${RESET} ${buildContextBar(getCliCtxUsage())}`;
|
|
781
1331
|
// 2026-09-08 (leo 规格): 图标下元信息层数据 — 目录(home→~) / 模型 / Session id (Hermes 风格: YYYYMMDD_HHMMSS_xxxx)
|
|
782
1332
|
const bootDirShort = process.cwd().replace(os.homedir(), '~');
|
|
783
1333
|
const bootSessionId = (() => {
|
|
@@ -785,7 +1335,42 @@ async function startCLI(commReady) {
|
|
|
785
1335
|
const p = (n, l = 2) => String(n).padStart(l, '0');
|
|
786
1336
|
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}_${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}_${Math.random().toString(16).slice(2, 8)}`;
|
|
787
1337
|
})();
|
|
1338
|
+
const notes = takeStartupPanelNotes();
|
|
1339
|
+
const verbosePreamble = startupPreambleVisible();
|
|
1340
|
+
const bootBox = bootPanelPrinted ? null : await bootPanel({
|
|
1341
|
+
dir: bootDirShort,
|
|
1342
|
+
model: (cliModelName && cliModelName !== '…') ? cliModelName : undefined,
|
|
1343
|
+
session: bootSessionId,
|
|
1344
|
+
});
|
|
1345
|
+
cliSessionKey = bootSessionId; // 2026-09-30: 退出存档要用 (两条退出路径都读它)
|
|
1346
|
+
void refreshSessionTitle(cliSessionKey); // 启动就把当前会话的标题带上
|
|
1347
|
+
if (bootBox) {
|
|
1348
|
+
// 2026-09-30 (leo: 「面板和回复要往上面无限推」): 面板就是这条流的**第一条** —— 渲染侧走 <Static>, 高度无限
|
|
1349
|
+
appendLine(bootBox);
|
|
1350
|
+
bootPanelPrinted = true;
|
|
1351
|
+
}
|
|
1352
|
+
// 2026-09-27 (leo 口径: `bolloon --cli` 起来直接就是面板, 启动前不刷前言):
|
|
1353
|
+
// 就绪度报告 / 初始化续跑前言**默认不上屏** —— 但它们不是消失, 而是变成**面板里的行**:
|
|
1354
|
+
// · 就绪度折成一行 `就绪: basic ✓ · agent ✓ · durable ✗ · network ✓ · 详情: …`
|
|
1355
|
+
// · 失败 / 需人介入 (缺 key、连通性实测失败、初始化中断) 折成 `/!\` 开头的提示行 —— **不许吞**
|
|
1356
|
+
// 注: 字面量里必须写 `\\ ` (反斜杠要转义), 否则 JS 会把 `\ ` 吃成空格 —— 屏上就变成 `/! ` 了
|
|
1357
|
+
// (验收门 `verify-cli-panel` 的 A4 按 `/!\` 判, 真踩过一次)。
|
|
1358
|
+
// 全量版仍在 `startup.log`; `--verbose` / BOLLOON_VERBOSE=1 下逐字回到启动前的位置 (走 stderr)。
|
|
1359
|
+
if (notes.readiness && !verbosePreamble && !bootReadinessPrinted) {
|
|
1360
|
+
appendLine(`${C_DIM}${notes.readiness}${RESET}`);
|
|
1361
|
+
bootReadinessPrinted = true;
|
|
1362
|
+
}
|
|
1363
|
+
for (const a of notes.alerts)
|
|
1364
|
+
appendLine(`${C_WARN}/!\\ ${a}${RESET}`);
|
|
1365
|
+
// 已经到面板了: 缓冲里剩下的纯前言**不上屏**(已在 startup.log), 丢掉引用免得下次启动重放
|
|
1366
|
+
clearBufferedNotices();
|
|
1367
|
+
// 面板第一帧之后: 晚到的降级提示 (后台 iroh / bootstrap / DID 发布…) 直接追加进面板,
|
|
1368
|
+
// 不再等下一次 `takeStartupPanelNotes()` —— 否则它们会静静地烂在告警数组里 (= 吞)
|
|
1369
|
+
startupPanelReady = true;
|
|
1370
|
+
// 2026-09-30: 面板在起帧前已进 store ⇒ 首帧里与输入框一起出现
|
|
788
1371
|
startInk((text) => { processInput(text, comm); }, initialStatus, getStatus);
|
|
1372
|
+
// 2026-10-01: Ink 起来了 ⇒ 把启动期缓冲的日志一次性灌进对话流(不丢, 也不跟面板交错 ✓)
|
|
1373
|
+
flushBootBuffer();
|
|
789
1374
|
// 2026-08-10: 自动整理心跳 (CLI 侧, 与社交心跳并列) — 启动后立即"固定看一下 skills view"
|
|
790
1375
|
// (扫描遗留 skills), 之后按周期 (默认 30min, env BOLLOON_ORGANIZE_HEARTBEAT_MS) 完整进化经验.
|
|
791
1376
|
// 显示走 transient 颜文字行: 触发时显示, 结束后清空 (显示为空).
|
|
@@ -811,7 +1396,15 @@ async function startCLI(commReady) {
|
|
|
811
1396
|
boxLines.push(`🧠 知识整理: ${kSections.map(s => s.error ? `${s.label}✗` : `${s.label}✓`).join(' ')}`);
|
|
812
1397
|
}
|
|
813
1398
|
if (boxLines.length > 0) {
|
|
814
|
-
|
|
1399
|
+
/**
|
|
1400
|
+
* 2026-09-30 (leo: 「还是没有把开启过程的日志去掉」): 自动整理是**内部运行日志** ——
|
|
1401
|
+
* ① 开机第一轮是纯扫描(不取 LLM、什么都没改) ⇒ 一个字符都不该上屏;
|
|
1402
|
+
* ② 周期轮就算真做了事, 也不许**往对话回复流塞一个 10 行盒子** ——
|
|
1403
|
+
* 那会挤掉/截断正在渲染的回复(实测回复被压成窄列).
|
|
1404
|
+
* 处置: 只落盘(与其它内部运行日志同一条路径/同一个 [运行] 标记), 屏上只留 transient 颜文字.
|
|
1405
|
+
* 去哪儿查: grep '[运行]' ~/.bolloon/logs/startup.log
|
|
1406
|
+
*/
|
|
1407
|
+
appendInternalRunLog(`自动整理完成: ${boxLines.join(' | ')}`, 'organize-heartbeat');
|
|
815
1408
|
}
|
|
816
1409
|
},
|
|
817
1410
|
onError: () => inkSetTransient(null),
|
|
@@ -887,35 +1480,11 @@ async function startCLI(commReady) {
|
|
|
887
1480
|
}
|
|
888
1481
|
catch { /* cron 调度启动失败不阻塞 CLI */ }
|
|
889
1482
|
// 启动会话面板 (大框): 栈式 = face 艺术字居中 + BOLLOON 字标 logo 在其下 + 预设信息(skills/工具/模型/目录/Session/分支/时间)
|
|
890
|
-
//
|
|
891
|
-
//
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
'⟳ 正在加载技能 / 工具...',
|
|
896
|
-
]);
|
|
897
|
-
appendLine(bootBox);
|
|
898
|
-
// 2026-09-27 (leo 口径: `bolloon --cli` 起来直接就是面板, 启动前不刷前言):
|
|
899
|
-
// 就绪度报告 / 初始化续跑前言**默认不上屏** —— 但它们不是消失, 而是变成**面板里的行**:
|
|
900
|
-
// · 就绪度折成一行 `就绪: basic ✓ · agent ✓ · durable ✗ · network ✓ · 详情: …`
|
|
901
|
-
// · 失败 / 需人介入 (缺 key、连通性实测失败、初始化中断) 折成 `/!\` 开头的提示行 —— **不许吞**
|
|
902
|
-
// 注: 字面量里必须写 `\\ ` (反斜杠要转义), 否则 JS 会把 `\ ` 吃成空格 —— 屏上就变成 `/! ` 了
|
|
903
|
-
// (验收门 `verify-cli-panel` 的 A4 按 `/!\` 判, 真踩过一次)。
|
|
904
|
-
// 全量版仍在 `startup.log`; `--verbose` / BOLLOON_VERBOSE=1 下逐字回到启动前的位置 (走 stderr)。
|
|
905
|
-
const notes = takeStartupPanelNotes();
|
|
906
|
-
const verbosePreamble = startupPreambleVisible();
|
|
907
|
-
if (notes.readiness && !verbosePreamble)
|
|
908
|
-
appendLine(`${C_DIM}${notes.readiness}${RESET}`);
|
|
909
|
-
for (const a of notes.alerts)
|
|
910
|
-
appendLine(`${C_WARN}/!\\ ${a}${RESET}`);
|
|
911
|
-
// 已经到面板了: 缓冲里剩下的纯前言**不上屏**(已在 startup.log), 丢掉引用免得下次启动重放
|
|
912
|
-
clearBufferedNotices();
|
|
913
|
-
// 面板第一帧之后: 晚到的降级提示 (后台 iroh / bootstrap / DID 发布…) 直接追加进面板,
|
|
914
|
-
// 不再等下一次 `takeStartupPanelNotes()` —— 否则它们会静静地烂在告警数组里 (= 吞)
|
|
915
|
-
startupPanelReady = true;
|
|
916
|
-
void bootPanel({ dir: bootDirShort, model: (cliModelName && cliModelName !== '…') ? cliModelName : undefined, session: bootSessionId })
|
|
917
|
-
.then((box) => { if (box)
|
|
918
|
-
inkReplaceMatchingLine(bootBox, box); }).catch(() => { });
|
|
1483
|
+
//
|
|
1484
|
+
// 2026-09-30 (leo: 「加载过程不渲染, 直接展示结果」): **不再先打「⟳ 正在加载技能 / 工具...」占位框再替换** ——
|
|
1485
|
+
// 先把内容算完 (skills/tools 各有 2.5s 预算), 然后**一次性**把成品框打出来.
|
|
1486
|
+
// 代价: 起来到出框之间屏幕是空的 (不给任何假的"加载中"画面); 好处: 屏上只会出现一份面板, 没有替换中间态.
|
|
1487
|
+
// (面板已在上面一次性打出 —— 不再有占位框需要替换)
|
|
919
1488
|
// Wait on a promise that resolves on Ctrl+C / 双击 Esc
|
|
920
1489
|
// (ink-app 的 requestExit 调 __inkRequestExit → resolve, 清理后 process.exit)
|
|
921
1490
|
let cliExitResolve = () => { };
|
|
@@ -924,7 +1493,14 @@ async function startCLI(commReady) {
|
|
|
924
1493
|
await exitPromise;
|
|
925
1494
|
delete globalThis.__inkRequestExit;
|
|
926
1495
|
stopInk();
|
|
927
|
-
|
|
1496
|
+
// 2026-09-30 (leo): 退出前**自动存档 session**, 存档结果先上屏, 最后才是「再见👋」
|
|
1497
|
+
try {
|
|
1498
|
+
const note = await archiveSessionOnExit(cliSessionKey);
|
|
1499
|
+
if (note)
|
|
1500
|
+
writeExitLine(note);
|
|
1501
|
+
}
|
|
1502
|
+
catch { /* 存档绝不阻塞退出 */ }
|
|
1503
|
+
writeExitLine(`\n${CYAN}👋 再见!${RESET}`);
|
|
928
1504
|
try {
|
|
929
1505
|
cliOrganizeHeartbeat?.stop();
|
|
930
1506
|
}
|
|
@@ -948,6 +1524,7 @@ async function startCLI(commReady) {
|
|
|
948
1524
|
* scheduler 的每个 tick 会先查 resolveDnd(), 处于勿扰时把 due job 记 deferred 并留到下一轮.
|
|
949
1525
|
*/
|
|
950
1526
|
async function processInput(input, comm) {
|
|
1527
|
+
endBootPhase(); // 用户已接管 ⇒ 启动期结束(之后的输出照常上屏 ✓)
|
|
951
1528
|
const gate = globalThis.__bolloonMainTask;
|
|
952
1529
|
if (gate?.enterMainTask) {
|
|
953
1530
|
try {
|
|
@@ -1072,6 +1649,7 @@ async function processInputInner(input, comm) {
|
|
|
1072
1649
|
await store.setActive(r.channel.id);
|
|
1073
1650
|
cliAgentName = r.identity.name;
|
|
1074
1651
|
cliActiveChannelId = r.channel.id;
|
|
1652
|
+
setHistoryScope(r.channel.id);
|
|
1075
1653
|
cliAgentId = r.channel.agentId || null; // 2026-08-12: memory 路径一致
|
|
1076
1654
|
// 2026-08-09: 切 channel 必须重建 agent session — 否则身份/记忆停留在旧 channel (bug 修复)
|
|
1077
1655
|
invalidateAgent();
|
|
@@ -1084,7 +1662,31 @@ async function processInputInner(input, comm) {
|
|
|
1084
1662
|
const extra = prev && prev.name !== r.identity.name ? ` (从 ${prev.name} 切换)` : '';
|
|
1085
1663
|
appendLine(`${C_ACCENT}→ 当前智能体: ${r.identity.name}${RESET}${extra}`);
|
|
1086
1664
|
appendLine(`${C_DIM} channel: ${r.channel.id} [${r.match}]${RESET}`);
|
|
1087
|
-
|
|
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
|
+
}
|
|
1088
1690
|
}
|
|
1089
1691
|
catch (e) {
|
|
1090
1692
|
appendLine(`${C_ERROR}/channel 失败: ${String(e.message || e).slice(0, 200)}${RESET}`);
|
|
@@ -1166,7 +1768,7 @@ async function processInputInner(input, comm) {
|
|
|
1166
1768
|
});
|
|
1167
1769
|
}
|
|
1168
1770
|
await fs.writeFile(agentsPath, JSON.stringify(arr, null, 2), 'utf-8');
|
|
1169
|
-
|
|
1771
|
+
writeOut(`[创建频道] agent 同步进 agents.json: ${agentId} → channel ${id}`);
|
|
1170
1772
|
}
|
|
1171
1773
|
catch { /* agents.json 写失败不阻塞创建 */ }
|
|
1172
1774
|
// 刷新 store 缓存 (updateChannels 走了 server-storage, store 内存还是旧的)
|
|
@@ -1174,6 +1776,7 @@ async function processInputInner(input, comm) {
|
|
|
1174
1776
|
await store.setActive(id);
|
|
1175
1777
|
cliAgentName = name.trim();
|
|
1176
1778
|
cliActiveChannelId = id;
|
|
1779
|
+
setHistoryScope(id);
|
|
1177
1780
|
cliAgentId = agentId; // 2026-08-12: memory 路径一致
|
|
1178
1781
|
// 2026-08-09: 新建 agent 后立即重建 session — 否则新 agent 身份不加载 (bug 修复)
|
|
1179
1782
|
invalidateAgent();
|
|
@@ -1257,6 +1860,19 @@ async function processInputInner(input, comm) {
|
|
|
1257
1860
|
}
|
|
1258
1861
|
// ==================== 2026-08-06: 系统命令组 (/model /now /ipfs /memory ...) ====================
|
|
1259
1862
|
const cmd = trimmed.toLowerCase();
|
|
1863
|
+
// 2026-09-30 (leo: 「/session 1 是切换到该 session,目前这个命令还没成功」「bolloon 把这个命令当成了 prompt」):
|
|
1864
|
+
// `cmd` 是**整行**小写 ⇒ '/session 1' ≠ '/session' ⇒ 带参数的命令掉进 prompt 路径 (被当用户消息发下去)。
|
|
1865
|
+
// 不能全局改成"首词" —— 同文件里 `/email clear` 这类多词命令就是靠整行匹配的。
|
|
1866
|
+
// 所以加一个**首词**给"带参数的命令"用 (cmd 保持原样给多词命令用)。
|
|
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
|
+
}
|
|
1260
1876
|
// /net — Agent 网络快捷命令 (join/status/ctx, 2026-09-08)
|
|
1261
1877
|
if (cmd === '/net' || cmd.startsWith('/net ')) {
|
|
1262
1878
|
const arg = trimmed.slice(4).trim();
|
|
@@ -2173,17 +2789,190 @@ async function processInputInner(input, comm) {
|
|
|
2173
2789
|
}
|
|
2174
2790
|
return;
|
|
2175
2791
|
}
|
|
2176
|
-
// /session —
|
|
2177
|
-
|
|
2792
|
+
// /session — 会话信息 + **最近会话列表**(用 /session <序号> 继续)
|
|
2793
|
+
// 2026-09-30 (leo: 「/session 没有反应, 没有展示最新的记录, 无法选择继续」):
|
|
2794
|
+
// 原来只打三行信息, 且整段被 `catch {}` **静默吞错** ⇒ getAgent 一抛错就"什么都没发生"。
|
|
2795
|
+
// 现在: 错误如实说出来; 并列出最近会话(最新在上), 可直接选一个接着聊。
|
|
2796
|
+
// /fork <#|id> — 从某条会话分叉出新会话 (标准 branch 语义: 共享 fork 点之前历史, 之后各自独立)
|
|
2797
|
+
// 树用 git 管: ~/.bolloon/sessions/ 是仓库, 每次 fork / 退出存档 = 一次 commit (git log --graph 即会话树)
|
|
2798
|
+
if (cmdHead === '/fork') {
|
|
2799
|
+
const forkArg = trimmed.split(/\s+/)[1] || '';
|
|
2800
|
+
if (!forkArg || forkArg === '--tree') {
|
|
2801
|
+
try {
|
|
2802
|
+
const { treeLog } = await import('./cli/session-tree.js');
|
|
2803
|
+
const store = new SessionStore();
|
|
2804
|
+
if (forkArg === '--tree') {
|
|
2805
|
+
const t = await treeLog(store, 12);
|
|
2806
|
+
appendLine(t.ok ? `${C_ACCENT}会话树 (最近 12 次提交):${RESET}\n${t.text.split('\n').map(l => ` ${C_DIM}${l}${RESET}`).join('\n')}` : `${C_WARN}⚠ 读会话树失败: ${t.error}${RESET}`);
|
|
2807
|
+
}
|
|
2808
|
+
else {
|
|
2809
|
+
appendLine(`${C_DIM}用法: ${RESET}${C_ACCENT}/fork <#|id>${RESET}${C_DIM} — 从某条会话分叉 (语义 = 共享 fork 点之前历史, 之后各自独立)${RESET}`);
|
|
2810
|
+
appendLine(`${C_DIM} ${RESET}${C_ACCENT}/fork --tree${RESET}${C_DIM} — 看会话树 (git log) · 序号见 /sessions${RESET}`);
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
catch (e) {
|
|
2814
|
+
appendLine(`${C_WARN}⚠ ${String(e?.message || e).slice(0, 120)}${RESET}`);
|
|
2815
|
+
}
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
try {
|
|
2819
|
+
const store = new SessionStore();
|
|
2820
|
+
const list = await store.listRecent(12);
|
|
2821
|
+
const idx = Number(forkArg);
|
|
2822
|
+
const src = Number.isFinite(idx) && idx >= 1 ? list[idx - 1] : list.find(r => r.key === forkArg);
|
|
2823
|
+
if (!src) {
|
|
2824
|
+
appendLine(`${C_WARN}⚠ 没有第 ${forkArg} 个会话 —— 用 /sessions 列表里的 # 或完整 ID${RESET}`);
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
const { forkSession } = await import('./cli/session-tree.js');
|
|
2828
|
+
const r = await forkSession(store, src.key);
|
|
2829
|
+
if (!r.ok || !r.newKey) {
|
|
2830
|
+
appendLine(`${C_ERROR}/fork 失败: ${r.error || '未知原因'}${RESET}`);
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
cliSessionKey = r.newKey; // 之后就在这条分支上继续 (独立)
|
|
2834
|
+
let resumed = 0;
|
|
2835
|
+
try {
|
|
2836
|
+
const a = await getAgent();
|
|
2837
|
+
if (typeof a.resumeSession === 'function')
|
|
2838
|
+
resumed = await a.resumeSession(r.newKey, 30);
|
|
2839
|
+
}
|
|
2840
|
+
catch { /* 载入失败不影响 fork 本身 */ }
|
|
2841
|
+
// channel 的 currentSessionId 指到新分支 (与 /sessions <#> 同一套落盘口径)
|
|
2842
|
+
try {
|
|
2843
|
+
const { readFile, writeFile } = await import('fs/promises');
|
|
2844
|
+
const { join } = await import('path');
|
|
2845
|
+
const cp = join(process.env.HOME || '/tmp', '.bolloon', 'sessions', 'channels.json');
|
|
2846
|
+
const parsed = JSON.parse(await readFile(cp, 'utf-8'));
|
|
2847
|
+
const channels = Array.isArray(parsed) ? parsed : parsed?.channels || [];
|
|
2848
|
+
for (const c of channels) {
|
|
2849
|
+
if (cliActiveChannelId && c.id === cliActiveChannelId)
|
|
2850
|
+
c.currentSessionId = r.newKey;
|
|
2851
|
+
else if (!cliActiveChannelId && c.id === channels[0]?.id)
|
|
2852
|
+
c.currentSessionId = r.newKey;
|
|
2853
|
+
}
|
|
2854
|
+
await writeFile(cp, JSON.stringify(Array.isArray(parsed) ? channels : { ...parsed, channels }, null, 2), 'utf-8');
|
|
2855
|
+
}
|
|
2856
|
+
catch { /* 无 channels.json 也能继续 */ }
|
|
2857
|
+
appendLine(`${C_OK}✓ 已分叉${RESET} ${C_DIM}${r.newKey}${RESET} ← ${C_DIM}${src.key}${RESET} ${C_DIM}(继承 ${r.inherited} 条历史 · 已载入 ${resumed} 条 · 之后两条各自独立)${RESET}`);
|
|
2858
|
+
appendLine(r.commit ? ` ${C_DIM}git: ${r.commit}${RESET}` : ` ${C_WARN}⚠ 树未记上: ${r.error}${RESET}`);
|
|
2859
|
+
}
|
|
2860
|
+
catch (e) {
|
|
2861
|
+
appendLine(`${C_ERROR}/fork 失败: ${String(e?.message || e).slice(0, 200)}${RESET}`);
|
|
2862
|
+
}
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
if (cmdHead === '/session' || cmdHead === '/sessions') { // 带参数 (如 /session 1) 也认
|
|
2866
|
+
const sessArg = trimmed.split(/\s+/)[1] || '';
|
|
2867
|
+
const sessStore = new SessionStore();
|
|
2178
2868
|
try {
|
|
2179
2869
|
const a = await getAgent();
|
|
2180
2870
|
const h = a.messageHistory ?? [];
|
|
2181
|
-
appendLine(`${C_ACCENT}
|
|
2182
|
-
appendLine(` ${C_DIM}channel:${RESET} ${a.currentChannelId || '—'}`);
|
|
2183
|
-
appendLine(` ${C_DIM}
|
|
2184
|
-
|
|
2871
|
+
appendLine(`${C_ACCENT}当前会话:${RESET} ${C_DIM}${cliSessionKey || '—'}${RESET}`);
|
|
2872
|
+
appendLine(` ${C_DIM}channel:${RESET} ${a.currentChannelId || cliActiveChannelId || '—'} ${C_DIM}agent:${RESET} ${a.currentAgentId || cliAgentId || '—'}`);
|
|
2873
|
+
appendLine(` ${C_DIM}消息:${RESET} ${h.length} 条 ${h.length > 15 ? `${C_DIM}(${h.length - 15} 条已压缩)${RESET}` : ''}`);
|
|
2874
|
+
}
|
|
2875
|
+
catch (e) {
|
|
2876
|
+
appendLine(`${C_WARN}⚠ 读当前会话失败: ${String(e?.message || e).slice(0, 120)}${RESET}`);
|
|
2877
|
+
}
|
|
2878
|
+
let recent = [];
|
|
2879
|
+
try {
|
|
2880
|
+
recent = await sessStore.listRecent(10);
|
|
2881
|
+
}
|
|
2882
|
+
catch (e) {
|
|
2883
|
+
appendLine(`${C_WARN}⚠ 读会话目录失败: ${String(e?.message || e).slice(0, 120)}${RESET}`);
|
|
2884
|
+
}
|
|
2885
|
+
if (!recent.length) {
|
|
2886
|
+
appendLine(`${C_DIM}没有可继续的历史会话 (${sessStore.dir})${RESET}`);
|
|
2887
|
+
}
|
|
2888
|
+
else {
|
|
2889
|
+
// 2026-09-30 (leo: 「Title, Preview 要有内容, 使用 AI 来总结进去」):
|
|
2890
|
+
// 缺标题的会话 (title == key) 现场用 AI 总结一次, 结果写回文件 metadata (只算一次)。
|
|
2891
|
+
// 每次最多 3 条 (每条 12s 超时), 免得 /sessions 变成一次长跑; 失败的**如实标注**不编。
|
|
2892
|
+
const need = recent.filter(r => r.title === r.key).slice(0, 3);
|
|
2893
|
+
if (need.length) {
|
|
2894
|
+
appendLine(`${C_DIM}正在用 AI 总结 ${need.length} 个会话的标题/摘要…${RESET}`);
|
|
2895
|
+
try {
|
|
2896
|
+
const { summarizeSession } = await import('./cli/session-summary.js');
|
|
2897
|
+
for (const r of need) {
|
|
2898
|
+
const d = await summarizeSession(r.key, { store: sessStore });
|
|
2899
|
+
r.title = d.title;
|
|
2900
|
+
if (d.preview)
|
|
2901
|
+
r.preview = d.preview;
|
|
2902
|
+
r.summarySource = d.source;
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
catch (e) {
|
|
2906
|
+
appendLine(`${C_WARN}⚠ AI 总结不可用 (标题保持会话 ID): ${String(e?.message || e).slice(0, 100)}${RESET}`);
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
// 表格 (leo 2026-09-30 指定格式): # / Title / Preview / Last Active / ID
|
|
2910
|
+
// 列宽按终端宽动态分配 (先给 Preview 让位), 中文按**显示宽**对齐 (dispWidth)。
|
|
2911
|
+
const relEn = (ms) => {
|
|
2912
|
+
const age = Math.max(0, Date.now() - ms);
|
|
2913
|
+
if (age < 60_000)
|
|
2914
|
+
return 'just now';
|
|
2915
|
+
if (age < 3_600_000)
|
|
2916
|
+
return `${Math.floor(age / 60_000)}m ago`;
|
|
2917
|
+
if (age < 86_400_000)
|
|
2918
|
+
return `${Math.floor(age / 3_600_000)}h ago`;
|
|
2919
|
+
return `${Math.floor(age / 86_400_000)}d ago`;
|
|
2920
|
+
};
|
|
2921
|
+
const PAD = (v, w) => v + ' '.repeat(Math.max(0, w - dispWidth(v)));
|
|
2922
|
+
const avail = Math.max(60, termWidth() - 2);
|
|
2923
|
+
const wN = 3, wTitle = 32, wAgo = 13, wId = 24;
|
|
2924
|
+
const wPrev = Math.max(12, Math.min(40, avail - (wN + wTitle + wAgo + wId + 4)));
|
|
2925
|
+
const hdr = [PAD('#', wN), PAD('Title', wTitle), PAD('Preview', wPrev), PAD('Last Active', wAgo), PAD('ID', wId)];
|
|
2926
|
+
const rule = [wN, wTitle, wPrev, wAgo, wId].map(w => '─'.repeat(w));
|
|
2927
|
+
appendLine(`${C_ACCENT}Recent sessions:${RESET}`);
|
|
2928
|
+
appendLine('');
|
|
2929
|
+
appendLine(` ${C_DIM}${hdr.join(' ')}${RESET}`);
|
|
2930
|
+
appendLine(` ${C_DIM}${rule.join(' ')}${RESET}`);
|
|
2931
|
+
recent.forEach((r, i) => {
|
|
2932
|
+
const cur = r.key === cliSessionKey ? ` ${C_OK}←当前${RESET}` : '';
|
|
2933
|
+
appendLine(` ${C_ACCENT}${PAD(String(i + 1), wN)}${RESET} ${PAD(truncate(r.title, wTitle), wTitle)} ${C_DIM}${PAD(truncate(r.preview || '—', wPrev), wPrev)}${RESET} ${PAD(relEn(r.mtimeMs), wAgo)} ${C_DIM}${PAD(truncate(r.key, wId), wId)}${RESET}${cur}`);
|
|
2934
|
+
});
|
|
2935
|
+
appendLine('');
|
|
2936
|
+
appendLine(` ${C_DIM}继续某个会话: ${C_ACCENT}/sessions <#>${C_DIM} (例如 /sessions 1)${RESET}`);
|
|
2937
|
+
}
|
|
2938
|
+
if (sessArg) {
|
|
2939
|
+
const idx = Number(sessArg);
|
|
2940
|
+
const pick = Number.isFinite(idx) && idx >= 1 ? recent[idx - 1] : recent.find(r => r.key === sessArg);
|
|
2941
|
+
if (!pick) {
|
|
2942
|
+
appendLine(`${C_WARN}⚠ 没有第 ${sessArg} 个会话 —— 用上面列表的 # 或完整 ID${RESET}`);
|
|
2943
|
+
return;
|
|
2944
|
+
}
|
|
2945
|
+
try {
|
|
2946
|
+
const a = await getAgent();
|
|
2947
|
+
const n = typeof a.resumeSession === 'function' ? await a.resumeSession(pick.key, 30) : 0;
|
|
2948
|
+
cliSessionKey = pick.key; // 之后退出存档也写回这个会话
|
|
2949
|
+
let saved = false;
|
|
2950
|
+
try {
|
|
2951
|
+
const { readFile, writeFile } = await import('fs/promises');
|
|
2952
|
+
const { join } = await import('path');
|
|
2953
|
+
const cp = join(process.env.HOME || '/tmp', '.bolloon', 'sessions', 'channels.json');
|
|
2954
|
+
const parsed = JSON.parse(await readFile(cp, 'utf-8'));
|
|
2955
|
+
const channels = Array.isArray(parsed) ? parsed : parsed?.channels || [];
|
|
2956
|
+
for (const c of channels) {
|
|
2957
|
+
if (cliActiveChannelId && c.id === cliActiveChannelId) {
|
|
2958
|
+
c.currentSessionId = pick.key;
|
|
2959
|
+
saved = true;
|
|
2960
|
+
}
|
|
2961
|
+
else if (!cliActiveChannelId && c.id === channels[0]?.id) {
|
|
2962
|
+
c.currentSessionId = pick.key;
|
|
2963
|
+
saved = true;
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
await writeFile(cp, JSON.stringify(Array.isArray(parsed) ? channels : { ...parsed, channels }, null, 2), 'utf-8');
|
|
2967
|
+
}
|
|
2968
|
+
catch { /* 无 channels.json 也能继续 */ }
|
|
2969
|
+
await refreshSessionTitle(pick.key); // 切完立刻把这条会话的标题带上 (leo 2026-09-30)
|
|
2970
|
+
appendLine(`${C_OK}✓ 已切到会话 ${pick.key}${RESET} ${C_DIM}(载入 ${n} 条历史${saved ? ' · 已写入 channel' : ''}; 屏上不重放旧消息, 直接接着聊)${RESET}`);
|
|
2971
|
+
}
|
|
2972
|
+
catch (e) {
|
|
2973
|
+
appendLine(`${C_ERROR}/session ${sessArg} 失败: ${String(e?.message || e).slice(0, 200)}${RESET}`);
|
|
2974
|
+
}
|
|
2185
2975
|
}
|
|
2186
|
-
catch { /* 静默 */ }
|
|
2187
2976
|
return;
|
|
2188
2977
|
}
|
|
2189
2978
|
// /memory — 记忆摘要 (memory-compressor 落盘文件)
|
|
@@ -2307,7 +3096,7 @@ async function processInputInner(input, comm) {
|
|
|
2307
3096
|
return;
|
|
2308
3097
|
}
|
|
2309
3098
|
// /todo — 循环过程工具: 查看/勾选步骤 (2026-08-08)
|
|
2310
|
-
if (
|
|
3099
|
+
if (cmdHead === '/todo') {
|
|
2311
3100
|
try {
|
|
2312
3101
|
const { listActivePlans } = await import('./agents/plan-store.js');
|
|
2313
3102
|
const plans = await listActivePlans();
|
|
@@ -2670,24 +3459,142 @@ async function processInputInner(input, comm) {
|
|
|
2670
3459
|
return;
|
|
2671
3460
|
}
|
|
2672
3461
|
// /wallet — 钱包状态
|
|
2673
|
-
|
|
3462
|
+
// /wallet — 钱包台账 (2026-09-30 做实: 以前只读一个**从来没人写过**的 wallets.json ⇒ 永远「钱包 (0)」)
|
|
3463
|
+
// 纪律: 私钥/助记词只落 0600 台账 (~/.bolloon/wallets.json), **命令输出永不回显**;
|
|
3464
|
+
// 不读别的 agent 的钱包目录 (chain-config.ts 硬规则), 要收编只能 /wallet import <path> 显式指定。
|
|
3465
|
+
if (cmdHead === '/wallet') {
|
|
2674
3466
|
try {
|
|
2675
|
-
const {
|
|
2676
|
-
const
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
3467
|
+
const { loadWallets, addWallet, walletsDir, shortAddr } = await import('./cli/wallet-store.js');
|
|
3468
|
+
const arg = trimmed.split(/\s+/).slice(1);
|
|
3469
|
+
const sub = (arg[0] || '').toLowerCase();
|
|
3470
|
+
const list = await loadWallets();
|
|
3471
|
+
const pick = (spec) => {
|
|
3472
|
+
const n = Number(spec);
|
|
3473
|
+
if (spec && Number.isFinite(n) && n >= 1)
|
|
3474
|
+
return list[n - 1];
|
|
3475
|
+
if (spec?.startsWith('0x'))
|
|
3476
|
+
return list.find(w => w.address.toLowerCase() === spec.toLowerCase());
|
|
3477
|
+
return list.find(w => w.name === spec);
|
|
3478
|
+
};
|
|
3479
|
+
if (!sub) {
|
|
3480
|
+
// 表格 (leo 2026-09-30: 「钱包格式也统一一个表格」) —— 与 /sessions 同一套 idiom:
|
|
3481
|
+
// 列宽按终端宽动态分配, 中文按**显示宽**对齐 (PAD/dispWidth), 宽不够就让 Name 先窄。
|
|
3482
|
+
// 地址优先给满 42 字符 (能直接复制); 列宽不足时退回缩写 (0x1234…abcd), 不截半截地址。
|
|
3483
|
+
appendLine(`${C_ACCENT}钱包 (${list.length}):${RESET}`);
|
|
3484
|
+
if (!list.length) {
|
|
3485
|
+
appendLine('');
|
|
3486
|
+
appendLine(` ${C_DIM}无 — 用 ${RESET}${C_ACCENT}/wallet new [名字]${RESET}${C_DIM} 生成一个 EVM 钱包${RESET}`);
|
|
3487
|
+
}
|
|
3488
|
+
else {
|
|
3489
|
+
// 表格渲染抽到 src/cli/wallet-table.ts (纯函数 ⇒ 可确定性验证对齐, 不用靠 PTY 抓包)
|
|
3490
|
+
const { renderWalletTable } = await import('./cli/wallet-table.js');
|
|
3491
|
+
const rows = renderWalletTable(list, { width: termWidth(), shortAddr });
|
|
3492
|
+
appendLine('');
|
|
3493
|
+
rows.forEach((r, k) => {
|
|
3494
|
+
appendLine(k < 2 ? ` ${C_DIM}${r}${RESET}` : ` ${C_TEXT}${r}${RESET}`);
|
|
3495
|
+
});
|
|
3496
|
+
}
|
|
3497
|
+
appendLine('');
|
|
3498
|
+
appendLine(` ${C_DIM}台账目录: ${walletsDir()}/ (一钱包一文件 · 0600 · 私钥不回显)${RESET}`);
|
|
3499
|
+
appendLine(` ${C_DIM}用法: ${RESET}${C_ACCENT}/wallet new [名字]${RESET}${C_DIM} · ${RESET}${C_ACCENT}/wallet import <文件路径> [名字]${RESET}${C_DIM} · ${RESET}${C_ACCENT}/wallet show <#|地址>${RESET}${C_DIM} · ${RESET}${C_ACCENT}/wallet bal <#|地址>${RESET}`);
|
|
3500
|
+
return;
|
|
2680
3501
|
}
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
3502
|
+
if (sub === 'new') {
|
|
3503
|
+
const name = arg[1] || `wallet-${new Date().toISOString().slice(0, 10)}`;
|
|
3504
|
+
const mod = await loadWalletTool('createWallet');
|
|
3505
|
+
if (!mod?.createWallet) {
|
|
3506
|
+
appendLine(`${C_ERROR}/wallet new 失败: 找不到 createWallet 实现${RESET}`);
|
|
3507
|
+
return;
|
|
3508
|
+
}
|
|
3509
|
+
const r = await mod.createWallet();
|
|
3510
|
+
const { rec, replaced } = await addWallet({
|
|
3511
|
+
name, address: r.address, network: '—', source: 'created',
|
|
3512
|
+
createdAt: r.createdAt || new Date().toISOString(), privateKey: r.privateKey, mnemonic: r.mnemonic,
|
|
3513
|
+
});
|
|
3514
|
+
appendLine(`${C_OK}✓ 钱包${replaced ? '已在台账 (补齐信息)' : '已创建'}${RESET} ${C_ACCENT}${rec.name}${RESET}`);
|
|
3515
|
+
appendLine(` ${C_DIM}地址:${RESET} ${rec.address}`);
|
|
3516
|
+
appendLine(` ${C_DIM}私钥/助记词: 已写入 ${walletsDir()}/${rec.name}.json (0600) — 不回显, 需要时自己打开看${RESET}`);
|
|
3517
|
+
return;
|
|
3518
|
+
}
|
|
3519
|
+
if (sub === 'import') {
|
|
3520
|
+
const src = arg[1];
|
|
3521
|
+
if (!src) {
|
|
3522
|
+
appendLine(`${C_WARN}用法: /wallet import <文件路径> [名字] — 路径必须你显式给出 (不扫别的 agent 目录)${RESET}`);
|
|
3523
|
+
return;
|
|
3524
|
+
}
|
|
3525
|
+
let raw;
|
|
3526
|
+
try {
|
|
3527
|
+
raw = JSON.parse(await (await import('fs/promises')).readFile(src.replace(/^~/, process.env.HOME || ''), 'utf-8'));
|
|
3528
|
+
}
|
|
3529
|
+
catch (err) {
|
|
3530
|
+
appendLine(`${C_ERROR}读不了 ${src}: ${String(err?.message || err).slice(0, 100)}${RESET}`);
|
|
3531
|
+
return;
|
|
3532
|
+
}
|
|
3533
|
+
const pk = raw?.privateKey || raw?.private_key;
|
|
3534
|
+
const mn = raw?.mnemonic || raw?.phrase;
|
|
3535
|
+
if (!pk && !mn) {
|
|
3536
|
+
appendLine(`${C_ERROR}文件里没有 privateKey / mnemonic — 不收${RESET}`);
|
|
3537
|
+
return;
|
|
3538
|
+
}
|
|
3539
|
+
let addr = raw?.address;
|
|
3540
|
+
if (!addr && pk) {
|
|
3541
|
+
const mod = await loadWalletTool('importWallet');
|
|
3542
|
+
try {
|
|
3543
|
+
const r = await mod.importWallet({ privateKey: pk, mnemonic: mn });
|
|
3544
|
+
addr = r.address;
|
|
3545
|
+
}
|
|
3546
|
+
catch { /* 下面照实报 */ }
|
|
3547
|
+
}
|
|
3548
|
+
if (!addr) {
|
|
3549
|
+
appendLine(`${C_ERROR}拿不到地址 — 文件里没有 address 且无法从私钥推导${RESET}`);
|
|
3550
|
+
return;
|
|
3551
|
+
}
|
|
3552
|
+
const { rec, replaced } = await addWallet({
|
|
3553
|
+
name: arg[2] || String(src).split('/').pop().replace(/\.json$/, ''), address: addr,
|
|
3554
|
+
network: raw?.network || '—', source: 'imported', createdAt: new Date().toISOString(), privateKey: pk, mnemonic: mn,
|
|
3555
|
+
});
|
|
3556
|
+
appendLine(`${C_OK}✓ 已收编${RESET} ${C_ACCENT}${rec.name}${RESET}${replaced ? ` ${C_DIM}(同地址已在台账, 已合并)${RESET}` : ''}`);
|
|
3557
|
+
appendLine(` ${C_DIM}地址:${RESET} ${rec.address} ${C_DIM}(私钥只进 0600 台账, 不回显)${RESET}`);
|
|
3558
|
+
return;
|
|
2685
3559
|
}
|
|
2686
|
-
|
|
2687
|
-
|
|
3560
|
+
if (sub === 'show') {
|
|
3561
|
+
const w = pick(arg[1] || '1');
|
|
3562
|
+
if (!w) {
|
|
3563
|
+
appendLine(`${C_WARN}没有这个钱包 — /wallet 看列表${RESET}`);
|
|
3564
|
+
return;
|
|
3565
|
+
}
|
|
3566
|
+
appendLine(`${C_ACCENT}${w.name}${RESET}`);
|
|
3567
|
+
appendLine(` ${C_DIM}地址:${RESET} ${w.address}`);
|
|
3568
|
+
appendLine(` ${C_DIM}网络:${RESET} ${w.network || '—'} ${C_DIM}· 来源:${RESET} ${w.source === 'imported' ? '导入' : '生成'} ${C_DIM}· 建立:${RESET} ${String(w.createdAt).slice(0, 19).replace('T', ' ')}`);
|
|
3569
|
+
appendLine(` ${C_DIM}密钥:${RESET} ${w.privateKey || w.mnemonic ? '已存 (只在本机 0600 文件里)' : '未存 (只有地址)'}`);
|
|
3570
|
+
return;
|
|
3571
|
+
}
|
|
3572
|
+
if (sub === 'bal' || sub === 'balance') {
|
|
3573
|
+
const w = pick(arg[1] || '1');
|
|
3574
|
+
if (!w) {
|
|
3575
|
+
appendLine(`${C_WARN}没有这个钱包 — /wallet 看列表${RESET}`);
|
|
3576
|
+
return;
|
|
3577
|
+
}
|
|
3578
|
+
const mod = await loadWalletTool('getBalance');
|
|
3579
|
+
if (!mod?.getBalance) {
|
|
3580
|
+
appendLine(`${C_ERROR}找不到 getBalance 实现${RESET}`);
|
|
3581
|
+
return;
|
|
3582
|
+
}
|
|
3583
|
+
appendLine(`${C_DIM}查询中… ${shortAddr(w.address)}${RESET}`);
|
|
3584
|
+
try {
|
|
3585
|
+
const r = await mod.getBalance({ address: w.address, rpcUrl: arg[2] });
|
|
3586
|
+
appendLine(`${C_OK}💰${RESET} ${w.name} ${C_DIM}${r.address || w.address}${RESET} ${C_ACCENT}${r.balanceEth} ${r.symbol || 'ETH'}${RESET} ${C_DIM}(${r.balance} wei)${RESET}`);
|
|
3587
|
+
}
|
|
3588
|
+
catch (err) {
|
|
3589
|
+
appendLine(`${C_ERROR}查余额失败: ${String(err?.message || err).slice(0, 140)}${RESET}`);
|
|
3590
|
+
}
|
|
3591
|
+
return;
|
|
2688
3592
|
}
|
|
3593
|
+
appendLine(`${C_WARN}未知子命令 ${sub} — 用法: /wallet [new|import|show|bal]${RESET}`);
|
|
3594
|
+
}
|
|
3595
|
+
catch (e) {
|
|
3596
|
+
appendLine(`${C_ERROR}/wallet 失败: ${String(e?.message || e).slice(0, 200)}${RESET}`);
|
|
2689
3597
|
}
|
|
2690
|
-
catch { /* 静默 */ }
|
|
2691
3598
|
return;
|
|
2692
3599
|
}
|
|
2693
3600
|
// /email — 邮件配置管理; /email <host:port:user:from> 设置 / /email clear 清除 (2026-08-08)
|
|
@@ -2923,6 +3830,153 @@ async function processInputInner(input, comm) {
|
|
|
2923
3830
|
}
|
|
2924
3831
|
return;
|
|
2925
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
|
+
}
|
|
2926
3980
|
if (trimmed.toLowerCase() === '/help' || trimmed === 'help') {
|
|
2927
3981
|
appendLine(`${C_DIM}命令:${RESET}`);
|
|
2928
3982
|
appendLine(` ${C_ACCENT}!<cmd>${RESET} 执行终端命令 ${C_DIM}如 !ls -la${RESET}`);
|
|
@@ -2980,6 +4034,13 @@ async function processInputInner(input, comm) {
|
|
|
2980
4034
|
return;
|
|
2981
4035
|
}
|
|
2982
4036
|
if (trimmed === '退出' || trimmed === 'exit' || trimmed === 'quit') {
|
|
4037
|
+
// 同一条退出纪律: 先存档, 再告别 (幂等 —— 与 Ctrl+C 路径共用)
|
|
4038
|
+
try {
|
|
4039
|
+
const note = await archiveSessionOnExit(cliSessionKey);
|
|
4040
|
+
if (note)
|
|
4041
|
+
appendLine(note);
|
|
4042
|
+
}
|
|
4043
|
+
catch { /* 非致命 */ }
|
|
2983
4044
|
appendLine(`\n${CYAN}👋 再见!${RESET}`);
|
|
2984
4045
|
isRunning = false;
|
|
2985
4046
|
return;
|
|
@@ -3047,12 +4108,27 @@ async function processInputInner(input, comm) {
|
|
|
3047
4108
|
const a = await getAgent();
|
|
3048
4109
|
const boxW = Math.min(termWidth() - 2, 76);
|
|
3049
4110
|
// 工具调用显示由 tui-shell 的 onStream handler 处理
|
|
4111
|
+
cliTurnStartedAt = Date.now();
|
|
4112
|
+
cliTurnReplyBytes = 0;
|
|
4113
|
+
cliTurnToolCount = 0;
|
|
3050
4114
|
const response = await a.prompt(trimmed, {
|
|
3051
4115
|
onStream: (e) => {
|
|
3052
4116
|
// 2026-08-07: 中间思考/状态显示 — 之前只显示工具步骤, LLM 的思考过程
|
|
3053
4117
|
// (thinking / status / phase / Reflection) 全被丢弃 → 用户只能看到输入和最终输出
|
|
3054
4118
|
// 用户偏好 (2026-08-07): 思考过程 = 圆角框渲染 (和回复同路径 renderMessageBox),
|
|
3055
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
|
+
}
|
|
3056
4132
|
if (e.type === 'thinking' && e.content) {
|
|
3057
4133
|
// thinking 事件只有 "🤔 开始思考..." 占位 → 不 appendLine, 运行过程由动画表示;
|
|
3058
4134
|
// 真正思考内容在 status 的 Reflection/💡 事件 → 下方框渲染
|
|
@@ -3079,7 +4155,10 @@ async function processInputInner(input, comm) {
|
|
|
3079
4155
|
// Reflection / 反思 / 💡 → 圆角思考框 (和回复一样走 renderMessageBox, 白字+亮边框)
|
|
3080
4156
|
else if (content.includes('Reflection') || content.includes('反思') || content.includes('💡')) {
|
|
3081
4157
|
const body = content.replace(/^💡\s*/, '').slice(0, 1500);
|
|
3082
|
-
|
|
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)
|
|
3083
4162
|
appendLine(renderMessageBox({ title: '💡 反思', body, color: C_WARN }));
|
|
3084
4163
|
}
|
|
3085
4164
|
else if (!content.includes('🔄 循环') && !content.includes('📋 参数')
|
|
@@ -3095,11 +4174,14 @@ async function processInputInner(input, comm) {
|
|
|
3095
4174
|
tuiToolCounter++;
|
|
3096
4175
|
const toolName = e.tool || '?';
|
|
3097
4176
|
tuiToolCalls.push({ tool: toolName, args: e.args, _t: Date.now() });
|
|
4177
|
+
cliTurnToolCount = tuiToolCalls.filter(c => c.tool !== 'system' && c.tool !== 'loop' && c.tool !== '?').length; // ⚙ 启动即算
|
|
3098
4178
|
// 2026-08-12 (TaskA): 工具命中要干净 — step_start 不 appendLine 到消息流 (避免重复),
|
|
3099
4179
|
// 改用 transient 行显示"正在执行"(消息流只在 done 时出现一次完成行).
|
|
3100
4180
|
if (toolName !== 'system' && toolName !== 'loop' && toolName !== '?') {
|
|
3101
4181
|
const activeNames = tuiToolCalls.map(c => c.tool).filter(t => t !== 'system' && t !== 'loop' && t !== '?');
|
|
3102
|
-
const label = activeNames.length > 1
|
|
4182
|
+
const label = activeNames.length > 1
|
|
4183
|
+
? `执行 ${activeNames.length} 个工具: ${activeNames.join(', ')}`
|
|
4184
|
+
: `🔧 ${traceLabel(toolName)}`; // trace 执行描述: 写清这一步在做什么
|
|
3103
4185
|
inkSetTransient(`${C_DIM}${label} 运行中...${RESET}`);
|
|
3104
4186
|
}
|
|
3105
4187
|
}
|
|
@@ -3127,8 +4209,9 @@ async function processInputInner(input, comm) {
|
|
|
3127
4209
|
}
|
|
3128
4210
|
// 更新 transient: 还有进行中的工具 → 显示下一个; 否则清空 (交回 thinking 动画)
|
|
3129
4211
|
const remaining = tuiToolCalls.filter(c => c.tool !== 'system' && c.tool !== 'loop' && c.tool !== '?');
|
|
4212
|
+
cliTurnToolCount = remaining.length; // ⚙ 状态栏用 (模块级)
|
|
3130
4213
|
if (remaining.length > 0) {
|
|
3131
|
-
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 执行描述
|
|
3132
4215
|
inkSetTransient(`${C_DIM}${label} 运行中...${RESET}`);
|
|
3133
4216
|
}
|
|
3134
4217
|
else {
|
|
@@ -3139,6 +4222,59 @@ async function processInputInner(input, comm) {
|
|
|
3139
4222
|
});
|
|
3140
4223
|
// 智能体回复框
|
|
3141
4224
|
appendLine(renderAgentMessage(response));
|
|
4225
|
+
try {
|
|
4226
|
+
cliLastTurnMs = cliTurnStartedAt ? Date.now() - cliTurnStartedAt : 0;
|
|
4227
|
+
cliTurnReplyBytes = String(response ?? '').length;
|
|
4228
|
+
cliTurnStartedAt = 0;
|
|
4229
|
+
cliTurnToolCount = 0;
|
|
4230
|
+
}
|
|
4231
|
+
catch { /* 结算失败不影响回复 */ }
|
|
4232
|
+
// 2026-09-30 (leo: 「session 的 Preview 在对话框工具栏右侧做显示」):
|
|
4233
|
+
// 回合结束后**后台**给这条会话生成一次标题/摘要 (600ms 级, 失败不打扰), 生成后工具栏右侧就有内容;
|
|
4234
|
+
// 顺带把 metadata 写盘 ⇒ 这段会话在 /sessions 里也立刻有 Title/Preview。
|
|
4235
|
+
if (cliSessionKey && !cliSessionPreview && !cliSessionSummarizing) {
|
|
4236
|
+
cliSessionSummarizing = true;
|
|
4237
|
+
const keyForNote = cliSessionKey;
|
|
4238
|
+
setImmediate(async () => {
|
|
4239
|
+
try {
|
|
4240
|
+
const { summarizeSession } = await import('./cli/session-summary.js');
|
|
4241
|
+
const store = new SessionStore();
|
|
4242
|
+
// 先存档! 2026-09-30 诊断日志挖出的根因: 回合结束时**会话还没落盘**
|
|
4243
|
+
// (CLI 只在退出时存档) ⇒ 总结器看到的文件里没有用户那句话 ⇒ source=none ⇒ 永远没标题
|
|
4244
|
+
// (屏幕上因此看不到 ▸; 退出后才出现, 因为那次存档把消息写进去了)。
|
|
4245
|
+
try {
|
|
4246
|
+
await agent?.saveCurrentSession?.(keyForNote);
|
|
4247
|
+
}
|
|
4248
|
+
catch { /* 存不下也继续试总结 */ }
|
|
4249
|
+
const d = await summarizeSession(keyForNote, { store });
|
|
4250
|
+
// 2026-09-30 (leo: 「Title 放在右侧吧,不要摘要了」): 右侧显示的是**标题** (≤20 字, 比摘要短得多);
|
|
4251
|
+
// 优先用调用的返回值, 取不到再回读文件 (标题可能由退出存档/别的进程写进去)。
|
|
4252
|
+
let note = String(d?.title && d.title !== keyForNote ? d.title : '').trim();
|
|
4253
|
+
if (!note) {
|
|
4254
|
+
try {
|
|
4255
|
+
const fs = await import('fs/promises');
|
|
4256
|
+
const raw = JSON.parse(await fs.readFile(store.pathFor(keyForNote), 'utf-8'));
|
|
4257
|
+
const md = (Array.isArray(raw) ? {} : raw?.metadata) || {};
|
|
4258
|
+
note = String(md.title || '').trim();
|
|
4259
|
+
}
|
|
4260
|
+
catch { /* 回读失败就用返回值 */ }
|
|
4261
|
+
}
|
|
4262
|
+
if (note && note !== keyForNote) {
|
|
4263
|
+
cliSessionPreview = note.slice(0, 24);
|
|
4264
|
+
inkSetStatus(getStatus());
|
|
4265
|
+
console.error(`[session-note] 已更新: ${note.slice(0, 24)}`);
|
|
4266
|
+
}
|
|
4267
|
+
else
|
|
4268
|
+
console.error(`[session-note] 没拿到标题 (source=${d?.source}, key=${keyForNote})`);
|
|
4269
|
+
}
|
|
4270
|
+
catch (e) {
|
|
4271
|
+
console.error(`[session-note] 失败: ${String(e?.message || e).slice(0, 120)}`);
|
|
4272
|
+
}
|
|
4273
|
+
finally {
|
|
4274
|
+
cliSessionSummarizing = false;
|
|
4275
|
+
}
|
|
4276
|
+
});
|
|
4277
|
+
}
|
|
3142
4278
|
// 停止思考动画
|
|
3143
4279
|
inkSetThinking(false);
|
|
3144
4280
|
// 2026-08-04: run-end 经验整理 — 连续成功工具 ≥2 自动写 skill 候选
|
|
@@ -3213,7 +4349,7 @@ async function processInputInner(input, comm) {
|
|
|
3213
4349
|
maxTokens: usage.maxTokens,
|
|
3214
4350
|
stage: usage.stage,
|
|
3215
4351
|
};
|
|
3216
|
-
const statusText = `${C_ACCENT}${
|
|
4352
|
+
const statusText = `${C_ACCENT}${statusModelName()}${RESET}${C_DIM} │${RESET} ${cliAgentName} ${C_DIM}│${RESET} ⏱ ${fmtDuration(Date.now() - cliStartTime)}${C_DIM} │${RESET} ${buildContextBar(usageView)}`;
|
|
3217
4353
|
inkSetStatus(statusText);
|
|
3218
4354
|
}
|
|
3219
4355
|
catch { /* 降级容忍 */ }
|
|
@@ -4177,18 +5313,18 @@ goalFlag) {
|
|
|
4177
5313
|
error,
|
|
4178
5314
|
metadata
|
|
4179
5315
|
};
|
|
4180
|
-
|
|
5316
|
+
writeOut(JSON.stringify(result, null, 2));
|
|
4181
5317
|
}
|
|
4182
5318
|
else {
|
|
4183
5319
|
if (error) {
|
|
4184
5320
|
s.divider();
|
|
4185
|
-
|
|
5321
|
+
writeOut(`${MAGENTA}${error}${RESET}\n`);
|
|
4186
5322
|
}
|
|
4187
5323
|
else {
|
|
4188
5324
|
s.divider();
|
|
4189
|
-
|
|
5325
|
+
writeOut(`${response}\n`);
|
|
4190
5326
|
}
|
|
4191
|
-
|
|
5327
|
+
writeOut(`${GRAY}耗时: ${metadata.duration}ms${RESET}`);
|
|
4192
5328
|
}
|
|
4193
5329
|
}
|
|
4194
5330
|
async function runNonInteractive(args, comm) {
|
|
@@ -4204,11 +5340,11 @@ async function runNonInteractive(args, comm) {
|
|
|
4204
5340
|
}
|
|
4205
5341
|
else if (prompt) {
|
|
4206
5342
|
const a = await getAgent();
|
|
4207
|
-
|
|
5343
|
+
writeOut(await a.prompt(prompt));
|
|
4208
5344
|
}
|
|
4209
5345
|
console.log = originalLog;
|
|
4210
5346
|
await fs.writeFile(output, outputBuffer.trim(), 'utf-8');
|
|
4211
|
-
|
|
5347
|
+
writeOut(`✅ 结果已保存到: ${output}`);
|
|
4212
5348
|
return;
|
|
4213
5349
|
}
|
|
4214
5350
|
if (tool === 'prompt' && prompt) {
|
|
@@ -4220,20 +5356,20 @@ async function runNonInteractive(args, comm) {
|
|
|
4220
5356
|
const response = await a.prompt(prompt);
|
|
4221
5357
|
const elapsed = Date.now() - startTime;
|
|
4222
5358
|
if (json) {
|
|
4223
|
-
|
|
5359
|
+
writeOut(JSON.stringify({ success: true, response, elapsedMs: elapsed }, null, 2));
|
|
4224
5360
|
}
|
|
4225
5361
|
else {
|
|
4226
|
-
|
|
4227
|
-
|
|
5362
|
+
writeOut(response);
|
|
5363
|
+
writeOut(`\n耗时: ${elapsed}ms`);
|
|
4228
5364
|
}
|
|
4229
5365
|
}
|
|
4230
5366
|
catch (e) {
|
|
4231
5367
|
const error = e?.message || String(e);
|
|
4232
5368
|
if (json) {
|
|
4233
|
-
|
|
5369
|
+
writeOut(JSON.stringify({ success: false, error }, null, 2));
|
|
4234
5370
|
}
|
|
4235
5371
|
else {
|
|
4236
|
-
|
|
5372
|
+
writeOut(`\n错误: ${error}`);
|
|
4237
5373
|
}
|
|
4238
5374
|
process.exit(1);
|
|
4239
5375
|
}
|
|
@@ -4261,10 +5397,10 @@ async function runNonInteractive(args, comm) {
|
|
|
4261
5397
|
error: response.startsWith('错误:') ? response : undefined,
|
|
4262
5398
|
metadata: { duration, peers }
|
|
4263
5399
|
};
|
|
4264
|
-
|
|
5400
|
+
writeOut(JSON.stringify(result, null, 2));
|
|
4265
5401
|
}
|
|
4266
5402
|
else {
|
|
4267
|
-
|
|
5403
|
+
writeOut(response);
|
|
4268
5404
|
}
|
|
4269
5405
|
}
|
|
4270
5406
|
}
|
|
@@ -4582,7 +5718,7 @@ function parseArgs() {
|
|
|
4582
5718
|
return result;
|
|
4583
5719
|
}
|
|
4584
5720
|
function printHelp() {
|
|
4585
|
-
|
|
5721
|
+
writeOut(`
|
|
4586
5722
|
🤖 Bolloon Agent - AI 可调用文档处理智能体
|
|
4587
5723
|
|
|
4588
5724
|
用法:
|
|
@@ -4810,21 +5946,21 @@ async function main() {
|
|
|
4810
5946
|
const res = await runStandaloneSupervisorHost({
|
|
4811
5947
|
once: !!args.superviseOnce,
|
|
4812
5948
|
dryRun: !!args.superviseDryRun,
|
|
4813
|
-
log: (m) =>
|
|
5949
|
+
log: (m) => writeOut(m),
|
|
4814
5950
|
});
|
|
4815
5951
|
if (args.superviseOnce) {
|
|
4816
5952
|
const rep = res.lastReport;
|
|
4817
5953
|
if (rep) {
|
|
4818
|
-
|
|
5954
|
+
writeOut(`调度周期 #${rep.tick}: 认领 ${rep.claimed.length} · 执行 ${rep.executed.length} · 跳过 ${rep.skipped.length}${rep.errors.length ? ` · 错误 ${rep.errors.length}` : ''}`);
|
|
4819
5955
|
for (const s of rep.skipped.slice(0, 8))
|
|
4820
|
-
|
|
5956
|
+
writeOut(` 跳过 ${s.goalId}: ${s.reason}`);
|
|
4821
5957
|
for (const e of rep.executed)
|
|
4822
|
-
|
|
5958
|
+
writeOut(` ▶ ${e.goalId} → run=${e.runId || '-'} ${e.status || ''}${e.error ? ` (${e.error})` : ''}`);
|
|
4823
5959
|
}
|
|
4824
|
-
|
|
5960
|
+
writeOut(`supervisor 宿主: owner=${res.state.owner} worker=${res.state.workerId} ticks=${res.ticks} 状态文件=~/.bolloon/supervisor.json`);
|
|
4825
5961
|
}
|
|
4826
5962
|
else {
|
|
4827
|
-
|
|
5963
|
+
writeOut(`[supervisor] 常驻宿主已启动 (owner=${res.state.owner}, worker=${res.state.workerId}); Ctrl-C 优雅停止`);
|
|
4828
5964
|
}
|
|
4829
5965
|
return;
|
|
4830
5966
|
}
|
|
@@ -4875,6 +6011,14 @@ async function main() {
|
|
|
4875
6011
|
if (process.env.BOLLOON_SKIP_SETUP === '1') {
|
|
4876
6012
|
noticePreamble('⚠ BOLLOON_SKIP_SETUP=1 → 诊断模式: 可以看状态/修配置, 但 agent 执行被门禁拦住 (不能绕过)');
|
|
4877
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
|
+
}
|
|
4878
6022
|
else {
|
|
4879
6023
|
const { runSetupWizard } = await import('./cli/setup-wizard.js');
|
|
4880
6024
|
// quietStartup: 向导的续跑前言默认不上屏 (它自己会在"要人回答/有失败"时先 flush)
|
|
@@ -4960,12 +6104,19 @@ async function main() {
|
|
|
4960
6104
|
void (async () => {
|
|
4961
6105
|
let kuboReady = false;
|
|
4962
6106
|
try {
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
6107
|
+
// 2026-09-30: 已经在跑 → 直接算就绪, 不触发 SDK 那次注定失败的 `ipfs init`(它会在面板前打一行 warn)
|
|
6108
|
+
if (await kuboAlreadyRunning()) {
|
|
6109
|
+
kuboReady = true;
|
|
6110
|
+
bootNotice('info', 'IPFS 本地 Kubo 已在运行 → IPNS 发布/解析可用');
|
|
6111
|
+
}
|
|
6112
|
+
else {
|
|
6113
|
+
const sdk = await import('@diap/sdk');
|
|
6114
|
+
const checkKuboSetup = sdk.checkKuboSetup;
|
|
6115
|
+
if (typeof checkKuboSetup === 'function') {
|
|
6116
|
+
const setup = await checkKuboSetup(true, true);
|
|
6117
|
+
kuboReady = !!(setup?.ready && setup?.daemonRunning);
|
|
6118
|
+
bootNotice('info', kuboReady ? 'IPFS 本地 Kubo 就绪 → IPNS 发布/解析可用' : 'Kubo 不可用, IPFS 降级本地模式');
|
|
6119
|
+
}
|
|
4969
6120
|
}
|
|
4970
6121
|
}
|
|
4971
6122
|
catch (e) {
|
|
@@ -5043,7 +6194,7 @@ async function main() {
|
|
|
5043
6194
|
bootNotice('info', `Bootstrap 完成 (${bs.durationMs}ms, ${bs.errors.length} 个非致命错误)`);
|
|
5044
6195
|
}
|
|
5045
6196
|
catch (err) {
|
|
5046
|
-
bootNotice('warn',
|
|
6197
|
+
bootNotice('warn', `上下文扫描未完成 (可选步骤, 已跳过, 主流程继续): ${err.message} —— 详情见 startup.log`);
|
|
5047
6198
|
}
|
|
5048
6199
|
})();
|
|
5049
6200
|
}
|
|
@@ -5059,7 +6210,7 @@ async function main() {
|
|
|
5059
6210
|
bootNotice('info', `Bootstrap 完成 (${bs.durationMs}ms, ${bs.errors.length} 个非致命错误)`);
|
|
5060
6211
|
}
|
|
5061
6212
|
catch (err) {
|
|
5062
|
-
bootNotice('warn',
|
|
6213
|
+
bootNotice('warn', `上下文扫描未完成 (可选步骤, 已跳过, 主流程继续): ${err.message} —— 详情见 startup.log`);
|
|
5063
6214
|
}
|
|
5064
6215
|
}
|
|
5065
6216
|
if (mode === 'web') {
|
|
@@ -5068,7 +6219,7 @@ async function main() {
|
|
|
5068
6219
|
// 默认用户模式: 不自迭代, 自改卡片不自动出现, 仍可 POST /api/self-improve/trigger 手动触发
|
|
5069
6220
|
const selfImprove = process.env.BOLLOON_DEV_MODE === '1' || process.env.BOLLOON_DEV_MODE === 'true';
|
|
5070
6221
|
if (selfImprove) {
|
|
5071
|
-
|
|
6222
|
+
writeOut('[startup] BOLLOON_DEV_MODE=1, 开发者模式: 自迭代已启用');
|
|
5072
6223
|
}
|
|
5073
6224
|
const { createWebServer, openBrowser } = await import('./web/server.js');
|
|
5074
6225
|
// 2026-06-24: CLI 默认 loopback bind (安全), LAN 访问需 BOLLOON_HOST=0.0.0.0
|
|
@@ -5083,7 +6234,7 @@ async function main() {
|
|
|
5083
6234
|
console.info = originalInfo;
|
|
5084
6235
|
process.stdout.write = originalStdoutWrite;
|
|
5085
6236
|
s.info('执行命令...');
|
|
5086
|
-
|
|
6237
|
+
writeOut('');
|
|
5087
6238
|
await runNonInteractive(args, comm);
|
|
5088
6239
|
comm?.stop();
|
|
5089
6240
|
// chat-watch / chat-p2p-listen 是长循环, 不会自然 return, 走 SIGINT 自然退出
|