@bolloon/bolloon-agent 0.3.23 → 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.
- package/dist/cli/ink-app.js +116 -0
- package/dist/index.js +121 -142
- package/package.json +4 -2
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ink-app.tsx — Ink (React for CLI) 渲染入口
|
|
4
|
+
*
|
|
5
|
+
* 用 Yoga flexbox 布局实现: 内容置顶, 输入栏固定底部, 状态栏固定
|
|
6
|
+
*/
|
|
7
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
8
|
+
import { render, Box, Text, useInput, useApp } from 'ink';
|
|
9
|
+
import TextInput from 'ink-text-input';
|
|
10
|
+
import { brandArtLines, boxTop, boxRow, boxBottom, dispWidth } from './loading-tui.js';
|
|
11
|
+
// ─── 组件: Logo Box ──────────────────────────────────────────────────────────
|
|
12
|
+
const LogoBox = ({ width }) => {
|
|
13
|
+
const art = brandArtLines();
|
|
14
|
+
const mw = Math.max(40, ...art.map(l => dispWidth(l))) + 4;
|
|
15
|
+
const bw = Math.min(width - 2, mw);
|
|
16
|
+
const rows = [boxTop('Bolloon Agent', bw)];
|
|
17
|
+
for (const l of art)
|
|
18
|
+
rows.push(boxRow(l, bw, 'center'));
|
|
19
|
+
rows.push(boxBottom(bw));
|
|
20
|
+
return (_jsx(Box, { flexDirection: "column", children: rows.map((r, i) => _jsx(Text, { children: r }, i)) }));
|
|
21
|
+
};
|
|
22
|
+
// ─── 组件: 消息列表 ──────────────────────────────────────────────────────────
|
|
23
|
+
const Messages = ({ msgs }) => (_jsx(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "flex-start", children: msgs.map((m, i) => {
|
|
24
|
+
const clean = m.replace(/\x1b\[[0-9;]*m/g, '');
|
|
25
|
+
return clean.trim() ? _jsx(Text, { children: clean ? m : '' }, i) : null;
|
|
26
|
+
}) }));
|
|
27
|
+
const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH }) => {
|
|
28
|
+
const [input, setInput] = useState('');
|
|
29
|
+
const [msgs, setMsgs] = useState([]);
|
|
30
|
+
const [status, setStatus] = useState(initialStatus);
|
|
31
|
+
const { exit } = useApp();
|
|
32
|
+
const [thinking, setThinking] = useState(false);
|
|
33
|
+
const thinkingIdx = useRef(0);
|
|
34
|
+
// 全局: 思考动画控制
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
globalThis.__inkSetThinking = (v) => setThinking(v);
|
|
37
|
+
globalThis.__inkAppend = (line) => {
|
|
38
|
+
setMsgs(prev => [...prev, line]);
|
|
39
|
+
};
|
|
40
|
+
globalThis.__inkSetStatus = (s) => {
|
|
41
|
+
setStatus(s);
|
|
42
|
+
};
|
|
43
|
+
return () => {
|
|
44
|
+
delete globalThis.__inkAppend;
|
|
45
|
+
delete globalThis.__inkSetStatus;
|
|
46
|
+
delete globalThis.__inkSetThinking;
|
|
47
|
+
};
|
|
48
|
+
}, []);
|
|
49
|
+
const onSubmit = useCallback((value) => {
|
|
50
|
+
const trimmed = value.trim();
|
|
51
|
+
if (!trimmed)
|
|
52
|
+
return;
|
|
53
|
+
setInput('');
|
|
54
|
+
// 用户消息由 processInput 统一通过 appendLine(renderUserMessage) 显示
|
|
55
|
+
onPrompt(trimmed);
|
|
56
|
+
}, [onPrompt]);
|
|
57
|
+
useInput((_input, key) => {
|
|
58
|
+
if (key.ctrl && _input === 'c')
|
|
59
|
+
exit();
|
|
60
|
+
// TextInput handles actual input; useInput only for Ctrl+C
|
|
61
|
+
});
|
|
62
|
+
// 自动更新状态栏 (每秒)
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const timer = setInterval(() => {
|
|
65
|
+
const s = getStatusUpdate();
|
|
66
|
+
if (s)
|
|
67
|
+
setStatus(s);
|
|
68
|
+
}, 1000);
|
|
69
|
+
return () => clearInterval(timer);
|
|
70
|
+
}, [getStatusUpdate]);
|
|
71
|
+
// 思考动画 — kaomoji 旋转
|
|
72
|
+
const KAOMOJI = ['(`・ω・´)', '(´・_・`)', '(。•́︿•̀。)', 'ᕙ(▀̿̿Ĺ̯̿̿▀̿ ̿)ᕗ', '(◕‿◕)'];
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!thinking)
|
|
75
|
+
return;
|
|
76
|
+
const timer = setInterval(() => {
|
|
77
|
+
thinkingIdx.current = (thinkingIdx.current + 1) % KAOMOJI.length;
|
|
78
|
+
}, 600);
|
|
79
|
+
return () => clearInterval(timer);
|
|
80
|
+
}, [thinking]);
|
|
81
|
+
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsxs(Box, { flexGrow: 1, flexDirection: "column", justifyContent: "flex-start", children: [_jsx(LogoBox, { width: terminalW }), _jsx(Messages, { msgs: msgs }), thinking && (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: [KAOMOJI[thinkingIdx.current], " \u601D\u8003\u4E2D..."] }) }))] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: "white", children: '─'.repeat(terminalW) }) }), _jsx(Box, { children: _jsx(Text, { children: status }) }), _jsx(Box, { children: _jsx(Text, { bold: true, color: "white", children: '─'.repeat(terminalW) }) }), _jsxs(Box, { children: [_jsx(Text, { bold: true, color: "green", children: "\u276F " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: "\u8F93\u5165\u6D88\u606F..." })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: "white", children: '─'.repeat(terminalW) }) })] }));
|
|
82
|
+
};
|
|
83
|
+
// ─── 启动 ────────────────────────────────────────────────────────────────────
|
|
84
|
+
let _inkInstance = null;
|
|
85
|
+
export function startInk(onPrompt, initialStatus, getStatusUpdate) {
|
|
86
|
+
const tw = process.stdout.columns || 80;
|
|
87
|
+
const th = process.stdout.rows || 24;
|
|
88
|
+
_inkInstance = render(_jsx(InkApp, { onPrompt: onPrompt, initialStatus: initialStatus, getStatusUpdate: getStatusUpdate, terminalW: tw, terminalH: th }), {
|
|
89
|
+
stdout: process.stdout,
|
|
90
|
+
stdin: process.stdin,
|
|
91
|
+
exitOnCtrlC: false,
|
|
92
|
+
patchConsole: false, // 关键: 阻止 Ink 劫持 console.log
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
export function stopInk() {
|
|
96
|
+
if (_inkInstance) {
|
|
97
|
+
_inkInstance.unmount();
|
|
98
|
+
_inkInstance.clear();
|
|
99
|
+
_inkInstance = null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function inkAppendLine(line) {
|
|
103
|
+
const fn = globalThis.__inkAppend;
|
|
104
|
+
if (fn)
|
|
105
|
+
fn(line);
|
|
106
|
+
}
|
|
107
|
+
export function inkSetStatus(s) {
|
|
108
|
+
const fn = globalThis.__inkSetStatus;
|
|
109
|
+
if (fn)
|
|
110
|
+
fn(s);
|
|
111
|
+
}
|
|
112
|
+
export function inkSetThinking(v) {
|
|
113
|
+
const fn = globalThis.__inkSetThinking;
|
|
114
|
+
if (fn)
|
|
115
|
+
fn(v);
|
|
116
|
+
}
|
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, renderToolCallListItem,
|
|
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);
|
|
@@ -94,12 +95,12 @@ const s = {
|
|
|
94
95
|
let i = 0;
|
|
95
96
|
let dots = 0;
|
|
96
97
|
const frame = frames[0];
|
|
97
|
-
|
|
98
|
+
appendLine(` ${frame} 思考...`);
|
|
98
99
|
return setInterval(() => {
|
|
99
100
|
i = (i + 1) % frames.length;
|
|
100
101
|
dots = (dots + 1) % 4;
|
|
101
102
|
const dotStr = '.'.repeat(dots || 1);
|
|
102
|
-
|
|
103
|
+
appendLine(`\r ${frames[i]} 思考${dotStr} `);
|
|
103
104
|
}, 600);
|
|
104
105
|
},
|
|
105
106
|
clearThinking: (interval) => {
|
|
@@ -185,7 +186,7 @@ function publishDID(name, kp) {
|
|
|
185
186
|
}
|
|
186
187
|
catch (e) {
|
|
187
188
|
// 一次失败直接放弃 — 本地模式运行就够了, 不重试
|
|
188
|
-
|
|
189
|
+
appendLine(` ${YELLOW}⚠ IPFS 发布失败 (${e?.message?.slice(0, 80) || 'unknown'}), 本地模式运行${RESET}`);
|
|
189
190
|
s.step(2, 5, '发布 DID → IPFS', 'warn');
|
|
190
191
|
resolve({});
|
|
191
192
|
}
|
|
@@ -353,13 +354,12 @@ function rpcErr(code, msg) {
|
|
|
353
354
|
// CLI with persistent bottom prompt
|
|
354
355
|
// 2026-07-28: 改用 readline.createInterface + replReadline 循环
|
|
355
356
|
let isRunning = false;
|
|
357
|
+
let cliContextPct = 0;
|
|
356
358
|
let queueMode = false;
|
|
357
359
|
const pendingQueue = [];
|
|
358
|
-
// 底部状态栏数据
|
|
359
360
|
let cliStartTime = 0;
|
|
360
361
|
let cliModelName = '…';
|
|
361
362
|
let cliAgentName = '…';
|
|
362
|
-
let cliContextPct = 0;
|
|
363
363
|
function fmtDuration(ms) {
|
|
364
364
|
const s = Math.floor(ms / 1000);
|
|
365
365
|
const m = Math.floor(s / 60);
|
|
@@ -377,16 +377,21 @@ function statusBarLine() {
|
|
|
377
377
|
const bar = C_OK + '█'.repeat(filled) + C_DIM + '░'.repeat(barLen - filled) + RESET;
|
|
378
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}`;
|
|
379
379
|
}
|
|
380
|
-
function startCLI(comm) {
|
|
380
|
+
async function startCLI(comm) {
|
|
381
381
|
isRunning = true;
|
|
382
|
-
// CLI
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
+
});
|
|
390
395
|
let peerCount = 0;
|
|
391
396
|
try {
|
|
392
397
|
peerCount = comm.getConnections().length;
|
|
@@ -407,111 +412,96 @@ function startCLI(comm) {
|
|
|
407
412
|
cliModelName = foundProvider ? foundProvider[1] : '未配置';
|
|
408
413
|
cliAgentName = agentIdentity?.name || 'bolloon';
|
|
409
414
|
cliStartTime = Date.now();
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const SEP = '\x1b[90m─';
|
|
425
|
-
const RST = '\x1b[0m';
|
|
426
|
-
while (isRunning) {
|
|
427
|
-
const tw = process.stdout.columns || 80;
|
|
428
|
-
const sepLine = SEP.repeat(tw) + RST;
|
|
429
|
-
const prefix = queueMode ? `${C_WARN}▸${RST}` : `${C_ACCENT}❯${RST}`;
|
|
430
|
-
const raw = await new Promise(resolve => rl.question(`\n${sepLine}\n${statusBarLine()}\n${sepLine}\n${prefix} `, resolve));
|
|
431
|
-
const trimmed = raw.trim();
|
|
432
|
-
// 清除 readline echo 行, 避免与 renderUserMessage 重复
|
|
433
|
-
process.stdout.write('\r\x1b[K');
|
|
434
|
-
process.stdout.write(`\n${sepLine}\n\n\n\n\n`);
|
|
435
|
-
if (!trimmed)
|
|
436
|
-
continue;
|
|
437
|
-
if (!isRunning)
|
|
438
|
-
break;
|
|
439
|
-
await processInput(trimmed, comm);
|
|
440
|
-
}
|
|
441
|
-
rl.close();
|
|
442
|
-
process.stdout.write(`\n${CYAN}👋 再见!${RESET}\n`);
|
|
443
|
-
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}`);
|
|
444
429
|
comm.stop();
|
|
445
430
|
}
|
|
446
431
|
async function processInput(input, comm) {
|
|
447
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;
|
|
448
438
|
// !command — 直接执行终端命令
|
|
449
439
|
if (trimmed.startsWith('!')) {
|
|
450
440
|
const cmd = trimmed.slice(1).trim();
|
|
451
441
|
if (!cmd) {
|
|
452
|
-
|
|
442
|
+
appendLine(`${C_DIM}!<命令> 执行终端命令, 如 !ls -la${RESET}`);
|
|
453
443
|
return;
|
|
454
444
|
}
|
|
455
|
-
|
|
445
|
+
appendLine(`${C_DIM}── $ ${cmd}${RESET}`);
|
|
456
446
|
try {
|
|
457
447
|
const { execSync } = await import('child_process');
|
|
458
448
|
const out = execSync(cmd, { timeout: 30000, encoding: 'utf-8', cwd: process.cwd() });
|
|
459
|
-
|
|
449
|
+
appendLine(`${C_DIM}${out || '(无输出)'}${RESET}`);
|
|
460
450
|
}
|
|
461
451
|
catch (e) {
|
|
462
|
-
|
|
452
|
+
appendLine(`${C_ERROR}${e.stderr || e.message}${RESET}`);
|
|
463
453
|
}
|
|
464
|
-
|
|
454
|
+
appendLine(`${C_DIM}──${RESET}`);
|
|
465
455
|
return;
|
|
466
456
|
}
|
|
467
457
|
// /queue — 切换队列模式
|
|
468
458
|
if (trimmed.toLowerCase() === '/queue') {
|
|
469
459
|
queueMode = !queueMode;
|
|
470
|
-
|
|
460
|
+
appendLine(`${C_WARN}队列 ${queueMode ? '开启' : '关闭'}${RESET} (${pendingQueue.length} 条)`);
|
|
471
461
|
return;
|
|
472
462
|
}
|
|
473
463
|
// /dequeue — 出队一条
|
|
474
464
|
if (trimmed.toLowerCase() === '/dequeue' || trimmed.toLowerCase() === '/dq') {
|
|
475
465
|
const next = pendingQueue.shift();
|
|
476
466
|
if (next)
|
|
477
|
-
|
|
467
|
+
appendLine(`${C_WARN}出队:${RESET} ${next}`);
|
|
478
468
|
else
|
|
479
|
-
|
|
469
|
+
appendLine(`${C_DIM}队列为空${RESET}`);
|
|
480
470
|
return;
|
|
481
471
|
}
|
|
482
472
|
// 队列模式: 入队
|
|
483
473
|
if (queueMode) {
|
|
484
474
|
pendingQueue.push(trimmed);
|
|
485
|
-
|
|
475
|
+
appendLine(`${C_WARN}[${pendingQueue.length}]${RESET} 已入队`);
|
|
486
476
|
return;
|
|
487
477
|
}
|
|
488
478
|
// 队列非空: 也入队末尾 (排队执行)
|
|
489
479
|
if (pendingQueue.length > 0) {
|
|
490
480
|
pendingQueue.push(trimmed);
|
|
491
|
-
|
|
481
|
+
appendLine(`${C_WARN}[队列 ${pendingQueue.length}]${RESET} 已入队, 执行完当前后自动运行`);
|
|
492
482
|
return;
|
|
493
483
|
}
|
|
494
484
|
if (trimmed.toLowerCase() === '/help' || trimmed === 'help') {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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} 退出`);
|
|
503
493
|
return;
|
|
504
494
|
}
|
|
505
495
|
if (trimmed === '退出' || trimmed === 'exit' || trimmed === 'quit') {
|
|
506
|
-
|
|
496
|
+
appendLine(`\n${CYAN}👋 再见!${RESET}`);
|
|
507
497
|
isRunning = false;
|
|
508
498
|
return;
|
|
509
499
|
}
|
|
510
500
|
if (trimmed.toLowerCase() === 'peers') {
|
|
511
501
|
const peers = comm.getConnections();
|
|
512
|
-
|
|
502
|
+
appendLine(`${GRAY}已连接节点: ${peers.length}${RESET}`);
|
|
513
503
|
for (const c of peers) {
|
|
514
|
-
|
|
504
|
+
appendLine(` ${GRAY}·${RESET} ${c.publicKey.substring(0, 16)}...`);
|
|
515
505
|
}
|
|
516
506
|
return;
|
|
517
507
|
}
|
|
@@ -519,25 +509,25 @@ async function processInput(input, comm) {
|
|
|
519
509
|
const nodeId = irohTransport.getNodeId();
|
|
520
510
|
const running = irohTransport.isRunning();
|
|
521
511
|
const peers = irohTransport.getPeers();
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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}`);
|
|
526
516
|
if (hybridMessenger) {
|
|
527
|
-
|
|
517
|
+
appendLine(` ${GRAY}HybridMessenger:${RESET} 就绪`);
|
|
528
518
|
}
|
|
529
519
|
return;
|
|
530
520
|
}
|
|
531
521
|
if (trimmed.toLowerCase().startsWith('add_friend ') || trimmed.toLowerCase() === 'add_friend') {
|
|
532
522
|
const parts = trimmed.split(/\s+/);
|
|
533
523
|
if (parts.length < 2 || (parts.length === 2 && parts[1].length !== 64)) {
|
|
534
|
-
|
|
535
|
-
|
|
524
|
+
appendLine(`${GRAY}用法: add_friend <64字符hex publicKey> [备注名]\n${RESET}`);
|
|
525
|
+
appendLine(`${GRAY}示例: add_friend a1b2c3d4e5f6... 同事-张磊\n${RESET}`);
|
|
536
526
|
return;
|
|
537
527
|
}
|
|
538
528
|
const pk = parts[1];
|
|
539
529
|
const name = parts.slice(2).join(' ') || '';
|
|
540
|
-
|
|
530
|
+
appendLine(`${GRAY}正在发送好友申请给 ${pk.substring(0, 16)}...${RESET}`);
|
|
541
531
|
try {
|
|
542
532
|
const port = process.env.PORT || '54188';
|
|
543
533
|
const res = await fetch(`http://127.0.0.1:${port}/api/friend-request`, {
|
|
@@ -548,88 +538,77 @@ async function processInput(input, comm) {
|
|
|
548
538
|
const data = await res.json();
|
|
549
539
|
if (!res.ok) {
|
|
550
540
|
const reason = data.code === 'NO_CONN' ? '对方未在线, 已本地记住, 等对方上线后自动重连' : (data.error || '请求失败');
|
|
551
|
-
|
|
541
|
+
appendLine(`${MAGENTA}✗ 添加好友失败: ${reason}${RESET}`);
|
|
552
542
|
if (data.persistedAs)
|
|
553
|
-
|
|
543
|
+
appendLine(`${GRAY}本地已保存为: ${data.persistedAs}${RESET}`);
|
|
554
544
|
}
|
|
555
545
|
else {
|
|
556
|
-
|
|
546
|
+
appendLine(`${GREEN}✓ 好友申请已发送给 ${data.persistedAs || name || pk.substring(0, 12)}...${RESET}`);
|
|
557
547
|
}
|
|
558
548
|
}
|
|
559
549
|
catch (err) {
|
|
560
|
-
|
|
550
|
+
appendLine(`${MAGENTA}✗ 添加好友失败: ${err.message || String(err)}${RESET}`);
|
|
561
551
|
}
|
|
562
552
|
return;
|
|
563
553
|
}
|
|
564
554
|
try {
|
|
565
555
|
// 双横线分割
|
|
566
|
-
|
|
567
|
-
|
|
556
|
+
appendLine(`${C_DIM}${'─'.repeat(8)} · ${'─'.repeat(8)}${RESET}`);
|
|
557
|
+
appendLine(renderUserMessage(trimmed));
|
|
558
|
+
// 启动思考动画
|
|
559
|
+
inkSetThinking(true);
|
|
568
560
|
const a = await getAgent();
|
|
569
561
|
const boxW = Math.min(termWidth() - 2, 76);
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const p = toolCalls.shift();
|
|
589
|
-
const doneItem = {
|
|
590
|
-
tool: e.tool ?? p?.tool ?? '?',
|
|
591
|
-
args: p?.args,
|
|
592
|
-
status: e.type === 'step_done' ? 'ok' : 'error',
|
|
593
|
-
output: e.output,
|
|
594
|
-
error: e.error,
|
|
595
|
-
durationMs: p ? Date.now() - p._t : undefined,
|
|
596
|
-
};
|
|
597
|
-
process.stdout.write(renderToolCallListItem(doneItem, toolCalls.length + 1, toolCounter) + '\n');
|
|
598
|
-
const bodyText = e.type === 'step_done' ? e.output : e.error;
|
|
599
|
-
if (bodyText && bodyText.length > 0) {
|
|
600
|
-
const bodyRendered = renderToolCallBody(doneItem, boxW);
|
|
601
|
-
if (bodyRendered)
|
|
602
|
-
process.stdout.write(bodyRendered + '\n');
|
|
603
|
-
}
|
|
604
|
-
// 全部完成时打印 footer
|
|
605
|
-
if (toolCalls.length === 0 && toolCounter > 0) {
|
|
606
|
-
process.stdout.write(renderToolCallsFooter(toolCounter) + '\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));
|
|
607
580
|
}
|
|
608
581
|
}
|
|
609
|
-
};
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
//
|
|
613
|
-
|
|
614
|
-
//
|
|
582
|
+
});
|
|
583
|
+
// 智能体回复框
|
|
584
|
+
appendLine(renderAgentMessage(response));
|
|
585
|
+
// 停止思考动画
|
|
586
|
+
inkSetThinking(false);
|
|
587
|
+
// 更新状态栏: 上下文进度
|
|
615
588
|
try {
|
|
616
|
-
// 用 messageHistory 长度推断上下文占用
|
|
617
589
|
const msgLen = JSON.stringify(a.messageHistory ?? []).length;
|
|
618
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);
|
|
619
597
|
}
|
|
620
598
|
catch { /* 降级容忍 */ }
|
|
621
|
-
//
|
|
622
|
-
process.stdout.write(`${C_DIM}${'─'.repeat(8)} · ${'─'.repeat(8)}${RESET}\n`);
|
|
599
|
+
// 自动消费队列
|
|
623
600
|
if (pendingQueue.length > 0) {
|
|
624
601
|
const next = pendingQueue.shift();
|
|
625
|
-
|
|
602
|
+
appendLine(`${C_WARN}⏩ 自动执行队列 [${pendingQueue.length + 1}/${pendingQueue.length + 1}]${RESET}`);
|
|
626
603
|
await processInput(next, comm);
|
|
627
604
|
return;
|
|
628
605
|
}
|
|
606
|
+
inkSetThinking(false);
|
|
629
607
|
}
|
|
630
608
|
catch (e) {
|
|
609
|
+
inkSetThinking(false);
|
|
631
610
|
if (!e.message?.includes('ERR_USE_AFTER_CLOSE') && !e.message?.includes('write after end')) {
|
|
632
|
-
|
|
611
|
+
appendLine(`${MAGENTA}❌ ${e.message}${RESET}`);
|
|
633
612
|
}
|
|
634
613
|
}
|
|
635
614
|
}
|
|
@@ -873,7 +852,7 @@ async function runToolCommand(tool, args, outputJson, comm, model, prompt) {
|
|
|
873
852
|
catch { }
|
|
874
853
|
break;
|
|
875
854
|
}
|
|
876
|
-
|
|
855
|
+
appendLine(`⏳ 任务已派给 ${targetPk.slice(0, 12)}..., 等回复 (最多 90s)...`);
|
|
877
856
|
const reply = await replyPromise;
|
|
878
857
|
const lines = [
|
|
879
858
|
`✅ 协作完成 (${reply.durationMs ? Math.round(reply.durationMs / 1000) + 's' : '?'})`,
|
|
@@ -1439,8 +1418,8 @@ async function runToolCommand(tool, args, outputJson, comm, model, prompt) {
|
|
|
1439
1418
|
const { P2PDirect } = await import('./network/p2p-direct.js');
|
|
1440
1419
|
const id = await resolveIdentity();
|
|
1441
1420
|
const p2p = new P2PDirect({ name: 'cli-listen', role: id.role });
|
|
1442
|
-
|
|
1443
|
-
|
|
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`);
|
|
1444
1423
|
const onData = (ev) => {
|
|
1445
1424
|
try {
|
|
1446
1425
|
const text = Buffer.isBuffer(ev.data) ? ev.data.toString('utf8') : String(ev.data);
|
|
@@ -1449,15 +1428,15 @@ async function runToolCommand(tool, args, outputJson, comm, model, prompt) {
|
|
|
1449
1428
|
if (env && env.v === 3 && env.op === 'agent.chat.direct') {
|
|
1450
1429
|
const { text: body, fromRole } = env.payload || {};
|
|
1451
1430
|
const ts = (env.payload?.ts || new Date().toISOString()).replace('T', ' ').replace(/\.\d+Z$/, '');
|
|
1452
|
-
|
|
1431
|
+
appendLine(`\n[${ts} ${fromRole || ev.fromPublicKey?.slice(0, 12)} → me] ${body}\n> `);
|
|
1453
1432
|
return;
|
|
1454
1433
|
}
|
|
1455
1434
|
}
|
|
1456
1435
|
catch { /* 非 v3 envelope, 当 raw 显示 */ }
|
|
1457
|
-
|
|
1436
|
+
appendLine(`\n[raw ${ev.fromPublicKey?.slice(0, 12)}] ${text.slice(0, 200)}\n> `);
|
|
1458
1437
|
}
|
|
1459
1438
|
catch (e) {
|
|
1460
|
-
|
|
1439
|
+
appendLine(`[chat-p2p-listen] decode error: ${e?.message ?? e}`);
|
|
1461
1440
|
}
|
|
1462
1441
|
};
|
|
1463
1442
|
p2p.on('data', onData);
|
|
@@ -1465,12 +1444,12 @@ async function runToolCommand(tool, args, outputJson, comm, model, prompt) {
|
|
|
1465
1444
|
const keepAlive = setInterval(() => {
|
|
1466
1445
|
const now = Date.now();
|
|
1467
1446
|
if (now - lastPing > 5 * 60_000) {
|
|
1468
|
-
|
|
1447
|
+
appendLine(`[chat-p2p-listen] alive, role=${id.role}`);
|
|
1469
1448
|
lastPing = now;
|
|
1470
1449
|
}
|
|
1471
1450
|
}, 30_000);
|
|
1472
1451
|
const stop = async () => {
|
|
1473
|
-
|
|
1452
|
+
appendLine(`\n[chat-p2p-listen] stopping...`);
|
|
1474
1453
|
try {
|
|
1475
1454
|
p2p.off('data', onData);
|
|
1476
1455
|
}
|
|
@@ -1487,7 +1466,7 @@ async function runToolCommand(tool, args, outputJson, comm, model, prompt) {
|
|
|
1487
1466
|
process.on('SIGHUP', stop);
|
|
1488
1467
|
await p2p.start();
|
|
1489
1468
|
await p2p.joinTopic(Buffer.from('bolloon-agent-harness'));
|
|
1490
|
-
|
|
1469
|
+
appendLine(`[chat-p2p-listen] joined topic ✓\n> `);
|
|
1491
1470
|
await new Promise(() => { });
|
|
1492
1471
|
break;
|
|
1493
1472
|
}
|
|
@@ -2167,7 +2146,7 @@ async function main() {
|
|
|
2167
2146
|
console.log = originalLog;
|
|
2168
2147
|
console.info = originalInfo;
|
|
2169
2148
|
process.stdout.write = originalStdoutWrite;
|
|
2170
|
-
startCLI(comm);
|
|
2149
|
+
await startCLI(comm);
|
|
2171
2150
|
}
|
|
2172
2151
|
}
|
|
2173
2152
|
catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bolloon/bolloon-agent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.24",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
|
|
6
6
|
"main": "dist/cli-entry.js",
|
|
@@ -87,11 +87,13 @@
|
|
|
87
87
|
"dotenv": "^17.4.2",
|
|
88
88
|
"esbuild": "^0.24.0",
|
|
89
89
|
"express": "^5.2.1",
|
|
90
|
+
"ink": "^4.4.1",
|
|
91
|
+
"ink-text-input": "^5.0.0",
|
|
90
92
|
"libp2p": "^3.3.0",
|
|
91
93
|
"mammoth": "^1.6.0",
|
|
92
94
|
"pdf-parse": "^1.1.4",
|
|
93
95
|
"platform": "^1.3.6",
|
|
94
|
-
"react": "^18.3.
|
|
96
|
+
"react": "^18.3.1",
|
|
95
97
|
"react-dom": "^18.3.0",
|
|
96
98
|
"viem": "^2.52.0"
|
|
97
99
|
},
|