@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.
- package/dist/agents/agent-identity.js +138 -0
- package/dist/agents/error-classifier.js +118 -0
- package/dist/agents/parse-tool-call.js +196 -1
- package/dist/agents/pi-sdk-tools.js +13 -0
- package/dist/agents/pi-sdk.js +195 -262
- package/dist/cli/loading-tui.js +43 -36
- package/dist/cli-entry.js +15 -8
- package/dist/electron/config.js +9 -14
- package/dist/electron/dialogs.js +16 -53
- package/dist/electron/first-run.js +24 -65
- package/dist/electron/ipc.js +10 -14
- package/dist/electron/logger.js +7 -44
- package/dist/electron/main.js +42 -45
- package/dist/electron/menu.js +13 -18
- package/dist/electron/paths.js +12 -54
- package/dist/electron/server.js +18 -57
- package/dist/electron/tray.js +15 -53
- package/dist/electron/window.js +22 -61
- package/dist/electron-preload.js +16 -19
- package/dist/electron.js +1 -4
- package/dist/external-engines/discovery.js +11 -0
- package/dist/index.js +172 -15
- package/dist/lsp/lsp-manager.js +281 -0
- package/dist/lsp/lsp-tools.js +222 -0
- package/dist/network/auto-peer-discovery.js +77 -0
- package/dist/network/did-agent-resolver.js +206 -0
- package/dist/network/p2p-direct.js +1 -1
- package/dist/network/p2p-outbox.js +2 -2
- package/dist/utils/auto-update.js +12 -51
- package/dist/web/client.js +4323 -4812
- package/dist/web/components/p2p/P2PModal.js +188 -0
- package/dist/web/components/p2p/index.js +276 -234
- package/dist/web/components/p2p/p2p-modal.js +664 -0
- package/dist/web/components/p2p/p2p-tools.js +248 -0
- package/dist/web/index.html +7 -0
- package/dist/web/server.js +164 -41
- package/dist/web/style.css +58 -3
- package/dist/web/ui/message-renderer.js +535 -396
- package/dist/web/ui/step-timeline.js +371 -272
- 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] 工具模块已加载');
|
package/dist/web/index.html
CHANGED
|
@@ -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>
|
package/dist/web/server.js
CHANGED
|
@@ -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
|
-
//
|
|
1380
|
-
const
|
|
1381
|
-
|
|
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
|
-
|
|
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:
|
|
1423
|
-
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
|
-
// 每
|
|
1986
|
-
setInterval(flushAllOutboxes,
|
|
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:
|
|
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
|
-
//
|
|
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]
|
|
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
|
|
3227
|
+
let identity = null;
|
|
3099
3228
|
try {
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
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:
|
|
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);
|
package/dist/web/style.css
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|