@bolloon/bolloon-agent 0.5.0 → 0.5.1

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 (106) hide show
  1. package/dist/agents/execution-supervisor.js +87 -5
  2. package/dist/agents/model-policy.js +651 -0
  3. package/dist/agents/pi-sdk-tools.js +84 -33
  4. package/dist/agents/pi-sdk.js +83 -23
  5. package/dist/agents/run-store.js +53 -0
  6. package/dist/agents/task/task-runner.js +8 -1
  7. package/dist/agents/workflow-pivot-loop.js +19 -12
  8. package/dist/cli/clipboard.js +65 -0
  9. package/dist/cli/ink-app.js +145 -17
  10. package/dist/cli/log-gate.js +386 -0
  11. package/dist/cli/mention-data.js +2 -0
  12. package/dist/cli/model-selector.js +538 -0
  13. package/dist/cli/reply-hygiene.js +54 -0
  14. package/dist/cli/setup-wizard.js +686 -69
  15. package/dist/cli/startup-notice.js +160 -0
  16. package/dist/cli/stores.js +22 -1
  17. package/dist/cli/theme.js +49 -0
  18. package/dist/cli/tui-select.js +970 -0
  19. package/dist/cli-entry.js +125 -18
  20. package/dist/electron-build/agents/execution-supervisor.js +86 -4
  21. package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
  22. package/dist/electron-build/agents/goal-store.js.map +1 -1
  23. package/dist/electron-build/agents/model-policy.js +706 -0
  24. package/dist/electron-build/agents/model-policy.js.map +1 -0
  25. package/dist/electron-build/agents/pi-sdk-tools.js +84 -33
  26. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  27. package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
  28. package/dist/electron-build/agents/pi-sdk.js +83 -23
  29. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  30. package/dist/electron-build/agents/run-store.js +55 -1
  31. package/dist/electron-build/agents/run-store.js.map +1 -1
  32. package/dist/electron-build/agents/task/task-runner.js +8 -1
  33. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  34. package/dist/electron-build/agents/workflow-pivot-loop.js +19 -12
  35. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  36. package/dist/electron-build/llm/config-store.js +146 -6
  37. package/dist/electron-build/llm/config-store.js.map +1 -1
  38. package/dist/electron-build/llm/connection-probe.js +764 -0
  39. package/dist/electron-build/llm/connection-probe.js.map +1 -0
  40. package/dist/electron-build/llm/custom-provider-store.js +272 -0
  41. package/dist/electron-build/llm/custom-provider-store.js.map +1 -0
  42. package/dist/electron-build/llm/data/provider-catalog-baked.js +23 -0
  43. package/dist/electron-build/llm/data/provider-catalog-baked.js.map +1 -0
  44. package/dist/electron-build/llm/model-catalog.js +778 -0
  45. package/dist/electron-build/llm/model-catalog.js.map +1 -0
  46. package/dist/electron-build/llm/model-discovery.js +1202 -0
  47. package/dist/electron-build/llm/model-discovery.js.map +1 -0
  48. package/dist/electron-build/llm/model-selection.js +1315 -0
  49. package/dist/electron-build/llm/model-selection.js.map +1 -0
  50. package/dist/electron-build/llm/pi-ai.js +70 -11
  51. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  52. package/dist/electron-build/llm/provider-catalog.js +969 -0
  53. package/dist/electron-build/llm/provider-catalog.js.map +1 -0
  54. package/dist/electron-build/llm/provider-registry.js +950 -0
  55. package/dist/electron-build/llm/provider-registry.js.map +1 -0
  56. package/dist/electron-build/llm/tool-name.js +266 -0
  57. package/dist/electron-build/llm/tool-name.js.map +1 -0
  58. package/dist/electron-build/utils/dual-source-facts.js +236 -0
  59. package/dist/electron-build/utils/dual-source-facts.js.map +1 -0
  60. package/dist/electron-build/utils/dual-source.js +32 -156
  61. package/dist/electron-build/utils/dual-source.js.map +1 -1
  62. package/dist/electron-build/utils/update-manager.js +22 -72
  63. package/dist/electron-build/utils/update-manager.js.map +1 -1
  64. package/dist/electron-build/utils/update-state.js +2 -19
  65. package/dist/electron-build/utils/update-state.js.map +1 -1
  66. package/dist/electron-build/utils/version-identity.js +193 -0
  67. package/dist/electron-build/utils/version-identity.js.map +1 -0
  68. package/dist/electron-build/utils/version-info.js +47 -101
  69. package/dist/electron-build/utils/version-info.js.map +1 -1
  70. package/dist/index.js +282 -96
  71. package/dist/ios/bolloon-web.json +10 -0
  72. package/dist/ios/mobile-contacts.js +44 -11
  73. package/dist/ios/mobile-core.js +1723 -84
  74. package/dist/ios/mobile-tasks.js +339 -0
  75. package/dist/ios/mobile-update.js +1442 -0
  76. package/dist/ios/mobile.js +308 -35
  77. package/dist/ios/routes-llm-config.js +344 -52
  78. package/dist/ios/routes-mobile-tasks.js +353 -0
  79. package/dist/ios/server.js +163 -4
  80. package/dist/ios/sw.js +1 -1
  81. package/dist/llm/config-store.js +146 -6
  82. package/dist/llm/connection-probe.js +744 -0
  83. package/dist/llm/custom-provider-store.js +224 -0
  84. package/dist/llm/data/provider-catalog-baked.js +19 -0
  85. package/dist/llm/model-catalog.js +717 -0
  86. package/dist/llm/model-discovery.js +1137 -0
  87. package/dist/llm/model-selection.js +1241 -0
  88. package/dist/llm/pi-ai.js +70 -11
  89. package/dist/llm/provider-catalog.js +898 -0
  90. package/dist/llm/provider-registry.js +917 -0
  91. package/dist/llm/tool-name.js +220 -0
  92. package/dist/setup/onboard.js +34 -8
  93. package/dist/utils/dual-source-facts.js +223 -0
  94. package/dist/utils/dual-source.js +12 -141
  95. package/dist/utils/update-manager.js +16 -59
  96. package/dist/utils/update-state.js +6 -18
  97. package/dist/utils/version-identity.js +176 -0
  98. package/dist/utils/version-info.js +9 -78
  99. package/dist/web/bolloon-web.json +10 -0
  100. package/dist/web/mobile-core.js +1721 -84
  101. package/dist/web/mobile-update.js +1442 -0
  102. package/dist/web/mobile.js +308 -35
  103. package/dist/web/routes-llm-config.js +344 -52
  104. package/dist/web/sw.js +1 -1
  105. package/package.json +1 -1
  106. package/scripts/build-web.ts +36 -0
@@ -1697,6 +1697,110 @@
1697
1697
  return r;
1698
1698
  }
1699
1699
 
1700
+ /**
1701
+ * 「App 更新 (web 资源层)」页 (2026-09-26, update-protocol §13)
1702
+ * 看得见的: 当前装的是哪个源 / 哪个版本·sha / 能切回哪个源 —— 与桌面 `bolloon update --status` 同一口径。
1703
+ * 做得出的: 检查 → (智能体可自动跑到"准备并验证") → 人工确认切换 → 重载; 一键回滚。
1704
+ * 说不出的不说: 原生壳层不能在这里自更 (iOS 走商店/TestFlight, Android 侧载需允许未知来源)。
1705
+ */
1706
+ async function openUpdatePage() {
1707
+ const page = document.createElement('div');
1708
+ page.className = 'chat-page';
1709
+ page.id = 'update-page';
1710
+ page.innerHTML = `
1711
+ <div class="chat-topbar">
1712
+ <button class="icon-btn" id="update-back">←</button>
1713
+ <div style="flex:1;font-weight:600">App 更新</div>
1714
+ <button class="icon-btn" id="update-check" title="检查更新">↻</button>
1715
+ </div>
1716
+ <div style="padding:12px;overflow:auto">
1717
+ <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>
1718
+ <div style="display:flex;gap:8px;margin-bottom:10px">
1719
+ <button class="conv-item" id="update-ch-stable" style="flex:1;justify-content:center">stable (npm)</button>
1720
+ <button class="conv-item" id="update-ch-dev" style="flex:1;justify-content:center">dev (GitHub master)</button>
1721
+ </div>
1722
+ <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>
1723
+ <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>
1724
+ <div class="conv-item" id="update-reload"><span class="list-icon">${ICONS.themeAuto}</span><span>重载界面让更新生效</span></div>
1725
+ <div id="update-ceiling" style="font-size:12px;line-height:1.6;color:var(--fg-2,#888);margin:10px 0"></div>
1726
+ <pre id="update-log" style="white-space:pre-wrap;word-break:break-all;font-size:12px;line-height:1.6;margin:0"></pre>
1727
+ </div>`;
1728
+ document.body.appendChild(page);
1729
+
1730
+ const identityEl = page.querySelector('#update-identity');
1731
+ const logEl = page.querySelector('#update-log');
1732
+ const applySub = page.querySelector('#update-apply-sub');
1733
+ const ceilingEl = page.querySelector('#update-ceiling');
1734
+ let lastCheck = null;
1735
+
1736
+ const log = (line) => { logEl.textContent += `${line}\n`; logEl.scrollTop = logEl.scrollHeight; };
1737
+ const UP = () => (window.BolloonCore && window.BolloonCore.update) || null;
1738
+ const setIdentity = async () => {
1739
+ const up = UP();
1740
+ if (!up) { identityEl.textContent = '内核未就绪'; return; }
1741
+ const voc = up.vocabulary();
1742
+ ceilingEl.textContent = voc.nativeCeiling;
1743
+ const st = await up.state();
1744
+ const applied = st.installedChannel ? `${st.installedChannel}${st.installedDevSha ? ` @ ${st.installedDevSha}` : ''}` : '(还没用这个功能装过)';
1745
+ identityEl.textContent = [
1746
+ `当前装的: ${applied}`,
1747
+ `通道偏好: ${up.channel()}`,
1748
+ `能切回: ${st.switchableTo ? `${st.switchableTo.channel} (${st.switchableTo.source}${st.switchableTo.target ? ` @ ${st.switchableTo.target}` : ''})` : '—'}`,
1749
+ `上次来源: ${st.installedFrom ? `${st.installedFrom.source}${st.installedFrom.digest ? ` · ${st.installedFrom.digest}` : ''}` : '—'}`,
1750
+ `更新层: web 资源层(界面与逻辑:入群 / 发任务公告 / 看飞轮进度 / 授权签名)`,
1751
+ `会生效吗: ${up.nativeWired() ? '会(原生壳已指向可写目录)' : '不会 —— 原生壳还没指向可写目录,装了也不生效,所以不会切换'}`,
1752
+ ].join('\n');
1753
+ };
1754
+ const runCheck = async () => {
1755
+ const up = UP();
1756
+ if (!up) { log('内核未就绪'); return; }
1757
+ log('· 检查两源(npm + GitHub)…');
1758
+ const r = await up.report({});
1759
+ lastCheck = r.result;
1760
+ identityEl.textContent = r.lines.join('\n');
1761
+ applySub.textContent = r.result.status === 'update_available'
1762
+ ? `可装: ${r.result.targetIdentity || r.result.latestVersion}` : `结论: ${r.result.status}`;
1763
+ log(`· 结论: ${r.result.status}${r.result.reason ? ` — ${r.result.reason}` : ''}`);
1764
+ if (r.result.warnings && r.result.warnings.length) r.result.warnings.forEach((w) => log(` ⚠ ${w}`));
1765
+ await setIdentity();
1766
+ };
1767
+
1768
+ page.querySelector('#update-back').addEventListener('click', () => { page.remove(); void openSettings(); });
1769
+ page.querySelector('#update-check').addEventListener('click', () => { logEl.textContent = ''; void runCheck(); });
1770
+ page.querySelector('#update-ch-stable').addEventListener('click', async () => { const r = await UP().setChannel('stable'); log(`· ${r.detail}`); await setIdentity(); });
1771
+ page.querySelector('#update-ch-dev').addEventListener('click', async () => { const r = await UP().setChannel('dev'); log(`· ${r.detail}`); await setIdentity(); });
1772
+ page.querySelector('#update-apply').addEventListener('click', async () => {
1773
+ const up = UP();
1774
+ if (!up) return;
1775
+ const target = lastCheck && (lastCheck.targetIdentity || lastCheck.latestVersion);
1776
+ if (!lastCheck || lastCheck.status !== 'update_available') { log('· 先检查更新(右上角 ↻):现在没有可装的目标,或源不可达/交叉校验不一致 —— 拒绝安装。'); return; }
1777
+ // 人在环: 说清要换的是哪一层, 再动手
1778
+ if (!confirm(`要装 ${target} 吗?\n\n换的是「web 资源层」(界面与逻辑)。装完需要重载界面才生效;装坏了可以一键回滚。\n原生壳层(二进制)不在这里更新。`)) return;
1779
+ log('· 准备并验证 …');
1780
+ const prep = await up.prepare({ onStage: (s, d) => log(` [${s}] ${d}`) });
1781
+ if (!prep.ok) { log(`· 准备/验证失败: ${prep.status} — ${prep.reason}`); return; }
1782
+ log(`· 已验证 ${prep.to}(${prep.fileCount} 个文件)—— 现在切换`);
1783
+ const out = await up.apply({ confirm: true, reuseStaging: true, onStage: (s, d) => log(` [${s}] ${d}`) });
1784
+ log(out.ok ? `· 成功: ${out.from} → ${out.to}(重载界面后生效)` : `· 未生效 (${out.stage}): ${out.reason}`);
1785
+ applySub.textContent = out.ok ? '已装好 · 点「重载界面」生效' : `未生效: ${out.status}`;
1786
+ await setIdentity();
1787
+ });
1788
+ page.querySelector('#update-rollback').addEventListener('click', async () => {
1789
+ const up = UP();
1790
+ if (!up) return;
1791
+ if (!confirm('换回上一份能用的 web 资源吗?(当前这一份会被替换掉)')) return;
1792
+ const out = await up.rollback({ confirm: true, onStage: (s, d) => log(` [${s}] ${d}`) });
1793
+ log(out.ok ? `· 已回滚到 ${out.to}` : `· 回滚未执行 (${out.stage}): ${out.reason}`);
1794
+ await setIdentity();
1795
+ });
1796
+ page.querySelector('#update-reload').addEventListener('click', async () => {
1797
+ const r = await UP().reload();
1798
+ log(`· ${r.detail}(方式: ${r.how})`);
1799
+ });
1800
+
1801
+ await setIdentity();
1802
+ }
1803
+
1700
1804
  function openSettings() {
1701
1805
  const page = document.createElement('div');
1702
1806
  page.className = 'chat-page';
@@ -1709,6 +1813,7 @@
1709
1813
  <div style="padding:12px">
1710
1814
  <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
1815
  <div class="conv-item" id="theme-toggle"><span class="list-icon" id="theme-icon">${ICONS.themeAuto}</span><span id="theme-text">跟随系统</span></div>
1816
+ <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
1817
  <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
1818
  <div class="conv-item" id="settings-desktop"><span class="list-icon">${ICONS.globe}</span><span>电脑端同步</span><span class="list-arrow">›</span></div>
1714
1819
  <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 +1830,22 @@
1725
1830
  applyTheme(resolveThemePref(), false);
1726
1831
  $('#settings-back').addEventListener('click', () => page.remove());
1727
1832
  $('#api-config-item').addEventListener('click', openApiConfig);
1833
+ // App 更新: 预览里就说清"现在装的是哪个源 / 哪个版本·sha / 能切回谁"(= 桌面 update --status 同一口径)
1834
+ $('#settings-update').addEventListener('click', () => { page.remove(); void openUpdatePage(); });
1835
+ void (async () => {
1836
+ const el = page.querySelector('#update-preview');
1837
+ if (!el) return;
1838
+ try {
1839
+ const up = window.BolloonCore && window.BolloonCore.update;
1840
+ if (!up) { el.textContent = '内核未就绪'; return; }
1841
+ const st = await up.state();
1842
+ const applied = st.installedChannel
1843
+ ? `${st.installedChannel}${st.installedDevSha ? ` @ ${st.installedDevSha}` : ''}`
1844
+ : '内置资源';
1845
+ const back = st.switchableTo ? ` · 能切回 ${st.switchableTo.channel}` : '';
1846
+ el.textContent = `当前 ${applied}${back} · 通道 ${up.channel()}${up.nativeWired() ? '' : ' · 原生壳未接通'}`;
1847
+ } catch (e) { el.textContent = '读取失败: ' + ((e && e.message) || e); }
1848
+ })();
1728
1849
  $('#theme-toggle').addEventListener('click', () => {
1729
1850
  const next = currentTheme === 'auto' ? 'light' : (currentTheme === 'light' ? 'dark' : 'auto');
1730
1851
  applyTheme(next, true);
@@ -1827,9 +1948,28 @@
1827
1948
  $('#settings-did').addEventListener('click', () => { api.get('/api/auth/status').then((s) => alert('DID: ' + (s.did || '未生成'))); });
1828
1949
  }
1829
1950
 
1830
- // === API 配置 (LLM 供应商) ===
1831
- // 注意: 手机端 RemoteLlm 走 OpenAI 兼容协议 (baseUrl + /chat/completions) —— 新增 provider 必须是
1832
- // OpenAI 兼容端点 (gemini 用 /v1beta/openai, 智谱 v4 / dashscope compatible-mode 都兼容)。
1951
+ // === 模型配置 (供应商 / 模型 / 地址) ===
1952
+ //
1953
+ // 2026-09-26 (模型链路升级 P6 的手机侧): 这一页**只显示与触发**, 自己不算配置 ——
1954
+ // 唯一事实来自桌面端的模型端点 (与 CLI `/model` / Web 桌面页**同一份**):
1955
+ //
1956
+ // 当前真实生效 GET /api/models/providers → body.effective
1957
+ // (provider · model · baseUrl · scope · configHash)
1958
+ // 目录 + 来源 GET /api/models/options?provider=<id> → body.catalog.origin
1959
+ // (live | cached | curated | custom | unavailable)
1960
+ // 刷新发现 POST /api/models/discover {action:'refresh', provider}
1961
+ // 手输模型 POST /api/models/discover {action:'admit', provider, model}
1962
+ //
1963
+ // 纪律: **不回读 /api/llm-config 自己拼 activeProvider/providers 来显示"生效配置"** —— 那是
1964
+ // 第二套读配置逻辑, 与服务端早晚漂移。下面 `localCfg` 只喂"编辑表单"和"保存"(写路径),
1965
+ // 展示层一个字都不从它取; 拿不到桌面端就如实说"显示不了", 不猜不编。
1966
+ //
1967
+ // 注意: 手机端 RemoteLlm 走 OpenAI 兼容协议 (baseUrl + /chat/completions) —— 自定义 provider
1968
+ // 必须是 OpenAI 兼容端点 (gemini 用 /v1beta/openai, 智谱 v4 / dashscope compatible-mode 都兼容)。
1969
+ const ORIGIN_LABEL = {
1970
+ live: '实时发现 (live)', cached: '缓存发现 (cached)', curated: '内置清单 (curated)',
1971
+ custom: '自定义 (custom)', unavailable: '不可用 (unavailable)',
1972
+ };
1833
1973
  const LLM_PROVIDERS = [
1834
1974
  { id: 'deepseek', label: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1', model: 'deepseek-chat' },
1835
1975
  { id: 'openai', label: 'OpenAI', baseUrl: 'https://api.openai.com/v1', model: 'gpt-4o-mini' },
@@ -1847,43 +1987,155 @@
1847
1987
  { id: 'custom', label: '自定义', baseUrl: '', model: '' },
1848
1988
  ];
1849
1989
  const LLM_BY_ID = LLM_PROVIDERS.reduce((m, p) => (m[p.id] = p, m), {});
1990
+
1991
+ /** configHash 只显示前 8 位 (完整值在桌面端 / Run 快照里, 手机屏不需要 64 位) */
1992
+ function shortConfigHash(h) {
1993
+ const s = String(h == null ? '' : h);
1994
+ return s ? s.slice(0, 8) : '—';
1995
+ }
1996
+
1997
+ /** 目录来源标记 → 人话 (认不出来就原样回显, 不编) */
1998
+ function originLabel(origin) {
1999
+ const k = String(origin || '');
2000
+ return ORIGIN_LABEL[k] || (k ? k : '—');
2001
+ }
2002
+
2003
+ /** 电脑端基址 (设置里填; 手机自足模式拿不到就如实说, 不假装成功) */
2004
+ async function modelApiGet(path) {
2005
+ const base = await desktopBaseUrl();
2006
+ if (!base) return { ok: false, error: '未接电脑端 —— 设置 → 电脑端同步 里填桌面地址后可看' };
2007
+ const r = await desktopFetch(path);
2008
+ if (r === null) return { ok: false, error: '电脑端不可达或返回非 200' };
2009
+ return { ok: true, data: r };
2010
+ }
2011
+ async function modelApiPost(path, body) {
2012
+ const base = await desktopBaseUrl();
2013
+ if (!base) return { ok: false, error: '未接电脑端 —— 设置 → 电脑端同步 里填桌面地址后可看' };
2014
+ return await desktopPostRaw(path, body); // 保留后端错误原文
2015
+ }
2016
+
1850
2017
  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
2018
  const page = document.createElement('div');
1855
2019
  page.className = 'chat-page';
1856
2020
  page.id = 'api-config-page';
1857
- const provider = cfg.activeProvider || 'deepseek';
1858
- const pc = cfg.providers?.[provider] || {};
2021
+ // 编辑表单的本地草稿 (写路径) —— 展示层不读它
2022
+ let localCfg = null;
2023
+ try { localCfg = await api.get('/api/llm-config'); } catch (e) { localCfg = null; }
2024
+ if (!localCfg || !localCfg.providers) localCfg = { activeProvider: 'deepseek', providers: {}, updatedAt: Date.now() };
2025
+ 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';
2026
+ const _btn = 'padding:10px 14px;border:1px solid var(--border);border-radius:8px;background:var(--bg-hover);color:var(--text)';
1859
2027
  page.innerHTML = `
1860
2028
  <div class="chat-topbar">
1861
2029
  <button class="icon-btn" id="api-config-back">←</button>
1862
- <div style="flex:1;font-weight:600">API 配置</div>
2030
+ <div style="flex:1;font-weight:600">模型配置</div>
1863
2031
  </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('')}
2032
+ <div style="padding:12px;display:flex;flex-direction:column;gap:12px;overflow:auto">
2033
+ <div class="section-label">当前真实生效 (电脑端 /api/models/providers)</div>
2034
+ <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">
2035
+ <div style="font-size:13px">供应商: <b id="api-eff-provider">读取中…</b></div>
2036
+ <div style="font-size:13px">模型: <b id="api-eff-model">—</b></div>
2037
+ <div style="font-size:12px;color:var(--text-secondary);word-break:break-all">地址: <span id="api-eff-baseurl">—</span></div>
2038
+ <div style="font-size:12px;color:var(--text-secondary)">来源(scope): <span id="api-eff-scope">—</span> · 配置指纹: <span id="api-eff-hash">—</span></div>
2039
+ <div id="api-eff-note" style="font-size:12px;color:var(--text-muted)"></div>
2040
+ </div>
2041
+
2042
+ <div class="section-label">模型目录 (电脑端 /api/models/options)</div>
2043
+ <div id="api-catalog-origin" style="font-size:13px">—</div>
2044
+ <div id="api-catalog-head" style="font-size:12px;color:var(--text-secondary)"></div>
2045
+ <div id="api-catalog-list" style="font-size:12px;color:var(--text-secondary);max-height:170px;overflow:auto;word-break:break-all"></div>
2046
+ <div id="api-catalog-note" style="font-size:12px;color:var(--text-muted)"></div>
2047
+ <button id="api-discover-refresh" style="${_btn}">刷新发现</button>
2048
+ <div style="display:flex;gap:8px">
2049
+ <input id="api-manual-model" placeholder="手输模型名 (目录里没有的)" style="flex:1;${_in}">
2050
+ <button id="api-manual-admit" style="${_btn}">加入目录</button>
1868
2051
  </div>
2052
+ <div id="api-discover-result" style="font-size:12px;color:var(--text-secondary)"></div>
2053
+
2054
+ <div class="section-label">供应商 / 端点 / 凭据 (本机保存) <span id="api-provider-count" style="opacity:.55"></span></div>
2055
+ <div class="provider-chips" id="api-provider-chips"></div>
1869
2056
  <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)">
2057
+ <input id="api-baseurl" placeholder="https://api.xxx.com/v1" style="${_in}">
2058
+ <input id="api-key" type="password" placeholder="sk-..." style="${_in}">
2059
+ <input id="api-model" placeholder="模型名" style="${_in}">
1876
2060
  <button id="api-save" style="padding:12px;border:none;border-radius:10px;background:var(--accent);color:var(--bg);font-weight:700">保存配置</button>
1877
2061
  </div>`;
1878
2062
  document.body.appendChild(page);
1879
2063
  $('#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]) || {};
2064
+ const setText = (sel, v) => { const el = page.querySelector(sel); if (el) el.textContent = String(v == null ? '' : v); };
2065
+
2066
+ // ── ① 当前真实生效: 只认服务端 effective ──────────────────────────
2067
+ let lastProviders = null;
2068
+ async function paintEffective() {
2069
+ const r = await modelApiGet('/api/models/providers');
2070
+ if (!r.ok) {
2071
+ setText('#api-eff-provider', '—'); setText('#api-eff-model', '—');
2072
+ setText('#api-eff-baseurl', '—'); setText('#api-eff-scope', '—'); setText('#api-eff-hash', '—');
2073
+ setText('#api-eff-note', '读不到生效配置: ' + r.error);
2074
+ return null;
2075
+ }
2076
+ const eff = r.data && r.data.effective;
2077
+ if (!eff) {
2078
+ setText('#api-eff-note', '电脑端没给出 effective (不猜: 不显示任何默认值)');
2079
+ return r.data;
2080
+ }
2081
+ setText('#api-eff-provider', eff.provider);
2082
+ setText('#api-eff-model', eff.model || '—');
2083
+ setText('#api-eff-baseurl', eff.baseUrl || '—');
2084
+ setText('#api-eff-scope', eff.scope || eff.source || '—');
2085
+ setText('#api-eff-hash', shortConfigHash(eff.configHash));
2086
+ setText('#api-eff-note', '协议 ' + (eff.protocol || '?') + ' · 凭据 ' + (eff.authRef || 'none')
2087
+ + ' · 指纹前 8 位 (完整值见电脑端 Run 快照)');
2088
+ return r.data;
2089
+ }
2090
+
2091
+ // ── ② 目录 + 来源标记 ────────────────────────────────────────────
2092
+ let picked = (localCfg.activeProvider && LLM_BY_ID[localCfg.activeProvider]) ? localCfg.activeProvider : 'deepseek';
2093
+ async function paintCatalog() {
2094
+ setText('#api-catalog-origin', '来源: 读取中…');
2095
+ setText('#api-catalog-head', ''); setText('#api-catalog-list', ''); setText('#api-catalog-note', '');
2096
+ const r = await modelApiGet('/api/models/options?provider=' + encodeURIComponent(picked));
2097
+ if (!r.ok) { setText('#api-catalog-origin', '来源: —'); setText('#api-catalog-note', '读不到目录: ' + r.error); return; }
2098
+ const d = r.data || {};
2099
+ const cat = d.catalog || null;
2100
+ setText('#api-catalog-origin', '来源: ' + originLabel(cat && cat.origin));
2101
+ const models = Array.isArray(d.models) ? d.models : [];
2102
+ setText('#api-catalog-head', String(d.count == null ? models.length : d.count) + ' 个模型'
2103
+ + (cat && cat.stale ? ' · 缓存已过期' : '')
2104
+ + (cat && cat.discoveryFailed ? ' · 上游发现失败' : ''));
2105
+ const listEl = page.querySelector('#api-catalog-list');
2106
+ if (listEl) {
2107
+ listEl.innerHTML = models.slice(0, 40).map((m) => {
2108
+ const id = escapeHtml(String((m && (m.id || m.name)) || ''));
2109
+ const tool = (m && m.toolCalling) ? ' · 工具调用 ' + escapeHtml(String(m.toolCalling)) : '';
2110
+ return '<div>' + id + tool + '</div>';
2111
+ }).join('') || '(空目录)';
2112
+ }
2113
+ if (cat && cat.failure) {
2114
+ const f = cat.failure;
2115
+ setText('#api-catalog-note', '上游失败类目: ' + String(f.failureClass || f.class || f.message || JSON.stringify(f)).slice(0, 160));
2116
+ }
2117
+ }
2118
+
2119
+ // ── ③ 供应商芯片 (名字/可用性来自服务端 providers, 不是本机硬编码表) ──
2120
+ function paintChips() {
2121
+ const box = page.querySelector('#api-provider-chips');
2122
+ if (!box) return;
2123
+ const rows = (lastProviders && Array.isArray(lastProviders.providers) && lastProviders.providers.length)
2124
+ ? 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 }))
2125
+ : LLM_PROVIDERS;
2126
+ 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('');
2127
+ const cn = page.querySelector('#api-provider-count');
2128
+ if (cn) {
2129
+ const ready = rows.filter((p) => p.configured !== false).length;
2130
+ cn.textContent = '(' + ready + ' / ' + rows.length + ' 家可用凭据就绪)';
2131
+ }
2132
+ }
2133
+ function fillFrom(id) {
2134
+ const saved = (localCfg.providers && localCfg.providers[id]) || {};
1884
2135
  const d = LLM_BY_ID[id] || {};
1885
- $('#api-baseurl').value = saved.baseUrl || d.baseUrl || '';
1886
- $('#api-model').value = saved.model || d.model || '';
2136
+ const row = lastProviders && Array.isArray(lastProviders.providers) ? lastProviders.providers.find((x) => String(x.id) === String(id)) : null;
2137
+ $('#api-baseurl').value = saved.baseUrl || (row && row.baseUrl) || d.baseUrl || '';
2138
+ $('#api-model').value = saved.model || (row && row.configuredModel) || d.model || '';
1887
2139
  $('#api-key').value = saved.apiKey || '';
1888
2140
  const hint = $('#api-hint');
1889
2141
  if (hint) {
@@ -1892,23 +2144,37 @@
1892
2144
  : (saved.apiKey ? '该供应商已保存过 key(改完记得再点保存)'
1893
2145
  : '填官方文档里的 API key; 保存后本机智能体就走它');
1894
2146
  }
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) => {
2147
+ }
2148
+ page.querySelector('#api-provider-chips').addEventListener('click', (e) => {
1902
2149
  const b = e.target.closest && e.target.closest('.provider-chip');
1903
2150
  if (!b) return;
1904
2151
  picked = b.dataset.provider;
1905
2152
  paintChips();
1906
2153
  fillFrom(picked);
2154
+ void paintCatalog();
1907
2155
  });
1908
- fillFrom(picked);
2156
+
2157
+ // ── ④ 刷新发现 / ⑤ 手输模型 (都打在服务端唯一入口上) ─────────────
2158
+ page.querySelector('#api-discover-refresh').addEventListener('click', async () => {
2159
+ setText('#api-discover-result', '刷新中…');
2160
+ const r = await modelApiPost('/api/models/discover', { action: 'refresh', provider: picked, force: true });
2161
+ if (r && r.ok === false) { setText('#api-discover-result', '刷新失败: ' + String(r.error || r.message || '').slice(0, 200)); return; }
2162
+ setText('#api-discover-result', '刷新完成 · 来源 ' + originLabel(r && r.catalog && r.catalog.origin));
2163
+ await paintCatalog();
2164
+ });
2165
+ page.querySelector('#api-manual-admit').addEventListener('click', async () => {
2166
+ const model = String($('#api-manual-model').value || '').trim();
2167
+ if (!model) { setText('#api-discover-result', '先填一个模型名'); return; }
2168
+ const r = await modelApiPost('/api/models/discover', { action: 'admit', provider: picked, model });
2169
+ if (r && r.ok === false) { setText('#api-discover-result', '加入失败: ' + String(r.error || r.message || '').slice(0, 200)); return; }
2170
+ setText('#api-discover-result', '已加入目录: ' + model);
2171
+ await paintCatalog();
2172
+ });
2173
+
2174
+ // ── ⑥ 编辑表单保存 (写路径, 与从前同一口) ─────────────────────────
1909
2175
  $('#api-save').addEventListener('click', async () => {
1910
2176
  const p = picked;
1911
- const next = (cfg && cfg.providers) ? cfg : { activeProvider: cfg.activeProvider, providers: {}, updatedAt: Date.now() };
2177
+ const next = (localCfg && localCfg.providers) ? localCfg : { activeProvider: localCfg.activeProvider, providers: {}, updatedAt: Date.now() };
1912
2178
  next.activeProvider = p;
1913
2179
  next.providers[p] = Object.assign({}, (next.providers[p] || {}), {
1914
2180
  enabled: true, apiKey: $('#api-key').value.trim(), baseUrl: $('#api-baseurl').value.trim(),
@@ -1920,6 +2186,12 @@
1920
2186
  page.remove();
1921
2187
  } catch (e) { alert('保存失败: ' + (e.message || e)); }
1922
2188
  });
2189
+
2190
+ // 启动: 先画芯片(服务端清单) → 再画生效卡片与目录
2191
+ lastProviders = await paintEffective();
2192
+ paintChips();
2193
+ fillFrom(picked);
2194
+ await paintCatalog();
1923
2195
  }
1924
2196
 
1925
2197
  // === 加密钱包 (只读 MVP) ===
@@ -3693,7 +3965,8 @@
3693
3965
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bindEvents);
3694
3966
  else bindEvents();
3695
3967
 
3696
- // 供验收脚本驱动 (真 headless DOM 用): 只暴露行为入口, 不暴露数据
3968
+ // 供验收脚本/壳层驱动 (真 headless DOM 用): 只暴露行为入口, 不暴露数据
3969
+ window.__mobileUpdateUi = { open: () => openUpdatePage(), openSettings: () => openSettings() };
3697
3970
  window.__mobileTasksUi = {
3698
3971
  refresh: refreshAll, askJoin: askJoin, askCreate: askCreate, askPublish: askPublish,
3699
3972
  askTrail: askTrail, askAnnounceToGroup: askAnnounceToGroup, confirm: doConfirmed,