@bolloon/bolloon-agent 0.1.24 → 0.1.26

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/src/web/server.ts CHANGED
@@ -72,6 +72,8 @@ interface Channel {
72
72
  updatedAt: string;
73
73
  currentSessionId?: string;
74
74
  sessions?: SessionSummary[];
75
+ /** 用户在盾牌里手动绑定的判断力 (LLM 跑 channel 时会注入). 默认 []. */
76
+ bound_judgment_ids?: string[];
75
77
  }
76
78
 
77
79
  interface SessionSummary {
@@ -380,9 +382,93 @@ interface SSEClient {
380
382
  }
381
383
 
382
384
  let sseClients: Set<SSEClient> = new Set();
385
+ // v3: 远端 channel UI 元数据缓存 — key: peerId, value: sanitize 过的 channel 列表
386
+ // in-memory only, 进程重启清空 (judgment 内容永远不在这里)
387
+ let remoteChannelCache: Map<string, Array<Record<string, unknown>>> = new Map();
383
388
  let channelSessions: Map<string, AgentSession> = new Map(); // key: channelId
384
389
  let sessionMessages: Map<string, any[]> = new Map(); // key: channelId + sessionId
385
390
 
391
+ /**
392
+ * v3 重做: 构造 channel 的两路 judgment prompt 片段
393
+ * 路 1: 用户在盾牌里手动绑定的 judgment (channel.bound_judgment_ids)
394
+ * 路 2: 全局 judgment 列表 (供 LLM 在主调用中按需挑选, 写入回复)
395
+ * 返回 "" 表示完全没数据; 否则返回完整 "[系统上下文] ..." 块 (含尾部换行)
396
+ * 失败非致命 — 任何异常都返回空串, 保证 LLM 调用不被阻塞
397
+ */
398
+
399
+ /**
400
+ * v3: 过滤 channel 元数据, 只返回对远端 peer 安全的字段.
401
+ * 关键: bound_judgment_ids / walletBinding / autoInvokeTools 内部状态不外传.
402
+ * judgment 内容永远不会出现在 RPC 响应里 (judgment 始终在 A 节点内存, 由 A 跑 LLM).
403
+ */
404
+ function sanitizeChannelForPeer(ch: Channel): Record<string, unknown> {
405
+ return {
406
+ id: ch.id,
407
+ name: ch.name,
408
+ did: ch.did,
409
+ publicKey: ch.publicKey,
410
+ createdAt: ch.createdAt,
411
+ updatedAt: ch.updatedAt,
412
+ hasWallet: !!ch.walletAddress, // 只告诉 B "有没有钱包", 不传地址
413
+ boundJudgmentCount: Array.isArray(ch.bound_judgment_ids) ? ch.bound_judgment_ids.length : 0,
414
+ // 🔒 不返回: bound_judgment_ids, walletAddress, walletBinding, autoInvokeTools, sessions
415
+ };
416
+ }
417
+
418
+ async function buildJudgmentHint(
419
+ channel: Channel | undefined | null,
420
+ channelIdForLog: string
421
+ ): Promise<string> {
422
+ try {
423
+ const { loadAllJudgments, initializeValueStore } = await import(
424
+ '../pi-ecosystem-judgment/human-value-store.js'
425
+ );
426
+ await initializeValueStore();
427
+ const allJudgments = await loadAllJudgments();
428
+ if (allJudgments.length === 0) return '';
429
+
430
+ const boundIds = new Set(
431
+ channel && Array.isArray(channel.bound_judgment_ids) ? channel.bound_judgment_ids : []
432
+ );
433
+ const bound = allJudgments.filter(j => j.id !== undefined && boundIds.has(j.id));
434
+ const others = allJudgments.filter(j => j.id !== undefined && !boundIds.has(j.id));
435
+
436
+ let hint = '';
437
+
438
+ // 路 1: 用户手动绑定的 judgment — 硬约束, 必须遵循
439
+ if (bound.length > 0) {
440
+ hint += `[系统上下文] 此 channel 用户绑定了 ${bound.length} 条判断力, 必须严格遵循:\n`;
441
+ for (const j of bound) {
442
+ const decision = (j.decision || '').toString().slice(0, 200);
443
+ const reasonList = Array.isArray(j.reasons) ? j.reasons : [];
444
+ const reasonText = reasonList.length > 0
445
+ ? ` (理由: ${reasonList.join('; ').slice(0, 100)})`
446
+ : '';
447
+ hint += `- ${decision}${reasonText}\n`;
448
+ }
449
+ hint += '\n';
450
+ }
451
+
452
+ // 路 2: 全局 judgment 候选池 — 软参考, LLM 自己挑
453
+ if (others.length > 0) {
454
+ hint += `[系统上下文] 候选判断力 (用户未明确绑定, 你可以按相关性自主选择参考):\n`;
455
+ for (const j of others) {
456
+ const decision = (j.decision || '').toString().slice(0, 120);
457
+ hint += `- [id=${j.id}] ${decision}\n`;
458
+ }
459
+ hint += `\n[系统上下文] 如果你的回复参考了某条候选判断力, 请在回复中自然提及 "我参考了你的判断: <decision 简述>" 即可, 无需复述 id.\n\n`;
460
+ }
461
+
462
+ console.log(
463
+ `[v3] channel ${channelIdForLog} 注入: 绑定 ${bound.length} 条, 候选 ${others.length} 条`
464
+ );
465
+ return hint;
466
+ } catch (err) {
467
+ console.error(`[v3] 加载判断力失败 (非致命):`, (err as Error).message);
468
+ return '';
469
+ }
470
+ }
471
+
386
472
  async function getAgentForChannel(
387
473
  channelId: string,
388
474
  channelDid?: string,
@@ -640,6 +726,8 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
640
726
  // (line ~638) 与这里外层的 const channel 形成 shadowing 让 TS 误报"使用前未声明"
641
727
  const boundWalletAddress = channel?.walletAddress;
642
728
  const autoToolsEnabled = channel?.autoInvokeTools !== false; // 默认开启
729
+ // 捕获外层 channel 到独立变量, 避免被 try 块内 (line 740+) 的 const channel 遮蔽
730
+ const channelForJudgment = channel;
643
731
 
644
732
  try {
645
733
  const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
@@ -675,6 +763,12 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
675
763
  } else {
676
764
  contextHint += `[系统上下文] 自动工具调用已关闭: 每次执行工具前必须先与用户确认。\n`;
677
765
  }
766
+
767
+ // v3: 注入 channel 绑定的判断力 (judgment_ids)
768
+ // 这是 v3 的核心 — channel 跑 LLM 时, 它的判断力 = 绑定的 judgment 列表
769
+ const judgmentHint = await buildJudgmentHint(channelForJudgment, channelId);
770
+ if (judgmentHint) contextHint += judgmentHint;
771
+
678
772
  if (contextHint) contextHint += '\n';
679
773
  fullResponse = await agent.promptStream(contextHint + text, streamCallback);
680
774
 
@@ -833,10 +927,47 @@ app.get('/channels', async (_req, res) => {
833
927
  }
834
928
  });
835
929
 
930
+ // v3: 列出本节点缓存的远端 channel (按 peerId 分组)
931
+ app.get('/api/remote-channels', async (_req, res) => {
932
+ try {
933
+ const out: Array<{ peerId: string; channels: Array<Record<string, unknown>> }> = [];
934
+ for (const [peerId, list] of remoteChannelCache.entries()) {
935
+ out.push({ peerId, channels: list });
936
+ }
937
+ res.json({ count: out.length, peers: out });
938
+ } catch (err: any) {
939
+ res.status(500).json({ error: err.message });
940
+ }
941
+ });
942
+
943
+ // v3: 主动向所有已连接 P2P peer 拉 channel 列表
944
+ // 用法: B 端用户点 "刷新远端智能体" → 触发本 endpoint
945
+ app.post('/api/remote-channels/refresh', async (_req, res) => {
946
+ try {
947
+ const peers = irohTransport.getPeers ? irohTransport.getPeers() : [];
948
+ const peerIds = Array.isArray(peers) ? peers.map((p: any) => p.nodeId || p) : [];
949
+ if (peerIds.length === 0) {
950
+ return res.json({ ok: true, sent: 0, note: 'no connected peers' });
951
+ }
952
+ let sent = 0;
953
+ for (const peerId of peerIds) {
954
+ const ok = await irohTransport.sendMessage(
955
+ peerId,
956
+ 'agent.meta.list',
957
+ new TextEncoder().encode('{}')
958
+ );
959
+ if (ok) sent++;
960
+ }
961
+ res.json({ ok: true, sent, total: peerIds.length });
962
+ } catch (err: any) {
963
+ res.status(500).json({ error: err.message });
964
+ }
965
+ });
966
+
836
967
  app.post('/channels', async (req, res) => {
837
968
  try {
838
- const { name, agentId, walletAddress, autoInvokeTools } = req.body;
839
- console.log(`[创建频道] 收到请求: name=${name}, agentId=${agentId}, wallet=${walletAddress ? 'yes' : 'no'}`);
969
+ const { name, agentId, walletAddress, autoInvokeTools, bound_judgment_ids } = req.body;
970
+ console.log(`[创建频道] 收到请求: name=${name}, agentId=${agentId}, wallet=${walletAddress ? 'yes' : 'no'}, boundJudgments=${Array.isArray(bound_judgment_ids) ? bound_judgment_ids.length : 0}`);
840
971
  if (!name || !agentId) {
841
972
  return res.status(400).json({ error: 'name and agentId required' });
842
973
  }
@@ -846,6 +977,11 @@ app.get('/channels', async (_req, res) => {
846
977
  // 校验钱包地址格式 (粗校验: 0x + 40 hex / Solana base58 / Sui 0x+64)
847
978
  const validWallet = isValidWalletAddress(walletAddress);
848
979
 
980
+ // 过滤 bound_judgment_ids: 只保留 string
981
+ const safeBoundIds = Array.isArray(bound_judgment_ids)
982
+ ? bound_judgment_ids.filter((x: unknown) => typeof x === 'string' && (x as string).length > 0)
983
+ : [];
984
+
849
985
  // 先创建频道(不阻塞等待 DID 生成)
850
986
  const channel: Channel = {
851
987
  id,
@@ -857,6 +993,7 @@ app.get('/channels', async (_req, res) => {
857
993
  walletAddress: validWallet || undefined,
858
994
  walletRegisteredAt: validWallet ? new Date().toISOString() : undefined,
859
995
  autoInvokeTools: autoInvokeTools !== false, // 默认 true
996
+ bound_judgment_ids: safeBoundIds,
860
997
  sessions: [{
861
998
  id: `sess_${Date.now()}`,
862
999
  createdAt: new Date().toISOString(),
@@ -1035,7 +1172,7 @@ app.get('/channels', async (_req, res) => {
1035
1172
  app.patch('/channels/:channelId', async (req, res) => {
1036
1173
  try {
1037
1174
  const { channelId } = req.params;
1038
- const { name, walletAddress, autoInvokeTools } = req.body;
1175
+ const { name, walletAddress, autoInvokeTools, bound_judgment_ids } = req.body;
1039
1176
  const channels = await loadChannels();
1040
1177
  const channel = channels.find(c => c.id === channelId);
1041
1178
  if (!channel) {
@@ -1061,6 +1198,19 @@ app.get('/channels', async (_req, res) => {
1061
1198
  if (typeof autoInvokeTools === 'boolean') {
1062
1199
  channel.autoInvokeTools = autoInvokeTools;
1063
1200
  }
1201
+ // bound_judgment_ids: 允许数组(替换)/null(清空)/undefined(不改)
1202
+ if (bound_judgment_ids !== undefined) {
1203
+ if (bound_judgment_ids === null) {
1204
+ channel.bound_judgment_ids = [];
1205
+ } else if (Array.isArray(bound_judgment_ids)) {
1206
+ channel.bound_judgment_ids = bound_judgment_ids.filter(
1207
+ (x: unknown) => typeof x === 'string' && (x as string).length > 0
1208
+ );
1209
+ } else {
1210
+ return res.status(400).json({ error: 'bound_judgment_ids must be array or null' });
1211
+ }
1212
+ console.log(`[Channel ${channelId}] 绑定判断力: ${channel.bound_judgment_ids.length} 条`);
1213
+ }
1064
1214
  channel.updatedAt = new Date().toISOString();
1065
1215
  await saveChannels(channels);
1066
1216
  res.json(channel);
@@ -1225,8 +1375,9 @@ app.get('/channels', async (_req, res) => {
1225
1375
  }
1226
1376
  };
1227
1377
 
1228
- // 重新生成时只发送用户消息
1229
- fullResponse = await agent.promptStream(userMessage, streamCallback);
1378
+ // 重新生成时只发送用户消息 (v3: 同时注入 channel 绑定的判断力)
1379
+ const regenHint = await buildJudgmentHint(channel, channelId);
1380
+ fullResponse = await agent.promptStream(regenHint + userMessage, streamCallback);
1230
1381
 
1231
1382
  broadcast({ type: 'ai', content: fullResponse }, channelId);
1232
1383
 
@@ -1767,6 +1918,88 @@ app.get('/channels', async (_req, res) => {
1767
1918
  }, 'p2p-global');
1768
1919
  });
1769
1920
 
1921
+ // ============ v3: 跨用户 channel 元数据 RPC ============
1922
+ // 设计原则: judgment / bound_judgment_ids / wallet 等敏感字段绝不出现在 RPC 响应里.
1923
+ // 收到 'agent.meta.list' → 返回本节点所有 channel 的 UI 元数据 (无 judgment)
1924
+ // 收到 'agent.meta.get' + channelId → 返回单条 channel 的 UI 元数据
1925
+ // B 节点收到响应 → 存到远端 cache → 渲染到 "远端智能体" 区域
1926
+
1927
+ // B 侧: 收到对端的 list/get 回复 → 更新远端 cache → SSE 推给前端
1928
+ irohTransport.onMessage('agent.meta.list.reply', (msg) => {
1929
+ try {
1930
+ const data = JSON.parse(new TextDecoder().decode(msg.payload));
1931
+ if (!data.ok) return;
1932
+ const peerId = msg.from;
1933
+ const list = Array.isArray(data.channels) ? data.channels : [];
1934
+ remoteChannelCache.set(peerId, list);
1935
+ console.log(`[v3] 缓存远端 peer ${peerId.substring(0, 12)}... 的 ${list.length} 个 channel`);
1936
+ broadcast({
1937
+ type: 'remote-channel-update',
1938
+ peerId,
1939
+ channels: list
1940
+ }, 'p2p-global');
1941
+ } catch (err) {
1942
+ console.error('[v3] 处理 agent.meta.list.reply 失败:', err);
1943
+ }
1944
+ });
1945
+
1946
+ irohTransport.onMessage('agent.meta.get.reply', (msg) => {
1947
+ try {
1948
+ const data = JSON.parse(new TextDecoder().decode(msg.payload));
1949
+ if (!data.ok || !data.channel) return;
1950
+ const peerId = msg.from;
1951
+ const ch = data.channel;
1952
+ const list = remoteChannelCache.get(peerId) || [];
1953
+ const idx = list.findIndex(c => c.id === ch.id);
1954
+ if (idx >= 0) list[idx] = ch;
1955
+ else list.push(ch);
1956
+ remoteChannelCache.set(peerId, list);
1957
+ broadcast({
1958
+ type: 'remote-channel-update',
1959
+ peerId,
1960
+ channels: list
1961
+ }, 'p2p-global');
1962
+ } catch (err) {
1963
+ console.error('[v3] 处理 agent.meta.get.reply 失败:', err);
1964
+ }
1965
+ });
1966
+
1967
+ // A 侧: 收到对端的 list/get 请求
1968
+ irohTransport.onMessage('agent.meta.list', async (msg) => {
1969
+ console.log(`[v3] 收到 agent.meta.list from ${msg.from.substring(0, 12)}...`);
1970
+ try {
1971
+ const channels = await loadChannels();
1972
+ const publicMeta = channels.map(sanitizeChannelForPeer);
1973
+ const response = JSON.stringify({ ok: true, channels: publicMeta });
1974
+ const encoded = new TextEncoder().encode(response);
1975
+ // 沿用 msg.from 路由回去
1976
+ irohTransport.sendMessage(msg.from, 'agent.meta.list.reply', encoded).catch(err => {
1977
+ console.error('[v3] 发送 agent.meta.list.reply 失败:', err);
1978
+ });
1979
+ } catch (err) {
1980
+ console.error('[v3] 处理 agent.meta.list 失败:', err);
1981
+ }
1982
+ });
1983
+
1984
+ irohTransport.onMessage('agent.meta.get', async (msg) => {
1985
+ try {
1986
+ const req = JSON.parse(new TextDecoder().decode(msg.payload));
1987
+ const channelId = req.channelId;
1988
+ console.log(`[v3] 收到 agent.meta.get for ${channelId} from ${msg.from.substring(0, 12)}...`);
1989
+ const channels = await loadChannels();
1990
+ const ch = channels.find(c => c.id === channelId);
1991
+ if (!ch) {
1992
+ const response = JSON.stringify({ ok: false, error: 'channel not found' });
1993
+ irohTransport.sendMessage(msg.from, 'agent.meta.get.reply', new TextEncoder().encode(response));
1994
+ return;
1995
+ }
1996
+ const response = JSON.stringify({ ok: true, channel: sanitizeChannelForPeer(ch) });
1997
+ irohTransport.sendMessage(msg.from, 'agent.meta.get.reply', new TextEncoder().encode(response));
1998
+ } catch (err) {
1999
+ console.error('[v3] 处理 agent.meta.get 失败:', err);
2000
+ }
2001
+ });
2002
+
1770
2003
  irohTransport.onMessage('ai-dialogue', (msg) => {
1771
2004
  const content = new TextDecoder().decode(msg.payload);
1772
2005
  console.log(`[iroh] 收到 AI 对话 from ${msg.from.substring(0, 12)}...`);
@@ -2451,7 +2684,8 @@ app.get('/channels', async (_req, res) => {
2451
2684
  try {
2452
2685
  const { createHealthMonitor, createWatchdog } = await import('../heartbeat/index.js');
2453
2686
  healthMonitor = createHealthMonitor();
2454
- watchdog = createWatchdog();
2687
+ // 把 watchdog 静默阈值拉到 30 分钟, 避免开发期 / 用户空闲时被误杀
2688
+ watchdog = createWatchdog({ silentThresholdMs: 30 * 60 * 1000 });
2455
2689
 
2456
2690
  console.log('[24h] Heartbeat modules loaded');
2457
2691
  } catch (err) {
package/src/web/style.css CHANGED
@@ -269,6 +269,41 @@ body {
269
269
  padding: 16px;
270
270
  position: relative;
271
271
  }
272
+
273
+ /* v3: 本地/远端 flex 比例由 CSS variable 驱动, 拖拽分隔线时改这两个值 */
274
+ .sidebar-section.local-flex { flex: var(--local-flex, 1) 1 0; }
275
+ .sidebar-section.remote-flex { flex: var(--remote-flex, 1) 1 0; transition: flex 0.15s; }
276
+
277
+ /* v3: 可拖拽分隔线 */
278
+ #sidebar-split-handle {
279
+ flex: 0 0 auto;
280
+ height: 6px;
281
+ cursor: ns-resize;
282
+ display: flex;
283
+ align-items: center;
284
+ justify-content: center;
285
+ position: relative;
286
+ background: transparent;
287
+ user-select: none;
288
+ }
289
+ #sidebar-split-handle:hover { background: var(--bg-hover); }
290
+ #sidebar-split-handle.dragging { background: var(--accent-glow); }
291
+ #sidebar-split-handle .split-handle-grip {
292
+ width: 32px;
293
+ height: 3px;
294
+ border-radius: 2px;
295
+ background: var(--border-light);
296
+ pointer-events: none;
297
+ }
298
+ #sidebar-split-handle:hover .split-handle-grip,
299
+ #sidebar-split-handle.dragging .split-handle-grip { background: var(--accent); }
300
+
301
+ /* v3: 远端区域折叠状态 */
302
+ #remote-agents-section.collapsed .channel-list { display: none; }
303
+ #remote-agents-section.collapsed { flex: 0 0 auto; padding-top: 0; padding-bottom: 0; }
304
+ #remote-agents-section.collapsed .section-header { margin-bottom: 0; }
305
+ #remote-agents-section.collapsed #remote-agents-toggle { transform: rotate(-90deg); }
306
+
272
307
  .section-header {
273
308
  display: flex;
274
309
  align-items: center;