@bolloon/bolloon-agent 0.5.0 → 0.5.2

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 (121) hide show
  1. package/README.md +39 -0
  2. package/dist/agents/agent-lang.js +101 -0
  3. package/dist/agents/agent-manifest-protocol.js +4 -0
  4. package/dist/agents/execution-supervisor.js +87 -5
  5. package/dist/agents/gateway-group.js +44 -3
  6. package/dist/agents/model-policy.js +651 -0
  7. package/dist/agents/pi-sdk-tools.js +84 -33
  8. package/dist/agents/pi-sdk.js +184 -31
  9. package/dist/agents/run-store.js +53 -0
  10. package/dist/agents/task/task-runner.js +8 -1
  11. package/dist/agents/workflow-pivot-loop.js +22 -13
  12. package/dist/cli/clipboard.js +65 -0
  13. package/dist/cli/ink-app.js +145 -17
  14. package/dist/cli/log-gate.js +386 -0
  15. package/dist/cli/mention-data.js +2 -0
  16. package/dist/cli/model-selector.js +538 -0
  17. package/dist/cli/reply-hygiene.js +54 -0
  18. package/dist/cli/setup-wizard.js +686 -69
  19. package/dist/cli/startup-notice.js +160 -0
  20. package/dist/cli/stores.js +22 -1
  21. package/dist/cli/theme.js +49 -0
  22. package/dist/cli/tui-select.js +970 -0
  23. package/dist/cli-entry.js +125 -18
  24. package/dist/efficode/compress.js +78 -0
  25. package/dist/efficode/crc32.js +38 -0
  26. package/dist/efficode/did.js +66 -0
  27. package/dist/efficode/index.js +24 -0
  28. package/dist/efficode/lz77.js +170 -0
  29. package/dist/efficode/negotiate.js +187 -0
  30. package/dist/efficode/ops.js +87 -0
  31. package/dist/efficode/packet.js +271 -0
  32. package/dist/efficode/types.js +45 -0
  33. package/dist/electron-build/agents/agent-manifest-protocol.js +4 -0
  34. package/dist/electron-build/agents/agent-manifest-protocol.js.map +1 -1
  35. package/dist/electron-build/agents/execution-supervisor.js +86 -4
  36. package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
  37. package/dist/electron-build/agents/goal-store.js.map +1 -1
  38. package/dist/electron-build/agents/model-policy.js +706 -0
  39. package/dist/electron-build/agents/model-policy.js.map +1 -0
  40. package/dist/electron-build/agents/pi-sdk-tools.js +84 -33
  41. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  42. package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
  43. package/dist/electron-build/agents/pi-sdk.js +185 -31
  44. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  45. package/dist/electron-build/agents/run-store.js +55 -1
  46. package/dist/electron-build/agents/run-store.js.map +1 -1
  47. package/dist/electron-build/agents/task/task-runner.js +8 -1
  48. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  49. package/dist/electron-build/agents/workflow-pivot-loop.js +22 -13
  50. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  51. package/dist/electron-build/llm/config-store.js +146 -6
  52. package/dist/electron-build/llm/config-store.js.map +1 -1
  53. package/dist/electron-build/llm/connection-probe.js +764 -0
  54. package/dist/electron-build/llm/connection-probe.js.map +1 -0
  55. package/dist/electron-build/llm/custom-provider-store.js +272 -0
  56. package/dist/electron-build/llm/custom-provider-store.js.map +1 -0
  57. package/dist/electron-build/llm/data/provider-catalog-baked.js +23 -0
  58. package/dist/electron-build/llm/data/provider-catalog-baked.js.map +1 -0
  59. package/dist/electron-build/llm/model-catalog.js +778 -0
  60. package/dist/electron-build/llm/model-catalog.js.map +1 -0
  61. package/dist/electron-build/llm/model-discovery.js +1202 -0
  62. package/dist/electron-build/llm/model-discovery.js.map +1 -0
  63. package/dist/electron-build/llm/model-selection.js +1315 -0
  64. package/dist/electron-build/llm/model-selection.js.map +1 -0
  65. package/dist/electron-build/llm/pi-ai.js +751 -55
  66. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  67. package/dist/electron-build/llm/provider-catalog.js +969 -0
  68. package/dist/electron-build/llm/provider-catalog.js.map +1 -0
  69. package/dist/electron-build/llm/provider-registry.js +950 -0
  70. package/dist/electron-build/llm/provider-registry.js.map +1 -0
  71. package/dist/electron-build/llm/tool-name.js +266 -0
  72. package/dist/electron-build/llm/tool-name.js.map +1 -0
  73. package/dist/electron-build/utils/dual-source-facts.js +236 -0
  74. package/dist/electron-build/utils/dual-source-facts.js.map +1 -0
  75. package/dist/electron-build/utils/dual-source.js +32 -156
  76. package/dist/electron-build/utils/dual-source.js.map +1 -1
  77. package/dist/electron-build/utils/update-manager.js +22 -72
  78. package/dist/electron-build/utils/update-manager.js.map +1 -1
  79. package/dist/electron-build/utils/update-state.js +2 -19
  80. package/dist/electron-build/utils/update-state.js.map +1 -1
  81. package/dist/electron-build/utils/version-identity.js +193 -0
  82. package/dist/electron-build/utils/version-identity.js.map +1 -0
  83. package/dist/electron-build/utils/version-info.js +47 -101
  84. package/dist/electron-build/utils/version-info.js.map +1 -1
  85. package/dist/index.js +282 -96
  86. package/dist/ios/bolloon-web.json +10 -0
  87. package/dist/ios/mobile-contacts.js +44 -11
  88. package/dist/ios/mobile-core.js +1723 -84
  89. package/dist/ios/mobile-tasks.js +339 -0
  90. package/dist/ios/mobile-update.js +1442 -0
  91. package/dist/ios/mobile.js +308 -35
  92. package/dist/ios/routes-llm-config.js +344 -52
  93. package/dist/ios/routes-mobile-tasks.js +353 -0
  94. package/dist/ios/server.js +163 -4
  95. package/dist/ios/sw.js +1 -1
  96. package/dist/llm/config-store.js +146 -6
  97. package/dist/llm/connection-probe.js +744 -0
  98. package/dist/llm/custom-provider-store.js +224 -0
  99. package/dist/llm/data/provider-catalog-baked.js +19 -0
  100. package/dist/llm/model-catalog.js +717 -0
  101. package/dist/llm/model-discovery.js +1137 -0
  102. package/dist/llm/model-selection.js +1241 -0
  103. package/dist/llm/pi-ai.js +732 -54
  104. package/dist/llm/provider-catalog.js +898 -0
  105. package/dist/llm/provider-registry.js +917 -0
  106. package/dist/llm/tool-name.js +220 -0
  107. package/dist/setup/onboard.js +34 -8
  108. package/dist/utils/dual-source-facts.js +223 -0
  109. package/dist/utils/dual-source.js +12 -141
  110. package/dist/utils/update-manager.js +16 -59
  111. package/dist/utils/update-state.js +6 -18
  112. package/dist/utils/version-identity.js +176 -0
  113. package/dist/utils/version-info.js +9 -78
  114. package/dist/web/bolloon-web.json +10 -0
  115. package/dist/web/mobile-core.js +1721 -84
  116. package/dist/web/mobile-update.js +1442 -0
  117. package/dist/web/mobile.js +372 -35
  118. package/dist/web/routes-llm-config.js +344 -52
  119. package/dist/web/sw.js +1 -1
  120. package/package.json +1 -1
  121. package/scripts/build-web.ts +36 -0
@@ -225,6 +225,68 @@
225
225
  }
226
226
  }
227
227
 
228
+ // ── Efficode 条目 (内置参考实现: 仓内 src/efficode/**, 不靠"从电脑端同步") ──────
229
+ // 纪律: 这段**不用 innerHTML 拼任何数据** (纯 createElement + textContent) ·
230
+ // 双语走 data-zh/data-en (切语言时外面那段 applyLang 会一起改文字节点) ·
231
+ // role=button + tabindex + aria-label + Enter/Space 可达 · 无外部依赖。
232
+ const EFFICODE_SPEC_PATH = 'docs/wiki/efficode.md';
233
+ const EFFICODE_IMPL_PATH = 'src/efficode/** · src/agents/agent-lang.ts';
234
+ const EFFICODE_GATE = 'scripts/verify-efficode.ts (55/0)';
235
+ function effiLangNow() {
236
+ try { return localStorage.getItem('bolloon_lang') === 'en' ? 'en' : 'zh'; } catch (e) { return 'zh'; }
237
+ }
238
+ function buildEfficodeEntry() {
239
+ const el = document.createElement('div');
240
+ el.className = 'conv-item';
241
+ el.id = 'item-efficode';
242
+ el.setAttribute('role', 'button');
243
+ el.setAttribute('tabindex', '0');
244
+ el.setAttribute('aria-label', 'Efficode 规范页 (中文): ' + EFFICODE_SPEC_PATH);
245
+ const avatar = document.createElement('div');
246
+ avatar.className = 'conv-avatar';
247
+ avatar.textContent = 'E';
248
+ const body = document.createElement('div');
249
+ body.className = 'conv-body';
250
+ const name = document.createElement('div');
251
+ name.className = 'conv-name';
252
+ name.textContent = '/efficode';
253
+ const desc = document.createElement('div');
254
+ desc.className = 'conv-preview';
255
+ desc.setAttribute('data-zh', '智能体间的符号化交流语言 · 参考实现 (签名/声波未实现) — 点开看规范页 docs/wiki/efficode.md');
256
+ desc.setAttribute('data-en', 'Symbolic agent-to-agent language · reference impl (signing/audio not implemented) — tap for docs/wiki/efficode.md');
257
+ desc.textContent = effiLangNow() === 'en' ? desc.getAttribute('data-en') : desc.getAttribute('data-zh');
258
+ body.appendChild(name);
259
+ body.appendChild(desc);
260
+ el.appendChild(avatar);
261
+ el.appendChild(body);
262
+ const open = () => alert(
263
+ '/efficode\n\n' +
264
+ (effiLangNow() === 'en'
265
+ ? 'Symbolic binary language between agents (Efficient + Code). Reference implementation in this repo; negotiation falls back to natural language unless both sides declare efficode. Signing / ECC session keys / sound-wave mode are NOT implemented.\n\n'
266
+ : '智能体之间的符号化二进制交流语言 (Efficient + Code)。本仓有参考实现; 只有双方都声明 efficode 才用, 否则明确回落自然语言。签名验证 / ECC 临时会话密钥 / 声波模式尚未实现。\n\n') +
267
+ '规范页: ' + EFFICODE_SPEC_PATH + '\n' +
268
+ '实现: ' + EFFICODE_IMPL_PATH + '\n' +
269
+ '验收门: ' + EFFICODE_GATE
270
+ );
271
+ el.addEventListener('click', open);
272
+ el.addEventListener('keydown', (ev) => {
273
+ if (ev.key === 'Enter' || ev.key === ' ' || ev.key === 'Spacebar') { ev.preventDefault(); open(); }
274
+ });
275
+ return el;
276
+ }
277
+
278
+ /** 把内置 Efficode 条目追加到 Skills 页 (空态 / 有列表都显示 —— 它是仓内参考实现) */
279
+ function appendEfficodeBlock(body) {
280
+ if (!body || body.querySelector('#item-efficode')) return;
281
+ const cap = document.createElement('div');
282
+ cap.style.cssText = 'font-size:11px;color:var(--text-muted);margin:14px 0 6px';
283
+ cap.setAttribute('data-zh', '内置 (随客户端发布, 不来自电脑端同步)');
284
+ cap.setAttribute('data-en', 'Built in (ships with the client, not synced from desktop)');
285
+ cap.textContent = effiLangNow() === 'en' ? cap.getAttribute('data-en') : cap.getAttribute('data-zh');
286
+ body.appendChild(cap);
287
+ body.appendChild(buildEfficodeEntry());
288
+ }
289
+
228
290
  async function openSkillsPage() {
229
291
  if ($('#skills-page')) return;
230
292
  const page = mkOverlayPage('skills-page', 'Skills', '<div style="font-size:12px;color:var(--text-muted)">读取中…</div>');
@@ -241,6 +303,7 @@
241
303
  body.innerHTML = `<div style="padding:18px 8px;text-align:center;color:var(--text-muted);line-height:1.8">
242
304
  还没有技能列表<br><span style="font-size:12px">电脑端 ~/.bolloon/skills/ 下的 skills,会在「从电脑端同步」后出现在这里</span>
243
305
  ${err ? `<div style="font-size:11px;margin-top:8px">(读取失败:${escapeHtml(err)})</div>` : ''}</div>`;
306
+ appendEfficodeBlock(body);
244
307
  return;
245
308
  }
246
309
  body.innerHTML = skills.map((s, i) => `<div class="conv-item" data-sk="${i}">
@@ -252,6 +315,7 @@
252
315
  const s = skills[Number(el.dataset.sk)] || {};
253
316
  alert(`/${s.name || ''}\n\n${s.description || '(无说明)'}`);
254
317
  }));
318
+ appendEfficodeBlock(body);
255
319
  };
256
320
  await render();
257
321
  const btn = page.querySelector('#skills-sync');
@@ -1697,6 +1761,110 @@
1697
1761
  return r;
1698
1762
  }
1699
1763
 
1764
+ /**
1765
+ * 「App 更新 (web 资源层)」页 (2026-09-26, update-protocol §13)
1766
+ * 看得见的: 当前装的是哪个源 / 哪个版本·sha / 能切回哪个源 —— 与桌面 `bolloon update --status` 同一口径。
1767
+ * 做得出的: 检查 → (智能体可自动跑到"准备并验证") → 人工确认切换 → 重载; 一键回滚。
1768
+ * 说不出的不说: 原生壳层不能在这里自更 (iOS 走商店/TestFlight, Android 侧载需允许未知来源)。
1769
+ */
1770
+ async function openUpdatePage() {
1771
+ const page = document.createElement('div');
1772
+ page.className = 'chat-page';
1773
+ page.id = 'update-page';
1774
+ page.innerHTML = `
1775
+ <div class="chat-topbar">
1776
+ <button class="icon-btn" id="update-back">←</button>
1777
+ <div style="flex:1;font-weight:600">App 更新</div>
1778
+ <button class="icon-btn" id="update-check" title="检查更新">↻</button>
1779
+ </div>
1780
+ <div style="padding:12px;overflow:auto">
1781
+ <pre id="update-identity" style="white-space:pre-wrap;word-break:break-all;font-size:12px;line-height:1.6;background:var(--bg-2,#0001);padding:10px;border-radius:10px;margin:0 0 10px">读取中…</pre>
1782
+ <div style="display:flex;gap:8px;margin-bottom:10px">
1783
+ <button class="conv-item" id="update-ch-stable" style="flex:1;justify-content:center">stable (npm)</button>
1784
+ <button class="conv-item" id="update-ch-dev" style="flex:1;justify-content:center">dev (GitHub master)</button>
1785
+ </div>
1786
+ <div class="conv-item" id="update-apply"><span class="list-icon">${ICONS.chip}</span><span style="flex:1;min-width:0"><span style="display:block">安装更新(需要你确认)</span><span class="conv-preview" style="display:block" id="update-apply-sub">先点右上角 ↻ 检查</span></span><span class="list-arrow">›</span></div>
1787
+ <div class="conv-item" id="update-rollback"><span class="list-icon">${ICONS.trash}</span><span style="flex:1;min-width:0"><span style="display:block">回滚到上一份</span><span class="conv-preview" style="display:block">装坏了就换回上一次能用的资源</span></span><span class="list-arrow">›</span></div>
1788
+ <div class="conv-item" id="update-reload"><span class="list-icon">${ICONS.themeAuto}</span><span>重载界面让更新生效</span></div>
1789
+ <div id="update-ceiling" style="font-size:12px;line-height:1.6;color:var(--fg-2,#888);margin:10px 0"></div>
1790
+ <pre id="update-log" style="white-space:pre-wrap;word-break:break-all;font-size:12px;line-height:1.6;margin:0"></pre>
1791
+ </div>`;
1792
+ document.body.appendChild(page);
1793
+
1794
+ const identityEl = page.querySelector('#update-identity');
1795
+ const logEl = page.querySelector('#update-log');
1796
+ const applySub = page.querySelector('#update-apply-sub');
1797
+ const ceilingEl = page.querySelector('#update-ceiling');
1798
+ let lastCheck = null;
1799
+
1800
+ const log = (line) => { logEl.textContent += `${line}\n`; logEl.scrollTop = logEl.scrollHeight; };
1801
+ const UP = () => (window.BolloonCore && window.BolloonCore.update) || null;
1802
+ const setIdentity = async () => {
1803
+ const up = UP();
1804
+ if (!up) { identityEl.textContent = '内核未就绪'; return; }
1805
+ const voc = up.vocabulary();
1806
+ ceilingEl.textContent = voc.nativeCeiling;
1807
+ const st = await up.state();
1808
+ const applied = st.installedChannel ? `${st.installedChannel}${st.installedDevSha ? ` @ ${st.installedDevSha}` : ''}` : '(还没用这个功能装过)';
1809
+ identityEl.textContent = [
1810
+ `当前装的: ${applied}`,
1811
+ `通道偏好: ${up.channel()}`,
1812
+ `能切回: ${st.switchableTo ? `${st.switchableTo.channel} (${st.switchableTo.source}${st.switchableTo.target ? ` @ ${st.switchableTo.target}` : ''})` : '—'}`,
1813
+ `上次来源: ${st.installedFrom ? `${st.installedFrom.source}${st.installedFrom.digest ? ` · ${st.installedFrom.digest}` : ''}` : '—'}`,
1814
+ `更新层: web 资源层(界面与逻辑:入群 / 发任务公告 / 看飞轮进度 / 授权签名)`,
1815
+ `会生效吗: ${up.nativeWired() ? '会(原生壳已指向可写目录)' : '不会 —— 原生壳还没指向可写目录,装了也不生效,所以不会切换'}`,
1816
+ ].join('\n');
1817
+ };
1818
+ const runCheck = async () => {
1819
+ const up = UP();
1820
+ if (!up) { log('内核未就绪'); return; }
1821
+ log('· 检查两源(npm + GitHub)…');
1822
+ const r = await up.report({});
1823
+ lastCheck = r.result;
1824
+ identityEl.textContent = r.lines.join('\n');
1825
+ applySub.textContent = r.result.status === 'update_available'
1826
+ ? `可装: ${r.result.targetIdentity || r.result.latestVersion}` : `结论: ${r.result.status}`;
1827
+ log(`· 结论: ${r.result.status}${r.result.reason ? ` — ${r.result.reason}` : ''}`);
1828
+ if (r.result.warnings && r.result.warnings.length) r.result.warnings.forEach((w) => log(` ⚠ ${w}`));
1829
+ await setIdentity();
1830
+ };
1831
+
1832
+ page.querySelector('#update-back').addEventListener('click', () => { page.remove(); void openSettings(); });
1833
+ page.querySelector('#update-check').addEventListener('click', () => { logEl.textContent = ''; void runCheck(); });
1834
+ page.querySelector('#update-ch-stable').addEventListener('click', async () => { const r = await UP().setChannel('stable'); log(`· ${r.detail}`); await setIdentity(); });
1835
+ page.querySelector('#update-ch-dev').addEventListener('click', async () => { const r = await UP().setChannel('dev'); log(`· ${r.detail}`); await setIdentity(); });
1836
+ page.querySelector('#update-apply').addEventListener('click', async () => {
1837
+ const up = UP();
1838
+ if (!up) return;
1839
+ const target = lastCheck && (lastCheck.targetIdentity || lastCheck.latestVersion);
1840
+ if (!lastCheck || lastCheck.status !== 'update_available') { log('· 先检查更新(右上角 ↻):现在没有可装的目标,或源不可达/交叉校验不一致 —— 拒绝安装。'); return; }
1841
+ // 人在环: 说清要换的是哪一层, 再动手
1842
+ if (!confirm(`要装 ${target} 吗?\n\n换的是「web 资源层」(界面与逻辑)。装完需要重载界面才生效;装坏了可以一键回滚。\n原生壳层(二进制)不在这里更新。`)) return;
1843
+ log('· 准备并验证 …');
1844
+ const prep = await up.prepare({ onStage: (s, d) => log(` [${s}] ${d}`) });
1845
+ if (!prep.ok) { log(`· 准备/验证失败: ${prep.status} — ${prep.reason}`); return; }
1846
+ log(`· 已验证 ${prep.to}(${prep.fileCount} 个文件)—— 现在切换`);
1847
+ const out = await up.apply({ confirm: true, reuseStaging: true, onStage: (s, d) => log(` [${s}] ${d}`) });
1848
+ log(out.ok ? `· 成功: ${out.from} → ${out.to}(重载界面后生效)` : `· 未生效 (${out.stage}): ${out.reason}`);
1849
+ applySub.textContent = out.ok ? '已装好 · 点「重载界面」生效' : `未生效: ${out.status}`;
1850
+ await setIdentity();
1851
+ });
1852
+ page.querySelector('#update-rollback').addEventListener('click', async () => {
1853
+ const up = UP();
1854
+ if (!up) return;
1855
+ if (!confirm('换回上一份能用的 web 资源吗?(当前这一份会被替换掉)')) return;
1856
+ const out = await up.rollback({ confirm: true, onStage: (s, d) => log(` [${s}] ${d}`) });
1857
+ log(out.ok ? `· 已回滚到 ${out.to}` : `· 回滚未执行 (${out.stage}): ${out.reason}`);
1858
+ await setIdentity();
1859
+ });
1860
+ page.querySelector('#update-reload').addEventListener('click', async () => {
1861
+ const r = await UP().reload();
1862
+ log(`· ${r.detail}(方式: ${r.how})`);
1863
+ });
1864
+
1865
+ await setIdentity();
1866
+ }
1867
+
1700
1868
  function openSettings() {
1701
1869
  const page = document.createElement('div');
1702
1870
  page.className = 'chat-page';
@@ -1709,6 +1877,7 @@
1709
1877
  <div style="padding:12px">
1710
1878
  <div class="conv-item" id="api-config-item"><span class="list-icon">${ICONS.chip}</span><span>API 配置</span><span class="list-arrow">›</span></div>
1711
1879
  <div class="conv-item" id="theme-toggle"><span class="list-icon" id="theme-icon">${ICONS.themeAuto}</span><span id="theme-text">跟随系统</span></div>
1880
+ <div class="conv-item" id="settings-update"><span class="list-icon">${ICONS.themeAuto}</span><span style="flex:1;min-width:0"><span style="display:block">App 更新(web 资源层)</span><span class="conv-preview" style="display:block" id="update-preview">读取中…</span></span><span class="list-arrow">›</span></div>
1712
1881
  <div class="conv-item" id="settings-data"><span class="list-icon">${ICONS.chip}</span><span style="flex:1;min-width:0"><span style="display:block">本机数据</span><span class="conv-preview" style="display:block">读取中…</span></span><span class="list-arrow">›</span></div>
1713
1882
  <div class="conv-item" id="settings-desktop"><span class="list-icon">${ICONS.globe}</span><span>电脑端同步</span><span class="list-arrow">›</span></div>
1714
1883
  <div class="conv-item" id="settings-chain"><span class="list-icon">${ICONS.chip}</span><span>链上配置 (RPC/网络)</span><span class="list-arrow">›</span></div>
@@ -1725,6 +1894,22 @@
1725
1894
  applyTheme(resolveThemePref(), false);
1726
1895
  $('#settings-back').addEventListener('click', () => page.remove());
1727
1896
  $('#api-config-item').addEventListener('click', openApiConfig);
1897
+ // App 更新: 预览里就说清"现在装的是哪个源 / 哪个版本·sha / 能切回谁"(= 桌面 update --status 同一口径)
1898
+ $('#settings-update').addEventListener('click', () => { page.remove(); void openUpdatePage(); });
1899
+ void (async () => {
1900
+ const el = page.querySelector('#update-preview');
1901
+ if (!el) return;
1902
+ try {
1903
+ const up = window.BolloonCore && window.BolloonCore.update;
1904
+ if (!up) { el.textContent = '内核未就绪'; return; }
1905
+ const st = await up.state();
1906
+ const applied = st.installedChannel
1907
+ ? `${st.installedChannel}${st.installedDevSha ? ` @ ${st.installedDevSha}` : ''}`
1908
+ : '内置资源';
1909
+ const back = st.switchableTo ? ` · 能切回 ${st.switchableTo.channel}` : '';
1910
+ el.textContent = `当前 ${applied}${back} · 通道 ${up.channel()}${up.nativeWired() ? '' : ' · 原生壳未接通'}`;
1911
+ } catch (e) { el.textContent = '读取失败: ' + ((e && e.message) || e); }
1912
+ })();
1728
1913
  $('#theme-toggle').addEventListener('click', () => {
1729
1914
  const next = currentTheme === 'auto' ? 'light' : (currentTheme === 'light' ? 'dark' : 'auto');
1730
1915
  applyTheme(next, true);
@@ -1827,9 +2012,28 @@
1827
2012
  $('#settings-did').addEventListener('click', () => { api.get('/api/auth/status').then((s) => alert('DID: ' + (s.did || '未生成'))); });
1828
2013
  }
1829
2014
 
1830
- // === API 配置 (LLM 供应商) ===
1831
- // 注意: 手机端 RemoteLlm 走 OpenAI 兼容协议 (baseUrl + /chat/completions) —— 新增 provider 必须是
1832
- // OpenAI 兼容端点 (gemini 用 /v1beta/openai, 智谱 v4 / dashscope compatible-mode 都兼容)。
2015
+ // === 模型配置 (供应商 / 模型 / 地址) ===
2016
+ //
2017
+ // 2026-09-26 (模型链路升级 P6 的手机侧): 这一页**只显示与触发**, 自己不算配置 ——
2018
+ // 唯一事实来自桌面端的模型端点 (与 CLI `/model` / Web 桌面页**同一份**):
2019
+ //
2020
+ // 当前真实生效 GET /api/models/providers → body.effective
2021
+ // (provider · model · baseUrl · scope · configHash)
2022
+ // 目录 + 来源 GET /api/models/options?provider=<id> → body.catalog.origin
2023
+ // (live | cached | curated | custom | unavailable)
2024
+ // 刷新发现 POST /api/models/discover {action:'refresh', provider}
2025
+ // 手输模型 POST /api/models/discover {action:'admit', provider, model}
2026
+ //
2027
+ // 纪律: **不回读 /api/llm-config 自己拼 activeProvider/providers 来显示"生效配置"** —— 那是
2028
+ // 第二套读配置逻辑, 与服务端早晚漂移。下面 `localCfg` 只喂"编辑表单"和"保存"(写路径),
2029
+ // 展示层一个字都不从它取; 拿不到桌面端就如实说"显示不了", 不猜不编。
2030
+ //
2031
+ // 注意: 手机端 RemoteLlm 走 OpenAI 兼容协议 (baseUrl + /chat/completions) —— 自定义 provider
2032
+ // 必须是 OpenAI 兼容端点 (gemini 用 /v1beta/openai, 智谱 v4 / dashscope compatible-mode 都兼容)。
2033
+ const ORIGIN_LABEL = {
2034
+ live: '实时发现 (live)', cached: '缓存发现 (cached)', curated: '内置清单 (curated)',
2035
+ custom: '自定义 (custom)', unavailable: '不可用 (unavailable)',
2036
+ };
1833
2037
  const LLM_PROVIDERS = [
1834
2038
  { id: 'deepseek', label: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1', model: 'deepseek-chat' },
1835
2039
  { id: 'openai', label: 'OpenAI', baseUrl: 'https://api.openai.com/v1', model: 'gpt-4o-mini' },
@@ -1847,43 +2051,155 @@
1847
2051
  { id: 'custom', label: '自定义', baseUrl: '', model: '' },
1848
2052
  ];
1849
2053
  const LLM_BY_ID = LLM_PROVIDERS.reduce((m, p) => (m[p.id] = p, m), {});
2054
+
2055
+ /** configHash 只显示前 8 位 (完整值在桌面端 / Run 快照里, 手机屏不需要 64 位) */
2056
+ function shortConfigHash(h) {
2057
+ const s = String(h == null ? '' : h);
2058
+ return s ? s.slice(0, 8) : '—';
2059
+ }
2060
+
2061
+ /** 目录来源标记 → 人话 (认不出来就原样回显, 不编) */
2062
+ function originLabel(origin) {
2063
+ const k = String(origin || '');
2064
+ return ORIGIN_LABEL[k] || (k ? k : '—');
2065
+ }
2066
+
2067
+ /** 电脑端基址 (设置里填; 手机自足模式拿不到就如实说, 不假装成功) */
2068
+ async function modelApiGet(path) {
2069
+ const base = await desktopBaseUrl();
2070
+ if (!base) return { ok: false, error: '未接电脑端 —— 设置 → 电脑端同步 里填桌面地址后可看' };
2071
+ const r = await desktopFetch(path);
2072
+ if (r === null) return { ok: false, error: '电脑端不可达或返回非 200' };
2073
+ return { ok: true, data: r };
2074
+ }
2075
+ async function modelApiPost(path, body) {
2076
+ const base = await desktopBaseUrl();
2077
+ if (!base) return { ok: false, error: '未接电脑端 —— 设置 → 电脑端同步 里填桌面地址后可看' };
2078
+ return await desktopPostRaw(path, body); // 保留后端错误原文
2079
+ }
2080
+
1850
2081
  async function openApiConfig() {
1851
- let cfg;
1852
- try { cfg = await api.get('/api/llm-config'); } catch { cfg = null; }
1853
- if (!cfg || !cfg.providers) cfg = { activeProvider: 'deepseek', providers: {}, updatedAt: Date.now() };
1854
2082
  const page = document.createElement('div');
1855
2083
  page.className = 'chat-page';
1856
2084
  page.id = 'api-config-page';
1857
- const provider = cfg.activeProvider || 'deepseek';
1858
- const pc = cfg.providers?.[provider] || {};
2085
+ // 编辑表单的本地草稿 (写路径) —— 展示层不读它
2086
+ let localCfg = null;
2087
+ try { localCfg = await api.get('/api/llm-config'); } catch (e) { localCfg = null; }
2088
+ if (!localCfg || !localCfg.providers) localCfg = { activeProvider: 'deepseek', providers: {}, updatedAt: Date.now() };
2089
+ const _in = 'width:100%;padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-hover);color:var(--text);box-sizing:border-box';
2090
+ const _btn = 'padding:10px 14px;border:1px solid var(--border);border-radius:8px;background:var(--bg-hover);color:var(--text)';
1859
2091
  page.innerHTML = `
1860
2092
  <div class="chat-topbar">
1861
2093
  <button class="icon-btn" id="api-config-back">←</button>
1862
- <div style="flex:1;font-weight:600">API 配置</div>
2094
+ <div style="flex:1;font-weight:600">模型配置</div>
1863
2095
  </div>
1864
- <div style="padding:12px;display:flex;flex-direction:column;gap:12px">
1865
- <label style="font-size:13px;color:var(--text-secondary)">供应商 <span id="api-provider-count" style="opacity:.55"></span></label>
1866
- <div class="provider-chips" id="api-provider-chips">
1867
- ${LLM_PROVIDERS.map((p) => `<button type="button" class="provider-chip${p.id === provider ? ' active' : ''}" data-provider="${p.id}">${p.label}</button>`).join('')}
2096
+ <div style="padding:12px;display:flex;flex-direction:column;gap:12px;overflow:auto">
2097
+ <div class="section-label">当前真实生效 (电脑端 /api/models/providers)</div>
2098
+ <div id="api-eff-card" style="padding:12px;border:1px solid var(--border);border-radius:10px;background:var(--bg-card);display:flex;flex-direction:column;gap:6px">
2099
+ <div style="font-size:13px">供应商: <b id="api-eff-provider">读取中…</b></div>
2100
+ <div style="font-size:13px">模型: <b id="api-eff-model">—</b></div>
2101
+ <div style="font-size:12px;color:var(--text-secondary);word-break:break-all">地址: <span id="api-eff-baseurl">—</span></div>
2102
+ <div style="font-size:12px;color:var(--text-secondary)">来源(scope): <span id="api-eff-scope">—</span> · 配置指纹: <span id="api-eff-hash">—</span></div>
2103
+ <div id="api-eff-note" style="font-size:12px;color:var(--text-muted)"></div>
2104
+ </div>
2105
+
2106
+ <div class="section-label">模型目录 (电脑端 /api/models/options)</div>
2107
+ <div id="api-catalog-origin" style="font-size:13px">—</div>
2108
+ <div id="api-catalog-head" style="font-size:12px;color:var(--text-secondary)"></div>
2109
+ <div id="api-catalog-list" style="font-size:12px;color:var(--text-secondary);max-height:170px;overflow:auto;word-break:break-all"></div>
2110
+ <div id="api-catalog-note" style="font-size:12px;color:var(--text-muted)"></div>
2111
+ <button id="api-discover-refresh" style="${_btn}">刷新发现</button>
2112
+ <div style="display:flex;gap:8px">
2113
+ <input id="api-manual-model" placeholder="手输模型名 (目录里没有的)" style="flex:1;${_in}">
2114
+ <button id="api-manual-admit" style="${_btn}">加入目录</button>
1868
2115
  </div>
2116
+ <div id="api-discover-result" style="font-size:12px;color:var(--text-secondary)"></div>
2117
+
2118
+ <div class="section-label">供应商 / 端点 / 凭据 (本机保存) <span id="api-provider-count" style="opacity:.55"></span></div>
2119
+ <div class="provider-chips" id="api-provider-chips"></div>
1869
2120
  <div id="api-hint" style="font-size:12px;color:var(--text-muted)"></div>
1870
- <label style="font-size:13px;color:var(--text-secondary)">Base URL</label>
1871
- <input id="api-baseurl" placeholder="https://api.xxx.com/v1" value="${escapeHtml(pc.baseUrl || '')}" style="padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-hover);color:var(--text)">
1872
- <label style="font-size:13px;color:var(--text-secondary)">API Key</label>
1873
- <input id="api-key" type="password" placeholder="sk-..." value="${escapeHtml(pc.apiKey || '')}" style="padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-hover);color:var(--text)">
1874
- <label style="font-size:13px;color:var(--text-secondary)">模型</label>
1875
- <input id="api-model" placeholder="模型名" value="${escapeHtml(pc.model || '')}" style="padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-hover);color:var(--text)">
2121
+ <input id="api-baseurl" placeholder="https://api.xxx.com/v1" style="${_in}">
2122
+ <input id="api-key" type="password" placeholder="sk-..." style="${_in}">
2123
+ <input id="api-model" placeholder="模型名" style="${_in}">
1876
2124
  <button id="api-save" style="padding:12px;border:none;border-radius:10px;background:var(--accent);color:var(--bg);font-weight:700">保存配置</button>
1877
2125
  </div>`;
1878
2126
  document.body.appendChild(page);
1879
2127
  $('#api-config-back').addEventListener('click', () => page.remove());
1880
- // 供应商选择: 芯片式 (原来是一个原生 select, 小屏上难点、也看不出有哪些可选)
1881
- let picked = LLM_BY_ID[provider] ? provider : 'deepseek';
1882
- const fillFrom = (id) => {
1883
- const saved = (cfg.providers && cfg.providers[id]) || {};
2128
+ const setText = (sel, v) => { const el = page.querySelector(sel); if (el) el.textContent = String(v == null ? '' : v); };
2129
+
2130
+ // ── ① 当前真实生效: 只认服务端 effective ──────────────────────────
2131
+ let lastProviders = null;
2132
+ async function paintEffective() {
2133
+ const r = await modelApiGet('/api/models/providers');
2134
+ if (!r.ok) {
2135
+ setText('#api-eff-provider', '—'); setText('#api-eff-model', '—');
2136
+ setText('#api-eff-baseurl', '—'); setText('#api-eff-scope', '—'); setText('#api-eff-hash', '—');
2137
+ setText('#api-eff-note', '读不到生效配置: ' + r.error);
2138
+ return null;
2139
+ }
2140
+ const eff = r.data && r.data.effective;
2141
+ if (!eff) {
2142
+ setText('#api-eff-note', '电脑端没给出 effective (不猜: 不显示任何默认值)');
2143
+ return r.data;
2144
+ }
2145
+ setText('#api-eff-provider', eff.provider);
2146
+ setText('#api-eff-model', eff.model || '—');
2147
+ setText('#api-eff-baseurl', eff.baseUrl || '—');
2148
+ setText('#api-eff-scope', eff.scope || eff.source || '—');
2149
+ setText('#api-eff-hash', shortConfigHash(eff.configHash));
2150
+ setText('#api-eff-note', '协议 ' + (eff.protocol || '?') + ' · 凭据 ' + (eff.authRef || 'none')
2151
+ + ' · 指纹前 8 位 (完整值见电脑端 Run 快照)');
2152
+ return r.data;
2153
+ }
2154
+
2155
+ // ── ② 目录 + 来源标记 ────────────────────────────────────────────
2156
+ let picked = (localCfg.activeProvider && LLM_BY_ID[localCfg.activeProvider]) ? localCfg.activeProvider : 'deepseek';
2157
+ async function paintCatalog() {
2158
+ setText('#api-catalog-origin', '来源: 读取中…');
2159
+ setText('#api-catalog-head', ''); setText('#api-catalog-list', ''); setText('#api-catalog-note', '');
2160
+ const r = await modelApiGet('/api/models/options?provider=' + encodeURIComponent(picked));
2161
+ if (!r.ok) { setText('#api-catalog-origin', '来源: —'); setText('#api-catalog-note', '读不到目录: ' + r.error); return; }
2162
+ const d = r.data || {};
2163
+ const cat = d.catalog || null;
2164
+ setText('#api-catalog-origin', '来源: ' + originLabel(cat && cat.origin));
2165
+ const models = Array.isArray(d.models) ? d.models : [];
2166
+ setText('#api-catalog-head', String(d.count == null ? models.length : d.count) + ' 个模型'
2167
+ + (cat && cat.stale ? ' · 缓存已过期' : '')
2168
+ + (cat && cat.discoveryFailed ? ' · 上游发现失败' : ''));
2169
+ const listEl = page.querySelector('#api-catalog-list');
2170
+ if (listEl) {
2171
+ listEl.innerHTML = models.slice(0, 40).map((m) => {
2172
+ const id = escapeHtml(String((m && (m.id || m.name)) || ''));
2173
+ const tool = (m && m.toolCalling) ? ' · 工具调用 ' + escapeHtml(String(m.toolCalling)) : '';
2174
+ return '<div>' + id + tool + '</div>';
2175
+ }).join('') || '(空目录)';
2176
+ }
2177
+ if (cat && cat.failure) {
2178
+ const f = cat.failure;
2179
+ setText('#api-catalog-note', '上游失败类目: ' + String(f.failureClass || f.class || f.message || JSON.stringify(f)).slice(0, 160));
2180
+ }
2181
+ }
2182
+
2183
+ // ── ③ 供应商芯片 (名字/可用性来自服务端 providers, 不是本机硬编码表) ──
2184
+ function paintChips() {
2185
+ const box = page.querySelector('#api-provider-chips');
2186
+ if (!box) return;
2187
+ const rows = (lastProviders && Array.isArray(lastProviders.providers) && lastProviders.providers.length)
2188
+ ? lastProviders.providers.map((p) => ({ id: String(p.id), label: String(p.name || p.id), configured: p.configured !== false, current: p.current === true, isLocal: p.isLocal === true }))
2189
+ : LLM_PROVIDERS;
2190
+ box.innerHTML = rows.map((p) => `<button type="button" class="provider-chip${p.id === picked ? ' active' : ''}" data-provider="${escapeHtml(p.id)}">${escapeHtml(p.label)}</button>`).join('');
2191
+ const cn = page.querySelector('#api-provider-count');
2192
+ if (cn) {
2193
+ const ready = rows.filter((p) => p.configured !== false).length;
2194
+ cn.textContent = '(' + ready + ' / ' + rows.length + ' 家可用凭据就绪)';
2195
+ }
2196
+ }
2197
+ function fillFrom(id) {
2198
+ const saved = (localCfg.providers && localCfg.providers[id]) || {};
1884
2199
  const d = LLM_BY_ID[id] || {};
1885
- $('#api-baseurl').value = saved.baseUrl || d.baseUrl || '';
1886
- $('#api-model').value = saved.model || d.model || '';
2200
+ const row = lastProviders && Array.isArray(lastProviders.providers) ? lastProviders.providers.find((x) => String(x.id) === String(id)) : null;
2201
+ $('#api-baseurl').value = saved.baseUrl || (row && row.baseUrl) || d.baseUrl || '';
2202
+ $('#api-model').value = saved.model || (row && row.configuredModel) || d.model || '';
1887
2203
  $('#api-key').value = saved.apiKey || '';
1888
2204
  const hint = $('#api-hint');
1889
2205
  if (hint) {
@@ -1892,23 +2208,37 @@
1892
2208
  : (saved.apiKey ? '该供应商已保存过 key(改完记得再点保存)'
1893
2209
  : '填官方文档里的 API key; 保存后本机智能体就走它');
1894
2210
  }
1895
- };
1896
- const paintChips = () => {
1897
- $$('#api-provider-chips .provider-chip').forEach((c) => c.classList.toggle('active', c.dataset.provider === picked));
1898
- };
1899
- const configured = Object.keys(cfg.providers || {}).filter((k) => cfg.providers[k] && cfg.providers[k].apiKey);
1900
- $('#api-provider-count').textContent = configured.length ? `(已配 ${configured.length} 个: ${configured.join(' / ')})` : '';
1901
- $('#api-provider-chips').addEventListener('click', (e) => {
2211
+ }
2212
+ page.querySelector('#api-provider-chips').addEventListener('click', (e) => {
1902
2213
  const b = e.target.closest && e.target.closest('.provider-chip');
1903
2214
  if (!b) return;
1904
2215
  picked = b.dataset.provider;
1905
2216
  paintChips();
1906
2217
  fillFrom(picked);
2218
+ void paintCatalog();
1907
2219
  });
1908
- fillFrom(picked);
2220
+
2221
+ // ── ④ 刷新发现 / ⑤ 手输模型 (都打在服务端唯一入口上) ─────────────
2222
+ page.querySelector('#api-discover-refresh').addEventListener('click', async () => {
2223
+ setText('#api-discover-result', '刷新中…');
2224
+ const r = await modelApiPost('/api/models/discover', { action: 'refresh', provider: picked, force: true });
2225
+ if (r && r.ok === false) { setText('#api-discover-result', '刷新失败: ' + String(r.error || r.message || '').slice(0, 200)); return; }
2226
+ setText('#api-discover-result', '刷新完成 · 来源 ' + originLabel(r && r.catalog && r.catalog.origin));
2227
+ await paintCatalog();
2228
+ });
2229
+ page.querySelector('#api-manual-admit').addEventListener('click', async () => {
2230
+ const model = String($('#api-manual-model').value || '').trim();
2231
+ if (!model) { setText('#api-discover-result', '先填一个模型名'); return; }
2232
+ const r = await modelApiPost('/api/models/discover', { action: 'admit', provider: picked, model });
2233
+ if (r && r.ok === false) { setText('#api-discover-result', '加入失败: ' + String(r.error || r.message || '').slice(0, 200)); return; }
2234
+ setText('#api-discover-result', '已加入目录: ' + model);
2235
+ await paintCatalog();
2236
+ });
2237
+
2238
+ // ── ⑥ 编辑表单保存 (写路径, 与从前同一口) ─────────────────────────
1909
2239
  $('#api-save').addEventListener('click', async () => {
1910
2240
  const p = picked;
1911
- const next = (cfg && cfg.providers) ? cfg : { activeProvider: cfg.activeProvider, providers: {}, updatedAt: Date.now() };
2241
+ const next = (localCfg && localCfg.providers) ? localCfg : { activeProvider: localCfg.activeProvider, providers: {}, updatedAt: Date.now() };
1912
2242
  next.activeProvider = p;
1913
2243
  next.providers[p] = Object.assign({}, (next.providers[p] || {}), {
1914
2244
  enabled: true, apiKey: $('#api-key').value.trim(), baseUrl: $('#api-baseurl').value.trim(),
@@ -1920,6 +2250,12 @@
1920
2250
  page.remove();
1921
2251
  } catch (e) { alert('保存失败: ' + (e.message || e)); }
1922
2252
  });
2253
+
2254
+ // 启动: 先画芯片(服务端清单) → 再画生效卡片与目录
2255
+ lastProviders = await paintEffective();
2256
+ paintChips();
2257
+ fillFrom(picked);
2258
+ await paintCatalog();
1923
2259
  }
1924
2260
 
1925
2261
  // === 加密钱包 (只读 MVP) ===
@@ -3693,7 +4029,8 @@
3693
4029
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bindEvents);
3694
4030
  else bindEvents();
3695
4031
 
3696
- // 供验收脚本驱动 (真 headless DOM 用): 只暴露行为入口, 不暴露数据
4032
+ // 供验收脚本/壳层驱动 (真 headless DOM 用): 只暴露行为入口, 不暴露数据
4033
+ window.__mobileUpdateUi = { open: () => openUpdatePage(), openSettings: () => openSettings() };
3697
4034
  window.__mobileTasksUi = {
3698
4035
  refresh: refreshAll, askJoin: askJoin, askCreate: askCreate, askPublish: askPublish,
3699
4036
  askTrail: askTrail, askAnnounceToGroup: askAnnounceToGroup, confirm: doConfirmed,