magic-agent 2.3.0 → 2.3.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 (52) hide show
  1. package/CLIENT-CONTRACT.md +51 -12
  2. package/README.md +10 -4
  3. package/client-ui/pc/app.css +14 -0
  4. package/client-ui/pc/js/93-rail.js +3 -1
  5. package/client-ui/shared/conv/03-chat-message.js +20 -3
  6. package/client-ui/shared/conv/27-conv-ledger.js +1 -1
  7. package/client-ui/shared/conv/conv.css +17 -0
  8. package/client-ui/shared/conversation.js +170 -39
  9. package/client-ui/shared/transport-http.js +183 -38
  10. package/client-ui-mobile/index.html +370 -68
  11. package/client-ui-mobile/reqs.js +42 -29
  12. package/internal/agent/capability.go +8 -1
  13. package/internal/agent/claude.go +5 -3
  14. package/internal/agent/codebuddy.go +6 -2
  15. package/internal/agent/codex.go +7 -2
  16. package/internal/agent/dsh_sdk.go +8 -0
  17. package/internal/agent/dsh_sdk_test.go +17 -0
  18. package/internal/agent/engine.go +20 -0
  19. package/internal/agent/engine_test.go +52 -0
  20. package/internal/agent/llmengine.go +14 -0
  21. package/internal/agent/llmengine_test.go +24 -0
  22. package/internal/agent/llmhttp.go +6 -0
  23. package/internal/agent/llmhttp_test.go +27 -0
  24. package/internal/agent/llmmodels.go +48 -0
  25. package/internal/agent/permission.go +5 -0
  26. package/internal/cli/ask.go +32 -17
  27. package/internal/cli/requirements.go +2 -0
  28. package/internal/cli/sessionlog.go +5 -0
  29. package/internal/cli/sessionlog_user_test.go +49 -0
  30. package/internal/client/requirements.go +27 -7
  31. package/internal/client/requirements_test.go +51 -0
  32. package/internal/client/service.go +24 -2
  33. package/internal/client/service_test.go +35 -0
  34. package/internal/requirements/consolidate.go +86 -24
  35. package/internal/requirements/requirements.go +9 -0
  36. package/internal/requirements/requirements_test.go +129 -0
  37. package/internal/requirements/store.go +11 -3
  38. package/internal/suggest/suggest.go +118 -7
  39. package/internal/suggest/suggest_test.go +50 -0
  40. package/npm/apk/magic-agent-mobile-release.apk +0 -0
  41. package/npm/apk/version.json +3 -3
  42. package/npm/dist/darwin-arm64/magic-agent +0 -0
  43. package/npm/dist/darwin-arm64/magic-client +0 -0
  44. package/npm/dist/darwin-x64/magic-agent +0 -0
  45. package/npm/dist/darwin-x64/magic-client +0 -0
  46. package/npm/dist/linux-arm64/magic-agent +0 -0
  47. package/npm/dist/linux-arm64/magic-client +0 -0
  48. package/npm/dist/linux-x64/magic-agent +0 -0
  49. package/npm/dist/linux-x64/magic-client +0 -0
  50. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  51. package/npm/dist/win32-x64/magic-client.exe +0 -0
  52. package/package.json +3 -2
@@ -67,6 +67,12 @@
67
67
  /* 状态色底(半透明叠加色,明暗主题通用) */
68
68
  --tint-run:rgba(59,130,246,.18); --tint-wait:rgba(245,158,11,.20); --tint-done:rgba(34,197,94,.18);
69
69
  --tint-queued:rgba(139,92,246,.18); --queued:#8B5CF6;
70
+ /* 异常终态两档(2026-10-23 用户:「现在没有异常状态的颜色」):
71
+ · 失败(failed / gone)→ 红:这条没跑完就没了,要人看一眼;
72
+ · 已取消 / 已打断(cancelled / interrupted)→ 石褐:人为收工,不是故障,
73
+ 所以不上红,但也绝不能和「正常结束」的绿或兜底的白同色。 */
74
+ --tint-fail:rgba(239,68,68,.18); --fail:#F87171;
75
+ --tint-stop:rgba(120,113,108,.24); --stop:#A8A29E;
70
76
 
71
77
  /* ── 掌天瓶绿液(可选令牌,shared/conversation.css 用 `var(--q-bottle*, 兜底)` 取)──
72
78
  瓶是**玉**不是玻璃(见 conversation.js 的形制说明),所以颜色分两组:
@@ -112,6 +118,9 @@
112
118
  --error-surface:#FEF2F2; --error-border:#FECACA; --error-text:#B91C1C; --error-icon:#DC2626;
113
119
  --running:#16A34A; --transitioning:#2563EB; --active:#D97706;
114
120
  --running:#16A34A; --transitioning:#2563EB; --active:#D97706;
121
+ /* 浅底上异常终态要更重(半透明红 / 石褐在 #F8FAFC 上否则会糊成灰) */
122
+ --tint-fail:rgba(220,38,38,.13); --fail:#DC2626;
123
+ --tint-stop:rgba(120,113,108,.16); --stop:#78716C;
115
124
  /* 浅底上玉胎要更浓:`.42` 那档实测会被冲成白瓷,读不出「翠」。浓到 .66 又像实心塑料。 */
116
125
  --q-bottle-jade-hi:rgba(146,231,184,.62);
117
126
  --q-bottle-jade:rgba(96,205,152,.60);
@@ -137,6 +146,9 @@ html[data-theme="light"]{color-scheme:light;
137
146
  --accent:#2563EB; --warning:#D97706; --success:#16A34A;
138
147
  --error-surface:#FEF2F2; --error-border:#FECACA; --error-text:#B91C1C; --error-icon:#DC2626;
139
148
  --running:#16A34A; --transitioning:#2563EB; --active:#D97706;
149
+ /* 浅底上异常终态要更重(同 prefers-color-scheme:light 那一份) */
150
+ --tint-fail:rgba(220,38,38,.13); --fail:#DC2626;
151
+ --tint-stop:rgba(120,113,108,.16); --stop:#78716C;
140
152
  /* 浅底上玉胎要更浓(同上一条注释) */
141
153
  --q-bottle-jade-hi:rgba(146,231,184,.62);
142
154
  --q-bottle-jade:rgba(96,205,152,.60);
@@ -288,6 +300,11 @@ textarea{font:inherit}
288
300
  .srow__ico--run{background:var(--tint-run);color:var(--primary-action)}
289
301
  .srow__ico--wait{background:var(--tint-wait);color:var(--warning)}
290
302
  .srow__ico--done{background:var(--tint-done);color:var(--success)}
303
+ /* 异常终态:以前和 idle 一起落在灰白兜底档,用户看不出「这条是挂了 / 被停了,
304
+ 还是只是没跑过」(2026-10-23 用户:「现在没有异常状态的颜色」)。
305
+ 失败(failed / gone)= 红;已取消 / 已打断(cancelled / interrupted)= 石褐。 */
306
+ .srow__ico--fail{background:var(--tint-fail);color:var(--fail)}
307
+ .srow__ico--stop{background:var(--tint-stop);color:var(--stop)}
291
308
  .srow__ico--idle{background:var(--subtle);color:var(--muted)}
292
309
  /* 等待中(串行排队):紫色 + 一圈**呼吸**的环。
293
310
  与「运行中」的扫光刻意分开 —— 扫光在转(在动),这圈只明暗(在等);
@@ -971,20 +988,18 @@ button.setrow{font-family:inherit}
971
988
  <button class="seg__b" data-v="serial">串行</button>
972
989
  </span>
973
990
  </div>
974
- <!-- 会话托管(2026-10-23 用户:「托管模式下自动按照最佳建议提交到会话中执行,
975
- 是否开启在我的 tab 中可配置」)。开关是**设备级**,落 localStorage;
976
- 组件执行、这里只存与显示。给一行说明,避免只看到四个字不知道会发生什么。 -->
977
- <div class="setrow">
978
- <span class="setrow__main">
979
- <span class="setrow__l">会话托管</span>
980
- <span class="setrow__sub">每轮结束自动发送最合适的建议;提问卡自动选第一项继续(最多连续 12 轮)</span>
981
- </span>
982
- <button class="tgl" id="tglHosting" aria-label="会话托管"></button>
983
- </div>
991
+ <!-- 会话托管(2026-10-23 用户:「托管模式下自动按照最佳建议提交到会话中执行」;
992
+ 同日再定:「跟团队模式一样改成挂在会话上,子会话继承,不再是全局」)。
993
+ 所以这里**没有**全局开关 —— 与团队模式同一处入口:会话页右上角 → 任务运行设置。 -->
984
994
  <!-- 默认引擎 / 模型:新建任务的初值(会话里输入框上方的级联切换会回写这里)。
985
995
  两行点开的是同一个「引擎 → 模型」二列选择器,模型必须挂在引擎下。 -->
986
996
  <button class="setrow" id="btnDefaultEngine"><span class="setrow__l">默认引擎</span><span class="setrow__v" id="defaultEngine">自动(首个可用)</span></button>
987
997
  <button class="setrow" id="btnDefaultModel"><span class="setrow__l">默认模型</span><span class="setrow__v" id="defaultModel">跟随引擎</span></button>
998
+ <!-- 建议引擎 / 模型(2026-10-23 用户:「是选择引擎和模型 跟默认引擎那种抽屉一样」)。
999
+ 两行点开同一个「引擎 → 模型」二列抽屉;**默认不开启**:引擎空 = 组件不请求
1000
+ /desk/suggest、不画建议、不补历史建议、托管不自动跑。选了引擎才启用。 -->
1001
+ <button class="setrow" id="btnSuggestEngine"><span class="setrow__l">建议引擎</span><span class="setrow__v" id="suggestEngine">不开启</span></button>
1002
+ <button class="setrow" id="btnSuggestModel"><span class="setrow__l">建议模型</span><span class="setrow__v" id="suggestModel">跟随引擎</span></button>
988
1003
  <!-- 2026-10-22 用户:「触感反馈 / 语言」这两项不需要 —— 已移除(设备级偏好只剩主题/字体)。 -->
989
1004
  <div class="setrow"><span class="setrow__l">网络代理</span><button class="tgl" id="tglVpn" aria-label="网络代理"></button></div>
990
1005
  <button class="setrow" id="btnNotify"><span class="setrow__l">消息通知设置</span><span class="setrow__v" id="notifyState">—</span></button>
@@ -1033,7 +1048,8 @@ button.setrow{font-family:inherit}
1033
1048
 
1034
1049
  <!-- ══════════ 会话页 → 任务运行设置(独立子页面)══════════
1035
1050
  会话页右上角的调节键点进来(2026-10-21 用户:「运行设置在任务对话框里 不也应该是独立页面吗」)。
1036
- 四项都是**当前这条会话**的运行参数:设备只读,其余三项点开还是原来的选择器(选择器只有一份)。 -->
1051
+ 五项都是**当前这条会话**的运行参数:设备只读,其余点开还是原来的选择器(选择器只有一份)。
1052
+ 会话托管(2026-10-23)也在这里:它跟团队模式一样**属于这条会话**,不再是「我的」里的全局开关。 -->
1037
1053
  <div class="history" id="runSettingsPage" hidden>
1038
1054
  <div class="history__top">
1039
1055
  <button class="topbar__icon" id="btnRunSettingsBack" aria-label="返回"><svg width="20" height="20"><use href="#i-back"/></svg></button>
@@ -1045,7 +1061,9 @@ button.setrow{font-family:inherit}
1045
1061
  <button class="setrow" id="btnRsWorkspace"><span class="setrow__l">工作空间</span><span class="setrow__v" id="rsWorkspace">默认(不属于任何空间)</span></button>
1046
1062
  <button class="setrow" id="btnRsPermission"><span class="setrow__l">权限模式</span><span class="setrow__v" id="rsPermission">沿用默认</span></button>
1047
1063
  <button class="setrow" id="btnRsTeam"><span class="setrow__l">团队模式</span><span class="setrow__v" id="rsTeam">关闭</span></button>
1064
+ <button class="setrow" id="btnRsHosting"><span class="setrow__l">会话托管</span><span class="setrow__v" id="rsHosting">关闭</span></button>
1048
1065
  </div>
1066
+ <div class="history__hint" style="text-align:left">会话托管:每轮成功结束自动发送最合适的建议(最多连续 12 轮)。只作用于当前会话;团队子会话默认继承主会话的开关。</div>
1049
1067
  </div>
1050
1068
  </div>
1051
1069
 
@@ -1286,6 +1304,8 @@ button.setrow{font-family:inherit}
1286
1304
  /* 默认引擎 / 模型(「我的」可改;函数与选择器见下面「默认引擎 / 模型」段)。
1287
1305
  提前到这里声明:首屏 paintRunPref 会读它,晚于 PREFS 初始化赋值就会踩 undefined。 */
1288
1306
  var RUN_PREF = loadRunPref();
1307
+ /* 建议引擎 / 模型(「我的」):引擎空 = 不开启建议功能(默认)。与默认引擎分开存,互不影响。 */
1308
+ var SUGGEST = loadSuggestPref();
1289
1309
  var ENGINES = []; // 最近一次从 /desk/engines?models=1 拿到的清单
1290
1310
  var ENGINES_STALE_AT = ''; // 非空 = 这份清单是插件侧的兜底缓存(探测超时/失败),值是缓存时间
1291
1311
  /* ── 团队模式(2026-10-21)──
@@ -1409,6 +1429,87 @@ button.setrow{font-family:inherit}
1409
1429
  saveTeamPref();
1410
1430
  }
1411
1431
 
1432
+ /* ── 会话托管(2026-10-23,按会话)──
1433
+ 与团队模式同一套形状:`sessions` = sid → 开/关,`fresh` = 新会话还没拿到 session_id
1434
+ 时的临时开关。旧版 `magic.hosting` 是设备级全局值,不迁移(见上面的注释)。 */
1435
+ var HOSTING_KEY = 'magic.hosting.sessions.v1';
1436
+ function loadHostingPref() {
1437
+ var o = { sessions: {}, fresh: false };
1438
+ try {
1439
+ var m = JSON.parse(localStorage.getItem(HOSTING_KEY) || '{}');
1440
+ if (m && typeof m === 'object' && !Array.isArray(m)) {
1441
+ Object.keys(m).forEach(function (sid) { o.sessions[sid] = !!m[sid]; });
1442
+ }
1443
+ } catch (e) {}
1444
+ /* 旧版全局开关:属于「改一条会话,所有会话都变」的旧语义,清掉不迁移。 */
1445
+ try { localStorage.removeItem('magic.hosting'); } catch (e) {}
1446
+ return o;
1447
+ }
1448
+ function saveHostingPref() {
1449
+ /* 与团队模式同一条容量护栏:最多 400 条,超出按 key 顺序丢(只影响容量)。 */
1450
+ var ids = Object.keys(HOSTING.sessions || {});
1451
+ if (ids.length > 400) {
1452
+ ids.slice(0, ids.length - 400).forEach(function (sid) { delete HOSTING.sessions[sid]; });
1453
+ }
1454
+ try { localStorage.setItem(HOSTING_KEY, JSON.stringify(HOSTING.sessions || {})); } catch (e) {}
1455
+ schedulePushPrefs();
1456
+ paintRunSettings();
1457
+ }
1458
+ /* 按任意 id(session_id / run_id)找会话对象:父指针与本地表可能各用一个别名。 */
1459
+ function sessionByAnyId(key) {
1460
+ var k = String(key || '');
1461
+ if (!k) return null;
1462
+ for (var i = 0; i < LAST_SESSIONS.length; i++) {
1463
+ var s = LAST_SESSIONS[i];
1464
+ if (!s) continue;
1465
+ if (sidOf(s) === k || String(s.session_id || '') === k || String(s.run_id || '') === k) return s;
1466
+ }
1467
+ return null;
1468
+ }
1469
+ /* 这条会话的显式记录(session_id / run_id 任一命中即算)。 */
1470
+ function hostingRecordOf(s) {
1471
+ if (!s) return null;
1472
+ var keys = [sidOf(s), s.session_id, s.run_id];
1473
+ for (var i = 0; i < keys.length; i++) {
1474
+ var k = keys[i];
1475
+ if (k && Object.prototype.hasOwnProperty.call(HOSTING.sessions, String(k))) {
1476
+ return HOSTING.sessions[String(k)];
1477
+ }
1478
+ }
1479
+ return null;
1480
+ }
1481
+ /* 这条会话的托管开关。自己有记录就用自己那条;没有就**沿父链继承**(用户 2026-10-23:
1482
+ 「子会话继承」):团队子会话默认跟主会话一致,主会话改开关,没单独设过的子会话也跟着变。
1483
+ 子会话里手动改一次 = 写下自己的记录,从此不再继承(显式覆盖 > 继承)。
1484
+ 新会话(还没有 sid)用 `fresh`,不跨会话继承。 */
1485
+ function hostingOnFor(sid, s) {
1486
+ if (!sid) return !!HOSTING.fresh;
1487
+ var cur = s || sessionByAnyId(sid);
1488
+ var seen = {};
1489
+ for (var depth = 0; cur && depth < 16; depth++) {
1490
+ var key = sidOf(cur) || String(sid);
1491
+ if (seen[key]) break; // 脏数据成环:最多走 16 层并去重
1492
+ seen[key] = 1;
1493
+ var rec = hostingRecordOf(cur);
1494
+ if (rec !== null) return !!rec;
1495
+ var pk = parentKeyOf(cur);
1496
+ if (!pk) break;
1497
+ /* 父会话可能已经不在清单里(被清理 / 只给了指针),仍然认它的显式记录。 */
1498
+ if (Object.prototype.hasOwnProperty.call(HOSTING.sessions, String(pk))) {
1499
+ return !!HOSTING.sessions[String(pk)];
1500
+ }
1501
+ cur = sessionByAnyId(pk);
1502
+ }
1503
+ return false;
1504
+ }
1505
+ /* 新会话的开关要等 core 给出 session_id 才落得下来(见 mountConversation 的 onEvent)。 */
1506
+ function bindFreshHosting(sid) {
1507
+ if (!HOSTING.fresh || !sid) return;
1508
+ HOSTING.sessions[String(sid)] = true;
1509
+ HOSTING.fresh = false;
1510
+ saveHostingPref();
1511
+ }
1512
+
1412
1513
  /* ══════════ 环境(多连接)══════════
1413
1514
  2026-10-22 用户:「移动端要支持环境切换 可以有多个连接」。
1414
1515
  一台手机可以记住多台桌面(家里 / 公司 / 另一台机器),任务页标题就是切换入口。
@@ -1620,7 +1721,8 @@ button.setrow{font-family:inherit}
1620
1721
  —— 内存态全部重新初始化,不留上一台机器的影子。
1621
1722
  ⚠️ 分桶键用 host:port 而不是 url|token:token 会随 App 重启变,按 token 分桶等于每次都丢现场。 */
1622
1723
  var ENV_SCOPE_KEYS = [
1623
- 'magic.ws', 'magic.taskmode', 'magic.hosting', 'magic.engine', 'magic.model',
1724
+ 'magic.ws', 'magic.taskmode', 'magic.hosting.sessions.v1', 'magic.engine', 'magic.model',
1725
+ 'magic.suggest.engine', 'magic.suggest.model',
1624
1726
  'magic.theme', 'magic.font', 'magic.notify',
1625
1727
  'magic.pinned', 'magic.spaces', 'magic.browser.stars.v1',
1626
1728
  'magic.team.sessions.v1', 'magic.team.routes.v1', 'magic.team.rules.v1',
@@ -1676,7 +1778,8 @@ button.setrow{font-family:inherit}
1676
1778
  · 本地任何一处改了,防抖推回远端。
1677
1779
  纯设备偏好(主题/字体/通知)**不同步** —— 那是手机自己的事。 */
1678
1780
  var PREF_SYNC_KEYS = [
1679
- 'magic.engine', 'magic.model', 'magic.ws', 'magic.spaces', 'magic.pinned',
1781
+ 'magic.engine', 'magic.model', 'magic.suggest.engine', 'magic.suggest.model', 'magic.ws', 'magic.spaces', 'magic.pinned',
1782
+ 'magic.hosting.sessions.v1',
1680
1783
  'magic.team.sessions.v1', 'magic.team.routes.v1', 'magic.team.rules.v1',
1681
1784
  'magic.team.parallel', 'magic.team.split', 'magic.team.splitcode', 'magic.team.adaptive'
1682
1785
  ];
@@ -2061,6 +2164,7 @@ button.setrow{font-family:inherit}
2061
2164
  function showMine() {
2062
2165
  hideAll(); mineEl.hidden = false; tabbarEl.hidden = false; setActive('mine');
2063
2166
  mineDevice.textContent = histSubtitle || '本机';
2167
+ paintSuggestPref();
2064
2168
  var ns = document.getElementById('notifyState');
2065
2169
  if (ns) { var p = notifyPermState(); ns.textContent = p === 'plugin' ? '已接入' : (p === 'granted' ? '已开启' : '未开启'); }
2066
2170
  markSeg(segTheme, PREFS.theme); markSeg(segFont, PREFS.font); markSeg(segMode, TASK_MODE);
@@ -2629,10 +2733,12 @@ button.setrow{font-family:inherit}
2629
2733
  /* 任务模式:parallel(默认,全部并行)| serial(同一空间同时只跑一个新任务)。 */
2630
2734
  var TASK_MODE = 'parallel';
2631
2735
  try { TASK_MODE = localStorage.getItem('magic.taskmode') || 'parallel'; } catch (e) {}
2632
- /* 会话托管(2026-10-23):默认关;跟着环境走(见 ENV_SCOPE_KEYS),
2633
- 同一台手机切到另一台桌面时,两台各自的托管开关互不继承。 */
2634
- var HOSTING = false;
2635
- try { HOSTING = localStorage.getItem('magic.hosting') === '1'; } catch (e) {}
2736
+ /* 会话托管(2026-10-23):跟团队模式一样**挂在会话上**(用户:「跟团队模式一样改成
2737
+ 挂在会话上,子会话继承,不再是全局」)。`sessions` 是 sid → 开/关;`fresh` 只临时装
2738
+ 「新会话还没拿到 session_id 时」的开关(首帧带回 id 就落到 `sessions` 上)。
2739
+ 旧版是全设备一个 `magic.hosting` —— 那正是「改一条会话,所有会话都变」的来源,
2740
+ 不迁移(把全局值抄给每条会话等于把 bug 换个地方),直接清掉。 */
2741
+ var HOSTING = loadHostingPref();
2636
2742
  var PREFS = loadPrefs();
2637
2743
  applyThemePref(PREFS.theme); applyFontPref(PREFS.font);
2638
2744
  paintRunPref();
@@ -2692,7 +2798,8 @@ button.setrow{font-family:inherit}
2692
2798
  宿主这边的筛选只是第一道。 */
2693
2799
  session: p.session,
2694
2800
  attachments: (p.attachments || []).slice(),
2695
- atts: (p.atts || []).slice()
2801
+ atts: (p.atts || []).slice(),
2802
+ hosted: !!p.hosted
2696
2803
  };
2697
2804
  });
2698
2805
  try { return conv.adoptQueue(items) || 0; } catch (e) { return 0; }
@@ -2708,16 +2815,48 @@ button.setrow{font-family:inherit}
2708
2815
  adoptPendingFor(sid);
2709
2816
  });
2710
2817
  }
2711
- /* 会话托管的开关落盘 + 同步「我的」里那枚 toggle。
2818
+ /* 会话托管的开关:**按当前这条会话**落盘(用户 2026-10-23:「跟团队模式一样改成
2819
+ 挂在会话上,子会话继承,不再是全局」)。开关的权威值在这里,组件只执行。
2712
2820
  reason 非空 = 组件自己暂停的(失败 / 被停 / 到上限 / 用户点了「托管中」标记),
2713
- 这里要如实弹一句;宿主自己拨开关时传空串,由调用处给更具体的提示。 */
2714
- function setHostingPref(on, reason) {
2715
- HOSTING = !!on;
2716
- try { localStorage.setItem('magic.hosting', HOSTING ? '1' : '0'); } catch (e) {}
2717
- var tgl = document.getElementById('tglHosting');
2718
- if (tgl) tgl.classList.toggle('is-on', HOSTING);
2821
+ 这里要如实弹一句;宿主自己拨开关时由调用处给更具体的提示。 */
2822
+ function convSessionKey() {
2823
+ var raw = (conv && conv.state && conv.state.session) || convSid || '';
2824
+ var s = raw ? sessionByAnyId(raw) : null;
2825
+ return s ? (sidOf(s) || String(raw || '')) : String(raw || '');
2826
+ }
2827
+ function writeHostingFor(on) {
2828
+ var key = convSessionKey();
2829
+ if (key) {
2830
+ var s = sessionByAnyId(key);
2831
+ /* 同一会话的 run_id ↔ session_id 别名只留一条,避免两个键各说一套。 */
2832
+ if (s) {
2833
+ [s.session_id, s.run_id].forEach(function (k) {
2834
+ if (k && String(k) !== key) delete HOSTING.sessions[String(k)];
2835
+ });
2836
+ }
2837
+ HOSTING.sessions[key] = !!on;
2838
+ HOSTING.fresh = false;
2839
+ } else {
2840
+ HOSTING.fresh = !!on;
2841
+ }
2842
+ saveHostingPref();
2843
+ }
2844
+ function onHostingToggleFromConv(on, reason) {
2845
+ writeHostingFor(!!on);
2719
2846
  if (reason) toast('会话托管已暂停:' + reason);
2720
2847
  }
2848
+ /* 运行设置里那一行:直接翻当前会话的开关(不弹选择器 —— 托管只有开 / 关两档)。 */
2849
+ function toggleHostingHere() {
2850
+ if (!SUGGEST.engine) { toast('先在「我的 → 建议引擎」里选一个引擎,才能开启会话托管'); return; }
2851
+ if (!conv) { toast('会话还没准备好'); return; }
2852
+ var on = typeof conv.hostingOn === 'function' ? !!conv.hostingOn() : false;
2853
+ var next = !on;
2854
+ try { conv.setHosting(next); } catch (e) {}
2855
+ writeHostingFor(next);
2856
+ toast(next
2857
+ ? '会话托管已开启:每轮结束自动发送最合适的建议,提问卡自动选第一项继续'
2858
+ : '会话托管已关闭');
2859
+ }
2721
2860
 
2722
2861
  function mountConversation(sid, s) {
2723
2862
  /* 同一条会话、同一个空间:组件还活着,画面也还在 —— 直接把这一屏端回来。
@@ -2733,6 +2872,7 @@ button.setrow{font-family:inherit}
2733
2872
  /* same:true —— 上面已经确认是同一屏会话。组件按字面判 id 会把别名当成
2734
2873
  「换了会话」→ clearQueue() 删掉排队追问(本次报障的病根之一)。 */
2735
2874
  try { conv.setSession(sid, s, { same: true }); } catch (e) {}
2875
+ if (typeof conv.setSuggestEngine === 'function') conv.setSuggestEngine(SUGGEST.engine, SUGGEST.model);
2736
2876
  window.magicConv = conv;
2737
2877
  return;
2738
2878
  }
@@ -2741,14 +2881,18 @@ button.setrow{font-family:inherit}
2741
2881
  convSid = sid || null;
2742
2882
  convWs = WS;
2743
2883
  /* 每次重建组件都丢掉「上一条还没落定的新会话」的临时开关 —— 否则用户在新会话里
2744
- 开了团队但没发消息,转去打开一条历史会话时,那条会话的首帧会把开关误记成开启。
2745
- 老会话的开关由 teamOnFor 从它自己那份记录里取;新会话从「关」起。 */
2884
+ 开了团队 / 托管但没发消息,转去打开一条历史会话时,那条会话的首帧会把开关误记成开启。
2885
+ 老会话的开关由 teamOnFor / hostingOnFor 从它自己那份记录里取;新会话从「关」起。 */
2746
2886
  TEAM.fresh = false;
2887
+ HOSTING.fresh = false;
2747
2888
  try {
2748
2889
  var transport = window.MagicTransportHttp({ base: BASE, token: TOKEN, client: 'mobile' });
2749
2890
  conv = window.MagicConversation.create({
2750
2891
  el: convEl, transport: transport, titleEl: titleEl, workspace: WS,
2751
2892
  engine: RUN_PREF.engine, model: RUN_PREF.model,
2893
+ /* 建议功能默认关闭:只有「我的 → 建议引擎」里显式选了引擎才传非空值;
2894
+ 模型为空 = 引擎默认(core 对 llm 会自动挑最快变体)。 */
2895
+ suggestEngine: SUGGEST.engine, suggestModel: SUGGEST.model,
2752
2896
  showPermission: false, showAttach: true, beforeAsk: gateAsk,
2753
2897
  onInterrupt: cancelWaitingAsk,
2754
2898
  /* 组件内排队也要落库(2026-10-21 用户:「非团队的串行排队也没落库」):
@@ -2759,9 +2903,10 @@ button.setrow{font-family:inherit}
2759
2903
  onQueueTake: queueTaken,
2760
2904
  onQueueDrop: queueDropped,
2761
2905
  onPick: rememberRunPick,
2762
- /* 会话托管:开关值由宿主给(权威在 localStorage),自动暂停时组件回调这里同步。 */
2763
- hosting: HOSTING,
2764
- onHostingChange: setHostingPref,
2906
+ /* 会话托管:开关初值按**这条会话**给(自己没有记录就沿父链继承,见 hostingOnFor);
2907
+ 组件自己暂停时回调这里把这条会话的开关关掉。 */
2908
+ hosting: hostingOnFor(sid, s),
2909
+ onHostingChange: onHostingToggleFromConv,
2765
2910
  /* 团队模式:开关初值按**这条会话**给;配置是设备偏好(每轮现读,改配置立刻生效)。
2766
2911
  features 也给函数:能力清单是异步拉到的(/desk/health),
2767
2912
  查到之后开关才会亮出来,不必重新挂载组件。 */
@@ -2778,9 +2923,12 @@ button.setrow{font-family:inherit}
2778
2923
  };
2779
2924
  },
2780
2925
  features: function () { return CORE_FEATURES; },
2781
- /* 新会话的开关先记在 TEAM.fresh:core 的首帧带回 session_id 时,当场绑到这条会话上
2782
- (否则返回列表再点进来就找不到它了)。 */
2783
- onEvent: function (ev) { bindFreshTeam(ev && ev.session_id); },
2926
+ /* 新会话的开关先记在 TEAM.fresh / HOSTING.fresh:core 的首帧带回 session_id 时,
2927
+ 当场绑到这条会话上(否则返回列表再点进来就找不到它了)。 */
2928
+ onEvent: function (ev) {
2929
+ bindFreshTeam(ev && ev.session_id);
2930
+ bindFreshHosting(ev && ev.session_id);
2931
+ },
2784
2932
  /* 菜单里的「定时任务」还在,但底部那个 tab 已经删了(2026-10-22 用户:
2785
2933
  「定时任务tab先删除」)。现在点它给一句说明,而不是跳去一张只有一个占位符的页 ——
2786
2934
  原先那页的正文就是这句说明,页面本身没有承载任何数据。
@@ -2805,6 +2953,39 @@ button.setrow{font-family:inherit}
2805
2953
  convEl.appendChild(d);
2806
2954
  }
2807
2955
  }
2956
+ /* ── 进详情时补拉这一条的状态(2026-10-23 用户:「白色会话进会话详情时应该再拉一波更新状态」)──
2957
+ 列表里的 status 是**进列表那一刻**的快照;而 core 的 `--sessions` 读侧还会把「记录写着
2958
+ 在跑、进程其实已经不在」的行当场归一成 gone。用户点开一条**没有颜色**的会话(既不是
2959
+ 运行中 / 等审批,也不是已完成 / 失败 / 已取消)时,多半就是这份快照已经过期。
2960
+ 这里只补这一条:拉一次登记表、按 id 别名(session_id ↔ run_id)对上就地更新,
2961
+ 状态真的变了才重绘。失败静默 —— 它是补刀,不是主路径。 */
2962
+ function refreshRowStatus(sid) {
2963
+ if (!sid || !TOKEN) return;
2964
+ var seq = CONN_SEQ;
2965
+ fetch(BASE + '/desk/sessions', { headers: { 'X-Magic-Token': TOKEN } })
2966
+ .then(function (r) { return r.ok ? r.json() : null; })
2967
+ .then(function (list) {
2968
+ if (seq !== CONN_SEQ || !Array.isArray(list)) return;
2969
+ var row = null, cur = null, i;
2970
+ for (i = 0; i < list.length; i++) {
2971
+ var x = list[i];
2972
+ if (!x) continue;
2973
+ if (sameConvId(x.session_id, sid) || sameConvId(x.run_id, sid)) { row = x; break; }
2974
+ }
2975
+ if (!row) return;
2976
+ for (i = 0; i < LAST_SESSIONS.length; i++) {
2977
+ if (sameConvId(sidOf(LAST_SESSIONS[i]), sid)) { cur = LAST_SESSIONS[i]; break; }
2978
+ }
2979
+ if (!cur) return;
2980
+ var was = statusOf(cur);
2981
+ if (row.status) cur.status = row.status;
2982
+ if (row.state) cur.state = row.state;
2983
+ cur.alive = row.alive === true;
2984
+ if (row.updated_at) cur.updated_at = row.updated_at;
2985
+ if (statusOf(cur) !== was) renderSessions(LAST_SESSIONS);
2986
+ })
2987
+ .catch(function () { /* 拉不到就保持原样 */ });
2988
+ }
2808
2989
  function openSession(sid, s) {
2809
2990
  /* ⚠️ 一律以会话自己的归属为准,**不能**只在其有 workspace 时才改 WS(2026-10-21):
2810
2991
  原写法 `if (s && s.workspace) { WS = s.workspace; }` 会让「打开一条默认空间的会话」
@@ -2814,6 +2995,9 @@ button.setrow{font-family:inherit}
2814
2995
  saveWs();
2815
2996
  showSession(s && s.workspace ? basename(s.workspace) : histSubtitle);
2816
2997
  mountConversation(sid, s);
2998
+ /* 灰白档 = 状态未知(或列表快照过期):进详情时补拉一次,见 refreshRowStatus。
2999
+ 其它档(运行中 / 等审批 / 已完成 / 失败 / 已取消)列表已经说清楚了,不必再打一发。 */
3000
+ if (statusOf(s) === 'idle') refreshRowStatus(sid);
2817
3001
  }
2818
3002
  /* ws:不传 = 默认(不属于任何空间);传字符串 = 在该空间里新建(列表里空间组的「+」)。
2819
3003
  ⚠️ 「新建任务」大按钮必须从**默认**起(2026-10-21 用户:「大的新建会话 空间应该是
@@ -3265,12 +3449,24 @@ button.setrow{font-family:inherit}
3265
3449
  return h;
3266
3450
  }
3267
3451
  /* 会话状态:由 /desk/sessions 的 status(会话级统一状态,见 core 的 session-status.json)
3268
- 映射;老版本 CLI 不给 status 时回落 state。running / waiting_approval / done / 其它。 */
3452
+ 映射;老版本 CLI 不给 status 时回落 state。
3453
+ 词表与 core 的 session.Record.State 一一对应(不再是「除了三种都算灰色」):
3454
+ running / alive → run 蓝 + 扫光环
3455
+ waiting_approval → wait 琥珀(活着的等待)
3456
+ done / idle → done 绿(idle = 常驻会话本轮跑完、进程还在等追加,
3457
+ 与 PC 左栏的「已完成」同口径)
3458
+ failed / gone → fail 红(gone = 记录写着在跑、进程已不在)
3459
+ cancelled / interrupted → stop 石褐(人为收工 / 被打断,不是故障)
3460
+ 其它(空 / 陌生值) → idle 灰白兜底
3461
+ ⚠️ 2026-10-23 之前 failed / cancelled / gone 全落进 idle,列表上是一片白 ——
3462
+ 用户分不清「正常结束」和「挂了」(报障:「这两个会话为什么是白色」)。 */
3269
3463
  function statusOf(s) {
3270
3464
  var st = String((s && (s.status || s.state)) || '');
3271
3465
  if (st === 'running' || s.alive === true) return 'run';
3272
3466
  if (st === 'waiting_approval') return 'wait';
3273
- if (st === 'done') return 'done';
3467
+ if (st === 'done' || st === 'idle') return 'done';
3468
+ if (st === 'failed' || st === 'gone') return 'fail';
3469
+ if (st === 'cancelled' || st === 'interrupted' || st === 'stopped') return 'stop';
3274
3470
  return 'idle';
3275
3471
  }
3276
3472
  /* 会话行图标:团队会话=双人图形(mode:"team"),其余**设备形状按来源端**
@@ -3854,6 +4050,11 @@ button.setrow{font-family:inherit}
3854
4050
  if (permBtn) permBtn.classList.toggle('setrow--danger', !!(conv && conv.permissionTier && conv.permissionTier() === 'full'));
3855
4051
  var t = document.getElementById('rsTeam');
3856
4052
  if (t) t.textContent = (conv && conv.teamLabel) ? conv.teamLabel() : '关闭';
4053
+ /* 会话托管(2026-10-23,按会话):与团队模式并列在同一张卡里。
4054
+ 标签读**组件此刻的真实状态**(子会话可能是从父会话继承来的开启值)。 */
4055
+ var h = document.getElementById('rsHosting');
4056
+ if (h) h.textContent = !SUGGEST.engine ? '未配置建议引擎'
4057
+ : ((conv && typeof conv.hostingOn === 'function' && conv.hostingOn()) ? '开启' : '关闭');
3857
4058
  /* 团队模式:与权限模式并列(用户 2026-10-21:「团队模式开关在界面右上角跟权限模式一样」)。
3858
4059
  core 没有这个能力位时**不显示**这一行 —— 显示一个点了必然失败的开关是最坏的做法。 */
3859
4060
  var showTeam = !conv || typeof conv.featureOn !== 'function' || conv.featureOn('feature.session.team');
@@ -3923,6 +4124,9 @@ button.setrow{font-family:inherit}
3923
4124
  /* team:重发时原样还原团队配置(见 fireQueued)——
3924
4125
  丢了它这条排队任务就会从「团队」静默降级成单任务。 */
3925
4126
  team: info.team || null,
4127
+ /* hosted:托管自动发的那一轮被串行门挡住 / 重启恢复时要原样还原,
4128
+ 否则重发出去的历史事件会退回「用户亲手发的」画法。 */
4129
+ hosted: !!info.hosted,
3926
4130
  attachments: (info.attachments || []).slice(),
3927
4131
  /* atts:气泡缩略图的快照({name, remoteUrl},见 conversation.js 的 notifyQueue)。
3928
4132
  重进会话时 adoptPendingFor 用它把排队气泡原样画回来 —— 只存路径的话恢复出来会丢图。 */
@@ -4301,6 +4505,8 @@ button.setrow{font-family:inherit}
4301
4505
  /* 团队配置是在发送那一刻快照下来的(见 addPending):重发必须带上,
4302
4506
  否则这条排队任务会被当成普通单任务跑。 */
4303
4507
  if (p.team) payload.team = p.team;
4508
+ /* 托管自动发的那一轮:重发也要把标识带上(历史回放才能继续画成「托管发送」)。 */
4509
+ if (p.hosted) payload.hosted = true;
4304
4510
  if (p.attachments && p.attachments.length) payload.attachments = p.attachments;
4305
4511
  return new Promise(function (resolve) {
4306
4512
  var done = false, handle = null, t0 = Date.now();
@@ -4570,6 +4776,40 @@ button.setrow{font-family:inherit}
4570
4776
  会话里输入框上方的级联切换也会回写这里(见 mountConversation 的 onPick)——
4571
4777
  与 PC 端「选过一次就一直用」的语义一致,「我的」里显示的就是下次新建会用的那一组。
4572
4778
  ⚠️ 模型必须挂在引擎下:engine 为空时 model 一律清空,不留孤儿模型。 */
4779
+ /* 建议引擎:只认**显式配置过**的值;没有 / 空串 = 不开启建议功能(默认)。
4780
+ 不能回落到 RUN_PREF.engine —— 那等于「配了默认编码引擎就自动有建议」,
4781
+ 与用户「默认不配置就不开启」相反。 */
4782
+ function loadSuggestPref() {
4783
+ var o = { engine: '', model: '' };
4784
+ try {
4785
+ o.engine = String(localStorage.getItem('magic.suggest.engine') || '').trim();
4786
+ o.model = String(localStorage.getItem('magic.suggest.model') || '').trim();
4787
+ } catch (e) {}
4788
+ if (!o.engine) o.model = '';
4789
+ return o;
4790
+ }
4791
+ function saveSuggestPref() {
4792
+ try {
4793
+ localStorage.setItem('magic.suggest.engine', SUGGEST.engine || '');
4794
+ localStorage.setItem('magic.suggest.model', SUGGEST.model || '');
4795
+ } catch (e) {}
4796
+ schedulePushPrefs();
4797
+ paintSuggestPref();
4798
+ }
4799
+ function paintSuggestPref() {
4800
+ var e = document.getElementById('suggestEngine');
4801
+ var m = document.getElementById('suggestModel');
4802
+ if (e) e.textContent = SUGGEST.engine || '不开启';
4803
+ if (m) m.textContent = SUGGEST.engine ? (qaModelText(SUGGEST.model) || '引擎默认') : '跟随引擎';
4804
+ }
4805
+ function applySuggestToLive() {
4806
+ if (conv && typeof conv.setSuggestEngine === 'function') {
4807
+ conv.setSuggestEngine(SUGGEST.engine, SUGGEST.model);
4808
+ }
4809
+ paintSuggestPref();
4810
+ paintRunSettings();
4811
+ }
4812
+
4573
4813
  function loadRunPref() {
4574
4814
  var o = { engine: '', model: '' };
4575
4815
  try {
@@ -4945,6 +5185,67 @@ button.setrow{font-family:inherit}
4945
5185
  el.style.textAlign = 'left';
4946
5186
  return el;
4947
5187
  }
5188
+ /* 「我的 → 建议引擎」:单列,首项「不开启」。不展示模型 —— 建议是纯文本短任务,
5189
+ 选模型只会让界面更复杂;core 对 llm 会自动挑不思考的变体。 */
5190
+ /* 「我的 → 建议引擎 / 模型」:与默认引擎同一个二列抽屉(左引擎 · 右模型)。
5191
+ 首项「不开启」;点左列只浏览、点右列才落定。llm 放最前,模型为空时 core 自动挑最快变体。 */
5192
+ function openSuggestEnginePicker() {
5193
+ openHostSheet('建议引擎 / 模型', function (body) {
5194
+ body.innerHTML = '';
5195
+ var hint = hEl('div', 'history__hint', '正在探测引擎与模型…');
5196
+ hint.style.textAlign = 'left';
5197
+ body.appendChild(hint);
5198
+ fetchEngines().then(function (list) {
5199
+ if (hint.parentNode) hint.remove();
5200
+ var stale = enginesStaleHint();
5201
+ if (stale) body.appendChild(stale);
5202
+ var avail = (list || []).filter(function (x) { return x && x.ok; });
5203
+ /* 推荐的放最上面:llm 是建议这条旁路最快的引擎。 */
5204
+ avail.sort(function (a, b) {
5205
+ if (a.engine === 'llm') return -1;
5206
+ if (b.engine === 'llm') return 1;
5207
+ return String(a.engine).localeCompare(String(b.engine));
5208
+ });
5209
+ if (!avail.length) {
5210
+ var d = hEl('div', 'history__hint',
5211
+ '没有可用引擎:先在这台机器上装一个(终端跑 magic-agent --engines 会给出安装命令)。');
5212
+ d.style.textAlign = 'left';
5213
+ body.appendChild(d);
5214
+ return;
5215
+ }
5216
+ buildEngineCascade(body, avail, {
5217
+ prefs: SUGGEST,
5218
+ pick: pickSuggestPref,
5219
+ offLabel: '不开启',
5220
+ offDesc: '不生成下一轮建议(默认)',
5221
+ modelDefaultDesc: '不指定模型(llm 会自动挑最快变体)',
5222
+ });
5223
+ /* 建议生成一律不思考。各引擎都走官方关闭参数;某些模型路由不支持时
5224
+ 才在超时/报错后降级 llm,不预先吞掉用户的选择。 */
5225
+ var nk = hEl('div', 'history__hint',
5226
+ '建议生成固定关闭思考:claude --thinking disabled、codex effort none、llm no-think / reasoning_effort、codebuddy alwaysThinkingEnabled=false、dsh reasoningEffort=off;引擎失败才降级 llm。');
5227
+ nk.style.textAlign = 'left';
5228
+ body.appendChild(nk);
5229
+ }, function (e) {
5230
+ hint.textContent = '拉取引擎清单失败:' + ((e && e.message) || e);
5231
+ var retry = hEl('button', 'pick__citem', '重试');
5232
+ retry.type = 'button';
5233
+ retry.onclick = function () { openSuggestEnginePicker(); };
5234
+ body.appendChild(retry);
5235
+ });
5236
+ });
5237
+ }
5238
+ function pickSuggestPref(engine, model) {
5239
+ SUGGEST.engine = String(engine || '').trim();
5240
+ SUGGEST.model = (SUGGEST.engine && model) ? String(model).trim() : '';
5241
+ saveSuggestPref();
5242
+ applySuggestToLive();
5243
+ closeHostSheet();
5244
+ toast(SUGGEST.engine
5245
+ ? ('建议引擎:' + SUGGEST.engine + ' · ' + (qaModelText(SUGGEST.model) || '引擎默认模型'))
5246
+ : '已关闭建议功能');
5247
+ }
5248
+
4948
5249
  function openDefaultEnginePicker() {
4949
5250
  openHostSheet('默认引擎 / 模型', function (body) {
4950
5251
  function load() {
@@ -4977,8 +5278,16 @@ button.setrow{font-family:inherit}
4977
5278
  });
4978
5279
  }
4979
5280
  /* 二列级联(左引擎 · 右该引擎的模型),与对话框里那套同构:
4980
- 点左列只切右列,点右列才落定 —— 避免「引擎换了、模型还挂在旧引擎下」。 */
4981
- function buildEngineCascade(body, avail) {
5281
+ 点左列只切右列,点右列才落定 —— 避免「引擎换了、模型还挂在旧引擎下」。
5282
+ 默认引擎与建议引擎共用;o 可换目标 / 落定回调 / 关闭项文案。 */
5283
+ function buildEngineCascade(body, avail, o) {
5284
+ o = o || {};
5285
+ var prefs = o.prefs || RUN_PREF;
5286
+ var pick = o.pick || pickRunPref;
5287
+ var offLabel = o.offLabel || '自动';
5288
+ var offDesc = o.offDesc || '首个可用引擎';
5289
+ var modelDefaultDesc = o.modelDefaultDesc || '不指定模型,由引擎自己挑';
5290
+
4982
5291
  var wrap = hEl('div', 'pick__cols');
4983
5292
  var left = hEl('div', 'pick__col');
4984
5293
  var right = hEl('div', 'pick__col pick__col--r');
@@ -4986,24 +5295,24 @@ button.setrow{font-family:inherit}
4986
5295
  body.appendChild(wrap);
4987
5296
 
4988
5297
  var cur = avail[0];
4989
- avail.forEach(function (en) { if (en.engine === RUN_PREF.engine) cur = en; });
5298
+ avail.forEach(function (en) { if (en.engine === prefs.engine) cur = en; });
4990
5299
 
4991
5300
  function paint() {
4992
5301
  left.innerHTML = ''; right.innerHTML = '';
4993
- var auto = hEl('button', 'pick__citem' + (!RUN_PREF.engine ? ' is-on' : ''));
4994
- auto.type = 'button';
4995
- auto.appendChild(hEl('span', 'pick__cl', '自动'));
4996
- auto.appendChild(hEl('span', 'pick__cd', '首个可用引擎'));
4997
- auto.onclick = function () { pickRunPref('', ''); };
4998
- left.appendChild(auto);
5302
+ var off = hEl('button', 'pick__citem' + (!prefs.engine ? ' is-on' : ''));
5303
+ off.type = 'button';
5304
+ off.appendChild(hEl('span', 'pick__cl', offLabel));
5305
+ off.appendChild(hEl('span', 'pick__cd', offDesc));
5306
+ off.onclick = function () { pick('', ''); };
5307
+ left.appendChild(off);
4999
5308
 
5000
5309
  avail.forEach(function (en) {
5001
5310
  var n = (en.models || []).length;
5002
- /* is-on = 已保存的默认(浅蓝实底);is-cur = 右列此刻在浏览谁(左侧细条,无底色)——
5003
- 两者分开,默认是「自动」时不会出现两个一样的选中块。 */
5311
+ /* is-on = 已保存的值(浅蓝实底);is-cur = 右列此刻在浏览谁(左侧细条,无底色)——
5312
+ 两者分开,关闭项选中时不会出现两个一样的选中块。 */
5004
5313
  var b = hEl('button', 'pick__citem'
5005
- + (RUN_PREF.engine === en.engine ? ' is-on' : '')
5006
- + (en.engine === cur.engine && RUN_PREF.engine !== en.engine ? ' is-cur' : ''));
5314
+ + (prefs.engine === en.engine ? ' is-on' : '')
5315
+ + (en.engine === cur.engine && prefs.engine !== en.engine ? ' is-cur' : ''));
5007
5316
  b.type = 'button';
5008
5317
  b.appendChild(hEl('span', 'pick__cl', en.engine));
5009
5318
  b.appendChild(hEl('span', 'pick__cd', n ? (n + ' 个模型') : '引擎默认'));
@@ -5011,20 +5320,20 @@ button.setrow{font-family:inherit}
5011
5320
  left.appendChild(b);
5012
5321
  });
5013
5322
 
5014
- var d = hEl('button', 'pick__citem' + (RUN_PREF.engine === cur.engine && !RUN_PREF.model ? ' is-on' : ''));
5323
+ var d = hEl('button', 'pick__citem' + (prefs.engine === cur.engine && !prefs.model ? ' is-on' : ''));
5015
5324
  d.type = 'button';
5016
5325
  d.appendChild(hEl('span', 'pick__cl', '引擎默认'));
5017
- d.appendChild(hEl('span', 'pick__cd', '不指定模型,由引擎自己挑'));
5018
- d.onclick = function () { pickRunPref(cur.engine, ''); };
5326
+ d.appendChild(hEl('span', 'pick__cd', modelDefaultDesc));
5327
+ d.onclick = function () { pick(cur.engine, ''); };
5019
5328
  right.appendChild(d);
5020
5329
 
5021
5330
  (cur.models || []).forEach(function (mm) {
5022
- var b = hEl('button', 'pick__citem' + (RUN_PREF.engine === cur.engine && RUN_PREF.model === mm ? ' is-on' : ''));
5331
+ var b = hEl('button', 'pick__citem' + (prefs.engine === cur.engine && prefs.model === mm ? ' is-on' : ''));
5023
5332
  b.type = 'button';
5024
- /* 显示剥掉 `custom-local:` 前缀(`mm` 本身是完整 id,选中时原样存进 RUN_PREF),
5333
+ /* 显示剥掉 `custom-local:` 前缀(`mm` 本身是完整 id,选中时原样存进 prefs),
5025
5334
  倍率挂同行右侧。 */
5026
5335
  modelItem(b, cur.engine, mm);
5027
- b.onclick = function () { pickRunPref(cur.engine, mm); };
5336
+ b.onclick = function () { pick(cur.engine, mm); };
5028
5337
  right.appendChild(b);
5029
5338
  });
5030
5339
  }
@@ -5121,6 +5430,8 @@ button.setrow{font-family:inherit}
5121
5430
  /* 权限 / 团队沿用会话组件里那唯一一份选择器(浮层盖在本页之上);选完就地重画。 */
5122
5431
  on('btnRsPermission', function () { if (conv && conv.openPermissionPicker) conv.openPermissionPicker(); });
5123
5432
  on('btnRsTeam', function () { if (conv && conv.openTeamPicker) conv.openTeamPicker(); });
5433
+ /* 会话托管(2026-10-23,按会话):只有开 / 关两档,直接翻当前这条会话。 */
5434
+ on('btnRsHosting', toggleHostingHere);
5124
5435
  /* 选择器由会话组件管理、没有回调通知宿主,所以用一次点击兜底重画:
5125
5436
  本页可见时任何一次点击(包括选择器里点中一行)之后重读状态。 */
5126
5437
  document.addEventListener('click', function () {
@@ -5190,6 +5501,8 @@ button.setrow{font-family:inherit}
5190
5501
  on('btnNotify', openNotifySettings);
5191
5502
  on('btnDefaultEngine', openDefaultEnginePicker);
5192
5503
  on('btnDefaultModel', openDefaultEnginePicker);
5504
+ on('btnSuggestEngine', openSuggestEnginePicker);
5505
+ on('btnSuggestModel', openSuggestEnginePicker);
5193
5506
  /* 团队模式配置(「我的」→ 子页面):计划 + 7 个角色,行由 paintTeamRoles 动态渲染,
5194
5507
  行的点击在渲染时绑定(见 addTeamRouteRow / addTeamRuleRow),这里只留入口与返回。 */
5195
5508
  on('btnTeamPage', showTeamPage);
@@ -5201,20 +5514,9 @@ button.setrow{font-family:inherit}
5201
5514
  if (segFont) takeSeg(segFont, 'magic.font', function (v) { PREFS.font = v; applyFontPref(v); });
5202
5515
  if (segMode) takeSeg(segMode, 'magic.taskmode', function (v) { TASK_MODE = v; });
5203
5516
 
5204
- /* 会话托管开关(我的 tab)。拨动只改「接下来的轮次」:
5205
- 正在跑的这一轮不受影响,收尾后若打开就自动接上。 */
5206
- var tglHosting = document.getElementById('tglHosting');
5207
- if (tglHosting) {
5208
- tglHosting.classList.toggle('is-on', HOSTING);
5209
- tglHosting.onclick = function () {
5210
- var next = !HOSTING;
5211
- setHostingPref(next, '');
5212
- if (conv) { try { conv.setHosting(next); } catch (e) {} }
5213
- toast(next
5214
- ? '会话托管已开启:每轮结束自动发送最合适的建议,提问卡自动选第一项继续'
5215
- : '会话托管已关闭');
5216
- };
5217
- }
5517
+ /* 会话托管(2026-10-23 起按会话):入口在会话页 → 任务运行设置(见 btnRsHosting)。
5518
+ 「我的」里不再有全局开关 —— 曾经那枚 `tglHosting` 写的是设备级 `magic.hosting`,
5519
+ 改一条会话会把所有会话都改掉(用户 2026-10-23:「跟团队模式一样…不再是全局」)。 */
5218
5520
 
5219
5521
  /* ══════════ 网络代理(VPN 隧道)══════════
5220
5522
  把整机流量经隧道送到桌面端的 SOCKS5 出口:TUN → 内核 → 桌面端 → 互联网。