@bolloon/bolloon-agent 0.4.23 → 0.4.24
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/ios/index.html +74 -17
- package/dist/ios/mobile-agent.js +49 -4
- package/dist/ios/mobile-chain.js +481 -0
- package/dist/ios/mobile-core.js +373 -13
- package/dist/ios/mobile-helia.js +683 -0
- package/dist/ios/mobile-ipfs.js +680 -0
- package/dist/ios/mobile-orbit.js +268 -0
- package/dist/ios/mobile-p2p.js +130 -1
- package/dist/ios/mobile-social.js +624 -0
- package/dist/ios/mobile-sync.js +193 -0
- package/dist/ios/mobile-trade.js +492 -0
- package/dist/ios/mobile-wallet.js +11 -0
- package/dist/ios/mobile.css +24 -0
- package/dist/ios/mobile.html +74 -17
- package/dist/ios/mobile.js +911 -55
- package/dist/ios/routes-x402-info.js +194 -0
- package/dist/ios/server.js +303 -9
- package/dist/ios/sw.js +26 -2
- package/dist/web/agent-delegate-server.js +58 -12
- package/dist/web/mobile-agent.js +195 -1
- package/dist/web/mobile-core.js +170 -1
- package/dist/web/server.js +51 -4
- package/package.json +1 -1
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import express from 'express';
|
|
19
19
|
import { buildAgentDelegateRequest, buildAgentResponse, buildManifestPayload, parseFrame, setLocalManifest, getLocalManifest, getRemoteManifests, cacheRemoteManifest, pickAgent, } from '../agents/agent-manifest-protocol.js';
|
|
20
|
-
export function createAgentDelegateApp(transport) {
|
|
20
|
+
export function createAgentDelegateApp(transport, options = {}) {
|
|
21
21
|
const app = express();
|
|
22
22
|
app.use(express.json({ limit: '2mb' }));
|
|
23
|
+
const executeTimeoutMs = options.executeTimeoutMs ?? 60_000;
|
|
23
24
|
// ---- 本地 manifest ----
|
|
24
25
|
app.get('/api/agent/local-manifest', (_req, res) => {
|
|
25
26
|
res.json(getLocalManifest());
|
|
@@ -81,7 +82,10 @@ export function createAgentDelegateApp(transport) {
|
|
|
81
82
|
}
|
|
82
83
|
res.json({
|
|
83
84
|
ok: true,
|
|
84
|
-
|
|
85
|
+
// 2026-09-15: 没缓存到对端 manifest 就如实给 null —— 旧版会编一个
|
|
86
|
+
// 「capabilities:[capability], name:<id>」的假目标, 让人以为已经知道对端是谁。
|
|
87
|
+
targetAgent: targetAgent || null,
|
|
88
|
+
targetAgentKnown: !!targetAgent,
|
|
85
89
|
response: f.payload,
|
|
86
90
|
});
|
|
87
91
|
}
|
|
@@ -104,18 +108,60 @@ export function createAgentDelegateApp(transport) {
|
|
|
104
108
|
return null; // 不需要回包
|
|
105
109
|
}
|
|
106
110
|
if (f.type === 'agent_delegate') {
|
|
107
|
-
// 路由到本地匹配 agent
|
|
108
111
|
const req = f.payload;
|
|
112
|
+
const capability = String(req?.capability || '');
|
|
113
|
+
// 2026-09-15: 严格按文档 §6 / §9 —— 只认 capabilities 含该能力且 active 的 agent。
|
|
114
|
+
// 旧实现 `|| local.agents[0]` 会把不匹配的指令塞给任意一个本地 agent,
|
|
115
|
+
// 与「pick 404 = 没有匹配能力」的语义自相矛盾。
|
|
109
116
|
const local = getLocalManifest();
|
|
110
|
-
const target = local.agents.find((a) => a.capabilities.includes(
|
|
111
|
-
if (!target)
|
|
112
|
-
return buildAgentResponse({
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
117
|
+
const target = local.agents.find((a) => a.capabilities.includes(capability) && a.status === 'active');
|
|
118
|
+
if (!target) {
|
|
119
|
+
return buildAgentResponse({
|
|
120
|
+
ok: false,
|
|
121
|
+
delegatedTo: 'none',
|
|
122
|
+
summary: `no local agent available for capability '${capability}'`,
|
|
123
|
+
error: 'no-capability-match',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// 匹配到了, 但本节点没接执行器 → 如实说"干不了", 不假签收
|
|
127
|
+
if (!options.execute) {
|
|
128
|
+
return buildAgentResponse({
|
|
129
|
+
ok: false,
|
|
130
|
+
delegatedTo: target.id,
|
|
131
|
+
summary: `matched agent '${target.name}' but this node has no executor wired`,
|
|
132
|
+
error: 'no-executor',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const raced = await Promise.race([
|
|
137
|
+
options.execute({
|
|
138
|
+
capability,
|
|
139
|
+
instruction: String(req?.instruction || ''),
|
|
140
|
+
docPath: req?.docPath ? String(req.docPath) : undefined,
|
|
141
|
+
docContent: req?.docContent ? String(req.docContent) : undefined,
|
|
142
|
+
fromAgentId: req?.fromAgentId ? String(req.fromAgentId) : undefined,
|
|
143
|
+
fromPublicKey,
|
|
144
|
+
targetAgentId: target.id,
|
|
145
|
+
targetAgentName: target.name,
|
|
146
|
+
}),
|
|
147
|
+
new Promise((resolve) => setTimeout(() => resolve({ ok: false, summary: `executor timed out after ${executeTimeoutMs}ms`, error: 'executor-timeout' }), executeTimeoutMs)),
|
|
148
|
+
]);
|
|
149
|
+
return buildAgentResponse({
|
|
150
|
+
ok: !!raced.ok,
|
|
151
|
+
delegatedTo: target.id,
|
|
152
|
+
resultCid: raced.resultCid,
|
|
153
|
+
summary: String(raced.summary || '').slice(0, 4000),
|
|
154
|
+
error: raced.error,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
return buildAgentResponse({
|
|
159
|
+
ok: false,
|
|
160
|
+
delegatedTo: target.id,
|
|
161
|
+
summary: `executor threw: ${String(e?.message || e).slice(0, 300)}`,
|
|
162
|
+
error: 'executor-error',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
119
165
|
}
|
|
120
166
|
return null;
|
|
121
167
|
});
|
package/dist/web/mobile-agent.js
CHANGED
|
@@ -160,11 +160,205 @@ async function applyLlmConfigToBridge() {
|
|
|
160
160
|
}
|
|
161
161
|
catch { /* 注入失败不阻塞本地执行 */ }
|
|
162
162
|
}
|
|
163
|
-
// ============
|
|
163
|
+
// ============ 手机端「读入网说明 → 入网」(2026-09-15) ============
|
|
164
|
+
//
|
|
165
|
+
// 背景: 手机端「一键入网」发出的口令是 `read https://bolloon.cn/bolloon-gateway-join.md`
|
|
166
|
+
// (mobile.js DEFAULT_JOIN_PROMPT), 但手机侧此前只会走到「已收到: "…"」的兜底回复 ——
|
|
167
|
+
// 也就是说这句话在手机上是**空转**的: 既不读文档, 也不入网。
|
|
168
|
+
// 现在手机自己就能走完: 读说明(校验 frontmatter) → 本机 DID → 服务登记
|
|
169
|
+
// (桌面可达则登记进桌面的网络 registry, 否则本机登记并如实说明) → P2P 公告(尽力) → 落盘入网态。
|
|
170
|
+
// 桌面不可达不是失败: 手机是自治节点; 但每一步都如实报 ok/note, 不假装入网。
|
|
171
|
+
/** 入网口令识别 (与 mobile.js 的默认 prompt 同源) */
|
|
172
|
+
export const MOBILE_JOIN_DOC_RE = /read\s+(https?:\/\/\S*bolloon-gateway-join\.md)/i;
|
|
173
|
+
const MOBILE_JOIN_STATE_KEY = 'bolloon_gateway_join';
|
|
174
|
+
export function detectJoinDocUrl(text) {
|
|
175
|
+
const m = MOBILE_JOIN_DOC_RE.exec(String(text || ''));
|
|
176
|
+
return m ? m[1] : null;
|
|
177
|
+
}
|
|
178
|
+
/** 极简 SKILL.md frontmatter 解析 (只认 name/version) */
|
|
179
|
+
function parseFm(text) {
|
|
180
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(String(text || ''));
|
|
181
|
+
if (!m)
|
|
182
|
+
return {};
|
|
183
|
+
const out = {};
|
|
184
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
185
|
+
const kv = /^([A-Za-z_][\w-]*)\s*:\s*(.*)$/.exec(line.trim());
|
|
186
|
+
if (!kv)
|
|
187
|
+
continue;
|
|
188
|
+
const k = kv[1].toLowerCase();
|
|
189
|
+
if (k === 'name')
|
|
190
|
+
out.name = kv[2].trim().replace(/^["']|["']$/g, '');
|
|
191
|
+
if (k === 'version')
|
|
192
|
+
out.version = kv[2].trim().replace(/^["']|["']$/g, '');
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
export async function getMobileJoinState() {
|
|
197
|
+
try {
|
|
198
|
+
const raw = typeof localStorage !== 'undefined' ? localStorage.getItem(MOBILE_JOIN_STATE_KEY) : null;
|
|
199
|
+
return raw ? JSON.parse(raw) : null;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function saveMobileJoinState(s) {
|
|
206
|
+
try {
|
|
207
|
+
if (typeof localStorage !== 'undefined')
|
|
208
|
+
localStorage.setItem(MOBILE_JOIN_STATE_KEY, JSON.stringify(s));
|
|
209
|
+
}
|
|
210
|
+
catch { /* 忽略 */ }
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* 按入网说明文档入网 (手机端自足执行)。
|
|
214
|
+
* opts.desktopBaseUrl 可注入 (测试用); 默认读 mobile-gateway 持久化的桌面基址。
|
|
215
|
+
*/
|
|
216
|
+
export async function joinGatewayFromDoc(docUrl, opts = {}) {
|
|
217
|
+
const f = opts.fetchImpl || fetch;
|
|
218
|
+
const steps = [];
|
|
219
|
+
const url = String(docUrl || '').trim();
|
|
220
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
221
|
+
return { ok: false, docUrl: url, steps, error: `入网说明地址必须是 http(s) URL (收到: ${url.slice(0, 60)})` };
|
|
222
|
+
}
|
|
223
|
+
// ① 读入网说明 + 校验
|
|
224
|
+
let docVersion;
|
|
225
|
+
try {
|
|
226
|
+
const r = await f(url, { signal: AbortSignal.timeout(opts.timeoutMs ?? 15000) });
|
|
227
|
+
if (!r.ok) {
|
|
228
|
+
steps.push({ step: '读入网说明', ok: false, note: `文档不可达 (HTTP ${r.status})` });
|
|
229
|
+
return { ok: false, docUrl: url, steps, error: `入网说明不可达 (HTTP ${r.status})` };
|
|
230
|
+
}
|
|
231
|
+
const text = await r.text();
|
|
232
|
+
const fm = parseFm(text);
|
|
233
|
+
if (fm.name !== 'bolloon-gateway-join' && !/加入网关|bolloon-gateway-join/.test(text)) {
|
|
234
|
+
steps.push({ step: '读入网说明', ok: false, note: `不是 Bolloon 入网说明 (name=${fm.name || '无'})` });
|
|
235
|
+
return { ok: false, docUrl: url, steps, error: '该文档不是 Bolloon 网关入网说明, 拒绝据此入网' };
|
|
236
|
+
}
|
|
237
|
+
docVersion = fm.version;
|
|
238
|
+
steps.push({ step: '读入网说明', ok: true, note: `${fm.name || 'bolloon-gateway-join'} v${fm.version || '?'} (${text.length} 字符)` });
|
|
239
|
+
}
|
|
240
|
+
catch (e) {
|
|
241
|
+
steps.push({ step: '读入网说明', ok: false, note: `读取失败: ${String(e?.message || e).slice(0, 120)}` });
|
|
242
|
+
return { ok: false, docUrl: url, steps, error: `入网说明读取失败: ${String(e?.message || e).slice(0, 120)}` };
|
|
243
|
+
}
|
|
244
|
+
// ② 本机 DID (手机端身份层; 可注入, 便于测试/无 IndexedDB 环境)
|
|
245
|
+
let did = String(opts.did || '');
|
|
246
|
+
if (did) {
|
|
247
|
+
steps.push({ step: 'DID 身份', ok: true, note: `${did} (注入身份)` });
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
try {
|
|
251
|
+
const id = await ensureIdentity();
|
|
252
|
+
did = id.did;
|
|
253
|
+
steps.push({ step: 'DID 身份', ok: true, note: `${did} (手机端本机生成)` });
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
steps.push({ step: 'DID 身份', ok: false, note: String(e?.message || e).slice(0, 120) });
|
|
257
|
+
return { ok: false, docUrl: url, docVersion, steps, error: '手机端 DID 生成失败' };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const name = String(opts.name || 'phone-agent');
|
|
261
|
+
const capabilities = ['chat', 'gateway-join'];
|
|
262
|
+
// ③ 服务登记: 桌面可达 → 登记进桌面(网络) registry; 否则本机登记并如实说明
|
|
263
|
+
let desktopBase = String(opts.desktopBaseUrl ?? '');
|
|
264
|
+
if (opts.desktopBaseUrl === undefined) {
|
|
265
|
+
try {
|
|
266
|
+
const g = await import('./mobile-gateway.js');
|
|
267
|
+
desktopBase = String(g.getDesktopBaseUrl() || '');
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
desktopBase = '';
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
desktopBase = desktopBase.replace(/\/+$/, '');
|
|
274
|
+
let registeredOn = 'local';
|
|
275
|
+
if (desktopBase) {
|
|
276
|
+
try {
|
|
277
|
+
const r = await f(`${desktopBase}/api/registry/register`, {
|
|
278
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
279
|
+
body: JSON.stringify({
|
|
280
|
+
agentId: did, name, wallet: '',
|
|
281
|
+
service: { name: 'chat', description: '手机端智能体 (自足节点)', price: { amount: '0', currency: 'USDC', per: 'task' }, endpoint: '' },
|
|
282
|
+
capabilities,
|
|
283
|
+
}),
|
|
284
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 15000),
|
|
285
|
+
});
|
|
286
|
+
if (r.ok) {
|
|
287
|
+
registeredOn = 'desktop';
|
|
288
|
+
steps.push({ step: '服务登记', ok: true, note: `已登记进电脑端网络 registry (${desktopBase}) —— 网络内其他智能体可按能力发现我` });
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
steps.push({ step: '服务登记', ok: false, note: `电脑端 registry 拒绝 (HTTP ${r.status}); 已改为本机登记` });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch (e) {
|
|
295
|
+
steps.push({ step: '服务登记', ok: false, note: `电脑端不可达 (${String(e?.message || e).slice(0, 80)}); 已改为本机登记` });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
steps.push({ step: '服务登记', ok: true, note: '未配置电脑端基址 → 只在本机登记 (手机是自治节点; 设置里填电脑端地址可登记进网络 registry)' });
|
|
300
|
+
}
|
|
301
|
+
// ④ P2P 公告 (尽力): 让已连接的对端知道本机服务; 没连上不是入网失败
|
|
302
|
+
try {
|
|
303
|
+
const p2p = await import('./mobile-p2p.js');
|
|
304
|
+
let peers = 0;
|
|
305
|
+
try {
|
|
306
|
+
peers = (p2p.getConnectedPeers?.() || []).length;
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
peers = 0;
|
|
310
|
+
}
|
|
311
|
+
if (peers > 0 && typeof p2p.sendMobileP2PMessage === 'function') {
|
|
312
|
+
const okAnnounce = await p2p.sendMobileP2PMessage('*', 'registry.register', JSON.stringify({ agent_id: did, name, capabilities }), did);
|
|
313
|
+
steps.push({ step: 'P2P 公告', ok: !!okAnnounce, note: okAnnounce ? `已向 ${peers} 个对端广播本机声明` : `广播失败 (对端 ${peers} 个)` });
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
steps.push({ step: 'P2P 公告', ok: false, note: '当前无已连接对端 (浏览器/未连电脑端时正常) — 本机声明已就绪, 连上即生效' });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
catch (e) {
|
|
320
|
+
steps.push({ step: 'P2P 公告', ok: false, note: `P2P 层不可用: ${String(e?.message || e).slice(0, 80)}` });
|
|
321
|
+
}
|
|
322
|
+
// ⑤ 落盘入网态 (幂等: 同 url 再次入网覆盖时间戳)
|
|
323
|
+
saveMobileJoinState({ url, did, name, capabilities, docVersion, registeredOn, desktopBaseUrl: desktopBase || undefined, joinedAt: new Date().toISOString() });
|
|
324
|
+
const state = await getMobileJoinState();
|
|
325
|
+
steps.push({ step: '落盘入网态', ok: !!state?.did, note: `localStorage:${MOBILE_JOIN_STATE_KEY}` });
|
|
326
|
+
return { ok: true, docUrl: url, docVersion, did, steps };
|
|
327
|
+
}
|
|
328
|
+
/** 把入网结果渲染成给用户看的回复 (与桌面工具的 steps 汇报风格一致) */
|
|
329
|
+
export function formatMobileJoinResult(r) {
|
|
330
|
+
if (!r.ok) {
|
|
331
|
+
return `❌ 入网失败: ${r.error || '未知原因'}\n\n${r.steps.map((s) => `${s.ok ? '✓' : '✗'} ${s.step}: ${s.note}`).join('\n')}`;
|
|
332
|
+
}
|
|
333
|
+
const s = r.steps.find((x) => x.step === '服务登记');
|
|
334
|
+
return [
|
|
335
|
+
'✅ 已加入全球智能体网络 (手机端自足执行)',
|
|
336
|
+
'',
|
|
337
|
+
`DID: ${r.did}`,
|
|
338
|
+
`入网说明: v${r.docVersion || '?'} (${r.docUrl})`,
|
|
339
|
+
s ? `登记: ${s.note}` : '',
|
|
340
|
+
'',
|
|
341
|
+
...r.steps.map((x) => `${x.ok ? '✓' : '✗'} ${x.step}: ${x.note}`),
|
|
342
|
+
'',
|
|
343
|
+
'用「网络 → Agent 网络」可查看成员; 设置里填电脑端地址可把本机登记进网络 registry。',
|
|
344
|
+
].filter((l) => l !== '').join('\n');
|
|
345
|
+
}
|
|
346
|
+
// ============ 本地执行 (Kotlin AgentRuntime / 离线兜底) ============
|
|
164
347
|
/** 手机端本地 agent 执行 (优先 Kotlin, 离线内置规则) */
|
|
165
348
|
export async function runLocalAgent(goal) {
|
|
166
349
|
const win = typeof window !== 'undefined' ? window : null;
|
|
167
350
|
const cap = win?.Capacitor;
|
|
351
|
+
// 2026-09-15: 「读入网说明 → 入网」在手机端本地自足执行 (先于 Kotlin 桥: 原生工具集里没有入网能力,
|
|
352
|
+
// 交给它只会得到空转回复)。这样浏览器 / WebView / 真机三种环境行为一致。
|
|
353
|
+
const joinDocUrl = detectJoinDocUrl(goal);
|
|
354
|
+
if (joinDocUrl) {
|
|
355
|
+
_lastWorklog = [`🧩 识别为入网口令: ${joinDocUrl}`];
|
|
356
|
+
const r = await joinGatewayFromDoc(joinDocUrl).catch((e) => ({
|
|
357
|
+
ok: false, docUrl: joinDocUrl, steps: [{ step: '入网', ok: false, note: String(e?.message || e).slice(0, 120) }], error: String(e?.message || e),
|
|
358
|
+
}));
|
|
359
|
+
_lastWorklog = [..._lastWorklog, ...r.steps.map((s) => `${s.ok ? '✓' : '✗'} ${s.step}: ${s.note}`)];
|
|
360
|
+
return formatMobileJoinResult(r);
|
|
361
|
+
}
|
|
168
362
|
const bridge = cap && cap.Plugins && cap.Plugins.RokidBridge;
|
|
169
363
|
if (bridge && cap.isNativePlatform?.()) {
|
|
170
364
|
try {
|
package/dist/web/mobile-core.js
CHANGED
|
@@ -86655,15 +86655,20 @@ ${[...listenStats.errors.entries()].map(([addr, err2]) => {
|
|
|
86655
86655
|
// src/web/mobile-agent.ts
|
|
86656
86656
|
var mobile_agent_exports = {};
|
|
86657
86657
|
__export(mobile_agent_exports, {
|
|
86658
|
+
MOBILE_JOIN_DOC_RE: () => MOBILE_JOIN_DOC_RE,
|
|
86658
86659
|
callRemoteAgent: () => callRemoteAgent,
|
|
86659
86660
|
cancelPhoneAgent: () => cancelPhoneAgent,
|
|
86660
86661
|
default: () => mobile_agent_default,
|
|
86662
|
+
detectJoinDocUrl: () => detectJoinDocUrl,
|
|
86661
86663
|
ensureIdentity: () => ensureIdentity,
|
|
86664
|
+
formatMobileJoinResult: () => formatMobileJoinResult,
|
|
86662
86665
|
getLastWorklog: () => getLastWorklog,
|
|
86663
86666
|
getLlmConfig: () => getLlmConfig,
|
|
86667
|
+
getMobileJoinState: () => getMobileJoinState,
|
|
86664
86668
|
handleIncomingAgentMessage: () => handleIncomingAgentMessage,
|
|
86665
86669
|
handleIncomingPhoneMessage: () => handleIncomingPhoneMessage,
|
|
86666
86670
|
identityStatus: () => identityStatus,
|
|
86671
|
+
joinGatewayFromDoc: () => joinGatewayFromDoc,
|
|
86667
86672
|
loginIdentity: () => loginIdentity,
|
|
86668
86673
|
logoutIdentity: () => logoutIdentity,
|
|
86669
86674
|
notifyAgentReply: () => notifyAgentReply,
|
|
@@ -86810,9 +86815,171 @@ ${[...listenStats.errors.entries()].map(([addr, err2]) => {
|
|
|
86810
86815
|
} catch {
|
|
86811
86816
|
}
|
|
86812
86817
|
}
|
|
86818
|
+
function detectJoinDocUrl(text) {
|
|
86819
|
+
const m2 = MOBILE_JOIN_DOC_RE.exec(String(text || ""));
|
|
86820
|
+
return m2 ? m2[1] : null;
|
|
86821
|
+
}
|
|
86822
|
+
function parseFm(text) {
|
|
86823
|
+
const m2 = /^---\r?\n([\s\S]*?)\r?\n---/.exec(String(text || ""));
|
|
86824
|
+
if (!m2) return {};
|
|
86825
|
+
const out = {};
|
|
86826
|
+
for (const line of m2[1].split(/\r?\n/)) {
|
|
86827
|
+
const kv = /^([A-Za-z_][\w-]*)\s*:\s*(.*)$/.exec(line.trim());
|
|
86828
|
+
if (!kv) continue;
|
|
86829
|
+
const k = kv[1].toLowerCase();
|
|
86830
|
+
if (k === "name") out.name = kv[2].trim().replace(/^["']|["']$/g, "");
|
|
86831
|
+
if (k === "version") out.version = kv[2].trim().replace(/^["']|["']$/g, "");
|
|
86832
|
+
}
|
|
86833
|
+
return out;
|
|
86834
|
+
}
|
|
86835
|
+
async function getMobileJoinState() {
|
|
86836
|
+
try {
|
|
86837
|
+
const raw = typeof localStorage !== "undefined" ? localStorage.getItem(MOBILE_JOIN_STATE_KEY) : null;
|
|
86838
|
+
return raw ? JSON.parse(raw) : null;
|
|
86839
|
+
} catch {
|
|
86840
|
+
return null;
|
|
86841
|
+
}
|
|
86842
|
+
}
|
|
86843
|
+
function saveMobileJoinState(s2) {
|
|
86844
|
+
try {
|
|
86845
|
+
if (typeof localStorage !== "undefined") localStorage.setItem(MOBILE_JOIN_STATE_KEY, JSON.stringify(s2));
|
|
86846
|
+
} catch {
|
|
86847
|
+
}
|
|
86848
|
+
}
|
|
86849
|
+
async function joinGatewayFromDoc(docUrl, opts = {}) {
|
|
86850
|
+
const f = opts.fetchImpl || fetch;
|
|
86851
|
+
const steps = [];
|
|
86852
|
+
const url = String(docUrl || "").trim();
|
|
86853
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
86854
|
+
return { ok: false, docUrl: url, steps, error: `\u5165\u7F51\u8BF4\u660E\u5730\u5740\u5FC5\u987B\u662F http(s) URL (\u6536\u5230: ${url.slice(0, 60)})` };
|
|
86855
|
+
}
|
|
86856
|
+
let docVersion;
|
|
86857
|
+
try {
|
|
86858
|
+
const r = await f(url, { signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3) });
|
|
86859
|
+
if (!r.ok) {
|
|
86860
|
+
steps.push({ step: "\u8BFB\u5165\u7F51\u8BF4\u660E", ok: false, note: `\u6587\u6863\u4E0D\u53EF\u8FBE (HTTP ${r.status})` });
|
|
86861
|
+
return { ok: false, docUrl: url, steps, error: `\u5165\u7F51\u8BF4\u660E\u4E0D\u53EF\u8FBE (HTTP ${r.status})` };
|
|
86862
|
+
}
|
|
86863
|
+
const text = await r.text();
|
|
86864
|
+
const fm = parseFm(text);
|
|
86865
|
+
if (fm.name !== "bolloon-gateway-join" && !/加入网关|bolloon-gateway-join/.test(text)) {
|
|
86866
|
+
steps.push({ step: "\u8BFB\u5165\u7F51\u8BF4\u660E", ok: false, note: `\u4E0D\u662F Bolloon \u5165\u7F51\u8BF4\u660E (name=${fm.name || "\u65E0"})` });
|
|
86867
|
+
return { ok: false, docUrl: url, steps, error: "\u8BE5\u6587\u6863\u4E0D\u662F Bolloon \u7F51\u5173\u5165\u7F51\u8BF4\u660E, \u62D2\u7EDD\u636E\u6B64\u5165\u7F51" };
|
|
86868
|
+
}
|
|
86869
|
+
docVersion = fm.version;
|
|
86870
|
+
steps.push({ step: "\u8BFB\u5165\u7F51\u8BF4\u660E", ok: true, note: `${fm.name || "bolloon-gateway-join"} v${fm.version || "?"} (${text.length} \u5B57\u7B26)` });
|
|
86871
|
+
} catch (e) {
|
|
86872
|
+
steps.push({ step: "\u8BFB\u5165\u7F51\u8BF4\u660E", ok: false, note: `\u8BFB\u53D6\u5931\u8D25: ${String(e?.message || e).slice(0, 120)}` });
|
|
86873
|
+
return { ok: false, docUrl: url, steps, error: `\u5165\u7F51\u8BF4\u660E\u8BFB\u53D6\u5931\u8D25: ${String(e?.message || e).slice(0, 120)}` };
|
|
86874
|
+
}
|
|
86875
|
+
let did = String(opts.did || "");
|
|
86876
|
+
if (did) {
|
|
86877
|
+
steps.push({ step: "DID \u8EAB\u4EFD", ok: true, note: `${did} (\u6CE8\u5165\u8EAB\u4EFD)` });
|
|
86878
|
+
} else {
|
|
86879
|
+
try {
|
|
86880
|
+
const id = await ensureIdentity();
|
|
86881
|
+
did = id.did;
|
|
86882
|
+
steps.push({ step: "DID \u8EAB\u4EFD", ok: true, note: `${did} (\u624B\u673A\u7AEF\u672C\u673A\u751F\u6210)` });
|
|
86883
|
+
} catch (e) {
|
|
86884
|
+
steps.push({ step: "DID \u8EAB\u4EFD", ok: false, note: String(e?.message || e).slice(0, 120) });
|
|
86885
|
+
return { ok: false, docUrl: url, docVersion, steps, error: "\u624B\u673A\u7AEF DID \u751F\u6210\u5931\u8D25" };
|
|
86886
|
+
}
|
|
86887
|
+
}
|
|
86888
|
+
const name10 = String(opts.name || "phone-agent");
|
|
86889
|
+
const capabilities = ["chat", "gateway-join"];
|
|
86890
|
+
let desktopBase = String(opts.desktopBaseUrl ?? "");
|
|
86891
|
+
if (opts.desktopBaseUrl === void 0) {
|
|
86892
|
+
try {
|
|
86893
|
+
const g = await Promise.resolve().then(() => (init_mobile_gateway(), mobile_gateway_exports));
|
|
86894
|
+
desktopBase = String(g.getDesktopBaseUrl() || "");
|
|
86895
|
+
} catch {
|
|
86896
|
+
desktopBase = "";
|
|
86897
|
+
}
|
|
86898
|
+
}
|
|
86899
|
+
desktopBase = desktopBase.replace(/\/+$/, "");
|
|
86900
|
+
let registeredOn = "local";
|
|
86901
|
+
if (desktopBase) {
|
|
86902
|
+
try {
|
|
86903
|
+
const r = await f(`${desktopBase}/api/registry/register`, {
|
|
86904
|
+
method: "POST",
|
|
86905
|
+
headers: { "content-type": "application/json" },
|
|
86906
|
+
body: JSON.stringify({
|
|
86907
|
+
agentId: did,
|
|
86908
|
+
name: name10,
|
|
86909
|
+
wallet: "",
|
|
86910
|
+
service: { name: "chat", description: "\u624B\u673A\u7AEF\u667A\u80FD\u4F53 (\u81EA\u8DB3\u8282\u70B9)", price: { amount: "0", currency: "USDC", per: "task" }, endpoint: "" },
|
|
86911
|
+
capabilities
|
|
86912
|
+
}),
|
|
86913
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
|
|
86914
|
+
});
|
|
86915
|
+
if (r.ok) {
|
|
86916
|
+
registeredOn = "desktop";
|
|
86917
|
+
steps.push({ step: "\u670D\u52A1\u767B\u8BB0", ok: true, note: `\u5DF2\u767B\u8BB0\u8FDB\u7535\u8111\u7AEF\u7F51\u7EDC registry (${desktopBase}) \u2014\u2014 \u7F51\u7EDC\u5185\u5176\u4ED6\u667A\u80FD\u4F53\u53EF\u6309\u80FD\u529B\u53D1\u73B0\u6211` });
|
|
86918
|
+
} else {
|
|
86919
|
+
steps.push({ step: "\u670D\u52A1\u767B\u8BB0", ok: false, note: `\u7535\u8111\u7AEF registry \u62D2\u7EDD (HTTP ${r.status}); \u5DF2\u6539\u4E3A\u672C\u673A\u767B\u8BB0` });
|
|
86920
|
+
}
|
|
86921
|
+
} catch (e) {
|
|
86922
|
+
steps.push({ step: "\u670D\u52A1\u767B\u8BB0", ok: false, note: `\u7535\u8111\u7AEF\u4E0D\u53EF\u8FBE (${String(e?.message || e).slice(0, 80)}); \u5DF2\u6539\u4E3A\u672C\u673A\u767B\u8BB0` });
|
|
86923
|
+
}
|
|
86924
|
+
} else {
|
|
86925
|
+
steps.push({ step: "\u670D\u52A1\u767B\u8BB0", ok: true, note: "\u672A\u914D\u7F6E\u7535\u8111\u7AEF\u57FA\u5740 \u2192 \u53EA\u5728\u672C\u673A\u767B\u8BB0 (\u624B\u673A\u662F\u81EA\u6CBB\u8282\u70B9; \u8BBE\u7F6E\u91CC\u586B\u7535\u8111\u7AEF\u5730\u5740\u53EF\u767B\u8BB0\u8FDB\u7F51\u7EDC registry)" });
|
|
86926
|
+
}
|
|
86927
|
+
try {
|
|
86928
|
+
const p2p = await Promise.resolve().then(() => (init_mobile_p2p(), mobile_p2p_exports));
|
|
86929
|
+
let peers = 0;
|
|
86930
|
+
try {
|
|
86931
|
+
peers = (p2p.getConnectedPeers?.() || []).length;
|
|
86932
|
+
} catch {
|
|
86933
|
+
peers = 0;
|
|
86934
|
+
}
|
|
86935
|
+
if (peers > 0 && typeof p2p.sendMobileP2PMessage === "function") {
|
|
86936
|
+
const okAnnounce = await p2p.sendMobileP2PMessage("*", "registry.register", JSON.stringify({ agent_id: did, name: name10, capabilities }), did);
|
|
86937
|
+
steps.push({ step: "P2P \u516C\u544A", ok: !!okAnnounce, note: okAnnounce ? `\u5DF2\u5411 ${peers} \u4E2A\u5BF9\u7AEF\u5E7F\u64AD\u672C\u673A\u58F0\u660E` : `\u5E7F\u64AD\u5931\u8D25 (\u5BF9\u7AEF ${peers} \u4E2A)` });
|
|
86938
|
+
} else {
|
|
86939
|
+
steps.push({ step: "P2P \u516C\u544A", ok: false, note: "\u5F53\u524D\u65E0\u5DF2\u8FDE\u63A5\u5BF9\u7AEF (\u6D4F\u89C8\u5668/\u672A\u8FDE\u7535\u8111\u7AEF\u65F6\u6B63\u5E38) \u2014 \u672C\u673A\u58F0\u660E\u5DF2\u5C31\u7EEA, \u8FDE\u4E0A\u5373\u751F\u6548" });
|
|
86940
|
+
}
|
|
86941
|
+
} catch (e) {
|
|
86942
|
+
steps.push({ step: "P2P \u516C\u544A", ok: false, note: `P2P \u5C42\u4E0D\u53EF\u7528: ${String(e?.message || e).slice(0, 80)}` });
|
|
86943
|
+
}
|
|
86944
|
+
saveMobileJoinState({ url, did, name: name10, capabilities, docVersion, registeredOn, desktopBaseUrl: desktopBase || void 0, joinedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
86945
|
+
const state2 = await getMobileJoinState();
|
|
86946
|
+
steps.push({ step: "\u843D\u76D8\u5165\u7F51\u6001", ok: !!state2?.did, note: `localStorage:${MOBILE_JOIN_STATE_KEY}` });
|
|
86947
|
+
return { ok: true, docUrl: url, docVersion, did, steps };
|
|
86948
|
+
}
|
|
86949
|
+
function formatMobileJoinResult(r) {
|
|
86950
|
+
if (!r.ok) {
|
|
86951
|
+
return `\u274C \u5165\u7F51\u5931\u8D25: ${r.error || "\u672A\u77E5\u539F\u56E0"}
|
|
86952
|
+
|
|
86953
|
+
${r.steps.map((s3) => `${s3.ok ? "\u2713" : "\u2717"} ${s3.step}: ${s3.note}`).join("\n")}`;
|
|
86954
|
+
}
|
|
86955
|
+
const s2 = r.steps.find((x) => x.step === "\u670D\u52A1\u767B\u8BB0");
|
|
86956
|
+
return [
|
|
86957
|
+
"\u2705 \u5DF2\u52A0\u5165\u5168\u7403\u667A\u80FD\u4F53\u7F51\u7EDC (\u624B\u673A\u7AEF\u81EA\u8DB3\u6267\u884C)",
|
|
86958
|
+
"",
|
|
86959
|
+
`DID: ${r.did}`,
|
|
86960
|
+
`\u5165\u7F51\u8BF4\u660E: v${r.docVersion || "?"} (${r.docUrl})`,
|
|
86961
|
+
s2 ? `\u767B\u8BB0: ${s2.note}` : "",
|
|
86962
|
+
"",
|
|
86963
|
+
...r.steps.map((x) => `${x.ok ? "\u2713" : "\u2717"} ${x.step}: ${x.note}`),
|
|
86964
|
+
"",
|
|
86965
|
+
"\u7528\u300C\u7F51\u7EDC \u2192 Agent \u7F51\u7EDC\u300D\u53EF\u67E5\u770B\u6210\u5458; \u8BBE\u7F6E\u91CC\u586B\u7535\u8111\u7AEF\u5730\u5740\u53EF\u628A\u672C\u673A\u767B\u8BB0\u8FDB\u7F51\u7EDC registry\u3002"
|
|
86966
|
+
].filter((l) => l !== "").join("\n");
|
|
86967
|
+
}
|
|
86813
86968
|
async function runLocalAgent(goal) {
|
|
86814
86969
|
const win = typeof window !== "undefined" ? window : null;
|
|
86815
86970
|
const cap = win?.Capacitor;
|
|
86971
|
+
const joinDocUrl = detectJoinDocUrl(goal);
|
|
86972
|
+
if (joinDocUrl) {
|
|
86973
|
+
_lastWorklog = [`\u{1F9E9} \u8BC6\u522B\u4E3A\u5165\u7F51\u53E3\u4EE4: ${joinDocUrl}`];
|
|
86974
|
+
const r = await joinGatewayFromDoc(joinDocUrl).catch((e) => ({
|
|
86975
|
+
ok: false,
|
|
86976
|
+
docUrl: joinDocUrl,
|
|
86977
|
+
steps: [{ step: "\u5165\u7F51", ok: false, note: String(e?.message || e).slice(0, 120) }],
|
|
86978
|
+
error: String(e?.message || e)
|
|
86979
|
+
}));
|
|
86980
|
+
_lastWorklog = [..._lastWorklog, ...r.steps.map((s2) => `${s2.ok ? "\u2713" : "\u2717"} ${s2.step}: ${s2.note}`)];
|
|
86981
|
+
return formatMobileJoinResult(r);
|
|
86982
|
+
}
|
|
86816
86983
|
const bridge = cap && cap.Plugins && cap.Plugins.RokidBridge;
|
|
86817
86984
|
if (bridge && cap.isNativePlatform?.()) {
|
|
86818
86985
|
try {
|
|
@@ -87068,7 +87235,7 @@ ${[...listenStats.errors.entries()].map(([addr, err2]) => {
|
|
|
87068
87235
|
} catch {
|
|
87069
87236
|
}
|
|
87070
87237
|
}
|
|
87071
|
-
var IDENTITY_DB, _identity, _identityDb, _llmConfig, _lastWorklog, _send, _ownDid, replyHandlers, inboundChatHandlers, mobile_agent_default;
|
|
87238
|
+
var IDENTITY_DB, _identity, _identityDb, _llmConfig, _lastWorklog, MOBILE_JOIN_DOC_RE, MOBILE_JOIN_STATE_KEY, _send, _ownDid, replyHandlers, inboundChatHandlers, mobile_agent_default;
|
|
87072
87239
|
var init_mobile_agent = __esm({
|
|
87073
87240
|
"src/web/mobile-agent.ts"() {
|
|
87074
87241
|
"use strict";
|
|
@@ -87078,6 +87245,8 @@ ${[...listenStats.errors.entries()].map(([addr, err2]) => {
|
|
|
87078
87245
|
_identityDb = null;
|
|
87079
87246
|
_llmConfig = null;
|
|
87080
87247
|
_lastWorklog = [];
|
|
87248
|
+
MOBILE_JOIN_DOC_RE = /read\s+(https?:\/\/\S*bolloon-gateway-join\.md)/i;
|
|
87249
|
+
MOBILE_JOIN_STATE_KEY = "bolloon_gateway_join";
|
|
87081
87250
|
_send = null;
|
|
87082
87251
|
_ownDid = "";
|
|
87083
87252
|
replyHandlers = /* @__PURE__ */ new Set();
|
package/dist/web/server.js
CHANGED
|
@@ -3020,14 +3020,61 @@ ${goalDesc}
|
|
|
3020
3020
|
// 2026-09-15: agent-delegate (manifest 协议 + agent_delegate) **启动即挂载**
|
|
3021
3021
|
// 文档 bolloon-gateway-join.md 第 3 节要求 POST /api/agent/register 可用;
|
|
3022
3022
|
// 之前只在 iroh 懒初始化 (/api/iroh/info 首次访问) 的收尾挂上 → 没触发时恒 404。
|
|
3023
|
+
//
|
|
3024
|
+
// 2026-09-15 (二修): 同时注入**真执行器** —— 被委派的一方不再是
|
|
3025
|
+
// `resultCid: mock-<ts>` 的假签收: 真的跑本地 agent session 出结果,
|
|
3026
|
+
// 并把结果存进 OrbitDB 得到真实内容寻址 CID; 无活跃 channel / 无 LLM 时如实报错。
|
|
3027
|
+
const delegateExecutor = async (req) => {
|
|
3028
|
+
try {
|
|
3029
|
+
const channels = await loadChannels();
|
|
3030
|
+
const active = (() => {
|
|
3031
|
+
try {
|
|
3032
|
+
const raw = fsSync.existsSync(path.join(process.env.HOME || '/tmp', '.bolloon', 'active-channel.json'))
|
|
3033
|
+
? fsSync.readFileSync(path.join(process.env.HOME || '/tmp', '.bolloon', 'active-channel.json'), 'utf-8') : '';
|
|
3034
|
+
return raw ? JSON.parse(raw) : null;
|
|
3035
|
+
}
|
|
3036
|
+
catch {
|
|
3037
|
+
return null;
|
|
3038
|
+
}
|
|
3039
|
+
})();
|
|
3040
|
+
const ch = channels.find((c) => c.id === active?.channelId) || channels.find((c) => c.id === active?.id) || channels[0];
|
|
3041
|
+
if (!ch)
|
|
3042
|
+
return { ok: false, summary: 'no local channel to execute with', error: 'no-channel' };
|
|
3043
|
+
const agent = await getAgentForChannel(ch.id, ch.publicKey, ch.name, ch.didDocument);
|
|
3044
|
+
const task = [
|
|
3045
|
+
`【被委派任务 · 能力 ${req.capability}】`,
|
|
3046
|
+
req.instruction,
|
|
3047
|
+
req.docPath ? `\n资料路径: ${req.docPath}` : '',
|
|
3048
|
+
req.docContent ? `\n资料内容:\n${req.docContent}` : '',
|
|
3049
|
+
`\n(来自 agent ${req.fromAgentId || 'unknown'}),请直接给出可交付结果。`,
|
|
3050
|
+
].join('');
|
|
3051
|
+
const out = await agent.prompt(task);
|
|
3052
|
+
const text = String(out || '').trim();
|
|
3053
|
+
if (!text || text.startsWith('❌') || text.startsWith('[AI 服务调用失败]')) {
|
|
3054
|
+
return { ok: false, summary: text.slice(0, 800) || '(空结果)', error: 'execution-failed' };
|
|
3055
|
+
}
|
|
3056
|
+
// 真结果 → 内容寻址存储 (CID), 失败不编造 CID, 只回摘要
|
|
3057
|
+
let cid;
|
|
3058
|
+
try {
|
|
3059
|
+
const { getCIDDatabase } = await import('../orbitdb/cid-database.js');
|
|
3060
|
+
const rec = await getCIDDatabase().save({ agentId: req.targetAgentId, type: 'context', content: text, metadata: { capability: req.capability, fromAgentId: req.fromAgentId, kind: 'delegate-result' } });
|
|
3061
|
+
cid = rec?.id;
|
|
3062
|
+
}
|
|
3063
|
+
catch { /* 存不上就不给 CID */ }
|
|
3064
|
+
return { ok: true, summary: text.slice(0, 4000), resultCid: cid };
|
|
3065
|
+
}
|
|
3066
|
+
catch (e) {
|
|
3067
|
+
return { ok: false, summary: `delegate executor failed: ${String(e?.message || e).slice(0, 300)}`, error: 'executor-error' };
|
|
3068
|
+
}
|
|
3069
|
+
};
|
|
3023
3070
|
let agentDelegateMounted = false;
|
|
3024
3071
|
try {
|
|
3025
3072
|
const delegateTransport = createIrohDelegateTransport({ verbose: true });
|
|
3026
3073
|
// 注意: createAgentDelegateApp 内部声明的是绝对路径 (/api/agent/...),
|
|
3027
3074
|
// 所以挂载时**不能**再带 '/api/agent' 前缀 (否则变成 /api/agent/api/agent/... 恒 404)
|
|
3028
|
-
app.use(createAgentDelegateApp(delegateTransport));
|
|
3075
|
+
app.use(createAgentDelegateApp(delegateTransport, { execute: delegateExecutor }));
|
|
3029
3076
|
agentDelegateMounted = true;
|
|
3030
|
-
console.log('[agent-delegate] 已挂载到 /api/agent (启动即用: local-manifest / register / pick / delegate)');
|
|
3077
|
+
console.log('[agent-delegate] 已挂载到 /api/agent (启动即用: local-manifest / register / pick / delegate, 真执行器已注入)');
|
|
3031
3078
|
}
|
|
3032
3079
|
catch (e) {
|
|
3033
3080
|
console.warn('[agent-delegate] 挂载失败 (非致命):', e?.message);
|
|
@@ -6588,10 +6635,10 @@ ${goalDesc}
|
|
|
6588
6635
|
if (!agentDelegateMounted) {
|
|
6589
6636
|
try {
|
|
6590
6637
|
const delegateTransport = createIrohDelegateTransport({ verbose: true });
|
|
6591
|
-
const delegateApp = createAgentDelegateApp(delegateTransport);
|
|
6638
|
+
const delegateApp = createAgentDelegateApp(delegateTransport, { execute: delegateExecutor });
|
|
6592
6639
|
app.use(delegateApp);
|
|
6593
6640
|
agentDelegateMounted = true;
|
|
6594
|
-
console.log('[iroh API] agent-delegate app 已挂载到 /api/agent (
|
|
6641
|
+
console.log('[iroh API] agent-delegate app 已挂载到 /api/agent (补挂, 真执行器已注入)');
|
|
6595
6642
|
}
|
|
6596
6643
|
catch (e) {
|
|
6597
6644
|
console.error('[iroh API] 挂载 agent-delegate app 失败:', e);
|