@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.
@@ -254,8 +254,77 @@ async function executeTask(task, channelId) {
254
254
  executionTaskId = null;
255
255
  }
256
256
  let sseClients = new Set();
257
+ // v3: 远端 channel UI 元数据缓存 — key: peerId, value: sanitize 过的 channel 列表
258
+ // in-memory only, 进程重启清空 (judgment 内容永远不在这里)
259
+ let remoteChannelCache = new Map();
257
260
  let channelSessions = new Map(); // key: channelId
258
261
  let sessionMessages = new Map(); // key: channelId + sessionId
262
+ /**
263
+ * v3 重做: 构造 channel 的两路 judgment prompt 片段
264
+ * 路 1: 用户在盾牌里手动绑定的 judgment (channel.bound_judgment_ids)
265
+ * 路 2: 全局 judgment 列表 (供 LLM 在主调用中按需挑选, 写入回复)
266
+ * 返回 "" 表示完全没数据; 否则返回完整 "[系统上下文] ..." 块 (含尾部换行)
267
+ * 失败非致命 — 任何异常都返回空串, 保证 LLM 调用不被阻塞
268
+ */
269
+ /**
270
+ * v3: 过滤 channel 元数据, 只返回对远端 peer 安全的字段.
271
+ * 关键: bound_judgment_ids / walletBinding / autoInvokeTools 内部状态不外传.
272
+ * judgment 内容永远不会出现在 RPC 响应里 (judgment 始终在 A 节点内存, 由 A 跑 LLM).
273
+ */
274
+ function sanitizeChannelForPeer(ch) {
275
+ return {
276
+ id: ch.id,
277
+ name: ch.name,
278
+ did: ch.did,
279
+ publicKey: ch.publicKey,
280
+ createdAt: ch.createdAt,
281
+ updatedAt: ch.updatedAt,
282
+ hasWallet: !!ch.walletAddress, // 只告诉 B "有没有钱包", 不传地址
283
+ boundJudgmentCount: Array.isArray(ch.bound_judgment_ids) ? ch.bound_judgment_ids.length : 0,
284
+ // 🔒 不返回: bound_judgment_ids, walletAddress, walletBinding, autoInvokeTools, sessions
285
+ };
286
+ }
287
+ async function buildJudgmentHint(channel, channelIdForLog) {
288
+ try {
289
+ const { loadAllJudgments, initializeValueStore } = await import('../pi-ecosystem-judgment/human-value-store.js');
290
+ await initializeValueStore();
291
+ const allJudgments = await loadAllJudgments();
292
+ if (allJudgments.length === 0)
293
+ return '';
294
+ const boundIds = new Set(channel && Array.isArray(channel.bound_judgment_ids) ? channel.bound_judgment_ids : []);
295
+ const bound = allJudgments.filter(j => j.id !== undefined && boundIds.has(j.id));
296
+ const others = allJudgments.filter(j => j.id !== undefined && !boundIds.has(j.id));
297
+ let hint = '';
298
+ // 路 1: 用户手动绑定的 judgment — 硬约束, 必须遵循
299
+ if (bound.length > 0) {
300
+ hint += `[系统上下文] 此 channel 用户绑定了 ${bound.length} 条判断力, 必须严格遵循:\n`;
301
+ for (const j of bound) {
302
+ const decision = (j.decision || '').toString().slice(0, 200);
303
+ const reasonList = Array.isArray(j.reasons) ? j.reasons : [];
304
+ const reasonText = reasonList.length > 0
305
+ ? ` (理由: ${reasonList.join('; ').slice(0, 100)})`
306
+ : '';
307
+ hint += `- ${decision}${reasonText}\n`;
308
+ }
309
+ hint += '\n';
310
+ }
311
+ // 路 2: 全局 judgment 候选池 — 软参考, LLM 自己挑
312
+ if (others.length > 0) {
313
+ hint += `[系统上下文] 候选判断力 (用户未明确绑定, 你可以按相关性自主选择参考):\n`;
314
+ for (const j of others) {
315
+ const decision = (j.decision || '').toString().slice(0, 120);
316
+ hint += `- [id=${j.id}] ${decision}\n`;
317
+ }
318
+ hint += `\n[系统上下文] 如果你的回复参考了某条候选判断力, 请在回复中自然提及 "我参考了你的判断: <decision 简述>" 即可, 无需复述 id.\n\n`;
319
+ }
320
+ console.log(`[v3] channel ${channelIdForLog} 注入: 绑定 ${bound.length} 条, 候选 ${others.length} 条`);
321
+ return hint;
322
+ }
323
+ catch (err) {
324
+ console.error(`[v3] 加载判断力失败 (非致命):`, err.message);
325
+ return '';
326
+ }
327
+ }
259
328
  async function getAgentForChannel(channelId, channelDid, channelName, channelDidDoc) {
260
329
  // 获取当前 channel 的 currentSessionId
261
330
  const channels = await loadChannels();
@@ -468,6 +537,8 @@ export async function createWebServer(port = 3000, options = {}) {
468
537
  // (line ~638) 与这里外层的 const channel 形成 shadowing 让 TS 误报"使用前未声明"
469
538
  const boundWalletAddress = channel?.walletAddress;
470
539
  const autoToolsEnabled = channel?.autoInvokeTools !== false; // 默认开启
540
+ // 捕获外层 channel 到独立变量, 避免被 try 块内 (line 740+) 的 const channel 遮蔽
541
+ const channelForJudgment = channel;
471
542
  try {
472
543
  const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
473
544
  let fullResponse = '';
@@ -503,6 +574,11 @@ export async function createWebServer(port = 3000, options = {}) {
503
574
  else {
504
575
  contextHint += `[系统上下文] 自动工具调用已关闭: 每次执行工具前必须先与用户确认。\n`;
505
576
  }
577
+ // v3: 注入 channel 绑定的判断力 (judgment_ids)
578
+ // 这是 v3 的核心 — channel 跑 LLM 时, 它的判断力 = 绑定的 judgment 列表
579
+ const judgmentHint = await buildJudgmentHint(channelForJudgment, channelId);
580
+ if (judgmentHint)
581
+ contextHint += judgmentHint;
506
582
  if (contextHint)
507
583
  contextHint += '\n';
508
584
  fullResponse = await agent.promptStream(contextHint + text, streamCallback);
@@ -659,10 +735,44 @@ export async function createWebServer(port = 3000, options = {}) {
659
735
  res.status(500).json({ error: err.message });
660
736
  }
661
737
  });
738
+ // v3: 列出本节点缓存的远端 channel (按 peerId 分组)
739
+ app.get('/api/remote-channels', async (_req, res) => {
740
+ try {
741
+ const out = [];
742
+ for (const [peerId, list] of remoteChannelCache.entries()) {
743
+ out.push({ peerId, channels: list });
744
+ }
745
+ res.json({ count: out.length, peers: out });
746
+ }
747
+ catch (err) {
748
+ res.status(500).json({ error: err.message });
749
+ }
750
+ });
751
+ // v3: 主动向所有已连接 P2P peer 拉 channel 列表
752
+ // 用法: B 端用户点 "刷新远端智能体" → 触发本 endpoint
753
+ app.post('/api/remote-channels/refresh', async (_req, res) => {
754
+ try {
755
+ const peers = irohTransport.getPeers ? irohTransport.getPeers() : [];
756
+ const peerIds = Array.isArray(peers) ? peers.map((p) => p.nodeId || p) : [];
757
+ if (peerIds.length === 0) {
758
+ return res.json({ ok: true, sent: 0, note: 'no connected peers' });
759
+ }
760
+ let sent = 0;
761
+ for (const peerId of peerIds) {
762
+ const ok = await irohTransport.sendMessage(peerId, 'agent.meta.list', new TextEncoder().encode('{}'));
763
+ if (ok)
764
+ sent++;
765
+ }
766
+ res.json({ ok: true, sent, total: peerIds.length });
767
+ }
768
+ catch (err) {
769
+ res.status(500).json({ error: err.message });
770
+ }
771
+ });
662
772
  app.post('/channels', async (req, res) => {
663
773
  try {
664
- const { name, agentId, walletAddress, autoInvokeTools } = req.body;
665
- console.log(`[创建频道] 收到请求: name=${name}, agentId=${agentId}, wallet=${walletAddress ? 'yes' : 'no'}`);
774
+ const { name, agentId, walletAddress, autoInvokeTools, bound_judgment_ids } = req.body;
775
+ console.log(`[创建频道] 收到请求: name=${name}, agentId=${agentId}, wallet=${walletAddress ? 'yes' : 'no'}, boundJudgments=${Array.isArray(bound_judgment_ids) ? bound_judgment_ids.length : 0}`);
666
776
  if (!name || !agentId) {
667
777
  return res.status(400).json({ error: 'name and agentId required' });
668
778
  }
@@ -670,6 +780,10 @@ export async function createWebServer(port = 3000, options = {}) {
670
780
  const id = `ch_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
671
781
  // 校验钱包地址格式 (粗校验: 0x + 40 hex / Solana base58 / Sui 0x+64)
672
782
  const validWallet = isValidWalletAddress(walletAddress);
783
+ // 过滤 bound_judgment_ids: 只保留 string
784
+ const safeBoundIds = Array.isArray(bound_judgment_ids)
785
+ ? bound_judgment_ids.filter((x) => typeof x === 'string' && x.length > 0)
786
+ : [];
673
787
  // 先创建频道(不阻塞等待 DID 生成)
674
788
  const channel = {
675
789
  id,
@@ -681,6 +795,7 @@ export async function createWebServer(port = 3000, options = {}) {
681
795
  walletAddress: validWallet || undefined,
682
796
  walletRegisteredAt: validWallet ? new Date().toISOString() : undefined,
683
797
  autoInvokeTools: autoInvokeTools !== false, // 默认 true
798
+ bound_judgment_ids: safeBoundIds,
684
799
  sessions: [{
685
800
  id: `sess_${Date.now()}`,
686
801
  createdAt: new Date().toISOString(),
@@ -839,7 +954,7 @@ export async function createWebServer(port = 3000, options = {}) {
839
954
  app.patch('/channels/:channelId', async (req, res) => {
840
955
  try {
841
956
  const { channelId } = req.params;
842
- const { name, walletAddress, autoInvokeTools } = req.body;
957
+ const { name, walletAddress, autoInvokeTools, bound_judgment_ids } = req.body;
843
958
  const channels = await loadChannels();
844
959
  const channel = channels.find(c => c.id === channelId);
845
960
  if (!channel) {
@@ -866,6 +981,19 @@ export async function createWebServer(port = 3000, options = {}) {
866
981
  if (typeof autoInvokeTools === 'boolean') {
867
982
  channel.autoInvokeTools = autoInvokeTools;
868
983
  }
984
+ // bound_judgment_ids: 允许数组(替换)/null(清空)/undefined(不改)
985
+ if (bound_judgment_ids !== undefined) {
986
+ if (bound_judgment_ids === null) {
987
+ channel.bound_judgment_ids = [];
988
+ }
989
+ else if (Array.isArray(bound_judgment_ids)) {
990
+ channel.bound_judgment_ids = bound_judgment_ids.filter((x) => typeof x === 'string' && x.length > 0);
991
+ }
992
+ else {
993
+ return res.status(400).json({ error: 'bound_judgment_ids must be array or null' });
994
+ }
995
+ console.log(`[Channel ${channelId}] 绑定判断力: ${channel.bound_judgment_ids.length} 条`);
996
+ }
869
997
  channel.updatedAt = new Date().toISOString();
870
998
  await saveChannels(channels);
871
999
  res.json(channel);
@@ -1027,8 +1155,9 @@ export async function createWebServer(port = 3000, options = {}) {
1027
1155
  broadcast({ type: 'error', content: event.content }, channelId);
1028
1156
  }
1029
1157
  };
1030
- // 重新生成时只发送用户消息
1031
- fullResponse = await agent.promptStream(userMessage, streamCallback);
1158
+ // 重新生成时只发送用户消息 (v3: 同时注入 channel 绑定的判断力)
1159
+ const regenHint = await buildJudgmentHint(channel, channelId);
1160
+ fullResponse = await agent.promptStream(regenHint + userMessage, streamCallback);
1032
1161
  broadcast({ type: 'ai', content: fullResponse }, channelId);
1033
1162
  // 更新 session
1034
1163
  const existingSession = await loadSession(channelId, currentSessionId);
@@ -1516,6 +1645,91 @@ export async function createWebServer(port = 3000, options = {}) {
1516
1645
  timestamp: Date.now()
1517
1646
  }, 'p2p-global');
1518
1647
  });
1648
+ // ============ v3: 跨用户 channel 元数据 RPC ============
1649
+ // 设计原则: judgment / bound_judgment_ids / wallet 等敏感字段绝不出现在 RPC 响应里.
1650
+ // 收到 'agent.meta.list' → 返回本节点所有 channel 的 UI 元数据 (无 judgment)
1651
+ // 收到 'agent.meta.get' + channelId → 返回单条 channel 的 UI 元数据
1652
+ // B 节点收到响应 → 存到远端 cache → 渲染到 "远端智能体" 区域
1653
+ // B 侧: 收到对端的 list/get 回复 → 更新远端 cache → SSE 推给前端
1654
+ irohTransport.onMessage('agent.meta.list.reply', (msg) => {
1655
+ try {
1656
+ const data = JSON.parse(new TextDecoder().decode(msg.payload));
1657
+ if (!data.ok)
1658
+ return;
1659
+ const peerId = msg.from;
1660
+ const list = Array.isArray(data.channels) ? data.channels : [];
1661
+ remoteChannelCache.set(peerId, list);
1662
+ console.log(`[v3] 缓存远端 peer ${peerId.substring(0, 12)}... 的 ${list.length} 个 channel`);
1663
+ broadcast({
1664
+ type: 'remote-channel-update',
1665
+ peerId,
1666
+ channels: list
1667
+ }, 'p2p-global');
1668
+ }
1669
+ catch (err) {
1670
+ console.error('[v3] 处理 agent.meta.list.reply 失败:', err);
1671
+ }
1672
+ });
1673
+ irohTransport.onMessage('agent.meta.get.reply', (msg) => {
1674
+ try {
1675
+ const data = JSON.parse(new TextDecoder().decode(msg.payload));
1676
+ if (!data.ok || !data.channel)
1677
+ return;
1678
+ const peerId = msg.from;
1679
+ const ch = data.channel;
1680
+ const list = remoteChannelCache.get(peerId) || [];
1681
+ const idx = list.findIndex(c => c.id === ch.id);
1682
+ if (idx >= 0)
1683
+ list[idx] = ch;
1684
+ else
1685
+ list.push(ch);
1686
+ remoteChannelCache.set(peerId, list);
1687
+ broadcast({
1688
+ type: 'remote-channel-update',
1689
+ peerId,
1690
+ channels: list
1691
+ }, 'p2p-global');
1692
+ }
1693
+ catch (err) {
1694
+ console.error('[v3] 处理 agent.meta.get.reply 失败:', err);
1695
+ }
1696
+ });
1697
+ // A 侧: 收到对端的 list/get 请求
1698
+ irohTransport.onMessage('agent.meta.list', async (msg) => {
1699
+ console.log(`[v3] 收到 agent.meta.list from ${msg.from.substring(0, 12)}...`);
1700
+ try {
1701
+ const channels = await loadChannels();
1702
+ const publicMeta = channels.map(sanitizeChannelForPeer);
1703
+ const response = JSON.stringify({ ok: true, channels: publicMeta });
1704
+ const encoded = new TextEncoder().encode(response);
1705
+ // 沿用 msg.from 路由回去
1706
+ irohTransport.sendMessage(msg.from, 'agent.meta.list.reply', encoded).catch(err => {
1707
+ console.error('[v3] 发送 agent.meta.list.reply 失败:', err);
1708
+ });
1709
+ }
1710
+ catch (err) {
1711
+ console.error('[v3] 处理 agent.meta.list 失败:', err);
1712
+ }
1713
+ });
1714
+ irohTransport.onMessage('agent.meta.get', async (msg) => {
1715
+ try {
1716
+ const req = JSON.parse(new TextDecoder().decode(msg.payload));
1717
+ const channelId = req.channelId;
1718
+ console.log(`[v3] 收到 agent.meta.get for ${channelId} from ${msg.from.substring(0, 12)}...`);
1719
+ const channels = await loadChannels();
1720
+ const ch = channels.find(c => c.id === channelId);
1721
+ if (!ch) {
1722
+ const response = JSON.stringify({ ok: false, error: 'channel not found' });
1723
+ irohTransport.sendMessage(msg.from, 'agent.meta.get.reply', new TextEncoder().encode(response));
1724
+ return;
1725
+ }
1726
+ const response = JSON.stringify({ ok: true, channel: sanitizeChannelForPeer(ch) });
1727
+ irohTransport.sendMessage(msg.from, 'agent.meta.get.reply', new TextEncoder().encode(response));
1728
+ }
1729
+ catch (err) {
1730
+ console.error('[v3] 处理 agent.meta.get 失败:', err);
1731
+ }
1732
+ });
1519
1733
  irohTransport.onMessage('ai-dialogue', (msg) => {
1520
1734
  const content = new TextDecoder().decode(msg.payload);
1521
1735
  console.log(`[iroh] 收到 AI 对话 from ${msg.from.substring(0, 12)}...`);
@@ -2140,7 +2354,8 @@ export async function createWebServer(port = 3000, options = {}) {
2140
2354
  try {
2141
2355
  const { createHealthMonitor, createWatchdog } = await import('../heartbeat/index.js');
2142
2356
  healthMonitor = createHealthMonitor();
2143
- watchdog = createWatchdog();
2357
+ // 把 watchdog 静默阈值拉到 30 分钟, 避免开发期 / 用户空闲时被误杀
2358
+ watchdog = createWatchdog({ silentThresholdMs: 30 * 60 * 1000 });
2144
2359
  console.log('[24h] Heartbeat modules loaded');
2145
2360
  }
2146
2361
  catch (err) {
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli.js",