@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
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// ─── 手机端 Gateway (browser-safe, 与桌面同一协议) ────────────────────────
|
|
2
|
+
// 手机 WebView 跑不了 OrbitDB/Helia → 入网走 browser-safe 路径:
|
|
3
|
+
// ① https://.../registry → 直接 fetch JSON+meta (与桌面 fetchRemoteRegistry 同构)
|
|
4
|
+
// ② orbitdb:// | ipns:// → 需桌面节点 → 若有 desktopBaseUrl 则转发 /api/gateway/join, 否则明确提示
|
|
5
|
+
// 成员 schema 与桌面一致 (AgentService: agentId=did / service{name,price} / capabilities)。
|
|
6
|
+
import { parseNetworkLink, detectGatewayLink } from '../agents/network-link.js';
|
|
7
|
+
const MEMBERS_KEY = 'bolloon_mobile_net_members';
|
|
8
|
+
const DESKTOP_URL_KEY = 'bolloon_desktop_base_url';
|
|
9
|
+
/** 手机侧持久化的桌面节点 API 基址 (设置页填; localStorage) */
|
|
10
|
+
export function getDesktopBaseUrl() {
|
|
11
|
+
try {
|
|
12
|
+
return typeof localStorage !== 'undefined' ? (localStorage.getItem(DESKTOP_URL_KEY) || '') : '';
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function setDesktopBaseUrl(url) {
|
|
19
|
+
try {
|
|
20
|
+
if (typeof localStorage !== 'undefined')
|
|
21
|
+
localStorage.setItem(DESKTOP_URL_KEY, String(url || ''));
|
|
22
|
+
}
|
|
23
|
+
catch { /* 忽略 */ }
|
|
24
|
+
}
|
|
25
|
+
function defaultStorage() {
|
|
26
|
+
let mem = [];
|
|
27
|
+
return {
|
|
28
|
+
get: () => {
|
|
29
|
+
try {
|
|
30
|
+
const s = typeof localStorage !== 'undefined' ? localStorage.getItem(MEMBERS_KEY) : null;
|
|
31
|
+
return s ? JSON.parse(s) : [];
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
set: (m) => {
|
|
38
|
+
mem = m;
|
|
39
|
+
try {
|
|
40
|
+
if (typeof localStorage !== 'undefined')
|
|
41
|
+
localStorage.setItem(MEMBERS_KEY, JSON.stringify(m));
|
|
42
|
+
}
|
|
43
|
+
catch { /* 忽略 */ }
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function afetch(opts) {
|
|
48
|
+
return opts.fetch || globalThis.fetch;
|
|
49
|
+
}
|
|
50
|
+
/** 从 HTTP registry 端点拉 {services, meta} (browser-safe) */
|
|
51
|
+
async function fetchHttpRegistry(url, opts) {
|
|
52
|
+
try {
|
|
53
|
+
const f = afetch(opts);
|
|
54
|
+
const signal = typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function' ? AbortSignal.timeout(15000) : undefined;
|
|
55
|
+
const r = await f(url, signal ? { signal } : undefined);
|
|
56
|
+
if (!r.ok)
|
|
57
|
+
return null;
|
|
58
|
+
const d = await r.json();
|
|
59
|
+
const services = Array.isArray(d) ? d : d?.services;
|
|
60
|
+
if (!Array.isArray(services))
|
|
61
|
+
return null;
|
|
62
|
+
return { services: services, meta: d?.meta };
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function mergeMembers(remote, local) {
|
|
69
|
+
const out = local.slice();
|
|
70
|
+
let joined = 0;
|
|
71
|
+
for (const m of remote) {
|
|
72
|
+
if (!m?.agentId)
|
|
73
|
+
continue;
|
|
74
|
+
const exists = out.some((l) => l.agentId === m.agentId && (l.service?.name || '') === (m.service?.name || ''));
|
|
75
|
+
if (!exists) {
|
|
76
|
+
out.push(m);
|
|
77
|
+
joined++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { merged: out, joined };
|
|
81
|
+
}
|
|
82
|
+
/** 手机入网: https registry 直接拉; orbitdb/ipns 转发桌面 (或提示) */
|
|
83
|
+
export async function mobileJoinNetwork(link, opts = {}) {
|
|
84
|
+
const parsed = parseNetworkLink(link);
|
|
85
|
+
if (!parsed)
|
|
86
|
+
return { ok: false, error: '无法解析链接' };
|
|
87
|
+
if (parsed.kind === 'http') {
|
|
88
|
+
const r = await fetchHttpRegistry(parsed.url || parsed.value, opts);
|
|
89
|
+
if (!r)
|
|
90
|
+
return { ok: false, error: '远端 registry 不可达' };
|
|
91
|
+
const st = opts.storage || defaultStorage();
|
|
92
|
+
const { merged, joined } = mergeMembers(r.services, st.get());
|
|
93
|
+
st.set(merged);
|
|
94
|
+
return { ok: true, joined, total: r.services.length, networkName: parsed.networkName, meta: r.meta };
|
|
95
|
+
}
|
|
96
|
+
// orbitdb/ipns → 桌面转发 (默认用设置页持久化的 desktopBaseUrl)
|
|
97
|
+
const desktopBaseUrl = opts.desktopBaseUrl || getDesktopBaseUrl();
|
|
98
|
+
if (desktopBaseUrl) {
|
|
99
|
+
try {
|
|
100
|
+
const f = afetch(opts);
|
|
101
|
+
const r = await f(`${String(desktopBaseUrl).replace(/\/$/, '')}/api/gateway/join`, {
|
|
102
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ link }),
|
|
103
|
+
});
|
|
104
|
+
const j = r.ok ? await r.json() : null;
|
|
105
|
+
if (r.ok && j?.ok)
|
|
106
|
+
return { ok: true, joined: j.joined ?? 0, total: j.total ?? 0, viaDesktop: true };
|
|
107
|
+
return { ok: false, error: j?.error || '桌面加入失败', viaDesktop: true };
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
return { ok: false, error: `桌面转发失败: ${String(e?.message || e).slice(0, 120)}`, viaDesktop: true };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { ok: false, error: 'orbitdb/ipns 链接需桌面节点: 请用 https://.../registry 链接, 或配置 desktopBaseUrl' };
|
|
114
|
+
}
|
|
115
|
+
/** 手机注册本机声明 (写入本地成员表, 供网络画像展示) */
|
|
116
|
+
export function mobileRegister(self, opts = {}) {
|
|
117
|
+
const st = opts.storage || defaultStorage();
|
|
118
|
+
const cur = st.get();
|
|
119
|
+
const i = cur.findIndex((m) => m.agentId === self.agentId && (m.service?.name || '') === (self.service?.name || ''));
|
|
120
|
+
if (i >= 0)
|
|
121
|
+
cur[i] = self;
|
|
122
|
+
else
|
|
123
|
+
cur.push(self);
|
|
124
|
+
st.set(cur);
|
|
125
|
+
}
|
|
126
|
+
/** 手机网络状态 (本地成员表) */
|
|
127
|
+
export function mobileNetworkStatus(opts = {}) {
|
|
128
|
+
return (opts.storage || defaultStorage()).get();
|
|
129
|
+
}
|
|
130
|
+
/** 拉共享 context (browser-safe: HTTP IPFS 网关) */
|
|
131
|
+
export async function mobilePullSharedContext(cid, opts = {}) {
|
|
132
|
+
try {
|
|
133
|
+
const gw = String(opts.ipfsGateway || 'https://ipfs.io').replace(/\/$/, '');
|
|
134
|
+
const f = afetch(opts);
|
|
135
|
+
const r = await f(`${gw}/ipfs/${cid}`, { signal: AbortSignal.timeout?.(15000) });
|
|
136
|
+
return r.ok ? await r.text() : null;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** 自动加入入口: 文本检测到 gateway 链接 → join (幂等). 返回通知或 null */
|
|
143
|
+
export async function mobileAutoJoinGateway(text, opts = {}) {
|
|
144
|
+
const link = detectGatewayLink(text);
|
|
145
|
+
if (!link)
|
|
146
|
+
return null;
|
|
147
|
+
const r = await mobileJoinNetwork(link, opts);
|
|
148
|
+
if (r.ok) {
|
|
149
|
+
return `🆕 已加入 Agent 网络${r.networkName ? `「${r.networkName}」` : ''}${r.viaDesktop ? '(经桌面)' : ''}: 拉取 ${r.total ?? 0} 个成员, 新增 ${r.joined ?? 0} 个。`;
|
|
150
|
+
}
|
|
151
|
+
return `⚠️ 检测到网络链接但加入失败: ${r.error}`;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* 手机端 gateway 工具统一分派 (供 Kotlin AgentRuntime 或 JS 路由调用):
|
|
155
|
+
* gateway_join(link) / gateway_status() / gateway_register(self) / gateway_context(cid)
|
|
156
|
+
* 返回 {ok, output} 给 agent 作为工具结果文本.
|
|
157
|
+
*/
|
|
158
|
+
export async function mobileGatewayTool(name, args, opts = {}) {
|
|
159
|
+
const n = String(name || '').trim();
|
|
160
|
+
if (n === 'gateway_join') {
|
|
161
|
+
const link = String(args?.link || args?.url || args?.value || '');
|
|
162
|
+
if (!link)
|
|
163
|
+
return { ok: false, output: 'gateway_join 需要 link 参数 (orbitdb:// / ipns:// / https://.../registry)' };
|
|
164
|
+
const r = await mobileJoinNetwork(link, opts);
|
|
165
|
+
return r.ok
|
|
166
|
+
? { ok: true, output: `已加入 Agent 网络${r.networkName ? `「${r.networkName}」` : ''}: ${r.total ?? 0} 成员, 新增 ${r.joined ?? 0}` }
|
|
167
|
+
: { ok: false, output: `加入失败: ${r.error}` };
|
|
168
|
+
}
|
|
169
|
+
if (n === 'gateway_status') {
|
|
170
|
+
const m = mobileNetworkStatus(opts);
|
|
171
|
+
return m.length
|
|
172
|
+
? { ok: true, output: m.map((x) => `${x.name} (${String(x.agentId).slice(0, 12)}…) ${x.service?.name || ''}`).join('\n') }
|
|
173
|
+
: { ok: true, output: '(网络为空: 先 gateway_join 或设置 desktopBaseUrl)' };
|
|
174
|
+
}
|
|
175
|
+
if (n === 'gateway_register') {
|
|
176
|
+
const self = args?.self || args;
|
|
177
|
+
if (self?.agentId) {
|
|
178
|
+
mobileRegister(self, opts);
|
|
179
|
+
return { ok: true, output: '已注册本机声明' };
|
|
180
|
+
}
|
|
181
|
+
return { ok: false, output: 'gateway_register 需要 self{agentId,name,service?}' };
|
|
182
|
+
}
|
|
183
|
+
if (n === 'gateway_context') {
|
|
184
|
+
const cid = String(args?.cid || '');
|
|
185
|
+
if (!cid)
|
|
186
|
+
return { ok: false, output: 'gateway_context 需要 cid' };
|
|
187
|
+
const t = await mobilePullSharedContext(cid, opts);
|
|
188
|
+
return t ? { ok: true, output: t.slice(0, 400) } : { ok: false, output: '共享 context 拉取失败' };
|
|
189
|
+
}
|
|
190
|
+
return { ok: false, output: `未知 gateway 工具: ${n}` };
|
|
191
|
+
}
|
package/dist/web/mobile.html
CHANGED
|
@@ -52,6 +52,13 @@
|
|
|
52
52
|
|
|
53
53
|
<!-- 网络 tab -->
|
|
54
54
|
<section class="page" id="page-network" data-tab="network" hidden>
|
|
55
|
+
<div class="list">
|
|
56
|
+
<div class="list-item" id="item-join-net"><span class="list-icon">🛜</span><span>加入网络</span></div>
|
|
57
|
+
<div class="list-item" id="item-scan-net"><span class="list-icon">📷</span><span>扫码入网</span></div>
|
|
58
|
+
</div>
|
|
59
|
+
<input type="file" id="qr-scan-input" accept="image/*" capture="environment" style="display:none">
|
|
60
|
+
<div class="section-label">Agent 网络</div>
|
|
61
|
+
<div class="list" id="net-members"></div>
|
|
55
62
|
<div class="list">
|
|
56
63
|
<div class="list-item" id="item-p2p"><span class="list-icon">🌐</span><span>P2P 好友</span></div>
|
|
57
64
|
<div class="list-item" id="item-p2p-id"><span class="list-icon">🪪</span><span>我的 P2P ID</span></div>
|
package/dist/web/mobile.js
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
$$('.tab').forEach((t) => t.classList.toggle('active', t.dataset.tab === tab));
|
|
47
47
|
$('#topbar-title').textContent = TITLES[tab] || '会话';
|
|
48
48
|
const cs = $('#btn-create-session'); if (cs) cs.hidden = tab !== 'main';
|
|
49
|
-
if (tab === 'network') { loadContacts(); loadMcpTools(); loadApprovals(); }
|
|
49
|
+
if (tab === 'network') { loadContacts(); loadMcpTools(); loadApprovals(); loadNetMembers(); }
|
|
50
50
|
if (tab === 'main') { loadAgentCovers(); }
|
|
51
51
|
window.__mobileTouch?.('tab', tab);
|
|
52
52
|
}
|
|
@@ -1199,6 +1199,57 @@
|
|
|
1199
1199
|
try { const net = await api.get('/api/network/status'); const p2p = net && net.nodeId; alert('P2P ID (通信ID, ≠ DID):\n' + (p2p || '未连接')); }
|
|
1200
1200
|
catch (e) { alert('P2P ID: 获取失败'); }
|
|
1201
1201
|
});
|
|
1202
|
+
// #2 极简入网按钮: 粘贴/输入链接 → BolloonCore.gateway.join (懒加载 mobile-gateway)
|
|
1203
|
+
const joinNetBtn = $('#item-join-net');
|
|
1204
|
+
if (joinNetBtn) joinNetBtn.addEventListener('click', async () => {
|
|
1205
|
+
const link = (window.prompt && window.prompt('粘贴网络链接\n(orbitdb:// ipns:// https://.../registry)') || '').trim();
|
|
1206
|
+
if (!link) return;
|
|
1207
|
+
try {
|
|
1208
|
+
const r = await (window.BolloonCore && window.BolloonCore.gateway && window.BolloonCore.gateway.join(link));
|
|
1209
|
+
alert(r ? (r.output || '已处理') : 'BolloonCore.gateway 不可用');
|
|
1210
|
+
} catch (e) { alert('加入失败: ' + String((e && e.message) || e).slice(0, 120)); }
|
|
1211
|
+
loadNetMembers();
|
|
1212
|
+
});
|
|
1213
|
+
// #3 扫码入网: 拍照/选图 → BolloonCore.qr.decode(jsQR) → gateway.join (免原生插件)
|
|
1214
|
+
const scanBtn = $('#item-scan-net');
|
|
1215
|
+
const scanInput = $('#qr-scan-input');
|
|
1216
|
+
if (scanBtn && scanInput) scanBtn.addEventListener('click', () => scanInput.click());
|
|
1217
|
+
if (scanInput) scanInput.addEventListener('change', (e) => {
|
|
1218
|
+
const f = e.target.files && e.target.files[0];
|
|
1219
|
+
if (!f) return;
|
|
1220
|
+
scanInput.value = '';
|
|
1221
|
+
const img = new Image();
|
|
1222
|
+
const url = URL.createObjectURL(f);
|
|
1223
|
+
img.onload = async () => {
|
|
1224
|
+
try {
|
|
1225
|
+
const c = document.createElement('canvas');
|
|
1226
|
+
c.width = img.naturalWidth; c.height = img.naturalHeight;
|
|
1227
|
+
const c2 = c.getContext('2d');
|
|
1228
|
+
if (!c2) return alert('画布不可用');
|
|
1229
|
+
c2.drawImage(img, 0, 0);
|
|
1230
|
+
const id = c2.getImageData(0, 0, c.width, c.height);
|
|
1231
|
+
const text = await (window.BolloonCore && window.BolloonCore.qr && window.BolloonCore.qr.decode(id.data, id.width, id.height));
|
|
1232
|
+
if (!text) { alert('未识别到二维码 (试试 /net qr 重新出码)'); return; }
|
|
1233
|
+
const r = await (window.BolloonCore && window.BolloonCore.gateway && window.BolloonCore.gateway.join(text));
|
|
1234
|
+
alert(r ? (r.output || '已入网') : 'gateway.join 不可用');
|
|
1235
|
+
loadNetMembers();
|
|
1236
|
+
} catch (err) { alert('解码失败: ' + String((err && err.message) || err).slice(0, 120)); }
|
|
1237
|
+
finally { URL.revokeObjectURL(url); }
|
|
1238
|
+
};
|
|
1239
|
+
img.onerror = () => { alert('图片读取失败'); URL.revokeObjectURL(url); };
|
|
1240
|
+
img.src = url;
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
async function loadNetMembers() {
|
|
1245
|
+
const el = $('#net-members');
|
|
1246
|
+
if (!el) return;
|
|
1247
|
+
try {
|
|
1248
|
+
const r = await (window.BolloonCore && window.BolloonCore.gateway && window.BolloonCore.gateway.status());
|
|
1249
|
+
if (r && r.output) {
|
|
1250
|
+
el.innerHTML = r.output.split('\n').filter(Boolean).map((l) => `<div class="list-item">${escapeHtml(l)}</div>`).join('');
|
|
1251
|
+
} else el.innerHTML = '<div class="list-item">(网络为空)</div>';
|
|
1252
|
+
} catch (e) { el.innerHTML = '<div class="list-item">网络状态获取失败</div>'; }
|
|
1202
1253
|
}
|
|
1203
1254
|
|
|
1204
1255
|
function escapeHtml(s) {
|
package/dist/web/qr.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// ─── 扫码入网: 二维码编解码 (纯 JS, PC 出码 / 手机扫码) ─────────────────────
|
|
2
|
+
// encode: qrcode → 终端 ASCII / dataURL; decode: jsQR (浏览器 camera/capture 图).
|
|
3
|
+
// payload 是一份短文本: <网络链接>?name=..&ctx=<cid>&v=.., 手机 decode 后 detectGatewayLink → join.
|
|
4
|
+
import QRCode from 'qrcode';
|
|
5
|
+
import jsQR from 'jsqr';
|
|
6
|
+
/** 组装"扫码即入网"短文本 (链接 + 可选 query) */
|
|
7
|
+
export function buildQrPayload(o) {
|
|
8
|
+
let s = String(o.link || '').trim();
|
|
9
|
+
const params = new URLSearchParams();
|
|
10
|
+
if (o.name)
|
|
11
|
+
params.set('name', o.name);
|
|
12
|
+
if (o.ctxCid)
|
|
13
|
+
params.set('ctx', o.ctxCid);
|
|
14
|
+
if (o.version)
|
|
15
|
+
params.set('v', o.version);
|
|
16
|
+
const q = params.toString();
|
|
17
|
+
if (q)
|
|
18
|
+
s += (s.includes('?') ? '&' : '?') + q;
|
|
19
|
+
return s;
|
|
20
|
+
}
|
|
21
|
+
/** 终端 ASCII 二维码 (半块字符 ▄█), 供 CLI /net qr 面板显示 */
|
|
22
|
+
export async function encodeQrTerminal(text) {
|
|
23
|
+
try {
|
|
24
|
+
const s = await QRCode.toString(text, { type: 'terminal', small: true });
|
|
25
|
+
return s || '';
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** 浏览器/客户端图片二维码 (dataURL), 供 Web 端显示 */
|
|
32
|
+
export async function encodeQrDataUrl(text) {
|
|
33
|
+
try {
|
|
34
|
+
return await QRCode.toDataURL(text, { width: 320, margin: 2 });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** 解码: 传入 RGBA 像素 (来自 canvas getImageData), 返回二维码文本或 null */
|
|
41
|
+
export function decodeQrImageData(data, width, height) {
|
|
42
|
+
try {
|
|
43
|
+
const r = jsQR(data, width, height);
|
|
44
|
+
return r?.data ?? null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bolloon/bolloon-agent",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
|
|
6
6
|
"main": "dist/cli-entry.js",
|
|
@@ -88,10 +88,12 @@
|
|
|
88
88
|
"ink": "^7.1.1",
|
|
89
89
|
"ink-text-input": "^6.0.0",
|
|
90
90
|
"js-yaml": "^5.4.1",
|
|
91
|
+
"jsqr": "^1.4.0",
|
|
91
92
|
"libp2p": "^3.3.11",
|
|
92
93
|
"mammoth": "^1.12.2",
|
|
93
94
|
"pdf-parse": "^2.4.5",
|
|
94
95
|
"platform": "^1.3.6",
|
|
96
|
+
"qrcode": "^1.5.4",
|
|
95
97
|
"react": "^19.2.8",
|
|
96
98
|
"react-dom": "^19.2.8",
|
|
97
99
|
"viem": "^2.56.3"
|
|
@@ -102,6 +104,7 @@
|
|
|
102
104
|
"@types/express": "^5.0.6",
|
|
103
105
|
"@types/node": "^26.5.0",
|
|
104
106
|
"@types/pdf-parse": "^1.1.5",
|
|
107
|
+
"@types/qrcode": "^1.5.6",
|
|
105
108
|
"@types/react": "^19.2.18",
|
|
106
109
|
"@types/react-dom": "^19.2.7",
|
|
107
110
|
"concurrently": "^10.0.5",
|