@bolloon/bolloon-agent 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/agents/p2p-chat-tools.js +321 -0
  2. package/dist/agents/p2p-document-tools.js +121 -1
  3. package/dist/agents/pi-sdk.js +185 -0
  4. package/dist/agents/shell-guard.js +354 -0
  5. package/dist/agents/shell-tool.js +83 -0
  6. package/dist/agents/skill-loader.js +174 -0
  7. package/dist/agents/workflow-pivot-loop.js +4 -4
  8. package/dist/bollharness-integration/context-chain-router.js +3 -3
  9. package/dist/bollharness-integration/context-router.js +1 -1
  10. package/dist/cli-entry.js +1 -1
  11. package/dist/documents/reader.js +5 -0
  12. package/dist/documents/store.js +1 -1
  13. package/dist/heartbeat/Watchdog.js +7 -5
  14. package/dist/heartbeat/index.js +1 -0
  15. package/dist/heartbeat/self-improve-bus.js +85 -0
  16. package/dist/llm/pi-ai.js +6 -5
  17. package/dist/network/iroh-discovery.js +2 -1
  18. package/dist/network/iroh-transport.js +15 -2
  19. package/dist/network/p2p.js +9 -8
  20. package/dist/network/storage/adapters/json-adapter.js +16 -1
  21. package/dist/network/storage/index.js +2 -1
  22. package/dist/pi-ecosystem-judgment/index.js +42 -115
  23. package/dist/social/channels/channel-heartbeat-agent.js +1 -1
  24. package/dist/utils/auto-update.js +44 -12
  25. package/dist/web/client.js +839 -103
  26. package/dist/web/index.html +100 -8
  27. package/dist/web/server.js +568 -98
  28. package/dist/web/style.css +506 -9
  29. package/package.json +2 -2
  30. package/scripts/build-cli.js +11 -1
  31. package/scripts/build-web.ts +1 -1
  32. package/src/agents/p2p-chat-tools.ts +383 -0
  33. package/src/agents/p2p-document-tools.ts +151 -1
  34. package/src/agents/pi-sdk.ts +196 -0
  35. package/src/agents/shell-guard.ts +417 -0
  36. package/src/agents/shell-tool.ts +103 -0
  37. package/src/agents/skill-loader.ts +202 -0
  38. package/src/agents/workflow-pivot-loop.ts +13 -12
  39. package/src/bollharness-integration/channel-judgment-engine.ts +1 -1
  40. package/src/bollharness-integration/context-chain-router.ts +3 -3
  41. package/src/bollharness-integration/context-router.ts +1 -1
  42. package/src/documents/reader.ts +5 -0
  43. package/src/documents/store.ts +1 -1
  44. package/src/heartbeat/Watchdog.ts +7 -5
  45. package/src/heartbeat/index.ts +1 -0
  46. package/src/heartbeat/self-improve-bus.ts +110 -0
  47. package/src/llm/pi-ai.ts +6 -5
  48. package/src/network/iroh-discovery.ts +2 -1
  49. package/src/network/iroh-transport.ts +15 -2
  50. package/src/network/p2p.ts +9 -8
  51. package/src/network/storage/adapters/json-adapter.ts +17 -2
  52. package/src/network/storage/index.ts +19 -3
  53. package/src/social/channels/channel-heartbeat-agent.ts +1 -1
  54. package/src/types.d.ts +12 -0
  55. package/src/utils/auto-update.ts +45 -14
  56. package/src/web/client.js +839 -103
  57. package/src/web/index.html +88 -8
  58. package/src/web/server.ts +577 -102
  59. package/src/web/style.css +506 -9
  60. package/tsconfig.electron.json +1 -1
  61. package/tsconfig.json +1 -1
  62. package/dist/bollharness-integration/bollharness-integration/context-router-judgment.d.ts +0 -48
  63. package/dist/bollharness-integration/bollharness-integration/context-router-judgment.js +0 -261
  64. package/dist/bollharness-integration/bollharness-integration/context-router.d.ts +0 -110
  65. package/dist/bollharness-integration/bollharness-integration/context-router.js +0 -542
  66. package/dist/bollharness-integration/bollharness-integration/gate-state-machine.d.ts +0 -87
  67. package/dist/bollharness-integration/bollharness-integration/gate-state-machine.js +0 -231
  68. package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.d.ts +0 -30
  69. package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.js +0 -91
  70. package/dist/bollharness-integration/bollharness-integration/guard-checker.d.ts +0 -105
  71. package/dist/bollharness-integration/bollharness-integration/guard-checker.js +0 -353
  72. package/dist/bollharness-integration/bollharness-integration/index.d.ts +0 -66
  73. package/dist/bollharness-integration/bollharness-integration/index.js +0 -32
  74. package/dist/bollharness-integration/bollharness-integration/integration.d.ts +0 -219
  75. package/dist/bollharness-integration/bollharness-integration/integration.js +0 -420
  76. package/dist/bollharness-integration/bollharness-integration/skill-adapter.d.ts +0 -151
  77. package/dist/bollharness-integration/bollharness-integration/skill-adapter.js +0 -518
@@ -24,6 +24,24 @@ let irohInitialized = false;
24
24
  async function ensureSessionDirs() {
25
25
  await fs.mkdir(SESSION_CACHE_PATH, { recursive: true });
26
26
  }
27
+ /** 粗校验链上地址格式 — 不做 EIP-55 校验, 避免阻塞 UI; 失败返回空字符串 */
28
+ function isValidWalletAddress(addr) {
29
+ if (typeof addr !== 'string')
30
+ return '';
31
+ const a = addr.trim();
32
+ if (!a)
33
+ return '';
34
+ // EVM: 0x + 40 hex chars
35
+ if (/^0x[0-9a-fA-F]{40}$/.test(a))
36
+ return a;
37
+ // Sui / Aptos: 0x + 64 hex chars
38
+ if (/^0x[0-9a-fA-F]{64}$/.test(a))
39
+ return a;
40
+ // Solana: base58, 32-44 chars
41
+ if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(a) && !a.startsWith('0x'))
42
+ return a;
43
+ return '';
44
+ }
27
45
  async function loadChannels() {
28
46
  try {
29
47
  const data = await fs.readFile(CHANNELS_PATH, 'utf-8');
@@ -34,23 +52,32 @@ async function loadChannels() {
34
52
  }
35
53
  }
36
54
  async function saveChannels(channels) {
37
- const jsonStr = JSON.stringify(channels, null, 2);
38
- console.log('[saveChannels] 保存频道数据, 数量:', channels.length);
55
+ // 写盘前剥掉任何遗留的 didDocument 字段, 防止历史脏数据撑大文件
56
+ const sanitized = channels.map(ch => {
57
+ const { didDocument: _omit, ...rest } = ch;
58
+ return rest;
59
+ });
60
+ const jsonStr = JSON.stringify(sanitized, null, 2);
61
+ console.log('[saveChannels] 保存频道数据, 数量:', sanitized.length);
39
62
  console.log('[saveChannels] JSON 长度:', jsonStr.length);
40
63
  await fs.writeFile(CHANNELS_PATH, jsonStr);
41
- // 验证保存的内容
42
- const verifyData = await fs.readFile(CHANNELS_PATH, 'utf-8');
43
- const verifyChannels = JSON.parse(verifyData);
44
- console.log('[saveChannels] 验证 - 保存了', verifyChannels.length, '个频道');
45
- verifyChannels.forEach((ch, i) => {
46
- console.log(` [${i}] ${ch.name}: did=${ch.did || '无'}`);
47
- });
64
+ // 写盘即令缓存失效: 用 lastChannelsWriteAt 标记, getChannelsWithDID 会检查
65
+ lastChannelsWriteAt = Date.now();
48
66
  }
67
+ // 模块级: 最近一次 channels.json 写盘时间. saveChannels 在模块顶层,
68
+ // getChannelsWithDID 在 createWebServer 内部, 跨作用域用模块变量桥接.
69
+ let lastChannelsWriteAt = 0;
49
70
  async function loadSession(channelId, sessionId) {
50
71
  // sessionId is optional for backward compatibility; if provided, load specific session
51
72
  const key = sessionId ? `${channelId}:${sessionId}` : channelId;
52
73
  const sessionPath = path.join(SESSION_CACHE_PATH, `${key}.json`);
53
74
  try {
75
+ // 内存保护: 拒绝加载过大的 session 文件 (> 50MB 视为异常, 避免 OOM)
76
+ const stat = await fs.stat(sessionPath);
77
+ if (stat.size > 50 * 1024 * 1024) {
78
+ console.warn(`[loadSession] session 过大 (${stat.size} bytes): ${key}`);
79
+ return null;
80
+ }
54
81
  const data = await fs.readFile(sessionPath, 'utf-8');
55
82
  return JSON.parse(data);
56
83
  }
@@ -250,7 +277,7 @@ async function getAgentForChannel(channelId, channelDid, channelName, channelDid
250
277
  }
251
278
  return existingSession;
252
279
  }
253
- // 构建频道的身份文档
280
+ // 构建频道的身份文档 (从 didDocRef 拿 cid/ipnsName, 不读整份 didDocument)
254
281
  const identityDoc = channelDid ? {
255
282
  did: channelDid,
256
283
  name: channelName || `Channel-${channelId.slice(-6)}`,
@@ -375,11 +402,19 @@ export async function createWebServer(port = 3000) {
375
402
  res.setHeader('Content-Type', 'text/event-stream');
376
403
  res.setHeader('Cache-Control', 'no-cache');
377
404
  res.setHeader('Connection', 'keep-alive');
405
+ // 反向代理 (nginx/cloudflair) 需要: 禁用缓冲 + 立即 flush
406
+ res.setHeader('X-Accel-Buffering', 'no');
378
407
  res.flushHeaders();
379
408
  const clientInfo = { res, channelId };
380
409
  sseClients.add(clientInfo);
410
+ console.log(`[SSE] 客户端连接 channelId=${channelId || '(broadcast)'}, 总数=${sseClients.size}`);
381
411
  req.on('close', () => {
382
412
  sseClients.delete(clientInfo);
413
+ try {
414
+ res.end();
415
+ }
416
+ catch { }
417
+ console.log(`[SSE] 客户端断开 channelId=${channelId || '(broadcast)'}, 剩余=${sseClients.size}`);
383
418
  });
384
419
  });
385
420
  app.post('/message', async (req, res) => {
@@ -390,14 +425,18 @@ export async function createWebServer(port = 3000) {
390
425
  if (!channelId) {
391
426
  return res.status(400).json({ error: 'No channelId provided' });
392
427
  }
393
- // 获取频道信息,包括真实 DID 和完整 DID 文档
428
+ // 获取频道信息(只取轻量引用, 不再读完整 DID 文档)
394
429
  const channels = await loadChannels();
395
430
  const channel = channels.find(c => c.id === channelId);
396
431
  const currentSessionId = channel?.currentSessionId || 'default';
397
432
  const realChannelDid = channelDid || channel?.did || '';
398
433
  const realChannelName = channel?.name || '';
399
- const realChannelDidDoc = channel?.didDocument;
434
+ const realChannelDidDoc = channel?.didDocRef;
400
435
  broadcast({ type: 'user', content: text }, channelId);
436
+ // 提前捕获 wallet/autoTools 到本地变量, 避免下面 try 块内的 inner const channel
437
+ // (line ~638) 与这里外层的 const channel 形成 shadowing 让 TS 误报"使用前未声明"
438
+ const boundWalletAddress = channel?.walletAddress;
439
+ const autoToolsEnabled = channel?.autoInvokeTools !== false; // 默认开启
401
440
  try {
402
441
  const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
403
442
  let fullResponse = '';
@@ -421,7 +460,20 @@ export async function createWebServer(port = 3000) {
421
460
  };
422
461
  console.log(`[消息处理] 开始处理用户消息, channelId: ${channelId}, sessionId: ${currentSessionId}`);
423
462
  // 将真实 DID 作为上下文前缀,让 AI 使用真实的 DID 而不是自己编造的
424
- const contextHint = realChannelDid ? `[系统上下文] 当前频道名称: ${realChannelName}, 你的真实 DID: ${realChannelDid}\n\n` : '';
463
+ let contextHint = '';
464
+ if (realChannelDid)
465
+ contextHint += `[系统上下文] 当前频道名称: ${realChannelName}, 你的真实 DID: ${realChannelDid}\n`;
466
+ if (boundWalletAddress) {
467
+ contextHint += `[系统上下文] 已绑定的加密钱包地址: ${boundWalletAddress}。当用户授权或启用自动工具调用时, 可使用该地址发起链上操作。\n`;
468
+ }
469
+ if (autoToolsEnabled) {
470
+ contextHint += `[系统上下文] 自动工具调用已开启: 你可以使用受信任的本地工具 (shell / 文件 / skill) 而无需逐次询问用户。\n`;
471
+ }
472
+ else {
473
+ contextHint += `[系统上下文] 自动工具调用已关闭: 每次执行工具前必须先与用户确认。\n`;
474
+ }
475
+ if (contextHint)
476
+ contextHint += '\n';
425
477
  fullResponse = await agent.promptStream(contextHint + text, streamCallback);
426
478
  broadcast({ type: 'ai', content: fullResponse }, channelId);
427
479
  const existingSession = await loadSession(channelId, currentSessionId);
@@ -454,57 +506,112 @@ export async function createWebServer(port = 3000) {
454
506
  res.status(500).json({ error: err.message });
455
507
  }
456
508
  });
457
- // 获取频道并确保每个频道都有 DID
458
- async function getChannelsWithDID() {
459
- const channels = await loadChannels();
460
- console.log(`[getChannelsWithDID] 加载了 ${channels.length} 个频道`);
461
- let changed = false;
462
- for (const channel of channels) {
463
- // 检查 DID 是否有效
464
- const didMissing = channel.did === undefined || channel.did === null || channel.did === 'undefined' || channel.did === 'null' || channel.did === '';
465
- console.log(`[getChannelsWithDID] 频道 ${channel.name}: did=${JSON.stringify(channel.did)}, 缺失=${didMissing}`);
466
- if (didMissing) {
467
- console.log(`[修复频道] ${channel.name} (${channel.id}) 缺少 DID,正在生成...`);
509
+ // ---------- 频道元数据后台修复队列 ----------
510
+ // 关键点: 旧实现会在每次 GET /channels 时同步执行 KeyManager.generate() + IPFS POST,
511
+ // 多频道场景下持续分配密钥对 + 发起 HTTP 请求, 几轮就会把 Node 内存撑爆。
512
+ // 新实现: 入队 + 节流(2s) + 单飞, 立刻返回当前 channels, 修复异步进行。
513
+ const didFixQueue = new Set(); // 待修复的 channelId
514
+ let didFixRunning = false;
515
+ let didFixTimer = null;
516
+ function scheduleDidFix(channelId) {
517
+ didFixQueue.add(channelId);
518
+ if (didFixTimer)
519
+ return;
520
+ didFixTimer = setTimeout(() => {
521
+ didFixTimer = null;
522
+ void runDidFixOnce();
523
+ }, 2000);
524
+ }
525
+ async function runDidFixOnce() {
526
+ if (didFixRunning)
527
+ return;
528
+ didFixRunning = true;
529
+ try {
530
+ while (didFixQueue.size > 0) {
531
+ const id = didFixQueue.values().next().value;
532
+ didFixQueue.delete(id);
468
533
  try {
469
- const kp = KeyManager.generate();
470
- const generatedDid = kp.did;
471
- console.log(`[修复频道] KeyManager.generate() 结果: kp=${!!kp}, did=${generatedDid}`);
472
- if (generatedDid && typeof generatedDid === 'string' && generatedDid.length > 0) {
473
- channel.did = generatedDid;
474
- channel.publicKey = Buffer.from(kp.publicKey).toString('hex');
475
- console.log(`[修复频道] ${channel.name} 生成了 DID: ${channel.did}`);
476
- // 发布到 IPFS 并保存完整 DID 文档
477
- try {
478
- const auth = await AgentAuthManager.newWithRemoteIpfs('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
479
- const result = await auth.registerAgent({ name: channel.name, services: [] }, kp, '');
480
- channel.cid = result.cid || '';
481
- // 保存完整 DID 文档(用于传递给 session)
482
- if (result.didDocument) {
483
- channel.didDocument = result.didDocument;
484
- }
485
- console.log(`[修复频道] ${channel.name} CID: ${channel.cid}`);
486
- }
487
- catch (ipfsErr) {
488
- console.log(`[修复频道] ${channel.name} IPFS 失败`);
489
- }
490
- }
491
- else {
492
- console.log(`[修复频道] ${channel.name} KeyManager 返回无效 DID`);
493
- channel.did = `did:web:${channel.id}`;
494
- channel.publicKey = `pk_${channel.id}`;
495
- }
496
- changed = true;
534
+ await fixOneChannelDID(id);
497
535
  }
498
536
  catch (e) {
499
- console.log(`[修复频道] ${channel.name} 失败: ${e}`);
537
+ console.log(`[DID 修复] ${id} 失败: ${e.message}`);
500
538
  }
501
539
  }
502
540
  }
503
- if (changed) {
504
- console.log('[getChannelsWithDID] 保存修改后的频道');
505
- await saveChannels(channels);
541
+ finally {
542
+ didFixRunning = false;
543
+ }
544
+ }
545
+ async function fixOneChannelDID(channelId) {
546
+ const channels = await loadChannels();
547
+ const channel = channels.find(c => c.id === channelId);
548
+ if (!channel)
549
+ return;
550
+ const didMissing = !channel.did || channel.did === 'undefined' || channel.did === 'null' || channel.did === '';
551
+ if (!didMissing)
552
+ return;
553
+ let kp;
554
+ try {
555
+ kp = KeyManager.generate();
556
+ }
557
+ catch {
558
+ kp = null;
559
+ }
560
+ if (kp && kp.did) {
561
+ channel.did = kp.did;
562
+ channel.publicKey = Buffer.from(kp.publicKey).toString('hex');
563
+ }
564
+ else {
565
+ // 兜底: 用 channelId 派生, 不阻塞 UI
566
+ channel.did = `did:web:${channel.id}`;
567
+ channel.publicKey = `pk_${channel.id}`;
568
+ }
569
+ console.log(`[DID 修复] ${channel.name} DID = ${channel.did}`);
570
+ // IPFS 注册: 失败也无所谓, 后续可重试
571
+ try {
572
+ const auth = await AgentAuthManager.newWithRemoteIpfs('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
573
+ const result = await auth.registerAgent({ name: channel.name, services: [] }, kp, '');
574
+ channel.cid = result.cid || channel.cid;
575
+ // 关键: 不再保存整份 didDocument, 只留 cid/ipnsName 两个引用字段
576
+ if (result.didDocument) {
577
+ channel.didDocRef = {
578
+ cid: result.cid,
579
+ ipnsName: result.didDocument?.ipnsName
580
+ };
581
+ delete channel.didDocument;
582
+ }
583
+ }
584
+ catch {
585
+ // IPFS 不可用, 跳过 — 下次再试
586
+ }
587
+ await saveChannels(channels);
588
+ }
589
+ // 频道列表响应缓存: 短时间内重复请求走缓存, 避免每次重读 + 重序列化 channels.json
590
+ // 跨作用域 (saveChannels 在模块顶层, 本函数在 createWebServer 内) 用 lastChannelsWriteAt 协调失效
591
+ const channelsCache = { data: null, cachedAt: 0 };
592
+ const CHANNELS_CACHE_TTL_MS = 500;
593
+ /** 获取频道列表 — 立即返回, 缺 DID 的频道入队后台修复 */
594
+ async function getChannelsWithDID() {
595
+ const now = Date.now();
596
+ // 缓存命中: 数据有效 AND 在写盘之后 AND 在 TTL 内
597
+ if (channelsCache.data && channelsCache.cachedAt > lastChannelsWriteAt && channelsCache.cachedAt + CHANNELS_CACHE_TTL_MS > now) {
598
+ return channelsCache.data;
599
+ }
600
+ const channels = await loadChannels();
601
+ // 防御性剥除: 任何旧 channels.json 残留的 didDocument 都不返回给客户端
602
+ const sanitized = channels.map(ch => {
603
+ const { didDocument: _omit, ...rest } = ch;
604
+ return rest;
605
+ });
606
+ for (const channel of sanitized) {
607
+ const didMissing = !channel.did || channel.did === 'undefined' || channel.did === 'null' || channel.did === '';
608
+ if (didMissing) {
609
+ scheduleDidFix(channel.id);
610
+ }
506
611
  }
507
- return channels;
612
+ channelsCache.data = sanitized;
613
+ channelsCache.cachedAt = now;
614
+ return sanitized;
508
615
  }
509
616
  app.get('/channels', async (_req, res) => {
510
617
  try {
@@ -523,13 +630,15 @@ export async function createWebServer(port = 3000) {
523
630
  });
524
631
  app.post('/channels', async (req, res) => {
525
632
  try {
526
- const { name, agentId } = req.body;
527
- console.log(`[创建频道] 收到请求: name=${name}, agentId=${agentId}`);
633
+ const { name, agentId, walletAddress, autoInvokeTools } = req.body;
634
+ console.log(`[创建频道] 收到请求: name=${name}, agentId=${agentId}, wallet=${walletAddress ? 'yes' : 'no'}`);
528
635
  if (!name || !agentId) {
529
636
  return res.status(400).json({ error: 'name and agentId required' });
530
637
  }
531
638
  const channels = await loadChannels();
532
639
  const id = `ch_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
640
+ // 校验钱包地址格式 (粗校验: 0x + 40 hex / Solana base58 / Sui 0x+64)
641
+ const validWallet = isValidWalletAddress(walletAddress);
533
642
  // 先创建频道(不阻塞等待 DID 生成)
534
643
  const channel = {
535
644
  id,
@@ -538,6 +647,9 @@ export async function createWebServer(port = 3000) {
538
647
  createdAt: new Date().toISOString(),
539
648
  updatedAt: new Date().toISOString(),
540
649
  currentSessionId: `sess_${Date.now()}`,
650
+ walletAddress: validWallet || undefined,
651
+ walletRegisteredAt: validWallet ? new Date().toISOString() : undefined,
652
+ autoInvokeTools: autoInvokeTools !== false, // 默认 true
541
653
  sessions: [{
542
654
  id: `sess_${Date.now()}`,
543
655
  createdAt: new Date().toISOString(),
@@ -550,34 +662,9 @@ export async function createWebServer(port = 3000) {
550
662
  await saveChannels(channels);
551
663
  await saveSession({ channelId: id, sessionId: 'default', messages: [], lastUpdated: new Date().toISOString() });
552
664
  res.json(channel);
553
- // 后台生成 DID
554
- console.log(`[创建频道] 后台生成 DID...`);
555
- setTimeout(async () => {
556
- try {
557
- const kp = KeyManager.generate();
558
- if (kp.did) {
559
- const allChannels = await loadChannels();
560
- const ch = allChannels.find(c => c.id === id);
561
- if (ch) {
562
- ch.did = kp.did;
563
- ch.publicKey = Buffer.from(kp.publicKey).toString('hex');
564
- console.log(`[创建频道] DID 生成完成: ${ch.did}`);
565
- // 发布到 IPFS
566
- try {
567
- const auth = await AgentAuthManager.newWithRemoteIpfs('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
568
- const result = await auth.registerAgent({ name, services: [] }, kp, '');
569
- ch.cid = result.cid || '';
570
- console.log(`[创建频道] CID: ${ch.cid}`);
571
- }
572
- catch { }
573
- await saveChannels(allChannels);
574
- }
575
- }
576
- }
577
- catch (e) {
578
- console.log(`[创建频道] ${name} 后台生成 DID 失败`);
579
- }
580
- }, 100);
665
+ // 后台生成 DID — 用统一的修复队列, 避免每个 POST 都启动独立 setTimeout
666
+ console.log(`[创建频道] 加入 DID 修复队列...`);
667
+ scheduleDidFix(id);
581
668
  }
582
669
  catch (err) {
583
670
  console.error('[创建频道] 错误:', err);
@@ -653,6 +740,43 @@ export async function createWebServer(port = 3000) {
653
740
  res.status(500).json({ error: err.message });
654
741
  }
655
742
  });
743
+ // 删除单个 Session
744
+ app.delete('/channels/:channelId/sessions/:sessionId', async (req, res) => {
745
+ try {
746
+ const { channelId, sessionId } = req.params;
747
+ const channels = await loadChannels();
748
+ const channel = channels.find(c => c.id === channelId);
749
+ if (!channel) {
750
+ return res.status(404).json({ error: 'Channel not found' });
751
+ }
752
+ // 不允许删除最后一个 session —— 至少要保留一个
753
+ if (!channel.sessions || channel.sessions.length <= 1) {
754
+ return res.status(400).json({ error: 'At least one session is required' });
755
+ }
756
+ const sessionIndex = channel.sessions.findIndex(s => s.id === sessionId);
757
+ if (sessionIndex === -1) {
758
+ return res.status(404).json({ error: 'Session not found' });
759
+ }
760
+ channel.sessions.splice(sessionIndex, 1);
761
+ // 如果删除的是当前 session,切换到列表里的第一个
762
+ if (channel.currentSessionId === sessionId) {
763
+ const nextSession = channel.sessions[0];
764
+ channel.currentSessionId = nextSession.id;
765
+ }
766
+ channel.updatedAt = new Date().toISOString();
767
+ await saveChannels(channels);
768
+ // 删除 session 文件
769
+ try {
770
+ await fs.unlink(path.join(SESSION_CACHE_PATH, `${channelId}:${sessionId}.json`));
771
+ }
772
+ catch { }
773
+ res.json({ ok: true, currentSessionId: channel.currentSessionId });
774
+ }
775
+ catch (err) {
776
+ console.error('[删除Session] 错误:', err);
777
+ res.status(500).json({ error: err.message });
778
+ }
779
+ });
656
780
  app.delete('/channels/:channelId', async (req, res) => {
657
781
  try {
658
782
  const { channelId } = req.params;
@@ -661,12 +785,20 @@ export async function createWebServer(port = 3000) {
661
785
  if (index === -1) {
662
786
  return res.status(404).json({ error: 'Channel not found' });
663
787
  }
788
+ const channel = channels[index];
664
789
  channels.splice(index, 1);
665
790
  await saveChannels(channels);
666
- try {
667
- await fs.unlink(path.join(SESSION_CACHE_PATH, `${channelId}.json`));
791
+ // 清理该 channel 名下所有的 session 文件 + 默认 session 文件
792
+ const candidates = new Set([`${channelId}.json`]);
793
+ if (channel.sessions) {
794
+ channel.sessions.forEach(s => candidates.add(`${channelId}:${s.id}.json`));
795
+ }
796
+ for (const filename of candidates) {
797
+ try {
798
+ await fs.unlink(path.join(SESSION_CACHE_PATH, filename));
799
+ }
800
+ catch { }
668
801
  }
669
- catch { }
670
802
  res.json({ ok: true });
671
803
  }
672
804
  catch (err) {
@@ -676,16 +808,33 @@ export async function createWebServer(port = 3000) {
676
808
  app.patch('/channels/:channelId', async (req, res) => {
677
809
  try {
678
810
  const { channelId } = req.params;
679
- const { name } = req.body;
680
- if (!name) {
681
- return res.status(400).json({ error: 'Name required' });
682
- }
811
+ const { name, walletAddress, autoInvokeTools } = req.body;
683
812
  const channels = await loadChannels();
684
813
  const channel = channels.find(c => c.id === channelId);
685
814
  if (!channel) {
686
815
  return res.status(404).json({ error: 'Channel not found' });
687
816
  }
688
- channel.name = name;
817
+ if (typeof name === 'string' && name.trim()) {
818
+ channel.name = name.trim();
819
+ }
820
+ // walletAddress 允许 null/'' 来解绑
821
+ if (walletAddress !== undefined) {
822
+ if (walletAddress === null || walletAddress === '') {
823
+ channel.walletAddress = undefined;
824
+ channel.walletRegisteredAt = undefined;
825
+ }
826
+ else {
827
+ const valid = isValidWalletAddress(walletAddress);
828
+ if (!valid) {
829
+ return res.status(400).json({ error: 'Invalid wallet address format' });
830
+ }
831
+ channel.walletAddress = valid;
832
+ channel.walletRegisteredAt = channel.walletRegisteredAt || new Date().toISOString();
833
+ }
834
+ }
835
+ if (typeof autoInvokeTools === 'boolean') {
836
+ channel.autoInvokeTools = autoInvokeTools;
837
+ }
689
838
  channel.updatedAt = new Date().toISOString();
690
839
  await saveChannels(channels);
691
840
  res.json(channel);
@@ -696,8 +845,39 @@ export async function createWebServer(port = 3000) {
696
845
  });
697
846
  app.get('/sessions/:channelId', async (req, res) => {
698
847
  try {
699
- const session = await loadSession(req.params.channelId);
700
- res.json(session || { channelId: req.params.channelId, messages: [], lastUpdated: null });
848
+ const session = await loadSession(req.params.channelId, req.query.sessionId);
849
+ res.json(session || { channelId: req.params.channelId, sessionId: req.query.sessionId || 'default', messages: [], lastUpdated: null });
850
+ }
851
+ catch (err) {
852
+ res.status(500).json({ error: err.message });
853
+ }
854
+ });
855
+ // 增量追加消息到 session (前端落盘用, 避免丢消息)
856
+ // body: { message: { type, content, timestamp? } }
857
+ app.patch('/sessions/:channelId/:sessionId', async (req, res) => {
858
+ try {
859
+ const { channelId, sessionId } = req.params;
860
+ const { message } = req.body || {};
861
+ if (!message || (message.type !== 'user' && message.type !== 'ai') || typeof message.content !== 'string') {
862
+ return res.status(400).json({ error: 'invalid message' });
863
+ }
864
+ const existing = await loadSession(channelId, sessionId);
865
+ const session = existing || { channelId, sessionId, messages: [], lastUpdated: new Date().toISOString() };
866
+ session.sessionId = sessionId;
867
+ // 去重: 跳过与最后一条完全相同的 (避免 SSE 重复推导致双写)
868
+ const last = session.messages[session.messages.length - 1];
869
+ if (last && last.type === message.type && last.content === message.content) {
870
+ return res.json({ ok: true, count: session.messages.length, deduped: true });
871
+ }
872
+ session.messages.push({
873
+ id: message.id || crypto.randomUUID(),
874
+ type: message.type,
875
+ content: message.content,
876
+ timestamp: message.timestamp || new Date().toISOString()
877
+ });
878
+ session.lastUpdated = new Date().toISOString();
879
+ await saveSession(session);
880
+ res.json({ ok: true, count: session.messages.length });
701
881
  }
702
882
  catch (err) {
703
883
  res.status(500).json({ error: err.message });
@@ -740,7 +920,7 @@ export async function createWebServer(port = 3000) {
740
920
  const currentSessionId = channel?.currentSessionId || 'default';
741
921
  const realChannelDid = channel?.did || '';
742
922
  const realChannelName = channel?.name || '';
743
- const realChannelDidDoc = channel?.didDocument;
923
+ const realChannelDidDoc = channel?.didDocRef;
744
924
  // 通知前端开始重新生成
745
925
  broadcast({ type: 'regenerating', channelId }, channelId);
746
926
  const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
@@ -1048,6 +1228,75 @@ export async function createWebServer(port = 3000) {
1048
1228
  res.status(500).json({ error: err.message });
1049
1229
  }
1050
1230
  });
1231
+ // 统一 AI 解析入口:CLI / 接收方节点 调这里完成 LLM + judgment + harness
1232
+ // 入参: { text, mimeType, fileName, fromNodeId, source }
1233
+ // 出参: { summary, qualityScore, judgmentId?, gateArtifact? }
1234
+ app.post('/api/ai-parse', async (req, res) => {
1235
+ try {
1236
+ const { text, mimeType, fileName, fromNodeId, source } = req.body || {};
1237
+ if (!text || !fileName) {
1238
+ return res.status(400).json({ error: 'text and fileName required' });
1239
+ }
1240
+ const truncated = text.length > 6000 ? text.substring(0, 6000) + '...[截断]' : text;
1241
+ const prompt = `请分析以下 ${mimeType || 'text'} 文档,并给出 (1) 一句话中文摘要 (2) 三个关键要点 (3) 质量评分(0-1)。\n\n文件名: ${fileName}\n\n内容:\n${truncated}`;
1242
+ // 1. LLM 解析
1243
+ const llm = getMinimax();
1244
+ const t0 = Date.now();
1245
+ const llmResult = await llm.summarize(prompt);
1246
+ const dt = Date.now() - t0;
1247
+ const out = {
1248
+ ok: true,
1249
+ summary: llmResult.summary,
1250
+ qualityScore: llmResult.qualityScore,
1251
+ latencyMs: dt,
1252
+ mimeType: mimeType || 'text/plain',
1253
+ fileName,
1254
+ };
1255
+ // 2. 蒸馏为 judgment (异步,失败不影响主返回)
1256
+ try {
1257
+ const judgmentMod = await import('../pi-ecosystem-judgment/index.js');
1258
+ await judgmentMod.initializeJudgmentStore();
1259
+ const j = await judgmentMod.createJudgment({
1260
+ type: 'trajectory',
1261
+ content: `AI 解析 ${fileName}: ${llmResult.summary.slice(0, 200)}`,
1262
+ source: 'agent',
1263
+ confidence: Math.min(1, llmResult.qualityScore),
1264
+ context: `ai-parse:${mimeType || 'text'}:${source || 'p2p'}`,
1265
+ evidence: {
1266
+ trajectory: [{
1267
+ timestamp: new Date().toISOString(),
1268
+ action: `parse:${fileName}`,
1269
+ outcome: `score=${llmResult.qualityScore.toFixed(2)}`,
1270
+ approved: true,
1271
+ }],
1272
+ },
1273
+ });
1274
+ out.judgmentId = j.id;
1275
+ }
1276
+ catch (e) {
1277
+ out.judgmentError = e.message;
1278
+ }
1279
+ // 3. 在 harness 落产物 (异步,失败不影响)
1280
+ try {
1281
+ const harnessMod = await import('../bollharness-integration/index.js');
1282
+ const gate = new harnessMod.GateStateMachine();
1283
+ gate.submitArtifact(`ai-parse:${fileName}`, {
1284
+ summary: llmResult.summary,
1285
+ score: llmResult.qualityScore,
1286
+ fromNodeId: fromNodeId || null,
1287
+ parsedAt: Date.now(),
1288
+ });
1289
+ out.gateArtifact = `ai-parse:${fileName}`;
1290
+ }
1291
+ catch (e) {
1292
+ out.gateError = e.message;
1293
+ }
1294
+ res.json(out);
1295
+ }
1296
+ catch (err) {
1297
+ res.status(500).json({ error: err.message });
1298
+ }
1299
+ });
1051
1300
  // ==================== P2P Network API ====================
1052
1301
  // 获取当前身份
1053
1302
  app.get('/api/identity', async (_req, res) => {
@@ -1470,6 +1719,63 @@ export async function createWebServer(port = 3000) {
1470
1719
  res.status(500).json({ error: err.message });
1471
1720
  }
1472
1721
  });
1722
+ // Chat inbox: 列出所有 peer 的 inbox + outbox
1723
+ app.get('/api/chat/inbox', async (_req, res) => {
1724
+ try {
1725
+ const { getInbox } = await import('../agents/p2p-chat-tools.js');
1726
+ const entries = await getInbox();
1727
+ // 按 status 分组, 时间倒序
1728
+ const grouped = {
1729
+ received: entries.filter((e) => e.status === 'received'),
1730
+ drafted: entries.filter((e) => e.status === 'drafted'),
1731
+ sent: entries.filter((e) => e.status === 'sent'),
1732
+ dismissed: entries.filter((e) => e.status === 'dismissed'),
1733
+ };
1734
+ res.json({ total: entries.length, grouped, all: entries });
1735
+ }
1736
+ catch (err) {
1737
+ res.status(500).json({ error: err.message });
1738
+ }
1739
+ });
1740
+ // 触发 processPendingInbox (手动 wake-up)
1741
+ app.post('/api/chat/process-pending', async (_req, res) => {
1742
+ try {
1743
+ const { processPendingInbox } = await import('../agents/p2p-chat-tools.js');
1744
+ const r = await processPendingInbox();
1745
+ res.json({ ok: true, ...r });
1746
+ }
1747
+ catch (err) {
1748
+ res.status(500).json({ error: err.message });
1749
+ }
1750
+ });
1751
+ // 主人审阅: 批准 draft
1752
+ app.post('/api/chat/approve', async (req, res) => {
1753
+ try {
1754
+ const { messageId, peerDID, finalText } = req.body || {};
1755
+ if (!messageId || !peerDID)
1756
+ return res.status(400).json({ error: 'messageId and peerDID required' });
1757
+ const { approveAndSend } = await import('../agents/p2p-chat-tools.js');
1758
+ const ok = await approveAndSend(messageId, peerDID, finalText);
1759
+ res.json({ ok, messageId });
1760
+ }
1761
+ catch (err) {
1762
+ res.status(500).json({ error: err.message });
1763
+ }
1764
+ });
1765
+ // 主人审阅: 丢弃 draft
1766
+ app.post('/api/chat/dismiss', async (req, res) => {
1767
+ try {
1768
+ const { messageId, peerDID } = req.body || {};
1769
+ if (!messageId || !peerDID)
1770
+ return res.status(400).json({ error: 'messageId and peerDID required' });
1771
+ const { dismissDraft } = await import('../agents/p2p-chat-tools.js');
1772
+ const ok = await dismissDraft(messageId, peerDID);
1773
+ res.json({ ok, messageId });
1774
+ }
1775
+ catch (err) {
1776
+ res.status(500).json({ error: err.message });
1777
+ }
1778
+ });
1473
1779
  // 标记消息已读
1474
1780
  app.post('/api/peer-messages/:messageId/read', async (req, res) => {
1475
1781
  try {
@@ -1786,6 +2092,12 @@ export async function createWebServer(port = 3000) {
1786
2092
  });
1787
2093
  // 启动看门狗监控
1788
2094
  if (watchdog) {
2095
+ // level 1 (内存爆) → 进程自杀, 依赖外层 supervisor / 用户重启 (Windows 任务计划/手动)
2096
+ // 否则 Node.js 高 GC 压力下 HTTP 响应丢失, 客户端 fetch 永远 pending
2097
+ watchdog.registerRestartStrategy(1, () => {
2098
+ console.error('[Watchdog] memory critical, 进程退出 (期望外层重启)');
2099
+ setTimeout(() => process.exit(1), 100);
2100
+ });
1789
2101
  watchdog.start();
1790
2102
  console.log('[24h] Watchdog started');
1791
2103
  }
@@ -1794,10 +2106,112 @@ export async function createWebServer(port = 3000) {
1794
2106
  healthMonitor.startPeriodicCheck(60000);
1795
2107
  console.log('[24h] Health monitor periodic check started');
1796
2108
  }
2109
+ // ==================== Self-Improve 端点 ====================
2110
+ // 查看当前策略 (白名单 / 黑名单)
2111
+ app.get('/api/self-improve/policy', async (_req, res) => {
2112
+ const { loadPolicy } = await import('../agents/shell-guard.js');
2113
+ const policy = loadPolicy(true); // 强制重读
2114
+ if (!policy) {
2115
+ res.status(500).json({ error: '策略加载失败, 当前用硬编码兜底' });
2116
+ return;
2117
+ }
2118
+ res.json(policy);
2119
+ });
2120
+ // 更新策略 (白名单 / 黑名单)
2121
+ // **仅供人手动调用**, 不会暴露给 AI
2122
+ app.put('/api/self-improve/policy', async (req, res) => {
2123
+ const { writePolicy, auditShellCall } = await import('../agents/shell-guard.js');
2124
+ const newPolicy = req.body;
2125
+ if (!newPolicy || typeof newPolicy !== 'object') {
2126
+ res.status(400).json({ error: 'body 必须是对象' });
2127
+ return;
2128
+ }
2129
+ // 极简校验
2130
+ if (!Array.isArray(newPolicy.commandAllowlist) || !Array.isArray(newPolicy.pathAllowlist) || !Array.isArray(newPolicy.pathDenylist)) {
2131
+ res.status(400).json({ error: 'commandAllowlist/pathAllowlist/pathDenylist 必须是数组' });
2132
+ return;
2133
+ }
2134
+ try {
2135
+ const success = writePolicy(newPolicy);
2136
+ if (success) {
2137
+ auditShellCall('allowed', 'api:PUT:/api/self-improve/policy', [], `人类用户更新策略`);
2138
+ res.json({ ok: true, message: '策略已更新, 60 秒内生效' });
2139
+ }
2140
+ else {
2141
+ res.status(500).json({ error: '写入策略文件失败' });
2142
+ }
2143
+ }
2144
+ catch (err) {
2145
+ res.status(500).json({ error: err.message });
2146
+ }
2147
+ });
2148
+ // 查看审计日志
2149
+ app.get('/api/self-improve/audit', async (_req, res) => {
2150
+ try {
2151
+ const { POLICY_AUDIT_PATH_PUBLIC } = await import('../agents/shell-guard.js');
2152
+ const fs = await import('fs/promises');
2153
+ const auditPath = POLICY_AUDIT_PATH_PUBLIC;
2154
+ const exists = await fs.stat(auditPath).then(() => true).catch(() => false);
2155
+ if (!exists) {
2156
+ res.json([]);
2157
+ return;
2158
+ }
2159
+ const content = await fs.readFile(auditPath, 'utf-8');
2160
+ const lines = content.split('\n').filter(Boolean).slice(-200); // 最近 200 条
2161
+ const entries = lines.map((l) => {
2162
+ try {
2163
+ return JSON.parse(l);
2164
+ }
2165
+ catch {
2166
+ return { raw: l };
2167
+ }
2168
+ });
2169
+ res.json(entries);
2170
+ }
2171
+ catch (err) {
2172
+ res.status(500).json({ error: err.message });
2173
+ }
2174
+ });
2175
+ // 手动触发 (供前端按钮 / 调试用)
2176
+ app.post('/api/self-improve/trigger', async (req, res) => {
2177
+ const { goal, kind } = req.body || {};
2178
+ const { reportSelfImproveEvent } = await import('../heartbeat/self-improve-bus.js');
2179
+ const result = reportSelfImproveEvent({
2180
+ kind: kind || 'user-requested',
2181
+ details: String(goal || '用户手动触发')
2182
+ });
2183
+ res.json(result);
2184
+ });
2185
+ // 事件历史 (供前端显示 / 调试)
2186
+ app.get('/api/self-improve/history', async (_req, res) => {
2187
+ const { getEventHistory } = await import('../heartbeat/self-improve-bus.js');
2188
+ res.json(getEventHistory());
2189
+ });
2190
+ // 健康检查错误数 ≥ 2 -> 触发自改信号
2191
+ if (healthMonitor) {
2192
+ healthMonitor.startPeriodicCheck(60000, (status) => {
2193
+ const errorCount = Object.values(status.checks)
2194
+ .filter((c) => c.status === 'error').length;
2195
+ if (errorCount >= 2) {
2196
+ import('../heartbeat/self-improve-bus.js').then(({ reportSelfImproveEvent }) => {
2197
+ const failedKeys = Object.entries(status.checks)
2198
+ .filter(([_, c]) => c.status === 'error').map(([k]) => k).join(', ');
2199
+ reportSelfImproveEvent({
2200
+ kind: 'silent-timeout',
2201
+ details: `健康检查有 ${errorCount} 项失败: ${failedKeys}`
2202
+ });
2203
+ });
2204
+ }
2205
+ });
2206
+ }
2207
+ // 安装自改总线 -> SSE 桥
2208
+ void installSelfImproveHook();
1797
2209
  return new Promise((resolve) => {
1798
2210
  server.listen(port, () => {
1799
2211
  console.log(`Web 服务器启动完成: http://localhost:${port}`);
1800
2212
  console.log('服务器已监听');
2213
+ // 安装 chat bus -> SSE 桥 (供前端 inbox UI 实时刷新)
2214
+ void installChatBusHook();
1801
2215
  setInterval(() => {
1802
2216
  for (const client of sseClients) {
1803
2217
  client.res.write(': ping\n\n');
@@ -1822,6 +2236,62 @@ function broadcast(data, channelId) {
1822
2236
  }
1823
2237
  }
1824
2238
  }
2239
+ // ============================================================================
2240
+ // Chat 事件总线 -> SSE 桥 (供前端 inbox UI 用)
2241
+ // ============================================================================
2242
+ let chatBusHookInstalled = false;
2243
+ async function installChatBusHook() {
2244
+ if (chatBusHookInstalled)
2245
+ return;
2246
+ chatBusHookInstalled = true;
2247
+ try {
2248
+ const { chatEventBus } = await import('../agents/p2p-chat-tools.js');
2249
+ chatEventBus.on('chat', (ev) => {
2250
+ // 推送给所有 SSE 客户端 (channelId 留空 = 广播)
2251
+ broadcast({ type: 'chat_event', chatKind: ev.kind, payload: ev }, undefined);
2252
+ });
2253
+ console.log('[chat-bus] SSE bridge installed');
2254
+ }
2255
+ catch (e) {
2256
+ console.warn('[chat-bus] install failed:', e.message);
2257
+ }
2258
+ }
2259
+ // ============================================================================
2260
+ // Self-Improve Bus -> SSE 桥 (供前端 / 用户看到自改触发)
2261
+ // ============================================================================
2262
+ let selfImproveHookInstalled = false;
2263
+ async function installSelfImproveHook() {
2264
+ if (selfImproveHookInstalled)
2265
+ return;
2266
+ selfImproveHookInstalled = true;
2267
+ try {
2268
+ const { onSelfImproveTrigger } = await import('../heartbeat/self-improve-bus.js');
2269
+ const { runSelfImproveLoop } = await import('../agents/pi-sdk.js');
2270
+ // 监听自改事件 -> 跑循环 + 广播到前端
2271
+ onSelfImproveTrigger(async (event, goal) => {
2272
+ broadcast({
2273
+ type: 'self_improve_triggered',
2274
+ eventKind: event.kind,
2275
+ details: event.details,
2276
+ goal,
2277
+ ts: Date.now()
2278
+ }, undefined);
2279
+ // 实际跑循环 (创分支等)
2280
+ const result = await runSelfImproveLoop(goal);
2281
+ broadcast({
2282
+ type: 'self_improve_result',
2283
+ success: result.success,
2284
+ output: result.output,
2285
+ error: result.error,
2286
+ ts: Date.now()
2287
+ }, undefined);
2288
+ });
2289
+ console.log('[self-improve] SSE bridge installed');
2290
+ }
2291
+ catch (e) {
2292
+ console.warn('[self-improve] install failed:', e.message);
2293
+ }
2294
+ }
1825
2295
  function getUserName() {
1826
2296
  const home = process.env.HOME || process.env.USERPROFILE || '';
1827
2297
  const match = home.match(/\/Users\/(\w+)/);