@bolloon/bolloon-agent 0.6.6 → 0.6.7

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 (52) hide show
  1. package/dist/agents/ha-daemon.js +85 -0
  2. package/dist/agents/ha-supervisor.js +146 -0
  3. package/dist/agents/memory-store.js +25 -0
  4. package/dist/agents/opportunity-match.js +15 -3
  5. package/dist/agents/pi-sdk-session-manager.js +8 -0
  6. package/dist/agents/pi-sdk.js +67 -494
  7. package/dist/bootstrap/context-collector.js +65 -3
  8. package/dist/bootstrap/persona-loader.js +26 -0
  9. package/dist/electron-build/agents/memory-store.js +26 -0
  10. package/dist/electron-build/agents/memory-store.js.map +1 -1
  11. package/dist/electron-build/agents/pi-sdk-session-manager.js +8 -0
  12. package/dist/electron-build/agents/pi-sdk-session-manager.js.map +1 -1
  13. package/dist/electron-build/agents/pi-sdk.js +65 -492
  14. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  15. package/dist/electron-build/bootstrap/context-collector.js +66 -3
  16. package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
  17. package/dist/electron-build/bootstrap/persona-loader.js +27 -0
  18. package/dist/electron-build/bootstrap/persona-loader.js.map +1 -1
  19. package/dist/electron-build/llm/pi-ai.js +16 -3
  20. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  21. package/dist/electron-build/llm/system-prompt/registry.js +8 -6
  22. package/dist/electron-build/llm/system-prompt/registry.js.map +1 -1
  23. package/dist/ios/bolloon-web.json +3 -3
  24. package/dist/ios/index.html +19 -4
  25. package/dist/ios/mobile-agent.js +267 -41
  26. package/dist/ios/mobile-contacts.js +0 -4
  27. package/dist/ios/mobile-core.js +806 -30
  28. package/dist/ios/mobile-memory.js +317 -0
  29. package/dist/ios/mobile.html +19 -4
  30. package/dist/ios/mobile.js +144 -30
  31. package/dist/ios/routes-contacts.js +94 -1
  32. package/dist/ios/routes-world.js +9 -1
  33. package/dist/ios/server.js +51 -40
  34. package/dist/ios/sw.js +1 -1
  35. package/dist/kernel/gate-scan.js +1 -1
  36. package/dist/kernel/ha/ha-arbiter.js +177 -0
  37. package/dist/kernel/ha/ha-guard.js +151 -0
  38. package/dist/kernel/index.js +3 -0
  39. package/dist/kernel/plan.js +5 -5
  40. package/dist/kernel/roster.js +3 -3
  41. package/dist/llm/pi-ai.js +16 -3
  42. package/dist/llm/system-prompt/registry.js +8 -6
  43. package/dist/web/bolloon-web.json +3 -3
  44. package/dist/web/mobile-agent.js +265 -44
  45. package/dist/web/mobile-core.js +681 -32
  46. package/dist/web/mobile-memory.js +317 -0
  47. package/dist/web/mobile.html +4 -4
  48. package/dist/web/mobile.js +36 -27
  49. package/dist/web/routes-world.js +9 -1
  50. package/dist/web/server.js +50 -39
  51. package/dist/web/sw.js +1 -1
  52. package/package.json +1 -1
@@ -76,12 +76,12 @@
76
76
  <div class="list-item" id="item-p2p"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.5"/><path d="M3.5 12h17"/><path d="M12 3.5c2.3 2.3 3.5 5.3 3.5 8.5s-1.2 6.2-3.5 8.5c-2.3-2.3-3.5-5.3-3.5-8.5S9.7 5.8 12 3.5z"/></svg></span><span>群聊</span><span class="list-arrow">›</span></div>
77
77
  <div class="list-item" id="item-scan-net"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><path d="M4 8V6a2 2 0 0 1 2-2h2M16 4h2a2 2 0 0 1 2 2v2M20 16v2a2 2 0 0 1-2 2h-2M8 20H6a2 2 0 0 1-2-2v-2"/><path d="M4 12h16"/></svg></span><span>扫码入网 / 加好友</span></div>
78
78
  <div class="list-item" id="item-nearby"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><path d="M4 12a8 8 0 0 1 16 0"/><path d="M7.5 12a4.5 4.5 0 0 1 9 0"/><circle cx="12" cy="12" r="1.4"/><path d="M12 13.6V19"/></svg></span><span>附近设备</span><span class="list-arrow">›</span></div>
79
- <div class="list-item" id="item-p2p-id"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><rect x="3" y="5.5" width="18" height="13" rx="2.5"/><circle cx="8.5" cy="11" r="2"/><path d="M5.6 15.8a3.2 3.2 0 0 1 5.8 0M13.5 10h5M13.5 13.5h5"/></svg></span><span>我的 P2P ID</span></div>
79
+ <div class="list-item" id="item-p2p-id"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><rect x="3" y="5.5" width="18" height="13" rx="2.5"/><circle cx="8.5" cy="11" r="2"/><path d="M5.6 15.8a3.2 3.2 0 0 1 5.8 0M13.5 10h5M13.5 13.5h5"/></svg></span><span>我的连接标识</span></div>
80
80
  </div>
81
81
  <input type="file" id="qr-scan-input" accept="image/*" capture="environment" style="display:none">
82
- <div class="section-label">P2P 连接</div>
82
+ <div class="section-label">网络连接</div>
83
83
  <div class="list" id="p2p-status"></div>
84
- <div class="section-label">好友列表</div>
84
+ <div class="section-label">联系人列表</div>
85
85
  <!-- 通讯录式联系人列表: 按字母分组 + 右侧索引 (由 loadContacts 渲染) -->
86
86
  <div class="contacts-wrap" id="contacts-wrap">
87
87
  <div class="list" id="contacts-list"></div>
@@ -184,7 +184,7 @@
184
184
  <!-- 底部 tab -->
185
185
  <nav class="tabbar" id="tabbar">
186
186
  <button class="tab active" data-tab="main"><span class="tab-icon"><svg class="ico" viewBox="0 0 24 24"><rect x="3.5" y="3.5" width="7" height="7" rx="1.6"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.6"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.6"/><rect x="13.5" y="13.5" width="7" height="7" rx="1.6"/></svg></span><span>首页</span></button>
187
- <button class="tab" data-tab="friends"><span class="tab-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="9" cy="8" r="3.2"/><path d="M3.5 19c.8-3 3-4.6 5.5-4.6S13.7 16 14.5 19"/><path d="M17 8v6M14 11h6"/></svg></span><span>好友</span></button>
187
+ <button class="tab" data-tab="friends"><span class="tab-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="9" cy="8" r="3.2"/><path d="M3.5 19c.8-3 3-4.6 5.5-4.6S13.7 16 14.5 19"/><path d="M17 8v6M14 11h6"/></svg></span><span>通讯录</span></button>
188
188
  <!-- 2026-10-06: 网络+任务合一, 任务 tab 内页内子切换 -->
189
189
  <button class="tab" data-tab="tasks"><span class="tab-icon"><svg class="ico" viewBox="0 0 24 24"><path d="M4 7.5h10M4 12h7M4 16.5h12"/><path d="M18 5.5l1.6 1.6L22 4.5"/><path d="M18 14.5l1.6 1.6L22 13.5"/></svg></span><span>任务</span></button>
190
190
  <button class="tab" data-tab="me"><span class="tab-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="12" cy="8.5" r="4"/><path d="M4.5 20.5a7.5 7.5 0 0 1 15 0"/></svg></span><span>我</span></button>
@@ -224,6 +224,7 @@
224
224
  <div class="sheet" id="addfriend-sheet" hidden>
225
225
  <div class="sheet-inner sheet-inner-choices">
226
226
  <div class="sheet-title">连接好友</div>
227
+ <button class="sheet-choice" id="choice-phone"><svg class="ico" viewBox="0 0 24 24"><path d="M5 4h4l1.5 4.5L8 10a12 12 0 0 0 6 6l1.5-2.5L20 15v4a1.5 1.5 0 0 1-1.6 1.5A17 17 0 0 1 3.5 5.6 1.5 1.5 0 0 1 5 4z"/></svg> 手机号搜索添加</button>
227
228
  <button class="sheet-choice" id="choice-nearby"><svg class="ico" viewBox="0 0 24 24"><path d="M4 12a8 8 0 0 1 16 0"/><path d="M7.5 12a4.5 4.5 0 0 1 9 0"/><circle cx="12" cy="12" r="1.4"/><path d="M12 13.6V19"/></svg> 附近的设备</button>
228
229
  <button class="sheet-choice" id="choice-scan"><svg class="ico" viewBox="0 0 24 24"><path d="M4 8V6a2 2 0 0 1 2-2h2M16 4h2a2 2 0 0 1 2 2v2M20 16v2a2 2 0 0 1-2 2h-2M8 20H6a2 2 0 0 1-2-2v-2"/><path d="M4 12h16"/></svg> 扫码添加</button>
229
230
  <button class="sheet-choice" id="choice-requests"><svg class="ico" viewBox="0 0 24 24"><path d="M6 4h9l4 4v12a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1z"/><path d="M9 12h6M9 16h4"/></svg> 待处理申请</button>
@@ -232,6 +233,20 @@
232
233
  </div>
233
234
  </div>
234
235
 
236
+ <!-- 手机号搜索添加好友: 输入手机号 → 目录查账号 → 发好友申请 (2026-10-15) -->
237
+ <div class="sheet" id="phone-search-sheet" hidden>
238
+ <div class="sheet-inner">
239
+ <div class="sheet-title">手机号搜索添加</div>
240
+ <input id="phone-search-input" type="tel" inputmode="numeric" placeholder="输入对方手机号 (如 13812345678)" style="width:100%;padding:10px 12px;border-radius:8px;border:1px solid var(--border);background:var(--bg-card);color:var(--text);font-size:15px;box-sizing:border-box">
241
+ <div id="phone-search-note" style="font-size:12px;color:var(--text-secondary);margin:8px 2px;line-height:1.6">通过手机号目录查对方账号: 对方需已在电脑端发布目录 (手机号 → DID 指针)。查不到 = 对方未公开, 与不存在同义。</div>
242
+ <div id="phone-search-result" style="display:none;margin-top:8px"></div>
243
+ <div style="display:flex;gap:8px;margin-top:12px">
244
+ <button class="sheet-choice" id="phone-search-do" style="flex:1">搜索</button>
245
+ <button class="sheet-choice sheet-cancel" id="phone-search-close" style="flex:1">关闭</button>
246
+ </div>
247
+ </div>
248
+ </div>
249
+
235
250
  <!-- 加入网络: 点按式 sheet (扫一扫 / 附近设备 / 手动粘贴兜底) -->
236
251
  <div class="sheet" id="network-sheet" hidden>
237
252
  <div class="sheet-inner sheet-inner-choices">
@@ -469,10 +469,12 @@ export async function runLocalAgent(goal) {
469
469
  // 内置极简回复 (纯离线可用)
470
470
  const t = (goal || '').trim();
471
471
  // 2026-10-07: 手机自治 — 先用 LLM 循环 (经内置公共网关), 失败再降级纯规则
472
+ // 2026-10-16: silentDegrade=true — LLM 不可达时 → throw → 下面 catch → 内置离线规则,
473
+ // (上一层 runWebAgentLoop 若默认返回降级文本, 内置规则层就永远走不到)
472
474
  try {
473
475
  const llm = getLlmConfig();
474
476
  if (llm && llm.baseUrl) {
475
- const reply = await runWebAgentLoop(goal, { baseUrl: llm.baseUrl, apiKey: llm.apiKey, model: llm.model, maxTokens: llm.maxTokens });
477
+ const reply = await runWebAgentLoop(goal, { baseUrl: llm.baseUrl, apiKey: llm.apiKey, model: llm.model, maxTokens: llm.maxTokens }, { silentDegrade: true });
476
478
  if (reply)
477
479
  return reply;
478
480
  }
@@ -512,31 +514,115 @@ export async function runLocalAgent(goal) {
512
514
  }
513
515
  return `已收到: "${(goal || '').slice(0, 40)}"。这是手机端 Agent 功能层的本地执行 (数据同步与 agent 功能已分离)。`;
514
516
  }
515
- // ============ 手机自治 agent 循环 (2026-10-07) ============
516
- // 桌面端 WorkflowPivotLoop 的浏览器移植: LLM 多轮迭代 + 手机端可用工具集。
517
+ // ============ 手机自治 agent 循环 — 原生运行内核 (2026-10-16) ============
518
+ // 桌面 Kernel 的浏览器移植, 手机原生功能逐项落地 (leo 点名清单):
519
+ // · memory 设计迭代 + 人际关系分类 (mobile-memory.ts: fact/preference/event/relationship/skill-feedback,
520
+ // relation{family|friend|work|acquaintance|stranger, trust}, TTL/decay/快照/指纹)
521
+ // · 错误恢复 (分类 + 退避重试 + 失败反思注入上下文)
522
+ // · 自适应 API 超时 (按上下文长度: max(10s, ceil(ctxChars/1024)*4000+10000))
523
+ // · 记忆快照 / bootstrap (启动恢复快照 + 收尾落快照)
524
+ // · 文档冻结 + 隐式化 (user 文档 / soul 文档只注摘要, 不整篇塞 — leo: 系统 prompt 注入太多, 删)
525
+ // · 上下文压缩 / 缓存命中 (指纹装配缓存: 同冻结上下文指纹 → 复用 system 不重建)
526
+ // · 工具指纹 (工具集 hash → 缓存 key, 工具变了 key 变)
527
+ // · 生命周期 (idle→running→paused(确认)→done/cancelled/error) + TTL
528
+ // · 多模态通道标注 (text/image/audio/file) + 能动性 (先动手别先问)
529
+ // · 中间阶段性环境 (已完成步骤注入) + 计划设计 (阶段提示) + 确认模式 (高风险工具)
530
+ // · task / process 运行记录 (run 记录落 localStorage, 可查)
517
531
  // 能力范围: 手机 WebView 能做的 (身份/钱包/入网/P2P/信息查询), 不假装能做 fs/shell。
518
- // 保护: maxSteps=5 防死循环; 每轮把工具结果拼回对话; LLM 说完成或没有下一步即停。
519
532
  // 2026-10-07: persona 注入 — 性格/价值观/兴趣/说话方式来自 mobile-persona (设置 → 性格)。
520
533
  import { loadPersona, buildPersonaPrompt } from './mobile-persona.js';
521
- function buildAgentSystem() {
534
+ import { MobileMemory, MobileCache, quickHash } from './mobile-memory.js';
535
+ const runsKey = (did) => `bolloon_mobile_runs_${did}`;
536
+ /** 最近运行记录 (task/process 台账, 手机原生) */
537
+ export function listMobileRuns(did, limit = 20) {
538
+ try {
539
+ const raw = typeof localStorage !== 'undefined' ? localStorage.getItem(runsKey(did)) : null;
540
+ if (!raw)
541
+ return [];
542
+ const parsed = JSON.parse(raw);
543
+ return Array.isArray(parsed) ? parsed.slice(0, limit) : [];
544
+ }
545
+ catch {
546
+ return [];
547
+ }
548
+ }
549
+ function saveRunRecord(did, rec) {
550
+ try {
551
+ const prev = listMobileRuns(did, 50);
552
+ const next = [rec, ...prev.filter((r) => r.runId !== rec.runId)].slice(0, 50);
553
+ if (typeof localStorage !== 'undefined')
554
+ localStorage.setItem(runsKey(did), JSON.stringify(next));
555
+ }
556
+ catch { /* 台账写失败不阻断运行 */ }
557
+ }
558
+ // ── 工具指纹 + 上下文指纹 (缓存命中) ───────────────────────────────────────
559
+ const toolNames = () => Object.keys(WEB_AGENT_TOOLS).sort().join(',');
560
+ let _toolFingerprint = '';
561
+ /** 工具指纹: 工具集 hash (工具变了 → 指纹变 → system 装配缓存 miss) */
562
+ export function toolFingerprint() {
563
+ if (!_toolFingerprint)
564
+ _toolFingerprint = quickHash('tools|' + toolNames());
565
+ return _toolFingerprint;
566
+ }
567
+ const CHANNEL_LABEL = {
568
+ text: '文字', image: '图片', audio: '语音', file: '文件',
569
+ };
570
+ // ── 确认模式 (高风险工具执行前) ───────────────────────────────────────────
571
+ // 默认空名单 (机制就位; 需要拦的工具由调用方注入 confirm 回调裁决)
572
+ const CONFIRM_REQUIRED_TOOLS = [];
573
+ export function isConfirmRequiredTool(tool) {
574
+ return CONFIRM_REQUIRED_TOOLS.includes(tool);
575
+ }
576
+ function buildAgentSystem(opts = {}) {
522
577
  const persona = loadPersona();
523
578
  const personaBlock = buildPersonaPrompt(persona);
579
+ // 文档隐式化 (leo: 系统 prompt 注入太多, 文档可以删):
580
+ // user 文档 → 身份/偏好摘要; soul 文档 → persona 摘要 (buildPersonaPrompt 本身就是摘要形)
581
+ // 完整文档不整篇塞进 prompt, 按需由工具读取。
582
+ const userBlock = opts.userDoc && opts.userDoc.trim()
583
+ ? `【用户档案】\n${opts.userDoc.trim().slice(0, 400)}`
584
+ : '';
585
+ const memoryBlock = opts.memoryBlock && opts.memoryBlock.trim()
586
+ ? `【记忆】\n${opts.memoryBlock.trim().slice(0, 600)}`
587
+ : '';
588
+ // 稳态记忆块 (对齐桌面 formatMemorySystemBlock, 2026-10-16 补齐): 固化优先 + importance 降序,
589
+ // 始终注入 system — 不是只靠当轮 query 召回。与 memoryBlock (recall) 互补。
590
+ const stableBlock = opts.memory && opts.memory.stableBlock
591
+ ? opts.memory.stableBlock(6)
592
+ : '';
593
+ const progressBlock = opts.progress && opts.progress.length
594
+ ? `【本轮进度】\n已完成: ${opts.progress.slice(-8).map((s, i) => `${i + 1}. ${s.slice(0, 80)}`).join('\n')}`
595
+ : '';
596
+ const planHint = opts.plan && opts.plan.trim()
597
+ ? `【计划】\n${opts.plan.trim().slice(0, 300)}`
598
+ : '任务复杂时先在内心拆步骤, 逐步执行, 每步用一个工具推进。';
599
+ const channelBlock = opts.channel && opts.channel !== 'text'
600
+ ? `【输入通道】\n本次输入是${CHANNEL_LABEL[opts.channel]}。你能理解${CHANNEL_LABEL[opts.channel]}内容并据此回答/执行。`
601
+ : '';
524
602
  return [
525
603
  '你是手机端 Bolloon 智能体 (自治节点), 用中文简洁回复。',
526
604
  personaBlock,
605
+ userBlock,
606
+ memoryBlock,
607
+ stableBlock,
527
608
  '你有以下工具, 按需调用 (JSON: {"tool":"名字","args":{...}}), 不需要工具就直接回答:',
528
- '- get_status: 查本机身份/入网/钱包/P2P 状态 (args: 无)',
609
+ '- get_status: 查本机身份/入网/钱包/连接状态 (args: 无)',
529
610
  '- get_wallet: 查钱包余额 (args: 无)',
530
- '- get_identity: 查本机 DID (args: 无)',
611
+ '- get_identity: 查本机身份证明 (args: 无)',
531
612
  '- get_contacts: 查联系方式授权状态 (args: 无)',
532
- '每轮只能用一个工具; 拿到结果后继续思考, 直到任务完成给出最终回答。',
533
- ].join('\n');
613
+ '每轮最多用一个工具; 拿到结果后继续思考, 直到任务完成给出最终回答。',
614
+ progressBlock,
615
+ planHint,
616
+ channelBlock,
617
+ '能动性: 先动手别先问; 做到底 (交付=能用的结果); 一个工具不够就换方式继续; 被阻塞如实说, 不编造。',
618
+ ].filter(Boolean).join('\n');
534
619
  }
620
+ // 2026-10-16: 工具签名带 args (更灵活调用; 现有工具忽略 args)
535
621
  const WEB_AGENT_TOOLS = {
536
622
  get_status: async () => {
537
623
  try {
538
624
  const id = await ensureIdentity();
539
- return `DID: ${id.did.slice(0, 16)}... 名字: ${id.name}`;
625
+ return `身份: ${id.did.slice(0, 16)}... 名字: ${id.name}`;
540
626
  }
541
627
  catch (e) {
542
628
  return '身份读取失败: ' + String(e?.message || e).slice(0, 60);
@@ -569,7 +655,7 @@ const WEB_AGENT_TOOLS = {
569
655
  },
570
656
  get_identity: async () => {
571
657
  const id = await ensureIdentity();
572
- return `DID: ${id.did} 名字: ${id.name}`;
658
+ return `身份证明: ${id.did} 名字: ${id.name}`;
573
659
  },
574
660
  get_contacts: async () => {
575
661
  try {
@@ -580,25 +666,61 @@ const WEB_AGENT_TOOLS = {
580
666
  }
581
667
  },
582
668
  };
583
- /** 单轮 LLM 调用 (OpenAI 兼容, 经内置公共网关) */
669
+ export function classifyMobileError(err) {
670
+ const msg = String(err?.message || err?.cause?.message || err || '');
671
+ if (/timeout|timed out|AbortError|aborted/i.test(msg))
672
+ return { cls: 'timeout', retryable: true, hint: '请求超时, 稍后重试' };
673
+ if (/terminated|ECONNRESET|socket hang up|fetch failed|network|ETIMEDOUT|ECONNREFUSED|UND_ERR|Failed to fetch/i.test(msg))
674
+ return { cls: 'network', retryable: true, hint: '网络波动, 正在重试' };
675
+ if (/401|403|quota|rate limit|API key|invalid api|HTTP 40\d|HTTP 429/i.test(msg))
676
+ return { cls: 'api', retryable: false, hint: '接口/配额错误, 无法自动恢复' };
677
+ return { cls: 'unknown', retryable: true, hint: '未知错误, 尝试继续' };
678
+ }
679
+ /** 自适应 API 超时: max(10s, ceil(ctxChars/1024)*4000 + 10000) — 长上下文不被误杀 (桌面同规则) */
680
+ export function adaptiveTimeoutMs(ctxChars, baseMs = 10_000) {
681
+ const byLen = Math.ceil(Math.max(0, ctxChars) / 1024) * 4000 + baseMs;
682
+ return Math.max(baseMs, byLen);
683
+ }
684
+ /** 单轮 LLM 调用 (OpenAI 兼容, 经内置公共网关) — 自适应超时 + 网络错误退避重试 */
584
685
  async function webAgentLlmCall(messages, llm) {
585
- const resp = await fetch(llm.baseUrl.replace(/\/$/, '') + '/chat/completions', {
586
- method: 'POST',
587
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + (llm.apiKey || '') },
588
- body: JSON.stringify({
589
- model: llm.model || 'glm-5.3',
590
- messages,
591
- max_tokens: llm.maxTokens || 2048,
592
- temperature: 0.4,
593
- }),
594
- });
595
- if (!resp.ok)
596
- throw new Error('LLM HTTP ' + resp.status + ': ' + (await resp.text()).slice(0, 120));
597
- const data = await resp.json();
598
- const content = data?.choices?.[0]?.message?.content;
599
- if (!content)
600
- throw new Error('LLM 空回复');
601
- return String(content).trim();
686
+ // 2026-10-15: 网关不可达时**快速降级**而不是挂死 — 公共网关限流/断网时 fetch 可能永久挂起,
687
+ // 手机端 runLocalAgent 会先走 LLM 再降级内置规则, 挂起 = 测试 20s 超时 + 真机"发消息无反应"。
688
+ // 自适应超时 (2026-10-16): 短上下文 10s 底线, 长上下文按 4s/1KB 加时, 最坏 150 步有上界又不会误杀长任务。
689
+ const ctxChars = messages.reduce((n, m) => n + String(m.content || '').length, 0);
690
+ const timeoutMs = adaptiveTimeoutMs(ctxChars);
691
+ // 错误恢复: 网络类错误退避重试 (500ms/1500ms, 共 2 次重试); 超时/API 错误不重试 (明确失败)
692
+ let lastErr = null;
693
+ for (let attempt = 0; attempt < 3; attempt++) {
694
+ try {
695
+ const resp = await fetch(llm.baseUrl.replace(/\/$/, '') + '/chat/completions', {
696
+ method: 'POST',
697
+ headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + (llm.apiKey || '') },
698
+ body: JSON.stringify({
699
+ model: llm.model || 'glm-5.3',
700
+ messages,
701
+ max_tokens: llm.maxTokens || 2048,
702
+ temperature: 0.4,
703
+ }),
704
+ // eslint-disable-next-line no-undef
705
+ signal: AbortSignal.timeout(timeoutMs),
706
+ });
707
+ if (!resp.ok)
708
+ throw new Error('LLM HTTP ' + resp.status + ': ' + (await resp.text()).slice(0, 120));
709
+ const data = await resp.json();
710
+ const content = data?.choices?.[0]?.message?.content;
711
+ if (!content)
712
+ throw new Error('LLM 空回复');
713
+ return String(content).trim();
714
+ }
715
+ catch (e) {
716
+ const cls = classifyMobileError(e);
717
+ lastErr = e;
718
+ if (cls.cls === 'timeout' || cls.cls === 'api' || attempt >= 2)
719
+ throw e; // 明确失败不重试
720
+ await new Promise((r) => setTimeout(r, attempt === 0 ? 500 : 1500)); // 网络波动退避
721
+ }
722
+ }
723
+ throw lastErr || new Error('LLM 调用失败');
602
724
  }
603
725
  /**
604
726
  * 宽容提取 LLM 回复里的工具调用 JSON。
@@ -669,37 +791,141 @@ function parseBalancedJson(raw, start) {
669
791
  }
670
792
  throw new Error('unbalanced');
671
793
  }
672
- /**
673
- * 手机端 agent 循环: goal → LLM 思考 → (工具调用 → 结果回填 → 再思考) → 最终回答。
674
- * 与桌面 pivot loop 同构但浏览器安全边界内; 任何一步失败即降级。
675
- */
676
- export async function runWebAgentLoop(goal, llm) {
794
+ export async function runWebAgentLoop(goal, llm, opts = {}) {
795
+ // ── bootstrap ─────────────────────────────────────────────────────────────
796
+ const id = await ensureIdentity();
797
+ const did = id.did;
798
+ const memory = opts.memory || new MobileMemory(did);
799
+ memory.restore(memory.snapshot()); // 启动恢复记忆 (幂等: restore 只合并缺失条目)
800
+ const channel = opts.channel || 'text';
801
+ const runId = quickHash(did + '|' + goal + '|' + Date.now());
802
+ // task/process 记录: 生命周期开始
803
+ const record = { runId, goal: String(goal || '').slice(0, 80), status: 'running', steps: 0, startedAt: new Date().toISOString(), channel };
804
+ saveRunRecord(did, record);
805
+ // 文档冻结 + 隐式化 (leo: 系统 prompt 注入太多, 文档可以删):
806
+ // userDoc 缺省 = 身份 profile 摘要; soulDoc 缺省 = persona 摘要 (buildAgentSystem 内取)
807
+ const userDoc = opts.userDoc ?? ([id.name ? `名字: ${id.name}` : '', id.phone ? `手机号: ${id.phone}` : '', id.email ? `邮箱: ${id.email}` : '', id.note ? `备注: ${id.note}` : '']
808
+ .filter(Boolean).join('\n'));
809
+ // memory 整段注入: 按目标召回 (冻结快照, 循环内不重建 → 缓存命中稳定)
810
+ const memoryBlock = memory.recall(String(goal || ''), { limit: 3 }).map((e) => e.content).join('\n');
811
+ // 缓存命中: system 装配指纹 = 冻结上下文摘要 + 工具指纹 + 目标前缀
812
+ // 同指纹 (文档/记忆/工具没变) → 复用 system, 不重建 (桌面 kv-prefix-cache 同原理)
813
+ const sysCache = new MobileCache('bolloon_sys');
814
+ // 稳态记忆块参与指纹: 固化记忆变了 → 指纹变 → system 重建 (memory 文档冻结与失效对齐)
815
+ const stableFp = memory.stableBlock(6);
816
+ const sysFp = quickHash([
817
+ userDoc, opts.soulDoc || 'soul-default', memoryBlock, stableFp, toolFingerprint(), channel, opts.plan || '',
818
+ ].join('|'));
677
819
  const messages = [
678
- { role: 'system', content: buildAgentSystem() },
679
- { role: 'user', content: goal },
820
+ { role: 'system', content: sysCache.get(sysFp, 10 * 60_000) || buildAgentSystem({ goal, memoryBlock, userDoc, soulDoc: opts.soulDoc, channel, plan: opts.plan, memory }) },
821
+ { role: 'user', content: String(goal || '') },
680
822
  ];
681
- const MAX_STEPS = 5;
823
+ sysCache.set(sysFp, messages[0].content); // 冻结 system 进缓存 (TTL 10min)
824
+ const MAX_STEPS = 150;
825
+ const NO_PROGRESS_LIMIT = 10; // 同一工具+参数连续 N 轮 → 死循环
826
+ const COMPACT_AT = 30; // 消息超过 N 条 → 压缩中间轮
682
827
  const steps = [];
828
+ let lastToolSig = '';
829
+ let noProgress = 0;
683
830
  for (let i = 0; i < MAX_STEPS; i++) {
684
- const raw = await webAgentLlmCall(messages, llm);
831
+ // 生命周期: 外部取消 → cancelled
832
+ if (opts.signal?.aborted) {
833
+ record.status = 'cancelled';
834
+ record.finishedAt = new Date().toISOString();
835
+ saveRunRecord(did, record);
836
+ _lastWorklog = [...steps, '⚠ 已取消'];
837
+ return `[手机自治执行 ${steps.length} 步 · 已取消]`;
838
+ }
839
+ let raw;
840
+ try {
841
+ raw = await webAgentLlmCall(messages, llm);
842
+ }
843
+ catch (e) {
844
+ // 错误恢复: LLM 调用失败 → 记录 error 生命周期 + 降级
845
+ const hint = classifyMobileError(e).hint;
846
+ record.status = 'error';
847
+ record.error = String(e?.message || e).slice(0, 120);
848
+ record.finishedAt = new Date().toISOString();
849
+ saveRunRecord(did, record);
850
+ _lastWorklog = [...steps, `⚠ ${hint}`];
851
+ // 0 步就失败 + silentDegrade → 抛错让调用方降级 (runLocalAgent 的 catch → 本地内置规则);
852
+ // 否则 (自治调用默认) 返回降级文本: 0 步 → 短提示, 已执行 N 步 → 部分成果摘要, 不让用户丢已完成的步骤。
853
+ if (!steps.length && opts.silentDegrade)
854
+ throw e;
855
+ if (!steps.length)
856
+ return `(${hint}, 请稍后重试)`;
857
+ return `[手机自治执行 ${steps.length} 步 · ${hint}]\n已完成的步骤结果如下:\n${steps.join('\n')}\n(${hint}, 请稍后重试)`;
858
+ }
685
859
  // 2026-10-07 (模拟器验证发现): LLM 常输出前置文本+JSON ("好的,我先查询...\n{\"tool\":...}")
686
860
  // 旧正则要求整段是 JSON → 工具调用被当普通回答, 闭环断。宽容提取第一个 JSON 对象。
687
861
  const toolJson = extractToolCall(raw);
688
862
  if (toolJson && typeof toolJson.tool === 'string' && WEB_AGENT_TOOLS[toolJson.tool]) {
689
- const out = await WEB_AGENT_TOOLS[toolJson.tool]();
863
+ // 无进展检测 (leo 2026-10-14: 不用超时截止任务周期, 任务该做多久做多久 —
864
+ // 但连续重复同一动作不放行, 由无进展兜底决定什么时候算完)
865
+ const sig = toolJson.tool + '|' + JSON.stringify(toolJson.args || {});
866
+ noProgress = (sig === lastToolSig) ? noProgress + 1 : 0;
867
+ lastToolSig = sig;
868
+ if (noProgress >= NO_PROGRESS_LIMIT) {
869
+ record.status = 'done';
870
+ record.finishedAt = new Date().toISOString();
871
+ saveRunRecord(did, record);
872
+ _lastWorklog = [...steps, `⚠ 连续 ${NO_PROGRESS_LIMIT} 轮重复同一工具调用 (${toolJson.tool}), 判断死循环提前停止`];
873
+ return `[手机自治执行 ${steps.length} 步 · 检测到重复动作提前停止]\n` + raw.slice(0, 200);
874
+ }
875
+ // 确认模式: 高风险工具 → paused → confirm 回调裁决 (拒绝 = 跳过执行, 结果如实告知)
876
+ if (isConfirmRequiredTool(toolJson.tool)) {
877
+ const allow = opts.confirm ? await opts.confirm(toolJson.tool, toolJson.args) : false;
878
+ if (!allow) {
879
+ const denied = `工具 ${toolJson.tool} 需要确认, 本次未执行 (请用户批准后重试)。`;
880
+ steps.push(`🔒 ${toolJson.tool} → ${denied}`);
881
+ messages.push({ role: 'assistant', content: raw });
882
+ messages.push({ role: 'user', content: `工具结果: ${denied}` });
883
+ continue;
884
+ }
885
+ }
886
+ // 工具执行 (带 args — 更灵活调用)
887
+ const out = await WEB_AGENT_TOOLS[toolJson.tool](toolJson.args || {});
690
888
  steps.push(`🔧 ${toolJson.tool} → ${out.slice(0, 80)}`);
889
+ record.steps = steps.length;
890
+ saveRunRecord(did, record);
891
+ // 中间阶段性环境: 回调 (UI 进度)
892
+ if (opts.onProgress) {
893
+ try {
894
+ opts.onProgress({ step: steps.length, tool: toolJson.tool, output: out.slice(0, 120) });
895
+ }
896
+ catch { /* 回调失败不影响循环 */ }
897
+ }
898
+ if (steps.length > 60)
899
+ steps.splice(0, steps.length - 60); // 摘要只留最近 60 条, 不无限涨
691
900
  messages.push({ role: 'assistant', content: raw });
692
901
  messages.push({ role: 'user', content: `工具结果: ${out}` });
902
+ // 上下文压缩: 超过阈值 → 中间轮压成一条摘要 (system + 目标 + 最近 K 轮保留)
903
+ if (messages.length > COMPACT_AT) {
904
+ const keep = messages.slice(0, 2);
905
+ const recent = messages.slice(-10);
906
+ const summary = `(中间 ${messages.length - 12} 轮对话已压缩)已完成步骤: ${steps.slice(-12).map((s) => s.slice(0, 60)).join(' | ')}`;
907
+ keep.push({ role: 'user', content: summary });
908
+ messages.length = 0;
909
+ messages.push(...keep, ...recent);
910
+ }
693
911
  continue;
694
912
  }
695
- // 无工具调用 = 最终回答
913
+ // 无工具调用 = 最终回答 → 记忆迭代 (新事实/偏好/关系入库) + 快照落盘 + done
914
+ memory.autoIterate({ goal: String(goal || ''), toolResults: steps, reply: raw, source: 'session' });
915
+ memory.snapshot(); // 记忆快照: 收尾落盘 (bootstrap 下次恢复)
916
+ record.status = 'done';
917
+ record.finishedAt = new Date().toISOString();
918
+ saveRunRecord(did, record);
696
919
  _lastWorklog = steps.length ? [...steps, `💬 ${raw.slice(0, 60)}`] : [`💬 ${raw.slice(0, 60)}`];
697
920
  if (steps.length)
698
921
  return `[手机自治执行 ${steps.length} 步]\n` + raw;
699
922
  return raw;
700
923
  }
701
- // 循环保护: 没收敛就给最后一条
924
+ // 循环保护: 没收敛就给最后一条 (生命周期 done)
702
925
  const last = messages[messages.length - 1]?.content || '';
926
+ record.status = 'done';
927
+ record.finishedAt = new Date().toISOString();
928
+ saveRunRecord(did, record);
703
929
  _lastWorklog = [...steps, '⚠ 达到步数上限, 已停止'];
704
930
  return `[手机自治执行, 达到 ${MAX_STEPS} 步上限]\n${last.slice(0, 200)}`;
705
931
  }
@@ -83,10 +83,6 @@ export async function loadOrCreateDeviceKey(storage, c) {
83
83
  storage.setItem(K_DEVICE, JSON.stringify(material));
84
84
  return material;
85
85
  }
86
- async function importPrivate(storage, c) {
87
- const k = await loadOrCreateDeviceKey(storage, c);
88
- return c.subtle.importKey('jwk', k.privateKeyJwk, { name: 'Ed25519' }, false, ['sign']);
89
- }
90
86
  /** 用设备私钥签一条 Grant (核心: 手机确认 → 桌面才认) */
91
87
  export async function signGrantOnDevice(grant, storage, c) {
92
88
  return signPayloadOnDevice(canonicalGrantPayload(grant), storage, c);