@bolloon/bolloon-agent 0.4.22 → 0.4.23
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/gateway-join.js +271 -0
- package/dist/agents/pi-sdk-tools.js +75 -2
- package/dist/agents/pi-sdk.js +6 -3
- package/dist/llm/pi-ai.js +35 -2
- package/dist/network/agent-network.js +78 -6
- package/dist/web/mobile-core.js +17 -0
- package/dist/web/mobile.css +24 -0
- package/dist/web/mobile.html +23 -14
- package/dist/web/mobile.js +439 -45
- package/dist/web/server.js +67 -9
- package/dist/web/sw.js +26 -2
- package/package.json +1 -1
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gateway-join.ts — 文档驱动的「加入全球智能体网络」编排 (2026-09-15)
|
|
3
|
+
*
|
|
4
|
+
* 背景: 手机端 / PC 端的人类入口只有一句话 —— `read https://bolloon.cn/bolloon-gateway-join.md`
|
|
5
|
+
* (默认 prompt, 见 src/web/mobile.js DEFAULT_JOIN_PROMPT)。文档本身是 SKILL.md 形状的入网说明,
|
|
6
|
+
* 但文档第 1-8 节写的是 SDK 伪码 (KeyManager / p2pNetwork.createNode / buildManifestRequest),
|
|
7
|
+
* 智能体手里只有工具, 照抄不了 → 需要一个工具把整条链路真正串起来。
|
|
8
|
+
*
|
|
9
|
+
* 本模块把「读文档 → 入网」收成一个可验证的闭环:
|
|
10
|
+
* ① 读入网说明 (http(s), 校验 frontmatter 是 bolloon-gateway-join)
|
|
11
|
+
* ② 确保 DID 身份
|
|
12
|
+
* ③ 确保 P2P 节点 (拿 peerId)
|
|
13
|
+
* ④ 注册本地 manifest (文档第 3 节 /api/agent/register 的等效物: setLocalManifest)
|
|
14
|
+
* ⑤ 建成可分享的网络 (orbitdb registry 链接) + 登记到本机 Agent 服务注册表
|
|
15
|
+
* ⑥ 落盘入网态 (幂等: 同 url 重复入网 → already)
|
|
16
|
+
*
|
|
17
|
+
* 诚实原则: 每一步都记 step.note; 拿不到的 (文档不可达 / 非入网说明 / 节点未就绪 / registry 离线)
|
|
18
|
+
* 一律如实报 ok=false 或 step.ok=false, 不假装入网成功。
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from 'fs';
|
|
21
|
+
import * as os from 'os';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
export const DEFAULT_GATEWAY_JOIN_DOC = 'https://bolloon.cn/bolloon-gateway-join.md';
|
|
24
|
+
/** 入网说明文档的识别名 (frontmatter name) / 能力标记 */
|
|
25
|
+
export const GATEWAY_JOIN_DOC_NAME = 'bolloon-gateway-join';
|
|
26
|
+
export const GATEWAY_JOIN_CAPABILITY = 'gateway-join';
|
|
27
|
+
const home = () => process.env.HOME || os.homedir() || '/tmp';
|
|
28
|
+
const STATE_FILE = () => path.join(home(), '.bolloon', 'gateway-join.json');
|
|
29
|
+
/** 极简 frontmatter 解析 (只认 name/version/capabilities 三个键, 够用且不引 yaml 依赖) */
|
|
30
|
+
export function parseSkillFrontmatter(text) {
|
|
31
|
+
const raw = String(text || '');
|
|
32
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(raw);
|
|
33
|
+
if (!m)
|
|
34
|
+
return { body: raw };
|
|
35
|
+
const head = m[1];
|
|
36
|
+
const body = raw.slice(m[0].length);
|
|
37
|
+
const out = {};
|
|
38
|
+
for (const line of head.split(/\r?\n/)) {
|
|
39
|
+
const kv = /^([A-Za-z_][\w-]*)\s*:\s*(.*)$/.exec(line.trim());
|
|
40
|
+
if (!kv)
|
|
41
|
+
continue;
|
|
42
|
+
const key = kv[1].toLowerCase();
|
|
43
|
+
const val = kv[2].trim();
|
|
44
|
+
if (key === 'name')
|
|
45
|
+
out.name = val.replace(/^["']|["']$/g, '');
|
|
46
|
+
else if (key === 'version')
|
|
47
|
+
out.version = val.replace(/^["']|["']$/g, '');
|
|
48
|
+
else if (key === 'capabilities') {
|
|
49
|
+
out.capabilities = val.replace(/^\[|\]$/g, '').split(',').map((s) => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { ...out, body };
|
|
53
|
+
}
|
|
54
|
+
/** 从文档文本判定「这是不是入网说明」 */
|
|
55
|
+
export function isGatewayJoinDoc(text) {
|
|
56
|
+
const fm = parseSkillFrontmatter(text);
|
|
57
|
+
if (fm.name === GATEWAY_JOIN_DOC_NAME)
|
|
58
|
+
return true;
|
|
59
|
+
if (fm.capabilities?.includes(GATEWAY_JOIN_CAPABILITY))
|
|
60
|
+
return true;
|
|
61
|
+
// 没有 frontmatter 也允许 (纯正文版本): 标题 + 关键步骤同时出现才算
|
|
62
|
+
return /加入网关|加入 Bolloon|bolloon-gateway-join/.test(text) && /api\/agent\/register/.test(text);
|
|
63
|
+
}
|
|
64
|
+
export async function readGatewaySkillDoc(url, opts = {}) {
|
|
65
|
+
const u = String(url || '').trim();
|
|
66
|
+
if (!/^https?:\/\//i.test(u))
|
|
67
|
+
return { ok: false, url: u, error: `url 必须以 http(s):// 开头 (收到: ${u.slice(0, 60)})` };
|
|
68
|
+
let text = opts.text;
|
|
69
|
+
if (text === undefined) {
|
|
70
|
+
const f = opts.fetchImpl || fetch;
|
|
71
|
+
try {
|
|
72
|
+
const r = await f(u, { signal: AbortSignal.timeout(opts.timeoutMs ?? 15000) });
|
|
73
|
+
if (!r.ok)
|
|
74
|
+
return { ok: false, url: u, error: `文档不可达 (HTTP ${r.status})` };
|
|
75
|
+
text = await r.text();
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
return { ok: false, url: u, error: `文档不可达: ${String(e?.message || e).slice(0, 140)}` };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const raw = String(text || '');
|
|
82
|
+
if (!raw.trim())
|
|
83
|
+
return { ok: false, url: u, error: '文档为空' };
|
|
84
|
+
const fm = parseSkillFrontmatter(raw);
|
|
85
|
+
if (!isGatewayJoinDoc(raw)) {
|
|
86
|
+
return { ok: false, url: u, name: fm.name, error: '这不是 Bolloon 网关入网说明 (缺 name: bolloon-gateway-join), 拒绝据此入网' };
|
|
87
|
+
}
|
|
88
|
+
return { ok: true, url: u, name: fm.name, version: fm.version, capabilities: fm.capabilities, body: fm.body };
|
|
89
|
+
}
|
|
90
|
+
export async function getGatewayJoinState() {
|
|
91
|
+
try {
|
|
92
|
+
const raw = fs.readFileSync(STATE_FILE(), 'utf-8');
|
|
93
|
+
const parsed = JSON.parse(raw);
|
|
94
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function writeGatewayJoinState(state) {
|
|
101
|
+
try {
|
|
102
|
+
const f = STATE_FILE();
|
|
103
|
+
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
104
|
+
fs.writeFileSync(f, JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
105
|
+
}
|
|
106
|
+
catch { /* 落盘失败不影响本次入网, 只影响幂等/重启恢复 */ }
|
|
107
|
+
}
|
|
108
|
+
/** 节点初始化并发去重 (同一进程内只起一次 libp2p 节点) */
|
|
109
|
+
let nodeInitPromise = null;
|
|
110
|
+
/**
|
|
111
|
+
* 确保本机 libp2p 节点在跑 (文档第 2 节 p2pNetwork.createNode 的等价物)。
|
|
112
|
+
* p2pNetwork.createNode 不是幂等的, 所以先用 getNodePeerId() 探测 + 单飞 promise 兜住并发。
|
|
113
|
+
*/
|
|
114
|
+
export async function ensureGatewayNode(deps = {}) {
|
|
115
|
+
if (deps.peerId)
|
|
116
|
+
return { ok: true, peerId: deps.peerId, started: false };
|
|
117
|
+
try {
|
|
118
|
+
const { p2pNetwork } = await import('../network/p2p.js');
|
|
119
|
+
const existing = String(p2pNetwork.getNodePeerId?.() || '');
|
|
120
|
+
if (existing)
|
|
121
|
+
return { ok: true, peerId: existing, multiaddrs: p2pNetwork.getWsMultiaddrs?.() || [], started: false };
|
|
122
|
+
if (!nodeInitPromise) {
|
|
123
|
+
const timeoutMs = deps.nodeTimeoutMs ?? 25_000;
|
|
124
|
+
nodeInitPromise = (async () => {
|
|
125
|
+
try {
|
|
126
|
+
const info = await Promise.race([
|
|
127
|
+
p2pNetwork.createNode({
|
|
128
|
+
enableRelay: true,
|
|
129
|
+
enableRelayServer: true,
|
|
130
|
+
enableAutoNat: deps.enableAutoNat ?? false,
|
|
131
|
+
enableUPnP: deps.enableUPnP ?? false,
|
|
132
|
+
}),
|
|
133
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error(`节点初始化超时 (${timeoutMs}ms)`)), timeoutMs)),
|
|
134
|
+
]);
|
|
135
|
+
const peerId = String(info?.peerId || p2pNetwork.getNodePeerId?.() || '');
|
|
136
|
+
return peerId
|
|
137
|
+
? { ok: true, peerId, multiaddrs: (info?.multiaddrs || []) }
|
|
138
|
+
: { ok: false, error: '节点起来了但拿不到 peerId' };
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
return { ok: false, error: String(e?.message || e).slice(0, 160) };
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
// 失败也允许下次重试 (成功时后续调用被 getNodePeerId 分支短路)
|
|
145
|
+
setTimeout(() => { nodeInitPromise = null; }, 1000);
|
|
146
|
+
}
|
|
147
|
+
})();
|
|
148
|
+
}
|
|
149
|
+
const r = await nodeInitPromise;
|
|
150
|
+
return { ...r, started: !!r.ok };
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
return { ok: false, error: String(e?.message || e).slice(0, 160) };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function readUserDid() {
|
|
157
|
+
try {
|
|
158
|
+
const f = path.join(home(), '.bolloon', 'identity', 'user.json');
|
|
159
|
+
const parsed = JSON.parse(fs.readFileSync(f, 'utf-8'));
|
|
160
|
+
return String(parsed?.did || '');
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return '';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 文档驱动入网: 读说明 → DID → 节点 → manifest → 建网 → 落盘。
|
|
168
|
+
* 核心三步 (文档/DID/manifest) 任一失败 → ok=false; 网络/服务登记失败只记 note (部分成功不谎报)。
|
|
169
|
+
*/
|
|
170
|
+
export async function joinGlobalGateway(opts = {}) {
|
|
171
|
+
const url = String(opts.url || DEFAULT_GATEWAY_JOIN_DOC).trim();
|
|
172
|
+
const deps = opts.deps || {};
|
|
173
|
+
const steps = [];
|
|
174
|
+
// 幂等: 已经按同一文档入过网 → already (不重复建, 但仍返回既有信息)
|
|
175
|
+
const prev = await getGatewayJoinState();
|
|
176
|
+
if (prev && prev.url === url && prev.did && !opts.force) {
|
|
177
|
+
return {
|
|
178
|
+
ok: true, already: true, url, did: prev.did, peerId: prev.peerId,
|
|
179
|
+
networkLink: prev.networkLink, networkId: prev.networkId,
|
|
180
|
+
steps: [{ step: '幂等', ok: true, note: `已于 ${prev.joinedAt} 入网 (${prev.did.slice(0, 24)}…)` }],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
// ① 读入网说明
|
|
184
|
+
const doc = await readGatewaySkillDoc(url, { fetchImpl: deps.fetchImpl, text: deps.docText });
|
|
185
|
+
steps.push({ step: '读入网说明', ok: doc.ok, note: doc.ok ? `${doc.name || 'doc'} v${doc.version || '?'} (${(doc.body || '').length} 字符)` : (doc.error || '') });
|
|
186
|
+
if (!doc.ok)
|
|
187
|
+
return { ok: false, url, error: doc.error, steps };
|
|
188
|
+
// ② DID 身份
|
|
189
|
+
const did = String(opts.did || deps.did || readUserDid() || '');
|
|
190
|
+
steps.push({ step: 'DID 身份', ok: !!did, note: did ? `${did.slice(0, 30)}…` : '本机无 DID (先运行 bolloon 让 server 生成 ~/.bolloon/identity/user.json)' });
|
|
191
|
+
if (!did)
|
|
192
|
+
return { ok: false, url, error: '缺少 DID 身份, 无法入网', steps };
|
|
193
|
+
const name = String(opts.name || 'bolloon-agent');
|
|
194
|
+
// ③ P2P 节点 (文档第 2 节: p2pNetwork.createNode) — 起不来的话如实在 note 里说
|
|
195
|
+
const node = await ensureGatewayNode(deps);
|
|
196
|
+
const peerId = String(node.peerId || '');
|
|
197
|
+
const multiaddrs = node.multiaddrs || [];
|
|
198
|
+
steps.push({
|
|
199
|
+
step: 'P2P 节点',
|
|
200
|
+
ok: !!peerId,
|
|
201
|
+
note: peerId ? `peerId=${peerId.slice(0, 16)}…${node.started ? ' (本次启动)' : ' (已在跑)'}` : `节点未就绪: ${node.error || '未知原因'}`,
|
|
202
|
+
});
|
|
203
|
+
// ③b 登记节点端点 + 广播地址 (文档第 2/7 节) — 非致命
|
|
204
|
+
if (peerId) {
|
|
205
|
+
try {
|
|
206
|
+
const { initializeAgentNetwork, broadcastOwnAddress } = await import('../network/agent-network.js');
|
|
207
|
+
await initializeAgentNetwork(did, name, peerId, multiaddrs);
|
|
208
|
+
await broadcastOwnAddress().catch(() => { });
|
|
209
|
+
steps.push({ step: '登记节点端点 + 广播地址', ok: true, note: `multiaddrs=${multiaddrs.length} 条` });
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
steps.push({ step: '登记节点端点 + 广播地址', ok: false, note: `失败: ${String(e?.message || e).slice(0, 120)}` });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// ④ 注册本地 manifest (文档第 3 节)
|
|
216
|
+
const capabilities = (opts.capabilities && opts.capabilities.length ? opts.capabilities : ['chat', 'gateway-join']).map(String);
|
|
217
|
+
const entry = { id: `${name}-main`.replace(/\s+/g, '-'), name, capabilities, status: 'active', ...(peerId ? { peerId } : {}) };
|
|
218
|
+
let registered = false;
|
|
219
|
+
try {
|
|
220
|
+
if (deps.registerManifest) {
|
|
221
|
+
deps.registerManifest({ ownerName: name, ownerPublicKey: did, agents: [entry] });
|
|
222
|
+
registered = true;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const { setLocalManifest, getLocalManifest } = await import('./agent-manifest-protocol.js');
|
|
226
|
+
setLocalManifest({ ownerName: name, ownerPublicKey: did, agents: [entry] });
|
|
227
|
+
registered = (getLocalManifest().agents || []).some((a) => a.id === entry.id);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
steps.push({ step: '注册 manifest', ok: false, note: `注册失败: ${String(e?.message || e).slice(0, 120)}` });
|
|
232
|
+
return { ok: false, url, did, error: 'manifest 注册失败', steps };
|
|
233
|
+
}
|
|
234
|
+
steps.push({ step: '注册 manifest', ok: registered, note: registered ? `agents=[${entry.id}] capabilities=[${capabilities.join(',')}]` : 'register 后读回为空' });
|
|
235
|
+
if (!registered)
|
|
236
|
+
return { ok: false, url, did, error: 'manifest 注册后读回为空', steps };
|
|
237
|
+
// ⑤ 建网 (可分享的 orbitdb 网络链接) — 离线/registry 未就绪时如实记 note
|
|
238
|
+
let networkLink;
|
|
239
|
+
let networkId;
|
|
240
|
+
try {
|
|
241
|
+
const share = deps.shareLink
|
|
242
|
+
? await deps.shareLink({ name })
|
|
243
|
+
: await (await import('./gateway-network.js')).shareNetworkLink({ name });
|
|
244
|
+
if (share.ok && share.link) {
|
|
245
|
+
networkLink = share.link;
|
|
246
|
+
networkId = new URLSearchParams(share.link.split('?')[1] || '').get('name') || undefined;
|
|
247
|
+
steps.push({ step: '建成网络 (可分享链接)', ok: true, note: share.link.slice(0, 72) });
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
steps.push({ step: '建成网络 (可分享链接)', ok: false, note: `未建成: ${share.error || 'registry 未就绪 (离线模式)'}` });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch (e) {
|
|
254
|
+
steps.push({ step: '建成网络 (可分享链接)', ok: false, note: `异常: ${String(e?.message || e).slice(0, 120)}` });
|
|
255
|
+
}
|
|
256
|
+
// ⑥ 登记到本机 Agent 服务注册表 (让他方按能力发现/委派)
|
|
257
|
+
try {
|
|
258
|
+
const r = deps.registerService
|
|
259
|
+
? await deps.registerService({ capability: capabilities[0], did, name })
|
|
260
|
+
: await (await import('./agent-gateway.js')).gatewayRegisterAgent({ capability: capabilities[0], description: `${name} 入网节点`, price: '0', per: 'task' }, { did, name, wallet: '' });
|
|
261
|
+
steps.push({ step: '服务登记 (可被发现)', ok: !!r?.ok, note: r?.ok ? '已写入本机 registry' : (r?.error || '未写入') });
|
|
262
|
+
}
|
|
263
|
+
catch (e) {
|
|
264
|
+
steps.push({ step: '服务登记 (可被发现)', ok: false, note: `异常: ${String(e?.message || e).slice(0, 120)}` });
|
|
265
|
+
}
|
|
266
|
+
// ⑦ 落盘入网态 (幂等 + 重启恢复)
|
|
267
|
+
writeGatewayJoinState({ url, did, name, capabilities, peerId: peerId || undefined, networkLink, networkId, joinedAt: new Date().toISOString() });
|
|
268
|
+
steps.push({ step: '落盘入网态', ok: (await getGatewayJoinState())?.url === url, note: STATE_FILE() });
|
|
269
|
+
return { ok: true, url, did, peerId: peerId || undefined, networkLink, networkId, docVersion: doc.version, steps };
|
|
270
|
+
}
|
|
271
|
+
export default { DEFAULT_GATEWAY_JOIN_DOC, readGatewaySkillDoc, isGatewayJoinDoc, parseSkillFrontmatter, joinGlobalGateway, getGatewayJoinState, writeGatewayJoinState };
|
|
@@ -1031,12 +1031,23 @@ export function registerBuiltinTools(ctx) {
|
|
|
1031
1031
|
// 通用文件读取 (M4)
|
|
1032
1032
|
ctx.tools.set('read_file', {
|
|
1033
1033
|
name: 'read_file',
|
|
1034
|
-
description: '读取任意文件内容 (相对 cwd). 只读操作, 无白名单限制.',
|
|
1035
|
-
parameters: { path: '
|
|
1034
|
+
description: '读取任意文件内容 (相对 cwd); 也支持 http(s) URL (如 "read https://…/doc.md" 这种口令 → 直接给 URL 即可). 只读操作, 无白名单限制.',
|
|
1035
|
+
parameters: { path: '相对路径或 http(s) URL (必填)', startLine: '起始行号 (可选, 默认 0)', maxLines: '最大行数 (可选, 默认 500)' },
|
|
1036
1036
|
execute: async (args) => {
|
|
1037
1037
|
const relPath = String(args.path || '').trim();
|
|
1038
1038
|
if (!relPath)
|
|
1039
1039
|
return { success: false, error: 'path 必填' };
|
|
1040
|
+
// 2026-09-15: URL 直读 — 人类口令 "read <url>" 落到的就是本工具, 之前会 ENOENT (当成相对路径)
|
|
1041
|
+
if (/^https?:\/\//i.test(relPath)) {
|
|
1042
|
+
const r = await readUrlAsText(relPath);
|
|
1043
|
+
if (!r.ok)
|
|
1044
|
+
return { success: false, error: `读取 URL 失败: ${r.error}` };
|
|
1045
|
+
const start = Math.max(0, parseInt(String(args.startLine || '0')) || 0);
|
|
1046
|
+
const max = parseInt(String(args.maxLines || '500')) || 500;
|
|
1047
|
+
const lines = String(r.text || '').split('\n');
|
|
1048
|
+
const slice = lines.slice(start, start + max);
|
|
1049
|
+
return { success: true, output: `🌐 ${relPath} (第 ${start + 1}-${start + slice.length} 行, 共 ${lines.length} 行):\n${slice.map((l, i) => `${String(start + i + 1).padStart(4)} | ${l}`).join('\n')}` };
|
|
1050
|
+
}
|
|
1040
1051
|
try {
|
|
1041
1052
|
const absPath = path.resolve(ctx.cwd, relPath);
|
|
1042
1053
|
const content = fsSync.readFileSync(absPath, 'utf-8');
|
|
@@ -1175,6 +1186,25 @@ export function registerBuiltinTools(ctx) {
|
|
|
1175
1186
|
// Web 上网工具 (2026-08-04) — fetch_url + web_search
|
|
1176
1187
|
// 用 undici request (独立连接池, 与 pi-ai 一致, 避开全局 fetch 僵尸连接问题)
|
|
1177
1188
|
// ============================================================
|
|
1189
|
+
/** 读一个 http(s) URL 的正文 (curl, 兼容 TLS 指纹风控). read_file 的 URL 分支复用. 函数声明提升, 上面可调用. */
|
|
1190
|
+
async function readUrlAsText(url, maxChars = 60_000) {
|
|
1191
|
+
try {
|
|
1192
|
+
const { execFile } = await import('child_process');
|
|
1193
|
+
const { promisify } = await import('util');
|
|
1194
|
+
const pExecFile = promisify(execFile);
|
|
1195
|
+
const { stdout, stderr } = await pExecFile('curl', [
|
|
1196
|
+
'-sL', '--max-time', '25',
|
|
1197
|
+
'-A', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36',
|
|
1198
|
+
url,
|
|
1199
|
+
], { maxBuffer: 4 * 1024 * 1024, timeout: 30_000 });
|
|
1200
|
+
if (!stdout)
|
|
1201
|
+
return { ok: false, error: String(stderr || '空响应').slice(0, 200) };
|
|
1202
|
+
return { ok: true, text: String(stdout).slice(0, maxChars) };
|
|
1203
|
+
}
|
|
1204
|
+
catch (e) {
|
|
1205
|
+
return { ok: false, error: String(e?.message || e).slice(0, 200) };
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1178
1208
|
ctx.tools.set('fetch_url', {
|
|
1179
1209
|
name: 'fetch_url',
|
|
1180
1210
|
description: '抓取一个 URL 的网页内容并转成纯文本. 适合查文档/新闻/API 页面. 返回前 4000 字符. HTML 自动去标签, JSON/文本原样返回. (走 curl, 兼容 TLS 指纹风控)',
|
|
@@ -2835,6 +2865,10 @@ export function registerBuiltinTools(ctx) {
|
|
|
2835
2865
|
const link = String(args.link || '').trim();
|
|
2836
2866
|
if (!link)
|
|
2837
2867
|
return { success: false, error: 'link 必填 (ipns:// / orbitdb:// / https://)' };
|
|
2868
|
+
// 2026-09-15: 入网说明文档 (.md) 不是 registry 链接 — 别静默当 registry 拉, 指到正确工具
|
|
2869
|
+
if (/^https?:\/\//i.test(link) && (/\.md(\?|#|$)/i.test(link) || /gateway-join/i.test(link))) {
|
|
2870
|
+
return { success: false, error: `这是一个「入网说明文档」而不是网络链接。请改用 join_global_gateway (url=${link}) 走文档驱动入网。` };
|
|
2871
|
+
}
|
|
2838
2872
|
try {
|
|
2839
2873
|
const { joinNetwork } = await import('./gateway-network.js');
|
|
2840
2874
|
const r = await joinNetwork(link);
|
|
@@ -2847,6 +2881,45 @@ export function registerBuiltinTools(ctx) {
|
|
|
2847
2881
|
}
|
|
2848
2882
|
},
|
|
2849
2883
|
});
|
|
2884
|
+
// ============================================================
|
|
2885
|
+
// join_global_gateway (2026-09-15) — 文档驱动的「加入全球智能体网络」
|
|
2886
|
+
// 人类口令只有一句: `read https://bolloon.cn/bolloon-gateway-join.md`
|
|
2887
|
+
// 文档写的是 SDK 伪码, agent 手里只有工具 → 本工具把整条链路串成闭环:
|
|
2888
|
+
// 读说明 → DID → P2P 节点 → 注册 manifest → 建成可分享网络 → 服务登记 → 落盘(幂等)
|
|
2889
|
+
// ============================================================
|
|
2890
|
+
ctx.tools.set('join_global_gateway', {
|
|
2891
|
+
name: 'join_global_gateway',
|
|
2892
|
+
description: '按「入网说明文档」加入全球智能体网络 (完整闭环: 读文档 → DID 身份 → P2P 节点 → 注册本地 manifest → 建成可分享的网络 → 服务登记 → 落盘). 默认文档即 https://bolloon.cn/bolloon-gateway-join.md (人类口令 "read <该文档>" 时用本工具)。幂等: 重复入网返回 already。文档不可达或不是入网说明 → 如实失败, 不假装入网。',
|
|
2893
|
+
parameters: {
|
|
2894
|
+
url: '入网说明文档 URL (可选, 默认 https://bolloon.cn/bolloon-gateway-join.md)',
|
|
2895
|
+
name: '本智能体名字 (可选, 默认取当前身份名)',
|
|
2896
|
+
capabilities: '声明能力, 逗号分隔 (可选, 默认 chat,gateway-join)',
|
|
2897
|
+
force: 'true = 忽略幂等重新入网 (可选)',
|
|
2898
|
+
},
|
|
2899
|
+
execute: async (args) => {
|
|
2900
|
+
try {
|
|
2901
|
+
const { joinGlobalGateway } = await import('./gateway-join.js');
|
|
2902
|
+
const caps = String(args.capabilities || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
2903
|
+
const r = await joinGlobalGateway({
|
|
2904
|
+
url: String(args.url || '').trim() || undefined,
|
|
2905
|
+
did: ctx.identity?.did || undefined,
|
|
2906
|
+
name: String(args.name || '').trim() || ctx.identity?.name || undefined,
|
|
2907
|
+
capabilities: caps.length ? caps : undefined,
|
|
2908
|
+
force: String(args.force || '') === 'true',
|
|
2909
|
+
});
|
|
2910
|
+
const lines = r.steps.map((s) => `${s.ok ? '✓' : '✗'} ${s.step}: ${s.note}`);
|
|
2911
|
+
if (!r.ok)
|
|
2912
|
+
return { success: false, error: `${r.error}\n${lines.join('\n')}` };
|
|
2913
|
+
return {
|
|
2914
|
+
success: true,
|
|
2915
|
+
output: `🌐 已加入全球智能体网络${r.already ? ' (已在网, 幂等)' : ''}\nDID: ${r.did}\npeerId: ${r.peerId || '(节点后台启动中)'}${r.networkLink ? `\n可分享网络链接: ${r.networkLink}` : ''}\n${lines.join('\n')}`,
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
catch (e) {
|
|
2919
|
+
return { success: false, error: `join_global_gateway 失败: ${String(e?.message || e).slice(0, 200)}` };
|
|
2920
|
+
}
|
|
2921
|
+
},
|
|
2922
|
+
});
|
|
2850
2923
|
ctx.tools.set('gateway_status', {
|
|
2851
2924
|
name: 'gateway_status',
|
|
2852
2925
|
description: '查看 Agent 网络状态 (已注册服务 + 信誉 + 已加入的网络).',
|
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -1473,6 +1473,8 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1473
1473
|
role: 'assistant',
|
|
1474
1474
|
content: reply,
|
|
1475
1475
|
toolCalls: toolCalls.length > 1 ? toolCalls : [toolCalls[0]],
|
|
1476
|
+
// 2026-09-15: 思考模式思维链原样存回 (下一轮带 tools 的请求必须回带, 否则 deepseek 400)
|
|
1477
|
+
reasoningContent: response?.reasoningContent,
|
|
1476
1478
|
});
|
|
1477
1479
|
// 2026-08-09: 并发执行本轮所有工具 (Hermes 式 Agent Runtime: 一轮内多工具并行,
|
|
1478
1480
|
// 一轮没跑完之前不中断 — 工具执行不检查 abort, 全部完成才 continue 下一轮)
|
|
@@ -1747,7 +1749,8 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1747
1749
|
// LLM 返回的不是 tool call 格式
|
|
1748
1750
|
this.messageHistory.push({
|
|
1749
1751
|
role: 'assistant',
|
|
1750
|
-
content: reply
|
|
1752
|
+
content: reply,
|
|
1753
|
+
reasoningContent: response?.reasoningContent,
|
|
1751
1754
|
});
|
|
1752
1755
|
// 通知前端收到非工具调用回复 (2026-08-09: 完整内容, 不再截断 150)
|
|
1753
1756
|
if (onStream) {
|
|
@@ -1979,7 +1982,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
1979
1982
|
continue;
|
|
1980
1983
|
}
|
|
1981
1984
|
if (r === 'assistant') {
|
|
1982
|
-
out.push({ role: 'assistant', content: (m.content || '').slice(0, 4000) });
|
|
1985
|
+
out.push({ role: 'assistant', content: (m.content || '').slice(0, 4000), reasoningContent: m.reasoningContent });
|
|
1983
1986
|
continue;
|
|
1984
1987
|
}
|
|
1985
1988
|
if (r === 'user') {
|
|
@@ -2001,7 +2004,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2001
2004
|
continue;
|
|
2002
2005
|
}
|
|
2003
2006
|
if (r === 'assistant') {
|
|
2004
|
-
out.push({ role: 'assistant', content: m.content || '' });
|
|
2007
|
+
out.push({ role: 'assistant', content: m.content || '', reasoningContent: m.reasoningContent });
|
|
2005
2008
|
continue;
|
|
2006
2009
|
}
|
|
2007
2010
|
if (r === 'user') {
|
package/dist/llm/pi-ai.js
CHANGED
|
@@ -303,6 +303,28 @@ export class PiAIModel {
|
|
|
303
303
|
};
|
|
304
304
|
return modelMap[this.provider];
|
|
305
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* 2026-09-15: 出网前把 messages 规整成 wire 形状.
|
|
308
|
+
*
|
|
309
|
+
* 背景 (真跑复现 + 逐项对照, 见 wiki log 2026-09-15): DeepSeek 思考模式 (deepseek-v4-*)
|
|
310
|
+
* 在**请求带 tools** 时, 任何 assistant 消息缺 `reasoning_content` 字段 →
|
|
311
|
+
* HTTP 400 "The `reasoning_content` in the thinking mode must be passed back to the API".
|
|
312
|
+
* 复现: 同一个 17 条消息的真实请求体, 不带 tools → 200; 带上 tools → 400;
|
|
313
|
+
* 给每条 assistant 补 `reasoning_content:""` → 带 tools 也 200。
|
|
314
|
+
* 表现: 多轮工具循环 (第 2 轮起) 直接断, 用户看到 "AI 服务调用失败"。
|
|
315
|
+
*
|
|
316
|
+
* 处置: 只对 deepseek 生效 (唯一实测过的 provider); 有原文用原文, 没有就补空串 —
|
|
317
|
+
* 空串已被官方接受, 且不改变语义 (思维链不是给模型看的历史内容)。
|
|
318
|
+
* 其他 provider 原样透传 (OpenAI 系对未知字段更敏感, 不做无证据的改动)。
|
|
319
|
+
*/
|
|
320
|
+
prepareWireMessages(messages) {
|
|
321
|
+
const echoReasoning = this.provider === 'deepseek';
|
|
322
|
+
if (!echoReasoning)
|
|
323
|
+
return messages;
|
|
324
|
+
return messages.map((m) => m.role === 'assistant'
|
|
325
|
+
? { role: 'assistant', content: m.content ?? '', reasoning_content: m.reasoningContent ?? '' }
|
|
326
|
+
: m);
|
|
327
|
+
}
|
|
306
328
|
async callOpenAI(messages, temperature, maxTokens, signal, tools) {
|
|
307
329
|
const apiKey = this.getApiKey();
|
|
308
330
|
if (!apiKey) {
|
|
@@ -310,7 +332,7 @@ export class PiAIModel {
|
|
|
310
332
|
}
|
|
311
333
|
const requestBody = {
|
|
312
334
|
model: this.mapModel(),
|
|
313
|
-
messages,
|
|
335
|
+
messages: this.prepareWireMessages(messages),
|
|
314
336
|
temperature,
|
|
315
337
|
max_tokens: maxTokens
|
|
316
338
|
};
|
|
@@ -370,6 +392,15 @@ export class PiAIModel {
|
|
|
370
392
|
const errBody = await body.text().catch(() => '(no body)');
|
|
371
393
|
console.log(`[pi-ai DEBUG] OpenAI 错误 ${statusCode}: ${String(errBody).slice(0, 500)}`);
|
|
372
394
|
console.log(`[pi-ai DEBUG] 请求体: model=${requestBody.model}, messages=${requestBody.messages?.length}, max_tokens=${requestBody.max_tokens}, baseUrl=${this.getBaseUrl()}`);
|
|
395
|
+
if (process.env.BOLLOON_DUMP_BODY === '1') {
|
|
396
|
+
try {
|
|
397
|
+
const fsx = await import('fs');
|
|
398
|
+
const p = `/tmp/bolloon-req-${Date.now()}.json`;
|
|
399
|
+
fsx.writeFileSync(p, JSON.stringify(requestBody, null, 2));
|
|
400
|
+
console.log(`[pi-ai DEBUG] 失败请求体已落盘: ${p}`);
|
|
401
|
+
}
|
|
402
|
+
catch { /* 调试用, 失败忽略 */ }
|
|
403
|
+
}
|
|
373
404
|
retryAgent?.destroy().catch(() => { });
|
|
374
405
|
throw new Error(`OpenAI API error: ${statusCode} ${String(errBody).slice(0, 300)}`);
|
|
375
406
|
}
|
|
@@ -378,6 +409,8 @@ export class PiAIModel {
|
|
|
378
409
|
const choice = data.choices?.[0];
|
|
379
410
|
const content = choice?.message?.content || '';
|
|
380
411
|
const toolCalls = choice?.message?.tool_calls;
|
|
412
|
+
// 2026-09-15: 思考模式思维链 — 原样存回 history, 下一轮必须回带 (见 prepareWireMessages)
|
|
413
|
+
const reasoningContent = choice?.message?.reasoning_content || undefined;
|
|
381
414
|
lastFinishReason = choice?.finish_reason || '';
|
|
382
415
|
// Bug 7: tool_calls 存在时不走重试 — LLM 选工具时 content 空是合法的
|
|
383
416
|
if (content || (toolCalls && toolCalls.length > 0)) {
|
|
@@ -388,7 +421,7 @@ export class PiAIModel {
|
|
|
388
421
|
const promptBytes = JSON.stringify(messages).length;
|
|
389
422
|
console.log(`[pi-ai timing] total=${_tAfter - _t0}ms attempt=${attempt + 1} fetch=${_tResp - _tFetch}ms parse=${_tParse - _tResp}ms reply=${content.length}B toolCalls=${toolCalls?.length ?? 0} model=${this.mapModel()} prompt=${promptBytes}B`);
|
|
390
423
|
retryAgent?.destroy().catch(() => { });
|
|
391
|
-
return { reply: content, toolCalls: toolCalls && toolCalls.length > 0 ? toolCalls : undefined };
|
|
424
|
+
return { reply: content, toolCalls: toolCalls && toolCalls.length > 0 ? toolCalls : undefined, reasoningContent };
|
|
392
425
|
}
|
|
393
426
|
console.warn(`[pi-ai] attempt ${attempt + 1}/3: 空 content (finish_reason=${lastFinishReason}), 退避 1.5s 重试`);
|
|
394
427
|
const _tSleep = Date.now();
|
|
@@ -10,6 +10,33 @@ const RELAY_RETRY_INTERVAL = 30000;
|
|
|
10
10
|
const MAX_RELAY_HOPS = 3;
|
|
11
11
|
const MESSAGE_TIMESTAMP_TOLERANCE = 24 * 60 * 60 * 1000;
|
|
12
12
|
const SIGNED_MESSAGE_TYPES = ['task', 'response', 'discovery', 'address_broadcast'];
|
|
13
|
+
/**
|
|
14
|
+
* 2026-09-15: did:key:z6Mk… 与 Ed25519 公钥字节是否一致。
|
|
15
|
+
*
|
|
16
|
+
* DID 由公钥派生 (did:key 规范: multibase base58btc of 0xed01 || rawKey), 所以首次接触时
|
|
17
|
+
* 自携公钥**无法伪造**: 冒充者换了公钥就解码不出同一个 DID。
|
|
18
|
+
* 返回: true 一致 / false 不一致 (疑似冒充) / null 非 did:key 形态 (无法判定, 不阻断, 交给签名验证)。
|
|
19
|
+
*/
|
|
20
|
+
export async function didKeyMatchesPublicKey(did, publicKeyHex) {
|
|
21
|
+
if (!/^did:key:z/.test(did))
|
|
22
|
+
return null;
|
|
23
|
+
try {
|
|
24
|
+
const { base58btc } = await import('multiformats/bases/base58');
|
|
25
|
+
const decoded = base58btc.decode(did.slice('did:key:'.length));
|
|
26
|
+
// 34 字节 = 2 字节 multicodec 前缀 (0xed01 = ed25519-pub) + 32 字节公钥
|
|
27
|
+
if (decoded.length !== 34)
|
|
28
|
+
return false;
|
|
29
|
+
if (decoded[0] !== 0xed || decoded[1] !== 0x01)
|
|
30
|
+
return false;
|
|
31
|
+
const claimed = Buffer.from(publicKeyHex, 'hex');
|
|
32
|
+
if (claimed.length !== 32)
|
|
33
|
+
return false;
|
|
34
|
+
return Buffer.compare(Buffer.from(decoded.slice(2)), claimed) === 0;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null; // 解码失败 → 不判定 (不当成冒充, 也不放行: 签名仍要过)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
13
40
|
export class AgentRegistry {
|
|
14
41
|
agents = new Map();
|
|
15
42
|
registryPath;
|
|
@@ -192,6 +219,7 @@ export class AgentRegistry {
|
|
|
192
219
|
return null;
|
|
193
220
|
}
|
|
194
221
|
const relayAddr = p2pNetwork.getRelayAddress?.() || null;
|
|
222
|
+
const ownPublicKeyHex = Buffer.from(this.keyPair.publicKey).toString('hex');
|
|
195
223
|
const broadcastData = JSON.stringify({
|
|
196
224
|
type: 'address_broadcast',
|
|
197
225
|
from: this.keyPair.did,
|
|
@@ -200,6 +228,7 @@ export class AgentRegistry {
|
|
|
200
228
|
multiaddrs: this.ownEndpoint.multiaddrs,
|
|
201
229
|
relayAddr: relayAddr || undefined,
|
|
202
230
|
canRelay: relayAddr ? true : false,
|
|
231
|
+
publicKey: ownPublicKeyHex,
|
|
203
232
|
timestamp: now
|
|
204
233
|
});
|
|
205
234
|
const signature = await this.signMessage(broadcastData);
|
|
@@ -213,6 +242,7 @@ export class AgentRegistry {
|
|
|
213
242
|
multiaddrs: this.ownEndpoint.multiaddrs,
|
|
214
243
|
relayAddr: relayAddr || undefined,
|
|
215
244
|
canRelay: relayAddr ? true : false,
|
|
245
|
+
publicKey: ownPublicKeyHex,
|
|
216
246
|
timestamp: now,
|
|
217
247
|
signature: Buffer.from(signature).toString('hex')
|
|
218
248
|
};
|
|
@@ -237,10 +267,35 @@ export class AgentRegistry {
|
|
|
237
267
|
multiaddrs: broadcast.multiaddrs,
|
|
238
268
|
relayAddr: broadcast.relayAddr,
|
|
239
269
|
canRelay: broadcast.canRelay,
|
|
270
|
+
publicKey: broadcast.publicKey,
|
|
240
271
|
timestamp: broadcast.timestamp
|
|
241
272
|
});
|
|
242
273
|
const signature = Buffer.from(broadcast.signature, 'hex');
|
|
243
|
-
const
|
|
274
|
+
const known = this.agents.get(broadcast.from);
|
|
275
|
+
// 2026-09-15: 首次接触 (全球化去中心网络里全是陌生人) —— 原来只认「registry 里已有的公钥」,
|
|
276
|
+
// 陌生人第一次广播必然验签失败 → 广播被丢弃 → 陌生人永远发现不了彼此 (入网文档 §7 的链路实际不通).
|
|
277
|
+
// 现在: 未知 DID 时用广播自带 publicKey 自证 (TOFU: 首次接触信任自携公钥);
|
|
278
|
+
// did:key 额外做 DID↔公钥 派生一致性检查 (DID 本身就是公钥, 自携公钥无法伪造).
|
|
279
|
+
let verifyKeyHex = known?.publicKey || undefined;
|
|
280
|
+
if (!verifyKeyHex) {
|
|
281
|
+
const claimed = String(broadcast.publicKey || '');
|
|
282
|
+
if (!claimed) {
|
|
283
|
+
console.warn(`[Registry] Unknown agent ${broadcast.from.substring(0, 20)}… 且广播未携带 publicKey → 无法验证, 拒收`);
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
const binding = await didKeyMatchesPublicKey(broadcast.from, claimed);
|
|
287
|
+
if (binding === false) {
|
|
288
|
+
console.warn(`[Registry] did:key 与自带公钥不匹配 (疑似冒充) from=${broadcast.from.substring(0, 24)}… → 拒收`);
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
verifyKeyHex = claimed;
|
|
292
|
+
}
|
|
293
|
+
else if (broadcast.publicKey && broadcast.publicKey !== verifyKeyHex) {
|
|
294
|
+
// 已认识这个 DID, 却报出另一把公钥 → 身份接管尝试, 拒收 (不覆盖已存公钥)
|
|
295
|
+
console.warn(`[Registry] ${broadcast.from.substring(0, 20)}… 广播的公钥与已知公钥不一致 → 拒收 (不覆盖)`);
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
const isValid = await this.verifySignatureWithKey(broadcast.from, verifyKeyHex, broadcastData, signature);
|
|
244
299
|
if (!isValid) {
|
|
245
300
|
console.warn(`[Registry] Invalid broadcast signature from ${broadcast.from.substring(0, 20)}`);
|
|
246
301
|
return false;
|
|
@@ -253,7 +308,9 @@ export class AgentRegistry {
|
|
|
253
308
|
registeredAt: Date.now(),
|
|
254
309
|
lastSeen: broadcast.timestamp,
|
|
255
310
|
lastBroadcast: 0,
|
|
256
|
-
|
|
311
|
+
// 2026-09-15 修复: 原来写的是**自己**的公钥 (this.keyPair.publicKey) →
|
|
312
|
+
// 对端后续任何签名消息都验不过 (拿我方公钥去验对方签名), 属于把对端身份写坏.
|
|
313
|
+
publicKey: verifyKeyHex,
|
|
257
314
|
relayAddr: broadcast.relayAddr,
|
|
258
315
|
canRelay: broadcast.canRelay
|
|
259
316
|
};
|
|
@@ -261,15 +318,30 @@ export class AgentRegistry {
|
|
|
261
318
|
if (existing) {
|
|
262
319
|
entry.registeredAt = existing.registeredAt;
|
|
263
320
|
entry.lastBroadcast = existing.lastBroadcast;
|
|
264
|
-
if (
|
|
265
|
-
|
|
266
|
-
}
|
|
321
|
+
if (existing.publicKey)
|
|
322
|
+
entry.publicKey = existing.publicKey; // 已有公钥永不覆盖
|
|
267
323
|
}
|
|
268
324
|
this.agents.set(broadcast.from, entry);
|
|
269
325
|
this.saveRegistry();
|
|
270
|
-
console.log(`[Registry] Verified broadcast from: ${broadcast.name} (${broadcast.from.substring(0, 20)}...) ${broadcast.canRelay ? '[Relay Capable]' : ''}`);
|
|
326
|
+
console.log(`[Registry] Verified broadcast from: ${broadcast.name} (${broadcast.from.substring(0, 20)}...) ${broadcast.canRelay ? '[Relay Capable]' : ''}${known ? '' : ' [首次接触 TOFU]'}`);
|
|
271
327
|
return true;
|
|
272
328
|
}
|
|
329
|
+
/** 用给定公钥 (hex) 验签 — 首次接触路径用 (未知 DID 时 registry 里没公钥) */
|
|
330
|
+
async verifySignatureWithKey(did, publicKeyHex, data, signature) {
|
|
331
|
+
try {
|
|
332
|
+
const publicKey = Buffer.from(publicKeyHex, 'hex');
|
|
333
|
+
if (publicKey.length !== 32) {
|
|
334
|
+
console.warn(`[Registry] publicKey 长度非法 (${publicKey.length} 字节, 期望 32): ${did.substring(0, 20)}…`);
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
const keyPair = { privateKey: new Uint8Array(32), publicKey, did };
|
|
338
|
+
return await KeyManager.verify(keyPair, new TextEncoder().encode(data), signature);
|
|
339
|
+
}
|
|
340
|
+
catch (e) {
|
|
341
|
+
console.warn(`[Registry] Verification with provided key failed:`, e);
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
273
345
|
async createSignedMessage(type, payload) {
|
|
274
346
|
if (!this.keyPair || !this.ownEndpoint)
|
|
275
347
|
return null;
|