@bolloon/bolloon-agent 0.4.18 → 0.4.19
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-registry.js +24 -0
- package/dist/agents/gateway-network.js +192 -14
- package/dist/agents/network-link.js +33 -0
- package/dist/index.js +68 -0
- package/dist/web/mobile-agent.js +6 -0
- package/dist/web/mobile-core.js +12533 -149
- package/dist/web/mobile-gateway.js +191 -0
- package/dist/web/mobile.html +7 -0
- package/dist/web/mobile.js +52 -1
- package/dist/web/qr.js +49 -0
- package/package.json +4 -1
|
@@ -13,6 +13,8 @@ import * as path from 'path';
|
|
|
13
13
|
import { getCIDDatabase } from '../orbitdb/cid-database.js';
|
|
14
14
|
const home = () => process.env.HOME || os.homedir() || '/tmp';
|
|
15
15
|
export const REGISTRY_ORBIT_KEY = 'services';
|
|
16
|
+
/** 网络启动包 meta 键 (networkId/name/version/capacityOfMembers/sharedContextCid) */
|
|
17
|
+
export const REGISTRY_ORBIT_META_KEY = 'meta';
|
|
16
18
|
/** OrbitDB store 名 (按 did 短名确定性) */
|
|
17
19
|
export function registryStoreName(did) {
|
|
18
20
|
const short = String(did || 'local').replace(/[^a-zA-Z0-9-]/g, '_').slice(0, 40) || 'local';
|
|
@@ -118,6 +120,28 @@ export class OrbitDBAgentRegistry {
|
|
|
118
120
|
return orbit;
|
|
119
121
|
return this.loadLocalFromDisk();
|
|
120
122
|
}
|
|
123
|
+
async writeMeta(meta) {
|
|
124
|
+
if (!this.ready || !this._store)
|
|
125
|
+
return { ok: false, error: 'registry store 未就绪 (离线模式)' };
|
|
126
|
+
try {
|
|
127
|
+
await this._store.put(REGISTRY_ORBIT_META_KEY, meta);
|
|
128
|
+
return { ok: true };
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
return { ok: false, error: String(e?.message || e).slice(0, 120) };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async readMeta() {
|
|
135
|
+
if (!this.ready || !this._store)
|
|
136
|
+
return null;
|
|
137
|
+
try {
|
|
138
|
+
const v = await this._store.get(REGISTRY_ORBIT_META_KEY);
|
|
139
|
+
return v && typeof v === 'object' ? v : null;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
121
145
|
async discover(query) {
|
|
122
146
|
const all = await this.list();
|
|
123
147
|
const q = String(query || '').trim().toLowerCase();
|
|
@@ -182,30 +182,34 @@ export async function joinNetwork(link, deps) {
|
|
|
182
182
|
// 合并到本地 registry (按 agentId + service.name 去重)
|
|
183
183
|
const registry = deps?.registry ?? getAgentRegistry();
|
|
184
184
|
const local = await registry.list();
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const exists = local.some((l) => l.agentId === svc.agentId && l.service?.name === svc.service?.name);
|
|
190
|
-
if (!exists) {
|
|
185
|
+
const remoteServices = Array.isArray(remote) ? remote : [];
|
|
186
|
+
const { merged, joined } = mergeRemoteServices(remoteServices, local);
|
|
187
|
+
for (const svc of merged) {
|
|
188
|
+
if (!local.some((l) => l.agentId === svc.agentId && l.service?.name === svc.service?.name)) {
|
|
191
189
|
await registry.register(svc).catch(() => { });
|
|
192
|
-
joined++;
|
|
193
190
|
}
|
|
194
191
|
}
|
|
195
|
-
//
|
|
192
|
+
// ① 网络启动包 (meta): 拉网络声明 (networkId/名称/版本/容量/共享context CID), 尽力而为
|
|
193
|
+
const meta = await fetchNetworkMeta(parsed).catch(() => null);
|
|
194
|
+
const bootstrap = buildNetworkBootstrap(meta);
|
|
195
|
+
// 记录成员身份 (持久化 → 重启后自动恢复) + 网络启动包信息
|
|
196
196
|
await saveNetworks([
|
|
197
197
|
...existing,
|
|
198
198
|
{
|
|
199
199
|
link: norm,
|
|
200
200
|
linkKey: identityKey,
|
|
201
201
|
kind: parsed.kind,
|
|
202
|
-
name: parsed.networkName,
|
|
202
|
+
name: bootstrap.name || parsed.networkName,
|
|
203
203
|
joinedAt: new Date().toISOString(),
|
|
204
|
-
serviceCount:
|
|
204
|
+
serviceCount: remoteServices.length,
|
|
205
205
|
lastSyncAt: new Date().toISOString(),
|
|
206
|
+
networkId: bootstrap.networkId,
|
|
207
|
+
version: bootstrap.version,
|
|
208
|
+
capacityOfMembers: bootstrap.capacityOfMembers,
|
|
209
|
+
sharedContextCid: bootstrap.sharedContextCid,
|
|
206
210
|
},
|
|
207
211
|
]);
|
|
208
|
-
return { ok: true, joined, total:
|
|
212
|
+
return { ok: true, joined, total: remoteServices.length, linkKind: parsed.kind, networkName: bootstrap.name || parsed.networkName, networkId: bootstrap.networkId, sharedContextCid: bootstrap.sharedContextCid };
|
|
209
213
|
}
|
|
210
214
|
/** 启动恢复: 重拉所有已加入网络 (失败静默, 保留记录). 返回恢复统计. */
|
|
211
215
|
export async function restoreJoinedNetworks() {
|
|
@@ -228,6 +232,156 @@ export async function restoreJoinedNetworks() {
|
|
|
228
232
|
}
|
|
229
233
|
return { restored, failed, total: nets.length };
|
|
230
234
|
}
|
|
235
|
+
/** 把任意 meta 对象规整成 NetworkBootstrap (纯函数, 容错) */
|
|
236
|
+
export function buildNetworkBootstrap(meta) {
|
|
237
|
+
const m = meta && typeof meta === 'object' ? meta : {};
|
|
238
|
+
const str = (v) => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
|
|
239
|
+
return {
|
|
240
|
+
networkId: str(m.networkId),
|
|
241
|
+
name: str(m.name),
|
|
242
|
+
version: str(m.version),
|
|
243
|
+
capacityOfMembers: typeof m.capacityOfMembers === 'number' ? m.capacityOfMembers : undefined,
|
|
244
|
+
sharedContextCid: str(m.sharedContextCid),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/** 拉取网络 meta (尽力而为, 失败返回 null): orbitdb('meta' 键) / ipns(network.json) / http(doc.meta) */
|
|
248
|
+
async function fetchNetworkMeta(parsed) {
|
|
249
|
+
try {
|
|
250
|
+
if (parsed.kind === 'http') {
|
|
251
|
+
const r = await fetch(parsed.url, { signal: AbortSignal.timeout(15000) });
|
|
252
|
+
if (!r.ok)
|
|
253
|
+
return null;
|
|
254
|
+
const d = await r.json();
|
|
255
|
+
return d?.meta ?? null;
|
|
256
|
+
}
|
|
257
|
+
if (parsed.kind === 'ipns') {
|
|
258
|
+
const r = await fetch(`http://127.0.0.1:8080/ipns/${parsed.name}/network.json`, { signal: AbortSignal.timeout(15000) });
|
|
259
|
+
if (!r.ok)
|
|
260
|
+
return null;
|
|
261
|
+
const d = await r.json();
|
|
262
|
+
return d?.meta ?? d;
|
|
263
|
+
}
|
|
264
|
+
if (parsed.kind === 'orbitdb') {
|
|
265
|
+
const { getCIDDatabase } = await import('../orbitdb/cid-database.js');
|
|
266
|
+
const db = getCIDDatabase();
|
|
267
|
+
const store = await db.openStoreByAddress(parsed.address, 'keyvalue');
|
|
268
|
+
if (!store)
|
|
269
|
+
return null;
|
|
270
|
+
const v = await store.get('meta').catch(() => null);
|
|
271
|
+
return v ?? null;
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/** 纯函数: 远端服务并入本地 (按 agentId + service.name 去重), 返回合并结果 + 新增数 */
|
|
280
|
+
export function mergeRemoteServices(remote, local) {
|
|
281
|
+
const out = local.slice();
|
|
282
|
+
let joined = 0;
|
|
283
|
+
for (const svc of remote) {
|
|
284
|
+
if (!svc?.agentId || !svc?.service?.name)
|
|
285
|
+
continue;
|
|
286
|
+
const exists = out.some((l) => l.agentId === svc.agentId && l.service?.name === svc.service?.name);
|
|
287
|
+
if (!exists) {
|
|
288
|
+
out.push(svc);
|
|
289
|
+
joined++;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return { merged: out, joined };
|
|
293
|
+
}
|
|
294
|
+
/** ② 拉取网络画像 (只读, 不落盘): 供 on-join 告知 agent "网络里有什么" */
|
|
295
|
+
export async function pullNetworkProfile(link) {
|
|
296
|
+
const parsed = parseNetworkLink(link);
|
|
297
|
+
if (!parsed)
|
|
298
|
+
return null;
|
|
299
|
+
let services = null;
|
|
300
|
+
if (parsed.kind === 'http')
|
|
301
|
+
services = await fetchRemoteRegistry(parsed.url);
|
|
302
|
+
else if (parsed.kind === 'ipns')
|
|
303
|
+
services = await fetchIpnsRegistry(parsed.name);
|
|
304
|
+
else if (parsed.kind === 'orbitdb')
|
|
305
|
+
services = await fetchOrbitdbRegistry(parsed.address);
|
|
306
|
+
const meta = await fetchNetworkMeta(parsed).catch(() => null);
|
|
307
|
+
if (!services || services.length === 0)
|
|
308
|
+
return null;
|
|
309
|
+
return { bootstrap: buildNetworkBootstrap(meta), members: services };
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* ③ 成员自描述广播: 把本机服务声明(list .agentId 匹配 self)写回共享网络 store ('services' 合并 self).
|
|
313
|
+
* ① ipns/http 无回写 → 只本地登记 (返回 note); ② orbitdb replica 只读时写穿失败 → 非致命 (返回 note).
|
|
314
|
+
* opts.members: 本机要广播的成员声明 (通常 = 自己注册的 AgentService 列表).
|
|
315
|
+
*/
|
|
316
|
+
export async function networkShareSelf(link, members, opts) {
|
|
317
|
+
if (!members || members.length === 0)
|
|
318
|
+
return { ok: true, note: '无本机成员声明可广播' };
|
|
319
|
+
const parsed = parseNetworkLink(link);
|
|
320
|
+
if (!parsed)
|
|
321
|
+
return { ok: false, error: '链接无法解析' };
|
|
322
|
+
if (parsed.kind !== 'orbitdb') {
|
|
323
|
+
return { ok: true, note: `${parsed.kind} 无回写能力, 已本地登记 (发起方自托管 registry 应含本机)` };
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const { getCIDDatabase } = await import('../orbitdb/cid-database.js');
|
|
327
|
+
const db = getCIDDatabase();
|
|
328
|
+
const store = await db.openStoreByAddress(parsed.address, 'keyvalue');
|
|
329
|
+
if (!store)
|
|
330
|
+
return { ok: false, error: '网络 store 不可达' };
|
|
331
|
+
const cur = (await store.get('services').catch(() => null));
|
|
332
|
+
const arr = Array.isArray(cur) ? cur.slice() : [];
|
|
333
|
+
for (const m of members) {
|
|
334
|
+
const i = arr.findIndex((s) => s.agentId === m.agentId && s.service?.name === m.service?.name);
|
|
335
|
+
const entry = { ...m, updatedAt: new Date().toISOString(), registeredAt: m.registeredAt || new Date().toISOString() };
|
|
336
|
+
if (i >= 0)
|
|
337
|
+
arr[i] = entry;
|
|
338
|
+
else
|
|
339
|
+
arr.push(entry);
|
|
340
|
+
}
|
|
341
|
+
await store.put('services', arr);
|
|
342
|
+
return { ok: true, note: `已广播 ${members.length} 条本机声明到网络` };
|
|
343
|
+
}
|
|
344
|
+
catch (e) {
|
|
345
|
+
return { ok: false, error: `写穿失败(疑似只读 replica): ${String(e?.message || e).slice(0, 120)}`, note: '已在本地登记, 可在你的分享链接中带上本机' };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* ① 拉取共享 context (由启动包 sharedContextCid 指向): 从本地 IPFS 网关读文本返回,
|
|
350
|
+
* 调用方负责注入本地 context (了解"所有信息"的最后一段).
|
|
351
|
+
*/
|
|
352
|
+
export async function pullNetworkSharedContext(cid) {
|
|
353
|
+
try {
|
|
354
|
+
const r = await fetch(`http://127.0.0.1:8080/ipfs/${cid}`, { signal: AbortSignal.timeout(15000) });
|
|
355
|
+
if (!r.ok)
|
|
356
|
+
return null;
|
|
357
|
+
return await r.text();
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* ① 发布共享 context 到 OrbitDB (内容寻址), 返回 CID — 供 shareNetworkLink 的 sharedContextCid,
|
|
365
|
+
* 手机/PC 入网后拉取即"近期上下文同步". opts.share=true 时把块放入 helia blockstore 供网络拉取.
|
|
366
|
+
*/
|
|
367
|
+
export async function publishNetworkSharedContext(text, opts) {
|
|
368
|
+
try {
|
|
369
|
+
const { getCIDDatabase } = await import('../orbitdb/cid-database.js');
|
|
370
|
+
const db = getCIDDatabase();
|
|
371
|
+
const rec = await db.save({ agentId: opts?.agentId || 'bolloon-network', type: 'context', content: text });
|
|
372
|
+
let cid = rec.id;
|
|
373
|
+
if (opts?.share !== false) {
|
|
374
|
+
try {
|
|
375
|
+
cid = await db.share(cid);
|
|
376
|
+
}
|
|
377
|
+
catch { /* 分享失败仍可用 CID 读 */ }
|
|
378
|
+
}
|
|
379
|
+
return { ok: true, cid };
|
|
380
|
+
}
|
|
381
|
+
catch (e) {
|
|
382
|
+
return { ok: false, error: String(e?.message || e).slice(0, 140) };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
231
385
|
// ============ 分享链接 ============
|
|
232
386
|
/**
|
|
233
387
|
* 生成本机可分享的网络链接: orbitdb://<registry storeAddress>?name=<网络名>.
|
|
@@ -244,8 +398,24 @@ export async function shareNetworkLink(opts) {
|
|
|
244
398
|
if (!registry.ready || !registry.storeAddress) {
|
|
245
399
|
return { ok: false, error: 'OrbitDB registry 未就绪 (离线模式). 备选: 把 registry 列表发布成 https://.../registry 端点分享' };
|
|
246
400
|
}
|
|
247
|
-
|
|
248
|
-
|
|
401
|
+
// ① 全量启动包: 把 networkId/名称/版本/容量/共享context CID 写进共享 store (尽力而为, 失败仍分享)
|
|
402
|
+
const networkId = opts?.meta?.networkId || registry.storeName;
|
|
403
|
+
const name = String(opts?.name || opts?.meta?.name || registry.storeName || 'bolloon-network');
|
|
404
|
+
const metaObj = {
|
|
405
|
+
networkId,
|
|
406
|
+
name,
|
|
407
|
+
version: opts?.meta?.version,
|
|
408
|
+
capacityOfMembers: opts?.meta?.capacityOfMembers,
|
|
409
|
+
sharedContextCid: opts?.meta?.sharedContextCid,
|
|
410
|
+
};
|
|
411
|
+
let note;
|
|
412
|
+
if (registry.writeMeta) {
|
|
413
|
+
const w = await registry.writeMeta(metaObj).catch(() => ({ ok: false, error: 'writeMeta 异常' }));
|
|
414
|
+
if (!w.ok)
|
|
415
|
+
note = `meta 写共享库失败 (${w.error || '未知'}) — 读方仍可拉到服务, 但拿不到启动包`;
|
|
416
|
+
}
|
|
417
|
+
const encodedName = encodeURIComponent(name);
|
|
418
|
+
return { ok: true, link: `orbitdb://${registry.storeAddress}?name=${encodedName}`, note };
|
|
249
419
|
}
|
|
250
420
|
catch (e) {
|
|
251
421
|
return { ok: false, error: `生成分享链接失败: ${String(e?.message || e).slice(0, 160)}` };
|
|
@@ -268,7 +438,15 @@ export async function maybeAutoJoinGateway(text, deps) {
|
|
|
268
438
|
return null; // 已在网络, 静默
|
|
269
439
|
if (r.ok) {
|
|
270
440
|
const netName = r.networkName ? `「${r.networkName}」` : ''; // URLSearchParams 已 decode
|
|
271
|
-
|
|
441
|
+
// ② on-join 广播: 写入自己的服务声明 (orbitdb 可回写时), 非致命
|
|
442
|
+
let selfNote = '';
|
|
443
|
+
if (deps?.self && deps.self.length > 0) {
|
|
444
|
+
const s = await networkShareSelf(link, deps.self, deps).catch(() => null);
|
|
445
|
+
selfNote = s?.ok ? ' · 已广播本机声明' : (s?.note ? ` · ${s.note}` : ' · 本机声明广播失败');
|
|
446
|
+
}
|
|
447
|
+
const bootPart = r.networkId ? ` (net=${String(r.networkId).slice(0, 16)})` : '';
|
|
448
|
+
const ctxPart = r.sharedContextCid ? ` · 共享ctx:${String(r.sharedContextCid).slice(0, 12)}` : '';
|
|
449
|
+
return `🆕 已自动加入 Agent 网络${netName}${bootPart} (${r.linkKind}): 拉取 ${r.total} 个服务, 新增 ${r.joined} 个${ctxPart}${selfNote}。用 gateway_status 查看, gateway_call 调用网络里的服务。`;
|
|
272
450
|
}
|
|
273
451
|
return `⚠️ 检测到 Agent 网络链接 (${r.linkKind || 'unknown'}) 但加入失败: ${r.error}`;
|
|
274
452
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// ─── 网络链接纯函数 (无 Node 依赖, 桌面/手机浏览器共用) ────────────────────
|
|
2
|
+
// parseNetworkLink / detectGatewayLink 从 gateway-network.ts 抽出,
|
|
3
|
+
// 供 browser 端 (mobile-gateway) 复用, 不拖 OrbitDB/fs 进 WebView.
|
|
4
|
+
/** 解析链接字符串 → ParsedNetworkLink (剥离 ?name= query) */
|
|
5
|
+
export function parseNetworkLink(link) {
|
|
6
|
+
const l = String(link || '').trim();
|
|
7
|
+
if (!l)
|
|
8
|
+
return null;
|
|
9
|
+
const [base, query] = l.split('?');
|
|
10
|
+
let networkName;
|
|
11
|
+
try {
|
|
12
|
+
networkName = query ? (new URLSearchParams(query).get('name') || undefined) : undefined;
|
|
13
|
+
}
|
|
14
|
+
catch { /* 忽略坏 query */ }
|
|
15
|
+
if (base.startsWith('ipns://'))
|
|
16
|
+
return { kind: 'ipns', value: base.slice('ipns://'.length), url: base, networkName };
|
|
17
|
+
if (base.startsWith('orbitdb://')) {
|
|
18
|
+
let address = base.slice('orbitdb://'.length);
|
|
19
|
+
if (!address.startsWith('/'))
|
|
20
|
+
address = `/${address}`;
|
|
21
|
+
return { kind: 'orbitdb', value: address, url: base, networkName };
|
|
22
|
+
}
|
|
23
|
+
if (base.startsWith('http://') || base.startsWith('https://'))
|
|
24
|
+
return { kind: 'http', value: base, url: base, networkName };
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/** 从消息文本检测 gateway 链接 (自动加入触发器用) */
|
|
28
|
+
export function detectGatewayLink(text) {
|
|
29
|
+
const t = String(text || '');
|
|
30
|
+
const re = /(orbitdb:\/\/\/?orbitdb\/[^\s)'"<>,。;]+|ipns:\/\/[^\s)'"<>,。;]+|https?:\/\/[^\s)'"<>,。;]*\/registry(?:\?[^\s)'"<>,。;]*)?)/;
|
|
31
|
+
const m = re.exec(t);
|
|
32
|
+
return m ? m[1].trim() : null;
|
|
33
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1122,6 +1122,74 @@ async function processInput(input, comm) {
|
|
|
1122
1122
|
}
|
|
1123
1123
|
// ==================== 2026-08-06: 系统命令组 (/model /now /ipfs /memory ...) ====================
|
|
1124
1124
|
const cmd = trimmed.toLowerCase();
|
|
1125
|
+
// /net — Agent 网络快捷命令 (join/status/ctx, 2026-09-08)
|
|
1126
|
+
if (cmd === '/net' || cmd.startsWith('/net ')) {
|
|
1127
|
+
const arg = trimmed.slice(4).trim();
|
|
1128
|
+
if (arg.toLowerCase().startsWith('join ')) {
|
|
1129
|
+
const link = arg.slice(5).trim();
|
|
1130
|
+
const { joinNetwork, pullNetworkProfile, pullNetworkSharedContext, networkShareSelf } = await import('./agents/gateway-network.js');
|
|
1131
|
+
const r = await joinNetwork(link);
|
|
1132
|
+
if (!r.ok) {
|
|
1133
|
+
appendLine(`${C_ERROR}加入失败: ${r.error}${RESET}`);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
appendLine(r.already ? `${C_DIM}已在网络 (${r.linkKind})${RESET}` : `${C_OK}已加入${RESET} (${r.linkKind}) · ${r.total} 服务 · 新增 ${r.joined}${r.networkId ? ` · ${C_DIM}net=${String(r.networkId).slice(0, 16)}${RESET}` : ''}`);
|
|
1137
|
+
const profile = await pullNetworkProfile(link).catch(() => null);
|
|
1138
|
+
if (profile?.members?.length) {
|
|
1139
|
+
appendLine(`${C_ACCENT}网络画像:${RESET}`);
|
|
1140
|
+
for (const m of profile.members.slice(0, 8))
|
|
1141
|
+
appendLine(` ${m.name} ${C_DIM}(${String(m.agentId).slice(0, 16)}…) · ${m.service?.name || ''}${RESET}`);
|
|
1142
|
+
}
|
|
1143
|
+
if (profile?.bootstrap?.sharedContextCid) {
|
|
1144
|
+
const ctx = await pullNetworkSharedContext(profile.bootstrap.sharedContextCid).catch(() => null);
|
|
1145
|
+
if (ctx)
|
|
1146
|
+
appendLine(`${C_DIM}📡 共享context: ${ctx.slice(0, 140).replace(/\n/g, ' ')}${RESET}`);
|
|
1147
|
+
}
|
|
1148
|
+
try {
|
|
1149
|
+
const self = { agentId: cliAgentId || 'cli-agent', name: cliAgentName || 'bolloon', wallet: '0x0', service: { name: 'agent', description: 'bolloon cli node', price: { amount: '0', currency: 'USDC', per: 'query' } } };
|
|
1150
|
+
const s = await networkShareSelf(link, [self]);
|
|
1151
|
+
if (s?.ok && s.note)
|
|
1152
|
+
appendLine(`${C_DIM}${s.note}${RESET}`);
|
|
1153
|
+
}
|
|
1154
|
+
catch { /* 广播失败不致命 */ }
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
if (arg.toLowerCase() === 'status' || arg === '') {
|
|
1158
|
+
const { listJoinedNetworks } = await import('./agents/gateway-network.js');
|
|
1159
|
+
const nets = await listJoinedNetworks();
|
|
1160
|
+
appendLine(nets.length
|
|
1161
|
+
? `🔗 ${C_ACCENT}已加入网络:${RESET}\n` + nets.map((n) => ` ${n.name || n.kind} ${C_DIM}(${n.serviceCount}S${n.networkId ? ` · ${String(n.networkId).slice(0, 16)}` : ''})${RESET}`).join('\n')
|
|
1162
|
+
: `${C_DIM}未加入任何网络 — /net join <链接>${RESET}`);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
if (arg.toLowerCase() === 'qr') {
|
|
1166
|
+
const { shareNetworkLink } = await import('./agents/gateway-network.js');
|
|
1167
|
+
const sh = await shareNetworkLink({ name: cliAgentName || 'bolloon' });
|
|
1168
|
+
if (!sh.link) {
|
|
1169
|
+
appendLine(`${C_ERROR}生成链接失败: ${sh.error}${RESET}`);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
const { buildQrPayload, encodeQrTerminal } = await import('./web/qr.js');
|
|
1173
|
+
const payload = buildQrPayload({ link: sh.link, version: '1' });
|
|
1174
|
+
const qr = await encodeQrTerminal(payload);
|
|
1175
|
+
if (qr) {
|
|
1176
|
+
appendLine(renderMessageBox({ title: '📷 扫码入网', body: `${qr}\n\n${C_DIM}链接 (手机粘贴也可): ${payload}${RESET}`, color: C_ACCENT, maxLines: 0 }));
|
|
1177
|
+
}
|
|
1178
|
+
else {
|
|
1179
|
+
appendLine(`${C_ERROR}二维码生成失败 — 链接: ${payload}${RESET}`);
|
|
1180
|
+
}
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
if (arg.toLowerCase().startsWith('ctx ')) {
|
|
1184
|
+
const text = arg.slice(4).trim();
|
|
1185
|
+
const { publishNetworkSharedContext } = await import('./agents/gateway-network.js');
|
|
1186
|
+
const p = await publishNetworkSharedContext(text);
|
|
1187
|
+
appendLine(p.ok ? `${C_OK}共享context已发布:${RESET} ${p.cid}` : `${C_ERROR}发布失败: ${p.error}${RESET}`);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
appendLine(`${C_DIM}用法: /net join <链接> | /net status | /net ctx <文本>${RESET}`);
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1125
1193
|
// /model — 模型供应商选择器 (ink 交互渲染, 复用 MentionPopup)
|
|
1126
1194
|
if (cmd === '/model') {
|
|
1127
1195
|
try {
|
package/dist/web/mobile-agent.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
// ============ 身份 (WebCrypto) ============
|
|
14
14
|
const IDENTITY_DB = 'bolloon-mobile';
|
|
15
15
|
let _identity = null;
|
|
16
|
+
// #2 手机自动入网 (browser-safe gateway, 与桌面同一协议): 检测到链接自动 join
|
|
17
|
+
import { mobileAutoJoinGateway } from './mobile-gateway.js';
|
|
16
18
|
async function generateDID() {
|
|
17
19
|
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
18
20
|
const digest = await crypto.subtle.digest('SHA-256', bytes);
|
|
@@ -269,6 +271,10 @@ export async function handleIncomingAgentMessage(type, payload, fromPeer) {
|
|
|
269
271
|
try {
|
|
270
272
|
const { text, channelId } = JSON.parse(payload);
|
|
271
273
|
notifyInboundChat(text || '', channelId || '', fromPeer);
|
|
274
|
+
// #2 手机自动入网: 消息里带 network 链接 → 自动 join (browser-safe, 不阻塞回复)
|
|
275
|
+
if (text && /orbitdb:\/\/|ipns:\/\/|https?:\/\/[^\s]*\/registry/.test(text)) {
|
|
276
|
+
void mobileAutoJoinGateway(text).catch(() => { });
|
|
277
|
+
}
|
|
272
278
|
const reply = await runLocalAgent(text || '');
|
|
273
279
|
await _send('agent.chat.reply', JSON.stringify({ channelId, text: reply, fromPublicKey: _ownDid }), fromPeer);
|
|
274
280
|
}
|