@bolloon/bolloon-agent 0.3.22 → 0.3.24

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 (37) hide show
  1. package/dist/agents/deny-pipeline.js +116 -0
  2. package/dist/agents/parse-tool-call.js +26 -4
  3. package/dist/agents/pi-sdk.js +247 -50
  4. package/dist/agents/session-store.js +87 -2
  5. package/dist/bootstrap/snip-collapse.js +135 -0
  6. package/dist/cli/ink-app.js +116 -0
  7. package/dist/cli/loading-tui.js +64 -10
  8. package/dist/electron/config.js +14 -9
  9. package/dist/electron/dialogs.js +53 -16
  10. package/dist/electron/first-run.js +65 -24
  11. package/dist/electron/ipc.js +14 -10
  12. package/dist/electron/logger.js +44 -7
  13. package/dist/electron/main.js +45 -42
  14. package/dist/electron/menu.js +18 -13
  15. package/dist/electron/paths.js +54 -12
  16. package/dist/electron/server.js +57 -18
  17. package/dist/electron/tray.js +53 -15
  18. package/dist/electron/window.js +61 -22
  19. package/dist/electron-preload.js +19 -16
  20. package/dist/electron.js +4 -1
  21. package/dist/external-engines/delegate.js +19 -0
  22. package/dist/hooks/hooks-engine.js +329 -0
  23. package/dist/index.js +130 -133
  24. package/dist/llm/pi-ai.js +5 -17
  25. package/dist/security/tool-gate.js +8 -1
  26. package/dist/social/dunbar-tier.js +409 -0
  27. package/dist/utils/auto-update.js +51 -12
  28. package/dist/web/client.js +4833 -4328
  29. package/dist/web/components/p2p/index.js +234 -276
  30. package/dist/web/server.js +17 -3
  31. package/dist/web/style.css +2 -2
  32. package/dist/web/ui/message-renderer.js +396 -535
  33. package/dist/web/ui/step-timeline.js +273 -372
  34. package/package.json +27 -25
  35. package/dist/web/components/p2p/P2PModal.js +0 -188
  36. package/dist/web/components/p2p/p2p-modal.js +0 -664
  37. package/dist/web/components/p2p/p2p-tools.js +0 -248
package/dist/index.js CHANGED
@@ -15,7 +15,8 @@ import { createSubAgentManager } from './agents/subagent-manager.js';
15
15
  import { getGlobalSharedContext } from './social/global-shared-context.js';
16
16
  import { createBollharnessIntegration } from './bollharness-integration/index.js';
17
17
  import * as readline from 'readline';
18
- import { printBanner, renderDialog, renderUserMessage, renderAgentMessage, renderToolCall, flowConnector, termWidth, brandArtLines, boxTop, boxRow, boxBottom, dispWidth } from './cli/loading-tui.js';
18
+ import { printBanner, renderDialog, renderUserMessage, renderAgentMessage, renderToolCallListItem, termWidth } from './cli/loading-tui.js';
19
+ import { startInk, stopInk, inkAppendLine as appendLine, inkSetStatus, inkSetThinking } from './cli/ink-app.js';
19
20
  // 启动自动检查更新:后台、节流、检测到新版本自动安装(可被 --no-update / BOLLOON_SKIP_UPDATE 关闭)
20
21
  import { createRequire } from 'module';
21
22
  const _require = createRequire(import.meta.url);
@@ -90,15 +91,21 @@ const s = {
90
91
  console.log();
91
92
  },
92
93
  Thinking: () => {
93
- const frames = ['', '', '', '⠸', '⠼', '', '⠦', '⠧', '⠇', '⠏'];
94
+ const frames = ['(`・ω・´)', '(´・_・`)', '(。•́︿•̀。)', 'ᕙ(▀̿̿Ĺ̯̿̿▀̿ ̿)ᕗ', '(◕‿◕)'];
94
95
  let i = 0;
96
+ let dots = 0;
97
+ const frame = frames[0];
98
+ appendLine(` ${frame} 思考...`);
95
99
  return setInterval(() => {
96
- process.stdout.write(`\r ${YELLOW}${frames[i++ % frames.length]} 思考中...${RESET} `);
97
- }, 80);
100
+ i = (i + 1) % frames.length;
101
+ dots = (dots + 1) % 4;
102
+ const dotStr = '.'.repeat(dots || 1);
103
+ appendLine(`\r ${frames[i]} 思考${dotStr} `);
104
+ }, 600);
98
105
  },
99
106
  clearThinking: (interval) => {
100
107
  clearInterval(interval);
101
- process.stdout.write('\r' + ' '.repeat(30) + '\r');
108
+ process.stdout.write('\r' + ' '.repeat(40) + '\r');
102
109
  },
103
110
  dialog: async (title, promptText) => {
104
111
  return new Promise((resolve) => {
@@ -179,7 +186,7 @@ function publishDID(name, kp) {
179
186
  }
180
187
  catch (e) {
181
188
  // 一次失败直接放弃 — 本地模式运行就够了, 不重试
182
- process.stdout.write(` ${YELLOW}⚠ IPFS 发布失败 (${e?.message?.slice(0, 80) || 'unknown'}), 本地模式运行${RESET}\n`);
189
+ appendLine(` ${YELLOW}⚠ IPFS 发布失败 (${e?.message?.slice(0, 80) || 'unknown'}), 本地模式运行${RESET}`);
183
190
  s.step(2, 5, '发布 DID → IPFS', 'warn');
184
191
  resolve({});
185
192
  }
@@ -347,13 +354,12 @@ function rpcErr(code, msg) {
347
354
  // CLI with persistent bottom prompt
348
355
  // 2026-07-28: 改用 readline.createInterface + replReadline 循环
349
356
  let isRunning = false;
357
+ let cliContextPct = 0;
350
358
  let queueMode = false;
351
359
  const pendingQueue = [];
352
- // 底部状态栏数据
353
360
  let cliStartTime = 0;
354
361
  let cliModelName = '…';
355
362
  let cliAgentName = '…';
356
- let cliContextPct = 0;
357
363
  function fmtDuration(ms) {
358
364
  const s = Math.floor(ms / 1000);
359
365
  const m = Math.floor(s / 60);
@@ -371,16 +377,21 @@ function statusBarLine() {
371
377
  const bar = C_OK + '█'.repeat(filled) + C_DIM + '░'.repeat(barLen - filled) + RESET;
372
378
  return `${C_ACCENT}${cliModelName}${RESET}${C_DIM} │${RESET} ${cliAgentName} ${C_DIM}│${RESET} ⏱ ${C_ACCENT}${dur}${RESET} ${C_DIM}│${RESET} ${bar} ${C_DIM}${cliContextPct}%${RESET}`;
373
379
  }
374
- function startCLI(comm) {
380
+ async function startCLI(comm) {
375
381
  isRunning = true;
376
- // CLI 模式下过滤 [xxx] 内部日志
377
- const _origLog = console.log;
378
- const _origWarn = console.warn;
379
- const _isInternal = (args) => args.length && typeof args[0] === 'string' && /^\[[A-Za-z _\-.]+/.test(args[0]);
380
- console.log = (...args) => { if (_isInternal(args))
381
- return; _origLog.apply(console, args); };
382
- console.warn = (...args) => { if (_isInternal(args))
383
- return; _origWarn.apply(console, args); };
382
+ // CLI 模式下静音所有 console.log/warn
383
+ // (Ink 用自己的 render 引擎, console.log 输出会污染终端)
384
+ console.log = () => { };
385
+ console.warn = () => { };
386
+ // 同时过滤 process.stdout.write 阻止 [xxx] 前缀的输出
387
+ const _origStdout = process.stdout.write.bind(process.stdout);
388
+ process.stdout.write = ((chunk, ...rest) => {
389
+ const s = typeof chunk === 'string' ? chunk : String(chunk);
390
+ // 过滤以 [ 开头的行 (agent 内部日志)
391
+ if (s.trimStart().startsWith('['))
392
+ return true;
393
+ return _origStdout(chunk, ...rest);
394
+ });
384
395
  let peerCount = 0;
385
396
  try {
386
397
  peerCount = comm.getConnections().length;
@@ -401,109 +412,96 @@ function startCLI(comm) {
401
412
  cliModelName = foundProvider ? foundProvider[1] : '未配置';
402
413
  cliAgentName = agentIdentity?.name || 'bolloon';
403
414
  cliStartTime = Date.now();
404
- process.stdout.write(`${C_DIM}输入 !cmd 执行终端 · /queue 入队 · /help 帮助${RESET}\n\n`);
405
- // 启动框: logo 左对齐(无状态栏 状态栏在输入框上方)
406
- const art = brandArtLines();
407
- const maxArt = art.reduce((m, l) => Math.max(m, dispWidth(l)), 0);
408
- const boxW = Math.min(termWidth() - 2, Math.max(40, maxArt) + 4);
409
- process.stdout.write(boxTop('Bolloon Agent', boxW) + '\n');
410
- for (const l of art)
411
- process.stdout.write(boxRow(l, boxW, 'left') + '\n');
412
- process.stdout.write(boxBottom(boxW) + '\n\n');
413
- // readline 输入循环(底部没有独立状态栏)
414
- replReadline(comm);
415
- }
416
- async function replReadline(comm) {
417
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
418
- const SEP = '\x1b[90m─';
419
- const RST = '\x1b[0m';
420
- while (isRunning) {
421
- const tw = process.stdout.columns || 80;
422
- const sepLine = SEP.repeat(tw) + RST;
423
- const prefix = queueMode ? `${C_WARN}▸${RST}` : `${C_ACCENT}❯${RST}`;
424
- const raw = await new Promise(resolve => rl.question(`\n${sepLine}\n${statusBarLine()}\n${sepLine}\n${prefix} `, resolve));
425
- const trimmed = raw.trim();
426
- process.stdout.write(`\n${sepLine}\n\n\n\n\n`);
427
- if (!trimmed)
428
- continue;
429
- if (!isRunning)
430
- break;
431
- await processInput(trimmed, comm);
432
- }
433
- rl.close();
434
- process.stdout.write(`\n${CYAN}👋 再见!${RESET}\n`);
435
- process.stdin.destroy();
415
+ // 进入 Ink TUI 输入循环
416
+ const initialStatus = `${C_ACCENT}${cliModelName}\x1b[0m\x1b[90m │\x1b[0m ${cliAgentName} \x1b[90m│\x1b[0m ⏱ 0s`;
417
+ const getStatus = () => {
418
+ const dur = Math.floor((Date.now() - cliStartTime) / 1000);
419
+ const barLen = 12;
420
+ const filled = Math.round((cliContextPct / 100) * barLen);
421
+ const bar = '█'.repeat(filled) + '░'.repeat(barLen - filled);
422
+ return `${C_ACCENT}${cliModelName}\x1b[0m\x1b[90m │\x1b[0m ${cliAgentName} \x1b[90m│\x1b[0m ⏱ ${dur}s\x1b[90m │\x1b[0m ${bar} ${cliContextPct}%`;
423
+ };
424
+ startInk((text) => { processInput(text, comm); }, initialStatus, getStatus);
425
+ // Wait on a promise that resolves on Ctrl+C
426
+ await new Promise(() => { });
427
+ stopInk();
428
+ appendLine(`\n${CYAN}👋 再见!${RESET}`);
436
429
  comm.stop();
437
430
  }
438
431
  async function processInput(input, comm) {
439
432
  const trimmed = input.trim();
433
+ // TUI tool call state (local to this invocation)
434
+ const tuiToolCalls = [];
435
+ let tuiToolCounter = 0;
436
+ // each iteration
437
+ let lastToolEvent = null;
440
438
  // !command — 直接执行终端命令
441
439
  if (trimmed.startsWith('!')) {
442
440
  const cmd = trimmed.slice(1).trim();
443
441
  if (!cmd) {
444
- process.stdout.write(`${C_DIM}!<命令> 执行终端命令, 如 !ls -la${RESET}\n`);
442
+ appendLine(`${C_DIM}!<命令> 执行终端命令, 如 !ls -la${RESET}`);
445
443
  return;
446
444
  }
447
- process.stdout.write(`${C_DIM}── $ ${cmd}${RESET}\n`);
445
+ appendLine(`${C_DIM}── $ ${cmd}${RESET}`);
448
446
  try {
449
447
  const { execSync } = await import('child_process');
450
448
  const out = execSync(cmd, { timeout: 30000, encoding: 'utf-8', cwd: process.cwd() });
451
- process.stdout.write(`${C_DIM}${out || '(无输出)'}${RESET}`);
449
+ appendLine(`${C_DIM}${out || '(无输出)'}${RESET}`);
452
450
  }
453
451
  catch (e) {
454
- process.stdout.write(`${C_ERROR}${e.stderr || e.message}${RESET}\n`);
452
+ appendLine(`${C_ERROR}${e.stderr || e.message}${RESET}`);
455
453
  }
456
- process.stdout.write(`${C_DIM}──${RESET}\n`);
454
+ appendLine(`${C_DIM}──${RESET}`);
457
455
  return;
458
456
  }
459
457
  // /queue — 切换队列模式
460
458
  if (trimmed.toLowerCase() === '/queue') {
461
459
  queueMode = !queueMode;
462
- process.stdout.write(`${C_WARN}队列 ${queueMode ? '开启' : '关闭'}${RESET} (${pendingQueue.length} 条)\n`);
460
+ appendLine(`${C_WARN}队列 ${queueMode ? '开启' : '关闭'}${RESET} (${pendingQueue.length} 条)`);
463
461
  return;
464
462
  }
465
463
  // /dequeue — 出队一条
466
464
  if (trimmed.toLowerCase() === '/dequeue' || trimmed.toLowerCase() === '/dq') {
467
465
  const next = pendingQueue.shift();
468
466
  if (next)
469
- process.stdout.write(`${C_WARN}出队:${RESET} ${next}\n`);
467
+ appendLine(`${C_WARN}出队:${RESET} ${next}`);
470
468
  else
471
- process.stdout.write(`${C_DIM}队列为空${RESET}\n`);
469
+ appendLine(`${C_DIM}队列为空${RESET}`);
472
470
  return;
473
471
  }
474
472
  // 队列模式: 入队
475
473
  if (queueMode) {
476
474
  pendingQueue.push(trimmed);
477
- process.stdout.write(`${C_WARN}[${pendingQueue.length}]${RESET} 已入队\n`);
475
+ appendLine(`${C_WARN}[${pendingQueue.length}]${RESET} 已入队`);
478
476
  return;
479
477
  }
480
478
  // 队列非空: 也入队末尾 (排队执行)
481
479
  if (pendingQueue.length > 0) {
482
480
  pendingQueue.push(trimmed);
483
- process.stdout.write(`${C_WARN}[队列 ${pendingQueue.length}]${RESET} 已入队, 执行完当前后自动运行\n`);
481
+ appendLine(`${C_WARN}[队列 ${pendingQueue.length}]${RESET} 已入队, 执行完当前后自动运行`);
484
482
  return;
485
483
  }
486
484
  if (trimmed.toLowerCase() === '/help' || trimmed === 'help') {
487
- process.stdout.write(`${C_DIM}命令:${RESET}\n`);
488
- process.stdout.write(` ${C_ACCENT}!<cmd>${RESET} 执行终端命令 ${C_DIM}如 !ls -la${RESET}\n`);
489
- process.stdout.write(` ${C_ACCENT}/queue${RESET} 切换队列模式 ${C_DIM}输入排队, 当前结束后自动执行${RESET}\n`);
490
- process.stdout.write(` ${C_ACCENT}/dequeue${RESET} 出队一条\n`);
491
- process.stdout.write(` ${C_ACCENT}peers${RESET} 查看 P2P 节点\n`);
492
- process.stdout.write(` ${C_ACCENT}iroh${RESET} 查看 iroh 状态\n`);
493
- process.stdout.write(` ${C_ACCENT}add_friend${RESET} 添加好友\n`);
494
- process.stdout.write(` ${C_ACCENT}exit${RESET} 退出\n`);
485
+ appendLine(`${C_DIM}命令:${RESET}`);
486
+ appendLine(` ${C_ACCENT}!<cmd>${RESET} 执行终端命令 ${C_DIM}如 !ls -la${RESET}`);
487
+ appendLine(` ${C_ACCENT}/queue${RESET} 切换队列模式 ${C_DIM}输入排队, 当前结束后自动执行${RESET}`);
488
+ appendLine(` ${C_ACCENT}/dequeue${RESET} 出队一条`);
489
+ appendLine(` ${C_ACCENT}peers${RESET} 查看 P2P 节点`);
490
+ appendLine(` ${C_ACCENT}iroh${RESET} 查看 iroh 状态`);
491
+ appendLine(` ${C_ACCENT}add_friend${RESET} 添加好友`);
492
+ appendLine(` ${C_ACCENT}exit${RESET} 退出`);
495
493
  return;
496
494
  }
497
495
  if (trimmed === '退出' || trimmed === 'exit' || trimmed === 'quit') {
498
- process.stdout.write(`\n${CYAN}👋 再见!${RESET}\n`);
496
+ appendLine(`\n${CYAN}👋 再见!${RESET}`);
499
497
  isRunning = false;
500
498
  return;
501
499
  }
502
500
  if (trimmed.toLowerCase() === 'peers') {
503
501
  const peers = comm.getConnections();
504
- process.stdout.write(`${GRAY}已连接节点: ${peers.length}${RESET}\n`);
502
+ appendLine(`${GRAY}已连接节点: ${peers.length}${RESET}`);
505
503
  for (const c of peers) {
506
- process.stdout.write(` ${GRAY}·${RESET} ${c.publicKey.substring(0, 16)}...\n`);
504
+ appendLine(` ${GRAY}·${RESET} ${c.publicKey.substring(0, 16)}...`);
507
505
  }
508
506
  return;
509
507
  }
@@ -511,25 +509,25 @@ async function processInput(input, comm) {
511
509
  const nodeId = irohTransport.getNodeId();
512
510
  const running = irohTransport.isRunning();
513
511
  const peers = irohTransport.getPeers();
514
- process.stdout.write(`${GRAY}iroh 状态:${RESET}\n`);
515
- process.stdout.write(` ${GRAY}运行中:${RESET} ${running ? '是' : '否'}\n`);
516
- process.stdout.write(` ${GRAY}Node ID:${RESET} ${nodeId ? nodeId.substring(0, 24) + '...' : 'N/A'}\n`);
517
- process.stdout.write(` ${GRAY}已知节点:${RESET} ${peers.length}\n`);
512
+ appendLine(`${GRAY}iroh 状态:${RESET}`);
513
+ appendLine(` ${GRAY}运行中:${RESET} ${running ? '是' : '否'}`);
514
+ appendLine(` ${GRAY}Node ID:${RESET} ${nodeId ? nodeId.substring(0, 24) + '...' : 'N/A'}`);
515
+ appendLine(` ${GRAY}已知节点:${RESET} ${peers.length}`);
518
516
  if (hybridMessenger) {
519
- process.stdout.write(` ${GRAY}HybridMessenger:${RESET} 就绪\n`);
517
+ appendLine(` ${GRAY}HybridMessenger:${RESET} 就绪`);
520
518
  }
521
519
  return;
522
520
  }
523
521
  if (trimmed.toLowerCase().startsWith('add_friend ') || trimmed.toLowerCase() === 'add_friend') {
524
522
  const parts = trimmed.split(/\s+/);
525
523
  if (parts.length < 2 || (parts.length === 2 && parts[1].length !== 64)) {
526
- process.stdout.write(`${GRAY}用法: add_friend <64字符hex publicKey> [备注名]\n${RESET}`);
527
- process.stdout.write(`${GRAY}示例: add_friend a1b2c3d4e5f6... 同事-张磊\n${RESET}`);
524
+ appendLine(`${GRAY}用法: add_friend <64字符hex publicKey> [备注名]\n${RESET}`);
525
+ appendLine(`${GRAY}示例: add_friend a1b2c3d4e5f6... 同事-张磊\n${RESET}`);
528
526
  return;
529
527
  }
530
528
  const pk = parts[1];
531
529
  const name = parts.slice(2).join(' ') || '';
532
- process.stdout.write(`${GRAY}正在发送好友申请给 ${pk.substring(0, 16)}...${RESET}\n`);
530
+ appendLine(`${GRAY}正在发送好友申请给 ${pk.substring(0, 16)}...${RESET}`);
533
531
  try {
534
532
  const port = process.env.PORT || '54188';
535
533
  const res = await fetch(`http://127.0.0.1:${port}/api/friend-request`, {
@@ -540,78 +538,77 @@ async function processInput(input, comm) {
540
538
  const data = await res.json();
541
539
  if (!res.ok) {
542
540
  const reason = data.code === 'NO_CONN' ? '对方未在线, 已本地记住, 等对方上线后自动重连' : (data.error || '请求失败');
543
- process.stdout.write(`${MAGENTA}✗ 添加好友失败: ${reason}${RESET}\n`);
541
+ appendLine(`${MAGENTA}✗ 添加好友失败: ${reason}${RESET}`);
544
542
  if (data.persistedAs)
545
- process.stdout.write(`${GRAY}本地已保存为: ${data.persistedAs}${RESET}\n`);
543
+ appendLine(`${GRAY}本地已保存为: ${data.persistedAs}${RESET}`);
546
544
  }
547
545
  else {
548
- process.stdout.write(`${GREEN}✓ 好友申请已发送给 ${data.persistedAs || name || pk.substring(0, 12)}...${RESET}\n`);
546
+ appendLine(`${GREEN}✓ 好友申请已发送给 ${data.persistedAs || name || pk.substring(0, 12)}...${RESET}`);
549
547
  }
550
548
  }
551
549
  catch (err) {
552
- process.stdout.write(`${MAGENTA}✗ 添加好友失败: ${err.message || String(err)}${RESET}\n`);
550
+ appendLine(`${MAGENTA}✗ 添加好友失败: ${err.message || String(err)}${RESET}`);
553
551
  }
554
552
  return;
555
553
  }
556
554
  try {
557
555
  // 双横线分割
558
- process.stdout.write(`${C_DIM}${'─'.repeat(8)} · ${'─'.repeat(8)}${RESET}\n`);
559
- // 已发送消息框
560
- process.stdout.write(renderUserMessage(trimmed) + '\n');
556
+ appendLine(`${C_DIM}${'─'.repeat(8)} · ${'─'.repeat(8)}${RESET}`);
557
+ appendLine(renderUserMessage(trimmed));
558
+ // 启动思考动画
559
+ inkSetThinking(true);
561
560
  const a = await getAgent();
562
561
  const boxW = Math.min(termWidth() - 2, 76);
563
- const pending = [];
564
- let firstEvent = true;
565
- const clearThinking = () => {
566
- if (firstEvent) {
567
- process.stdout.write('\r' + ' '.repeat(30) + '\r');
568
- firstEvent = false;
569
- }
570
- };
571
- const onStream = (e) => {
572
- if (e.type === 'step_start') {
573
- pending.push({ tool: e.tool, args: e.args, t0: Date.now() });
574
- }
575
- else if (e.type === 'step_done' || e.type === 'step_error') {
576
- const p = pending.shift();
577
- clearThinking();
578
- // 连接线只在第 2 个及之后的工具框前出现
579
- if (!firstEvent)
580
- process.stdout.write(flowConnector(boxW) + '\n');
581
- process.stdout.write(renderToolCall({
582
- tool: e.tool ?? p?.tool ?? '?',
583
- args: p?.args,
584
- status: e.type === 'step_done' ? 'ok' : 'error',
585
- output: e.output,
586
- error: e.error,
587
- durationMs: p ? Date.now() - p.t0 : undefined,
588
- width: boxW,
589
- }) + '\n');
562
+ // 工具调用显示由 tui-shell 的 onStream handler 处理
563
+ const response = await a.prompt(trimmed, {
564
+ onStream: (e) => {
565
+ if (e.type === 'step_start') {
566
+ tuiToolCounter++;
567
+ tuiToolCalls.push({ tool: e.tool || '?', args: e.args, _t: Date.now() });
568
+ }
569
+ else if (e.type === 'step_done' || e.type === 'step_error') {
570
+ const p = tuiToolCalls.shift();
571
+ const doneItem = {
572
+ tool: e.tool ?? (p?.tool ?? '?'),
573
+ args: p?.args,
574
+ status: e.type === 'step_done' ? 'ok' : 'error',
575
+ output: e.output,
576
+ error: e.error,
577
+ durationMs: p ? Date.now() - p._t : undefined,
578
+ };
579
+ appendLine(renderToolCallListItem(doneItem, tuiToolCalls.length + 1, tuiToolCounter));
580
+ }
590
581
  }
591
- };
592
- const response = await a.prompt(trimmed, { onStream });
593
- clearThinking();
594
- // 智能体回复框 (圆角)
595
- process.stdout.write(renderAgentMessage(response) + '\n');
596
- // 更新底部状态栏: 上下文进度
582
+ });
583
+ // 智能体回复框
584
+ appendLine(renderAgentMessage(response));
585
+ // 停止思考动画
586
+ inkSetThinking(false);
587
+ // 更新状态栏: 上下文进度
597
588
  try {
598
- // 用 messageHistory 长度推断上下文占用
599
589
  const msgLen = JSON.stringify(a.messageHistory ?? []).length;
600
590
  cliContextPct = Math.min(100, Math.round((msgLen / 240_000) * 100));
591
+ const dur = Math.floor((Date.now() - cliStartTime) / 1000);
592
+ const barLen = 12;
593
+ const filled = Math.round((cliContextPct / 100) * barLen);
594
+ const bar = '█'.repeat(filled) + '░'.repeat(barLen - filled);
595
+ const statusText = `${C_ACCENT}${cliModelName}\x1b[0m\x1b[90m │\x1b[0m ${cliAgentName} \x1b[90m│\x1b[0m ⏱ ${dur}s \x1b[90m│\x1b[0m ${msgLen.toLocaleString()}B/240K │ ${bar} ${cliContextPct}%`;
596
+ inkSetStatus(statusText);
601
597
  }
602
598
  catch { /* 降级容忍 */ }
603
- // 双横线分隔 + 自动消费队列
604
- process.stdout.write(`${C_DIM}${'─'.repeat(8)} · ${'─'.repeat(8)}${RESET}\n`);
599
+ // 自动消费队列
605
600
  if (pendingQueue.length > 0) {
606
601
  const next = pendingQueue.shift();
607
- process.stdout.write(`${C_WARN}⏩ 自动执行队列 [${pendingQueue.length + 1}/${pendingQueue.length + 1}]${RESET}\n`);
602
+ appendLine(`${C_WARN}⏩ 自动执行队列 [${pendingQueue.length + 1}/${pendingQueue.length + 1}]${RESET}`);
608
603
  await processInput(next, comm);
609
604
  return;
610
605
  }
606
+ inkSetThinking(false);
611
607
  }
612
608
  catch (e) {
609
+ inkSetThinking(false);
613
610
  if (!e.message?.includes('ERR_USE_AFTER_CLOSE') && !e.message?.includes('write after end')) {
614
- process.stdout.write(`${MAGENTA}❌ ${e.message}${RESET}\n`);
611
+ appendLine(`${MAGENTA}❌ ${e.message}${RESET}`);
615
612
  }
616
613
  }
617
614
  }
@@ -855,7 +852,7 @@ async function runToolCommand(tool, args, outputJson, comm, model, prompt) {
855
852
  catch { }
856
853
  break;
857
854
  }
858
- process.stdout.write(`⏳ 任务已派给 ${targetPk.slice(0, 12)}..., 等回复 (最多 90s)...\n`);
855
+ appendLine(`⏳ 任务已派给 ${targetPk.slice(0, 12)}..., 等回复 (最多 90s)...`);
859
856
  const reply = await replyPromise;
860
857
  const lines = [
861
858
  `✅ 协作完成 (${reply.durationMs ? Math.round(reply.durationMs / 1000) + 's' : '?'})`,
@@ -1421,8 +1418,8 @@ async function runToolCommand(tool, args, outputJson, comm, model, prompt) {
1421
1418
  const { P2PDirect } = await import('./network/p2p-direct.js');
1422
1419
  const id = await resolveIdentity();
1423
1420
  const p2p = new P2PDirect({ name: 'cli-listen', role: id.role });
1424
- process.stdout.write(`[chat-p2p-listen] role=${id.role} pk=${id.publicKey.slice(0, 12)} listening on bolloon-agent-harness\n`);
1425
- process.stdout.write(`[chat-p2p-listen] press Ctrl-C to stop\n`);
1421
+ appendLine(`[chat-p2p-listen] role=${id.role} pk=${id.publicKey.slice(0, 12)} listening on bolloon-agent-harness`);
1422
+ appendLine(`[chat-p2p-listen] press Ctrl-C to stop`);
1426
1423
  const onData = (ev) => {
1427
1424
  try {
1428
1425
  const text = Buffer.isBuffer(ev.data) ? ev.data.toString('utf8') : String(ev.data);
@@ -1431,15 +1428,15 @@ async function runToolCommand(tool, args, outputJson, comm, model, prompt) {
1431
1428
  if (env && env.v === 3 && env.op === 'agent.chat.direct') {
1432
1429
  const { text: body, fromRole } = env.payload || {};
1433
1430
  const ts = (env.payload?.ts || new Date().toISOString()).replace('T', ' ').replace(/\.\d+Z$/, '');
1434
- process.stdout.write(`\n[${ts} ${fromRole || ev.fromPublicKey?.slice(0, 12)} → me] ${body}\n> `);
1431
+ appendLine(`\n[${ts} ${fromRole || ev.fromPublicKey?.slice(0, 12)} → me] ${body}\n> `);
1435
1432
  return;
1436
1433
  }
1437
1434
  }
1438
1435
  catch { /* 非 v3 envelope, 当 raw 显示 */ }
1439
- process.stdout.write(`\n[raw ${ev.fromPublicKey?.slice(0, 12)}] ${text.slice(0, 200)}\n> `);
1436
+ appendLine(`\n[raw ${ev.fromPublicKey?.slice(0, 12)}] ${text.slice(0, 200)}\n> `);
1440
1437
  }
1441
1438
  catch (e) {
1442
- process.stdout.write(`[chat-p2p-listen] decode error: ${e?.message ?? e}\n`);
1439
+ appendLine(`[chat-p2p-listen] decode error: ${e?.message ?? e}`);
1443
1440
  }
1444
1441
  };
1445
1442
  p2p.on('data', onData);
@@ -1447,12 +1444,12 @@ async function runToolCommand(tool, args, outputJson, comm, model, prompt) {
1447
1444
  const keepAlive = setInterval(() => {
1448
1445
  const now = Date.now();
1449
1446
  if (now - lastPing > 5 * 60_000) {
1450
- process.stdout.write(`[chat-p2p-listen] alive, role=${id.role}\n`);
1447
+ appendLine(`[chat-p2p-listen] alive, role=${id.role}`);
1451
1448
  lastPing = now;
1452
1449
  }
1453
1450
  }, 30_000);
1454
1451
  const stop = async () => {
1455
- process.stdout.write(`\n[chat-p2p-listen] stopping...\n`);
1452
+ appendLine(`\n[chat-p2p-listen] stopping...`);
1456
1453
  try {
1457
1454
  p2p.off('data', onData);
1458
1455
  }
@@ -1469,7 +1466,7 @@ async function runToolCommand(tool, args, outputJson, comm, model, prompt) {
1469
1466
  process.on('SIGHUP', stop);
1470
1467
  await p2p.start();
1471
1468
  await p2p.joinTopic(Buffer.from('bolloon-agent-harness'));
1472
- process.stdout.write(`[chat-p2p-listen] joined topic ✓\n> `);
1469
+ appendLine(`[chat-p2p-listen] joined topic ✓\n> `);
1473
1470
  await new Promise(() => { });
1474
1471
  break;
1475
1472
  }
@@ -2149,7 +2146,7 @@ async function main() {
2149
2146
  console.log = originalLog;
2150
2147
  console.info = originalInfo;
2151
2148
  process.stdout.write = originalStdoutWrite;
2152
- startCLI(comm);
2149
+ await startCLI(comm);
2153
2150
  }
2154
2151
  }
2155
2152
  catch (e) {
package/dist/llm/pi-ai.js CHANGED
@@ -169,23 +169,11 @@ export class PiAIModel {
169
169
  }
170
170
  let openaiTools;
171
171
  if (tools && tools.length > 0) {
172
- try {
173
- const { getToolManifest, formatForPrompt, formatForOpenAI } = await import('./tool-manifest/index.js');
174
- const manifests = tools
175
- .map((id) => getToolManifest(id))
176
- .filter((m) => m !== undefined);
177
- if (manifests.length > 0) {
178
- const toolPrompt = formatForPrompt(manifests);
179
- finalMessages = [
180
- { role: 'system', content: toolPrompt },
181
- ...messages,
182
- ];
183
- // Bug 3: 从 manifests 生成原生 OpenAI tools 格式
184
- openaiTools = formatForOpenAI(manifests);
185
- }
186
- }
187
- catch (err) {
188
- console.warn('[pi-ai] tool-manifest 加载失败:', err.message?.slice(0, 100));
172
+ // 预格式化的 tools (含参数 schema) → 直接使用
173
+ if (typeof tools[0] === 'object' && tools[0]?.type === 'function') {
174
+ openaiTools = tools;
175
+ const toolDescriptions = tools.map(t => `- ${t.function.name}: ${t.function.description || ''} ${Object.keys(t.function.parameters?.properties || {}).length > 0 ? `(${Object.keys(t.function.parameters.properties).join(', ')})` : ''}`).join('\n');
176
+ finalMessages = [{ role: 'system', content: `可用工具:\n${toolDescriptions}` }, ...messages];
189
177
  }
190
178
  }
191
179
  switch (this.provider) {
@@ -38,8 +38,16 @@ const TOOL_WHITELIST = new Set([
38
38
  'safe_deploy',
39
39
  // MCP 注册的工具
40
40
  'mcp_tool',
41
+ // 2026-07-29: 同步 pi-sdk-tools.ts 注册的全部工具
42
+ 'read_directory', 'add_friend_by_id', 'delegate_to_engine',
43
+ 'set_persona', 'get_operation_logs', 'park_goal',
44
+ 'list_channels', 'list_local_channels',
41
45
  ]);
42
46
  export const gateWhitelist = { gate: 'whitelist', allowed: true };
47
+ /**
48
+ * @deprecated 不再被 TOOL_GATES 调用 (2026-07-29). 保留仅供测试直接引用.
49
+ * 工具准入由 `tools` 参数 (OpenAI 原生格式) 控制, 不再需要第二层白名单.
50
+ */
43
51
  export function checkWhitelist(ctx) {
44
52
  if (TOOL_WHITELIST.has(ctx.tool)) {
45
53
  return gateWhitelist;
@@ -217,7 +225,6 @@ export function checkBlacklist(ctx) {
217
225
  return { gate: 'blacklist', allowed: true };
218
226
  }
219
227
  const TOOL_GATES = [
220
- checkWhitelist,
221
228
  checkSchema,
222
229
  checkChannel,
223
230
  checkRate,