magic-agent 2.4.3 → 2.6.3
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.
- package/CLIENT-CONTRACT.md +467 -41
- package/README.md +15 -14
- package/client-ui/pc/app.css +74 -0
- package/client-ui/pc/js/04-task-list.js +5 -1
- package/client-ui/pc/js/07-chat-stream-render.js +3 -3
- package/client-ui/pc/js/90-glue.js +40 -2
- package/client-ui/pc/js/92-composer.js +19 -4
- package/client-ui/pc/js/93-rail.js +16 -5
- package/client-ui/shared/conv/02-media-kinds.js +2 -2
- package/client-ui/shared/conv/03-chat-message.js +247 -23
- package/client-ui/shared/conv/06-ask-answer.js +75 -1
- package/client-ui/shared/conv/09-stream-apply.js +72 -7
- package/client-ui/shared/conv/19-attachments.js +157 -6
- package/client-ui/shared/conv/27-conv-ledger.js +5 -2
- package/client-ui/shared/conv/29-msg-variants.js +217 -0
- package/client-ui/shared/conv/conv.css +329 -174
- package/client-ui/shared/conv/dom-map.md +1 -0
- package/client-ui/shared/conversation.css +13 -2
- package/client-ui/shared/conversation.js +373 -50
- package/client-ui/shared/transport-http.js +14 -0
- package/client-ui-mobile/apps.css +16 -0
- package/client-ui-mobile/apps.js +406 -0
- package/client-ui-mobile/index.html +5554 -639
- package/client-ui-mobile/reqs.css +110 -1
- package/client-ui-mobile/reqs.js +533 -74
- package/client-ui-mobile/reqshot.js +505 -0
- package/client-ui-mobile/scripts.css +67 -0
- package/client-ui-mobile/scripts.js +599 -0
- package/client-ui-mobile/vendor/cose-base.js +3214 -0
- package/client-ui-mobile/vendor/cytoscape-fcose.js +1549 -0
- package/client-ui-mobile/vendor/cytoscape.min.js +32 -0
- package/client-ui-mobile/vendor/layout-base.js +5230 -0
- package/cmd/magic-client/browsectl.go +199 -0
- package/cmd/magic-client/clientctl.go +69 -10
- package/cmd/magic-client/main.go +210 -32
- package/internal/agent/ask.go +45 -0
- package/internal/agent/ask_test.go +39 -0
- package/internal/agent/attachments_test.go +6 -2
- package/internal/agent/capability.go +7 -1
- package/internal/agent/claude.go +20 -15
- package/internal/agent/codebuddy.go +125 -34
- package/internal/agent/codebuddy_account_test.go +54 -0
- package/internal/agent/codebuddy_stream_test.go +103 -0
- package/internal/agent/credits.go +713 -0
- package/internal/agent/credits_minimax_test.go +97 -0
- package/internal/agent/credits_test.go +211 -0
- package/internal/agent/custom_models.go +155 -0
- package/internal/agent/custom_models_test.go +125 -0
- package/internal/agent/dsh.go +123 -49
- package/internal/agent/dsh_caps_cache_test.go +184 -0
- package/internal/agent/dsh_resume.go +157 -7
- package/internal/agent/dsh_test.go +108 -6
- package/internal/agent/engine.go +37 -1
- package/internal/agent/engine_base.go +21 -17
- package/internal/agent/engine_base_test.go +2 -2
- package/internal/agent/engine_test.go +47 -86
- package/internal/agent/env.go +173 -1
- package/internal/agent/env_test.go +114 -0
- package/internal/agent/fallback.go +313 -0
- package/internal/agent/fallback_test.go +340 -0
- package/internal/agent/launch.go +43 -1
- package/internal/agent/limitnotice.go +207 -1
- package/internal/agent/limitnotice_test.go +140 -0
- package/internal/agent/llmhttp.go +46 -4
- package/internal/agent/llmhttp_test.go +157 -0
- package/internal/agent/models.go +53 -1
- package/internal/agent/models_test.go +20 -5
- package/internal/agent/pidalive_windows_test.go +40 -0
- package/internal/agent/runcmd.go +26 -0
- package/internal/agent/runcmd_windows.go +36 -5
- package/internal/agent/stream.go +166 -6
- package/internal/agent/stream_test.go +25 -38
- package/internal/agent/stream_timeline_test.go +2 -37
- package/internal/agent/streamjson.go +119 -7
- package/internal/agent/tools_suffix_test.go +6 -3
- package/internal/agent/trae.go +50 -243
- package/internal/agent/trae_exec.go +433 -0
- package/internal/agent/trae_exec_test.go +181 -0
- package/internal/agent/wraplaunch_test.go +113 -0
- package/internal/cli/ask.go +168 -37
- package/internal/cli/ask_test.go +1 -1
- package/internal/cli/checkin.go +77 -0
- package/internal/cli/clientcmd.go +3 -2
- package/internal/cli/control.go +46 -5
- package/internal/cli/control_test.go +65 -0
- package/internal/cli/engines_test.go +103 -0
- package/internal/cli/events.go +15 -0
- package/internal/cli/events_test.go +50 -0
- package/internal/cli/judge.go +104 -0
- package/internal/cli/quotas.go +125 -0
- package/internal/cli/requirements.go +51 -0
- package/internal/cli/root.go +157 -11
- package/internal/cli/session.go +75 -3
- package/internal/cli/session_test.go +82 -3
- package/internal/cli/sessionlog.go +6 -0
- package/internal/cli/sessionlog_test.go +22 -2
- package/internal/cli/suggest.go +7 -5
- package/internal/cli/team.go +37 -9
- package/internal/cli/team_test.go +39 -0
- package/internal/client/ab.go +372 -0
- package/internal/client/ab_test.go +287 -0
- package/internal/client/apk.go +8 -0
- package/internal/client/apk_test.go +59 -0
- package/internal/client/apps.go +1078 -0
- package/internal/client/apps_test.go +516 -0
- package/internal/client/browse.go +84 -36
- package/internal/client/browse_test.go +107 -9
- package/internal/client/browseinput.go +481 -0
- package/internal/client/browseinput_test.go +216 -0
- package/internal/client/corecmd.go +23 -0
- package/internal/client/corecmd_other.go +8 -0
- package/internal/client/corecmd_windows.go +24 -0
- package/internal/client/coreexe.go +196 -0
- package/internal/client/coreexe_test.go +161 -0
- package/internal/client/coreupgrade.go +866 -0
- package/internal/client/coreupgrade_test.go +561 -0
- package/internal/client/daemon.go +297 -33
- package/internal/client/daemon_test.go +33 -0
- package/internal/client/engines_checkin_test.go +57 -0
- package/internal/client/fallback_flags_test.go +64 -0
- package/internal/client/feishu.go +1259 -0
- package/internal/client/feishu_test.go +649 -0
- package/internal/client/health_identity_test.go +84 -0
- package/internal/client/hostenv.go +68 -0
- package/internal/client/hostenv_test.go +37 -0
- package/internal/client/jobs.go +518 -24
- package/internal/client/jobs_auto.go +571 -0
- package/internal/client/jobs_auto_test.go +431 -0
- package/internal/client/jobs_test.go +299 -2
- package/internal/client/knowledge.go +854 -0
- package/internal/client/knowledge_test.go +510 -0
- package/internal/client/lan.go +3 -3
- package/internal/client/plugins.go +5 -1
- package/internal/client/prefs.go +67 -0
- package/internal/client/procstart_linux.go +59 -0
- package/internal/client/publicurl.go +82 -0
- package/internal/client/quotas_merge_test.go +107 -0
- package/internal/client/requirements.go +207 -2
- package/internal/client/requirements_shots_test.go +200 -0
- package/internal/client/requirements_test.go +103 -0
- package/internal/client/restart.go +288 -0
- package/internal/client/restart_handoff_test.go +230 -0
- package/internal/client/restart_test.go +290 -0
- package/internal/client/scripts.go +1047 -0
- package/internal/client/scripts_decode_implausible.go +38 -0
- package/internal/client/scripts_decode_other.go +31 -0
- package/internal/client/scripts_decode_test.go +130 -0
- package/internal/client/scripts_decode_windows.go +182 -0
- package/internal/client/scripts_test.go +739 -0
- package/internal/client/service.go +717 -97
- package/internal/client/service_test.go +217 -1
- package/internal/client/sessions_cache.go +148 -0
- package/internal/client/sessions_cache_test.go +163 -0
- package/internal/client/sessions_fold.go +165 -0
- package/internal/client/sessions_fold_test.go +204 -0
- package/internal/client/singleinstance.go +470 -0
- package/internal/client/singleinstance_test.go +607 -0
- package/internal/client/suggest.go +105 -2
- package/internal/client/suggest_test.go +86 -0
- package/internal/client/team_flags.go +25 -11
- package/internal/client/team_flags_test.go +39 -0
- package/internal/client/testmain_test.go +88 -0
- package/internal/client/workspace_default_test.go +158 -0
- package/internal/client/workspacelockenv_test.go +54 -0
- package/internal/config/config.go +48 -0
- package/internal/judge/judge.go +315 -0
- package/internal/requirements/bootstrap.go +16 -0
- package/internal/requirements/consolidate.go +100 -9
- package/internal/requirements/doc.go +154 -3
- package/internal/requirements/graph.go +138 -0
- package/internal/requirements/memory.go +7 -0
- package/internal/requirements/modules.go +312 -0
- package/internal/requirements/modules_test.go +373 -0
- package/internal/requirements/relations.go +155 -0
- package/internal/requirements/relations_test.go +231 -0
- package/internal/requirements/requirements.go +10 -0
- package/internal/requirements/shots.go +418 -0
- package/internal/requirements/shots_test.go +387 -0
- package/internal/session/multiclient_test.go +368 -0
- package/internal/session/session.go +175 -11
- package/internal/session/session_test.go +7 -0
- package/internal/session/status.go +21 -0
- package/internal/session/status_test.go +28 -0
- package/internal/session/title.go +16 -0
- package/internal/session/title_test.go +26 -0
- package/internal/session/workspace_lock.go +265 -28
- package/internal/suggest/suggest.go +23 -1
- package/internal/team/event.go +3 -1
- package/internal/team/execute.go +21 -6
- package/internal/team/fallback_route_test.go +80 -0
- package/internal/team/planner.go +15 -3
- package/internal/team/route.go +89 -17
- package/internal/team/team.go +18 -4
- package/npm/apk/magic-agent-mobile-release.apk +0 -0
- package/npm/apk/version.json +6 -3
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-arm64/magic-client +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-client +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-client +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-client +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/dist/win32-x64/magic-client.exe +0 -0
- package/npm/install.js +11 -0
- package/package.json +14 -3
|
@@ -176,6 +176,38 @@
|
|
|
176
176
|
⚠️ 轮廓是用 Catmull-Rom 由 15 个右半控制点算出来的三次贝塞尔(见 BOTTLE_PROFILE),
|
|
177
177
|
不是手写路径 —— 手调控制点必然在肩/腹出现折角,看着像塑料件。改形去改那串点,
|
|
178
178
|
别去改 BOTTLE_CAVITY 的 d。 */
|
|
179
|
+
/* 进选择器 / 参与「首个可用」判定的引擎:探测成功、**已登录**且能列出模型。
|
|
180
|
+
没登录或取不到模型清单的(如 trae、登出后的 codebuddy-ai)不出现
|
|
181
|
+
(2026-10-25 用户要求:「登录了几个账号就显示几个」)。
|
|
182
|
+
⚠️ 「取不到模型清单」必须分两种(core --engines 的 models_source):
|
|
183
|
+
· none —— 引擎**本身不提供**模型选择(arkclaw 按 claw_id 绑死模型、
|
|
184
|
+
codebuddy-gateway 用网关进程的模型)。这不是故障,引擎完全可用,
|
|
185
|
+
右列显示「引擎默认」即可 —— 以前按 models_note 一刀切,把它一起藏了。
|
|
186
|
+
· failed —— 有动态来源但探测失败(CLI 非 0 退出等),继续隐藏。
|
|
187
|
+
老 core 没有 models_source 时,按 ErrNoModelSource 的固定文案前缀兜底。 */
|
|
188
|
+
function modelsSourceOf(en) {
|
|
189
|
+
return en.models_source || en.modelsSource || '';
|
|
190
|
+
}
|
|
191
|
+
/* 「没有模型可选」的原因,翻成人话(core 的 models_note 是英文诊断串)。
|
|
192
|
+
arkclaw / MagicAI 的模型绑在网关的 claw_id 上,codebuddy-gateway 的绑在网关进程上 ——
|
|
193
|
+
两者都不是"选不了模型就不可用",只是模型不在这台机器上选。 */
|
|
194
|
+
function noModelChoiceHint(en) {
|
|
195
|
+
var note = String(en.models_note || en.modelsNote || '');
|
|
196
|
+
if (/claw_id/i.test(note)) return '模型由网关的 claw_id 决定';
|
|
197
|
+
if (/gateway/i.test(note)) return '模型由网关进程决定';
|
|
198
|
+
return '不指定模型,由引擎自己挑';
|
|
199
|
+
}
|
|
200
|
+
function pickableEngines(list) {
|
|
201
|
+
return (list || []).filter(function (en) {
|
|
202
|
+
if (!en || !en.ok) return false;
|
|
203
|
+
if (en.logged_in === false || en.loggedIn === false) return false;
|
|
204
|
+
var src = modelsSourceOf(en);
|
|
205
|
+
if (src === 'none') return true;
|
|
206
|
+
if (src === 'failed') return false;
|
|
207
|
+
if (/^no dynamic model source/.test(en.models_note || en.modelsNote || '')) return true;
|
|
208
|
+
return (en.models || []).length > 0 || !(en.models_note || en.modelsNote);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
179
211
|
var BOTTLE_VB = { w: 64, h: 104, x: 16, y: 4 };
|
|
180
212
|
/* 右半轮廓(x 以 48 为中轴):颈口 → 颈 → 肩 → 腹 → 足 → 底心 */
|
|
181
213
|
var BOTTLE_PROFILE = [
|
|
@@ -376,12 +408,20 @@
|
|
|
376
408
|
}
|
|
377
409
|
/* 选择器里的「一行模型」:左名字、右倍率(`×0.34`)。
|
|
378
410
|
两列布局用 CSS 的 `.pick__cr` 定宽靠右,名字过长只自己省略 —— 倍率列不跟着抖。
|
|
379
|
-
倍率为空(引擎不给数据)时只画名字,不占位、不显示「×1」这类编出来的值。
|
|
380
|
-
|
|
411
|
+
倍率为空(引擎不给数据)时只画名字,不占位、不显示「×1」这类编出来的值。
|
|
412
|
+
dsh 再补第二层:路由名(同名模型可能挂在多条路由下,只给末段分不出来)。 */
|
|
413
|
+
function modelRow(id, credit, engine, quota) {
|
|
414
|
+
var frag = document.createDocumentFragment();
|
|
381
415
|
var row = el('span', 'pick__crow');
|
|
382
|
-
row.appendChild(el('span', 'pick__cl', qaModelText(id)));
|
|
416
|
+
row.appendChild(el('span', 'pick__cl', qaModelText(id, engine)));
|
|
383
417
|
if (credit) row.appendChild(el('span', 'pick__cr', credit));
|
|
384
|
-
|
|
418
|
+
frag.appendChild(row);
|
|
419
|
+
/* 第二行:供应商(dsh = 路由名,其余引擎 = model_vendors)与套餐额度合在一行:
|
|
420
|
+
「MiniMax · 5h 100% · 周 0%」(用户口径:和供应商在一行即可)。 */
|
|
421
|
+
var sub = qaModelRoute(id, engine) || qaModelVendor(state.engines, engine, id);
|
|
422
|
+
var extra = [sub, quota].filter(Boolean).join(" · ");
|
|
423
|
+
if (extra) frag.appendChild(el("span", "pick__cd" + (quota ? " pick__cd--credit" : ""), extra));
|
|
424
|
+
return frag;
|
|
385
425
|
}
|
|
386
426
|
function svgIcon(id, size) {
|
|
387
427
|
var s = document.createElementNS(SVG_NS, 'svg');
|
|
@@ -407,7 +447,21 @@
|
|
|
407
447
|
var destroyed = false;
|
|
408
448
|
var state = {
|
|
409
449
|
engines: opts.engines || [], engine: opts.engine || '', model: opts.model || '',
|
|
450
|
+
/* 主备(2026-10-24):主引擎失败(额度用尽 / 单模型故障)且零产出时,core 自动
|
|
451
|
+
降级到这一对。空 = 不降级;模型为空 = 备用引擎自己的默认模型。 */
|
|
452
|
+
fallbackEngine: String(opts.fallbackEngine || '').trim(),
|
|
453
|
+
fallbackModel: String(opts.fallbackEngine ? (opts.fallbackModel || '') : '').trim(),
|
|
410
454
|
permission: opts.permission || '', workspace: opts.workspace || '', attachments: [], session: opts.session || '', busy: false, askId: null,
|
|
455
|
+
/* 执行阶段(2026-10-24 用户:「状态已经显示黄色的是不是算停止了 按钮应该变回提交状态,
|
|
456
|
+
只有真的运行中状态才是停止」)—— 与 busy 是**两件事**:
|
|
457
|
+
busy = 这条会话**占着一轮**(追问要排队、收尾要等它);
|
|
458
|
+
phase = 这一轮**现在在干什么**:
|
|
459
|
+
idle 空闲(没有在跑,也没有在等)
|
|
460
|
+
running 引擎**真的在执行**(红色停止键 / 绿液瓶 / 绿光边框只属于它)
|
|
461
|
+
waiting 引擎停下来等用户(waiting_approval:提问 / 工具待授权 —— 黄色)
|
|
462
|
+
以前两者混在一个 busy 里,等审批时按钮也是红的「中断」—— 可引擎根本没在跑,
|
|
463
|
+
点下去只会把正等着你的一轮直接打断。 */
|
|
464
|
+
phase: 'idle',
|
|
411
465
|
/* 这一轮选中的插件(2026-10-21 用户:「选择插件后 对话框中要出现一个胶囊
|
|
412
466
|
然后发送后就是提示词变成要使用这个插件做什么」)。
|
|
413
467
|
存的是 GET /desk/plugins 那一行**原样**(id/name/cli/usePrompt…),
|
|
@@ -477,6 +531,9 @@
|
|
|
477
531
|
默认不配置 = 关闭:不请求 / 不画 / 历史不补 / 托管不自动跑)。 */
|
|
478
532
|
suggestEngine: String(opts.suggestEngine || '').trim(),
|
|
479
533
|
suggestModel: String(opts.suggestEngine ? (opts.suggestModel || '') : '').trim(),
|
|
534
|
+
/* 建议引擎的主备(2026-10-24):主建议引擎失败时先试它,再落到 core 内置的 llm。 */
|
|
535
|
+
suggestFallbackEngine: String(opts.suggestFallbackEngine || '').trim(),
|
|
536
|
+
suggestFallbackModel: String(opts.suggestFallbackEngine ? (opts.suggestFallbackModel || '') : '').trim(),
|
|
480
537
|
/* 会话托管(2026-10-23 用户:「托管模式下自动按照最佳建议提交到会话中执行」):
|
|
481
538
|
开着时每个成功收尾的轮次会把第一条(模型排序里的最佳)建议**自动发出去**。
|
|
482
539
|
hostingRounds = 本次开启以来连续自动发了几轮(上限见 hostingMax,防跑飞);
|
|
@@ -755,6 +812,18 @@
|
|
|
755
812
|
? function (p) { return transport.fileUrl(p); } : null);
|
|
756
813
|
put('pathRead', typeof transport.pathRead === 'function'
|
|
757
814
|
? function (req) { return transport.pathRead(req); } : null);
|
|
815
|
+
/* 待授权(计划审批 / 工具授权)的裁决出口(2026-10-25):
|
|
816
|
+
内核的审批卡点「允许 / 拒绝」时调它,必须走 control 的 ask_answer ——
|
|
817
|
+
引擎正阻塞在 control_response 上,当成新消息发进去它不读(见 internal/cli/control.go)。 */
|
|
818
|
+
put('askApprove', function (p) {
|
|
819
|
+
var req = p || {};
|
|
820
|
+
return control('ask_answer', null, {
|
|
821
|
+
request_id: req.request_id || '',
|
|
822
|
+
allowed: !!req.allowed,
|
|
823
|
+
message: req.message || '',
|
|
824
|
+
tool_name: req.tool_name || '',
|
|
825
|
+
});
|
|
826
|
+
});
|
|
758
827
|
})();
|
|
759
828
|
|
|
760
829
|
/* 本轮产物:与 PC 工作台 / magic-test 用**同一套**(内核 qaTurnArtsTrack,见 19-attachments.js)——
|
|
@@ -779,6 +848,15 @@
|
|
|
779
848
|
if (el.parentNode) el.parentNode.removeChild(el);
|
|
780
849
|
}
|
|
781
850
|
|
|
851
|
+
/* 事件类型两份清单(执行阶段与日志末尾定档共用,见 lastTurnInfo):
|
|
852
|
+
· 终态 —— 这一轮到此结束。与 internal/client/service.go 的 isTerminalEvent 同口径,
|
|
853
|
+
另加 interrupted / stopped(打断 / 收工:事件流上它们先到,之后不一定再有终帧)。
|
|
854
|
+
· 执行 —— 引擎真的在吐增量;停在等(waiting)的时候由它们翻回 running。 */
|
|
855
|
+
var TERMINAL_KINDS = { turn_end: 1, turn_failed: 1, result: 1, error: 1, end: 1, interrupted: 1, stopped: 1 };
|
|
856
|
+
var EXEC_KINDS = { text: 1, thinking: 1, tool_use: 1, tool_call: 1, tool_result: 1,
|
|
857
|
+
image: 1, notice: 1, command: 1, tasks: 1, start: 1, user: 1,
|
|
858
|
+
answered: 1, answer_queued: 1 };
|
|
859
|
+
|
|
782
860
|
function kernelApply(ev) {
|
|
783
861
|
if (!ev || typeof ev !== 'object') return;
|
|
784
862
|
var t = ev.type || ev.kind;
|
|
@@ -791,13 +869,32 @@
|
|
|
791
869
|
进程退出时的 `end` 那条兜底收尾。 */
|
|
792
870
|
if (t === 'turn_end' && !String(ev.text == null ? '' : ev.text).trim() && !ev.item_id && !turnHasContent(state.qa && state.qa.h)) return;
|
|
793
871
|
var e = ev.type ? ev : Object.assign({}, ev, { type: t });
|
|
872
|
+
/* 待授权(计划审批 / 工具授权)是**现场**裁决:回放时那一轮 CLI 早就不等了,
|
|
873
|
+
再画一张「允许 / 拒绝」只会给用户一个点了必然失败的按钮
|
|
874
|
+
(ask_answer 找不到对应的 control_request)。提问卡不受此限 ——
|
|
875
|
+
历史里的提问点一下仍能把答案作为追问发进那条会话。 */
|
|
876
|
+
if (state.replay && t === 'ask' && e.ask && e.ask.kind === 'permission') return;
|
|
794
877
|
if (t === 'start') {
|
|
795
|
-
/* 一轮真正用的引擎 / 模型(配置里的模型链会自动降级)→
|
|
796
|
-
|
|
878
|
+
/* 一轮真正用的引擎 / 模型(配置里的模型链会自动降级)→ 选择器跟着改口。
|
|
879
|
+
历史会话锁:start 里的 engine 不得改写归属(模型链降级会带新 engine),
|
|
880
|
+
锁定态只采纳 model。 */
|
|
881
|
+
if (e.engine && !engineLockReason()) state.engine = String(e.engine);
|
|
797
882
|
if (e.model) state.model = String(e.model);
|
|
798
883
|
syncPickers();
|
|
799
884
|
}
|
|
800
885
|
if (t === 'ask') { state.awaitingAnswer = true; state.qaHadAsk = true; } // 引擎在等这一问的答案(见 MagicHost.send)
|
|
886
|
+
/* 执行阶段推进(口径与 core 自己的状态机一致:internal/cli/ask.go ——
|
|
887
|
+
ask → waiting_approval;其后任何非终帧 → 又在跑)。
|
|
888
|
+
⚠️ 回放(state.replay)不在这里翻:回放的档位由 loadHistory 末尾按
|
|
889
|
+
lastTurnInfo().waiting 一次定死。
|
|
890
|
+
⚠️ 终态翻 idle 后再来的迟到 EXEC 帧(收尾后才到的 tool_result / notice)
|
|
891
|
+
不会点亮任何东西:is-running 的判据是 busy && phase==='running'(见 setSendMode),
|
|
892
|
+
终态已把 busy 放开,setPhase 重画时走的是 idle 模式 —— 相位值脏一点无害。 */
|
|
893
|
+
if (!state.replay) {
|
|
894
|
+
if (t === 'ask') setPhase('waiting');
|
|
895
|
+
else if (TERMINAL_KINDS[t]) setPhase('idle');
|
|
896
|
+
else if (EXEC_KINDS[t]) setPhase('running');
|
|
897
|
+
}
|
|
801
898
|
/* 画布上已经有东西了 → 撤掉那句空态说明(它只在一条都还没画时该在) */
|
|
802
899
|
var em = document.getElementById('qa-stream');
|
|
803
900
|
var empty = em && em.querySelector('.conv__empty');
|
|
@@ -917,9 +1014,11 @@
|
|
|
917
1014
|
whoModelOnly: true,
|
|
918
1015
|
onError: function (e) {
|
|
919
1016
|
/* 显示「根因」优先:`reason` 是 CLI 给的最内层原因(429 / 额度 / 认证),
|
|
920
|
-
`error` 是带引擎包装的完整链 —— 前 80 字常被包装词吃掉。
|
|
1017
|
+
`error` 是带引擎包装的完整链 —— 前 80 字常被包装词吃掉。
|
|
1018
|
+
把整份错误信封(e)作为 detail 传给收尾卡,卡片会取 reason 做根因、
|
|
1019
|
+
取 error 做可展开的完整链。 */
|
|
921
1020
|
var why = String((e && e.reason) || '').trim();
|
|
922
|
-
kernelDone(false, '调用失败 · ' + (why ? why.slice(0, 200) : String((e && e.error) || '未知错误').slice(0, 80)));
|
|
1021
|
+
kernelDone(false, '调用失败 · ' + (why ? why.slice(0, 200) : String((e && e.error) || '未知错误').slice(0, 80)), e);
|
|
923
1022
|
},
|
|
924
1023
|
onDone: function (e) { kernelDone(true, (e && e.stopped) ? '已停止' : '已完成'); },
|
|
925
1024
|
};
|
|
@@ -1029,7 +1128,10 @@
|
|
|
1029
1128
|
engine 有值、model 为空 = 引擎默认(llm 会挑不思考变体,约 1-2s);选 dsh/codebuddy
|
|
1030
1129
|
则是它们自己的无工具 JSON 通道(实测 6-10s,用户自己承担)。 */
|
|
1031
1130
|
engine: state.suggestEngine,
|
|
1032
|
-
model: state.suggestModel
|
|
1131
|
+
model: state.suggestModel,
|
|
1132
|
+
/* 建议引擎的主备:core 先试这一对,再落到内置 llm。 */
|
|
1133
|
+
fallbackEngine: state.suggestFallbackEngine || undefined,
|
|
1134
|
+
fallbackModel: state.suggestFallbackModel || undefined
|
|
1033
1135
|
}, { signal: ac ? ac.signal : undefined }))
|
|
1034
1136
|
.then(function (res) {
|
|
1035
1137
|
if (seq !== state.suggestSeq) return; // 已经开新轮 / 换会话 / 又收尾了
|
|
@@ -1258,13 +1360,19 @@
|
|
|
1258
1360
|
/** 宿主改「建议引擎」:空 = 关闭建议功能。
|
|
1259
1361
|
* 关闭时立刻清掉建议行 / 在途请求 / 托管计时器,并暂停托管(回调宿主落盘)。
|
|
1260
1362
|
* 开启后从**下一轮**收尾开始生成;不追着已经画过的旧轮补请求。 */
|
|
1261
|
-
function setSuggestEngine(engine, model) {
|
|
1363
|
+
function setSuggestEngine(engine, model, fallbackEngine, fallbackModel) {
|
|
1262
1364
|
var next = String(engine == null ? '' : engine).trim();
|
|
1263
1365
|
var nextModel = String(model == null ? '' : model).trim();
|
|
1264
1366
|
if (!next) nextModel = ''; // 引擎关了就不留孤儿模型
|
|
1265
|
-
|
|
1367
|
+
var nextFb = String(fallbackEngine == null ? '' : fallbackEngine).trim();
|
|
1368
|
+
var nextFbModel = String(fallbackModel == null ? '' : fallbackModel).trim();
|
|
1369
|
+
if (!nextFb) nextFbModel = '';
|
|
1370
|
+
if (next === state.suggestEngine && nextModel === state.suggestModel
|
|
1371
|
+
&& nextFb === state.suggestFallbackEngine && nextFbModel === state.suggestFallbackModel) return;
|
|
1266
1372
|
state.suggestEngine = next;
|
|
1267
1373
|
state.suggestModel = nextModel;
|
|
1374
|
+
state.suggestFallbackEngine = nextFb;
|
|
1375
|
+
state.suggestFallbackModel = nextFbModel;
|
|
1268
1376
|
clearSuggest();
|
|
1269
1377
|
if (!next) pauseHosting('未配置建议引擎');
|
|
1270
1378
|
}
|
|
@@ -1325,7 +1433,7 @@
|
|
|
1325
1433
|
|
|
1326
1434
|
/** 一轮收尾(成功 / 失败 / 被停):内核的收尾动作已经做完(收思考区 / 工具卡 / 运行胶囊),
|
|
1327
1435
|
* 这一层只补**收尾行**(状态语 + 总耗时,取墙上时钟 —— 与掌天瓶那边同一口径)。 */
|
|
1328
|
-
function kernelDone(ok, label) {
|
|
1436
|
+
function kernelDone(ok, label, detail) {
|
|
1329
1437
|
/* 销毁后迟到的终态:这一轮已经不属于任何画面了 —— 收尾行、托管判定、建议请求
|
|
1330
1438
|
一条都不该再跑(它们会写进新实例的画布,或替新实例发下一句)。 */
|
|
1331
1439
|
if (destroyed) return;
|
|
@@ -1353,7 +1461,7 @@
|
|
|
1353
1461
|
(2026-10-20 用户截图:空块 + 「已完成 · 总耗时 1.8s」)。收尾行本身挂在线程容器上,
|
|
1354
1462
|
不在这条消息块里,所以撤掉不丢收尾行。 */
|
|
1355
1463
|
dropEmptyAnswer(q.h);
|
|
1356
|
-
qaRenderDone(ok, label, thread(), ms);
|
|
1464
|
+
qaRenderDone(ok, label, thread(), ms, detail);
|
|
1357
1465
|
/* 只有「真的跑完了」才有下一步可取:失败 / 被停 / 回放的轮次都不问。
|
|
1358
1466
|
请求异步进行,界面不等待 —— 结果回来再画(见 requestSuggestions)。 */
|
|
1359
1467
|
if (ok && label !== '已停止' && !state.replay) requestSuggestions(state.lastPrompt, state.agentRaw);
|
|
@@ -1369,6 +1477,7 @@
|
|
|
1369
1477
|
if (!state.replay) {
|
|
1370
1478
|
clearWorking();
|
|
1371
1479
|
state.busy = false;
|
|
1480
|
+
state.phase = 'idle';
|
|
1372
1481
|
state.askId = null;
|
|
1373
1482
|
setSendMode('idle');
|
|
1374
1483
|
/* 顺手把还挂着的那条流掐掉:让 runTurn 的 Promise 收口(否则它会一直等 end 帧)。
|
|
@@ -1431,7 +1540,7 @@
|
|
|
1431
1540
|
⚠️ 例外:这一轮**还在宿主那里排队**(beforeAsk 没 resolve)时本来就没有 askId,
|
|
1432
1541
|
此时打断的真实含义是「别发这一轮了」。宿主用 opts.onInterrupt 认领这层语义:
|
|
1433
1542
|
返回 true = 它已经把排队撤掉,组件不再往下走 control。 */
|
|
1434
|
-
async function control(op, text) {
|
|
1543
|
+
async function control(op, text, extra) {
|
|
1435
1544
|
if (!state.askId) {
|
|
1436
1545
|
if (op === 'interrupt' && typeof opts.onInterrupt === 'function') {
|
|
1437
1546
|
var owned = false;
|
|
@@ -1485,8 +1594,16 @@
|
|
|
1485
1594
|
try {
|
|
1486
1595
|
var payload = { op: op, id: state.askId };
|
|
1487
1596
|
if (text != null) payload.text = text;
|
|
1597
|
+
/* 附加字段(2026-10-25):ask_answer 要带 request_id / allowed / message / tool_name ——
|
|
1598
|
+
与 core `--control` 的命令形状一致,别在这里丢字段。 */
|
|
1599
|
+
if (extra && typeof extra === 'object') {
|
|
1600
|
+
Object.keys(extra).forEach(function (k) {
|
|
1601
|
+
if (extra[k] !== undefined && extra[k] !== null) payload[k] = extra[k];
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1488
1604
|
await transport.control(payload);
|
|
1489
|
-
if (op
|
|
1605
|
+
if (op === 'interrupt') noticeItem('已请求打断');
|
|
1606
|
+
else if (op === 'stop') noticeItem('已请求收工');
|
|
1490
1607
|
return true;
|
|
1491
1608
|
} catch (e) {
|
|
1492
1609
|
noticeItem('控制失败:' + (e.message || e));
|
|
@@ -1841,7 +1958,9 @@
|
|
|
1841
1958
|
而新一轮的事件也在同一张画布上,正文就重复了。也不走 stopFollow:
|
|
1842
1959
|
它会把 busy 放开,而这里马上就要置 busy。 */
|
|
1843
1960
|
cancelFollowQuiet();
|
|
1844
|
-
|
|
1961
|
+
/* 直接赋值而不走 setPhase:上一轮迟到的 EXEC 帧可能把 phase 留在 'running',
|
|
1962
|
+
setPhase 的幂等挡板会吞掉这次重画 —— 这里必须无条件切到 busy 运行态。 */
|
|
1963
|
+
state.busy = true; state.phase = 'running'; setSendMode('busy');
|
|
1845
1964
|
var askHandle = null; // 本轮 transport.ask 的返回值(收尾时取消用)
|
|
1846
1965
|
/* 串行门(beforeAsk)真正把这一轮挡住时,画布上必须看得见「排队中」——
|
|
1847
1966
|
2026-10-21 用户:「其实在串行排队 但是没显示出来任何标识」:气泡已经画出来了,
|
|
@@ -1889,7 +2008,7 @@
|
|
|
1889
2008
|
markQueued(false);
|
|
1890
2009
|
if (gate === false) {
|
|
1891
2010
|
clearWorking();
|
|
1892
|
-
state.busy = false; state.askId = null; setSendMode('idle');
|
|
2011
|
+
state.busy = false; state.phase = 'idle'; state.askId = null; setSendMode('idle');
|
|
1893
2012
|
/* 气泡已经画在画布上了(fireTurn 先画再等门):这一轮不会有人回答它,
|
|
1894
2013
|
标成「已取消」比留一句悬空的话强 —— 用户至少知道它被撤了、不是还在等。 */
|
|
1895
2014
|
markLastMineCancelled();
|
|
@@ -1921,6 +2040,11 @@
|
|
|
1921
2040
|
try {
|
|
1922
2041
|
var payload = { engine: state.engine, prompt: prompt };
|
|
1923
2042
|
if (state.model) payload.model = state.model;
|
|
2043
|
+
/* 主备:只配了备用也要带上(主引擎仍是 state.engine)。 */
|
|
2044
|
+
if (state.fallbackEngine) {
|
|
2045
|
+
payload.fallbackEngine = state.fallbackEngine;
|
|
2046
|
+
if (state.fallbackModel) payload.fallbackModel = state.fallbackModel;
|
|
2047
|
+
}
|
|
1924
2048
|
if (state.workspace) payload.workspace = state.workspace;
|
|
1925
2049
|
if (attPaths && attPaths.length) payload.attachments = attPaths;
|
|
1926
2050
|
/* 空值就不传:让 core 用它自己的默认档,不替用户预设(安全语义)。 */
|
|
@@ -2048,7 +2172,7 @@
|
|
|
2048
2172
|
这里只摘掉本壳自己的「运行中」指示并把状态放开 —— 别再补「完成」这类行,
|
|
2049
2173
|
内核那条收尾行已经写了,补一条会变成画布上多一行重复的「完成」。 */
|
|
2050
2174
|
clearWorking();
|
|
2051
|
-
state.busy = false; state.askId = null; setSendMode('idle');
|
|
2175
|
+
state.busy = false; state.phase = 'idle'; state.askId = null; setSendMode('idle');
|
|
2052
2176
|
/* 只收自己那条流:收尾后用户可能已经发了新一轮,别把新一轮的钩子抹掉 */
|
|
2053
2177
|
if (state.askHandle === askHandle) state.askHandle = null;
|
|
2054
2178
|
if (opts.onBusy) { try { opts.onBusy(false); } catch (e) {} }
|
|
@@ -2058,6 +2182,15 @@
|
|
|
2058
2182
|
}
|
|
2059
2183
|
}
|
|
2060
2184
|
|
|
2185
|
+
/* 执行阶段归一 + 唯一出口:phase 一变就把按钮/胶囊重画一遍。
|
|
2186
|
+
busy 不动 —— 占着一轮(排队/收尾)是占着一轮,现在在跑还是在等是另一件事。 */
|
|
2187
|
+
function setPhase(p) {
|
|
2188
|
+
var v = p === 'running' ? 'running' : (p === 'waiting' ? 'waiting' : 'idle');
|
|
2189
|
+
if (state.phase === v) return;
|
|
2190
|
+
state.phase = v;
|
|
2191
|
+
setSendMode(state.busy ? 'busy' : 'idle');
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2061
2194
|
function setSendMode(mode) {
|
|
2062
2195
|
var b = ctx.sendBtn;
|
|
2063
2196
|
var busy = mode === 'busy';
|
|
@@ -2067,7 +2200,12 @@
|
|
|
2067
2200
|
输入框里的字一敲一删都会回调这里(见下面 input 监听)。 */
|
|
2068
2201
|
var typed = '';
|
|
2069
2202
|
try { typed = String(ctx.input.value || '').trim(); } catch (e) { typed = ''; }
|
|
2070
|
-
|
|
2203
|
+
/* 2026-10-24 用户:「状态已经显示黄色的是不是算停止了 按钮应该变回提交状态,
|
|
2204
|
+
只有真的运行中状态才是停止」—— 停止键只属于 phase==='running'。
|
|
2205
|
+
waiting(waiting_approval:引擎停下来等确认)时按钮回到「发送」,
|
|
2206
|
+
但仍占着一轮 → 有字照样排队,由 send() 的 busy 守卫拦重复。 */
|
|
2207
|
+
var running = busy && state.phase === 'running';
|
|
2208
|
+
var stop = running && !typed && !state.plugin;
|
|
2071
2209
|
/* ⚠️ busy 时**不能**把它 disabled:那样点它不会派发 click,
|
|
2072
2210
|
它就从「中断」退化成一枚按不动的图标。改指中断后,
|
|
2073
2211
|
重复发送由 send() 自己的 state.busy 守卫拦住,不需要靠 disabled 防。 */
|
|
@@ -2076,17 +2214,19 @@
|
|
|
2076
2214
|
b.innerHTML = '';
|
|
2077
2215
|
/* 停止用方块、且比发送箭头略小(方块本身比箭头「重」,同尺寸会显得头重脚轻) */
|
|
2078
2216
|
b.appendChild(svgIcon(stop ? 'i-stop' : 'i-up', stop ? 14 : 17));
|
|
2079
|
-
b.setAttribute('aria-label', !busy ? '发送' : (stop ? '中断' : '排队发送'));
|
|
2217
|
+
b.setAttribute('aria-label', !busy ? '发送' : (stop ? '中断' : (running ? '排队发送' : '发送')));
|
|
2080
2218
|
b.title = !busy ? '发送'
|
|
2081
|
-
: (stop ? '中断这一轮'
|
|
2219
|
+
: (stop ? '中断这一轮'
|
|
2220
|
+
: (running ? '排队发送 · 上一条处理中,结束后自动提交'
|
|
2221
|
+
: '发送 · 上一轮在等你确认,现在发出的消息会先排队'));
|
|
2082
2222
|
/* 运行态并进引擎 · 模型那枚胶囊(2026-10-20 用户:「两个胶囊合并成1个」
|
|
2083
2223
|
→「是图中胶囊下方的生成中 应该跟胶囊合并」→「不需要文字了 用其他动画效果代替」)——
|
|
2084
2224
|
运行态时这枚胶囊**自己**变成运行态:前面的小图标换成绿液瓶(会涨落)。
|
|
2085
|
-
⚠️ 跟着
|
|
2086
|
-
|
|
2087
|
-
|
|
2225
|
+
⚠️ 跟着 **phase** 走而不是 busy:等审批(黄色)时引擎没在跑,
|
|
2226
|
+
绿液瓶 / 绿光边框必须熄掉,只有真执行才亮(见 state.phase 注释)。
|
|
2227
|
+
引擎·模型的真值仍在胶囊里:降级换模型时 'start' 事件会改写它
|
|
2088
2228
|
(见 kernelApply 的 'start' 分支 → syncPickers),所以不丢信息。 */
|
|
2089
|
-
if (ctx.casc) ctx.casc.classList.toggle('is-running',
|
|
2229
|
+
if (ctx.casc) ctx.casc.classList.toggle('is-running', running);
|
|
2090
2230
|
}
|
|
2091
2231
|
|
|
2092
2232
|
/* ══════════ 底部抽屉组件 Sheet(引擎 / 模型 / 授权共用)══════════
|
|
@@ -2096,6 +2236,8 @@
|
|
|
2096
2236
|
和叠加元素盖住;现在收成一个组件:层级拉高、高度用 dvh 贴合真实可视区。 */
|
|
2097
2237
|
var Sheet = (function () {
|
|
2098
2238
|
var scrim = null, panel = null, bodyEl = null, titleEl = null, curAnchor = null;
|
|
2239
|
+
/* 每次 open 递增:异步回调据此判断「这次打开是否还在」 */
|
|
2240
|
+
var gen = 0;
|
|
2099
2241
|
function onKey(e) { if (e.key === 'Escape') { e.preventDefault(); close(); } }
|
|
2100
2242
|
function build() {
|
|
2101
2243
|
scrim = el('div', 'pick-scrim');
|
|
@@ -2156,11 +2298,19 @@
|
|
|
2156
2298
|
}
|
|
2157
2299
|
function open(anchor, fn) {
|
|
2158
2300
|
/* 同一按钮再点一次 = 收起 */
|
|
2159
|
-
if (panel && curAnchor === anchor) { close(); return; }
|
|
2301
|
+
if (panel && curAnchor === anchor) { close(); return null; }
|
|
2160
2302
|
close();
|
|
2303
|
+
gen++;
|
|
2304
|
+
var my = gen;
|
|
2161
2305
|
curAnchor = anchor;
|
|
2162
2306
|
build();
|
|
2163
2307
|
render(fn);
|
|
2308
|
+
/* 返回这一次打开的手柄:异步拿到新数据时用来判断「抽屉还是同一次打开」
|
|
2309
|
+
(用户可能已经关掉或换了别的抽屉,那就不该再往里画)。 */
|
|
2310
|
+
return {
|
|
2311
|
+
alive: function () { return panel != null && my === gen; },
|
|
2312
|
+
render: function (next) { if (panel != null && my === gen) render(next); }
|
|
2313
|
+
};
|
|
2164
2314
|
}
|
|
2165
2315
|
return { open: open, close: close, render: render };
|
|
2166
2316
|
})();
|
|
@@ -2168,7 +2318,16 @@
|
|
|
2168
2318
|
/* 引擎 · 模型:**省市联动**(左列引擎 · 右列该引擎的模型,选中实时联动切换)。
|
|
2169
2319
|
—— 不是「下钻二级」那种一列换一列;两列同屏,像省市选择器。 */
|
|
2170
2320
|
function openEnginePicker(anchor) {
|
|
2171
|
-
Sheet.open(anchor, function (M) { engineModelCascade(M); });
|
|
2321
|
+
var h = Sheet.open(anchor, function (M) { engineModelCascade(M); });
|
|
2322
|
+
if (!h) return;
|
|
2323
|
+
/* 额度 / 签到**不跟缓存走**:模型清单可以缓存 5 分钟(要起引擎 CLI),
|
|
2324
|
+
额度不行 —— 它随用量和 key 变化(core --quotas 只读票据 + 一两条 HTTP)。
|
|
2325
|
+
打开选择器时后台再取一次;数据真变了且抽屉还开着,就地重画。 */
|
|
2326
|
+
var before = JSON.stringify(state.engines);
|
|
2327
|
+
refreshEngines(true).then(function () {
|
|
2328
|
+
if (!h.alive() || JSON.stringify(state.engines) === before) return;
|
|
2329
|
+
h.render(function (M) { engineModelCascade(M); });
|
|
2330
|
+
}, function () { /* 取不到就继续用旧值,不打扰用户 */ });
|
|
2172
2331
|
}
|
|
2173
2332
|
/* 引擎/模型**落定**(点了具体模型或「默认模型」)时通知宿主。
|
|
2174
2333
|
宿主据此把它记成下次新建任务的默认值(移动端「我的 → 默认引擎/模型」);
|
|
@@ -2178,10 +2337,28 @@
|
|
|
2178
2337
|
if (!opts.onPick) return;
|
|
2179
2338
|
try { opts.onPick(state.engine, state.model); } catch (e) {}
|
|
2180
2339
|
}
|
|
2340
|
+
/* 历史会话锁:会话已有归属(state.session 非空)时引擎不可换,模型照旧可换。
|
|
2341
|
+
会话 id 是引擎命名空间的 —— codebuddy 开的会话拿 dsh 去续,dsh 直接报
|
|
2342
|
+
「session does not exist」。state.engine 为空时不锁(无可锁对象,锁了发送
|
|
2343
|
+
就没有引擎可带)。返回锁定原因文案(供 title / desc 复用),未锁返回 ''。 */
|
|
2344
|
+
function engineLockReason() {
|
|
2345
|
+
if (state.session && state.engine) return '这条会话由 ' + state.engine + ' 开的 · 引擎不能换,模型可以换';
|
|
2346
|
+
return '';
|
|
2347
|
+
}
|
|
2181
2348
|
function engineModelCascade(M) {
|
|
2182
2349
|
var all = state.engines || [];
|
|
2183
|
-
var available = all
|
|
2184
|
-
|
|
2350
|
+
var available = pickableEngines(all);
|
|
2351
|
+
var locked = engineLockReason();
|
|
2352
|
+
if (locked) {
|
|
2353
|
+
/* 锁定态左列只画当前引擎这一项(置灰不可点)。当前引擎不在可用清单里
|
|
2354
|
+
(探测失败等)也要画 —— 会话归属不随探测结果变。 */
|
|
2355
|
+
var own = null;
|
|
2356
|
+
all.forEach(function (x) { if (x.engine === state.engine) own = x; });
|
|
2357
|
+
if (!own) own = { engine: state.engine, ok: true, models: [] };
|
|
2358
|
+
available = [own];
|
|
2359
|
+
}
|
|
2360
|
+
M.title(locked ? '这条会话的引擎不能换(模型可以换)'
|
|
2361
|
+
: '选择引擎 / 模型(' + available.length + ' / ' + all.length + ')');
|
|
2185
2362
|
if (!available.length) {
|
|
2186
2363
|
M.row({ label: '没有可用引擎', desc: 'magic-agent --engines 会给每个引擎的一键安装命令' });
|
|
2187
2364
|
return;
|
|
@@ -2197,6 +2374,7 @@
|
|
|
2197
2374
|
if (!cur) cur = available[0];
|
|
2198
2375
|
|
|
2199
2376
|
function chooseEngine(en) {
|
|
2377
|
+
if (engineLockReason()) return; // 历史会话:引擎归属锁死,堵死一切绕过路径
|
|
2200
2378
|
cur = en;
|
|
2201
2379
|
var models = en.models || [];
|
|
2202
2380
|
state.engine = en.engine;
|
|
@@ -2205,30 +2383,94 @@
|
|
|
2205
2383
|
syncPickers();
|
|
2206
2384
|
paint();
|
|
2207
2385
|
}
|
|
2386
|
+
/* 引擎行副标题:没模型时说清「取不到(未登录等)」还是「用引擎默认」——
|
|
2387
|
+
之前一律写「引擎默认」,会把 trae 这类没登录、models 为空的引擎误写成默认可用。
|
|
2388
|
+
多账号引擎(codebuddy 系)把登录的账号昵称放在最前面,一眼看出是哪个号。 */
|
|
2389
|
+
function engineSubLine(en) {
|
|
2390
|
+
var n = (en.models || []).length;
|
|
2391
|
+
var head = n ? (n + ' 个模型') : '引擎默认';
|
|
2392
|
+
return en.account ? (en.account + ' · ' + head) : head;
|
|
2393
|
+
}
|
|
2394
|
+
/* 每日积分领取(codebuddy 系):点「领取」只领积分,不切换引擎。
|
|
2395
|
+
成功后就地更新这一行的状态并重画,不重新探测整份引擎清单(太慢)。
|
|
2396
|
+
领取结果**不弹任何提示**(2026-10-25 用户要求):领完「领取」直接消失,
|
|
2397
|
+
失败静默恢复按钮。 */
|
|
2398
|
+
function claimCheckin(en) {
|
|
2399
|
+
if (!transport.claimCheckin || en._claimBusy) return;
|
|
2400
|
+
en._claimBusy = true;
|
|
2401
|
+
paint();
|
|
2402
|
+
transport.claimCheckin(en.engine).then(function (res) {
|
|
2403
|
+
en._claimBusy = false;
|
|
2404
|
+
if (res && res.ok) {
|
|
2405
|
+
en.checkin_today = true;
|
|
2406
|
+
en.checkin_credit = '';
|
|
2407
|
+
if (res.credit_left) en.credit_left = res.credit_left;
|
|
2408
|
+
} else if (res && res.checked_today) {
|
|
2409
|
+
en.checkin_today = true;
|
|
2410
|
+
en.checkin_credit = '';
|
|
2411
|
+
}
|
|
2412
|
+
paint();
|
|
2413
|
+
}, function () {
|
|
2414
|
+
en._claimBusy = false;
|
|
2415
|
+
paint();
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2208
2418
|
function paint() {
|
|
2209
2419
|
left.innerHTML = ''; right.innerHTML = '';
|
|
2210
2420
|
available.forEach(function (en) {
|
|
2211
2421
|
var n = (en.models || []).length;
|
|
2212
2422
|
var b = el('button', 'pick__citem' + (en.engine === cur.engine ? ' is-on' : ''));
|
|
2213
2423
|
b.type = 'button';
|
|
2424
|
+
if (locked) b.disabled = true; // 历史会话:引擎项置灰不可点
|
|
2214
2425
|
b.appendChild(el('span', 'pick__cl', en.engine));
|
|
2215
|
-
b.appendChild(el('span', 'pick__cd',
|
|
2426
|
+
b.appendChild(el('span', 'pick__cd', engineSubLine(en)));
|
|
2427
|
+
if (locked) b.appendChild(el('span', 'pick__cd', engineLockReason()));
|
|
2428
|
+
/* 账号剩余积分(codebuddy 系才有 credit_left,见 core --engines)。
|
|
2429
|
+
只显示「剩余」,总量随套餐变化、对选引擎没判据价值。
|
|
2430
|
+
⚠️ PC 宿主的 glue 会归一成 camelCase,两种拼写都认。 */
|
|
2431
|
+
var creditLeft = en.credit_left || en.creditLeft;
|
|
2432
|
+
var creditSuffix = en.credit_suffix || en.creditSuffix || ' 积分';
|
|
2433
|
+
if (creditLeft) {
|
|
2434
|
+
b.appendChild(el('span', 'pick__cd pick__cd--credit', '剩余 ' + creditLeft + creditSuffix));
|
|
2435
|
+
}
|
|
2436
|
+
/* 每日积分:今天没领就画「领取」;已领显示状态。点 chip 只领取,不切引擎。 */
|
|
2437
|
+
var checkedToday = (typeof en.checkin_today === 'boolean') ? en.checkin_today : en.checkinToday;
|
|
2438
|
+
if (checkedToday === false) {
|
|
2439
|
+
var checkinCredit = en.checkin_credit || en.checkinCredit || '';
|
|
2440
|
+
var chip = el('span', 'pick__cd pick__cd--credit pick__claim',
|
|
2441
|
+
en._claimBusy ? '领取中…' : ('领取 ' + checkinCredit + ' 积分'));
|
|
2442
|
+
chip.onclick = function (ev) {
|
|
2443
|
+
if (ev) { ev.preventDefault(); ev.stopPropagation(); }
|
|
2444
|
+
claimCheckin(en);
|
|
2445
|
+
};
|
|
2446
|
+
b.appendChild(chip);
|
|
2447
|
+
}
|
|
2216
2448
|
b.onclick = function () { chooseEngine(en); };
|
|
2217
2449
|
left.appendChild(b);
|
|
2218
2450
|
});
|
|
2219
|
-
var models = cur.models || [];
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2451
|
+
var models = qaModelsOrdered(state.engines, cur.engine, cur.models || []);
|
|
2452
|
+
if (!models.length) {
|
|
2453
|
+
/* 引擎不提供模型清单(models_source = none:arkclaw 按 claw_id 绑模型、
|
|
2454
|
+
codebuddy-gateway 用网关进程的模型)。必须给一个「引擎默认」落定项 ——
|
|
2455
|
+
否则左列能选中、右列却是空的,用户无从"确定",notifyPick 也永远不触发。 */
|
|
2456
|
+
var d = el('button', 'pick__citem' + (state.engine === cur.engine && !state.model ? ' is-on' : ''));
|
|
2457
|
+
d.type = 'button';
|
|
2458
|
+
d.appendChild(el('span', 'pick__cl', '引擎默认'));
|
|
2459
|
+
d.appendChild(el('span', 'pick__cd',
|
|
2460
|
+
modelsSourceOf(cur) === 'none' ? noModelChoiceHint(cur) : '不指定模型,由引擎自己挑'));
|
|
2461
|
+
d.onclick = function () {
|
|
2462
|
+
state.engine = cur.engine; state.model = '';
|
|
2463
|
+
syncPickers(); notifyPick(); Sheet.close();
|
|
2464
|
+
};
|
|
2465
|
+
right.appendChild(d);
|
|
2466
|
+
}
|
|
2226
2467
|
models.forEach(function (mm) {
|
|
2227
2468
|
var b = el('button', 'pick__citem' + (state.engine === cur.engine && state.model === mm ? ' is-on' : ''));
|
|
2228
2469
|
b.type = 'button';
|
|
2229
2470
|
/* 清单项显示剥离前缀后的名字(`mm` 本身是 CLI 认的完整 id,选它时原样写回)。
|
|
2230
2471
|
倍率(`model_credits`)靠右挂在同一行:模型名再长也只挤自己,倍率列不会错位。 */
|
|
2231
|
-
b.appendChild(modelRow(mm, qaModelCredit(state.engines, cur.engine, mm)
|
|
2472
|
+
b.appendChild(modelRow(mm, qaModelCredit(state.engines, cur.engine, mm), cur.engine,
|
|
2473
|
+
qaModelQuota(state.engines, cur.engine, mm)));
|
|
2232
2474
|
b.onclick = function () { state.engine = cur.engine; state.model = mm; syncPickers(); notifyPick(); Sheet.close(); };
|
|
2233
2475
|
right.appendChild(b);
|
|
2234
2476
|
});
|
|
@@ -2257,10 +2499,18 @@
|
|
|
2257
2499
|
Object.keys(routes).forEach(function (k) {
|
|
2258
2500
|
var r = routes[k] || {};
|
|
2259
2501
|
var engine = String(r.engine || '').trim();
|
|
2260
|
-
|
|
2261
|
-
|
|
2502
|
+
var fe = String(r.fallbackEngine || '').trim();
|
|
2503
|
+
/* 主/备任一非空都要下发:只配了备用 = 主引擎跟随主引擎 + 这一路的备。 */
|
|
2504
|
+
if (!engine && !fe) return;
|
|
2505
|
+
var item = {};
|
|
2506
|
+
if (engine) item.engine = engine;
|
|
2262
2507
|
var model = String(r.model || '').trim();
|
|
2263
2508
|
if (model) item.model = model;
|
|
2509
|
+
if (fe) {
|
|
2510
|
+
item.fallbackEngine = fe;
|
|
2511
|
+
var fm = String(r.fallbackModel || '').trim();
|
|
2512
|
+
if (fm) item.fallbackModel = fm;
|
|
2513
|
+
}
|
|
2264
2514
|
clean[k] = item;
|
|
2265
2515
|
});
|
|
2266
2516
|
if (Object.keys(clean).length) out.routes = clean;
|
|
@@ -2445,12 +2695,16 @@
|
|
|
2445
2695
|
var k = events[i] && (events[i].kind || events[i].type);
|
|
2446
2696
|
if (k === 'user') idx = i;
|
|
2447
2697
|
}
|
|
2448
|
-
var terminal = false;
|
|
2698
|
+
var terminal = false, waiting = false;
|
|
2449
2699
|
for (var j = idx + 1; j < events.length; j++) {
|
|
2450
2700
|
var k2 = events[j] && (events[j].kind || events[j].type);
|
|
2451
|
-
|
|
2701
|
+
/* waiting:最后一轮停在等用户(ask:提问 / 工具待授权)。其后若又有执行帧
|
|
2702
|
+
(answered 之后引擎继续跑),清掉 —— 口径与 core 的 ask.go 状态机一致。 */
|
|
2703
|
+
if (k2 === 'ask') waiting = true;
|
|
2704
|
+
else if (EXEC_KINDS[k2]) waiting = false;
|
|
2705
|
+
if (TERMINAL_KINDS[k2]) { terminal = true; waiting = false; break; }
|
|
2452
2706
|
}
|
|
2453
|
-
return { index: idx, terminal: terminal, at: idx >= 0 ? replayAt(events[idx]) : 0 };
|
|
2707
|
+
return { index: idx, terminal: terminal, waiting: waiting, at: idx >= 0 ? replayAt(events[idx]) : 0 };
|
|
2454
2708
|
}
|
|
2455
2709
|
|
|
2456
2710
|
/* 历史最后一轮能不能补建议:必须是**成功收尾**、没有待答提问卡、正文够长。
|
|
@@ -2555,6 +2809,7 @@
|
|
|
2555
2809
|
if (state.askId && state.askId !== runID) return;
|
|
2556
2810
|
clearWorking();
|
|
2557
2811
|
state.busy = false;
|
|
2812
|
+
state.phase = 'idle';
|
|
2558
2813
|
state.askId = null;
|
|
2559
2814
|
state.resumeT0 = 0;
|
|
2560
2815
|
setSendMode('idle');
|
|
@@ -2625,6 +2880,7 @@
|
|
|
2625
2880
|
if (!state.askId) {
|
|
2626
2881
|
clearWorking();
|
|
2627
2882
|
state.busy = false;
|
|
2883
|
+
state.phase = 'idle';
|
|
2628
2884
|
setSendMode('idle');
|
|
2629
2885
|
if (opts.onBusy) { try { opts.onBusy(false); } catch (e) {} }
|
|
2630
2886
|
}
|
|
@@ -2647,6 +2903,7 @@
|
|
|
2647
2903
|
var st = String((info && info.state) || '');
|
|
2648
2904
|
clearWorking();
|
|
2649
2905
|
state.busy = false;
|
|
2906
|
+
state.phase = 'idle';
|
|
2650
2907
|
state.askId = null;
|
|
2651
2908
|
state.resumeT0 = 0;
|
|
2652
2909
|
setSendMode('idle');
|
|
@@ -2708,6 +2965,11 @@
|
|
|
2708
2965
|
tok.retryNotice = null;
|
|
2709
2966
|
var t = n && n.querySelector ? n.querySelector('.notice__t') : null;
|
|
2710
2967
|
if (t) t.textContent = '已重新连接,继续同步';
|
|
2968
|
+
/* 「已恢复」是一次过去式的确认,不是这一轮的历史:短暂提示后收走,
|
|
2969
|
+
别让这行红条一直挂在画布里(2026-10-24 用户:「重连成功了不要一直还显示提示」)。 */
|
|
2970
|
+
setTimeout(function () {
|
|
2971
|
+
if (n && n.parentNode) n.parentNode.removeChild(n);
|
|
2972
|
+
}, 1800);
|
|
2711
2973
|
},
|
|
2712
2974
|
/* 终态 / 重试用尽 / 4xx 才收摊;普通网络抖动不会走到这里(传输层在重连)。 */
|
|
2713
2975
|
onEnd: function (info) { if (state.follow === tok) followEnd(info); },
|
|
@@ -2952,9 +3214,20 @@
|
|
|
2952
3214
|
续传中的那一轮(partial)**不封**:它的收尾由后面接上的实时通道落定。 */
|
|
2953
3215
|
if (!partial) finishReplayRound();
|
|
2954
3216
|
} finally { state.replay = false; }
|
|
3217
|
+
/* 回放的档位在这里一次定死(回放期间 kernelApply 不翻相位):
|
|
3218
|
+
最后一轮停在等用户(日志里有 ask、其后没有终帧)→ waiting;
|
|
3219
|
+
有终态 → idle;还在跑(partial,马上接实时流)→ running。 */
|
|
3220
|
+
var lti = lastTurnInfo(replayList || []);
|
|
3221
|
+
if (partial) setPhase('running');
|
|
3222
|
+
else if (lti.waiting && !lti.terminal) setPhase('waiting');
|
|
3223
|
+
else setPhase('idle');
|
|
2955
3224
|
var t = thread(); t.scrollTop = t.scrollHeight;
|
|
2956
3225
|
/* 打开 / 切换会话 = 从底部开始看:把内核的「用户离开底部」闸复位(见 qaScrollReset)。 */
|
|
2957
3226
|
if (typeof qaScrollReset === 'function') { try { qaScrollReset(); } catch (e) {} }
|
|
3227
|
+
/* 兜底贴底(2026-10-25 用户:「历史会话也应该默认到最底部」):上面那次同步贴底
|
|
3228
|
+
在缩略图 / 字体加载完成**之前**执行,图一到位画布就被撑高、停在半空(手机上几乎必现)。
|
|
3229
|
+
rAF + 300ms + 慢图 load 三重补贴,见内核 qaHistoryStickBottom。 */
|
|
3230
|
+
if (typeof qaHistoryStickBottom === 'function') { try { qaHistoryStickBottom(); } catch (e) {} }
|
|
2958
3231
|
state.autoFollow = true;
|
|
2959
3232
|
/* 分页游标落定后才显示入口(旧服务端 hist = null → 不显示,行为与改动前一致)。 */
|
|
2960
3233
|
state.hist = after == null ? hist : state.hist;
|
|
@@ -3029,6 +3302,20 @@
|
|
|
3029
3302
|
Array.prototype.slice.call(t.childNodes).forEach(function (n) {
|
|
3030
3303
|
if (n !== moreBtn) frag.appendChild(n);
|
|
3031
3304
|
});
|
|
3305
|
+
/* ⚠️ 翻页可能发生在**一轮还在跑**的时候(2026-10-24 用户:「点击向上滚动没反应」)。
|
|
3306
|
+
前插走的是回放那条管线,而回放会「遇到 user 就把当前这一笔封口」、产物也挂到
|
|
3307
|
+
「当前这一笔」上 —— 不隔离就会把正在流式增长的那张实时卡当历史轮封掉,
|
|
3308
|
+
后续增量只能另起一张(同一轮变两卡)。所以把实时那一轮属于宿主的状态整体摘下来,
|
|
3309
|
+
回放完原样接回去。回放是同步的(detach → 画 → 接回都在同一段 JS 里),实时事件插不进来。 */
|
|
3310
|
+
var live = {
|
|
3311
|
+
qa: state.qa, qaClosed: state.qaClosed, lastUserSig: state.lastUserSig,
|
|
3312
|
+
agentRaw: state.agentRaw, working: state.working,
|
|
3313
|
+
awaitingAnswer: state.awaitingAnswer, qaHadAsk: state.qaHadAsk,
|
|
3314
|
+
engine: state.engine, model: state.model, arts: turnArts,
|
|
3315
|
+
};
|
|
3316
|
+
state.qa = null;
|
|
3317
|
+
state.working = null; // 不回放里抢「运行中」那一行(mountWorking 就没得搬)
|
|
3318
|
+
if (typeof qaTurnArtsTrack === 'function') turnArts = qaTurnArtsTrack(); // 这一页的产物自己收,别和实时混账
|
|
3032
3319
|
state.replay = true;
|
|
3033
3320
|
state.replayT0 = 0; state.replayLastAt = 0;
|
|
3034
3321
|
state.qaClosed = false; // 历史轮次要先能开新的一笔
|
|
@@ -3039,7 +3326,22 @@
|
|
|
3039
3326
|
/* 页尾通常切在同一轮中间(页首才是 user,见 histCursor)→ 封口,
|
|
3040
3327
|
否则最后一笔会一直挂着「生成中」胶囊;整轮完整时这里已是空操作。 */
|
|
3041
3328
|
finishReplayRound();
|
|
3042
|
-
} finally {
|
|
3329
|
+
} finally {
|
|
3330
|
+
state.replay = false;
|
|
3331
|
+
turnArts = live.arts;
|
|
3332
|
+
state.qa = live.qa;
|
|
3333
|
+
state.working = live.working;
|
|
3334
|
+
state.qaClosed = live.qaClosed;
|
|
3335
|
+
state.lastUserSig = live.lastUserSig;
|
|
3336
|
+
state.agentRaw = live.agentRaw;
|
|
3337
|
+
state.awaitingAnswer = live.awaitingAnswer;
|
|
3338
|
+
state.qaHadAsk = live.qaHadAsk;
|
|
3339
|
+
/* 旧轮次里的 start 事件会把选择器改口成那一轮的引擎 / 模型 —— 翻页不该改现在这一轮 */
|
|
3340
|
+
if (state.engine !== live.engine || state.model !== live.model) {
|
|
3341
|
+
state.engine = live.engine; state.model = live.model;
|
|
3342
|
+
syncPickers();
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3043
3345
|
t.appendChild(frag);
|
|
3044
3346
|
/* 别让内核把这个动作当成「有新内容」把画面拽回底部(qaUserAway 是它的滚动闸)。 */
|
|
3045
3347
|
if (typeof qaUserAway !== 'undefined') { try { qaUserAway = true; } catch (e) {} }
|
|
@@ -3053,8 +3355,9 @@
|
|
|
3053
3355
|
var h = state.hist;
|
|
3054
3356
|
var sid = state.session;
|
|
3055
3357
|
if (!sid || !h || h.loading || !h.more) return Promise.resolve(false);
|
|
3056
|
-
/*
|
|
3057
|
-
|
|
3358
|
+
/* ⚠️ 不再因为「这一轮还在跑」就拒绝翻页(2026-10-24 用户:「点击向上滚动没反应」):
|
|
3359
|
+
此前 busy 直接 return,按钮看着能点、点了一点动静都没有。回放与实时那一轮的状态
|
|
3360
|
+
隔离在 prependPage 里做(见那里的注释)。 */
|
|
3058
3361
|
h.loading = true;
|
|
3059
3362
|
syncMore();
|
|
3060
3363
|
/* 一页装不下一整轮时(实测一条会话有 5000 条思考/工具的分页上限),服务端只能给出
|
|
@@ -3152,8 +3455,15 @@
|
|
|
3152
3455
|
if (casc) {
|
|
3153
3456
|
casc.querySelector('.casc__p').textContent = state.engine || '选择引擎';
|
|
3154
3457
|
/* 展示走 qaModelText:剥掉 codebuddy 系自定义模型的 `custom-local:` 前缀
|
|
3155
|
-
(`state.model` 本身仍是完整 id,`-m` 要用它)。
|
|
3156
|
-
|
|
3458
|
+
(`state.model` 本身仍是完整 id,`-m` 要用它)。
|
|
3459
|
+
dsh 的 id 是 `route/model`:同名模型可能挂在多条路由下(modelverse 与
|
|
3460
|
+
volcengine 都有 deepseek-v4.1-flash-1017),胶囊只显示末段就分不清选的是
|
|
3461
|
+
哪条 —— 2026-10-25 报障里「我明明选了另一条」就是这么来的,故 dsh 带上路由。 */
|
|
3462
|
+
var modelLabel = qaModelText(state.model, state.engine) || '默认模型';
|
|
3463
|
+
var modelRoute = qaModelRoute(state.model, state.engine);
|
|
3464
|
+
if (modelRoute) modelLabel = modelRoute + '/' + modelLabel;
|
|
3465
|
+
casc.querySelector('.casc__s').textContent = modelLabel;
|
|
3466
|
+
casc.title = engineLockReason(); // 置灰胶囊的悬停说明(历史会话锁)
|
|
3157
3467
|
}
|
|
3158
3468
|
var chip = ctx.permBtn;
|
|
3159
3469
|
if (chip) {
|
|
@@ -3384,6 +3694,11 @@
|
|
|
3384
3694
|
try { typed = String(ctx.input.value || '').trim(); } catch (e0) { typed = ''; }
|
|
3385
3695
|
/* 附件也算「有内容」:忙时贴了图再点发送 = 排队,不能把它当成「停止」(同 send 的闸门)。 */
|
|
3386
3696
|
if (typed || state.plugin || (state.attachments || []).length) return;
|
|
3697
|
+
/* 2026-10-24 用户:「状态已经显示黄色的是不是算停止了 按钮应该变回提交状态,
|
|
3698
|
+
只有真的运行中状态才是停止」—— waiting(等审批)时按钮是「发送」,空输入点它
|
|
3699
|
+
不是中断:不许把正等着你确认的那一轮打断。不 preventDefault,让表单照常走
|
|
3700
|
+
send()(空输入它自己 no-op)。 */
|
|
3701
|
+
if (state.phase !== 'running') return;
|
|
3387
3702
|
e.preventDefault();
|
|
3388
3703
|
/* 用户按了「停止」:托管当场暂停,不等收尾行 —— 否则它可能在这一轮结束的
|
|
3389
3704
|
同一秒自动发出下一条,看起来像「点了停止还在自己跑」。 */
|
|
@@ -3430,7 +3745,7 @@
|
|
|
3430
3745
|
/* 内核的角色行 / 「生成中」胶囊都走这里取显示名。**必须剥掉 `custom-local:`**
|
|
3431
3746
|
—— 本壳没有 modelLabels,宿主回不出可读名时内核会退回 id,前缀就露出来了
|
|
3432
3747
|
(移动端尤其明显:它没有 PC 那份 qaModelName 标签表,见 04-task-list.js)。 */
|
|
3433
|
-
name: function (id) { return qaModelText(id) || qaModelText(state.model); },
|
|
3748
|
+
name: function (id) { return qaModelText(id, state.engine) || qaModelText(state.model, state.engine); },
|
|
3434
3749
|
set: function (id) { state.model = id ? String(id) : ''; syncPickers(); return true; },
|
|
3435
3750
|
};
|
|
3436
3751
|
window.MagicHost.toast = function (msg) { console.info('[conv] ' + String(msg == null ? '' : msg)); };
|
|
@@ -3944,6 +4259,8 @@
|
|
|
3944
4259
|
`t.model = s.model || ""`):空值 = 让 core 用默认档,不替用户预设。 */
|
|
3945
4260
|
state.model = (s && s.model) || '';
|
|
3946
4261
|
clearThread();
|
|
4262
|
+
/* 新开 / 换到空会话:相位归零(有历史的会话由 loadHistory 末尾按日志定档)。 */
|
|
4263
|
+
if (!state.busy) setPhase('idle');
|
|
3947
4264
|
/* 换到另一条会话:上一条留下的插件胶囊不跟过来(它是「这一轮用哪个 CLI」,
|
|
3948
4265
|
属于未发出的那一句,换了会话就该作废)。 */
|
|
3949
4266
|
setPlugin(null);
|
|
@@ -3982,7 +4299,7 @@
|
|
|
3982
4299
|
不做这步的话,带着一个跑不起来的引擎发任务,报错指向 core 而不是
|
|
3983
4300
|
「这个引擎没了」;宿主记的默认值也会一直卡在死引擎上。
|
|
3984
4301
|
(清单为空 = 探测失败,保持原引擎不动,别把用户的默认值清掉。) */
|
|
3985
|
-
var okRows = state.engines
|
|
4302
|
+
var okRows = pickableEngines(state.engines);
|
|
3986
4303
|
if (!state.engine) {
|
|
3987
4304
|
var firstOk = okRows[0];
|
|
3988
4305
|
if (firstOk) state.engine = firstOk.engine; // 「自动」档:静默取首个可用,不回写宿主
|
|
@@ -4021,6 +4338,7 @@
|
|
|
4021
4338
|
免得一块已经不在页面上的排队条继续替自己发车。 */
|
|
4022
4339
|
state.queue = [];
|
|
4023
4340
|
state.busy = false;
|
|
4341
|
+
state.phase = 'idle'; // 销毁即归零:新实例从干净相位起步
|
|
4024
4342
|
Sheet.close();
|
|
4025
4343
|
if (wrap.parentNode) wrap.parentNode.removeChild(wrap);
|
|
4026
4344
|
}
|
|
@@ -4042,6 +4360,11 @@
|
|
|
4042
4360
|
answer: function (text) { return control('answer', text); },
|
|
4043
4361
|
setEngine: function (n) { state.engine = n; syncPickers(); },
|
|
4044
4362
|
setModel: function (n) { state.model = n; syncPickers(); },
|
|
4363
|
+
/* 主备(2026-10-24):宿主改「我的 → 备用引擎」时同步进来(新会话立即生效)。 */
|
|
4364
|
+
setFallback: function (engine, model) {
|
|
4365
|
+
state.fallbackEngine = String(engine || '').trim();
|
|
4366
|
+
state.fallbackModel = state.fallbackEngine ? String(model || '').trim() : '';
|
|
4367
|
+
},
|
|
4045
4368
|
setPermission: function (v) { state.permission = v; syncPickers(); },
|
|
4046
4369
|
setWorkspace: function (v) { state.workspace = v || ''; },
|
|
4047
4370
|
permissionLabel: function () { return tierOf(state.permission || '').label; },
|