@bolloon/bolloon-agent 0.1.13 → 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 (52) hide show
  1. package/dist/agents/pi-sdk.js +185 -0
  2. package/dist/agents/shell-guard.js +354 -0
  3. package/dist/agents/shell-tool.js +83 -0
  4. package/dist/agents/skill-loader.js +174 -0
  5. package/dist/bollharness-integration/context-chain-router.js +3 -3
  6. package/dist/bollharness-integration/context-router.js +1 -1
  7. package/dist/heartbeat/Watchdog.js +7 -5
  8. package/dist/heartbeat/index.js +1 -0
  9. package/dist/heartbeat/self-improve-bus.js +85 -0
  10. package/dist/pi-ecosystem-judgment/index.js +1 -2
  11. package/dist/utils/auto-update.js +44 -12
  12. package/dist/web/client.js +839 -103
  13. package/dist/web/components/p2p/P2PModal.js +188 -0
  14. package/dist/web/components/p2p/index.js +264 -226
  15. package/dist/web/components/p2p/p2p-modal.js +657 -0
  16. package/dist/web/components/p2p/p2p-tools.js +248 -0
  17. package/dist/web/index.html +88 -8
  18. package/dist/web/server.js +2360 -0
  19. package/dist/web/style.css +506 -9
  20. package/package.json +2 -2
  21. package/scripts/build-cli.js +11 -1
  22. package/src/agents/pi-sdk.ts +196 -0
  23. package/src/agents/shell-guard.ts +417 -0
  24. package/src/agents/shell-tool.ts +103 -0
  25. package/src/agents/skill-loader.ts +202 -0
  26. package/src/bollharness-integration/context-chain-router.ts +3 -3
  27. package/src/bollharness-integration/context-router.ts +1 -1
  28. package/src/heartbeat/Watchdog.ts +7 -5
  29. package/src/heartbeat/index.ts +1 -0
  30. package/src/heartbeat/self-improve-bus.ts +110 -0
  31. package/src/types.d.ts +12 -0
  32. package/src/utils/auto-update.ts +45 -14
  33. package/src/web/client.js +839 -103
  34. package/src/web/index.html +88 -8
  35. package/src/web/server.ts +427 -101
  36. package/src/web/style.css +506 -9
  37. package/dist/bollharness-integration/bollharness-integration/context-router-judgment.d.ts +0 -48
  38. package/dist/bollharness-integration/bollharness-integration/context-router-judgment.js +0 -261
  39. package/dist/bollharness-integration/bollharness-integration/context-router.d.ts +0 -110
  40. package/dist/bollharness-integration/bollharness-integration/context-router.js +0 -542
  41. package/dist/bollharness-integration/bollharness-integration/gate-state-machine.d.ts +0 -87
  42. package/dist/bollharness-integration/bollharness-integration/gate-state-machine.js +0 -231
  43. package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.d.ts +0 -30
  44. package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.js +0 -91
  45. package/dist/bollharness-integration/bollharness-integration/guard-checker.d.ts +0 -105
  46. package/dist/bollharness-integration/bollharness-integration/guard-checker.js +0 -353
  47. package/dist/bollharness-integration/bollharness-integration/index.d.ts +0 -66
  48. package/dist/bollharness-integration/bollharness-integration/index.js +0 -32
  49. package/dist/bollharness-integration/bollharness-integration/integration.d.ts +0 -219
  50. package/dist/bollharness-integration/bollharness-integration/integration.js +0 -420
  51. package/dist/bollharness-integration/bollharness-integration/skill-adapter.d.ts +0 -151
  52. package/dist/bollharness-integration/bollharness-integration/skill-adapter.js +0 -518
package/src/web/server.ts CHANGED
@@ -48,7 +48,14 @@ interface Channel {
48
48
  did?: string;
49
49
  publicKey?: string;
50
50
  cid?: string;
51
- didDocument?: any;
51
+ /** 轻量引用:从 didDocument 只挑出 cid/ipnsName, 不存整份文档 */
52
+ didDocRef?: { cid?: string; ipnsName?: string };
53
+ /** 加密钱包地址(公链地址, e.g. 0x...)— 与频道绑定, 启用自动 on-chain 工具调用 */
54
+ walletAddress?: string;
55
+ /** 钱包注册时间 */
56
+ walletRegisteredAt?: string;
57
+ /** 自动工具调用开关 — 当 LLM 决定调用受信任工具时, agent 是否自动执行 */
58
+ autoInvokeTools?: boolean;
52
59
  createdAt: string;
53
60
  updatedAt: string;
54
61
  currentSessionId?: string;
@@ -80,6 +87,20 @@ async function ensureSessionDirs() {
80
87
  await fs.mkdir(SESSION_CACHE_PATH, { recursive: true });
81
88
  }
82
89
 
90
+ /** 粗校验链上地址格式 — 不做 EIP-55 校验, 避免阻塞 UI; 失败返回空字符串 */
91
+ function isValidWalletAddress(addr: unknown): string {
92
+ if (typeof addr !== 'string') return '';
93
+ const a = addr.trim();
94
+ if (!a) return '';
95
+ // EVM: 0x + 40 hex chars
96
+ if (/^0x[0-9a-fA-F]{40}$/.test(a)) return a;
97
+ // Sui / Aptos: 0x + 64 hex chars
98
+ if (/^0x[0-9a-fA-F]{64}$/.test(a)) return a;
99
+ // Solana: base58, 32-44 chars
100
+ if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(a) && !a.startsWith('0x')) return a;
101
+ return '';
102
+ }
103
+
83
104
  async function loadChannels(): Promise<Channel[]> {
84
105
  try {
85
106
  const data = await fs.readFile(CHANNELS_PATH, 'utf-8');
@@ -90,25 +111,34 @@ async function loadChannels(): Promise<Channel[]> {
90
111
  }
91
112
 
92
113
  async function saveChannels(channels: Channel[]): Promise<void> {
93
- const jsonStr = JSON.stringify(channels, null, 2);
94
- console.log('[saveChannels] 保存频道数据, 数量:', channels.length);
114
+ // 写盘前剥掉任何遗留的 didDocument 字段, 防止历史脏数据撑大文件
115
+ const sanitized = channels.map(ch => {
116
+ const { didDocument: _omit, ...rest } = ch as any;
117
+ return rest as Channel;
118
+ });
119
+ const jsonStr = JSON.stringify(sanitized, null, 2);
120
+ console.log('[saveChannels] 保存频道数据, 数量:', sanitized.length);
95
121
  console.log('[saveChannels] JSON 长度:', jsonStr.length);
96
122
  await fs.writeFile(CHANNELS_PATH, jsonStr);
97
-
98
- // 验证保存的内容
99
- const verifyData = await fs.readFile(CHANNELS_PATH, 'utf-8');
100
- const verifyChannels = JSON.parse(verifyData);
101
- console.log('[saveChannels] 验证 - 保存了', verifyChannels.length, '个频道');
102
- verifyChannels.forEach((ch: Channel, i: number) => {
103
- console.log(` [${i}] ${ch.name}: did=${ch.did || '无'}`);
104
- });
123
+ // 写盘即令缓存失效: 用 lastChannelsWriteAt 标记, getChannelsWithDID 会检查
124
+ lastChannelsWriteAt = Date.now();
105
125
  }
106
126
 
127
+ // 模块级: 最近一次 channels.json 写盘时间. saveChannels 在模块顶层,
128
+ // getChannelsWithDID 在 createWebServer 内部, 跨作用域用模块变量桥接.
129
+ let lastChannelsWriteAt = 0;
130
+
107
131
  async function loadSession(channelId: string, sessionId?: string): Promise<Session | null> {
108
132
  // sessionId is optional for backward compatibility; if provided, load specific session
109
133
  const key = sessionId ? `${channelId}:${sessionId}` : channelId;
110
134
  const sessionPath = path.join(SESSION_CACHE_PATH, `${key}.json`);
111
135
  try {
136
+ // 内存保护: 拒绝加载过大的 session 文件 (> 50MB 视为异常, 避免 OOM)
137
+ const stat = await fs.stat(sessionPath);
138
+ if (stat.size > 50 * 1024 * 1024) {
139
+ console.warn(`[loadSession] session 过大 (${stat.size} bytes): ${key}`);
140
+ return null;
141
+ }
112
142
  const data = await fs.readFile(sessionPath, 'utf-8');
113
143
  return JSON.parse(data);
114
144
  } catch {
@@ -375,7 +405,7 @@ async function getAgentForChannel(
375
405
  return existingSession;
376
406
  }
377
407
 
378
- // 构建频道的身份文档
408
+ // 构建频道的身份文档 (从 didDocRef 拿 cid/ipnsName, 不读整份 didDocument)
379
409
  const identityDoc = channelDid ? {
380
410
  did: channelDid,
381
411
  name: channelName || `Channel-${channelId.slice(-6)}`,
@@ -522,13 +552,18 @@ export async function createWebServer(port: number = 3000) {
522
552
  res.setHeader('Content-Type', 'text/event-stream');
523
553
  res.setHeader('Cache-Control', 'no-cache');
524
554
  res.setHeader('Connection', 'keep-alive');
555
+ // 反向代理 (nginx/cloudflair) 需要: 禁用缓冲 + 立即 flush
556
+ res.setHeader('X-Accel-Buffering', 'no');
525
557
  res.flushHeaders();
526
558
 
527
559
  const clientInfo = { res, channelId };
528
560
  sseClients.add(clientInfo as any);
561
+ console.log(`[SSE] 客户端连接 channelId=${channelId || '(broadcast)'}, 总数=${sseClients.size}`);
529
562
 
530
563
  req.on('close', () => {
531
564
  sseClients.delete(clientInfo as any);
565
+ try { res.end(); } catch {}
566
+ console.log(`[SSE] 客户端断开 channelId=${channelId || '(broadcast)'}, 剩余=${sseClients.size}`);
532
567
  });
533
568
  });
534
569
 
@@ -542,16 +577,21 @@ export async function createWebServer(port: number = 3000) {
542
577
  return res.status(400).json({ error: 'No channelId provided' });
543
578
  }
544
579
 
545
- // 获取频道信息,包括真实 DID 和完整 DID 文档
580
+ // 获取频道信息(只取轻量引用, 不再读完整 DID 文档)
546
581
  const channels = await loadChannels();
547
582
  const channel = channels.find(c => c.id === channelId);
548
583
  const currentSessionId = channel?.currentSessionId || 'default';
549
584
  const realChannelDid = channelDid || channel?.did || '';
550
585
  const realChannelName = channel?.name || '';
551
- const realChannelDidDoc = channel?.didDocument;
586
+ const realChannelDidDoc = channel?.didDocRef;
552
587
 
553
588
  broadcast({ type: 'user', content: text }, channelId);
554
589
 
590
+ // 提前捕获 wallet/autoTools 到本地变量, 避免下面 try 块内的 inner const channel
591
+ // (line ~638) 与这里外层的 const channel 形成 shadowing 让 TS 误报"使用前未声明"
592
+ const boundWalletAddress = channel?.walletAddress;
593
+ const autoToolsEnabled = channel?.autoInvokeTools !== false; // 默认开启
594
+
555
595
  try {
556
596
  const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
557
597
  let fullResponse = '';
@@ -576,7 +616,17 @@ export async function createWebServer(port: number = 3000) {
576
616
  console.log(`[消息处理] 开始处理用户消息, channelId: ${channelId}, sessionId: ${currentSessionId}`);
577
617
 
578
618
  // 将真实 DID 作为上下文前缀,让 AI 使用真实的 DID 而不是自己编造的
579
- const contextHint = realChannelDid ? `[系统上下文] 当前频道名称: ${realChannelName}, 你的真实 DID: ${realChannelDid}\n\n` : '';
619
+ let contextHint = '';
620
+ if (realChannelDid) contextHint += `[系统上下文] 当前频道名称: ${realChannelName}, 你的真实 DID: ${realChannelDid}\n`;
621
+ if (boundWalletAddress) {
622
+ contextHint += `[系统上下文] 已绑定的加密钱包地址: ${boundWalletAddress}。当用户授权或启用自动工具调用时, 可使用该地址发起链上操作。\n`;
623
+ }
624
+ if (autoToolsEnabled) {
625
+ contextHint += `[系统上下文] 自动工具调用已开启: 你可以使用受信任的本地工具 (shell / 文件 / skill) 而无需逐次询问用户。\n`;
626
+ } else {
627
+ contextHint += `[系统上下文] 自动工具调用已关闭: 每次执行工具前必须先与用户确认。\n`;
628
+ }
629
+ if (contextHint) contextHint += '\n';
580
630
  fullResponse = await agent.promptStream(contextHint + text, streamCallback);
581
631
 
582
632
  broadcast({ type: 'ai', content: fullResponse }, channelId);
@@ -613,61 +663,111 @@ export async function createWebServer(port: number = 3000) {
613
663
  }
614
664
  });
615
665
 
616
- // 获取频道并确保每个频道都有 DID
617
- async function getChannelsWithDID(): Promise<Channel[]> {
618
- const channels = await loadChannels();
619
- console.log(`[getChannelsWithDID] 加载了 ${channels.length} 个频道`);
620
- let changed = false;
666
+ // ---------- 频道元数据后台修复队列 ----------
667
+ // 关键点: 旧实现会在每次 GET /channels 时同步执行 KeyManager.generate() + IPFS POST,
668
+ // 多频道场景下持续分配密钥对 + 发起 HTTP 请求, 几轮就会把 Node 内存撑爆。
669
+ // 新实现: 入队 + 节流(2s) + 单飞, 立刻返回当前 channels, 修复异步进行。
670
+ const didFixQueue = new Set<string>(); // 待修复的 channelId
671
+ let didFixRunning = false;
672
+ let didFixTimer: NodeJS.Timeout | null = null;
621
673
 
622
- for (const channel of channels) {
623
- // 检查 DID 是否有效
624
- const didMissing = channel.did === undefined || channel.did === null || channel.did === 'undefined' || channel.did === 'null' || channel.did === '';
625
- console.log(`[getChannelsWithDID] 频道 ${channel.name}: did=${JSON.stringify(channel.did)}, 缺失=${didMissing}`);
674
+ function scheduleDidFix(channelId: string) {
675
+ didFixQueue.add(channelId);
676
+ if (didFixTimer) return;
677
+ didFixTimer = setTimeout(() => {
678
+ didFixTimer = null;
679
+ void runDidFixOnce();
680
+ }, 2000);
681
+ }
626
682
 
627
- if (didMissing) {
628
- console.log(`[修复频道] ${channel.name} (${channel.id}) 缺少 DID,正在生成...`);
629
- try {
630
- const kp = KeyManager.generate();
631
- const generatedDid = kp.did;
632
- console.log(`[修复频道] KeyManager.generate() 结果: kp=${!!kp}, did=${generatedDid}`);
633
-
634
- if (generatedDid && typeof generatedDid === 'string' && generatedDid.length > 0) {
635
- channel.did = generatedDid;
636
- channel.publicKey = Buffer.from(kp.publicKey).toString('hex');
637
- console.log(`[修复频道] ${channel.name} 生成了 DID: ${channel.did}`);
638
-
639
- // 发布到 IPFS 并保存完整 DID 文档
640
- try {
641
- const auth = await AgentAuthManager.newWithRemoteIpfs('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
642
- const result = await auth.registerAgent({ name: channel.name, services: [] }, kp, '');
643
- channel.cid = result.cid || '';
644
- // 保存完整 DID 文档(用于传递给 session)
645
- if (result.didDocument) {
646
- channel.didDocument = result.didDocument;
647
- }
648
- console.log(`[修复频道] ${channel.name} CID: ${channel.cid}`);
649
- } catch (ipfsErr) {
650
- console.log(`[修复频道] ${channel.name} IPFS 失败`);
651
- }
652
- } else {
653
- console.log(`[修复频道] ${channel.name} KeyManager 返回无效 DID`);
654
- channel.did = `did:web:${channel.id}`;
655
- channel.publicKey = `pk_${channel.id}`;
683
+ async function runDidFixOnce(): Promise<void> {
684
+ if (didFixRunning) return;
685
+ didFixRunning = true;
686
+ try {
687
+ while (didFixQueue.size > 0) {
688
+ const id = didFixQueue.values().next().value as string;
689
+ didFixQueue.delete(id);
690
+ try {
691
+ await fixOneChannelDID(id);
692
+ } catch (e) {
693
+ console.log(`[DID 修复] ${id} 失败: ${(e as Error).message}`);
656
694
  }
657
- changed = true;
658
- } catch (e) {
659
- console.log(`[修复频道] ${channel.name} 失败: ${e}`);
660
695
  }
696
+ } finally {
697
+ didFixRunning = false;
661
698
  }
662
699
  }
663
700
 
664
- if (changed) {
665
- console.log('[getChannelsWithDID] 保存修改后的频道');
701
+ async function fixOneChannelDID(channelId: string): Promise<void> {
702
+ const channels = await loadChannels();
703
+ const channel = channels.find(c => c.id === channelId);
704
+ if (!channel) return;
705
+ const didMissing = !channel.did || channel.did === 'undefined' || channel.did === 'null' || channel.did === '';
706
+ if (!didMissing) return;
707
+
708
+ let kp: any;
709
+ try {
710
+ kp = KeyManager.generate();
711
+ } catch {
712
+ kp = null;
713
+ }
714
+ if (kp && kp.did) {
715
+ channel.did = kp.did;
716
+ channel.publicKey = Buffer.from(kp.publicKey).toString('hex');
717
+ } else {
718
+ // 兜底: 用 channelId 派生, 不阻塞 UI
719
+ channel.did = `did:web:${channel.id}`;
720
+ channel.publicKey = `pk_${channel.id}`;
721
+ }
722
+ console.log(`[DID 修复] ${channel.name} DID = ${channel.did}`);
723
+
724
+ // IPFS 注册: 失败也无所谓, 后续可重试
725
+ try {
726
+ const auth = await AgentAuthManager.newWithRemoteIpfs('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
727
+ const result = await auth.registerAgent({ name: channel.name, services: [] }, kp, '');
728
+ channel.cid = result.cid || channel.cid;
729
+ // 关键: 不再保存整份 didDocument, 只留 cid/ipnsName 两个引用字段
730
+ if (result.didDocument) {
731
+ channel.didDocRef = {
732
+ cid: result.cid,
733
+ ipnsName: (result.didDocument as any)?.ipnsName
734
+ };
735
+ delete (channel as any).didDocument;
736
+ }
737
+ } catch {
738
+ // IPFS 不可用, 跳过 — 下次再试
739
+ }
666
740
  await saveChannels(channels);
667
741
  }
668
742
 
669
- return channels;
670
- }
743
+ // 频道列表响应缓存: 短时间内重复请求走缓存, 避免每次重读 + 重序列化 channels.json
744
+ // 跨作用域 (saveChannels 在模块顶层, 本函数在 createWebServer 内) 用 lastChannelsWriteAt 协调失效
745
+ const channelsCache = { data: null as Channel[] | null, cachedAt: 0 };
746
+ const CHANNELS_CACHE_TTL_MS = 500;
747
+
748
+ /** 获取频道列表 — 立即返回, 缺 DID 的频道入队后台修复 */
749
+ async function getChannelsWithDID(): Promise<Channel[]> {
750
+ const now = Date.now();
751
+ // 缓存命中: 数据有效 AND 在写盘之后 AND 在 TTL 内
752
+ if (channelsCache.data && channelsCache.cachedAt > lastChannelsWriteAt && channelsCache.cachedAt + CHANNELS_CACHE_TTL_MS > now) {
753
+ return channelsCache.data;
754
+ }
755
+ const channels = await loadChannels();
756
+ // 防御性剥除: 任何旧 channels.json 残留的 didDocument 都不返回给客户端
757
+ const sanitized = channels.map(ch => {
758
+ const { didDocument: _omit, ...rest } = ch as any;
759
+ return rest as Channel;
760
+ });
761
+ for (const channel of sanitized) {
762
+ const didMissing = !channel.did || channel.did === 'undefined' || channel.did === 'null' || channel.did === '';
763
+ if (didMissing) {
764
+ scheduleDidFix(channel.id);
765
+ }
766
+ }
767
+ channelsCache.data = sanitized;
768
+ channelsCache.cachedAt = now;
769
+ return sanitized;
770
+ }
671
771
 
672
772
  app.get('/channels', async (_req, res) => {
673
773
  try {
@@ -686,14 +786,17 @@ app.get('/channels', async (_req, res) => {
686
786
 
687
787
  app.post('/channels', async (req, res) => {
688
788
  try {
689
- const { name, agentId } = req.body;
690
- console.log(`[创建频道] 收到请求: name=${name}, agentId=${agentId}`);
789
+ const { name, agentId, walletAddress, autoInvokeTools } = req.body;
790
+ console.log(`[创建频道] 收到请求: name=${name}, agentId=${agentId}, wallet=${walletAddress ? 'yes' : 'no'}`);
691
791
  if (!name || !agentId) {
692
792
  return res.status(400).json({ error: 'name and agentId required' });
693
793
  }
694
794
  const channels = await loadChannels();
695
795
  const id = `ch_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
696
796
 
797
+ // 校验钱包地址格式 (粗校验: 0x + 40 hex / Solana base58 / Sui 0x+64)
798
+ const validWallet = isValidWalletAddress(walletAddress);
799
+
697
800
  // 先创建频道(不阻塞等待 DID 生成)
698
801
  const channel: Channel = {
699
802
  id,
@@ -702,6 +805,9 @@ app.get('/channels', async (_req, res) => {
702
805
  createdAt: new Date().toISOString(),
703
806
  updatedAt: new Date().toISOString(),
704
807
  currentSessionId: `sess_${Date.now()}`,
808
+ walletAddress: validWallet || undefined,
809
+ walletRegisteredAt: validWallet ? new Date().toISOString() : undefined,
810
+ autoInvokeTools: autoInvokeTools !== false, // 默认 true
705
811
  sessions: [{
706
812
  id: `sess_${Date.now()}`,
707
813
  createdAt: new Date().toISOString(),
@@ -716,34 +822,9 @@ app.get('/channels', async (_req, res) => {
716
822
  await saveSession({ channelId: id, sessionId: 'default', messages: [], lastUpdated: new Date().toISOString() });
717
823
  res.json(channel);
718
824
 
719
- // 后台生成 DID
720
- console.log(`[创建频道] 后台生成 DID...`);
721
- setTimeout(async () => {
722
- try {
723
- const kp = KeyManager.generate();
724
- if (kp.did) {
725
- const allChannels = await loadChannels();
726
- const ch = allChannels.find(c => c.id === id);
727
- if (ch) {
728
- ch.did = kp.did;
729
- ch.publicKey = Buffer.from(kp.publicKey).toString('hex');
730
- console.log(`[创建频道] DID 生成完成: ${ch.did}`);
731
-
732
- // 发布到 IPFS
733
- try {
734
- const auth = await AgentAuthManager.newWithRemoteIpfs('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
735
- const result = await auth.registerAgent({ name, services: [] }, kp, '');
736
- ch.cid = result.cid || '';
737
- console.log(`[创建频道] CID: ${ch.cid}`);
738
- } catch {}
739
-
740
- await saveChannels(allChannels);
741
- }
742
- }
743
- } catch (e) {
744
- console.log(`[创建频道] ${name} 后台生成 DID 失败`);
745
- }
746
- }, 100);
825
+ // 后台生成 DID — 用统一的修复队列, 避免每个 POST 都启动独立 setTimeout
826
+ console.log(`[创建频道] 加入 DID 修复队列...`);
827
+ scheduleDidFix(id);
747
828
  } catch (err: any) {
748
829
  console.error('[创建频道] 错误:', err);
749
830
  res.status(500).json({ error: err.message });
@@ -829,6 +910,50 @@ app.get('/channels', async (_req, res) => {
829
910
  }
830
911
  });
831
912
 
913
+ // 删除单个 Session
914
+ app.delete('/channels/:channelId/sessions/:sessionId', async (req, res) => {
915
+ try {
916
+ const { channelId, sessionId } = req.params;
917
+ const channels = await loadChannels();
918
+ const channel = channels.find(c => c.id === channelId);
919
+
920
+ if (!channel) {
921
+ return res.status(404).json({ error: 'Channel not found' });
922
+ }
923
+
924
+ // 不允许删除最后一个 session —— 至少要保留一个
925
+ if (!channel.sessions || channel.sessions.length <= 1) {
926
+ return res.status(400).json({ error: 'At least one session is required' });
927
+ }
928
+
929
+ const sessionIndex = channel.sessions.findIndex(s => s.id === sessionId);
930
+ if (sessionIndex === -1) {
931
+ return res.status(404).json({ error: 'Session not found' });
932
+ }
933
+
934
+ channel.sessions.splice(sessionIndex, 1);
935
+
936
+ // 如果删除的是当前 session,切换到列表里的第一个
937
+ if (channel.currentSessionId === sessionId) {
938
+ const nextSession = channel.sessions[0];
939
+ channel.currentSessionId = nextSession.id;
940
+ }
941
+
942
+ channel.updatedAt = new Date().toISOString();
943
+ await saveChannels(channels);
944
+
945
+ // 删除 session 文件
946
+ try {
947
+ await fs.unlink(path.join(SESSION_CACHE_PATH, `${channelId}:${sessionId}.json`));
948
+ } catch {}
949
+
950
+ res.json({ ok: true, currentSessionId: channel.currentSessionId });
951
+ } catch (err: any) {
952
+ console.error('[删除Session] 错误:', err);
953
+ res.status(500).json({ error: err.message });
954
+ }
955
+ });
956
+
832
957
  app.delete('/channels/:channelId', async (req, res) => {
833
958
  try {
834
959
  const { channelId } = req.params;
@@ -837,11 +962,21 @@ app.get('/channels', async (_req, res) => {
837
962
  if (index === -1) {
838
963
  return res.status(404).json({ error: 'Channel not found' });
839
964
  }
965
+ const channel = channels[index];
840
966
  channels.splice(index, 1);
841
967
  await saveChannels(channels);
842
- try {
843
- await fs.unlink(path.join(SESSION_CACHE_PATH, `${channelId}.json`));
844
- } catch {}
968
+
969
+ // 清理该 channel 名下所有的 session 文件 + 默认 session 文件
970
+ const candidates = new Set<string>([`${channelId}.json`]);
971
+ if (channel.sessions) {
972
+ channel.sessions.forEach(s => candidates.add(`${channelId}:${s.id}.json`));
973
+ }
974
+ for (const filename of candidates) {
975
+ try {
976
+ await fs.unlink(path.join(SESSION_CACHE_PATH, filename));
977
+ } catch {}
978
+ }
979
+
845
980
  res.json({ ok: true });
846
981
  } catch (err: any) {
847
982
  res.status(500).json({ error: err.message });
@@ -851,16 +986,32 @@ app.get('/channels', async (_req, res) => {
851
986
  app.patch('/channels/:channelId', async (req, res) => {
852
987
  try {
853
988
  const { channelId } = req.params;
854
- const { name } = req.body;
855
- if (!name) {
856
- return res.status(400).json({ error: 'Name required' });
857
- }
989
+ const { name, walletAddress, autoInvokeTools } = req.body;
858
990
  const channels = await loadChannels();
859
991
  const channel = channels.find(c => c.id === channelId);
860
992
  if (!channel) {
861
993
  return res.status(404).json({ error: 'Channel not found' });
862
994
  }
863
- channel.name = name;
995
+ if (typeof name === 'string' && name.trim()) {
996
+ channel.name = name.trim();
997
+ }
998
+ // walletAddress 允许 null/'' 来解绑
999
+ if (walletAddress !== undefined) {
1000
+ if (walletAddress === null || walletAddress === '') {
1001
+ channel.walletAddress = undefined;
1002
+ channel.walletRegisteredAt = undefined;
1003
+ } else {
1004
+ const valid = isValidWalletAddress(walletAddress);
1005
+ if (!valid) {
1006
+ return res.status(400).json({ error: 'Invalid wallet address format' });
1007
+ }
1008
+ channel.walletAddress = valid;
1009
+ channel.walletRegisteredAt = channel.walletRegisteredAt || new Date().toISOString();
1010
+ }
1011
+ }
1012
+ if (typeof autoInvokeTools === 'boolean') {
1013
+ channel.autoInvokeTools = autoInvokeTools;
1014
+ }
864
1015
  channel.updatedAt = new Date().toISOString();
865
1016
  await saveChannels(channels);
866
1017
  res.json(channel);
@@ -871,8 +1022,39 @@ app.get('/channels', async (_req, res) => {
871
1022
 
872
1023
  app.get('/sessions/:channelId', async (req, res) => {
873
1024
  try {
874
- const session = await loadSession(req.params.channelId);
875
- res.json(session || { channelId: req.params.channelId, messages: [], lastUpdated: null });
1025
+ const session = await loadSession(req.params.channelId, req.query.sessionId as string | undefined);
1026
+ res.json(session || { channelId: req.params.channelId, sessionId: req.query.sessionId || 'default', messages: [], lastUpdated: null });
1027
+ } catch (err: any) {
1028
+ res.status(500).json({ error: err.message });
1029
+ }
1030
+ });
1031
+
1032
+ // 增量追加消息到 session (前端落盘用, 避免丢消息)
1033
+ // body: { message: { type, content, timestamp? } }
1034
+ app.patch('/sessions/:channelId/:sessionId', async (req, res) => {
1035
+ try {
1036
+ const { channelId, sessionId } = req.params;
1037
+ const { message } = req.body || {};
1038
+ if (!message || (message.type !== 'user' && message.type !== 'ai') || typeof message.content !== 'string') {
1039
+ return res.status(400).json({ error: 'invalid message' });
1040
+ }
1041
+ const existing = await loadSession(channelId, sessionId);
1042
+ const session: Session = existing || { channelId, sessionId, messages: [], lastUpdated: new Date().toISOString() };
1043
+ session.sessionId = sessionId;
1044
+ // 去重: 跳过与最后一条完全相同的 (避免 SSE 重复推导致双写)
1045
+ const last = session.messages[session.messages.length - 1];
1046
+ if (last && last.type === message.type && last.content === message.content) {
1047
+ return res.json({ ok: true, count: session.messages.length, deduped: true });
1048
+ }
1049
+ session.messages.push({
1050
+ id: message.id || crypto.randomUUID(),
1051
+ type: message.type,
1052
+ content: message.content,
1053
+ timestamp: message.timestamp || new Date().toISOString()
1054
+ });
1055
+ session.lastUpdated = new Date().toISOString();
1056
+ await saveSession(session);
1057
+ res.json({ ok: true, count: session.messages.length });
876
1058
  } catch (err: any) {
877
1059
  res.status(500).json({ error: err.message });
878
1060
  }
@@ -916,7 +1098,7 @@ app.get('/channels', async (_req, res) => {
916
1098
  const currentSessionId = channel?.currentSessionId || 'default';
917
1099
  const realChannelDid = channel?.did || '';
918
1100
  const realChannelName = channel?.name || '';
919
- const realChannelDidDoc = channel?.didDocument;
1101
+ const realChannelDidDoc = channel?.didDocRef;
920
1102
 
921
1103
  // 通知前端开始重新生成
922
1104
  broadcast({ type: 'regenerating', channelId }, channelId);
@@ -2209,6 +2391,12 @@ app.get('/channels', async (_req, res) => {
2209
2391
 
2210
2392
  // 启动看门狗监控
2211
2393
  if (watchdog) {
2394
+ // level 1 (内存爆) → 进程自杀, 依赖外层 supervisor / 用户重启 (Windows 任务计划/手动)
2395
+ // 否则 Node.js 高 GC 压力下 HTTP 响应丢失, 客户端 fetch 永远 pending
2396
+ watchdog.registerRestartStrategy(1, () => {
2397
+ console.error('[Watchdog] memory critical, 进程退出 (期望外层重启)');
2398
+ setTimeout(() => process.exit(1), 100);
2399
+ });
2212
2400
  watchdog.start();
2213
2401
  console.log('[24h] Watchdog started');
2214
2402
  }
@@ -2219,6 +2407,105 @@ app.get('/channels', async (_req, res) => {
2219
2407
  console.log('[24h] Health monitor periodic check started');
2220
2408
  }
2221
2409
 
2410
+ // ==================== Self-Improve 端点 ====================
2411
+ // 查看当前策略 (白名单 / 黑名单)
2412
+ app.get('/api/self-improve/policy', async (_req: any, res: any) => {
2413
+ const { loadPolicy } = await import('../agents/shell-guard.js');
2414
+ const policy = loadPolicy(true); // 强制重读
2415
+ if (!policy) {
2416
+ res.status(500).json({ error: '策略加载失败, 当前用硬编码兜底' });
2417
+ return;
2418
+ }
2419
+ res.json(policy);
2420
+ });
2421
+
2422
+ // 更新策略 (白名单 / 黑名单)
2423
+ // **仅供人手动调用**, 不会暴露给 AI
2424
+ app.put('/api/self-improve/policy', async (req: any, res: any) => {
2425
+ const { writePolicy, auditShellCall } = await import('../agents/shell-guard.js');
2426
+ const newPolicy = req.body;
2427
+ if (!newPolicy || typeof newPolicy !== 'object') {
2428
+ res.status(400).json({ error: 'body 必须是对象' });
2429
+ return;
2430
+ }
2431
+ // 极简校验
2432
+ if (!Array.isArray(newPolicy.commandAllowlist) || !Array.isArray(newPolicy.pathAllowlist) || !Array.isArray(newPolicy.pathDenylist)) {
2433
+ res.status(400).json({ error: 'commandAllowlist/pathAllowlist/pathDenylist 必须是数组' });
2434
+ return;
2435
+ }
2436
+ try {
2437
+ const success = writePolicy(newPolicy);
2438
+ if (success) {
2439
+ auditShellCall('allowed', 'api:PUT:/api/self-improve/policy', [], `人类用户更新策略`);
2440
+ res.json({ ok: true, message: '策略已更新, 60 秒内生效' });
2441
+ } else {
2442
+ res.status(500).json({ error: '写入策略文件失败' });
2443
+ }
2444
+ } catch (err: any) {
2445
+ res.status(500).json({ error: err.message });
2446
+ }
2447
+ });
2448
+
2449
+ // 查看审计日志
2450
+ app.get('/api/self-improve/audit', async (_req: any, res: any) => {
2451
+ try {
2452
+ const { POLICY_AUDIT_PATH_PUBLIC } = await import('../agents/shell-guard.js');
2453
+ const fs = await import('fs/promises');
2454
+ const auditPath = POLICY_AUDIT_PATH_PUBLIC;
2455
+ const exists = await fs.stat(auditPath).then(() => true).catch(() => false);
2456
+ if (!exists) {
2457
+ res.json([]);
2458
+ return;
2459
+ }
2460
+ const content = await fs.readFile(auditPath, 'utf-8');
2461
+ const lines = content.split('\n').filter(Boolean).slice(-200); // 最近 200 条
2462
+ const entries = lines.map((l) => {
2463
+ try { return JSON.parse(l); } catch { return { raw: l }; }
2464
+ });
2465
+ res.json(entries);
2466
+ } catch (err: any) {
2467
+ res.status(500).json({ error: err.message });
2468
+ }
2469
+ });
2470
+
2471
+ // 手动触发 (供前端按钮 / 调试用)
2472
+ app.post('/api/self-improve/trigger', async (req: any, res: any) => {
2473
+ const { goal, kind } = req.body || {};
2474
+ const { reportSelfImproveEvent } = await import('../heartbeat/self-improve-bus.js');
2475
+ const result = reportSelfImproveEvent({
2476
+ kind: kind || 'user-requested',
2477
+ details: String(goal || '用户手动触发')
2478
+ });
2479
+ res.json(result);
2480
+ });
2481
+
2482
+ // 事件历史 (供前端显示 / 调试)
2483
+ app.get('/api/self-improve/history', async (_req: any, res: any) => {
2484
+ const { getEventHistory } = await import('../heartbeat/self-improve-bus.js');
2485
+ res.json(getEventHistory());
2486
+ });
2487
+
2488
+ // 健康检查错误数 ≥ 2 -> 触发自改信号
2489
+ if (healthMonitor) {
2490
+ healthMonitor.startPeriodicCheck(60000, (status: any) => {
2491
+ const errorCount = Object.values(status.checks as Record<string, { status: string }>)
2492
+ .filter((c) => c.status === 'error').length;
2493
+ if (errorCount >= 2) {
2494
+ import('../heartbeat/self-improve-bus.js').then(({ reportSelfImproveEvent }) => {
2495
+ const failedKeys = Object.entries(status.checks as Record<string, { status: string }>)
2496
+ .filter(([_, c]) => c.status === 'error').map(([k]) => k).join(', ');
2497
+ reportSelfImproveEvent({
2498
+ kind: 'silent-timeout',
2499
+ details: `健康检查有 ${errorCount} 项失败: ${failedKeys}`
2500
+ });
2501
+ });
2502
+ }
2503
+ });
2504
+ }
2505
+
2506
+ // 安装自改总线 -> SSE 桥
2507
+ void installSelfImproveHook();
2508
+
2222
2509
  return new Promise<{ app: express.Express; server: typeof server }>((resolve) => {
2223
2510
  server.listen(port, () => {
2224
2511
  console.log(`Web 服务器启动完成: http://localhost:${port}`);
@@ -2269,6 +2556,45 @@ async function installChatBusHook(): Promise<void> {
2269
2556
  }
2270
2557
  }
2271
2558
 
2559
+ // ============================================================================
2560
+ // Self-Improve Bus -> SSE 桥 (供前端 / 用户看到自改触发)
2561
+ // ============================================================================
2562
+ let selfImproveHookInstalled = false;
2563
+ async function installSelfImproveHook(): Promise<void> {
2564
+ if (selfImproveHookInstalled) return;
2565
+ selfImproveHookInstalled = true;
2566
+ try {
2567
+ const { onSelfImproveTrigger } = await import('../heartbeat/self-improve-bus.js');
2568
+ const { runSelfImproveLoop } = await import('../agents/pi-sdk.js');
2569
+
2570
+ // 监听自改事件 -> 跑循环 + 广播到前端
2571
+ onSelfImproveTrigger(async (event, goal) => {
2572
+ broadcast({
2573
+ type: 'self_improve_triggered',
2574
+ eventKind: event.kind,
2575
+ details: event.details,
2576
+ goal,
2577
+ ts: Date.now()
2578
+ }, undefined);
2579
+
2580
+ // 实际跑循环 (创分支等)
2581
+ const result = await runSelfImproveLoop(goal);
2582
+
2583
+ broadcast({
2584
+ type: 'self_improve_result',
2585
+ success: result.success,
2586
+ output: result.output,
2587
+ error: result.error,
2588
+ ts: Date.now()
2589
+ }, undefined);
2590
+ });
2591
+
2592
+ console.log('[self-improve] SSE bridge installed');
2593
+ } catch (e) {
2594
+ console.warn('[self-improve] install failed:', (e as Error).message);
2595
+ }
2596
+ }
2597
+
2272
2598
  function getUserName(): string {
2273
2599
  const home = process.env.HOME || process.env.USERPROFILE || '';
2274
2600
  const match = home.match(/\/Users\/(\w+)/);