@bolloon/bolloon-agent 0.3.46 → 0.3.47

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.
@@ -1776,7 +1776,8 @@ export function registerBuiltinTools(ctx) {
1776
1776
  execute: async () => {
1777
1777
  try {
1778
1778
  const { readContextAssets, formatLayerListing } = await import('../bootstrap/context-os.js');
1779
- const listings = await readContextAssets();
1779
+ // 2026-08-09: agentId 分区读取 (每个智能体独立 Context OS)
1780
+ const listings = await readContextAssets(undefined, undefined, undefined, ctx.agentId || '');
1780
1781
  const total = listings.reduce((s, l) => s + l.fileCount, 0);
1781
1782
  if (total === 0)
1782
1783
  return { success: true, output: '📂 Context OS 资产层已就绪 (12+3 层), 当前暂无资产. 有价值的内容用 write_context_asset 写入对应层.' };
@@ -1816,7 +1817,7 @@ export function registerBuiltinTools(ctx) {
1816
1817
  tags = t.map(String);
1817
1818
  }
1818
1819
  catch { /* tags 解析失败 */ }
1819
- const r = await writeContextAsset({ layer, title, content, tags, domain: args.domain ? String(args.domain) : undefined });
1820
+ const r = await writeContextAsset({ layer, title, content, tags, domain: args.domain ? String(args.domain) : undefined }, undefined, ctx.agentId || '');
1820
1821
  if (!r.ok)
1821
1822
  return { success: false, error: r.error };
1822
1823
  if (r.skipped)
@@ -1840,7 +1841,8 @@ export function registerBuiltinTools(ctx) {
1840
1841
  const { readContextAssets, formatLayerListing } = await import('../bootstrap/context-os.js');
1841
1842
  const layer = args.layer ? String(args.layer) : undefined;
1842
1843
  const kw = args.keyword ? String(args.keyword) : undefined;
1843
- const listings = await readContextAssets(layer, kw);
1844
+ // 2026-08-09: agentId 分区读取 (每个智能体独立 Context OS)
1845
+ const listings = await readContextAssets(layer, kw, undefined, ctx.agentId || '');
1844
1846
  if (listings.every((l) => l.fileCount === 0)) {
1845
1847
  return { success: true, output: layer ? `📂 资产层 ${layer} 暂无资产` : '📂 资产层暂无资产' };
1846
1848
  }
@@ -48,7 +48,16 @@ const LAYER_KEYS = new Set(CONTEXT_OS_LAYERS.map((l) => l.key));
48
48
  export function getContextOsRoot(home = os.homedir()) {
49
49
  return path.join(home, '.bolloon', 'context-os');
50
50
  }
51
- export function getLayerDir(layer, home = os.homedir()) {
51
+ /**
52
+ * 2026-08-09: 层目录 — 支持按 agentId 分区 (每个智能体独立 Context OS).
53
+ * agentId 有值 → ~/.bolloon/context-os/<sanitizeAgentId>/<layer>
54
+ * agentId 空 → ~/.bolloon/context-os/<layer> (旧全局路径, 兼容)
55
+ */
56
+ export function getLayerDir(layer, home = os.homedir(), agentId) {
57
+ const safeId = String(agentId || '').replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
58
+ if (safeId) {
59
+ return path.join(getContextOsRoot(home), safeId, layer);
60
+ }
52
61
  return path.join(getContextOsRoot(home), layer);
53
62
  }
54
63
  /** 校验 layer 合法; 非法返回 null */
@@ -86,11 +95,11 @@ ${l.usage}
86
95
  → 阶段2 固化 (本层唯一位置) → 阶段3 索引化 (高频引用) → 阶段4 归档/删除.
87
96
  `;
88
97
  }
89
- export async function ensureContextOsDirs(home) {
98
+ export async function ensureContextOsDirs(home, agentId) {
90
99
  const root = getContextOsRoot(home);
91
100
  await fs.mkdir(root, { recursive: true });
92
101
  for (const l of CONTEXT_OS_LAYERS) {
93
- const dir = getLayerDir(l.key, home);
102
+ const dir = getLayerDir(l.key, home, agentId);
94
103
  await fs.mkdir(dir, { recursive: true });
95
104
  const readmePath = path.join(dir, 'README.md');
96
105
  try {
@@ -106,7 +115,7 @@ export async function ensureContextOsDirs(home) {
106
115
  * 文件名: <ts>-<slug>.md; frontmatter v2 (stage0 = 临时价值点, 待验证).
107
116
  * 幂等: 同层同 slug 已存在 → 跳过 (不重复造文件, Context OS §6 Step3).
108
117
  */
109
- export async function writeContextAsset(input, home) {
118
+ export async function writeContextAsset(input, home, agentId) {
110
119
  const layer = resolveLayer(input.layer);
111
120
  if (!layer) {
112
121
  return { ok: false, error: `layer 非法: '${input.layer}'. 合法: ${CONTEXT_OS_LAYERS.map((l) => l.key).join(' / ')}` };
@@ -117,15 +126,15 @@ export async function writeContextAsset(input, home) {
117
126
  const content = String(input.content || '').trim();
118
127
  if (!content)
119
128
  return { ok: false, error: 'content 必填' };
120
- await ensureContextOsDirs(home);
129
+ await ensureContextOsDirs(home, agentId);
121
130
  const now = new Date().toISOString();
122
131
  const ts = Date.now();
123
132
  const slug = slugify(title);
124
133
  const fileName = `${ts}-${slug}.md`;
125
- const filePath = path.join(getLayerDir(layer.key, home), fileName);
134
+ const filePath = path.join(getLayerDir(layer.key, home, agentId), fileName);
126
135
  // 幂等: 同 slug 已存在 → 跳过
127
136
  try {
128
- const files = await fs.readdir(getLayerDir(layer.key, home));
137
+ const files = await fs.readdir(getLayerDir(layer.key, home, agentId));
129
138
  if (files.some((f) => f.endsWith(`-${slug}.md`))) {
130
139
  return { ok: true, skipped: true, error: `同标题资产已存在 (${slug}.md), 未重复写入` };
131
140
  }
@@ -157,7 +166,7 @@ export async function writeContextAsset(input, home) {
157
166
  }
158
167
  }
159
168
  /** 列出层资产; layer 为空 → 全层汇总 */
160
- export async function readContextAssets(layer, keyword, home) {
169
+ export async function readContextAssets(layer, keyword, home, agentId) {
161
170
  const root = getContextOsRoot(home);
162
171
  const kw = String(keyword || '').trim().toLowerCase();
163
172
  const wanted = layer ? [resolveLayer(layer)].filter(Boolean).map((l) => l.key) : CONTEXT_OS_LAYERS.map((l) => l.key);
@@ -165,11 +174,11 @@ export async function readContextAssets(layer, keyword, home) {
165
174
  for (const key of wanted) {
166
175
  const l = resolveLayer(key);
167
176
  try {
168
- const files = (await fs.readdir(getLayerDir(key, home))).filter((f) => f.endsWith('.md') && f !== 'README.md');
177
+ const files = (await fs.readdir(getLayerDir(key, home, agentId))).filter((f) => f.endsWith('.md') && f !== 'README.md');
169
178
  const entries = [];
170
179
  for (const f of files) {
171
180
  try {
172
- const raw = await fs.readFile(path.join(getLayerDir(key, home), f), 'utf-8');
181
+ const raw = await fs.readFile(path.join(getLayerDir(key, home, agentId), f), 'utf-8');
173
182
  const titleM = raw.match(/^title:\s*(.+)$/m);
174
183
  const createdM = raw.match(/^created:\s*(.+)$/m);
175
184
  const title = titleM ? titleM[1].trim() : f.replace(/\.md$/, '');
@@ -45,14 +45,20 @@ function getPackageVersion() {
45
45
  }
46
46
  }
47
47
  const BOLLOON_VERSION = getPackageVersion();
48
- // ── 品牌图标: 顶部带圆标注的 0 (气球) ──────────────
48
+ // ── 品牌图标: 笑脸机器人 (2026-08-09, bolloon 色系填充) ──────
49
+ // 头: 主色边框 + 亮绿填充 (C_ACCENT_BG); 眼睛 ◉ / 嘴 ◡ 用亮色填充;
50
+ // 末行 BOLLOON 主色艺术字 (仅 printBanner 用, brandArtLines 会裁掉避免双 logo).
51
+ const ROBOT_HEAD = [
52
+ `${C_ACCENT} ╭───────╮${RESET}`,
53
+ `${C_ACCENT} ╭─╯${C_ACCENT_BG} ${C_WHITE}◉${C_ACCENT_BG} ${C_WHITE}◉${C_ACCENT_BG} ${RESET}${C_ACCENT}╰─╮${RESET}`,
54
+ `${C_ACCENT} │${C_ACCENT_BG} ${C_WHITE}◡${C_ACCENT_BG} ${RESET}${C_ACCENT}│${RESET}`,
55
+ `${C_ACCENT} ╰─╮${C_ACCENT_BG} ${RESET}${C_ACCENT}╭─╯${RESET}`,
56
+ `${C_ACCENT} ╰───┬───╯${RESET}`,
57
+ `${C_ACCENT} │${RESET}`,
58
+ ];
49
59
  export const BOLLOON_ICON = [
50
- `${C_ACCENT} ✦${RESET}`,
51
- `${C_ACCENT} ╱ ╲${RESET}`,
52
- `${C_ACCENT} ════◆════${RESET}`,
53
- `${C_ACCENT} ╲ ╱${RESET}`,
54
- `${C_ACCENT} ╲${RESET}`,
55
- `${C_ACCENT} ✦ ╲${RESET}`,
60
+ ...ROBOT_HEAD,
61
+ `${C_ACCENT}${BOLD} BOLLOON${RESET}`,
56
62
  ].join('\n');
57
63
  // ── 艺术字: BOLLOON (box 字体) + Bolloon Agent 副标题 ──
58
64
  export const BOLLOON_BANNER = [
@@ -64,9 +70,10 @@ export const BOLLOON_BANNER = [
64
70
  `${C_TEXT}${BOLD}╚═════╝ ╚═════╝ ╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝${RESET}`,
65
71
  `${C_DIM}Bolloon Agent v${BOLLOON_VERSION}${RESET}`,
66
72
  ].join('\n');
67
- /** 艺术字全部行 (图标在左, BOLLOON 艺术字在右), 供框内渲染 */
73
+ /** 艺术字全部行 (机器人头在左, BOLLOON 艺术字在右), 供框内渲染 */
68
74
  export function brandArtLines() {
69
- const icon = BOLLOON_ICON.split('\n');
75
+ // 2026-08-09: icon 只取机器人头 (裁掉末行 BOLLOON 文字), 避免和右侧 banner 双 logo
76
+ const icon = ROBOT_HEAD;
70
77
  const banner = BOLLOON_BANNER.split('\n');
71
78
  const gap = 2;
72
79
  const iconW = Math.max(1, ...icon.map(l => dispWidth(l)));
@@ -81,8 +88,9 @@ export function brandArtLines() {
81
88
  return rows;
82
89
  }
83
90
  export function printBanner(version) {
91
+ // 2026-08-09: 新品牌 logo = 笑脸机器人 (BOLLOON_ICON 自带 BOLLOON 文字),
92
+ // 不再叠加旧 box 字体 banner (避免双 logo)
84
93
  console.log(BOLLOON_ICON);
85
- console.log(BOLLOON_BANNER);
86
94
  if (version)
87
95
  console.log(`${C_DIM} Bolloon Agent v${version}${RESET}`);
88
96
  console.log(`${C_DIM} P2P AI Agent · 文档智能体${RESET}`);
package/dist/index.js CHANGED
@@ -271,12 +271,44 @@ async function bootstrapIroh(keypair, name) {
271
271
  // Agent 懒加载
272
272
  // ---------------------------------------------------------------------------
273
273
  let agent = null;
274
+ /** 2026-08-09: agent 当前绑定的 channel id (null = 默认 harness 身份) — 切换时据此重建 */
275
+ let agentBoundChannelId = null;
274
276
  let harness = null;
275
277
  let hybridMessenger = null;
276
278
  let agentIdentity = null;
277
279
  async function getAgent() {
278
- if (!agent) {
279
- const identityDoc = agentIdentity ? {
280
+ // 2026-08-09: agent 身份绑定当前 active channel — 切换 / 新建 channel 后重建.
281
+ // 旧实现: agent 全局单例 + peerId:'harness' 固定, 切 channel 身份不变 (bug).
282
+ // 新实现: channel 有 agentId/did/publicKey/persona 时按 channel 建 session,
283
+ // agentIdentity 同步更新, loadSessionKey 回灌该 channel 的历史.
284
+ const targetChannelId = cliActiveChannelId || null;
285
+ if (agent && agentBoundChannelId === targetChannelId)
286
+ return agent;
287
+ // 读取当前 active channel 的持久身份
288
+ let chIdentity = null;
289
+ if (targetChannelId) {
290
+ try {
291
+ const { getIdentityStore } = await import('./agents/agent-identity-store.js');
292
+ const store = getIdentityStore();
293
+ await store.load();
294
+ const ch = store.rawChannels.find((c) => c.id === targetChannelId);
295
+ if (ch)
296
+ chIdentity = ch;
297
+ }
298
+ catch { /* 读不到就退默认 */ }
299
+ }
300
+ let identityDoc;
301
+ if (chIdentity?.did && chIdentity.publicKey) {
302
+ // channel 已有持久 DID → 用 channel 身份
303
+ identityDoc = {
304
+ did: chIdentity.did,
305
+ name: chIdentity.persona?.name || chIdentity.name || 'agent',
306
+ publicKey: chIdentity.publicKey,
307
+ createdAt: Date.now(),
308
+ };
309
+ }
310
+ else if (agentIdentity) {
311
+ identityDoc = {
280
312
  did: agentIdentity.did,
281
313
  name: agentIdentity.name,
282
314
  publicKey: agentIdentity.publicKey,
@@ -285,15 +317,39 @@ async function getAgent() {
285
317
  p2pChannel: agentIdentity.p2pChannel,
286
318
  cid: agentIdentity.cid,
287
319
  ipnsName: agentIdentity.ipnsName
288
- } : undefined;
289
- agent = await createAgentSession({
290
- cwd: process.cwd(),
291
- peerId: 'harness',
292
- identityDoc
293
- });
320
+ };
321
+ }
322
+ else {
323
+ identityDoc = undefined;
324
+ }
325
+ const loadSessionKey = targetChannelId
326
+ ? `${targetChannelId}:${chIdentity?.currentSessionId || 'default'}`
327
+ : undefined;
328
+ agent = await createAgentSession({
329
+ cwd: process.cwd(),
330
+ peerId: targetChannelId ?? 'harness',
331
+ identityDoc,
332
+ // 2026-08-09: 透传 channel.agentId → persona docs 按 agent 加载 (身份真正变化)
333
+ agentId: chIdentity?.agentId || (targetChannelId ? undefined : agentIdentity?.name),
334
+ loadSessionKey,
335
+ });
336
+ agentBoundChannelId = targetChannelId;
337
+ // 同步 agentIdentity (状态栏 / 身份引用)
338
+ if (chIdentity) {
339
+ agentIdentity = {
340
+ did: chIdentity.did || agentIdentity?.did || '',
341
+ name: chIdentity.persona?.name || chIdentity.name || 'agent',
342
+ publicKey: chIdentity.publicKey || agentIdentity?.publicKey || '',
343
+ peerId: targetChannelId ?? undefined,
344
+ };
294
345
  }
295
346
  return agent;
296
347
  }
348
+ /** 强制重建 agent (切 channel / 新建 agent 后调用) */
349
+ function invalidateAgent() {
350
+ agent = null;
351
+ agentBoundChannelId = null;
352
+ }
297
353
  // ---------------------------------------------------------------------------
298
354
  // Dispatch
299
355
  // ---------------------------------------------------------------------------
@@ -575,6 +631,13 @@ async function processInput(input, comm) {
575
631
  await store.setActive(r.channel.id);
576
632
  cliAgentName = r.identity.name;
577
633
  cliActiveChannelId = r.channel.id;
634
+ // 2026-08-09: 切 channel 必须重建 agent session — 否则身份/记忆停留在旧 channel (bug 修复)
635
+ invalidateAgent();
636
+ // 立即重建 (提前建好, 避免下次输入才卡顿; 失败不阻塞切换)
637
+ try {
638
+ await getAgent();
639
+ }
640
+ catch { /* 非致命, 下次输入时再试 */ }
578
641
  inkSetStatus(getStatus()); // 触发状态栏立即重绘 (无需等 1s 定时器)
579
642
  const extra = prev && prev.name !== r.identity.name ? ` (从 ${prev.name} 切换)` : '';
580
643
  appendLine(`${C_ACCENT}→ 当前智能体: ${r.identity.name}${RESET}${extra}`);
@@ -599,38 +662,47 @@ async function processInput(input, comm) {
599
662
  const { getIdentityStore } = await import('./agents/agent-identity-store.js');
600
663
  const store = getIdentityStore();
601
664
  await store.load();
602
- const { readFile, writeFile, mkdir } = await import('fs/promises');
603
- const { join } = await import('path');
604
- const home = process.env.HOME || '/tmp';
605
- const channelsPath = join(home, '.bolloon', 'sessions', 'channels.json');
606
- let channels = [];
607
- try {
608
- const parsed = JSON.parse(await readFile(channelsPath, 'utf-8'));
609
- channels = Array.isArray(parsed) ? parsed : parsed?.channels || [];
610
- }
611
- catch { /* 首次无文件 */ }
612
- const dupName = channels.find((c) => c.name === name.trim());
665
+ // 2026-08-09: 复用 server-storage updateChannels 原子写 (互斥锁) 旧实现裸 readFile→push→writeFile
666
+ // 与 Web server 并发写 channels.json 互相覆盖 → 创建的 agent 重启后丢失 (bug 修复)
667
+ const { updateChannels } = await import('./web/server-storage.js');
668
+ const dupName = store.rawChannels.find((c) => c.name === name.trim());
613
669
  if (dupName) {
614
670
  appendLine(`${C_ERROR}同名智能体已存在: '${dupName.name}' (id=${dupName.id})${RESET}`);
615
671
  return;
616
672
  }
617
673
  const id = `ch_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
674
+ const agentId = `agent-${name.trim().toLowerCase().replace(/\s+/g, '-')}`;
618
675
  const ch = {
619
676
  id,
620
677
  name: name.trim(),
621
- agentId: `agent-${name.trim().toLowerCase().replace(/\s+/g, '-')}`,
678
+ agentId,
622
679
  createdAt: new Date().toISOString(),
623
680
  updatedAt: new Date().toISOString(),
624
681
  currentSessionId: 'default',
625
682
  };
626
683
  if (personaHint)
627
684
  ch.persona = { name: name.trim(), description: personaHint };
628
- channels.push(ch);
629
- await mkdir(join(home, '.bolloon', 'sessions'), { recursive: true });
630
- await writeFile(channelsPath, JSON.stringify(channels, null, 2), 'utf-8');
685
+ // 2026-08-09: 立即生成该 agent 的持久 DID 身份 (agent-keys/<agentId>.json)
686
+ // 与 server fixOneChannelDID 对齐, 保证 CLI 新建的 agent 身份稳定且归属用户 DID
687
+ try {
688
+ const { loadOrCreateAgentIdentity } = await import('./agents/agent-identity.js');
689
+ const idt = loadOrCreateAgentIdentity(agentId);
690
+ ch.did = idt.did;
691
+ ch.publicKey = idt.publicKey;
692
+ }
693
+ catch { /* DID 生成失败不阻塞创建 */ }
694
+ const channels = await updateChannels((chs) => [...chs, ch]);
695
+ // 刷新 store 缓存 (updateChannels 走了 server-storage, store 内存还是旧的)
696
+ await store.load();
631
697
  await store.setActive(id);
632
698
  cliAgentName = name.trim();
633
699
  cliActiveChannelId = id;
700
+ // 2026-08-09: 新建 agent 后立即重建 session — 否则新 agent 身份不加载 (bug 修复)
701
+ invalidateAgent();
702
+ try {
703
+ await getAgent();
704
+ }
705
+ catch { /* 非致命 */ }
634
706
  inkSetStatus(getStatus());
635
707
  appendLine(`${C_OK}✓ 已创建智能体 channel: ${name.trim()}${RESET} (${C_DIM}${id}${RESET})${personaHint ? `\n ${C_DIM}persona: ${personaHint}${RESET}` : ''}`);
636
708
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.3.46",
3
+ "version": "0.3.47",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli-entry.js",