@bolloon/bolloon-agent 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +39 -0
  2. package/dist/agents/agent-lang.js +101 -0
  3. package/dist/agents/agent-manifest-protocol.js +4 -0
  4. package/dist/agents/execution-supervisor.js +87 -5
  5. package/dist/agents/gateway-group.js +44 -3
  6. package/dist/agents/model-policy.js +651 -0
  7. package/dist/agents/pi-sdk-tools.js +84 -33
  8. package/dist/agents/pi-sdk.js +184 -31
  9. package/dist/agents/run-store.js +53 -0
  10. package/dist/agents/task/task-runner.js +8 -1
  11. package/dist/agents/workflow-pivot-loop.js +22 -13
  12. package/dist/cli/clipboard.js +65 -0
  13. package/dist/cli/ink-app.js +145 -17
  14. package/dist/cli/log-gate.js +386 -0
  15. package/dist/cli/mention-data.js +2 -0
  16. package/dist/cli/model-selector.js +538 -0
  17. package/dist/cli/reply-hygiene.js +54 -0
  18. package/dist/cli/setup-wizard.js +686 -69
  19. package/dist/cli/startup-notice.js +160 -0
  20. package/dist/cli/stores.js +22 -1
  21. package/dist/cli/theme.js +49 -0
  22. package/dist/cli/tui-select.js +970 -0
  23. package/dist/cli-entry.js +125 -18
  24. package/dist/efficode/compress.js +78 -0
  25. package/dist/efficode/crc32.js +38 -0
  26. package/dist/efficode/did.js +66 -0
  27. package/dist/efficode/index.js +24 -0
  28. package/dist/efficode/lz77.js +170 -0
  29. package/dist/efficode/negotiate.js +187 -0
  30. package/dist/efficode/ops.js +87 -0
  31. package/dist/efficode/packet.js +271 -0
  32. package/dist/efficode/types.js +45 -0
  33. package/dist/electron-build/agents/agent-manifest-protocol.js +4 -0
  34. package/dist/electron-build/agents/agent-manifest-protocol.js.map +1 -1
  35. package/dist/electron-build/agents/execution-supervisor.js +86 -4
  36. package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
  37. package/dist/electron-build/agents/goal-store.js.map +1 -1
  38. package/dist/electron-build/agents/model-policy.js +706 -0
  39. package/dist/electron-build/agents/model-policy.js.map +1 -0
  40. package/dist/electron-build/agents/pi-sdk-tools.js +84 -33
  41. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  42. package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
  43. package/dist/electron-build/agents/pi-sdk.js +185 -31
  44. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  45. package/dist/electron-build/agents/run-store.js +55 -1
  46. package/dist/electron-build/agents/run-store.js.map +1 -1
  47. package/dist/electron-build/agents/task/task-runner.js +8 -1
  48. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  49. package/dist/electron-build/agents/workflow-pivot-loop.js +22 -13
  50. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  51. package/dist/electron-build/llm/config-store.js +146 -6
  52. package/dist/electron-build/llm/config-store.js.map +1 -1
  53. package/dist/electron-build/llm/connection-probe.js +764 -0
  54. package/dist/electron-build/llm/connection-probe.js.map +1 -0
  55. package/dist/electron-build/llm/custom-provider-store.js +272 -0
  56. package/dist/electron-build/llm/custom-provider-store.js.map +1 -0
  57. package/dist/electron-build/llm/data/provider-catalog-baked.js +23 -0
  58. package/dist/electron-build/llm/data/provider-catalog-baked.js.map +1 -0
  59. package/dist/electron-build/llm/model-catalog.js +778 -0
  60. package/dist/electron-build/llm/model-catalog.js.map +1 -0
  61. package/dist/electron-build/llm/model-discovery.js +1202 -0
  62. package/dist/electron-build/llm/model-discovery.js.map +1 -0
  63. package/dist/electron-build/llm/model-selection.js +1315 -0
  64. package/dist/electron-build/llm/model-selection.js.map +1 -0
  65. package/dist/electron-build/llm/pi-ai.js +751 -55
  66. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  67. package/dist/electron-build/llm/provider-catalog.js +969 -0
  68. package/dist/electron-build/llm/provider-catalog.js.map +1 -0
  69. package/dist/electron-build/llm/provider-registry.js +950 -0
  70. package/dist/electron-build/llm/provider-registry.js.map +1 -0
  71. package/dist/electron-build/llm/tool-name.js +266 -0
  72. package/dist/electron-build/llm/tool-name.js.map +1 -0
  73. package/dist/electron-build/utils/dual-source-facts.js +236 -0
  74. package/dist/electron-build/utils/dual-source-facts.js.map +1 -0
  75. package/dist/electron-build/utils/dual-source.js +32 -156
  76. package/dist/electron-build/utils/dual-source.js.map +1 -1
  77. package/dist/electron-build/utils/update-manager.js +22 -72
  78. package/dist/electron-build/utils/update-manager.js.map +1 -1
  79. package/dist/electron-build/utils/update-state.js +2 -19
  80. package/dist/electron-build/utils/update-state.js.map +1 -1
  81. package/dist/electron-build/utils/version-identity.js +193 -0
  82. package/dist/electron-build/utils/version-identity.js.map +1 -0
  83. package/dist/electron-build/utils/version-info.js +47 -101
  84. package/dist/electron-build/utils/version-info.js.map +1 -1
  85. package/dist/index.js +282 -96
  86. package/dist/ios/bolloon-web.json +10 -0
  87. package/dist/ios/mobile-contacts.js +44 -11
  88. package/dist/ios/mobile-core.js +1723 -84
  89. package/dist/ios/mobile-tasks.js +339 -0
  90. package/dist/ios/mobile-update.js +1442 -0
  91. package/dist/ios/mobile.js +308 -35
  92. package/dist/ios/routes-llm-config.js +344 -52
  93. package/dist/ios/routes-mobile-tasks.js +353 -0
  94. package/dist/ios/server.js +163 -4
  95. package/dist/ios/sw.js +1 -1
  96. package/dist/llm/config-store.js +146 -6
  97. package/dist/llm/connection-probe.js +744 -0
  98. package/dist/llm/custom-provider-store.js +224 -0
  99. package/dist/llm/data/provider-catalog-baked.js +19 -0
  100. package/dist/llm/model-catalog.js +717 -0
  101. package/dist/llm/model-discovery.js +1137 -0
  102. package/dist/llm/model-selection.js +1241 -0
  103. package/dist/llm/pi-ai.js +732 -54
  104. package/dist/llm/provider-catalog.js +898 -0
  105. package/dist/llm/provider-registry.js +917 -0
  106. package/dist/llm/tool-name.js +220 -0
  107. package/dist/setup/onboard.js +34 -8
  108. package/dist/utils/dual-source-facts.js +223 -0
  109. package/dist/utils/dual-source.js +12 -141
  110. package/dist/utils/update-manager.js +16 -59
  111. package/dist/utils/update-state.js +6 -18
  112. package/dist/utils/version-identity.js +176 -0
  113. package/dist/utils/version-info.js +9 -78
  114. package/dist/web/bolloon-web.json +10 -0
  115. package/dist/web/mobile-core.js +1721 -84
  116. package/dist/web/mobile-update.js +1442 -0
  117. package/dist/web/mobile.js +372 -35
  118. package/dist/web/routes-llm-config.js +344 -52
  119. package/dist/web/sw.js +1 -1
  120. package/package.json +1 -1
  121. package/scripts/build-web.ts +36 -0
@@ -0,0 +1,386 @@
1
+ /**
2
+ * log-gate.ts — 启动期日志闸门 (2026-09-26)
3
+ *
4
+ * 为什么要有这一层 (而不是逐个文件删 console.log):
5
+ * 启动刷屏不是某一处, 而是**很多模块各自 console.log** ——
6
+ * `src/web/server.ts` → `[web] webRoot` / `[createWebServer]` / `[runs] 对账` / `[context]` /
7
+ * `[did-catalog]` / `[自愈]` / `[ipfs]` / `[supervisor]` / `开始生成 P2P 身份...`
8
+ * `src/llm/pi-ai.ts` → `[PiAIModel]` / `[pi-ai timing]`
9
+ * `src/bootstrap/*` → `[bootstrap]`
10
+ * `src/pi-ecosystem-mcp` → `[McpAdapter]`
11
+ * `src/network/*` → `[IrohTransport]`
12
+ * `node_modules/@diap/sdk` (依赖库) → `2026-…T [info]: …`
13
+ * 逐个文件去删既不收敛 (后来者会再加回来), 也挡不住依赖库。所以闸门装在**输出层**
14
+ * (`process.stdout.write` / `process.stderr.write` / `console.*`), 一处生效, 覆盖三个启动面:
15
+ * - `cli-interactive` CLI 交互启动 (stdout 归 Ink 所有: 加载日志直接丢, 信号行改道 stderr)
16
+ * - `web` dashboard / web 启动 (按行丢加载日志, 其它原样透传)
17
+ * - `plain` 其它启动面 (同上, 不做 console 替换)
18
+ *
19
+ * 三条不可违反的口径 (leo 明确):
20
+ * ① **默认静默** —— 加载日志默认不上控制台 (必要的 spinner / 品牌框 / 状态栏由 Ink 自己渲染, 不走这里);
21
+ * ② **诊断不丢** —— 被静默掉的每一行都落到日志文件; `--verbose` / `BOLLOON_VERBOSE=1` 时**一行不改**地全量回流;
22
+ * ③ **错误 / 降级 / 需人介入的信息不算加载日志** —— `carriesHumanSignal()` 判定的行在**任何模式**下都可见
23
+ * (交互 CLI 里改道 stderr, 不污染 Ink 的 stdout 画布)。
24
+ *
25
+ * 逃生口: `BOLLOON_LOG_GATE=0` (或 `off` / `false`) 彻底不装闸门 (与 verbose 的区别是 verbose 仍写文件)。
26
+ */
27
+ import * as fs from 'fs';
28
+ import * as os from 'os';
29
+ import * as path from 'path';
30
+ import { inspect } from 'util';
31
+ export const VERBOSE_ENV = 'BOLLOON_VERBOSE';
32
+ export const GATE_ENV = 'BOLLOON_LOG_GATE';
33
+ // ---------------------------------------------------------------------------
34
+ // 行分类
35
+ // ---------------------------------------------------------------------------
36
+ /** `[web]` `[bootstrap]` `[supervisor-host]` `[McpAdapter]` `[PiAIModel]` … (ASCII 模块 tag) */
37
+ const MODULE_TAG_ASCII = /^[ \t]{0,8}\[[A-Za-z_][A-Za-z0-9_.:-]*\]/;
38
+ /** `[自愈]` `[迁移:openclaw]` … (中文模块 tag) */
39
+ const MODULE_TAG_CJK = /^[ \t]{0,8}\[[\u4e00-\u9fff][^\]\n]{0,24}\]/;
40
+ /** `2026-09-26T06:16:57.905Z [info]: …` (依赖库/自研 logger 的 ISO 时间戳行) */
41
+ const ISO_TIMESTAMP_LINE = /^\s*\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
42
+ /** `kp.publicKey: Uint8Array(32) [` —— console.log 直接打印对象时的 inspect dump 开头 */
43
+ const INSPECT_DUMP_OPEN = /^[A-Za-z_$][\w$]*(?:\.[\w$]+)*: (?:Uint8Array|Buffer|Map|Set|Array)\(\d+\) \[$/;
44
+ /**
45
+ * 少量**没有 module tag** 的启动自述行 (`src/web/server.ts` 的 P2P 身份打印)。
46
+ * 它们是加载过程的叙述, 不是结果 —— 默认静默, 但照样落日志文件。
47
+ * 每条 pattern 都按行首锚定, 只覆盖实测观察到的那几句, 不做宽泛匹配。
48
+ */
49
+ const UNTAGGED_STARTUP_NARRATION = [
50
+ /^开始生成 P2P 身份\.\.\.$/,
51
+ /^(?:复用|新建) P2P 身份: /,
52
+ /^P2P 身份已生成: /,
53
+ /^DID: did:/,
54
+ ];
55
+ /** 「错误 / 降级 / 需人介入」信号 —— 命中的行**任何模式下都不许被吞** */
56
+ const HUMAN_SIGNAL = new RegExp([
57
+ '失败', '错误', '异常', '拒绝', '未就绪', '未完成', '未配置', '不可用', '降级', '回退',
58
+ '超时', '转人工', '中断', '损坏', '缺失', '冲突', '警告', '不安全', '越权', '请先', '占用',
59
+ '⚠', '✗', '⛔', '❌',
60
+ '\\berror\\b', '\\bfail(?:ed|ure|s)?\\b', '\\bfatal\\b', '\\bwarn(?:ing)?\\b', '\\btimeout\\b',
61
+ '\\bcannot\\b', '\\bunable\\b', '\\brefused\\b', '\\bdenied\\b', '\\bunavailable\\b',
62
+ '\\bdegraded\\b', '\\bcrash(?:ed)?\\b', '\\bnot ready\\b',
63
+ 'ERR_[A-Z]+', 'ECONN', 'EACCES', 'ENOENT', 'EADDRINUSE', 'EADDRNOTAVAIL',
64
+ ].join('|'), 'i');
65
+ /**
66
+ * 「0 个错误 / 0 个转人工 / 0 errors」这类**零计数**短语 —— 意思是「没有要人管的事」, 不算信号。
67
+ * 先把这些短语抠掉再判信号: 这样「同一行里还写了别的失败/超时」仍然会被判成信号,
68
+ * 而 `N 个转人工` (N>0) 这种真要人介入的行照样保留。
69
+ */
70
+ const BENIGN_COUNT = /0\s*(?:个|条)(?:非致命)?(?:错误|转人工|失败|异常)|no errors?|0 errors?/gi;
71
+ /** 这一行是不是「加载日志」(默认模式下从控制台丢掉, 写进日志文件) */
72
+ export function isStartupLogLine(line) {
73
+ if (!line)
74
+ return false;
75
+ return MODULE_TAG_ASCII.test(line)
76
+ || MODULE_TAG_CJK.test(line)
77
+ || ISO_TIMESTAMP_LINE.test(line)
78
+ || INSPECT_DUMP_OPEN.test(line)
79
+ || UNTAGGED_STARTUP_NARRATION.some((re) => re.test(line));
80
+ // inspect dump 的续行 (纯数字/逗号) 由 filterText 的 dump 状态机处理, 这里不判
81
+ }
82
+ /** 这一行是不是「错误 / 降级 / 需人介入」—— 是则任何模式下都不许被静默 */
83
+ export function carriesHumanSignal(line) {
84
+ if (!line)
85
+ return false;
86
+ return HUMAN_SIGNAL.test(line.replace(BENIGN_COUNT, ' '));
87
+ }
88
+ // ---------------------------------------------------------------------------
89
+ // verbose / disabled / 日志路径
90
+ // ---------------------------------------------------------------------------
91
+ const TRUTHY = new Set(['1', 'true', 'yes', 'on', 'verbose']);
92
+ export function isStartupVerbose(args = process.argv.slice(2), env = process.env) {
93
+ const envVal = (env[VERBOSE_ENV] ?? '').trim().toLowerCase();
94
+ if (TRUTHY.has(envVal))
95
+ return true;
96
+ return args.some((a) => a === '--verbose' || /^--verbose=(1|true|yes|on)$/i.test(a));
97
+ }
98
+ export function isGateDisabled(env = process.env) {
99
+ const v = (env[GATE_ENV] ?? '').trim().toLowerCase();
100
+ return v === '0' || v === 'off' || v === 'false' || v === 'no';
101
+ }
102
+ /**
103
+ * 日志文件路径。
104
+ * `$BOLLOON_HOME/logs/startup.log` 优先 (与 setup-store 的 home 口径一致), 否则 `~/.bolloon/logs/startup.log`。
105
+ * ⚠️ `homeDir` 默认值在**调用时**求值 —— 覆盖 `process.env.HOME` 之前取到的才是真实家目录。
106
+ */
107
+ export function startupLogPath(env = process.env, homeDir = os.homedir()) {
108
+ const explicit = (env.BOLLOON_HOME ?? '').trim();
109
+ const base = explicit || path.join(homeDir, '.bolloon');
110
+ return path.join(base, 'logs', 'startup.log');
111
+ }
112
+ // ---------------------------------------------------------------------------
113
+ // 闸门本体
114
+ // ---------------------------------------------------------------------------
115
+ let activeGate = null;
116
+ /**
117
+ * 当前闸门的**文件写入口** (只落盘不上屏的那条路)。
118
+ *
119
+ * 为什么要有它: `startup-notice.ts` 默认把"启动前言"收进内存**不上屏**, 但"诊断不丢"这条
120
+ * 规矩不因为不上屏就作废 —— 那一段必须照样落进 `startup.log`。落盘逻辑(路径/时间戳/失败退场)
121
+ * 只允许有一份, 所以由闸门把它开放出来, 而不是让调用方自己拼一遍。
122
+ */
123
+ let activeFileWriter = null;
124
+ /** 追加一行到启动日志文件 (不上屏)。闸门没装时按**已文档化的路径契约**自己落盘。 */
125
+ export function logStartupLine(line) {
126
+ try {
127
+ if (activeFileWriter) {
128
+ activeFileWriter([line]);
129
+ return;
130
+ }
131
+ const p = startupLogPath();
132
+ fs.mkdirSync(path.dirname(p), { recursive: true });
133
+ fs.appendFileSync(p, `[${new Date().toISOString()}] ${line}\n`);
134
+ }
135
+ catch { /* 落盘失败绝不影响启动 */ }
136
+ }
137
+ /** 取当前闸门 (未装 = null); 验收脚本用它读 stats */
138
+ export function getStartupLogGate() {
139
+ return activeGate;
140
+ }
141
+ /** 单测用: 卸掉当前闸门 */
142
+ export function resetStartupLogGate() {
143
+ activeGate?.stop();
144
+ activeGate = null;
145
+ }
146
+ /** 把 console.* 的参数拼成一行文本 (对齐 console 的 printf 风格: 第一个参数含 %s 时替换) */
147
+ function formatConsoleArgs(args) {
148
+ if (args.length === 0)
149
+ return '';
150
+ const first = args[0];
151
+ if (typeof first === 'string' && /%[sdioOj]/.test(first)) {
152
+ let i = 1;
153
+ // eslint-disable-next-line no-control-regex
154
+ const interp = first.replace(/%[sdioOj]/g, () => stringify(args[i++]));
155
+ return [interp, ...args.slice(i).map(stringify)].join(' ');
156
+ }
157
+ return args.map(stringify).join(' ');
158
+ }
159
+ function stringify(v) {
160
+ if (typeof v === 'string')
161
+ return v;
162
+ if (v instanceof Error)
163
+ return v.stack || `${v.name}: ${v.message}`;
164
+ if (typeof v === 'object' && v !== null) {
165
+ try {
166
+ return inspect(v, { depth: 2, breakLength: 120 });
167
+ }
168
+ catch {
169
+ return String(v);
170
+ }
171
+ }
172
+ return String(v);
173
+ }
174
+ export function installStartupLogGate(opts = {}) {
175
+ if (activeGate)
176
+ return activeGate;
177
+ const env = opts.env ?? process.env;
178
+ const mode = opts.mode ?? 'plain';
179
+ const verbose = opts.verbose ?? isStartupVerbose(opts.args ?? process.argv.slice(2), env);
180
+ const disabled = opts.disabled ?? isGateDisabled(env);
181
+ const logPath = opts.logPath ?? startupLogPath(env);
182
+ const filterConsole = mode === 'cli-interactive';
183
+ const rerouteSignals = mode === 'cli-interactive';
184
+ const writeFile = opts.writeFile !== false;
185
+ const stats = {
186
+ suppressed: 0, forwarded: 0, signalKept: 0, fileLines: 0, fileError: null,
187
+ };
188
+ const filtering = !verbose && !disabled;
189
+ // ---- 日志文件 (append; 每 chunk 一次 appendFileSync, 保证进程被强杀也不丢) ----
190
+ let fileReady = writeFile;
191
+ if (fileReady) {
192
+ try {
193
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
194
+ fs.appendFileSync(logPath, `# ${new Date().toISOString()} startup log (mode=${mode} verbose=${verbose} pid=${process.pid}) argv=${process.argv.slice(2).join(' ')}\n`);
195
+ }
196
+ catch (e) {
197
+ fileReady = false;
198
+ stats.fileError = `日志文件不可写: ${logPath} (${e.message})`;
199
+ }
200
+ }
201
+ const fileBuf = [];
202
+ function writeLines(lines) {
203
+ if (!fileReady || lines.length === 0)
204
+ return;
205
+ for (const l of lines)
206
+ fileBuf.push(l);
207
+ const payload = fileBuf.map((l) => `[${new Date().toISOString()}] ${l}`).join('\n') + '\n';
208
+ fileBuf.length = 0;
209
+ try {
210
+ fs.appendFileSync(logPath, payload);
211
+ stats.fileLines += lines.length;
212
+ }
213
+ catch (e) {
214
+ fileReady = false;
215
+ stats.fileError = `日志文件写入失败: ${e.message}`;
216
+ }
217
+ }
218
+ const origStdout = process.stdout.write.bind(process.stdout);
219
+ const origStderr = process.stderr.write.bind(process.stderr);
220
+ const origConsole = {
221
+ log: console.log, info: console.info, debug: console.debug,
222
+ warn: console.warn, error: console.error,
223
+ };
224
+ // inspect dump 的多行续行状态 (按流各自维护)
225
+ const dumpState = { stdout: false, stderr: false };
226
+ function classify(line, stream) {
227
+ const trimmed = line.trim();
228
+ if (dumpState[stream]) {
229
+ if (trimmed === ']')
230
+ dumpState[stream] = false;
231
+ return 'loading';
232
+ }
233
+ if (carriesHumanSignal(line))
234
+ return 'signal';
235
+ if (isStartupLogLine(line)) {
236
+ if (INSPECT_DUMP_OPEN.test(line))
237
+ dumpState[stream] = true;
238
+ return 'loading';
239
+ }
240
+ return 'plain';
241
+ }
242
+ /** 按行判定: 返回要写到「本流」的内容; 加载日志丢掉 (写文件), 信号行按模式处理 */
243
+ function filterText(text, stream) {
244
+ const parts = text.split('\n');
245
+ const trailingNewline = parts.length > 1 && parts[parts.length - 1] === '';
246
+ if (trailingNewline)
247
+ parts.pop();
248
+ const kept = [];
249
+ const toFile = [];
250
+ for (const line of parts) {
251
+ const verdict = classify(line, stream);
252
+ toFile.push(line);
253
+ if (verdict === 'loading') {
254
+ stats.suppressed++;
255
+ continue;
256
+ }
257
+ if (verdict === 'signal') {
258
+ stats.signalKept++;
259
+ stats.forwarded++;
260
+ // 交互 CLI: stdout 是 Ink 的画布 —— 信号行改道 stderr, 既不丢也不糊屏
261
+ if (rerouteSignals && stream === 'stdout') {
262
+ origStderr(line + '\n');
263
+ continue;
264
+ }
265
+ kept.push(line);
266
+ continue;
267
+ }
268
+ stats.forwarded++;
269
+ kept.push(line);
270
+ }
271
+ writeLines(toFile);
272
+ if (kept.length === 0)
273
+ return '';
274
+ return kept.join('\n') + (trailingNewline ? '\n' : '');
275
+ }
276
+ function makeStreamWrapper(orig, stream) {
277
+ return function wrapper(chunk, encOrCb, cb) {
278
+ const cbFn = typeof encOrCb === 'function' ? encOrCb : typeof cb === 'function' ? cb : null;
279
+ const enc = typeof encOrCb === 'string' ? encOrCb : undefined;
280
+ let out = chunk;
281
+ try {
282
+ const text = typeof chunk === 'string'
283
+ ? chunk
284
+ : (Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk));
285
+ const filtered = filterText(text, stream);
286
+ if (filtered === text)
287
+ return orig(chunk, encOrCb, cb);
288
+ out = filtered;
289
+ }
290
+ catch {
291
+ // 闸门自身出错绝不吞内容: 原样放行
292
+ return orig(chunk, encOrCb, cb);
293
+ }
294
+ if (out === '') {
295
+ // 整 chunk 都被静默: 仍要按 Node 语义回调一次, 否则 Ink 的渲染链会卡死
296
+ if (cbFn)
297
+ process.nextTick(cbFn);
298
+ return true;
299
+ }
300
+ return orig(out, enc, cbFn ?? undefined);
301
+ };
302
+ }
303
+ function makeConsoleFn() {
304
+ return (...args) => {
305
+ const text = formatConsoleArgs(args);
306
+ const toFile = [];
307
+ for (const line of text.split('\n')) {
308
+ toFile.push(line);
309
+ if (carriesHumanSignal(line)) {
310
+ // 需人介入的信息必须可见: 交互 CLI 的 stdout 归 Ink, 所以走 stderr
311
+ stats.signalKept++;
312
+ origStderr(line + '\n');
313
+ }
314
+ else {
315
+ stats.suppressed++;
316
+ }
317
+ }
318
+ writeLines(toFile);
319
+ };
320
+ }
321
+ function makeConsoleError() {
322
+ return (...args) => {
323
+ const text = formatConsoleArgs(args);
324
+ writeLines(text.split('\n'));
325
+ stats.forwarded++;
326
+ origStderr(text + '\n');
327
+ };
328
+ }
329
+ let stopped = false;
330
+ const handle = {
331
+ mode,
332
+ verbose,
333
+ filtering,
334
+ logPath,
335
+ stats,
336
+ stop() {
337
+ if (stopped)
338
+ return;
339
+ stopped = true;
340
+ if (activeFileWriter === writeLines)
341
+ activeFileWriter = null;
342
+ if (filtering) {
343
+ process.stdout.write = origStdout;
344
+ process.stderr.write = origStderr;
345
+ if (filterConsole) {
346
+ console.log = origConsole.log;
347
+ console.info = origConsole.info;
348
+ console.debug = origConsole.debug;
349
+ console.warn = origConsole.warn;
350
+ console.error = origConsole.error;
351
+ }
352
+ }
353
+ },
354
+ };
355
+ if (filtering) {
356
+ process.stdout.write = makeStreamWrapper(origStdout, 'stdout');
357
+ process.stderr.write = makeStreamWrapper(origStderr, 'stderr');
358
+ if (filterConsole) {
359
+ console.log = makeConsoleFn();
360
+ console.info = makeConsoleFn();
361
+ console.debug = makeConsoleFn();
362
+ console.warn = makeConsoleFn();
363
+ console.error = makeConsoleError();
364
+ }
365
+ }
366
+ else if (fileReady) {
367
+ // verbose / disabled: 控制台一行不改, 但为了「诊断不丢」仍把内容抄一份进日志文件
368
+ const tee = (orig) => (chunk, encOrCb, cb) => {
369
+ try {
370
+ const text = typeof chunk === 'string' ? chunk : String(chunk);
371
+ writeLines(text.split('\n').filter((l, i, arr) => !(i === arr.length - 1 && l === '')));
372
+ }
373
+ catch { /* 抄写失败不影响放行 */ }
374
+ return orig(chunk, encOrCb, cb);
375
+ };
376
+ process.stdout.write = tee(origStdout);
377
+ process.stderr.write = tee(origStderr);
378
+ }
379
+ if (stats.fileError && verbose) {
380
+ origStderr(`${stats.fileError}\n`);
381
+ }
382
+ activeGate = handle;
383
+ if (fileReady)
384
+ activeFileWriter = writeLines;
385
+ return handle;
386
+ }
@@ -32,6 +32,8 @@ const CLI_COMMANDS = [
32
32
  { kind: 'command', label: 'resume', hint: '恢复上下文', insert: 'resume' },
33
33
  { kind: 'command', label: 'goal', hint: '进行中目标', insert: 'goal' },
34
34
  { kind: 'command', label: 'tools', hint: '可用工具列表', insert: 'tools' },
35
+ // 2026-09-27: 面板是 Ink 整屏自绘, 鼠标框选会被"重画一帧"打断 → 给一条不靠鼠标的复制通路
36
+ { kind: 'command', label: 'copy', hint: '复制最近一条回复到系统剪贴板 (/copy all · /copy N)', insert: 'copy' },
35
37
  { kind: 'command', label: 'skill', hint: '技能候选', insert: 'skill' },
36
38
  { kind: 'command', label: 'mcp', hint: 'MCP 服务器列表', insert: 'mcp' },
37
39
  { kind: 'command', label: 'agent', hint: '当前智能体', insert: 'agent' },