@bolloon/bolloon-agent 0.1.27 → 0.1.29
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/network/iroh-transport.js +21 -4
- package/dist/network/known-peers.js +81 -0
- package/dist/network/p2p-direct.js +151 -0
- package/dist/network/p2p-secret.js +126 -0
- package/dist/web/client.js +430 -42
- package/dist/web/index.html +9 -13
- package/dist/web/server.js +733 -15
- package/package.json +1 -1
- package/src/network/iroh-transport.ts +20 -4
- package/src/network/known-peers.ts +102 -0
- package/src/network/p2p-direct.ts +184 -0
- package/src/network/p2p-secret.ts +153 -0
- package/src/web/client.js +430 -42
- package/src/web/index.html +9 -13
- package/src/web/server.ts +747 -18
package/dist/web/server.js
CHANGED
|
@@ -257,6 +257,10 @@ let sseClients = new Set();
|
|
|
257
257
|
// v3: 远端 channel UI 元数据缓存 — key: peerId, value: sanitize 过的 channel 列表
|
|
258
258
|
// in-memory only, 进程重启清空 (judgment 内容永远不在这里)
|
|
259
259
|
let remoteChannelCache = new Map();
|
|
260
|
+
// v3: P2PDirect 引用 (Hyperswarm 薄包装) - 模块级, 因为 web server 闭包里不可用
|
|
261
|
+
let v3P2PRef = null;
|
|
262
|
+
// v3: 等待中的 history RPC (B 端 chat-history endpoint 用) — rpcId → { resolve, reject }
|
|
263
|
+
const v3PendingHistoryGets = new Map();
|
|
260
264
|
let channelSessions = new Map(); // key: channelId
|
|
261
265
|
let sessionMessages = new Map(); // key: channelId + sessionId
|
|
262
266
|
/**
|
|
@@ -270,8 +274,18 @@ let sessionMessages = new Map(); // key: channelId + sessionId
|
|
|
270
274
|
* v3: 过滤 channel 元数据, 只返回对远端 peer 安全的字段.
|
|
271
275
|
* 关键: bound_judgment_ids / walletBinding / autoInvokeTools 内部状态不外传.
|
|
272
276
|
* judgment 内容永远不会出现在 RPC 响应里 (judgment 始终在 A 节点内存, 由 A 跑 LLM).
|
|
277
|
+
*
|
|
278
|
+
* Phase 3 分享模式: 加 peerPublicKey 参数 — 只有 shared_with_peers 包含此 peer 的 channel 才返回.
|
|
279
|
+
* peerPublicKey 不传 = admin 路径, 返回所有 channel (老行为).
|
|
273
280
|
*/
|
|
274
|
-
function sanitizeChannelForPeer(ch) {
|
|
281
|
+
function sanitizeChannelForPeer(ch, peerPublicKey) {
|
|
282
|
+
// Phase 3 核心: 分享过滤
|
|
283
|
+
if (peerPublicKey) {
|
|
284
|
+
const shared = Array.isArray(ch.shared_with_peers) ? ch.shared_with_peers : [];
|
|
285
|
+
if (!shared.includes(peerPublicKey)) {
|
|
286
|
+
return null; // 没分享给这个 peer, 不返回
|
|
287
|
+
}
|
|
288
|
+
}
|
|
275
289
|
return {
|
|
276
290
|
id: ch.id,
|
|
277
291
|
name: ch.name,
|
|
@@ -279,11 +293,281 @@ function sanitizeChannelForPeer(ch) {
|
|
|
279
293
|
publicKey: ch.publicKey,
|
|
280
294
|
createdAt: ch.createdAt,
|
|
281
295
|
updatedAt: ch.updatedAt,
|
|
282
|
-
hasWallet: !!ch.walletAddress,
|
|
296
|
+
hasWallet: !!ch.walletAddress,
|
|
283
297
|
boundJudgmentCount: Array.isArray(ch.bound_judgment_ids) ? ch.bound_judgment_ids.length : 0,
|
|
284
|
-
|
|
298
|
+
share_id: ch.share_id,
|
|
299
|
+
// 🔒 不返回: bound_judgment_ids, walletAddress, walletBinding, autoInvokeTools, sessions, shared_with_peers
|
|
285
300
|
};
|
|
286
301
|
}
|
|
302
|
+
/** v3 新增: 判断 channel 是否分享给 peerPublicKey */
|
|
303
|
+
function isSharedWith(ch, peerPublicKey) {
|
|
304
|
+
const shared = Array.isArray(ch.shared_with_peers) ? ch.shared_with_peers : [];
|
|
305
|
+
return shared.includes(peerPublicKey);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* v3: 处理 Hyperswarm 通道收到的 v3 RPC 消息
|
|
309
|
+
* 设计: 用 HyperswarmCommunicator (DHT topic 自动发现) 取代 iroh 直接 connect
|
|
310
|
+
* - A 启动 → broadcast(agent.meta.list.reply) → 所有已连接 peer 缓存 A 的 channel
|
|
311
|
+
* - B 启动 → 同样 broadcast
|
|
312
|
+
* - 任何节点收到 list 请求 → 回 list.reply
|
|
313
|
+
*/
|
|
314
|
+
async function handleV3P2PMessage(parsed, conn, comm) {
|
|
315
|
+
const op = parsed.op;
|
|
316
|
+
const peerKey = conn.publicKey;
|
|
317
|
+
if (op === 'agent.meta.list') {
|
|
318
|
+
// 对方问我的 channel 列表 — 只返回分享给他的
|
|
319
|
+
try {
|
|
320
|
+
const channels = await loadChannels();
|
|
321
|
+
const publicMeta = channels
|
|
322
|
+
.map(ch => sanitizeChannelForPeer(ch, peerKey))
|
|
323
|
+
.filter((x) => x !== null);
|
|
324
|
+
const reply = JSON.stringify({ v: 3, op: 'agent.meta.list.reply', payload: { channels: publicMeta } });
|
|
325
|
+
await comm.sendToConnection(conn.id, reply);
|
|
326
|
+
console.log(`[v3] 回 ${peerKey.substring(0, 12)}... list.reply (${publicMeta.length} 个分享给 ta)`);
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
console.error('[v3] 处理 agent.meta.list 失败:', err.message);
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (op === 'agent.meta.list.reply') {
|
|
334
|
+
// 对方把他自己的 channel 列表推给我 — 缓存
|
|
335
|
+
const list = parsed.payload?.channels || [];
|
|
336
|
+
remoteChannelCache.set(peerKey, list);
|
|
337
|
+
console.log(`[v3] 收到 ${peerKey.substring(0, 12)}... 的 ${list.length} 个 channel, 已缓存`);
|
|
338
|
+
broadcast({
|
|
339
|
+
type: 'remote-channel-update',
|
|
340
|
+
peerId: peerKey,
|
|
341
|
+
channels: list
|
|
342
|
+
}, 'p2p-global');
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (op === 'agent.meta.get') {
|
|
346
|
+
// 对方问单条 channel — 回
|
|
347
|
+
const channelId = parsed.payload?.channelId;
|
|
348
|
+
if (channelId) {
|
|
349
|
+
const channels = await loadChannels();
|
|
350
|
+
const ch = channels.find(c => c.id === channelId);
|
|
351
|
+
if (ch) {
|
|
352
|
+
// Phase 3: 分享过滤 — 必须分享给该 peer
|
|
353
|
+
const sanitized = sanitizeChannelForPeer(ch, peerKey);
|
|
354
|
+
if (sanitized) {
|
|
355
|
+
const reply = JSON.stringify({ v: 3, op: 'agent.meta.get.reply', payload: { channel: sanitized } });
|
|
356
|
+
await comm.sendToConnection(conn.id, reply);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
const reply = JSON.stringify({ v: 3, op: 'agent.meta.get.reply', payload: { error: 'not shared with you' } });
|
|
360
|
+
await comm.sendToConnection(conn.id, reply);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (op === 'agent.meta.get.reply') {
|
|
367
|
+
const ch = parsed.payload?.channel;
|
|
368
|
+
if (ch && ch.id) {
|
|
369
|
+
const list = remoteChannelCache.get(peerKey) || [];
|
|
370
|
+
const idx = list.findIndex((c) => c.id === ch.id);
|
|
371
|
+
if (idx >= 0)
|
|
372
|
+
list[idx] = ch;
|
|
373
|
+
else
|
|
374
|
+
list.push(ch);
|
|
375
|
+
remoteChannelCache.set(peerKey, list);
|
|
376
|
+
broadcast({
|
|
377
|
+
type: 'remote-channel-update',
|
|
378
|
+
peerId: peerKey,
|
|
379
|
+
channels: list
|
|
380
|
+
}, 'p2p-global');
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (op === 'agent.chat.send') {
|
|
385
|
+
// B 端发来: 在 A 节点上对 channelId 跑 LLM, 结果回 B
|
|
386
|
+
// judgment 永远在 A 节点 (buildJudgmentHint 已经用 bound_judgment_ids)
|
|
387
|
+
const { channelId, text, fromPublicKey } = parsed.payload || {};
|
|
388
|
+
if (!channelId || !text) {
|
|
389
|
+
console.warn(`[v3] agent.chat.send 缺少 channelId/text`);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const senderKey = fromPublicKey || peerKey;
|
|
393
|
+
console.log(`[v3] 收到 ${senderKey.substring(0, 12)}... 对 channel ${channelId} 的 chat: "${text.substring(0, 40)}..."`);
|
|
394
|
+
try {
|
|
395
|
+
// 1. 找到 channel
|
|
396
|
+
const channels = await loadChannels();
|
|
397
|
+
const ch = channels.find(c => c.id === channelId);
|
|
398
|
+
if (!ch) {
|
|
399
|
+
const reply = JSON.stringify({
|
|
400
|
+
v: 3, op: 'agent.chat.reply',
|
|
401
|
+
payload: { channelId, fromPublicKey: v3P2PRef?.getPublicKey() || '', error: 'channel not found', text: '' }
|
|
402
|
+
});
|
|
403
|
+
await comm.sendToConnection(conn.id, reply);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
// v3 新增: 持久化 B 的 user 消息到 A 的 session — 让历史可拉
|
|
407
|
+
try {
|
|
408
|
+
const existing = await loadSession(channelId, 'default');
|
|
409
|
+
const session = existing || {
|
|
410
|
+
channelId, sessionId: 'default', messages: [], lastUpdated: new Date().toISOString()
|
|
411
|
+
};
|
|
412
|
+
session.messages.push({
|
|
413
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
414
|
+
type: 'user',
|
|
415
|
+
content: text,
|
|
416
|
+
timestamp: new Date().toISOString()
|
|
417
|
+
});
|
|
418
|
+
session.lastUpdated = new Date().toISOString();
|
|
419
|
+
await saveSession(session);
|
|
420
|
+
console.log(`[v3] (${channelId}) 存 user 消息 (${text.length} chars) 到 A 的 session`);
|
|
421
|
+
}
|
|
422
|
+
catch (saveErr) {
|
|
423
|
+
console.warn(`[v3] 存 user 消息失败 (不影响 chat):`, saveErr.message);
|
|
424
|
+
}
|
|
425
|
+
// v3 新增: 告诉 B "我开始想了, 用了哪些 judgment" — 让 B 看到决策依据
|
|
426
|
+
const judgmentHint = await buildJudgmentHint(ch, channelId);
|
|
427
|
+
const usedJudgments = await extractJudgmentsFromHint(ch);
|
|
428
|
+
try {
|
|
429
|
+
const thinkingStart = JSON.stringify({
|
|
430
|
+
v: 3, op: 'agent.chat.thinking',
|
|
431
|
+
payload: {
|
|
432
|
+
channelId,
|
|
433
|
+
phase: 'start',
|
|
434
|
+
fromPublicKey: v3P2PRef?.getPublicKey() || '',
|
|
435
|
+
hint: judgmentHint,
|
|
436
|
+
usedJudgments,
|
|
437
|
+
userText: text
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
await comm.sendToConnection(conn.id, thinkingStart);
|
|
441
|
+
}
|
|
442
|
+
catch { }
|
|
443
|
+
// 2. 跑 LLM (复用 Phase 1 的 buildJudgmentHint — 注入 channel 的 judgment)
|
|
444
|
+
const { getMinimax } = await import('../constraints/index.js');
|
|
445
|
+
const llm = getMinimax();
|
|
446
|
+
const fullPrompt = `${judgmentHint}${text}`;
|
|
447
|
+
let fullResponse = '';
|
|
448
|
+
// v3 新增: 流式 token 节流推给 B — 让 B 看到过程
|
|
449
|
+
let lastFlushAt = 0;
|
|
450
|
+
const streamCallback = (event) => {
|
|
451
|
+
if (event.type === 'token') {
|
|
452
|
+
fullResponse += event.content;
|
|
453
|
+
if (fullResponse.length - lastFlushAt >= 20) {
|
|
454
|
+
lastFlushAt = fullResponse.length;
|
|
455
|
+
const msg = JSON.stringify({
|
|
456
|
+
v: 3, op: 'agent.chat.thinking',
|
|
457
|
+
payload: { channelId, phase: 'token', partial: fullResponse, fromPublicKey: v3P2PRef?.getPublicKey() || '' }
|
|
458
|
+
});
|
|
459
|
+
comm.sendToConnection(conn.id, msg).catch(() => { });
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
const agent = await getAgentForChannel(channelId, ch.did || '', ch.name, ch.didDocRef);
|
|
464
|
+
fullResponse = await agent.promptStream(fullPrompt, streamCallback);
|
|
465
|
+
// v3 新增: 存 A 的 assistant 消息到 session — B 拉历史时能看到完整对话
|
|
466
|
+
try {
|
|
467
|
+
const existing = await loadSession(channelId, 'default');
|
|
468
|
+
const session = existing || {
|
|
469
|
+
channelId, sessionId: 'default', messages: [], lastUpdated: new Date().toISOString()
|
|
470
|
+
};
|
|
471
|
+
session.messages.push({
|
|
472
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
473
|
+
type: 'ai',
|
|
474
|
+
content: fullResponse,
|
|
475
|
+
timestamp: new Date().toISOString()
|
|
476
|
+
});
|
|
477
|
+
session.lastUpdated = new Date().toISOString();
|
|
478
|
+
await saveSession(session);
|
|
479
|
+
console.log(`[v3] (${channelId}) 存 assistant 回复 (${fullResponse.length} chars) 到 A 的 session`);
|
|
480
|
+
}
|
|
481
|
+
catch (saveErr) {
|
|
482
|
+
console.warn(`[v3] 存 assistant 消息失败 (不影响):`, saveErr.message);
|
|
483
|
+
}
|
|
484
|
+
// 3. 把完整回复发给 B
|
|
485
|
+
const reply = JSON.stringify({
|
|
486
|
+
v: 3, op: 'agent.chat.reply',
|
|
487
|
+
payload: {
|
|
488
|
+
channelId,
|
|
489
|
+
fromPublicKey: v3P2PRef?.getPublicKey() || '',
|
|
490
|
+
text: fullResponse
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
await comm.sendToConnection(conn.id, reply);
|
|
494
|
+
console.log(`[v3] 回 chat.reply 给 ${senderKey.substring(0, 12)}... (${fullResponse.length} chars)`);
|
|
495
|
+
}
|
|
496
|
+
catch (err) {
|
|
497
|
+
console.error(`[v3] agent.chat.send 处理失败:`, err.message);
|
|
498
|
+
try {
|
|
499
|
+
const reply = JSON.stringify({
|
|
500
|
+
v: 3, op: 'agent.chat.reply',
|
|
501
|
+
payload: { channelId, fromPublicKey: v3P2PRef?.getPublicKey() || '', error: err.message, text: '' }
|
|
502
|
+
});
|
|
503
|
+
await comm.sendToConnection(conn.id, reply);
|
|
504
|
+
}
|
|
505
|
+
catch { }
|
|
506
|
+
}
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (op === 'agent.history.get') {
|
|
510
|
+
// v3 新增: B 拉 A 的 channel 历史 (含所有 message + judgment hint)
|
|
511
|
+
// 共享过滤: 只返回 B 可见的 channel + 包含的 judgment
|
|
512
|
+
const { channelId, rpcId, fromPublicKey } = parsed.payload || {};
|
|
513
|
+
if (!channelId || !rpcId) {
|
|
514
|
+
console.warn(`[v3] agent.history.get 缺少 channelId/rpcId`);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
const channels = await loadChannels();
|
|
519
|
+
const ch = channels.find(c => c.id === channelId);
|
|
520
|
+
if (!ch) {
|
|
521
|
+
const err = JSON.stringify({
|
|
522
|
+
v: 3, op: 'agent.history.get.reply',
|
|
523
|
+
payload: { rpcId, error: 'channel not found', messages: [], judgments: { bound: [], candidates: [] } }
|
|
524
|
+
});
|
|
525
|
+
await comm.sendToConnection(conn.id, err);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
// 共享过滤: 必须 peerKey 在 shared_with_peers 里 (避免泄露未分享的 channel)
|
|
529
|
+
const peerKey = fromPublicKey;
|
|
530
|
+
if (!peerKey || !isSharedWith(ch, peerKey)) {
|
|
531
|
+
const err = JSON.stringify({
|
|
532
|
+
v: 3, op: 'agent.history.get.reply',
|
|
533
|
+
payload: { rpcId, error: 'channel not shared with you', messages: [], judgments: { bound: [], candidates: [] } }
|
|
534
|
+
});
|
|
535
|
+
await comm.sendToConnection(conn.id, err);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
// 加载 A 端 session
|
|
539
|
+
const session = await loadSession(channelId, 'default');
|
|
540
|
+
// 加载 channel 用到的 judgment
|
|
541
|
+
const judgments = await extractJudgmentsFromHint(ch);
|
|
542
|
+
const reply = JSON.stringify({
|
|
543
|
+
v: 3, op: 'agent.history.get.reply',
|
|
544
|
+
payload: {
|
|
545
|
+
rpcId,
|
|
546
|
+
channelId,
|
|
547
|
+
messages: session?.messages || [],
|
|
548
|
+
lastUpdated: session?.lastUpdated,
|
|
549
|
+
judgments,
|
|
550
|
+
channelName: ch.name
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
await comm.sendToConnection(conn.id, reply);
|
|
554
|
+
console.log(`[v3] 回 history.reply 给 ${peerKey.substring(0, 12)}... (channelId=${channelId}, ${session?.messages?.length || 0} messages)`);
|
|
555
|
+
}
|
|
556
|
+
catch (err) {
|
|
557
|
+
console.error(`[v3] agent.history.get 处理失败:`, err.message);
|
|
558
|
+
try {
|
|
559
|
+
const errMsg = JSON.stringify({
|
|
560
|
+
v: 3, op: 'agent.history.get.reply',
|
|
561
|
+
payload: { rpcId, error: err.message, messages: [], judgments: { bound: [], candidates: [] } }
|
|
562
|
+
});
|
|
563
|
+
await comm.sendToConnection(conn.id, errMsg);
|
|
564
|
+
}
|
|
565
|
+
catch { }
|
|
566
|
+
}
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
console.log(`[v3] 收到未知 op: ${op}`);
|
|
570
|
+
}
|
|
287
571
|
async function buildJudgmentHint(channel, channelIdForLog) {
|
|
288
572
|
try {
|
|
289
573
|
const { loadAllJudgments, initializeValueStore } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
@@ -325,6 +609,38 @@ async function buildJudgmentHint(channel, channelIdForLog) {
|
|
|
325
609
|
return '';
|
|
326
610
|
}
|
|
327
611
|
}
|
|
612
|
+
/**
|
|
613
|
+
* v3 新增: 把 channel 当前用到的 judgment 提取成结构化数据, 给 B 端 UI 显示.
|
|
614
|
+
* 返回 { bound: [...], candidates: [...] } — bound 是硬绑定, candidates 是参考池.
|
|
615
|
+
*/
|
|
616
|
+
async function extractJudgmentsFromHint(channel) {
|
|
617
|
+
try {
|
|
618
|
+
const { loadAllJudgments, initializeValueStore } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
619
|
+
await initializeValueStore();
|
|
620
|
+
const allJudgments = await loadAllJudgments();
|
|
621
|
+
if (allJudgments.length === 0)
|
|
622
|
+
return { bound: [], candidates: [] };
|
|
623
|
+
const boundIds = new Set(channel && Array.isArray(channel.bound_judgment_ids) ? channel.bound_judgment_ids : []);
|
|
624
|
+
const summarize = (j) => ({
|
|
625
|
+
id: j.id,
|
|
626
|
+
decision: (j.decision || '').toString().slice(0, 200),
|
|
627
|
+
reasons: Array.isArray(j.reasons) ? j.reasons : [],
|
|
628
|
+
domain: j.domain,
|
|
629
|
+
stakes: j.stakes
|
|
630
|
+
});
|
|
631
|
+
const bound = allJudgments
|
|
632
|
+
.filter((j) => j.id !== undefined && boundIds.has(j.id))
|
|
633
|
+
.map(summarize);
|
|
634
|
+
const candidates = allJudgments
|
|
635
|
+
.filter((j) => j.id !== undefined && !boundIds.has(j.id))
|
|
636
|
+
.map(summarize);
|
|
637
|
+
return { bound, candidates };
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
console.warn(`[v3] extractJudgmentsFromHint 失败:`, err.message);
|
|
641
|
+
return { bound: [], candidates: [] };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
328
644
|
async function getAgentForChannel(channelId, channelDid, channelName, channelDidDoc) {
|
|
329
645
|
// 获取当前 channel 的 currentSessionId
|
|
330
646
|
const channels = await loadChannels();
|
|
@@ -428,7 +744,164 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
428
744
|
catch (e) {
|
|
429
745
|
console.log('P2P DID 本地模式运行');
|
|
430
746
|
}
|
|
431
|
-
//
|
|
747
|
+
// v3: 完全用 P2PDirect 取代 @diap/sdk 的 HyperswarmCommunicator
|
|
748
|
+
// 原因: @diap/sdk 的 sendToConnection 是 stub, 不真发数据
|
|
749
|
+
// 这里故意不启动 p2pCommunicator (保持 null), 让 P2PDirect 独占 hyperswarm 通道
|
|
750
|
+
try {
|
|
751
|
+
const { P2PDirect } = await import('../network/p2p-direct.js');
|
|
752
|
+
v3P2PRef = new P2PDirect({ name: 'v3' });
|
|
753
|
+
await v3P2PRef.start();
|
|
754
|
+
await v3P2PRef.joinTopic(Buffer.from('bolloon-agent-harness'));
|
|
755
|
+
v3P2PRef.on('data', (evt) => {
|
|
756
|
+
try {
|
|
757
|
+
const parsed = JSON.parse(evt.data.toString('utf-8'));
|
|
758
|
+
if (parsed && parsed.v === 3 && parsed.op) {
|
|
759
|
+
// v3 跨用户 chat: B 端收到 A 的 chat.reply, 直接 SSE 推给前端
|
|
760
|
+
if (parsed.op === 'agent.chat.reply') {
|
|
761
|
+
console.log(`[v3] 收到来自 ${evt.fromPublicKey.substring(0, 12)}... 的 chat.reply (${(parsed.payload?.text || '').length} chars)`);
|
|
762
|
+
broadcast({
|
|
763
|
+
type: 'remote-chat-reply',
|
|
764
|
+
fromPublicKey: evt.fromPublicKey,
|
|
765
|
+
channelId: parsed.payload?.channelId,
|
|
766
|
+
text: parsed.payload?.text || '',
|
|
767
|
+
error: parsed.payload?.error
|
|
768
|
+
}, 'p2p-global');
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
// v3 新增: B 端收到 A 的 thinking (开始 + 流式 token)
|
|
772
|
+
if (parsed.op === 'agent.chat.thinking') {
|
|
773
|
+
const phase = parsed.payload?.phase;
|
|
774
|
+
if (phase === 'start') {
|
|
775
|
+
console.log(`[v3] 收到来自 ${evt.fromPublicKey.substring(0, 12)}... 的 thinking start (judgments: bound=${(parsed.payload?.usedJudgments?.bound || []).length}, candidates=${(parsed.payload?.usedJudgments?.candidates || []).length})`);
|
|
776
|
+
}
|
|
777
|
+
broadcast({
|
|
778
|
+
type: 'remote-chat-thinking',
|
|
779
|
+
fromPublicKey: evt.fromPublicKey,
|
|
780
|
+
channelId: parsed.payload?.channelId,
|
|
781
|
+
phase: parsed.payload?.phase,
|
|
782
|
+
partial: parsed.payload?.partial,
|
|
783
|
+
hint: parsed.payload?.hint,
|
|
784
|
+
usedJudgments: parsed.payload?.usedJudgments,
|
|
785
|
+
userText: parsed.payload?.userText
|
|
786
|
+
}, 'p2p-global');
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
// v3 新增: B 端收到 A 的 history reply → resolve pending promise
|
|
790
|
+
if (parsed.op === 'agent.history.get.reply') {
|
|
791
|
+
const rpcId = parsed.payload?.rpcId;
|
|
792
|
+
if (rpcId && v3PendingHistoryGets.has(rpcId)) {
|
|
793
|
+
const pending = v3PendingHistoryGets.get(rpcId);
|
|
794
|
+
v3PendingHistoryGets.delete(rpcId);
|
|
795
|
+
if (parsed.payload?.error) {
|
|
796
|
+
pending.reject(new Error(parsed.payload.error));
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
pending.resolve({
|
|
800
|
+
channelId: parsed.payload.channelId,
|
|
801
|
+
messages: parsed.payload.messages || [],
|
|
802
|
+
lastUpdated: parsed.payload.lastUpdated,
|
|
803
|
+
judgments: parsed.payload.judgments || { bound: [], candidates: [] },
|
|
804
|
+
channelName: parsed.payload.channelName
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const commShim = {
|
|
811
|
+
sendToConnection: (_id, data) => {
|
|
812
|
+
v3P2PRef.sendTo(evt.fromPublicKey, data);
|
|
813
|
+
return Promise.resolve();
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
handleV3P2PMessage(parsed, { id: evt.fromPublicKey, publicKey: evt.fromPublicKey }, commShim);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
catch (err) {
|
|
820
|
+
console.error('[v3-P2PDirect] 解析/处理消息失败:', err.message);
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
// 新连接进来 → 主动发我分享给 ta 的 channel 列表
|
|
824
|
+
v3P2PRef.on('connection', (evt) => {
|
|
825
|
+
setTimeout(async () => {
|
|
826
|
+
try {
|
|
827
|
+
const channels = await loadChannels();
|
|
828
|
+
const publicMeta = channels
|
|
829
|
+
.map(ch => sanitizeChannelForPeer(ch, evt.remotePublicKey))
|
|
830
|
+
.filter((x) => x !== null);
|
|
831
|
+
const msg = JSON.stringify({ v: 3, op: 'agent.meta.list.reply', payload: { channels: publicMeta } });
|
|
832
|
+
v3P2PRef.sendTo(evt.remotePublicKey, msg);
|
|
833
|
+
console.log(`[v3] 新连接 ${evt.remotePublicKey.substring(0, 12)}... → 发 ${publicMeta.length} 个分享给 ta`);
|
|
834
|
+
}
|
|
835
|
+
catch (err) {
|
|
836
|
+
console.error('[v3] 新连接发 list.reply 失败:', err.message);
|
|
837
|
+
}
|
|
838
|
+
}, 500);
|
|
839
|
+
});
|
|
840
|
+
console.log(`[v3] P2PDirect 已启动, role=${v3P2PRef.getRole()}, publicKey=${v3P2PRef.getPublicKey().substring(0, 12)}...`);
|
|
841
|
+
// v3: 启动后自动重连 known peers — 让"启动就互联"成为现实
|
|
842
|
+
setTimeout(async () => {
|
|
843
|
+
try {
|
|
844
|
+
const { listPeers, markConnected } = await import('../network/known-peers.js');
|
|
845
|
+
const peers = await listPeers();
|
|
846
|
+
if (peers.length === 0) {
|
|
847
|
+
console.log(`[v3] 没有 known peers, 跳过自动重连`);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const swarm = v3P2PRef.swarm;
|
|
851
|
+
if (!swarm)
|
|
852
|
+
return;
|
|
853
|
+
for (const peer of peers) {
|
|
854
|
+
try {
|
|
855
|
+
await swarm.joinPeer(Buffer.from(peer.publicKey, 'hex'));
|
|
856
|
+
await markConnected(peer.name || '');
|
|
857
|
+
console.log(`[v3] 自动重连 ${peer.name} (${peer.publicKey.substring(0, 12)}...) ✓`);
|
|
858
|
+
}
|
|
859
|
+
catch (err) {
|
|
860
|
+
console.warn(`[v3] 自动重连 ${peer.name} 失败:`, err.message);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
// 触发一次 broadcast 推送给所有重连的 peer
|
|
864
|
+
setTimeout(() => v3BroadcastOwn(), 2000);
|
|
865
|
+
}
|
|
866
|
+
catch (err) {
|
|
867
|
+
console.error('[v3] 自动重连失败:', err.message);
|
|
868
|
+
}
|
|
869
|
+
}, 5000); // 5s 后再重连, 让 swarm 充分 bootstrap
|
|
870
|
+
}
|
|
871
|
+
catch (err) {
|
|
872
|
+
console.error('[v3] P2PDirect 启动失败:', err.message);
|
|
873
|
+
v3P2PRef = null;
|
|
874
|
+
}
|
|
875
|
+
// v3: 定期 broadcast — 每个 peer 只收到分享给他的 channel (按 peer 个性化)
|
|
876
|
+
const v3BroadcastOwn = () => {
|
|
877
|
+
if (!v3P2PRef)
|
|
878
|
+
return;
|
|
879
|
+
loadChannels().then(channels => {
|
|
880
|
+
const conns = v3P2PRef.conns;
|
|
881
|
+
if (!conns)
|
|
882
|
+
return;
|
|
883
|
+
for (const [peerPk, conn] of conns.entries()) {
|
|
884
|
+
if (conn?.destroyed)
|
|
885
|
+
continue;
|
|
886
|
+
const sharedForPeer = channels
|
|
887
|
+
.map(ch => sanitizeChannelForPeer(ch, peerPk))
|
|
888
|
+
.filter((x) => x !== null);
|
|
889
|
+
if (sharedForPeer.length > 0) {
|
|
890
|
+
const msg = JSON.stringify({ v: 3, op: 'agent.meta.list.reply', payload: { channels: sharedForPeer } });
|
|
891
|
+
try {
|
|
892
|
+
conn.write(Buffer.from(msg));
|
|
893
|
+
}
|
|
894
|
+
catch { }
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
console.log(`[v3] broadcast 个性化: ${conns.size} 个 peer, 各自收到分享的 channel`);
|
|
898
|
+
}).catch(err => console.error('[v3] broadcast 失败:', err.message));
|
|
899
|
+
};
|
|
900
|
+
setTimeout(v3BroadcastOwn, 3000);
|
|
901
|
+
setTimeout(v3BroadcastOwn, 10000);
|
|
902
|
+
setTimeout(v3BroadcastOwn, 20000);
|
|
903
|
+
setTimeout(v3BroadcastOwn, 40000);
|
|
904
|
+
// 保留 @diap/sdk 的旧实例 (它的 Hyperswarm 实例能帮 P2PDirect 做 DHT bootstrap)
|
|
432
905
|
try {
|
|
433
906
|
const rawSeed = crypto.getRandomValues(new Uint8Array(32));
|
|
434
907
|
p2pCommunicator = createHyperswarmCommunicator({
|
|
@@ -438,22 +911,20 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
438
911
|
maxConnections: 50,
|
|
439
912
|
seed: rawSeed
|
|
440
913
|
});
|
|
441
|
-
p2pCommunicator.on('connection', (conn) => {
|
|
442
|
-
console.log(`P2P 连接: ${conn.publicKey.substring(0, 8)}...`);
|
|
443
|
-
});
|
|
444
914
|
p2pCommunicator.on('message', async (msg, conn) => {
|
|
915
|
+
// 旧 p2p_message 路径 (非 v3)
|
|
445
916
|
const content = new TextDecoder().decode(msg.content);
|
|
446
|
-
console.log(`P2P 收到消息: ${content.substring(0, 50)}...`);
|
|
447
|
-
// 可以在这里处理接收到的消息
|
|
448
917
|
broadcast({ type: 'p2p_message', from: conn.publicKey.substring(0, 8), content }, undefined);
|
|
449
918
|
});
|
|
450
919
|
await p2pCommunicator.start();
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
920
|
+
// @diap/sdk 也 join topic — 它的 Hyperswarm 实例帮 P2PDirect 做 DHT 引导
|
|
921
|
+
// @diap/sdk 收到的数据是 mock (不真发), 但 DHT 发现 + 节点连接是 OK 的
|
|
922
|
+
const oldTopic = createTopic('bolloon-agent-harness');
|
|
923
|
+
await p2pCommunicator.joinTopic(oldTopic);
|
|
924
|
+
console.log(`P2P 老通道已就绪 (DHT bootstrap 帮 P2PDirect, 实际数据走 P2PDirect)`);
|
|
454
925
|
}
|
|
455
926
|
catch (e) {
|
|
456
|
-
console.log(`P2P
|
|
927
|
+
console.log(`P2P 老通道初始化失败: ${e.message}`);
|
|
457
928
|
}
|
|
458
929
|
}
|
|
459
930
|
catch (e) {
|
|
@@ -752,6 +1223,26 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
752
1223
|
// 用法: B 端用户点 "刷新远端智能体" → 触发本 endpoint
|
|
753
1224
|
app.post('/api/remote-channels/refresh', async (_req, res) => {
|
|
754
1225
|
try {
|
|
1226
|
+
// Phase 3: 优先用 P2PDirect conns (Phase 2/3 的真实通道)
|
|
1227
|
+
if (v3P2PRef) {
|
|
1228
|
+
const conns = v3P2PRef.conns;
|
|
1229
|
+
const peerIds = Array.from(conns.keys()).filter(pk => {
|
|
1230
|
+
const c = conns.get(pk);
|
|
1231
|
+
return c && !c.destroyed;
|
|
1232
|
+
});
|
|
1233
|
+
if (peerIds.length === 0) {
|
|
1234
|
+
return res.json({ ok: true, sent: 0, note: 'no connected peers (P2PDirect)' });
|
|
1235
|
+
}
|
|
1236
|
+
// 让每个 peer 拉 list — Phase 3 个性化分享过滤
|
|
1237
|
+
let sent = 0;
|
|
1238
|
+
for (const peerPk of peerIds) {
|
|
1239
|
+
const ok = await v3P2PRef.sendTo(peerPk, JSON.stringify({ v: 3, op: 'agent.meta.list', payload: {} }));
|
|
1240
|
+
if (ok)
|
|
1241
|
+
sent++;
|
|
1242
|
+
}
|
|
1243
|
+
return res.json({ ok: true, sent, total: peerIds.length });
|
|
1244
|
+
}
|
|
1245
|
+
// Fallback: 老 iroh 路径
|
|
755
1246
|
const peers = irohTransport.getPeers ? irohTransport.getPeers() : [];
|
|
756
1247
|
const peerIds = Array.isArray(peers) ? peers.map((p) => p.nodeId || p) : [];
|
|
757
1248
|
if (peerIds.length === 0) {
|
|
@@ -769,6 +1260,36 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
769
1260
|
res.status(500).json({ error: err.message });
|
|
770
1261
|
}
|
|
771
1262
|
});
|
|
1263
|
+
// ===== v3: 主动 connect 到对端, 再发 agent.meta.list =====
|
|
1264
|
+
// 用法: POST /api/remote-channels/connect { targetAddr: "<完整 EndpointAddr 含 relay URL>" }
|
|
1265
|
+
// targetAddr 应来自对端 GET /api/iroh-addr (完整字符串, 不只是 nodeId)
|
|
1266
|
+
// 兼容旧用法: 也接受 targetNodeId, 但只有 nodeId 不一定能 connect 成功
|
|
1267
|
+
app.post('/api/remote-channels/connect', async (req, res) => {
|
|
1268
|
+
try {
|
|
1269
|
+
const { targetAddr, targetNodeId } = req.body || {};
|
|
1270
|
+
const target = targetAddr || targetNodeId;
|
|
1271
|
+
if (!target || typeof target !== 'string') {
|
|
1272
|
+
return res.status(400).json({ error: 'targetAddr (or targetNodeId) required' });
|
|
1273
|
+
}
|
|
1274
|
+
console.log(`[v3] 主动 connect 到 ${target.substring(0, 32)}...`);
|
|
1275
|
+
// iroh connect 接受 nodeId 或完整 addr 字符串 — 用完整 addr 才会成功
|
|
1276
|
+
const ok = await irohTransport.connect(target);
|
|
1277
|
+
if (!ok) {
|
|
1278
|
+
return res.status(502).json({
|
|
1279
|
+
error: 'connect failed',
|
|
1280
|
+
hint: '传 targetAddr (完整 EndpointAddr 字符串, 含 relay URL) 而非仅 nodeId'
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
// 立即发 agent.meta.list 请求对端返回元数据
|
|
1284
|
+
const sent = await irohTransport.sendMessage(target, 'agent.meta.list', new TextEncoder().encode('{}'));
|
|
1285
|
+
console.log(`[v3] connect+list 发送结果: connect=ok, list=${sent}`);
|
|
1286
|
+
res.json({ ok: true, connected: true, sent, target });
|
|
1287
|
+
}
|
|
1288
|
+
catch (err) {
|
|
1289
|
+
console.error('[v3] /api/remote-channels/connect 失败:', err);
|
|
1290
|
+
res.status(500).json({ error: err.message });
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
772
1293
|
app.post('/channels', async (req, res) => {
|
|
773
1294
|
try {
|
|
774
1295
|
const { name, agentId, walletAddress, autoInvokeTools, bound_judgment_ids } = req.body;
|
|
@@ -954,7 +1475,7 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
954
1475
|
app.patch('/channels/:channelId', async (req, res) => {
|
|
955
1476
|
try {
|
|
956
1477
|
const { channelId } = req.params;
|
|
957
|
-
const { name, walletAddress, autoInvokeTools, bound_judgment_ids } = req.body;
|
|
1478
|
+
const { name, walletAddress, autoInvokeTools, bound_judgment_ids, shared_with_peers } = req.body;
|
|
958
1479
|
const channels = await loadChannels();
|
|
959
1480
|
const channel = channels.find(c => c.id === channelId);
|
|
960
1481
|
if (!channel) {
|
|
@@ -994,6 +1515,25 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
994
1515
|
}
|
|
995
1516
|
console.log(`[Channel ${channelId}] 绑定判断力: ${channel.bound_judgment_ids.length} 条`);
|
|
996
1517
|
}
|
|
1518
|
+
// Phase 3: shared_with_peers (显式分享给指定 peerPublicKey 列表)
|
|
1519
|
+
if (shared_with_peers !== undefined) {
|
|
1520
|
+
if (shared_with_peers === null) {
|
|
1521
|
+
channel.shared_with_peers = [];
|
|
1522
|
+
}
|
|
1523
|
+
else if (Array.isArray(shared_with_peers)) {
|
|
1524
|
+
channel.shared_with_peers = shared_with_peers.filter((x) => typeof x === 'string' && x.length === 64 // iroh/hyperswarm pubkey 32 字节 = 64 hex
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
else {
|
|
1528
|
+
return res.status(400).json({ error: 'shared_with_peers must be array of publicKey hex' });
|
|
1529
|
+
}
|
|
1530
|
+
console.log(`[Channel ${channelId}] 分享给 ${channel.shared_with_peers.length} 个 peer`);
|
|
1531
|
+
}
|
|
1532
|
+
// 首次保存时自动生成 share_id (短字符串, 方便粘贴)
|
|
1533
|
+
if (!channel.share_id) {
|
|
1534
|
+
channel.share_id = `shr_${channelId.slice(3, 12)}_${Math.random().toString(36).substring(2, 8)}`;
|
|
1535
|
+
console.log(`[Channel ${channelId}] 自动生成 share_id: ${channel.share_id}`);
|
|
1536
|
+
}
|
|
997
1537
|
channel.updatedAt = new Date().toISOString();
|
|
998
1538
|
await saveChannels(channels);
|
|
999
1539
|
res.json(channel);
|
|
@@ -1534,6 +2074,183 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1534
2074
|
res.status(500).json({ error: err.message });
|
|
1535
2075
|
}
|
|
1536
2076
|
});
|
|
2077
|
+
// v3 测试: 返回 iroh endpoint 完整地址 (含 relay URL), 这才是 connect() 真正需要的
|
|
2078
|
+
app.get('/api/iroh-addr', async (_req, res) => {
|
|
2079
|
+
try {
|
|
2080
|
+
const addr = irohTransport.getEndpointAddr
|
|
2081
|
+
? irohTransport.getEndpointAddr()
|
|
2082
|
+
: irohTransport.getNodeId();
|
|
2083
|
+
res.json({ addr });
|
|
2084
|
+
}
|
|
2085
|
+
catch (err) {
|
|
2086
|
+
res.status(500).json({ error: err.message });
|
|
2087
|
+
}
|
|
2088
|
+
});
|
|
2089
|
+
// v3: 暴露 P2PDirect 自己的 publicKey, 对方可用它主动 connect
|
|
2090
|
+
app.get('/api/p2p-publickey', async (_req, res) => {
|
|
2091
|
+
try {
|
|
2092
|
+
if (!v3P2PRef) {
|
|
2093
|
+
return res.status(503).json({ error: 'P2PDirect not started' });
|
|
2094
|
+
}
|
|
2095
|
+
res.json({ publicKey: v3P2PRef.getPublicKey() });
|
|
2096
|
+
}
|
|
2097
|
+
catch (err) {
|
|
2098
|
+
res.status(500).json({ error: err.message });
|
|
2099
|
+
}
|
|
2100
|
+
});
|
|
2101
|
+
// v3: known peers CRUD (持久化到 ~/.bolloon/known_peers.json)
|
|
2102
|
+
// GET 列表, POST 加/更新, DELETE 删, PATCH 重命名
|
|
2103
|
+
app.get('/api/p2p-peers', async (_req, res) => {
|
|
2104
|
+
try {
|
|
2105
|
+
const { listPeers } = await import('../network/known-peers.js');
|
|
2106
|
+
const peers = await listPeers();
|
|
2107
|
+
res.json({ count: peers.length, peers });
|
|
2108
|
+
}
|
|
2109
|
+
catch (err) {
|
|
2110
|
+
res.status(500).json({ error: err.message });
|
|
2111
|
+
}
|
|
2112
|
+
});
|
|
2113
|
+
app.post('/api/p2p-peers', async (req, res) => {
|
|
2114
|
+
try {
|
|
2115
|
+
const { name, publicKey, notes } = req.body || {};
|
|
2116
|
+
if (!name || !publicKey)
|
|
2117
|
+
return res.status(400).json({ error: 'name and publicKey required' });
|
|
2118
|
+
if (typeof publicKey !== 'string' || publicKey.length !== 64) {
|
|
2119
|
+
return res.status(400).json({ error: 'publicKey must be 64-char hex (32 bytes)' });
|
|
2120
|
+
}
|
|
2121
|
+
const { addOrUpdatePeer } = await import('../network/known-peers.js');
|
|
2122
|
+
await addOrUpdatePeer(name, publicKey, notes);
|
|
2123
|
+
res.json({ ok: true });
|
|
2124
|
+
}
|
|
2125
|
+
catch (err) {
|
|
2126
|
+
res.status(500).json({ error: err.message });
|
|
2127
|
+
}
|
|
2128
|
+
});
|
|
2129
|
+
app.delete('/api/p2p-peers/:name', async (req, res) => {
|
|
2130
|
+
try {
|
|
2131
|
+
const { removePeer } = await import('../network/known-peers.js');
|
|
2132
|
+
await removePeer(req.params.name);
|
|
2133
|
+
res.json({ ok: true });
|
|
2134
|
+
}
|
|
2135
|
+
catch (err) {
|
|
2136
|
+
res.status(500).json({ error: err.message });
|
|
2137
|
+
}
|
|
2138
|
+
});
|
|
2139
|
+
// v3: 主动 connect 到对端的 P2PDirect publicKey
|
|
2140
|
+
// 用法: POST /api/remote-channels/p2p-connect { targetPublicKey: "<hex>" }
|
|
2141
|
+
app.post('/api/remote-channels/p2p-connect', async (req, res) => {
|
|
2142
|
+
try {
|
|
2143
|
+
if (!v3P2PRef) {
|
|
2144
|
+
return res.status(503).json({ error: 'P2PDirect not started' });
|
|
2145
|
+
}
|
|
2146
|
+
const { targetPublicKey, name, persist } = req.body || {};
|
|
2147
|
+
if (!targetPublicKey || typeof targetPublicKey !== 'string') {
|
|
2148
|
+
return res.status(400).json({ error: 'targetPublicKey required (hex)' });
|
|
2149
|
+
}
|
|
2150
|
+
// v3P2PRef 直接连到目标 publicKey (用 hyperswarm 的 joinPeer API)
|
|
2151
|
+
const swarm = v3P2PRef.swarm;
|
|
2152
|
+
if (!swarm)
|
|
2153
|
+
return res.status(503).json({ error: 'swarm not available' });
|
|
2154
|
+
const conn = await swarm.joinPeer(Buffer.from(targetPublicKey, 'hex'));
|
|
2155
|
+
console.log(`[v3] 已主动 joinPeer ${targetPublicKey.substring(0, 12)}...`);
|
|
2156
|
+
// 自动持久化 (默认开启) — 之后启动自动重连
|
|
2157
|
+
let persistedAs = null;
|
|
2158
|
+
if (persist !== false) {
|
|
2159
|
+
const { addOrUpdatePeer, findNameByPublicKey } = await import('../network/known-peers.js');
|
|
2160
|
+
// 优先用客户端传的 name, 否则用 publicKey 前 8 位
|
|
2161
|
+
const peerName = name || `peer-${targetPublicKey.substring(0, 8)}`;
|
|
2162
|
+
// 如果 publicKey 已被别的 name 占用, 用现有 name
|
|
2163
|
+
const existingName = await findNameByPublicKey(targetPublicKey);
|
|
2164
|
+
persistedAs = existingName ?? peerName ?? `peer-${targetPublicKey.substring(0, 8)}`;
|
|
2165
|
+
await addOrUpdatePeer(persistedAs, targetPublicKey);
|
|
2166
|
+
console.log(`[v3] 自动持久化 peer: ${persistedAs}`);
|
|
2167
|
+
}
|
|
2168
|
+
res.json({ ok: true, target: targetPublicKey, persistedAs });
|
|
2169
|
+
}
|
|
2170
|
+
catch (err) {
|
|
2171
|
+
console.error('[v3] p2p-connect 失败:', err);
|
|
2172
|
+
res.status(500).json({ error: err.message });
|
|
2173
|
+
}
|
|
2174
|
+
});
|
|
2175
|
+
// v3: 给远端 channel 发消息 (B 节点) - 通过 P2PDirect 转发到 A, A 跑 LLM, 回 B
|
|
2176
|
+
// 用法: POST /api/remote-channels/chat-send
|
|
2177
|
+
// { targetPublicKey, channelId, text }
|
|
2178
|
+
app.post('/api/remote-channels/chat-send', async (req, res) => {
|
|
2179
|
+
try {
|
|
2180
|
+
if (!v3P2PRef) {
|
|
2181
|
+
return res.status(503).json({ error: 'P2PDirect not started' });
|
|
2182
|
+
}
|
|
2183
|
+
const { targetPublicKey, channelId, text } = req.body || {};
|
|
2184
|
+
if (!targetPublicKey || !channelId || !text) {
|
|
2185
|
+
return res.status(400).json({ error: 'targetPublicKey, channelId, text required' });
|
|
2186
|
+
}
|
|
2187
|
+
if (typeof text !== 'string' || text.length === 0 || text.length > 8000) {
|
|
2188
|
+
return res.status(400).json({ error: 'text length must be 1-8000' });
|
|
2189
|
+
}
|
|
2190
|
+
const fromPk = v3P2PRef.getPublicKey();
|
|
2191
|
+
const msg = JSON.stringify({
|
|
2192
|
+
v: 3,
|
|
2193
|
+
op: 'agent.chat.send',
|
|
2194
|
+
payload: { channelId, text, fromPublicKey: fromPk }
|
|
2195
|
+
});
|
|
2196
|
+
const ok = v3P2PRef.sendTo(targetPublicKey, msg);
|
|
2197
|
+
if (!ok) {
|
|
2198
|
+
return res.status(502).json({
|
|
2199
|
+
error: 'peer not connected. POST /api/remote-channels/p2p-connect first.'
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
console.log(`[v3] chat-send 转发到 ${targetPublicKey.substring(0, 12)}... (channelId=${channelId})`);
|
|
2203
|
+
res.json({ ok: true, sent: true });
|
|
2204
|
+
}
|
|
2205
|
+
catch (err) {
|
|
2206
|
+
console.error('[v3] chat-send 失败:', err);
|
|
2207
|
+
res.status(500).json({ error: err.message });
|
|
2208
|
+
}
|
|
2209
|
+
});
|
|
2210
|
+
// v3 新增: B 拉 A 的 channel 历史 + 用了哪些 judgment
|
|
2211
|
+
// GET /api/remote-channels/chat-history?targetPublicKey=...&channelId=...
|
|
2212
|
+
// 实现: B → POST 给 A 一个 agent.history.get RPC → A 把 session 返回 → B 渲染
|
|
2213
|
+
app.get('/api/remote-channels/chat-history', async (req, res) => {
|
|
2214
|
+
try {
|
|
2215
|
+
if (!v3P2PRef) {
|
|
2216
|
+
return res.status(503).json({ error: 'P2PDirect not started' });
|
|
2217
|
+
}
|
|
2218
|
+
const targetPublicKey = String(req.query.targetPublicKey || '');
|
|
2219
|
+
const channelId = String(req.query.channelId || '');
|
|
2220
|
+
if (!targetPublicKey || !channelId) {
|
|
2221
|
+
return res.status(400).json({ error: 'targetPublicKey, channelId required' });
|
|
2222
|
+
}
|
|
2223
|
+
// 通过 RPC 拉 A 的 session — A 端收到后异步回复
|
|
2224
|
+
const fromPk = v3P2PRef.getPublicKey();
|
|
2225
|
+
const rpcId = `hist-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2226
|
+
const msg = JSON.stringify({
|
|
2227
|
+
v: 3,
|
|
2228
|
+
op: 'agent.history.get',
|
|
2229
|
+
payload: { rpcId, channelId, fromPublicKey: fromPk }
|
|
2230
|
+
});
|
|
2231
|
+
const ok = v3P2PRef.sendTo(targetPublicKey, msg);
|
|
2232
|
+
if (!ok) {
|
|
2233
|
+
return res.status(502).json({ error: 'peer not connected' });
|
|
2234
|
+
}
|
|
2235
|
+
// 等待 A 异步回复 (15s timeout) — 用一个 Promise 等
|
|
2236
|
+
const result = await new Promise((resolve, reject) => {
|
|
2237
|
+
const timer = setTimeout(() => {
|
|
2238
|
+
v3PendingHistoryGets.delete(rpcId);
|
|
2239
|
+
reject(new Error('A 端 15s 内未回复, 可能未分享该 channel'));
|
|
2240
|
+
}, 15000);
|
|
2241
|
+
v3PendingHistoryGets.set(rpcId, {
|
|
2242
|
+
resolve: (data) => { clearTimeout(timer); resolve(data); },
|
|
2243
|
+
reject: (err) => { clearTimeout(timer); reject(err); }
|
|
2244
|
+
});
|
|
2245
|
+
});
|
|
2246
|
+
console.log(`[v3] chat-history 从 ${targetPublicKey.substring(0, 12)}... 拉到 ${(result.messages || []).length} 条`);
|
|
2247
|
+
res.json(result);
|
|
2248
|
+
}
|
|
2249
|
+
catch (err) {
|
|
2250
|
+
console.error('[v3] chat-history 失败:', err.message);
|
|
2251
|
+
res.status(504).json({ error: err.message });
|
|
2252
|
+
}
|
|
2253
|
+
});
|
|
1537
2254
|
// 获取已连接的节点
|
|
1538
2255
|
app.get('/api/peers', async (_req, res) => {
|
|
1539
2256
|
try {
|
|
@@ -1699,7 +2416,8 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1699
2416
|
console.log(`[v3] 收到 agent.meta.list from ${msg.from.substring(0, 12)}...`);
|
|
1700
2417
|
try {
|
|
1701
2418
|
const channels = await loadChannels();
|
|
1702
|
-
|
|
2419
|
+
// iroh 路径保留 (admin / debug 用, 不走分享过滤)
|
|
2420
|
+
const publicMeta = channels.map((ch) => sanitizeChannelForPeer(ch));
|
|
1703
2421
|
const response = JSON.stringify({ ok: true, channels: publicMeta });
|
|
1704
2422
|
const encoded = new TextEncoder().encode(response);
|
|
1705
2423
|
// 沿用 msg.from 路由回去
|