@bolloon/bolloon-agent 0.1.28 → 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/web/client.js +148 -7
- package/dist/web/index.html +1 -0
- package/dist/web/server.js +251 -4
- package/package.json +1 -1
- package/src/web/client.js +148 -7
- package/src/web/index.html +1 -0
- package/src/web/server.ts +263 -4
package/dist/web/client.js
CHANGED
|
@@ -118,6 +118,8 @@ function startV3GlobalSSE() {
|
|
|
118
118
|
if (msg.type === 'remote-chat-reply') {
|
|
119
119
|
// 找到当前打开的远端 chat modal 的 log
|
|
120
120
|
const log = document.getElementById('rcm-log');
|
|
121
|
+
const thinkingEl = document.getElementById('rcm-thinking');
|
|
122
|
+
if (thinkingEl) thinkingEl.style.display = 'none'; // 思考结束, 隐藏
|
|
121
123
|
if (log) {
|
|
122
124
|
const bubble = document.createElement('div');
|
|
123
125
|
bubble.style.cssText = 'padding:8px 10px;margin:4px 0;border-radius:6px;font-size:13px;line-height:1.4;max-width:80%;word-wrap:break-word;background:#e5e7eb;color:#111;';
|
|
@@ -129,6 +131,44 @@ function startV3GlobalSSE() {
|
|
|
129
131
|
log.appendChild(bubble);
|
|
130
132
|
log.scrollTop = log.scrollHeight;
|
|
131
133
|
}
|
|
134
|
+
} else if (msg.type === 'remote-chat-thinking') {
|
|
135
|
+
// v3 新增: B 端实时显示 A 节点的思考过程
|
|
136
|
+
const phase = msg.phase;
|
|
137
|
+
const log = document.getElementById('rcm-log');
|
|
138
|
+
if (!log) return;
|
|
139
|
+
|
|
140
|
+
if (phase === 'start') {
|
|
141
|
+
// 头部插入"判断力依据"区块 (只第一次)
|
|
142
|
+
const judgments = msg.usedJudgments || { bound: [], candidates: [] };
|
|
143
|
+
const judgmentBlock = document.createElement('div');
|
|
144
|
+
judgmentBlock.className = 'rcm-judgment-block';
|
|
145
|
+
judgmentBlock.style.cssText = 'margin:6px 0;padding:8px 10px;background:#fef3c7;border-left:3px solid #f59e0b;border-radius:4px;font-size:12px;';
|
|
146
|
+
let jh = '<div style="font-weight:600;color:#92400e;margin-bottom:4px;">🛡️ 对方使用的判断力 (来自 ta 的 channel)</div>';
|
|
147
|
+
if (judgments.bound && judgments.bound.length > 0) {
|
|
148
|
+
jh += '<div style="color:#78350f;margin-bottom:4px;"><b>硬绑定</b> (必须遵循):</div>';
|
|
149
|
+
for (const j of judgments.bound) {
|
|
150
|
+
jh += `<div style="margin:2px 0;padding-left:8px;">• <b>${escapeHtml((j.decision || '').slice(0, 80))}</b>${j.reasons && j.reasons.length ? '<br><span style="color:#92400e;font-size:11px;">理由: ' + escapeHtml(j.reasons.join('; ').slice(0, 80)) + '</span>' : ''}</div>`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (judgments.candidates && judgments.candidates.length > 0) {
|
|
154
|
+
jh += `<div style="color:#78350f;margin-top:4px;"><b>候选池</b> (${judgments.candidates.length} 条, LLM 自选)</div>`;
|
|
155
|
+
}
|
|
156
|
+
log.appendChild(judgmentBlock);
|
|
157
|
+
// "思考中" 区块
|
|
158
|
+
const thinkingEl = document.createElement('div');
|
|
159
|
+
thinkingEl.id = 'rcm-thinking-live';
|
|
160
|
+
thinkingEl.style.cssText = 'margin:6px 0;padding:8px 10px;background:#ede9fe;border-left:3px solid #8b5cf6;border-radius:4px;font-size:12px;color:#5b21b6;font-style:italic;';
|
|
161
|
+
thinkingEl.textContent = '💭 对方正在思考...';
|
|
162
|
+
log.appendChild(thinkingEl);
|
|
163
|
+
log.scrollTop = log.scrollHeight;
|
|
164
|
+
} else if (phase === 'token') {
|
|
165
|
+
// 实时更新思考中的 partial
|
|
166
|
+
const thinkingEl = document.getElementById('rcm-thinking-live');
|
|
167
|
+
if (thinkingEl) {
|
|
168
|
+
thinkingEl.textContent = '💭 对方正在思考: ' + (msg.partial || '').slice(-200);
|
|
169
|
+
log.scrollTop = log.scrollHeight;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
132
172
|
}
|
|
133
173
|
} catch (err) {
|
|
134
174
|
console.error('[v3] 全局 SSE 解析失败:', err);
|
|
@@ -2683,15 +2723,19 @@ function openRemoteChannelChat(peerPublicKey, channelId, channelName) {
|
|
|
2683
2723
|
document.getElementById('remote-chat-modal')?.remove();
|
|
2684
2724
|
const html = `
|
|
2685
2725
|
<div id="remote-chat-modal" style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:10002;display:flex;align-items:center;justify-content:center;">
|
|
2686
|
-
<div style="background:#fff;border-radius:8px;width:
|
|
2726
|
+
<div style="background:#fff;border-radius:8px;width:560px;max-width:92vw;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 10px 40px rgba(0,0,0,0.2);">
|
|
2687
2727
|
<div style="padding:12px 16px;border-bottom:1px solid #e5e7eb;display:flex;align-items:center;justify-content:space-between;">
|
|
2688
|
-
<div>
|
|
2728
|
+
<div style="flex:1;min-width:0;">
|
|
2689
2729
|
<div style="font-size:15px;font-weight:600;">跟 ${escapeHtml(channelName)} 聊天</div>
|
|
2690
|
-
<div style="font-size:11px;color:#6b7280;margin-top:2px;">远端 peer: ${escapeHtml(peerPublicKey.substring(0,16))}… ·
|
|
2730
|
+
<div style="font-size:11px;color:#6b7280;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">远端 peer: ${escapeHtml(peerPublicKey.substring(0,16))}… · ${escapeHtml(channelId)}</div>
|
|
2691
2731
|
</div>
|
|
2732
|
+
<button id="rcm-refresh-history" title="重新拉历史" style="background:none;border:1px solid var(--border);color:#6b7280;cursor:pointer;padding:2px 8px;border-radius:4px;font-size:11px;margin-right:6px;">↻ 历史</button>
|
|
2692
2733
|
<button id="rcm-close" style="background:none;border:none;font-size:20px;color:#6b7280;cursor:pointer;">×</button>
|
|
2693
2734
|
</div>
|
|
2694
|
-
<div id="rcm-
|
|
2735
|
+
<div id="rcm-thinking" style="display:none;padding:8px 16px;background:#fef3c7;color:#92400e;font-size:12px;border-bottom:1px solid #e5e7eb;">
|
|
2736
|
+
📥 正在从远端拉历史 + 判断力…
|
|
2737
|
+
</div>
|
|
2738
|
+
<div id="rcm-log" style="flex:1;overflow-y:auto;padding:12px 16px;min-height:240px;max-height:60vh;background:#f9fafb;"></div>
|
|
2695
2739
|
<div style="padding:10px 12px;border-top:1px solid #e5e7eb;display:flex;gap:6px;">
|
|
2696
2740
|
<input id="rcm-input" type="text" placeholder="输入消息, 发送到远端 channel..."
|
|
2697
2741
|
style="flex:1;padding:8px 10px;border:1px solid #d1d5db;border-radius:4px;font-size:13px;">
|
|
@@ -2705,10 +2749,13 @@ function openRemoteChannelChat(peerPublicKey, channelId, channelName) {
|
|
|
2705
2749
|
const log = document.getElementById('rcm-log');
|
|
2706
2750
|
const inputEl = document.getElementById('rcm-input');
|
|
2707
2751
|
const sendBtn = document.getElementById('rcm-send');
|
|
2752
|
+
const thinkingEl = document.getElementById('rcm-thinking');
|
|
2708
2753
|
document.getElementById('rcm-close').onclick = () => document.getElementById('remote-chat-modal').remove();
|
|
2754
|
+
document.getElementById('rcm-refresh-history').onclick = () => loadHistory();
|
|
2709
2755
|
|
|
2710
|
-
const append = (text,
|
|
2756
|
+
const append = (text, role) => {
|
|
2711
2757
|
const bubble = document.createElement('div');
|
|
2758
|
+
const isUser = role === 'user';
|
|
2712
2759
|
bubble.style.cssText = `padding:8px 10px;margin:4px 0;border-radius:6px;font-size:13px;line-height:1.4;max-width:80%;word-wrap:break-word;${
|
|
2713
2760
|
isUser ? 'background:#2563eb;color:#fff;margin-left:auto;text-align:left;'
|
|
2714
2761
|
: 'background:#e5e7eb;color:#111;'
|
|
@@ -2718,10 +2765,75 @@ function openRemoteChannelChat(peerPublicKey, channelId, channelName) {
|
|
|
2718
2765
|
log.scrollTop = log.scrollHeight;
|
|
2719
2766
|
};
|
|
2720
2767
|
|
|
2768
|
+
const appendSystem = (text, kind = 'info') => {
|
|
2769
|
+
const el = document.createElement('div');
|
|
2770
|
+
const colors = {
|
|
2771
|
+
info: { bg: '#dbeafe', fg: '#1e40af' },
|
|
2772
|
+
warn: { bg: '#fef3c7', fg: '#92400e' },
|
|
2773
|
+
error: { bg: '#fca5a5', fg: '#7f1d1d' }
|
|
2774
|
+
};
|
|
2775
|
+
const c = colors[kind] || colors.info;
|
|
2776
|
+
el.style.cssText = `margin:6px 0;padding:6px 10px;background:${c.bg};color:${c.fg};border-radius:4px;font-size:11px;text-align:center;`;
|
|
2777
|
+
el.textContent = text;
|
|
2778
|
+
log.appendChild(el);
|
|
2779
|
+
log.scrollTop = log.scrollHeight;
|
|
2780
|
+
};
|
|
2781
|
+
|
|
2782
|
+
// v3 新增: 拉 A 端的 channel 历史 (含 messages + judgments)
|
|
2783
|
+
async function loadHistory() {
|
|
2784
|
+
thinkingEl.style.display = 'block';
|
|
2785
|
+
log.innerHTML = '';
|
|
2786
|
+
appendSystem('正在拉取远端 channel 的历史和判断力...', 'info');
|
|
2787
|
+
try {
|
|
2788
|
+
const res = await fetch(`/api/remote-channels/chat-history?targetPublicKey=${encodeURIComponent(peerPublicKey)}&channelId=${encodeURIComponent(channelId)}`);
|
|
2789
|
+
const data = await res.json();
|
|
2790
|
+
if (!res.ok) {
|
|
2791
|
+
appendSystem(`拉取失败: ${data.error || 'unknown'}`, 'error');
|
|
2792
|
+
thinkingEl.style.display = 'none';
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
// 清掉 loading
|
|
2796
|
+
log.innerHTML = '';
|
|
2797
|
+
|
|
2798
|
+
// 1. 显示 judgment 依据 (header)
|
|
2799
|
+
const judgments = data.judgments || { bound: [], candidates: [] };
|
|
2800
|
+
if (judgments.bound && judgments.bound.length > 0) {
|
|
2801
|
+
const jh = document.createElement('div');
|
|
2802
|
+
jh.style.cssText = 'margin:0 0 8px;padding:8px 10px;background:#fef3c7;border-left:3px solid #f59e0b;border-radius:4px;font-size:12px;';
|
|
2803
|
+
let h = `<div style="font-weight:600;color:#92400e;margin-bottom:4px;">🛡️ 对方 channel 绑定的判断力 (${judgments.bound.length} 条硬约束)</div>`;
|
|
2804
|
+
for (const j of judgments.bound) {
|
|
2805
|
+
h += `<div style="margin:3px 0;padding-left:8px;">• <b>${escapeHtml((j.decision || '').slice(0, 100))}</b>${j.domain ? `<span style="color:#a16207;font-size:10px;"> [${escapeHtml(j.domain)}${j.stakes ? '/' + escapeHtml(j.stakes) : ''}]</span>` : ''}${j.reasons && j.reasons.length ? '<br><span style="color:#92400e;font-size:11px;">理由: ' + escapeHtml(j.reasons.join('; ').slice(0, 100)) + '</span>' : ''}</div>`;
|
|
2806
|
+
}
|
|
2807
|
+
if (judgments.candidates && judgments.candidates.length > 0) {
|
|
2808
|
+
h += `<div style="margin-top:6px;color:#92400e;font-size:11px;">+ ${judgments.candidates.length} 条候选判断力 (LLM 可自选参考)</div>`;
|
|
2809
|
+
}
|
|
2810
|
+
jh.innerHTML = h;
|
|
2811
|
+
log.appendChild(jh);
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
// 2. 显示历史 messages
|
|
2815
|
+
const msgs = data.messages || [];
|
|
2816
|
+
if (msgs.length === 0) {
|
|
2817
|
+
appendSystem('还没有历史消息, 在下面发第一条吧', 'info');
|
|
2818
|
+
} else {
|
|
2819
|
+
appendSystem(`从远端拉到 ${msgs.length} 条历史消息`, 'info');
|
|
2820
|
+
for (const m of msgs) {
|
|
2821
|
+
append(m.content || '', m.type === 'user' ? 'user' : 'ai');
|
|
2822
|
+
}
|
|
2823
|
+
// 滚到底部
|
|
2824
|
+
setTimeout(() => { log.scrollTop = log.scrollHeight; }, 50);
|
|
2825
|
+
}
|
|
2826
|
+
} catch (err) {
|
|
2827
|
+
appendSystem(`拉取异常: ${err.message}`, 'error');
|
|
2828
|
+
} finally {
|
|
2829
|
+
thinkingEl.style.display = 'none';
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2721
2833
|
const doSend = async () => {
|
|
2722
2834
|
const text = inputEl.value.trim();
|
|
2723
2835
|
if (!text) return;
|
|
2724
|
-
append(text,
|
|
2836
|
+
append(text, 'user');
|
|
2725
2837
|
inputEl.value = '';
|
|
2726
2838
|
sendBtn.disabled = true;
|
|
2727
2839
|
sendBtn.textContent = '...';
|
|
@@ -2733,8 +2845,9 @@ function openRemoteChannelChat(peerPublicKey, channelId, channelName) {
|
|
|
2733
2845
|
});
|
|
2734
2846
|
const data = await res.json();
|
|
2735
2847
|
if (!res.ok) throw new Error(data.error || 'send failed');
|
|
2848
|
+
appendSystem('已发送, 等待对方回复...', 'info');
|
|
2736
2849
|
} catch (err) {
|
|
2737
|
-
append('(发送失败: ' + (err.message || err) + ')',
|
|
2850
|
+
append('(发送失败: ' + (err.message || err) + ')', 'ai');
|
|
2738
2851
|
} finally {
|
|
2739
2852
|
sendBtn.disabled = false;
|
|
2740
2853
|
sendBtn.textContent = '发送';
|
|
@@ -2744,6 +2857,9 @@ function openRemoteChannelChat(peerPublicKey, channelId, channelName) {
|
|
|
2744
2857
|
inputEl.onkeydown = (e) => { if (e.key === 'Enter') doSend(); };
|
|
2745
2858
|
inputEl.focus();
|
|
2746
2859
|
startV3GlobalSSE();
|
|
2860
|
+
|
|
2861
|
+
// 打开时立即拉历史
|
|
2862
|
+
loadHistory();
|
|
2747
2863
|
}
|
|
2748
2864
|
|
|
2749
2865
|
// Phase 3: 我的 ID 按钮 → 弹窗显示并支持复制自己的 P2PDirect publicKey
|
|
@@ -2817,6 +2933,31 @@ if (addPeerBtn) {
|
|
|
2817
2933
|
});
|
|
2818
2934
|
}
|
|
2819
2935
|
|
|
2936
|
+
// v3 双向刷新: 主动向所有好友发 agent.meta.list, 拿到 ta 们分享给我的 channel
|
|
2937
|
+
const refreshSharedBtn = document.getElementById('refresh-shared-btn');
|
|
2938
|
+
if (refreshSharedBtn) {
|
|
2939
|
+
refreshSharedBtn.addEventListener('click', async (e) => {
|
|
2940
|
+
e.stopPropagation();
|
|
2941
|
+
const originalText = refreshSharedBtn.textContent;
|
|
2942
|
+
refreshSharedBtn.disabled = true;
|
|
2943
|
+
refreshSharedBtn.textContent = '...';
|
|
2944
|
+
try {
|
|
2945
|
+
const res = await fetch('/api/remote-channels/refresh', { method: 'POST' });
|
|
2946
|
+
const data = await res.json();
|
|
2947
|
+
if (!res.ok) throw new Error(data.error || 'refresh failed');
|
|
2948
|
+
// 等 1.5s 让 RPC 回复回来 (向所有 peer 广播)
|
|
2949
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
2950
|
+
await loadRemoteChannels();
|
|
2951
|
+
console.log(`[v3] 双向刷新: 向 ${data.peerCount || 0} 个好友发 list 请求`);
|
|
2952
|
+
} catch (err) {
|
|
2953
|
+
alert('刷新失败: ' + (err.message || err));
|
|
2954
|
+
} finally {
|
|
2955
|
+
refreshSharedBtn.disabled = false;
|
|
2956
|
+
refreshSharedBtn.textContent = originalText;
|
|
2957
|
+
}
|
|
2958
|
+
});
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2820
2961
|
// 启动时拉一次 + 定期轮询 (SSE 接收 P2P reply 后也会更新)
|
|
2821
2962
|
loadRemoteChannels();
|
|
2822
2963
|
setInterval(loadRemoteChannels, 8000);
|
package/dist/web/index.html
CHANGED
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
</span>
|
|
79
79
|
<span style="display:flex;gap:4px;">
|
|
80
80
|
<button id="show-my-p2p-id-btn" title="查看并复制我的 P2P publicKey" style="background:none;border:1px solid var(--border);color:var(--text-secondary);cursor:pointer;padding:2px 6px;border-radius:4px;font-size:11px;line-height:1;">我的 ID</button>
|
|
81
|
+
<button id="refresh-shared-btn" title="向所有好友拉取最新分享列表 (双向: A 拉 B 的, B 拉 A 的)" style="background:none;border:1px solid var(--border);color:var(--text-secondary);cursor:pointer;padding:2px 6px;border-radius:4px;font-size:11px;line-height:1;">↻ 刷新</button>
|
|
81
82
|
<button id="add-p2p-peer-btn" title="添加 P2P 好友 (输入对方的 publicKey)" style="background:none;border:1px solid var(--border);color:var(--text-secondary);cursor:pointer;padding:2px 6px;border-radius:4px;font-size:11px;line-height:1;">+ 好友</button>
|
|
82
83
|
</span>
|
|
83
84
|
</div>
|
package/dist/web/server.js
CHANGED
|
@@ -259,6 +259,8 @@ let sseClients = new Set();
|
|
|
259
259
|
let remoteChannelCache = new Map();
|
|
260
260
|
// v3: P2PDirect 引用 (Hyperswarm 薄包装) - 模块级, 因为 web server 闭包里不可用
|
|
261
261
|
let v3P2PRef = null;
|
|
262
|
+
// v3: 等待中的 history RPC (B 端 chat-history endpoint 用) — rpcId → { resolve, reject }
|
|
263
|
+
const v3PendingHistoryGets = new Map();
|
|
262
264
|
let channelSessions = new Map(); // key: channelId
|
|
263
265
|
let sessionMessages = new Map(); // key: channelId + sessionId
|
|
264
266
|
/**
|
|
@@ -297,6 +299,11 @@ function sanitizeChannelForPeer(ch, peerPublicKey) {
|
|
|
297
299
|
// 🔒 不返回: bound_judgment_ids, walletAddress, walletBinding, autoInvokeTools, sessions, shared_with_peers
|
|
298
300
|
};
|
|
299
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
|
+
}
|
|
300
307
|
/**
|
|
301
308
|
* v3: 处理 Hyperswarm 通道收到的 v3 RPC 消息
|
|
302
309
|
* 设计: 用 HyperswarmCommunicator (DHT topic 自动发现) 取代 iroh 直接 connect
|
|
@@ -382,7 +389,8 @@ async function handleV3P2PMessage(parsed, conn, comm) {
|
|
|
382
389
|
console.warn(`[v3] agent.chat.send 缺少 channelId/text`);
|
|
383
390
|
return;
|
|
384
391
|
}
|
|
385
|
-
|
|
392
|
+
const senderKey = fromPublicKey || peerKey;
|
|
393
|
+
console.log(`[v3] 收到 ${senderKey.substring(0, 12)}... 对 channel ${channelId} 的 chat: "${text.substring(0, 40)}..."`);
|
|
386
394
|
try {
|
|
387
395
|
// 1. 找到 channel
|
|
388
396
|
const channels = await loadChannels();
|
|
@@ -395,20 +403,84 @@ async function handleV3P2PMessage(parsed, conn, comm) {
|
|
|
395
403
|
await comm.sendToConnection(conn.id, reply);
|
|
396
404
|
return;
|
|
397
405
|
}
|
|
398
|
-
//
|
|
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 看到决策依据
|
|
399
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)
|
|
400
444
|
const { getMinimax } = await import('../constraints/index.js');
|
|
401
445
|
const llm = getMinimax();
|
|
402
446
|
const fullPrompt = `${judgmentHint}${text}`;
|
|
403
447
|
let fullResponse = '';
|
|
448
|
+
// v3 新增: 流式 token 节流推给 B — 让 B 看到过程
|
|
449
|
+
let lastFlushAt = 0;
|
|
404
450
|
const streamCallback = (event) => {
|
|
405
|
-
// 流式 token, 不广播给 B (避免半成品噪音), 只记 A 自己的日志
|
|
406
451
|
if (event.type === 'token') {
|
|
407
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
|
+
}
|
|
408
461
|
}
|
|
409
462
|
};
|
|
410
463
|
const agent = await getAgentForChannel(channelId, ch.did || '', ch.name, ch.didDocRef);
|
|
411
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
|
+
}
|
|
412
484
|
// 3. 把完整回复发给 B
|
|
413
485
|
const reply = JSON.stringify({
|
|
414
486
|
v: 3, op: 'agent.chat.reply',
|
|
@@ -419,7 +491,7 @@ async function handleV3P2PMessage(parsed, conn, comm) {
|
|
|
419
491
|
}
|
|
420
492
|
});
|
|
421
493
|
await comm.sendToConnection(conn.id, reply);
|
|
422
|
-
console.log(`[v3] 回 chat.reply 给 ${
|
|
494
|
+
console.log(`[v3] 回 chat.reply 给 ${senderKey.substring(0, 12)}... (${fullResponse.length} chars)`);
|
|
423
495
|
}
|
|
424
496
|
catch (err) {
|
|
425
497
|
console.error(`[v3] agent.chat.send 处理失败:`, err.message);
|
|
@@ -434,6 +506,66 @@ async function handleV3P2PMessage(parsed, conn, comm) {
|
|
|
434
506
|
}
|
|
435
507
|
return;
|
|
436
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
|
+
}
|
|
437
569
|
console.log(`[v3] 收到未知 op: ${op}`);
|
|
438
570
|
}
|
|
439
571
|
async function buildJudgmentHint(channel, channelIdForLog) {
|
|
@@ -477,6 +609,38 @@ async function buildJudgmentHint(channel, channelIdForLog) {
|
|
|
477
609
|
return '';
|
|
478
610
|
}
|
|
479
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
|
+
}
|
|
480
644
|
async function getAgentForChannel(channelId, channelDid, channelName, channelDidDoc) {
|
|
481
645
|
// 获取当前 channel 的 currentSessionId
|
|
482
646
|
const channels = await loadChannels();
|
|
@@ -604,6 +768,45 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
604
768
|
}, 'p2p-global');
|
|
605
769
|
return;
|
|
606
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
|
+
}
|
|
607
810
|
const commShim = {
|
|
608
811
|
sendToConnection: (_id, data) => {
|
|
609
812
|
v3P2PRef.sendTo(evt.fromPublicKey, data);
|
|
@@ -2004,6 +2207,50 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
2004
2207
|
res.status(500).json({ error: err.message });
|
|
2005
2208
|
}
|
|
2006
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
|
+
});
|
|
2007
2254
|
// 获取已连接的节点
|
|
2008
2255
|
app.get('/api/peers', async (_req, res) => {
|
|
2009
2256
|
try {
|
package/package.json
CHANGED
package/src/web/client.js
CHANGED
|
@@ -118,6 +118,8 @@ function startV3GlobalSSE() {
|
|
|
118
118
|
if (msg.type === 'remote-chat-reply') {
|
|
119
119
|
// 找到当前打开的远端 chat modal 的 log
|
|
120
120
|
const log = document.getElementById('rcm-log');
|
|
121
|
+
const thinkingEl = document.getElementById('rcm-thinking');
|
|
122
|
+
if (thinkingEl) thinkingEl.style.display = 'none'; // 思考结束, 隐藏
|
|
121
123
|
if (log) {
|
|
122
124
|
const bubble = document.createElement('div');
|
|
123
125
|
bubble.style.cssText = 'padding:8px 10px;margin:4px 0;border-radius:6px;font-size:13px;line-height:1.4;max-width:80%;word-wrap:break-word;background:#e5e7eb;color:#111;';
|
|
@@ -129,6 +131,44 @@ function startV3GlobalSSE() {
|
|
|
129
131
|
log.appendChild(bubble);
|
|
130
132
|
log.scrollTop = log.scrollHeight;
|
|
131
133
|
}
|
|
134
|
+
} else if (msg.type === 'remote-chat-thinking') {
|
|
135
|
+
// v3 新增: B 端实时显示 A 节点的思考过程
|
|
136
|
+
const phase = msg.phase;
|
|
137
|
+
const log = document.getElementById('rcm-log');
|
|
138
|
+
if (!log) return;
|
|
139
|
+
|
|
140
|
+
if (phase === 'start') {
|
|
141
|
+
// 头部插入"判断力依据"区块 (只第一次)
|
|
142
|
+
const judgments = msg.usedJudgments || { bound: [], candidates: [] };
|
|
143
|
+
const judgmentBlock = document.createElement('div');
|
|
144
|
+
judgmentBlock.className = 'rcm-judgment-block';
|
|
145
|
+
judgmentBlock.style.cssText = 'margin:6px 0;padding:8px 10px;background:#fef3c7;border-left:3px solid #f59e0b;border-radius:4px;font-size:12px;';
|
|
146
|
+
let jh = '<div style="font-weight:600;color:#92400e;margin-bottom:4px;">🛡️ 对方使用的判断力 (来自 ta 的 channel)</div>';
|
|
147
|
+
if (judgments.bound && judgments.bound.length > 0) {
|
|
148
|
+
jh += '<div style="color:#78350f;margin-bottom:4px;"><b>硬绑定</b> (必须遵循):</div>';
|
|
149
|
+
for (const j of judgments.bound) {
|
|
150
|
+
jh += `<div style="margin:2px 0;padding-left:8px;">• <b>${escapeHtml((j.decision || '').slice(0, 80))}</b>${j.reasons && j.reasons.length ? '<br><span style="color:#92400e;font-size:11px;">理由: ' + escapeHtml(j.reasons.join('; ').slice(0, 80)) + '</span>' : ''}</div>`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (judgments.candidates && judgments.candidates.length > 0) {
|
|
154
|
+
jh += `<div style="color:#78350f;margin-top:4px;"><b>候选池</b> (${judgments.candidates.length} 条, LLM 自选)</div>`;
|
|
155
|
+
}
|
|
156
|
+
log.appendChild(judgmentBlock);
|
|
157
|
+
// "思考中" 区块
|
|
158
|
+
const thinkingEl = document.createElement('div');
|
|
159
|
+
thinkingEl.id = 'rcm-thinking-live';
|
|
160
|
+
thinkingEl.style.cssText = 'margin:6px 0;padding:8px 10px;background:#ede9fe;border-left:3px solid #8b5cf6;border-radius:4px;font-size:12px;color:#5b21b6;font-style:italic;';
|
|
161
|
+
thinkingEl.textContent = '💭 对方正在思考...';
|
|
162
|
+
log.appendChild(thinkingEl);
|
|
163
|
+
log.scrollTop = log.scrollHeight;
|
|
164
|
+
} else if (phase === 'token') {
|
|
165
|
+
// 实时更新思考中的 partial
|
|
166
|
+
const thinkingEl = document.getElementById('rcm-thinking-live');
|
|
167
|
+
if (thinkingEl) {
|
|
168
|
+
thinkingEl.textContent = '💭 对方正在思考: ' + (msg.partial || '').slice(-200);
|
|
169
|
+
log.scrollTop = log.scrollHeight;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
132
172
|
}
|
|
133
173
|
} catch (err) {
|
|
134
174
|
console.error('[v3] 全局 SSE 解析失败:', err);
|
|
@@ -2683,15 +2723,19 @@ function openRemoteChannelChat(peerPublicKey, channelId, channelName) {
|
|
|
2683
2723
|
document.getElementById('remote-chat-modal')?.remove();
|
|
2684
2724
|
const html = `
|
|
2685
2725
|
<div id="remote-chat-modal" style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:10002;display:flex;align-items:center;justify-content:center;">
|
|
2686
|
-
<div style="background:#fff;border-radius:8px;width:
|
|
2726
|
+
<div style="background:#fff;border-radius:8px;width:560px;max-width:92vw;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 10px 40px rgba(0,0,0,0.2);">
|
|
2687
2727
|
<div style="padding:12px 16px;border-bottom:1px solid #e5e7eb;display:flex;align-items:center;justify-content:space-between;">
|
|
2688
|
-
<div>
|
|
2728
|
+
<div style="flex:1;min-width:0;">
|
|
2689
2729
|
<div style="font-size:15px;font-weight:600;">跟 ${escapeHtml(channelName)} 聊天</div>
|
|
2690
|
-
<div style="font-size:11px;color:#6b7280;margin-top:2px;">远端 peer: ${escapeHtml(peerPublicKey.substring(0,16))}… ·
|
|
2730
|
+
<div style="font-size:11px;color:#6b7280;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">远端 peer: ${escapeHtml(peerPublicKey.substring(0,16))}… · ${escapeHtml(channelId)}</div>
|
|
2691
2731
|
</div>
|
|
2732
|
+
<button id="rcm-refresh-history" title="重新拉历史" style="background:none;border:1px solid var(--border);color:#6b7280;cursor:pointer;padding:2px 8px;border-radius:4px;font-size:11px;margin-right:6px;">↻ 历史</button>
|
|
2692
2733
|
<button id="rcm-close" style="background:none;border:none;font-size:20px;color:#6b7280;cursor:pointer;">×</button>
|
|
2693
2734
|
</div>
|
|
2694
|
-
<div id="rcm-
|
|
2735
|
+
<div id="rcm-thinking" style="display:none;padding:8px 16px;background:#fef3c7;color:#92400e;font-size:12px;border-bottom:1px solid #e5e7eb;">
|
|
2736
|
+
📥 正在从远端拉历史 + 判断力…
|
|
2737
|
+
</div>
|
|
2738
|
+
<div id="rcm-log" style="flex:1;overflow-y:auto;padding:12px 16px;min-height:240px;max-height:60vh;background:#f9fafb;"></div>
|
|
2695
2739
|
<div style="padding:10px 12px;border-top:1px solid #e5e7eb;display:flex;gap:6px;">
|
|
2696
2740
|
<input id="rcm-input" type="text" placeholder="输入消息, 发送到远端 channel..."
|
|
2697
2741
|
style="flex:1;padding:8px 10px;border:1px solid #d1d5db;border-radius:4px;font-size:13px;">
|
|
@@ -2705,10 +2749,13 @@ function openRemoteChannelChat(peerPublicKey, channelId, channelName) {
|
|
|
2705
2749
|
const log = document.getElementById('rcm-log');
|
|
2706
2750
|
const inputEl = document.getElementById('rcm-input');
|
|
2707
2751
|
const sendBtn = document.getElementById('rcm-send');
|
|
2752
|
+
const thinkingEl = document.getElementById('rcm-thinking');
|
|
2708
2753
|
document.getElementById('rcm-close').onclick = () => document.getElementById('remote-chat-modal').remove();
|
|
2754
|
+
document.getElementById('rcm-refresh-history').onclick = () => loadHistory();
|
|
2709
2755
|
|
|
2710
|
-
const append = (text,
|
|
2756
|
+
const append = (text, role) => {
|
|
2711
2757
|
const bubble = document.createElement('div');
|
|
2758
|
+
const isUser = role === 'user';
|
|
2712
2759
|
bubble.style.cssText = `padding:8px 10px;margin:4px 0;border-radius:6px;font-size:13px;line-height:1.4;max-width:80%;word-wrap:break-word;${
|
|
2713
2760
|
isUser ? 'background:#2563eb;color:#fff;margin-left:auto;text-align:left;'
|
|
2714
2761
|
: 'background:#e5e7eb;color:#111;'
|
|
@@ -2718,10 +2765,75 @@ function openRemoteChannelChat(peerPublicKey, channelId, channelName) {
|
|
|
2718
2765
|
log.scrollTop = log.scrollHeight;
|
|
2719
2766
|
};
|
|
2720
2767
|
|
|
2768
|
+
const appendSystem = (text, kind = 'info') => {
|
|
2769
|
+
const el = document.createElement('div');
|
|
2770
|
+
const colors = {
|
|
2771
|
+
info: { bg: '#dbeafe', fg: '#1e40af' },
|
|
2772
|
+
warn: { bg: '#fef3c7', fg: '#92400e' },
|
|
2773
|
+
error: { bg: '#fca5a5', fg: '#7f1d1d' }
|
|
2774
|
+
};
|
|
2775
|
+
const c = colors[kind] || colors.info;
|
|
2776
|
+
el.style.cssText = `margin:6px 0;padding:6px 10px;background:${c.bg};color:${c.fg};border-radius:4px;font-size:11px;text-align:center;`;
|
|
2777
|
+
el.textContent = text;
|
|
2778
|
+
log.appendChild(el);
|
|
2779
|
+
log.scrollTop = log.scrollHeight;
|
|
2780
|
+
};
|
|
2781
|
+
|
|
2782
|
+
// v3 新增: 拉 A 端的 channel 历史 (含 messages + judgments)
|
|
2783
|
+
async function loadHistory() {
|
|
2784
|
+
thinkingEl.style.display = 'block';
|
|
2785
|
+
log.innerHTML = '';
|
|
2786
|
+
appendSystem('正在拉取远端 channel 的历史和判断力...', 'info');
|
|
2787
|
+
try {
|
|
2788
|
+
const res = await fetch(`/api/remote-channels/chat-history?targetPublicKey=${encodeURIComponent(peerPublicKey)}&channelId=${encodeURIComponent(channelId)}`);
|
|
2789
|
+
const data = await res.json();
|
|
2790
|
+
if (!res.ok) {
|
|
2791
|
+
appendSystem(`拉取失败: ${data.error || 'unknown'}`, 'error');
|
|
2792
|
+
thinkingEl.style.display = 'none';
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
// 清掉 loading
|
|
2796
|
+
log.innerHTML = '';
|
|
2797
|
+
|
|
2798
|
+
// 1. 显示 judgment 依据 (header)
|
|
2799
|
+
const judgments = data.judgments || { bound: [], candidates: [] };
|
|
2800
|
+
if (judgments.bound && judgments.bound.length > 0) {
|
|
2801
|
+
const jh = document.createElement('div');
|
|
2802
|
+
jh.style.cssText = 'margin:0 0 8px;padding:8px 10px;background:#fef3c7;border-left:3px solid #f59e0b;border-radius:4px;font-size:12px;';
|
|
2803
|
+
let h = `<div style="font-weight:600;color:#92400e;margin-bottom:4px;">🛡️ 对方 channel 绑定的判断力 (${judgments.bound.length} 条硬约束)</div>`;
|
|
2804
|
+
for (const j of judgments.bound) {
|
|
2805
|
+
h += `<div style="margin:3px 0;padding-left:8px;">• <b>${escapeHtml((j.decision || '').slice(0, 100))}</b>${j.domain ? `<span style="color:#a16207;font-size:10px;"> [${escapeHtml(j.domain)}${j.stakes ? '/' + escapeHtml(j.stakes) : ''}]</span>` : ''}${j.reasons && j.reasons.length ? '<br><span style="color:#92400e;font-size:11px;">理由: ' + escapeHtml(j.reasons.join('; ').slice(0, 100)) + '</span>' : ''}</div>`;
|
|
2806
|
+
}
|
|
2807
|
+
if (judgments.candidates && judgments.candidates.length > 0) {
|
|
2808
|
+
h += `<div style="margin-top:6px;color:#92400e;font-size:11px;">+ ${judgments.candidates.length} 条候选判断力 (LLM 可自选参考)</div>`;
|
|
2809
|
+
}
|
|
2810
|
+
jh.innerHTML = h;
|
|
2811
|
+
log.appendChild(jh);
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
// 2. 显示历史 messages
|
|
2815
|
+
const msgs = data.messages || [];
|
|
2816
|
+
if (msgs.length === 0) {
|
|
2817
|
+
appendSystem('还没有历史消息, 在下面发第一条吧', 'info');
|
|
2818
|
+
} else {
|
|
2819
|
+
appendSystem(`从远端拉到 ${msgs.length} 条历史消息`, 'info');
|
|
2820
|
+
for (const m of msgs) {
|
|
2821
|
+
append(m.content || '', m.type === 'user' ? 'user' : 'ai');
|
|
2822
|
+
}
|
|
2823
|
+
// 滚到底部
|
|
2824
|
+
setTimeout(() => { log.scrollTop = log.scrollHeight; }, 50);
|
|
2825
|
+
}
|
|
2826
|
+
} catch (err) {
|
|
2827
|
+
appendSystem(`拉取异常: ${err.message}`, 'error');
|
|
2828
|
+
} finally {
|
|
2829
|
+
thinkingEl.style.display = 'none';
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2721
2833
|
const doSend = async () => {
|
|
2722
2834
|
const text = inputEl.value.trim();
|
|
2723
2835
|
if (!text) return;
|
|
2724
|
-
append(text,
|
|
2836
|
+
append(text, 'user');
|
|
2725
2837
|
inputEl.value = '';
|
|
2726
2838
|
sendBtn.disabled = true;
|
|
2727
2839
|
sendBtn.textContent = '...';
|
|
@@ -2733,8 +2845,9 @@ function openRemoteChannelChat(peerPublicKey, channelId, channelName) {
|
|
|
2733
2845
|
});
|
|
2734
2846
|
const data = await res.json();
|
|
2735
2847
|
if (!res.ok) throw new Error(data.error || 'send failed');
|
|
2848
|
+
appendSystem('已发送, 等待对方回复...', 'info');
|
|
2736
2849
|
} catch (err) {
|
|
2737
|
-
append('(发送失败: ' + (err.message || err) + ')',
|
|
2850
|
+
append('(发送失败: ' + (err.message || err) + ')', 'ai');
|
|
2738
2851
|
} finally {
|
|
2739
2852
|
sendBtn.disabled = false;
|
|
2740
2853
|
sendBtn.textContent = '发送';
|
|
@@ -2744,6 +2857,9 @@ function openRemoteChannelChat(peerPublicKey, channelId, channelName) {
|
|
|
2744
2857
|
inputEl.onkeydown = (e) => { if (e.key === 'Enter') doSend(); };
|
|
2745
2858
|
inputEl.focus();
|
|
2746
2859
|
startV3GlobalSSE();
|
|
2860
|
+
|
|
2861
|
+
// 打开时立即拉历史
|
|
2862
|
+
loadHistory();
|
|
2747
2863
|
}
|
|
2748
2864
|
|
|
2749
2865
|
// Phase 3: 我的 ID 按钮 → 弹窗显示并支持复制自己的 P2PDirect publicKey
|
|
@@ -2817,6 +2933,31 @@ if (addPeerBtn) {
|
|
|
2817
2933
|
});
|
|
2818
2934
|
}
|
|
2819
2935
|
|
|
2936
|
+
// v3 双向刷新: 主动向所有好友发 agent.meta.list, 拿到 ta 们分享给我的 channel
|
|
2937
|
+
const refreshSharedBtn = document.getElementById('refresh-shared-btn');
|
|
2938
|
+
if (refreshSharedBtn) {
|
|
2939
|
+
refreshSharedBtn.addEventListener('click', async (e) => {
|
|
2940
|
+
e.stopPropagation();
|
|
2941
|
+
const originalText = refreshSharedBtn.textContent;
|
|
2942
|
+
refreshSharedBtn.disabled = true;
|
|
2943
|
+
refreshSharedBtn.textContent = '...';
|
|
2944
|
+
try {
|
|
2945
|
+
const res = await fetch('/api/remote-channels/refresh', { method: 'POST' });
|
|
2946
|
+
const data = await res.json();
|
|
2947
|
+
if (!res.ok) throw new Error(data.error || 'refresh failed');
|
|
2948
|
+
// 等 1.5s 让 RPC 回复回来 (向所有 peer 广播)
|
|
2949
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
2950
|
+
await loadRemoteChannels();
|
|
2951
|
+
console.log(`[v3] 双向刷新: 向 ${data.peerCount || 0} 个好友发 list 请求`);
|
|
2952
|
+
} catch (err) {
|
|
2953
|
+
alert('刷新失败: ' + (err.message || err));
|
|
2954
|
+
} finally {
|
|
2955
|
+
refreshSharedBtn.disabled = false;
|
|
2956
|
+
refreshSharedBtn.textContent = originalText;
|
|
2957
|
+
}
|
|
2958
|
+
});
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2820
2961
|
// 启动时拉一次 + 定期轮询 (SSE 接收 P2P reply 后也会更新)
|
|
2821
2962
|
loadRemoteChannels();
|
|
2822
2963
|
setInterval(loadRemoteChannels, 8000);
|
package/src/web/index.html
CHANGED
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
</span>
|
|
79
79
|
<span style="display:flex;gap:4px;">
|
|
80
80
|
<button id="show-my-p2p-id-btn" title="查看并复制我的 P2P publicKey" style="background:none;border:1px solid var(--border);color:var(--text-secondary);cursor:pointer;padding:2px 6px;border-radius:4px;font-size:11px;line-height:1;">我的 ID</button>
|
|
81
|
+
<button id="refresh-shared-btn" title="向所有好友拉取最新分享列表 (双向: A 拉 B 的, B 拉 A 的)" style="background:none;border:1px solid var(--border);color:var(--text-secondary);cursor:pointer;padding:2px 6px;border-radius:4px;font-size:11px;line-height:1;">↻ 刷新</button>
|
|
81
82
|
<button id="add-p2p-peer-btn" title="添加 P2P 好友 (输入对方的 publicKey)" style="background:none;border:1px solid var(--border);color:var(--text-secondary);cursor:pointer;padding:2px 6px;border-radius:4px;font-size:11px;line-height:1;">+ 好友</button>
|
|
82
83
|
</span>
|
|
83
84
|
</div>
|
package/src/web/server.ts
CHANGED
|
@@ -391,6 +391,8 @@ let sseClients: Set<SSEClient> = new Set();
|
|
|
391
391
|
let remoteChannelCache: Map<string, Array<Record<string, unknown>>> = new Map();
|
|
392
392
|
// v3: P2PDirect 引用 (Hyperswarm 薄包装) - 模块级, 因为 web server 闭包里不可用
|
|
393
393
|
let v3P2PRef: import('../network/p2p-direct.js').P2PDirect | null = null;
|
|
394
|
+
// v3: 等待中的 history RPC (B 端 chat-history endpoint 用) — rpcId → { resolve, reject }
|
|
395
|
+
const v3PendingHistoryGets: Map<string, { resolve: (data: any) => void; reject: (err: Error) => void }> = new Map();
|
|
394
396
|
let channelSessions: Map<string, AgentSession> = new Map(); // key: channelId
|
|
395
397
|
let sessionMessages: Map<string, any[]> = new Map(); // key: channelId + sessionId
|
|
396
398
|
|
|
@@ -435,6 +437,12 @@ function sanitizeChannelForPeer(
|
|
|
435
437
|
};
|
|
436
438
|
}
|
|
437
439
|
|
|
440
|
+
/** v3 新增: 判断 channel 是否分享给 peerPublicKey */
|
|
441
|
+
function isSharedWith(ch: Channel, peerPublicKey: string): boolean {
|
|
442
|
+
const shared = Array.isArray(ch.shared_with_peers) ? ch.shared_with_peers : [];
|
|
443
|
+
return shared.includes(peerPublicKey);
|
|
444
|
+
}
|
|
445
|
+
|
|
438
446
|
/**
|
|
439
447
|
* v3: 处理 Hyperswarm 通道收到的 v3 RPC 消息
|
|
440
448
|
* 设计: 用 HyperswarmCommunicator (DHT topic 自动发现) 取代 iroh 直接 connect
|
|
@@ -521,7 +529,8 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
|
|
|
521
529
|
console.warn(`[v3] agent.chat.send 缺少 channelId/text`);
|
|
522
530
|
return;
|
|
523
531
|
}
|
|
524
|
-
|
|
532
|
+
const senderKey = fromPublicKey || peerKey;
|
|
533
|
+
console.log(`[v3] 收到 ${senderKey.substring(0,12)}... 对 channel ${channelId} 的 chat: "${text.substring(0, 40)}..."`);
|
|
525
534
|
try {
|
|
526
535
|
// 1. 找到 channel
|
|
527
536
|
const channels = await loadChannels();
|
|
@@ -534,20 +543,85 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
|
|
|
534
543
|
await comm.sendToConnection(conn.id, reply);
|
|
535
544
|
return;
|
|
536
545
|
}
|
|
537
|
-
//
|
|
546
|
+
// v3 新增: 持久化 B 的 user 消息到 A 的 session — 让历史可拉
|
|
547
|
+
try {
|
|
548
|
+
const existing = await loadSession(channelId, 'default');
|
|
549
|
+
const session: Session = existing || {
|
|
550
|
+
channelId, sessionId: 'default', messages: [], lastUpdated: new Date().toISOString()
|
|
551
|
+
};
|
|
552
|
+
session.messages.push({
|
|
553
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
554
|
+
type: 'user',
|
|
555
|
+
content: text,
|
|
556
|
+
timestamp: new Date().toISOString()
|
|
557
|
+
});
|
|
558
|
+
session.lastUpdated = new Date().toISOString();
|
|
559
|
+
await saveSession(session);
|
|
560
|
+
console.log(`[v3] (${channelId}) 存 user 消息 (${text.length} chars) 到 A 的 session`);
|
|
561
|
+
} catch (saveErr) {
|
|
562
|
+
console.warn(`[v3] 存 user 消息失败 (不影响 chat):`, (saveErr as Error).message);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// v3 新增: 告诉 B "我开始想了, 用了哪些 judgment" — 让 B 看到决策依据
|
|
538
566
|
const judgmentHint = await buildJudgmentHint(ch, channelId);
|
|
567
|
+
const usedJudgments = await extractJudgmentsFromHint(ch);
|
|
568
|
+
try {
|
|
569
|
+
const thinkingStart = JSON.stringify({
|
|
570
|
+
v: 3, op: 'agent.chat.thinking',
|
|
571
|
+
payload: {
|
|
572
|
+
channelId,
|
|
573
|
+
phase: 'start',
|
|
574
|
+
fromPublicKey: v3P2PRef?.getPublicKey() || '',
|
|
575
|
+
hint: judgmentHint,
|
|
576
|
+
usedJudgments,
|
|
577
|
+
userText: text
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
await comm.sendToConnection(conn.id, thinkingStart);
|
|
581
|
+
} catch {}
|
|
582
|
+
|
|
583
|
+
// 2. 跑 LLM (复用 Phase 1 的 buildJudgmentHint — 注入 channel 的 judgment)
|
|
539
584
|
const { getMinimax } = await import('../constraints/index.js');
|
|
540
585
|
const llm = getMinimax();
|
|
541
586
|
const fullPrompt = `${judgmentHint}${text}`;
|
|
542
587
|
let fullResponse = '';
|
|
588
|
+
// v3 新增: 流式 token 节流推给 B — 让 B 看到过程
|
|
589
|
+
let lastFlushAt = 0;
|
|
543
590
|
const streamCallback: any = (event: any) => {
|
|
544
|
-
// 流式 token, 不广播给 B (避免半成品噪音), 只记 A 自己的日志
|
|
545
591
|
if (event.type === 'token') {
|
|
546
592
|
fullResponse += event.content;
|
|
593
|
+
if (fullResponse.length - lastFlushAt >= 20) {
|
|
594
|
+
lastFlushAt = fullResponse.length;
|
|
595
|
+
const msg = JSON.stringify({
|
|
596
|
+
v: 3, op: 'agent.chat.thinking',
|
|
597
|
+
payload: { channelId, phase: 'token', partial: fullResponse, fromPublicKey: v3P2PRef?.getPublicKey() || '' }
|
|
598
|
+
});
|
|
599
|
+
comm.sendToConnection(conn.id, msg).catch(() => {});
|
|
600
|
+
}
|
|
547
601
|
}
|
|
548
602
|
};
|
|
549
603
|
const agent = await getAgentForChannel(channelId, ch.did || '', ch.name, ch.didDocRef);
|
|
550
604
|
fullResponse = await agent.promptStream(fullPrompt, streamCallback);
|
|
605
|
+
|
|
606
|
+
// v3 新增: 存 A 的 assistant 消息到 session — B 拉历史时能看到完整对话
|
|
607
|
+
try {
|
|
608
|
+
const existing = await loadSession(channelId, 'default');
|
|
609
|
+
const session: Session = existing || {
|
|
610
|
+
channelId, sessionId: 'default', messages: [], lastUpdated: new Date().toISOString()
|
|
611
|
+
};
|
|
612
|
+
session.messages.push({
|
|
613
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
614
|
+
type: 'ai',
|
|
615
|
+
content: fullResponse,
|
|
616
|
+
timestamp: new Date().toISOString()
|
|
617
|
+
});
|
|
618
|
+
session.lastUpdated = new Date().toISOString();
|
|
619
|
+
await saveSession(session);
|
|
620
|
+
console.log(`[v3] (${channelId}) 存 assistant 回复 (${fullResponse.length} chars) 到 A 的 session`);
|
|
621
|
+
} catch (saveErr) {
|
|
622
|
+
console.warn(`[v3] 存 assistant 消息失败 (不影响):`, (saveErr as Error).message);
|
|
623
|
+
}
|
|
624
|
+
|
|
551
625
|
// 3. 把完整回复发给 B
|
|
552
626
|
const reply = JSON.stringify({
|
|
553
627
|
v: 3, op: 'agent.chat.reply',
|
|
@@ -558,7 +632,7 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
|
|
|
558
632
|
}
|
|
559
633
|
});
|
|
560
634
|
await comm.sendToConnection(conn.id, reply);
|
|
561
|
-
console.log(`[v3] 回 chat.reply 给 ${
|
|
635
|
+
console.log(`[v3] 回 chat.reply 给 ${senderKey.substring(0,12)}... (${fullResponse.length} chars)`);
|
|
562
636
|
} catch (err) {
|
|
563
637
|
console.error(`[v3] agent.chat.send 处理失败:`, (err as Error).message);
|
|
564
638
|
try {
|
|
@@ -572,6 +646,65 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
|
|
|
572
646
|
return;
|
|
573
647
|
}
|
|
574
648
|
|
|
649
|
+
if (op === 'agent.history.get') {
|
|
650
|
+
// v3 新增: B 拉 A 的 channel 历史 (含所有 message + judgment hint)
|
|
651
|
+
// 共享过滤: 只返回 B 可见的 channel + 包含的 judgment
|
|
652
|
+
const { channelId, rpcId, fromPublicKey } = parsed.payload || {};
|
|
653
|
+
if (!channelId || !rpcId) {
|
|
654
|
+
console.warn(`[v3] agent.history.get 缺少 channelId/rpcId`);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
const channels = await loadChannels();
|
|
659
|
+
const ch = channels.find(c => c.id === channelId);
|
|
660
|
+
if (!ch) {
|
|
661
|
+
const err = JSON.stringify({
|
|
662
|
+
v: 3, op: 'agent.history.get.reply',
|
|
663
|
+
payload: { rpcId, error: 'channel not found', messages: [], judgments: { bound: [], candidates: [] } }
|
|
664
|
+
});
|
|
665
|
+
await comm.sendToConnection(conn.id, err);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
// 共享过滤: 必须 peerKey 在 shared_with_peers 里 (避免泄露未分享的 channel)
|
|
669
|
+
const peerKey = fromPublicKey;
|
|
670
|
+
if (!peerKey || !isSharedWith(ch, peerKey)) {
|
|
671
|
+
const err = JSON.stringify({
|
|
672
|
+
v: 3, op: 'agent.history.get.reply',
|
|
673
|
+
payload: { rpcId, error: 'channel not shared with you', messages: [], judgments: { bound: [], candidates: [] } }
|
|
674
|
+
});
|
|
675
|
+
await comm.sendToConnection(conn.id, err);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
// 加载 A 端 session
|
|
679
|
+
const session = await loadSession(channelId, 'default');
|
|
680
|
+
// 加载 channel 用到的 judgment
|
|
681
|
+
const judgments = await extractJudgmentsFromHint(ch);
|
|
682
|
+
const reply = JSON.stringify({
|
|
683
|
+
v: 3, op: 'agent.history.get.reply',
|
|
684
|
+
payload: {
|
|
685
|
+
rpcId,
|
|
686
|
+
channelId,
|
|
687
|
+
messages: session?.messages || [],
|
|
688
|
+
lastUpdated: session?.lastUpdated,
|
|
689
|
+
judgments,
|
|
690
|
+
channelName: ch.name
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
await comm.sendToConnection(conn.id, reply);
|
|
694
|
+
console.log(`[v3] 回 history.reply 给 ${peerKey.substring(0,12)}... (channelId=${channelId}, ${session?.messages?.length || 0} messages)`);
|
|
695
|
+
} catch (err) {
|
|
696
|
+
console.error(`[v3] agent.history.get 处理失败:`, (err as Error).message);
|
|
697
|
+
try {
|
|
698
|
+
const errMsg = JSON.stringify({
|
|
699
|
+
v: 3, op: 'agent.history.get.reply',
|
|
700
|
+
payload: { rpcId, error: (err as Error).message, messages: [], judgments: { bound: [], candidates: [] } }
|
|
701
|
+
});
|
|
702
|
+
await comm.sendToConnection(conn.id, errMsg);
|
|
703
|
+
} catch {}
|
|
704
|
+
}
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
575
708
|
console.log(`[v3] 收到未知 op: ${op}`);
|
|
576
709
|
}
|
|
577
710
|
|
|
@@ -629,6 +762,47 @@ async function buildJudgmentHint(
|
|
|
629
762
|
}
|
|
630
763
|
}
|
|
631
764
|
|
|
765
|
+
/**
|
|
766
|
+
* v3 新增: 把 channel 当前用到的 judgment 提取成结构化数据, 给 B 端 UI 显示.
|
|
767
|
+
* 返回 { bound: [...], candidates: [...] } — bound 是硬绑定, candidates 是参考池.
|
|
768
|
+
*/
|
|
769
|
+
async function extractJudgmentsFromHint(
|
|
770
|
+
channel: Channel | undefined | null
|
|
771
|
+
): Promise<{ bound: any[]; candidates: any[] }> {
|
|
772
|
+
try {
|
|
773
|
+
const { loadAllJudgments, initializeValueStore } = await import(
|
|
774
|
+
'../pi-ecosystem-judgment/human-value-store.js'
|
|
775
|
+
);
|
|
776
|
+
await initializeValueStore();
|
|
777
|
+
const allJudgments = await loadAllJudgments();
|
|
778
|
+
if (allJudgments.length === 0) return { bound: [], candidates: [] };
|
|
779
|
+
|
|
780
|
+
const boundIds = new Set(
|
|
781
|
+
channel && Array.isArray(channel.bound_judgment_ids) ? channel.bound_judgment_ids : []
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
const summarize = (j: any) => ({
|
|
785
|
+
id: j.id,
|
|
786
|
+
decision: (j.decision || '').toString().slice(0, 200),
|
|
787
|
+
reasons: Array.isArray(j.reasons) ? j.reasons : [],
|
|
788
|
+
domain: j.domain,
|
|
789
|
+
stakes: j.stakes
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
const bound = allJudgments
|
|
793
|
+
.filter((j: any) => j.id !== undefined && boundIds.has(j.id))
|
|
794
|
+
.map(summarize);
|
|
795
|
+
const candidates = allJudgments
|
|
796
|
+
.filter((j: any) => j.id !== undefined && !boundIds.has(j.id))
|
|
797
|
+
.map(summarize);
|
|
798
|
+
|
|
799
|
+
return { bound, candidates };
|
|
800
|
+
} catch (err) {
|
|
801
|
+
console.warn(`[v3] extractJudgmentsFromHint 失败:`, (err as Error).message);
|
|
802
|
+
return { bound: [], candidates: [] };
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
632
806
|
async function getAgentForChannel(
|
|
633
807
|
channelId: string,
|
|
634
808
|
channelDid?: string,
|
|
@@ -787,6 +961,44 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
|
|
|
787
961
|
}, 'p2p-global');
|
|
788
962
|
return;
|
|
789
963
|
}
|
|
964
|
+
// v3 新增: B 端收到 A 的 thinking (开始 + 流式 token)
|
|
965
|
+
if (parsed.op === 'agent.chat.thinking') {
|
|
966
|
+
const phase = parsed.payload?.phase;
|
|
967
|
+
if (phase === 'start') {
|
|
968
|
+
console.log(`[v3] 收到来自 ${evt.fromPublicKey.substring(0,12)}... 的 thinking start (judgments: bound=${(parsed.payload?.usedJudgments?.bound || []).length}, candidates=${(parsed.payload?.usedJudgments?.candidates || []).length})`);
|
|
969
|
+
}
|
|
970
|
+
broadcast({
|
|
971
|
+
type: 'remote-chat-thinking',
|
|
972
|
+
fromPublicKey: evt.fromPublicKey,
|
|
973
|
+
channelId: parsed.payload?.channelId,
|
|
974
|
+
phase: parsed.payload?.phase,
|
|
975
|
+
partial: parsed.payload?.partial,
|
|
976
|
+
hint: parsed.payload?.hint,
|
|
977
|
+
usedJudgments: parsed.payload?.usedJudgments,
|
|
978
|
+
userText: parsed.payload?.userText
|
|
979
|
+
}, 'p2p-global');
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
// v3 新增: B 端收到 A 的 history reply → resolve pending promise
|
|
983
|
+
if (parsed.op === 'agent.history.get.reply') {
|
|
984
|
+
const rpcId = parsed.payload?.rpcId;
|
|
985
|
+
if (rpcId && v3PendingHistoryGets.has(rpcId)) {
|
|
986
|
+
const pending = v3PendingHistoryGets.get(rpcId)!;
|
|
987
|
+
v3PendingHistoryGets.delete(rpcId);
|
|
988
|
+
if (parsed.payload?.error) {
|
|
989
|
+
pending.reject(new Error(parsed.payload.error));
|
|
990
|
+
} else {
|
|
991
|
+
pending.resolve({
|
|
992
|
+
channelId: parsed.payload.channelId,
|
|
993
|
+
messages: parsed.payload.messages || [],
|
|
994
|
+
lastUpdated: parsed.payload.lastUpdated,
|
|
995
|
+
judgments: parsed.payload.judgments || { bound: [], candidates: [] },
|
|
996
|
+
channelName: parsed.payload.channelName
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
790
1002
|
const commShim = {
|
|
791
1003
|
sendToConnection: (_id: string, data: string) => {
|
|
792
1004
|
v3P2PRef!.sendTo(evt.fromPublicKey, data);
|
|
@@ -2265,6 +2477,53 @@ app.get('/channels', async (_req, res) => {
|
|
|
2265
2477
|
}
|
|
2266
2478
|
});
|
|
2267
2479
|
|
|
2480
|
+
// v3 新增: B 拉 A 的 channel 历史 + 用了哪些 judgment
|
|
2481
|
+
// GET /api/remote-channels/chat-history?targetPublicKey=...&channelId=...
|
|
2482
|
+
// 实现: B → POST 给 A 一个 agent.history.get RPC → A 把 session 返回 → B 渲染
|
|
2483
|
+
app.get('/api/remote-channels/chat-history', async (req, res) => {
|
|
2484
|
+
try {
|
|
2485
|
+
if (!v3P2PRef) {
|
|
2486
|
+
return res.status(503).json({ error: 'P2PDirect not started' });
|
|
2487
|
+
}
|
|
2488
|
+
const targetPublicKey = String(req.query.targetPublicKey || '');
|
|
2489
|
+
const channelId = String(req.query.channelId || '');
|
|
2490
|
+
if (!targetPublicKey || !channelId) {
|
|
2491
|
+
return res.status(400).json({ error: 'targetPublicKey, channelId required' });
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
// 通过 RPC 拉 A 的 session — A 端收到后异步回复
|
|
2495
|
+
const fromPk = v3P2PRef.getPublicKey();
|
|
2496
|
+
const rpcId = `hist-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2497
|
+
const msg = JSON.stringify({
|
|
2498
|
+
v: 3,
|
|
2499
|
+
op: 'agent.history.get',
|
|
2500
|
+
payload: { rpcId, channelId, fromPublicKey: fromPk }
|
|
2501
|
+
});
|
|
2502
|
+
const ok = v3P2PRef.sendTo(targetPublicKey, msg);
|
|
2503
|
+
if (!ok) {
|
|
2504
|
+
return res.status(502).json({ error: 'peer not connected' });
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
// 等待 A 异步回复 (15s timeout) — 用一个 Promise 等
|
|
2508
|
+
const result = await new Promise<any>((resolve, reject) => {
|
|
2509
|
+
const timer = setTimeout(() => {
|
|
2510
|
+
v3PendingHistoryGets.delete(rpcId);
|
|
2511
|
+
reject(new Error('A 端 15s 内未回复, 可能未分享该 channel'));
|
|
2512
|
+
}, 15000);
|
|
2513
|
+
v3PendingHistoryGets.set(rpcId, {
|
|
2514
|
+
resolve: (data) => { clearTimeout(timer); resolve(data); },
|
|
2515
|
+
reject: (err) => { clearTimeout(timer); reject(err); }
|
|
2516
|
+
});
|
|
2517
|
+
});
|
|
2518
|
+
|
|
2519
|
+
console.log(`[v3] chat-history 从 ${targetPublicKey.substring(0,12)}... 拉到 ${(result.messages || []).length} 条`);
|
|
2520
|
+
res.json(result);
|
|
2521
|
+
} catch (err: any) {
|
|
2522
|
+
console.error('[v3] chat-history 失败:', err.message);
|
|
2523
|
+
res.status(504).json({ error: err.message });
|
|
2524
|
+
}
|
|
2525
|
+
});
|
|
2526
|
+
|
|
2268
2527
|
// 获取已连接的节点
|
|
2269
2528
|
app.get('/api/peers', async (_req, res) => {
|
|
2270
2529
|
try {
|