@bolloon/bolloon-agent 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/pi-sdk.js +222 -9
- package/dist/agents/shell-guard.js +354 -0
- package/dist/agents/shell-tool.js +83 -0
- package/dist/agents/skill-loader.js +174 -0
- package/dist/bollharness-integration/context-chain-router.js +3 -3
- package/dist/bollharness-integration/context-router.js +1 -1
- package/dist/heartbeat/Watchdog.js +7 -5
- package/dist/heartbeat/index.js +1 -0
- package/dist/heartbeat/self-improve-bus.js +85 -0
- package/dist/pi-ecosystem-judgment/index.js +1 -2
- package/dist/utils/auto-update.js +44 -12
- package/dist/web/client.js +841 -103
- package/dist/web/index.html +88 -8
- package/dist/web/style.css +506 -9
- package/package.json +2 -2
- package/scripts/build-cli.js +11 -1
- package/src/agents/pi-sdk.ts +230 -10
- package/src/agents/shell-guard.ts +417 -0
- package/src/agents/shell-tool.ts +103 -0
- package/src/agents/skill-loader.ts +202 -0
- package/src/bollharness-integration/context-chain-router.ts +3 -3
- package/src/bollharness-integration/context-router.ts +1 -1
- package/src/heartbeat/Watchdog.ts +7 -5
- package/src/heartbeat/index.ts +1 -0
- package/src/heartbeat/self-improve-bus.ts +110 -0
- package/src/types.d.ts +12 -0
- package/src/utils/auto-update.ts +45 -14
- package/src/web/client.js +841 -103
- package/src/web/index.html +88 -8
- package/src/web/server.ts +427 -101
- package/src/web/style.css +506 -9
- package/dist/bollharness-integration/bollharness-integration/context-router-judgment.d.ts +0 -48
- package/dist/bollharness-integration/bollharness-integration/context-router-judgment.js +0 -261
- package/dist/bollharness-integration/bollharness-integration/context-router.d.ts +0 -110
- package/dist/bollharness-integration/bollharness-integration/context-router.js +0 -542
- package/dist/bollharness-integration/bollharness-integration/gate-state-machine.d.ts +0 -87
- package/dist/bollharness-integration/bollharness-integration/gate-state-machine.js +0 -231
- package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.d.ts +0 -30
- package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.js +0 -91
- package/dist/bollharness-integration/bollharness-integration/guard-checker.d.ts +0 -105
- package/dist/bollharness-integration/bollharness-integration/guard-checker.js +0 -353
- package/dist/bollharness-integration/bollharness-integration/index.d.ts +0 -66
- package/dist/bollharness-integration/bollharness-integration/index.js +0 -32
- package/dist/bollharness-integration/bollharness-integration/integration.d.ts +0 -219
- package/dist/bollharness-integration/bollharness-integration/integration.js +0 -420
- package/dist/bollharness-integration/bollharness-integration/skill-adapter.d.ts +0 -151
- 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
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
//
|
|
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?.
|
|
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
|
-
|
|
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
|
-
//
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
console.log(`[
|
|
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
|
-
|
|
665
|
-
|
|
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
|
-
|
|
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(`[创建频道]
|
|
721
|
-
|
|
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
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
-
|
|
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?.
|
|
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+)/);
|