@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
@@ -69,7 +69,7 @@
69
69
  });
70
70
  } catch (e) {}
71
71
 
72
- const TITLES = { main: '首页', friends: '好友', tasks: '任务', me: '我' };
72
+ const TITLES = { main: '首页', friends: '通讯录', tasks: '任务', me: '我' };
73
73
  let currentTab = 'main';
74
74
  function switchTab(tab) {
75
75
  currentTab = tab;
@@ -166,7 +166,7 @@
166
166
  if (!list) return;
167
167
  list.innerHTML = '';
168
168
  if (!Array.isArray(peers) || peers.length === 0) {
169
- list.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-muted)">暂无好友</div>';
169
+ list.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-muted)">暂无联系人</div>';
170
170
  const idx = $('#contacts-index'); if (idx) idx.innerHTML = '';
171
171
  return;
172
172
  }
@@ -453,15 +453,15 @@
453
453
  try {
454
454
  const s = await api.get('/api/auth/status');
455
455
  $('#me-name').textContent = s.loggedIn ? (s.name || '未登录') : '未登录';
456
- $('#me-did').textContent = s.did ? ('DID: ' + (s.didShort || s.did)) : '';
456
+ $('#me-did').textContent = s.did ? ('身份证明: ' + (s.didShort || s.did)) : '';
457
457
  if (avatarEl && !av) avatarEl.textContent = (s.loggedIn && s.name ? s.name : 'U').charAt(0);
458
458
  $('#login-label').textContent = s.loggedIn ? '已登录' : '登录';
459
459
  } catch (e) { /* 默认 */ }
460
- // P2P ID = 通信ID, 与 DID 不同 (libp2p nodeId)
460
+ // 身份证明 (连接标识)
461
461
  try {
462
462
  const net = await api.get('/api/network/status');
463
463
  const p2p = net && net.nodeId;
464
- $('#me-p2p').textContent = p2p ? ('P2P: ' + p2p.slice(0, 14) + '…') : '';
464
+ $('#me-p2p').textContent = p2p ? ('连接标识: ' + p2p.slice(0, 14) + '…') : '';
465
465
  } catch (e) { /* 未连接 */ }
466
466
  }
467
467
 
@@ -488,10 +488,10 @@
488
488
  <div class="avatar" id="identity-avatar" style="width:72px;height:72px"></div>
489
489
  <div><div class="profile-name">${escapeHtml(name)}</div><div style="font-size:12px;color:var(--text-muted)">人类认证身份</div></div>
490
490
  </div>
491
- <div class="identity-row"><div class="k">DID 身份 (全局唯一)</div><div class="v">${escapeHtml(did || '未生成')}</div></div>
492
- <div class="identity-row"><div class="k">P2P ID (通信 ID)</div><div class="v">${escapeHtml(p2p || '未连接')}</div></div>
491
+ <div class="identity-row"><div class="k">身份证明 (全局唯一)</div><div class="v">${escapeHtml(did || '未生成')}</div></div>
492
+ <div class="identity-row"><div class="k">连接标识</div><div class="v">${escapeHtml(p2p || '未连接')}</div></div>
493
493
  <div class="identity-row"><div class="k">创建时间</div><div class="v">${escapeHtml(created || '未知')}</div></div>
494
- <div class="identity-row"><div class="k">说明</div><div class="v">DID 是全局唯一的人类认证身份; P2P ID 只是本机 libp2p 通信节点号, 两者不同。</div></div>`;
494
+ <div class="identity-row"><div class="k">说明</div><div class="v">身份证明是全局唯一的人类认证身份; 连接标识是本机的通信编号, 两者不同。</div></div>`;
495
495
  const av = localStorage.getItem('bolloon_avatar');
496
496
  const ia = body.querySelector('#identity-avatar');
497
497
  if (ia) { if (av) { ia.style.backgroundImage = 'url(' + av + ')'; ia.style.backgroundSize = 'cover'; ia.textContent = ''; } else ia.textContent = (name || 'U').charAt(0); }
@@ -592,9 +592,9 @@
592
592
  const cards = $('#world-feed-cards');
593
593
  if (!feed || !cards) return;
594
594
  try {
595
- // 2026-10-05 (leo: 手机端 ↔ 电脑端互联): 走 api 层 → BolloonCore.resolve
596
- // 转发电脑端 desktopUrl (不再裸 fetch 相对路径 — 手机上相对路径连手机自己, 永远空)
597
- const data = await api.get('/api/opportunities?min-score=0.2&limit=10');
595
+ // 2026-10-16 (leo: 首页卡片按「附近 + 通讯录」动态传送): 三源聚合 (桌面机会流 + 通讯录卡 + 附近卡)
596
+ // 不再只拉桌面机会流; 来源不上屏 (leo: 主动推送不贴元解释文案)
597
+ const data = await api.get('/api/world/cards');
598
598
  const opps = ((data && data.opportunities) || []).slice(0, 6);
599
599
  if (!opps.length) { feed.hidden = true; return; }
600
600
  feed.hidden = false;
@@ -748,8 +748,8 @@
748
748
  allAgentCards.push({
749
749
  id: p.id || p.publicKey?.slice(0, 12) || `peer-${Date.now()}`,
750
750
  agentId: p.id || p.publicKey?.slice(0, 12),
751
- name: p.name || '未知好友',
752
- desc: 'P2P 好友智能体',
751
+ name: p.name || '未知联系人',
752
+ desc: '通讯录智能体',
753
753
  avatar: null,
754
754
  peer: { name: p.name || '好友', online: p.online || false },
755
755
  status: p.online ? 'online' : 'offline',
@@ -1102,7 +1102,7 @@
1102
1102
  <div style="flex:1;font-weight:600">搜索</div>
1103
1103
  </div>
1104
1104
  <div style="padding:12px;display:flex;flex-direction:column;gap:10px;flex:1;min-height:0">
1105
- <input id="search-input" type="search" autocomplete="off" placeholder="搜智能体 / 好友:名称、DID、节点 ID"
1105
+ <input id="search-input" type="search" autocomplete="off" placeholder="搜智能体 / 通讯录:名称、身份证明、编号"
1106
1106
  style="padding:10px 12px;border:1px solid var(--border);border-radius:10px;background:var(--bg-hover);color:var(--text);font-size:14px">
1107
1107
  <div id="search-hint" style="font-size:12px;color:var(--text-muted)">正在建立索引…</div>
1108
1108
  <div id="search-results" style="flex:1;overflow:auto;-webkit-overflow-scrolling:touch"></div>
@@ -1122,10 +1122,10 @@
1122
1122
  act: () => { page.remove(); openChat(c); },
1123
1123
  })),
1124
1124
  ...peers.map((p) => ({
1125
- tag: '好友', name: p.name || String(p.publicKey || p.id || '好友').slice(0, 12),
1125
+ tag: '通讯录', name: p.name || String(p.publicKey || p.id || '联系人').slice(0, 12),
1126
1126
  sub: String(p.publicKey || p.id || p.address || '').slice(0, 28),
1127
1127
  hay: [p.name, p.publicKey, p.id, p.address, p.did].filter(Boolean).join(' '),
1128
- act: () => alert(`好友信息\n名称: ${p.name || '(未命名)'}\n节点ID: ${p.publicKey || p.id || '—'}\n地址: ${p.address || '—'}`),
1128
+ act: () => alert(`联系人信息\n名称: ${p.name || '(未命名)'}\n编号: ${p.publicKey || p.id || '—'}\n地址: ${p.address || '—'}`),
1129
1129
  })),
1130
1130
  ...services.map((s) => ({
1131
1131
  tag: '全局智能体',
@@ -1142,10 +1142,10 @@
1142
1142
  const render = (q) => {
1143
1143
  const k = String(q || '').trim().toLowerCase();
1144
1144
  const hits = rows.filter((r) => !k || r.hay.includes(k) || r.name.toLowerCase().includes(k));
1145
- if (hint) hint.innerHTML = `本机智能体 ${channels.length} · 好友 ${peers.length} · 全局智能体 ${services.length}` + (k ? ` · 命中 ${hits.length}` : '');
1145
+ if (hint) hint.innerHTML = `本机智能体 ${channels.length} · 通讯录 ${peers.length} · 全局智能体 ${services.length}` + (k ? ` · 命中 ${hits.length}` : '');
1146
1146
  if (!box) return;
1147
1147
  if (!hits.length) {
1148
- box.innerHTML = `<div style="padding:24px;text-align:center;color:var(--text-muted);font-size:13px">没有匹配「${escapeHtml(String(q))}」的智能体或好友</div>`;
1148
+ box.innerHTML = `<div style="padding:24px;text-align:center;color:var(--text-muted);font-size:13px">没有匹配「${escapeHtml(String(q))}」的智能体或联系人</div>`;
1149
1149
  return;
1150
1150
  }
1151
1151
  let html = '';
@@ -2135,11 +2135,12 @@
2135
2135
  $('#settings-persona').addEventListener('click', () => void openPersonaPage());
2136
2136
  }
2137
2137
 
2138
- // === 身份页 (DID / 公钥 / 签名测试 / 邮箱 / 手机号) ===
2138
+ // === 身份页 (身份证明 / 签名 / 邮箱 / 手机号) ===
2139
2139
  // 2026-10-07: 完整身份系统 — DIAP did:key (与桌面同构) + 附带条件 (邮箱/手机号/备注)
2140
+ // 2026-10-16 (leo: 显示端降低认知负担): 不外露 DID/Ed25519 术语 — 「身份证明」「身份密钥」「签名能力」
2140
2141
  async function openIdentityPage() {
2141
2142
  if ($('#identity-page')) return;
2142
- const page = mkOverlayPage('identity-page', 'DID 与身份', '<div style="font-size:12px;color:var(--text-muted)">读取中…</div>');
2143
+ const page = mkOverlayPage('identity-page', '身份与证明', '<div style="font-size:12px;color:var(--text-muted)">读取中…</div>');
2143
2144
  const body = page.querySelector('#identity-page-body');
2144
2145
  try {
2145
2146
  const core = window.BolloonCore || (await import('./mobile-core.js')).core;
@@ -2147,8 +2148,8 @@
2147
2148
  const st = await ag.identityStatus();
2148
2149
  body.innerHTML = `
2149
2150
  <div class="list">
2150
- <div class="list-item"><span class="list-icon">${ICONS.idcard}</span><span style="flex:1"><span style="display:block">DID</span><span class="conv-preview" style="display:block;word-break:break-all">${escapeHtml(st.did || '未生成')}</span></span></div>
2151
- <div class="list-item"><span class="list-icon">${ICONS.chip}</span><span style="flex:1"><span style="display:block">公钥 (Ed25519)</span><span class="conv-preview" style="display:block;word-break:break-all">${st.publicKey ? escapeHtml(st.publicKey.slice(0, 32) + '…') : '(老设备哈希身份,无签名)'}</span></span></div>
2151
+ <div class="list-item"><span class="list-icon">${ICONS.idcard}</span><span style="flex:1"><span style="display:block">身份证明</span><span class="conv-preview" style="display:block;word-break:break-all">${escapeHtml(st.did || '未生成')}</span></span></div>
2152
+ <div class="list-item"><span class="list-icon">${ICONS.chip}</span><span style="flex:1"><span style="display:block">身份密钥</span><span class="conv-preview" style="display:block;word-break:break-all">${st.publicKey ? escapeHtml(st.publicKey.slice(0, 32) + '…') : '(老设备哈希身份,无签名)'}</span></span></div>
2152
2153
  <div class="list-item"><span class="list-icon">${ICONS.chip}</span><span style="flex:1"><span style="display:block">名字</span><span class="conv-preview" style="display:block">${escapeHtml(st.name || '未登录')}</span></span></div>
2153
2154
  </div>
2154
2155
  <div class="section-label">附带身份条件(仅存本机)</div>
@@ -2158,7 +2159,7 @@
2158
2159
  <div class="list-item"><span class="list-icon">📝</span><span style="flex:1"><span style="display:block">备注</span><span class="conv-preview" id="id-note" style="display:block">${escapeHtml(st.note || '未设置')}</span></span></div>
2159
2160
  <button class="sheet-choice" id="id-edit" style="width:100%;margin-top:8px">编辑邮箱 / 手机号 / 备注</button>
2160
2161
  </div>
2161
- <div class="section-label">签名能力(DIAP Ed25519)</div>
2162
+ <div class="section-label">签名能力</div>
2162
2163
  <div class="list">
2163
2164
  <button class="sheet-choice" id="id-sign-test" style="width:100%">测试签名(本地签 + 验,证明身份可签名)</button>
2164
2165
  </div>`;
@@ -2733,6 +2734,52 @@
2733
2734
  inp.click();
2734
2735
  }
2735
2736
 
2737
+ // === 手机号搜索添加好友 (2026-10-15, 手机原生手机号功能) ===
2738
+ // 目录数据在电脑端 (human-directory.json), 手机端只转发; 电脑端不可达 → 明确报错不假装。
2739
+ function openPhoneSearch() {
2740
+ hideSheet('#addfriend-sheet');
2741
+ showSheet('#phone-search-sheet');
2742
+ const inp = $('#phone-search-input');
2743
+ if (inp) { inp.value = ''; inp.focus(); }
2744
+ const result = $('#phone-search-result');
2745
+ if (result) { result.style.display = 'none'; result.innerHTML = ''; }
2746
+ }
2747
+ async function doPhoneSearch() {
2748
+ const inp = $('#phone-search-input');
2749
+ const result = $('#phone-search-result');
2750
+ if (!inp || !result) return;
2751
+ const phone = (inp.value || '').replace(/[^\d+]/g, '');
2752
+ if (!/^\+?\d{8,15}$/.test(phone)) { result.style.display = 'block'; result.innerHTML = '<div style="color:var(--danger,#e05555);font-size:13px">请输入正确的手机号 (8-15 位数字)</div>'; return; }
2753
+ result.style.display = 'block';
2754
+ result.innerHTML = '<div style="color:var(--text-secondary);font-size:13px">搜索中…</div>';
2755
+ try {
2756
+ const r = await api.post('/api/phone/directory/lookup', { phone });
2757
+ if (r && r.found) {
2758
+ const name = r.displayName || String(r.did || '').slice(0, 12);
2759
+ result.innerHTML = `<div style="font-size:14px;margin-bottom:6px">✅ 找到账号: <b>${escapeHtml(name)}</b></div>
2760
+ <div style="font-size:12px;color:var(--text-secondary);margin-bottom:10px">身份证明: ${escapeHtml(String(r.did || ''))}</div>
2761
+ <button class="sheet-choice" id="phone-add-btn" style="width:100%">发送好友申请</button>`;
2762
+ const btn = $('#phone-add-btn');
2763
+ if (btn) btn.addEventListener('click', async () => {
2764
+ btn.disabled = true; btn.textContent = '发送中…';
2765
+ const a = await api.post('/api/phone/directory/add-friend', { phone });
2766
+ btn.disabled = false;
2767
+ if (a && a.ok !== false) {
2768
+ btn.textContent = '✅ 已发送 (对方同意后建立好友)';
2769
+ if (a.friend && a.friend.requestId) result.innerHTML += `<div style="font-size:12px;color:var(--text-secondary);margin-top:6px">申请号: ${escapeHtml(String(a.friend.requestId).slice(0, 12))}… 可到「待处理申请」查看</div>`;
2770
+ } else {
2771
+ btn.textContent = '重试发送';
2772
+ result.innerHTML += `<div style="color:var(--danger,#e05555);font-size:12px;margin-top:6px">发送失败: ${escapeHtml((a && a.error) || '未知原因')}</div>`;
2773
+ }
2774
+ });
2775
+ } else {
2776
+ result.innerHTML = '<div style="color:var(--text-secondary);font-size:13px">未找到该手机号的账号 (对方未发布目录或已撤销)。</div>';
2777
+ }
2778
+ } catch (e) {
2779
+ result.innerHTML = `<div style="color:var(--danger,#e05555);font-size:13px">查询失败: ${escapeHtml((e && e.message) || e || '未知错误')}</div>`;
2780
+ }
2781
+ }
2782
+
2736
2783
  // === 复制 (助记词/私钥/地址 快捷复制; 全局委托 [data-copy]) ===
2737
2784
  async function copyText(t) {
2738
2785
  const txt = String(t || '');
@@ -2755,7 +2802,9 @@
2755
2802
  setTimeout(() => { b.textContent = old; }, 1200);
2756
2803
  });
2757
2804
 
2758
- // === 登录页 (设置本机身份昵称) ===
2805
+ // === 登录页 (手机号验证码登录 + 昵称兜底) ===
2806
+ // 2026-10-15: 手机原生手机号登录 — 输手机号 → 发验证码 → 验证通过 → 身份绑定手机号。
2807
+ // 验证码链路转发电脑端 /api/contacts/bind + verify (与桌面同一 OTP 通道)。
2759
2808
  function openLoginPage() {
2760
2809
  const page = document.createElement('div');
2761
2810
  page.className = 'chat-page';
@@ -2766,13 +2815,61 @@
2766
2815
  <div style="flex:1;font-weight:600">登录</div>
2767
2816
  </div>
2768
2817
  <div style="padding:12px;display:flex;flex-direction:column;gap:12px">
2769
- <div style="font-size:13px;color:var(--text-secondary)">登录 = 在本机创建/启用身份并设置昵称。DID 由本机生成, 不上传服务器。</div>
2818
+ <div style="font-size:13px;color:var(--text-secondary)">手机号登录: 输手机号 → 收验证码 → 登录。身份仍由本机 DID 承载, 手机号只是登录/查找凭证。</div>
2819
+ <input id="login-phone" type="tel" inputmode="numeric" placeholder="手机号 (如 13812345678)" style="${_walletInput}">
2820
+ <div style="display:flex;gap:8px">
2821
+ <input id="login-code" inputmode="numeric" placeholder="验证码" style="${_walletInput};flex:1" disabled>
2822
+ <button id="login-sendcode" style="${_walletBtn};width:110px;flex:none">发送验证码</button>
2823
+ </div>
2824
+ <button id="login-submit" style="${_walletBtn}">手机号登录</button>
2825
+ <div style="border-top:1px solid var(--border);margin-top:4px;padding-top:12px;font-size:12px;color:var(--text-secondary)">或者用昵称登录 (本机身份, 不上传):</div>
2770
2826
  <input id="login-name" placeholder="昵称 (如 觉者)" style="${_walletInput}">
2771
- <button id="login-submit" style="${_walletBtn}">登录</button>
2827
+ <button id="login-name-submit" style="${_walletBtn}">昵称登录</button>
2772
2828
  </div>`;
2773
2829
  document.body.appendChild(page);
2774
2830
  $('#login-back').addEventListener('click', () => page.remove());
2775
- $('#login-submit').addEventListener('click', async () => {
2831
+
2832
+ // 手机号登录: 登录成功后把手机号写进身份 profile (手机号即凭证)
2833
+ const phoneInput = $('#login-phone');
2834
+ const codeInput = $('#login-code');
2835
+ const sendBtn = $('#login-sendcode');
2836
+ const submitBtn = $('#login-submit');
2837
+ let _pending = { contactId: '', challengeId: '' };
2838
+ sendBtn.addEventListener('click', async () => {
2839
+ const phone = (phoneInput.value || '').replace(/[^\d+]/g, '');
2840
+ if (!/^\+?\d{8,15}$/.test(phone)) { alert('请输入正确的手机号 (8-15 位数字)'); return; }
2841
+ sendBtn.disabled = true; sendBtn.textContent = '发送中…';
2842
+ try {
2843
+ const r = await api.post('/api/phone/auth/bind', { phone });
2844
+ if (!r || r.ok === false) { alert((r && r.error) || '发送失败'); sendBtn.disabled = false; sendBtn.textContent = '发送验证码'; return; }
2845
+ _pending = { contactId: String(r.contactId || ''), challengeId: String(r.challengeId || '') };
2846
+ codeInput.disabled = false; codeInput.focus();
2847
+ sendBtn.textContent = '已发送 ✓';
2848
+ // 本地落盘通道会回显验证码 (方便无短信设备的调试; 真 SMS 通道不回显)
2849
+ if (r.otpForLocalSink) { showToast ? showToast('验证码: ' + String(r.otpForLocalSink)) : alert('验证码: ' + String(r.otpForLocalSink)); }
2850
+ setTimeout(() => { sendBtn.disabled = false; sendBtn.textContent = '重新发送'; }, 30000);
2851
+ } catch (e) { alert('发送失败: ' + (e.message || e)); sendBtn.disabled = false; sendBtn.textContent = '发送验证码'; }
2852
+ });
2853
+ submitBtn.addEventListener('click', async () => {
2854
+ const code = (codeInput.value || '').trim();
2855
+ if (!_pending.contactId || !code) { alert('先发送验证码并输入验证码'); return; }
2856
+ submitBtn.disabled = true; submitBtn.textContent = '验证中…';
2857
+ try {
2858
+ const v = await api.post('/api/phone/auth/verify', { contactId: _pending.contactId, challengeId: _pending.challengeId, code });
2859
+ if (!v || v.ok === false) { alert((v && v.error) || '验证失败'); submitBtn.disabled = false; submitBtn.textContent = '手机号登录'; return; }
2860
+ // 验证通过 → 昵称默认取手机号, 登录本机身份, 手机号写进 profile
2861
+ const phone = (phoneInput.value || '').replace(/[^\d+]/g, '');
2862
+ await api.post('/api/auth/login', { name: '用户' + phone.slice(-4) });
2863
+ try { await api.post('/api/phone/profile/phone', { phone }); } catch { /* 本地 profile 更新失败不阻塞登录 */ }
2864
+ page.remove();
2865
+ await loadMe();
2866
+ switchTab('me');
2867
+ await autoSyncDesktop();
2868
+ } catch (e) { alert('验证失败: ' + (e.message || e)); submitBtn.disabled = false; submitBtn.textContent = '手机号登录'; }
2869
+ });
2870
+ // 手机号 → 本机身份 profile (只存本机; 电脑端目录发布仍需用户在电脑端做)
2871
+ // 昵称登录兜底
2872
+ $('#login-name-submit').addEventListener('click', async () => {
2776
2873
  const name = (($('#login-name') || {}).value || '').trim();
2777
2874
  try {
2778
2875
  await api.post('/api/auth/login', { name });
@@ -2796,7 +2893,7 @@
2796
2893
  bindAvatarInput();
2797
2894
  $('#item-login').addEventListener('click', () => { openLoginPage(); });
2798
2895
  $('#item-logout').addEventListener('click', async () => {
2799
- if (!confirm('确定注销? 将清除本机登录态 (DID 保留, 不影响频道/P2P)。')) return;
2896
+ if (!confirm('确定注销? 将清除本机登录态 (身份证明保留, 不影响频道/连接)。')) return;
2800
2897
  try { await api.post('/api/auth/logout', {}); await loadMe(); } catch (e) { alert('注销失败: ' + (e.message || e)); }
2801
2898
  });
2802
2899
  $('#btn-add').addEventListener('click', addFriend);
@@ -2813,8 +2910,8 @@
2813
2910
  if (first) openTradeCall(first); else openTradeHistory();
2814
2911
  });
2815
2912
  $('#item-p2p-id').addEventListener('click', async () => {
2816
- try { const net = await api.get('/api/network/status'); const p2p = net && net.nodeId; alert('P2P ID (通信ID, ≠ DID):\n' + (p2p || '未连接')); }
2817
- catch (e) { alert('P2P ID: 获取失败'); }
2913
+ try { const net = await api.get('/api/network/status'); const p2p = net && net.nodeId; alert('连接标识 (本机通信编号):\n' + (p2p || '未连接')); }
2914
+ catch (e) { alert('连接标识: 获取失败'); }
2818
2915
  });
2819
2916
  // #1 一键入网 (全球智能体网络): 点一下 → 默认 prompt 交给智能体执行
2820
2917
  const joinGlobalBtn = $('#item-join-global');
@@ -2838,16 +2935,33 @@
2838
2935
  // #2b 连接好友 (点按式) + 附近设备 + 待处理申请
2839
2936
  const addFriendBtn = $('#item-add-friend');
2840
2937
  if (addFriendBtn) addFriendBtn.addEventListener('click', () => showSheet('#addfriend-sheet'));
2938
+ const cPhone = $('#choice-phone');
2939
+ if (cPhone) cPhone.addEventListener('click', () => openPhoneSearch());
2841
2940
  const cNearby = $('#choice-nearby');
2842
2941
  if (cNearby) cNearby.addEventListener('click', () => { hideSheet('#addfriend-sheet'); openNearbySheet('friend'); });
2843
2942
  const cRequests = $('#choice-requests');
2844
2943
  if (cRequests) cRequests.addEventListener('click', () => { hideSheet('#addfriend-sheet'); openRequestsSheet(); });
2944
+ // #2c 手机号搜索添加好友 sheet 的按键绑定
2945
+ const psDo = $('#phone-search-do');
2946
+ if (psDo) psDo.addEventListener('click', () => { void doPhoneSearch(); });
2947
+ const psClose = $('#phone-search-close');
2948
+ if (psClose) psClose.addEventListener('click', () => hideSheet('#phone-search-sheet'));
2949
+ const psInput = $('#phone-search-input');
2950
+ if (psInput) psInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); void doPhoneSearch(); } });
2845
2951
  const nearbyBtn = $('#item-nearby');
2846
2952
  if (nearbyBtn) nearbyBtn.addEventListener('click', () => openNearbySheet('friend'));
2847
2953
  const nearbyRefresh = $('#nearby-refresh');
2848
2954
  if (nearbyRefresh) nearbyRefresh.addEventListener('click', () => loadNearbyList(_nearbyMode));
2849
2955
  const nearbyClose = $('#nearby-close');
2850
2956
  if (nearbyClose) nearbyClose.addEventListener('click', () => hideSheet('#nearby-sheet'));
2957
+ // 2026-10-16 (leo: 首页卡片动态传送): P2P 收到附近卡 → 即时刷新首页 (不等 45s 轮询)
2958
+ try {
2959
+ window.BolloonCore && window.BolloonCore.events && window.BolloonCore.events.subscribe((m) => {
2960
+ if (m && m.type === 'world-card') loadWorldFeed();
2961
+ });
2962
+ } catch (e) { /* 事件订阅失败不影响其它功能 */ }
2963
+ // 连上对端即要一次卡 (动态传送主动触发; 无 peer 静默)
2964
+ api.get('/api/world/cards/request').then(() => {}).catch(() => {});
2851
2965
  // 微信息 (x402 付费信息): 详情 → 购买并验真 / 只看元数据
2852
2966
  const x402Buy = $('#x402-buy');
2853
2967
  if (x402Buy) x402Buy.addEventListener('click', () => buyX402Info());
@@ -413,8 +413,101 @@ export function registerContactRoutes(app, opts = {}) {
413
413
  res.status(500).json({ ok: false, error: String(err?.message || err) });
414
414
  }
415
415
  });
416
+ // ── 手机号目录 (2026-10-11, phone-account-lookup-design 落点 A) ────────────
417
+ // 隐私: 请求/响应全程只过「盲化哈希」, 明文手机号只在请求体出现一次 (发布/查询),
418
+ // 永不出现在响应里 (与 contacts 既有红线一致)。
419
+ const dirHome = () => opts.home || undefined;
420
+ /** 发布手机号 → DID 指针 (opt-in; 用户显式调用) */
421
+ app.post('/api/contacts/directory/publish', async (req, res) => {
422
+ try {
423
+ const { publishEntry, isPlausiblePhone } = await import('../agents/contacts/human-directory.js');
424
+ const phone = String(req.body?.phone || '');
425
+ const did = String(req.body?.did || ownerDidFromDisk());
426
+ if (!isPlausiblePhone(phone))
427
+ return res.status(400).json({ ok: false, error: '手机号格式不正确' });
428
+ const e = await publishEntry(phone, did, {
429
+ displayName: req.body?.displayName ? String(req.body.displayName) : undefined,
430
+ publicKeyHex: req.body?.publicKeyHex ? String(req.body.publicKeyHex) : undefined,
431
+ homeDir: dirHome(),
432
+ });
433
+ res.json({ ok: true, phoneHash: e.phoneHash.slice(0, 8) + '…', did: e.did, publicKeyHex: e.publicKeyHex || null, publishedAt: e.publishedAt });
434
+ }
435
+ catch (err) {
436
+ res.status(500).json({ ok: false, error: String(err?.message || err) });
437
+ }
438
+ });
439
+ /** 撤销目录条目 (软删) */
440
+ app.post('/api/contacts/directory/retract', async (req, res) => {
441
+ try {
442
+ const { retractEntry } = await import('../agents/contacts/human-directory.js');
443
+ const did = String(req.body?.did || ownerDidFromDisk());
444
+ const ok = await retractEntry(did, dirHome());
445
+ res.json({ ok, revoked: ok });
446
+ }
447
+ catch (err) {
448
+ res.status(500).json({ ok: false, error: String(err?.message || err) });
449
+ }
450
+ });
451
+ /** 按手机号查账号 (在本机目录内找; 响应只回 DID/昵称, 绝不回手机号) */
452
+ app.post('/api/contacts/directory/lookup', async (req, res) => {
453
+ try {
454
+ const { lookupPhone, isPlausiblePhone } = await import('../agents/contacts/human-directory.js');
455
+ const phone = String(req.body?.phone || '');
456
+ if (!isPlausiblePhone(phone))
457
+ return res.status(400).json({ ok: false, error: '手机号格式不正确' });
458
+ const r = await lookupPhone(phone, dirHome());
459
+ if (!r.found)
460
+ return res.json({ ok: true, found: false, note: '未找到 (未注册或未公开)' });
461
+ res.json({ ok: true, found: true, did: r.did, publicKeyHex: r.publicKeyHex || null, displayName: r.displayName || null });
462
+ }
463
+ catch (err) {
464
+ res.status(500).json({ ok: false, error: String(err?.message || err) });
465
+ }
466
+ });
467
+ /** 按手机号直接发好友请求 (P1, 2026-10-15): directory lookup → publicKeyHex → 既有 /api/friend-request */
468
+ app.post('/api/contacts/directory/add-friend', async (req, res) => {
469
+ try {
470
+ const { lookupPhone, isPlausiblePhone } = await import('../agents/contacts/human-directory.js');
471
+ const phone = String(req.body?.phone || '');
472
+ if (!isPlausiblePhone(phone))
473
+ return res.status(400).json({ ok: false, error: '手机号格式不正确' });
474
+ const r = await lookupPhone(phone, dirHome());
475
+ if (!r.found)
476
+ return res.json({ ok: true, found: false, note: '未找到 (未注册或未公开)' });
477
+ if (!r.publicKeyHex)
478
+ return res.status(422).json({ ok: false, found: true, error: `对方已注册目录但未公开 P2P 公钥 (${r.did?.slice(0, 20)}…), 无法直接加好友` });
479
+ // 委托给既有好友请求链路 (本机端口; 与 pi-sdk accept 工具同口径)
480
+ const port = process.env.PORT || '54188';
481
+ const fr = await fetch(`http://127.0.0.1:${port}/api/friend-request`, {
482
+ method: 'POST',
483
+ headers: { 'Content-Type': 'application/json' },
484
+ body: JSON.stringify({
485
+ targetPublicKey: r.publicKeyHex,
486
+ name: req.body?.name ? String(req.body.name) : r.displayName || undefined,
487
+ message: req.body?.message ? String(req.body.message) : `来自手机号目录: 想加你为好友`,
488
+ note: `via-phone-directory`,
489
+ }),
490
+ });
491
+ const data = await fr.json();
492
+ res.status(fr.status).json({ ok: data.ok !== false, found: true, did: r.did, friend: data });
493
+ }
494
+ catch (err) {
495
+ res.status(500).json({ ok: false, error: String(err?.message || err) });
496
+ }
497
+ });
498
+ /** 列出已发布条目 (本机审计; 只回脱敏哈希) */
499
+ app.get('/api/contacts/directory/list', async (_req, res) => {
500
+ try {
501
+ const { readDirectory, summarizeEntries } = await import('../agents/contacts/human-directory.js');
502
+ const entries = await readDirectory(dirHome());
503
+ res.json({ ok: true, entries: summarizeEntries(entries) });
504
+ }
505
+ catch (err) {
506
+ res.status(500).json({ ok: false, error: String(err?.message || err) });
507
+ }
508
+ });
416
509
  // ── 隐藏守卫自检 (验收/测试直接用) ──────────────────────────────────────────
417
510
  app.get('/api/contacts/_selfcheck', (_req, res) => {
418
- res.json({ ok: true, plaintextGuard: looksLikePlaintextSecret({ contacts: [{ value: '+8613812345678' }] }) });
511
+ res.json({ ok: true, plaintextGuard: looksLikePlaintextSecret({ contacts: [{ value: '+861****5678' }] }) });
419
512
  });
420
513
  }
@@ -72,7 +72,15 @@ export function registerWorldRoutes(app) {
72
72
  const minScore = Number(minScoreRaw);
73
73
  const limitRaw = typeof req.query.limit === 'string' ? req.query.limit : '50';
74
74
  const limit = Math.max(1, Math.min(200, parseInt(limitRaw, 10) || 50));
75
- const r = await scanOpportunities({ minScore: Number.isFinite(minScore) ? minScore : 0.4, limit });
75
+ // 2026-10-16: 通讯录卡片 — dids=<did1>,<did2> 只返回这些 provider 投递的机会 (手机号通讯录联系人)
76
+ const providerDids = typeof req.query.dids === 'string'
77
+ ? req.query.dids.split(',').map((d) => d.trim()).filter(Boolean)
78
+ : [];
79
+ const r = await scanOpportunities({
80
+ minScore: Number.isFinite(minScore) ? minScore : 0.4,
81
+ limit,
82
+ providerDids: providerDids.length ? providerDids : undefined,
83
+ });
76
84
  if (!r.ok)
77
85
  return json(res, 500, { ok: false, error: r.error });
78
86
  json(res, 200, { ok: true, count: r.results.length, minScore, opportunities: r.results });
@@ -1669,6 +1669,30 @@ export async function createWebServer(port = 3000, options = {}) {
1669
1669
  catch (err) {
1670
1670
  console.warn('[runs] 对账失败 (非致命):', err?.message);
1671
1671
  }
1672
+ // 2026-10-16 (leo: 双内核冗余 — 独立 kernel 守护进程): HA 双活 — 主进程以 primary 写心跳,
1673
+ // spawn 独立 kernel 守护进程 (standby), 主死 → daemon 接管; daemon 死 → 自动重启。
1674
+ // 环境变量门控: BOLLOON_HA=1 启用 (默认关 — 单节点常规运行不启动额外进程)。
1675
+ if (process.env.BOLLOON_HA === '1') {
1676
+ try {
1677
+ const { createHaSupervisor } = await import('../agents/ha-supervisor.js');
1678
+ const haSup = createHaSupervisor({
1679
+ log: (level, message) => {
1680
+ if (level === 'error')
1681
+ console.warn(message);
1682
+ else if (level === 'warn')
1683
+ console.warn(message);
1684
+ else if (process.env.BOLLOON_VERBOSE === '1')
1685
+ console.log(message);
1686
+ },
1687
+ });
1688
+ await haSup.start();
1689
+ globalThis.__bolloonHaSupervisor = haSup;
1690
+ console.log('[ha] 双活守护已启动 (primary 心跳 + standby kernel 守护进程)');
1691
+ }
1692
+ catch (err) {
1693
+ console.warn('[ha] 双活守护启动失败 (非致命):', err?.message);
1694
+ }
1695
+ }
1672
1696
  // 2026-09-16 (M2-B): ExecutionSupervisor —— 长期执行层。
1673
1697
  // 页面关掉、进程重启后由它把 Goal 继续下去; 跨进程排他靠 Goal lease (不是内存状态)。
1674
1698
  // Harness 管"这一段能不能安全执行", Supervisor 管"这个目标还要不要继续执行"。
@@ -5167,56 +5191,38 @@ fetchState();
5167
5191
  const judgmentHint = await buildJudgmentHint(channelForJudgment, channelId);
5168
5192
  if (judgmentHint)
5169
5193
  contextHint += judgmentHint;
5170
- // 2026-06-10: 注入 skills 列表 (本机 ~/.bolloon/skills/ 下所有 skills)
5171
- // 让 LLM 知道有哪些 skill 可用, 在回复中提示用户
5194
+ // 2026-06-10: 注入 skills 存在性提示 (2026-10-16 按需化: 删逐条列表 —
5195
+ // pi-sdk buildSkillsIndex 已注入前 40 技能名+描述, 这里只保留存在提示与触发语法)
5172
5196
  try {
5173
- const { loadSkillsFromPaths, defaultSkillPaths, describeSkill } = await import('../agents/skill-loader.js');
5197
+ const { loadSkillsFromPaths, defaultSkillPaths } = await import('../agents/skill-loader.js');
5174
5198
  const paths = defaultSkillPaths();
5175
5199
  const skills = await loadSkillsFromPaths(paths);
5176
5200
  if (skills.length > 0) {
5177
- contextHint += `[系统上下文] 本机已加载的 skills (${skills.length} 个, 你可以提示用户主动调用):\n`;
5178
- for (const s of skills.slice(0, 20)) { // 上限 20 避免 prompt 过长
5179
- const desc = (s.description || '').slice(0, 80);
5180
- contextHint += ` - /${s.name}${desc ? ' — ' + desc : ''}\n`;
5181
- }
5182
- contextHint += '调用语法: 用户说 "/技能名 ..." 或 你回复时建议 "/技能名 ..." 让用户主动触发.\n\n';
5201
+ contextHint += `[系统上下文] 本机已加载 ${skills.length} 个 skills (技能名+描述见上方技能库索引; 需要详情时用 list_skills / read_skill 按需读取). 调用语法: 用户说 "/技能名 ..." 或 你回复时建议 "/技能名 ..." 让用户主动触发.\n\n`;
5183
5202
  }
5184
5203
  }
5185
5204
  catch (err) {
5186
5205
  // 静默失败 — skills 不是核心, 加载失败不阻塞
5187
5206
  }
5188
- // 2026-06-10: 注入 human values 摘要 (最常用的 judgment / 价值偏好)
5189
- // 与 judgment 不同: values 是更宏观的"用户偏好", judgment 是针对具体决策的约束
5207
+ // 2026-06-10: human values 存在性提示 (2026-10-16 按需化: 删 8 条摘要列表 —
5208
+ // buildJudgmentHint 已注入绑定判断力 + 候选, 这里只保留"库里有多少"的指针)
5190
5209
  try {
5191
5210
  const { loadAllJudgments } = await import('../pi-ecosystem-judgment/human-value-store.js');
5192
5211
  const allJudgments = await loadAllJudgments().catch(() => []);
5193
- // 把所有 judgment 视作软参考 (跟 buildJudgmentHint 的 candidates 同理)
5194
5212
  if (Array.isArray(allJudgments) && allJudgments.length > 0) {
5195
- contextHint += `[系统上下文] 用户的核心价值倾向 (来自 ${allJudgments.length} 条历史 judgment, 软参考, 体现而非复述):\n`;
5196
- for (const j of allJudgments.slice(0, 8)) {
5197
- const decision = (j.decision || '').slice(0, 80);
5198
- contextHint += ` - ${decision}\n`;
5199
- }
5200
- contextHint += '\n';
5213
+ contextHint += `[系统上下文] 判断力库存 ${allJudgments.length} 条历史 judgment (绑定判断力见上方; 需要更多时用读取判断力的工具按需取, 软参考体现而非复述).\n\n`;
5201
5214
  }
5202
5215
  }
5203
5216
  catch (err) {
5204
5217
  // 静默失败
5205
5218
  }
5206
- // 2026-06-10: 注入 documents 列表 (本机 documents/ 目录的文档元数据)
5207
- // 让 LLM 知道有文档存在, 用户可主动要求读
5219
+ // 2026-06-10: documents 存在性提示 (2026-10-16 按需化: 删逐条元数据 —
5220
+ // 只留计数, 用户提到某文档时用读文档工具按需读取)
5208
5221
  try {
5209
5222
  const { documentStore } = await import('../documents/store.js');
5210
5223
  const docs = await documentStore.getReceivedDocuments(50).catch(() => []);
5211
5224
  if (Array.isArray(docs) && docs.length > 0) {
5212
- contextHint += `[系统上下文] 本机 documents (${docs.length} 篇, 用户可让你读):\n`;
5213
- for (const d of docs.slice(0, 10)) {
5214
- const name = d.fileName || d.id || '(未命名)';
5215
- const size = d.fileSize ? ` (${Math.round(d.fileSize / 1024)}KB)` : '';
5216
- const sender = d.fromNodeId ? ` [来自 ${d.fromNodeIdShort || d.fromNodeId.substring(0, 8)}…]` : '';
5217
- contextHint += ` - ${name}${size}${sender}\n`;
5218
- }
5219
- contextHint += '用户提到某文档时, 你可以调用读文档工具读取并总结.\n\n';
5225
+ contextHint += `[系统上下文] 本机 documents ${docs.length} 篇 (用户提到某文档时, 用读文档工具按需读取并总结).\n\n`;
5220
5226
  }
5221
5227
  }
5222
5228
  catch (err) {
@@ -5282,8 +5288,10 @@ fetchState();
5282
5288
  catch { /* 无 sessions 目录 */ }
5283
5289
  }
5284
5290
  if (summariesToRead.length > 0) {
5285
- const memBlock = summariesToRead.map(s => s.trim().slice(-1500)).join('\n\n---\n\n');
5286
- contextHint += `[系统上下文] 动态状态层 · chat-worksite (上次做到哪 — 本 channel 历史记忆, 来自 memory-compressor 摘要, 引用而非复述):\n${memBlock.slice(-2500)}\n\n`;
5291
+ // 2026-10-16 按需化: 历史记忆只注尾部 800 chars 作为"上次做到哪"的锚,
5292
+ // 完整摘要按需用 read 工具取 (记忆是引用而非复述, 全量 2500 常驻浪费)
5293
+ const memBlock = summariesToRead.map(s => s.trim().slice(-800)).join('\n\n---\n\n');
5294
+ contextHint += `[系统上下文] 动态状态层 · chat-worksite (上次做到哪 — 本 channel 历史记忆尾部摘录, 来自 memory-compressor 摘要, 引用而非复述):\n${memBlock.slice(-800)}\n\n`;
5287
5295
  }
5288
5296
  else {
5289
5297
  // 2026-08-08 (DID 目录读侧): 磁盘无摘要时, 回退读 DID 目录 memory 表 (跨设备同步来的记忆)
@@ -5294,11 +5302,11 @@ fetchState();
5294
5302
  const rows = catalogMemoryRows(cat, agentId);
5295
5303
  if (rows.length > 0) {
5296
5304
  const memBlock = rows.slice(0, 2)
5297
- .map(({ row }) => String(row.data.summary || '').slice(-1500))
5305
+ .map(({ row }) => String(row.data.summary || '').slice(-800))
5298
5306
  .filter(Boolean)
5299
5307
  .join('\n\n---\n\n');
5300
5308
  if (memBlock) {
5301
- contextHint += `[系统上下文] 动态状态层 · chat-worksite (DID 目录同步记忆, 引用而非复述):\n${memBlock.slice(-2500)}\n\n`;
5309
+ contextHint += `[系统上下文] 动态状态层 · chat-worksite (DID 目录同步记忆尾部摘录, 引用而非复述):\n${memBlock.slice(-800)}\n\n`;
5302
5310
  }
5303
5311
  }
5304
5312
  }
@@ -5339,24 +5347,27 @@ fetchState();
5339
5347
  }
5340
5348
  const linkedIds = channelForJudgment?.linkedDocumentIds;
5341
5349
  if (Array.isArray(linkedIds) && linkedIds.length > 0) {
5350
+ // 2026-10-16 按需化: 关联文档不再每轮全文注入 (10篇×1.5KB=15KB 常驻 prompt),
5351
+ // 只列文件名指针, 用户提到某文档时用读文档工具按需读取。
5352
+ // 关联文档本身不常在单轮对话中被用到, 全量注入是浪费。
5342
5353
  try {
5343
5354
  const { documentStore } = await import('../documents/store.js');
5344
- contextHint += `[系统上下文] 此 channel 关联了 ${linkedIds.length} 篇文档 (已自动加载内容, 你应基于它们回答):\n`;
5345
- let loaded = 0;
5355
+ const names = [];
5346
5356
  for (const docId of linkedIds.slice(0, 10)) {
5347
5357
  const doc = await documentStore.readDocument(docId).catch(() => null);
5348
5358
  if (!doc)
5349
5359
  continue;
5350
5360
  const name = doc.metadata?.fileName || docId;
5351
- const content = (doc.content || '').slice(0, 1500); // 单篇 1.5KB 上限, 总 prompt 防爆
5352
- contextHint += `\n--- 文档: ${name} ---\n${content}\n--- 文档结束 ---\n`;
5353
- loaded++;
5361
+ names.push(name);
5362
+ if (names.length >= 5)
5363
+ break; // 指针只列前 5, 其余计数
5354
5364
  }
5355
- if (loaded === 0) {
5356
- contextHint += `(但加载失败, 文档可能已被删除)\n\n`;
5365
+ if (names.length > 0) {
5366
+ const more = linkedIds.length > names.length ? ` (共 ${linkedIds.length} 篇)` : '';
5367
+ contextHint += `[系统上下文] 此 channel 关联了 ${linkedIds.length} 篇文档 — ${names.join('、')}${more}. 用户提到某篇时, 用读文档工具按需读取全文再回答.\n\n`;
5357
5368
  }
5358
5369
  else {
5359
- contextHint += '\n';
5370
+ contextHint += `[系统上下文] 此 channel 关联了 ${linkedIds.length} 篇文档 (内容读取失败, 用读文档工具按需读取).\n\n`;
5360
5371
  }
5361
5372
  }
5362
5373
  catch (err) {
package/dist/ios/sw.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * 现在: 代码类资源 (html/js/css/json) 一律 **network-first** (拿不到网才回退缓存),
7
7
  * 缓存名带版本号, activate 时清掉所有旧缓存; 图标等静态资源仍 cache-first。
8
8
  */
9
- const CACHE = 'bolloon-mobile-v0.6.5';
9
+ const CACHE = 'bolloon-mobile-v0.6.6';
10
10
  const SHELL = [
11
11
  './mobile.html',
12
12
  './mobile.css',