@bolloon/bolloon-agent 0.1.28 → 0.1.30
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/web/client.js +191 -22
- package/dist/web/index.html +1 -0
- package/dist/web/server.js +277 -8
- package/package.json +1 -1
- package/src/llm/config-store.ts +61 -1
- package/src/llm/pi-ai.ts +5 -1
- package/src/web/client.js +191 -22
- package/src/web/index.html +1 -0
- package/src/web/server.ts +288 -8
package/src/web/server.ts
CHANGED
|
@@ -23,10 +23,31 @@ import { createAgentDelegateApp } from './agent-delegate-server.js';
|
|
|
23
23
|
import { createIrohDelegateTransport } from './iroh-delegate-transport.js';
|
|
24
24
|
import { verifyMessage, isAddress, getAddress } from 'viem';
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
// 前端资源路径: 兼容 src 运行 + dist 运行 + npm 全局安装
|
|
27
|
+
// - src 跑 (tsx): __dirname = .../src/web → .../dist/web
|
|
28
|
+
// - dist 跑 (npm): __dirname = .../dist/web → 自身就是 web 根
|
|
29
|
+
// - 环境变量覆盖: BOLLOON_WEB_ROOT=xxx
|
|
30
|
+
// ESM scope 没有 __dirname, 这里自己声明
|
|
31
|
+
const __filename_local = fileURLToPath(import.meta.url);
|
|
32
|
+
const __dirname_local = dirname(__filename_local);
|
|
33
|
+
let _baseDirname = __dirname_local;
|
|
34
|
+
function resolveWebRoot(): string {
|
|
35
|
+
if (process.env.BOLLOON_WEB_ROOT && fsSync.existsSync(process.env.BOLLOON_WEB_ROOT)) {
|
|
36
|
+
return process.env.BOLLOON_WEB_ROOT;
|
|
37
|
+
}
|
|
38
|
+
const d = _baseDirname;
|
|
39
|
+
const candidates = [
|
|
40
|
+
path.join(d), // dist/web
|
|
41
|
+
path.join(d, '..', '..', 'dist', 'web'), // src/web → dist/web
|
|
42
|
+
path.join(d, '..', 'web'), // dist/ → web/ 兄弟
|
|
43
|
+
];
|
|
44
|
+
for (const c of candidates) {
|
|
45
|
+
if (fsSync.existsSync(path.join(c, 'index.html'))) return c;
|
|
46
|
+
}
|
|
47
|
+
return candidates[1];
|
|
48
|
+
}
|
|
49
|
+
const webRoot = resolveWebRoot();
|
|
50
|
+
console.log(`[web] webRoot = ${webRoot}`);
|
|
30
51
|
|
|
31
52
|
const SHARED_SESSION_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'sessions');
|
|
32
53
|
const SESSION_CACHE_PATH = path.join(SHARED_SESSION_PATH, 'cache');
|
|
@@ -391,6 +412,8 @@ let sseClients: Set<SSEClient> = new Set();
|
|
|
391
412
|
let remoteChannelCache: Map<string, Array<Record<string, unknown>>> = new Map();
|
|
392
413
|
// v3: P2PDirect 引用 (Hyperswarm 薄包装) - 模块级, 因为 web server 闭包里不可用
|
|
393
414
|
let v3P2PRef: import('../network/p2p-direct.js').P2PDirect | null = null;
|
|
415
|
+
// v3: 等待中的 history RPC (B 端 chat-history endpoint 用) — rpcId → { resolve, reject }
|
|
416
|
+
const v3PendingHistoryGets: Map<string, { resolve: (data: any) => void; reject: (err: Error) => void }> = new Map();
|
|
394
417
|
let channelSessions: Map<string, AgentSession> = new Map(); // key: channelId
|
|
395
418
|
let sessionMessages: Map<string, any[]> = new Map(); // key: channelId + sessionId
|
|
396
419
|
|
|
@@ -435,6 +458,12 @@ function sanitizeChannelForPeer(
|
|
|
435
458
|
};
|
|
436
459
|
}
|
|
437
460
|
|
|
461
|
+
/** v3 新增: 判断 channel 是否分享给 peerPublicKey */
|
|
462
|
+
function isSharedWith(ch: Channel, peerPublicKey: string): boolean {
|
|
463
|
+
const shared = Array.isArray(ch.shared_with_peers) ? ch.shared_with_peers : [];
|
|
464
|
+
return shared.includes(peerPublicKey);
|
|
465
|
+
}
|
|
466
|
+
|
|
438
467
|
/**
|
|
439
468
|
* v3: 处理 Hyperswarm 通道收到的 v3 RPC 消息
|
|
440
469
|
* 设计: 用 HyperswarmCommunicator (DHT topic 自动发现) 取代 iroh 直接 connect
|
|
@@ -521,7 +550,8 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
|
|
|
521
550
|
console.warn(`[v3] agent.chat.send 缺少 channelId/text`);
|
|
522
551
|
return;
|
|
523
552
|
}
|
|
524
|
-
|
|
553
|
+
const senderKey = fromPublicKey || peerKey;
|
|
554
|
+
console.log(`[v3] 收到 ${senderKey.substring(0,12)}... 对 channel ${channelId} 的 chat: "${text.substring(0, 40)}..."`);
|
|
525
555
|
try {
|
|
526
556
|
// 1. 找到 channel
|
|
527
557
|
const channels = await loadChannels();
|
|
@@ -534,20 +564,85 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
|
|
|
534
564
|
await comm.sendToConnection(conn.id, reply);
|
|
535
565
|
return;
|
|
536
566
|
}
|
|
537
|
-
//
|
|
567
|
+
// v3 新增: 持久化 B 的 user 消息到 A 的 session — 让历史可拉
|
|
568
|
+
try {
|
|
569
|
+
const existing = await loadSession(channelId, 'default');
|
|
570
|
+
const session: Session = existing || {
|
|
571
|
+
channelId, sessionId: 'default', messages: [], lastUpdated: new Date().toISOString()
|
|
572
|
+
};
|
|
573
|
+
session.messages.push({
|
|
574
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
575
|
+
type: 'user',
|
|
576
|
+
content: text,
|
|
577
|
+
timestamp: new Date().toISOString()
|
|
578
|
+
});
|
|
579
|
+
session.lastUpdated = new Date().toISOString();
|
|
580
|
+
await saveSession(session);
|
|
581
|
+
console.log(`[v3] (${channelId}) 存 user 消息 (${text.length} chars) 到 A 的 session`);
|
|
582
|
+
} catch (saveErr) {
|
|
583
|
+
console.warn(`[v3] 存 user 消息失败 (不影响 chat):`, (saveErr as Error).message);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// v3 新增: 告诉 B "我开始想了, 用了哪些 judgment" — 让 B 看到决策依据
|
|
538
587
|
const judgmentHint = await buildJudgmentHint(ch, channelId);
|
|
588
|
+
const usedJudgments = await extractJudgmentsFromHint(ch);
|
|
589
|
+
try {
|
|
590
|
+
const thinkingStart = JSON.stringify({
|
|
591
|
+
v: 3, op: 'agent.chat.thinking',
|
|
592
|
+
payload: {
|
|
593
|
+
channelId,
|
|
594
|
+
phase: 'start',
|
|
595
|
+
fromPublicKey: v3P2PRef?.getPublicKey() || '',
|
|
596
|
+
hint: judgmentHint,
|
|
597
|
+
usedJudgments,
|
|
598
|
+
userText: text
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
await comm.sendToConnection(conn.id, thinkingStart);
|
|
602
|
+
} catch {}
|
|
603
|
+
|
|
604
|
+
// 2. 跑 LLM (复用 Phase 1 的 buildJudgmentHint — 注入 channel 的 judgment)
|
|
539
605
|
const { getMinimax } = await import('../constraints/index.js');
|
|
540
606
|
const llm = getMinimax();
|
|
541
607
|
const fullPrompt = `${judgmentHint}${text}`;
|
|
542
608
|
let fullResponse = '';
|
|
609
|
+
// v3 新增: 流式 token 节流推给 B — 让 B 看到过程
|
|
610
|
+
let lastFlushAt = 0;
|
|
543
611
|
const streamCallback: any = (event: any) => {
|
|
544
|
-
// 流式 token, 不广播给 B (避免半成品噪音), 只记 A 自己的日志
|
|
545
612
|
if (event.type === 'token') {
|
|
546
613
|
fullResponse += event.content;
|
|
614
|
+
if (fullResponse.length - lastFlushAt >= 20) {
|
|
615
|
+
lastFlushAt = fullResponse.length;
|
|
616
|
+
const msg = JSON.stringify({
|
|
617
|
+
v: 3, op: 'agent.chat.thinking',
|
|
618
|
+
payload: { channelId, phase: 'token', partial: fullResponse, fromPublicKey: v3P2PRef?.getPublicKey() || '' }
|
|
619
|
+
});
|
|
620
|
+
comm.sendToConnection(conn.id, msg).catch(() => {});
|
|
621
|
+
}
|
|
547
622
|
}
|
|
548
623
|
};
|
|
549
624
|
const agent = await getAgentForChannel(channelId, ch.did || '', ch.name, ch.didDocRef);
|
|
550
625
|
fullResponse = await agent.promptStream(fullPrompt, streamCallback);
|
|
626
|
+
|
|
627
|
+
// v3 新增: 存 A 的 assistant 消息到 session — B 拉历史时能看到完整对话
|
|
628
|
+
try {
|
|
629
|
+
const existing = await loadSession(channelId, 'default');
|
|
630
|
+
const session: Session = existing || {
|
|
631
|
+
channelId, sessionId: 'default', messages: [], lastUpdated: new Date().toISOString()
|
|
632
|
+
};
|
|
633
|
+
session.messages.push({
|
|
634
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
635
|
+
type: 'ai',
|
|
636
|
+
content: fullResponse,
|
|
637
|
+
timestamp: new Date().toISOString()
|
|
638
|
+
});
|
|
639
|
+
session.lastUpdated = new Date().toISOString();
|
|
640
|
+
await saveSession(session);
|
|
641
|
+
console.log(`[v3] (${channelId}) 存 assistant 回复 (${fullResponse.length} chars) 到 A 的 session`);
|
|
642
|
+
} catch (saveErr) {
|
|
643
|
+
console.warn(`[v3] 存 assistant 消息失败 (不影响):`, (saveErr as Error).message);
|
|
644
|
+
}
|
|
645
|
+
|
|
551
646
|
// 3. 把完整回复发给 B
|
|
552
647
|
const reply = JSON.stringify({
|
|
553
648
|
v: 3, op: 'agent.chat.reply',
|
|
@@ -558,7 +653,7 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
|
|
|
558
653
|
}
|
|
559
654
|
});
|
|
560
655
|
await comm.sendToConnection(conn.id, reply);
|
|
561
|
-
console.log(`[v3] 回 chat.reply 给 ${
|
|
656
|
+
console.log(`[v3] 回 chat.reply 给 ${senderKey.substring(0,12)}... (${fullResponse.length} chars)`);
|
|
562
657
|
} catch (err) {
|
|
563
658
|
console.error(`[v3] agent.chat.send 处理失败:`, (err as Error).message);
|
|
564
659
|
try {
|
|
@@ -572,6 +667,65 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
|
|
|
572
667
|
return;
|
|
573
668
|
}
|
|
574
669
|
|
|
670
|
+
if (op === 'agent.history.get') {
|
|
671
|
+
// v3 新增: B 拉 A 的 channel 历史 (含所有 message + judgment hint)
|
|
672
|
+
// 共享过滤: 只返回 B 可见的 channel + 包含的 judgment
|
|
673
|
+
const { channelId, rpcId, fromPublicKey } = parsed.payload || {};
|
|
674
|
+
if (!channelId || !rpcId) {
|
|
675
|
+
console.warn(`[v3] agent.history.get 缺少 channelId/rpcId`);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
const channels = await loadChannels();
|
|
680
|
+
const ch = channels.find(c => c.id === channelId);
|
|
681
|
+
if (!ch) {
|
|
682
|
+
const err = JSON.stringify({
|
|
683
|
+
v: 3, op: 'agent.history.get.reply',
|
|
684
|
+
payload: { rpcId, error: 'channel not found', messages: [], judgments: { bound: [], candidates: [] } }
|
|
685
|
+
});
|
|
686
|
+
await comm.sendToConnection(conn.id, err);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
// 共享过滤: 必须 peerKey 在 shared_with_peers 里 (避免泄露未分享的 channel)
|
|
690
|
+
const peerKey = fromPublicKey;
|
|
691
|
+
if (!peerKey || !isSharedWith(ch, peerKey)) {
|
|
692
|
+
const err = JSON.stringify({
|
|
693
|
+
v: 3, op: 'agent.history.get.reply',
|
|
694
|
+
payload: { rpcId, error: 'channel not shared with you', messages: [], judgments: { bound: [], candidates: [] } }
|
|
695
|
+
});
|
|
696
|
+
await comm.sendToConnection(conn.id, err);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
// 加载 A 端 session
|
|
700
|
+
const session = await loadSession(channelId, 'default');
|
|
701
|
+
// 加载 channel 用到的 judgment
|
|
702
|
+
const judgments = await extractJudgmentsFromHint(ch);
|
|
703
|
+
const reply = JSON.stringify({
|
|
704
|
+
v: 3, op: 'agent.history.get.reply',
|
|
705
|
+
payload: {
|
|
706
|
+
rpcId,
|
|
707
|
+
channelId,
|
|
708
|
+
messages: session?.messages || [],
|
|
709
|
+
lastUpdated: session?.lastUpdated,
|
|
710
|
+
judgments,
|
|
711
|
+
channelName: ch.name
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
await comm.sendToConnection(conn.id, reply);
|
|
715
|
+
console.log(`[v3] 回 history.reply 给 ${peerKey.substring(0,12)}... (channelId=${channelId}, ${session?.messages?.length || 0} messages)`);
|
|
716
|
+
} catch (err) {
|
|
717
|
+
console.error(`[v3] agent.history.get 处理失败:`, (err as Error).message);
|
|
718
|
+
try {
|
|
719
|
+
const errMsg = JSON.stringify({
|
|
720
|
+
v: 3, op: 'agent.history.get.reply',
|
|
721
|
+
payload: { rpcId, error: (err as Error).message, messages: [], judgments: { bound: [], candidates: [] } }
|
|
722
|
+
});
|
|
723
|
+
await comm.sendToConnection(conn.id, errMsg);
|
|
724
|
+
} catch {}
|
|
725
|
+
}
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
575
729
|
console.log(`[v3] 收到未知 op: ${op}`);
|
|
576
730
|
}
|
|
577
731
|
|
|
@@ -629,6 +783,47 @@ async function buildJudgmentHint(
|
|
|
629
783
|
}
|
|
630
784
|
}
|
|
631
785
|
|
|
786
|
+
/**
|
|
787
|
+
* v3 新增: 把 channel 当前用到的 judgment 提取成结构化数据, 给 B 端 UI 显示.
|
|
788
|
+
* 返回 { bound: [...], candidates: [...] } — bound 是硬绑定, candidates 是参考池.
|
|
789
|
+
*/
|
|
790
|
+
async function extractJudgmentsFromHint(
|
|
791
|
+
channel: Channel | undefined | null
|
|
792
|
+
): Promise<{ bound: any[]; candidates: any[] }> {
|
|
793
|
+
try {
|
|
794
|
+
const { loadAllJudgments, initializeValueStore } = await import(
|
|
795
|
+
'../pi-ecosystem-judgment/human-value-store.js'
|
|
796
|
+
);
|
|
797
|
+
await initializeValueStore();
|
|
798
|
+
const allJudgments = await loadAllJudgments();
|
|
799
|
+
if (allJudgments.length === 0) return { bound: [], candidates: [] };
|
|
800
|
+
|
|
801
|
+
const boundIds = new Set(
|
|
802
|
+
channel && Array.isArray(channel.bound_judgment_ids) ? channel.bound_judgment_ids : []
|
|
803
|
+
);
|
|
804
|
+
|
|
805
|
+
const summarize = (j: any) => ({
|
|
806
|
+
id: j.id,
|
|
807
|
+
decision: (j.decision || '').toString().slice(0, 200),
|
|
808
|
+
reasons: Array.isArray(j.reasons) ? j.reasons : [],
|
|
809
|
+
domain: j.domain,
|
|
810
|
+
stakes: j.stakes
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
const bound = allJudgments
|
|
814
|
+
.filter((j: any) => j.id !== undefined && boundIds.has(j.id))
|
|
815
|
+
.map(summarize);
|
|
816
|
+
const candidates = allJudgments
|
|
817
|
+
.filter((j: any) => j.id !== undefined && !boundIds.has(j.id))
|
|
818
|
+
.map(summarize);
|
|
819
|
+
|
|
820
|
+
return { bound, candidates };
|
|
821
|
+
} catch (err) {
|
|
822
|
+
console.warn(`[v3] extractJudgmentsFromHint 失败:`, (err as Error).message);
|
|
823
|
+
return { bound: [], candidates: [] };
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
632
827
|
async function getAgentForChannel(
|
|
633
828
|
channelId: string,
|
|
634
829
|
channelDid?: string,
|
|
@@ -787,6 +982,44 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
|
|
|
787
982
|
}, 'p2p-global');
|
|
788
983
|
return;
|
|
789
984
|
}
|
|
985
|
+
// v3 新增: B 端收到 A 的 thinking (开始 + 流式 token)
|
|
986
|
+
if (parsed.op === 'agent.chat.thinking') {
|
|
987
|
+
const phase = parsed.payload?.phase;
|
|
988
|
+
if (phase === 'start') {
|
|
989
|
+
console.log(`[v3] 收到来自 ${evt.fromPublicKey.substring(0,12)}... 的 thinking start (judgments: bound=${(parsed.payload?.usedJudgments?.bound || []).length}, candidates=${(parsed.payload?.usedJudgments?.candidates || []).length})`);
|
|
990
|
+
}
|
|
991
|
+
broadcast({
|
|
992
|
+
type: 'remote-chat-thinking',
|
|
993
|
+
fromPublicKey: evt.fromPublicKey,
|
|
994
|
+
channelId: parsed.payload?.channelId,
|
|
995
|
+
phase: parsed.payload?.phase,
|
|
996
|
+
partial: parsed.payload?.partial,
|
|
997
|
+
hint: parsed.payload?.hint,
|
|
998
|
+
usedJudgments: parsed.payload?.usedJudgments,
|
|
999
|
+
userText: parsed.payload?.userText
|
|
1000
|
+
}, 'p2p-global');
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
// v3 新增: B 端收到 A 的 history reply → resolve pending promise
|
|
1004
|
+
if (parsed.op === 'agent.history.get.reply') {
|
|
1005
|
+
const rpcId = parsed.payload?.rpcId;
|
|
1006
|
+
if (rpcId && v3PendingHistoryGets.has(rpcId)) {
|
|
1007
|
+
const pending = v3PendingHistoryGets.get(rpcId)!;
|
|
1008
|
+
v3PendingHistoryGets.delete(rpcId);
|
|
1009
|
+
if (parsed.payload?.error) {
|
|
1010
|
+
pending.reject(new Error(parsed.payload.error));
|
|
1011
|
+
} else {
|
|
1012
|
+
pending.resolve({
|
|
1013
|
+
channelId: parsed.payload.channelId,
|
|
1014
|
+
messages: parsed.payload.messages || [],
|
|
1015
|
+
lastUpdated: parsed.payload.lastUpdated,
|
|
1016
|
+
judgments: parsed.payload.judgments || { bound: [], candidates: [] },
|
|
1017
|
+
channelName: parsed.payload.channelName
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
790
1023
|
const commShim = {
|
|
791
1024
|
sendToConnection: (_id: string, data: string) => {
|
|
792
1025
|
v3P2PRef!.sendTo(evt.fromPublicKey, data);
|
|
@@ -2265,6 +2498,53 @@ app.get('/channels', async (_req, res) => {
|
|
|
2265
2498
|
}
|
|
2266
2499
|
});
|
|
2267
2500
|
|
|
2501
|
+
// v3 新增: B 拉 A 的 channel 历史 + 用了哪些 judgment
|
|
2502
|
+
// GET /api/remote-channels/chat-history?targetPublicKey=...&channelId=...
|
|
2503
|
+
// 实现: B → POST 给 A 一个 agent.history.get RPC → A 把 session 返回 → B 渲染
|
|
2504
|
+
app.get('/api/remote-channels/chat-history', async (req, res) => {
|
|
2505
|
+
try {
|
|
2506
|
+
if (!v3P2PRef) {
|
|
2507
|
+
return res.status(503).json({ error: 'P2PDirect not started' });
|
|
2508
|
+
}
|
|
2509
|
+
const targetPublicKey = String(req.query.targetPublicKey || '');
|
|
2510
|
+
const channelId = String(req.query.channelId || '');
|
|
2511
|
+
if (!targetPublicKey || !channelId) {
|
|
2512
|
+
return res.status(400).json({ error: 'targetPublicKey, channelId required' });
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
// 通过 RPC 拉 A 的 session — A 端收到后异步回复
|
|
2516
|
+
const fromPk = v3P2PRef.getPublicKey();
|
|
2517
|
+
const rpcId = `hist-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2518
|
+
const msg = JSON.stringify({
|
|
2519
|
+
v: 3,
|
|
2520
|
+
op: 'agent.history.get',
|
|
2521
|
+
payload: { rpcId, channelId, fromPublicKey: fromPk }
|
|
2522
|
+
});
|
|
2523
|
+
const ok = v3P2PRef.sendTo(targetPublicKey, msg);
|
|
2524
|
+
if (!ok) {
|
|
2525
|
+
return res.status(502).json({ error: 'peer not connected' });
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
// 等待 A 异步回复 (15s timeout) — 用一个 Promise 等
|
|
2529
|
+
const result = await new Promise<any>((resolve, reject) => {
|
|
2530
|
+
const timer = setTimeout(() => {
|
|
2531
|
+
v3PendingHistoryGets.delete(rpcId);
|
|
2532
|
+
reject(new Error('A 端 15s 内未回复, 可能未分享该 channel'));
|
|
2533
|
+
}, 15000);
|
|
2534
|
+
v3PendingHistoryGets.set(rpcId, {
|
|
2535
|
+
resolve: (data) => { clearTimeout(timer); resolve(data); },
|
|
2536
|
+
reject: (err) => { clearTimeout(timer); reject(err); }
|
|
2537
|
+
});
|
|
2538
|
+
});
|
|
2539
|
+
|
|
2540
|
+
console.log(`[v3] chat-history 从 ${targetPublicKey.substring(0,12)}... 拉到 ${(result.messages || []).length} 条`);
|
|
2541
|
+
res.json(result);
|
|
2542
|
+
} catch (err: any) {
|
|
2543
|
+
console.error('[v3] chat-history 失败:', err.message);
|
|
2544
|
+
res.status(504).json({ error: err.message });
|
|
2545
|
+
}
|
|
2546
|
+
});
|
|
2547
|
+
|
|
2268
2548
|
// 获取已连接的节点
|
|
2269
2549
|
app.get('/api/peers', async (_req, res) => {
|
|
2270
2550
|
try {
|