@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.
- package/dist/agents/p2p-chat-tools.js +321 -0
- package/dist/agents/p2p-document-tools.js +121 -1
- package/dist/agents/pi-sdk.js +185 -0
- 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/agents/workflow-pivot-loop.js +4 -4
- package/dist/bollharness-integration/context-chain-router.js +3 -3
- package/dist/bollharness-integration/context-router.js +1 -1
- package/dist/cli-entry.js +1 -1
- package/dist/documents/reader.js +5 -0
- package/dist/documents/store.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/llm/pi-ai.js +6 -5
- package/dist/network/iroh-discovery.js +2 -1
- package/dist/network/iroh-transport.js +15 -2
- package/dist/network/p2p.js +9 -8
- package/dist/network/storage/adapters/json-adapter.js +16 -1
- package/dist/network/storage/index.js +2 -1
- package/dist/pi-ecosystem-judgment/index.js +42 -115
- package/dist/social/channels/channel-heartbeat-agent.js +1 -1
- package/dist/utils/auto-update.js +44 -12
- package/dist/web/client.js +839 -103
- package/dist/web/index.html +100 -8
- package/dist/web/server.js +568 -98
- package/dist/web/style.css +506 -9
- package/package.json +2 -2
- package/scripts/build-cli.js +11 -1
- package/scripts/build-web.ts +1 -1
- package/src/agents/p2p-chat-tools.ts +383 -0
- package/src/agents/p2p-document-tools.ts +151 -1
- package/src/agents/pi-sdk.ts +196 -0
- 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/agents/workflow-pivot-loop.ts +13 -12
- package/src/bollharness-integration/channel-judgment-engine.ts +1 -1
- package/src/bollharness-integration/context-chain-router.ts +3 -3
- package/src/bollharness-integration/context-router.ts +1 -1
- package/src/documents/reader.ts +5 -0
- package/src/documents/store.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/llm/pi-ai.ts +6 -5
- package/src/network/iroh-discovery.ts +2 -1
- package/src/network/iroh-transport.ts +15 -2
- package/src/network/p2p.ts +9 -8
- package/src/network/storage/adapters/json-adapter.ts +17 -2
- package/src/network/storage/index.ts +19 -3
- package/src/social/channels/channel-heartbeat-agent.ts +1 -1
- package/src/types.d.ts +12 -0
- package/src/utils/auto-update.ts +45 -14
- package/src/web/client.js +839 -103
- package/src/web/index.html +88 -8
- package/src/web/server.ts +577 -102
- package/src/web/style.css +506 -9
- package/tsconfig.electron.json +1 -1
- package/tsconfig.json +1 -1
- 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/dist/web/server.js
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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?.
|
|
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
|
-
|
|
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
|
-
//
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
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(`[
|
|
537
|
+
console.log(`[DID 修复] ${id} 失败: ${e.message}`);
|
|
500
538
|
}
|
|
501
539
|
}
|
|
502
540
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
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(`[创建频道]
|
|
555
|
-
|
|
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
|
-
|
|
667
|
-
|
|
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
|
-
|
|
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?.
|
|
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+)/);
|