@bolloon/bolloon-agent 0.3.15 → 0.3.17

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.
Files changed (40) hide show
  1. package/dist/agents/agent-identity.js +138 -0
  2. package/dist/agents/error-classifier.js +118 -0
  3. package/dist/agents/parse-tool-call.js +196 -1
  4. package/dist/agents/pi-sdk-tools.js +13 -0
  5. package/dist/agents/pi-sdk.js +195 -262
  6. package/dist/cli/loading-tui.js +43 -36
  7. package/dist/cli-entry.js +15 -8
  8. package/dist/electron/config.js +9 -14
  9. package/dist/electron/dialogs.js +16 -53
  10. package/dist/electron/first-run.js +24 -65
  11. package/dist/electron/ipc.js +10 -14
  12. package/dist/electron/logger.js +7 -44
  13. package/dist/electron/main.js +42 -45
  14. package/dist/electron/menu.js +13 -18
  15. package/dist/electron/paths.js +12 -54
  16. package/dist/electron/server.js +18 -57
  17. package/dist/electron/tray.js +15 -53
  18. package/dist/electron/window.js +22 -61
  19. package/dist/electron-preload.js +16 -19
  20. package/dist/electron.js +1 -4
  21. package/dist/external-engines/discovery.js +11 -0
  22. package/dist/index.js +172 -15
  23. package/dist/lsp/lsp-manager.js +281 -0
  24. package/dist/lsp/lsp-tools.js +222 -0
  25. package/dist/network/auto-peer-discovery.js +77 -0
  26. package/dist/network/did-agent-resolver.js +206 -0
  27. package/dist/network/p2p-direct.js +1 -1
  28. package/dist/network/p2p-outbox.js +2 -2
  29. package/dist/utils/auto-update.js +12 -51
  30. package/dist/web/client.js +4323 -4812
  31. package/dist/web/components/p2p/P2PModal.js +188 -0
  32. package/dist/web/components/p2p/index.js +276 -234
  33. package/dist/web/components/p2p/p2p-modal.js +664 -0
  34. package/dist/web/components/p2p/p2p-tools.js +248 -0
  35. package/dist/web/index.html +7 -0
  36. package/dist/web/server.js +164 -41
  37. package/dist/web/style.css +58 -3
  38. package/dist/web/ui/message-renderer.js +535 -396
  39. package/dist/web/ui/step-timeline.js +371 -272
  40. package/package.json +3 -3
@@ -0,0 +1,248 @@
1
+ /**
2
+ * P2P 工具调用模块
3
+ * 支持文件和本地信息的 P2P 传递
4
+ */
5
+ // 工具类型枚举
6
+ export var P2PToolType;
7
+ (function (P2PToolType) {
8
+ P2PToolType["FILE_TRANSFER"] = "file_transfer";
9
+ P2PToolType["LOCAL_INFO"] = "local_info";
10
+ P2PToolType["SYSTEM_INFO"] = "system_info";
11
+ P2PToolType["FILE_LIST"] = "file_list";
12
+ })(P2PToolType || (P2PToolType = {}));
13
+ // 文件传输
14
+ export async function transferFile(targetDid, fileInfo, messageId) {
15
+ try {
16
+ const payload = {
17
+ type: 'file',
18
+ tool: P2PToolType.FILE_TRANSFER,
19
+ data: {
20
+ name: fileInfo.name,
21
+ size: fileInfo.size,
22
+ mimeType: fileInfo.mimeType,
23
+ content: fileInfo.content
24
+ }
25
+ };
26
+ const res = await fetch('/api/message-p2p', {
27
+ method: 'POST',
28
+ headers: { 'Content-Type': 'application/json' },
29
+ body: JSON.stringify({
30
+ targetDid,
31
+ content: JSON.stringify(payload),
32
+ type: 'file'
33
+ })
34
+ });
35
+ if (res.ok) {
36
+ const data = await res.json();
37
+ return {
38
+ success: true,
39
+ messageId: data.messageId || messageId
40
+ };
41
+ }
42
+ else {
43
+ return { success: false, error: '文件传输失败' };
44
+ }
45
+ }
46
+ catch (e) {
47
+ return { success: false, error: e.message };
48
+ }
49
+ }
50
+ // 本地信息查询
51
+ export async function queryLocalInfo(targetDid, query) {
52
+ try {
53
+ const payload = {
54
+ type: 'info_query',
55
+ tool: P2PToolType.LOCAL_INFO,
56
+ query: query
57
+ };
58
+ const res = await fetch('/api/message-p2p', {
59
+ method: 'POST',
60
+ headers: { 'Content-Type': 'application/json' },
61
+ body: JSON.stringify({
62
+ targetDid,
63
+ content: JSON.stringify(payload),
64
+ type: 'ai-dialogue'
65
+ })
66
+ });
67
+ if (res.ok) {
68
+ const data = await res.json();
69
+ return {
70
+ success: true,
71
+ data: data.response,
72
+ messageId: data.messageId
73
+ };
74
+ }
75
+ else {
76
+ return { success: false, error: '信息查询失败' };
77
+ }
78
+ }
79
+ catch (e) {
80
+ return { success: false, error: e.message };
81
+ }
82
+ }
83
+ // 系统信息请求
84
+ export async function getSystemInfo(targetDid) {
85
+ try {
86
+ const payload = {
87
+ type: 'system_info_request',
88
+ tool: P2PToolType.SYSTEM_INFO
89
+ };
90
+ const res = await fetch('/api/message-p2p', {
91
+ method: 'POST',
92
+ headers: { 'Content-Type': 'application/json' },
93
+ body: JSON.stringify({
94
+ targetDid,
95
+ content: JSON.stringify(payload),
96
+ type: 'ai-dialogue'
97
+ })
98
+ });
99
+ if (res.ok) {
100
+ const data = await res.json();
101
+ return {
102
+ success: true,
103
+ data: data.response,
104
+ messageId: data.messageId
105
+ };
106
+ }
107
+ else {
108
+ return { success: false, error: '系统信息获取失败' };
109
+ }
110
+ }
111
+ catch (e) {
112
+ return { success: false, error: e.message };
113
+ }
114
+ }
115
+ // 文件列表请求
116
+ export async function listFiles(targetDid, path = '/') {
117
+ try {
118
+ const payload = {
119
+ type: 'file_list_request',
120
+ tool: P2PToolType.FILE_LIST,
121
+ path: path
122
+ };
123
+ const res = await fetch('/api/message-p2p', {
124
+ method: 'POST',
125
+ headers: { 'Content-Type': 'application/json' },
126
+ body: JSON.stringify({
127
+ targetDid,
128
+ content: JSON.stringify(payload),
129
+ type: 'ai-dialogue'
130
+ })
131
+ });
132
+ if (res.ok) {
133
+ const data = await res.json();
134
+ return {
135
+ success: true,
136
+ data: data.response,
137
+ messageId: data.messageId
138
+ };
139
+ }
140
+ else {
141
+ return { success: false, error: '文件列表获取失败' };
142
+ }
143
+ }
144
+ catch (e) {
145
+ return { success: false, error: e.message };
146
+ }
147
+ }
148
+ // 工具调用入口
149
+ export async function executeP2PTool(request) {
150
+ const { toolName, payload, targetDid } = request;
151
+ if (!targetDid) {
152
+ return { success: false, error: '未指定目标节点' };
153
+ }
154
+ switch (toolName) {
155
+ case P2PToolType.FILE_TRANSFER:
156
+ return transferFile(targetDid, payload.data);
157
+ case P2PToolType.LOCAL_INFO:
158
+ return queryLocalInfo(targetDid, payload.data);
159
+ case P2PToolType.SYSTEM_INFO:
160
+ return getSystemInfo(targetDid);
161
+ case P2PToolType.FILE_LIST:
162
+ return listFiles(targetDid, payload.data?.path || '/');
163
+ default:
164
+ return { success: false, error: `未知工具: ${toolName}` };
165
+ }
166
+ }
167
+ // 本地系统信息获取 (用于响应来自远程的请求)
168
+ export function getLocalSystemInfo() {
169
+ const memUsage = process.memoryUsage();
170
+ const cpuCores = require('os').cpus();
171
+ return {
172
+ platform: process.platform,
173
+ arch: process.arch,
174
+ nodeVersion: process.version,
175
+ memory: {
176
+ total: memUsage.heapTotal,
177
+ free: memUsage.heapTotal - memUsage.heapUsed,
178
+ used: memUsage.heapUsed
179
+ },
180
+ cpu: {
181
+ cores: cpuCores.length,
182
+ model: cpuCores[0]?.model || 'Unknown'
183
+ },
184
+ uptime: process.uptime()
185
+ };
186
+ }
187
+ // 本地文件列表 (用于响应来自远程的请求)
188
+ export function getLocalFileList(dirPath) {
189
+ try {
190
+ // Dynamic import for ESM compatibility
191
+ const pathModule = require('path');
192
+ const fs = require('fs');
193
+ const fullPath = pathModule.resolve(dirPath);
194
+ const files = fs.readdirSync(fullPath);
195
+ const fileList = files.map((name) => {
196
+ const fullFilePath = pathModule.join(fullPath, name);
197
+ let stat;
198
+ try {
199
+ stat = fs.statSync(fullFilePath);
200
+ }
201
+ catch {
202
+ return null;
203
+ }
204
+ return {
205
+ name: name,
206
+ size: stat.size,
207
+ isDirectory: stat.isDirectory(),
208
+ modified: stat.mtime.toISOString()
209
+ };
210
+ }).filter(Boolean);
211
+ return {
212
+ success: true,
213
+ path: fullPath,
214
+ files: fileList
215
+ };
216
+ }
217
+ catch (e) {
218
+ return {
219
+ success: false,
220
+ path: dirPath,
221
+ error: e.message
222
+ };
223
+ }
224
+ }
225
+ // 导出工具名称列表
226
+ export const P2P_TOOLS = [
227
+ {
228
+ name: P2PToolType.FILE_TRANSFER,
229
+ description: '传输文件到远程节点',
230
+ parameters: ['targetDid', 'fileInfo']
231
+ },
232
+ {
233
+ name: P2PToolType.LOCAL_INFO,
234
+ description: '查询远程节点本地信息',
235
+ parameters: ['targetDid', 'query']
236
+ },
237
+ {
238
+ name: P2PToolType.SYSTEM_INFO,
239
+ description: '获取远程节点系统信息',
240
+ parameters: ['targetDid']
241
+ },
242
+ {
243
+ name: P2PToolType.FILE_LIST,
244
+ description: '列出远程节点目录文件',
245
+ parameters: ['targetDid', 'path']
246
+ }
247
+ ];
248
+ console.log('[P2P Tools] 工具模块已加载');
@@ -78,6 +78,13 @@
78
78
  </div>
79
79
 
80
80
  <div class="sidebar-footer">
81
+ <div class="user-avatar" id="user-avatar" title="点击查看 DID 身份">
82
+ <span class="avatar-letter" id="avatar-letter">?</span>
83
+ </div>
84
+ <div class="user-info">
85
+ <span class="user-name" id="user-name">加载中...</span>
86
+ <span class="user-did" id="user-did"></span>
87
+ </div>
81
88
  <div class="agent-status">
82
89
  <span class="status-dot"></span>
83
90
  <span class="status-text">已连接</span>
@@ -15,6 +15,7 @@ import { registerLlmConfigRoutes } from './routes-llm-config.js';
15
15
  import { registerExternalEngineRoutes } from './routes-external-engines.js';
16
16
  import { registerTaskRoutes } from './routes-tasks.js';
17
17
  import { registerHearthRoutes } from './routes-hearth.js';
18
+ import { loadOrCreateAgentIdentity } from '../agents/agent-identity.js';
18
19
  // 2026-07-06: 类型抽到 ./server-types.ts (channel / session / task / sse client / iroh info / paths)
19
20
  import { SESSION_CACHE_PATH, SHARED_SESSION_PATH, IPFS_ENDPOINT, } from './server-types.js';
20
21
  // 同时也 re-export 出去 (其它地方可能从 './server.js' 引用)
@@ -1376,9 +1377,41 @@ export async function createWebServer(port = 3000, options = {}) {
1376
1377
  };
1377
1378
  try {
1378
1379
  console.log('开始生成 P2P 身份...');
1379
- // 生成 DIAP 身份
1380
- const kp = KeyManager.generate();
1381
- console.log('KeyManager.generate() 完成, kp:', !!kp, 'kp.did:', kp?.did);
1380
+ // 加载或生成持久化的 P2P 身份
1381
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '.';
1382
+ const p2pIdentityPath = path.join(homeDir, '.bolloon', 'p2p-identity.json');
1383
+ let kp;
1384
+ let reused = false;
1385
+ try {
1386
+ if (fsSync.existsSync(p2pIdentityPath)) {
1387
+ const raw = fsSync.readFileSync(p2pIdentityPath, 'utf-8');
1388
+ const j = JSON.parse(raw);
1389
+ const pkBytes = Buffer.from(j.privateKey, 'hex');
1390
+ if (pkBytes.length === 32) {
1391
+ kp = KeyManager.fromPrivateKey(pkBytes);
1392
+ reused = true;
1393
+ }
1394
+ else
1395
+ throw 0;
1396
+ }
1397
+ else
1398
+ throw 0;
1399
+ }
1400
+ catch {
1401
+ kp = KeyManager.generate();
1402
+ const privateKeyHex = Buffer.from(kp.privateKey).toString('hex');
1403
+ const publicKeyHex = Buffer.from(kp.publicKey).toString('hex');
1404
+ fsSync.mkdirSync(path.dirname(p2pIdentityPath), { recursive: true });
1405
+ fsSync.writeFileSync(p2pIdentityPath, JSON.stringify({
1406
+ keyType: 'Ed25519', privateKey: privateKeyHex, publicKey: publicKeyHex,
1407
+ did: kp.did, createdAt: new Date().toISOString(), version: '1.0'
1408
+ }, null, 2), { mode: 0o600 });
1409
+ try {
1410
+ fsSync.chmodSync(p2pIdentityPath, 0o600);
1411
+ }
1412
+ catch { /* ignore */ }
1413
+ }
1414
+ console.log(reused ? `复用 P2P 身份: ${kp.did.substring(0, 24)}...` : `新建 P2P 身份: ${kp.did.substring(0, 24)}...`);
1382
1415
  console.log('kp.publicKey:', kp?.publicKey);
1383
1416
  const did = kp.did || 'did:unknown:123456';
1384
1417
  console.log(`DID: ${did}`);
@@ -1415,14 +1448,39 @@ export async function createWebServer(port = 3000, options = {}) {
1415
1448
  if (parsed && parsed.v === 3 && parsed.op) {
1416
1449
  // v3 跨用户 chat: B 端收到 A 的 chat.reply, 直接 SSE 推给前端
1417
1450
  if (parsed.op === 'agent.chat.reply') {
1418
- console.log(`[v3] 收到来自 ${evt.fromPublicKey.substring(0, 12)}... 的 chat.reply (${(parsed.payload?.text || '').length} chars)`);
1451
+ const replyText = parsed.payload?.text || '';
1452
+ const replyChannelId = parsed.payload?.channelId;
1453
+ console.log(`[v3] 收到来自 ${evt.fromPublicKey.substring(0, 12)}... 的 chat.reply (${replyText.length} chars, channel=${replyChannelId})`);
1419
1454
  broadcast({
1420
1455
  type: 'remote-chat-reply',
1421
1456
  fromPublicKey: evt.fromPublicKey,
1422
- channelId: parsed.payload?.channelId,
1423
- text: parsed.payload?.text || '',
1457
+ channelId: replyChannelId,
1458
+ text: replyText,
1424
1459
  error: parsed.payload?.error
1425
1460
  }, 'p2p-global');
1461
+ // 2026-07-27: 把远端回复持久化到本地 session, 让 LLM 下次能读到上下文
1462
+ if (replyChannelId && replyText) {
1463
+ import('../web/server-storage.js').then(async ({ loadSession, saveSession }) => {
1464
+ try {
1465
+ const existing = await loadSession(replyChannelId, 'default');
1466
+ const session = existing || { channelId: replyChannelId, sessionId: 'default', messages: [], lastUpdated: '' };
1467
+ session.messages.push({
1468
+ id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
1469
+ type: 'ai',
1470
+ content: replyText,
1471
+ timestamp: new Date().toISOString(),
1472
+ source: 'remote-reply',
1473
+ fromPublicKey: evt.fromPublicKey,
1474
+ });
1475
+ session.lastUpdated = new Date().toISOString();
1476
+ await saveSession(session);
1477
+ console.log(`[v3] chat.reply 已持久化到 session (${replyChannelId}): ${replyText.substring(0, 40)}...`);
1478
+ }
1479
+ catch (e) {
1480
+ console.warn('[v3] chat.reply 持久化失败:', e?.message?.substring(0, 100));
1481
+ }
1482
+ }).catch(() => { });
1483
+ }
1426
1484
  return;
1427
1485
  }
1428
1486
  // 2026-07-21: 社交心跳 beacon — 远端智能体宣告存活/能力, 更新本地 liveness
@@ -1488,9 +1546,19 @@ export async function createWebServer(port = 3000, options = {}) {
1488
1546
  return;
1489
1547
  }
1490
1548
  const commShim = {
1491
- sendToConnection: (_id, data) => {
1549
+ sendToConnection: async (_id, data) => {
1550
+ // 尝试解析 JSON 提取 op + payload,走 outbox(连接断开也不丢消息)
1551
+ try {
1552
+ const parsed = JSON.parse(data);
1553
+ if (parsed && parsed.v === 3 && parsed.op) {
1554
+ const { sendOrQueue } = await import('../network/p2p-outbox.js');
1555
+ await sendOrQueue(evt.fromPublicKey, parsed.op, parsed.payload || {}, v3P2PRef);
1556
+ return;
1557
+ }
1558
+ }
1559
+ catch { }
1560
+ // 兜底:直接发送
1492
1561
  v3P2PRef.sendTo(evt.fromPublicKey, data);
1493
- return Promise.resolve();
1494
1562
  }
1495
1563
  };
1496
1564
  // v3 新增: 好友申请 RPC — 任何对端可以发, 推到前端 UI 让用户接受
@@ -1599,6 +1667,10 @@ export async function createWebServer(port = 3000, options = {}) {
1599
1667
  const counts = await writeRemoteResources(peerKey2, manifest);
1600
1668
  await peerFs.writeCapabilityIndex(peerKey2, idx);
1601
1669
  console.log(`[v3-manifest] (P2PDirect) 收到 ${peerKey2.substring(0, 12)}... manifest (${manifest.agents.length} agents, owner=${manifest.ownerName || '?'}, +g${counts.groups}/f${counts.functions}/e${counts.exportments}/s${counts.sciences}) → 落盘`);
1670
+ // 2026-07-27: 如果是自动发现的 peer,用 manifest 中的名字更新
1671
+ if (manifest.ownerName) {
1672
+ import('../network/auto-peer-discovery.js').then(({ updateDiscoveredPeerName }) => updateDiscoveredPeerName(peerKey2, manifest.ownerName).catch(() => { })).catch(() => { });
1673
+ }
1602
1674
  broadcast({
1603
1675
  type: 'peer-manifest-updated',
1604
1676
  fromPublicKey: peerKey2,
@@ -1606,6 +1678,16 @@ export async function createWebServer(port = 3000, options = {}) {
1606
1678
  agentCount: manifest.agents.length,
1607
1679
  capabilityIndex: await peerFs.readCapabilityIndex(peerKey2),
1608
1680
  }, 'p2p-global');
1681
+ // 2026-07-27: 对 manifest 中有 cid/ipnsName 的 agent 解析 DID 文档
1682
+ // 不阻塞主流程,fire-and-forget 后台进行
1683
+ const agentsWithDID = manifest.agents.filter((a) => a.cid || a.ipnsName);
1684
+ if (agentsWithDID.length > 0) {
1685
+ import('../network/did-agent-resolver.js').then(({ resolveAgentsFromManifest, persistResolvedAgent }) => resolveAgentsFromManifest(agentsWithDID).then(resolved => {
1686
+ for (const r of resolved) {
1687
+ persistResolvedAgent(peerKey2, r).catch(() => { });
1688
+ }
1689
+ }).catch(() => { })).catch(() => { });
1690
+ }
1609
1691
  }
1610
1692
  catch (err) {
1611
1693
  console.error('[v3-manifest] (P2PDirect) manifest.exchange.reply 失败:', err.message);
@@ -1788,10 +1870,16 @@ ${goalDesc}
1788
1870
  return { initiate: false };
1789
1871
  }
1790
1872
  }
1791
- // 新连接进来 → 主动发我分享给 ta 的 channel 列表
1873
+ // 新连接进来 → 主动发我分享给 ta 的 channel 列表 + 自动发现好友
1792
1874
  v3P2PRef.on('connection', (evt) => {
1793
1875
  // 2026-06-10: 喂 watchdog —— 新连接到来是真实业务活动
1794
1876
  watchdogRef?.recordActivity?.();
1877
+ // 2026-07-27: 自动发现 — 通过 topic 进来的新 peer 自动加好友
1878
+ // fire-and-forget: 不阻塞 connection 处理主流程
1879
+ import('../network/auto-peer-discovery.js').then(({ tryAutoDiscoverPeer }) => {
1880
+ const localPk = v3P2PRef?.getPublicKey() || '';
1881
+ tryAutoDiscoverPeer(evt.remotePublicKey, localPk).catch((e) => console.warn('[auto-discover] 失败:', e?.message));
1882
+ }).catch(() => { });
1795
1883
  setTimeout(async () => {
1796
1884
  try {
1797
1885
  const channels = await loadChannels();
@@ -1982,8 +2070,8 @@ ${goalDesc}
1982
2070
  console.warn('[v3-outbox] flushAllOutboxes failed:', err.message);
1983
2071
  }
1984
2072
  }
1985
- // 每 2 分钟兜底 flush
1986
- setInterval(flushAllOutboxes, 2 * 60 * 1000);
2073
+ // 每 15s 兜底 flush (连接窗口通常 5-10s, 要能抓住窗口 flush 出去)
2074
+ setInterval(flushAllOutboxes, 15 * 1000);
1987
2075
  }
1988
2076
  catch (err) {
1989
2077
  console.error('[v3] P2PDirect 启动失败:', err.message);
@@ -1994,9 +2082,7 @@ ${goalDesc}
1994
2082
  // v3 修复: 用 setInterval 替代一次性 setTimeout, 确保分享变更后能持续推送给 peer
1995
2083
  setInterval(v3BroadcastOwn, 30000);
1996
2084
  // 保留 @diap/sdk 的旧实例 (它的 Hyperswarm 实例能帮 P2PDirect 做 DHT bootstrap)
1997
- // 2026-07-04: hyperswarm 4.x 删除了 Discovery.update() (被 .flushed() 替代).
1998
- // @diap/sdk 0.1.10 还调 .update(), 是上游 bug (已记录 docs/plans/2026-06-17-supervisor-iter-1.md).
1999
- // 这里静默 joinTopic 失败, P2PDirect (v3 主路径) 不依赖 @diap/sdk, 不影响功能.
2085
+ // 2026-07-04: @diap/sdk v0.2.0 已修复 seed/*update*/connect 类型, 此处走正常路径.
2000
2086
  try {
2001
2087
  const rawSeed = crypto.getRandomValues(new Uint8Array(32));
2002
2088
  p2pCommunicator = createHyperswarmCommunicator({
@@ -2025,18 +2111,17 @@ ${goalDesc}
2025
2111
  });
2026
2112
  await p2pCommunicator.start();
2027
2113
  // @diap/sdk 也 join topic — 它的 Hyperswarm 实例帮 P2PDirect 做 DHT 引导
2028
- // joinTopic 失败已知 (hyperswarm 4.x + @diap/sdk 0.1.10), catch 静默
2114
+ // v0.2.1 (2026-07-28): hyperswarm seed 类型固定 + join() 返回 { refresh, flushed, destroy }
2115
+ // discovery.update 错误已彻底修复. catch 保留做防御性兜底.
2029
2116
  const oldTopic = createTopic('bolloon-agent-harness');
2030
2117
  try {
2031
2118
  await p2pCommunicator.joinTopic(oldTopic);
2032
2119
  console.log(`P2P 老通道已就绪 (DHT bootstrap 帮 P2PDirect, 实际数据走 P2PDirect)`);
2033
2120
  }
2034
2121
  catch (joinErr) {
2035
- // 已知: @diap/sdk 0.1.10 + hyperswarm 4.x → discovery.update is not a function
2036
- // v3 P2PDirect 是主路径, 此处不阻断
2037
2122
  const msg = String(joinErr?.message || joinErr);
2038
2123
  if (msg.includes('discovery.update') || msg.includes('is not a function')) {
2039
- console.warn(`[v3-legacy] @diap/sdk joinTopic 失败 (已知 hyperswarm 4.x 兼容问题, 已忽略): ${msg}`);
2124
+ console.warn(`[v3-legacy] joinTopic 触发旧版兼容警告: ${msg}`);
2040
2125
  }
2041
2126
  else {
2042
2127
  throw joinErr;
@@ -2135,6 +2220,50 @@ ${goalDesc}
2135
2220
  app.get('/api/tools', (_req, res) => {
2136
2221
  res.json(listTools().map(t => t.id));
2137
2222
  });
2223
+ // 2026-07-28: 用户 DID 身份端点 — 静默生成/加载, 持久化到 ~/.bolloon/identity/user.json
2224
+ let userIdentityCache = null;
2225
+ const IDENTITY_DIR = `${process.env.HOME || '/tmp'}/.bolloon/identity`;
2226
+ async function loadOrCreateUserIdentity() {
2227
+ if (userIdentityCache)
2228
+ return userIdentityCache;
2229
+ try {
2230
+ const { readFileSync, existsSync, mkdirSync, writeFileSync } = await import('fs');
2231
+ const file = `${IDENTITY_DIR}/user.json`;
2232
+ if (existsSync(file)) {
2233
+ const raw = readFileSync(file, 'utf-8');
2234
+ const parsed = JSON.parse(raw);
2235
+ if (parsed.did && parsed.publicKeyHex) {
2236
+ userIdentityCache = parsed;
2237
+ return parsed;
2238
+ }
2239
+ }
2240
+ // 生成新 DID keypair
2241
+ const kp = KeyManager.generate();
2242
+ const didShort = kp.did.split(':').pop()?.substring(0, 8) || 'unknown';
2243
+ const publicKeyHex = Buffer.from(kp.publicKey).toString('hex');
2244
+ const username = getUserName();
2245
+ const identity = {
2246
+ did: kp.did,
2247
+ didShort,
2248
+ publicKeyHex,
2249
+ name: `blln-${username}`,
2250
+ createdAt: new Date().toISOString(),
2251
+ };
2252
+ mkdirSync(IDENTITY_DIR, { recursive: true });
2253
+ writeFileSync(file, JSON.stringify(identity, null, 2), { mode: 0o600 });
2254
+ userIdentityCache = identity;
2255
+ console.log(`[user-identity] ✅ DID: ${kp.did.substring(0, 30)}...`);
2256
+ return identity;
2257
+ }
2258
+ catch (e) {
2259
+ console.warn('[user-identity] 加载失败:', e.message);
2260
+ return { did: '', didShort: 'anon', publicKeyHex: '', name: getUserName() };
2261
+ }
2262
+ }
2263
+ app.get('/api/user/identity', async (_req, res) => {
2264
+ const identity = await loadOrCreateUserIdentity();
2265
+ res.json(identity);
2266
+ });
2138
2267
  // 2026-07-01 (v0.2.6): 前后端分离核心 — 后端切 LLM 输出为结构化 segments
2139
2268
  // - POST /api/segment-reply { reply, knownTools }
2140
2269
  // - 返回 ChatSegment[] (think / text / env_details / tool_call / final)
@@ -3095,25 +3224,24 @@ ${goalDesc}
3095
3224
  const didMissing = !channel.did || channel.did === 'undefined' || channel.did === 'null' || channel.did === '';
3096
3225
  if (!didMissing)
3097
3226
  return;
3098
- let kp;
3227
+ let identity = null;
3099
3228
  try {
3100
- kp = KeyManager.generate();
3101
- }
3102
- catch {
3103
- kp = null;
3104
- }
3105
- if (kp && kp.did) {
3106
- channel.did = kp.did;
3107
- channel.publicKey = Buffer.from(kp.publicKey).toString('hex');
3229
+ // agentId 作为持久化身份的 key — 确保同一 agentId 跨重启稳定
3230
+ identity = loadOrCreateAgentIdentity(channel.agentId || channel.id);
3231
+ channel.did = identity.did;
3232
+ channel.publicKey = identity.publicKey;
3108
3233
  }
3109
- else {
3234
+ catch (e) {
3110
3235
  // 兜底: 用 channelId 派生, 不阻塞 UI
3236
+ console.warn(`[DID 修复] ${channel.name} agentIdentity 加载失败:`, e.message);
3111
3237
  channel.did = `did:web:${channel.id}`;
3112
3238
  channel.publicKey = `pk_${channel.id}`;
3113
3239
  }
3114
- console.log(`[DID 修复] ${channel.name} DID = ${channel.did}`);
3240
+ console.log(`[DID 修复] ${channel.name} DID = ${channel.did} (${identity?.reused ? '复用' : '新建'} agent 持久身份)`);
3115
3241
  // IPFS 注册: 失败也无所谓, 后续可重试
3116
3242
  try {
3243
+ const pkBytes = Buffer.from(channel.publicKey, 'hex');
3244
+ const kp = { privateKey: new Uint8Array(32), publicKey: pkBytes, did: channel.did };
3117
3245
  const auth = await AgentAuthManager.newWithRemoteIpfs('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
3118
3246
  const result = await auth.registerAgent({ name: channel.name, services: [] }, kp, '');
3119
3247
  channel.cid = result.cid || channel.cid;
@@ -4275,21 +4403,16 @@ ${goalDesc}
4275
4403
  return res.status(400).json({ error: 'text length must be 1-8000' });
4276
4404
  }
4277
4405
  const fromPk = v3P2PRef.getPublicKey();
4278
- const msg = JSON.stringify({
4279
- v: 3,
4280
- op: 'agent.chat.send',
4281
- payload: { channelId, text, fromPublicKey: fromPk }
4282
- });
4283
- const ok = v3P2PRef.sendTo(targetPublicKey, msg);
4284
- if (!ok) {
4285
- return res.status(502).json({
4286
- error: 'peer not connected. POST /api/remote-channels/p2p-connect first.'
4287
- });
4406
+ // 2026-07-27: 改用 sendOrQueue (先尝试直发, 失败则入队, 不断线不丢)
4407
+ const { sendOrQueue } = await import('../network/p2p-outbox.js');
4408
+ const r = await sendOrQueue(targetPublicKey, 'agent.chat.send', { channelId, text, fromPublicKey: fromPk }, v3P2PRef);
4409
+ if (r === 'FAILED') {
4410
+ return res.status(502).json({ error: 'send failed: peer not reachable' });
4288
4411
  }
4289
4412
  // 2026-06-10: 喂 watchdog — chat-send 成功是真实业务活动
4290
4413
  watchdogRef?.recordActivity?.();
4291
- console.log(`[v3] chat-send 转发到 ${targetPublicKey.substring(0, 12)}... (channelId=${channelId})`);
4292
- res.json({ ok: true, sent: true });
4414
+ console.log(`[v3] chat-send 转发到 ${targetPublicKey.substring(0, 12)}... (channelId=${channelId}) => ${r}`);
4415
+ res.json({ ok: true, sent: r === 'SENT', queued: r === 'QUEUED' });
4293
4416
  }
4294
4417
  catch (err) {
4295
4418
  console.error('[v3] chat-send 失败:', err);
@@ -822,16 +822,71 @@ body {
822
822
 
823
823
  /* Sidebar Footer */
824
824
  .sidebar-footer {
825
- padding: 16px;
825
+ padding: 12px 16px;
826
826
  border-top: 1px solid var(--border);
827
+ display: flex;
828
+ align-items: center;
829
+ gap: 10px;
827
830
  }
828
831
 
829
- .agent-status {
832
+ /* 用户头像 — DID 身份 */
833
+ .user-avatar {
834
+ width: 32px;
835
+ height: 32px;
836
+ border-radius: 50%;
837
+ background: linear-gradient(135deg, var(--accent) 0%, var(--accent-dark) 100%);
830
838
  display: flex;
831
839
  align-items: center;
832
- gap: 8px;
840
+ justify-content: center;
841
+ flex-shrink: 0;
842
+ cursor: pointer;
843
+ transition: transform 0.2s, box-shadow 0.2s;
844
+ }
845
+ .user-avatar:hover {
846
+ transform: scale(1.1);
847
+ box-shadow: 0 0 12px var(--accent-glow);
848
+ }
849
+ .avatar-letter {
850
+ font-size: 14px;
851
+ font-weight: 700;
852
+ color: #1a1a18;
853
+ text-transform: uppercase;
854
+ user-select: none;
855
+ }
856
+
857
+ .user-info {
858
+ flex: 1;
859
+ min-width: 0;
860
+ display: flex;
861
+ flex-direction: column;
862
+ gap: 2px;
863
+ }
864
+ .user-name {
833
865
  font-size: 12px;
866
+ font-weight: 600;
867
+ color: var(--text);
868
+ white-space: nowrap;
869
+ overflow: hidden;
870
+ text-overflow: ellipsis;
871
+ line-height: 1.2;
872
+ }
873
+ .user-did {
874
+ font-size: 10px;
875
+ font-family: var(--font-mono);
876
+ color: var(--text-muted);
877
+ white-space: nowrap;
878
+ overflow: hidden;
879
+ text-overflow: ellipsis;
880
+ line-height: 1.2;
881
+ }
882
+
883
+ .agent-status {
884
+ display: flex;
885
+ align-items: center;
886
+ gap: 6px;
887
+ font-size: 11px;
834
888
  color: var(--text-secondary);
889
+ flex-shrink: 0;
835
890
  }
836
891
 
837
892
  .status-dot {