@bolloon/bolloon-agent 0.1.33 → 0.1.35
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/.auto-evolve-calls +1 -0
- package/.last-auto-evolve-baseline +1 -0
- package/Bolloon.md +103 -0
- package/README.md +7 -2
- package/dist/agents/pi-sdk.js +264 -12
- package/dist/bollharness-integration/index.js +8 -1
- package/dist/bootstrap/bootstrap.js +114 -0
- package/dist/bootstrap/context-collector.js +296 -0
- package/dist/bootstrap/lifecycle-hooks.js +109 -0
- package/dist/bootstrap/project-context.js +151 -0
- package/dist/heartbeat/Watchdog.js +9 -1
- package/dist/index.js +11 -0
- package/dist/llm/pi-ai.js +31 -21
- package/dist/network/p2p-direct.js +59 -2
- package/dist/pi-ecosystem/index.js +9 -6
- package/dist/pi-ecosystem-judgment/adaptive-scan.js +231 -0
- package/dist/pi-ecosystem-judgment/causal-judge.js +449 -0
- package/dist/pi-ecosystem-judgment/decision.js +5 -2
- package/dist/pi-ecosystem-judgment/detect-hook.js +168 -0
- package/dist/pi-ecosystem-judgment/distill-prompt.js +226 -0
- package/dist/pi-ecosystem-judgment/evolve-judgment.js +170 -0
- package/dist/pi-ecosystem-judgment/human-value-pipeline.js +21 -0
- package/dist/pi-ecosystem-judgment/human-value-store.js +283 -22
- package/dist/pi-ecosystem-judgment/injection-gate.js +166 -0
- package/dist/pi-ecosystem-judgment/monitor-gate.js +188 -0
- package/dist/security/builtin-guards.js +124 -0
- package/dist/security/context-router-tool.js +106 -0
- package/dist/security/react-harness.js +143 -0
- package/dist/security/tool-gate.js +235 -0
- package/dist/social/heartbeat.js +19 -2
- package/dist/utils/auto-evolve-policy.js +117 -0
- package/dist/utils/clamp.js +7 -0
- package/dist/utils/double.js +6 -0
- package/dist/web/api-config.html +3 -3
- package/dist/web/client.js +1328 -351
- package/dist/web/index.html +34 -31
- package/dist/web/server.js +1128 -58
- package/dist/web/style.css +370 -0
- package/lefthook.yml +29 -0
- package/package.json +4 -2
- package/scripts/auto-evolve-loop.ts +376 -0
- package/scripts/auto-evolve-oneshot.sh +155 -0
- package/scripts/auto-evolve-snapshot.sh +136 -0
- package/scripts/detect-schema-changes.sh +48 -0
- package/scripts/diff-reviewer.ts +159 -0
- package/scripts/weekly-report.ts +364 -0
- package/src/agents/pi-sdk.ts +293 -15
- package/src/bollharness-integration/index.ts +8 -32
- package/src/bootstrap/bootstrap.ts +132 -0
- package/src/bootstrap/context-collector.ts +342 -0
- package/src/bootstrap/lifecycle-hooks.ts +176 -0
- package/src/bootstrap/project-context.ts +163 -0
- package/src/heartbeat/Watchdog.ts +9 -1
- package/src/index.ts +11 -0
- package/src/llm/pi-ai.ts +33 -22
- package/src/network/p2p-direct.ts +59 -3
- package/src/security/builtin-guards.ts +162 -0
- package/src/security/context-router-tool.ts +122 -0
- package/src/security/react-harness.ts +177 -0
- package/src/security/tool-gate.ts +294 -0
- package/src/social/ant-colony/index.js +19 -0
- package/src/social/heartbeat.ts +18 -2
- package/src/utils/auto-evolve-policy.ts +138 -0
- package/src/utils/clamp.ts +5 -0
- package/src/web/api-config.html +3 -3
- package/src/web/client.js +1328 -351
- package/src/web/index.html +34 -31
- package/src/web/server.ts +1179 -53
- package/src/web/style.css +370 -0
- package/staging/auto-evolve/clean-001/.review-verdict +9 -0
- package/staging/auto-evolve/clean-001/clean-001.patch +14 -0
- package/staging/auto-evolve/e2e-001/.patch-id +1 -0
- package/staging/auto-evolve/e2e-001/.review-verdict +12 -0
- package/staging/auto-evolve/e2e-001/e2e-001.patch +11 -0
- package/staging/auto-evolve/test-bad/.review-verdict +12 -0
- package/staging/auto-evolve/test-bad/test-bad.patch +11 -0
- package/src/social/ant-colony/AdaptiveHeartbeat.ts +0 -131
- package/src/social/ant-colony/PheromoneEngine.ts +0 -302
- package/src/social/ant-colony/index.ts +0 -18
- package/src/social/ant-colony/types.ts +0 -94
package/src/web/server.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
import * as fs from 'fs/promises';
|
|
6
6
|
import * as fsSync from 'fs';
|
|
7
7
|
import * as path from 'path';
|
|
8
|
+
import * as os from 'os';
|
|
8
9
|
import {
|
|
9
10
|
HyperswarmCommunicator,
|
|
10
11
|
createHyperswarmCommunicator,
|
|
@@ -73,6 +74,17 @@ interface Channel {
|
|
|
73
74
|
name: string;
|
|
74
75
|
agentId: string;
|
|
75
76
|
did?: string;
|
|
77
|
+
// 2026-06-11: channel 级 persona + 关联文档 (从 ~/.bolloon/persona.json 复制或独立覆盖)
|
|
78
|
+
persona?: {
|
|
79
|
+
name?: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
personality?: string;
|
|
82
|
+
greeting?: string;
|
|
83
|
+
capabilities?: string[];
|
|
84
|
+
interests?: string[];
|
|
85
|
+
};
|
|
86
|
+
// 关联的文档 ID 列表 (启动 LLM 时自动加载到 context)
|
|
87
|
+
linkedDocumentIds?: string[];
|
|
76
88
|
publicKey?: string;
|
|
77
89
|
cid?: string;
|
|
78
90
|
/** 轻量引用:从 didDocument 只挑出 cid/ipnsName, 不存整份文档 */
|
|
@@ -419,8 +431,52 @@ let sseClients: Set<SSEClient> = new Set();
|
|
|
419
431
|
// v3: 远端 channel UI 元数据缓存 — key: peerId, value: sanitize 过的 channel 列表
|
|
420
432
|
// in-memory only, 进程重启清空 (judgment 内容永远不在这里)
|
|
421
433
|
let remoteChannelCache: Map<string, Array<Record<string, unknown>>> = new Map();
|
|
434
|
+
|
|
435
|
+
// 2026-06-10: 持久化 remote channel cache 到 ~/.bolloon/remote-channels-cache.json
|
|
436
|
+
// 之前是纯内存 Map, nodeA 重启后所有对端 channel 列表丢失, 需要等对面再推一次
|
|
437
|
+
const REMOTE_CACHE_FILE = `${process.env.HOME || '/tmp'}/.bolloon/remote-channels-cache.json`;
|
|
438
|
+
async function loadRemoteChannelCacheFromDisk(): Promise<void> {
|
|
439
|
+
try {
|
|
440
|
+
const { readFile, mkdir } = await import('fs/promises');
|
|
441
|
+
const { existsSync } = await import('fs');
|
|
442
|
+
if (!existsSync(REMOTE_CACHE_FILE)) return;
|
|
443
|
+
const raw = await readFile(REMOTE_CACHE_FILE, 'utf-8');
|
|
444
|
+
const obj = JSON.parse(raw);
|
|
445
|
+
if (obj && typeof obj === 'object') {
|
|
446
|
+
for (const [pk, list] of Object.entries(obj)) {
|
|
447
|
+
if (Array.isArray(list)) {
|
|
448
|
+
remoteChannelCache.set(pk, list as Array<Record<string, unknown>>);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
console.log(`[v3-meta] 从磁盘恢复 ${remoteChannelCache.size} 个 peer 的 channel cache`);
|
|
452
|
+
}
|
|
453
|
+
} catch (err) {
|
|
454
|
+
console.warn('[v3-meta] 恢复 remote channel cache 失败 (非致命):', (err as Error).message);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
async function persistRemoteChannelCache(): Promise<void> {
|
|
458
|
+
try {
|
|
459
|
+
const { writeFile, mkdir } = await import('fs/promises');
|
|
460
|
+
const { existsSync } = await import('fs');
|
|
461
|
+
if (!existsSync(`${process.env.HOME || '/tmp'}/.bolloon`)) {
|
|
462
|
+
await mkdir(`${process.env.HOME || '/tmp'}/.bolloon`, { recursive: true });
|
|
463
|
+
}
|
|
464
|
+
const obj: Record<string, unknown> = {};
|
|
465
|
+
for (const [pk, list] of remoteChannelCache.entries()) {
|
|
466
|
+
obj[pk] = list;
|
|
467
|
+
}
|
|
468
|
+
await writeFile(REMOTE_CACHE_FILE, JSON.stringify(obj, null, 2), 'utf-8');
|
|
469
|
+
} catch (err) {
|
|
470
|
+
console.warn('[v3-meta] 持久化 remote channel cache 失败 (非致命):', (err as Error).message);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// 启动时立即同步读一次 (异步, 不阻塞)
|
|
474
|
+
loadRemoteChannelCacheFromDisk();
|
|
422
475
|
// v3: P2PDirect 引用 (Hyperswarm 薄包装) - 模块级, 因为 web server 闭包里不可用
|
|
423
476
|
let v3P2PRef: import('../network/p2p-direct.js').P2PDirect | null = null;
|
|
477
|
+
// 2026-06-10: watchdog 提升到 module-level, 让 broadcast() / 模块级业务函数能埋点喂活动
|
|
478
|
+
// 之前在 createWebServer 闭包内, 闭包外的 broadcast() 拿不到 → 误判 30min 无活动 → 自杀.
|
|
479
|
+
let watchdogRef: any = null;
|
|
424
480
|
// v3: 等待中的 history RPC (B 端 chat-history endpoint 用) — rpcId → { resolve, reject }
|
|
425
481
|
const v3PendingHistoryGets: Map<string, { resolve: (data: any) => void; reject: (err: Error) => void }> = new Map();
|
|
426
482
|
let channelSessions: Map<string, AgentSession> = new Map(); // key: channelId
|
|
@@ -461,7 +517,6 @@ function sanitizeChannelForPeer(
|
|
|
461
517
|
createdAt: ch.createdAt,
|
|
462
518
|
updatedAt: ch.updatedAt,
|
|
463
519
|
hasWallet: !!ch.walletAddress,
|
|
464
|
-
boundJudgmentCount: Array.isArray(ch.bound_judgment_ids) ? ch.bound_judgment_ids.length : 0,
|
|
465
520
|
share_id: ch.share_id,
|
|
466
521
|
// 🔒 不返回: bound_judgment_ids, walletAddress, walletBinding, autoInvokeTools, sessions, shared_with_peers
|
|
467
522
|
};
|
|
@@ -706,6 +761,15 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
|
|
|
706
761
|
console.warn(`[v3] 存 user 消息失败 (不影响 chat):`, (saveErr as Error).message);
|
|
707
762
|
}
|
|
708
763
|
|
|
764
|
+
// v3 修复: 同步给 A 自己的 UI — broadcast SSE 事件让 A 的 owner 实时看到 B 的消息
|
|
765
|
+
broadcast({
|
|
766
|
+
type: 'user',
|
|
767
|
+
content: text,
|
|
768
|
+
channelId,
|
|
769
|
+
source: 'remote',
|
|
770
|
+
fromPublicKey: senderKey
|
|
771
|
+
}, channelId);
|
|
772
|
+
|
|
709
773
|
// v3 新增: 告诉 B "我开始想了, 用了哪些 judgment" — 让 B 看到决策依据
|
|
710
774
|
const judgmentHint = await buildJudgmentHint(ch, channelId);
|
|
711
775
|
const usedJudgments = await extractJudgmentsFromHint(ch);
|
|
@@ -753,7 +817,13 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
|
|
|
753
817
|
let fullResponse = '';
|
|
754
818
|
// v3 新增: 流式 token 节流推给 B — 让 B 看到过程
|
|
755
819
|
let lastFlushAt = 0;
|
|
820
|
+
let usedJudgmentIds: string[] = [];
|
|
756
821
|
const streamCallback: any = (event: any) => {
|
|
822
|
+
// P0.5: 注入门回传
|
|
823
|
+
if (event?.type === 'used_judgments' && Array.isArray(event.usedIds)) {
|
|
824
|
+
usedJudgmentIds = event.usedIds;
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
757
827
|
if (event.type === 'token') {
|
|
758
828
|
fullResponse += event.content;
|
|
759
829
|
if (fullResponse.length - lastFlushAt >= 20) {
|
|
@@ -767,7 +837,7 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
|
|
|
767
837
|
}
|
|
768
838
|
};
|
|
769
839
|
const agent = await getAgentForChannel(channelId, ch.did || '', ch.name, ch.didDocRef);
|
|
770
|
-
fullResponse = await agent.promptStream(fullPrompt, streamCallback);
|
|
840
|
+
fullResponse = await agent.promptStream(fullPrompt, streamCallback, undefined, channelId);
|
|
771
841
|
|
|
772
842
|
// v3 新增: 存 A 的 assistant 消息到 session — B 拉历史时能看到完整对话
|
|
773
843
|
try {
|
|
@@ -779,6 +849,7 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
|
|
|
779
849
|
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
780
850
|
type: 'ai',
|
|
781
851
|
content: fullResponse,
|
|
852
|
+
...(usedJudgmentIds.length > 0 ? { metadata: { usedJudgmentIds } } : {}),
|
|
782
853
|
timestamp: new Date().toISOString()
|
|
783
854
|
});
|
|
784
855
|
session.lastUpdated = new Date().toISOString();
|
|
@@ -788,6 +859,13 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
|
|
|
788
859
|
console.warn(`[v3] 存 assistant 消息失败 (不影响):`, (saveErr as Error).message);
|
|
789
860
|
}
|
|
790
861
|
|
|
862
|
+
// v3 修复: 同步给 A 自己的 UI — broadcast AI 回复给 A 的 owner 实时看到
|
|
863
|
+
broadcast({
|
|
864
|
+
type: 'ai',
|
|
865
|
+
content: fullResponse,
|
|
866
|
+
channelId
|
|
867
|
+
}, channelId);
|
|
868
|
+
|
|
791
869
|
// 3. 把完整回复发给 B
|
|
792
870
|
const reply = JSON.stringify({
|
|
793
871
|
v: 3, op: 'agent.chat.reply',
|
|
@@ -1100,6 +1178,18 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
|
|
|
1100
1178
|
console.error('[警告] 未处理的 Promise 拒绝:', reason);
|
|
1101
1179
|
});
|
|
1102
1180
|
|
|
1181
|
+
// Bolloon Bootstrap (幂等, 重复调不会重复挂定时器)
|
|
1182
|
+
// 这里独立调一次以保证 CLI-only 模式 (无 index.ts 引导) 也能 bootstrap
|
|
1183
|
+
try {
|
|
1184
|
+
const { bootstrapBolloon } = await import(
|
|
1185
|
+
'../pi-ecosystem-judgment/human-value-pipeline.js'
|
|
1186
|
+
);
|
|
1187
|
+
const bs = await bootstrapBolloon({ cwd: process.cwd() });
|
|
1188
|
+
console.log(`[createWebServer] bootstrap 完成 (${bs.durationMs}ms)`);
|
|
1189
|
+
} catch (err) {
|
|
1190
|
+
console.warn('[createWebServer] bootstrap 失败 (非致命):', err);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1103
1193
|
// 重置旧的 agent session,确保使用新的 LLM 配置
|
|
1104
1194
|
const { resetAgentSession } = await import('../agents/pi-sdk.js');
|
|
1105
1195
|
resetAgentSession();
|
|
@@ -1116,6 +1206,47 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
|
|
|
1116
1206
|
};
|
|
1117
1207
|
let p2pCommunicator: HyperswarmCommunicator | null = null;
|
|
1118
1208
|
|
|
1209
|
+
// v3: 定期 broadcast — 每个 peer 只收到分享给他的 channel (按 peer 个性化)
|
|
1210
|
+
// 走 known_peers (持久化) + sendTo (自动 joinPeer 重连), 不只 conns
|
|
1211
|
+
// 定义在此处 (所有 try 外部), 确保 route handlers 也能访问
|
|
1212
|
+
const v3BroadcastOwn = async () => {
|
|
1213
|
+
if (!v3P2PRef) return { sent: 0, total: 0 };
|
|
1214
|
+
const channels = await loadChannels();
|
|
1215
|
+
const { listPeers } = await import('../network/known-peers.js');
|
|
1216
|
+
const peers = await listPeers();
|
|
1217
|
+
const myPk = v3P2PRef.getPublicKey();
|
|
1218
|
+
// 2026-06-10: 本机名字一起携带, 对端能直接显示 + 落到自己的 known_peers
|
|
1219
|
+
let myName = process.env.BOLLOON_USER_NAME || process.env.USER || 'node';
|
|
1220
|
+
try {
|
|
1221
|
+
const { readFileSync, existsSync } = await import('fs');
|
|
1222
|
+
const cfgPath = `${process.env.HOME || '/tmp'}/.bolloon/config.json`;
|
|
1223
|
+
if (existsSync(cfgPath)) {
|
|
1224
|
+
const cfg = JSON.parse(readFileSync(cfgPath, 'utf-8'));
|
|
1225
|
+
if (cfg.userName) myName = cfg.userName;
|
|
1226
|
+
}
|
|
1227
|
+
} catch {}
|
|
1228
|
+
let sent = 0;
|
|
1229
|
+
for (const peer of peers) {
|
|
1230
|
+
if (peer.publicKey === myPk) continue;
|
|
1231
|
+
const sharedForPeer = channels
|
|
1232
|
+
.map(ch => sanitizeChannelForPeer(ch, peer.publicKey))
|
|
1233
|
+
.filter((x): x is Record<string, unknown> => x !== null);
|
|
1234
|
+
if (sharedForPeer.length > 0) {
|
|
1235
|
+
const msg = JSON.stringify({
|
|
1236
|
+
v: 3, op: 'agent.meta.list.reply',
|
|
1237
|
+
payload: { channels: sharedForPeer, name: myName, fromPublicKey: myPk }
|
|
1238
|
+
});
|
|
1239
|
+
const ok = v3P2PRef.sendTo(peer.publicKey, msg);
|
|
1240
|
+
if (ok) {
|
|
1241
|
+
sent++;
|
|
1242
|
+
console.log(`[v3] broadcast: ${peer.name || peer.publicKey.substring(0,8)} → ${sharedForPeer.length} 个 channel`);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
console.log(`[v3] broadcast 完成: sent=${sent}/${peers.length} 个 peer`);
|
|
1247
|
+
return { sent, total: peers.length };
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1119
1250
|
try {
|
|
1120
1251
|
console.log('开始生成 P2P 身份...');
|
|
1121
1252
|
|
|
@@ -1218,6 +1349,83 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
|
|
|
1218
1349
|
return Promise.resolve();
|
|
1219
1350
|
}
|
|
1220
1351
|
};
|
|
1352
|
+
// v3 新增: 好友申请 RPC — 任何对端可以发, 推到前端 UI 让用户接受
|
|
1353
|
+
if (parsed.op === 'agent.friend.request') {
|
|
1354
|
+
console.log(`[v3-friend] 收到 ${evt.fromPublicKey.substring(0,12)}... 的好友申请: ${parsed.payload?.name || '(无名字)'}`);
|
|
1355
|
+
broadcast({
|
|
1356
|
+
type: 'friend-request',
|
|
1357
|
+
fromPublicKey: evt.fromPublicKey,
|
|
1358
|
+
fromName: parsed.payload?.name || ('peer-' + evt.fromPublicKey.substring(0, 8)),
|
|
1359
|
+
message: parsed.payload?.message || '想加你为 P2P 好友',
|
|
1360
|
+
requestId: parsed.payload?.requestId, // 2026-06-10: 透传 requestId 给前端
|
|
1361
|
+
timestamp: Date.now()
|
|
1362
|
+
}, 'p2p-global');
|
|
1363
|
+
// 2026-06-10 新增: 立刻发 ack 回给发送方, 让发送方 UI 知道"对方收到了"
|
|
1364
|
+
try {
|
|
1365
|
+
const ackRpc = JSON.stringify({
|
|
1366
|
+
v: 3,
|
|
1367
|
+
op: 'agent.friend.request.ack',
|
|
1368
|
+
payload: {
|
|
1369
|
+
requestId: parsed.payload?.requestId,
|
|
1370
|
+
receivedBy: v3P2PRef?.getPublicKey(),
|
|
1371
|
+
timestamp: Date.now()
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
v3P2PRef?.sendTo(evt.fromPublicKey, ackRpc);
|
|
1375
|
+
} catch (err) {
|
|
1376
|
+
console.warn('[v3-friend] 发 ack 失败 (不阻塞):', (err as Error).message);
|
|
1377
|
+
}
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
// 2026-06-10 新增: 发送方收到对方 ack → SSE 推前端, 显示"对方已收到"
|
|
1381
|
+
if (parsed.op === 'agent.friend.request.ack') {
|
|
1382
|
+
console.log(`[v3-friend] 收到 ack: requestId=${(parsed.payload?.requestId || '').substring(0,8)} 来自 ${evt.fromPublicKey.substring(0,12)}...`);
|
|
1383
|
+
broadcast({
|
|
1384
|
+
type: 'friend-request-ack',
|
|
1385
|
+
requestId: parsed.payload?.requestId,
|
|
1386
|
+
receivedBy: parsed.payload?.receivedBy,
|
|
1387
|
+
timestamp: Date.now()
|
|
1388
|
+
}, 'p2p-global');
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
// v3 修复: agent.meta.list.reply 也走 v3P2PRef.on('data') (因为 handleV3P2PMessage 只走老通道)
|
|
1392
|
+
if (parsed.op === 'agent.meta.list.reply') {
|
|
1393
|
+
const list = parsed.payload?.channels || [];
|
|
1394
|
+
remoteChannelCache.set(evt.fromPublicKey, list);
|
|
1395
|
+
// 2026-06-10: 持久化到 ~/.bolloon/remote-channels-cache.json, 重启后不丢
|
|
1396
|
+
persistRemoteChannelCache();
|
|
1397
|
+
// 2026-06-10: 接收侧记录对方名字 (来自 list.reply payload.name), 落 known_peers
|
|
1398
|
+
const senderName = parsed.payload?.name;
|
|
1399
|
+
if (senderName && typeof senderName === 'string') {
|
|
1400
|
+
import('../network/known-peers.js').then(({ addOrUpdatePeer }) =>
|
|
1401
|
+
addOrUpdatePeer(senderName, evt.fromPublicKey)
|
|
1402
|
+
).catch(err => console.warn('[v3] 记录对端名字失败:', (err as Error).message));
|
|
1403
|
+
}
|
|
1404
|
+
console.log(`[v3] 收到 ${evt.fromPublicKey.substring(0,12)}... 的 ${list.length} 个 channel, 已缓存 (sender=${senderName || '?'})`);
|
|
1405
|
+
broadcast({
|
|
1406
|
+
type: 'remote-channel-update',
|
|
1407
|
+
peerId: evt.fromPublicKey,
|
|
1408
|
+
peerName: senderName, // 2026-06-10: 一并带名字到 UI
|
|
1409
|
+
channels: list
|
|
1410
|
+
}, 'p2p-global');
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
// 2026-06-10: 收到对方请求本机的 channel 列表 (启动时主动发请求, 加速 cache 填充)
|
|
1414
|
+
if (parsed.op === 'agent.meta.list.request') {
|
|
1415
|
+
console.log(`[v3-meta] 收到 ${evt.fromPublicKey.substring(0,12)}... 的 channel 列表请求 → 立刻回包`);
|
|
1416
|
+
// 不能 await (在 on('data') sync 回调里), 改用 .then 异步处理
|
|
1417
|
+
loadChannels().then(channels => {
|
|
1418
|
+
const sharedForPeer = channels
|
|
1419
|
+
.map(ch => sanitizeChannelForPeer(ch, evt.fromPublicKey))
|
|
1420
|
+
.filter((x): x is Record<string, unknown> => x !== null);
|
|
1421
|
+
const msg = JSON.stringify({
|
|
1422
|
+
v: 3, op: 'agent.meta.list.reply',
|
|
1423
|
+
payload: { channels: sharedForPeer }
|
|
1424
|
+
});
|
|
1425
|
+
v3P2PRef!.sendTo(evt.fromPublicKey, msg);
|
|
1426
|
+
}).catch(err => console.warn('[v3-meta] 回应 channel 列表失败:', (err as Error).message));
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1221
1429
|
handleV3P2PMessage(parsed, { id: evt.fromPublicKey, publicKey: evt.fromPublicKey } as any, commShim as any);
|
|
1222
1430
|
}
|
|
1223
1431
|
} catch (err) {
|
|
@@ -1227,6 +1435,8 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
|
|
|
1227
1435
|
|
|
1228
1436
|
// 新连接进来 → 主动发我分享给 ta 的 channel 列表
|
|
1229
1437
|
v3P2PRef.on('connection', (evt: any) => {
|
|
1438
|
+
// 2026-06-10: 喂 watchdog —— 新连接到来是真实业务活动
|
|
1439
|
+
watchdogRef?.recordActivity?.();
|
|
1230
1440
|
setTimeout(async () => {
|
|
1231
1441
|
try {
|
|
1232
1442
|
const channels = await loadChannels();
|
|
@@ -1266,38 +1476,46 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
|
|
|
1266
1476
|
}
|
|
1267
1477
|
// 触发一次 broadcast 推送给所有重连的 peer
|
|
1268
1478
|
setTimeout(() => v3BroadcastOwn(), 2000);
|
|
1479
|
+
// 2026-06-10: 同时主动请求每个 known peer 把 ta 的 channel 列表推过来
|
|
1480
|
+
// 避免对面 publicKey 没变但 cache 丢了(本机重启) → 一直空
|
|
1481
|
+
setTimeout(() => requestChannelsFromAllPeers(), 3500);
|
|
1269
1482
|
} catch (err) {
|
|
1270
1483
|
console.error('[v3] 自动重连失败:', (err as Error).message);
|
|
1271
1484
|
}
|
|
1272
1485
|
}, 5000); // 5s 后再重连, 让 swarm 充分 bootstrap
|
|
1486
|
+
|
|
1487
|
+
// 2026-06-10 新增: 主动向所有 known peer 发起 channel 列表请求
|
|
1488
|
+
async function requestChannelsFromAllPeers() {
|
|
1489
|
+
if (!v3P2PRef) return;
|
|
1490
|
+
try {
|
|
1491
|
+
const { listPeers } = await import('../network/known-peers.js');
|
|
1492
|
+
const peers = await listPeers();
|
|
1493
|
+
const myPk = v3P2PRef.getPublicKey();
|
|
1494
|
+
const req = JSON.stringify({ v: 3, op: 'agent.meta.list.request', payload: { fromPublicKey: myPk } });
|
|
1495
|
+
let sent = 0;
|
|
1496
|
+
for (const peer of peers) {
|
|
1497
|
+
if (peer.publicKey === myPk) continue;
|
|
1498
|
+
// 用 sendToWithWait, 等 conn 就绪再发 (同 Step 5 sendToWithWait 修复)
|
|
1499
|
+
const r = await v3P2PRef.sendToWithWait(peer.publicKey, req, 3000);
|
|
1500
|
+
if (r === 'SENT') sent++;
|
|
1501
|
+
}
|
|
1502
|
+
console.log(`[v3-meta] requestChannelsFromAllPeers → sent=${sent}/${peers.length - 1}`);
|
|
1503
|
+
} catch (err) {
|
|
1504
|
+
console.warn('[v3-meta] requestChannelsFromAllPeers failed:', (err as Error).message);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
// 立即跑一次 + 每 30s 兜底 (跟 v3BroadcastOwn 一样的节奏)
|
|
1508
|
+
setTimeout(requestChannelsFromAllPeers, 4000);
|
|
1509
|
+
setInterval(requestChannelsFromAllPeers, 30000);
|
|
1273
1510
|
} catch (err) {
|
|
1274
1511
|
console.error('[v3] P2PDirect 启动失败:', (err as Error).message);
|
|
1275
1512
|
v3P2PRef = null;
|
|
1276
1513
|
}
|
|
1277
1514
|
|
|
1278
|
-
//
|
|
1279
|
-
const v3BroadcastOwn = () => {
|
|
1280
|
-
if (!v3P2PRef) return;
|
|
1281
|
-
loadChannels().then(channels => {
|
|
1282
|
-
const conns = (v3P2PRef as any).conns as Map<string, any>;
|
|
1283
|
-
if (!conns) return;
|
|
1284
|
-
for (const [peerPk, conn] of conns.entries()) {
|
|
1285
|
-
if (conn?.destroyed) continue;
|
|
1286
|
-
const sharedForPeer = channels
|
|
1287
|
-
.map(ch => sanitizeChannelForPeer(ch, peerPk))
|
|
1288
|
-
.filter((x): x is Record<string, unknown> => x !== null);
|
|
1289
|
-
if (sharedForPeer.length > 0) {
|
|
1290
|
-
const msg = JSON.stringify({ v: 3, op: 'agent.meta.list.reply', payload: { channels: sharedForPeer } });
|
|
1291
|
-
try { conn.write(Buffer.from(msg)); } catch {}
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
console.log(`[v3] broadcast 个性化: ${conns.size} 个 peer, 各自收到分享的 channel`);
|
|
1295
|
-
}).catch(err => console.error('[v3] broadcast 失败:', (err as Error).message));
|
|
1296
|
-
};
|
|
1515
|
+
// 首次广播: 等 swarm bootstrap 完成后推一次
|
|
1297
1516
|
setTimeout(v3BroadcastOwn, 3000);
|
|
1298
|
-
setTimeout
|
|
1299
|
-
|
|
1300
|
-
setTimeout(v3BroadcastOwn, 40000);
|
|
1517
|
+
// v3 修复: 用 setInterval 替代一次性 setTimeout, 确保分享变更后能持续推送给 peer
|
|
1518
|
+
setInterval(v3BroadcastOwn, 30000);
|
|
1301
1519
|
|
|
1302
1520
|
// 保留 @diap/sdk 的旧实例 (它的 Hyperswarm 实例能帮 P2PDirect 做 DHT bootstrap)
|
|
1303
1521
|
try {
|
|
@@ -1378,7 +1596,10 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
|
|
|
1378
1596
|
const channelId = req.query.channelId as string;
|
|
1379
1597
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
1380
1598
|
res.setHeader('Cache-Control', 'no-cache');
|
|
1381
|
-
|
|
1599
|
+
// 2026-06-11: 改 keep-alive → close
|
|
1600
|
+
// 原因: SSE 长连接占着 keep-alive 槽 (HTTP/1.1 + 浏览器 max 6 并发), 后续同源 fetch 排队 30s+
|
|
1601
|
+
// 设 close 让浏览器把 SSE 当长期流, 不抢占普通请求的 keep-alive 槽
|
|
1602
|
+
res.setHeader('Connection', 'close');
|
|
1382
1603
|
// 反向代理 (nginx/cloudflair) 需要: 禁用缓冲 + 立即 flush
|
|
1383
1604
|
res.setHeader('X-Accel-Buffering', 'no');
|
|
1384
1605
|
res.flushHeaders();
|
|
@@ -1414,6 +1635,14 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
|
|
|
1414
1635
|
|
|
1415
1636
|
broadcast({ type: 'user', content: text }, channelId);
|
|
1416
1637
|
|
|
1638
|
+
// 2026-06-11: /message 端点立即返回 202, LLM 后续处理挪到 setImmediate 后台跑
|
|
1639
|
+
// 之前 res.json 在 try 块末尾 (line 1815), 需要等 LLM (5-15s) + 落盘 + suggestRename (5-8s) = 13s+
|
|
1640
|
+
// 客户端 fetch 占用 13s, 视觉像"卡死", 切其他 channel 也感觉"无法加载"
|
|
1641
|
+
// 修法: 立即 res.json(202), try 块主体仍跑 (LLM 流 + 落盘) 但不阻塞 HTTP 响应
|
|
1642
|
+
// 关键: res.json 之后不能再调用 res.json (会抛 ERR_HTTP_HEADERS_SENT), 所以 try 块末尾的 res.json 必须用 res.headersSent 守卫
|
|
1643
|
+
res.status(202).json({ ok: true, async: true, channelId, sessionId: currentSessionId });
|
|
1644
|
+
console.log(`[v3-async] /message 立即返回 202, channel=${channelId}, text length=${text.length}`);
|
|
1645
|
+
|
|
1417
1646
|
// 提前捕获 wallet/autoTools 到本地变量, 避免下面 try 块内的 inner const channel
|
|
1418
1647
|
// (line ~638) 与这里外层的 const channel 形成 shadowing 让 TS 误报"使用前未声明"
|
|
1419
1648
|
const boundWalletAddress = channel?.walletAddress;
|
|
@@ -1421,11 +1650,42 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
|
|
|
1421
1650
|
// 捕获外层 channel 到独立变量, 避免被 try 块内 (line 740+) 的 const channel 遮蔽
|
|
1422
1651
|
const channelForJudgment = channel;
|
|
1423
1652
|
|
|
1653
|
+
// per-channel queue 检查: 已在跑就入队, 等当前跑完自动接上
|
|
1654
|
+
const runState = getOrCreateRunState(channelId);
|
|
1655
|
+
if (runState.running) {
|
|
1656
|
+
runState.queue.push({ channelId, text, boundWalletAddress, autoToolsEnabled });
|
|
1657
|
+
broadcastQueueUpdate(channelId);
|
|
1658
|
+
console.log(`[queue] /message 入队 channel=${channelId}, queue len=${runState.queue.length}`);
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
runState.running = true;
|
|
1662
|
+
runState.abortController = new AbortController();
|
|
1663
|
+
broadcastQueueUpdate(channelId);
|
|
1664
|
+
|
|
1424
1665
|
try {
|
|
1425
1666
|
const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
|
|
1426
1667
|
let fullResponse = '';
|
|
1668
|
+
// P0.5: 注入门回传的 usedIds, 落 session message metadata, UI 可查
|
|
1669
|
+
let usedJudgmentIds: string[] = [];
|
|
1427
1670
|
|
|
1428
1671
|
const streamCallback: StreamCallback = (event: StreamEvent) => {
|
|
1672
|
+
// P0.5: 捕获注入门回传
|
|
1673
|
+
if ((event as any).type === 'used_judgments' && Array.isArray((event as any).usedIds)) {
|
|
1674
|
+
usedJudgmentIds = (event as any).usedIds;
|
|
1675
|
+
// 同步推给前端 (用于 finalizeTimelineAsMessage 时给 addMessage 传 usedIds)
|
|
1676
|
+
broadcast({ type: 'used_judgments', usedIds: usedJudgmentIds }, channelId);
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
// 阶段事件 (注入门 / D 触发)
|
|
1680
|
+
if ((event as any).type === 'phase') {
|
|
1681
|
+
broadcast({
|
|
1682
|
+
type: 'phase',
|
|
1683
|
+
phase: (event as any).phase,
|
|
1684
|
+
detail: (event as any).detail,
|
|
1685
|
+
usedCount: (event as any).usedCount,
|
|
1686
|
+
}, channelId);
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1429
1689
|
// 同时发送给流式显示和工作流显示
|
|
1430
1690
|
if (event.type === 'token' || event.type === 'thinking') {
|
|
1431
1691
|
broadcast({ type: 'stream', streamType: event.type, content: event.content }, channelId);
|
|
@@ -1463,6 +1723,101 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
|
|
|
1463
1723
|
const judgmentHint = await buildJudgmentHint(channelForJudgment, channelId);
|
|
1464
1724
|
if (judgmentHint) contextHint += judgmentHint;
|
|
1465
1725
|
|
|
1726
|
+
// 2026-06-10: 注入 skills 列表 (本机 ~/.bolloon/skills/ 下所有 skills)
|
|
1727
|
+
// 让 LLM 知道有哪些 skill 可用, 在回复中提示用户
|
|
1728
|
+
try {
|
|
1729
|
+
const { loadSkillsFromPaths, defaultSkillPaths, describeSkill } = await import('../agents/skill-loader.js');
|
|
1730
|
+
const paths = defaultSkillPaths();
|
|
1731
|
+
const skills = await loadSkillsFromPaths(paths);
|
|
1732
|
+
if (skills.length > 0) {
|
|
1733
|
+
contextHint += `[系统上下文] 本机已加载的 skills (${skills.length} 个, 你可以提示用户主动调用):\n`;
|
|
1734
|
+
for (const s of skills.slice(0, 20)) { // 上限 20 避免 prompt 过长
|
|
1735
|
+
const desc = (s.description || '').slice(0, 80);
|
|
1736
|
+
contextHint += ` - /${s.name}${desc ? ' — ' + desc : ''}\n`;
|
|
1737
|
+
}
|
|
1738
|
+
contextHint += '调用语法: 用户说 "/技能名 ..." 或 你回复时建议 "/技能名 ..." 让用户主动触发.\n\n';
|
|
1739
|
+
}
|
|
1740
|
+
} catch (err) {
|
|
1741
|
+
// 静默失败 — skills 不是核心, 加载失败不阻塞
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// 2026-06-10: 注入 human values 摘要 (最常用的 judgment / 价值偏好)
|
|
1745
|
+
// 与 judgment 不同: values 是更宏观的"用户偏好", judgment 是针对具体决策的约束
|
|
1746
|
+
try {
|
|
1747
|
+
const { loadAllJudgments } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
1748
|
+
const allJudgments = await loadAllJudgments().catch(() => []);
|
|
1749
|
+
// 把所有 judgment 视作软参考 (跟 buildJudgmentHint 的 candidates 同理)
|
|
1750
|
+
if (Array.isArray(allJudgments) && allJudgments.length > 0) {
|
|
1751
|
+
contextHint += `[系统上下文] 用户的核心价值倾向 (来自 ${allJudgments.length} 条历史 judgment, 软参考, 体现而非复述):\n`;
|
|
1752
|
+
for (const j of allJudgments.slice(0, 8)) {
|
|
1753
|
+
const decision = (j.decision || '').slice(0, 80);
|
|
1754
|
+
contextHint += ` - ${decision}\n`;
|
|
1755
|
+
}
|
|
1756
|
+
contextHint += '\n';
|
|
1757
|
+
}
|
|
1758
|
+
} catch (err) {
|
|
1759
|
+
// 静默失败
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// 2026-06-10: 注入 documents 列表 (本机 documents/ 目录的文档元数据)
|
|
1763
|
+
// 让 LLM 知道有文档存在, 用户可主动要求读
|
|
1764
|
+
try {
|
|
1765
|
+
const { documentStore } = await import('../documents/store.js');
|
|
1766
|
+
const docs = await documentStore.getReceivedDocuments(50).catch(() => []);
|
|
1767
|
+
if (Array.isArray(docs) && docs.length > 0) {
|
|
1768
|
+
contextHint += `[系统上下文] 本机 documents (${docs.length} 篇, 用户可让你读):\n`;
|
|
1769
|
+
for (const d of docs.slice(0, 10)) {
|
|
1770
|
+
const name = d.fileName || d.id || '(未命名)';
|
|
1771
|
+
const size = d.fileSize ? ` (${Math.round(d.fileSize / 1024)}KB)` : '';
|
|
1772
|
+
const sender = d.fromNodeId ? ` [来自 ${d.fromNodeIdShort || d.fromNodeId.substring(0,8)}…]` : '';
|
|
1773
|
+
contextHint += ` - ${name}${size}${sender}\n`;
|
|
1774
|
+
}
|
|
1775
|
+
contextHint += '用户提到某文档时, 你可以调用读文档工具读取并总结.\n\n';
|
|
1776
|
+
}
|
|
1777
|
+
} catch (err) {
|
|
1778
|
+
// 静默失败
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// 2026-06-11: 注入此 channel 专属的 persona + 关联文档 (从 channel 字段读, LLM 长期记忆)
|
|
1782
|
+
const chPersona = channelForJudgment?.persona;
|
|
1783
|
+
if (chPersona && typeof chPersona === 'object') {
|
|
1784
|
+
contextHint += '[系统上下文] 此 channel 的人设 (你是这个角色):\n';
|
|
1785
|
+
if (chPersona.name) contextHint += ` 名字: ${chPersona.name}\n`;
|
|
1786
|
+
if (chPersona.description) contextHint += ` 描述: ${chPersona.description}\n`;
|
|
1787
|
+
if (chPersona.personality) contextHint += ` 性格: ${chPersona.personality}\n`;
|
|
1788
|
+
if (chPersona.greeting) contextHint += ` 问候: ${chPersona.greeting}\n`;
|
|
1789
|
+
if (Array.isArray(chPersona.capabilities) && chPersona.capabilities.length > 0) {
|
|
1790
|
+
contextHint += ` 能力: ${chPersona.capabilities.join('、')}\n`;
|
|
1791
|
+
}
|
|
1792
|
+
if (Array.isArray(chPersona.interests) && chPersona.interests.length > 0) {
|
|
1793
|
+
contextHint += ` 兴趣: ${chPersona.interests.join('、')}\n`;
|
|
1794
|
+
}
|
|
1795
|
+
contextHint += '回复时应自然体现这个角色 (不要硬搬原文, 像这个角色说话即可).\n\n';
|
|
1796
|
+
}
|
|
1797
|
+
const linkedIds = channelForJudgment?.linkedDocumentIds;
|
|
1798
|
+
if (Array.isArray(linkedIds) && linkedIds.length > 0) {
|
|
1799
|
+
try {
|
|
1800
|
+
const { documentStore } = await import('../documents/store.js');
|
|
1801
|
+
contextHint += `[系统上下文] 此 channel 关联了 ${linkedIds.length} 篇文档 (已自动加载内容, 你应基于它们回答):\n`;
|
|
1802
|
+
let loaded = 0;
|
|
1803
|
+
for (const docId of linkedIds.slice(0, 10)) {
|
|
1804
|
+
const doc = await documentStore.readDocument(docId).catch(() => null);
|
|
1805
|
+
if (!doc) continue;
|
|
1806
|
+
const name = doc.metadata?.fileName || docId;
|
|
1807
|
+
const content = (doc.content || '').slice(0, 1500); // 单篇 1.5KB 上限, 总 prompt 防爆
|
|
1808
|
+
contextHint += `\n--- 文档: ${name} ---\n${content}\n--- 文档结束 ---\n`;
|
|
1809
|
+
loaded++;
|
|
1810
|
+
}
|
|
1811
|
+
if (loaded === 0) {
|
|
1812
|
+
contextHint += `(但加载失败, 文档可能已被删除)\n\n`;
|
|
1813
|
+
} else {
|
|
1814
|
+
contextHint += '\n';
|
|
1815
|
+
}
|
|
1816
|
+
} catch (err) {
|
|
1817
|
+
console.warn('[v3-persona] 加载关联文档失败 (非致命):', (err as Error).message);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1466
1821
|
// v3 新增: 注入"可用渠道"目录, 让 LLM 知道可以 @-mention 哪些 channel
|
|
1467
1822
|
// - 本地 channels (除了自己)
|
|
1468
1823
|
// - 远端 channels (remoteChannelCache 缓存的)
|
|
@@ -1481,11 +1836,31 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
|
|
|
1481
1836
|
for (const c of remoteChannels) {
|
|
1482
1837
|
contextHint += ` - [远端, owner=${(c._ownerPublicKey || '').substring(0,8)}…] @${c.name} (id=${c.id})\n`;
|
|
1483
1838
|
}
|
|
1484
|
-
contextHint += '语法: 当你想给其他渠道发消息, 在回复中写 "@渠道名 我要说的话" 即可. 消息会持久化到目标 channel 的 session, 你之后能看到"自己"在那里说的话.\n
|
|
1839
|
+
contextHint += '语法: 当你想给其他渠道发消息, 在回复中写 "@渠道名 我要说的话" 即可. 消息会持久化到目标 channel 的 session, 你之后能看到"自己"在那里说的话.\n';
|
|
1840
|
+
// 2026-06-10 强化: 当用户消息里出现 @渠道名, 默认是请你代为转发, 务必在回复里包含对应的 @ 转发
|
|
1841
|
+
if (remoteChannels.length > 0) {
|
|
1842
|
+
contextHint += '重要: 上面列表里 [远端] 标记的 channel 在另一台机器上, 你可以像 @本地 channel 一样 @ 它们 — 我会通过 P2P 自动把消息送达对方智能体, 对方智能体的回复也会同步回来.\n';
|
|
1843
|
+
contextHint += '当用户在消息里 @ 了某个 (本地或远端) channel, 默认意图是希望你代为转发 — 你应该在回复中写出对应的 "@渠道名 转发内容", 否则用户的请求不会被路由出去.\n\n';
|
|
1844
|
+
} else {
|
|
1845
|
+
contextHint += '\n';
|
|
1846
|
+
}
|
|
1485
1847
|
}
|
|
1486
1848
|
|
|
1487
1849
|
if (contextHint) contextHint += '\n';
|
|
1488
|
-
|
|
1850
|
+
try {
|
|
1851
|
+
fullResponse = await agent.promptStream(contextHint + text, streamCallback, runState.abortController?.signal, channelId);
|
|
1852
|
+
} catch (err: any) {
|
|
1853
|
+
// abort 抛错: 保留已输出的部分 (fullResponse 可能是空字符串)
|
|
1854
|
+
if (runState.abortController?.signal.aborted || err?.name === 'AbortError') {
|
|
1855
|
+
console.log(`[chat] aborted channel=${channelId}`);
|
|
1856
|
+
} else {
|
|
1857
|
+
throw err;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
// abort 模式: 给 partial 拼后缀
|
|
1861
|
+
if (runState.abortController?.signal.aborted && fullResponse.trim().length > 0) {
|
|
1862
|
+
fullResponse = fullResponse + '\n\n_[生成已中断]_';
|
|
1863
|
+
}
|
|
1489
1864
|
|
|
1490
1865
|
// v3 新增: 解析 LLM 回复里的 @-mentions, 转发到目标 channel
|
|
1491
1866
|
await routeMentionsInReply(channelId, fullResponse, localChannels, remoteChannels);
|
|
@@ -1497,31 +1872,85 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
|
|
|
1497
1872
|
session.sessionId = currentSessionId;
|
|
1498
1873
|
// v3: 加 source 标记 (local = 内部 owner, remote = 远端访客)
|
|
1499
1874
|
session.messages.push({ id: crypto.randomUUID(), type: 'user' as const, content: text, timestamp: new Date().toISOString(), source: 'local' as any });
|
|
1500
|
-
session.messages.push({
|
|
1875
|
+
session.messages.push({
|
|
1876
|
+
id: crypto.randomUUID(),
|
|
1877
|
+
type: 'ai' as const,
|
|
1878
|
+
content: fullResponse,
|
|
1879
|
+
timestamp: new Date().toISOString(),
|
|
1880
|
+
source: 'local' as any,
|
|
1881
|
+
// P0.5: 这条 AI 回复引用了哪些 judgment (注入门回传)
|
|
1882
|
+
...(usedJudgmentIds.length > 0 ? { metadata: { usedJudgmentIds } } : {}),
|
|
1883
|
+
});
|
|
1501
1884
|
session.lastUpdated = new Date().toISOString();
|
|
1502
1885
|
await saveSession(session);
|
|
1503
1886
|
|
|
1504
1887
|
const channels = await loadChannels();
|
|
1505
1888
|
const channel = channels.find(c => c.id === channelId);
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
channel.name = renameSuggestion;
|
|
1510
|
-
await saveChannels(channels);
|
|
1511
|
-
broadcast({ type: 'renamed', channelId, newName: renameSuggestion }, channelId);
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1889
|
+
// 2026-06-11: 移除 suggestRename 的二次 LLM 调用 — 之前每次用户发消息, 智能体 channel 都会再调一次 LLM (5-8s) 自动改名
|
|
1890
|
+
// 影响: (1) /message 端点被拖慢 5-8s (2) LLM 客户端排队, 其他 channel 跟着卡
|
|
1891
|
+
// 现在改名逻辑挪到 /api/agent-rename 端点, 用户主动触发才跑
|
|
1514
1892
|
if (channel) {
|
|
1515
1893
|
channel.updatedAt = new Date().toISOString();
|
|
1516
1894
|
await saveChannels(channels);
|
|
1517
1895
|
}
|
|
1518
1896
|
|
|
1519
1897
|
broadcast({ type: 'done' }, channelId);
|
|
1520
|
-
|
|
1898
|
+
|
|
1899
|
+
// D 触发: AI 被动捕获判断力 (后台异步, 不阻塞主对话)
|
|
1900
|
+
setImmediate(() => {
|
|
1901
|
+
try {
|
|
1902
|
+
const lastTurns = session.messages.slice(-6).map((m) => ({
|
|
1903
|
+
role: (m.type === 'user' ? 'human' : 'agent') as 'human' | 'agent',
|
|
1904
|
+
content: m.content,
|
|
1905
|
+
}));
|
|
1906
|
+
if (lastTurns.length < 2) return;
|
|
1907
|
+
broadcast({ type: 'phase', phase: 'd_detect', detail: '监测对话...' }, channelId);
|
|
1908
|
+
import('../pi-ecosystem-judgment/human-value-pipeline.js')
|
|
1909
|
+
.then(async ({ detectAndDistillFromChannel, throttleDHook }) => {
|
|
1910
|
+
// channel 维度 5min 节流, 防对话卡顿时 LLM 反复触发
|
|
1911
|
+
if (!throttleDHook(channelId, 5 * 60_000)) {
|
|
1912
|
+
console.log(`[D-hook ${channelId}] throttled (within 5min)`);
|
|
1913
|
+
broadcast({ type: 'phase', phase: 'd_skip', detail: 'throttled' }, channelId);
|
|
1914
|
+
return null;
|
|
1915
|
+
}
|
|
1916
|
+
broadcast({ type: 'phase', phase: 'd_distill', detail: '蒸馏判断力...' }, channelId);
|
|
1917
|
+
return detectAndDistillFromChannel(lastTurns, { channelId });
|
|
1918
|
+
})
|
|
1919
|
+
.then((result) => {
|
|
1920
|
+
if (result && result.triggered) {
|
|
1921
|
+
console.log(
|
|
1922
|
+
`[D-hook ${channelId}] stored: ${result.reason}`,
|
|
1923
|
+
result.evolved
|
|
1924
|
+
);
|
|
1925
|
+
broadcast({ type: 'phase', phase: 'd_done', detail: result.reason }, channelId);
|
|
1926
|
+
} else if (result && result.reason) {
|
|
1927
|
+
console.log(`[D-hook ${channelId}] skipped: ${result.reason}`);
|
|
1928
|
+
broadcast({ type: 'phase', phase: 'd_skip', detail: result.reason }, channelId);
|
|
1929
|
+
}
|
|
1930
|
+
})
|
|
1931
|
+
.catch((err) => {
|
|
1932
|
+
console.warn(`[D-hook ${channelId}] failed:`, err);
|
|
1933
|
+
broadcast({ type: 'phase', phase: 'd_error', detail: String(err) }, channelId);
|
|
1934
|
+
});
|
|
1935
|
+
} catch (err) {
|
|
1936
|
+
console.warn(`[D-hook ${channelId}] sync error:`, err);
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
1939
|
+
|
|
1940
|
+
// 2026-06-11: 202 已发的话, 不要重复 res.json (会抛 ERR_HTTP_HEADERS_SENT)
|
|
1941
|
+
if (!res.headersSent) res.json({ ok: true });
|
|
1521
1942
|
} catch (err: any) {
|
|
1522
1943
|
broadcast({ type: 'error', content: err.message }, channelId);
|
|
1523
1944
|
broadcast({ type: 'done' }, channelId);
|
|
1524
|
-
res.status(500).json({ error: err.message });
|
|
1945
|
+
if (!res.headersSent) res.status(500).json({ error: err.message });
|
|
1946
|
+
} finally {
|
|
1947
|
+
// queue dequeue: 跑完或失败都要清状态
|
|
1948
|
+
// 当前实现: 自动接下一条需要把 ~200 行 try 块抽函数, 暂不抽.
|
|
1949
|
+
// 替代: 用户点 [队列 +N] 按钮时, 客户端发起一个特殊的 HTTP 请求触发下一条
|
|
1950
|
+
// (在 client.js 实现). 这里只清状态 + 广播.
|
|
1951
|
+
runState.running = false;
|
|
1952
|
+
runState.abortController = null;
|
|
1953
|
+
broadcastQueueUpdate(channelId);
|
|
1525
1954
|
}
|
|
1526
1955
|
});
|
|
1527
1956
|
|
|
@@ -1533,6 +1962,36 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
|
|
|
1533
1962
|
let didFixRunning = false;
|
|
1534
1963
|
let didFixTimer: NodeJS.Timeout | null = null;
|
|
1535
1964
|
|
|
1965
|
+
// ---------- per-channel 消息 queue + abort 状态 ----------
|
|
1966
|
+
// 同 channel 串行 (避免 LLM 调用互踩上下文), 跨 channel 互不干扰
|
|
1967
|
+
interface PendingMessage {
|
|
1968
|
+
channelId: string;
|
|
1969
|
+
text: string;
|
|
1970
|
+
boundWalletAddress?: string;
|
|
1971
|
+
autoToolsEnabled?: boolean;
|
|
1972
|
+
// (req, res 已经在 /message 里 res.status(202) 返回, 入队的只是要重跑的内容参数)
|
|
1973
|
+
}
|
|
1974
|
+
interface ChannelRunState {
|
|
1975
|
+
running: boolean;
|
|
1976
|
+
queue: PendingMessage[];
|
|
1977
|
+
abortController: AbortController | null;
|
|
1978
|
+
}
|
|
1979
|
+
const channelRunState: Map<string, ChannelRunState> = new Map();
|
|
1980
|
+
function getOrCreateRunState(channelId: string): ChannelRunState {
|
|
1981
|
+
let s = channelRunState.get(channelId);
|
|
1982
|
+
if (!s) {
|
|
1983
|
+
s = { running: false, queue: [], abortController: null };
|
|
1984
|
+
channelRunState.set(channelId, s);
|
|
1985
|
+
}
|
|
1986
|
+
return s;
|
|
1987
|
+
}
|
|
1988
|
+
function broadcastQueueUpdate(channelId: string): void {
|
|
1989
|
+
const s = channelRunState.get(channelId);
|
|
1990
|
+
const queueLength = s ? s.queue.length : 0;
|
|
1991
|
+
const running = s ? s.running : false;
|
|
1992
|
+
try { broadcast({ type: 'queue_update', channelId, queueLength, running }, channelId); } catch { /* */ }
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1536
1995
|
function scheduleDidFix(channelId: string) {
|
|
1537
1996
|
didFixQueue.add(channelId);
|
|
1538
1997
|
if (didFixTimer) return;
|
|
@@ -1649,9 +2108,21 @@ app.get('/channels', async (_req, res) => {
|
|
|
1649
2108
|
// v3: 列出本节点缓存的远端 channel (按 peerId 分组)
|
|
1650
2109
|
app.get('/api/remote-channels', async (_req, res) => {
|
|
1651
2110
|
try {
|
|
1652
|
-
const out: Array<{ peerId: string; channels: Array<Record<string, unknown
|
|
2111
|
+
const out: Array<{ peerId: string; channels: Array<Record<string, unknown>>; peerName?: string }> = [];
|
|
2112
|
+
// 2026-06-11: 合并 known_peers + cache, 避免 cache 空时 UI 一个 peer 都看不到
|
|
2113
|
+
// (cache 是纯内存, 重启即丢; known_peers 持久化, 至少能让 UI 显示"这些 peer 我认识")
|
|
2114
|
+
const { listPeers } = await import('../network/known-peers.js');
|
|
2115
|
+
const knownPeers = await listPeers();
|
|
2116
|
+
const knownByPk = new Map<string, { name?: string }>();
|
|
2117
|
+
for (const p of knownPeers) knownByPk.set(p.publicKey, { name: p.name });
|
|
1653
2118
|
for (const [peerId, list] of remoteChannelCache.entries()) {
|
|
1654
|
-
out.push({ peerId, channels: list });
|
|
2119
|
+
out.push({ peerId, channels: list, peerName: knownByPk.get(peerId)?.name });
|
|
2120
|
+
}
|
|
2121
|
+
// known_peers 里但 cache 没的, 占位推进 out (channels=[]) 让 UI 能渲染 peer header
|
|
2122
|
+
for (const [peerId, info] of knownByPk.entries()) {
|
|
2123
|
+
if (!remoteChannelCache.has(peerId)) {
|
|
2124
|
+
out.push({ peerId, channels: [], peerName: info.name });
|
|
2125
|
+
}
|
|
1655
2126
|
}
|
|
1656
2127
|
res.json({ count: out.length, peers: out });
|
|
1657
2128
|
} catch (err: any) {
|
|
@@ -1659,6 +2130,27 @@ app.get('/channels', async (_req, res) => {
|
|
|
1659
2130
|
}
|
|
1660
2131
|
});
|
|
1661
2132
|
|
|
2133
|
+
// v3 测试专用: 直接注入远端频道缓存 (绕过 P2P)
|
|
2134
|
+
// 仅当 NODE_ENV=test 时可用
|
|
2135
|
+
app.post('/api/test/inject-remote-channel', async (req, res) => {
|
|
2136
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
2137
|
+
return res.status(403).json({ error: 'only available in test mode' });
|
|
2138
|
+
}
|
|
2139
|
+
try {
|
|
2140
|
+
const { peerPublicKey, channel } = req.body || {};
|
|
2141
|
+
if (!peerPublicKey || !channel) {
|
|
2142
|
+
return res.status(400).json({ error: 'peerPublicKey and channel required' });
|
|
2143
|
+
}
|
|
2144
|
+
const list = remoteChannelCache.get(peerPublicKey) || [];
|
|
2145
|
+
list.push(channel);
|
|
2146
|
+
remoteChannelCache.set(peerPublicKey, list);
|
|
2147
|
+
broadcast({ type: 'remote-channel-update', peerId: peerPublicKey, channels: list }, 'p2p-global');
|
|
2148
|
+
res.json({ ok: true, count: list.length });
|
|
2149
|
+
} catch (err: any) {
|
|
2150
|
+
res.status(500).json({ error: err.message });
|
|
2151
|
+
}
|
|
2152
|
+
});
|
|
2153
|
+
|
|
1662
2154
|
// v3: 主动向所有已连接 P2P peer 拉 channel 列表
|
|
1663
2155
|
// 用法: B 端用户点 "刷新远端智能体" → 触发本 endpoint
|
|
1664
2156
|
app.post('/api/remote-channels/refresh', async (_req, res) => {
|
|
@@ -1738,7 +2230,7 @@ app.get('/channels', async (_req, res) => {
|
|
|
1738
2230
|
|
|
1739
2231
|
app.post('/channels', async (req, res) => {
|
|
1740
2232
|
try {
|
|
1741
|
-
const { name, agentId, walletAddress, autoInvokeTools, bound_judgment_ids } = req.body;
|
|
2233
|
+
const { name, agentId, walletAddress, autoInvokeTools, bound_judgment_ids, personaOverride, linkedDocumentIds } = req.body;
|
|
1742
2234
|
console.log(`[创建频道] 收到请求: name=${name}, agentId=${agentId}, wallet=${walletAddress ? 'yes' : 'no'}, boundJudgments=${Array.isArray(bound_judgment_ids) ? bound_judgment_ids.length : 0}`);
|
|
1743
2235
|
if (!name || !agentId) {
|
|
1744
2236
|
return res.status(400).json({ error: 'name and agentId required' });
|
|
@@ -1754,6 +2246,42 @@ app.get('/channels', async (_req, res) => {
|
|
|
1754
2246
|
? bound_judgment_ids.filter((x: unknown) => typeof x === 'string' && (x as string).length > 0)
|
|
1755
2247
|
: [];
|
|
1756
2248
|
|
|
2249
|
+
// 2026-06-11: persona 加载 — 优先用 personaOverride, 否则从 ~/.bolloon/persona.json 读全局默认
|
|
2250
|
+
let channelPersona: Channel['persona'];
|
|
2251
|
+
if (personaOverride && typeof personaOverride === 'object') {
|
|
2252
|
+
channelPersona = {
|
|
2253
|
+
name: personaOverride.name,
|
|
2254
|
+
description: personaOverride.description,
|
|
2255
|
+
personality: personaOverride.personality,
|
|
2256
|
+
greeting: personaOverride.greeting,
|
|
2257
|
+
capabilities: Array.isArray(personaOverride.capabilities) ? personaOverride.capabilities.slice(0, 20) : undefined,
|
|
2258
|
+
interests: Array.isArray(personaOverride.interests) ? personaOverride.interests.slice(0, 20) : undefined,
|
|
2259
|
+
};
|
|
2260
|
+
} else {
|
|
2261
|
+
try {
|
|
2262
|
+
const { readFileSync, existsSync } = await import('fs');
|
|
2263
|
+
const personaPath = `${process.env.HOME || '/tmp'}/.bolloon/persona.json`;
|
|
2264
|
+
if (existsSync(personaPath)) {
|
|
2265
|
+
const p = JSON.parse(readFileSync(personaPath, 'utf-8'));
|
|
2266
|
+
channelPersona = {
|
|
2267
|
+
name: p.name,
|
|
2268
|
+
description: p.description,
|
|
2269
|
+
personality: p.personality,
|
|
2270
|
+
greeting: p.greeting,
|
|
2271
|
+
capabilities: Array.isArray(p.capabilities) ? p.capabilities.slice(0, 20) : undefined,
|
|
2272
|
+
interests: Array.isArray(p.interests) ? p.interests.slice(0, 20) : undefined,
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
} catch (err) {
|
|
2276
|
+
console.warn('[创建频道] 加载 persona.json 失败 (非致命):', (err as Error).message);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
// 过滤 linkedDocumentIds: 只保留 string
|
|
2281
|
+
const safeLinkedDocIds = Array.isArray(linkedDocumentIds)
|
|
2282
|
+
? linkedDocumentIds.filter((x: unknown) => typeof x === 'string' && (x as string).length > 0).slice(0, 50)
|
|
2283
|
+
: [];
|
|
2284
|
+
|
|
1757
2285
|
// 先创建频道(不阻塞等待 DID 生成)
|
|
1758
2286
|
const channel: Channel = {
|
|
1759
2287
|
id,
|
|
@@ -1766,6 +2294,8 @@ app.get('/channels', async (_req, res) => {
|
|
|
1766
2294
|
walletRegisteredAt: validWallet ? new Date().toISOString() : undefined,
|
|
1767
2295
|
autoInvokeTools: autoInvokeTools !== false, // 默认 true
|
|
1768
2296
|
bound_judgment_ids: safeBoundIds,
|
|
2297
|
+
persona: channelPersona,
|
|
2298
|
+
linkedDocumentIds: safeLinkedDocIds,
|
|
1769
2299
|
sessions: [{
|
|
1770
2300
|
id: `sess_${Date.now()}`,
|
|
1771
2301
|
createdAt: new Date().toISOString(),
|
|
@@ -1944,7 +2474,7 @@ app.get('/channels', async (_req, res) => {
|
|
|
1944
2474
|
app.patch('/channels/:channelId', async (req, res) => {
|
|
1945
2475
|
try {
|
|
1946
2476
|
const { channelId } = req.params;
|
|
1947
|
-
const { name, walletAddress, autoInvokeTools, bound_judgment_ids, shared_with_peers } = req.body;
|
|
2477
|
+
const { name, walletAddress, autoInvokeTools, bound_judgment_ids, shared_with_peers, persona, linkedDocumentIds } = req.body;
|
|
1948
2478
|
const channels = await loadChannels();
|
|
1949
2479
|
const channel = channels.find(c => c.id === channelId);
|
|
1950
2480
|
if (!channel) {
|
|
@@ -1953,6 +2483,25 @@ app.get('/channels', async (_req, res) => {
|
|
|
1953
2483
|
if (typeof name === 'string' && name.trim()) {
|
|
1954
2484
|
channel.name = name.trim();
|
|
1955
2485
|
}
|
|
2486
|
+
// 2026-06-11: 改 persona (允许 null 重置回全局默认)
|
|
2487
|
+
if (persona !== undefined) {
|
|
2488
|
+
if (persona === null) {
|
|
2489
|
+
channel.persona = undefined;
|
|
2490
|
+
} else if (typeof persona === 'object') {
|
|
2491
|
+
channel.persona = {
|
|
2492
|
+
name: persona.name,
|
|
2493
|
+
description: persona.description,
|
|
2494
|
+
personality: persona.personality,
|
|
2495
|
+
greeting: persona.greeting,
|
|
2496
|
+
capabilities: Array.isArray(persona.capabilities) ? persona.capabilities.slice(0, 20) : channel.persona?.capabilities,
|
|
2497
|
+
interests: Array.isArray(persona.interests) ? persona.interests.slice(0, 20) : channel.persona?.interests,
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
// 2026-06-11: 改关联文档列表 (数组整体替换, 空数组 = 解绑所有)
|
|
2502
|
+
if (Array.isArray(linkedDocumentIds)) {
|
|
2503
|
+
channel.linkedDocumentIds = linkedDocumentIds.filter((x: unknown) => typeof x === 'string' && (x as string).length > 0).slice(0, 50);
|
|
2504
|
+
}
|
|
1956
2505
|
// walletAddress 允许 null/'' 来解绑
|
|
1957
2506
|
if (walletAddress !== undefined) {
|
|
1958
2507
|
if (walletAddress === null || walletAddress === '') {
|
|
@@ -2003,6 +2552,10 @@ app.get('/channels', async (_req, res) => {
|
|
|
2003
2552
|
}
|
|
2004
2553
|
channel.updatedAt = new Date().toISOString();
|
|
2005
2554
|
await saveChannels(channels);
|
|
2555
|
+
// v3 修复: 分享变更后立即广播给所有 peer, 不用等对方手动刷新
|
|
2556
|
+
if (shared_with_peers !== undefined) {
|
|
2557
|
+
v3BroadcastOwn().catch(err => console.error('[v3] broadcast after share update failed:', err));
|
|
2558
|
+
}
|
|
2006
2559
|
res.json(channel);
|
|
2007
2560
|
} catch (err: any) {
|
|
2008
2561
|
res.status(500).json({ error: err.message });
|
|
@@ -2154,8 +2707,14 @@ app.get('/channels', async (_req, res) => {
|
|
|
2154
2707
|
|
|
2155
2708
|
const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
|
|
2156
2709
|
let fullResponse = '';
|
|
2710
|
+
let usedJudgmentIds: string[] = [];
|
|
2157
2711
|
|
|
2158
2712
|
const streamCallback: StreamCallback = (event: StreamEvent) => {
|
|
2713
|
+
// P0.5: 注入门回传
|
|
2714
|
+
if ((event as any).type === 'used_judgments' && Array.isArray((event as any).usedIds)) {
|
|
2715
|
+
usedJudgmentIds = (event as any).usedIds;
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2159
2718
|
if (event.type === 'token' || event.type === 'thinking') {
|
|
2160
2719
|
broadcast({ type: 'stream', streamType: event.type, content: event.content }, channelId);
|
|
2161
2720
|
} else if (event.type === 'status' || event.type === 'tool') {
|
|
@@ -2167,7 +2726,7 @@ app.get('/channels', async (_req, res) => {
|
|
|
2167
2726
|
|
|
2168
2727
|
// 重新生成时只发送用户消息 (v3: 同时注入 channel 绑定的判断力)
|
|
2169
2728
|
const regenHint = await buildJudgmentHint(channel, channelId);
|
|
2170
|
-
fullResponse = await agent.promptStream(regenHint + userMessage, streamCallback);
|
|
2729
|
+
fullResponse = await agent.promptStream(regenHint + userMessage, streamCallback, undefined, channelId);
|
|
2171
2730
|
|
|
2172
2731
|
broadcast({ type: 'ai', content: fullResponse }, channelId);
|
|
2173
2732
|
|
|
@@ -2183,7 +2742,8 @@ app.get('/channels', async (_req, res) => {
|
|
|
2183
2742
|
id: crypto.randomUUID(),
|
|
2184
2743
|
type: 'ai' as const,
|
|
2185
2744
|
content: fullResponse,
|
|
2186
|
-
timestamp: new Date().toISOString()
|
|
2745
|
+
timestamp: new Date().toISOString(),
|
|
2746
|
+
...(usedJudgmentIds.length > 0 ? { metadata: { usedJudgmentIds } } : {}),
|
|
2187
2747
|
});
|
|
2188
2748
|
existingSession.lastUpdated = new Date().toISOString();
|
|
2189
2749
|
await saveSession(existingSession);
|
|
@@ -2728,13 +3288,24 @@ app.get('/channels', async (_req, res) => {
|
|
|
2728
3288
|
}
|
|
2729
3289
|
});
|
|
2730
3290
|
|
|
2731
|
-
// v3: 暴露 P2PDirect 自己的 publicKey
|
|
3291
|
+
// v3: 暴露 P2PDirect 自己的 publicKey + 本机名字, 对方可用它主动 connect 并自动取名
|
|
2732
3292
|
app.get('/api/p2p-publickey', async (_req, res) => {
|
|
2733
3293
|
try {
|
|
2734
3294
|
if (!v3P2PRef) {
|
|
2735
3295
|
return res.status(503).json({ error: 'P2PDirect not started' });
|
|
2736
3296
|
}
|
|
2737
|
-
|
|
3297
|
+
const publicKey = v3P2PRef.getPublicKey();
|
|
3298
|
+
// 2026-06-10: 把本机 user/agent name 一起返回, 对方拿到后能直接用
|
|
3299
|
+
let name = process.env.BOLLOON_USER_NAME || process.env.USER || 'node';
|
|
3300
|
+
try {
|
|
3301
|
+
const { readFileSync, existsSync } = await import('fs');
|
|
3302
|
+
const cfgPath = `${process.env.HOME || '/tmp'}/.bolloon/config.json`;
|
|
3303
|
+
if (existsSync(cfgPath)) {
|
|
3304
|
+
const cfg = JSON.parse(readFileSync(cfgPath, 'utf-8'));
|
|
3305
|
+
if (cfg.userName) name = cfg.userName;
|
|
3306
|
+
}
|
|
3307
|
+
} catch {}
|
|
3308
|
+
res.json({ publicKey, name, role: v3P2PRef.getRole() });
|
|
2738
3309
|
} catch (err: any) {
|
|
2739
3310
|
res.status(500).json({ error: err.message });
|
|
2740
3311
|
}
|
|
@@ -2774,6 +3345,37 @@ app.get('/channels', async (_req, res) => {
|
|
|
2774
3345
|
res.status(500).json({ error: err.message });
|
|
2775
3346
|
}
|
|
2776
3347
|
});
|
|
3348
|
+
// 2026-06-10: PATCH 重命名 / 改备注 / 同时影响 publicKey
|
|
3349
|
+
// 用法: PATCH /api/p2p-peers/:name { name?, notes?, publicKey? }
|
|
3350
|
+
app.patch('/api/p2p-peers/:name', async (req, res) => {
|
|
3351
|
+
try {
|
|
3352
|
+
const { addOrUpdatePeer, removePeer } = await import('../network/known-peers.js');
|
|
3353
|
+
const { readFile, writeFile } = await import('fs/promises');
|
|
3354
|
+
const { existsSync } = await import('fs');
|
|
3355
|
+
const filePath = `${process.env.HOME || '/tmp'}/.bolloon/known_peers.json`;
|
|
3356
|
+
if (!existsSync(filePath)) return res.status(404).json({ error: 'no known_peers.json' });
|
|
3357
|
+
const data = JSON.parse(await readFile(filePath, 'utf-8'));
|
|
3358
|
+
const oldName = req.params.name;
|
|
3359
|
+
const oldEntry = data.peers[oldName];
|
|
3360
|
+
if (!oldEntry) return res.status(404).json({ error: `peer "${oldName}" not found` });
|
|
3361
|
+
const { name: newName, notes, publicKey: newPk } = req.body || {};
|
|
3362
|
+
const finalName = newName || oldName;
|
|
3363
|
+
const finalPk = newPk || oldEntry.publicKey;
|
|
3364
|
+
if (finalName !== oldName) {
|
|
3365
|
+
delete data.peers[oldName];
|
|
3366
|
+
}
|
|
3367
|
+
data.peers[finalName] = {
|
|
3368
|
+
...oldEntry,
|
|
3369
|
+
publicKey: finalPk,
|
|
3370
|
+
name: finalName,
|
|
3371
|
+
notes: notes !== undefined ? notes : oldEntry.notes
|
|
3372
|
+
};
|
|
3373
|
+
await writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
3374
|
+
res.json({ ok: true, peer: data.peers[finalName] });
|
|
3375
|
+
} catch (err: any) {
|
|
3376
|
+
res.status(500).json({ error: err.message });
|
|
3377
|
+
}
|
|
3378
|
+
});
|
|
2777
3379
|
|
|
2778
3380
|
// v3: 主动 connect 到对端的 P2PDirect publicKey
|
|
2779
3381
|
// 用法: POST /api/remote-channels/p2p-connect { targetPublicKey: "<hex>" }
|
|
@@ -2812,6 +3414,93 @@ app.get('/channels', async (_req, res) => {
|
|
|
2812
3414
|
}
|
|
2813
3415
|
});
|
|
2814
3416
|
|
|
3417
|
+
// v3: 主动给对端发好友申请 — 推到对端 UI 让对方接受
|
|
3418
|
+
// 用法: POST /api/friend-request { targetPublicKey, name, message }
|
|
3419
|
+
// 2026-06-10 改: 用 sendToWithWait 等握手完成, 不再 fire-and-forget; 返回结构化 code 让前端知道失败
|
|
3420
|
+
app.post('/api/friend-request', async (req, res) => {
|
|
3421
|
+
try {
|
|
3422
|
+
if (!v3P2PRef) {
|
|
3423
|
+
return res.status(503).json({ ok: false, code: 'P2P_NOT_STARTED', error: 'P2PDirect not started' });
|
|
3424
|
+
}
|
|
3425
|
+
const { targetPublicKey, name, message } = req.body || {};
|
|
3426
|
+
if (!targetPublicKey || typeof targetPublicKey !== 'string' || targetPublicKey.length !== 64) {
|
|
3427
|
+
return res.status(400).json({ ok: false, code: 'BAD_REQUEST', error: 'targetPublicKey (64 hex) required' });
|
|
3428
|
+
}
|
|
3429
|
+
// 先 joinPeer 触发握手 (注意: joinPeer 不阻塞到 conn 就绪)
|
|
3430
|
+
const swarm = (v3P2PRef as any).swarm;
|
|
3431
|
+
if (swarm) {
|
|
3432
|
+
try { await swarm.joinPeer(Buffer.from(targetPublicKey, 'hex')); } catch {}
|
|
3433
|
+
}
|
|
3434
|
+
// 主动把对方加为本机 known_peers (本地视角认为对方是朋友)
|
|
3435
|
+
const { addOrUpdatePeer, findNameByPublicKey } = await import('../network/known-peers.js');
|
|
3436
|
+
const existing = await findNameByPublicKey(targetPublicKey);
|
|
3437
|
+
const peerName = name || existing || `peer-${targetPublicKey.substring(0, 8)}`;
|
|
3438
|
+
await addOrUpdatePeer(peerName, targetPublicKey);
|
|
3439
|
+
// 构造 RPC, 推到对端 — 对端会 SSE 推 friend-request 到前端
|
|
3440
|
+
const myPk = v3P2PRef.getPublicKey();
|
|
3441
|
+
const requestId = crypto.randomUUID();
|
|
3442
|
+
const rpc = JSON.stringify({
|
|
3443
|
+
v: 3,
|
|
3444
|
+
op: 'agent.friend.request',
|
|
3445
|
+
payload: {
|
|
3446
|
+
requestId, // 2026-06-10: 加 requestId, ack 时回带
|
|
3447
|
+
fromPublicKey: myPk,
|
|
3448
|
+
name: peerName,
|
|
3449
|
+
message: message || '想加你为 P2P 好友, 共享 channel 协作'
|
|
3450
|
+
}
|
|
3451
|
+
});
|
|
3452
|
+
// 2026-06-10: 用 sendToWithWait, 等 conn 真就绪后再发, 默认 5s 超时
|
|
3453
|
+
const result = await v3P2PRef.sendToWithWait(targetPublicKey, rpc, 5000);
|
|
3454
|
+
console.log(`[v3-friend] ${myPk.substring(0,12)}... 发送好友申请给 ${targetPublicKey.substring(0,12)}... (result=${result}, requestId=${requestId.substring(0,8)})`);
|
|
3455
|
+
if (result !== 'SENT') {
|
|
3456
|
+
return res.status(502).json({
|
|
3457
|
+
ok: false,
|
|
3458
|
+
code: result, // NO_CONN / WRITE_FAIL
|
|
3459
|
+
error: result === 'NO_CONN'
|
|
3460
|
+
? '对方未在线, 请确认对方已启动 bolloon 并互联'
|
|
3461
|
+
: '写入 P2P 通道失败, 请重试',
|
|
3462
|
+
persistedAs: peerName // 本地仍持久化, 等对方上线再 retry 即可
|
|
3463
|
+
});
|
|
3464
|
+
}
|
|
3465
|
+
res.json({ ok: true, sent: true, code: 'SENT', persistedAs: peerName, requestId });
|
|
3466
|
+
} catch (err: any) {
|
|
3467
|
+
console.error('[v3-friend] friend-request 失败:', err);
|
|
3468
|
+
res.status(500).json({ ok: false, code: 'EXCEPTION', error: err.message });
|
|
3469
|
+
}
|
|
3470
|
+
});
|
|
3471
|
+
|
|
3472
|
+
// v3: 接受对方的好友申请 — 把对方加为 known_peers, 立即推我的 channel 列表给 ta
|
|
3473
|
+
// 用法: POST /api/friend-accept { fromPublicKey, name }
|
|
3474
|
+
app.post('/api/friend-accept', async (req, res) => {
|
|
3475
|
+
try {
|
|
3476
|
+
if (!v3P2PRef) {
|
|
3477
|
+
return res.status(503).json({ error: 'P2PDirect not started' });
|
|
3478
|
+
}
|
|
3479
|
+
const { fromPublicKey, name } = req.body || {};
|
|
3480
|
+
if (!fromPublicKey || typeof fromPublicKey !== 'string' || fromPublicKey.length !== 64) {
|
|
3481
|
+
return res.status(400).json({ error: 'fromPublicKey (64 hex) required' });
|
|
3482
|
+
}
|
|
3483
|
+
// 持久化
|
|
3484
|
+
const { addOrUpdatePeer, findNameByPublicKey } = await import('../network/known-peers.js');
|
|
3485
|
+
const existing = await findNameByPublicKey(fromPublicKey);
|
|
3486
|
+
const peerName = name || existing || `peer-${fromPublicKey.substring(0, 8)}`;
|
|
3487
|
+
await addOrUpdatePeer(peerName, fromPublicKey);
|
|
3488
|
+
// joinPeer 确保连接存在 (连接可能已在 friend-request 时建立, 这里可能是 no-op)
|
|
3489
|
+
const swarm = (v3P2PRef as any).swarm;
|
|
3490
|
+
if (swarm) {
|
|
3491
|
+
try { await swarm.joinPeer(Buffer.from(fromPublicKey, 'hex')); } catch {}
|
|
3492
|
+
}
|
|
3493
|
+
// v3 修复: 主动广播自己的 channel 列表给新好友,
|
|
3494
|
+
// 不能依赖 connection handler, 因为连接在 friend-request 阶段已建立, 不会触发新 connection 事件
|
|
3495
|
+
v3BroadcastOwn().catch(err => console.error('[v3] broadcast after friend-accept failed:', err));
|
|
3496
|
+
console.log(`[v3-friend] 接受好友申请: ${fromPublicKey.substring(0,12)}... → ${peerName}`);
|
|
3497
|
+
res.json({ ok: true, persistedAs: peerName });
|
|
3498
|
+
} catch (err: any) {
|
|
3499
|
+
console.error('[v3-friend] friend-accept 失败:', err);
|
|
3500
|
+
res.status(500).json({ error: err.message });
|
|
3501
|
+
}
|
|
3502
|
+
});
|
|
3503
|
+
|
|
2815
3504
|
// v3: 给远端 channel 发消息 (B 节点) - 通过 P2PDirect 转发到 A, A 跑 LLM, 回 B
|
|
2816
3505
|
// 用法: POST /api/remote-channels/chat-send
|
|
2817
3506
|
// { targetPublicKey, channelId, text }
|
|
@@ -2839,6 +3528,8 @@ app.get('/channels', async (_req, res) => {
|
|
|
2839
3528
|
error: 'peer not connected. POST /api/remote-channels/p2p-connect first.'
|
|
2840
3529
|
});
|
|
2841
3530
|
}
|
|
3531
|
+
// 2026-06-10: 喂 watchdog — chat-send 成功是真实业务活动
|
|
3532
|
+
watchdogRef?.recordActivity?.();
|
|
2842
3533
|
console.log(`[v3] chat-send 转发到 ${targetPublicKey.substring(0, 12)}... (channelId=${channelId})`);
|
|
2843
3534
|
res.json({ ok: true, sent: true });
|
|
2844
3535
|
} catch (err: any) {
|
|
@@ -3470,6 +4161,23 @@ app.get('/channels', async (_req, res) => {
|
|
|
3470
4161
|
}
|
|
3471
4162
|
});
|
|
3472
4163
|
|
|
4164
|
+
// 终止当前 channel 的 LLM 流 (UI 终止按钮)
|
|
4165
|
+
app.post('/api/chat/abort', async (req, res) => {
|
|
4166
|
+
try {
|
|
4167
|
+
const { channelId } = req.body as { channelId?: string };
|
|
4168
|
+
if (!channelId) return res.status(400).json({ error: 'channelId required' });
|
|
4169
|
+
const s = channelRunState.get(channelId);
|
|
4170
|
+
if (s?.abortController) {
|
|
4171
|
+
s.abortController.abort();
|
|
4172
|
+
console.log(`[abort] user aborted channel=${channelId}`);
|
|
4173
|
+
return res.json({ ok: true, aborted: true });
|
|
4174
|
+
}
|
|
4175
|
+
res.json({ ok: true, aborted: false });
|
|
4176
|
+
} catch (err: any) {
|
|
4177
|
+
res.status(500).json({ error: err.message });
|
|
4178
|
+
}
|
|
4179
|
+
});
|
|
4180
|
+
|
|
3473
4181
|
// 主人审阅: 批准 draft
|
|
3474
4182
|
app.post('/api/chat/approve', async (req, res) => {
|
|
3475
4183
|
try {
|
|
@@ -3785,6 +4493,8 @@ app.get('/channels', async (_req, res) => {
|
|
|
3785
4493
|
healthMonitor = createHealthMonitor();
|
|
3786
4494
|
// 把 watchdog 静默阈值拉到 30 分钟, 避免开发期 / 用户空闲时被误杀
|
|
3787
4495
|
watchdog = createWatchdog({ silentThresholdMs: 30 * 60 * 1000 });
|
|
4496
|
+
// 2026-06-10: 同步到 module-level, 让 broadcast() / P2P handler / chat-send 都能喂活动
|
|
4497
|
+
watchdogRef = watchdog;
|
|
3788
4498
|
|
|
3789
4499
|
console.log('[24h] Heartbeat modules loaded');
|
|
3790
4500
|
} catch (err) {
|
|
@@ -3883,22 +4593,432 @@ app.get('/channels', async (_req, res) => {
|
|
|
3883
4593
|
}
|
|
3884
4594
|
});
|
|
3885
4595
|
|
|
3886
|
-
app.get('/api/judgments', async (
|
|
4596
|
+
app.get('/api/judgments', async (req, res) => {
|
|
3887
4597
|
try {
|
|
3888
|
-
const {
|
|
4598
|
+
const { listJudgmentsByStatus, initializeValueStore } = await import(
|
|
3889
4599
|
'../pi-ecosystem-judgment/human-value-store.js'
|
|
3890
4600
|
);
|
|
3891
4601
|
await initializeValueStore();
|
|
3892
|
-
const
|
|
3893
|
-
|
|
4602
|
+
const status = (typeof req.query.status === 'string' ? req.query.status : 'all') as
|
|
4603
|
+
| 'active'
|
|
4604
|
+
| 'pending'
|
|
4605
|
+
| 'superseded'
|
|
4606
|
+
| 'rejected'
|
|
4607
|
+
| 'all';
|
|
4608
|
+
const all = await listJudgmentsByStatus(status);
|
|
3894
4609
|
all.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
|
|
3895
|
-
res.json({ count: all.length, judgments: all });
|
|
4610
|
+
res.json({ count: all.length, status, judgments: all });
|
|
3896
4611
|
} catch (err: any) {
|
|
3897
4612
|
console.error('[judgments] GET failed:', err);
|
|
3898
4613
|
res.status(500).json({ error: err.message });
|
|
3899
4614
|
}
|
|
3900
4615
|
});
|
|
3901
4616
|
|
|
4617
|
+
// 蒸馏 B 触发 (人类点按钮) — 同步执行演化对齐
|
|
4618
|
+
app.post('/api/judgments/distill-from-conversation', async (req, res) => {
|
|
4619
|
+
try {
|
|
4620
|
+
const { channelId, messageId, recentTurns } = req.body as {
|
|
4621
|
+
channelId?: string;
|
|
4622
|
+
messageId?: string;
|
|
4623
|
+
recentTurns?: number;
|
|
4624
|
+
};
|
|
4625
|
+
if (!channelId) {
|
|
4626
|
+
return res.status(400).json({ error: 'channelId required' });
|
|
4627
|
+
}
|
|
4628
|
+
|
|
4629
|
+
// 取 channel 最近的对话
|
|
4630
|
+
const channels = await loadChannels();
|
|
4631
|
+
const channel = channels.find((c) => c.id === channelId);
|
|
4632
|
+
if (!channel) return res.status(404).json({ error: 'channel not found' });
|
|
4633
|
+
|
|
4634
|
+
const currentSessionId = channel.currentSessionId;
|
|
4635
|
+
if (!currentSessionId) {
|
|
4636
|
+
return res.status(400).json({ error: 'no active session in channel' });
|
|
4637
|
+
}
|
|
4638
|
+
const session = await loadSession(channelId, currentSessionId);
|
|
4639
|
+
if (!session) return res.status(404).json({ error: 'session not found' });
|
|
4640
|
+
|
|
4641
|
+
// 取最近 N 轮 (默认 10), 转成 DistillTurn 格式
|
|
4642
|
+
const limit = Math.min(Math.max(recentTurns ?? 10, 2), 30);
|
|
4643
|
+
const turns = session.messages.slice(-limit).map((m) => ({
|
|
4644
|
+
role: (m.type === 'user' ? 'human' : 'agent') as 'human' | 'agent',
|
|
4645
|
+
content: m.content,
|
|
4646
|
+
}));
|
|
4647
|
+
|
|
4648
|
+
const { distillAndStoreFromChannel } = await import(
|
|
4649
|
+
'../pi-ecosystem-judgment/human-value-pipeline.js'
|
|
4650
|
+
);
|
|
4651
|
+
const result = await distillAndStoreFromChannel(turns, { channelId });
|
|
4652
|
+
|
|
4653
|
+
res.json({
|
|
4654
|
+
ok: true,
|
|
4655
|
+
triggered: result.triggered,
|
|
4656
|
+
reason: result.reason,
|
|
4657
|
+
judgment: result.judgment,
|
|
4658
|
+
evolved: result.evolved,
|
|
4659
|
+
});
|
|
4660
|
+
} catch (err: any) {
|
|
4661
|
+
console.error('[judgments] distill-from-conversation failed:', err);
|
|
4662
|
+
res.status(500).json({ error: err.message });
|
|
4663
|
+
}
|
|
4664
|
+
});
|
|
4665
|
+
|
|
4666
|
+
// 蒸馏 D 触发 (AI 被动) — 后台异步,不阻塞 HTTP 响应
|
|
4667
|
+
app.post('/api/judgments/detect-and-distill', async (req, res) => {
|
|
4668
|
+
try {
|
|
4669
|
+
const { channelId, turns } = req.body as {
|
|
4670
|
+
channelId?: string;
|
|
4671
|
+
turns?: Array<{ role: 'human' | 'agent'; content: string }>;
|
|
4672
|
+
};
|
|
4673
|
+
|
|
4674
|
+
// 先立即返回 202, 不等 LLM
|
|
4675
|
+
res.status(202).json({ ok: true, queued: true });
|
|
4676
|
+
|
|
4677
|
+
if (!channelId || !Array.isArray(turns) || turns.length === 0) {
|
|
4678
|
+
return;
|
|
4679
|
+
}
|
|
4680
|
+
|
|
4681
|
+
// 异步处理 (不 await, 不阻塞响应)
|
|
4682
|
+
setImmediate(async () => {
|
|
4683
|
+
try {
|
|
4684
|
+
const { detectAndDistillFromChannel } = await import(
|
|
4685
|
+
'../pi-ecosystem-judgment/human-value-pipeline.js'
|
|
4686
|
+
);
|
|
4687
|
+
const result = await detectAndDistillFromChannel(turns, { channelId });
|
|
4688
|
+
if (result.triggered) {
|
|
4689
|
+
console.log(`[D-hook] ${channelId}: ${result.reason}`, result.evolved);
|
|
4690
|
+
}
|
|
4691
|
+
} catch (err) {
|
|
4692
|
+
console.warn('[D-hook] background failed:', err);
|
|
4693
|
+
}
|
|
4694
|
+
});
|
|
4695
|
+
} catch (err: any) {
|
|
4696
|
+
console.error('[judgments] detect-and-distill failed:', err);
|
|
4697
|
+
res.status(500).json({ error: err.message });
|
|
4698
|
+
}
|
|
4699
|
+
});
|
|
4700
|
+
|
|
4701
|
+
// 判断力使用回溯 (P0.5): 给定 judgmentIds, 反查对应的 decision 文本
|
|
4702
|
+
// 用途: UI 上"这条 AI 回复引用了哪些原则"
|
|
4703
|
+
app.post('/api/judgments/resolve-usage', async (req, res) => {
|
|
4704
|
+
try {
|
|
4705
|
+
const { ids } = req.body as { ids?: string[] };
|
|
4706
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
4707
|
+
return res.json({ items: [] });
|
|
4708
|
+
}
|
|
4709
|
+
const { loadAllJudgments } = await import(
|
|
4710
|
+
'../pi-ecosystem-judgment/human-value-store.js'
|
|
4711
|
+
);
|
|
4712
|
+
const all = await loadAllJudgments();
|
|
4713
|
+
const byId = new Map(all.map((j) => [j.id, j]));
|
|
4714
|
+
const items = ids
|
|
4715
|
+
.map((id) => byId.get(id))
|
|
4716
|
+
.filter((j): j is NonNullable<typeof j> => Boolean(j))
|
|
4717
|
+
.map((j) => ({
|
|
4718
|
+
id: j.id,
|
|
4719
|
+
decision: j.decision,
|
|
4720
|
+
status: j.status ?? 'active',
|
|
4721
|
+
timestamp: j.timestamp,
|
|
4722
|
+
}));
|
|
4723
|
+
res.json({ items });
|
|
4724
|
+
} catch (err: any) {
|
|
4725
|
+
console.error('[judgments] resolve-usage failed:', err);
|
|
4726
|
+
res.status(500).json({ error: err.message });
|
|
4727
|
+
}
|
|
4728
|
+
});
|
|
4729
|
+
|
|
4730
|
+
// 判断力违规日志 (P3 UI): 读 violations.jsonl
|
|
4731
|
+
app.get('/api/judgments/violations', async (req, res) => {
|
|
4732
|
+
try {
|
|
4733
|
+
const { getRecentViolations } = await import(
|
|
4734
|
+
'../pi-ecosystem-judgment/monitor-gate.js'
|
|
4735
|
+
);
|
|
4736
|
+
const limit = Math.min(Math.max(parseInt(String(req.query.limit ?? '20'), 10) || 20, 1), 200);
|
|
4737
|
+
const items = await getRecentViolations(limit);
|
|
4738
|
+
res.json({ count: items.length, items });
|
|
4739
|
+
} catch (err: any) {
|
|
4740
|
+
console.error('[judgments] violations failed:', err);
|
|
4741
|
+
res.status(500).json({ error: err.message });
|
|
4742
|
+
}
|
|
4743
|
+
});
|
|
4744
|
+
|
|
4745
|
+
// 类 B 自适应扫描: 读 judgments.json + usage.jsonl, 给出 stale/rising/unused 建议
|
|
4746
|
+
// ?force=1 跳过 24h 缓存
|
|
4747
|
+
app.get('/api/judgments/adaptive-suggestions', async (req, res) => {
|
|
4748
|
+
try {
|
|
4749
|
+
const { getCachedScan } = await import(
|
|
4750
|
+
'../pi-ecosystem-judgment/adaptive-scan.js'
|
|
4751
|
+
);
|
|
4752
|
+
const force = String(req.query.force ?? '') === '1';
|
|
4753
|
+
const result = await getCachedScan(force);
|
|
4754
|
+
res.json(result);
|
|
4755
|
+
} catch (err: any) {
|
|
4756
|
+
console.error('[judgments] adaptive-scan failed:', err);
|
|
4757
|
+
res.status(500).json({ error: err.message });
|
|
4758
|
+
}
|
|
4759
|
+
});
|
|
4760
|
+
|
|
4761
|
+
// Bootstrap Context 调试视图: 返出完整 BolloonContext
|
|
4762
|
+
app.get('/api/bolloon/context', async (req, res) => {
|
|
4763
|
+
try {
|
|
4764
|
+
const { getCachedBolloonContext } = await import(
|
|
4765
|
+
'../pi-ecosystem-judgment/human-value-pipeline.js'
|
|
4766
|
+
);
|
|
4767
|
+
const force = String(req.query.force ?? '') === '1';
|
|
4768
|
+
const ctx = await getCachedBolloonContext({ cwd: process.cwd() }, force);
|
|
4769
|
+
res.json(ctx);
|
|
4770
|
+
} catch (err: any) {
|
|
4771
|
+
console.error('[bolloon] context failed:', err);
|
|
4772
|
+
res.status(500).json({ error: err.message });
|
|
4773
|
+
}
|
|
4774
|
+
});
|
|
4775
|
+
|
|
4776
|
+
// 阶段 B: 周报 (weekly-report.ts 产物) — 仅 API 读取, 不做 UI tab
|
|
4777
|
+
// GET /api/reports → { files: ['2026-W24.md', ...] }
|
|
4778
|
+
// GET /api/reports/2026-W24 → { week, content }
|
|
4779
|
+
app.get('/api/reports', async (_req, res) => {
|
|
4780
|
+
try {
|
|
4781
|
+
const dir = path.join(os.homedir(), '.bolloon', 'reports');
|
|
4782
|
+
try {
|
|
4783
|
+
const entries = await fs.readdir(dir);
|
|
4784
|
+
const files = entries
|
|
4785
|
+
.filter((f) => f.endsWith('.md'))
|
|
4786
|
+
.sort()
|
|
4787
|
+
.reverse(); // 新的在前
|
|
4788
|
+
res.json({ dir, files });
|
|
4789
|
+
} catch {
|
|
4790
|
+
res.json({ dir, files: [] });
|
|
4791
|
+
}
|
|
4792
|
+
} catch (err: any) {
|
|
4793
|
+
res.status(500).json({ error: err.message });
|
|
4794
|
+
}
|
|
4795
|
+
});
|
|
4796
|
+
|
|
4797
|
+
app.get('/api/reports/:week', async (req, res) => {
|
|
4798
|
+
try {
|
|
4799
|
+
const week = req.params.week;
|
|
4800
|
+
// 严格校验, 防路径穿越
|
|
4801
|
+
if (!/^\d{4}-W\d{1,2}$/.test(week)) {
|
|
4802
|
+
return res.status(400).json({ error: 'week must match YYYY-Www' });
|
|
4803
|
+
}
|
|
4804
|
+
const file = path.join(os.homedir(), '.bolloon', 'reports', `${week}.md`);
|
|
4805
|
+
try {
|
|
4806
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
4807
|
+
res.json({ week, content, length: content.length });
|
|
4808
|
+
} catch {
|
|
4809
|
+
res.status(404).json({ error: 'not found', week });
|
|
4810
|
+
}
|
|
4811
|
+
} catch (err: any) {
|
|
4812
|
+
res.status(500).json({ error: err.message });
|
|
4813
|
+
}
|
|
4814
|
+
});
|
|
4815
|
+
|
|
4816
|
+
// 阶段 C 护栏 5: auto-evolve baseline 管理 (无 UI, 仅 API)
|
|
4817
|
+
// GET /api/auto-evolve/baselines → 列出所有 baseline tag
|
|
4818
|
+
// GET /api/auto-evolve/baselines/:tag/diff → 看某 baseline 的 diff 摘要
|
|
4819
|
+
// POST /api/auto-evolve/rollback {tag} → 回滚到指定 baseline
|
|
4820
|
+
app.get('/api/auto-evolve/baselines', async (_req, res) => {
|
|
4821
|
+
try {
|
|
4822
|
+
const { execFile } = await import('child_process');
|
|
4823
|
+
const { promisify } = await import('util');
|
|
4824
|
+
const pExec = promisify(execFile);
|
|
4825
|
+
const { stdout } = await pExec('git', [
|
|
4826
|
+
'tag', '-l', 'auto-evolve-baseline-*', '--format=%(refname:short)|%(contents)|%(objectname:short)|%(taggerdate:iso)',
|
|
4827
|
+
], { cwd: process.cwd() });
|
|
4828
|
+
const tags = stdout.trim().split('\n').filter(Boolean).map((line) => {
|
|
4829
|
+
const [tag, msg, sha, date] = line.split('|');
|
|
4830
|
+
return { tag, message: msg || '', sha, date };
|
|
4831
|
+
});
|
|
4832
|
+
res.json({ tags, count: tags.length });
|
|
4833
|
+
} catch (err: any) {
|
|
4834
|
+
res.status(500).json({ error: err.message });
|
|
4835
|
+
}
|
|
4836
|
+
});
|
|
4837
|
+
|
|
4838
|
+
app.get('/api/auto-evolve/baselines/:tag/diff', async (req, res) => {
|
|
4839
|
+
try {
|
|
4840
|
+
const { execFile } = await import('child_process');
|
|
4841
|
+
const { promisify } = await import('util');
|
|
4842
|
+
const pExec = promisify(execFile);
|
|
4843
|
+
const tag = req.params.tag;
|
|
4844
|
+
if (!/^auto-evolve-baseline-[\w-]+$/.test(tag)) {
|
|
4845
|
+
return res.status(400).json({ error: 'tag must match auto-evolve-baseline-*' });
|
|
4846
|
+
}
|
|
4847
|
+
const { stdout } = await pExec('git', ['show', '--stat', '--no-color', tag], { cwd: process.cwd() });
|
|
4848
|
+
res.json({ tag, diff: stdout.slice(0, 5000) }); // 限长 5KB
|
|
4849
|
+
} catch (err: any) {
|
|
4850
|
+
res.status(500).json({ error: err.message });
|
|
4851
|
+
}
|
|
4852
|
+
});
|
|
4853
|
+
|
|
4854
|
+
// Bootstrap Context → 拼好的 system prompt 片段 (供调试看注入效果)
|
|
4855
|
+
app.get('/api/bolloon/context/system-prompt', async (req, res) => {
|
|
4856
|
+
try {
|
|
4857
|
+
const { getCachedBolloonContext } = await import(
|
|
4858
|
+
'../pi-ecosystem-judgment/human-value-pipeline.js'
|
|
4859
|
+
);
|
|
4860
|
+
const { formatContextForSystemPrompt } = await import(
|
|
4861
|
+
'../bootstrap/project-context.js'
|
|
4862
|
+
);
|
|
4863
|
+
const ctx = await getCachedBolloonContext({ cwd: process.cwd() });
|
|
4864
|
+
const systemAddition = formatContextForSystemPrompt(ctx, {
|
|
4865
|
+
maxChars: parseInt(String(req.query.max ?? '4000'), 10) || 4000,
|
|
4866
|
+
});
|
|
4867
|
+
res.json({ systemAddition, length: systemAddition.length, truncated: systemAddition.includes('截断模式') });
|
|
4868
|
+
} catch (err: any) {
|
|
4869
|
+
console.error('[bolloon] context/system-prompt failed:', err);
|
|
4870
|
+
res.status(500).json({ error: err.message });
|
|
4871
|
+
}
|
|
4872
|
+
});
|
|
4873
|
+
|
|
4874
|
+
// 自适应接受/拒绝: 写 evolution.jsonl 留痕, 接受时同时 patch judgments.json
|
|
4875
|
+
// body: { action: 'accept'|'reject'|'revert', suggestion, appliedPatch? }
|
|
4876
|
+
// query: ?auto=1 → 类 B 自动路径, 受 auto-evolve-policy 网关保护
|
|
4877
|
+
// 缺省 → 用户在 UI 手动触发, 不查开关 (避免阻塞用户)
|
|
4878
|
+
app.post('/api/judgments/adaptive-apply', async (req, res) => {
|
|
4879
|
+
try {
|
|
4880
|
+
const isAuto = req.query.auto === '1' || req.query.auto === 'true';
|
|
4881
|
+
const { action, suggestion, appliedPatch } = req.body as {
|
|
4882
|
+
action: 'accept' | 'reject' | 'revert';
|
|
4883
|
+
suggestion: { judgmentId: string; kind: string; decision: string; reason: string; action: string; metrics: unknown; scannedAt: string; key: string };
|
|
4884
|
+
appliedPatch?: Record<string, unknown>;
|
|
4885
|
+
};
|
|
4886
|
+
if (!action || !suggestion?.judgmentId) {
|
|
4887
|
+
return res.status(400).json({ error: 'action and suggestion.judgmentId required' });
|
|
4888
|
+
}
|
|
4889
|
+
const { updateJudgmentStatus } = await import(
|
|
4890
|
+
'../pi-ecosystem-judgment/human-value-store.js'
|
|
4891
|
+
);
|
|
4892
|
+
const { logEvolution } = await import(
|
|
4893
|
+
'../pi-ecosystem-judgment/adaptive-scan.js'
|
|
4894
|
+
);
|
|
4895
|
+
// accept 时: 真正改库
|
|
4896
|
+
if (action === 'accept') {
|
|
4897
|
+
// 阶段 A: 自动路径需先过 auto-evolve-policy 网关
|
|
4898
|
+
if (isAuto) {
|
|
4899
|
+
const { requireDataLayerAutoEvolve } = await import(
|
|
4900
|
+
'../utils/auto-evolve-policy.js'
|
|
4901
|
+
);
|
|
4902
|
+
try {
|
|
4903
|
+
await requireDataLayerAutoEvolve('adaptive-apply.auto.deprecate');
|
|
4904
|
+
} catch (err: any) {
|
|
4905
|
+
return res.status(423).json({
|
|
4906
|
+
error: 'data-layer-auto-evolve-disabled',
|
|
4907
|
+
message: err.message,
|
|
4908
|
+
hint: '设 BOLLOON_AUTO_EVOLVE_DATA=1 或在 self-improve-policy.json 加 dataLayerAutoEvolve: true',
|
|
4909
|
+
});
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
if (suggestion.action === 'deprecate') {
|
|
4913
|
+
// 标记 superseded (语义: 不再用, 但保留可回滚)
|
|
4914
|
+
await updateJudgmentStatus(suggestion.judgmentId, 'superseded', {
|
|
4915
|
+
evolutionReason: 'merged', // 借 merged 字段表达"被自适应废弃"
|
|
4916
|
+
});
|
|
4917
|
+
} else if (suggestion.action === 'boost') {
|
|
4918
|
+
// boost: 用户手动接受后, 不改库本身 (weight 在 getRelevantValues 里动态算),
|
|
4919
|
+
// 但写 evolution 留痕, 未来可以基于此调整算法
|
|
4920
|
+
// 当前不直接改库, 仅留痕
|
|
4921
|
+
}
|
|
4922
|
+
// 'review' 类不需要自动改库, 仅 log 接受
|
|
4923
|
+
}
|
|
4924
|
+
await logEvolution({
|
|
4925
|
+
ts: new Date().toISOString(),
|
|
4926
|
+
action,
|
|
4927
|
+
suggestion: suggestion as any,
|
|
4928
|
+
appliedPatch,
|
|
4929
|
+
});
|
|
4930
|
+
res.json({ ok: true });
|
|
4931
|
+
} catch (err: any) {
|
|
4932
|
+
console.error('[judgments] adaptive-apply failed:', err);
|
|
4933
|
+
res.status(500).json({ error: err.message });
|
|
4934
|
+
}
|
|
4935
|
+
});
|
|
4936
|
+
|
|
4937
|
+
// 演化日志 (audit / 一键回滚源)
|
|
4938
|
+
app.get('/api/judgments/evolution-log', async (req, res) => {
|
|
4939
|
+
try {
|
|
4940
|
+
const { readEvolutionLog } = await import(
|
|
4941
|
+
'../pi-ecosystem-judgment/adaptive-scan.js'
|
|
4942
|
+
);
|
|
4943
|
+
const limit = Math.min(Math.max(parseInt(String(req.query.limit ?? '50'), 10) || 50, 1), 200);
|
|
4944
|
+
const items = await readEvolutionLog(limit);
|
|
4945
|
+
res.json({ count: items.length, items });
|
|
4946
|
+
} catch (err: any) {
|
|
4947
|
+
console.error('[judgments] evolution-log failed:', err);
|
|
4948
|
+
res.status(500).json({ error: err.message });
|
|
4949
|
+
}
|
|
4950
|
+
});
|
|
4951
|
+
|
|
4952
|
+
// 阶段 2: Causal-judge 4 个 endpoint
|
|
4953
|
+
app.get('/api/judgments/causal/correlation', async (req, res) => {
|
|
4954
|
+
try {
|
|
4955
|
+
const { runCorrelationAnalysis } = await import(
|
|
4956
|
+
'../pi-ecosystem-judgment/human-value-pipeline.js'
|
|
4957
|
+
);
|
|
4958
|
+
const topN = Math.min(Math.max(parseInt(String(req.query.topN ?? '5'), 10) || 5, 1), 50);
|
|
4959
|
+
const useLLM = String(req.query.useLLM ?? '1') !== '0';
|
|
4960
|
+
const items = await runCorrelationAnalysis({ topN, useLLM });
|
|
4961
|
+
res.json({ count: items.length, items });
|
|
4962
|
+
} catch (err: any) {
|
|
4963
|
+
console.error('[causal] correlation failed:', err);
|
|
4964
|
+
res.status(500).json({ error: err.message });
|
|
4965
|
+
}
|
|
4966
|
+
});
|
|
4967
|
+
|
|
4968
|
+
app.get('/api/judgments/causal/intervention', async (req, res) => {
|
|
4969
|
+
try {
|
|
4970
|
+
const { runIntervention } = await import(
|
|
4971
|
+
'../pi-ecosystem-judgment/human-value-pipeline.js'
|
|
4972
|
+
);
|
|
4973
|
+
const { judgmentId, scenario } = req.query as { judgmentId?: string; scenario?: string };
|
|
4974
|
+
if (!judgmentId) return res.status(400).json({ error: 'judgmentId required' });
|
|
4975
|
+
const result = await runIntervention(judgmentId, { scenarioContext: scenario });
|
|
4976
|
+
res.json(result);
|
|
4977
|
+
} catch (err: any) {
|
|
4978
|
+
console.error('[causal] intervention failed:', err);
|
|
4979
|
+
res.status(500).json({ error: err.message });
|
|
4980
|
+
}
|
|
4981
|
+
});
|
|
4982
|
+
|
|
4983
|
+
app.post('/api/judgments/causal/counterfactual', async (req, res) => {
|
|
4984
|
+
try {
|
|
4985
|
+
const { runCounterfactualAudit } = await import(
|
|
4986
|
+
'../pi-ecosystem-judgment/human-value-pipeline.js'
|
|
4987
|
+
);
|
|
4988
|
+
const { userInput, aiReply, violatedPrinciples } = req.body as {
|
|
4989
|
+
userInput?: string;
|
|
4990
|
+
aiReply?: string;
|
|
4991
|
+
violatedPrinciples?: Array<{ principle: string; reason: string }>;
|
|
4992
|
+
};
|
|
4993
|
+
if (!userInput || !aiReply) {
|
|
4994
|
+
return res.status(400).json({ error: 'userInput and aiReply required' });
|
|
4995
|
+
}
|
|
4996
|
+
const audit = await runCounterfactualAudit({
|
|
4997
|
+
userInput,
|
|
4998
|
+
aiReply,
|
|
4999
|
+
violatedPrinciples: violatedPrinciples ?? [],
|
|
5000
|
+
});
|
|
5001
|
+
res.json(audit);
|
|
5002
|
+
} catch (err: any) {
|
|
5003
|
+
console.error('[causal] counterfactual failed:', err);
|
|
5004
|
+
res.status(500).json({ error: err.message });
|
|
5005
|
+
}
|
|
5006
|
+
});
|
|
5007
|
+
|
|
5008
|
+
app.get('/api/judgments/causal/audit-log', async (req, res) => {
|
|
5009
|
+
try {
|
|
5010
|
+
const { readCounterfactualLog } = await import(
|
|
5011
|
+
'../pi-ecosystem-judgment/human-value-pipeline.js'
|
|
5012
|
+
);
|
|
5013
|
+
const limit = Math.min(Math.max(parseInt(String(req.query.limit ?? '20'), 10) || 20, 1), 200);
|
|
5014
|
+
const items = await readCounterfactualLog(limit);
|
|
5015
|
+
res.json({ count: items.length, items });
|
|
5016
|
+
} catch (err: any) {
|
|
5017
|
+
console.error('[causal] audit-log failed:', err);
|
|
5018
|
+
res.status(500).json({ error: err.message });
|
|
5019
|
+
}
|
|
5020
|
+
});
|
|
5021
|
+
|
|
3902
5022
|
// 导入判断: 接受 { filename, content (base64), context }.
|
|
3903
5023
|
// 支持 .json / .yaml / .yml / .md / .txt / .html. 完全离线解析, 不调 LLM.
|
|
3904
5024
|
// 解析规则:
|
|
@@ -4138,8 +5258,12 @@ app.get('/channels', async (_req, res) => {
|
|
|
4138
5258
|
// level 1 (内存爆) → 进程自杀, 依赖外层 supervisor / 用户重启 (Windows 任务计划/手动)
|
|
4139
5259
|
// 否则 Node.js 高 GC 压力下 HTTP 响应丢失, 客户端 fetch 永远 pending
|
|
4140
5260
|
watchdog.registerRestartStrategy(1, () => {
|
|
4141
|
-
|
|
4142
|
-
|
|
5261
|
+
// 2026-06-10: 改为不退出, 因为我们直接后台 tsx 启动没有外层 supervisor.
|
|
5262
|
+
// 误判主要因 recordActivity 仅在显式调用时刷新, 而 broadcast/SSE/连接均不触发.
|
|
5263
|
+
// 退出策略原文保留在注释里:
|
|
5264
|
+
// console.error('[Watchdog] memory critical, 进程退出 (期望外层重启)');
|
|
5265
|
+
// setTimeout(() => process.exit(1), 100);
|
|
5266
|
+
console.warn('[Watchdog] silentThreshold 触发, 但跳过 process.exit (无 supervisor)');
|
|
4143
5267
|
});
|
|
4144
5268
|
watchdog.start();
|
|
4145
5269
|
console.log('[24h] Watchdog started');
|
|
@@ -4299,6 +5423,8 @@ app.get('/channels', async (_req, res) => {
|
|
|
4299
5423
|
}
|
|
4300
5424
|
|
|
4301
5425
|
function broadcast(data: { type: string; [key: string]: unknown }, channelId?: string) {
|
|
5426
|
+
// 2026-06-10: 喂 watchdog, 避免 30min 空闲被误判 (recordActivity 内有 5s 去抖)
|
|
5427
|
+
watchdogRef?.recordActivity?.();
|
|
4302
5428
|
const envelope = { ...data, channelId };
|
|
4303
5429
|
const message = `data: ${JSON.stringify(envelope)}\n\n`;
|
|
4304
5430
|
console.log(`[broadcast] type=${data.type}, channelId=${channelId}, clients=${sseClients.size}`);
|