@bolloon/bolloon-agent 0.4.24 → 0.4.26

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.
Files changed (49) hide show
  1. package/dist/agents/execution-supervisor.js +446 -0
  2. package/dist/agents/external-events.js +162 -0
  3. package/dist/agents/goal-criteria.js +124 -0
  4. package/dist/agents/goal-store.js +526 -0
  5. package/dist/agents/pi-harness.js +263 -0
  6. package/dist/agents/pi-sdk.js +607 -126
  7. package/dist/agents/run-store.js +772 -0
  8. package/dist/agents/runner-resolver.js +225 -0
  9. package/dist/agents/skill-readiness.js +133 -0
  10. package/dist/agents/skill-supervisor-link.js +70 -0
  11. package/dist/agents/skills-manager.js +717 -0
  12. package/dist/agents/supervisor-host.js +249 -0
  13. package/dist/cli/setup-wizard.js +96 -127
  14. package/dist/cron/tick-lock.js +1 -1
  15. package/dist/electron/first-run.js +33 -2
  16. package/dist/electron-build/electron/first-run.js +35 -2
  17. package/dist/electron-build/electron/first-run.js.map +1 -1
  18. package/dist/index.js +549 -26
  19. package/dist/ios/agent-delegate-server.js +58 -12
  20. package/dist/ios/icons/icon-1024x1024.png +0 -0
  21. package/dist/ios/icons/icon-1024x1024.webp +0 -0
  22. package/dist/ios/icons/icon-216x216.png +0 -0
  23. package/dist/ios/icons/icon-216x216.webp +0 -0
  24. package/dist/ios/index.html +21 -1
  25. package/dist/ios/manifest.json +1 -1
  26. package/dist/ios/mobile-agent.js +195 -1
  27. package/dist/ios/mobile-core.js +24876 -24723
  28. package/dist/ios/mobile.css +15 -0
  29. package/dist/ios/mobile.html +21 -1
  30. package/dist/ios/mobile.js +143 -0
  31. package/dist/ios/server.js +51 -4
  32. package/dist/llm/config-store.js +35 -4
  33. package/dist/network/agent-network.js +10 -0
  34. package/dist/network/goal-event-bridge.js +57 -0
  35. package/dist/setup/onboard.js +549 -0
  36. package/dist/setup/setup-store.js +592 -0
  37. package/dist/web/icons/icon-1024x1024.png +0 -0
  38. package/dist/web/icons/icon-1024x1024.webp +0 -0
  39. package/dist/web/icons/icon-216x216.png +0 -0
  40. package/dist/web/icons/icon-216x216.webp +0 -0
  41. package/dist/web/manifest.json +1 -1
  42. package/dist/web/mobile-agent.js +2 -2
  43. package/dist/web/mobile-core.js +24884 -24726
  44. package/dist/web/mobile-privacy.js +185 -0
  45. package/dist/web/mobile.css +15 -0
  46. package/dist/web/mobile.html +21 -1
  47. package/dist/web/mobile.js +179 -6
  48. package/dist/web/server.js +633 -0
  49. package/package.json +2 -2
@@ -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
- targetAgent: targetAgent || { id: f.payload.delegatedTo, capabilities: [capability], status: 'active', name: f.payload.delegatedTo },
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(req.capability) && a.status === 'active') || local.agents[0];
111
- if (!target)
112
- return buildAgentResponse({ ok: false, delegatedTo: 'none', summary: 'no local agent available' });
113
- return buildAgentResponse({
114
- ok: true,
115
- delegatedTo: target.id,
116
- resultCid: `mock-${Date.now()}`,
117
- summary: `[${target.name}] 已处理任务: ${req.instruction?.substring(0, 30)}`,
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
  });
Binary file
Binary file
@@ -43,7 +43,7 @@
43
43
  </header>
44
44
 
45
45
  <!-- 主页面: 双频滑动卡片 -->
46
- <main class="page-container" id="page-main" data-tab="main">
46
+ <main class="page-container" id="page-main" data-tab="main" hidden>
47
47
  <!-- 水平滑动卡片轨道 -->
48
48
  <div class="card-carousel" id="card-carousel">
49
49
  <div class="card-track" id="card-track">
@@ -139,6 +139,26 @@
139
139
  <button class="create-session-btn" id="btn-create-session" hidden>+ 创建新会话</button>
140
140
 
141
141
  <!-- 创建智能体: 底部滑入加载 sheet -->
142
+ <!-- 2026-09-16: 首启隐私同意门 — 上架要求: 用户同意前不读取本机数据 / 不连网 / 不申请任何权限 -->
143
+ <div class="sheet" id="privacy-gate" hidden>
144
+ <div class="sheet-inner">
145
+ <div class="sheet-title" id="privacy-gate-title">隐私政策与权限说明</div>
146
+ <div class="sheet-text" id="privacy-gate-body"></div>
147
+ <a class="privacy-link" href="#" id="privacy-gate-link">阅读完整隐私政策 ›</a>
148
+ <button class="sheet-choice" id="privacy-agree">同意并继续</button>
149
+ <button class="sheet-choice sheet-cancel" id="privacy-decline">不同意</button>
150
+ </div>
151
+ </div>
152
+
153
+ <!-- 2026-09-16: 应用内隐私政策 (全屏, 离线可用; 权威版 = bolloon.cn/privacy.html) -->
154
+ <div class="chat-page" id="policy-page" hidden>
155
+ <div class="chat-topbar">
156
+ <button class="icon-btn" id="policy-back">←</button>
157
+ <div style="flex:1;font-weight:600">隐私政策</div>
158
+ </div>
159
+ <div class="policy-body" id="policy-body"></div>
160
+ </div>
161
+
142
162
  <div class="sheet" id="create-sheet" hidden>
143
163
  <div class="sheet-inner">
144
164
  <div class="spinner"></div>
@@ -2,7 +2,7 @@
2
2
  "name": "Bolloon Agent",
3
3
  "short_name": "Bolloon",
4
4
  "description": "去中心化智能体网络 — 手机端 (P2P / DID / 数字资源)",
5
- "id": "com.bolloon.agent.mobile",
5
+ "id": "com.hibs.bolloon",
6
6
  "start_url": "./mobile.html",
7
7
  "scope": "./",
8
8
  "display": "standalone",
@@ -160,11 +160,205 @@ async function applyLlmConfigToBridge() {
160
160
  }
161
161
  catch { /* 注入失败不阻塞本地执行 */ }
162
162
  }
163
- // ============ 本地执行 (Kotlin AgentRuntime / 内置规则) ============
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 {