@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,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lsp/lsp-tools.ts — 注册 LSP 工具到 Bolloon agent 工具系统
|
|
3
|
+
*
|
|
4
|
+
* 暴露 6 个工具:
|
|
5
|
+
* - lsp_detect: 检测本机已安装的语言服务器
|
|
6
|
+
* - lsp_hover: 悬停查看类型/文档
|
|
7
|
+
* - lsp_completion: 代码补全
|
|
8
|
+
* - lsp_diagnostics: 诊断当前文件
|
|
9
|
+
* - lsp_go_to_definition: 跳转到定义
|
|
10
|
+
* - lsp_start / lsp_stop: 手动管理 LSP 服务器生命周期
|
|
11
|
+
*/
|
|
12
|
+
import { detectInstalledLspServers, startLspServer, stopLspServer, stopAllLspServers, findLspForFile, lspDidOpen, lspHover, lspCompletion, lspDiagnostics, lspGoToDefinition, } from './lsp-manager.js';
|
|
13
|
+
/** 已检测到的 LSP 服务器列表 (缓存) */
|
|
14
|
+
let cachedSpecs = null;
|
|
15
|
+
async function getSpecs() {
|
|
16
|
+
if (!cachedSpecs)
|
|
17
|
+
cachedSpecs = await detectInstalledLspServers();
|
|
18
|
+
return cachedSpecs;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 注册 6 个 LSP 工具到 Bolloon agent 的工具系统.
|
|
22
|
+
* 在 registerBuiltinTools 内部调用.
|
|
23
|
+
*/
|
|
24
|
+
export function registerLspTools(ctx) {
|
|
25
|
+
ctx.tools.set('lsp_detect', {
|
|
26
|
+
name: 'lsp_detect',
|
|
27
|
+
description: '检测本机已安装的 LSP (Language Server Protocol) 服务器, 返回可用的语言列表',
|
|
28
|
+
parameters: {},
|
|
29
|
+
execute: async (_args) => {
|
|
30
|
+
try {
|
|
31
|
+
const specs = await getSpecs();
|
|
32
|
+
if (specs.length === 0) {
|
|
33
|
+
return { success: true, output: '⚠️ 未检测到任何 LSP 服务器。可安装:\n npm install -g typescript-language-server\n cargo install rust-analyzer' };
|
|
34
|
+
}
|
|
35
|
+
const lines = specs.map(s => ` ✅ ${s.displayName} (${s.language}): ${s.fileExtensions.join(', ')}`);
|
|
36
|
+
return { success: true, output: `已检测到 ${specs.length} 个 LSP 服务器:\n${lines.join('\n')}` };
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
return { success: false, error: String(e) };
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
ctx.tools.set('lsp_start', {
|
|
44
|
+
name: 'lsp_start',
|
|
45
|
+
description: '启动指定语言的语言服务器 (如 typescript / rust / python)',
|
|
46
|
+
parameters: { language: '语言 id, 如 typescript / rust / python (必填)' },
|
|
47
|
+
execute: async (args) => {
|
|
48
|
+
try {
|
|
49
|
+
const language = String(args.language || '').trim().toLowerCase();
|
|
50
|
+
if (!language)
|
|
51
|
+
return { success: false, error: 'language 必填' };
|
|
52
|
+
const inst = await startLspServer(language);
|
|
53
|
+
if (!inst)
|
|
54
|
+
return { success: false, error: `未找到 ${language} 的 LSP 服务器, 请先安装` };
|
|
55
|
+
return { success: true, output: `✅ ${inst.spec.displayName} 已启动` };
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
return { success: false, error: String(e) };
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
ctx.tools.set('lsp_hover', {
|
|
63
|
+
name: 'lsp_hover',
|
|
64
|
+
description: '在文件某位置悬停, 查看类型/文档 (需要先 lsp_start)',
|
|
65
|
+
parameters: {
|
|
66
|
+
file: '文件路径 (必填)',
|
|
67
|
+
line: '行号 (从 0 开始)',
|
|
68
|
+
character: '列号 (从 0 开始)',
|
|
69
|
+
content: '文件内容 (可选, 首次需要以触发 didOpen)',
|
|
70
|
+
},
|
|
71
|
+
execute: async (args) => {
|
|
72
|
+
try {
|
|
73
|
+
const file = String(args.file || '').trim();
|
|
74
|
+
if (!file)
|
|
75
|
+
return { success: false, error: 'file 必填' };
|
|
76
|
+
const line = parseInt(String(args.line || '0'), 10);
|
|
77
|
+
const character = parseInt(String(args.character || '0'), 10);
|
|
78
|
+
const content = args.content ? String(args.content) : undefined;
|
|
79
|
+
const specs = await getSpecs();
|
|
80
|
+
const spec = findLspForFile(file, specs);
|
|
81
|
+
if (!spec)
|
|
82
|
+
return { success: false, error: `未找到 ${file} 对应的 LSP 服务器` };
|
|
83
|
+
const inst = await startLspServer(spec.language);
|
|
84
|
+
if (!inst)
|
|
85
|
+
return { success: false, error: `LSP ${spec.language} 启动失败` };
|
|
86
|
+
if (content)
|
|
87
|
+
lspDidOpen(inst, file, content);
|
|
88
|
+
const result = await lspHover(inst, file, line, character);
|
|
89
|
+
if (!result)
|
|
90
|
+
return { success: true, output: '(无悬停信息)' };
|
|
91
|
+
return { success: true, output: result.contents };
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
return { success: false, error: String(e) };
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
ctx.tools.set('lsp_completion', {
|
|
99
|
+
name: 'lsp_completion',
|
|
100
|
+
description: '在文件某位置获取代码补全建议 (需要先 lsp_start)',
|
|
101
|
+
parameters: {
|
|
102
|
+
file: '文件路径 (必填)',
|
|
103
|
+
line: '行号 (从 0 开始)',
|
|
104
|
+
character: '列号 (从 0 开始)',
|
|
105
|
+
content: '文件内容 (可选)',
|
|
106
|
+
},
|
|
107
|
+
execute: async (args) => {
|
|
108
|
+
try {
|
|
109
|
+
const file = String(args.file || '').trim();
|
|
110
|
+
if (!file)
|
|
111
|
+
return { success: false, error: 'file 必填' };
|
|
112
|
+
const line = parseInt(String(args.line || '0'), 10);
|
|
113
|
+
const character = parseInt(String(args.character || '0'), 10);
|
|
114
|
+
const content = args.content ? String(args.content) : undefined;
|
|
115
|
+
const specs = await getSpecs();
|
|
116
|
+
const spec = findLspForFile(file, specs);
|
|
117
|
+
if (!spec)
|
|
118
|
+
return { success: false, error: `未找到 ${file} 对应的 LSP 服务器` };
|
|
119
|
+
const inst = await startLspServer(spec.language);
|
|
120
|
+
if (!inst)
|
|
121
|
+
return { success: false, error: `LSP ${spec.language} 启动失败` };
|
|
122
|
+
if (content)
|
|
123
|
+
lspDidOpen(inst, file, content);
|
|
124
|
+
const result = await lspCompletion(inst, file, line, character);
|
|
125
|
+
if (result.items.length === 0)
|
|
126
|
+
return { success: true, output: '(无补全建议)' };
|
|
127
|
+
const items = result.items.slice(0, 20).map(i => ` ${i.label}${i.detail ? ` — ${i.detail}` : ''}`);
|
|
128
|
+
return { success: true, output: `补全建议 (显示前 20 条):\n${items.join('\n')}` };
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
return { success: false, error: String(e) };
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
ctx.tools.set('lsp_diagnostics', {
|
|
136
|
+
name: 'lsp_diagnostics',
|
|
137
|
+
description: '获取文件的诊断结果 (错误/警告)',
|
|
138
|
+
parameters: { file: '文件路径 (必填)', content: '文件内容 (可选)' },
|
|
139
|
+
execute: async (args) => {
|
|
140
|
+
try {
|
|
141
|
+
const file = String(args.file || '').trim();
|
|
142
|
+
if (!file)
|
|
143
|
+
return { success: false, error: 'file 必填' };
|
|
144
|
+
const content = args.content ? String(args.content) : undefined;
|
|
145
|
+
const specs = await getSpecs();
|
|
146
|
+
const spec = findLspForFile(file, specs);
|
|
147
|
+
if (!spec)
|
|
148
|
+
return { success: false, error: `未找到 ${file} 对应的 LSP 服务器` };
|
|
149
|
+
const inst = await startLspServer(spec.language);
|
|
150
|
+
if (!inst)
|
|
151
|
+
return { success: false, error: `LSP ${spec.language} 启动失败` };
|
|
152
|
+
if (content)
|
|
153
|
+
lspDidOpen(inst, file, content);
|
|
154
|
+
const result = await lspDiagnostics(inst, file);
|
|
155
|
+
if (result.diagnostics.length === 0)
|
|
156
|
+
return { success: true, output: '✅ 无诊断问题' };
|
|
157
|
+
const lines = result.diagnostics.map(d => {
|
|
158
|
+
const sev = ['', '错误', '警告', '信息', '提示'][d.severity || 0] || '未知';
|
|
159
|
+
const pos = `L${d.range.start.line}:${d.range.start.character}`;
|
|
160
|
+
return ` ${sev === '错误' ? '❌' : sev === '警告' ? '⚠️' : 'ℹ️'} [${pos}] ${sev}: ${d.message}`;
|
|
161
|
+
});
|
|
162
|
+
return { success: true, output: `诊断结果 (${result.diagnostics.length} 条):\n${lines.join('\n')}` };
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
return { success: false, error: String(e) };
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
ctx.tools.set('lsp_go_to_definition', {
|
|
170
|
+
name: 'lsp_go_to_definition',
|
|
171
|
+
description: '跳转到符号定义的位置',
|
|
172
|
+
parameters: {
|
|
173
|
+
file: '当前文件路径 (必填)',
|
|
174
|
+
line: '行号 (从 0 开始)',
|
|
175
|
+
character: '列号 (从 0 开始)',
|
|
176
|
+
},
|
|
177
|
+
execute: async (args) => {
|
|
178
|
+
try {
|
|
179
|
+
const file = String(args.file || '').trim();
|
|
180
|
+
if (!file)
|
|
181
|
+
return { success: false, error: 'file 必填' };
|
|
182
|
+
const line = parseInt(String(args.line || '0'), 10);
|
|
183
|
+
const character = parseInt(String(args.character || '0'), 10);
|
|
184
|
+
const specs = await getSpecs();
|
|
185
|
+
const spec = findLspForFile(file, specs);
|
|
186
|
+
if (!spec)
|
|
187
|
+
return { success: false, error: `未找到 ${file} 对应的 LSP 服务器` };
|
|
188
|
+
const inst = await startLspServer(spec.language);
|
|
189
|
+
if (!inst)
|
|
190
|
+
return { success: false, error: `LSP ${spec.language} 启动失败` };
|
|
191
|
+
const loc = await lspGoToDefinition(inst, file, line, character);
|
|
192
|
+
if (!loc)
|
|
193
|
+
return { success: true, output: '(未找到定义位置)' };
|
|
194
|
+
return { success: true, output: `定义位置: ${loc.uri} (L${loc.range.start.line}:${loc.range.start.character})` };
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
return { success: false, error: String(e) };
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
ctx.tools.set('lsp_stop', {
|
|
202
|
+
name: 'lsp_stop',
|
|
203
|
+
description: '关闭指定语言的语言服务器 (或 all 关闭所有)',
|
|
204
|
+
parameters: { language: '语言 id, 如 typescript, 或 all 关闭全部' },
|
|
205
|
+
execute: async (args) => {
|
|
206
|
+
try {
|
|
207
|
+
const language = String(args.language || '').trim().toLowerCase();
|
|
208
|
+
if (!language)
|
|
209
|
+
return { success: false, error: 'language 必填 (或 "all")' };
|
|
210
|
+
if (language === 'all') {
|
|
211
|
+
stopAllLspServers();
|
|
212
|
+
return { success: true, output: '✅ 所有 LSP 服务器已关闭' };
|
|
213
|
+
}
|
|
214
|
+
await stopLspServer(language);
|
|
215
|
+
return { success: true, output: `✅ ${language} LSP 服务器已关闭` };
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
return { success: false, error: String(e) };
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auto-peer-discovery.ts — 基于 Hyperswarm topic 的 P2P 自动发现
|
|
3
|
+
*
|
|
4
|
+
* 当 P2PDirect 通过 topic 发现新 peer 时,自动将其加入 known_peers 好友列表,
|
|
5
|
+
* 无需用户手动添加好友。
|
|
6
|
+
*
|
|
7
|
+
* 工作流程:
|
|
8
|
+
* 1. P2PDirect 加入 topic(如 'bolloon-agent-harness')
|
|
9
|
+
* 2. Hyperswarm DHT 自动发现同 topic 的其他节点
|
|
10
|
+
* 3. P2PDirect 触发 connection 事件
|
|
11
|
+
* 4. 本模块检查该 publicKey 是否已在 known_peers
|
|
12
|
+
* 5. 不在 → 自动添加(后续 AgentHeartbeat 的 getPeers 就会看到它)
|
|
13
|
+
* 6. 如果在 → 更新 lastConnectedAt(已经在 known-peers 做的)
|
|
14
|
+
*
|
|
15
|
+
* 不依赖 Web UI、不依赖用户交互、纯后台自动完成。
|
|
16
|
+
*/
|
|
17
|
+
// 不使用 logger 模块,用 console.log 保持与 bolloon 项目风格一致
|
|
18
|
+
const PREFIX = '[auto-discover]';
|
|
19
|
+
// 发现 peers 的缓存 key → 本机 publicKey 映射(用于去重)
|
|
20
|
+
const TOPIC = 'bolloon-agent-harness';
|
|
21
|
+
/**
|
|
22
|
+
* 尝试将一个通过 topic 发现的 peer 自动加入好友列表
|
|
23
|
+
*
|
|
24
|
+
* 核心逻辑:
|
|
25
|
+
* - 跳过自己(P2PDirect 可能把自己也当 peer push 过来)
|
|
26
|
+
* - 跳过已存在的好友(仅标记连接时间)
|
|
27
|
+
* - 新 peer → 自动生成名字并添加
|
|
28
|
+
*
|
|
29
|
+
* @param remotePublicKey 远端 64 字符 hex publicKey
|
|
30
|
+
* @param knownPeers { addOrUpdatePeer, listPeers, findNameByPublicKey } 接口
|
|
31
|
+
* @param localPublicKey 本机 publicKey(用于跳过自己)
|
|
32
|
+
* @returns 是否是新添加的 peer
|
|
33
|
+
*/
|
|
34
|
+
export async function tryAutoDiscoverPeer(remotePublicKey, localPublicKey) {
|
|
35
|
+
// 1. 跳过自己
|
|
36
|
+
if (remotePublicKey === localPublicKey) {
|
|
37
|
+
return 'self';
|
|
38
|
+
}
|
|
39
|
+
// 2. 动态 import known-peers(避免循环依赖)
|
|
40
|
+
const { findNameByPublicKey, addOrUpdatePeer } = await import('../network/known-peers.js');
|
|
41
|
+
// 3. 检查是否已存在
|
|
42
|
+
const existingName = await findNameByPublicKey(remotePublicKey);
|
|
43
|
+
if (existingName) {
|
|
44
|
+
// 已经在好友列表里了,只是更新连接状态
|
|
45
|
+
console.log(`[auto-discover] 已知好友 ${existingName} (${remotePublicKey.substring(0, 12)}...) 重新上线`);
|
|
46
|
+
return 'known';
|
|
47
|
+
}
|
|
48
|
+
// 4. 新 peer!自动添加
|
|
49
|
+
const shortId = remotePublicKey.substring(0, 8);
|
|
50
|
+
const autoName = `discovered-${shortId}`;
|
|
51
|
+
await addOrUpdatePeer(autoName, remotePublicKey, `通过 topic "${TOPIC}" 自动发现`);
|
|
52
|
+
console.log(`✅ [auto-discover] 自动发现新 peer: ${autoName} (${remotePublicKey.substring(0, 12)}...)`);
|
|
53
|
+
console.log(` 已添加到 known_peers, AgentHeartbeat 将自动开始社交`);
|
|
54
|
+
return 'discovered';
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 从 peer 的 manifest 中提取名字,更新自动发现的 peer 名称
|
|
58
|
+
*
|
|
59
|
+
* 如果 peer 是 auto-discovered(名字为 discovered-xxxx),
|
|
60
|
+
* 且 manifest 中有 ownerName,就把名字更新为更友好的格式。
|
|
61
|
+
*
|
|
62
|
+
* @param remotePublicKey 远端 publicKey
|
|
63
|
+
* @param ownerName manifest 中的 ownerName
|
|
64
|
+
*/
|
|
65
|
+
export async function updateDiscoveredPeerName(remotePublicKey, ownerName) {
|
|
66
|
+
if (!ownerName)
|
|
67
|
+
return;
|
|
68
|
+
const { findNameByPublicKey, addOrUpdatePeer, removePeer } = await import('../network/known-peers.js');
|
|
69
|
+
const existingName = await findNameByPublicKey(remotePublicKey);
|
|
70
|
+
// 只有 auto-discovered 的 peer 才重命名
|
|
71
|
+
if (!existingName || !existingName.startsWith('discovered-'))
|
|
72
|
+
return;
|
|
73
|
+
// 删除旧名字的条目,用新名字重新添加
|
|
74
|
+
await removePeer(existingName);
|
|
75
|
+
await addOrUpdatePeer(ownerName, remotePublicKey, `自动发现后通过 manifest 获取名称`);
|
|
76
|
+
console.log(`[auto-discover] 更新 peer 名称: ${existingName} → ${ownerName}`);
|
|
77
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* did-agent-resolver.ts — 从 DIAP DID 文档解析并落盘远端智能体
|
|
3
|
+
*
|
|
4
|
+
* 流程:
|
|
5
|
+
* 1. 收到 peer manifest → 有 agents[i].cid 或 agents[i].ipnsName
|
|
6
|
+
* 2. 通过 IPFS 公共网关读取 DID 文档(纯只读,不需要上传能力)
|
|
7
|
+
* 3. 解析 DID 文档 → 提取 agent 身份信息(DID、公钥、服务端点)
|
|
8
|
+
* 4. 把解析后的 agent 写入本地 peers/<pk>/agents/<agentId>.md
|
|
9
|
+
* 5. 把 DID 身份信息用于更丰富的好友添加(用 DID 名而不是 generated name)
|
|
10
|
+
*/
|
|
11
|
+
import { IpfsClient } from '@diap/sdk';
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// 读取 DID 文档
|
|
14
|
+
// ============================================================================
|
|
15
|
+
/**
|
|
16
|
+
* 通过 IPFS 公共网关读取 DID 文档
|
|
17
|
+
* 使用 @diap/sdk 的 IpfsClient(public-only 只读模式)
|
|
18
|
+
*/
|
|
19
|
+
async function fetchDIDDocument(cid) {
|
|
20
|
+
const ipfs = await IpfsClient.newPublicOnly(15);
|
|
21
|
+
try {
|
|
22
|
+
const content = await ipfs.get(cid);
|
|
23
|
+
const doc = JSON.parse(content);
|
|
24
|
+
// 基本校验:是合法的 DID 文档
|
|
25
|
+
if (!doc || !doc.id || !doc['@context']) {
|
|
26
|
+
console.warn(`[did-resolver] CID ${cid} 不是有效的 DID 文档(缺 id 或 @context)`);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return doc;
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
console.warn(`[did-resolver] 从 CID ${cid} 获取 DID 文档失败: ${e.message?.substring(0, 100) || e}`);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 通过公共网关解析 IPNS 名称 → CID
|
|
38
|
+
* 大多数公共网关不支持 IPNS 解析,这是尽力而为
|
|
39
|
+
* 真实环境建议用本地 Kubo 节点
|
|
40
|
+
*/
|
|
41
|
+
async function resolveIPNSToCid(ipnsName) {
|
|
42
|
+
const gateways = [
|
|
43
|
+
`https://ipfs.io/ipns/${ipnsName}`,
|
|
44
|
+
`https://dweb.link/ipns/${ipnsName}`,
|
|
45
|
+
];
|
|
46
|
+
for (const url of gateways) {
|
|
47
|
+
try {
|
|
48
|
+
const resp = await fetch(url, {
|
|
49
|
+
method: 'HEAD',
|
|
50
|
+
signal: AbortSignal.timeout(5000),
|
|
51
|
+
});
|
|
52
|
+
// IPFS 网关会在响应头中返回 x-ipfs-path 或 x-ipfs-roots
|
|
53
|
+
const ipfsPath = resp.headers.get('x-ipfs-path') || resp.headers.get('x-ipfs-roots');
|
|
54
|
+
if (ipfsPath) {
|
|
55
|
+
const cid = ipfsPath.replace('/ipfs/', '').trim();
|
|
56
|
+
if (cid)
|
|
57
|
+
return cid;
|
|
58
|
+
}
|
|
59
|
+
// 也可以直接 GET
|
|
60
|
+
if (resp.ok) {
|
|
61
|
+
const body = await resp.text();
|
|
62
|
+
try {
|
|
63
|
+
const doc = JSON.parse(body);
|
|
64
|
+
if (doc.id && doc['@context'])
|
|
65
|
+
return ipnsName; // 已经拿到内容,用原始 IPNS 名当 key
|
|
66
|
+
}
|
|
67
|
+
catch { /* not JSON, ignore */ }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch { /* timeout or network error, try next */ }
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// 解析 DID 文档 → ResolvedDIDAgent
|
|
76
|
+
// ============================================================================
|
|
77
|
+
/**
|
|
78
|
+
* 从 DID 文档解析 agent 身份信息
|
|
79
|
+
*/
|
|
80
|
+
function parseDIDDocument(doc, cid, ipnsName) {
|
|
81
|
+
const did = doc.id || '';
|
|
82
|
+
// 从 DID 提取友好名:did:key:z6Mk... → 取后 8 位
|
|
83
|
+
const shortDid = did.includes(':') ? did.split(':').pop() || did : did;
|
|
84
|
+
const name = shortDid.substring(0, 8);
|
|
85
|
+
// 提取公钥
|
|
86
|
+
const vm = doc.verificationMethod?.[0];
|
|
87
|
+
const publicKeyMultibase = vm?.publicKeyMultibase;
|
|
88
|
+
// 提取服务端点
|
|
89
|
+
const services = [];
|
|
90
|
+
if (Array.isArray(doc.service)) {
|
|
91
|
+
for (const s of doc.service) {
|
|
92
|
+
const ep = typeof s.serviceEndpoint === 'string'
|
|
93
|
+
? s.serviceEndpoint
|
|
94
|
+
: JSON.stringify(s.serviceEndpoint);
|
|
95
|
+
services.push({ type: s.type || 'unknown', endpoint: ep });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { did, name, publicKeyMultibase, services, cid, ipnsName };
|
|
99
|
+
}
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// 主入口
|
|
102
|
+
// ============================================================================
|
|
103
|
+
/**
|
|
104
|
+
* 从 CID 读取并解析 agent 的 DID 文档
|
|
105
|
+
*
|
|
106
|
+
* @param cid IPFS CID
|
|
107
|
+
* @returns 解析结果或 null
|
|
108
|
+
*/
|
|
109
|
+
export async function resolveAgentFromCID(cid) {
|
|
110
|
+
const doc = await fetchDIDDocument(cid);
|
|
111
|
+
if (!doc)
|
|
112
|
+
return null;
|
|
113
|
+
return parseDIDDocument(doc, cid);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 通过 IPNS 名称解析 agent 的 DID 文档
|
|
117
|
+
* 先尝试 IPNS → CID 解析,再用 CID 获取文档
|
|
118
|
+
*
|
|
119
|
+
* @param ipnsName IPNS 名称
|
|
120
|
+
* @returns 解析结果或 null
|
|
121
|
+
*/
|
|
122
|
+
export async function resolveAgentFromIPNS(ipnsName) {
|
|
123
|
+
// 先直接尝试用 IPNS 名当 CID 读取(部分场景 IPNS 名 = CIDv1 字符串)
|
|
124
|
+
const directDoc = await fetchDIDDocument(ipnsName);
|
|
125
|
+
if (directDoc)
|
|
126
|
+
return parseDIDDocument(directDoc, ipnsName, ipnsName);
|
|
127
|
+
// 再尝试 IPNS 解析
|
|
128
|
+
const cid = await resolveIPNSToCid(ipnsName);
|
|
129
|
+
if (cid) {
|
|
130
|
+
const doc = await fetchDIDDocument(cid);
|
|
131
|
+
if (doc)
|
|
132
|
+
return parseDIDDocument(doc, cid, ipnsName);
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* 从单个 AgentManifestEntry 解析 agent
|
|
138
|
+
* 优先用 CID,回退 IPNS
|
|
139
|
+
*/
|
|
140
|
+
export async function resolveAgentFromEntry(entry) {
|
|
141
|
+
if (entry.cid) {
|
|
142
|
+
const r = await resolveAgentFromCID(entry.cid);
|
|
143
|
+
if (r)
|
|
144
|
+
return r;
|
|
145
|
+
}
|
|
146
|
+
if (entry.ipnsName) {
|
|
147
|
+
const r = await resolveAgentFromIPNS(entry.ipnsName);
|
|
148
|
+
if (r)
|
|
149
|
+
return r;
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* 批量解析 peer manifest 中的 agents
|
|
155
|
+
* 对每个有 cid/ipnsName 的 agent 尝试解析 DID 文档
|
|
156
|
+
* 返回成功解析的列表
|
|
157
|
+
*/
|
|
158
|
+
export async function resolveAgentsFromManifest(agents) {
|
|
159
|
+
const results = [];
|
|
160
|
+
for (const agent of agents) {
|
|
161
|
+
if (!agent.cid && !agent.ipnsName)
|
|
162
|
+
continue;
|
|
163
|
+
const r = await resolveAgentFromEntry(agent);
|
|
164
|
+
if (r) {
|
|
165
|
+
results.push(r);
|
|
166
|
+
console.log(`[did-resolver] ✅ 解析 agent ${agent.name || agent.id}: DID=${r.did ? r.did.substring(0, 20) + '...' : 'N/A'}`);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
console.log(`[did-resolver] ⚠️ agent ${agent.name || agent.id} 没有可用的 cid/ipnsName,跳过 DID 解析`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return results;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 把解析后的 DID agent 写入本地 peer-fs 目录
|
|
176
|
+
* 同时更新 known_peers 中的 name 为 DID 文档中的名字
|
|
177
|
+
*/
|
|
178
|
+
export async function persistResolvedAgent(peerPublicKey, resolved) {
|
|
179
|
+
try {
|
|
180
|
+
const { writeAgentDescription } = await import('../network/peer-fs.js');
|
|
181
|
+
const { addOrUpdatePeer } = await import('../network/known-peers.js');
|
|
182
|
+
// 1. 写入 agent 详细描述到 peers/<pk>/agents/<id>.md
|
|
183
|
+
const agentEntry = {
|
|
184
|
+
id: resolved.did,
|
|
185
|
+
name: resolved.name,
|
|
186
|
+
capabilities: resolved.services.map(s => s.type),
|
|
187
|
+
status: 'active',
|
|
188
|
+
cid: resolved.cid,
|
|
189
|
+
ipnsName: resolved.ipnsName,
|
|
190
|
+
};
|
|
191
|
+
await writeAgentDescription(peerPublicKey, agentEntry);
|
|
192
|
+
console.log(`[did-resolver] 📝 agent 已落盘: ${resolved.name} (${resolved.did?.substring(0, 20)}... → peers/${peerPublicKey.substring(0, 8)}.../agents/)`);
|
|
193
|
+
// 2. 如果 peer 是 auto-discovered 的,用 DID 名更新好友名称
|
|
194
|
+
const { findNameByPublicKey, removePeer } = await import('../network/known-peers.js');
|
|
195
|
+
const existingName = await findNameByPublicKey(peerPublicKey);
|
|
196
|
+
if (existingName && existingName.startsWith('discovered-')) {
|
|
197
|
+
const newName = `did-${resolved.name}`;
|
|
198
|
+
await removePeer(existingName);
|
|
199
|
+
await addOrUpdatePeer(newName, peerPublicKey, `DID: ${resolved.did}`);
|
|
200
|
+
console.log(`[did-resolver] 📛 更新 peer 名称: ${existingName} → ${newName}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch (e) {
|
|
204
|
+
console.warn(`[did-resolver] 持久化 agent 失败:`, e.message?.substring(0, 100) || e);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -164,7 +164,7 @@ export class P2PDirect extends EventEmitter {
|
|
|
164
164
|
* 上层调用: const r = await p2p.sendToWithWait(pk, rpc, 5000);
|
|
165
165
|
* if (r !== 'SENT') return 502 给前端.
|
|
166
166
|
*/
|
|
167
|
-
async sendToWithWait(publicKeyHex, data, timeoutMs =
|
|
167
|
+
async sendToWithWait(publicKeyHex, data, timeoutMs = 15000) {
|
|
168
168
|
// 2026-06-11: 先主动触发 joinPeer, 否则 DHT 上对面可能没 push conn
|
|
169
169
|
if (this.swarm) {
|
|
170
170
|
try {
|
|
@@ -25,7 +25,7 @@ export async function sendOrQueue(publicKey, op, payload, p2p, opts = {}) {
|
|
|
25
25
|
// 先尝试直发
|
|
26
26
|
if (!opts.forceQueue && p2p) {
|
|
27
27
|
try {
|
|
28
|
-
const r = await p2p.sendToWithWait(publicKey, rpc,
|
|
28
|
+
const r = await p2p.sendToWithWait(publicKey, rpc, 15000);
|
|
29
29
|
if (r === 'SENT')
|
|
30
30
|
return 'SENT';
|
|
31
31
|
}
|
|
@@ -74,7 +74,7 @@ export async function flushAllOutboxes(p2p) {
|
|
|
74
74
|
for (const entry of outbox) {
|
|
75
75
|
const rpc = JSON.stringify({ v: 3, op: entry.op, payload: entry.payload });
|
|
76
76
|
try {
|
|
77
|
-
const r = await p2p.sendToWithWait(peer.publicKey, rpc,
|
|
77
|
+
const r = await p2p.sendToWithWait(peer.publicKey, rpc, 15000);
|
|
78
78
|
if (r === 'SENT') {
|
|
79
79
|
sent++;
|
|
80
80
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* npm 自动更新检查器
|
|
4
3
|
*
|
|
@@ -14,48 +13,11 @@
|
|
|
14
13
|
* 开发态下若 cwd 的 package.json 就是本包则回退到 cwd,不受任意工作目录影响。
|
|
15
14
|
* - 检查频率受节流缓存约束(默认 24h 一次),不会每次启动都打 npm。
|
|
16
15
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
Object.defineProperty(o, k2, desc);
|
|
24
|
-
}) : (function(o, m, k, k2) {
|
|
25
|
-
if (k2 === undefined) k2 = k;
|
|
26
|
-
o[k2] = m[k];
|
|
27
|
-
}));
|
|
28
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
-
}) : function(o, v) {
|
|
31
|
-
o["default"] = v;
|
|
32
|
-
});
|
|
33
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
-
var ownKeys = function(o) {
|
|
35
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
-
var ar = [];
|
|
37
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
-
return ar;
|
|
39
|
-
};
|
|
40
|
-
return ownKeys(o);
|
|
41
|
-
};
|
|
42
|
-
return function (mod) {
|
|
43
|
-
if (mod && mod.__esModule) return mod;
|
|
44
|
-
var result = {};
|
|
45
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
-
__setModuleDefault(result, mod);
|
|
47
|
-
return result;
|
|
48
|
-
};
|
|
49
|
-
})();
|
|
50
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
-
exports.checkAndUpdate = checkAndUpdate;
|
|
52
|
-
exports.checkForUpdates = checkForUpdates;
|
|
53
|
-
exports.performUpdate = performUpdate;
|
|
54
|
-
const child_process_1 = require("child_process");
|
|
55
|
-
const fs = __importStar(require("fs"));
|
|
56
|
-
const path = __importStar(require("path"));
|
|
57
|
-
const https = __importStar(require("https"));
|
|
58
|
-
const http = __importStar(require("http"));
|
|
16
|
+
import { execSync } from 'child_process';
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import * as https from 'https';
|
|
20
|
+
import * as http from 'http';
|
|
59
21
|
// ANSI 颜色
|
|
60
22
|
const RESET = '\x1b[0m';
|
|
61
23
|
const BOLD = '\x1b[1m';
|
|
@@ -113,7 +75,7 @@ function getGlobalBolloonDir() {
|
|
|
113
75
|
const candidates = [];
|
|
114
76
|
// 1. npm root -g(最可靠)
|
|
115
77
|
try {
|
|
116
|
-
const npmRoot =
|
|
78
|
+
const npmRoot = execSync('npm root -g', { encoding: 'utf-8', timeout: 8000 }).trim();
|
|
117
79
|
if (npmRoot)
|
|
118
80
|
candidates.push(path.join(npmRoot, '@bolloon', 'bolloon-agent'));
|
|
119
81
|
}
|
|
@@ -122,7 +84,7 @@ function getGlobalBolloonDir() {
|
|
|
122
84
|
}
|
|
123
85
|
// 2. npm prefix -g
|
|
124
86
|
try {
|
|
125
|
-
const npmPrefix =
|
|
87
|
+
const npmPrefix = execSync('npm prefix -g', { encoding: 'utf-8', timeout: 8000 }).trim();
|
|
126
88
|
if (npmPrefix)
|
|
127
89
|
candidates.push(path.join(npmPrefix, 'lib', 'node_modules', '@bolloon', 'bolloon-agent'));
|
|
128
90
|
}
|
|
@@ -280,7 +242,7 @@ async function checkBolloonUpdates() {
|
|
|
280
242
|
*/
|
|
281
243
|
function checkNpmOutdated() {
|
|
282
244
|
try {
|
|
283
|
-
const output =
|
|
245
|
+
const output = execSync('npm outdated --json', {
|
|
284
246
|
encoding: 'utf-8',
|
|
285
247
|
timeout: 30000,
|
|
286
248
|
maxBuffer: 10 * 1024 * 1024,
|
|
@@ -344,7 +306,7 @@ async function updatePackagesWithVersion(packagesWithVersion) {
|
|
|
344
306
|
const args = ['npm', 'install', '-g', ...packagesWithVersion];
|
|
345
307
|
notify(`\n${CYAN}📦 正在更新包...${RESET}\n`, RESET);
|
|
346
308
|
try {
|
|
347
|
-
|
|
309
|
+
execSync(args.join(' '), {
|
|
348
310
|
encoding: 'utf-8',
|
|
349
311
|
timeout: 300000,
|
|
350
312
|
stdio: 'inherit',
|
|
@@ -462,7 +424,7 @@ function resolveAutoUpdatePolicy() {
|
|
|
462
424
|
* 例如 Electron 用 app.relaunch(),Node 用 detached 重新 spawn)。
|
|
463
425
|
* 若未提供或 autoRestart=false,则仅提示用户手动重启。
|
|
464
426
|
*/
|
|
465
|
-
async function checkAndUpdate(opts = {}) {
|
|
427
|
+
export async function checkAndUpdate(opts = {}) {
|
|
466
428
|
const policy = resolveAutoUpdatePolicy();
|
|
467
429
|
if (policy.blocked) {
|
|
468
430
|
return { hasUpdate: false, info: null, updated: false, message: '跳过更新检查(已显式禁用)' };
|
|
@@ -566,13 +528,13 @@ async function checkAndUpdate(opts = {}) {
|
|
|
566
528
|
/**
|
|
567
529
|
* 仅检查更新,不自动安装
|
|
568
530
|
*/
|
|
569
|
-
async function checkForUpdates() {
|
|
531
|
+
export async function checkForUpdates() {
|
|
570
532
|
return await checkBolloonUpdates();
|
|
571
533
|
}
|
|
572
534
|
/**
|
|
573
535
|
* 手动触发更新
|
|
574
536
|
*/
|
|
575
|
-
async function performUpdate(packages) {
|
|
537
|
+
export async function performUpdate(packages) {
|
|
576
538
|
return await updatePackages(packages);
|
|
577
539
|
}
|
|
578
540
|
// CLI 入口
|
|
@@ -595,4 +557,3 @@ if (process.argv[1]?.includes('auto-update')) {
|
|
|
595
557
|
}
|
|
596
558
|
})();
|
|
597
559
|
}
|
|
598
|
-
//# sourceMappingURL=auto-update.js.map
|