@bolloon/bolloon-agent 0.3.32 → 0.3.34

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.
@@ -0,0 +1,171 @@
1
+ /**
2
+ * agent-identity-store.ts — 统一 Agent Identity 源 (2026-08-06)
3
+ *
4
+ * 解决 CLI 状态栏与 Web UI 智能体名称不一致的根因:
5
+ * 多个地方各自维护 agent 名称 → 统一从这里读。
6
+ *
7
+ * 数据流:
8
+ * channels.json (唯一数据源)
9
+ * │
10
+ * AgentIdentityStore (读取 + active 持久化)
11
+ * │
12
+ * CLI 状态栏 / /channel 命令 ── Web UI (GET /active-channel + /channels)
13
+ *
14
+ * active channel 持久化: ~/.bolloon/active-channel.json (CLI 与 Web 共用,
15
+ * 重启后自动恢复上次 channel / identity)。
16
+ */
17
+ import * as fs from 'fs/promises';
18
+ import * as path from 'path';
19
+ const HOME = () => process.env.HOME || '/tmp';
20
+ /** channels.json 路径 (与 server-types.ts CHANNELS_PATH 对齐) */
21
+ export function channelsPaths(home = HOME()) {
22
+ return [
23
+ path.join(home, '.bolloon', 'sessions', 'channels.json'),
24
+ path.join(home, '.bolloon', 'channels.json'),
25
+ ];
26
+ }
27
+ export function activeChannelFile(home = HOME()) {
28
+ return path.join(home, '.bolloon', 'active-channel.json');
29
+ }
30
+ export class AgentIdentityStore {
31
+ home;
32
+ channels = [];
33
+ activeChannelId = null;
34
+ loaded = false;
35
+ constructor(home = HOME()) {
36
+ this.home = home;
37
+ }
38
+ /** 读 channels.json + active-channel.json (幂等, 可重复调) */
39
+ async load() {
40
+ for (const p of channelsPaths(this.home)) {
41
+ try {
42
+ const raw = JSON.parse(await fs.readFile(p, 'utf-8'));
43
+ if (Array.isArray(raw)) {
44
+ this.channels = raw;
45
+ break;
46
+ }
47
+ }
48
+ catch { /* 该路径不存在则试下一个 */ }
49
+ }
50
+ try {
51
+ const a = JSON.parse(await fs.readFile(activeChannelFile(this.home), 'utf-8'));
52
+ if (a && typeof a.channelId === 'string')
53
+ this.activeChannelId = a.channelId;
54
+ }
55
+ catch { /* 无 active 记录 */ }
56
+ this.loaded = true;
57
+ }
58
+ get isLoaded() { return this.loaded; }
59
+ get rawChannels() { return this.channels; }
60
+ /** channel → AgentIdentity (persona.name 优先) */
61
+ toIdentity(c) {
62
+ const name = c.persona?.name?.trim() || c.name || c.agentId || 'agent';
63
+ return {
64
+ id: c.id,
65
+ name,
66
+ channelId: c.id,
67
+ avatar: c.persona?.name ? undefined : undefined,
68
+ metadata: {
69
+ agentId: c.agentId,
70
+ did: c.did,
71
+ persona: c.persona,
72
+ publicKey: c.publicKey,
73
+ cid: c.cid,
74
+ ipnsName: c.ipnsName,
75
+ },
76
+ };
77
+ }
78
+ /** 全部智能体身份 (channel 顺序 = 索引顺序, 1-based) */
79
+ getIdentities() {
80
+ return this.channels.map(c => this.toIdentity(c));
81
+ }
82
+ /**
83
+ * 解析 /channel <query>:
84
+ * 纯数字 → number (1-based 索引)
85
+ * 匹配 id → id (完整或前缀)
86
+ * 匹配 name → name (大小写不敏感)
87
+ * 优先级: number > id > name
88
+ */
89
+ async resolve(query) {
90
+ if (!this.loaded)
91
+ await this.load();
92
+ const q = String(query || '').trim();
93
+ if (!q)
94
+ return null;
95
+ // 1. number: 纯数字 → 1-based 索引
96
+ if (/^\d+$/.test(q)) {
97
+ const idx = parseInt(q, 10);
98
+ const ch = this.channels[idx - 1];
99
+ if (ch)
100
+ return { identity: this.toIdentity(ch), channel: ch, match: 'number', index: idx };
101
+ }
102
+ // 2. id: 完整或前缀
103
+ let found = this.channels.find(c => c.id === q);
104
+ if (found) {
105
+ const idx = this.channels.indexOf(found) + 1;
106
+ return { identity: this.toIdentity(found), channel: found, match: 'id', index: idx };
107
+ }
108
+ found = this.channels.find(c => c.id.startsWith(q));
109
+ if (found) {
110
+ const idx = this.channels.indexOf(found) + 1;
111
+ return { identity: this.toIdentity(found), channel: found, match: 'id', index: idx };
112
+ }
113
+ // 3. name: persona.name / channel.name 大小写不敏感 (含子串)
114
+ const ql = q.toLowerCase();
115
+ found = this.channels.find(c => {
116
+ const names = [c.persona?.name, c.name].filter(Boolean).map(n => String(n).toLowerCase());
117
+ return names.some(n => n === ql || n.includes(ql));
118
+ });
119
+ if (found) {
120
+ const idx = this.channels.indexOf(found) + 1;
121
+ return { identity: this.toIdentity(found), channel: found, match: 'name', index: idx };
122
+ }
123
+ return null;
124
+ }
125
+ /** 当前 active 身份 (无 active 或找不到时 → 第一个 channel, 与 Web UI 默认一致) */
126
+ async getActive() {
127
+ if (!this.loaded)
128
+ await this.load();
129
+ if (this.activeChannelId) {
130
+ const ch = this.channels.find(c => c.id === this.activeChannelId);
131
+ if (ch)
132
+ return this.toIdentity(ch);
133
+ }
134
+ return this.channels.length > 0 ? this.toIdentity(this.channels[0]) : null;
135
+ }
136
+ /** 切换 active channel + 持久化 (CLI /channel 与 Web POST /active-channel 共用) */
137
+ async setActive(channelId) {
138
+ if (!this.loaded)
139
+ await this.load();
140
+ const ch = this.channels.find(c => c.id === channelId);
141
+ if (!ch)
142
+ return null;
143
+ this.activeChannelId = channelId;
144
+ try {
145
+ await fs.mkdir(path.dirname(activeChannelFile(this.home)), { recursive: true });
146
+ await fs.writeFile(activeChannelFile(this.home), JSON.stringify({ channelId, updatedAt: Date.now() }, null, 2), 'utf-8');
147
+ }
148
+ catch (e) {
149
+ console.warn(`[identity-store] 持久化 active channel 失败 (非致命): ${e?.message}`);
150
+ }
151
+ return this.toIdentity(ch);
152
+ }
153
+ /** 列出所有 channel 供 /channel 无参显示 */
154
+ async listForDisplay() {
155
+ if (!this.loaded)
156
+ await this.load();
157
+ const active = this.activeChannelId;
158
+ return this.channels.map((c, i) => ({
159
+ index: i + 1,
160
+ identity: this.toIdentity(c),
161
+ active: c.id === active,
162
+ }));
163
+ }
164
+ }
165
+ let _store = null;
166
+ /** 单例 (CLI / server 共用; 测试可 new AgentIdentityStore(tmpHome)) */
167
+ export function getIdentityStore() {
168
+ if (!_store)
169
+ _store = new AgentIdentityStore();
170
+ return _store;
171
+ }
@@ -2574,6 +2574,17 @@ export function registerWalletTools(ctx) {
2574
2574
  catch (lspErr) {
2575
2575
  console.warn('[registerTools] LSP 工具注册失败 (非致命):', lspErr);
2576
2576
  }
2577
+ // 2026-08-06: 注册 OrbitDB/CID 数据层工具 (cid_save/load/update/version/list/share + context 快照 + UI CID)
2578
+ try {
2579
+ import('../orbitdb/agent-tools.js').then(({ registerOrbitdbTools }) => {
2580
+ registerOrbitdbTools(ctx);
2581
+ }).catch((e) => {
2582
+ console.warn('[registerTools] OrbitDB 工具注册失败 (非致命):', e);
2583
+ });
2584
+ }
2585
+ catch (odbErr) {
2586
+ console.warn('[registerTools] OrbitDB 工具注册失败 (非致命):', odbErr);
2587
+ }
2577
2588
  }
2578
2589
  /**
2579
2590
  * 监听 P2P 消息 (远端 + 本地 inbox bus), 把消息存到 _inboxMessages 供 check_inbox 读.
@@ -251,6 +251,10 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
251
251
  const [tiKey, setTiKey] = useState(0);
252
252
  // 全局: 思考动画控制
253
253
  useEffect(() => {
254
+ // 2026-08-06: 防御 — 某些环境 (tsx/完整 CLI 初始化) 下 stdin 会处于 paused,
255
+ // 不恢复则 useInput 收不到任何输入 (实测 isPaused=true, listeners=0)
256
+ if (process.stdin.isPaused())
257
+ process.stdin.resume();
254
258
  globalThis.__inkSetThinking = (v) => setThinking(v);
255
259
  globalThis.__inkAppend = (line) => {
256
260
  setMsgs(prev => [...prev, line]);
@@ -308,6 +312,15 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
308
312
  acceptMention(it);
309
313
  return;
310
314
  }
315
+ if (key.return && filtered.length === 0) {
316
+ // 弹窗无匹配项: Enter = 提交当前输入 (否则 /channel 无参 + Enter 永远提交不了 — 2026-08-06)
317
+ const v = input.trim();
318
+ if (v)
319
+ onSubmit(v);
320
+ else
321
+ setDismissed(mentionKey);
322
+ return;
323
+ }
311
324
  if (key.escape) {
312
325
  if (tabState)
313
326
  setTabState(null);
@@ -346,11 +359,11 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
346
359
  });
347
360
  return;
348
361
  }
349
- if (_input && !key.ctrl && !key.meta) {
362
+ if (_input && !key.ctrl && !key.meta && !key.return) {
350
363
  setInput(cur => cur + _input);
351
364
  return;
352
365
  }
353
- return; // 其余键忽略
366
+ return; // 其余键忽略 (return/tab/esc 等由 TextInput 或上层处理)
354
367
  }
355
368
  // ── 正常模式 ──
356
369
  // Tab 命令补齐 (无触发符的普通 token 也补)
@@ -151,11 +151,14 @@ const SKIP_DIRS = new Set([
151
151
  'coverage', '.next', '.nuxt', '.venv', 'venv', '__pycache__', 'tmp', 'release',
152
152
  '.bolloon', '.boll', '.turbo', '.idea', '.vscode', 'bin', 'lib', 'assets',
153
153
  ]);
154
- /** cwd 有限深度 BFS, 只收文件, 上限 cap 个, 排序稳定 */
155
- export async function loadFiles(_query, base = process.cwd(), maxDepth = 3, cap = 400) {
154
+ /** cwd 有限深度 BFS, 只收文件, 上限 cap 个, 排序稳定; 超时兜底 (冷缓存 fs.readdir 偶发挂起, 2026-08-05) */
155
+ export async function loadFiles(_query, base = process.cwd(), maxDepth = 3, cap = 400, timeoutMs = 5000) {
156
156
  const out = [];
157
+ const deadline = Date.now() + timeoutMs;
157
158
  const stack = [{ dir: base, depth: 0 }];
158
159
  while (stack.length > 0 && out.length < cap) {
160
+ if (Date.now() > deadline)
161
+ break; // 超时返回已收集部分, 弹窗不卡"扫描中"
159
162
  const { dir, depth } = stack.pop();
160
163
  let entries;
161
164
  try {
package/dist/index.js CHANGED
@@ -360,15 +360,24 @@ const pendingQueue = [];
360
360
  let cliStartTime = 0;
361
361
  let cliModelName = '…';
362
362
  let cliAgentName = '…';
363
+ let cliActiveChannelId = null;
363
364
  function fmtDuration(ms) {
364
365
  const s = Math.floor(ms / 1000);
366
+ if (s < 60)
367
+ return `${s}s`;
365
368
  const m = Math.floor(s / 60);
369
+ if (m < 60)
370
+ return `${m}m ${s % 60}s`;
366
371
  const h = Math.floor(m / 60);
367
- if (h > 0)
368
- return `${h}h${m % 60}m`;
369
- if (m > 0)
370
- return `${m}m${s % 60}s`;
371
- return `${s}s`;
372
+ return `${h}h ${m % 60}m`;
373
+ }
374
+ /** 状态栏: 模型 │ 当前智能体 ( channel) │ ⏱ 时间 │ 上下文进度条 (模块级, CLI/Web 共用) */
375
+ function getStatus() {
376
+ const barLen = 12;
377
+ const filled = Math.round((cliContextPct / 100) * barLen);
378
+ const bar = '█'.repeat(filled) + '░'.repeat(barLen - filled);
379
+ const agentPart = cliActiveChannelId ? `${cliAgentName} ${C_DIM}(ch:${cliActiveChannelId.slice(0, 10)})${RESET}` : cliAgentName;
380
+ return `${C_ACCENT}${cliModelName}\x1b[0m\x1b[90m │\x1b[0m ${agentPart} \x1b[90m│\x1b[0m ⏱ ${fmtDuration(Date.now() - cliStartTime)}\x1b[90m │\x1b[0m ${bar} ${cliContextPct}%`;
372
381
  }
373
382
  function statusBarLine() {
374
383
  const dur = cliStartTime ? fmtDuration(Date.now() - cliStartTime) : '0s';
@@ -412,14 +421,22 @@ async function startCLI(comm) {
412
421
  cliModelName = foundProvider ? foundProvider[1] : '未配置';
413
422
  cliAgentName = agentIdentity?.name || 'bolloon';
414
423
  cliStartTime = Date.now();
424
+ // 恢复上次 active channel (session 恢复: CLI 与 Web 共用 active-channel.json)
425
+ try {
426
+ const { getIdentityStore } = await import('./agents/agent-identity-store.js');
427
+ const store = getIdentityStore();
428
+ await store.load();
429
+ const active = await store.getActive();
430
+ if (active) {
431
+ cliAgentName = active.name;
432
+ cliActiveChannelId = active.channelId ?? null;
433
+ }
434
+ }
435
+ catch {
436
+ /* 无 channels/active 记录时保持默认 */
437
+ }
415
438
  // 进入 Ink TUI 输入循环
416
439
  const initialStatus = `${C_ACCENT}${cliModelName}\x1b[0m\x1b[90m │\x1b[0m ${cliAgentName} \x1b[90m│\x1b[0m ⏱ 0s`;
417
- const getStatus = () => {
418
- const barLen = 12;
419
- const filled = Math.round((cliContextPct / 100) * barLen);
420
- const bar = '█'.repeat(filled) + '░'.repeat(barLen - filled);
421
- return `${C_ACCENT}${cliModelName}\x1b[0m\x1b[90m │\x1b[0m ${cliAgentName} \x1b[90m│\x1b[0m ⏱ ${fmtDuration(Date.now() - cliStartTime)}\x1b[90m │\x1b[0m ${bar} ${cliContextPct}%`;
422
- };
423
440
  startInk((text) => { processInput(text, comm); }, initialStatus, getStatus);
424
441
  // Wait on a promise that resolves on Ctrl+C / 双击 Esc
425
442
  // (ink-app 的 requestExit 调 __inkRequestExit → resolve, 清理后 process.exit)
@@ -461,6 +478,49 @@ async function processInput(input, comm) {
461
478
  appendLine(`${C_DIM}──${RESET}`);
462
479
  return;
463
480
  }
481
+ // /channel — 切换当前智能体 (agent channel), 参数 name/id/number 自动解析
482
+ if (trimmed.toLowerCase().startsWith('/channel')) {
483
+ const q = trimmed.slice('/channel'.length).trim();
484
+ try {
485
+ const { getIdentityStore } = await import('./agents/agent-identity-store.js');
486
+ const store = getIdentityStore();
487
+ await store.load();
488
+ if (!q) {
489
+ // 无参: 列出所有 channel + active
490
+ const list = await store.listForDisplay();
491
+ const active = await store.getActive();
492
+ if (list.length === 0) {
493
+ appendLine(`${C_DIM}暂无智能体 channel (channels.json 为空)${RESET}`);
494
+ return;
495
+ }
496
+ appendLine(`${C_ACCENT}智能体列表:${RESET} (${active ? `当前: ${active.name}` : ''})`);
497
+ for (const { index, identity, active: isActive } of list) {
498
+ const mark = isActive ? '●' : '○';
499
+ appendLine(` ${mark} ${index}. ${identity.name} ${C_DIM}${identity.id.slice(0, 24)}${RESET}`);
500
+ }
501
+ appendLine(`${C_DIM}用法: /channel <名字|id|序号>${RESET}`);
502
+ return;
503
+ }
504
+ const r = await store.resolve(q);
505
+ if (!r) {
506
+ appendLine(`${C_ERROR}未找到智能体: '${q}'${RESET} (可用 /channel 查看列表)`);
507
+ return;
508
+ }
509
+ const prev = await store.getActive();
510
+ await store.setActive(r.channel.id);
511
+ cliAgentName = r.identity.name;
512
+ cliActiveChannelId = r.channel.id;
513
+ inkSetStatus(getStatus()); // 触发状态栏立即重绘 (无需等 1s 定时器)
514
+ const extra = prev && prev.name !== r.identity.name ? ` (从 ${prev.name} 切换)` : '';
515
+ appendLine(`${C_ACCENT}→ 当前智能体: ${r.identity.name}${RESET}${extra}`);
516
+ appendLine(`${C_DIM} channel: ${r.channel.id} [${r.match}]${RESET}`);
517
+ appendLine(`${C_DIM} persona: ${r.channel.persona?.description || r.channel.persona?.personality || '无'}${RESET}`);
518
+ }
519
+ catch (e) {
520
+ appendLine(`${C_ERROR}/channel 失败: ${String(e.message || e).slice(0, 200)}${RESET}`);
521
+ }
522
+ return;
523
+ }
464
524
  // /queue — 切换队列模式
465
525
  if (trimmed.toLowerCase() === '/queue') {
466
526
  queueMode = !queueMode;
@@ -493,6 +553,7 @@ async function processInput(input, comm) {
493
553
  appendLine(` ${C_ACCENT}!<cmd>${RESET} 执行终端命令 ${C_DIM}如 !ls -la${RESET}`);
494
554
  appendLine(` ${C_ACCENT}/queue${RESET} 切换队列模式 ${C_DIM}输入排队, 当前结束后自动执行${RESET}`);
495
555
  appendLine(` ${C_ACCENT}/dequeue${RESET} 出队一条`);
556
+ appendLine(` ${C_ACCENT}/channel [名字|id|序号]${RESET} 切换当前智能体 ${C_DIM}无参列出所有; 支持名字/ID/序号三种解析${RESET}`);
496
557
  appendLine(` ${C_ACCENT}@名字${RESET} @ 命中智能体 ${C_DIM}弹出窗选择后发送给智能体${RESET}`);
497
558
  appendLine(` ${C_ACCENT}/名字${RESET} / 命中命令/技能/插件 ${C_DIM}输入 / 自动弹出${RESET}`);
498
559
  appendLine(` ${C_ACCENT}#路径${RESET} # 命中文件 ${C_DIM}输入 # 自动弹出文件列表${RESET}`);
@@ -0,0 +1,225 @@
1
+ /**
2
+ * agent-tools.ts — OrbitDB/CID 数据层的 agent 工具注册 (2026-08-06)
3
+ *
4
+ * 注册 10 个工具 (懒加载: 首次调用才拉起 helia, 不阻塞启动):
5
+ * cid_save / cid_load / cid_update / cid_version / cid_list / cid_share
6
+ * context_save_snapshot / context_restore
7
+ * ui_save_component / ui_load_component
8
+ *
9
+ * 对应 Step 7 能力: agent.createMemory (cid_save type=memory),
10
+ * context.saveSnapshot (context_save_snapshot), ui.loadCID (ui_load_component),
11
+ * share(cid) (cid_share).
12
+ */
13
+ /** 包装: 动态 import cid-database (首调拉起 helia) */
14
+ async function db() {
15
+ const { getCIDDatabase } = await import('./cid-database.js');
16
+ return getCIDDatabase();
17
+ }
18
+ async function contextStore() {
19
+ const { getContextStore } = await import('./context-store.js');
20
+ return getContextStore();
21
+ }
22
+ async function uiStore() {
23
+ const { getUICidStore } = await import('./ui-cid.js');
24
+ return getUICidStore();
25
+ }
26
+ const fmt = (o) => JSON.stringify(o, null, 2).slice(0, 3000);
27
+ export function registerOrbitdbTools(ctx) {
28
+ ctx.tools.set('cid_save', {
29
+ name: 'cid_save',
30
+ description: '保存数据到去中心化数据库 (OrbitDB), 返回内容寻址 CID. type 支持 memory/context/state/ui/knowledge. 同内容同 CID, 可用 cid_load 读回、cid_share 分享、cid_update 更新版本.',
31
+ parameters: { agentId: '所属智能体 id (必填)', type: '记录类型: memory/context/state/ui/knowledge (必填)', content: '要保存的数据 (JSON 对象, 必填)', metadata: '可选元数据 (JSON 对象)' },
32
+ execute: async (args) => {
33
+ try {
34
+ const agentId = String(args.agentId || '').trim();
35
+ const type = String(args.type || '').trim();
36
+ if (!agentId || !type)
37
+ return { success: false, error: 'agentId 和 type 必填' };
38
+ const content = args.content ?? {};
39
+ const metadata = typeof args.metadata === 'object' && args.metadata ? args.metadata : undefined;
40
+ const rec = await (await db()).save({ agentId, type: type, content, metadata });
41
+ return { success: true, output: `✅ 已保存 (type=${type}):\n CID: ${rec.id}\n version: ${rec.version}\n 读回: cid_load(cid="${rec.id}")\n 分享: cid_share(cid="${rec.id}")` };
42
+ }
43
+ catch (e) {
44
+ return { success: false, error: `cid_save 失败: ${String(e.message || e).slice(0, 200)}` };
45
+ }
46
+ }
47
+ });
48
+ ctx.tools.set('cid_load', {
49
+ name: 'cid_load',
50
+ description: '按 CID 从去中心化数据库加载记录 (含跨节点分享的). 返回完整记录: id/agentId/timestamp/type/content/metadata/version.',
51
+ parameters: { cid: '记录 CID (必填)' },
52
+ execute: async (args) => {
53
+ try {
54
+ const cid = String(args.cid || '').trim();
55
+ if (!cid)
56
+ return { success: false, error: 'cid 必填' };
57
+ const rec = await (await db()).load(cid);
58
+ if (!rec)
59
+ return { success: false, error: `记录不存在: ${cid}` };
60
+ return { success: true, output: `📄 ${cid}\n${fmt(rec)}` };
61
+ }
62
+ catch (e) {
63
+ return { success: false, error: `cid_load 失败: ${String(e.message || e).slice(0, 200)}` };
64
+ }
65
+ }
66
+ });
67
+ ctx.tools.set('cid_update', {
68
+ name: 'cid_update',
69
+ description: '更新记录 → 生成新版本 (parentId 指向旧 CID). 用 cid_version 查看版本链.',
70
+ parameters: { cid: '旧记录 CID (必填)', content: '新内容 (JSON, 必填)', metadata: '可选新元数据' },
71
+ execute: async (args) => {
72
+ try {
73
+ const cid = String(args.cid || '').trim();
74
+ if (!cid)
75
+ return { success: false, error: 'cid 必填' };
76
+ const rec = await (await db()).update(cid, args.content ?? {}, typeof args.metadata === 'object' && args.metadata ? args.metadata : undefined);
77
+ if (!rec)
78
+ return { success: false, error: `记录不存在: ${cid}` };
79
+ return { success: true, output: `✅ 已更新 v${rec.version}:\n 新 CID: ${rec.id}\n 版本链: cid_version(cid="${rec.id}")` };
80
+ }
81
+ catch (e) {
82
+ return { success: false, error: `cid_update 失败: ${String(e.message || e).slice(0, 200)}` };
83
+ }
84
+ }
85
+ });
86
+ ctx.tools.set('cid_version', {
87
+ name: 'cid_version',
88
+ description: '查看记录的完整版本链 (从旧到新).',
89
+ parameters: { cid: '任意版本 CID (必填)' },
90
+ execute: async (args) => {
91
+ try {
92
+ const cid = String(args.cid || '').trim();
93
+ if (!cid)
94
+ return { success: false, error: 'cid 必填' };
95
+ const chain = await (await db()).version(cid);
96
+ if (chain.length === 0)
97
+ return { success: false, error: `记录不存在: ${cid}` };
98
+ return { success: true, output: `📚 版本链 (${chain.length} 个版本):\n${chain.map(r => ` v${r.version} ${r.id} ${new Date(r.timestamp).toISOString()}${r.parentId ? '' : ' (根)'}`).join('\n')}` };
99
+ }
100
+ catch (e) {
101
+ return { success: false, error: `cid_version 失败: ${String(e.message || e).slice(0, 200)}` };
102
+ }
103
+ }
104
+ });
105
+ ctx.tools.set('cid_list', {
106
+ name: 'cid_list',
107
+ description: '列出数据库记录 (可按 agentId / type 过滤). 适合查看某个智能体的全部记忆/状态/组件.',
108
+ parameters: { agentId: '可选: 按智能体过滤', type: '可选: 按类型过滤 (memory/context/state/ui/knowledge)' },
109
+ execute: async (args) => {
110
+ try {
111
+ const agentId = String(args.agentId || '').trim() || undefined;
112
+ const type = String(args.type || '').trim() || undefined;
113
+ const list = await (await db()).list({ agentId, type });
114
+ if (list.length === 0)
115
+ return { success: true, output: '📭 无记录' };
116
+ return { success: true, output: `📋 ${list.length} 条记录:\n${list.slice(-20).map(r => ` v${r.version} [${r.type}] ${r.id.slice(0, 24)}… ${new Date(r.timestamp).toISOString().slice(0, 19)}`).join('\n')}` };
117
+ }
118
+ catch (e) {
119
+ return { success: false, error: `cid_list 失败: ${String(e.message || e).slice(0, 200)}` };
120
+ }
121
+ }
122
+ });
123
+ ctx.tools.set('cid_share', {
124
+ name: 'cid_share',
125
+ description: '分享记录: 把记录块写入 IPFS 网络 (helia blockstore), 返回 bolloon-cid:// 引用, 其他节点可用 cid_load 拉取.',
126
+ parameters: { cid: '记录 CID (必填)' },
127
+ execute: async (args) => {
128
+ try {
129
+ const cid = String(args.cid || '').trim();
130
+ if (!cid)
131
+ return { success: false, error: 'cid 必填' };
132
+ const ref = await (await db()).share(cid);
133
+ return { success: true, output: `🔗 已分享: ${ref}` };
134
+ }
135
+ catch (e) {
136
+ return { success: false, error: `cid_share 失败: ${String(e.message || e).slice(0, 200)}` };
137
+ }
138
+ }
139
+ });
140
+ ctx.tools.set('context_save_snapshot', {
141
+ name: 'context_save_snapshot',
142
+ description: '保存 Context OS 快照: 抓取当前资产层 + 可选记忆摘要/focus → CID 版本化. 用 context_restore 恢复.',
143
+ parameters: { agentId: '智能体 id (必填)', memorySummary: '可选: 当前记忆摘要', focus: '可选: 当前 focus' },
144
+ execute: async (args) => {
145
+ try {
146
+ const agentId = String(args.agentId || '').trim();
147
+ if (!agentId)
148
+ return { success: false, error: 'agentId 必填' };
149
+ const store = await contextStore();
150
+ const snap = await store.captureCurrentContext(agentId, {
151
+ memorySummary: String(args.memorySummary || '').trim() || undefined,
152
+ focus: String(args.focus || '').trim() || undefined,
153
+ });
154
+ const rec = await store.saveSnapshot(snap);
155
+ return { success: true, output: `📸 Context 快照已保存:\n CID: ${rec.id}\n layers: ${Object.keys(snap.layers).filter(k => snap.layers[k].length).length} 层有资产\n 恢复: context_restore(agentId="${agentId}")` };
156
+ }
157
+ catch (e) {
158
+ return { success: false, error: `context_save_snapshot 失败: ${String(e.message || e).slice(0, 200)}` };
159
+ }
160
+ }
161
+ });
162
+ ctx.tools.set('context_restore', {
163
+ name: 'context_restore',
164
+ description: '恢复某智能体最近一次 Context 快照 (含资产层/记忆摘要/focus). 用于跨会话恢复上下文.',
165
+ parameters: { agentId: '智能体 id (必填)' },
166
+ execute: async (args) => {
167
+ try {
168
+ const agentId = String(args.agentId || '').trim();
169
+ if (!agentId)
170
+ return { success: false, error: 'agentId 必填' };
171
+ const snap = await (await contextStore()).restoreContext(agentId);
172
+ if (!snap)
173
+ return { success: false, error: `无快照: ${agentId}` };
174
+ return { success: true, output: `♻️ 已恢复 ${agentId} 的 Context 快照 (${new Date(snap.capturedAt).toISOString()}):\n${fmt(snap).slice(0, 2500)}` };
175
+ }
176
+ catch (e) {
177
+ return { success: false, error: `context_restore 失败: ${String(e.message || e).slice(0, 200)}` };
178
+ }
179
+ }
180
+ });
181
+ ctx.tools.set('ui_save_component', {
182
+ name: 'ui_save_component',
183
+ description: '保存 UI 组件到去中心化存储 (CID 化). code 是 React 函数组件源码 (function Name(props){ return React.createElement(...) }), framework 默认 react. 之后可用 ui_load_component 按 CID 动态加载渲染.',
184
+ parameters: { agentId: '智能体 id (必填)', name: '组件名 (必填)', code: 'React 组件源码 (必填)', framework: '可选: react/vanilla', theme: '可选: 主题 JSON', description: '可选: 组件说明' },
185
+ execute: async (args) => {
186
+ try {
187
+ const agentId = String(args.agentId || '').trim();
188
+ const name = String(args.name || '').trim();
189
+ const code = String(args.code || '').trim();
190
+ if (!agentId || !name || !code)
191
+ return { success: false, error: 'agentId/name/code 必填' };
192
+ const rec = await (await uiStore()).saveComponent(agentId, {
193
+ name,
194
+ code,
195
+ framework: args.framework === 'vanilla' ? 'vanilla' : 'react',
196
+ theme: typeof args.theme === 'object' && args.theme ? args.theme : undefined,
197
+ description: String(args.description || '').trim() || undefined,
198
+ });
199
+ return { success: true, output: `🖼️ UI 组件已保存: ${name}\n CID: ${rec.id}\n 加载: ui_load_component(cid="${rec.id}")` };
200
+ }
201
+ catch (e) {
202
+ return { success: false, error: `ui_save_component 失败: ${String(e.message || e).slice(0, 200)}` };
203
+ }
204
+ }
205
+ });
206
+ ctx.tools.set('ui_load_component', {
207
+ name: 'ui_load_component',
208
+ description: '按 CID 加载 UI 组件定义 (代码/theme/propsSchema). 配合 React 前端可动态渲染.',
209
+ parameters: { cid: '组件 CID (必填)' },
210
+ execute: async (args) => {
211
+ try {
212
+ const cid = String(args.cid || '').trim();
213
+ if (!cid)
214
+ return { success: false, error: 'cid 必填' };
215
+ const def = await (await uiStore()).loadComponent(cid);
216
+ if (!def)
217
+ return { success: false, error: `组件不存在: ${cid}` };
218
+ return { success: true, output: `🖼️ ${def.name} (${def.framework})\n CID: ${cid}\n theme: ${def.theme ? fmt(def.theme) : '无'}\n code:\n${String(def.code).slice(0, 1500)}` };
219
+ }
220
+ catch (e) {
221
+ return { success: false, error: `ui_load_component 失败: ${String(e.message || e).slice(0, 200)}` };
222
+ }
223
+ }
224
+ });
225
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * cid-database.ts — 统一 CID 数据库层: CIDDatabase 接口 + OrbitDBAdapter (2026-08-06)
3
+ *
4
+ * 数据模型 (用户设计):
5
+ * {
6
+ * id: CID, // 内容寻址 CID (dag-cbor encode + sha2-256, 内容不变 CID 不变)
7
+ * agentId: string,
8
+ * timestamp: number,
9
+ * type: "memory" | "context" | "state" | "ui" | "knowledge",
10
+ * content: object,
11
+ * metadata: object,
12
+ * version: number, // 版本号 (update 递增)
13
+ * parentId?: string // 上一版本 CID (版本链)
14
+ * }
15
+ *
16
+ * 存储:
17
+ * - OrbitDB keyvalue store (持久化 ~/.bolloon/orbitdb/, 数据库名 bolloon-cid-store)
18
+ * - key = record.id (CID), 支持 save/load/update/version/list/share
19
+ * - CID 用 multiformats 本地计算 (不依赖 helia dag API), share() 时才把块放入 helia
20
+ */
21
+ import { CID } from 'multiformats/cid';
22
+ import * as dagCbor from '@ipld/dag-cbor';
23
+ import { sha256 } from 'multiformats/hashes/sha2';
24
+ import { concat as uint8Concat } from 'uint8arrays/concat';
25
+ import { createOrbitDB } from '@orbitdb/core';
26
+ import { createBolloonIpfs } from './ipfs-node.js';
27
+ import * as path from 'path';
28
+ import * as os from 'os';
29
+ /** 内容 → CID (dag-cbor, sha2-256, codec 0x71); 先 JSON 清洗 (dag-cbor 不支持 undefined) */
30
+ export async function contentToCid(obj) {
31
+ const cleaned = JSON.parse(JSON.stringify(obj)); // 丢弃 undefined 字段
32
+ const bytes = dagCbor.encode(cleaned);
33
+ const hash = await sha256.digest(bytes);
34
+ return CID.createV1(0x71, hash).toString();
35
+ }
36
+ const home = () => process.env.HOME || os.homedir() || '/tmp';
37
+ /**
38
+ * OrbitDB 后端实现。单例: 同一进程只建一个 (helia/OrbitDB 都是重量级节点)。
39
+ */
40
+ export class OrbitDBAdapter {
41
+ dataDir;
42
+ node = null;
43
+ db = null;
44
+ _orbitdb = null;
45
+ orbitdb;
46
+ constructor(dataDir = path.join(home(), '.bolloon', 'orbitdb')) {
47
+ this.dataDir = dataDir;
48
+ }
49
+ /** 懒初始化: 首次使用时启动 helia + OrbitDB + 打开 keyvalue store */
50
+ async ensure() {
51
+ if (this.db)
52
+ return;
53
+ this.node = await createBolloonIpfs(path.join(this.dataDir, 'ipfs'));
54
+ this._orbitdb = await createOrbitDB({
55
+ ipfs: this.node.helia,
56
+ directory: path.join(this.dataDir, 'stores'),
57
+ });
58
+ this.db = await this._orbitdb.open('bolloon-cid-store', { type: 'keyvalue' });
59
+ // 共享底层实例 (只读暴露)
60
+ this.orbitdb = this._orbitdb;
61
+ }
62
+ async save(data) {
63
+ await this.ensure();
64
+ // 内容寻址: CID 只基于业务内容 (agentId/type/content), 不含时间戳/版本 → 同内容同 CID
65
+ const record = {
66
+ id: await contentToCid({ agentId: data.agentId, type: data.type, content: data.content }),
67
+ agentId: data.agentId,
68
+ timestamp: Date.now(),
69
+ type: data.type,
70
+ content: data.content,
71
+ metadata: data.metadata ?? {},
72
+ version: 1,
73
+ dbAddress: this.db.address,
74
+ };
75
+ // OrbitDB 用 dag-cbor 编码 value, 不支持 undefined 字段 → put 前 JSON 清洗
76
+ await this.db.put(record.id, JSON.parse(JSON.stringify(record)));
77
+ return record;
78
+ }
79
+ async load(cid) {
80
+ await this.ensure();
81
+ const rec = await this.db.get(cid);
82
+ if (rec)
83
+ return rec;
84
+ // KV 无 → 尝试从 helia 拉块 (网络分享的 CID)
85
+ try {
86
+ const stream = this.node.helia.blockstore.get(CID.parse(cid));
87
+ let bytes = new Uint8Array(0);
88
+ for await (const chunk of stream)
89
+ bytes = uint8Concat([bytes, chunk]);
90
+ return dagCbor.decode(bytes);
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ async update(cid, content, metadata) {
97
+ await this.ensure();
98
+ const old = await this.load(cid);
99
+ if (!old)
100
+ return null;
101
+ const record = {
102
+ id: await contentToCid({ agentId: old.agentId, type: old.type, content }),
103
+ agentId: old.agentId,
104
+ timestamp: Date.now(),
105
+ type: old.type,
106
+ content,
107
+ metadata: metadata ?? old.metadata,
108
+ version: old.version + 1,
109
+ parentId: old.id,
110
+ dbAddress: this.db.address,
111
+ };
112
+ await this.db.put(record.id, JSON.parse(JSON.stringify(record)));
113
+ return record;
114
+ }
115
+ async version(cid) {
116
+ await this.ensure();
117
+ const chain = [];
118
+ let cur = await this.load(cid);
119
+ // 从目标 CID 往回找最老, 再正序返回
120
+ const rev = [];
121
+ let guard = 0;
122
+ while (cur && guard++ < 1000) {
123
+ rev.push(cur);
124
+ cur = cur.parentId ? await this.load(cur.parentId) : null;
125
+ }
126
+ return rev.reverse();
127
+ }
128
+ async list(filter) {
129
+ await this.ensure();
130
+ // OrbitDB keyvalue.all() 返回 [{ key, value, hash }] 数组
131
+ const all = (await this.db.all());
132
+ const records = all.map(e => e.value);
133
+ return records
134
+ .filter(r => {
135
+ if (!r || typeof r !== 'object')
136
+ return false;
137
+ if (filter?.agentId && r.agentId !== filter.agentId)
138
+ return false;
139
+ if (filter?.type && r.type !== filter.type)
140
+ return false;
141
+ return true;
142
+ })
143
+ .sort((a, b) => a.timestamp - b.timestamp);
144
+ }
145
+ async share(cid) {
146
+ await this.ensure();
147
+ const rec = await this.load(cid);
148
+ if (!rec)
149
+ throw new Error(`记录不存在: ${cid}`);
150
+ // 把记录块写入 helia blockstore, 供网络 peers 通过 DHT 拉取
151
+ await this.node.helia.blockstore.put(CID.parse(rec.id), dagCbor.encode(rec));
152
+ return `bolloon-cid://${rec.id}`;
153
+ }
154
+ async close() {
155
+ try {
156
+ await this._orbitdb?.stop();
157
+ }
158
+ catch { /* 忽略 */ }
159
+ try {
160
+ await this.node?.stop();
161
+ }
162
+ catch { /* 忽略 */ }
163
+ this.db = null;
164
+ this._orbitdb = null;
165
+ this.node = null;
166
+ }
167
+ }
168
+ /** 单例访问 (server/CLI 共享) */
169
+ let _adapter = null;
170
+ export function getCIDDatabase() {
171
+ if (!_adapter)
172
+ _adapter = new OrbitDBAdapter();
173
+ return _adapter;
174
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * context-store.ts — Context OS 的 CID 化适配层 (2026-08-06)
3
+ *
4
+ * 在现有 Context OS 文件夹体系之上叠加 CID 快照/版本/共享能力 (不动原实现):
5
+ * - saveSnapshot: 抓取当前资产层 (readContextAssets) → 存 CIDDatabase (type: 'context')
6
+ * - restoreContext: 按 agentId 恢复最近快照
7
+ * - contextVersions: 快照版本历史
8
+ * - sharedMemory: 多 agent 共享记忆 (type: 'memory' 记录跨 agent 可见)
9
+ *
10
+ * 架构: Agent → ContextStore → CIDDatabase → OrbitDB → CID → IPFS
11
+ */
12
+ import { getCIDDatabase, } from './cid-database.js';
13
+ import { readContextAssets } from '../bootstrap/context-os.js';
14
+ /** 快照 → 可用于恢复的上下文文本 (注入 prompt 用) */
15
+ export function formatSnapshot(s) {
16
+ const lines = [`[Context 快照 @${new Date(s.capturedAt).toISOString()}]`];
17
+ for (const [layer, assets] of Object.entries(s.layers)) {
18
+ if (assets.length)
19
+ lines.push(` ${layer}: ${assets.join(', ')}`);
20
+ }
21
+ if (s.memorySummary)
22
+ lines.push(` 记忆: ${s.memorySummary.slice(0, 200)}`);
23
+ if (s.focus)
24
+ lines.push(` focus: ${s.focus}`);
25
+ return lines.join('\n');
26
+ }
27
+ export class ContextStore {
28
+ db;
29
+ constructor(db = getCIDDatabase()) {
30
+ this.db = db;
31
+ }
32
+ /** 抓取当前 Context OS 资产层 → 快照 (与现有 readContextAssets 打通; ctx 可带 identity/channel) */
33
+ async captureCurrentContext(agentId, extra, ctx) {
34
+ const layers = {};
35
+ try {
36
+ const listings = await readContextAssets();
37
+ for (const l of listings) {
38
+ layers[l.layer] = (l.files ?? []).map(a => a.file);
39
+ }
40
+ }
41
+ catch {
42
+ /* 资产层读取失败不阻塞快照 */
43
+ }
44
+ return {
45
+ agentId,
46
+ layers,
47
+ memorySummary: extra?.memorySummary,
48
+ focus: extra?.focus,
49
+ capturedAt: Date.now(),
50
+ identity: ctx?.identity,
51
+ channelId: ctx?.channelId,
52
+ };
53
+ }
54
+ /** 保存快照 → CID 记录 (type: 'context') */
55
+ async saveSnapshot(snapshot) {
56
+ return this.db.save({
57
+ agentId: snapshot.agentId,
58
+ type: 'context',
59
+ content: snapshot,
60
+ metadata: { kind: 'context-snapshot' },
61
+ });
62
+ }
63
+ /** 恢复: agentId 最近一次快照 */
64
+ async restoreContext(agentId) {
65
+ const snaps = await this.db.list({ agentId, type: 'context' });
66
+ const latest = snaps[snaps.length - 1];
67
+ return latest ? latest.content : null;
68
+ }
69
+ /** 快照版本历史 (全量, 从旧到新) */
70
+ async contextVersions(agentId) {
71
+ return this.db.list({ agentId, type: 'context' });
72
+ }
73
+ /** 多 agent 共享记忆: 全部 memory 记录 (跨 agent 可见), 可指定 agentId */
74
+ async sharedMemory(agentId) {
75
+ return this.db.list(agentId ? { agentId, type: 'memory' } : { type: 'memory' });
76
+ }
77
+ /** 保存一条记忆 (多 agent 共享池) */
78
+ async saveMemory(agentId, content, metadata) {
79
+ return this.db.save({ agentId, type: 'memory', content, metadata: { ...metadata, kind: 'shared-memory' } });
80
+ }
81
+ /** 按 CID 恢复任意记录 (含跨节点分享的) */
82
+ async loadRecord(cid) {
83
+ return this.db.load(cid);
84
+ }
85
+ }
86
+ /** 单例 */
87
+ let _contextStore = null;
88
+ export function getContextStore() {
89
+ if (!_contextStore)
90
+ _contextStore = new ContextStore();
91
+ return _contextStore;
92
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * ipfs-node.ts — Bolloon 的 OrbitDB 底层 IPFS 节点工厂 (2026-08-06)
3
+ *
4
+ * 基于 helia 7:
5
+ * - createHelia() 内部已 withLibp2p 但**不传 opts** → 无法自定义 services
6
+ * (HeliaInit 没有 libp2p 字段, 传了也被丢弃, 实测服务列表仍是默认 13 个)
7
+ * - 正确姿势: createHeliaLight() (无 libp2p) + 手动 withLibp2p(helia, { services })
8
+ * - OrbitDB 的 P2P 同步依赖 ipfs.libp2p.services.pubsub (sync.js:113) → 必须加 gossipsub
9
+ * - libp2p 的 createLibp2p 是 { ...defaults, ...options } 浅合并: services 整个覆盖,
10
+ * 必须显式列出要保留的默认服务 (dht/identify/keychain/...)
11
+ * - withLibp2p().start() 之后 libp2p getter 才可用 (之前抛 NotStartedError)
12
+ *
13
+ * 跑法: npx tsx scripts/smoke-orbitdb.ts
14
+ */
15
+ import { createHeliaLight } from 'helia';
16
+ import { withLibp2p } from '@helia/libp2p';
17
+ import * as dagCbor from '@ipld/dag-cbor';
18
+ import * as dagJson from '@ipld/dag-json';
19
+ import * as json from 'multiformats/codecs/json';
20
+ import { sha512 } from 'multiformats/hashes/sha2';
21
+ import { gossipsub } from '@libp2p/gossipsub';
22
+ import { identify, identifyPush } from '@libp2p/identify';
23
+ import { kadDHT } from '@libp2p/kad-dht';
24
+ import { keychain } from '@libp2p/keychain';
25
+ import { autoNAT } from '@libp2p/autonat';
26
+ import { uPnPNAT } from '@libp2p/upnp-nat';
27
+ import { ping } from '@libp2p/ping';
28
+ import { mdns } from '@libp2p/mdns';
29
+ import { circuitRelayServer } from '@libp2p/circuit-relay-v2';
30
+ import { dcutr } from '@libp2p/dcutr';
31
+ import { http } from '@libp2p/http';
32
+ import { delegatedRoutingV1HttpApiClientContentRouting, delegatedRoutingV1HttpApiClientPeerRouting } from '@helia/delegated-routing-v1-http-api-client';
33
+ import { delegatedHTTPRoutingDefaults } from '@helia/delegated-routing-client';
34
+ import { autoTLS } from '@ipshipyard/libp2p-auto-tls';
35
+ import * as path from 'path';
36
+ import * as os from 'os';
37
+ /**
38
+ * 创建 Bolloon 用的 helia 节点 (libp2p 完整默认服务 + gossipsub pubsub)。
39
+ * dataDir 持久化节点身份/数据 (默认 ~/.bolloon/orbitdb-ipfs)。
40
+ */
41
+ export async function createBolloonIpfs(dataDir) {
42
+ const dir = dataDir ?? path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'orbitdb-ipfs');
43
+ // createHeliaLight 无 libp2p → withLibp2p 手动装配 (可传 services)
44
+ // codecs/hashers 照抄 createHelia 默认: OrbitDB 的 log entry 用 dag-cbor (codec 113),
45
+ // 不注册会报 "Could not load codec for 113"
46
+ const helia = withLibp2p(createHeliaLight({
47
+ codecs: [dagCbor, dagJson, json],
48
+ hashers: [sha512],
49
+ }), {
50
+ // 显式列出服务: createLibp2p 浅合并会覆盖默认 services
51
+ services: {
52
+ pubsub: gossipsub({ emitSelf: true }), // OrbitDB 同步必需; emitSelf 让单机也能 publish (否则 NoPeersSubscribedToTopic)
53
+ autoNAT: autoNAT(),
54
+ autoTLS: autoTLS(),
55
+ dcutr: dcutr(),
56
+ delegatedPeerRouting: delegatedRoutingV1HttpApiClientPeerRouting(delegatedHTTPRoutingDefaults()),
57
+ delegatedContentRouting: delegatedRoutingV1HttpApiClientContentRouting(delegatedHTTPRoutingDefaults()),
58
+ dht: kadDHT(),
59
+ identify: identify(),
60
+ identifyPush: identifyPush(),
61
+ keychain: keychain({ pass: 'bolloon-orbitdb-keychain-pass-2026' }),
62
+ ping: ping(),
63
+ relay: circuitRelayServer(),
64
+ upnp: uPnPNAT(),
65
+ mdns: mdns(),
66
+ http: http(),
67
+ },
68
+ });
69
+ await helia.start();
70
+ const peerId = helia.libp2p.peerId.toString();
71
+ return {
72
+ helia,
73
+ peerId,
74
+ start: async () => { await helia.start(); },
75
+ stop: async () => { await helia.stop(); },
76
+ };
77
+ }
78
+ /** 从地址字符串解析 OrbitDB 数据库地址的 database name */
79
+ export function dbNameFromAddress(address) {
80
+ const parts = address.split('/');
81
+ return parts[parts.length - 1] || address;
82
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * ui-cid.ts — UI 组件 CID 化层 (2026-08-06)
3
+ *
4
+ * 让 UI 组件也可以内容寻址:
5
+ * - saveComponent: 组件定义 (代码 + props schema + theme) → CIDDatabase (type: 'ui')
6
+ * - loadComponent: 按 CID 加载组件定义
7
+ * - versionComponent: 组件版本管理 (复用 CIDDatabase.update 版本链)
8
+ * - loadReactComponent: 从 CID 拉组件代码 → React 组件 (动态渲染)
9
+ *
10
+ * 架构: UI 组件 → UICidStore → CIDDatabase → OrbitDB/CID → IPFS
11
+ * 注: npm 无标准 "UI CID" 库, 按用户减法哲学自研轻量层 (数据层 node 通用,
12
+ * 浏览器渲染集成点留给 Web client)。
13
+ */
14
+ import React from 'react';
15
+ import { getCIDDatabase, } from './cid-database.js';
16
+ export class UICidStoreImpl {
17
+ db;
18
+ constructor(db = getCIDDatabase()) {
19
+ this.db = db;
20
+ }
21
+ async saveComponent(agentId, def) {
22
+ return this.db.save({
23
+ agentId,
24
+ type: 'ui',
25
+ content: def,
26
+ metadata: { kind: 'ui-component', framework: def.framework },
27
+ });
28
+ }
29
+ async loadComponent(cid) {
30
+ const rec = await this.db.load(cid);
31
+ if (!rec)
32
+ return null;
33
+ return rec.content;
34
+ }
35
+ async listComponents(agentId) {
36
+ return this.db.list(agentId ? { agentId, type: 'ui' } : { type: 'ui' });
37
+ }
38
+ async versionComponent(cid, code, extra) {
39
+ const old = await this.loadComponent(cid);
40
+ if (!old)
41
+ return null;
42
+ return this.db.update(cid, {
43
+ ...old,
44
+ ...extra,
45
+ code,
46
+ });
47
+ }
48
+ async loadReactComponent(cid) {
49
+ const def = await this.loadComponent(cid);
50
+ if (!def)
51
+ throw new Error(`组件不存在: ${cid}`);
52
+ if (def.framework !== 'react')
53
+ throw new Error(`不是 React 组件: ${def.framework}`);
54
+ // 动态构造: code 是函数组件源码 → new Function 编译 (受限环境: 无 module 作用域)
55
+ const factory = new Function('React', `return (${def.code})`);
56
+ const component = factory(React);
57
+ if (typeof component !== 'function')
58
+ throw new Error('组件代码必须返回 React 组件函数');
59
+ return { component, def };
60
+ }
61
+ }
62
+ /** 单例 */
63
+ let _uiStore = null;
64
+ export function getUICidStore() {
65
+ if (!_uiStore)
66
+ _uiStore = new UICidStoreImpl();
67
+ return _uiStore;
68
+ }
@@ -60,6 +60,10 @@ const TOOL_WHITELIST = new Set([
60
60
  'fetch_url', 'web_search',
61
61
  // 2026-08-02: 远端 channel 工具 (本地智能体 @ 远程交流)
62
62
  'list_remote_channels', 'send_to_remote_channel',
63
+ // 2026-08-06: OrbitDB/CID 数据层工具 (src/orbitdb/agent-tools.ts)
64
+ 'cid_save', 'cid_load', 'cid_update', 'cid_version', 'cid_list', 'cid_share',
65
+ 'context_save_snapshot', 'context_restore',
66
+ 'ui_save_component', 'ui_load_component',
63
67
  ]);
64
68
  export const gateWhitelist = { gate: 'whitelist', allowed: true };
65
69
  /**
@@ -1546,6 +1546,7 @@
1546
1546
  var channelNameEl = document.getElementById("channel-name");
1547
1547
  var eventSources = /* @__PURE__ */ new Map();
1548
1548
  var currentChannelId = null;
1549
+ var activeChannelId = null;
1549
1550
  var currentAgentId = "";
1550
1551
  var channels = [];
1551
1552
  var remoteChannels = [];
@@ -1676,6 +1677,18 @@
1676
1677
  channels.forEach((ch, i) => {
1677
1678
  console.log(` [${i}] ${ch.name} - did: "${ch.did}"`);
1678
1679
  });
1680
+ try {
1681
+ const ar = await fetch("/active-channel");
1682
+ const a = await ar.json();
1683
+ if (a && a.channelId) {
1684
+ activeChannelId = a.channelId;
1685
+ if (!currentChannelId) currentChannelId = a.channelId;
1686
+ document.title = `Bolloon \xB7 ${a.identity?.name || a.channelId}`;
1687
+ console.log("[\u52A0\u8F7D\u9891\u9053] active channel:", a.channelId, "\u2192", a.identity?.name);
1688
+ }
1689
+ } catch (e) {
1690
+ console.warn("[\u52A0\u8F7D\u9891\u9053] \u8BFB active channel \u5931\u8D25 (\u975E\u81F4\u547D):", e);
1691
+ }
1679
1692
  renderChannels();
1680
1693
  } catch (err) {
1681
1694
  console.error("[\u52A0\u8F7D\u9891\u9053] \u5931\u8D25:", err);
@@ -2182,18 +2195,18 @@ ${msg.text || ""}`, "ai", false, log);
2182
2195
  }
2183
2196
  renderChannels();
2184
2197
  }
2185
- function renderChannelsLite(activeChannelId, activeSessionId) {
2198
+ function renderChannelsLite(activeChannelId2, activeSessionId) {
2186
2199
  if (!channelList) return;
2187
2200
  channelList.querySelectorAll(".agent-row").forEach((row) => {
2188
2201
  const li = row.closest(".agent-group");
2189
2202
  const chId = li?.dataset.channelId;
2190
- row.classList.toggle("active", chId === activeChannelId);
2203
+ row.classList.toggle("active", chId === activeChannelId2);
2191
2204
  });
2192
- if (activeChannelId) expandedAgents.add(activeChannelId);
2193
- const activeLi = channelList.querySelector(`.agent-group[data-channel-id="${activeChannelId}"]`);
2205
+ if (activeChannelId2) expandedAgents.add(activeChannelId2);
2206
+ const activeLi = channelList.querySelector(`.agent-group[data-channel-id="${activeChannelId2}"]`);
2194
2207
  if (activeLi) {
2195
2208
  activeLi.classList.add("expanded");
2196
- const ch = channels.find((c) => c.id === activeChannelId);
2209
+ const ch = channels.find((c) => c.id === activeChannelId2);
2197
2210
  activeLi.querySelectorAll(".session-item").forEach((sessLi) => {
2198
2211
  const sessId = sessLi.dataset.sessionId;
2199
2212
  const shouldBeActive = sessId === activeSessionId;
@@ -4031,6 +4031,52 @@ ${goalDesc}
4031
4031
  res.status(500).json({ error: err.message });
4032
4032
  }
4033
4033
  });
4034
+ // 2026-08-06: active channel (统一 Agent Identity) — CLI /channel 与 Web UI 共用
4035
+ // active-channel.json 是唯一持久化点: CLI 切换写, Web 读同一文件 → 两边一致
4036
+ const ACTIVE_CHANNEL_FILE = `${process.env.HOME || '/tmp'}/.bolloon/active-channel.json`;
4037
+ const CHANNELS_JSON = `${process.env.HOME || '/tmp'}/.bolloon/sessions/channels.json`;
4038
+ async function readActiveChannel() {
4039
+ try {
4040
+ const a = JSON.parse(await fs.readFile(ACTIVE_CHANNEL_FILE, 'utf-8'));
4041
+ if (!a || typeof a.channelId !== 'string')
4042
+ return { channelId: null };
4043
+ // 解析 identity name (persona.name 优先)
4044
+ let channels = [];
4045
+ try {
4046
+ channels = JSON.parse(await fs.readFile(CHANNELS_JSON, 'utf-8'));
4047
+ }
4048
+ catch { /* */ }
4049
+ const ch = channels.find((c) => c?.id === a.channelId);
4050
+ const name = ch?.persona?.name?.trim() || ch?.name || ch?.agentId || 'agent';
4051
+ return { channelId: a.channelId, identity: { id: a.channelId, name, channelId: a.channelId } };
4052
+ }
4053
+ catch {
4054
+ return { channelId: null };
4055
+ }
4056
+ }
4057
+ app.get('/active-channel', async (_req, res) => {
4058
+ try {
4059
+ const a = await readActiveChannel();
4060
+ res.json(a);
4061
+ }
4062
+ catch (err) {
4063
+ res.status(500).json({ error: err.message });
4064
+ }
4065
+ });
4066
+ app.post('/active-channel', async (req, res) => {
4067
+ try {
4068
+ const channelId = String(req.body?.channelId || '').trim();
4069
+ if (!channelId)
4070
+ return res.status(400).json({ error: 'channelId 必填' });
4071
+ await fs.mkdir(path.dirname(ACTIVE_CHANNEL_FILE), { recursive: true });
4072
+ await fs.writeFile(ACTIVE_CHANNEL_FILE, JSON.stringify({ channelId, updatedAt: Date.now() }, null, 2), 'utf-8');
4073
+ const a = await readActiveChannel();
4074
+ res.json(a);
4075
+ }
4076
+ catch (err) {
4077
+ res.status(500).json({ error: err.message });
4078
+ }
4079
+ });
4034
4080
  // v3: 列出本节点缓存的远端 channel (按 peerId 分组)
4035
4081
  app.get('/api/remote-channels', async (_req, res) => {
4036
4082
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.3.32",
3
+ "version": "0.3.34",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli-entry.js",
@@ -62,12 +62,14 @@
62
62
  "@libp2p/autonat": "^3.0.25",
63
63
  "@libp2p/circuit-relay-v2": "^4.2.9",
64
64
  "@libp2p/dcutr": "^3.0.20",
65
+ "@libp2p/gossipsub": "^16.1.1",
65
66
  "@libp2p/identify": "^4.1.6",
66
67
  "@libp2p/kad-dht": "^16.2.6",
67
68
  "@libp2p/tcp": "^11.0.20",
68
69
  "@libp2p/upnp-nat": "^4.0.20",
69
70
  "@multiformats/multiaddr": "^13.0.3",
70
71
  "@noble/hashes": "^1.3.0",
72
+ "@orbitdb/core": "^4.0.0",
71
73
  "@polymarket/client": "^0.2.0",
72
74
  "@rayhanadev/iroh": "^0.1.1",
73
75
  "@x402/aptos": "^2.20.0",
@@ -92,6 +94,7 @@
92
94
  "dotenv": "^17.4.2",
93
95
  "esbuild": "^0.24.0",
94
96
  "express": "^5.2.1",
97
+ "helia": "^7.1.3",
95
98
  "ink": "^4.4.1",
96
99
  "ink-text-input": "^5.0.0",
97
100
  "libp2p": "^3.3.0",