@bolloon/bolloon-agent 0.4.17 → 0.4.19
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-registry.js +24 -0
- package/dist/agents/gateway-network.js +192 -14
- package/dist/agents/network-link.js +33 -0
- package/dist/cli/content.js +10 -0
- package/dist/cli/ink-app.js +131 -70
- package/dist/cli/keymap.js +36 -0
- package/dist/cli/loading-tui.js +11 -8
- package/dist/cli/markdown.js +18 -0
- package/dist/cli/stores.js +64 -0
- package/dist/cli/theme.js +23 -0
- package/dist/cli/timing.js +8 -0
- package/dist/cli/widget-host.js +68 -0
- package/dist/index.js +243 -12
- package/dist/web/mobile-agent.js +6 -0
- package/dist/web/mobile-core.js +12533 -149
- package/dist/web/mobile-gateway.js +191 -0
- package/dist/web/mobile.html +7 -0
- package/dist/web/mobile.js +52 -1
- package/dist/web/qr.js +49 -0
- package/package.json +4 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// ─── 输入层 keymap (Hermes 学习 #7) ────────────────────────────────────────
|
|
2
|
+
// 正常模式按键绑定表 + 纯函数 resolver: useInput 只做分发, 键->意图映射集中在这,
|
|
3
|
+
// 便于单测与扩展 (新增 Ctrl/Meta 组合不再改 useInput 巨块).
|
|
4
|
+
// 注: OSC52 剪贴板 / 精确滚轮 依赖终端能力, Ink useInput 不暴露, 不在本表范围.
|
|
5
|
+
/** 纯函数绑定表: 键 -> 意图 (仅在 scrollable 时响应滚动) */
|
|
6
|
+
export function resolveNormalKey(key, ctx) {
|
|
7
|
+
if (!ctx.scrollable)
|
|
8
|
+
return 'none';
|
|
9
|
+
const ch = ctx.input.toLowerCase();
|
|
10
|
+
// 上滚: Ctrl+U / PgUp / Alt+↑
|
|
11
|
+
if (key.ctrl && ch === 'u' || key.pageUp || key.meta && key.upArrow)
|
|
12
|
+
return 'scrollUp';
|
|
13
|
+
// 下滚: Ctrl+D / PgDn / Alt+↓
|
|
14
|
+
if (key.ctrl && ch === 'd' || key.pageDown || key.meta && key.downArrow)
|
|
15
|
+
return 'scrollDown';
|
|
16
|
+
// 顶部: Home / Ctrl+Home
|
|
17
|
+
if (key.home || key.ctrl && ch === 'a')
|
|
18
|
+
return 'scrollHome';
|
|
19
|
+
// 底部: End / Ctrl+End
|
|
20
|
+
if (key.end || key.ctrl && ch === 'e')
|
|
21
|
+
return 'scrollEnd';
|
|
22
|
+
return 'none';
|
|
23
|
+
}
|
|
24
|
+
/** 把意图映射成 scrollTop 平移量 (纯函数, 便于断言) */
|
|
25
|
+
export function applyScroll(act, cur, page, maxTop) {
|
|
26
|
+
switch (act) {
|
|
27
|
+
case 'scrollUp': return { next: Math.max(0, Math.min(cur - page, maxTop)), stick: false };
|
|
28
|
+
case 'scrollDown': {
|
|
29
|
+
const nx = Math.min(cur + page, maxTop);
|
|
30
|
+
return { next: nx, stick: nx >= maxTop };
|
|
31
|
+
}
|
|
32
|
+
case 'scrollHome': return { next: 0, stick: false };
|
|
33
|
+
case 'scrollEnd': return { next: maxTop, stick: true };
|
|
34
|
+
default: return { next: cur, stick: false };
|
|
35
|
+
}
|
|
36
|
+
}
|
package/dist/cli/loading-tui.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import { fileURLToPath } from 'url';
|
|
17
|
+
import { mdInline } from './markdown.js';
|
|
17
18
|
const RESET = '\x1b[0m';
|
|
18
19
|
const BOLD = '\x1b[1m';
|
|
19
20
|
const DIM = '\x1b[2m';
|
|
@@ -48,7 +49,7 @@ const BOLLOON_VERSION = getPackageVersion();
|
|
|
48
49
|
// ── 品牌图标: 笑脸机器人 (2026-08-09, bolloon 色系填充) ──────
|
|
49
50
|
// 头: 主色边框 + 亮绿填充 (C_ACCENT_BG); 眼睛 ◉ / 嘴 ◡ 用亮色填充;
|
|
50
51
|
// 末行 BOLLOON 主色艺术字 (仅 printBanner 用, brandArtLines 会裁掉避免双 logo).
|
|
51
|
-
const ROBOT_HEAD = [
|
|
52
|
+
export const ROBOT_HEAD = [
|
|
52
53
|
`${C_ACCENT} ╭───────╮${RESET}`,
|
|
53
54
|
`${C_ACCENT} ╭─╯${C_ACCENT_BG} ${C_WHITE}◉${C_ACCENT_BG} ${C_WHITE}◉${C_ACCENT_BG} ${RESET}${C_ACCENT}╰─╮${RESET}`,
|
|
54
55
|
`${C_ACCENT} │${C_ACCENT_BG} ${C_WHITE}◡${C_ACCENT_BG} ${RESET}${C_ACCENT}│${RESET}`,
|
|
@@ -328,11 +329,11 @@ function renderReference(opts) {
|
|
|
328
329
|
}
|
|
329
330
|
/** 已发送消息框 (用户输入) */
|
|
330
331
|
export function renderUserMessage(body) {
|
|
331
|
-
return renderMessageBox({ title: '✓ 已发送', body, color: C_OK, maxLines: DEFAULT_MAX_LINES });
|
|
332
|
+
return renderMessageBox({ title: '✓ 已发送', body: mdInline(body), color: C_OK, maxLines: DEFAULT_MAX_LINES });
|
|
332
333
|
}
|
|
333
334
|
/** 智能体回复框 (不压缩, 用户需要看到完整回复) */
|
|
334
335
|
export function renderAgentMessage(body) {
|
|
335
|
-
return renderMessageBox({ title: '◉ Bolloon Agent', body, color: C_ACCENT, maxLines: 0 });
|
|
336
|
+
return renderMessageBox({ title: '◉ Bolloon Agent', body: mdInline(body), color: C_ACCENT, maxLines: 0 });
|
|
336
337
|
}
|
|
337
338
|
/** 循环工作流连接线: 用 ╼ ╾ 串联相邻工具框 */
|
|
338
339
|
export function flowConnector(width) {
|
|
@@ -423,7 +424,9 @@ export function renderToolCall(v) {
|
|
|
423
424
|
lines.push(boxBottom(w, RD));
|
|
424
425
|
return lines.join('\n');
|
|
425
426
|
}
|
|
426
|
-
|
|
427
|
+
// 2026-09-08 (Hermes TUI 学习): 帧序列单一来源 — loading-tui 导出, ink-app 思考动画复用,
|
|
428
|
+
// 原来两份拷贝 (FRAMES / KAOMOJI) 会漂移.
|
|
429
|
+
export const LOADING_FRAMES = ['(`・ω・´)', '(´・_・`)', '(。•́︿•̀。)', 'ᕙ(▀̿̿Ĺ̯̿̿▀̿ ̿)ᕗ', '(◕‿◕)', 'ヽ(´▽`)/'];
|
|
427
430
|
export class LoadingTUI {
|
|
428
431
|
write;
|
|
429
432
|
timer = null;
|
|
@@ -432,7 +435,6 @@ export class LoadingTUI {
|
|
|
432
435
|
currentLabel = 'Bolloon loading...';
|
|
433
436
|
finished = false;
|
|
434
437
|
ok = true;
|
|
435
|
-
width = 0;
|
|
436
438
|
constructor() {
|
|
437
439
|
this.write = process.stdout.write.bind(process.stdout);
|
|
438
440
|
}
|
|
@@ -448,7 +450,9 @@ export class LoadingTUI {
|
|
|
448
450
|
return 1 + brandArtLines().length + this.steps.length + 1 + 1;
|
|
449
451
|
}
|
|
450
452
|
draw(showSpinner) {
|
|
451
|
-
|
|
453
|
+
// 2026-09-08 (Hermes TUI 学习): 每次 draw 实时算宽度 — 原首次缓存 (this.width),
|
|
454
|
+
// 启动过程中 resize 终端 → 仪表盘整框错位; computeWidth 每次成本可忽略
|
|
455
|
+
const w = this.computeWidth();
|
|
452
456
|
const out = [];
|
|
453
457
|
out.push(boxTop('Bolloon Agent · 启动仪表盘', w));
|
|
454
458
|
for (const l of brandArtLines())
|
|
@@ -457,7 +461,7 @@ export class LoadingTUI {
|
|
|
457
461
|
out.push(boxRow(`${STATUS_SYMBOL[step.status]} ${step.label}`, w));
|
|
458
462
|
}
|
|
459
463
|
if (showSpinner) {
|
|
460
|
-
const sp = C_WARN +
|
|
464
|
+
const sp = C_WARN + LOADING_FRAMES[this.frameIdx % LOADING_FRAMES.length] + RESET;
|
|
461
465
|
out.push(boxRow(`${sp} ${this.currentLabel}`, w));
|
|
462
466
|
}
|
|
463
467
|
else {
|
|
@@ -470,7 +474,6 @@ export class LoadingTUI {
|
|
|
470
474
|
}
|
|
471
475
|
setSteps(steps) {
|
|
472
476
|
this.steps = steps.map(label => ({ label, status: 'pending' }));
|
|
473
|
-
this.width = 0;
|
|
474
477
|
if (this.timer)
|
|
475
478
|
this.draw(true);
|
|
476
479
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// ─── 内联 Markdown 分词渲染 (Hermes 学习 #4 之 MessageLine) ────────────────
|
|
2
|
+
// 把正文里的 `code` / **bold** / _italic_ / __underline__ 转成 ANSI 高亮,
|
|
3
|
+
// 供 renderUserMessage / renderAgentMessage 在出框前套用 (字节变长, dispWidth 剥 ANSI 不计宽).
|
|
4
|
+
// 仅内联分段; ```
|
|
5
|
+
// 多行代码块折叠/Thinking/ToolTrail 折叠 仍待续.
|
|
6
|
+
const R = '\x1b[0m';
|
|
7
|
+
const BOLD = '\x1b[1m';
|
|
8
|
+
const DIM = '\x1b[2m';
|
|
9
|
+
const UNDER = '\x1b[4m';
|
|
10
|
+
const CYAN = '\x1b[36m';
|
|
11
|
+
const GREEN = '\x1b[32m';
|
|
12
|
+
export function mdInline(s) {
|
|
13
|
+
return s
|
|
14
|
+
.replace(/`([^`]+)`/g, (_, c) => `${CYAN}${c}${R}`)
|
|
15
|
+
.replace(/\*\*([^*]+)\*\*/g, (_, c) => `${BOLD}${c}${R}`)
|
|
16
|
+
.replace(/__([^_]+)__/g, (_, c) => `${UNDER}${c}${R}`)
|
|
17
|
+
.replace(/(^|[^*\w])\*([^*\s][^*]*)\*/g, (_m, p, c) => `${p}${GREEN}${c}${R}`);
|
|
18
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// ─── 状态外置 (Hermes 学习 #2): 每域一小 store + 纯函数 action ──────────────
|
|
2
|
+
// 目的: 状态不再散在 InkApp 单组件 useState 里, 而是集中到可订阅的 store,
|
|
3
|
+
// 输入/桥接 handler 变为 over store 的纯函数, 便于单测与热置换.
|
|
4
|
+
// 用 useSyncExternalStore 订阅 (React 18+, 与 nanostores 同款订阅语义).
|
|
5
|
+
import { useSyncExternalStore } from 'react';
|
|
6
|
+
export function createStore(initial) {
|
|
7
|
+
let state = initial;
|
|
8
|
+
const subs = new Set();
|
|
9
|
+
return {
|
|
10
|
+
get: () => state,
|
|
11
|
+
set: (v) => {
|
|
12
|
+
if (Object.is(v, state))
|
|
13
|
+
return;
|
|
14
|
+
state = v;
|
|
15
|
+
subs.forEach((cb) => cb());
|
|
16
|
+
},
|
|
17
|
+
subscribe: (cb) => {
|
|
18
|
+
subs.add(cb);
|
|
19
|
+
return () => { subs.delete(cb); };
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** 订阅 store, 值变化触发重渲染 (外部状态源统一入口) */
|
|
24
|
+
export function useStore(s) {
|
|
25
|
+
return useSyncExternalStore(s.subscribe, s.get, s.get);
|
|
26
|
+
}
|
|
27
|
+
// ── 域 store ────────────────────────────────────────────────────────────────
|
|
28
|
+
/** transcript: 消息列表 (agent/用户/系统行) — 供虚拟化/滚动/桥接读写 */
|
|
29
|
+
export const transcriptStore = createStore([]);
|
|
30
|
+
export const uiStore = createStore({ status: '', thinking: false, transient: null });
|
|
31
|
+
// ── 纯函数 action (bridge 与组件都走这里) ────────────────────────────────────
|
|
32
|
+
export function appendMsg(line) {
|
|
33
|
+
const c = transcriptStore.get();
|
|
34
|
+
transcriptStore.set([...c, line]);
|
|
35
|
+
}
|
|
36
|
+
export function replaceLastMsg(line) {
|
|
37
|
+
const c = transcriptStore.get();
|
|
38
|
+
if (c.length === 0) {
|
|
39
|
+
transcriptStore.set([line]);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const n = c.slice();
|
|
43
|
+
n[n.length - 1] = line;
|
|
44
|
+
transcriptStore.set(n);
|
|
45
|
+
}
|
|
46
|
+
/** 按内容标记原地替换一条 (占位框 → 完整内容); 未命中不改 */
|
|
47
|
+
export function replaceMarkerMsg(marker, line) {
|
|
48
|
+
const c = transcriptStore.get();
|
|
49
|
+
const i = c.indexOf(marker);
|
|
50
|
+
if (i < 0)
|
|
51
|
+
return;
|
|
52
|
+
const n = c.slice();
|
|
53
|
+
n[i] = line;
|
|
54
|
+
transcriptStore.set(n);
|
|
55
|
+
}
|
|
56
|
+
export function setUiStatus(status) {
|
|
57
|
+
uiStore.set({ ...uiStore.get(), status });
|
|
58
|
+
}
|
|
59
|
+
export function setUiThinking(thinking) {
|
|
60
|
+
uiStore.set({ ...uiStore.get(), thinking });
|
|
61
|
+
}
|
|
62
|
+
export function setUiTransient(v) {
|
|
63
|
+
uiStore.set({ ...uiStore.get(), transient: v === undefined ? null : v });
|
|
64
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* theme.ts — bolloon TUI 主题 token (唯一颜色事实源)
|
|
3
|
+
* 与 Web UI 一致: 主色 #c4d640, 文本 #d8d8c8, 警告 #f59e0b, 成功 #22c55e, 错误 #ef4444。
|
|
4
|
+
* 组件一律引用 THEME.*, 不再散落 hex 字面量。后续可扩展皮肤/深色切换。
|
|
5
|
+
*/
|
|
6
|
+
export const THEME = {
|
|
7
|
+
accent: '#c4d640', // 主色 (bolloon 绿)
|
|
8
|
+
text: '#d8d8c8', // 正文
|
|
9
|
+
muted: '#606058', // 次要/暗层
|
|
10
|
+
dim: '#909088', // 更暗
|
|
11
|
+
ok: '#22c55e', // 成功
|
|
12
|
+
error: '#ef4444', // 错误
|
|
13
|
+
warn: '#f59e0b', // 警告
|
|
14
|
+
border: '#3a3a36', // 暗描边
|
|
15
|
+
borderBright: '#8a8a7e', // 对话框边框提亮
|
|
16
|
+
};
|
|
17
|
+
/** '#c4d640' → '\x1b[38;2;196;214;64m' ANSI 前景色码 */
|
|
18
|
+
export function fg(hex) {
|
|
19
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
20
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
21
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
22
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* timing.ts — TUI 时序常量 (原来散落魔法数字)
|
|
3
|
+
*/
|
|
4
|
+
export const DOUBLE_ESC_MS = 500; // 双击 Esc 退出窗口
|
|
5
|
+
export const STATUS_TICK_MS = 1000; // 状态栏刷新周期
|
|
6
|
+
export const THINK_FRAME_MS = 600; // 思考 kaomoji 帧间隔
|
|
7
|
+
export const LOAD_FRAME_MS = 100; // 启动仪表盘帧间隔
|
|
8
|
+
export const CHUNK_GUARD_DELAY_MS = 0; // 控制字符纠偏 setTimeout(0)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// ─── 占用槽 / widget host (Hermes 学习 #8) ────────────────────────────────
|
|
2
|
+
// 在 UI 右侧保留一个 rails 槽位 (占用槽), 供外部 widget 注册渲染.
|
|
3
|
+
// 触点: registerWidget / unregisterWidget / refreshWidgets / listWidgets.
|
|
4
|
+
// 默认无 widget 时槽位不占宽 (布局不变); 有 widget 时才预留右侧列.
|
|
5
|
+
// .mjs 热加载: hotReloadWidgets(dir) 监听目录, 动态 import 模块 (模块调用 registerWidget 注册).
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { pathToFileURL } from 'url';
|
|
9
|
+
import { useSyncExternalStore } from 'react';
|
|
10
|
+
let widgets = new Map();
|
|
11
|
+
let cache = null; // getSnapshot 必须缓存同引用, 否则 useSyncExternalStore 无限重渲
|
|
12
|
+
const subs = new Set();
|
|
13
|
+
function rebuild() {
|
|
14
|
+
const out = {};
|
|
15
|
+
for (const [name, w] of widgets)
|
|
16
|
+
out[name] = w.text;
|
|
17
|
+
cache = out;
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
function emit() { cache = null; subs.forEach((cb) => cb()); }
|
|
21
|
+
export function getWidgets() {
|
|
22
|
+
if (!cache)
|
|
23
|
+
rebuild();
|
|
24
|
+
return cache;
|
|
25
|
+
}
|
|
26
|
+
/** 4 触点之一: 注册 (render 在注册时立即执行一次, 得 text) */
|
|
27
|
+
export function registerWidget(name, render) {
|
|
28
|
+
widgets.set(name, { name, render, text: render() });
|
|
29
|
+
emit();
|
|
30
|
+
}
|
|
31
|
+
export function unregisterWidget(name) {
|
|
32
|
+
if (widgets.delete(name))
|
|
33
|
+
emit();
|
|
34
|
+
}
|
|
35
|
+
/** 重新执行所有 widget 的 render (数据/间隔刷新) */
|
|
36
|
+
export function refreshWidgets() {
|
|
37
|
+
for (const [name, w] of widgets) {
|
|
38
|
+
try {
|
|
39
|
+
w.text = w.render();
|
|
40
|
+
}
|
|
41
|
+
catch { /* 单个 widget 渲染失败不致命 */ }
|
|
42
|
+
}
|
|
43
|
+
emit();
|
|
44
|
+
}
|
|
45
|
+
export function listWidgets() { return [...widgets.keys()]; }
|
|
46
|
+
/** 仅测试用: 清空所有 widget */
|
|
47
|
+
export function resetWidgetsForTest() { widgets = new Map(); emit(); }
|
|
48
|
+
export function subscribeWidgets(cb) { subs.add(cb); return () => { subs.delete(cb); }; }
|
|
49
|
+
/** React 订阅: 返回 {name: text} (槽位渲染用) */
|
|
50
|
+
export function useWidgets() {
|
|
51
|
+
return useSyncExternalStore(subscribeWidgets, getWidgets, getWidgets);
|
|
52
|
+
}
|
|
53
|
+
/** .mjs 热加载: 监听 dir, 新增/变更的 .mjs 直接 import (模块内调 registerWidget); 失败静默 */
|
|
54
|
+
export function hotReloadWidgets(dir) {
|
|
55
|
+
if (!fs.existsSync(dir))
|
|
56
|
+
return null;
|
|
57
|
+
const loaded = new Set();
|
|
58
|
+
const load = (f) => {
|
|
59
|
+
if (f.endsWith('.mjs') && !loaded.has(f)) {
|
|
60
|
+
loaded.add(f);
|
|
61
|
+
import(pathToFileURL(path.join(dir, f)).href).catch(() => { });
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
for (const f of fs.readdirSync(dir))
|
|
65
|
+
load(f);
|
|
66
|
+
return setInterval(() => { for (const f of fs.readdirSync(dir))
|
|
67
|
+
load(f); }, 3000);
|
|
68
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { sha512 } from '@noble/hashes/sha2.js';
|
|
|
6
6
|
import * as fs from 'fs/promises';
|
|
7
7
|
import { existsSync, mkdirSync } from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
|
-
import { spawn } from 'child_process';
|
|
9
|
+
import { spawn, execSync } from 'child_process';
|
|
10
10
|
import * as os from 'os';
|
|
11
11
|
import { documentReader } from './documents/reader.js';
|
|
12
12
|
import { initMinimax } from './constraints/index.js';
|
|
@@ -15,8 +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, renderMessageBox, renderToolCallListItem, termWidth } from './cli/loading-tui.js';
|
|
19
|
-
import { startInk, stopInk, inkAppendLine as appendLine, inkSetStatus, inkSetThinking, inkSetTransient } from './cli/ink-app.js';
|
|
18
|
+
import { printBanner, renderDialog, renderUserMessage, renderAgentMessage, renderMessageBox, renderToolCallListItem, termWidth, ROBOT_HEAD, BOLLOON_BANNER, dispWidth } from './cli/loading-tui.js';
|
|
19
|
+
import { startInk, stopInk, inkAppendLine as appendLine, inkReplaceMatchingLine, inkSetStatus, inkSetThinking, inkSetTransient } from './cli/ink-app.js';
|
|
20
20
|
// 启动自动检查更新:后台、节流、检测到新版本自动安装(可被 --no-update / BOLLOON_SKIP_UPDATE 关闭)
|
|
21
21
|
import { createRequire } from 'module';
|
|
22
22
|
const _require = createRequire(import.meta.url);
|
|
@@ -497,25 +497,169 @@ function statusBarLine() {
|
|
|
497
497
|
const usage = getCliCtxUsage();
|
|
498
498
|
return `${C_ACCENT}${cliModelName}${RESET}${C_DIM} │${RESET} ${cliAgentName} ${C_DIM}│${RESET} ⏱ ${C_ACCENT}${dur}${RESET} ${C_DIM}│${RESET} ${buildContextBar(usage)}`;
|
|
499
499
|
}
|
|
500
|
+
/**
|
|
501
|
+
* 启动会话面板 — 按类别展示启动加载的 skills (真实类别 = 目录名去掉 frontmatter.name 后缀),
|
|
502
|
+
* 每类一行 (前 6 名 + '+N more'), 结尾 totals; skills/tools 并行 (各 2.5s 预算), 失败静默.
|
|
503
|
+
*/
|
|
504
|
+
/** 启动面板框: BOLLOON 字标 logo 顶部居中 → 下方两栏 = face 艺术字(左) + 加载内容(右) */
|
|
505
|
+
function buildBootBox(face, banner, rest) {
|
|
506
|
+
const faceW = Math.max(1, ...face.map((l) => dispWidth(l)));
|
|
507
|
+
const bannerMax = Math.max(1, ...banner.map((l) => dispWidth(l)));
|
|
508
|
+
const restMax = Math.max(1, ...rest.map((l) => dispWidth(l)));
|
|
509
|
+
const gap = 4;
|
|
510
|
+
const twoColW = faceW + gap + restMax;
|
|
511
|
+
const contentW = Math.max(bannerMax + 8, twoColW);
|
|
512
|
+
const center = (rows) => rows.map((l) => ' '.repeat(Math.max(0, Math.floor((contentW - dispWidth(l)) / 2))) + l);
|
|
513
|
+
const n = rest.length;
|
|
514
|
+
const fStart = Math.max(0, Math.floor((n - face.length) / 2)); // face 列对内容垂直居中 = 等高
|
|
515
|
+
const twoCol = rest.map((r, i) => {
|
|
516
|
+
const f = (i >= fStart && i < fStart + face.length) ? face[i - fStart] : null;
|
|
517
|
+
const fpart = f ? `${f}${' '.repeat(Math.max(0, faceW - dispWidth(f)) + gap)}` : ' '.repeat(faceW + gap);
|
|
518
|
+
return fpart + r;
|
|
519
|
+
});
|
|
520
|
+
const body = [...center(banner), '', ...twoCol];
|
|
521
|
+
return renderMessageBox({ title: '🚀 Bolloon · 启动面板', body: body.join('\n'), color: C_ACCENT, maxLines: 0 });
|
|
522
|
+
}
|
|
523
|
+
async function bootPanel(boot) {
|
|
524
|
+
const sub = []; // tools / MCP (Promise.all 里填充, 最后统一排到类别下方)
|
|
525
|
+
const catNames = new Map();
|
|
526
|
+
await Promise.all([
|
|
527
|
+
(async () => {
|
|
528
|
+
try {
|
|
529
|
+
// 真实类别 = 目录名前缀去掉技能名后缀 (SKILL.md 无 category 字段, 但 frontmatter.name 是真名:
|
|
530
|
+
// software-development-bolloon-development / name=bolloon-development → software-development)
|
|
531
|
+
const { loadSkillsDir, defaultSkillPaths } = await import('./agents/skill-loader.js');
|
|
532
|
+
const pushCat = (cat, name) => {
|
|
533
|
+
const arr = catNames.get(cat) || [];
|
|
534
|
+
if (!arr.includes(name))
|
|
535
|
+
arr.push(name);
|
|
536
|
+
catNames.set(cat, arr);
|
|
537
|
+
};
|
|
538
|
+
for (const root of defaultSkillPaths()) {
|
|
539
|
+
const metas = await loadSkillsDir(root);
|
|
540
|
+
for (const m of metas) {
|
|
541
|
+
if (m.status === 'archived')
|
|
542
|
+
continue;
|
|
543
|
+
const dir = m.sourcePath ? path.basename(path.dirname(m.sourcePath)) : '';
|
|
544
|
+
const nm = m.name || '';
|
|
545
|
+
let cat = dir;
|
|
546
|
+
if (dir && nm && dir.endsWith(nm)) {
|
|
547
|
+
const pre = dir.slice(0, dir.length - nm.length).replace(/-+$/, '');
|
|
548
|
+
if (pre)
|
|
549
|
+
cat = pre;
|
|
550
|
+
}
|
|
551
|
+
pushCat(cat || 'other', nm || dir);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
catch { /* 省略 */ }
|
|
556
|
+
})(),
|
|
557
|
+
(async () => {
|
|
558
|
+
try {
|
|
559
|
+
const a = await Promise.race([
|
|
560
|
+
getAgent().catch(() => null),
|
|
561
|
+
new Promise((res) => setTimeout(() => res(null), 2500)),
|
|
562
|
+
]);
|
|
563
|
+
const tools = a && typeof a.getToolList === 'function' ? a.getToolList() : null;
|
|
564
|
+
if (tools && tools.length > 0)
|
|
565
|
+
sub.push(`🔧 ${tools.length} tools`);
|
|
566
|
+
}
|
|
567
|
+
catch { /* 省略 */ }
|
|
568
|
+
})(),
|
|
569
|
+
(async () => {
|
|
570
|
+
try {
|
|
571
|
+
const { getAdapterStatus } = await import('./pi-ecosystem-mcp/index.js');
|
|
572
|
+
const st = getAdapterStatus();
|
|
573
|
+
if (st.initialized && st.serverCount > 0)
|
|
574
|
+
sub.push(`🔌 MCP ${st.serverCount} 服务器 · ${st.toolCount} tools`);
|
|
575
|
+
}
|
|
576
|
+
catch { /* 省略 */ }
|
|
577
|
+
})(),
|
|
578
|
+
]);
|
|
579
|
+
// 栈式布局: face 艺术字居中 → BOLLOON 字标 logo 在其下 → 内容左对齐
|
|
580
|
+
const art = ROBOT_HEAD;
|
|
581
|
+
const banner = BOLLOON_BANNER.split('\n');
|
|
582
|
+
// 头: 目录 / 模型 / Session (预先加载信息)
|
|
583
|
+
const rest = [];
|
|
584
|
+
if (boot.dir)
|
|
585
|
+
rest.push(`📁 ${boot.dir}`);
|
|
586
|
+
if (boot.model)
|
|
587
|
+
rest.push(`模型 ${boot.model}`);
|
|
588
|
+
if (boot.session)
|
|
589
|
+
rest.push(`Session: ${boot.session}`);
|
|
590
|
+
rest.push('');
|
|
591
|
+
// 类别行 (全部展开, 不截断类别; 每类列前 8 名 + '+N more') — 单一实例归 other
|
|
592
|
+
const normalized = new Map();
|
|
593
|
+
for (const [cat, arr] of catNames) {
|
|
594
|
+
if (arr.length === 1) {
|
|
595
|
+
const o = normalized.get('other') || [];
|
|
596
|
+
o.push(...arr);
|
|
597
|
+
normalized.set('other', o);
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
normalized.set(cat, (normalized.get(cat) || []).concat(arr));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const sorted = [...normalized.entries()].sort((a, b) => b[1].length - a[1].length);
|
|
604
|
+
const total = sorted.reduce((s, [, arr]) => s + arr.length, 0);
|
|
605
|
+
for (const [cat, arr] of sorted) {
|
|
606
|
+
const shown = arr.slice(0, 8);
|
|
607
|
+
const more = arr.length > shown.length ? `, +${arr.length - shown.length} more` : '';
|
|
608
|
+
rest.push(`${cat}: ${shown.join(', ')}${more}`);
|
|
609
|
+
}
|
|
610
|
+
rest.push(`⚡ ${total} skills · ${sorted.length} 类`, '');
|
|
611
|
+
if (sub.length)
|
|
612
|
+
rest.push(...sub, '');
|
|
613
|
+
// 自动整理 (经验/技能整理心跳) 模式并入面板
|
|
614
|
+
rest.push(`🧹 经验自动整理: 启动后每 30min 一次`);
|
|
615
|
+
try {
|
|
616
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', { encoding: 'utf8', timeout: 1500 }).trim();
|
|
617
|
+
if (branch)
|
|
618
|
+
rest.push(`⎇ ${branch}`);
|
|
619
|
+
}
|
|
620
|
+
catch { /* 非 git 目录省略 */ }
|
|
621
|
+
try {
|
|
622
|
+
rest.push(new Date().toLocaleTimeString('zh-CN', { hour12: false }));
|
|
623
|
+
}
|
|
624
|
+
catch { /* 忽略 */ }
|
|
625
|
+
if (rest.length <= 3)
|
|
626
|
+
return null;
|
|
627
|
+
// 启动面板框: BOLLOON 字标 logo 顶部居中 → 下方两栏 = face 艺术字(左) + skills/信息(右)
|
|
628
|
+
return buildBootBox(art, banner, rest);
|
|
629
|
+
}
|
|
500
630
|
async function startCLI(commReady) {
|
|
501
631
|
isRunning = true;
|
|
502
632
|
// 2026-09-08 加速启动: P2P 后台就绪, UI 直接渲染不阻塞 — comm 就绪前为 null,
|
|
503
633
|
// 内部用法全空安全 (P2P 功能自动降级, 就绪后立即可用)
|
|
504
634
|
let comm = null;
|
|
505
635
|
commReady.then((c) => { comm = c; }).catch(() => { });
|
|
506
|
-
// CLI 模式下静音所有 console.log/warn
|
|
507
|
-
// (Ink 用自己的 render 引擎, console
|
|
636
|
+
// CLI 模式下静音所有 console.log/warn/info/debug
|
|
637
|
+
// (Ink 用自己的 render 引擎, console 输出会污染终端)
|
|
508
638
|
console.log = () => { };
|
|
509
639
|
console.warn = () => { };
|
|
510
|
-
|
|
640
|
+
console.info = () => { };
|
|
641
|
+
console.debug = () => { };
|
|
642
|
+
// 过滤 process.stdout/stderr.write — 丢弃启动期 SDK/后台日志
|
|
643
|
+
// (ISO 时间戳前缀如 `2026-09-08T...Z [info]:` 或被 [info]/[warn]/[error] 标记的行)
|
|
511
644
|
const _origStdout = process.stdout.write.bind(process.stdout);
|
|
512
|
-
process.
|
|
645
|
+
const _origStderr = process.stderr.write.bind(process.stderr);
|
|
646
|
+
const isLogLine = (line) => {
|
|
647
|
+
const t = line.trimStart();
|
|
648
|
+
return t.startsWith('[') || /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(t) || /\[\s*(info|warn|error|debug|log)\s*\]/.test(t)
|
|
649
|
+
// Kubo/ipfs 启动噪声: "Use 'ipfs init --help'..." / "ipfs daemon is running..." 提示行无时间戳, 一并丢弃
|
|
650
|
+
|| /ipfs init --help|ipfs daemon is running|please stop it to run this command/i.test(t);
|
|
651
|
+
};
|
|
652
|
+
const wrap = (orig) => (chunk, ...rest) => {
|
|
513
653
|
const s = typeof chunk === 'string' ? chunk : String(chunk);
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
654
|
+
if (!isLogLine(s))
|
|
655
|
+
return orig(chunk, ...rest);
|
|
656
|
+
const keep = s.split('\n').filter((l) => !isLogLine(l)).join('\n');
|
|
657
|
+
if (keep)
|
|
658
|
+
orig(keep, ...rest);
|
|
659
|
+
return true;
|
|
660
|
+
};
|
|
661
|
+
process.stdout.write = wrap(_origStdout);
|
|
662
|
+
process.stderr.write = wrap(_origStderr);
|
|
519
663
|
let peerCount = 0;
|
|
520
664
|
void commReady.then((c) => { try {
|
|
521
665
|
if (c)
|
|
@@ -594,6 +738,13 @@ async function startCLI(commReady) {
|
|
|
594
738
|
}
|
|
595
739
|
catch { /* 降级: getCliCtxUsage 返回 0/1M */ }
|
|
596
740
|
const initialStatus = `${C_ACCENT}${cliModelName}${RESET}${C_DIM} │${RESET} ${cliAgentName} ${C_DIM}│${RESET} ⏱ 0s${C_DIM} │${RESET} ${buildContextBar(getCliCtxUsage())}`;
|
|
741
|
+
// 2026-09-08 (leo 规格): 图标下元信息层数据 — 目录(home→~) / 模型 / Session id (Hermes 风格: YYYYMMDD_HHMMSS_xxxx)
|
|
742
|
+
const bootDirShort = process.cwd().replace(os.homedir(), '~');
|
|
743
|
+
const bootSessionId = (() => {
|
|
744
|
+
const d = new Date(cliStartTime);
|
|
745
|
+
const p = (n, l = 2) => String(n).padStart(l, '0');
|
|
746
|
+
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)}`;
|
|
747
|
+
})();
|
|
597
748
|
startInk((text) => { processInput(text, comm); }, initialStatus, getStatus);
|
|
598
749
|
// 2026-08-10: 自动整理心跳 (CLI 侧, 与社交心跳并列) — 启动后立即"固定看一下 skills view"
|
|
599
750
|
// (扫描遗留 skills), 之后按周期 (默认 30min, env BOLLOON_ORGANIZE_HEARTBEAT_MS) 完整进化经验.
|
|
@@ -683,6 +834,18 @@ async function startCLI(commReady) {
|
|
|
683
834
|
setTimeout(() => { cronScheduler.tick().catch(() => { }); }, 15_000);
|
|
684
835
|
}
|
|
685
836
|
catch { /* cron 调度启动失败不阻塞 CLI */ }
|
|
837
|
+
// 启动会话面板 (大框): 栈式 = face 艺术字居中 + BOLLOON 字标 logo 在其下 + 预设信息(skills/工具/模型/目录/Session/分支/时间)
|
|
838
|
+
// 先立即渲染「艺术字 + logo + 正在加载...」, bootPanel 就绪后 inkReplaceMatchingLine 按标记原位替换为完整内容
|
|
839
|
+
// (用匹配替换而非 replaceLast — P2P/连接消息可能先于 bootPanel 追加, replaceLast 会覆盖错一条)
|
|
840
|
+
const bootBox = buildBootBox(ROBOT_HEAD, BOLLOON_BANNER.split('\n'), [
|
|
841
|
+
`${bootDirShort} · ${(cliModelName && cliModelName !== '…') ? cliModelName : ''} · Session: ${bootSessionId}`,
|
|
842
|
+
'',
|
|
843
|
+
'⟳ 正在加载技能 / 工具...',
|
|
844
|
+
]);
|
|
845
|
+
appendLine(bootBox);
|
|
846
|
+
void bootPanel({ dir: bootDirShort, model: (cliModelName && cliModelName !== '…') ? cliModelName : undefined, session: bootSessionId })
|
|
847
|
+
.then((box) => { if (box)
|
|
848
|
+
inkReplaceMatchingLine(bootBox, box); }).catch(() => { });
|
|
686
849
|
// Wait on a promise that resolves on Ctrl+C / 双击 Esc
|
|
687
850
|
// (ink-app 的 requestExit 调 __inkRequestExit → resolve, 清理后 process.exit)
|
|
688
851
|
let cliExitResolve = () => { };
|
|
@@ -959,6 +1122,74 @@ async function processInput(input, comm) {
|
|
|
959
1122
|
}
|
|
960
1123
|
// ==================== 2026-08-06: 系统命令组 (/model /now /ipfs /memory ...) ====================
|
|
961
1124
|
const cmd = trimmed.toLowerCase();
|
|
1125
|
+
// /net — Agent 网络快捷命令 (join/status/ctx, 2026-09-08)
|
|
1126
|
+
if (cmd === '/net' || cmd.startsWith('/net ')) {
|
|
1127
|
+
const arg = trimmed.slice(4).trim();
|
|
1128
|
+
if (arg.toLowerCase().startsWith('join ')) {
|
|
1129
|
+
const link = arg.slice(5).trim();
|
|
1130
|
+
const { joinNetwork, pullNetworkProfile, pullNetworkSharedContext, networkShareSelf } = await import('./agents/gateway-network.js');
|
|
1131
|
+
const r = await joinNetwork(link);
|
|
1132
|
+
if (!r.ok) {
|
|
1133
|
+
appendLine(`${C_ERROR}加入失败: ${r.error}${RESET}`);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
appendLine(r.already ? `${C_DIM}已在网络 (${r.linkKind})${RESET}` : `${C_OK}已加入${RESET} (${r.linkKind}) · ${r.total} 服务 · 新增 ${r.joined}${r.networkId ? ` · ${C_DIM}net=${String(r.networkId).slice(0, 16)}${RESET}` : ''}`);
|
|
1137
|
+
const profile = await pullNetworkProfile(link).catch(() => null);
|
|
1138
|
+
if (profile?.members?.length) {
|
|
1139
|
+
appendLine(`${C_ACCENT}网络画像:${RESET}`);
|
|
1140
|
+
for (const m of profile.members.slice(0, 8))
|
|
1141
|
+
appendLine(` ${m.name} ${C_DIM}(${String(m.agentId).slice(0, 16)}…) · ${m.service?.name || ''}${RESET}`);
|
|
1142
|
+
}
|
|
1143
|
+
if (profile?.bootstrap?.sharedContextCid) {
|
|
1144
|
+
const ctx = await pullNetworkSharedContext(profile.bootstrap.sharedContextCid).catch(() => null);
|
|
1145
|
+
if (ctx)
|
|
1146
|
+
appendLine(`${C_DIM}📡 共享context: ${ctx.slice(0, 140).replace(/\n/g, ' ')}${RESET}`);
|
|
1147
|
+
}
|
|
1148
|
+
try {
|
|
1149
|
+
const self = { agentId: cliAgentId || 'cli-agent', name: cliAgentName || 'bolloon', wallet: '0x0', service: { name: 'agent', description: 'bolloon cli node', price: { amount: '0', currency: 'USDC', per: 'query' } } };
|
|
1150
|
+
const s = await networkShareSelf(link, [self]);
|
|
1151
|
+
if (s?.ok && s.note)
|
|
1152
|
+
appendLine(`${C_DIM}${s.note}${RESET}`);
|
|
1153
|
+
}
|
|
1154
|
+
catch { /* 广播失败不致命 */ }
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
if (arg.toLowerCase() === 'status' || arg === '') {
|
|
1158
|
+
const { listJoinedNetworks } = await import('./agents/gateway-network.js');
|
|
1159
|
+
const nets = await listJoinedNetworks();
|
|
1160
|
+
appendLine(nets.length
|
|
1161
|
+
? `🔗 ${C_ACCENT}已加入网络:${RESET}\n` + nets.map((n) => ` ${n.name || n.kind} ${C_DIM}(${n.serviceCount}S${n.networkId ? ` · ${String(n.networkId).slice(0, 16)}` : ''})${RESET}`).join('\n')
|
|
1162
|
+
: `${C_DIM}未加入任何网络 — /net join <链接>${RESET}`);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
if (arg.toLowerCase() === 'qr') {
|
|
1166
|
+
const { shareNetworkLink } = await import('./agents/gateway-network.js');
|
|
1167
|
+
const sh = await shareNetworkLink({ name: cliAgentName || 'bolloon' });
|
|
1168
|
+
if (!sh.link) {
|
|
1169
|
+
appendLine(`${C_ERROR}生成链接失败: ${sh.error}${RESET}`);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
const { buildQrPayload, encodeQrTerminal } = await import('./web/qr.js');
|
|
1173
|
+
const payload = buildQrPayload({ link: sh.link, version: '1' });
|
|
1174
|
+
const qr = await encodeQrTerminal(payload);
|
|
1175
|
+
if (qr) {
|
|
1176
|
+
appendLine(renderMessageBox({ title: '📷 扫码入网', body: `${qr}\n\n${C_DIM}链接 (手机粘贴也可): ${payload}${RESET}`, color: C_ACCENT, maxLines: 0 }));
|
|
1177
|
+
}
|
|
1178
|
+
else {
|
|
1179
|
+
appendLine(`${C_ERROR}二维码生成失败 — 链接: ${payload}${RESET}`);
|
|
1180
|
+
}
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
if (arg.toLowerCase().startsWith('ctx ')) {
|
|
1184
|
+
const text = arg.slice(4).trim();
|
|
1185
|
+
const { publishNetworkSharedContext } = await import('./agents/gateway-network.js');
|
|
1186
|
+
const p = await publishNetworkSharedContext(text);
|
|
1187
|
+
appendLine(p.ok ? `${C_OK}共享context已发布:${RESET} ${p.cid}` : `${C_ERROR}发布失败: ${p.error}${RESET}`);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
appendLine(`${C_DIM}用法: /net join <链接> | /net status | /net ctx <文本>${RESET}`);
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
962
1193
|
// /model — 模型供应商选择器 (ink 交互渲染, 复用 MentionPopup)
|
|
963
1194
|
if (cmd === '/model') {
|
|
964
1195
|
try {
|
package/dist/web/mobile-agent.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
// ============ 身份 (WebCrypto) ============
|
|
14
14
|
const IDENTITY_DB = 'bolloon-mobile';
|
|
15
15
|
let _identity = null;
|
|
16
|
+
// #2 手机自动入网 (browser-safe gateway, 与桌面同一协议): 检测到链接自动 join
|
|
17
|
+
import { mobileAutoJoinGateway } from './mobile-gateway.js';
|
|
16
18
|
async function generateDID() {
|
|
17
19
|
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
18
20
|
const digest = await crypto.subtle.digest('SHA-256', bytes);
|
|
@@ -269,6 +271,10 @@ export async function handleIncomingAgentMessage(type, payload, fromPeer) {
|
|
|
269
271
|
try {
|
|
270
272
|
const { text, channelId } = JSON.parse(payload);
|
|
271
273
|
notifyInboundChat(text || '', channelId || '', fromPeer);
|
|
274
|
+
// #2 手机自动入网: 消息里带 network 链接 → 自动 join (browser-safe, 不阻塞回复)
|
|
275
|
+
if (text && /orbitdb:\/\/|ipns:\/\/|https?:\/\/[^\s]*\/registry/.test(text)) {
|
|
276
|
+
void mobileAutoJoinGateway(text).catch(() => { });
|
|
277
|
+
}
|
|
272
278
|
const reply = await runLocalAgent(text || '');
|
|
273
279
|
await _send('agent.chat.reply', JSON.stringify({ channelId, text: reply, fromPublicKey: _ownDid }), fromPeer);
|
|
274
280
|
}
|