magic-agent 2.4.2 → 2.6.0
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 +290 -30
- package/README.md +15 -14
- package/client-ui/pc/app.css +32 -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 +37 -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 +241 -23
- package/client-ui/shared/conv/06-ask-answer.js +75 -1
- package/client-ui/shared/conv/09-stream-apply.js +63 -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 +81 -0
- package/client-ui/shared/conv/conv.css +79 -19
- package/client-ui/shared/conv/dom-map.md +1 -0
- package/client-ui/shared/conversation.css +13 -2
- package/client-ui/shared/conversation.js +336 -50
- package/client-ui/shared/transport-http.js +14 -0
- package/client-ui-mobile/index.html +2932 -333
- package/client-ui-mobile/reqs.css +102 -0
- package/client-ui-mobile/reqs.js +485 -70
- package/client-ui-mobile/reqshot.js +505 -0
- package/client-ui-mobile/scripts.css +21 -0
- package/client-ui-mobile/scripts.js +539 -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 +33 -9
- package/cmd/magic-client/main.go +57 -26
- 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_resume.go +13 -2
- 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/fallback.go +313 -0
- package/internal/agent/fallback_test.go +340 -0
- package/internal/agent/launch.go +43 -1
- 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 +136 -6
- package/internal/agent/stream_test.go +25 -38
- package/internal/agent/stream_timeline_test.go +2 -37
- package/internal/agent/streamjson.go +108 -6
- 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 +160 -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/events.go +4 -0
- package/internal/cli/quotas.go +125 -0
- package/internal/cli/requirements.go +51 -0
- package/internal/cli/root.go +122 -5
- package/internal/cli/session.go +7 -10
- 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/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/daemon.go +90 -17
- 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/jobs.go +294 -21
- 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 +193 -7
- package/internal/client/lan_test.go +110 -0
- package/internal/client/quotas_merge_test.go +107 -0
- package/internal/client/requirements.go +198 -1
- package/internal/client/requirements_shots_test.go +200 -0
- package/internal/client/restart.go +134 -0
- package/internal/client/restart_test.go +151 -0
- package/internal/client/scripts.go +1047 -0
- package/internal/client/scripts_decode_other.go +27 -0
- package/internal/client/scripts_decode_test.go +125 -0
- package/internal/client/scripts_decode_windows.go +206 -0
- package/internal/client/scripts_test.go +726 -0
- package/internal/client/service.go +322 -18
- package/internal/client/service_test.go +51 -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/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/session.go +92 -0
- package/internal/session/sessionlog.go +23 -0
- package/internal/session/sessionlog_read_test.go +39 -0
- 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 +1 -5
- 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/package.json +8 -2
|
@@ -35,42 +35,69 @@
|
|
|
35
35
|
<!-- 需求页(「需求」tab)是独立子页面:样式在 reqs.css、结构/逻辑在 reqs.js。
|
|
36
36
|
本文件只给两个空容器与一次 mount,不再混入这一页的实现。 -->
|
|
37
37
|
<link rel="stylesheet" href="reqs.css">
|
|
38
|
+
<!-- 脚本页(「发现 → 脚本」)是独立子页面:样式在 scripts.css、结构/逻辑在 scripts.js。
|
|
39
|
+
本文件只给两个空容器与一次 mount,不再混入这一页的实现。 -->
|
|
40
|
+
<link rel="stylesheet" href="scripts.css">
|
|
38
41
|
<style>
|
|
39
|
-
/* =====
|
|
40
|
-
⚠️ 默认 =
|
|
42
|
+
/* ===== 令牌:几何/字阶照 ZCode 安卓端;颜色契约见下 =====
|
|
43
|
+
⚠️ 默认 = 暗色。浅色仍逐字段对齐 ZCode Color.kt;
|
|
44
|
+
暗色自 2026-10-24 起改为掌天瓶墨绿系(用户:「这个深色跟掌天瓶主色不搭」)。 */
|
|
41
45
|
:root{
|
|
42
46
|
color-scheme:dark;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
/* 2026-10-24 用户定稿:「这个深色跟掌天瓶主色不搭」——
|
|
48
|
+
暗色不再照抄 ZCode 的中性蓝灰(#111827/#1F2937),整套换成**掌天瓶墨绿系**:
|
|
49
|
+
页面底色 = 近乎黑的墨绿,卡片往上提一档,分界线带一层淡玉光。
|
|
50
|
+
色相统一压在绿(H≈158°),亮度层级与原来一致,只换色相,不动对比关系。 */
|
|
51
|
+
--canvas:#0B1512; /* background = 掌天瓶墨绿底 */
|
|
52
|
+
--ink:#F2FBF5; --ink-soft:#DCEAE2; --muted:#A7BFB2; --faint:#7C9488;
|
|
53
|
+
--border:rgba(167,243,208,.12); --subtle:#20322A; --raised:#14211C;
|
|
54
|
+
--primary-action:#6EE7A8; --on-primary-action:#07130E; /* primary = 掌天瓶玉青(原 ZCode 品牌蓝) */
|
|
47
55
|
/* 底栏「+」凹槽的两件套,**必须按主题分别给**(`--fab-well` 槽底,`--fab-notch` 接触影满强度色):
|
|
48
|
-
槽底不能用 `--canvas` —— 那是「页面底色」,暗色下 #
|
|
56
|
+
槽底不能用 `--canvas` —— 那是「页面底色」,暗色下 #0B1512 比栏面 #14211C 暗 14 阶,
|
|
49
57
|
整圈会读成一道黑箍(比浮起来更假)。槽底该是**栏面稍暗一档**的同一个色系:
|
|
50
|
-
暗色 #
|
|
58
|
+
暗色 #14211C → #0F1B17(同色相压 4 阶),浅色 #FFFFFF → #EEF2F7(同色相压 8 阶,必须够,
|
|
51
59
|
因为 248/250/252 与 255 只差 7 阶,直接用 canvas 等于没画)。 */
|
|
52
|
-
--fab-well:#
|
|
53
|
-
--app-shadow:rgba(0,0,0,.
|
|
60
|
+
--fab-well:#0F1B17; --fab-notch:rgba(0,0,0,.22);
|
|
61
|
+
--app-shadow:rgba(0,0,0,.5);
|
|
54
62
|
|
|
55
63
|
/* 用户气泡 = surface/foreground:ZCode 的用户消息是**中性表面**不是品牌色气泡 */
|
|
56
|
-
--bubble:#
|
|
57
|
-
--code:#
|
|
58
|
-
--activity:rgba(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
--
|
|
62
|
-
--
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
--bubble:#15231E; --bubble-text:#F2FBF5;
|
|
65
|
+
--code:#070F0D; --status-neutral:#20322A; --status-neutral-text:#A7BFB2;
|
|
66
|
+
--activity:rgba(67,201,138,.14);
|
|
67
|
+
/* 底栏选中态 = 玉青(2026-10-24 用户定稿「修仙器物图标 + 玉青选中」):
|
|
68
|
+
与掌天瓶绿同源但比 --q-bottle 亮一档,暗底上才压得住;浅底用沉一档的玉青。 */
|
|
69
|
+
--tab-on:#6EE7A8; --tab-on-bg:rgba(34,197,94,.16);
|
|
70
|
+
--tab-off:#A7BFB2;
|
|
71
|
+
/* 底栏图标的双色点缀(2026-10-24「大胆设计」):鎏金 + 丹血朱。
|
|
72
|
+
25px 上颜色本身就是最强的辨识信号 —— 单色描边怎么调都读不出「法器」。 */
|
|
73
|
+
--q-gold:#E8C77A; --q-blood:#E23B2E;
|
|
74
|
+
|
|
75
|
+
--md-body:#F2FBF5; --md-muted:#A7BFB2; --md-border:#25392F; --md-code-bg:#182720;
|
|
76
|
+
--link:#6EE7A8; --prompt:#6EE7A8; --shimmer:#F2FBF5;
|
|
77
|
+
--panel:#14211C; --panel-border:#25392F;
|
|
78
|
+
|
|
79
|
+
/* 语义三态必须互相区分的(2026-10-25 深色配色量化发现):
|
|
80
|
+
`--running` 绿 / `--queued` 金 / `--warning` 琥珀 三个在屏幕上是**并排出现**的
|
|
81
|
+
(列表行状态、需求页「待整理」徽标 + 「上次整理失败」横幅),但原来
|
|
82
|
+
warning 与 queued 同是橙金族(H43 S96 L56 vs H42 S71 L69),对比度只有 **1.02**
|
|
83
|
+
—— 等于同一个颜色,用户分不出「排队等跑」和「出问题了」。
|
|
84
|
+
现在把 warning 压成深琥珀 #D97706(H32 S95 L44):明度落到 gold 的 6 折,
|
|
85
|
+
两者拉开到 1.95(色相差也从 1° 拉到 10°);同时对页底仍有 5.83 —— 警告文字照读不误。
|
|
86
|
+
这也顺带治好了需求页那一整块 #FBBF24 淡底横幅(占屏 13.7% 像素)的刺眼问题。 */
|
|
87
|
+
--accent:#6EE7A8; --warning:#D97706; --success:#22C55E;
|
|
65
88
|
--error-surface:#3B1113; --error-border:#7F1D1D; --error-text:#FCA5A5; --error-icon:#F87171;
|
|
66
|
-
--running:#22C55E; --transitioning:#
|
|
67
|
-
/*
|
|
68
|
-
|
|
69
|
-
--tint-
|
|
89
|
+
--running:#22C55E; --transitioning:#6EE7A8; --active:#FBBF24;
|
|
90
|
+
/* 状态色底(半透明叠加色):暗色「运行中」用玉青底跟主色同族;
|
|
91
|
+
浅色另给一份蓝底(见下面两个浅色块),保持 ZCode 那份观感不变。 */
|
|
92
|
+
--tint-run:rgba(45,212,191,.18); --tint-wait:rgba(217,119,6,.20); --tint-done:rgba(34,197,94,.18);
|
|
93
|
+
/* 排队 = 蓄势待发 → 鎏金(同浅色那份)。原来用紫 #8B5CF6,
|
|
94
|
+
在墨绿底上没有任何亲缘色,一眼就是异物。 */
|
|
95
|
+
--tint-queued:rgba(232,199,122,.16); --queued:#E8C77A;
|
|
70
96
|
/* 异常终态两档(2026-10-23 用户:「现在没有异常状态的颜色」):
|
|
71
|
-
· 失败(failed
|
|
72
|
-
· 已取消 /
|
|
73
|
-
|
|
97
|
+
· 失败(failed)→ 红:引擎报错结束,这条没跑完就没了,要人看一眼;
|
|
98
|
+
· 已取消 / 已打断 / 断连(cancelled / interrupted / gone)→ 石褐:人为收工或进程
|
|
99
|
+
被外部带走(服务重启、终端关闭),不是故障,所以不上红 —— 但也绝不能和
|
|
100
|
+
「正常结束」的绿或兜底的白同色。 */
|
|
74
101
|
--tint-fail:rgba(239,68,68,.18); --fail:#F87171;
|
|
75
102
|
--tint-stop:rgba(120,113,108,.24); --stop:#A8A29E;
|
|
76
103
|
|
|
@@ -98,6 +125,15 @@
|
|
|
98
125
|
皮肤三端一份,基线各端给自己的 —— 这两个值就是 tests/check-ui-spec.mjs 里钉的那两条。 */
|
|
99
126
|
--qa-bubble-r:12px 4px 12px 12px; /* ConversationUserInputBubble.tsx:27 rounded-xl + rounded-tr-xs */
|
|
100
127
|
--qa-body-lh:28px; /* ai-elements/message.tsx MessageResponse leading-[1.75] */
|
|
128
|
+
|
|
129
|
+
/* 中间 FAB 的「深玉盘 + 金环」材质(2026-10-25 深色配色;与 html[data-theme="dark"] 同一份)。
|
|
130
|
+
这里是**默认深色**(无 data-theme、系统也不是浅色时走这里),所以也得给一份,
|
|
131
|
+
否则系统深色下 FAB 会退回平涂亮绿 —— 那就只有「手动选深色」才生效了。
|
|
132
|
+
浅色两个块都不给这两项 → 落到 .tabfab__b 的兜底值 = 原样。 */
|
|
133
|
+
--fab-sheen:linear-gradient(178deg,rgba(255,255,255,.20),rgba(255,255,255,0) 52%);
|
|
134
|
+
--fab-base:radial-gradient(circle at 50% 28%,#3FAE79 0%,#1B7449 44%,#093720 100%);
|
|
135
|
+
--fab-ring:rgba(232,199,122,.42); --fab-ring-w:1.5px;
|
|
136
|
+
--fab-halo:rgba(63,174,121,.12);
|
|
101
137
|
}
|
|
102
138
|
@media (prefers-color-scheme:light){
|
|
103
139
|
:root{
|
|
@@ -111,14 +147,17 @@
|
|
|
111
147
|
--bubble:#FFFFFF; --bubble-text:#111827;
|
|
112
148
|
--code:#F1F5F9; --status-neutral:#E2E8F0; --status-neutral-text:#475569;
|
|
113
149
|
--activity:rgba(37,99,235,.08);
|
|
150
|
+
/* 玉青选中(浅底沉一档,太浅会读不出「选中」) */
|
|
151
|
+
--tab-on:#0C8F5C; --tab-on-bg:rgba(16,163,90,.10);
|
|
152
|
+
/* 浅底:鎏金要压暗一档才看得清,丹血朱提亮一点 */
|
|
153
|
+
--q-gold:#B98A2E; --q-blood:#D13A2C;
|
|
114
154
|
--md-body:#111827; --md-muted:#64748B; --md-border:#E2E8F0; --md-code-bg:#F1F5F9;
|
|
115
155
|
--link:#2563EB; --prompt:#2563EB; --shimmer:#0F172A;
|
|
116
156
|
--panel:#FFFFFF; --panel-border:#E2E8F0;
|
|
117
157
|
--accent:#2563EB; --warning:#D97706; --success:#16A34A;
|
|
118
158
|
--error-surface:#FEF2F2; --error-border:#FECACA; --error-text:#B91C1C; --error-icon:#DC2626;
|
|
119
159
|
--running:#16A34A; --transitioning:#2563EB; --active:#D97706;
|
|
120
|
-
--
|
|
121
|
-
/* 浅底上异常终态要更重(半透明红 / 石褐在 #F8FAFC 上否则会糊成灰) */
|
|
160
|
+
--tint-run:rgba(59,130,246,.18);
|
|
122
161
|
--tint-fail:rgba(220,38,38,.13); --fail:#DC2626;
|
|
123
162
|
--tint-stop:rgba(120,113,108,.16); --stop:#78716C;
|
|
124
163
|
/* 浅底上玉胎要更浓:`.42` 那档实测会被冲成白瓷,读不出「翠」。浓到 .66 又像实心塑料。 */
|
|
@@ -127,6 +166,28 @@
|
|
|
127
166
|
--q-bottle-jade-lo:rgba(40,152,104,.66);
|
|
128
167
|
--q-bottle-line:#37955F; --q-bottle-rim:#FFFFFF;
|
|
129
168
|
--q-bottle-glow:rgba(45,200,120,.20);
|
|
169
|
+
/* 中间 FAB 材质:浅色**钉回原样**(平涂亮绿、无金环、无外晕)。
|
|
170
|
+
不钉的话会从 :root 继承到深色那份「深玉盘 + 金环」—— 浅色就被改了。
|
|
171
|
+
2026-10-25 用户:「浅色皮肤不变 改深色皮肤配色」。 */
|
|
172
|
+
--fab-sheen:linear-gradient(178deg,rgba(255,255,255,.16),rgba(255,255,255,0) 60%);
|
|
173
|
+
--fab-base:#22C55E;
|
|
174
|
+
--fab-ring:transparent; --fab-ring-w:0;
|
|
175
|
+
--fab-halo:transparent;
|
|
176
|
+
/* ── 浅色**保持不变**(2026-10-25 用户:「浅色皮肤不变 改深色皮肤配色」)──
|
|
177
|
+
下面四项是这一轮新加的令牌,取值刻意等于「改动前浅色实际渲染出来的那个颜色」,
|
|
178
|
+
所以浅色一个像素都不变,只是从「继承深色新值」改回「自己钉住旧值」:
|
|
179
|
+
--tab-off = 旧的 --muted(底栏未选中原本就用 --muted 画)
|
|
180
|
+
--gem-facet = 旧的 --q-gold(灵石刻面原本是鎏金线)
|
|
181
|
+
--btn-me-bg/fg= 顶栏头像的「金底 + 墨绿元婴」,与原公式 color-mix(q-gold 34%, raised) / #12211A 等值
|
|
182
|
+
--queued = 旧的排队紫(原本从 :root 继承;深色已改鎏金,浅色仍用紫) */
|
|
183
|
+
--tab-off:#64748B;
|
|
184
|
+
--gem-facet:#B98A2E;
|
|
185
|
+
--btn-me-bg:color-mix(in srgb,var(--q-gold) 34%,var(--raised)); --btn-me-fg:#12211A;
|
|
186
|
+
--queued:#8B5CF6; --tint-queued:rgba(139,92,246,.18);
|
|
187
|
+
/* --tint-wait 也得钉住:浅色块原来没写它(一直从 :root 继承 #F59E0B 那份),
|
|
188
|
+
这一轮深色把它改成深琥珀了,不钉就会连带把浅色的「待整理」徽标底色改深。
|
|
189
|
+
--tint-done / --q-bottle* 同样没写,但那几个 :root 的值这轮没动,无需重复钉。 */
|
|
190
|
+
--tint-wait:rgba(245,158,11,.20);
|
|
130
191
|
}
|
|
131
192
|
}
|
|
132
193
|
/* 手动主题:在「我的」页显式选了才生效(属性选择器优先级高于上面的 :root / media) */
|
|
@@ -140,13 +201,16 @@ html[data-theme="light"]{color-scheme:light;
|
|
|
140
201
|
--bubble:#FFFFFF; --bubble-text:#111827;
|
|
141
202
|
--code:#F1F5F9; --status-neutral:#E2E8F0; --status-neutral-text:#475569;
|
|
142
203
|
--activity:rgba(37,99,235,.08);
|
|
204
|
+
/* 玉青选中(同 prefers-color-scheme:light 那一份) */
|
|
205
|
+
--tab-on:#0C8F5C; --tab-on-bg:rgba(16,163,90,.10);
|
|
206
|
+
--q-gold:#B98A2E; --q-blood:#D13A2C;
|
|
143
207
|
--md-body:#111827; --md-muted:#64748B; --md-border:#E2E8F0; --md-code-bg:#F1F5F9;
|
|
144
208
|
--link:#2563EB; --prompt:#2563EB; --shimmer:#0F172A;
|
|
145
209
|
--panel:#FFFFFF; --panel-border:#E2E8F0;
|
|
146
210
|
--accent:#2563EB; --warning:#D97706; --success:#16A34A;
|
|
147
211
|
--error-surface:#FEF2F2; --error-border:#FECACA; --error-text:#B91C1C; --error-icon:#DC2626;
|
|
148
212
|
--running:#16A34A; --transitioning:#2563EB; --active:#D97706;
|
|
149
|
-
|
|
213
|
+
--tint-run:rgba(59,130,246,.18);
|
|
150
214
|
--tint-fail:rgba(220,38,38,.13); --fail:#DC2626;
|
|
151
215
|
--tint-stop:rgba(120,113,108,.16); --stop:#78716C;
|
|
152
216
|
/* 浅底上玉胎要更浓(同上一条注释) */
|
|
@@ -155,23 +219,73 @@ html[data-theme="light"]{color-scheme:light;
|
|
|
155
219
|
--q-bottle-jade-lo:rgba(40,152,104,.66);
|
|
156
220
|
--q-bottle-line:#37955F; --q-bottle-rim:#FFFFFF;
|
|
157
221
|
--q-bottle-glow:rgba(45,200,120,.20);
|
|
222
|
+
/* 与 prefers-color-scheme:light 那一份逐值相同 —— 浅色保持不变。 */
|
|
223
|
+
--tab-off:#64748B;
|
|
224
|
+
--gem-facet:#B98A2E;
|
|
225
|
+
--btn-me-bg:color-mix(in srgb,var(--q-gold) 34%,var(--raised)); --btn-me-fg:#12211A;
|
|
226
|
+
--queued:#8B5CF6; --tint-queued:rgba(139,92,246,.18);
|
|
227
|
+
/* --tint-wait 钉回原值(理由见 prefers-color-scheme:light 那份) */
|
|
228
|
+
--tint-wait:rgba(245,158,11,.20);
|
|
229
|
+
/* 中间 FAB 材质:浅色钉回原样(同 prefers-color-scheme:light 那份) */
|
|
230
|
+
--fab-sheen:linear-gradient(178deg,rgba(255,255,255,.16),rgba(255,255,255,0) 60%);
|
|
231
|
+
--fab-base:#22C55E;
|
|
232
|
+
--fab-ring:transparent; --fab-ring-w:0;
|
|
233
|
+
--fab-halo:transparent;
|
|
158
234
|
}
|
|
235
|
+
/* 手动主题:在「我的」页显式选了才生效(属性选择器优先级高于上面的 :root / media)。
|
|
236
|
+
⚠️ 这一块**必须把颜色令牌补全**(2026-10-25 实测发现的结构性 bug):
|
|
237
|
+
原来只写了 43 个令牌,比浅色块少了 30 个颜色令牌。系统偏好是**浅色**时手动选深色,
|
|
238
|
+
漏掉的令牌不会回落到 :root(:root 本身也在 media 浅色块里被覆盖了),
|
|
239
|
+
而是继续用浅色的值 —— 于是深色下出现蓝底头像(--tint-run 蓝)、暖米黄头像
|
|
240
|
+
(--btn-me-bg 硬编码米黄)、紫排队(--queued 紫)。补全后两套手动主题都自洽。 */
|
|
159
241
|
html[data-theme="dark"]{color-scheme:dark;
|
|
160
|
-
--canvas:#
|
|
161
|
-
--ink:#
|
|
162
|
-
--border:rgba(
|
|
163
|
-
--primary-action:#
|
|
164
|
-
--fab-well:#
|
|
165
|
-
--app-shadow:rgba(0,0,0,.
|
|
166
|
-
--bubble:#
|
|
167
|
-
--code:#
|
|
168
|
-
--activity:rgba(
|
|
169
|
-
|
|
170
|
-
--
|
|
171
|
-
--
|
|
172
|
-
--
|
|
242
|
+
--canvas:#0B1512;
|
|
243
|
+
--ink:#F2FBF5; --ink-soft:#DCEAE2; --muted:#A7BFB2; --faint:#7C9488;
|
|
244
|
+
--border:rgba(167,243,208,.12); --subtle:#20322A; --raised:#14211C;
|
|
245
|
+
--primary-action:#6EE7A8; --on-primary-action:#07130E;
|
|
246
|
+
--fab-well:#0F1B17; --fab-notch:rgba(0,0,0,.22);
|
|
247
|
+
--app-shadow:rgba(0,0,0,.5);
|
|
248
|
+
--bubble:#15231E; --bubble-text:#F2FBF5;
|
|
249
|
+
--code:#070F0D; --status-neutral:#20322A; --status-neutral-text:#A7BFB2;
|
|
250
|
+
--activity:rgba(67,201,138,.14);
|
|
251
|
+
/* 玉青选中(同 :root 默认暗色那一份) */
|
|
252
|
+
--tab-on:#6EE7A8; --tab-on-bg:rgba(34,197,94,.16);
|
|
253
|
+
--tab-off:#A7BFB2;
|
|
254
|
+
--q-gold:#E8C77A; --q-blood:#E23B2E;
|
|
255
|
+
--md-body:#F2FBF5; --md-muted:#A7BFB2; --md-border:#25392F; --md-code-bg:#182720;
|
|
256
|
+
--link:#6EE7A8; --prompt:#6EE7A8; --shimmer:#F2FBF5;
|
|
257
|
+
--panel:#14211C; --panel-border:#25392F;
|
|
258
|
+
/* warning 压成深琥珀 —— 理由见 :root 里那段注释(与 queued 的 1.02 区分度问题) */
|
|
259
|
+
--accent:#6EE7A8; --warning:#D97706; --success:#22C55E;
|
|
173
260
|
--error-surface:#3B1113; --error-border:#7F1D1D; --error-text:#FCA5A5; --error-icon:#F87171;
|
|
174
|
-
--running:#22C55E; --transitioning:#
|
|
261
|
+
--running:#22C55E; --transitioning:#6EE7A8; --active:#FBBF24;
|
|
262
|
+
/* ── 以下 30 个是「补齐原先漏写的深色值」(同 :root 那份,逐值相同)──
|
|
263
|
+
分四组:状态 tint / 招标示 / 底栏法器 / 顶栏头像。 */
|
|
264
|
+
--tint-run:rgba(45,212,191,.18); --tint-wait:rgba(217,119,6,.20); --tint-done:rgba(34,197,94,.18);
|
|
265
|
+
--tint-queued:rgba(232,199,122,.16); --queued:#E8C77A;
|
|
266
|
+
--tint-fail:rgba(239,68,68,.18); --fail:#F87171;
|
|
267
|
+
--tint-stop:rgba(120,113,108,.24); --stop:#A8A29E;
|
|
268
|
+
--q-bottle-hi:#7DF3A8; --q-bottle:#22C55E; --q-bottle-lo:#0F7B3C; --q-bottle-deep:#06371F;
|
|
269
|
+
--q-bottle-line:#43A06E; --q-bottle-rim:#D9FBE8; --q-bottle-cork:#8A7659;
|
|
270
|
+
--q-bottle-glow:rgba(34,197,94,.30);
|
|
271
|
+
--q-bottle-jade-hi:rgba(120,232,175,.44);
|
|
272
|
+
--q-bottle-jade:rgba(52,192,124,.46);
|
|
273
|
+
--q-bottle-jade-lo:rgba(8,96,60,.58);
|
|
274
|
+
--gem-facet:var(--canvas);
|
|
275
|
+
--btn-me-bg:color-mix(in srgb,var(--q-gold) 34%,var(--raised)); --btn-me-fg:#12211A;
|
|
276
|
+
/* 中间 FAB(蜂云剑阵)的材质 —— 深色专属(2026-10-25 用户:「浅色皮肤不变 改深色皮肤配色」)。
|
|
277
|
+
原来 FAB 是 `--q-bottle` = #22C55E 的**平涂实心圆**:在墨绿底上 H145/S71/L45,
|
|
278
|
+
同一个色相却高出 35 阶明度、40 阶饱和,读起来是一只**发光的塑料球**,
|
|
279
|
+
也是整屏最刺眼的一处(截图实测,1.68 万像素是这个色)。
|
|
280
|
+
深色改成「深玉盘」:径向从玉心 (#3FAE79) 落到墨底 (#093720),明度压在
|
|
281
|
+
底栏栏面 (#14211C, L10) 与选中图标 (#6EE7A8, L67) 之间;
|
|
282
|
+
再补一圈 **1.5px 鎏金环** —— 主按钮的「这是可点的」不能靠更亮更大声,
|
|
283
|
+
而是靠金环这个**形状**(金与墨绿是这套配色的互补对,底栏法器纹样本来就有点金)。
|
|
284
|
+
玉光 halo 收到 .12:原来 .30 的 22px 绿晕是「发光」的另外一半原因。 */
|
|
285
|
+
--fab-sheen:linear-gradient(178deg,rgba(255,255,255,.20),rgba(255,255,255,0) 52%);
|
|
286
|
+
--fab-base:radial-gradient(circle at 50% 28%,#3FAE79 0%,#1B7449 44%,#093720 100%);
|
|
287
|
+
--fab-ring:rgba(232,199,122,.42); --fab-ring-w:1.5px;
|
|
288
|
+
--fab-halo:rgba(63,174,121,.12);
|
|
175
289
|
}
|
|
176
290
|
|
|
177
291
|
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
|
@@ -284,6 +398,20 @@ textarea{font:inherit}
|
|
|
284
398
|
.history__list{flex:1;min-height:0;overflow-y:auto;padding:4px 10px calc(96px + env(safe-area-inset-bottom))}
|
|
285
399
|
.history__group{padding:12px 8px 6px;font-size:12px;font-weight:700;color:var(--muted)}
|
|
286
400
|
.history__hint{padding:22px 12px;color:var(--muted);font-size:14px;text-align:center}
|
|
401
|
+
/* 会话列表加载失败(连不上桌面端)的失败态:不再是一行灰字,给图标 + 人话原因 +
|
|
402
|
+
可复制地址 + 重试/编辑环境入口(2026-10-25:「失败时的界面显示要设计一下」)。
|
|
403
|
+
颜色全走变量,暗/亮主题自动跟随;图标与排队/失败行同一套「一枚图标 + 状态 tint」。 */
|
|
404
|
+
.netfail{display:flex;flex-direction:column;align-items:center;gap:6px;padding:44px 24px 12px;text-align:center}
|
|
405
|
+
.netfail__ico{width:56px;height:56px;border-radius:50%;background:var(--tint-fail);color:var(--fail);
|
|
406
|
+
display:flex;align-items:center;justify-content:center;margin-bottom:6px}
|
|
407
|
+
.netfail__t{font-size:15px;font-weight:600;color:var(--ink)}
|
|
408
|
+
.netfail__why{font-size:13px;line-height:1.6;color:var(--muted)}
|
|
409
|
+
.netfail__addr{max-width:100%;margin-top:2px;padding:5px 12px;border-radius:var(--r-full);
|
|
410
|
+
background:var(--subtle);color:var(--muted);font-size:12px;
|
|
411
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
412
|
+
.netfail__addr:active{opacity:.7}
|
|
413
|
+
.netfail__acts{display:flex;flex-direction:column;gap:8px;width:200px;margin-top:14px}
|
|
414
|
+
.netfail__acts .btn{margin-top:0}
|
|
287
415
|
.srow{display:flex;align-items:center;gap:12px;width:100%;padding:12px 8px;border-radius:14px;text-align:left}
|
|
288
416
|
.srow:active{background:var(--subtle)}
|
|
289
417
|
.srow--pending{cursor:default}
|
|
@@ -302,7 +430,8 @@ textarea{font:inherit}
|
|
|
302
430
|
.srow__ico--done{background:var(--tint-done);color:var(--success)}
|
|
303
431
|
/* 异常终态:以前和 idle 一起落在灰白兜底档,用户看不出「这条是挂了 / 被停了,
|
|
304
432
|
还是只是没跑过」(2026-10-23 用户:「现在没有异常状态的颜色」)。
|
|
305
|
-
失败(failed
|
|
433
|
+
失败(failed)= 红;已取消 / 已打断 / 断连(cancelled / interrupted / gone)= 石褐
|
|
434
|
+
(2026-10-24 用户:「会话全部断连 红色」—— 服务重启把进程带走的那些不该算故障)。 */
|
|
306
435
|
.srow__ico--fail{background:var(--tint-fail);color:var(--fail)}
|
|
307
436
|
.srow__ico--stop{background:var(--tint-stop);color:var(--stop)}
|
|
308
437
|
.srow__ico--idle{background:var(--subtle);color:var(--muted)}
|
|
@@ -337,12 +466,22 @@ textarea{font:inherit}
|
|
|
337
466
|
手动置顶靠「置顶」分段表达,不重复画;跑完 statusOf 不再是 run,图钉随之消失。 */
|
|
338
467
|
.srow__auto{flex:0 0 auto;display:flex;align-items:center;color:var(--primary-action);opacity:.75}
|
|
339
468
|
/* 子会话(团队模式拆出的子任务,2026-10-22):**缩进挂在主会话下面**,不占顶层一行。
|
|
340
|
-
|
|
341
|
-
|
|
469
|
+
左侧用**树形折线**(├─ / └─)接出来,不是一根通到底的竖线(2026-10-24 用户:
|
|
470
|
+
「团队模式的子会话的线设计成树状 不要一条竖线」)—— 多个子任务读起来是一棵树,
|
|
471
|
+
只有一个子任务时就是一条 └─。
|
|
472
|
+
点开仍是它自己那份完整过程(自己的日志);行本身可点、可长按(重命名 / 删除同普通会话)。
|
|
342
473
|
⚠️ 默认**折叠**(2026-10-23 用户:「列表中默认不展开显示子会话」):父行只留一枚
|
|
343
474
|
计数徽标(有几个子会话 / 是否有子任务在跑),点它才展开。 */
|
|
344
475
|
.sblock{min-width:0}
|
|
345
|
-
|
|
476
|
+
/* 竖线分段画在每个子行自己的伪元素上:非末行整段,末行只画到中点(收口成 └)。 */
|
|
477
|
+
.sblock__kids{margin:0 8px 6px 26px}
|
|
478
|
+
.sblock__kid{position:relative;padding-left:16px}
|
|
479
|
+
.sblock__kid::before{content:"";position:absolute;left:0;top:0;bottom:0;width:2px;
|
|
480
|
+
background:var(--border);border-radius:1px}
|
|
481
|
+
.sblock__kid:last-child::before{bottom:calc(50% - 1px)}
|
|
482
|
+
/* 横向短线:从竖线拐进这一行(折线的「肘」)。 */
|
|
483
|
+
.sblock__kid::after{content:"";position:absolute;left:0;top:calc(50% - 1px);width:12px;height:2px;
|
|
484
|
+
background:var(--border);border-radius:0 1px 1px 0}
|
|
346
485
|
.srow__kids{flex:0 0 auto;display:inline-flex;align-items:center;gap:3px;height:20px;padding:0 5px;
|
|
347
486
|
border:1px solid var(--border);border-radius:999px;background:var(--subtle);color:var(--muted);
|
|
348
487
|
font-size:11px;font-weight:600;line-height:1;cursor:pointer;user-select:none;-webkit-user-select:none}
|
|
@@ -369,7 +508,10 @@ textarea{font:inherit}
|
|
|
369
508
|
.tasks__tools .topbar__icon{width:40px;height:40px}
|
|
370
509
|
/* 「我的」头像是截图里那枚金色圆头像:比搜索键小一圈,仍是 40px 点击区里的 34px 视觉圆。 */
|
|
371
510
|
.tasks__tools #btnTaskMe{width:34px;height:34px;margin:0 3px;border-radius:50%;
|
|
372
|
-
|
|
511
|
+
/* 金底 + 深褐元婴:底色/元婴色都走令牌(--btn-me-bg / --btn-me-fg)——
|
|
512
|
+
浅色主题钉住原来那套暖米黄 #E8DCB0 / 深褐 #4A3F1E,一个像素都不动;
|
|
513
|
+
深色主题给鎏金淡面 + 墨绿黑,跟底栏法器纹样同源。 */
|
|
514
|
+
background:var(--btn-me-bg);color:var(--btn-me-fg)}
|
|
373
515
|
/* 搜索态:输入框 + 关闭(搜索时隐藏快捷入口与分组行,结果平铺) */
|
|
374
516
|
.tasks__search{display:flex;align-items:center;gap:8px;flex:0 0 auto;padding:2px 16px 8px}
|
|
375
517
|
/* 搜索态就是截图 2 那一屏:标题行让位给搜索框(✕ 在框外,与截图一致)。 */
|
|
@@ -406,6 +548,11 @@ textarea{font:inherit}
|
|
|
406
548
|
.wsrow__chev.is-open{transform:rotate(90deg)}
|
|
407
549
|
.wsrow__add{flex:0 0 auto;width:36px;height:36px;border-radius:var(--r-full);display:flex;align-items:center;justify-content:center;color:var(--muted)}
|
|
408
550
|
.wsrow__add:active{background:var(--subtle)}
|
|
551
|
+
/* 空间选择行里的行内小动作(自定义空间的「✎ 改名」):行本身是 <button>,
|
|
552
|
+
里面不能再塞按钮(HTML 不允许嵌套按钮),所以是 span + role=button。 */
|
|
553
|
+
.pick__act{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;
|
|
554
|
+
min-width:30px;height:30px;border-radius:var(--r-full);color:var(--muted);font-size:15px}
|
|
555
|
+
.pick__act:active{background:var(--subtle)}
|
|
409
556
|
/* 长按会话行的浮动菜单 */
|
|
410
557
|
.rowmenu{position:fixed;z-index:1201;min-width:184px;background:var(--raised);border:1px solid var(--border);
|
|
411
558
|
border-radius:14px;box-shadow:0 14px 34px -12px var(--app-shadow);padding:6px}
|
|
@@ -473,6 +620,9 @@ button.setrow{font-family:inherit}
|
|
|
473
620
|
.pbadge{flex:0 0 auto;padding:2px 8px;border-radius:var(--r-full);font-size:11px;line-height:16px;white-space:nowrap}
|
|
474
621
|
.pbadge--on{color:var(--success);background:var(--tint-done)}
|
|
475
622
|
.pbadge--off{color:var(--muted);background:var(--subtle)}
|
|
623
|
+
/* 「可升级」:主色 + 描边(不用底色 —— 底色会和「已安装」的绿撞)。
|
|
624
|
+
语义是「有新版可装」,所以不能长成「已完成」的样子。 */
|
|
625
|
+
.pbadge--new{color:var(--primary-action);background:transparent;border:1px solid var(--primary-action);font-weight:600}
|
|
476
626
|
/* 详情页 */
|
|
477
627
|
.pd{padding:0 16px calc(28px + env(safe-area-inset-bottom));font-size:14px;line-height:1.7;color:var(--ink)}
|
|
478
628
|
.pd__head{display:flex;align-items:center;gap:10px;padding:6px 0 2px}
|
|
@@ -497,6 +647,13 @@ button.setrow{font-family:inherit}
|
|
|
497
647
|
.setrow+.setrow{border-top:1px solid var(--border)}
|
|
498
648
|
.setrow__l{flex:1;min-width:0;font-size:15px;color:var(--ink)}
|
|
499
649
|
.setrow__v{flex:0 0 auto;font-size:14px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:50%}
|
|
650
|
+
/* 主备值:主一行、备一行(右上对齐)。挤成「主 | 备 …」会被省略号吃掉后半截
|
|
651
|
+
(2026-10-24 用户:「没显示出来」),所以这里允许换行、两行各自省略。 */
|
|
652
|
+
.setrow__v--pair{display:flex;flex-direction:column;align-items:flex-end;gap:1px;
|
|
653
|
+
max-width:62%;white-space:normal;overflow:visible}
|
|
654
|
+
.setrow__v--pair .setrow__pair-main{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
655
|
+
.setrow__v--pair .setrow__pair-fb{font-size:12px;color:var(--muted);opacity:.9;
|
|
656
|
+
max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
500
657
|
/* 有新版本时这一格变成可点的「升级」提示(2026-10-22 用户:「版本现在也没点击升级的地方」) */
|
|
501
658
|
.setrow__v.is-upgradable{color:var(--primary-action);font-weight:600}
|
|
502
659
|
.setrow--danger{color:var(--error-text)}
|
|
@@ -566,20 +723,59 @@ button.setrow{font-family:inherit}
|
|
|
566
723
|
.tabbar{position:absolute;left:0;right:0;bottom:0;z-index:60;
|
|
567
724
|
display:flex;align-items:stretch;
|
|
568
725
|
min-height:calc(56px + env(safe-area-inset-bottom));
|
|
569
|
-
|
|
570
|
-
|
|
726
|
+
/* ⚠️ 必须是**不透明**的栏面:86% 半透 + blur 在真机上够用,
|
|
727
|
+
但在列表滚到底时下面那行「magic-stock · codebuddy」会透上来压在图标上
|
|
728
|
+
(2026-10-25 截图实测,深浅两主题都会)。玻璃感靠上面的飞檐和投影做,
|
|
729
|
+
不靠让文字透过来 —— 图标才是底栏唯一要读的东西。 */
|
|
730
|
+
background:var(--raised);
|
|
571
731
|
box-shadow:0 -1px 0 var(--border),0 -14px 30px -26px var(--app-shadow);
|
|
572
732
|
padding-bottom:env(safe-area-inset-bottom)}
|
|
733
|
+
/* 上边框 = 「飞檐翘角」(2026-10-25 用户先要弧形上缘,看过中间拱起的版本后:
|
|
734
|
+
「弧度反了」→ 反过来 —— 不是中间拱,而是**两角上翘、中段落低**的飞檐:
|
|
735
|
+
中式檐角向上挑,正中半嵌的 FAB 像檐下悬月,也更贴修仙主题):
|
|
736
|
+
① 一对 <i class="eave"> 贴在栏顶上方(bottom:100%):120×15 的方块只留
|
|
737
|
+
「顶角大圆弧」(border-top-right-radius 120px 15px,恰好占满整块),
|
|
738
|
+
剪影 = 角上 15px、向内 120px 平滑落回栏顶 —— 侧看正是一片上翘的檐角。
|
|
739
|
+
② hairline 两笔:檐弧 = eave 的 border-top + 弧形 border-right/left(border 跟圆角走,
|
|
740
|
+
1px 矢量清晰);中段直线 = 栏本体 box-shadow 0 -1px 0(檐块 86% 同色半透,
|
|
741
|
+
压在直线上只剩残影,肉眼不可见),两笔在弧线落点(距角 120px)相接。
|
|
742
|
+
③ 檐块与栏体同 fill(86% raised),不加 blur —— 14% 透差在纯色栏面上读不出接缝;
|
|
743
|
+
pointer-events:none 不吃点击;FAB 槽(::before)被 clip-path 钉在栏内,互不越界。 */
|
|
744
|
+
.eave{position:absolute;bottom:100%;width:120px;height:15px;z-index:0;pointer-events:none;
|
|
745
|
+
/* 与栏面同色(实色)—— 栏面改成不透明后,檐块若仍是 86% 半透就会比栏面浅一档,
|
|
746
|
+
两者相接处露一道色差,像贴了块补丁。 */
|
|
747
|
+
background:var(--raised);
|
|
748
|
+
border-top:1px solid var(--border)}
|
|
749
|
+
.eave-l{left:0;border-top-right-radius:120px 15px;border-right:1px solid var(--border)}
|
|
750
|
+
.eave-r{right:0;border-top-left-radius:120px 15px;border-left:1px solid var(--border)}
|
|
573
751
|
/* 只有图标:文案从底栏拿掉(2026-10-22 用户),语义交给 aria-label,不靠视觉标签。
|
|
574
752
|
命中区仍是 flex:1 × 56,比"图标 + 文字"那版更高更宽,点起来更稳。 */
|
|
575
753
|
.tab{flex:1;display:flex;align-items:center;justify-content:center;
|
|
576
|
-
|
|
577
|
-
.
|
|
578
|
-
|
|
579
|
-
|
|
754
|
+
/* 未选中用 --tab-off 而不是 --muted:--muted 是「次要文字」的档位,
|
|
755
|
+
拿来画 25px 的器物剪影偏淡 —— 浅色下在白栏面上只有 5.1:1,
|
|
756
|
+
而剪影全靠实心面积撑辨识度,实测比正文更吃对比(正文有字重兜底,剪影没有)。
|
|
757
|
+
浅色下压到 #3D5348(8.3:1),深色下保持 #A7BFB2(8.5:1)。 */
|
|
758
|
+
color:var(--tab-off);transition:color .16s}
|
|
759
|
+
/* 选中态 = 玉青(--tab-on,与掌天瓶绿同源;2026-10-24 修仙器物套组定稿),不再借主色蓝。
|
|
760
|
+
选中再给一圈「灵光」(drop-shadow 用选中色本色,浅色主题自动变淡):
|
|
761
|
+
粗玉器物 + 灵光才是「修仙」,光换颜色读不出「法器被唤醒」。 */
|
|
762
|
+
.tab.is-on{color:var(--tab-on)}
|
|
763
|
+
/* 灵光:26px 的图标上 6px / 55% 的晕几乎看不见(2026-10-25 截图实测)。
|
|
764
|
+
浅色那份**保持原值 7px / 32%**(墨绿选中的晕再浓就会把图标糊成一团);
|
|
765
|
+
深色提高到 9px / 85%,才读得出「法器被唤醒」。
|
|
766
|
+
两个值都只在各自主题生效 —— 2026-10-25 用户:「浅色皮肤不变 改深色皮肤配色」。 */
|
|
767
|
+
.tab.is-on svg{filter:drop-shadow(0 0 9px color-mix(in srgb,var(--tab-on) 85%,transparent))}
|
|
768
|
+
html[data-theme="light"] .tab.is-on svg,
|
|
769
|
+
html:not([data-theme="dark"]) .tab.is-on svg{
|
|
770
|
+
filter:drop-shadow(0 0 7px color-mix(in srgb,var(--tab-on) 32%,transparent))}
|
|
771
|
+
@media (prefers-color-scheme:dark){
|
|
772
|
+
html:not([data-theme]) .tab.is-on svg{filter:drop-shadow(0 0 9px color-mix(in srgb,var(--tab-on) 85%,transparent))}
|
|
773
|
+
}
|
|
774
|
+
/* 选中态不只换颜色,还给一块 44×34 的浅底:
|
|
775
|
+
没有文字之后,颜色是唯一的语义线索,必须再补一个形状 —— 光靠颜色区分太弱。 */
|
|
580
776
|
.tab .icw{display:grid;place-items:center;width:44px;height:34px;border-radius:11px;
|
|
581
777
|
transition:background .18s ease}
|
|
582
|
-
.tab.is-on .icw{background:var(--
|
|
778
|
+
.tab.is-on .icw{background:var(--tab-on-bg)}
|
|
583
779
|
.tab svg{display:block;transition:transform .16s}
|
|
584
780
|
.tab:active svg{transform:scale(.92)}
|
|
585
781
|
/* ── 「快速新建」:**半嵌入**底栏的圆形主按钮(2026-10-22 定稿)
|
|
@@ -636,7 +832,6 @@ button.setrow{font-family:inherit}
|
|
|
636
832
|
width:68px;height:68px;
|
|
637
833
|
display:flex;align-items:center;justify-content:center;
|
|
638
834
|
color:var(--on-primary-action);
|
|
639
|
-
background:linear-gradient(178deg,rgba(255,255,255,.14),rgba(255,255,255,0) 60%),var(--primary-action);
|
|
640
835
|
border:0;border-radius:50%;
|
|
641
836
|
box-shadow:
|
|
642
837
|
inset 0 -7px 13px -7px rgba(15,23,42,.50), /* 下半圈内侧暗边:光从上来 → 这一圈在阴影里 */
|
|
@@ -645,7 +840,187 @@ button.setrow{font-family:inherit}
|
|
|
645
840
|
transition:transform .14s ease, box-shadow .14s ease}
|
|
646
841
|
.tabfab__b:active{transform:translateX(-50%) translateY(2px);
|
|
647
842
|
box-shadow:inset 0 -6px 12px -7px rgba(15,23,42,.56), inset 0 2px 3px -2px rgba(255,255,255,.26)}
|
|
843
|
+
/* 按下时也要留住金环(否则深色下按下瞬间环消失,像按钮掉了层壳) */
|
|
844
|
+
html[data-theme="dark"] .tabfab__b:active{
|
|
845
|
+
box-shadow:inset 0 -6px 12px -7px rgba(15,23,42,.56), inset 0 2px 3px -2px rgba(255,255,255,.26),
|
|
846
|
+
0 0 0 1.5px var(--fab-ring,rgba(232,199,122,.42)), 0 0 14px 2px var(--fab-halo,rgba(63,174,121,.12))}
|
|
847
|
+
@media (prefers-color-scheme:dark){
|
|
848
|
+
html:not([data-theme]) .tabfab__b:active{
|
|
849
|
+
box-shadow:inset 0 -6px 12px -7px rgba(15,23,42,.56), inset 0 2px 3px -2px rgba(255,255,255,.26),
|
|
850
|
+
0 0 0 1.5px var(--fab-ring,rgba(232,199,122,.42)), 0 0 14px 2px var(--fab-halo,rgba(63,174,121,.12))}
|
|
851
|
+
}
|
|
648
852
|
.tabfab__b svg{display:block;stroke-width:2.4;transform:translateY(-1px)}
|
|
853
|
+
/* ── 丹炉炼丹动画(2026-10-24 用户:「中间的丹炉做炼丹动画效果」→「火焰动画重新设计」→「丹炉到第一个去」)──
|
|
854
|
+
丹炉已从中间 FAB 挪到**底栏第一格**(任务 tab 的图标位),动画类选择器随之从 #btnFabNew
|
|
855
|
+
改挂在 .furnsvg 上(中间 FAB 换成修行动画,见下面 medRing 段)。
|
|
856
|
+
火是**一群各自为政的舌**,不是一块整体在晃 —— 所以按「谁在动」分层,而不是按「整簇动」:
|
|
857
|
+
① 整组缓摇(.fabfire):一炉火整体缓慢起伏,幅度极小,只负责「这一炉火是活的」;
|
|
858
|
+
② 三舌异速舔动(.fabflame):中/左/右三舌各有**不同周期**(1.05 / 1.37 / 1.19s)与不同
|
|
859
|
+
相位的关键帧 —— 周期互质 ⇒ 三舌永不同步,不再是「一块贴纸在上下抖」;
|
|
860
|
+
每舌**火根钉死**(transform-origin 落在舌根),只有舌尖摆动;
|
|
861
|
+
③ 火根护栏(.fabat-root):灶口一道暗影压住舌根,任何帧都看不见火浮在炉外;
|
|
862
|
+
④ 内芯(.fabcore):黄芯最快最碎(0.83s),是火最活跃处;
|
|
863
|
+
⑤ 火星:焰尖窜出、钻回炉腹;⑥ 炉内灵液沸腾;⑦ 呼吸光晕。
|
|
864
|
+
铁律:全部只动 transform / opacity(回放大日志会占死主线程,见 conversation.js bottleSVG 注释)。 */
|
|
865
|
+
.tabfab__b{
|
|
866
|
+
/* 材质走令牌 `--fab-sheen` / `--fab-base`(2026-10-25 深色配色):
|
|
867
|
+
浅色主题**不给这两项** → 落到兜底(= 原来的平涂亮绿 + 顶部那层柔光),一个像素不变。
|
|
868
|
+
深色主题在 html[data-theme="dark"] 里换成「深玉盘 + 金环」,见那边的注释。
|
|
869
|
+
注意 var() 的兜底不能塞逗号列表(`var(--a, b, c)` 的 `b, c` 会被当成一个值),
|
|
870
|
+
所以面光与底色拆成两个令牌,在这里用逗号拼成背景层。 */
|
|
871
|
+
background:var(--fab-sheen,linear-gradient(178deg,rgba(255,255,255,.16),rgba(255,255,255,0) 60%)),
|
|
872
|
+
var(--fab-base,var(--q-bottle,#22C55E))}
|
|
873
|
+
/* 深色专属:鎏金环 + 玉光。
|
|
874
|
+
⚠️ 单独一条规则、**不用令牌兜底**(2026-10-25):写成
|
|
875
|
+
`box-shadow: …inset…, 0 0 0 var(--fab-ring-w,0) var(--fab-ring,transparent)`
|
|
876
|
+
时,浅色下这一项会算成 `rgba(0,0,0,0) 0px 0px 0px 0px` —— 视觉上等价,
|
|
877
|
+
但计算样式与原来不一致(逐样式比对能看出来)。浅色这一轮要求「一个像素都不变」,
|
|
878
|
+
所以环只在深色出现,浅色的 box-shadow 保持 HEAD 那两条 inset 原样。 */
|
|
879
|
+
html[data-theme="dark"] .tabfab__b{
|
|
880
|
+
box-shadow:
|
|
881
|
+
inset 0 -7px 13px -7px rgba(15,23,42,.50),
|
|
882
|
+
inset 0 3px 3px -2px rgba(255,255,255,.34),
|
|
883
|
+
0 0 0 1.5px var(--fab-ring,rgba(232,199,122,.42)),
|
|
884
|
+
0 0 14px 2px var(--fab-halo,rgba(63,174,121,.12))}
|
|
885
|
+
@media (prefers-color-scheme:dark){
|
|
886
|
+
html:not([data-theme]) .tabfab__b{
|
|
887
|
+
box-shadow:
|
|
888
|
+
inset 0 -7px 13px -7px rgba(15,23,42,.50),
|
|
889
|
+
inset 0 3px 3px -2px rgba(255,255,255,.34),
|
|
890
|
+
0 0 0 1.5px var(--fab-ring,rgba(232,199,122,.42)),
|
|
891
|
+
0 0 14px 2px var(--fab-halo,rgba(63,174,121,.12))}
|
|
892
|
+
}
|
|
893
|
+
/* ① 整组缓摇:幅度小、周期长 */
|
|
894
|
+
@keyframes fabFire{
|
|
895
|
+
0%{transform:translateY(0) scale(1,1)}
|
|
896
|
+
33%{transform:translateY(-.16px) scale(.985,1.02)}
|
|
897
|
+
66%{transform:translateY(.12px) scale(1.012,.985)}
|
|
898
|
+
100%{transform:translateY(0) scale(1,1)}}
|
|
899
|
+
.furnsvg .fabfire{transform-box:fill-box;transform-origin:50% 100%;
|
|
900
|
+
animation:fabFire 2.9s cubic-bezier(.4,0,.6,1) infinite}
|
|
901
|
+
/* ② 三舌异速:中舌最高最快,两侧低而慢 —— 三条曲线互不重合 */
|
|
902
|
+
@keyframes fabTongueMid{
|
|
903
|
+
0%{transform:scale(1,1) skewX(0)}
|
|
904
|
+
22%{transform:scale(.86,1.16) skewX(-4deg)}
|
|
905
|
+
46%{transform:scale(1.1,.9) skewX(3.5deg)}
|
|
906
|
+
71%{transform:scale(.93,1.09) skewX(-2deg)}
|
|
907
|
+
100%{transform:scale(1,1) skewX(0)}}
|
|
908
|
+
@keyframes fabTongueL{
|
|
909
|
+
0%{transform:scale(1,1) skewX(0)}
|
|
910
|
+
31%{transform:scale(1.12,.88) skewX(3deg)}
|
|
911
|
+
58%{transform:scale(.88,1.14) skewX(-3.5deg)}
|
|
912
|
+
82%{transform:scale(1.05,.94) skewX(1.5deg)}
|
|
913
|
+
100%{transform:scale(1,1) skewX(0)}}
|
|
914
|
+
@keyframes fabTongueR{
|
|
915
|
+
0%{transform:scale(1,1) skewX(0)}
|
|
916
|
+
19%{transform:scale(.9,1.12) skewX(-2.5deg)}
|
|
917
|
+
47%{transform:scale(1.09,.92) skewX(3deg)}
|
|
918
|
+
76%{transform:scale(.94,1.07) skewX(-1.8deg)}
|
|
919
|
+
100%{transform:scale(1,1) skewX(0)}}
|
|
920
|
+
.furnsvg .fabflame{transform-box:fill-box;transform-origin:50% 100%}
|
|
921
|
+
.furnsvg .fabflame:nth-of-type(1){animation:fabTongueMid 1.05s cubic-bezier(.45,0,.55,1) infinite}
|
|
922
|
+
.furnsvg .fabflame:nth-of-type(2){animation:fabTongueL 1.37s cubic-bezier(.4,0,.6,1) infinite}
|
|
923
|
+
.furnsvg .fabflame:nth-of-type(3){animation:fabTongueR 1.19s cubic-bezier(.5,0,.5,1) infinite}
|
|
924
|
+
/* ③ 丹鼎本体:**实心剪影**(2026-10-24「效果不好」→ 重画)—— 白描边空心炉在 27px 下偏细、
|
|
925
|
+
换成 currentColor 实填炉体:耳朵(描边弯角)+ 鼓腹 + 深色炉膛(火从膛里冒出来,
|
|
926
|
+
于是「在炉里烧」成立)+ 鎏金口沿 + 三足。实心 + 亮火在深色栏上对比最足。 */
|
|
927
|
+
.furnsvg .fabpot{fill:currentColor;stroke:none}
|
|
928
|
+
.furnsvg .fablear{stroke:currentColor;stroke-width:2.2;fill:none}
|
|
929
|
+
.furnsvg .fabmouth{fill:var(--q-bottle-deep,#06371F);stroke:none}
|
|
930
|
+
.furnsvg .fabrim{stroke:var(--q-gold,#E8C77A);stroke-width:1.1;fill:none}
|
|
931
|
+
.furnsvg .fableg{stroke:currentColor;stroke-width:2.2;fill:none}
|
|
932
|
+
/* ④ 青烟:两缕从敞口升起、慢慢飘散淡出(青烟不是火,别用火的那套配色) */
|
|
933
|
+
@keyframes fabSmoke{
|
|
934
|
+
0%{opacity:0;transform:translate(0,1.2px)}
|
|
935
|
+
30%{opacity:.55}
|
|
936
|
+
100%{opacity:0;transform:translate(.5px,-1.4px)}}
|
|
937
|
+
.furnsvg .fabsmoke{opacity:0;animation:fabSmoke 3.1s ease-out infinite}
|
|
938
|
+
.furnsvg .fabsmoke path:nth-child(2){animation-delay:1.15s;animation-duration:3.6s}
|
|
939
|
+
/* ⑥ 内芯:最快最碎 */
|
|
940
|
+
@keyframes fabCore{
|
|
941
|
+
0%{transform:scale(1,1) skewX(0)}
|
|
942
|
+
19%{transform:scale(.82,1.22) skewX(5deg)}
|
|
943
|
+
44%{transform:scale(1.16,.84) skewX(-5.5deg)}
|
|
944
|
+
70%{transform:scale(.9,1.12) skewX(2.5deg)}
|
|
945
|
+
100%{transform:scale(1,1) skewX(0)}}
|
|
946
|
+
.furnsvg .fabcore{transform-box:fill-box;transform-origin:50% 100%;
|
|
947
|
+
animation:fabCore .83s cubic-bezier(.4,0,.6,1) infinite}
|
|
948
|
+
/* ⑤ 火星:焰尖出生 → 上飘(各带一点横向漂移,--sx 每枚不同)→ 消散 */@keyframes fabSpark{
|
|
949
|
+
0%{transform:translate(0,0) scale(.3);opacity:0}
|
|
950
|
+
15%{opacity:.95}
|
|
951
|
+
70%{opacity:.55}
|
|
952
|
+
100%{transform:translate(var(--sx,.8px),-3.6px) scale(.9);opacity:0}}
|
|
953
|
+
.furnsvg .fabspk circle{animation:fabSpark 1.4s linear infinite}
|
|
954
|
+
.furnsvg .fabspk circle:nth-child(1){--sx:.9px}
|
|
955
|
+
.furnsvg .fabspk circle:nth-child(2){--sx:-.7px;animation-delay:.5s;animation-duration:1.6s}
|
|
956
|
+
.furnsvg .fabspk circle:nth-child(3){--sx:1.3px;animation-delay:.95s;animation-duration:1.3s}
|
|
957
|
+
@keyframes fabBub{
|
|
958
|
+
0%{transform:translateY(0);opacity:0}
|
|
959
|
+
30%{opacity:.9}
|
|
960
|
+
100%{transform:translateY(-3.4px);opacity:0}}
|
|
961
|
+
.furnsvg .fabbub circle{animation:fabBub 1.6s linear infinite}
|
|
962
|
+
.furnsvg .fabbub circle:nth-child(2){animation-delay:.55s;animation-duration:1.9s}
|
|
963
|
+
.furnsvg .fabbub circle:nth-child(3){animation-delay:1.05s;animation-duration:1.75s}
|
|
964
|
+
@keyframes fabGlow{0%,100%{opacity:.28}50%{opacity:.75}}
|
|
965
|
+
.tabfab__b::after{content:"";position:absolute;inset:-7px;border-radius:50%;
|
|
966
|
+
box-shadow:0 0 22px 7px var(--q-bottle-glow,rgba(34,197,94,.30));opacity:.28;
|
|
967
|
+
animation:fabGlow 2.4s ease-in-out infinite;pointer-events:none}
|
|
968
|
+
/* ── 中间 FAB 蜂云剑阵动画(2026-10-24 用户:「剑没动画 可以横竖两个像加号」
|
|
969
|
+
→「两个剑颜色不要一样 也不要这么动画」→「剑不要旋转再细长一些 动画设计其他效果」
|
|
970
|
+
→「两个剑柄不要连一起」→「剑再长一些 动画效果明显一点」
|
|
971
|
+
→「剑体动画不需要 形状现在也不够精致圆滑」)──
|
|
972
|
+
横竖双剑 ✚ 细长(竖深横玉青),**不转不摆**。动画三路(只动 transform / opacity):
|
|
973
|
+
① 剑鸣灵气四射:四枚光点从交叉点沿四个剑向飞出 9.2px(周期 1.5~2.1s 互质、错相)
|
|
974
|
+
② 两口鎏金护手交替明灭(1→.22,1.4s 对半错相)
|
|
975
|
+
③ 中心剑气环:方环由中心炸开淡出(1.5s)—— 一眼能看出「剑在鸣」
|
|
976
|
+
**剑身上不再挂扫掠动画**(用户:「剑体动画不需要」):贴在刃上的色块在 26~34px 上
|
|
977
|
+
既看不清、又会把刃的轮廓弄脏;动画只发生在**剑之外**(飞出的灵气、炸开的环),
|
|
978
|
+
剑本体保持干净。 */
|
|
979
|
+
@keyframes swdQiUp{0%{transform:translate(0,0) scale(.25);opacity:0}15%{opacity:1}100%{transform:translate(0,-9.2px) scale(1.15);opacity:0}}
|
|
980
|
+
@keyframes swdQiDn{0%{transform:translate(0,0) scale(.25);opacity:0}15%{opacity:1}100%{transform:translate(0,9.2px) scale(1.15);opacity:0}}
|
|
981
|
+
@keyframes swdQiLf{0%{transform:translate(0,0) scale(.25);opacity:0}15%{opacity:1}100%{transform:translate(-9.2px,0) scale(1.15);opacity:0}}
|
|
982
|
+
@keyframes swdQiRt{0%{transform:translate(0,0) scale(.25);opacity:0}15%{opacity:1}100%{transform:translate(9.2px,0) scale(1.15);opacity:0}}
|
|
983
|
+
.swdsvg .qi-up{animation:swdQiUp 1.9s ease-out infinite}
|
|
984
|
+
.swdsvg .qi-dn{animation:swdQiDn 1.5s ease-out infinite;animation-delay:.6s}
|
|
985
|
+
.swdsvg .qi-lf{animation:swdQiLf 2.1s ease-out infinite;animation-delay:1s}
|
|
986
|
+
.swdsvg .qi-rt{animation:swdQiRt 1.7s ease-out infinite;animation-delay:.3s}
|
|
987
|
+
@keyframes swdRing{0%{transform:scale(.2);opacity:.85}100%{transform:scale(1.5);opacity:0}}
|
|
988
|
+
.swdsvg .swdring{transform-box:view-box;transform-origin:12px 12px;
|
|
989
|
+
animation:swdRing 1.5s ease-out infinite}
|
|
990
|
+
/* ── 其余三枚也各有一个小动画(2026-10-24 用户:「除了第一个全部换」→ 同轮重画;
|
|
991
|
+
「灵石看不出动画」→ 灵石加强:整石轻晃 + 刻面强闪 + 右上星芒明灭;
|
|
992
|
+
「元婴还没动画」→ 元婴加强:颠幅 -0.7→-1px(再大顶髻顶会冒出 viewBox 被裁)
|
|
993
|
+
2026-10-25 「元婴是凡人修仙传的概念」→ 改盘坐打坐 + 结印托金丹,删摇头)──
|
|
994
|
+
symbol 的 <use> 影子 DOM 选不中内部子路径,所以底栏全部是**内联 SVG**:
|
|
995
|
+
灵石 · 轻晃 + 鎏金刻面强闪 + 星芒(.gemsvg)
|
|
996
|
+
星盘 · 外圈刻度环缓转 + 盘心血石明灭(.starsvg)
|
|
997
|
+
元婴 · 盘坐打坐 + 金丹/顶髻明灭 + 偶尔眨眼(.yysvg)
|
|
998
|
+
顶栏右上角「我的」与底栏同款内联元婴(21px,class 共用 .yysvg,一起动)。 */
|
|
999
|
+
@keyframes gemFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-.6px)}}
|
|
1000
|
+
.gemsvg{animation:gemFloat 2.4s ease-in-out infinite}
|
|
1001
|
+
@keyframes gemShine{0%,100%{opacity:1}45%{opacity:.1}}
|
|
1002
|
+
.gemsvg .gemspk{animation:gemShine 2.2s ease-in-out infinite}
|
|
1003
|
+
@keyframes gemStar{0%,100%{transform:scale(.3);opacity:0}40%{transform:scale(1);opacity:1}70%{transform:scale(.5);opacity:.4}}
|
|
1004
|
+
.gemsvg .gemstar{transform-box:fill-box;transform-origin:50% 50%;
|
|
1005
|
+
animation:gemStar 1.9s ease-in-out infinite}
|
|
1006
|
+
@keyframes starSpin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}
|
|
1007
|
+
.starsvg .staring{transform-box:view-box;transform-origin:12px 12px;
|
|
1008
|
+
animation:starSpin 6s linear infinite}
|
|
1009
|
+
@keyframes starBlink{0%,100%{opacity:1}50%{opacity:.45}}
|
|
1010
|
+
.starsvg .starcore{transform-box:fill-box;transform-origin:50% 50%;
|
|
1011
|
+
animation:starBlink 1.8s ease-in-out infinite}
|
|
1012
|
+
@keyframes yyBob{0%,100%{transform:translateY(0)}50%{transform:translateY(-1px)}}
|
|
1013
|
+
.yysvg .yybaby{animation:yyBob 2.2s ease-in-out infinite}
|
|
1014
|
+
@keyframes yyPill{0%,100%{opacity:.55}50%{opacity:1}}
|
|
1015
|
+
.yysvg .yypill{animation:yyPill 2.2s ease-in-out infinite}
|
|
1016
|
+
@keyframes yyKnot{0%,100%{opacity:1}50%{opacity:.3}}
|
|
1017
|
+
.yysvg .yyknot{animation:yyKnot 2.2s ease-in-out infinite}
|
|
1018
|
+
/* 五官(2026-10-24 用户:「元婴要有五官」):眼帘同色盖眼孔 = 眨眼。
|
|
1019
|
+
周期取 4.7s(与其他动画互质):绝大部分时间睁眼,第 3.9s 附近快速眨两下。 */
|
|
1020
|
+
@keyframes yyBlink{0%,92%{opacity:0}94%{opacity:1}96%{opacity:0}97.5%{opacity:1}100%{opacity:0}}
|
|
1021
|
+
.yysvg .yylid{animation:yyBlink 4.7s linear infinite}
|
|
1022
|
+
@keyframes yyBlush{0%,100%{opacity:.55}50%{opacity:.9}}
|
|
1023
|
+
.yysvg .yyblush{animation:yyBlush 2.2s ease-in-out infinite}
|
|
649
1024
|
/* 会话页(2026-10-22 用户:「不要挤压对话框」「两个效果要一致」「圆可以更大」「再下沉一些 注意保留间距」):
|
|
650
1025
|
底栏浮起后不占位,「+」在**两页都上露 24**(68 直径、圆心在栏顶下方 10),所以 composer 只需让出
|
|
651
1026
|
「上露 24 + 呼吸 12 + 栏高 56」—— 输入框落在凸出圆弧上方 12px,不遮不压。
|
|
@@ -679,8 +1054,6 @@ button.setrow{font-family:inherit}
|
|
|
679
1054
|
background:var(--raised);border-bottom:1px solid var(--border)}
|
|
680
1055
|
.browser__bar{flex:1;min-width:0;display:flex;align-items:center;gap:8px;height:40px;
|
|
681
1056
|
padding:0 8px 0 12px;border-radius:12px;background:var(--subtle)}
|
|
682
|
-
.browser__shield{flex:0 0 auto;display:inline-flex;color:var(--success)}
|
|
683
|
-
.browser__shield.is-plain{color:var(--faint)}
|
|
684
1057
|
.browser__addrwrap{flex:1;min-width:0;display:flex;margin:0}
|
|
685
1058
|
.browser__addr{flex:1;min-width:0;font-size:14px;color:var(--ink);
|
|
686
1059
|
border:none;outline:none;background:transparent;padding:0;
|
|
@@ -704,6 +1077,41 @@ button.setrow{font-family:inherit}
|
|
|
704
1077
|
align-items:center;justify-content:center;border:2px solid var(--ink);
|
|
705
1078
|
border-radius:8px;font-size:13px;font-weight:700;color:var(--ink)}
|
|
706
1079
|
|
|
1080
|
+
/* ── 内置浏览器悬浮形态(2026-10-24 用户:「内置浏览器支持类似的悬浮效果」)──
|
|
1081
|
+
.browser.is-float:全屏浮层缩成**卡片**。占屏 **90vw**,整卡(含顶栏)**16:9**
|
|
1082
|
+
(2026-10-25 用户:「占整个屏幕的90%,扣除菜单栏维持16:9」—— 顶栏那行菜单
|
|
1083
|
+
**不算进比例**:卡高由 16:9 的内容区(90vw*9/16)+ 顶栏自然高度叠成)。
|
|
1084
|
+
再受高度约束 min(...,62vh) —— 保证 16:9 内容区 + 顶栏在矮屏里放得下
|
|
1085
|
+
(放不下时按高度收窄,比例不变形)。
|
|
1086
|
+
iframe 的 src 不动 —— 只是几何变化,页面不重载;
|
|
1087
|
+
悬浮态收掉底部导航,顶栏变成拖拽把手(touch-action:none)。
|
|
1088
|
+
.browser__fonly:只在悬浮态出现的迷你钮(展开回全屏 / 缩成悬浮球)。
|
|
1089
|
+
.brball 悬浮球:缩到最小后的「恢复」入口。z-index 90 —— 高于 tabbar(60)、
|
|
1090
|
+
低于扫码(400)与共享抽屉(1000),不打断别的浮层。位置由 JS 内联写入。 */
|
|
1091
|
+
.browser.is-float{inset:auto;width:min(90vw,calc(62vh * 16 / 9 - 40px));
|
|
1092
|
+
height:auto;border-radius:16px;overflow:hidden;
|
|
1093
|
+
box-shadow:0 18px 48px rgba(15,23,42,.30),0 2px 10px rgba(15,23,42,.18)}
|
|
1094
|
+
.browser.is-float .browser__top{padding:6px 8px;touch-action:none;flex:0 0 auto}
|
|
1095
|
+
.browser.is-float .browser__nav{display:none}
|
|
1096
|
+
.browser.is-float .browser__app{display:none}
|
|
1097
|
+
.browser.is-float .browser__addrwrap{pointer-events:none}
|
|
1098
|
+
.browser.is-float #brStar,.browser.is-float #brRefresh,.browser.is-float #brShare,.browser.is-float #brFloat{display:none}
|
|
1099
|
+
.browser__fonly{display:none}
|
|
1100
|
+
.browser.is-float .browser__fonly{display:inline-flex}
|
|
1101
|
+
/* 内容舞台:全屏时就是 iframe 的普通容器;悬浮时它扛 **16:9 比例**(卡宽的
|
|
1102
|
+
9/16 —— 顶栏菜单行不算进比例),顶栏叠在上面;裁切缩放后的页面。 */
|
|
1103
|
+
.browser__stage{flex:1;min-height:0;position:relative;overflow:hidden;background:#fff}
|
|
1104
|
+
.browser__frame{position:absolute;left:0;top:0;width:100%;height:100%;border:0;background:#fff}
|
|
1105
|
+
.browser.is-float .browser__stage{flex:0 0 auto;aspect-ratio:16/9;width:100%}
|
|
1106
|
+
.browser.is-float .browser__frame{transform-origin:0 0;overflow:hidden;scrollbar-width:none}
|
|
1107
|
+
.browser.is-float .browser__frame::-webkit-scrollbar{display:none}
|
|
1108
|
+
.brball{position:fixed;z-index:90;width:46px;height:46px;border-radius:50%;
|
|
1109
|
+
display:flex;align-items:center;justify-content:center;border:none;
|
|
1110
|
+
background:var(--raised);color:var(--primary-action);
|
|
1111
|
+
box-shadow:0 4px 14px rgba(15,23,42,.22),inset 0 0 0 1px var(--border);
|
|
1112
|
+
touch-action:none;padding:0}
|
|
1113
|
+
.brball:active{transform:scale(.94)}
|
|
1114
|
+
|
|
707
1115
|
/* ── 本壳自己的那几件(对话区内部)──
|
|
708
1116
|
⚠️ 气泡(旧 `.ubub`)、正文(旧 `.md`)、思考块(旧 `.rsn`)、工具卡(旧 `.tool` / `.cmdbar`)
|
|
709
1117
|
的样式**不在这里** —— 那些 DOM 现在由对话内核画,皮肤在共用的一份
|
|
@@ -852,27 +1260,37 @@ button.setrow{font-family:inherit}
|
|
|
852
1260
|
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5.5v13M5.5 12h13"/></symbol>
|
|
853
1261
|
<symbol id="i-scan" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9V6a2 2 0 0 1 2-2h3M15 4h3a2 2 0 0 1 2 2v3M20 15v3a2 2 0 0 1-2 2h-3M9 20H6a2 2 0 0 1-2-2v-3M4 12h16"/></symbol>
|
|
854
1262
|
<symbol id="i-doc" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/></symbol>
|
|
1263
|
+
<!-- 相机:需求页「页面完成情况」截图徽标 + 浏览器工具栏的截图按钮 -->
|
|
1264
|
+
<symbol id="i-shot" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 8h3l1.4-2.2a1 1 0 0 1 .84-.45h5.52a1 1 0 0 1 .84.45L17 8h3a1.6 1.6 0 0 1 1.6 1.6v7.8A1.6 1.6 0 0 1 20 19H4a1.6 1.6 0 0 1-1.6-1.6V9.6A1.6 1.6 0 0 1 4 8z"/><circle cx="12" cy="13" r="3.4"/></symbol>
|
|
855
1265
|
<symbol id="i-list" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></symbol>
|
|
856
1266
|
<symbol id="i-tune" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M4 7h8M18 7h2M4 17h2M12 17h8"/><circle cx="15" cy="7" r="2.2"/><circle cx="9" cy="17" r="2.2"/></symbol>
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1267
|
+
<!-- ── 底栏五枚 = 韩立的物(2026-10-24 用户:「除了第一个全部换」定稿)──
|
|
1268
|
+
题材:《凡人修仙传》主角韩立相关的实物/意象
|
|
1269
|
+
任务 · 丹鼎(腹内丹火 + 青烟 + 火星,第一格,.furnsvg)
|
|
1270
|
+
需求 · 灵石(实心宝石 + 鎏金刻面流光,.gemsvg)
|
|
1271
|
+
+ · 青竹蜂云剑阵(细长双剑交叉 ✚ + 灵气四射,.swdsvg)
|
|
1272
|
+
发现 · 星盘(刻度环缓转 + 盘心血石明灭,.starsvg)
|
|
1273
|
+
我的 · 元婴(盘坐打坐小人 + 鎏金顶髻/金丹,.yysvg)
|
|
1274
|
+
|
|
1275
|
+
**双色是这一版的核心**:前几版全是单色描边,所以怎么调都是"平庸"。
|
|
1276
|
+
25px 上实心块比线条读得清楚,**颜色本身就是最强的辨识信号**:
|
|
1277
|
+
--q-gold(鎏金)= 法器上的錾金/灵石/刻面
|
|
1278
|
+
--q-blood(丹血朱)= 星盘盘心血石
|
|
1279
|
+
主体仍走 currentColor(跟选中态联动),只有点缀用固定色。
|
|
1280
|
+
|
|
1281
|
+
这一版仍是按「25px 实际渲染」逐轮淘汰出来的(每轮截图目检):
|
|
1282
|
+
· 双层剪影(器物叠人物)在 25px 全糊 —— 一枚只放一件事。
|
|
1283
|
+
· 玉简、血玉蛛、韩立像、掌天瓶都试过,最终用户定「除了第一个全部换」。 -->
|
|
1284
|
+
|
|
1285
|
+
<!-- 底栏五枚全部内联(symbol 的 <use> 影子 DOM 选不中内部子路径做不了局部动画):
|
|
1286
|
+
丹炉 .furnsvg / 灵石 .gemsvg / 蜂云剑阵 .swdsvg / 星盘 .starsvg / 元婴 .yysvg。
|
|
1287
|
+
静态 symbol 版(i-req 玉简 / i-compass 星盘 / i-user 半身像)已删,别处没有引用。 -->
|
|
1288
|
+
<!-- 丹炉改为 FAB 内联 SVG(带炼丹动画,见 .tabfab__b 那段 CSS):
|
|
1289
|
+
symbol 没法从宿主页选中内部子路径做动画,所以这里不再留静态 i-furnace,
|
|
1290
|
+
免得两份炉身漂移。别处的「+」仍走 i-plus。 -->
|
|
873
1291
|
<symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8"/><path d="M12 8v4.3l3 1.8"/></symbol>
|
|
874
|
-
<symbol id="i-compass" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.6"/><path d="M15.2 8.8 13.4 13.4 8.8 15.2 10.6 10.6Z"/></symbol>
|
|
875
1292
|
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><rect x="7" y="7" width="10" height="10" rx="2"/><path d="M10 3.5v3M14 3.5v3M10 17.5v3M14 17.5v3M3.5 10h3M3.5 14h3M17.5 10h3M17.5 14h3"/></symbol>
|
|
1293
|
+
<symbol id="i-db" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="6" rx="7" ry="3"/><path d="M5 6v6c0 1.7 3.1 3 7 3s7-1.3 7-3V6"/><path d="M5 12v6c0 1.7 3.1 3 7 3s7-1.3 7-3v-6"/></symbol>
|
|
876
1294
|
<symbol id="i-pin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4.5h10a1 1 0 0 1 1 1V20l-6-3.6L6 20V5.5a1 1 0 0 1 1-1z"/></symbol>
|
|
877
1295
|
<symbol id="i-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7a2 2 0 0 1 2-2h3.2l1.6 2H18a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z"/></symbol>
|
|
878
1296
|
<symbol id="i-chev-r" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 5 7 7-7 7"/></symbol>
|
|
@@ -890,7 +1308,6 @@ button.setrow{font-family:inherit}
|
|
|
890
1308
|
<symbol id="i-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><rect x="4.5" y="4.5" width="6" height="6" rx="1.5"/><rect x="13.5" y="4.5" width="6" height="6" rx="1.5"/><rect x="4.5" y="13.5" width="6" height="6" rx="1.5"/><rect x="13.5" y="13.5" width="6" height="6" rx="1.5"/></symbol>
|
|
891
1309
|
<symbol id="i-plug" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3.5V8M15 3.5V8"/><path d="M7 8h10v3.2a5 5 0 0 1-3.6 4.8L12 16.5l-1.4-.5A5 5 0 0 1 7 11.2z"/><path d="M12 16.5v4"/></symbol>
|
|
892
1310
|
<symbol id="i-spark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"><path d="M12 3.5l2.1 5.4 5.4 2.1-5.4 2.1-2.1 5.4-2.1-5.4-5.4-2.1 5.4-2.1z"/></symbol>
|
|
893
|
-
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3.5l7 2.6v5.2c0 4.6-3 8-7 9.2-4-1.2-7-4.6-7-9.2V6.1z"/><path d="M9 11.8l2.1 2.1 4-4"/></symbol>
|
|
894
1311
|
<symbol id="i-star" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"><path d="M12 4.5l2.4 4.9 5.4.8-3.9 3.8.9 5.4-4.8-2.5-4.8 2.5.9-5.4-3.9-3.8 5.4-.8z"/></symbol>
|
|
895
1312
|
<symbol id="i-refresh" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M19.5 12a7.5 7.5 0 1 1-2.2-5.3"/><path d="M19.5 3.5v3.7h-3.7"/></symbol>
|
|
896
1313
|
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="6.5" cy="12" r="2.4"/><circle cx="17" cy="6" r="2.4"/><circle cx="17" cy="18" r="2.4"/><path d="M8.7 10.9l6.1-3.6M8.7 13.1l6.1 3.6"/></symbol>
|
|
@@ -942,7 +1359,48 @@ button.setrow{font-family:inherit}
|
|
|
942
1359
|
</button>
|
|
943
1360
|
<span class="tasks__tools">
|
|
944
1361
|
<button class="topbar__icon" type="button" id="btnTaskSearch" aria-label="搜索任务"><svg width="21" height="21"><use href="#i-search"/></svg></button>
|
|
945
|
-
<button class="topbar__icon" type="button" id="btnTaskMe" aria-label="我的"><svg width="
|
|
1362
|
+
<button class="topbar__icon" type="button" id="btnTaskMe" aria-label="我的"><svg class="yysvg" width="23" height="23" viewBox="1.6 0.7 20.8 20.8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
1363
|
+
<!-- 与底栏「我的」同款元婴(2026-10-24 用户:「我的图标右上角要跟着改」;
|
|
1364
|
+
「元婴要有五官」→ 头部开孔镂空五官):
|
|
1365
|
+
class 共用 .yysvg,轻颠/金丹明灭/眨眼动画一起生效。**标记与底栏那份保持一致**。 -->
|
|
1366
|
+
<g class="yybaby">
|
|
1367
|
+
<!-- 元婴打坐 · 定稿(2026-10-25「完全不行」→ 四案比选,F 案胜出):
|
|
1368
|
+
一体剪影(肩窄→袖外张→盘腿宽平底),盘腿上缘走浅玉弧线,
|
|
1369
|
+
两端金手搭膝,丹田金丹+灵光环;莲花座案因花瓣糊成金坨被否。 -->
|
|
1370
|
+
<g class="yyhead">
|
|
1371
|
+
<path d="M12 3.9a4.0 4.0 0 1 0 0 8.0 4.0 4.0 0 1 0 0-8.0z
|
|
1372
|
+
M10.5 6.05a.6 .6 0 1 0 0 1.2 .6 .6 0 1 0 0-1.2z
|
|
1373
|
+
M13.5 6.05a.6 .6 0 1 0 0 1.2 .6 .6 0 1 0 0-1.2z
|
|
1374
|
+
M10.75 9.35Q12 11.6 13.25 9.35Q12 10.25 10.75 9.35Z"
|
|
1375
|
+
fill="currentColor" fill-rule="evenodd" stroke="none"/>
|
|
1376
|
+
<circle cx="7.8" cy="7.7" r=".48" fill="currentColor" stroke="none"/>
|
|
1377
|
+
<circle cx="16.2" cy="7.7" r=".48" fill="currentColor" stroke="none"/>
|
|
1378
|
+
<ellipse class="yyblush" cx="8.95" cy="8.9" rx=".7" ry=".42" fill="var(--q-blood,#E23B2E)" stroke="none" opacity=".42"/>
|
|
1379
|
+
<ellipse class="yyblush" cx="15.05" cy="8.9" rx=".7" ry=".42" fill="var(--q-blood,#E23B2E)" stroke="none" opacity=".42"/>
|
|
1380
|
+
<circle cx="12" cy="5.2" r=".27" fill="var(--q-blood,#E23B2E)" stroke="none" opacity=".9"/>
|
|
1381
|
+
<rect class="yylid" x="9.65" y="6.15" width="4.7" height="1.8" rx=".65" fill="currentColor" stroke="none" opacity="0"/>
|
|
1382
|
+
<circle class="yyknot" cx="12" cy="2.85" r=".9" fill="var(--q-gold,#E8C77A)" stroke="none"/>
|
|
1383
|
+
</g>
|
|
1384
|
+
<path d="M12 11.9c-1.95 0-3.25 1.15-3.85 2.9-.42 1.25-.95 2.3-1.9 3.0v2.05h11.5v-2.05c-.95-.7-1.48-1.75-1.9-3.0-.6-1.75-1.9-2.9-3.85-2.9z" fill="currentColor" stroke="none"/>
|
|
1385
|
+
<circle cx="7.0" cy="18.45" r="2.15" fill="currentColor" stroke="none"/>
|
|
1386
|
+
<circle cx="17.0" cy="18.45" r="2.15" fill="currentColor" stroke="none"/>
|
|
1387
|
+
<rect x="7.0" y="17.6" width="10.0" height="2.9" fill="currentColor" stroke="none"/>
|
|
1388
|
+
<!-- 禅定印 · canvas 细线(2026-10-25 配色定稿):
|
|
1389
|
+
两条掌缘弧 + 拇指三角,用 --canvas 描线而不是填金/填孔 ——
|
|
1390
|
+
填金在选中绿上只有 1.29,填孔在 28px 下糊成一团黑;
|
|
1391
|
+
细线两头都不犯:暗底当亮线、亮底当暗线,且不占体积。
|
|
1392
|
+
金丹垫 canvas 暗衬后才立得住(朱 vs 绿 2.0 → 朱 vs 暗衬 4.0)。 -->
|
|
1393
|
+
<g fill="none" stroke="var(--canvas,#0B1512)" stroke-width=".62" stroke-linecap="round">
|
|
1394
|
+
<path d="M9.15 16.45q.55 2.4 2.85 2.4"/>
|
|
1395
|
+
<path d="M14.85 16.45q-.55 2.4-2.85 2.4"/>
|
|
1396
|
+
<path d="M10.45 16.15 12 14.5 13.55 16.15"/>
|
|
1397
|
+
</g>
|
|
1398
|
+
<!-- 金丹:卧在印心,外面垫一圈 canvas 暗衬 —— 朱在绿身上只有 2.0,
|
|
1399
|
+
垫了暗衬才有 4.0(深)/ 4.5(浅),28px 才立得住;明灭 = 吐纳 -->
|
|
1400
|
+
<circle cx="12" cy="16.15" r=".78" fill="var(--canvas,#0B1512)" stroke="none"/>
|
|
1401
|
+
<circle class="yypill" cx="12" cy="16.15" r=".5" fill="var(--q-blood,#E23B2E)" stroke="none"/>
|
|
1402
|
+
</g>
|
|
1403
|
+
</svg></button>
|
|
946
1404
|
</span>
|
|
947
1405
|
</div>
|
|
948
1406
|
<div class="tasks__search" id="taskSearchBar" hidden>
|
|
@@ -993,13 +1451,11 @@ button.setrow{font-family:inherit}
|
|
|
993
1451
|
所以这里**没有**全局开关 —— 与团队模式同一处入口:会话页右上角 → 任务运行设置。 -->
|
|
994
1452
|
<!-- 默认引擎 / 模型:新建任务的初值(会话里输入框上方的级联切换会回写这里)。
|
|
995
1453
|
两行点开的是同一个「引擎 → 模型」二列选择器,模型必须挂在引擎下。 -->
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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>
|
|
1454
|
+
<!-- 默认引擎(主 + 备一屏配完,2026-10-24 用户:「这样布局太浪费了,要在一次点击里面设置两个」)。
|
|
1455
|
+
一行显示「主 · 模型 | 备 …」;点开抽屉顶部切 主/备,选完**不关**,接着配另一档。 -->
|
|
1456
|
+
<button class="setrow" id="btnDefaultEngine"><span class="setrow__l">默认引擎</span><span class="setrow__v setrow__v--pair" id="defaultEngine">自动(首个可用)</span></button>
|
|
1457
|
+
<!-- 建议引擎同口径:主备一个抽屉里配,默认不开启(引擎空 = 不请求 /desk/suggest)。 -->
|
|
1458
|
+
<button class="setrow" id="btnSuggestEngine"><span class="setrow__l">建议引擎</span><span class="setrow__v setrow__v--pair" id="suggestEngine">不开启</span></button>
|
|
1003
1459
|
<!-- 2026-10-22 用户:「触感反馈 / 语言」这两项不需要 —— 已移除(设备级偏好只剩主题/字体)。 -->
|
|
1004
1460
|
<div class="setrow"><span class="setrow__l">网络代理</span><button class="tgl" id="tglVpn" aria-label="网络代理"></button></div>
|
|
1005
1461
|
<button class="setrow" id="btnNotify"><span class="setrow__l">消息通知设置</span><span class="setrow__v" id="notifyState">—</span></button>
|
|
@@ -1020,6 +1476,12 @@ button.setrow{font-family:inherit}
|
|
|
1020
1476
|
<span class="setrow__l">版本</span><span class="setrow__v" id="mineVersion">—</span>
|
|
1021
1477
|
</button>
|
|
1022
1478
|
<button class="setrow" id="btnUpgrade"><span class="setrow__l">重新加载界面</span><span class="setrow__v" id="upgradeState">重新加载</span></button>
|
|
1479
|
+
<!-- 重启桌面端服务(2026-10-24 用户:「在移动端我的中增加1个重启按钮 点击后触发magic-client重启」)。
|
|
1480
|
+
走 POST /desk/restart:桌面端先答再由一个脱离终端的助手停旧起新,端口 / 令牌沿用。
|
|
1481
|
+
⚠️ 会中断桌面端**正在跑的所有会话** —— 点击前必须二次确认。 -->
|
|
1482
|
+
<button class="setrow" id="btnRestartClient" type="button">
|
|
1483
|
+
<span class="setrow__l">重启服务</span><span class="setrow__v" id="restartClientState">重启</span>
|
|
1484
|
+
</button>
|
|
1023
1485
|
</div>
|
|
1024
1486
|
<div class="setcard">
|
|
1025
1487
|
<button class="setrow setrow--danger" id="btnLogout">断开连接</button>
|
|
@@ -1048,7 +1510,8 @@ button.setrow{font-family:inherit}
|
|
|
1048
1510
|
|
|
1049
1511
|
<!-- ══════════ 会话页 → 任务运行设置(独立子页面)══════════
|
|
1050
1512
|
会话页右上角的调节键点进来(2026-10-21 用户:「运行设置在任务对话框里 不也应该是独立页面吗」)。
|
|
1051
|
-
|
|
1513
|
+
六项都是**当前这条会话**的运行参数:设备只读,其余点开还是原来的选择器(选择器只有一份)。
|
|
1514
|
+
思考模式(2026-10-24)默认最高,按会话记档(magic.reasoning.sessions.v1)。
|
|
1052
1515
|
会话托管(2026-10-23)也在这里:它跟团队模式一样**属于这条会话**,不再是「我的」里的全局开关。 -->
|
|
1053
1516
|
<div class="history" id="runSettingsPage" hidden>
|
|
1054
1517
|
<div class="history__top">
|
|
@@ -1060,10 +1523,11 @@ button.setrow{font-family:inherit}
|
|
|
1060
1523
|
<div class="setrow"><span class="setrow__l">设备</span><span class="setrow__v" id="rsDevice">本机</span></div>
|
|
1061
1524
|
<button class="setrow" id="btnRsWorkspace"><span class="setrow__l">工作空间</span><span class="setrow__v" id="rsWorkspace">默认(不属于任何空间)</span></button>
|
|
1062
1525
|
<button class="setrow" id="btnRsPermission"><span class="setrow__l">权限模式</span><span class="setrow__v" id="rsPermission">沿用默认</span></button>
|
|
1526
|
+
<button class="setrow" id="btnRsReasoning"><span class="setrow__l">思考模式</span><span class="setrow__v" id="rsReasoning">最高(默认)</span></button>
|
|
1063
1527
|
<button class="setrow" id="btnRsTeam"><span class="setrow__l">团队模式</span><span class="setrow__v" id="rsTeam">关闭</span></button>
|
|
1064
1528
|
<button class="setrow" id="btnRsHosting"><span class="setrow__l">会话托管</span><span class="setrow__v" id="rsHosting">关闭</span></button>
|
|
1065
1529
|
</div>
|
|
1066
|
-
<div class="history__hint" style="text-align:left">会话托管:每轮成功结束自动发送最合适的建议(最多连续 12
|
|
1530
|
+
<div class="history__hint" style="text-align:left">会话托管:每轮成功结束自动发送最合适的建议(最多连续 12 轮)。只作用于当前会话;团队子会话默认继承主会话的开关。思考模式默认最高,只对支持推理档位的引擎(claude / codebuddy / codex 等)生效。</div>
|
|
1067
1531
|
</div>
|
|
1068
1532
|
</div>
|
|
1069
1533
|
|
|
@@ -1100,9 +1564,28 @@ button.setrow{font-family:inherit}
|
|
|
1100
1564
|
<span class="srow__col"><span class="srow__t">SKILL</span><span class="srow__s">技能包,扩展 AI 能力</span></span>
|
|
1101
1565
|
<span class="srow__time" style="color:var(--faint)"><svg width="16" height="16"><use href="#i-chev-r"/></svg></span>
|
|
1102
1566
|
</button>
|
|
1567
|
+
<button class="srow" id="dscScripts" type="button">
|
|
1568
|
+
<span class="srow__ico"><svg width="22" height="22"><use href="#i-cpu"/></svg></span>
|
|
1569
|
+
<span class="srow__col"><span class="srow__t">脚本</span><span class="srow__s">登记脚本,可启动 / 停止、看日志</span></span>
|
|
1570
|
+
<span class="srow__time" style="color:var(--faint)"><svg width="16" height="16"><use href="#i-chev-r"/></svg></span>
|
|
1571
|
+
</button>
|
|
1572
|
+
<button class="srow" id="dscKnowledge" type="button">
|
|
1573
|
+
<span class="srow__ico"><svg width="22" height="22"><use href="#i-db"/></svg></span>
|
|
1574
|
+
<span class="srow__col"><span class="srow__t">知识库</span><span class="srow__s">OpenViking 上下文数据库</span></span>
|
|
1575
|
+
<span class="srow__time" style="color:var(--faint)"><svg width="16" height="16"><use href="#i-chev-r"/></svg></span>
|
|
1576
|
+
</button>
|
|
1103
1577
|
</div>
|
|
1104
1578
|
</div>
|
|
1105
1579
|
|
|
1580
|
+
<!-- ══════════ 发现 → 脚本(用户登记的脚本清单)══════════
|
|
1581
|
+
**独立子页面**:内容、样式、逻辑都在 scripts.js / scripts.css,本文件只留两个空容器。
|
|
1582
|
+
脚本在**这台桌面**上跑(不是手机上),服务端负责 spawn / 杀进程组 / 收日志。
|
|
1583
|
+
数据形状与端点见 CLIENT-CONTRACT §2(/desk/scripts)。 -->
|
|
1584
|
+
<div class="history" id="scripts" hidden></div>
|
|
1585
|
+
|
|
1586
|
+
<!-- 脚本的二级视图:详情 + 启停按钮 + 实时日志(内容由 scripts.js 渲染) -->
|
|
1587
|
+
<div class="history" id="scriptDetail" hidden></div>
|
|
1588
|
+
|
|
1106
1589
|
<!-- ══════════ 发现 → 插件(工程 CLI 目录)══════════
|
|
1107
1590
|
列表与详情都是**服务端宣布什么就显示什么**(GET /desk/plugins)。 -->
|
|
1108
1591
|
<div class="history" id="plugins" hidden>
|
|
@@ -1121,6 +1604,29 @@ button.setrow{font-family:inherit}
|
|
|
1121
1604
|
<div class="history__list" id="pluginDetailBody"></div>
|
|
1122
1605
|
</div>
|
|
1123
1606
|
|
|
1607
|
+
<!-- ══════════ 发现 → 知识库(OpenViking 上下文数据库)══════════
|
|
1608
|
+
列表与详情都是**服务端宣布什么就显示什么**(GET /desk/knowledge):
|
|
1609
|
+
装没装、本机版本多少、PyPI 最新版本多少,都由桌面端探测后宣布。
|
|
1610
|
+
本页不自己判断状态 —— 那样两端会各说一套(「手机上显示已安装、装的时候说没装」最难查)。
|
|
1611
|
+
|
|
1612
|
+
安装/升级走 `POST /desk/knowledge/{id}/{action}` 的 SSE:命令是**服务端写死的**,
|
|
1613
|
+
手机只传 id 与动作;输出逐行回来,所以看得见在动,失败也能把 stderr 摊出来。 -->
|
|
1614
|
+
<div class="history" id="knowledge" hidden>
|
|
1615
|
+
<div class="history__top">
|
|
1616
|
+
<button class="topbar__icon" id="btnKnowledgeBack" aria-label="返回"><svg width="20" height="20"><use href="#i-back"/></svg></button>
|
|
1617
|
+
<span class="history__title">知识库</span>
|
|
1618
|
+
</div>
|
|
1619
|
+
<div class="history__list" id="knowledgeList"></div>
|
|
1620
|
+
</div>
|
|
1621
|
+
|
|
1622
|
+
<div class="history" id="knowledgeDetail" hidden>
|
|
1623
|
+
<div class="history__top">
|
|
1624
|
+
<button class="topbar__icon" id="btnKnowledgeDetailBack" aria-label="返回"><svg width="20" height="20"><use href="#i-back"/></svg></button>
|
|
1625
|
+
<span class="history__title" id="knowledgeDetailTitle">知识库</span>
|
|
1626
|
+
</div>
|
|
1627
|
+
<div class="history__list" id="knowledgeDetailBody"></div>
|
|
1628
|
+
</div>
|
|
1629
|
+
|
|
1124
1630
|
<!-- ══════════ 会话页 ══════════ -->
|
|
1125
1631
|
<div class="session" id="session" hidden>
|
|
1126
1632
|
<header class="topbar">
|
|
@@ -1147,17 +1653,25 @@ button.setrow{font-family:inherit}
|
|
|
1147
1653
|
<div class="browser" id="browser" hidden>
|
|
1148
1654
|
<header class="browser__top">
|
|
1149
1655
|
<div class="browser__bar">
|
|
1150
|
-
<
|
|
1656
|
+
<button class="browser__ib" id="brRelay" type="button" aria-label="桌面端中转" title="桌面端中转"><svg width="17" height="17"><use href="#i-laptop"/></svg></button>
|
|
1151
1657
|
<form class="browser__addrwrap" id="brForm" novalidate>
|
|
1152
1658
|
<input class="browser__addr" id="brAddr" type="url" inputmode="url" enterkeyhint="go" autocomplete="off" spellcheck="false" placeholder="输入网址">
|
|
1153
1659
|
</form>
|
|
1660
|
+
<!-- 截图:只有需求页的「页面完成情况」流程走中转时才出现(reqshot.js 打开它)。
|
|
1661
|
+
默认 hidden —— 平时内置浏览器就是浏览器,多一枚按钮只会让人以为是别的功能。 -->
|
|
1662
|
+
<button class="browser__ib" id="brShot" type="button" aria-label="截图" title="截图(需求页的页面完成情况)" hidden><svg width="18" height="18"><use href="#i-shot"/></svg></button>
|
|
1154
1663
|
<button class="browser__ib" id="brStar" type="button" aria-label="收藏"><svg width="18" height="18"><use href="#i-star"/></svg></button>
|
|
1155
1664
|
<button class="browser__ib" id="brRefresh" type="button" aria-label="刷新"><svg width="18" height="18"><use href="#i-refresh"/></svg></button>
|
|
1156
1665
|
<button class="browser__ib" id="brShare" type="button" aria-label="分享"><svg width="18" height="18"><use href="#i-share"/></svg></button>
|
|
1666
|
+
<button class="browser__ib" id="brFloat" type="button" aria-label="悬浮显示"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><rect x="12" y="12" width="7" height="5" rx="1" fill="currentColor" stroke="none"/></svg></button>
|
|
1667
|
+
<button class="browser__ib browser__fonly" id="brExpand" type="button" aria-label="展开回全屏"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3H3v6M15 3h6v6M9 21H3v-6M15 21h6v-6"/></svg></button>
|
|
1668
|
+
<button class="browser__ib browser__fonly" id="brMin" type="button" aria-label="缩成悬浮球"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 12h14"/></svg></button>
|
|
1157
1669
|
</div>
|
|
1158
1670
|
<button class="browser__app" id="brClose" type="button" aria-label="关闭浏览器">✕</button>
|
|
1159
1671
|
</header>
|
|
1160
|
-
<
|
|
1672
|
+
<div class="browser__stage" id="brStage">
|
|
1673
|
+
<iframe class="browser__frame" id="brFrame" src="about:blank" referrerpolicy="no-referrer" allow="fullscreen"></iframe>
|
|
1674
|
+
</div>
|
|
1161
1675
|
<nav class="browser__nav">
|
|
1162
1676
|
<button class="browser__nb" id="brBack" type="button" aria-label="返回"><svg width="22" height="22"><use href="#i-back"/></svg></button>
|
|
1163
1677
|
<button class="browser__nb" id="brSites" type="button" aria-label="快捷站点"><svg width="22" height="22"><use href="#i-grid"/></svg></button>
|
|
@@ -1167,6 +1681,9 @@ button.setrow{font-family:inherit}
|
|
|
1167
1681
|
</nav>
|
|
1168
1682
|
</div>
|
|
1169
1683
|
|
|
1684
|
+
<!-- 浏览器悬浮球:浏览器缩到最小后的恢复入口(点按恢复悬浮卡片,拖动换位置) -->
|
|
1685
|
+
<button class="brball" id="brBall" hidden aria-label="恢复浏览器"><svg width="22" height="22"><use href="#i-globe"/></svg></button>
|
|
1686
|
+
|
|
1170
1687
|
<!-- ══════════ 产物阅读浮层(文本 / Markdown 产物卡「查看」)══════════ -->
|
|
1171
1688
|
<div class="reader" id="reader" hidden>
|
|
1172
1689
|
<header class="reader__top">
|
|
@@ -1189,24 +1706,192 @@ button.setrow{font-family:inherit}
|
|
|
1189
1706
|
</div>
|
|
1190
1707
|
|
|
1191
1708
|
<!-- ══════════ 底部 Tab ══════════
|
|
1192
|
-
五个槽位:任务 · 需求
|
|
1709
|
+
五个槽位:任务(丹炉) · 需求(灵石) ·「蜂云剑阵✚」· 发现(星盘) · 我的(元婴)。
|
|
1710
|
+
(2026-10-24 用户:「都不行」→ 整套重设计并附专业审查;设计规范见 sprite 注释。
|
|
1711
|
+
「我的图标再换」→ 斗笠→酒葫芦→元婴(盘坐打坐小人)。)
|
|
1712
|
+
**每枚都有动画**(2026-10-24 用户:「每个图标都有动画」):
|
|
1713
|
+
丹炉 · 三舌异速丹火 + 青烟 + 火星(.furnsvg,第一格)
|
|
1714
|
+
灵石 · 整石轻晃 + 刻面强闪 + 星芒(.gemsvg)
|
|
1715
|
+
蜂云剑阵 · 细长双剑 ✚ 不转不摆 + 剑鸣灵气四射(.swdsvg,中间 FAB)
|
|
1716
|
+
星盘 · 刻度环缓转 + 盘心血石明灭(.starsvg)
|
|
1717
|
+
我的 · 元婴 盘坐打坐 + 金丹/顶髻明灭 + 偶尔眨眼(.yysvg)
|
|
1718
|
+
五枚全是**内联 SVG**:symbol 的 <use> 影子 DOM 选不中内部子路径,做不了
|
|
1719
|
+
「刻面单独流光」这种局部动画;顶栏右上角「我的」也是同款内联元婴(21px 一起动)。
|
|
1720
|
+
题材是《凡人修仙传》主角韩立的物:丹炉 / 灵石 / 蜂云剑阵(横竖双剑) / 星盘 / 元婴。
|
|
1721
|
+
**双色**(主体 currentColor + 鎏金/丹血点缀)是这一版的关键 ——
|
|
1722
|
+
25px 上颜色本身是最强辨识信号,单色描边怎么调都读不出「法器」。
|
|
1723
|
+
25px 下靠剪影+颜色分家;选中色玉青(--tab-on)。
|
|
1193
1724
|
「定时任务」那个 tab 已删(2026-10-22 用户:「定时任务tab先删除」)——
|
|
1194
1725
|
功能本身没删:会话菜单里的「定时任务」仍然能唤起,只是不再占一个底部入口
|
|
1195
1726
|
(那一页本来也只有一句「还没有这个接口」的说明,占着导航反而误导)。
|
|
1196
|
-
|
|
1727
|
+
中间那枚不是 tab:它没有 data-tab、不参与 setActive,点了就是新建会话。
|
|
1197
1728
|
**只有图标、没有文字**(2026-10-22 用户:「底栏只保留图标不需要文字了」):
|
|
1198
|
-
语义靠 aria-label
|
|
1729
|
+
语义靠 aria-label 承担。 -->
|
|
1199
1730
|
<nav class="tabbar" id="tabbar" hidden>
|
|
1200
|
-
<
|
|
1731
|
+
<i class="eave eave-l" aria-hidden="true"></i><i class="eave eave-r" aria-hidden="true"></i>
|
|
1732
|
+
<button class="tab" type="button" data-tab="tasks" aria-label="任务"><span class="icw"><svg class="furnsvg" width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
1733
|
+
<defs>
|
|
1734
|
+
<linearGradient id="i-furn-fire" x1="0" y1="0" x2="0" y2="1">
|
|
1735
|
+
<stop offset="0" stop-color="#FFE08A"/><stop offset=".35" stop-color="#FF9A3C"/><stop offset="1" stop-color="#E23B2E"/>
|
|
1736
|
+
</linearGradient></defs>
|
|
1737
|
+
<!-- 青烟:两缕从敞口升起、飘散淡出(不是火,只动 opacity + 轻微位移) -->
|
|
1738
|
+
<g class="fabsmoke" fill="none" stroke="var(--muted,#CBD5E1)" stroke-width=".85" stroke-linecap="round">
|
|
1739
|
+
<path d="M11.3 3.4c-.8-.9-.3-1.7.4-2.2"/>
|
|
1740
|
+
<path d="M13.5 2.6c.8-.8.4-1.7-.3-2.2"/>
|
|
1741
|
+
</g>
|
|
1742
|
+
<!-- 火星:从火尖窜出、飞散 -->
|
|
1743
|
+
<g class="fabspk" fill="#FFC93C" stroke="none">
|
|
1744
|
+
<circle cx="9.9" cy="5" r=".34"/>
|
|
1745
|
+
<circle cx="14.1" cy="4.8" r=".3"/>
|
|
1746
|
+
<circle cx="12" cy="3.1" r=".26"/>
|
|
1747
|
+
</g>
|
|
1748
|
+
<!-- ① 丹炉实心剪影:双立耳(描边弯角)压在炉体后 -->
|
|
1749
|
+
<path class="fablear" d="M6.2 7.8C4.9 6.4 4.6 3.5 6.2 2.2"/>
|
|
1750
|
+
<path class="fablear" d="M17.8 7.8C19.1 6.4 19.4 3.5 17.8 2.2"/>
|
|
1751
|
+
<!-- ② 鼓腹:实心,从口沿圆鼓到三足 -->
|
|
1752
|
+
<path class="fabpot" d="M5.9 7.6c-.8 3.6-.2 6.9 1.8 9.4 1.1 1.6 8.5 1.6 9.6 0 2-2.5 2.6-5.8 1.8-9.4z"/>
|
|
1753
|
+
<!-- 炉腹鎏金腰带:一圈錾金线,25px 下多一道细节就不像"灰疙瘩" -->
|
|
1754
|
+
<path class="fabbelt" d="M6.3 12.6c1.2 1.3 3.6 2 5.7 2s4.5-.7 5.7-2" stroke="var(--q-gold,#E8C77A)" stroke-width=".9" fill="none"/>
|
|
1755
|
+
<!-- ③ 炉膛:深色开口 + 鎏金口沿(椭圆=敞口),火只从这口里冒出来 -->
|
|
1756
|
+
<ellipse class="fabmouth" cx="12" cy="7.6" rx="5.7" ry="1.6"/>
|
|
1757
|
+
<ellipse class="fabrim" cx="12" cy="7.6" rx="5.7" ry="1.6"/>
|
|
1758
|
+
<!-- ④ 炉内丹火:三舌异速舔动,根扎在膛里、窜出口沿 -->
|
|
1759
|
+
<g class="fabfire">
|
|
1760
|
+
<path class="fabflame" d="M12 3.4c1.6 2 2.5 3.2 2.5 4.3 0 1.7-1.1 2.7-2.5 2.7s-2.5-1-2.5-2.7c0-1.1.9-2.3 2.5-4.3z" fill="url(#i-furn-fire)" stroke="none"/>
|
|
1761
|
+
<path class="fabflame" d="M8.9 4.8c1 1.3 1.5 2.1 1.5 3 0 1-.7 1.8-1.5 1.8s-1.5-.8-1.5-1.8c0-.9.5-1.7 1.5-3z" fill="url(#i-furn-fire)" stroke="none"/>
|
|
1762
|
+
<path class="fabflame" d="M15.1 4.8c1 1.3 1.5 2.1 1.5 3 0 1-.7 1.8-1.5 1.8s-1.5-.8-1.5-1.8c0-.9.5-1.7 1.5-3z" fill="url(#i-furn-fire)" stroke="none"/>
|
|
1763
|
+
<path class="fabcore" d="M12 6c.8 1 1.2 1.6 1.2 2.2 0 .8-.5 1.3-1.2 1.3s-1.2-.5-1.2-1.3c0-.6.4-1.2 1.2-2.2z" fill="#FFE08A" stroke="none"/>
|
|
1764
|
+
</g>
|
|
1765
|
+
<!-- ⑤ 三足 -->
|
|
1766
|
+
<path class="fableg" d="M8.2 16.6 7.3 20.4M11.4 17.4v3M14.6 16.6l.9 3.8"/>
|
|
1767
|
+
</svg></span></button>
|
|
1201
1768
|
<!-- 需求 tab 常驻:不做能力位控制(用户 2026-10-22:「移动端永远显示」)。
|
|
1202
1769
|
老 core 上点进来会由 reqs.js 的 501 分支给出明确文案,
|
|
1203
1770
|
不会是一片空白 —— 入口固定比入口忽隐忽现更好理解。 -->
|
|
1204
|
-
<button class="tab" type="button" data-tab="reqs" aria-label="需求"><span class="icw"><svg width="
|
|
1771
|
+
<button class="tab" type="button" data-tab="reqs" aria-label="需求"><span class="icw"><svg class="gemsvg" width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
1772
|
+
<!-- 灵石(2026-10-24 用户:「除了第一个全部换」→ 玉简换成灵石):
|
|
1773
|
+
实心宝石剪影 + 刻面;动画:整石轻晃 + 刻面强闪 + 右上星芒明灭。
|
|
1774
|
+
**刻面线不能用鎏金**(2026-10-25 实测):刻面线画在宝石实心剪影**之上**,
|
|
1775
|
+
所以要跟剪影比,而不是跟底栏比 —— 金(1.20 深 / 2.32 浅) 比主体还暗,线直接消失,
|
|
1776
|
+
整颗宝石读起来就是一块纯色块。改与主体反相的 `--canvas`:深色下 8.5、浅色下 8.3,
|
|
1777
|
+
两套主题都能读出「金刚石切面」。星芒在宝石之外,仍用鎏金。 -->
|
|
1778
|
+
<path d="M7.6 4.6h8.8L21.6 11.4 12 20.6 2.4 11.4z" fill="currentColor" stroke="none"/>
|
|
1779
|
+
<path class="gemspk" d="M7.6 4.6 12 9.2l4.4-4.6M2.4 11.4h19.2M12 9.2v11.4M12 20.6 9.2 15.2M12 20.6l2.8-5.4" stroke="var(--gem-facet,var(--canvas))" stroke-width="1.2" fill="none"/>
|
|
1780
|
+
<path class="gemspk" d="M9.4 6.2 12 8.4l2.6-2.2" stroke="var(--gem-facet,var(--canvas))" stroke-width="1.2" fill="none"/>
|
|
1781
|
+
<path class="gemstar" d="M20.6 1.4l1.1 2 2 1.1-2 1.1-1.1 2-1.1-2-2-1.1 2-1.1z" fill="var(--q-gold,#E8C77A)" stroke="none"/>
|
|
1782
|
+
</svg></span></button>
|
|
1205
1783
|
<span class="tabfab">
|
|
1206
|
-
<button class="tabfab__b" type="button" id="btnFabNew" aria-label="新建会话"
|
|
1784
|
+
<button class="tabfab__b" type="button" id="btnFabNew" aria-label="新建会话">
|
|
1785
|
+
<svg class="swdsvg" width="52" height="52" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
1786
|
+
<!-- 青竹蜂云剑阵 · 横竖双剑(2026-10-24 用户:「剑没动画 可以横竖两个像加号」
|
|
1787
|
+
→「两个剑颜色不要一样 也不要这么动画」→「剑不要旋转再细长一些 动画设计其他效果」
|
|
1788
|
+
→「两个剑柄不要连一起」→「剑再长一些 动画效果明显一点」
|
|
1789
|
+
→「剑体动画不需要 形状现在也不够精致圆滑」):
|
|
1790
|
+
一竖一横两口**细长**飞剑交叉成「✚」,两口不同色:竖剑深色(currentColor,
|
|
1791
|
+
跟选中态联动)、横剑淡金(--q-gold-lt,金雷竹原著「淡金色」)。
|
|
1792
|
+
**尺寸:SVG 元素 52px,图形可见边占 68px 圆的 ~70%(用户:「太大了 70即可」)**
|
|
1793
|
+
—— 68 * 0.7 = 47.6px 可见宽,52 * (23.45/24) = 50.8px,
|
|
1794
|
+
留 ~3px 安全余量给外发光。
|
|
1795
|
+
图形在 viewBox 内**居中**(四边留白实测 L25 R18 T26 B24 @58px / 3x 像素),
|
|
1796
|
+
不然放大后会明显偏。
|
|
1797
|
+
**形状是「竹叶形」细长飞剑**(2026-10-25 用户:「韩立的青竹蜂云剑不是这样的」
|
|
1798
|
+
→ 原著设定:金雷竹炼制、剑身细长轻盈**形似竹叶**、青碧色隐现金雷):
|
|
1799
|
+
最宽处偏根部 ~1/3,向尖端与根端**双向收窄**(两头收才是竹叶;
|
|
1800
|
+
根端平头藏进鎏金剑格后);刃上两道**竹节纹**短线(鎏金/玉青)替代血槽;
|
|
1801
|
+
剑格 = 圆头小方(rx 满,圆滑);剑柄 = 细杆 + 鎏金剑首圆珠。
|
|
1802
|
+
动画只发生在**剑之外**(飞出的灵气光点 + 中心炸开的气环),
|
|
1803
|
+
**剑身上不挂任何动画**(用户:「剑体动画不需要」)。 -->
|
|
1804
|
+
<!-- 竖剑(弧刃,近黑 / currentColor) -->
|
|
1805
|
+
<path d="M12 .55C12.35 3.2 12.8 5.2 12.8 7.5C12.8 11.5 12.4 15.5 12.4 18.2H11.6C11.6 15.5 11.2 11.5 11.2 7.5C11.2 5.2 11.65 3.2 12 .55z" fill="currentColor" stroke="none"/>
|
|
1806
|
+
<path d="M11.5 6.2h1.0v.3h-1.0z" fill="var(--q-bottle-rim,#D9FBE8)" stroke="none" opacity=".55"/><path d="M11.45 9.4h1.1v.32h-1.1z" fill="var(--q-bottle-rim,#D9FBE8)" stroke="none" opacity=".65"/><path d="M11.55 13.8h.9v.32h-.9z" fill="var(--q-bottle-rim,#D9FBE8)" stroke="none" opacity=".65"/><rect x="11.93" y="3" width=".14" height="14.4" fill="var(--q-bottle-rim,#D9FBE8)" stroke="none" opacity=".3"/><circle cx="12.35" cy="8.6" r=".13" fill="var(--q-gold,#E8C77A)" stroke="none" opacity=".9"/><circle cx="11.7" cy="11.9" r=".11" fill="var(--q-gold,#E8C77A)" stroke="none" opacity=".8"/><circle cx="12.3" cy="15.2" r=".12" fill="var(--q-gold,#E8C77A)" stroke="none" opacity=".85"/><path d="M12.55 9.7l-.45.85h.5l-.5.95" fill="none" stroke="var(--q-gold,#E8C77A)" stroke-width=".16" stroke-linecap="round" stroke-linejoin="round" opacity=".85"/><circle cx="12" cy="1.7" r=".13" fill="var(--q-bottle-rim,#D9FBE8)" stroke="none" opacity=".9"/>
|
|
1807
|
+
<rect class="swdgv" x="9.3" y="18" width="5.4" height="1.7" rx=".85" fill="var(--q-gold,#E8C77A)" stroke="none"/><path d="M12 18.45l.38.4-.38.4-.38-.4z" fill="currentColor" stroke="none" opacity=".8"/><circle cx="9.95" cy="18.85" r=".2" fill="var(--q-bottle-rim,#D9FBE8)" stroke="none" opacity=".75"/><circle cx="14.05" cy="18.85" r=".2" fill="var(--q-bottle-rim,#D9FBE8)" stroke="none" opacity=".75"/>
|
|
1808
|
+
<rect x="11.15" y="19.5" width="1.7" height="1.9" rx=".8" fill="currentColor" stroke="none"/><rect x="11.0" y="19.66" width="2.0" height=".26" rx=".13" fill="var(--q-gold,#E8C77A)" stroke="none"/><path d="M11.3 20.35l1.45-.3M11.3 20.85l1.45-.3M11.35 21.3l1.3-.26" stroke="var(--q-bottle-rim,#D9FBE8)" stroke-width=".13" stroke-linecap="round" opacity=".55" fill="none"/>
|
|
1809
|
+
<circle cx="12" cy="22.1" r="1.35" fill="var(--q-gold,#E8C77A)" stroke="none"/><circle cx="12" cy="22.1" r=".72" fill="none" stroke="var(--q-bottle-rim,#D9FBE8)" stroke-width=".14" opacity=".65"/><circle cx="12" cy="22.1" r=".18" fill="currentColor" stroke="none"/>
|
|
1810
|
+
<!-- 横剑(弧刃,淡金) -->
|
|
1811
|
+
<path d="M23.45 12C20.8 12.35 18.8 12.8 16.5 12.8C12.5 12.8 8.5 12.4 5.8 12.4V11.6C8.5 11.6 12.5 11.2 16.5 11.2C18.8 11.2 20.8 11.65 23.45 12z" fill="var(--q-gold-lt,#F2DCA2)" stroke="none"/>
|
|
1812
|
+
|
|
1813
|
+
<path d="M9.1 11.55h.9v.32h-.9z" fill="var(--q-gold-deep,#B98A2E)" stroke="none" opacity=".6"/><path d="M13.5 11.55h1.1v.32h-1.1z" fill="var(--q-gold-deep,#B98A2E)" stroke="none" opacity=".6"/><path d="M18.55 11.85h.8v.3h-.8z" fill="var(--q-gold-deep,#B98A2E)" stroke="none" opacity=".5"/><rect x="6.9" y="11.93" width="13" height=".14" fill="var(--q-gold-deep,#B98A2E)" stroke="none" opacity=".5"/><circle cx="14.9" cy="11.62" r=".12" fill="var(--q-bottle-rim,#D9FBE8)" stroke="none" opacity=".9"/><circle cx="17.6" cy="12.35" r=".11" fill="var(--q-bottle-rim,#D9FBE8)" stroke="none" opacity=".8"/><circle cx="10.9" cy="12.3" r=".11" fill="var(--q-bottle-rim,#D9FBE8)" stroke="none" opacity=".8"/><path d="M14.3 11.5l.85.45h-.5l.9.5" fill="none" stroke="var(--q-bottle-rim,#D9FBE8)" stroke-width=".16" stroke-linecap="round" stroke-linejoin="round" opacity=".9"/><circle cx="22.4" cy="12" r=".13" fill="var(--q-bottle-rim,#D9FBE8)" stroke="none" opacity=".95"/>
|
|
1814
|
+
<rect class="swdgh" x="4.3" y="9.3" width="1.7" height="5.4" rx=".85" fill="var(--q-gold,#E8C77A)" stroke="none"/><path d="M5.15 11.22l.4.38-.4.38-.38-.38z" fill="currentColor" stroke="none" opacity=".8"/><circle cx="5.15" cy="9.95" r=".2" fill="var(--q-bottle-rim,#D9FBE8)" stroke="none" opacity=".75"/><circle cx="5.15" cy="14.05" r=".2" fill="var(--q-bottle-rim,#D9FBE8)" stroke="none" opacity=".75"/>
|
|
1815
|
+
<rect x="2.6" y="11.15" width="1.9" height="1.7" rx=".8" fill="var(--q-bottle-rim,#D9FBE8)" stroke="none"/><rect x="4.44" y="11.0" width=".26" height="2.0" rx=".13" fill="var(--q-gold,#E8C77A)" stroke="none"/><path d="M3.65 11.3l.3 1.45M3.15 11.3l-.3 1.45M2.7 11.35l.26 1.3" stroke="var(--q-gold,#E8C77A)" stroke-width=".13" stroke-linecap="round" opacity=".55" fill="none"/>
|
|
1816
|
+
<circle cx="1.9" cy="12" r="1.35" fill="var(--q-gold,#E8C77A)" stroke="none"/><circle cx="1.9" cy="12" r=".72" fill="none" stroke="currentColor" stroke-width=".14" opacity=".5"/><circle cx="1.9" cy="12" r=".18" fill="var(--q-bottle-rim,#D9FBE8)" stroke="none"/>
|
|
1817
|
+
<!-- 中心剑气方环:自交叉点炸开淡出 -->
|
|
1818
|
+
<rect class="swdring" x="9.4" y="9.4" width="5.2" height="5.2" rx="1.6" fill="none" stroke="var(--q-bottle-rim,#D9FBE8)" stroke-width=".8"/>
|
|
1819
|
+
<!-- 剑鸣灵气:四枚光点从交叉点沿四个剑向飞出 -->
|
|
1820
|
+
<g class="swdqi" fill="var(--q-bottle-rim,#D9FBE8)" stroke="none">
|
|
1821
|
+
<circle class="qi-up" cx="12" cy="12" r=".5"/>
|
|
1822
|
+
<circle class="qi-dn" cx="12" cy="12" r=".5"/>
|
|
1823
|
+
<circle class="qi-lf" cx="12" cy="12" r=".5"/>
|
|
1824
|
+
<circle class="qi-rt" cx="12" cy="12" r=".5"/>
|
|
1825
|
+
</g>
|
|
1826
|
+
</svg>
|
|
1827
|
+
</button>
|
|
1207
1828
|
</span>
|
|
1208
|
-
<button class="tab" type="button" data-tab="discover" aria-label="发现"><span class="icw"><svg width="
|
|
1209
|
-
|
|
1829
|
+
<button class="tab" type="button" data-tab="discover" aria-label="发现"><span class="icw"><svg class="starsvg" width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
1830
|
+
<!-- 星盘(2026-10-24 用户:「除了第一个全部换」→ 血玉蛛换成星盘):
|
|
1831
|
+
「发现」= 观星探秘;外圈刻度环缓转,盘心丹血宝石一明一灭 -->
|
|
1832
|
+
<circle class="staring" cx="12" cy="12" r="8.8" stroke="currentColor" stroke-width="1.6" stroke-dasharray="1.7 2.5" fill="none"/>
|
|
1833
|
+
<circle cx="12" cy="12" r="6.2" fill="currentColor" stroke="none" opacity=".22"/>
|
|
1834
|
+
<circle cx="12" cy="12" r="6.2" stroke="currentColor" stroke-width="1.6" fill="none"/>
|
|
1835
|
+
<path d="M12 5.8v12.4M5.8 12h12.4" stroke="var(--q-gold,#E8C77A)" stroke-width="1.1"/>
|
|
1836
|
+
<circle class="starcore" cx="12" cy="12" r="2.2" fill="var(--q-blood,#E23B2E)" stroke="none"/>
|
|
1837
|
+
<circle cx="7.4" cy="7.4" r=".5" fill="var(--q-gold,#E8C77A)" stroke="none"/>
|
|
1838
|
+
<circle cx="16.6" cy="7.4" r=".5" fill="var(--q-gold,#E8C77A)" stroke="none"/>
|
|
1839
|
+
<circle cx="7.4" cy="16.6" r=".5" fill="var(--q-gold,#E8C77A)" stroke="none"/>
|
|
1840
|
+
<circle cx="16.6" cy="16.6" r=".5" fill="var(--q-gold,#E8C77A)" stroke="none"/>
|
|
1841
|
+
</svg></span></button>
|
|
1842
|
+
<button class="tab" type="button" data-tab="mine" aria-label="我的"><span class="icw"><svg class="yysvg" width="28" height="28" viewBox="1.6 0.7 20.8 20.8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
1843
|
+
<!-- 元婴 · 宝宝人形(2026-10-24 用户:「我的换元婴设计 要有宝宝人形」→ 酒葫芦换成元婴;
|
|
1844
|
+
「元婴还没动画」→ 加强:颠幅加大 + 摇头晃脑 + 明灭加深;
|
|
1845
|
+
「元婴要有五官」→ 头部开孔镂空五官):
|
|
1846
|
+
元婴出窍打坐(凡人修仙传:元婴=丹田中盘坐的小人,可出窍悬空):
|
|
1847
|
+
背光环 + 盘腿交叉 + 丹田金丹 + 蒲团浮空 —— 四个「打坐」符号全用上;
|
|
1848
|
+
剪影内部细节一律对比色(鎏金/浅玉),同色细节在剪影里读不出。
|
|
1849
|
+
**五官用镂空(fill-rule:evenodd 在头部剪影上开孔)而不是画实色** ——
|
|
1850
|
+
头部填充是 currentColor(随主题/选中态变),实色五官会跟头同色消失;
|
|
1851
|
+
镂空透出背景,任何主题、任何选中态都读得出是眼睛和嘴。
|
|
1852
|
+
眼 = 两个圆孔、嘴 = 下弯的笑口、腮红 = 丹血朱两颊;
|
|
1853
|
+
眨眼 = 一块与头同色的「眼帘」盖在眼孔上(opacity 0→1),只有同色才看不出接缝。
|
|
1854
|
+
动画:呼吸般的轻颠(整组 translateY -1px)+ 金丹明灭(修炼吐纳)
|
|
1855
|
+
+ 顶髻明灭 + 偶尔眨眼(打坐也要眨眼)。
|
|
1856
|
+
顶栏右上角「我的」是同款内联(21px,class 共用)—— **两处标记必须同步**。 -->
|
|
1857
|
+
<g class="yybaby">
|
|
1858
|
+
<!-- 元婴打坐 · 定稿(2026-10-25「完全不行」→ 四案比选,F 案胜出):
|
|
1859
|
+
一体剪影(肩窄→袖外张→盘腿宽平底),盘腿上缘走浅玉弧线,
|
|
1860
|
+
两端金手搭膝,丹田金丹+灵光环;莲花座案因花瓣糊成金坨被否。 -->
|
|
1861
|
+
<g class="yyhead">
|
|
1862
|
+
<path d="M12 3.9a4.0 4.0 0 1 0 0 8.0 4.0 4.0 0 1 0 0-8.0z
|
|
1863
|
+
M10.5 6.05a.6 .6 0 1 0 0 1.2 .6 .6 0 1 0 0-1.2z
|
|
1864
|
+
M13.5 6.05a.6 .6 0 1 0 0 1.2 .6 .6 0 1 0 0-1.2z
|
|
1865
|
+
M10.75 9.35Q12 11.6 13.25 9.35Q12 10.25 10.75 9.35Z"
|
|
1866
|
+
fill="currentColor" fill-rule="evenodd" stroke="none"/>
|
|
1867
|
+
<circle cx="7.8" cy="7.7" r=".48" fill="currentColor" stroke="none"/>
|
|
1868
|
+
<circle cx="16.2" cy="7.7" r=".48" fill="currentColor" stroke="none"/>
|
|
1869
|
+
<ellipse class="yyblush" cx="8.95" cy="8.9" rx=".7" ry=".42" fill="var(--q-blood,#E23B2E)" stroke="none" opacity=".42"/>
|
|
1870
|
+
<ellipse class="yyblush" cx="15.05" cy="8.9" rx=".7" ry=".42" fill="var(--q-blood,#E23B2E)" stroke="none" opacity=".42"/>
|
|
1871
|
+
<circle cx="12" cy="5.2" r=".27" fill="var(--q-blood,#E23B2E)" stroke="none" opacity=".9"/>
|
|
1872
|
+
<rect class="yylid" x="9.65" y="6.15" width="4.7" height="1.8" rx=".65" fill="currentColor" stroke="none" opacity="0"/>
|
|
1873
|
+
<circle class="yyknot" cx="12" cy="2.85" r=".9" fill="var(--q-gold,#E8C77A)" stroke="none"/>
|
|
1874
|
+
</g>
|
|
1875
|
+
<path d="M12 11.9c-1.95 0-3.25 1.15-3.85 2.9-.42 1.25-.95 2.3-1.9 3.0v2.05h11.5v-2.05c-.95-.7-1.48-1.75-1.9-3.0-.6-1.75-1.9-2.9-3.85-2.9z" fill="currentColor" stroke="none"/>
|
|
1876
|
+
<circle cx="7.0" cy="18.45" r="2.15" fill="currentColor" stroke="none"/>
|
|
1877
|
+
<circle cx="17.0" cy="18.45" r="2.15" fill="currentColor" stroke="none"/>
|
|
1878
|
+
<rect x="7.0" y="17.6" width="10.0" height="2.9" fill="currentColor" stroke="none"/>
|
|
1879
|
+
<!-- 禅定印 · canvas 细线(2026-10-25 配色定稿):
|
|
1880
|
+
两条掌缘弧 + 拇指三角,用 --canvas 描线而不是填金/填孔 ——
|
|
1881
|
+
填金在选中绿上只有 1.29,填孔在 28px 下糊成一团黑;
|
|
1882
|
+
细线两头都不犯:暗底当亮线、亮底当暗线,且不占体积。
|
|
1883
|
+
金丹垫 canvas 暗衬后才立得住(朱 vs 绿 2.0 → 朱 vs 暗衬 4.0)。 -->
|
|
1884
|
+
<g fill="none" stroke="var(--canvas,#0B1512)" stroke-width=".62" stroke-linecap="round">
|
|
1885
|
+
<path d="M9.15 16.45q.55 2.4 2.85 2.4"/>
|
|
1886
|
+
<path d="M14.85 16.45q-.55 2.4-2.85 2.4"/>
|
|
1887
|
+
<path d="M10.45 16.15 12 14.5 13.55 16.15"/>
|
|
1888
|
+
</g>
|
|
1889
|
+
<!-- 金丹:卧在印心,外面垫一圈 canvas 暗衬 —— 朱在绿身上只有 2.0,
|
|
1890
|
+
垫了暗衬才有 4.0(深)/ 4.5(浅),28px 才立得住;明灭 = 吐纳 -->
|
|
1891
|
+
<circle cx="12" cy="16.15" r=".78" fill="var(--canvas,#0B1512)" stroke="none"/>
|
|
1892
|
+
<circle class="yypill" cx="12" cy="16.15" r=".5" fill="var(--q-blood,#E23B2E)" stroke="none"/>
|
|
1893
|
+
</g>
|
|
1894
|
+
</svg></span></button>
|
|
1210
1895
|
</nav>
|
|
1211
1896
|
<!-- 第三方库归口 shared/vendor(与内核共用一份):markdown 渲染栈要用 marked + DOMPurify;
|
|
1212
1897
|
highlight.js 由内核按需加载,地址在下面给(内核自己不知道页面的相对根;
|
|
@@ -1233,9 +1918,22 @@ button.setrow{font-family:inherit}
|
|
|
1233
1918
|
<script src="shared/host-glue.js"></script>
|
|
1234
1919
|
<script src="shared/conversation.js"></script>
|
|
1235
1920
|
<script src="shared/transport-http.js"></script>
|
|
1921
|
+
<!-- 会话↔需求关系图谱的专业图谱组件(vendor,与 html2canvas 同目录):
|
|
1922
|
+
cytoscape.js(UMD,内置力导向 cose / 缩放 / 拖拽 / 命中测试)+ fCoSE 布局扩展
|
|
1923
|
+
(依赖 layout-base / cose-base,按序加载后 cytoscape.use() 注册)。 -->
|
|
1924
|
+
<script src="vendor/layout-base.js"></script>
|
|
1925
|
+
<script src="vendor/cose-base.js"></script>
|
|
1926
|
+
<script src="vendor/cytoscape-fcose.js"></script>
|
|
1927
|
+
<script src="vendor/cytoscape.min.js"></script>
|
|
1236
1928
|
<!-- 需求页(独立子页面模块):只在这里加载并注册 window.MagicReqs,
|
|
1237
1929
|
真正的 DOM/逻辑都在 reqs.js,主 IIFE 末尾 mount 时把宿主能力注入进去。 -->
|
|
1238
1930
|
<script src="reqs.js"></script>
|
|
1931
|
+
<!-- 需求页的「页面完成情况」截图与标注(reqshot.js):开抽屉 → 打开页面(中转)→
|
|
1932
|
+
截图 → 标注 → 写回需求文档。宿主只提供浏览器/网络这几件能力。 -->
|
|
1933
|
+
<script src="reqshot.js"></script>
|
|
1934
|
+
<!-- 脚本页(独立子页面模块):只在这里加载并注册 window.MagicScripts,
|
|
1935
|
+
真正的 DOM/逻辑都在 scripts.js,主 IIFE 末尾 mount 时把宿主能力注入进去。 -->
|
|
1936
|
+
<script src="scripts.js"></script>
|
|
1239
1937
|
<script>
|
|
1240
1938
|
(function () {
|
|
1241
1939
|
'use strict';
|
|
@@ -1276,12 +1974,20 @@ button.setrow{font-family:inherit}
|
|
|
1276
1974
|
var pluginDetailEl = document.getElementById('pluginDetail');
|
|
1277
1975
|
var pluginDetailTitle = document.getElementById('pluginDetailTitle');
|
|
1278
1976
|
var pluginDetailBody = document.getElementById('pluginDetailBody');
|
|
1977
|
+
var knowledgeEl = document.getElementById('knowledge');
|
|
1978
|
+
var knowledgeListEl = document.getElementById('knowledgeList');
|
|
1979
|
+
var knowledgeDetailEl = document.getElementById('knowledgeDetail');
|
|
1980
|
+
var knowledgeDetailTitle = document.getElementById('knowledgeDetailTitle');
|
|
1981
|
+
var knowledgeDetailBody = document.getElementById('knowledgeDetailBody');
|
|
1982
|
+
var scriptsEl = document.getElementById('scripts');
|
|
1983
|
+
var scriptDetailEl = document.getElementById('scriptDetail');
|
|
1279
1984
|
var brEl = document.getElementById('browser');
|
|
1280
1985
|
var brFrame = document.getElementById('brFrame');
|
|
1281
1986
|
var brAddr = document.getElementById('brAddr');
|
|
1282
1987
|
var brForm = document.getElementById('brForm');
|
|
1283
|
-
var brShield = document.getElementById('brShield');
|
|
1284
1988
|
var brStar = document.getElementById('brStar');
|
|
1989
|
+
var brRelayBtn = document.getElementById('brRelay');
|
|
1990
|
+
var brShotBtn = document.getElementById('brShot');
|
|
1285
1991
|
var readerEl = document.getElementById('reader');
|
|
1286
1992
|
var rdTitle = document.getElementById('rdTitle');
|
|
1287
1993
|
var rdSub = document.getElementById('rdSub');
|
|
@@ -1358,6 +2064,10 @@ button.setrow{font-family:inherit}
|
|
|
1358
2064
|
取到 undefined,但一取 .indexOf 就抛异常,整个页面初始化会在这里断掉。 */
|
|
1359
2065
|
var CORE_FEATURES = [];
|
|
1360
2066
|
function teamSupported() { return CORE_FEATURES.indexOf('feature.session.team') >= 0; }
|
|
2067
|
+
/* 需求截图能力位(2026-10-25):core 支持「把页面完成情况截图写进需求文档」。
|
|
2068
|
+
与 feature.requirements **分开** —— core 可能支持需求聚合但还不支持截图,
|
|
2069
|
+
那样就把相机徽标藏起来,而不是给一个点了必然 501 的入口。 */
|
|
2070
|
+
function hasReqShotFeature() { return CORE_FEATURES.indexOf('feature.requirement.shots') >= 0; }
|
|
1361
2071
|
function loadTeamPref() {
|
|
1362
2072
|
var o = { sessions: {}, fresh: false, routes: {}, rules: {}, parallel: 4, split: 'standard', splitCode: false, adaptive: true };
|
|
1363
2073
|
try {
|
|
@@ -1510,6 +2220,70 @@ button.setrow{font-family:inherit}
|
|
|
1510
2220
|
saveHostingPref();
|
|
1511
2221
|
}
|
|
1512
2222
|
|
|
2223
|
+
/* ── 思考模式(2026-10-24 用户:「移动端会话中可配置思考模式 默认开最高」)──
|
|
2224
|
+
与团队 / 托管同一套形状:**挂在会话上**,每条会话各记自己的档位;没记过 = 最高档。
|
|
2225
|
+
新会话还没拿到 session_id 时先记在 `fresh`(首帧带回 id 就落到那条会话上)。
|
|
2226
|
+
`off` 不走 --reasoning(core 把它归一成「不传参数」= 引擎默认,仍会思考),
|
|
2227
|
+
而是下发 noThinking,让 core 用各引擎的官方关闭开关(见 internal/agent/engine.go)。 */
|
|
2228
|
+
var REASONING_KEY = 'magic.reasoning.sessions.v1';
|
|
2229
|
+
var REASONING_DEFAULT = 'max';
|
|
2230
|
+
/* 只暴露**跨引擎有共识**的 5 档(2026-10-24 用户:「思考模式应该无法支持6档」):
|
|
2231
|
+
xhigh / minimal 只有 codebuddy / codex 认,claude 会把 minimal 降成 low,
|
|
2232
|
+
UI 上留着只会让同一档在不同引擎表现不一致。core 仍接受这两个值(老会话记录 /
|
|
2233
|
+
已排队条目照常下发),只是移动端不再提供入口。 */
|
|
2234
|
+
var REASONING_LEVELS = [
|
|
2235
|
+
{ v: 'max', label: '最高', desc: '默认' },
|
|
2236
|
+
{ v: 'high', label: '高', desc: '复杂任务' },
|
|
2237
|
+
{ v: 'medium', label: '中', desc: '日常' },
|
|
2238
|
+
{ v: 'low', label: '低', desc: '快问快答' },
|
|
2239
|
+
{ v: 'off', label: '关闭', desc: '不思考' }
|
|
2240
|
+
];
|
|
2241
|
+
function validReasoning(v) {
|
|
2242
|
+
for (var i = 0; i < REASONING_LEVELS.length; i++) if (REASONING_LEVELS[i].v === v) return true;
|
|
2243
|
+
return false;
|
|
2244
|
+
}
|
|
2245
|
+
function reasoningLabel(v) {
|
|
2246
|
+
for (var i = 0; i < REASONING_LEVELS.length; i++) if (REASONING_LEVELS[i].v === v) return REASONING_LEVELS[i].label;
|
|
2247
|
+
return '最高';
|
|
2248
|
+
}
|
|
2249
|
+
function loadReasoningPref() {
|
|
2250
|
+
var o = { sessions: {}, fresh: '' };
|
|
2251
|
+
try {
|
|
2252
|
+
var m = JSON.parse(localStorage.getItem(REASONING_KEY) || '{}');
|
|
2253
|
+
if (m && typeof m === 'object' && !Array.isArray(m)) {
|
|
2254
|
+
Object.keys(m).forEach(function (sid) { if (validReasoning(m[sid])) o.sessions[String(sid)] = m[sid]; });
|
|
2255
|
+
}
|
|
2256
|
+
} catch (e) {}
|
|
2257
|
+
return o;
|
|
2258
|
+
}
|
|
2259
|
+
function saveReasoningPref() {
|
|
2260
|
+
/* 与团队 / 托管同一条容量护栏:最多 400 条,超出按 key 顺序丢(只影响容量)。 */
|
|
2261
|
+
var ids = Object.keys(REASONING.sessions || {});
|
|
2262
|
+
if (ids.length > 400) {
|
|
2263
|
+
ids.slice(0, ids.length - 400).forEach(function (sid) { delete REASONING.sessions[sid]; });
|
|
2264
|
+
}
|
|
2265
|
+
try { localStorage.setItem(REASONING_KEY, JSON.stringify(REASONING.sessions || {})); } catch (e) {}
|
|
2266
|
+
schedulePushPrefs();
|
|
2267
|
+
paintRunSettings();
|
|
2268
|
+
}
|
|
2269
|
+
var REASONING = loadReasoningPref();
|
|
2270
|
+
/* 当前打开的那条会话生效的档位(切换 / 新建会话时由 mountConversation 重设)。 */
|
|
2271
|
+
var REASONING_CUR = REASONING_DEFAULT;
|
|
2272
|
+
/* 这条会话的档位:session_id / run_id 任一有显式记录就用它,否则最高档。
|
|
2273
|
+
不沿父链继承 —— 思考档位是这条会话自己的运行参数,与团队的开关语义不同。 */
|
|
2274
|
+
function reasoningOnFor(sid, s) {
|
|
2275
|
+
if (!sid) return REASONING.fresh || REASONING_DEFAULT;
|
|
2276
|
+
var cur = s || sessionByAnyId(sid);
|
|
2277
|
+
if (cur) {
|
|
2278
|
+
var keys = [sidOf(cur), cur.session_id, cur.run_id];
|
|
2279
|
+
for (var i = 0; i < keys.length; i++) {
|
|
2280
|
+
var k = keys[i];
|
|
2281
|
+
if (k && Object.prototype.hasOwnProperty.call(REASONING.sessions, String(k))) return REASONING.sessions[String(k)];
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
return REASONING.sessions[String(sid)] || REASONING_DEFAULT;
|
|
2285
|
+
}
|
|
2286
|
+
|
|
1513
2287
|
/* ══════════ 环境(多连接)══════════
|
|
1514
2288
|
2026-10-22 用户:「移动端要支持环境切换 可以有多个连接」。
|
|
1515
2289
|
一台手机可以记住多台桌面(家里 / 公司 / 另一台机器),任务页标题就是切换入口。
|
|
@@ -1798,8 +2572,10 @@ button.setrow{font-family:inherit}
|
|
|
1798
2572
|
⚠️ 分桶键用稳定 eid(见 ensureEnvId),不是 url|token(令牌会轮换)、也不是 host:port
|
|
1799
2573
|
(换 IP/端口/隧道域名就变成另一台,现场全丢)。老数据按 host 哈希做一次迁移。 */
|
|
1800
2574
|
var ENV_SCOPE_KEYS = [
|
|
1801
|
-
'magic.ws', 'magic.taskmode', 'magic.hosting.sessions.v1', 'magic.engine', 'magic.model',
|
|
2575
|
+
'magic.ws', 'magic.taskmode', 'magic.hosting.sessions.v1', 'magic.reasoning.sessions.v1', 'magic.engine', 'magic.model',
|
|
2576
|
+
'magic.fallback.engine', 'magic.fallback.model',
|
|
1802
2577
|
'magic.suggest.engine', 'magic.suggest.model',
|
|
2578
|
+
'magic.suggest.fallback.engine', 'magic.suggest.fallback.model',
|
|
1803
2579
|
'magic.theme', 'magic.font', 'magic.notify',
|
|
1804
2580
|
'magic.pinned', 'magic.spaces', 'magic.browser.stars.v1',
|
|
1805
2581
|
'magic.team.sessions.v1', 'magic.team.routes.v1', 'magic.team.rules.v1',
|
|
@@ -1908,8 +2684,10 @@ button.setrow{font-family:inherit}
|
|
|
1908
2684
|
· 本地任何一处改了,防抖推回远端。
|
|
1909
2685
|
纯设备偏好(主题/字体/通知)**不同步** —— 那是手机自己的事。 */
|
|
1910
2686
|
var PREF_SYNC_KEYS = [
|
|
1911
|
-
'magic.engine', 'magic.model', 'magic.
|
|
1912
|
-
'magic.
|
|
2687
|
+
'magic.engine', 'magic.model', 'magic.fallback.engine', 'magic.fallback.model',
|
|
2688
|
+
'magic.suggest.engine', 'magic.suggest.model',
|
|
2689
|
+
'magic.suggest.fallback.engine', 'magic.suggest.fallback.model', 'magic.ws', 'magic.spaces', 'magic.pinned',
|
|
2690
|
+
'magic.hosting.sessions.v1', 'magic.reasoning.sessions.v1',
|
|
1913
2691
|
'magic.team.sessions.v1', 'magic.team.routes.v1', 'magic.team.rules.v1',
|
|
1914
2692
|
'magic.team.parallel', 'magic.team.split', 'magic.team.splitcode', 'magic.team.adaptive'
|
|
1915
2693
|
];
|
|
@@ -2259,7 +3037,7 @@ button.setrow{font-family:inherit}
|
|
|
2259
3037
|
}
|
|
2260
3038
|
|
|
2261
3039
|
/* ══════════ 视图:启动页 → 会话列表 → 会话 ══════════ */
|
|
2262
|
-
function hideAll() { launcherEl.hidden = true; historyEl.hidden = true; mineEl.hidden = true; reqsEl.hidden = true; reqDetailEl.hidden = true; discoverEl.hidden = true; teamPageEl.hidden = true; runSettingsPageEl.hidden = true; pluginsEl.hidden = true; pluginDetailEl.hidden = true; sessionEl.hidden = true; }
|
|
3040
|
+
function hideAll() { launcherEl.hidden = true; historyEl.hidden = true; mineEl.hidden = true; reqsEl.hidden = true; reqDetailEl.hidden = true; discoverEl.hidden = true; teamPageEl.hidden = true; runSettingsPageEl.hidden = true; pluginsEl.hidden = true; pluginDetailEl.hidden = true; knowledgeEl.hidden = true; knowledgeDetailEl.hidden = true; scriptsEl.hidden = true; scriptDetailEl.hidden = true; sessionEl.hidden = true; }
|
|
2263
3041
|
function setActive(name) {
|
|
2264
3042
|
Array.prototype.forEach.call(tabbarEl.querySelectorAll('.tab'), function (b) {
|
|
2265
3043
|
b.classList.toggle('is-on', b.getAttribute('data-tab') === name);
|
|
@@ -2296,12 +3074,27 @@ button.setrow{font-family:inherit}
|
|
|
2296
3074
|
if (window.MagicReqs && window.MagicReqs.show) window.MagicReqs.show();
|
|
2297
3075
|
}
|
|
2298
3076
|
function showDiscover() { hideAll(); discoverEl.hidden = false; tabbarEl.hidden = false; setActive('discover'); }
|
|
3077
|
+
/* 脚本页:内容与逻辑在 scripts.js(独立子页面模块),这里只负责切页。
|
|
3078
|
+
进页面即拉一次清单(GET /desk/scripts,只读、不启停任何东西);
|
|
3079
|
+
启停与日志流由 scripts.js 自己管。 */
|
|
3080
|
+
function showScripts() {
|
|
3081
|
+
hideAll(); scriptsEl.hidden = false; tabbarEl.hidden = true;
|
|
3082
|
+
if (window.MagicScripts && window.MagicScripts.show) window.MagicScripts.show();
|
|
3083
|
+
}
|
|
3084
|
+
function showScriptDetail() { hideAll(); scriptDetailEl.hidden = false; tabbarEl.hidden = true; }
|
|
2299
3085
|
function showMine() {
|
|
2300
3086
|
hideAll(); mineEl.hidden = false; tabbarEl.hidden = false; setActive('mine');
|
|
2301
3087
|
mineDevice.textContent = histSubtitle || '本机';
|
|
2302
3088
|
paintSuggestPref();
|
|
2303
3089
|
var ns = document.getElementById('notifyState');
|
|
2304
|
-
|
|
3090
|
+
/* 权限状态只能问原生才知道(异步)—— 先按缓存画一个,再探一次回来刷新。
|
|
3091
|
+
旧写法把「插件对象在」当成「已接入」,其实那只说明 Capacitor 运行时注入了,
|
|
3092
|
+
传输层通不通是另一回事(用户看到的「已开启」其实什么也没开)。 */
|
|
3093
|
+
if (ns) ns.textContent = notifyStateLabel();
|
|
3094
|
+
notifyProbe().then(function () {
|
|
3095
|
+
var n2 = document.getElementById('notifyState');
|
|
3096
|
+
if (n2) n2.textContent = notifyStateLabel();
|
|
3097
|
+
});
|
|
2305
3098
|
markSeg(segTheme, PREFS.theme); markSeg(segFont, PREFS.font); markSeg(segMode, TASK_MODE);
|
|
2306
3099
|
paintRunPref();
|
|
2307
3100
|
refreshVpn();
|
|
@@ -2697,6 +3490,267 @@ button.setrow{font-family:inherit}
|
|
|
2697
3490
|
} catch (e) {}
|
|
2698
3491
|
}
|
|
2699
3492
|
|
|
3493
|
+
/* ══════════ 发现 → 知识库(OpenViking 上下文数据库)══════════
|
|
3494
|
+
数据全部来自桌面端 `GET /desk/knowledge`:装没装、本机版本多少、PyPI 最新版本多少,
|
|
3495
|
+
都由服务端探测后宣布。本页不自己判断状态 —— 那样两端会各说一套。
|
|
3496
|
+
|
|
3497
|
+
安装/升级走 `POST /desk/knowledge/{id}/{action}` 的 SSE:命令是**服务端写死的**,
|
|
3498
|
+
手机只传 id 与动作;输出逐行回来,所以看得见在动,失败也能把 stderr 摊出来。
|
|
3499
|
+
|
|
3500
|
+
与「插件」页的差别只有一个实质点:**有版本**。所以列表上多一枚「可升级」徽标,
|
|
3501
|
+
而「未安装 / 已安装 / 可升级」三态都由服务端说了算。 */
|
|
3502
|
+
var KNOWLEDGE = []; // 最近一次 GET /desk/knowledge 的结果
|
|
3503
|
+
var KNOWLEDGE_BUSY = {}; // id → 正在跑的动作(防重复点)
|
|
3504
|
+
var KNOWLEDGE_LOG = {}; // id → 上次安装/升级的日志文本(收尾重画后仍看得到)
|
|
3505
|
+
|
|
3506
|
+
function loadKnowledge() {
|
|
3507
|
+
/* 已经有缓存就立刻画一遍(从详情返回时不闪「读取中」),再后台刷新真实状态。 */
|
|
3508
|
+
if (KNOWLEDGE.length) renderKnowledge();
|
|
3509
|
+
else knowledgeListEl.innerHTML = '<div class="history__hint">读取中…</div>';
|
|
3510
|
+
fetch(BASE + '/desk/knowledge', { headers: pluginHeaders() })
|
|
3511
|
+
.then(function (r) {
|
|
3512
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
3513
|
+
return r.json();
|
|
3514
|
+
})
|
|
3515
|
+
.then(function (j) {
|
|
3516
|
+
KNOWLEDGE = (j && j.knowledge) || [];
|
|
3517
|
+
renderKnowledge();
|
|
3518
|
+
})
|
|
3519
|
+
.catch(function (e) {
|
|
3520
|
+
knowledgeListEl.innerHTML = '';
|
|
3521
|
+
knowledgeListEl.appendChild(hEl('div', 'history__hint',
|
|
3522
|
+
'读取知识库目录失败:' + (e && e.message ? e.message : e) +
|
|
3523
|
+
'。需要桌面端为较新版本(/desk/knowledge)。'));
|
|
3524
|
+
});
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
function renderKnowledge() {
|
|
3528
|
+
knowledgeListEl.innerHTML = '';
|
|
3529
|
+
if (!KNOWLEDGE.length) {
|
|
3530
|
+
knowledgeListEl.appendChild(hEl('div', 'history__hint', '桌面端没有返回任何知识库。'));
|
|
3531
|
+
return;
|
|
3532
|
+
}
|
|
3533
|
+
knowledgeListEl.appendChild(hEl('div', 'history__group', '上下文数据库'));
|
|
3534
|
+
KNOWLEDGE.forEach(function (k) {
|
|
3535
|
+
var row = hEl('button', 'plugin'); row.type = 'button';
|
|
3536
|
+
var ico = hEl('span', 'plugin__ico' + (k.installed ? ' is-on' : ''));
|
|
3537
|
+
ico.innerHTML = '<svg width="22" height="22"><use href="#i-db"/></svg>';
|
|
3538
|
+
var col = hEl('span', 'plugin__col');
|
|
3539
|
+
col.appendChild(hEl('span', 'plugin__t', k.name));
|
|
3540
|
+
col.appendChild(hEl('span', 'plugin__s', k.tagline || ''));
|
|
3541
|
+
row.appendChild(ico); row.appendChild(col);
|
|
3542
|
+
row.appendChild(knowledgeBadges(k));
|
|
3543
|
+
row.onclick = function () { showKnowledgeDetail(k.id); };
|
|
3544
|
+
knowledgeListEl.appendChild(row);
|
|
3545
|
+
});
|
|
3546
|
+
knowledgeListEl.appendChild(hEl('div', 'pd__note',
|
|
3547
|
+
'安装与升级在**桌面端**执行(手机只发起),装完还需配 ~/.openviking/ov.conf 才能真的开始检索。'));
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
/* 徽标:未安装 / 已安装 vX / 可升级 vY。
|
|
3551
|
+
「可升级」用描边那枚(.pbadge--new)—— 底色会和「已安装」的绿撞。 */
|
|
3552
|
+
function knowledgeBadges(k) {
|
|
3553
|
+
if (!k.installed) return hEl('span', 'pbadge pbadge--off', '未安装');
|
|
3554
|
+
var wrap = document.createElement('span');
|
|
3555
|
+
wrap.style.display = 'flex';
|
|
3556
|
+
wrap.style.flex = '0 0 auto';
|
|
3557
|
+
wrap.style.gap = '4px';
|
|
3558
|
+
wrap.appendChild(hEl('span', 'pbadge pbadge--on',
|
|
3559
|
+
'已安装' + (k.version ? ' v' + k.version : '')));
|
|
3560
|
+
if (k.upgradeAvailable && k.latestVersion) {
|
|
3561
|
+
wrap.appendChild(hEl('span', 'pbadge pbadge--new', '可升级 v' + k.latestVersion));
|
|
3562
|
+
}
|
|
3563
|
+
return wrap;
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
function knowledgeById(id) {
|
|
3567
|
+
for (var i = 0; i < KNOWLEDGE.length; i++) if (KNOWLEDGE[i].id === id) return KNOWLEDGE[i];
|
|
3568
|
+
return null;
|
|
3569
|
+
}
|
|
3570
|
+
|
|
3571
|
+
function showKnowledge() {
|
|
3572
|
+
hideAll(); knowledgeEl.hidden = false; tabbarEl.hidden = true;
|
|
3573
|
+
loadKnowledge();
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
function showKnowledgeDetail(id) {
|
|
3577
|
+
var k = knowledgeById(id);
|
|
3578
|
+
if (!k) { showKnowledge(); return; }
|
|
3579
|
+
hideAll(); knowledgeDetailEl.hidden = false; tabbarEl.hidden = true;
|
|
3580
|
+
knowledgeDetailTitle.textContent = k.name;
|
|
3581
|
+
renderKnowledgeDetail(k);
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
function renderKnowledgeDetail(k) {
|
|
3585
|
+
var box = knowledgeDetailBody;
|
|
3586
|
+
box.innerHTML = '';
|
|
3587
|
+
var wrap = hEl('div', 'pd');
|
|
3588
|
+
|
|
3589
|
+
var head = hEl('div', 'pd__head');
|
|
3590
|
+
head.appendChild(hEl('span', 'pd__name', k.name));
|
|
3591
|
+
head.appendChild(knowledgeBadges(k));
|
|
3592
|
+
wrap.appendChild(head);
|
|
3593
|
+
wrap.appendChild(hEl('p', 'pd__tag', k.tagline || ''));
|
|
3594
|
+
|
|
3595
|
+
/* 状态说明(服务端给的那句「为什么是这个状态」) */
|
|
3596
|
+
if (k.note) wrap.appendChild(hEl('div', 'pd__note', k.note));
|
|
3597
|
+
if (k.bin) wrap.appendChild(hEl('div', 'pd__note', '命令:' + k.bin));
|
|
3598
|
+
if (k.latestVersion) {
|
|
3599
|
+
wrap.appendChild(hEl('div', 'pd__note', '最新版本:v' + k.latestVersion +
|
|
3600
|
+
(k.upgradeAvailable ? '(本机可升级)' : '')));
|
|
3601
|
+
}
|
|
3602
|
+
if (k.pythonPath) {
|
|
3603
|
+
wrap.appendChild(hEl('div', 'pd__note',
|
|
3604
|
+
'Python:' + k.pythonPath + (k.pythonVersion ? '(' + k.pythonVersion + ')' : '')));
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
wrap.appendChild(pdSection('介绍', hEl('p', 'pd__p', k.description || '')));
|
|
3608
|
+
if (k.requirements) wrap.appendChild(pdSection('环境要求', hEl('p', 'pd__p', k.requirements)));
|
|
3609
|
+
|
|
3610
|
+
/* 没装 → 装;装了且有新版 → 升;装了但已是最新 → 仍然给「升级」(能装回滚后的新版本,
|
|
3611
|
+
而且界面上的按钮文案要与服务端给的 upgrade 命令一致,不自己发明动作)。 */
|
|
3612
|
+
var wantUpgrade = k.upgradeAvailable;
|
|
3613
|
+
var cmd = wantUpgrade ? k.upgrade : (k.installed ? k.upgrade : k.install);
|
|
3614
|
+
var cmdLabel = wantUpgrade ? '升级' : (k.installed ? '重新安装' : '一键安装');
|
|
3615
|
+
var action = wantUpgrade || k.installed ? 'upgrade' : 'install';
|
|
3616
|
+
var cmdSec = hEl('div', 'pd__sec');
|
|
3617
|
+
cmdSec.appendChild(hEl('div', 'pd__h', cmdLabel + '命令(在桌面端执行)'));
|
|
3618
|
+
var pre = hEl('pre', 'pd__code', cmd || '(服务端没有给出安装命令)');
|
|
3619
|
+
cmdSec.appendChild(pre);
|
|
3620
|
+
|
|
3621
|
+
var actions = hEl('div', 'pd__actions');
|
|
3622
|
+
var goBtn = hEl('button', 'btn btn--filled',
|
|
3623
|
+
KNOWLEDGE_BUSY[k.id] ? '正在' + KNOWLEDGE_BUSY[k.id] + '…' : cmdLabel);
|
|
3624
|
+
goBtn.type = 'button';
|
|
3625
|
+
goBtn.disabled = !!KNOWLEDGE_BUSY[k.id] || !cmd;
|
|
3626
|
+
goBtn.onclick = function () { runKnowledgeAction(k.id, action); };
|
|
3627
|
+
var copyBtn = hEl('button', 'btn btn--outlined', '复制命令');
|
|
3628
|
+
copyBtn.type = 'button';
|
|
3629
|
+
copyBtn.onclick = function () { copyText(cmd || '', '命令已复制'); };
|
|
3630
|
+
actions.appendChild(goBtn); actions.appendChild(copyBtn);
|
|
3631
|
+
cmdSec.appendChild(actions);
|
|
3632
|
+
|
|
3633
|
+
var log = hEl('pre', 'pd__log');
|
|
3634
|
+
log.textContent = KNOWLEDGE_LOG[k.id] || '';
|
|
3635
|
+
log.hidden = !log.textContent;
|
|
3636
|
+
cmdSec.appendChild(log);
|
|
3637
|
+
wrap.appendChild(cmdSec);
|
|
3638
|
+
k.__log = log;
|
|
3639
|
+
|
|
3640
|
+
/* 让它干活:预填一段话到输入框(不直接发 —— 问题得用户自己填) */
|
|
3641
|
+
var useSec = hEl('div', 'pd__sec');
|
|
3642
|
+
useSec.appendChild(hEl('div', 'pd__h', '用起来'));
|
|
3643
|
+
var useBtn = hEl('button', 'btn btn--filled', '新建会话并让它干活');
|
|
3644
|
+
useBtn.type = 'button';
|
|
3645
|
+
useBtn.style.marginTop = '0';
|
|
3646
|
+
useBtn.onclick = function () { useKnowledge(k); };
|
|
3647
|
+
useSec.appendChild(useBtn);
|
|
3648
|
+
if (k.examples && k.examples.length) {
|
|
3649
|
+
var ex = hEl('pre', 'pd__code', k.examples.join('\n'));
|
|
3650
|
+
ex.style.marginTop = '10px';
|
|
3651
|
+
useSec.appendChild(ex);
|
|
3652
|
+
}
|
|
3653
|
+
useSec.appendChild(hEl('div', 'pd__note',
|
|
3654
|
+
'会话里由 Agent 在桌面端执行这些命令;长任务可以在任务列表里看进度。'));
|
|
3655
|
+
wrap.appendChild(useSec);
|
|
3656
|
+
|
|
3657
|
+
if (k.homepage) {
|
|
3658
|
+
var home = hEl('button', 'setrow');
|
|
3659
|
+
home.appendChild(hEl('span', 'setrow__l', '项目主页'));
|
|
3660
|
+
home.appendChild(hEl('span', 'setrow__v', k.homepage));
|
|
3661
|
+
home.onclick = function () { closeHostSheet(); openBrowser(k.homepage); };
|
|
3662
|
+
var card = hEl('div', 'setcard'); card.appendChild(home);
|
|
3663
|
+
wrap.appendChild(card);
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
box.appendChild(wrap);
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
/* 安装 / 升级:POST 出去,按 SSE 逐行画日志(命令由服务端定,本页只传 id 与动作)。 */
|
|
3670
|
+
function runKnowledgeAction(id, action) {
|
|
3671
|
+
if (KNOWLEDGE_BUSY[id]) { toast('已有任务在跑,等它结束'); return; }
|
|
3672
|
+
var k = knowledgeById(id);
|
|
3673
|
+
var log = k && k.__log;
|
|
3674
|
+
KNOWLEDGE_BUSY[id] = action;
|
|
3675
|
+
KNOWLEDGE_LOG[id] = '▶ 开始' + (action === 'install' ? '安装' : '升级') + '…\n';
|
|
3676
|
+
if (log) { log.hidden = false; log.textContent = KNOWLEDGE_LOG[id]; }
|
|
3677
|
+
renderKnowledgeDetailButtonState(k, action);
|
|
3678
|
+
|
|
3679
|
+
fetch(BASE + '/desk/knowledge/' + encodeURIComponent(id) + '/' + action,
|
|
3680
|
+
{ method: 'POST', headers: pluginHeaders() })
|
|
3681
|
+
.then(function (resp) {
|
|
3682
|
+
if (!resp.ok || !resp.body) throw new Error('HTTP ' + resp.status);
|
|
3683
|
+
var reader = resp.body.getReader(), dec = new TextDecoder(), buf = '';
|
|
3684
|
+
return (function pump() {
|
|
3685
|
+
return reader.read().then(function (chunk) {
|
|
3686
|
+
if (chunk.done) return null;
|
|
3687
|
+
buf += dec.decode(chunk.value, { stream: true });
|
|
3688
|
+
var blocks = buf.split('\n\n');
|
|
3689
|
+
buf = blocks.pop();
|
|
3690
|
+
blocks.forEach(function (b) {
|
|
3691
|
+
var name = '', data = '';
|
|
3692
|
+
b.split('\n').forEach(function (ln) {
|
|
3693
|
+
if (ln.indexOf('event: ') === 0) name = ln.slice(7).trim();
|
|
3694
|
+
else if (ln.indexOf('data: ') === 0) data += ln.slice(6);
|
|
3695
|
+
});
|
|
3696
|
+
if (!data) return;
|
|
3697
|
+
var ev; try { ev = JSON.parse(data); } catch (e) { return; }
|
|
3698
|
+
var line = '';
|
|
3699
|
+
if (name === 'step') line = '\n$ ' + ev.command + '\n';
|
|
3700
|
+
else if (name === 'line') line = ev.line + '\n';
|
|
3701
|
+
else if (name === 'end') {
|
|
3702
|
+
line = ev.ok ? '\n✔ 完成\n' : ('\n✘ 失败:' + (ev.error || ('退出码 ' + ev.code)) + '\n');
|
|
3703
|
+
if (ev.knowledge) {
|
|
3704
|
+
KNOWLEDGE = KNOWLEDGE.map(function (x) { return x.id === ev.knowledge.id ? ev.knowledge : x; });
|
|
3705
|
+
renderKnowledge();
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
if (!line) return;
|
|
3709
|
+
KNOWLEDGE_LOG[id] = (KNOWLEDGE_LOG[id] || '') + line;
|
|
3710
|
+
if (log) { log.textContent = KNOWLEDGE_LOG[id]; log.scrollTop = log.scrollHeight; }
|
|
3711
|
+
});
|
|
3712
|
+
return pump();
|
|
3713
|
+
});
|
|
3714
|
+
})();
|
|
3715
|
+
})
|
|
3716
|
+
.then(function () { delete KNOWLEDGE_BUSY[id]; toast(action === 'install' ? '安装流程结束' : '升级流程结束'); })
|
|
3717
|
+
.catch(function (e) {
|
|
3718
|
+
delete KNOWLEDGE_BUSY[id];
|
|
3719
|
+
KNOWLEDGE_LOG[id] = (KNOWLEDGE_LOG[id] || '') + '\n✘ ' + (e && e.message ? e.message : e) + '\n';
|
|
3720
|
+
if (log) { log.textContent = KNOWLEDGE_LOG[id]; log.scrollTop = log.scrollHeight; }
|
|
3721
|
+
toast('执行失败:' + (e && e.message ? e.message : e));
|
|
3722
|
+
})
|
|
3723
|
+
.then(function () {
|
|
3724
|
+
/* 收尾后按服务端最新状态重画这一页(按钮文案要从「一键安装」变「升级」) */
|
|
3725
|
+
var fresh = knowledgeById(id);
|
|
3726
|
+
if (fresh && !knowledgeDetailEl.hidden) renderKnowledgeDetail(fresh);
|
|
3727
|
+
});
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
function renderKnowledgeDetailButtonState(k, action) {
|
|
3731
|
+
if (!k) return;
|
|
3732
|
+
var btns = knowledgeDetailBody.querySelectorAll('.pd__actions .btn');
|
|
3733
|
+
if (btns[0]) {
|
|
3734
|
+
btns[0].disabled = true;
|
|
3735
|
+
btns[0].textContent = '正在' + (action === 'install' ? '安装' : '升级') + '…';
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3739
|
+
/* 用它干活:新开会话,把「要问什么」预填进输入框(用户补问题后自己发)。 */
|
|
3740
|
+
function useKnowledge(k) {
|
|
3741
|
+
newConversation();
|
|
3742
|
+
var text = k.usePrompt || ('用 ' + k.cli + ' 查知识库。\n问题:');
|
|
3743
|
+
var ta = convEl.querySelector('textarea');
|
|
3744
|
+
if (!ta) { toast('输入框还没准备好,请重试'); return; }
|
|
3745
|
+
ta.value = text;
|
|
3746
|
+
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
3747
|
+
ta.focus();
|
|
3748
|
+
try {
|
|
3749
|
+
var i = text.indexOf(':\n');
|
|
3750
|
+
if (i >= 0) ta.setSelectionRange(i + 2, i + 2);
|
|
3751
|
+
} catch (e) {}
|
|
3752
|
+
}
|
|
3753
|
+
|
|
2700
3754
|
/* ══════════ 内置浏览器 ══════════
|
|
2701
3755
|
用 iframe 内嵌第三方站点(对方没设 X-Frame-Options 才显示得出来)。
|
|
2702
3756
|
导航栈只记我们主动打开的层级 —— 跨源页面内部跳转读不到,
|
|
@@ -2724,10 +3778,10 @@ button.setrow{font-family:inherit}
|
|
|
2724
3778
|
try { return new URL(u).host; } catch (e) { return u; }
|
|
2725
3779
|
}
|
|
2726
3780
|
function brPaint() {
|
|
3781
|
+
brPaintRelay();
|
|
2727
3782
|
var u = brStack[brStack.length - 1] || HOME_URL;
|
|
2728
3783
|
if (document.activeElement === brAddr) return;
|
|
2729
3784
|
brAddr.value = siteName(u);
|
|
2730
|
-
brShield.classList.toggle('is-plain', !/^https:/i.test(u));
|
|
2731
3785
|
brStar.classList.toggle('is-on', loadStars().indexOf(u) >= 0);
|
|
2732
3786
|
}
|
|
2733
3787
|
function brNav(url) {
|
|
@@ -2736,13 +3790,20 @@ button.setrow{font-family:inherit}
|
|
|
2736
3790
|
brLoad(url);
|
|
2737
3791
|
brPaint();
|
|
2738
3792
|
}
|
|
2739
|
-
/*
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
3793
|
+
/* ── 桌面端中转开关(2026-10-25 用户:「加开关,保留能力」)──
|
|
3794
|
+
默认**直连**。开了才走 /b/ 中转:页面由桌面端代取并改写,换来三样直连给不了的 ——
|
|
3795
|
+
桌面独有站点 / 内网服务、被站点拒绝内嵌的页面(中转剥 X-Frame-Options/CSP)、
|
|
3796
|
+
浏览器 MCP 的 relay 注入(magic-client browse 的驱动通道)。
|
|
3797
|
+
刻意**不挂**在「我的 → 网络代理」上:那个开关起的是整机 TUN,桌面端没开 SOCKS5
|
|
3798
|
+
出口时一开手机整机断网(连回桌面端的链路一起断),中转跟着陪葬;且中转+隧道
|
|
3799
|
+
双出口正是当年 Google 风控的坑。两个开关各管各的:网络代理管整机,这个管浏览器。 */
|
|
3800
|
+
var BR_RELAY_KEY = 'magic.browser.relay.v1';
|
|
3801
|
+
function brRelayOn() {
|
|
3802
|
+
try { return localStorage.getItem(BR_RELAY_KEY) === '1'; } catch (e) { return false; }
|
|
3803
|
+
}
|
|
3804
|
+
/* 中转地址:iframe 经桌面端 /b/ 取页面,走**桌面端的网络**。BASE/TOKEN 都空
|
|
3805
|
+
(未配对)时退回直连。令牌拼在 URL 里 —— 转发上游前桌面端会剥掉(stripMagicToken)。 */
|
|
2744
3806
|
function brProxyURL(u) {
|
|
2745
|
-
if (vpnActive()) return u; // 网络代理已开:整机都走隧道,这里不必再中转
|
|
2746
3807
|
try {
|
|
2747
3808
|
var b = BASE || (/^https?:$/i.test(location.protocol) ? location.origin : '');
|
|
2748
3809
|
if (!b || !TOKEN) return u;
|
|
@@ -2753,14 +3814,54 @@ button.setrow{font-family:inherit}
|
|
|
2753
3814
|
(a.search ? a.search + '&' + q : '?' + q);
|
|
2754
3815
|
} catch (e) { return u; }
|
|
2755
3816
|
}
|
|
2756
|
-
|
|
3817
|
+
/* iframe 加载:**默认直连**,走手机自己的网络 —— 内置浏览器首先得是个浏览器
|
|
3818
|
+
(2026-10-25 用户:「没开网络代理时就应该直连啊」)。brRelayOn 开时走 /b/ 中转。 */
|
|
3819
|
+
function brLoad(u) { brFrame.src = brRelayOn() ? brProxyURL(u) : u; }
|
|
3820
|
+
function brPaintRelay() {
|
|
3821
|
+
if (brRelayBtn) brRelayBtn.classList.toggle('is-on', brRelayOn());
|
|
3822
|
+
}
|
|
3823
|
+
function brToggleRelay() {
|
|
3824
|
+
if (!BASE || !TOKEN) { toast('未配对桌面端,用不了中转'); return; }
|
|
3825
|
+
var on = !brRelayOn();
|
|
3826
|
+
try { localStorage.setItem(BR_RELAY_KEY, on ? '1' : '0'); } catch (e) {}
|
|
3827
|
+
brPaintRelay();
|
|
3828
|
+
toast(on ? '已走桌面端中转(MCP 可用)' : '已切回手机直连');
|
|
3829
|
+
if (brStack.length) brReload(); // 立刻按新方式重载当前页
|
|
3830
|
+
}
|
|
2757
3831
|
function openBrowser(url) {
|
|
2758
3832
|
brStack = [url || HOME_URL];
|
|
2759
3833
|
brLoad(brStack[0]);
|
|
2760
3834
|
brPaint();
|
|
3835
|
+
brResetFloat();
|
|
2761
3836
|
brEl.hidden = false;
|
|
2762
3837
|
}
|
|
2763
|
-
function closeBrowser() {
|
|
3838
|
+
function closeBrowser() {
|
|
3839
|
+
brEl.hidden = true; brStack = []; brFrame.src = 'about:blank';
|
|
3840
|
+
brResetFloat();
|
|
3841
|
+
/* 截图流程收尾:关掉浏览器就意味着这次截图不做了 —— 把通道与按钮都收掉,
|
|
3842
|
+
否则下一次打开浏览器还挂着一个指向旧任务的截图入口。 */
|
|
3843
|
+
if (window.MagicReqShot && window.MagicReqShot.reset) window.MagicReqShot.reset();
|
|
3844
|
+
}
|
|
3845
|
+
/* 内核「HTML 产物卡点击」入口(19-attachments 经 MagicHost.openBrowser 调):
|
|
3846
|
+
同源地址(/desk/media 供的本地 HTML 产物)直接进 iframe —— 走 /b/ 中转会把它当外站
|
|
3847
|
+
再抓一遍,多一跳还可能丢 ?token=;其余网址照常 openBrowser(远站按 brRelayOn 决定
|
|
3848
|
+
直连还是走中转)。 */
|
|
3849
|
+
try {
|
|
3850
|
+
window.MagicHost = window.MagicHost || {};
|
|
3851
|
+
window.MagicHost.openBrowser = function (u) {
|
|
3852
|
+
u = String(u || '');
|
|
3853
|
+
if (!u) return;
|
|
3854
|
+
if (BASE && TOKEN && u.indexOf(location.origin) === 0 && /^https?:\/\//i.test(u)) {
|
|
3855
|
+
brStack = [u];
|
|
3856
|
+
brFrame.src = u;
|
|
3857
|
+
brPaint();
|
|
3858
|
+
brResetFloat();
|
|
3859
|
+
brEl.hidden = false;
|
|
3860
|
+
return;
|
|
3861
|
+
}
|
|
3862
|
+
openBrowser(u);
|
|
3863
|
+
};
|
|
3864
|
+
} catch (e) {}
|
|
2764
3865
|
function brBack() {
|
|
2765
3866
|
if (brStack.length > 1) { brStack.pop(); brLoad(brStack[brStack.length - 1]); brPaint(); }
|
|
2766
3867
|
else closeBrowser();
|
|
@@ -2840,6 +3941,157 @@ button.setrow{font-family:inherit}
|
|
|
2840
3941
|
brTyped(v);
|
|
2841
3942
|
});
|
|
2842
3943
|
brAddr.addEventListener('blur', function () { brPaint(); });
|
|
3944
|
+
|
|
3945
|
+
/* ── 悬浮形态(2026-10-24):全屏 ↔ 悬浮卡片 ↔ 悬浮球 ──
|
|
3946
|
+
三态间 iframe 的 src 不动 —— 只是几何变化,页面不重载;只有 closeBrowser() 才清 src。
|
|
3947
|
+
悬浮球挂起时 #browser 整体 hidden 但 brStack 保留,点球恢复。
|
|
3948
|
+
位置记忆:卡片 magic.browser.float.v1、球 magic.browser.ball.v1(localStorage)。
|
|
3949
|
+
悬浮态顶栏是拖拽把手(touch-action:none,见 .browser.is-float .browser__top),
|
|
3950
|
+
把手上的按钮照常可点 —— pointerdown 命中 button/input 时不启动拖拽。 */
|
|
3951
|
+
var brBall = document.getElementById('brBall');
|
|
3952
|
+
var BR_FLOAT_KEY = 'magic.browser.float.v1';
|
|
3953
|
+
var BR_BALL_KEY = 'magic.browser.ball.v1';
|
|
3954
|
+
var brMode = 'full';
|
|
3955
|
+
var brDragState = null; // {kind:'card'|'ball', dx, dy, moved, pid}
|
|
3956
|
+
|
|
3957
|
+
function brLoadPos(key) {
|
|
3958
|
+
try { var v = JSON.parse(localStorage.getItem(key) || 'null'); return v && typeof v.x === 'number' ? v : null; }
|
|
3959
|
+
catch (e) { return null; }
|
|
3960
|
+
}
|
|
3961
|
+
function brSavePos(key, v) { try { localStorage.setItem(key, JSON.stringify(v)); } catch (e) {} }
|
|
3962
|
+
function brClampX(x, w) { return Math.max(6, Math.min(x, window.innerWidth - w - 6)); }
|
|
3963
|
+
function brClampY(y, h) { return Math.max(6, Math.min(y, window.innerHeight - h - 6)); }
|
|
3964
|
+
|
|
3965
|
+
function brApplyFloatPos() {
|
|
3966
|
+
var saved = brLoadPos(BR_FLOAT_KEY);
|
|
3967
|
+
var w = brEl.offsetWidth, h = brEl.offsetHeight;
|
|
3968
|
+
var x = saved ? saved.x : (window.innerWidth - w - 12);
|
|
3969
|
+
var y = saved ? saved.y : (window.innerHeight - h - 70);
|
|
3970
|
+
brEl.style.left = brClampX(x, w) + 'px';
|
|
3971
|
+
brEl.style.top = brClampY(y, h) + 'px';
|
|
3972
|
+
}
|
|
3973
|
+
function brApplyBallPos() {
|
|
3974
|
+
var saved = brLoadPos(BR_BALL_KEY);
|
|
3975
|
+
var x = saved ? saved.x : (window.innerWidth - 52);
|
|
3976
|
+
var y = saved ? saved.y : Math.round(window.innerHeight * 0.6);
|
|
3977
|
+
brBall.style.left = brClampX(x, 46) + 'px';
|
|
3978
|
+
brBall.style.top = brClampY(y, 46) + 'px';
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
function brSetMode(m) {
|
|
3982
|
+
if (!brStack.length) m = 'full';
|
|
3983
|
+
brMode = m;
|
|
3984
|
+
if (m === 'ball') {
|
|
3985
|
+
brEl.hidden = true;
|
|
3986
|
+
brBall.hidden = false;
|
|
3987
|
+
brApplyBallPos();
|
|
3988
|
+
return;
|
|
3989
|
+
}
|
|
3990
|
+
brBall.hidden = true;
|
|
3991
|
+
brEl.hidden = false;
|
|
3992
|
+
var isFloat = m === 'float';
|
|
3993
|
+
brEl.classList.toggle('is-float', isFloat);
|
|
3994
|
+
if (isFloat) { brApplyFloatPos(); brFitStage(); }
|
|
3995
|
+
else { brEl.style.left = ''; brEl.style.top = ''; brUnfitStage(); }
|
|
3996
|
+
}
|
|
3997
|
+
|
|
3998
|
+
/* 复位到全屏初始态:openBrowser(新开一站)与 closeBrowser(彻底关)都走这里。 */
|
|
3999
|
+
function brResetFloat() {
|
|
4000
|
+
brMode = 'full';
|
|
4001
|
+
brEl.classList.remove('is-float');
|
|
4002
|
+
brEl.style.left = ''; brEl.style.top = '';
|
|
4003
|
+
brBall.hidden = true;
|
|
4004
|
+
brUnfitStage();
|
|
4005
|
+
}
|
|
4006
|
+
|
|
4007
|
+
/* ── 悬浮态的等比缩放 ──
|
|
4008
|
+
舞台扛 **16:9**(顶栏菜单行不算进比例,见 .browser.is-float .browser__stage),
|
|
4009
|
+
只有手机屏宽的一小截到 90vw。页面若直接按舞台宽排版,等于塞进一个又窄
|
|
4010
|
+
又矮的窗口 —— 布局挤、字小、还容易被裁。所以反过来做(与 magic-test 的
|
|
4011
|
+
「虚拟桌面等比缩小铺满舞台」同一路子):
|
|
4012
|
+
1) iframe 用一个**固定的 16:9 虚拟视口**渲染页面:宽 BR_VW(480,手机竖屏
|
|
4013
|
+
常见 CSS 视口宽;<768 站点走移动版布局),高 = 宽×9/16;
|
|
4014
|
+
2) 再用 CSS transform: scale 把它整体等比缩到舞台宽度上(origin 0 0)。
|
|
4015
|
+
于是:缩放后高度 = 舞台高度(16:9 对齐,不露白不裁切)、比例不变形、
|
|
4016
|
+
界面上就是「同比例缩小显示」。
|
|
4017
|
+
系数:舞台 90vw ≈ 392 宽时 k ≈ 0.82,14px 字缩完 ≈ 11.5 CSS px。
|
|
4018
|
+
代价:卡片里一次只看得见虚拟视口这一段;页面自身仍可上下滚(滚的是
|
|
4019
|
+
iframe 内部,transform 下的触摸坐标由浏览器换算,能正常滚)。 */
|
|
4020
|
+
var BR_VW = 480;
|
|
4021
|
+
function brFitStage() {
|
|
4022
|
+
var stage = document.getElementById('brStage');
|
|
4023
|
+
if (!stage) return;
|
|
4024
|
+
var sw = stage.clientWidth;
|
|
4025
|
+
if (!sw) return;
|
|
4026
|
+
/* 虚拟视口固定 16:9(480×270):舞台也是 16:9,缩放后正好铺满,不多不少。
|
|
4027
|
+
高度若跟着舞台走会算错 —— 缩放后高度 ≠ 舞台高度,底部会露白/裁切。 */
|
|
4028
|
+
var vh = Math.round(BR_VW * 9 / 16);
|
|
4029
|
+
brFrame.style.width = BR_VW + 'px';
|
|
4030
|
+
brFrame.style.height = vh + 'px';
|
|
4031
|
+
var k = sw / BR_VW; // 等比缩放系数
|
|
4032
|
+
brFrame.style.transform = 'scale(' + k + ')';
|
|
4033
|
+
}
|
|
4034
|
+
function brUnfitStage() {
|
|
4035
|
+
brFrame.style.width = '';
|
|
4036
|
+
brFrame.style.height = '';
|
|
4037
|
+
brFrame.style.transform = '';
|
|
4038
|
+
}
|
|
4039
|
+
|
|
4040
|
+
/* 通用拖拽:handle 上按下 → 移动 target(left/top 内联),抬起时落盘。
|
|
4041
|
+
点按(位移 < 6px)回调 tap;canDrag() 为假时不启动(全屏态不可拖)。
|
|
4042
|
+
把手内的 button/input 不触发拖拽。 */
|
|
4043
|
+
function brMakeDraggable(handle, target, canDrag, onTap, onSave) {
|
|
4044
|
+
handle.addEventListener('pointerdown', function (e) {
|
|
4045
|
+
/* 把手自身(如悬浮球是个 button)不算「命中控件」,内部的才算 */
|
|
4046
|
+
if (e.target !== handle && e.target.closest('button,input,form')) return;
|
|
4047
|
+
if (brDragState || (canDrag && !canDrag())) return;
|
|
4048
|
+
brDragState = { dx: e.clientX - target.offsetLeft, dy: e.clientY - target.offsetTop,
|
|
4049
|
+
moved: false, pid: e.pointerId, target: target, onTap: onTap, onSave: onSave };
|
|
4050
|
+
try { handle.setPointerCapture(e.pointerId); } catch (err) {}
|
|
4051
|
+
e.preventDefault();
|
|
4052
|
+
});
|
|
4053
|
+
handle.addEventListener('pointermove', function (e) {
|
|
4054
|
+
var st = brDragState;
|
|
4055
|
+
if (!st || st.pid !== e.pointerId) return;
|
|
4056
|
+
var x = e.clientX - st.dx, y = e.clientY - st.dy;
|
|
4057
|
+
if (!st.moved && Math.abs(x - st.target.offsetLeft) < 5 && Math.abs(y - st.target.offsetTop) < 5) return;
|
|
4058
|
+
st.moved = true;
|
|
4059
|
+
st.target.style.left = Math.max(0, x) + 'px';
|
|
4060
|
+
st.target.style.top = Math.max(0, y) + 'px';
|
|
4061
|
+
});
|
|
4062
|
+
function finish(e) {
|
|
4063
|
+
var st = brDragState;
|
|
4064
|
+
if (!st || st.pid !== e.pointerId) return;
|
|
4065
|
+
brDragState = null;
|
|
4066
|
+
if (!st.moved) { if (st.onTap) st.onTap(); return; }
|
|
4067
|
+
st.onSave(st.target.offsetLeft, st.target.offsetTop);
|
|
4068
|
+
}
|
|
4069
|
+
handle.addEventListener('pointerup', finish);
|
|
4070
|
+
handle.addEventListener('pointercancel', finish);
|
|
4071
|
+
}
|
|
4072
|
+
|
|
4073
|
+
(function () {
|
|
4074
|
+
var top = brEl.querySelector('.browser__top');
|
|
4075
|
+
/* 悬浮卡片的拖拽把手 = 顶栏;全屏态 canDrag() 为假,完全不干预。 */
|
|
4076
|
+
brMakeDraggable(top, brEl, function () { return brMode === 'float'; }, null, function (x, y) {
|
|
4077
|
+
brSavePos(BR_FLOAT_KEY, { x: x, y: y });
|
|
4078
|
+
});
|
|
4079
|
+
/* 悬浮球:拖动换位置,点按恢复悬浮卡片,双击回全屏。 */
|
|
4080
|
+
brMakeDraggable(brBall, brBall, null, function () { brSetMode('float'); }, function (x, y) {
|
|
4081
|
+
brSavePos(BR_BALL_KEY, { x: x, y: y });
|
|
4082
|
+
});
|
|
4083
|
+
brBall.addEventListener('dblclick', function () { brSetMode('full'); });
|
|
4084
|
+
})();
|
|
4085
|
+
|
|
4086
|
+
document.getElementById('brFloat').addEventListener('click', function () {
|
|
4087
|
+
brSetMode(brMode === 'float' ? 'full' : 'float');
|
|
4088
|
+
});
|
|
4089
|
+
document.getElementById('brExpand').addEventListener('click', function () { brSetMode('full'); });
|
|
4090
|
+
document.getElementById('brMin').addEventListener('click', function () { brSetMode('ball'); });
|
|
4091
|
+
window.addEventListener('resize', function () {
|
|
4092
|
+
if (brMode === 'float') { brApplyFloatPos(); brFitStage(); }
|
|
4093
|
+
else if (brMode === 'ball') brApplyBallPos();
|
|
4094
|
+
});
|
|
2843
4095
|
function takeSeg(seg, key, apply) {
|
|
2844
4096
|
seg.addEventListener('click', function (e) {
|
|
2845
4097
|
var b = e.target && e.target.closest ? e.target.closest('.seg__b') : null;
|
|
@@ -2993,6 +4245,55 @@ button.setrow{font-family:inherit}
|
|
|
2993
4245
|
: '会话托管已关闭');
|
|
2994
4246
|
}
|
|
2995
4247
|
|
|
4248
|
+
/* 写当前会话的思考档位(session_id ↔ run_id 只留一条,与托管同一口径)。 */
|
|
4249
|
+
function writeReasoningFor(v) {
|
|
4250
|
+
var key = convSessionKey();
|
|
4251
|
+
if (key) {
|
|
4252
|
+
var s = sessionByAnyId(key);
|
|
4253
|
+
if (s) {
|
|
4254
|
+
[s.session_id, s.run_id].forEach(function (k) {
|
|
4255
|
+
if (k && String(k) !== key) delete REASONING.sessions[String(k)];
|
|
4256
|
+
});
|
|
4257
|
+
}
|
|
4258
|
+
REASONING.sessions[key] = v;
|
|
4259
|
+
} else {
|
|
4260
|
+
REASONING.fresh = v; // 新会话还没有 id:先记着,首帧带回 id 再落库
|
|
4261
|
+
}
|
|
4262
|
+
REASONING_CUR = v;
|
|
4263
|
+
saveReasoningPref();
|
|
4264
|
+
}
|
|
4265
|
+
function bindFreshReasoning(sid) {
|
|
4266
|
+
if (!REASONING.fresh || !sid) return;
|
|
4267
|
+
REASONING.sessions[String(sid)] = REASONING.fresh;
|
|
4268
|
+
REASONING.fresh = '';
|
|
4269
|
+
saveReasoningPref();
|
|
4270
|
+
}
|
|
4271
|
+
/* 引擎对推理档位的支持情况(以 /desk/engines 每行的 reasoning 字段为准):
|
|
4272
|
+
true / false / null(清单里还没有这个引擎 = 未知)。
|
|
4273
|
+
⚠️ 未知时**不下发**:老 CLI 不认识这个 flag,传了会 exit 2 打死整轮;
|
|
4274
|
+
不传时 core 自己的默认档就是 max,与「默认最高」一致。 */
|
|
4275
|
+
function reasoningSupportState(engine) {
|
|
4276
|
+
var n = String(engine || '');
|
|
4277
|
+
if (!n) return null;
|
|
4278
|
+
var lists = [(conv && conv.state && conv.state.engines) || [], ENGINES || []];
|
|
4279
|
+
for (var j = 0; j < lists.length; j++) {
|
|
4280
|
+
var list = lists[j];
|
|
4281
|
+
for (var i = 0; i < list.length; i++) {
|
|
4282
|
+
if (list[i] && String(list[i].engine) === n) return !!list[i].reasoning;
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
return null;
|
|
4286
|
+
}
|
|
4287
|
+
function reasoningOfEngine(engine) { return reasoningSupportState(engine) === true; }
|
|
4288
|
+
/* 把档位写进下发体:off = 真正关闭思考(noThinking,core 用各引擎官方开关),
|
|
4289
|
+
其余档走 reasoning;引擎不支持时两样都不带。 */
|
|
4290
|
+
function applyReasoningTo(payload, engine, level) {
|
|
4291
|
+
if (!payload || !reasoningOfEngine(engine)) return;
|
|
4292
|
+
var v = level || REASONING_DEFAULT;
|
|
4293
|
+
if (v === 'off') payload.noThinking = true;
|
|
4294
|
+
else payload.reasoning = v;
|
|
4295
|
+
}
|
|
4296
|
+
|
|
2996
4297
|
function mountConversation(sid, s) {
|
|
2997
4298
|
/* 同一条会话、同一个空间:组件还活着,画面也还在 —— 直接把这一屏端回来。
|
|
2998
4299
|
以前这里无条件 destroy() + create() + innerHTML='',于是「返回列表 → 再点进来」
|
|
@@ -3004,6 +4305,8 @@ button.setrow{font-family:inherit}
|
|
|
3004
4305
|
(2026-10-22 用户:「排队中的任务重新从会话列表进入就看不到了」)。 */
|
|
3005
4306
|
if (conv && sid && WS === convWs && sameConvId(sid, convSid)) {
|
|
3006
4307
|
convSid = sid; // 列表那边可能已经从 run_id 切到 session_id
|
|
4308
|
+
/* 同一屏也可能换了别名(run_id ↔ session_id):思考档位按会话记录重读一次。 */
|
|
4309
|
+
REASONING_CUR = reasoningOnFor(sid, s);
|
|
3007
4310
|
/* same:true —— 上面已经确认是同一屏会话。组件按字面判 id 会把别名当成
|
|
3008
4311
|
「换了会话」→ clearQueue() 删掉排队追问(本次报障的病根之一)。 */
|
|
3009
4312
|
try { conv.setSession(sid, s, { same: true }); } catch (e) {}
|
|
@@ -3020,14 +4323,20 @@ button.setrow{font-family:inherit}
|
|
|
3020
4323
|
老会话的开关由 teamOnFor / hostingOnFor 从它自己那份记录里取;新会话从「关」起。 */
|
|
3021
4324
|
TEAM.fresh = false;
|
|
3022
4325
|
HOSTING.fresh = false;
|
|
4326
|
+
REASONING.fresh = '';
|
|
4327
|
+
/* 这条会话生效的思考档位(没记过 = 最高)。 */
|
|
4328
|
+
REASONING_CUR = reasoningOnFor(sid, s);
|
|
3023
4329
|
try {
|
|
3024
4330
|
var transport = wrapTransportForJobs(window.MagicTransportHttp({ base: BASE, token: TOKEN, client: 'mobile' }));
|
|
3025
4331
|
conv = window.MagicConversation.create({
|
|
3026
4332
|
el: convEl, transport: transport, titleEl: titleEl, workspace: WS,
|
|
3027
4333
|
engine: RUN_PREF.engine, model: RUN_PREF.model,
|
|
4334
|
+
/* 主备:新会话立即带上(主引擎失败时 core 自动降级)。 */
|
|
4335
|
+
fallbackEngine: RUN_PREF.fbEngine, fallbackModel: RUN_PREF.fbModel,
|
|
3028
4336
|
/* 建议功能默认关闭:只有「我的 → 建议引擎」里显式选了引擎才传非空值;
|
|
3029
4337
|
模型为空 = 引擎默认(建议生成对 llm 用 no-think 变体作默认值)。 */
|
|
3030
4338
|
suggestEngine: SUGGEST.engine, suggestModel: SUGGEST.model,
|
|
4339
|
+
suggestFallbackEngine: SUGGEST.fbEngine, suggestFallbackModel: SUGGEST.fbModel,
|
|
3031
4340
|
showPermission: false, showAttach: true, beforeAsk: gateAsk,
|
|
3032
4341
|
onInterrupt: cancelWaitingAsk,
|
|
3033
4342
|
/* 组件内排队也要落库(2026-10-21 用户:「非团队的串行排队也没落库」):
|
|
@@ -3063,6 +4372,7 @@ button.setrow{font-family:inherit}
|
|
|
3063
4372
|
onEvent: function (ev) {
|
|
3064
4373
|
bindFreshTeam(ev && ev.session_id);
|
|
3065
4374
|
bindFreshHosting(ev && ev.session_id);
|
|
4375
|
+
bindFreshReasoning(ev && ev.session_id);
|
|
3066
4376
|
},
|
|
3067
4377
|
/* 菜单里的「定时任务」还在,但底部那个 tab 已经删了(2026-10-22 用户:
|
|
3068
4378
|
「定时任务tab先删除」)。现在点它给一句说明,而不是跳去一张只有一个占位符的页 ——
|
|
@@ -3128,7 +4438,7 @@ button.setrow{font-family:inherit}
|
|
|
3128
4438
|
而设置面板显示的空间名也跟着串。空 workspace = 默认,同样要写回(写成 '')。 */
|
|
3129
4439
|
WS = (s && s.workspace) || '';
|
|
3130
4440
|
saveWs();
|
|
3131
|
-
showSession(s && s.workspace ?
|
|
4441
|
+
showSession(s && s.workspace ? spaceLabel(s.workspace) : histSubtitle);
|
|
3132
4442
|
mountConversation(sid, s);
|
|
3133
4443
|
/* 灰白档 = 状态未知(或列表快照过期):进详情时补拉一次,见 refreshRowStatus。
|
|
3134
4444
|
其它档(运行中 / 等审批 / 已完成 / 失败 / 已取消)列表已经说清楚了,不必再打一发。 */
|
|
@@ -3227,16 +4537,82 @@ button.setrow{font-family:inherit}
|
|
|
3227
4537
|
if (!TOKEN) return; // 还没配对,拉也拉不到
|
|
3228
4538
|
var hdr = TOKEN ? { 'X-Magic-Token': TOKEN } : {};
|
|
3229
4539
|
var seq = CONN_SEQ;
|
|
3230
|
-
|
|
4540
|
+
/* 排队 / 运行状态变化也要能触发重绘:旧服务端的排队作业根本不在 /desk/sessions 里;
|
|
4541
|
+
新服务端虽然有登记行(RegisterQueued),但「排队 → 开跑」的占位替换也靠这里
|
|
4542
|
+
把本地那条「等待中」撤掉(见 syncServerJobs)。 */
|
|
4543
|
+
var pList = fetch(BASE + '/desk/sessions', { headers: hdr })
|
|
4544
|
+
.then(function (r) { return r.ok ? r.json() : null; });
|
|
4545
|
+
var pJobs = SERVER_JOBS ? fetch(BASE + '/desk/jobs?limit=200', { headers: hdr })
|
|
3231
4546
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
3232
|
-
.
|
|
4547
|
+
.catch(function () { return null; }) : Promise.resolve(null);
|
|
4548
|
+
Promise.all([pList, pJobs])
|
|
4549
|
+
.then(function (a) {
|
|
3233
4550
|
if (seq !== CONN_SEQ) return; // 响应回来时已经切到另一台桌面了
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
4551
|
+
var list = a[0];
|
|
4552
|
+
/* 上一帧还是失败态、这次拉通了 → 安静重拉一次把失败块换回列表。
|
|
4553
|
+
不做这个的话,失败态永远不会自己恢复:签名比对里 LAST_SESSIONS 还是旧值,
|
|
4554
|
+
「没变就不重绘」恰好把失败块钉在屏幕上(只能退出重进)。 */
|
|
4555
|
+
if (LIST_FAIL && Array.isArray(list)) { LIST_FAIL = false; loadSessions(true); return; }
|
|
4556
|
+
if (Array.isArray(list) && sessionSig(list) !== sessionSig(LAST_SESSIONS)) {
|
|
4557
|
+
loadSessions(true); // 变了才走完整刷新(含标题归纳 / 健康),且不许闪
|
|
4558
|
+
return;
|
|
4559
|
+
}
|
|
4560
|
+
if (serverJobsSig(a[1] && a[1].jobs) !== LAST_JOBS_SIG) loadSessions(true);
|
|
3237
4561
|
}).catch(function () {});
|
|
3238
4562
|
}, 5000);
|
|
3239
4563
|
}
|
|
4564
|
+
/* ── 列表加载失败态(2026-10-25)──
|
|
4565
|
+
以前是拼一行灰字「读取会话失败:Failed to fetch」,没有任何出路 —— 重试要退出重进,
|
|
4566
|
+
改地址要去标题菜单找铅笔。现在给一块完整的失败态:图标 + 人话原因 + 可复制地址 +
|
|
4567
|
+
重试 / 编辑环境按钮。 LIST_FAIL 同时是自动恢复的开关(见 startListPoll 的说明)。 */
|
|
4568
|
+
var LIST_FAIL = false;
|
|
4569
|
+
/* 失败原因的中文化:fetch 的网络类失败在 WebView 里就是一句英文 "Failed to fetch",
|
|
4570
|
+
直接抛给用户读不通。归中文化先例(client-ui/shared/conv/19-attachments.js attErrText):
|
|
4571
|
+
网络类归成一句人话,HTTP 状态码按常见原因给提示,其余保留原文(截断)。 */
|
|
4572
|
+
function netFailText(why) {
|
|
4573
|
+
var raw = String(why || '').trim();
|
|
4574
|
+
if (/failed to fetch|networkerror|load failed|network error|timed?\s?out|err_(connection|network)/i.test(raw))
|
|
4575
|
+
return '手机和桌面端可能不在同一个网络,或桌面端没在运行。';
|
|
4576
|
+
if (/HTTP\s*40[13]/i.test(raw))
|
|
4577
|
+
return '配对令牌不对或已失效,重新配对试试。';
|
|
4578
|
+
if (/HTTP\s*404/i.test(raw))
|
|
4579
|
+
return '桌面端服务不存在,版本可能太旧,去桌面端升级一下。';
|
|
4580
|
+
if (/HTTP\s*5\d\d/i.test(raw))
|
|
4581
|
+
return '桌面端服务出错了,看看桌面端的日志。';
|
|
4582
|
+
return raw ? shortReason(raw, 80) : '连接失败。';
|
|
4583
|
+
}
|
|
4584
|
+
function paintListFail(why, addr) {
|
|
4585
|
+
LIST_FAIL = true;
|
|
4586
|
+
var box = hEl('div', 'netfail');
|
|
4587
|
+
var ico = hEl('span', 'netfail__ico');
|
|
4588
|
+
ico.appendChild(svgUse('#i-plug', 26));
|
|
4589
|
+
box.appendChild(ico);
|
|
4590
|
+
box.appendChild(hEl('div', 'netfail__t', '连不上桌面端'));
|
|
4591
|
+
var whyEl = hEl('div', 'netfail__why', netFailText(why));
|
|
4592
|
+
/* 完整原文挂 title:长按/悬停能看到浏览器原始报错(与排队行 stalled 同一口径)。 */
|
|
4593
|
+
if (why) whyEl.title = String(why);
|
|
4594
|
+
box.appendChild(whyEl);
|
|
4595
|
+
/* 地址必须带上(失败要能定位到「打的是哪里」),点按即复制,方便去桌面端核对。 */
|
|
4596
|
+
if (addr) {
|
|
4597
|
+
var addrEl = hEl('button', 'netfail__addr', addr);
|
|
4598
|
+
addrEl.type = 'button';
|
|
4599
|
+
addrEl.title = '点按复制地址';
|
|
4600
|
+
addrEl.onclick = function () { copyText(addr, '地址已复制'); };
|
|
4601
|
+
box.appendChild(addrEl);
|
|
4602
|
+
}
|
|
4603
|
+
var acts = hEl('div', 'netfail__acts');
|
|
4604
|
+
var retry = hEl('button', 'btn btn--filled', '重试');
|
|
4605
|
+
retry.type = 'button';
|
|
4606
|
+
retry.onclick = function () { loadSessions(); };
|
|
4607
|
+
acts.appendChild(retry);
|
|
4608
|
+
var cur = activeConn();
|
|
4609
|
+
var edit = hEl('button', 'btn btn--outlined', '编辑环境');
|
|
4610
|
+
edit.type = 'button';
|
|
4611
|
+
edit.onclick = function () { openEnvEditor(cur); };
|
|
4612
|
+
acts.appendChild(edit);
|
|
4613
|
+
box.appendChild(acts);
|
|
4614
|
+
historyList.replaceChildren(box);
|
|
4615
|
+
}
|
|
3240
4616
|
function loadSessions(quiet) {
|
|
3241
4617
|
var hdr = TOKEN ? { 'X-Magic-Token': TOKEN } : {};
|
|
3242
4618
|
var seq = CONN_SEQ; // 切环境后旧请求可能才回来:序号对不上就不许改这一台的画面
|
|
@@ -3254,8 +4630,14 @@ button.setrow{font-family:inherit}
|
|
|
3254
4630
|
.then(function (r) { return r.ok ? r.json() : {}; }).catch(function () { return {}; });
|
|
3255
4631
|
var pDirs = fetch(BASE + '/desk/dirs', { headers: hdr })
|
|
3256
4632
|
.then(function (r) { return r.ok ? r.json() : {}; }).catch(function () { return {}; });
|
|
3257
|
-
|
|
4633
|
+
/* 服务端作业队列:新服务端**受理即登记**(排队行已经在 /desk/sessions 里),旧服务端
|
|
4634
|
+
还没登记 —— 这份快照是那种情况下的兜底(见 syncServerJobs,命中登记表的会跳过)。
|
|
4635
|
+
老服务端 404 → null,整段自然跳过。 */
|
|
4636
|
+
var pJobs = fetch(BASE + '/desk/jobs?limit=200', { headers: hdr })
|
|
4637
|
+
.then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; });
|
|
4638
|
+
Promise.all([pList, pTitles, pHealth, pDirs, pJobs]).then(function (a) {
|
|
3258
4639
|
if (seq !== CONN_SEQ) return;
|
|
4640
|
+
LIST_FAIL = false; // 拉通了:失败态到此为止(手动重试成功也要清,否则轮询会多做一次冗余重拉)
|
|
3259
4641
|
TITLES = a[1] || {};
|
|
3260
4642
|
DW = (a[2] && a[2].defaultWorkspace) || '';
|
|
3261
4643
|
DW_READY = true;
|
|
@@ -3290,6 +4672,11 @@ button.setrow{font-family:inherit}
|
|
|
3290
4672
|
});
|
|
3291
4673
|
paintTeamPref();
|
|
3292
4674
|
ROOT_DIRS = (((a[3] && a[3].dirs) || [])).filter(function (d) { return String(d.name).indexOf('magic-') === 0; });
|
|
4675
|
+
/* 服务端排队作业并进列表(见 syncServerJobs 的说明)。必须排在 renderSessions
|
|
4676
|
+
**之前**:它改的是 PENDING,用户看到的第一帧就该带着「等待中」那几行。
|
|
4677
|
+
会话清单一起传:判断某条作业「跑起来了没」要看登记表(见 jobLanded)。 */
|
|
4678
|
+
LAST_JOBS_SIG = serverJobsSig(a[4] && a[4].jobs);
|
|
4679
|
+
syncServerJobs(a[4] && a[4].jobs, Array.isArray(a[0]) ? a[0] : null);
|
|
3293
4680
|
renderSessions(Array.isArray(a[0]) ? a[0] : []);
|
|
3294
4681
|
/* 排队任务的接续要等 DW 到手(空间键靠它归一)—— 首次列表到手正是这个时刻。
|
|
3295
4682
|
幂等:QUEUE_RESTORED 保证只接一次。之前若因 DW 未就绪被跳过,这里补上。 */
|
|
@@ -3308,9 +4695,7 @@ button.setrow{font-family:inherit}
|
|
|
3308
4695
|
/* 失败原因要带上**实际打的地址**:Photo/WebView 里 "Failed to fetch" 只说「没连上」,
|
|
3309
4696
|
而真正的原因通常是「这台手机到那个 IP 不通」(换了网络 / 环境里存的是别的网段)。
|
|
3310
4697
|
不写地址的话,用户和应用都无从判断该改哪一边。 */
|
|
3311
|
-
|
|
3312
|
-
var addr = BASE || location.origin || '(同源)';
|
|
3313
|
-
historyList.innerHTML = '<div class="history__hint">读取会话失败:' + why + '<br>地址:' + addr + '</div>';
|
|
4698
|
+
paintListFail(e && e.message ? e.message : String(e), BASE || location.origin || '(同源)');
|
|
3314
4699
|
});
|
|
3315
4700
|
}
|
|
3316
4701
|
/* ── 会话列表:**置顶 / 任务 / 空间** 三段(对齐截图)──
|
|
@@ -3319,6 +4704,10 @@ button.setrow{font-family:inherit}
|
|
|
3319
4704
|
任务:最近会话(默认前 8,可「展开更多」);
|
|
3320
4705
|
空间:会话里出现过的 workspace(点行=按空间筛选任务,+=在该空间新建)。 */
|
|
3321
4706
|
var LAST_SESSIONS = [];
|
|
4707
|
+
/* 上一次看到的服务端作业队列签名(见 serverJobsSig / startListPoll):
|
|
4708
|
+
排队中的任务在旧服务端上不会进 /desk/sessions(新服务端受理即登记,见 core 的
|
|
4709
|
+
RegisterQueued),只比会话签名的话队列入队 / 出队都不会重绘。 */
|
|
4710
|
+
var LAST_JOBS_SIG = '';
|
|
3322
4711
|
var SEC_COLLAPSED = {};
|
|
3323
4712
|
var TASKS_EXPANDED = false;
|
|
3324
4713
|
var WS_OPEN = {};
|
|
@@ -3338,19 +4727,69 @@ button.setrow{font-family:inherit}
|
|
|
3338
4727
|
var SPACES_ROOT = ''; // 工作空间根(health.workspacesRoot)
|
|
3339
4728
|
var ROOT_DIRS = []; // 根目录下 magic-* 目录(GET /desk/dirs)
|
|
3340
4729
|
var CUSTOM_SPACES = [];
|
|
3341
|
-
try { CUSTOM_SPACES = JSON.parse(localStorage.getItem('magic.spaces') || '[]')
|
|
4730
|
+
try { CUSTOM_SPACES = normSpaces(JSON.parse(localStorage.getItem('magic.spaces') || '[]')); } catch (e) { CUSTOM_SPACES = []; }
|
|
4731
|
+
/* 自定义空间:每项 = {path, alias}。alias = 用户给这个目录起的**显示名**(可以为空)。
|
|
4732
|
+
⚠️ 显示口径统一走 spaceLabel():**有别名显示别名,没有就显示最后一级文件夹名,
|
|
4733
|
+
永远不显示整条路径** —— /Users/x/work/某某项目 这种一长串路径在手机上一行放不下,
|
|
4734
|
+
用户扫一眼也认不出「到底是哪个目录」(用户 2026-10-24:「否则显示最后1级的文件夹不要路径」)。
|
|
4735
|
+
⚠️ 老数据('magic.spaces' 里)是**纯路径字符串数组**:读的时候归一化成 {path, alias:''},
|
|
4736
|
+
写回去也是对象数组。服务端 /desk/prefs 只当不透明字符串存(不解析里面的形状),
|
|
4737
|
+
所以改形状不动契约(CLIENT-CONTRACT §移动端环境配置:字段由移动端定义)。 */
|
|
4738
|
+
function normSpaces(v) {
|
|
4739
|
+
var arr = Array.isArray(v) ? v : [];
|
|
4740
|
+
var out = [], seen = {};
|
|
4741
|
+
arr.forEach(function (it) {
|
|
4742
|
+
var p = (it && typeof it === 'object') ? String(it.path || '') : String(it || '');
|
|
4743
|
+
if (!p) return;
|
|
4744
|
+
p = reqNormDir(p); // 统一分隔符 + 去尾斜杠:与服务端登记的 workspace 同一口径,
|
|
4745
|
+
if (!p || seen[spaceKey(p)]) return; // 否则同一个空间会在列表里出现两行
|
|
4746
|
+
seen[spaceKey(p)] = 1;
|
|
4747
|
+
out.push({ path: p, alias: (it && typeof it === 'object') ? String(it.alias || '') : '' });
|
|
4748
|
+
});
|
|
4749
|
+
return out;
|
|
4750
|
+
}
|
|
3342
4751
|
function saveSpaces() { try { localStorage.setItem('magic.spaces', JSON.stringify(CUSTOM_SPACES)); } catch (e) {} schedulePushPrefs(); }
|
|
4752
|
+
/* 某个路径是不是自定义空间:是就返回那一项(带 alias),不是返回 null。
|
|
4753
|
+
⚠️ 比路径前先归一(分隔符 + 尾斜杠 + 盘符大小写):用户手输的「/work/proj/」
|
|
4754
|
+
与服务端登记的「\work\proj」是同一个空间,别名得认得出。 */
|
|
4755
|
+
function customSpaceOf(p) {
|
|
4756
|
+
var k = spaceKey(p);
|
|
4757
|
+
for (var i = 0; i < CUSTOM_SPACES.length; i++) { if (spaceKey(CUSTOM_SPACES[i].path) === k) return CUSTOM_SPACES[i]; }
|
|
4758
|
+
return null;
|
|
4759
|
+
}
|
|
4760
|
+
/* 空间显示名:别名 > 最后一级文件夹名(**不显示路径**)。 */
|
|
4761
|
+
function spaceLabel(p) {
|
|
4762
|
+
var it = customSpaceOf(p);
|
|
4763
|
+
if (it && it.alias) return it.alias;
|
|
4764
|
+
return basename(p) || String(p || '');
|
|
4765
|
+
}
|
|
3343
4766
|
/* 空间候选 = 根目录下的 magic-* + 自定义 + 会话里出现过的 workspace(去重、保序)。
|
|
4767
|
+
⚠️ 去重要按**比较口径**(spaceKey)而不是原字符串:服务端给的是 Windows 原样
|
|
4768
|
+
(D:\workspace\x),自定义空间是归一后的(D:/workspace/x)—— 同一个目录会
|
|
4769
|
+
以两种写法各出现一次,列表里多出一行重复项(用户会以为有两个空间)。
|
|
3344
4770
|
⚠️ 会话的 workspace 为空时**不要**拿 DW 顶上(原写法 `s.workspace || DW`):
|
|
3345
4771
|
那样「默认(不指定)」与「默认目录名」会变成两行同名选项,一个落「任务」段、
|
|
3346
4772
|
一个落「空间」段,用户点哪个都说不清(2026-10-21 报「无法选择默认」)。
|
|
3347
4773
|
没有 workspace 的会话本来就属于「默认」,不该在候选里再出现一次。 */
|
|
3348
4774
|
function collectSpaceKeys() {
|
|
3349
4775
|
var seen = {}, out = [];
|
|
3350
|
-
function add(p) {
|
|
4776
|
+
function add(p) {
|
|
4777
|
+
if (!p) return;
|
|
4778
|
+
var k = spaceKey(p);
|
|
4779
|
+
if (seen[k]) return;
|
|
4780
|
+
seen[k] = 1;
|
|
4781
|
+
out.push(p);
|
|
4782
|
+
}
|
|
3351
4783
|
(ROOT_DIRS || []).forEach(function (d) { add(d.path); });
|
|
3352
|
-
CUSTOM_SPACES.forEach(add);
|
|
3353
|
-
(LAST_SESSIONS || []).forEach(function (s) {
|
|
4784
|
+
CUSTOM_SPACES.forEach(function (s) { add(s.path); });
|
|
4785
|
+
(LAST_SESSIONS || []).forEach(function (s) {
|
|
4786
|
+
/* 会话带来的 workspace 要过 isRealSpaceDir:团队轮次目录(<空间>/.magic/team/run-x
|
|
4787
|
+
及其下的 a1/t1 子代理目录)是这一轮的临时工作区,跑完就没 —— 混进选择器后
|
|
4788
|
+
既不是自定义空间(没有 ✕ 可删)也打不开,用户以为「删不掉」
|
|
4789
|
+
(2026-10-24 报「两个 t1 删不掉」,口径与需求页 isRealSpaceDir 一致)。 */
|
|
4790
|
+
if (!isRealSpaceDir(s && s.workspace)) return;
|
|
4791
|
+
add(s.workspace);
|
|
4792
|
+
});
|
|
3354
4793
|
return out;
|
|
3355
4794
|
}
|
|
3356
4795
|
/* 会话里哪些是**顶层会话**(不是别人的子会话)。
|
|
@@ -3374,10 +4813,20 @@ button.setrow{font-family:inherit}
|
|
|
3374
4813
|
return !(pk && pk !== sidOf(s));
|
|
3375
4814
|
});
|
|
3376
4815
|
}
|
|
3377
|
-
/*
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
4816
|
+
/* 路径归一:**统一分隔符 + 去尾部斜杠**。
|
|
4817
|
+
⚠️ 必须统一 '\\' → '/':桌面可能是 Windows,路径是 D:\workspace\magic-agent,
|
|
4818
|
+
而用户手输的往往是 D:/workspace/magic-agent —— 两者是同一个目录,
|
|
4819
|
+
不归一就认不出是同一个空间(别名贴不上、勾打不上、需求页空间数对不上)。
|
|
4820
|
+
注意这只是**比较口径**,不改要发给 core 的原路径(core 自己会 Abs/Clean)。
|
|
4821
|
+
⚠️ 需求页(reqs.js)自带一份同名实现 —— 改动两处要同步,否则两页口径就分叉了。 */
|
|
4822
|
+
function reqNormDir(p) { return String(p || '').replace(/\\/g, '/').replace(/\/+$/, ''); }
|
|
4823
|
+
/* 空间键(比较用):在归一之上再修**盘符大小写**。
|
|
4824
|
+
Windows 上 d: 与 D: 是同一个盘(用户手输常常是小写),但路径其余部分大小写敏感
|
|
4825
|
+
—— Linux 上 Foo 与 foo 是两个目录,不能一起小写。 */
|
|
4826
|
+
function spaceKey(p) {
|
|
4827
|
+
var s = reqNormDir(p);
|
|
4828
|
+
return s.replace(/^([a-z]):/, function (m, d) { return d.toUpperCase() + ':'; });
|
|
4829
|
+
}
|
|
3381
4830
|
/* 一个目录算不算「空间」:得是**用户认得的工作区**。
|
|
3382
4831
|
根目录 / 不是空间(哪怕历史上有会话在那儿跑过)—— 需求页显示它只会让人
|
|
3383
4832
|
以为「/ 是个项目」(用户 2026-10-22:「没有空间的不需要」)。
|
|
@@ -3390,7 +4839,9 @@ button.setrow{font-family:inherit}
|
|
|
3390
4839
|
return true;
|
|
3391
4840
|
}
|
|
3392
4841
|
/* 团队模式的运行目录:<空间>/.magic/team/run-xxx[/子代理目录]。
|
|
3393
|
-
这些是团队**这一轮**的工作区,跑完就没了 —— 不是用户意义上的空间。
|
|
4842
|
+
这些是团队**这一轮**的工作区,跑完就没了 —— 不是用户意义上的空间。
|
|
4843
|
+
⚠️ 用归一后的路径来判:桌面可能是 Windows,团队运行目录是
|
|
4844
|
+
D:\work\x\.magic\team\run-1 —— 不统一分隔符就认不出,需求页会把它当真空间。 */
|
|
3394
4845
|
function isTeamRunDir(dir) {
|
|
3395
4846
|
var d = reqNormDir(dir);
|
|
3396
4847
|
return d.indexOf('/.magic/team/') >= 0;
|
|
@@ -3407,7 +4858,7 @@ button.setrow{font-family:inherit}
|
|
|
3407
4858
|
(它确实跑在那儿:core 用调用方 cwd)。 */
|
|
3408
4859
|
function projectOf(s) {
|
|
3409
4860
|
var w = (s && s.workspace) || DW || '';
|
|
3410
|
-
return w ?
|
|
4861
|
+
return w ? spaceLabel(w) : '';
|
|
3411
4862
|
}
|
|
3412
4863
|
/* 搜索口径:标题 / 工程名 / 引擎 / 模型 / 首问,任一处命中即可(大小写不敏感)。 */
|
|
3413
4864
|
function taskMatches(s) {
|
|
@@ -3478,6 +4929,38 @@ button.setrow{font-family:inherit}
|
|
|
3478
4929
|
}
|
|
3479
4930
|
/* 列表分段用:手动置顶 ∪ 运行中自动置顶。 */
|
|
3480
4931
|
function isListPinned(s) { return isPinned(s) || isAutoPinned(s); }
|
|
4932
|
+
/* ── 列表去重:排队条目该不该自己占一行 ──
|
|
4933
|
+
ROW_IDS = 这一次列表里已经画出来的会话 id(含 run_id / session_id 两种写法);
|
|
4934
|
+
LIST_ALIAS = 两种写法 → 规范 id(session_id 优先),与 renderSessions 里认父的
|
|
4935
|
+
KEY2SID、pendingOfConv 的 sameConvId 是**同一套别名口径**。
|
|
4936
|
+
⚠️ 必须按别名判(2026-10-24 用户:「临时置顶也有重复显示」):排队条目里的
|
|
4937
|
+
`p.session` 是**提交那一刻**手里的 id —— 那会儿引擎还没吐出 session_id,
|
|
4938
|
+
存进去的是 run_id;而会话行的 sidOf 早已换成 session_id。只比字面就把同一个
|
|
4939
|
+
会话认成两条:运行中的那条自动置顶进「置顶」段,排队那条又在「任务 / 空间」里
|
|
4940
|
+
画一行「追问排队中」—— 用户看到的就是「置顶里有一条,下面还重复一条」。 */
|
|
4941
|
+
var ROW_IDS = {}, LIST_ALIAS = {};
|
|
4942
|
+
function buildRowIds(rows) {
|
|
4943
|
+
ROW_IDS = {}; LIST_ALIAS = {};
|
|
4944
|
+
(rows || []).forEach(function (s) {
|
|
4945
|
+
var sid = sidOf(s);
|
|
4946
|
+
if (!sid) return;
|
|
4947
|
+
ROW_IDS[sid] = 1; LIST_ALIAS[sid] = sid;
|
|
4948
|
+
if (s.run_id) { ROW_IDS[String(s.run_id)] = 1; LIST_ALIAS[String(s.run_id)] = sid; }
|
|
4949
|
+
if (s.session_id) { ROW_IDS[String(s.session_id)] = 1; LIST_ALIAS[String(s.session_id)] = sid; }
|
|
4950
|
+
});
|
|
4951
|
+
}
|
|
4952
|
+
/* 排队条目归属哪条会话行(空 = 新任务,不属于任何已有会话)。 */
|
|
4953
|
+
function pendOwnerKey(p) {
|
|
4954
|
+
if (!p || !p.session) return '';
|
|
4955
|
+
var id = String(p.session);
|
|
4956
|
+
return String(LIST_ALIAS[id] || id);
|
|
4957
|
+
}
|
|
4958
|
+
/* true = 列表里已有它的会话行 → 不单独占一行,只在那行上挂「追问排队中」角标。 */
|
|
4959
|
+
function pendOwnedByRow(p) {
|
|
4960
|
+
var key = pendOwnerKey(p);
|
|
4961
|
+
if (key) return !!ROW_IDS[key];
|
|
4962
|
+
return !!(ROW_IDS[p.id] || (p.job && ROW_IDS[p.job]));
|
|
4963
|
+
}
|
|
3481
4964
|
/* 置顶段 / 搜索结果的排序:**人工置顶在前 → 运行中自动置顶 → 普通**,同类按 updated_at 倒序。
|
|
3482
4965
|
用户 2026-10-22 定稿:「应该在人工置顶的后面」—— 自动置顶只是临时把在跑的活提上来,
|
|
3483
4966
|
不能压过用户自己钉的会话;跑完 isAutoPinned 变 false,它回到普通分段。 */
|
|
@@ -3499,6 +4982,10 @@ button.setrow{font-family:inherit}
|
|
|
3499
4982
|
if (rowScrimEl) { rowScrimEl.remove(); rowScrimEl = null; }
|
|
3500
4983
|
}
|
|
3501
4984
|
function openRowMenu(anchorEl, s) {
|
|
4985
|
+
/* 排队行是**预登记**(core 的 RegisterQueued):改名 / 置顶都会落在临时 id 上,
|
|
4986
|
+
真开跑后 id 换成 core 的 run_id,这些操作全部失效;「删除」也只删登记占位、
|
|
4987
|
+
停不掉作业。与其给一组骗人的动作,不如明说它在排队。 */
|
|
4988
|
+
if (statusOf(s) === 'queued') { toast('排队中 · 轮到时自动开始'); return; }
|
|
3502
4989
|
closeRowMenu();
|
|
3503
4990
|
rowScrimEl = document.createElement('div'); rowScrimEl.className = 'rowmenu__scrim';
|
|
3504
4991
|
rowScrimEl.onclick = closeRowMenu;
|
|
@@ -3614,18 +5101,27 @@ button.setrow{font-family:inherit}
|
|
|
3614
5101
|
waiting_approval → wait 琥珀(活着的等待)
|
|
3615
5102
|
done / idle → done 绿(idle = 常驻会话本轮跑完、进程还在等追加,
|
|
3616
5103
|
与 PC 左栏的「已完成」同口径)
|
|
3617
|
-
failed
|
|
5104
|
+
failed → fail 红(引擎报错结束)
|
|
5105
|
+
gone → stop 石褐(记录写着在跑、进程已不在 —— 服务重启 / 终端
|
|
5106
|
+
关闭把它带走了,是**断连**,不是引擎故障)
|
|
3618
5107
|
cancelled / interrupted → stop 石褐(人为收工 / 被打断,不是故障)
|
|
3619
5108
|
其它(空 / 陌生值) → idle 灰白兜底
|
|
3620
5109
|
⚠️ 2026-10-23 之前 failed / cancelled / gone 全落进 idle,列表上是一片白 ——
|
|
3621
|
-
用户分不清「正常结束」和「挂了」(报障:「这两个会话为什么是白色」)。
|
|
5110
|
+
用户分不清「正常结束」和「挂了」(报障:「这两个会话为什么是白色」)。
|
|
5111
|
+
⚠️ 2026-10-24 用户:「会话全部断连 红色」—— gone 曾被并进红档,于是服务重启
|
|
5112
|
+
一次就把所有被带走的会话涂成故障色。gone 与 failed 必须分开:前者是中断,
|
|
5113
|
+
后者才是失败。 */
|
|
3622
5114
|
function statusOf(s) {
|
|
3623
5115
|
var st = String((s && (s.status || s.state)) || '');
|
|
3624
5116
|
if (st === 'running' || s.alive === true) return 'run';
|
|
3625
5117
|
if (st === 'waiting_approval') return 'wait';
|
|
5118
|
+
/* queued:已受理、还没开跑(服务端作业队列的预登记,见 core 的 RegisterQueued)。
|
|
5119
|
+
它不是「没发生过」的灰白行 —— 用户刚发出去、正在排队,要看得见(紫色等待样式,
|
|
5120
|
+
与列表里那条「等待中」同一个视觉语言)。 */
|
|
5121
|
+
if (st === 'queued') return 'queued';
|
|
3626
5122
|
if (st === 'done' || st === 'idle') return 'done';
|
|
3627
|
-
if (st === 'failed'
|
|
3628
|
-
if (st === 'cancelled' || st === 'interrupted' || st === 'stopped') return 'stop';
|
|
5123
|
+
if (st === 'failed') return 'fail';
|
|
5124
|
+
if (st === 'gone' || st === 'cancelled' || st === 'interrupted' || st === 'stopped') return 'stop';
|
|
3629
5125
|
return 'idle';
|
|
3630
5126
|
}
|
|
3631
5127
|
/* 会话行图标:团队会话=双人图形(mode:"team"),其余**设备形状按来源端**
|
|
@@ -3733,7 +5229,14 @@ button.setrow{font-family:inherit}
|
|
|
3733
5229
|
if (kids.length && kidsOpenOf(sid)) {
|
|
3734
5230
|
var box = document.createElement('div');
|
|
3735
5231
|
box.className = 'sblock__kids';
|
|
3736
|
-
|
|
5232
|
+
/* 每个子行单独包一层:折线的竖段 / 肘段画在 .sblock__kid 的伪元素上
|
|
5233
|
+
(末行只画到中点,收口成 └)。 */
|
|
5234
|
+
kids.forEach(function (k) {
|
|
5235
|
+
var branch = document.createElement('div');
|
|
5236
|
+
branch.className = 'sblock__kid';
|
|
5237
|
+
branch.appendChild(sessionRow(k, true));
|
|
5238
|
+
box.appendChild(branch);
|
|
5239
|
+
});
|
|
3737
5240
|
wrap.appendChild(box);
|
|
3738
5241
|
}
|
|
3739
5242
|
return wrap;
|
|
@@ -3743,14 +5246,16 @@ button.setrow{font-family:inherit}
|
|
|
3743
5246
|
b.type = 'button'; b.className = 'srow' + (sub ? ' srow--sub' : '');
|
|
3744
5247
|
b.setAttribute('data-sid', sidOf(s)); // ensureTitle 就地改标题时要按 id 找这一行
|
|
3745
5248
|
var run = statusOf(s) === 'run';
|
|
5249
|
+
var queued = statusOf(s) === 'queued';
|
|
3746
5250
|
b.appendChild(sessionIcon(s));
|
|
3747
5251
|
var col = elSpan('srow__col');
|
|
3748
5252
|
col.appendChild(elSpan('srow__t', titleOf(s)));
|
|
3749
5253
|
var sid = sidOf(s);
|
|
3750
|
-
/* 副标题 = 引擎 · 模型;运行中换成「运行中 · 第 N
|
|
5254
|
+
/* 副标题 = 引擎 · 模型;运行中换成「运行中 · 第 N 步」,排队中说明还在等。
|
|
3751
5255
|
进度只改这一个文本节点(RUN_ROWS.subEl 指向它)。 */
|
|
3752
5256
|
var subEl = elSpan('srow__s', run ? runningSub(s, RUN_STEPS[sid] || 0)
|
|
3753
|
-
|
|
5257
|
+
: queued ? '等待中 · 上一个完成后自动开始'
|
|
5258
|
+
: [s.engine || '', qaModelText(s.model, s.engine)].filter(Boolean).join(' · '));
|
|
3754
5259
|
col.appendChild(subEl);
|
|
3755
5260
|
/* 排队与团队标记都不在这里做:两者都做在图标里(见 sessionIcon),行尾不再挂角标。 */
|
|
3756
5261
|
b.appendChild(col);
|
|
@@ -3812,7 +5317,13 @@ button.setrow{font-family:inherit}
|
|
|
3812
5317
|
(2026-10-22 用户:「桌面这个没有兼容pc端 比如右键无法出选择」)。
|
|
3813
5318
|
顺手 preventDefault 掉系统菜单,免得和浮动菜单一起冒出来。 */
|
|
3814
5319
|
b.addEventListener('contextmenu', function (e) { e.preventDefault(); cancel(); openRowMenu(b, s); });
|
|
3815
|
-
b.onclick = function () {
|
|
5320
|
+
b.onclick = function () {
|
|
5321
|
+
if (lp) { lp = false; return; }
|
|
5322
|
+
/* 排队行还没有会话日志(真开跑才有)—— 点开只会是一张空白页。
|
|
5323
|
+
如实说一句,等它轮到了列表自己会换成真正的会话行。 */
|
|
5324
|
+
if (queued) { toast('排队中 · 轮到时自动开始'); return; }
|
|
5325
|
+
openSession(sidOf(s), s);
|
|
5326
|
+
};
|
|
3816
5327
|
return b;
|
|
3817
5328
|
}
|
|
3818
5329
|
/* 空间组:标题行 = 「📁 空间名 · 引擎 ⌄ +」,展开就把该空间的会话内联列在下面。 */
|
|
@@ -3825,7 +5336,7 @@ button.setrow{font-family:inherit}
|
|
|
3825
5336
|
var eng = '';
|
|
3826
5337
|
for (var i = 0; i < list.length; i++) { if (list[i] && list[i].engine) { eng = String(list[i].engine); break; } }
|
|
3827
5338
|
var waiting = list.filter(function (x) { return x && x.pendingWait; }).length;
|
|
3828
|
-
tog.appendChild(elSpan('wsrow__l',
|
|
5339
|
+
tog.appendChild(elSpan('wsrow__l', spaceLabel(key) + (eng ? ' · ' + eng : '')));
|
|
3829
5340
|
/* 折叠状态也要看得见「这个空间有任务在排队」—— 徽标不随展开/折叠消失。 */
|
|
3830
5341
|
if (waiting) tog.appendChild(elSpan('wsrow__badge', waiting + ' 等待中'));
|
|
3831
5342
|
/* 默认展开规则:组里有等待中的任务就展开(否则它藏在折叠组里 = 用户以为没显示);
|
|
@@ -3871,10 +5382,74 @@ button.setrow{font-family:inherit}
|
|
|
3871
5382
|
if (s.run_id) KEY2SID[s.run_id] = sid;
|
|
3872
5383
|
if (s.session_id) KEY2SID[s.session_id] = sid;
|
|
3873
5384
|
});
|
|
5385
|
+
/* ── 同一会话的多张记录并成一组 ──
|
|
5386
|
+
团队主会话**每轮都会换 session_id**(第 2 轮记录里的 session_id 可能是第 1 轮的
|
|
5387
|
+
run_id,第 3 轮又换成一个新 UUID),而子任务的两根父指针分别指向不同轮:
|
|
5388
|
+
`parent_run_id` = 这一轮的主 run,`parent_session_id` = 上一轮留下的会话 id。
|
|
5389
|
+
只按 sidOf 去重,同一会话的两轮就各画一行 —— 运行中的那条自动进「置顶」,
|
|
5390
|
+
旧的那条(done)落「任务」,看着就是「临时置顶的会话显示两个」
|
|
5391
|
+
(2026-10-24 用户报障)。这里用子任务的**两根父指针**把两轮并成一组。 */
|
|
5392
|
+
var UF = {};
|
|
5393
|
+
function ufFind(x) {
|
|
5394
|
+
if (!x) return '';
|
|
5395
|
+
if (UF[x] === undefined) { UF[x] = x; return x; }
|
|
5396
|
+
var p = x;
|
|
5397
|
+
while (UF[p] !== p) p = UF[p];
|
|
5398
|
+
while (UF[x] !== p) { var n = UF[x]; UF[x] = p; x = n; }
|
|
5399
|
+
return p;
|
|
5400
|
+
}
|
|
5401
|
+
function ufUnion(a, b) {
|
|
5402
|
+
var ra = ufFind(a), rb = ufFind(b);
|
|
5403
|
+
if (ra && rb && ra !== rb) UF[rb] = ra;
|
|
5404
|
+
}
|
|
5405
|
+
rows.forEach(function (s) { ufFind(sidOf(s)); });
|
|
5406
|
+
rows.forEach(function (s) {
|
|
5407
|
+
var a = KEY2SID[s.parent_session_id || ''] || '';
|
|
5408
|
+
var b = KEY2SID[s.parent_run_id || ''] || '';
|
|
5409
|
+
if (a && b && a !== b) ufUnion(a, b);
|
|
5410
|
+
});
|
|
5411
|
+
/* 每组只显示**一条**:人工置顶优先 → 活着(运行中 / 等审批)→ updated_at 新。
|
|
5412
|
+
⚠️ 人工置顶排第一是为了「钉的是哪一轮就显示哪一轮」—— 否则折叠后用户会以为置顶掉了。 */
|
|
5413
|
+
function rowRank(s) {
|
|
5414
|
+
var rank = isPinned(s) ? 4 : 0;
|
|
5415
|
+
var st = statusOf(s);
|
|
5416
|
+
if (st === 'run') rank += 2; else if (st === 'wait') rank += 1;
|
|
5417
|
+
return rank;
|
|
5418
|
+
}
|
|
5419
|
+
var groupBest = {};
|
|
5420
|
+
rows.forEach(function (s) {
|
|
5421
|
+
var g = ufFind(sidOf(s));
|
|
5422
|
+
var cur = groupBest[g];
|
|
5423
|
+
if (!cur) { groupBest[g] = s; return; }
|
|
5424
|
+
var rs = rowRank(s), rc = rowRank(cur);
|
|
5425
|
+
if (rs > rc) { groupBest[g] = s; return; }
|
|
5426
|
+
if (rs === rc && String(s.updated_at || s.started_at || '')
|
|
5427
|
+
.localeCompare(String(cur.updated_at || cur.started_at || '')) > 0) groupBest[g] = s;
|
|
5428
|
+
});
|
|
5429
|
+
var GROUP_KEY = {}; // 组根 → 显示那条的 sid
|
|
5430
|
+
Object.keys(groupBest).forEach(function (g) { GROUP_KEY[g] = sidOf(groupBest[g]); });
|
|
5431
|
+
function groupKeyOfId(id) {
|
|
5432
|
+
var sid = KEY2SID[id || ''] || '';
|
|
5433
|
+
if (!sid) return '';
|
|
5434
|
+
return GROUP_KEY[ufFind(sid)] || '';
|
|
5435
|
+
}
|
|
5436
|
+
/* 所有 id(各轮的 run_id / session_id)→ 组里显示那条的 sid:认父、「排队追问」都按它。
|
|
5437
|
+
子会话自己的 id 不进来 —— 它只在父行展开时出现,排队追问仍要能单独看见。 */
|
|
5438
|
+
var GROUP_ALIAS = {};
|
|
5439
|
+
rows.forEach(function (s) {
|
|
5440
|
+
var sid = sidOf(s);
|
|
5441
|
+
var own = GROUP_KEY[ufFind(sid)] || sid;
|
|
5442
|
+
var pk = groupKeyOfId(parentKeyOf(s));
|
|
5443
|
+
if (pk && pk !== own) return; // 子会话
|
|
5444
|
+
GROUP_ALIAS[sid] = own;
|
|
5445
|
+
if (s.run_id) GROUP_ALIAS[s.run_id] = own;
|
|
5446
|
+
if (s.session_id) GROUP_ALIAS[s.session_id] = own;
|
|
5447
|
+
});
|
|
3874
5448
|
CHILD_OF = {}; CHILD_IDS = {};
|
|
3875
5449
|
var top = [];
|
|
3876
5450
|
rows.forEach(function (s) {
|
|
3877
|
-
|
|
5451
|
+
if (groupBest[ufFind(sidOf(s))] !== s) return; // 同组的其他轮不单独显示
|
|
5452
|
+
var pk = GROUP_ALIAS[parentKeyOf(s)] || '';
|
|
3878
5453
|
if (pk && pk !== sidOf(s)) {
|
|
3879
5454
|
(CHILD_OF[pk] = CHILD_OF[pk] || []).push(s);
|
|
3880
5455
|
CHILD_IDS[sidOf(s)] = 1;
|
|
@@ -3894,13 +5469,24 @@ button.setrow{font-family:inherit}
|
|
|
3894
5469
|
宁可多一行也不能让排队的任务从界面上消失(那正是上次报障「排队任务丢了」)。
|
|
3895
5470
|
⚠️ 这段必须排在**渲染置顶之前**:置顶行也是 sessionRow,算晚了角标就丢了。 */
|
|
3896
5471
|
var PEND_VIS = PENDING.filter(function (p) { return !p.hide; });
|
|
3897
|
-
var rowIds = {};
|
|
3898
5472
|
FUP = {}; // 模块级:sessionRow 要用(见其上方注释)
|
|
3899
|
-
|
|
5473
|
+
/* 「这一行在不在列表里」按别名表判(见 buildRowIds / pendOwnedByRow 的说明)——
|
|
5474
|
+
运行中自动置顶的那条与它的排队追问必须落在**同一行**上。 */
|
|
5475
|
+
buildRowIds(rows);
|
|
5476
|
+
/* 折叠掉的旧轮 / 各轮别名也要算「这一行已经在列表里」:排队条目手里可能是旧轮的 id。 */
|
|
5477
|
+
Object.keys(GROUP_ALIAS).forEach(function (id) {
|
|
5478
|
+
if (!id || !GROUP_ALIAS[id]) return;
|
|
5479
|
+
ROW_IDS[id] = 1;
|
|
5480
|
+
LIST_ALIAS[id] = GROUP_ALIAS[id];
|
|
5481
|
+
});
|
|
3900
5482
|
PEND_VIS.forEach(function (p) {
|
|
3901
|
-
|
|
5483
|
+
var key = pendOwnerKey(p);
|
|
5484
|
+
if (key && ROW_IDS[key]) (FUP[key] = FUP[key] || []).push(p);
|
|
3902
5485
|
});
|
|
3903
|
-
|
|
5486
|
+
/* 服务端已先登记的排队作业(core 的 RegisterQueued,run_id = request_id / job id):
|
|
5487
|
+
会话行已经把这条画出来了(state=queued),本地这条就不要再占一行 ——
|
|
5488
|
+
两行会让用户以为发重了(与「追问排队不占一行」同一个口径)。 */
|
|
5489
|
+
var PEND_ROWS = PEND_VIS.filter(function (p) { return !pendOwnedByRow(p); });
|
|
3904
5490
|
/* 一次性构建到 DocumentFragment,最后 replaceChildren 单次替换 —— 无「先清空」的中间空态,
|
|
3905
5491
|
所以删除、折叠、展开等重绘都不闪。 */
|
|
3906
5492
|
var frag = document.createDocumentFragment();
|
|
@@ -3971,11 +5557,17 @@ button.setrow{font-family:inherit}
|
|
|
3971
5557
|
}
|
|
3972
5558
|
}
|
|
3973
5559
|
|
|
3974
|
-
/* 空间:按 workspace
|
|
5560
|
+
/* 空间:按 workspace 分组(且未置顶的会话)。
|
|
5561
|
+
⚠️ 分组键用**归一后的空间键**(spaceKey):服务端登记的 workspace 是
|
|
5562
|
+
D:\workspace\x,而用户选的自定义空间是归一后的 D:/workspace/x ——
|
|
5563
|
+
按原字符串分组,同一个目录会裂成两个空间组,组头显示的还是同一个
|
|
5564
|
+
文件夹名(用户看到两个同名空间,以为有两个项目)。
|
|
5565
|
+
下发给 core 的路径另有 p.w(**原样**,见 addPending 的说明),不受影响。 */
|
|
3975
5566
|
var bySpace = {};
|
|
3976
5567
|
rest.forEach(function (s) {
|
|
3977
5568
|
var w = s.workspace; if (!w) return;
|
|
3978
|
-
|
|
5569
|
+
var k = spaceKey(w);
|
|
5570
|
+
(bySpace[k] = bySpace[k] || []).push(s);
|
|
3979
5571
|
});
|
|
3980
5572
|
/* 等待中的新任务:显式空间 → 放进该空间组**最上面**(状态最需要被看见)。
|
|
3981
5573
|
不能只靠上面的 rest 建 key:某空间只有排队任务、还没有任何会话时,bySpace 里
|
|
@@ -3983,7 +5575,8 @@ button.setrow{font-family:inherit}
|
|
|
3983
5575
|
用 PEND_ROWS 而不是 PEND_VIS:并进原会话行的追问不该再在空间组里插一行。 */
|
|
3984
5576
|
PEND_ROWS.forEach(function (p) {
|
|
3985
5577
|
if (!p.explicit || !p.ws) return;
|
|
3986
|
-
|
|
5578
|
+
var k = spaceKey(p.ws);
|
|
5579
|
+
(bySpace[k] = bySpace[k] || []).unshift(p);
|
|
3987
5580
|
});
|
|
3988
5581
|
/* 空间排序键:组内最新一条会话的 updated_at;组里只有排队任务时用它的 at。
|
|
3989
5582
|
混入排队行后不能再用 bySpace[key][0].updated_at(第一条可能是排队行,没有该字段)。 */
|
|
@@ -4019,7 +5612,11 @@ button.setrow{font-family:inherit}
|
|
|
4019
5612
|
document.removeEventListener('keydown', onHostKey, true);
|
|
4020
5613
|
}
|
|
4021
5614
|
function hEl(tag, cls, text) { var n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; }
|
|
4022
|
-
|
|
5615
|
+
/* 取最后一级文件名/目录名。
|
|
5616
|
+
⚠️ 必须同时按 '/' 与 '\\' 切:桌面可能是 Windows,路径是 D:\workspace\magic-agent,
|
|
5617
|
+
只按 '/' 切会切不动 —— 整条路径被当成名字显示,「只显示最后一级」这条要求等于没做
|
|
5618
|
+
(用户 2026-10-24:「否则显示最后1级的文件夹不要路径」)。 */
|
|
5619
|
+
function basename(p) { var s = String(p || '').replace(/\\/g, '/').split('/').filter(Boolean); return s.pop() || String(p); }
|
|
4023
5620
|
|
|
4024
5621
|
/* ══════════ 产物阅读(产物卡「查看」)══════════
|
|
4025
5622
|
PC 宿主自己绑了点击 → 右侧标签栏;移动端此前没人接这一下,所以「查看」点了没反应。
|
|
@@ -4103,8 +5700,18 @@ button.setrow{font-family:inherit}
|
|
|
4103
5700
|
}
|
|
4104
5701
|
var text = String(r.text == null ? '' : r.text);
|
|
4105
5702
|
if (/\.(?:md|markdown)$/i.test(abs) && typeof mdToHtmlOnly === 'function') {
|
|
5703
|
+
/* 需求文档里的「页面完成情况」截图写的是**本机绝对路径**
|
|
5704
|
+
(core 落盘在 ~/.magic-agent/requirements/… 下)。
|
|
5705
|
+
⚠️ 直接进 markdown 渲染会被 DOMPurify 的协议白名单**摘掉 src**
|
|
5706
|
+
(默认只放行 http/https/mailto/tel/cid/xmpp)—— 界面上只剩一枚破图。
|
|
5707
|
+
所以先改写成 <img data-reqshot="绝对路径">(data-* 消毒放行),
|
|
5708
|
+
渲染后再把 src 补成回读地址。 */
|
|
5709
|
+
var mdText = text;
|
|
5710
|
+
if (window.MagicReqShot && window.MagicReqShot.rewriteShotImages) {
|
|
5711
|
+
try { mdText = window.MagicReqShot.rewriteShotImages(text); } catch (e) {}
|
|
5712
|
+
}
|
|
4106
5713
|
var box = hEl('div', 'md-body');
|
|
4107
|
-
try { box.innerHTML = mdToHtmlOnly(
|
|
5714
|
+
try { box.innerHTML = mdToHtmlOnly(mdText); } catch (e) { box.textContent = text; }
|
|
4108
5715
|
rdBody.appendChild(box);
|
|
4109
5716
|
if (typeof highlightIn === 'function') { try { highlightIn(box); } catch (e) {} }
|
|
4110
5717
|
/* 文档里的本机图片:换成回读地址(移动端 fileUrl 自带 w=480,慢网也出得来)。 */
|
|
@@ -4115,6 +5722,13 @@ button.setrow{font-family:inherit}
|
|
|
4115
5722
|
var u = window.desk.mediaUrl(src.replace(/^file:\/\//, ''));
|
|
4116
5723
|
if (u) im.src = u;
|
|
4117
5724
|
});
|
|
5725
|
+
/* 需求截图:src 在 data-reqshot 里(见上面的注释)。 */
|
|
5726
|
+
Array.prototype.forEach.call(box.querySelectorAll('img[data-reqshot]'), function (im) {
|
|
5727
|
+
var p = im.getAttribute('data-reqshot') || '';
|
|
5728
|
+
if (!p) return;
|
|
5729
|
+
var u = window.desk.mediaUrl(p);
|
|
5730
|
+
if (u) im.src = u;
|
|
5731
|
+
});
|
|
4118
5732
|
}
|
|
4119
5733
|
} else {
|
|
4120
5734
|
var pre = document.createElement('pre');
|
|
@@ -4172,8 +5786,12 @@ button.setrow{font-family:inherit}
|
|
|
4172
5786
|
build(body);
|
|
4173
5787
|
}
|
|
4174
5788
|
/* selected:当前生效项打勾。**必须画出来**——底部抽屉关掉再开只需要一眼,不需要
|
|
4175
|
-
用户记住上次点了哪一行;没有勾时「点了没反应」和「点了已生效」长得一模一样。
|
|
4176
|
-
|
|
5789
|
+
用户记住上次点了哪一行;没有勾时「点了没反应」和「点了已生效」长得一模一样。
|
|
5790
|
+
action:可选的**行内小动作**(自定义空间的「✎ 改名」/「✕ 删除」)。传数组 = 多个动作并列。
|
|
5791
|
+
⚠️ 行本身是 <button>,里面**不能再塞 <button>**(HTML 不允许嵌套按钮,浏览器解析时
|
|
5792
|
+
会把外层按钮提前闭合,小动作就没了)—— 所以动作渲染成 <span role="button">,
|
|
5793
|
+
点它时 stopPropagation,免得顺手把整行也点了(整行 = 选中这个空间)。 */
|
|
5794
|
+
function hRow(body, label, value, onClick, danger, selected, action) {
|
|
4177
5795
|
var b = hEl('button', 'pick__row'); b.type = 'button';
|
|
4178
5796
|
if (selected) { b.setAttribute('aria-selected', 'true'); b.appendChild(hEl('span', 'pick__ck', '✓')); }
|
|
4179
5797
|
var l = hEl('span', 'pick__l', label); l.style.flex = '1'; l.style.minWidth = '0';
|
|
@@ -4183,15 +5801,30 @@ button.setrow{font-family:inherit}
|
|
|
4183
5801
|
if (danger) v.style.color = 'var(--error-text)';
|
|
4184
5802
|
b.appendChild(v);
|
|
4185
5803
|
}
|
|
5804
|
+
(Array.isArray(action) ? action : (action ? [action] : [])).forEach(function (act) {
|
|
5805
|
+
if (!act) return;
|
|
5806
|
+
var a = hEl('span', 'pick__act', act.label);
|
|
5807
|
+
a.setAttribute('role', 'button');
|
|
5808
|
+
a.setAttribute('aria-label', act.title || act.label);
|
|
5809
|
+
a.onclick = function (e) {
|
|
5810
|
+
if (e && e.stopPropagation) e.stopPropagation();
|
|
5811
|
+
if (e && e.preventDefault) e.preventDefault();
|
|
5812
|
+
act.onClick();
|
|
5813
|
+
};
|
|
5814
|
+
b.appendChild(a);
|
|
5815
|
+
});
|
|
4186
5816
|
if (onClick) { b.appendChild(hEl('span', 'pick__chev', '›')); b.onclick = onClick; }
|
|
4187
5817
|
else { b.style.cursor = 'default'; }
|
|
4188
5818
|
body.appendChild(b);
|
|
4189
5819
|
}
|
|
4190
|
-
/* 单行文本输入抽屉(重命名 /
|
|
5820
|
+
/* 单行文本输入抽屉(重命名 / 自定义路径 / 空间别名共用)。
|
|
4191
5821
|
⚠️ **不用 window.prompt**:悬浮球(Electron 小窗)与 Android WebView 都没实现它,
|
|
4192
5822
|
调用只会静默返回 null —— 表现就是「点击无效」且抽屉原样不动(2026-10-23 报障)。
|
|
4193
5823
|
这里换成抽屉里的真输入框,样式沿用启动页那套 .field / .btn;空值给红字提示,
|
|
4194
|
-
不再静默 return(否则和上面的 bug 长得一模一样)。
|
|
5824
|
+
不再静默 return(否则和上面的 bug 长得一模一样)。
|
|
5825
|
+
opt.allowEmpty:允许空值(**别名留空 = 不用别名**,不是错误)。
|
|
5826
|
+
opt.secondary:第二个输入框(自定义空间的「别名」)。有它时 onOk 收到两个值
|
|
5827
|
+
(主值, 次值);没有时 onOk 仍只收到一个值 —— 老调用方不受影响。 */
|
|
4195
5828
|
function openTextInputSheet(opt, onOk) {
|
|
4196
5829
|
openHostSheet(opt.title || '', function (body) {
|
|
4197
5830
|
var f = hEl('div', 'field');
|
|
@@ -4206,13 +5839,30 @@ button.setrow{font-family:inherit}
|
|
|
4206
5839
|
body.appendChild(f);
|
|
4207
5840
|
var err = hEl('div', 'field__err'); err.hidden = true;
|
|
4208
5841
|
body.appendChild(err);
|
|
5842
|
+
var inp2 = null;
|
|
5843
|
+
if (opt.secondary) {
|
|
5844
|
+
var f2 = hEl('div', 'field');
|
|
5845
|
+
var lab2 = document.createElement('label');
|
|
5846
|
+
lab2.textContent = opt.secondary.label || '';
|
|
5847
|
+
f2.appendChild(lab2);
|
|
5848
|
+
inp2 = document.createElement('input');
|
|
5849
|
+
inp2.type = 'text';
|
|
5850
|
+
inp2.value = opt.secondary.value || '';
|
|
5851
|
+
if (opt.secondary.placeholder) inp2.placeholder = opt.secondary.placeholder;
|
|
5852
|
+
inp2.setAttribute('autocapitalize', 'off');
|
|
5853
|
+
inp2.setAttribute('autocorrect', 'off');
|
|
5854
|
+
inp2.setAttribute('spellcheck', 'false');
|
|
5855
|
+
f2.appendChild(inp2);
|
|
5856
|
+
body.appendChild(f2);
|
|
5857
|
+
}
|
|
4209
5858
|
var ok = hEl('button', 'btn btn--filled', '确定'); ok.type = 'button';
|
|
4210
5859
|
body.appendChild(ok);
|
|
4211
5860
|
function commit() {
|
|
4212
5861
|
var v = String(inp.value || '').trim();
|
|
4213
|
-
if (!v) { err.textContent = opt.emptyHint || '不能为空'; err.hidden = false; inp.focus(); return; }
|
|
5862
|
+
if (!v && !opt.allowEmpty) { err.textContent = opt.emptyHint || '不能为空'; err.hidden = false; inp.focus(); return; }
|
|
5863
|
+
var v2 = inp2 ? String(inp2.value || '').trim() : '';
|
|
4214
5864
|
closeHostSheet();
|
|
4215
|
-
if (onOk) onOk(v);
|
|
5865
|
+
if (onOk) onOk(v, v2);
|
|
4216
5866
|
}
|
|
4217
5867
|
ok.onclick = commit;
|
|
4218
5868
|
inp.onkeydown = function (e) { if (e.key === 'Enter') { e.preventDefault(); commit(); } };
|
|
@@ -4226,7 +5876,7 @@ button.setrow{font-family:inherit}
|
|
|
4226
5876
|
仍然在说「你这个默认就是那个目录」。而 client 进程的 cwd 本身就在某个 magic-* 里,
|
|
4227
5877
|
那个目录又同时是空间列表的一项 —— 文案一带上名字,用户立刻读成「我把那个空间
|
|
4228
5878
|
选成了默认」。这里是一条**归属**声明,不是路径提示:默认 = 不属于任何空间。 */
|
|
4229
|
-
function wsLabel() { return WS ? (
|
|
5879
|
+
function wsLabel() { return WS ? (spaceLabel(WS) || WS) : '默认(不属于任何空间)'; }
|
|
4230
5880
|
/* 运行设置页的四个值:设备只读;工作空间来自 WS;权限 / 团队读当前会话组件。
|
|
4231
5881
|
组件没挂载时给「沿用默认 / 关闭」,不编造。 */
|
|
4232
5882
|
function paintRunSettings() {
|
|
@@ -4239,6 +5889,9 @@ button.setrow{font-family:inherit}
|
|
|
4239
5889
|
/* 全权限在本页也用红色行(与抽屉里 hRow 的 danger 同一口径)。 */
|
|
4240
5890
|
var permBtn = document.getElementById('btnRsPermission');
|
|
4241
5891
|
if (permBtn) permBtn.classList.toggle('setrow--danger', !!(conv && conv.permissionTier && conv.permissionTier() === 'full'));
|
|
5892
|
+
/* 思考模式(2026-10-24):按当前这条会话的档位显示,默认最高。 */
|
|
5893
|
+
var r = document.getElementById('rsReasoning');
|
|
5894
|
+
if (r) r.textContent = reasoningLabel(REASONING_CUR) + (REASONING_CUR === REASONING_DEFAULT ? '(默认)' : '');
|
|
4242
5895
|
var t = document.getElementById('rsTeam');
|
|
4243
5896
|
if (t) t.textContent = (conv && conv.teamLabel) ? conv.teamLabel() : '关闭';
|
|
4244
5897
|
/* 会话托管(2026-10-23,按会话):与团队模式并列在同一张卡里。
|
|
@@ -4287,8 +5940,13 @@ button.setrow{font-family:inherit}
|
|
|
4287
5940
|
|
|
4288
5941
|
/* 空间的归一键:**空 workspace 与桌面默认空间是同一个空间**(core 不传 -w 时用 cwd,
|
|
4289
5942
|
登记表里那条记录的 workspace 就是空 —— 两边必须归一到同一个键,否则「默认空间里
|
|
4290
|
-
已经有一条在跑」会被判成另一空间而照样并行)。
|
|
4291
|
-
|
|
5943
|
+
已经有一条在跑」会被判成另一空间而照样并行)。
|
|
5944
|
+
⚠️ 这个键是**纯客户端的比较键**(分组 / 串行队列 / 判忙),下发给 core 的路径另有
|
|
5945
|
+
p.w(原样,见 addPending 的说明)—— 所以在这里统一归一是安全的。
|
|
5946
|
+
⚠️ 归一(含分隔符与盘符大小写)是必需的:会话里的 workspace 是服务端登记的
|
|
5947
|
+
D:\workspace\x,用户选的自定义空间是 D:/workspace/x —— 字面不等,串行门就
|
|
5948
|
+
认不出「这个空间已经在跑了」,于是同一个空间并行跑了两轮。 */
|
|
5949
|
+
function wsKey(w) { return spaceKey(w || DW); }
|
|
4292
5950
|
function wsOf(s) { return wsKey(s && s.workspace); }
|
|
4293
5951
|
/* 「这个空间有任务在跑」的判据。waiting_approval **不算**(沿用旧口径):
|
|
4294
5952
|
它卡在等审批 / 等回答上,用户可能一直不理,算进去就等于把整个空间永久锁死;
|
|
@@ -4299,6 +5957,146 @@ button.setrow{font-family:inherit}
|
|
|
4299
5957
|
function serialQ(ws) {
|
|
4300
5958
|
return SERIAL[ws] || (SERIAL[ws] = { tail: Promise.resolve(), held: 0, waiters: 0 });
|
|
4301
5959
|
}
|
|
5960
|
+
/* ── 服务端排队作业 → 列表里的「等待中」行(2026-10-24)──
|
|
5961
|
+
用户报障:「串行模式下新发出的多个会话列表中看不见」。
|
|
5962
|
+
根因:串行由**服务端**裁决(见 SERVER_JOBS),排队中的作业**还没开跑**,
|
|
5963
|
+
core 也就还没写会话登记表 —— 它既不在 /desk/sessions 里,也不一定还留在这端的
|
|
5964
|
+
本地 PENDING 里(App 重启后 restoreQueue 把它交给服务端就删了本地行)。
|
|
5965
|
+
于是「发出去的几条里只有正在跑的那条看得见」。
|
|
5966
|
+
现在把 GET /desk/jobs 的 queued 作业并进 PENDING 渲染:谁排在队里、排第几,
|
|
5967
|
+
在列表上都能看见(换设备 / 重开 App / 别的客户端发的都一样)。
|
|
5968
|
+
⚠️ 与本地行的关系按 request_id 认:同一次提交**只画一行** —— 本地那条带附件、
|
|
5969
|
+
团队配置和取消入口,比服务端作业更完整,只把它的 hide 翻掉(服务端都确认在排队了,
|
|
5970
|
+
这条就该露面,不依赖那条可能已经错过的 SSE `queued` 事件)。
|
|
5971
|
+
⚠️ fromJob 行只是**渲染缓存**:不写 localStorage(saveQueue 会跳过)、不参与
|
|
5972
|
+
restoreQueue 重发(它没有本地 id,按它重发会多起一个作业)。
|
|
5973
|
+
⚠️ 反向对账也在这里做(2026-10-24 用户:「等待中然后到运行出现了两个,等待中没消失」):
|
|
5974
|
+
本地那条「等待中」原先只等 SSE 的 open 来删,用户离开会话页后流被 cancel,
|
|
5975
|
+
open 永远不来 → 会话行已经在跑,本地还挂着一条假的「等待中」。现在只要服务端作业
|
|
5976
|
+
不再排队、且登记表里确实看得见它,就把本地行撤掉(见 jobLanded)。 */
|
|
5977
|
+
var SERVER_ROW_MIN_AGE_MS = 1200; // 刚提交的别闪一下「等待中」再消失(空闲空间通常 1s 内就开跑)
|
|
5978
|
+
function jobTimeMs(t) {
|
|
5979
|
+
if (!t) return 0;
|
|
5980
|
+
var d = new Date(String(t)); // 服务端给的是 RFC3339
|
|
5981
|
+
return isNaN(d.getTime()) ? 0 : d.getTime();
|
|
5982
|
+
}
|
|
5983
|
+
/* 队列签名:排队 / 运行中的作业(状态、位置变了才值得重绘)。
|
|
5984
|
+
⚠️ running 也要算进来:本地那条「等待中」的撤销要靠作业状态变化来触发对账
|
|
5985
|
+
(见 syncServerJobs),只认 queued 的话「排队 → 开跑」这一步不会重画,
|
|
5986
|
+
那一行就一直停在「等待中」。 */
|
|
5987
|
+
function serverJobsSig(jobs) {
|
|
5988
|
+
if (!Array.isArray(jobs)) return '';
|
|
5989
|
+
return jobs.filter(function (j) { return j && (j.state === 'queued' || j.state === 'running'); })
|
|
5990
|
+
.map(function (j) { return String(j.id) + ':' + j.state + ':' + (j.position || 0); })
|
|
5991
|
+
.sort().join('|');
|
|
5992
|
+
}
|
|
5993
|
+
function syncServerJobs(jobs, sessions) {
|
|
5994
|
+
if (!SERVER_JOBS || !Array.isArray(jobs)) return;
|
|
5995
|
+
var now = Date.now();
|
|
5996
|
+
var localOf = {};
|
|
5997
|
+
PENDING.forEach(function (p) {
|
|
5998
|
+
if (!p || p.fromJob || p.gone) return;
|
|
5999
|
+
if (p.id) localOf[p.id] = p;
|
|
6000
|
+
if (p.requestId) localOf[p.requestId] = p;
|
|
6001
|
+
});
|
|
6002
|
+
/* 新服务端**受理即登记**(core 的 RegisterQueued,run_id = request_id,缺省 job id):
|
|
6003
|
+
这条作业已经在 /desk/sessions 里有一行「queued」了,别再画一条 fromJob 行
|
|
6004
|
+
—— 旧服务端没有登记行,才需要这份快照兜底。 */
|
|
6005
|
+
var registered = {};
|
|
6006
|
+
(sessions || []).forEach(function (s) {
|
|
6007
|
+
if (!s) return;
|
|
6008
|
+
var sid = sidOf(s);
|
|
6009
|
+
if (sid) registered[String(sid)] = 1;
|
|
6010
|
+
if (s.run_id) registered[String(s.run_id)] = 1;
|
|
6011
|
+
if (s.session_id) registered[String(s.session_id)] = 1;
|
|
6012
|
+
});
|
|
6013
|
+
var rows = [], dead = [], touched = false;
|
|
6014
|
+
jobs.forEach(function (j) {
|
|
6015
|
+
if (!j || !j.id) return;
|
|
6016
|
+
/* 本端同一次提交(request_id = 本地条目的 id,见 gateAsk / submitQueuedJob)。 */
|
|
6017
|
+
var lp = (j.request_id && localOf[j.request_id]) || null;
|
|
6018
|
+
if (lp) {
|
|
6019
|
+
lp.job = j.id;
|
|
6020
|
+
lp.pos = j.position || 0;
|
|
6021
|
+
/* ⚠️ 2026-10-24 用户:「等待中然后到运行出现了两个,等待中没消失」:
|
|
6022
|
+
本地这条「等待中」原先只在 SSE 的 open 到达时删。可用户一点进会话再退出来,
|
|
6023
|
+
组件就把那条流 cancel 掉了(conversation.js 的 destroy)—— 此后服务端开跑
|
|
6024
|
+
再也不会有任何事件回到这边,行就永远停在「等待中」,而会话行已经在跑(自动置顶)。
|
|
6025
|
+
所以非排队态一律以**服务端作业 + 登记表**为准对账:
|
|
6026
|
+
· 真的跑起来了 → 撤掉本地行(会话行接手显示);
|
|
6027
|
+
· 没跑起来就终结(failed / expired / cancelled)→ 标成可重试的「未能启动」,
|
|
6028
|
+
绝不静默消失(与 fireQueued 的判据同一口径)。 */
|
|
6029
|
+
if (j.state !== 'queued') {
|
|
6030
|
+
if (jobLanded(j, sessions)) { lp.gone = true; dead.push(lp.id); return; }
|
|
6031
|
+
if (j.state === 'running') return; // 登记表还没落盘:下一拍再看
|
|
6032
|
+
if (j.state === 'failed') {
|
|
6033
|
+
if (!lp.stalled) {
|
|
6034
|
+
lp.stalled = true;
|
|
6035
|
+
lp.stalledReason = j.error || '服务端启动失败';
|
|
6036
|
+
lp.hide = false;
|
|
6037
|
+
/* 只有 owner='host' 才走 enqueueDispatch 的重试入口(见 pendingRow 的点击)。 */
|
|
6038
|
+
lp.owner = 'host';
|
|
6039
|
+
touched = true;
|
|
6040
|
+
}
|
|
6041
|
+
} else {
|
|
6042
|
+
lp.gone = true;
|
|
6043
|
+
dead.push(lp.id); // cancelled / expired:这条不会再跑了
|
|
6044
|
+
}
|
|
6045
|
+
return;
|
|
6046
|
+
}
|
|
6047
|
+
if (lp.hide) { lp.hide = false; touched = true; }
|
|
6048
|
+
return;
|
|
6049
|
+
}
|
|
6050
|
+
if (j.state !== 'queued') return;
|
|
6051
|
+
/* 服务端受理即登记(见上):run_id 用 request_id;request_id 不适合当文件名时
|
|
6052
|
+
服务端会退回 job id,所以两个都要认。 */
|
|
6053
|
+
if (registered[String(j.request_id || '')] || registered[String(j.id)]) return;
|
|
6054
|
+
/* 刚进队的那 1 秒里它可能马上就被调度器拉起来:此时画一行「等待中」再秒删,
|
|
6055
|
+
正是用户 2026-10-22 抱怨的「没排队也先显示排队状态再切回」。 */
|
|
6056
|
+
if (now - jobTimeMs(j.created_at) < SERVER_ROW_MIN_AGE_MS) return;
|
|
6057
|
+
rows.push({
|
|
6058
|
+
id: 'srv-' + j.id, fromJob: true, job: j.id, requestId: j.request_id || '',
|
|
6059
|
+
/* 归组口径与本地条目一致:显式选了空间才进「空间」段(服务端把空 workspace
|
|
6060
|
+
归一成了 cwd,不能直接当成显式选择,否则默认任务会凭空多出一个空间组)。 */
|
|
6061
|
+
ws: wsKey(j.explicit_workspace ? j.workspace : ''),
|
|
6062
|
+
w: j.explicit_workspace ? j.workspace : '',
|
|
6063
|
+
explicit: !!j.explicit_workspace, pendingWait: true, hide: false, owner: 'server',
|
|
6064
|
+
session: j.session || '', prompt: j.prompt || '', engine: j.engine || '',
|
|
6065
|
+
model: j.model || '', team: j.team || null, hosted: !!j.hosted,
|
|
6066
|
+
pos: j.position || 0, at: jobTimeMs(j.created_at)
|
|
6067
|
+
});
|
|
6068
|
+
});
|
|
6069
|
+
rows.sort(function (a, b) { return (a.at || 0) - (b.at || 0); }); // FIFO,与队列顺序一致
|
|
6070
|
+
if (dead.length) {
|
|
6071
|
+
var gone = {};
|
|
6072
|
+
dead.forEach(function (id) { gone[id] = 1; });
|
|
6073
|
+
PENDING = PENDING.filter(function (p) { return p && !gone[p.id]; });
|
|
6074
|
+
/* 磁盘上也要一并删掉,否则下次启动它又冒出来(与 dropPending / cancelPending 同一口径)。 */
|
|
6075
|
+
dead.forEach(purgeQueue);
|
|
6076
|
+
touched = true;
|
|
6077
|
+
}
|
|
6078
|
+
var had = PENDING.filter(function (p) { return p && p.fromJob; });
|
|
6079
|
+
var sig = function (arr) {
|
|
6080
|
+
return arr.map(function (p) { return p.id + ':' + (p.pos || 0); }).join('|');
|
|
6081
|
+
};
|
|
6082
|
+
if (!touched && sig(had) === sig(rows)) return;
|
|
6083
|
+
PENDING = PENDING.filter(function (p) { return !p || !p.fromJob; }).concat(rows);
|
|
6084
|
+
renderSessions(LAST_SESSIONS);
|
|
6085
|
+
}
|
|
6086
|
+
/* 服务端这条作业是否**真的跑起来了**(与 fireQueued 同一口径:收到 open 不等于跑起来)。
|
|
6087
|
+
两条判据:① 作业自己带了 session_id / run_id(core 收尾时回填);
|
|
6088
|
+
② 登记表里看得见它 —— 追问看原会话,新任务看「t0 之后开始的同空间记录」。 */
|
|
6089
|
+
function jobLanded(j, sessions) {
|
|
6090
|
+
if (j.session_id || j.run_id) return true;
|
|
6091
|
+
if (!Array.isArray(sessions)) return false;
|
|
6092
|
+
var startMs = jobTimeMs(j.started_at);
|
|
6093
|
+
var ws = wsKey(j.explicit_workspace ? j.workspace : '');
|
|
6094
|
+
return sessions.some(function (s) {
|
|
6095
|
+
if (!s || wsOf(s) !== ws) return false;
|
|
6096
|
+
if (j.session) return sameConvId(sidOf(s), j.session);
|
|
6097
|
+
return startMs ? startedAfter(s, startMs) : false;
|
|
6098
|
+
});
|
|
6099
|
+
}
|
|
4302
6100
|
function addPending(ws, info, hide) {
|
|
4303
6101
|
/* explicit:**显式选了空间**才归进「空间」段;默认(不指定)落「任务」段 ——
|
|
4304
6102
|
与 renderSessions 里会话按 `workspace` 是否为空分组的判定保持一致。
|
|
@@ -4312,6 +6110,9 @@ button.setrow{font-family:inherit}
|
|
|
4312
6110
|
session: info.session || '',
|
|
4313
6111
|
prompt: info.prompt || '', engine: info.engine || '',
|
|
4314
6112
|
model: info.model || '', permission: info.permission || '',
|
|
6113
|
+
/* reasoning:思考档位快照(2026-10-24)。排队 / 重启恢复时按提交那一刻的
|
|
6114
|
+
档位重发 —— 用户在等待期间改了档位,已排队的那条不该被悄悄改档。 */
|
|
6115
|
+
reasoning: REASONING_CUR,
|
|
4315
6116
|
/* team:重发时原样还原团队配置(见 fireQueued)——
|
|
4316
6117
|
丢了它这条排队任务就会从「团队」静默降级成单任务。 */
|
|
4317
6118
|
team: info.team || null,
|
|
@@ -4393,6 +6194,8 @@ button.setrow{font-family:inherit}
|
|
|
4393
6194
|
var baseAsk = t.ask;
|
|
4394
6195
|
t.ask = function (payload, h) {
|
|
4395
6196
|
var p = payload || {};
|
|
6197
|
+
/* 思考模式:每一轮都按**当前这条会话**的档位下发(默认最高,见 REASONING_CUR)。 */
|
|
6198
|
+
applyReasoningTo(p, p.engine, REASONING_CUR);
|
|
4396
6199
|
var pend = pendingOf(p.prompt);
|
|
4397
6200
|
var rid = (pend && pend.id) || ('q' + Date.now() + '-' + Math.floor(Math.random() * 100000));
|
|
4398
6201
|
p.request_id = rid;
|
|
@@ -4472,11 +6275,11 @@ button.setrow{font-family:inherit}
|
|
|
4472
6275
|
/* 只落**持久**字段:hide / gone 是这一次页面会话里的显示态,存下去会让「关掉 App 时
|
|
4473
6276
|
正好还没到 300ms」的那条永远隐藏 —— 队列里有它、列表上却看不见,最难查的一种。
|
|
4474
6277
|
id 保留:恢复时据此与内存里已有的条目去重。 */
|
|
4475
|
-
var mineRows = PENDING.map(function (p) {
|
|
6278
|
+
var mineRows = PENDING.filter(function (p) { return p && !p.fromJob; }).map(function (p) {
|
|
4476
6279
|
return { id: p.id, ws: p.ws, w: p.w || '', explicit: !!p.explicit, pendingWait: true,
|
|
4477
6280
|
session: p.session || '',
|
|
4478
6281
|
prompt: p.prompt || '', engine: p.engine || '', model: p.model || '',
|
|
4479
|
-
permission: p.permission || '', team: p.team || null,
|
|
6282
|
+
permission: p.permission || '', reasoning: p.reasoning || '', team: p.team || null,
|
|
4480
6283
|
attachments: p.attachments || [], atts: p.atts || [],
|
|
4481
6284
|
/* owner 刻意不落盘:重启后所有残留条目都由 restoreQueue 交给宿主
|
|
4482
6285
|
(enqueueDispatch),语义与 'host' 一致 —— 存下来反而会让「上次是
|
|
@@ -4502,7 +6305,11 @@ button.setrow{font-family:inherit}
|
|
|
4502
6305
|
function syncPendingFromDisk() {
|
|
4503
6306
|
var disk = loadQueue();
|
|
4504
6307
|
var keep = [];
|
|
4505
|
-
|
|
6308
|
+
/* fromJob 行来自服务端队列快照,不在磁盘上:这里若丢掉它们,列表要等下一次
|
|
6309
|
+
loadSessions 才补回来(中间空一截)。其余按磁盘为准。 */
|
|
6310
|
+
PENDING.forEach(function (p) {
|
|
6311
|
+
if (p && (p.dispatching || p.owner === 'conv' || p.fromJob)) keep.push(p);
|
|
6312
|
+
});
|
|
4506
6313
|
var ids = {};
|
|
4507
6314
|
keep.forEach(function (p) { ids[p.id] = 1; });
|
|
4508
6315
|
PENDING = disk.filter(function (p) { return !ids[p.id]; }).concat(keep);
|
|
@@ -4729,10 +6536,47 @@ button.setrow{font-family:inherit}
|
|
|
4729
6536
|
renderSessions(LAST_SESSIONS);
|
|
4730
6537
|
return true;
|
|
4731
6538
|
}
|
|
4732
|
-
/*
|
|
6539
|
+
/* 任务模式切换要「马上生效」:已排队、还没开跑的作业跟着新模式走 —— 不然串行切并行后,
|
|
6540
|
+
旧条目还带着提交那一刻的 serial 快照,在服务端继续排独占队,直到整条串行队消化完。
|
|
6541
|
+
本地串行门(!SERVER_JOBS)不用管:waitForSlot 每一拍都看 TASK_MODE,最多 3s 自己生效。 */
|
|
6542
|
+
function applyTaskModeToQueued(v) {
|
|
6543
|
+
if (!SERVER_JOBS) return;
|
|
6544
|
+
var serial = (v === 'serial');
|
|
6545
|
+
PENDING.forEach(function (p) {
|
|
6546
|
+
if (!p || p.gone || p.cancelled) return;
|
|
6547
|
+
/* fromJob 行也一起改:它是服务端队列里的快照(App 重启后本端那条已被
|
|
6548
|
+
restoreQueue 交走,列表上就只剩这一行)。任务模式是**环境级**设置
|
|
6549
|
+
(magic.taskmode 按环境分桶、随 /desk/prefs 同步),同环境里排着的作业
|
|
6550
|
+
都该跟着新模式走 —— 只改本端本地条目的话,重启后切并行对它们无效,
|
|
6551
|
+
症状正是「切了没马上生效」。
|
|
6552
|
+
⚠️ fromJob 行有 job id,updateJobSerial 走 /desk/jobs/{id}/update:
|
|
6553
|
+
按 job id 定位,不会误伤别人的作业(本地条目按 request_id 定位,
|
|
6554
|
+
那条路才需要防撞车,见 pendingOf 的注释)。 */
|
|
6555
|
+
updateJobSerial(p, serial);
|
|
6556
|
+
});
|
|
6557
|
+
}
|
|
6558
|
+
/* 服务端改模式:有 job id 走作业路由;还没拿到(queued 事件未到)就按 request_id 改。
|
|
6559
|
+
只对排队中的生效 —— 已开跑的作业服务端回 updated=false,不打断。 */
|
|
6560
|
+
function updateJobSerial(p, serial) {
|
|
6561
|
+
try {
|
|
6562
|
+
var hdr = TOKEN ? { 'X-Magic-Token': TOKEN, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' };
|
|
6563
|
+
if (p.job) {
|
|
6564
|
+
fetch(BASE + '/desk/jobs/' + encodeURIComponent(p.job) + '/update', { method: 'POST', headers: hdr,
|
|
6565
|
+
body: JSON.stringify({ serial: serial }) }).catch(function () {});
|
|
6566
|
+
} else {
|
|
6567
|
+
fetch(BASE + '/desk/jobs/update', { method: 'POST', headers: hdr,
|
|
6568
|
+
body: JSON.stringify({ request_id: p.requestId || p.id, serial: serial }) }).catch(function () {});
|
|
6569
|
+
}
|
|
6570
|
+
} catch (e) {}
|
|
6571
|
+
}
|
|
6572
|
+
/* 「这一轮还在排队吗」——组件问宿主:真在排队就别去调 control(那条路没有 askId)。
|
|
6573
|
+
⚠️ fromJob 行是服务端队列里的**别人的/上次的**作业(见 syncServerJobs),按 prompt
|
|
6574
|
+
撞上就把它当成这一轮的 request_id,会把这条新提交撞成别人的幂等键。跳过。 */
|
|
4733
6575
|
function pendingOf(prompt) {
|
|
4734
6576
|
for (var i = 0; i < PENDING.length; i++) {
|
|
4735
|
-
|
|
6577
|
+
var p = PENDING[i];
|
|
6578
|
+
if (!p || p.gone || p.fromJob) continue;
|
|
6579
|
+
if (p.prompt === prompt) return p;
|
|
4736
6580
|
}
|
|
4737
6581
|
return null;
|
|
4738
6582
|
}
|
|
@@ -4772,7 +6616,9 @@ button.setrow{font-family:inherit}
|
|
|
4772
6616
|
/* 重启恢复的条目一律归宿主下发:它们的主人(会话组件)已经不在了,
|
|
4773
6617
|
再标成 'conv' 就没人会认领(见 adoptPendingFor 的判据)。 */
|
|
4774
6618
|
list.forEach(function (p) { p.owner = 'host'; });
|
|
4775
|
-
|
|
6619
|
+
/* 服务端队列快照生成的 fromJob 行不落盘、也不由这里重发(它没有本地 id,
|
|
6620
|
+
按它提交会多起一个作业);保留它们,由下一次 syncServerJobs 重新对账。 */
|
|
6621
|
+
PENDING = list.concat(PENDING.filter(function (p) { return p && p.fromJob; }));
|
|
4776
6622
|
renderSessions(LAST_SESSIONS);
|
|
4777
6623
|
if (!SERVER_JOBS) {
|
|
4778
6624
|
list.forEach(function (p) { enqueueDispatch(p); });
|
|
@@ -4801,6 +6647,7 @@ button.setrow{font-family:inherit}
|
|
|
4801
6647
|
if (p.w) body.workspace = p.w;
|
|
4802
6648
|
if (p.session) body.session = p.session;
|
|
4803
6649
|
if (p.permission) body.permission = p.permission;
|
|
6650
|
+
applyReasoningTo(body, p.engine, p.reasoning);
|
|
4804
6651
|
if (p.team) body.team = p.team;
|
|
4805
6652
|
if (p.hosted) body.hosted = true;
|
|
4806
6653
|
if (p.attachments && p.attachments.length) body.attachments = p.attachments;
|
|
@@ -4872,11 +6719,18 @@ button.setrow{font-family:inherit}
|
|
|
4872
6719
|
if (!window.MagicTransportHttp) return Promise.resolve(false);
|
|
4873
6720
|
var transport = window.MagicTransportHttp({ base: BASE, token: TOKEN, client: 'mobile' });
|
|
4874
6721
|
var payload = { engine: p.engine, prompt: p.prompt };
|
|
6722
|
+
/* 幂等键:与本地排队行同一个 id(gateAsk / submitQueuedJob 也用它)。
|
|
6723
|
+
① 服务端按它认出「还是这一条」,重试不会多跑一遍;
|
|
6724
|
+
② 列表侧 syncServerJobs 靠它把本地「等待中」和 /desk/jobs 里的同一作业认成
|
|
6725
|
+
一行 —— 不带的话,恢复队列重发时本地行 + 服务端快照行会各画一条。 */
|
|
6726
|
+
if (p.id) payload.request_id = p.id;
|
|
4875
6727
|
if (p.model) payload.model = p.model;
|
|
4876
6728
|
if (p.w) payload.workspace = p.w;
|
|
4877
6729
|
/* 带上原会话 id 才是「追问」;不带就是新起一段会话(见 addPending 里 session 的注释)。 */
|
|
4878
6730
|
if (p.session) payload.session = p.session;
|
|
4879
6731
|
if (p.permission) payload.permission = p.permission;
|
|
6732
|
+
/* 思考档位同样用提交那一刻的快照;引擎不支持时 applyReasoningTo 自己会跳过。 */
|
|
6733
|
+
applyReasoningTo(payload, p.engine, p.reasoning);
|
|
4880
6734
|
/* 团队配置是在发送那一刻快照下来的(见 addPending):重发必须带上,
|
|
4881
6735
|
否则这条排队任务会被当成普通单任务跑。 */
|
|
4882
6736
|
if (p.team) payload.team = p.team;
|
|
@@ -5034,8 +6888,23 @@ button.setrow{font-family:inherit}
|
|
|
5034
6888
|
}
|
|
5035
6889
|
|
|
5036
6890
|
/* ── 通知:配置 + 真触发 ──
|
|
5037
|
-
|
|
5038
|
-
|
|
6891
|
+
两条通道,按环境自动选:
|
|
6892
|
+
① App 内(Capacitor LocalNotifications)→ **系统通知**(通知栏里那条);
|
|
6893
|
+
② 浏览器(Web Notification)→ 系统通知(手机浏览器支持;**Android WebView 不支持**,
|
|
6894
|
+
所以 App 内这条路根本不存在,别拿它当 App 内的兜底);
|
|
6895
|
+
都不行 → 应用内 toast(一条灰条,App 内也看得到)。
|
|
6896
|
+
⚠️ App 被系统杀掉时 JS 不跑、触发不了 ——「任务完成 / 待确认」通知需要 App 在运行。
|
|
6897
|
+
|
|
6898
|
+
⚠️ **远程页面里 Capacitor 运行时不在**(根因已从 APK 的 smali 逐行确认,2026-10-25):
|
|
6899
|
+
APK 壳配对后加载的是桌面的 /m/ 远程页。Capacitor 的 JS 注入有两条路:
|
|
6900
|
+
① DOCUMENT_START_SCRIPT(现代 WebView 都支持)——只注册给壳自己的 origin(http://localhost);
|
|
6901
|
+
② 本地服务器代理时的 jsInjector —— 在 ① 生效的分支里被置成 **null**。
|
|
6902
|
+
两条路对远程页都不通:远程页被原样代理、**一个字节的 JS 都没注入**,
|
|
6903
|
+
所以远程页里 window.Capacitor 根本不存在(这就是「没有可用的系统通知通道」的根因)。
|
|
6904
|
+
但**传输层**在:useLegacyBridge 走 addJavascriptInterface("androidBridge"),
|
|
6905
|
+
注入**所有 frame**(App 内 APK 下载用的 window.MagicUpdate 就是这么工作的)。
|
|
6906
|
+
所以远程页自己补一个最小桥(notifyNativeShim):androidBridge.postMessage 发原生消息、
|
|
6907
|
+
window.Capacitor.fromNative 收响应 —— 纯前端修复,改本页即可,**无需重打 APK**。 */
|
|
5039
6908
|
var NOTIFY = { result: true, confirm: true, sched: true };
|
|
5040
6909
|
try {
|
|
5041
6910
|
var _nv = JSON.parse(localStorage.getItem('magic.notify') || '{}');
|
|
@@ -5044,19 +6913,178 @@ button.setrow{font-family:inherit}
|
|
|
5044
6913
|
}
|
|
5045
6914
|
} catch (e) {}
|
|
5046
6915
|
function saveNotify() { try { localStorage.setItem('magic.notify', JSON.stringify(NOTIFY)); } catch (e) {} }
|
|
6916
|
+
/* 通道探测结果(缓存)。权限状态只能问原生才知道,是异步的:
|
|
6917
|
+
BRIDGE: 'unknown' | 'alive' | 'dead'(传输层不通)| 'none'(压根没有原生通道)
|
|
6918
|
+
PERM: 'unknown' | 'granted' | 'denied' | 'prompt'(还没问过用户) */
|
|
6919
|
+
var NOTIFY_BRIDGE = 'unknown', NOTIFY_PERM = 'unknown';
|
|
6920
|
+
/* 传输层不通时 promise 永不 settle,只能靠超时判定。原生是同机调用,1.5s 足够宽。 */
|
|
6921
|
+
var NOTIFY_TIMEOUT_MS = 1500;
|
|
5047
6922
|
function notifyPlugin() {
|
|
5048
6923
|
try { var C = window.Capacitor; return (C && C.Plugins && C.Plugins.LocalNotifications) || null; } catch (e) { return null; }
|
|
5049
6924
|
}
|
|
5050
|
-
|
|
5051
|
-
|
|
6925
|
+
/* 远程页兜底桥:window.Capacitor 不在、但 androidBridge 在(legacy 桥注入所有 frame)。
|
|
6926
|
+
自建最小 Capacitor 响应面:nativePromise 发原生消息({callbackId, pluginId, methodName,
|
|
6927
|
+
options}),fromNative 收原生回调({callbackId, success, data|error})—— 协议与
|
|
6928
|
+
Capacitor MessageHandler 的 legacy 桥逐字段对齐。只挂 fromNative/nativePromise,
|
|
6929
|
+
**不**伪造 Plugins.*,保证 notifyPlugin()/诊断永远能区分「真运行时」和「自建桥」。
|
|
6930
|
+
返回带 __shim 的插件对象;环境不对(没有 androidBridge)返回 null。 */
|
|
6931
|
+
var NOTIFY_SHIM = null; /* null=未探测,false=探测过、不可用,否则是插件对象 */
|
|
6932
|
+
function notifyNativeShim() {
|
|
6933
|
+
if (NOTIFY_SHIM !== null) return NOTIFY_SHIM || null;
|
|
6934
|
+
NOTIFY_SHIM = false;
|
|
6935
|
+
try {
|
|
6936
|
+
var B = window.androidBridge;
|
|
6937
|
+
if (B && typeof B.postMessage === 'function') {
|
|
6938
|
+
var cap = window.Capacitor = window.Capacitor || {};
|
|
6939
|
+
if (typeof cap.fromNative !== 'function') {
|
|
6940
|
+
var cbs = {}, seq = 0;
|
|
6941
|
+
cap.fromNative = function (raw) {
|
|
6942
|
+
try {
|
|
6943
|
+
var d = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
6944
|
+
var c = cbs[d.callbackId];
|
|
6945
|
+
if (c) { delete cbs[d.callbackId]; if (d.success) c.resolve(d.data); else c.reject(d.error); }
|
|
6946
|
+
} catch (e) {}
|
|
6947
|
+
};
|
|
6948
|
+
cap.nativePromise = function (pid, m, o) {
|
|
6949
|
+
return new Promise(function (res, rej) {
|
|
6950
|
+
var id = 'shim' + (++seq);
|
|
6951
|
+
cbs[id] = { resolve: res, reject: rej };
|
|
6952
|
+
try { B.postMessage(JSON.stringify({ callbackId: id, pluginId: pid, methodName: m, options: o || {} })); }
|
|
6953
|
+
catch (e) { delete cbs[id]; rej(e); }
|
|
6954
|
+
});
|
|
6955
|
+
};
|
|
6956
|
+
}
|
|
6957
|
+
if (typeof cap.nativePromise === 'function') {
|
|
6958
|
+
NOTIFY_SHIM = {
|
|
6959
|
+
__shim: true,
|
|
6960
|
+
schedule: function (o) { return cap.nativePromise('LocalNotifications', 'schedule', o); },
|
|
6961
|
+
checkPermissions: function () { return cap.nativePromise('LocalNotifications', 'checkPermissions', {}); },
|
|
6962
|
+
requestPermissions: function () { return cap.nativePromise('LocalNotifications', 'requestPermissions', {}); }
|
|
6963
|
+
};
|
|
6964
|
+
}
|
|
6965
|
+
}
|
|
6966
|
+
} catch (e) { NOTIFY_SHIM = false; }
|
|
6967
|
+
return NOTIFY_SHIM || null;
|
|
6968
|
+
}
|
|
6969
|
+
/* 用通道一律走这个:真运行时优先,不在再试自建桥。 */
|
|
6970
|
+
function notifyNativePlugin() { return notifyPlugin() || notifyNativeShim(); }
|
|
6971
|
+
/* 通道诊断:把「为什么没有原生通道」说清楚,别只丢一句「没有可用通道」。
|
|
6972
|
+
两种完全不同的断点,用户看到的现象一模一样:
|
|
6973
|
+
· 'no-runtime' → window.Capacitor 压根不存在 —— **Capacitor 运行时没注入到本页**。
|
|
6974
|
+
典型场景:APK 壳加载的是**桌面的远程 /m/ 页**,而 Capacitor 的 JS 注入
|
|
6975
|
+
只发生在它自己的本地服务器(uriMatcher 只认已注册的 authority)上,
|
|
6976
|
+
远程页被原样返回、没被注入。
|
|
6977
|
+
· 'no-plugin' → 运行时在,但 Plugins 里没有 LocalNotifications(插件没装/没注册)。
|
|
6978
|
+
别的场景(传输层不通 / 权限问题)由 notifyProbe 的 alive/dead 与权限状态覆盖。 */
|
|
6979
|
+
function notifyDiagnose() {
|
|
6980
|
+
var d = { runtime: false, plugins: [], hasPlugin: false, shim: false, bridge: NOTIFY_BRIDGE, perm: NOTIFY_PERM, origin: location.origin, href: location.href };
|
|
6981
|
+
try {
|
|
6982
|
+
var C = window.Capacitor;
|
|
6983
|
+
d.runtime = !!(C && C.Plugins);
|
|
6984
|
+
if (d.runtime) d.plugins = Object.keys(C.Plugins);
|
|
6985
|
+
d.hasPlugin = !!notifyPlugin();
|
|
6986
|
+
} catch (e) {}
|
|
6987
|
+
/* 自建桥探测放最后:它会往 window 上挂 Capacitor.fromNative(但不挂 Plugins),
|
|
6988
|
+
runtime/hasPlugin 的判断必须在它之前完成,否则第二次打开诊断会把自建桥误认成真运行时。 */
|
|
6989
|
+
d.shim = !d.hasPlugin && !!notifyNativeShim();
|
|
6990
|
+
if (!d.runtime && d.shim) d.why = 'shim';
|
|
6991
|
+
else if (!d.runtime) d.why = 'no-runtime';
|
|
6992
|
+
else if (!d.hasPlugin) d.why = 'no-plugin';
|
|
6993
|
+
else if (NOTIFY_BRIDGE === 'dead') d.why = 'transport-dead';
|
|
6994
|
+
else d.why = 'ok';
|
|
6995
|
+
return d;
|
|
6996
|
+
}
|
|
6997
|
+
function notifyDiagText() {
|
|
6998
|
+
var d = notifyDiagnose();
|
|
6999
|
+
var lines = [
|
|
7000
|
+
'诊断:' + (d.why === 'no-runtime' ? 'Capacitor 运行时没注入本页'
|
|
7001
|
+
: d.why === 'shim' ? '运行时没注入本页,已用 androidBridge 自建桥(通知可发,先点授权)'
|
|
7002
|
+
: d.why === 'no-plugin' ? '运行时在,但没有 LocalNotifications 插件'
|
|
7003
|
+
: d.why === 'transport-dead' ? '运行时与插件都在,传输层不通'
|
|
7004
|
+
: '通道正常'),
|
|
7005
|
+
'页面来源:' + d.origin,
|
|
7006
|
+
'插件清单:' + (d.plugins.length ? d.plugins.join(', ') : '(无)'),
|
|
7007
|
+
'传输层:' + d.bridge + ' 权限:' + d.perm
|
|
7008
|
+
];
|
|
7009
|
+
if (d.why === 'no-runtime') {
|
|
7010
|
+
lines.push('原因:Capacitor 的 JS 注入只发生在它自己的本地服务器上,' +
|
|
7011
|
+
'桌面远程页(/m/)被原样返回、没注入 —— 这是壳加载远程页时的固有限制。');
|
|
7012
|
+
} else if (d.why === 'shim') {
|
|
7013
|
+
lines.push('说明:这是桌面远程页,壳的运行时注入不到这里;' +
|
|
7014
|
+
'本页已通过 androidBridge(legacy 桥,对所有 frame 可见)自建通知通道。');
|
|
7015
|
+
}
|
|
7016
|
+
return lines.join('\n');
|
|
7017
|
+
}
|
|
7018
|
+
function notifyWebPerm() {
|
|
5052
7019
|
try { if (window.Notification) return Notification.permission; } catch (e) {}
|
|
7020
|
+
return 'unsupported';
|
|
7021
|
+
}
|
|
7022
|
+
/* 原生有没有 Web Notification 这条路:App 里**没有**(WebView 不支持),
|
|
7023
|
+
只有真浏览器打开 /m/ 时才有。所以 App 内不许拿它当兜底。 */
|
|
7024
|
+
function notifyHasWeb() {
|
|
7025
|
+
if (notifyNativePlugin()) return false;
|
|
7026
|
+
try { return !!window.Notification; } catch (e) { return false; }
|
|
7027
|
+
}
|
|
7028
|
+
function notifyWithTimeout(p) {
|
|
7029
|
+
return Promise.race([
|
|
7030
|
+
p.then(function (v) { return { ok: true, v: v }; }, function (e) { return { ok: false, v: e }; }),
|
|
7031
|
+
new Promise(function (res) { setTimeout(function () { res({ ok: false, timeout: true }); }, NOTIFY_TIMEOUT_MS); })
|
|
7032
|
+
]);
|
|
7033
|
+
}
|
|
7034
|
+
/* 探一次原生通道:传输层活着吗?通知权限给了吗? */
|
|
7035
|
+
function notifyProbe() {
|
|
7036
|
+
var P = notifyNativePlugin();
|
|
7037
|
+
if (!P || !P.checkPermissions) { NOTIFY_BRIDGE = 'none'; NOTIFY_PERM = 'unknown'; return Promise.resolve(); }
|
|
7038
|
+
return notifyWithTimeout(Promise.resolve(P.checkPermissions())).then(function (r) {
|
|
7039
|
+
if (r.timeout) { NOTIFY_BRIDGE = 'dead'; NOTIFY_PERM = 'unknown'; return; }
|
|
7040
|
+
NOTIFY_BRIDGE = 'alive';
|
|
7041
|
+
var d = (r.ok && r.v) ? (r.v.display || r.v.granted) : null;
|
|
7042
|
+
NOTIFY_PERM = d === 'granted' ? 'granted' : (d === 'denied' ? 'denied' : (d === 'prompt' ? 'prompt' : 'denied'));
|
|
7043
|
+
});
|
|
7044
|
+
}
|
|
7045
|
+
/* 这一格该怎么写:优先看原生通道(App 内),没有原生再看浏览器通道。 */
|
|
7046
|
+
function notifyPermState() {
|
|
7047
|
+
if (notifyNativePlugin()) {
|
|
7048
|
+
if (NOTIFY_BRIDGE === 'dead') return 'dead';
|
|
7049
|
+
if (NOTIFY_PERM === 'granted') return 'granted';
|
|
7050
|
+
if (NOTIFY_PERM === 'denied') return 'denied';
|
|
7051
|
+
if (NOTIFY_PERM === 'prompt') return 'prompt';
|
|
7052
|
+
return 'unknown';
|
|
7053
|
+
}
|
|
7054
|
+
var w = notifyWebPerm();
|
|
7055
|
+
if (w === 'granted') return 'granted';
|
|
7056
|
+
if (w === 'denied') return 'denied';
|
|
7057
|
+
if (w === 'default') return 'prompt';
|
|
5053
7058
|
return 'unknown';
|
|
5054
7059
|
}
|
|
7060
|
+
function notifyStateLabel() {
|
|
7061
|
+
var st = notifyPermState();
|
|
7062
|
+
if (st === 'granted') return '已开启';
|
|
7063
|
+
if (st === 'denied') return '已拒绝';
|
|
7064
|
+
if (st === 'prompt') return '去授权';
|
|
7065
|
+
if (st === 'dead') return '未接入';
|
|
7066
|
+
return '—';
|
|
7067
|
+
}
|
|
5055
7068
|
function requestNotifyPermission() {
|
|
5056
|
-
var P =
|
|
5057
|
-
if (P && P.requestPermissions) {
|
|
5058
|
-
|
|
5059
|
-
|
|
7069
|
+
var P = notifyNativePlugin();
|
|
7070
|
+
if (P && P.requestPermissions) {
|
|
7071
|
+
return notifyWithTimeout(Promise.resolve(P.requestPermissions())).then(function (r) {
|
|
7072
|
+
if (r.timeout) { NOTIFY_BRIDGE = 'dead'; return null; }
|
|
7073
|
+
NOTIFY_BRIDGE = 'alive';
|
|
7074
|
+
var d = (r.ok && r.v) ? (r.v.display || r.v.granted) : null;
|
|
7075
|
+
NOTIFY_PERM = d === 'granted' ? 'granted' : (d === 'denied' ? 'denied' : 'prompt');
|
|
7076
|
+
return NOTIFY_PERM;
|
|
7077
|
+
});
|
|
7078
|
+
}
|
|
7079
|
+
try {
|
|
7080
|
+
if (window.Notification && Notification.permission === 'default') {
|
|
7081
|
+
return Promise.resolve(window.Notification.requestPermission()).then(function (p) {
|
|
7082
|
+
NOTIFY_PERM = p === 'granted' ? 'granted' : (p === 'denied' ? 'denied' : 'prompt');
|
|
7083
|
+
return NOTIFY_PERM;
|
|
7084
|
+
});
|
|
7085
|
+
}
|
|
7086
|
+
} catch (e) {}
|
|
7087
|
+
return Promise.resolve(null);
|
|
5060
7088
|
}
|
|
5061
7089
|
var toastEl = null, toastT = null;
|
|
5062
7090
|
function toast(msg) {
|
|
@@ -5068,20 +7096,47 @@ button.setrow{font-family:inherit}
|
|
|
5068
7096
|
}
|
|
5069
7097
|
function notify(kind, title, body) {
|
|
5070
7098
|
if (!NOTIFY[kind]) return;
|
|
5071
|
-
var
|
|
7099
|
+
var msg = title + (body ? ':' + body : '');
|
|
7100
|
+
var P = notifyNativePlugin();
|
|
5072
7101
|
if (P && P.schedule) {
|
|
5073
|
-
|
|
7102
|
+
/* 原生通道:走系统通知(通知栏里那条)。
|
|
7103
|
+
⚠️ **不传 schedule 字段 = 立即显示**(插件的 isScheduled() 为 false 时直接
|
|
7104
|
+
notificationManager.notify());传了 schedule.at 反而是「排到那个时刻」,
|
|
7105
|
+
那是定时任务的事、不是「现在弹一条」。
|
|
7106
|
+
⚠️ 远程页面里传输层不通时,这个 promise 永不 settle —— 所以给个超时兜底,
|
|
7107
|
+
超时按失败处理、落到 toast。**不能因为「调了没抛错」就当成功**(静默失败过一次)。 */
|
|
7108
|
+
var p;
|
|
7109
|
+
try {
|
|
7110
|
+
p = Promise.resolve(P.schedule({ notifications: [{ id: notifyNextId(), title: title, body: body }] }));
|
|
7111
|
+
} catch (e) { p = Promise.reject(e); }
|
|
7112
|
+
notifyWithTimeout(p).then(function (r) {
|
|
7113
|
+
if (r.ok) return; // 系统通知已发出
|
|
7114
|
+
/* 通知没发出去:别装作发了,落到应用内 toast,用户至少看得到。 */
|
|
7115
|
+
toast(msg);
|
|
7116
|
+
});
|
|
7117
|
+
return;
|
|
5074
7118
|
}
|
|
5075
|
-
|
|
5076
|
-
|
|
7119
|
+
/* 浏览器通道:只有真浏览器打开 /m/ 时才有(App 内 WebView 不支持 Notification)。 */
|
|
7120
|
+
if (notifyHasWeb()) {
|
|
7121
|
+
try {
|
|
5077
7122
|
if (Notification.permission === 'granted') { new Notification(title, { body: body }); return; }
|
|
5078
7123
|
if (Notification.permission !== 'denied') {
|
|
5079
|
-
Notification.requestPermission().then(function (p) {
|
|
7124
|
+
Notification.requestPermission().then(function (p) {
|
|
7125
|
+
if (p === 'granted') { try { new Notification(title, { body: body }); } catch (e) { toast(msg); } }
|
|
7126
|
+
else toast(msg);
|
|
7127
|
+
}, function () { toast(msg); });
|
|
5080
7128
|
return;
|
|
5081
7129
|
}
|
|
5082
|
-
}
|
|
5083
|
-
}
|
|
5084
|
-
toast(
|
|
7130
|
+
} catch (e) {}
|
|
7131
|
+
}
|
|
7132
|
+
toast(msg);
|
|
7133
|
+
}
|
|
7134
|
+
/* 通知 id:插件要求是 Java int(超范围会整条 call 被 reject),且同 id 会覆盖前一条。
|
|
7135
|
+
用一个自增计数配随机起点,避开 int 上界与重复。 */
|
|
7136
|
+
var NOTIFY_ID_SEQ = Math.floor(Math.random() * 100000);
|
|
7137
|
+
function notifyNextId() {
|
|
7138
|
+
NOTIFY_ID_SEQ = (NOTIFY_ID_SEQ + 1) % 2000000000;
|
|
7139
|
+
return NOTIFY_ID_SEQ;
|
|
5085
7140
|
}
|
|
5086
7141
|
function nToggle(body, label, on, onSet) {
|
|
5087
7142
|
var row = document.createElement('div'); row.className = 'setrow';
|
|
@@ -5090,14 +7145,72 @@ button.setrow{font-family:inherit}
|
|
|
5090
7145
|
t.onclick = function () { on = !on; t.classList.toggle('is-on', on); onSet(on); };
|
|
5091
7146
|
row.appendChild(t); body.appendChild(row);
|
|
5092
7147
|
}
|
|
7148
|
+
/* 「发一条测试通知」:把通道通没通**当场验给用户看**。
|
|
7149
|
+
没有这个入口,用户只能对着一个状态字猜 —— 静默失败过一次(传输层不通时
|
|
7150
|
+
promise 永不 settle,页面看起来一切正常,但通知根本没发出去)。
|
|
7151
|
+
返回 'sent' | 'timeout' | 'failed' | 'web' | 'toast',便于自测断言。 */
|
|
7152
|
+
function notifyTest() {
|
|
7153
|
+
var P = notifyNativePlugin();
|
|
7154
|
+
if (!P) {
|
|
7155
|
+
/* 没有原生通道:把**原因**说清楚(见 notifyDiagnose)。
|
|
7156
|
+
只说「没有可用的系统通知通道」的话,用户既不知道该重装 App、
|
|
7157
|
+
也不知道是自己没连上桌面 —— 白白折腾。 */
|
|
7158
|
+
var d = notifyDiagnose();
|
|
7159
|
+
var why = d.why === 'no-runtime'
|
|
7160
|
+
? '当前页面没有系统通知通道(App 加载的是桌面远程页)'
|
|
7161
|
+
: (d.why === 'no-plugin' ? '当前 App 未安装通知插件' : '当前没有可用的系统通知通道');
|
|
7162
|
+
toast(why + ',已改用应用内提示');
|
|
7163
|
+
return Promise.resolve('toast');
|
|
7164
|
+
}
|
|
7165
|
+
if (P && P.schedule) {
|
|
7166
|
+
var p;
|
|
7167
|
+
try {
|
|
7168
|
+
p = Promise.resolve(P.schedule({ notifications: [{ id: notifyNextId(), title: 'Magic Agent', body: '系统通知工作正常' }] }));
|
|
7169
|
+
} catch (e) { p = Promise.reject(e); }
|
|
7170
|
+
return notifyWithTimeout(p).then(function (r) {
|
|
7171
|
+
if (r.ok) { toast('测试通知已发送,请查看系统通知栏'); return 'sent'; }
|
|
7172
|
+
if (r.timeout) { toast('通知通道未响应,已改用应用内提示'); return 'timeout'; }
|
|
7173
|
+
var msg = (r.v && (r.v.message || r.v.error)) || '未知错误';
|
|
7174
|
+
toast('测试通知发送失败:' + msg);
|
|
7175
|
+
return 'failed';
|
|
7176
|
+
});
|
|
7177
|
+
}
|
|
7178
|
+
if (notifyHasWeb()) {
|
|
7179
|
+
try {
|
|
7180
|
+
if (Notification.permission === 'granted') { new Notification('Magic Agent', { body: '系统通知工作正常' }); toast('测试通知已发送'); return 'web'; }
|
|
7181
|
+
return Notification.requestPermission().then(function (p2) {
|
|
7182
|
+
if (p2 === 'granted') { try { new Notification('Magic Agent', { body: '系统通知工作正常' }); } catch (e) {} }
|
|
7183
|
+
toast(p2 === 'granted' ? '测试通知已发送' : '未获得通知权限');
|
|
7184
|
+
return 'web';
|
|
7185
|
+
}, function () { toast('未获得通知权限'); return 'toast'; });
|
|
7186
|
+
} catch (e) {}
|
|
7187
|
+
}
|
|
7188
|
+
toast('当前没有可用的系统通知通道,已改用应用内提示');
|
|
7189
|
+
return Promise.resolve('toast');
|
|
7190
|
+
}
|
|
5093
7191
|
function openNotifySettings() {
|
|
5094
|
-
openHostSheet('
|
|
7192
|
+
openHostSheet('消息通知', function (body) {
|
|
5095
7193
|
var st = notifyPermState();
|
|
5096
|
-
|
|
5097
|
-
hRow(body, '
|
|
7194
|
+
/* ① 系统通知(权限行):状态一眼可见,点了去授权;授权完重开本页刷新状态。 */
|
|
7195
|
+
hRow(body, '系统通知', notifyStateLabel(), function () {
|
|
7196
|
+
requestNotifyPermission().then(function () { openNotifySettings(); });
|
|
7197
|
+
});
|
|
7198
|
+
/* 传输层不通时把原因说清楚:用户点「去授权」什么也不会发生,
|
|
7199
|
+
没有这一行就只能对着一个永远不变的「未开启」发呆。 */
|
|
7200
|
+
if (st === 'dead') {
|
|
7201
|
+
var tip = elSpan('pick__d', '系统通知通道不可用(App 与桌面未连通),将使用应用内提示');
|
|
7202
|
+
tip.style.display = 'block'; tip.style.margin = '2px 0 6px';
|
|
7203
|
+
body.appendChild(tip);
|
|
7204
|
+
}
|
|
7205
|
+
/* ② 通知内容开关:用户来这个页面的主要目的,放前面。
|
|
7206
|
+
⚠️ 「定时任务通知」开关撤掉(2026-10-25 用户定调):定时任务功能本身不存在
|
|
7207
|
+
(tab 已删、core 无调度 API,notify('sched') 0 处调用),开关是假承诺。
|
|
7208
|
+
NOTIFY.sched 数据字段保留 —— 真做了定时任务时开关挂回来即可。 */
|
|
5098
7209
|
nToggle(body, '任务结果通知', NOTIFY.result, function (v) { NOTIFY.result = v; saveNotify(); });
|
|
5099
7210
|
nToggle(body, '任务待确认通知', NOTIFY.confirm, function (v) { NOTIFY.confirm = v; saveNotify(); });
|
|
5100
|
-
|
|
7211
|
+
/* 「发一条测试通知」「通道诊断」两个入口 2026-10-25 用户定调撤掉(通道已通,
|
|
7212
|
+
正式 UI 不需要调试工具)。底层 notifyTest()/notifyDiagnose() 保留 ——
|
|
7213
|
+
通道再出问题时把它们挂回任意入口就能当场自证。 */
|
|
5101
7214
|
});
|
|
5102
7215
|
}
|
|
5103
7216
|
|
|
@@ -5123,32 +7236,126 @@ button.setrow{font-family:inherit}
|
|
|
5123
7236
|
}).catch(function () {});
|
|
5124
7237
|
}
|
|
5125
7238
|
|
|
7239
|
+
/* 思考模式选择器:写**当前这条会话**的档位(默认最高)。 */
|
|
7240
|
+
function openReasoningPicker() {
|
|
7241
|
+
openHostSheet('思考模式', function (body) {
|
|
7242
|
+
REASONING_LEVELS.forEach(function (it) {
|
|
7243
|
+
hRow(body, it.label, it.desc, function () {
|
|
7244
|
+
writeReasoningFor(it.v);
|
|
7245
|
+
closeHostSheet();
|
|
7246
|
+
paintRunSettings();
|
|
7247
|
+
toast('思考模式:' + it.label);
|
|
7248
|
+
}, false, REASONING_CUR === it.v);
|
|
7249
|
+
});
|
|
7250
|
+
var eng = (conv && conv.state && conv.state.engine) || '';
|
|
7251
|
+
var sup = reasoningSupportState(eng);
|
|
7252
|
+
var hint = hEl('div', 'history__hint',
|
|
7253
|
+
sup === false
|
|
7254
|
+
? (eng + ' 没有推理档位参数,core 会静默忽略;换 claude / codebuddy / codex 等引擎才生效。')
|
|
7255
|
+
: (sup === null
|
|
7256
|
+
? '发送时按引擎能力自动判断:支持的引擎下发此档位,不支持的沿用引擎默认。'
|
|
7257
|
+
: '只对这一条会话生效,默认最高。'));
|
|
7258
|
+
hint.style.textAlign = 'left';
|
|
7259
|
+
body.appendChild(hint);
|
|
7260
|
+
});
|
|
7261
|
+
}
|
|
5126
7262
|
/* 工作空间选择:默认 / 根目录下 magic-* / 自定义 / 会话里出现过的,一行一个,外加「自定义路径…」。
|
|
5127
7263
|
⚠️ 当前生效的那一条要打勾(hRow 的 selected):抽屉里没有任何选中标记时,
|
|
5128
7264
|
点完「默认」看不出变化,用户无从判断到底选没选上(2026-10-21 报「无法选择默认」)。
|
|
5129
7265
|
⚠️ 首项文案 = 「默认(不属于任何空间)」:默认的语义是**不归属任何空间目录**,
|
|
5130
|
-
不是「用某个默认目录」。写「不指定」容易被读成「没选」,写目录名更会被读成「选了那个目录」。
|
|
7266
|
+
不是「用某个默认目录」。写「不指定」容易被读成「没选」,写目录名更会被读成「选了那个目录」。
|
|
7267
|
+
⚠️ 一律显示**短名**(别名 > 最后一级文件夹名),不显示路径:路径一长就撑爆一行,
|
|
7268
|
+
而且用户认的是「哪个项目」,不是「哪个绝对路径」。 */
|
|
5131
7269
|
function openWorkspacePicker() {
|
|
5132
7270
|
var keys = collectSpaceKeys();
|
|
5133
7271
|
openHostSheet('选择工作空间', function (body) {
|
|
5134
7272
|
hRow(body, '默认(不属于任何空间)', null, function () { WS = ''; saveWs(); applyWs(); closeHostSheet(); paintRunSettings(); }, false, !WS);
|
|
5135
7273
|
keys.forEach(function (w) {
|
|
5136
|
-
var
|
|
5137
|
-
|
|
7274
|
+
var it = customSpaceOf(w);
|
|
7275
|
+
var isCustom = !!it;
|
|
7276
|
+
/* 打勾要比**空间键**而不是原字符串:WS 可能是用户手输的「D:/work/proj/」,
|
|
7277
|
+
列表里这一行却是服务端登记的「D:\work\proj」—— 字面不等,勾就打不上,
|
|
7278
|
+
用户会以为「点了没生效」(2026-10-21 报「无法选择默认」是同一类毛病)。 */
|
|
7279
|
+
hRow(body, spaceLabel(w), isCustom ? '自定义' : null, function () { WS = w; saveWs(); applyWs(); closeHostSheet(); paintRunSettings(); }, false, !!WS && spaceKey(WS) === spaceKey(w),
|
|
7280
|
+
/* 自定义空间行尾两枚动作:「✎」起别名、「✕」删除(行本身仍是「选中这个空间」)。
|
|
7281
|
+
删除只把它从自定义清单里摘掉,**不动磁盘上的目录**;如果当前生效的 WS
|
|
7282
|
+
正是它,回落到「默认」(不然运行设置页还挂着一条已删空间的残影)。 */
|
|
7283
|
+
isCustom ? [
|
|
7284
|
+
{ label: '✎', title: '设置别名', onClick: function () { openSpaceAliasSheet(it); } },
|
|
7285
|
+
{ label: '✕', title: '删除空间', onClick: function () { openSpaceDeleteSheet(it); } }
|
|
7286
|
+
] : null);
|
|
5138
7287
|
});
|
|
5139
7288
|
hRow(body, '自定义路径…', '+', openCustomPathSheet);
|
|
5140
7289
|
});
|
|
5141
7290
|
}
|
|
5142
|
-
/*
|
|
7291
|
+
/* 自定义空间的别名:给目录起一个**显示名**,之后各处显示它而不是路径。
|
|
7292
|
+
留空 = 回到「最后一级文件夹名」(见 spaceLabel)。 */
|
|
7293
|
+
function openSpaceAliasSheet(item) {
|
|
7294
|
+
openTextInputSheet({
|
|
7295
|
+
title: '空间别名',
|
|
7296
|
+
value: item.alias || '',
|
|
7297
|
+
placeholder: '留空 = 用文件夹名',
|
|
7298
|
+
allowEmpty: true
|
|
7299
|
+
}, function (alias) {
|
|
7300
|
+
item.alias = String(alias || '').trim();
|
|
7301
|
+
saveSpaces();
|
|
7302
|
+
/* 别名一改,所有显示它的地方都要跟着变:运行设置页 / 空间选择器 / 任务页分组头。 */
|
|
7303
|
+
paintRunSettings();
|
|
7304
|
+
renderSessions(LAST_SESSIONS);
|
|
7305
|
+
openWorkspacePicker(); // 回到选择器,让用户当场看见新名字
|
|
7306
|
+
});
|
|
7307
|
+
}
|
|
7308
|
+
/* 删除自定义空间:二次确认后从清单摘掉(**只删登记,不删目录** —— 磁盘上的文件
|
|
7309
|
+
原样不动,用户的代码/文档不能因为一个 UI 操作消失)。当前生效的 WS 正是它时
|
|
7310
|
+
回落「默认」,并当场把对话组件的 workspace 也清掉(否则下一轮还会带着已删空间发)。 */
|
|
7311
|
+
function openSpaceDeleteSheet(item) {
|
|
7312
|
+
openHostSheet('删除空间', function (body) {
|
|
7313
|
+
var tip = hEl('div', 'pick__d', '从空间列表移除「' + spaceLabel(item.path) + '」?目录里的文件不会被删除。');
|
|
7314
|
+
tip.style.textAlign = 'left';
|
|
7315
|
+
tip.style.lineHeight = '1.6';
|
|
7316
|
+
body.appendChild(tip);
|
|
7317
|
+
var del = hEl('button', 'btn btn--filled', '删除'); del.type = 'button';
|
|
7318
|
+
/* 危险按钮配色走主题的 error 三色组(浅色 = 红底浅粉字,深色 = 浅红底深红字),
|
|
7319
|
+
两套主题都成立 —— 不写死色值(这份 UI 有浅/深双主题)。 */
|
|
7320
|
+
del.style.background = 'var(--error-text)';
|
|
7321
|
+
del.style.borderColor = 'var(--error-text)';
|
|
7322
|
+
del.style.color = 'var(--error-surface)';
|
|
7323
|
+
body.appendChild(del);
|
|
7324
|
+
del.onclick = function () {
|
|
7325
|
+
var k = spaceKey(item.path);
|
|
7326
|
+
CUSTOM_SPACES = CUSTOM_SPACES.filter(function (s) { return spaceKey(s.path) !== k; });
|
|
7327
|
+
saveSpaces();
|
|
7328
|
+
if (spaceKey(WS) === k) { WS = ''; saveWs(); applyWs(); }
|
|
7329
|
+
closeHostSheet();
|
|
7330
|
+
paintRunSettings();
|
|
7331
|
+
renderSessions(LAST_SESSIONS);
|
|
7332
|
+
openWorkspacePicker(); // 回到选择器,让用户当场看见它没了
|
|
7333
|
+
};
|
|
7334
|
+
});
|
|
7335
|
+
}
|
|
7336
|
+
/* 自定义路径:路径 + 可选的别名(复用单行输入抽屉,见 openTextInputSheet 上方说明)。 */
|
|
5143
7337
|
function openCustomPathSheet() {
|
|
5144
7338
|
openTextInputSheet({
|
|
5145
7339
|
title: '自定义路径',
|
|
5146
7340
|
value: SPACES_ROOT || DW || '',
|
|
5147
7341
|
placeholder: '/绝对路径',
|
|
5148
|
-
emptyHint: '请输入工作空间的绝对路径'
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
7342
|
+
emptyHint: '请输入工作空间的绝对路径',
|
|
7343
|
+
secondary: { label: '别名(可选)', placeholder: '留空 = 用文件夹名' }
|
|
7344
|
+
}, function (p, alias) {
|
|
7345
|
+
var path = String(p || '').trim();
|
|
7346
|
+
if (!path) return;
|
|
7347
|
+
/* 落库用**归一后的**路径(统一分隔符 + 去尾斜杠):与服务端登记的 workspace
|
|
7348
|
+
同一口径,否则同一个目录会以两种写法各存一份(列表里出现两行重复项),
|
|
7349
|
+
而且别名贴不上 —— 「D:/work/proj」与「D:\work\proj」是同一个目录。 */
|
|
7350
|
+
path = reqNormDir(path);
|
|
7351
|
+
if (!path) return;
|
|
7352
|
+
var it = customSpaceOf(path);
|
|
7353
|
+
if (!it) { it = { path: path, alias: '' }; CUSTOM_SPACES.push(it); }
|
|
7354
|
+
/* 别名可以留空 = 回到「最后一级文件夹名」(见 spaceLabel);
|
|
7355
|
+
但已经起过名的空间,再次走这里**不要**把别名抹掉(否则改个路径就把名字弄丢了)。 */
|
|
7356
|
+
if (String(alias || '').trim()) it.alias = String(alias).trim();
|
|
7357
|
+
saveSpaces();
|
|
7358
|
+
WS = path; saveWs(); applyWs(); loadSessions(); paintRunSettings();
|
|
5152
7359
|
});
|
|
5153
7360
|
}
|
|
5154
7361
|
|
|
@@ -5161,58 +7368,84 @@ button.setrow{font-family:inherit}
|
|
|
5161
7368
|
不能回落到 RUN_PREF.engine —— 那等于「配了默认编码引擎就自动有建议」,
|
|
5162
7369
|
与用户「默认不配置就不开启」相反。 */
|
|
5163
7370
|
function loadSuggestPref() {
|
|
5164
|
-
var o = { engine: '', model: '' };
|
|
7371
|
+
var o = { engine: '', model: '', fbEngine: '', fbModel: '' };
|
|
5165
7372
|
try {
|
|
5166
7373
|
o.engine = String(localStorage.getItem('magic.suggest.engine') || '').trim();
|
|
5167
7374
|
o.model = String(localStorage.getItem('magic.suggest.model') || '').trim();
|
|
7375
|
+
o.fbEngine = String(localStorage.getItem('magic.suggest.fallback.engine') || '').trim();
|
|
7376
|
+
o.fbModel = String(localStorage.getItem('magic.suggest.fallback.model') || '').trim();
|
|
5168
7377
|
} catch (e) {}
|
|
5169
7378
|
if (!o.engine) o.model = '';
|
|
7379
|
+
if (!o.fbEngine) o.fbModel = '';
|
|
5170
7380
|
return o;
|
|
5171
7381
|
}
|
|
5172
7382
|
function saveSuggestPref() {
|
|
5173
7383
|
try {
|
|
5174
7384
|
localStorage.setItem('magic.suggest.engine', SUGGEST.engine || '');
|
|
5175
7385
|
localStorage.setItem('magic.suggest.model', SUGGEST.model || '');
|
|
7386
|
+
localStorage.setItem('magic.suggest.fallback.engine', SUGGEST.fbEngine || '');
|
|
7387
|
+
localStorage.setItem('magic.suggest.fallback.model', SUGGEST.fbModel || '');
|
|
5176
7388
|
} catch (e) {}
|
|
5177
7389
|
schedulePushPrefs();
|
|
5178
7390
|
paintSuggestPref();
|
|
5179
7391
|
}
|
|
5180
7392
|
function paintSuggestPref() {
|
|
5181
|
-
|
|
5182
|
-
var
|
|
5183
|
-
|
|
5184
|
-
|
|
7393
|
+
if (!document.getElementById('suggestEngine')) return;
|
|
7394
|
+
var main = SUGGEST.engine
|
|
7395
|
+
? (SUGGEST.engine + (SUGGEST.model ? ' · ' + qaModelText(SUGGEST.model, SUGGEST.engine) : ''))
|
|
7396
|
+
: '不开启';
|
|
7397
|
+
paintEnginePair(document.getElementById('suggestEngine'), main, fallbackLine(SUGGEST.fbEngine, SUGGEST.fbModel));
|
|
5185
7398
|
}
|
|
5186
7399
|
function applySuggestToLive() {
|
|
5187
7400
|
if (conv && typeof conv.setSuggestEngine === 'function') {
|
|
5188
|
-
conv.setSuggestEngine(SUGGEST.engine, SUGGEST.model);
|
|
7401
|
+
conv.setSuggestEngine(SUGGEST.engine, SUGGEST.model, SUGGEST.fbEngine, SUGGEST.fbModel);
|
|
5189
7402
|
}
|
|
5190
7403
|
paintSuggestPref();
|
|
5191
7404
|
paintRunSettings();
|
|
5192
7405
|
}
|
|
5193
7406
|
|
|
5194
7407
|
function loadRunPref() {
|
|
5195
|
-
var o = { engine: '', model: '' };
|
|
7408
|
+
var o = { engine: '', model: '', fbEngine: '', fbModel: '' };
|
|
5196
7409
|
try {
|
|
5197
7410
|
o.engine = localStorage.getItem('magic.engine') || '';
|
|
5198
7411
|
o.model = localStorage.getItem('magic.model') || '';
|
|
7412
|
+
o.fbEngine = String(localStorage.getItem('magic.fallback.engine') || '').trim();
|
|
7413
|
+
o.fbModel = String(localStorage.getItem('magic.fallback.model') || '').trim();
|
|
5199
7414
|
} catch (e) {}
|
|
5200
7415
|
if (!o.engine) o.model = '';
|
|
7416
|
+
if (!o.fbEngine) o.fbModel = '';
|
|
5201
7417
|
return o;
|
|
5202
7418
|
}
|
|
5203
7419
|
function saveRunPref() {
|
|
5204
7420
|
try {
|
|
5205
7421
|
localStorage.setItem('magic.engine', RUN_PREF.engine || '');
|
|
5206
7422
|
localStorage.setItem('magic.model', RUN_PREF.model || '');
|
|
7423
|
+
localStorage.setItem('magic.fallback.engine', RUN_PREF.fbEngine || '');
|
|
7424
|
+
localStorage.setItem('magic.fallback.model', RUN_PREF.fbModel || '');
|
|
5207
7425
|
} catch (e) {}
|
|
5208
7426
|
schedulePushPrefs();
|
|
5209
7427
|
paintRunPref();
|
|
5210
7428
|
}
|
|
5211
7429
|
function paintRunPref() {
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
7430
|
+
if (!document.getElementById('defaultEngine')) return;
|
|
7431
|
+
/* 不再有「自动 / 引擎默认」这两档可选项:没选模型就只显示引擎名,空值写成「未设置」。 */
|
|
7432
|
+
var main = RUN_PREF.engine
|
|
7433
|
+
? (RUN_PREF.engine + (RUN_PREF.model ? ' · ' + qaModelText(RUN_PREF.model, RUN_PREF.engine) : ''))
|
|
7434
|
+
: '未设置';
|
|
7435
|
+
paintEnginePair(document.getElementById('defaultEngine'), main, fallbackLine(RUN_PREF.fbEngine, RUN_PREF.fbModel));
|
|
7436
|
+
}
|
|
7437
|
+
/* 备用那一行(第二行,小一档):没配就空串 = 不画。
|
|
7438
|
+
⚠️ 不要和主引擎挤在同一行 —— `setrow__v` 是单行省略,长组合会被截成
|
|
7439
|
+
「主 | …」,用户根本看不到备用(2026-10-24 报障)。 */
|
|
7440
|
+
function fallbackLine(engine, model) {
|
|
7441
|
+
if (!engine) return '';
|
|
7442
|
+
return '备 ' + engine + (model ? ' · ' + qaModelText(model, engine) : '');
|
|
7443
|
+
}
|
|
7444
|
+
function paintEnginePair(el, mainText, fbText) {
|
|
7445
|
+
if (!el) return;
|
|
7446
|
+
el.innerHTML = '';
|
|
7447
|
+
el.appendChild(hEl('span', 'setrow__pair-main', mainText));
|
|
7448
|
+
if (fbText) el.appendChild(hEl('span', 'setrow__pair-fb', fbText));
|
|
5216
7449
|
}
|
|
5217
7450
|
|
|
5218
7451
|
/* 团队模式配置(「我的」)══════════
|
|
@@ -5229,10 +7462,11 @@ button.setrow{font-family:inherit}
|
|
|
5229
7462
|
/* 摘要与子页面同一口径:计划 + 7 个角色里单独配过引擎**或规则**的项数。
|
|
5230
7463
|
旧版按类别配过的路由仍然生效(core 仍认),但不在界面上重复计数。 */
|
|
5231
7464
|
var custom = 0;
|
|
5232
|
-
|
|
7465
|
+
var planRt = (TEAM.routes || {}).plan || {};
|
|
7466
|
+
if (planRt.engine || planRt.fallbackEngine) custom++;
|
|
5233
7467
|
teamRoleList().forEach(function (r) {
|
|
5234
7468
|
var rt = (TEAM.routes || {})['role:' + r.name];
|
|
5235
|
-
if ((rt && rt.engine) || ((TEAM.rules || {})['role:' + r.name])) custom++;
|
|
7469
|
+
if ((rt && (rt.engine || rt.fallbackEngine)) || ((TEAM.rules || {})['role:' + r.name])) custom++;
|
|
5236
7470
|
});
|
|
5237
7471
|
paintTeamRoles(teamRoleList());
|
|
5238
7472
|
var sum = document.getElementById('teamSummary');
|
|
@@ -5250,10 +7484,12 @@ button.setrow{font-family:inherit}
|
|
|
5250
7484
|
var planSub = '拆解:' + ({ coarse: '少', standard: '标准', fine: '细' }[TEAM.split] || '标准')
|
|
5251
7485
|
+ (TEAM.adaptive !== false ? ' · 自动补任务' : '')
|
|
5252
7486
|
+ (TEAM.splitCode ? ' · 简单代码也拆' : '');
|
|
5253
|
-
var sig = (show ? '1' : '0') + '|plan|' + planSub + '|' + (plan.engine || '') + '|' + (plan.model || '')
|
|
7487
|
+
var sig = (show ? '1' : '0') + '|plan|' + planSub + '|' + (plan.engine || '') + '|' + (plan.model || '')
|
|
7488
|
+
+ '|' + (plan.fallbackEngine || '') + '|' + (plan.fallbackModel || '') + '|'
|
|
5254
7489
|
+ roles.map(function (r) {
|
|
5255
7490
|
var rt = (TEAM.routes || {})['role:' + r.name] || {};
|
|
5256
|
-
return r.name + '|' + teamRoleDuty(r) + '|' + (rt.engine || '') + '|' + (rt.model || '')
|
|
7491
|
+
return r.name + '|' + teamRoleDuty(r) + '|' + (rt.engine || '') + '|' + (rt.model || '')
|
|
7492
|
+
+ '|' + (rt.fallbackEngine || '') + '|' + (rt.fallbackModel || '');
|
|
5257
7493
|
}).join(',');
|
|
5258
7494
|
if (sig === TEAM_ROLE_SIG) return;
|
|
5259
7495
|
TEAM_ROLE_SIG = sig;
|
|
@@ -5277,8 +7513,14 @@ button.setrow{font-family:inherit}
|
|
|
5277
7513
|
row.appendChild(main);
|
|
5278
7514
|
var v = hEl('button', 'setrow__vbtn');
|
|
5279
7515
|
v.type = 'button';
|
|
5280
|
-
|
|
5281
|
-
|
|
7516
|
+
/* 主备都显示:主一行、备一行(与「我的」默认引擎那两行同构)。
|
|
7517
|
+
挤成一行会被省略号截断,备用等于没显示(2026-10-24 报障)。 */
|
|
7518
|
+
var mainTxt = rt.engine ? (rt.engine + (rt.model ? ' · ' + qaModelText(rt.model, rt.engine) : '')) : '跟随主引擎';
|
|
7519
|
+
var fbTxt = fallbackLine(rt.fallbackEngine, rt.fallbackModel);
|
|
7520
|
+
var val = hEl('span', 'setrow__v setrow__v--pair');
|
|
7521
|
+
val.appendChild(hEl('span', 'setrow__pair-main', mainTxt));
|
|
7522
|
+
if (fbTxt) val.appendChild(hEl('span', 'setrow__pair-fb', fbTxt));
|
|
7523
|
+
v.appendChild(val);
|
|
5282
7524
|
v.appendChild(hEl('span', 'setrow__chev', '›'));
|
|
5283
7525
|
v.onclick = function () {
|
|
5284
7526
|
openTeamEnginePicker(key,
|
|
@@ -5306,7 +7548,6 @@ button.setrow{font-family:inherit}
|
|
|
5306
7548
|
这里问的是「这类活谁擅长」,所以不写「交给 core 自动路由」—— 说「自动」会让人以为
|
|
5307
7549
|
不配也能跑,而实际语义是「跟主引擎一致」。 */
|
|
5308
7550
|
function openTeamEnginePicker(key, title, kind) {
|
|
5309
|
-
var isRole = String(key).indexOf('role:') === 0;
|
|
5310
7551
|
if (!kind) kind = key; // 计划键时 kind 就是键本身
|
|
5311
7552
|
openHostSheet(title, function (body) {
|
|
5312
7553
|
var hint = hEl('div', 'history__hint', '正在探测引擎与模型…');
|
|
@@ -5314,7 +7555,7 @@ button.setrow{font-family:inherit}
|
|
|
5314
7555
|
body.appendChild(hint);
|
|
5315
7556
|
fetchEngines().then(function (list) {
|
|
5316
7557
|
if (hint.parentNode) hint.remove();
|
|
5317
|
-
var avail = list
|
|
7558
|
+
var avail = pickableEngines(list);
|
|
5318
7559
|
if (!avail.length) {
|
|
5319
7560
|
var d = hEl('div', 'history__hint', '没有可用引擎:先在桌面上装一个(magic-agent --engines 会给出安装命令)。');
|
|
5320
7561
|
d.style.textAlign = 'left';
|
|
@@ -5323,54 +7564,93 @@ button.setrow{font-family:inherit}
|
|
|
5323
7564
|
}
|
|
5324
7565
|
var stale = enginesStaleHint();
|
|
5325
7566
|
if (stale) body.appendChild(stale);
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
7567
|
+
/* 主 / 备两档共用一套「引擎 → 模型」级联:顶上分段切目标,下面的点击落到
|
|
7568
|
+
当前那一档(2026-10-24 用户:「所有引擎设置都允许设置主备两个」)。 */
|
|
7569
|
+
var mode = 'main';
|
|
7570
|
+
var seg = hEl('span', 'seg');
|
|
7571
|
+
seg.style.margin = '0 12px 8px';
|
|
7572
|
+
[['main', '主引擎'], ['fallback', '备用']].forEach(function (p) {
|
|
7573
|
+
var b = hEl('button', 'seg__b' + (mode === p[0] ? ' is-on' : ''), p[1]);
|
|
7574
|
+
b.type = 'button';
|
|
7575
|
+
b.setAttribute('data-v', p[0]);
|
|
7576
|
+
b.onclick = function () {
|
|
7577
|
+
mode = p[0];
|
|
7578
|
+
/* 换档 = 从「已保存的那一个」重新开始浏览(不沿用另一档的浏览位置)。 */
|
|
7579
|
+
browse[mode] = '';
|
|
7580
|
+
markSeg(seg, mode);
|
|
7581
|
+
paint();
|
|
7582
|
+
};
|
|
7583
|
+
seg.appendChild(b);
|
|
7584
|
+
});
|
|
7585
|
+
body.appendChild(seg);
|
|
5338
7586
|
var wrap = hEl('div', 'pick__cols');
|
|
5339
7587
|
var left = hEl('div', 'pick__col');
|
|
5340
7588
|
var right = hEl('div', 'pick__col pick__col--r');
|
|
5341
7589
|
wrap.appendChild(left); wrap.appendChild(right); body.appendChild(wrap);
|
|
7590
|
+
var curEng = avail[0];
|
|
7591
|
+
/* 每一档「此刻在浏览哪个引擎」:点左列只切右列(还没落定),不能因为重画
|
|
7592
|
+
又跳回已保存的那一个 —— 否则点备用后选任何引擎都会被立刻弹回去,
|
|
7593
|
+
表现就是「备用选不了」(2026-10-24 报障)。 */
|
|
7594
|
+
var browse = { main: '', fallback: '' };
|
|
7595
|
+
function cur() {
|
|
7596
|
+
var rt = (TEAM.routes || {})[key] || {};
|
|
7597
|
+
if (mode === 'fallback') return { engine: rt.fallbackEngine || '', model: rt.fallbackModel || '' };
|
|
7598
|
+
return { engine: rt.engine || '', model: rt.model || '' };
|
|
7599
|
+
}
|
|
7600
|
+
function pick(engine, model) {
|
|
7601
|
+
/* 选完**不关**抽屉:主 / 备在同一屏里一次配完(2026-10-24 用户口径)。 */
|
|
7602
|
+
if (mode === 'fallback') setTeamFallback(key, engine, model, true);
|
|
7603
|
+
else setTeamRoute(key, engine, model, true);
|
|
7604
|
+
browse[mode] = engine || ''; // 清空后回到默认浏览位置
|
|
7605
|
+
paint();
|
|
7606
|
+
}
|
|
5342
7607
|
function paint() {
|
|
5343
7608
|
left.innerHTML = ''; right.innerHTML = '';
|
|
5344
|
-
var
|
|
7609
|
+
var c = cur();
|
|
7610
|
+
var want = browse[mode] || c.engine;
|
|
7611
|
+
curEng = avail.filter(function (en) { return en.engine === want; })[0] || avail[0];
|
|
7612
|
+
browse[mode] = curEng.engine;
|
|
7613
|
+
var offLabel = mode === 'fallback' ? '不设备用' : '跟随主引擎';
|
|
7614
|
+
var offDesc = mode === 'fallback'
|
|
7615
|
+
? (kind === 'plan' ? '计划阶段跟随主引擎,失败不降级' : '与主引擎一致,失败不降级')
|
|
7616
|
+
: (kind === 'plan' ? '计划阶段走主引擎' : '不单独指定,和主引擎一致');
|
|
7617
|
+
var auto = hEl('button', 'pick__citem' + (!c.engine ? ' is-on' : ''));
|
|
5345
7618
|
auto.type = 'button';
|
|
5346
|
-
auto.appendChild(hEl('span', 'pick__cl',
|
|
5347
|
-
auto.appendChild(hEl('span', 'pick__cd',
|
|
5348
|
-
auto.onclick = function () {
|
|
7619
|
+
auto.appendChild(hEl('span', 'pick__cl', offLabel));
|
|
7620
|
+
auto.appendChild(hEl('span', 'pick__cd', offDesc));
|
|
7621
|
+
auto.onclick = function () { pick('', ''); };
|
|
5349
7622
|
left.appendChild(auto);
|
|
5350
7623
|
|
|
5351
7624
|
avail.forEach(function (en) {
|
|
5352
7625
|
var b = hEl('button', 'pick__citem'
|
|
5353
|
-
+ (
|
|
5354
|
-
+ (en.engine === curEng.engine &&
|
|
7626
|
+
+ (c.engine === en.engine ? ' is-on' : '')
|
|
7627
|
+
+ (en.engine === curEng.engine && c.engine !== en.engine ? ' is-cur' : ''));
|
|
5355
7628
|
b.type = 'button';
|
|
5356
7629
|
b.appendChild(hEl('span', 'pick__cl', en.engine));
|
|
5357
|
-
b.appendChild(hEl('span', 'pick__cd', (en
|
|
5358
|
-
|
|
7630
|
+
b.appendChild(hEl('span', 'pick__cd', engineSubLine(en)));
|
|
7631
|
+
/* 账号剩余积分(codebuddy 系才有 credit_left,见 core --engines)。 */
|
|
7632
|
+
if (en.credit_left) {
|
|
7633
|
+
b.appendChild(hEl('span', 'pick__cd pick__cd--credit', '剩余 ' + en.credit_left + (en.credit_suffix || ' 积分')));
|
|
7634
|
+
}
|
|
7635
|
+
/* 每日积分:没领就画「领取」(点它只领积分,不切引擎);已领显示状态。 */
|
|
7636
|
+
if (en.checkin_today === false) {
|
|
7637
|
+
var chip = hEl('span', 'pick__cd pick__cd--credit pick__claim',
|
|
7638
|
+
en._claimBusy ? '领取中…' : ('领取 ' + (en.checkin_credit || '') + ' 积分'));
|
|
7639
|
+
chip.addEventListener('click', function (ev) {
|
|
7640
|
+
ev.preventDefault(); ev.stopPropagation();
|
|
7641
|
+
claimCheckinFor(en, paint);
|
|
7642
|
+
});
|
|
7643
|
+
b.appendChild(chip);
|
|
7644
|
+
}
|
|
7645
|
+
b.onclick = function () { browse[mode] = en.engine; paint(); };
|
|
5359
7646
|
left.appendChild(b);
|
|
5360
7647
|
});
|
|
5361
7648
|
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
d.appendChild(hEl('span', 'pick__cl', '引擎默认'));
|
|
5365
|
-
d.appendChild(hEl('span', 'pick__cd', '不指定模型,由引擎自己挑'));
|
|
5366
|
-
d.onclick = function () { setTeamRoute(key, curEng.engine, ''); };
|
|
5367
|
-
right.appendChild(d);
|
|
5368
|
-
|
|
5369
|
-
(curEng.models || []).forEach(function (mm) {
|
|
5370
|
-
var b = hEl('button', 'pick__citem' + (cur.engine === curEng.engine && cur.model === mm ? ' is-on' : ''));
|
|
7649
|
+
qaModelsOrdered(ENGINES, curEng.engine, curEng.models || []).forEach(function (mm) {
|
|
7650
|
+
var b = hEl('button', 'pick__citem' + (c.engine === curEng.engine && c.model === mm ? ' is-on' : ''));
|
|
5371
7651
|
b.type = 'button';
|
|
5372
7652
|
modelItem(b, curEng.engine, mm);
|
|
5373
|
-
b.onclick = function () {
|
|
7653
|
+
b.onclick = function () { pick(curEng.engine, mm); };
|
|
5374
7654
|
right.appendChild(b);
|
|
5375
7655
|
});
|
|
5376
7656
|
}
|
|
@@ -5474,18 +7754,54 @@ button.setrow{font-family:inherit}
|
|
|
5474
7754
|
row.appendChild(t);
|
|
5475
7755
|
return row;
|
|
5476
7756
|
}
|
|
5477
|
-
function setTeamRoute(key, engine, model) {
|
|
7757
|
+
function setTeamRoute(key, engine, model, keepOpen) {
|
|
5478
7758
|
TEAM.routes = TEAM.routes || {};
|
|
5479
|
-
|
|
5480
|
-
|
|
7759
|
+
var prev = TEAM.routes[key] || {};
|
|
7760
|
+
var next = {};
|
|
7761
|
+
if (engine) {
|
|
7762
|
+
next.engine = engine;
|
|
7763
|
+
if (model) next.model = model;
|
|
7764
|
+
}
|
|
7765
|
+
/* 主备是两份独立配置:改主引擎不能把已经配好的备用一起清掉。 */
|
|
7766
|
+
if (prev.fallbackEngine) {
|
|
7767
|
+
next.fallbackEngine = prev.fallbackEngine;
|
|
7768
|
+
if (prev.fallbackModel) next.fallbackModel = prev.fallbackModel;
|
|
7769
|
+
}
|
|
7770
|
+
if (Object.keys(next).length) TEAM.routes[key] = next;
|
|
7771
|
+
else delete TEAM.routes[key];
|
|
5481
7772
|
saveTeamPref();
|
|
5482
7773
|
applyTeamToLive();
|
|
7774
|
+
if (keepOpen) return;
|
|
5483
7775
|
closeHostSheet();
|
|
5484
7776
|
var r = (TEAM.routes || {})[key];
|
|
5485
7777
|
var label = teamRouteLabel(key);
|
|
5486
|
-
toast(r ? (label + '交给 ' + r.engine + (r.model ? ' · ' + qaModelText(r.model) : ''))
|
|
7778
|
+
toast(r && r.engine ? (label + '交给 ' + r.engine + (r.model ? ' · ' + qaModelText(r.model, r.engine) : ''))
|
|
5487
7779
|
: (label + ':跟随主引擎'));
|
|
5488
7780
|
}
|
|
7781
|
+
/* 备用引擎(2026-10-24):主引擎失败且零产出时降级到这一路。
|
|
7782
|
+
与主引擎分开存,换主引擎不动备用;空 = 不降级。 */
|
|
7783
|
+
function setTeamFallback(key, engine, model, keepOpen) {
|
|
7784
|
+
TEAM.routes = TEAM.routes || {};
|
|
7785
|
+
var prev = TEAM.routes[key] || {};
|
|
7786
|
+
var next = {};
|
|
7787
|
+
if (prev.engine) {
|
|
7788
|
+
next.engine = prev.engine;
|
|
7789
|
+
if (prev.model) next.model = prev.model;
|
|
7790
|
+
}
|
|
7791
|
+
if (engine) {
|
|
7792
|
+
next.fallbackEngine = engine;
|
|
7793
|
+
if (model) next.fallbackModel = model;
|
|
7794
|
+
}
|
|
7795
|
+
if (Object.keys(next).length) TEAM.routes[key] = next;
|
|
7796
|
+
else delete TEAM.routes[key];
|
|
7797
|
+
saveTeamPref();
|
|
7798
|
+
applyTeamToLive();
|
|
7799
|
+
if (keepOpen) return;
|
|
7800
|
+
closeHostSheet();
|
|
7801
|
+
toast(engine
|
|
7802
|
+
? (teamRouteLabel(key) + '备用:' + engine + (model ? ' · ' + qaModelText(model, engine) : ''))
|
|
7803
|
+
: (teamRouteLabel(key) + ':不设备用(失败不降级)'));
|
|
7804
|
+
}
|
|
5489
7805
|
/* 配置改了要同步到活着的组件(组件每轮现读 teamConfig,改完下一轮就生效)。
|
|
5490
7806
|
⚠️ 这里**不碰开关**:开关是每条会话自己的状态,改「哪类任务交给谁」不该顺手把
|
|
5491
7807
|
当前会话的团队模式打开/关掉。 */
|
|
@@ -5511,13 +7827,20 @@ button.setrow{font-family:inherit}
|
|
|
5511
7827
|
/* 一行模型(左名字、右倍率):名字过长只挤自己,倍率列不会跟着抖
|
|
5512
7828
|
(`.pick__crow` 样式见 shared/conversation.css)。
|
|
5513
7829
|
倍率本身不在宿主里再算一遍 —— 直接用内核的 qaModelCredit(它认 core 原样
|
|
5514
|
-
snake_case 与 PC 归一后的 camelCase,且**没数据就返回空串**,不编「×1」)。
|
|
7830
|
+
snake_case 与 PC 归一后的 camelCase,且**没数据就返回空串**,不编「×1」)。
|
|
7831
|
+
dsh 补第二层路由名:同名模型可能挂在多条路由下,只给末段分不出来。 */
|
|
5515
7832
|
function modelItem(b, engine, id) {
|
|
5516
7833
|
var row = hEl('span', 'pick__crow');
|
|
5517
|
-
row.appendChild(hEl('span', 'pick__cl', qaModelText(id)));
|
|
7834
|
+
row.appendChild(hEl('span', 'pick__cl', qaModelText(id, engine)));
|
|
5518
7835
|
var credit = qaModelCredit(ENGINES, engine, id);
|
|
5519
7836
|
if (credit) row.appendChild(hEl('span', 'pick__cr', credit));
|
|
5520
7837
|
b.appendChild(row);
|
|
7838
|
+
/* 第二行:供应商(dsh 是路由名,其余引擎用 core 的 model_vendors)与套餐额度
|
|
7839
|
+
合并成一行,例如「MiniMax · 5h 100% · 周 0%」—— 用户口径:和供应商在一行即可。 */
|
|
7840
|
+
var quota = qaModelQuota(ENGINES, engine, id);
|
|
7841
|
+
var sub = qaModelRoute(id, engine) || qaModelVendor(ENGINES, engine, id);
|
|
7842
|
+
var extra = [sub, quota].filter(Boolean).join(' · ');
|
|
7843
|
+
if (extra) b.appendChild(hEl('span', 'pick__cd' + (quota ? ' pick__cd--credit' : ''), extra));
|
|
5521
7844
|
}
|
|
5522
7845
|
/* 引擎清单:走 client 的 /desk/engines(= core --engines 的原样 JSON,含 models)。
|
|
5523
7846
|
⚠️ 这个请求会真的起各引擎的只读子命令探测模型,慢(秒级),所以只在打开选择器时拉,
|
|
@@ -5554,6 +7877,65 @@ button.setrow{font-family:inherit}
|
|
|
5554
7877
|
throw e;
|
|
5555
7878
|
});
|
|
5556
7879
|
}
|
|
7880
|
+
/* 进选择器的引擎:探测成功、**已登录**且**能列出模型**。没登录 / 取不到清单的
|
|
7881
|
+
(如 trae、登出后的 codebuddy-ai)不出现 —— 「登录了几个账号就显示几个」
|
|
7882
|
+
(2026-10-25 用户要求)。 */
|
|
7883
|
+
function pickableEngines(list) {
|
|
7884
|
+
return (list || []).filter(function (en) {
|
|
7885
|
+
if (!en || !en.ok) return false;
|
|
7886
|
+
if (en.logged_in === false) return false;
|
|
7887
|
+
return (en.models || []).length > 0 || !en.models_note;
|
|
7888
|
+
});
|
|
7889
|
+
}
|
|
7890
|
+
/* 引擎行副标题:有模型报数量,否则「引擎默认」;多账号引擎前面带上登录的账号昵称
|
|
7891
|
+
(codebuddy 系的 account 字段),一眼看出这行是哪个号。 */
|
|
7892
|
+
function engineSubLine(en) {
|
|
7893
|
+
var n = (en.models || []).length;
|
|
7894
|
+
var head = n ? (n + ' 个模型') : '引擎默认';
|
|
7895
|
+
return en.account ? (en.account + ' · ' + head) : head;
|
|
7896
|
+
}
|
|
7897
|
+
/* 领取每日积分(codebuddy 系):走插件侧 /desk/checkin → core --checkin。
|
|
7898
|
+
业务失败(今天已领)也是 200,ok:false + message,按普通提示显示。 */
|
|
7899
|
+
function claimCheckin(engine) {
|
|
7900
|
+
var hdr = TOKEN
|
|
7901
|
+
? { 'X-Magic-Token': TOKEN, 'Content-Type': 'application/json' }
|
|
7902
|
+
: { 'Content-Type': 'application/json' };
|
|
7903
|
+
return fetch(BASE + '/desk/checkin', {
|
|
7904
|
+
method: 'POST', headers: hdr, body: JSON.stringify({ engine: engine })
|
|
7905
|
+
}).then(function (r) {
|
|
7906
|
+
return r.json().then(function (j) {
|
|
7907
|
+
if (!r.ok) {
|
|
7908
|
+
var err = new Error((j && (j.error || j.message)) || ('HTTP ' + r.status));
|
|
7909
|
+
err.status = r.status;
|
|
7910
|
+
throw err;
|
|
7911
|
+
}
|
|
7912
|
+
return j;
|
|
7913
|
+
});
|
|
7914
|
+
});
|
|
7915
|
+
}
|
|
7916
|
+
/* 点「领取」后就地更新这一行并重画(不重探整份引擎清单)。
|
|
7917
|
+
领取结果**不弹任何提示**(2026-10-25 用户要求):成功/已领只把状态翻成「今日已领」,
|
|
7918
|
+
失败静默恢复按钮。 */
|
|
7919
|
+
function claimCheckinFor(en, repaint) {
|
|
7920
|
+
if (en._claimBusy) return;
|
|
7921
|
+
en._claimBusy = true;
|
|
7922
|
+
repaint();
|
|
7923
|
+
claimCheckin(en.engine).then(function (res) {
|
|
7924
|
+
en._claimBusy = false;
|
|
7925
|
+
if (res && res.ok) {
|
|
7926
|
+
en.checkin_today = true;
|
|
7927
|
+
en.checkin_credit = '';
|
|
7928
|
+
if (res.credit_left) en.credit_left = res.credit_left;
|
|
7929
|
+
} else if (res && res.checked_today) {
|
|
7930
|
+
en.checkin_today = true;
|
|
7931
|
+
en.checkin_credit = '';
|
|
7932
|
+
}
|
|
7933
|
+
repaint();
|
|
7934
|
+
}, function () {
|
|
7935
|
+
en._claimBusy = false;
|
|
7936
|
+
repaint();
|
|
7937
|
+
});
|
|
7938
|
+
}
|
|
5557
7939
|
/* 缓存提示(stale-while-revalidate:插件立刻回上次的清单,同时在后台重探)。无缓存返回 null。 */
|
|
5558
7940
|
function enginesStaleHint() {
|
|
5559
7941
|
if (!ENGINES_STALE_AT) return null;
|
|
@@ -5566,12 +7948,13 @@ button.setrow{font-family:inherit}
|
|
|
5566
7948
|
el.style.textAlign = 'left';
|
|
5567
7949
|
return el;
|
|
5568
7950
|
}
|
|
5569
|
-
/*
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
7951
|
+
/* ── 引擎设置抽屉:**一次点击里配主 + 备**(2026-10-24 用户:「这样布局太浪费了,
|
|
7952
|
+
要在一次点击里面设置两个」)。
|
|
7953
|
+
顶部「主引擎 / 备用」分段;选完**不关抽屉**,切到另一档继续配,关掉即生效。
|
|
7954
|
+
默认引擎与建议引擎共用(「建议引擎」= 纯文本短任务,llm 放最前、空模型用 no-think 变体)。
|
|
7955
|
+
cfg: getMain/getFallback/setMain/setFallback + 两档的文案(见下面两个入口)。 */
|
|
7956
|
+
function openPairEngineSheet(title, cfg) {
|
|
7957
|
+
openHostSheet(title, function (body) {
|
|
5575
7958
|
body.innerHTML = '';
|
|
5576
7959
|
var hint = hEl('div', 'history__hint', '正在探测引擎与模型…');
|
|
5577
7960
|
hint.style.textAlign = 'left';
|
|
@@ -5580,13 +7963,8 @@ button.setrow{font-family:inherit}
|
|
|
5580
7963
|
if (hint.parentNode) hint.remove();
|
|
5581
7964
|
var stale = enginesStaleHint();
|
|
5582
7965
|
if (stale) body.appendChild(stale);
|
|
5583
|
-
var avail = (list
|
|
5584
|
-
|
|
5585
|
-
avail.sort(function (a, b) {
|
|
5586
|
-
if (a.engine === 'llm') return -1;
|
|
5587
|
-
if (b.engine === 'llm') return 1;
|
|
5588
|
-
return String(a.engine).localeCompare(String(b.engine));
|
|
5589
|
-
});
|
|
7966
|
+
var avail = pickableEngines(list);
|
|
7967
|
+
if (cfg.sort) avail.sort(cfg.sort);
|
|
5590
7968
|
if (!avail.length) {
|
|
5591
7969
|
var d = hEl('div', 'history__hint',
|
|
5592
7970
|
'没有可用引擎:先在这台机器上装一个(终端跑 magic-agent --engines 会给出安装命令)。');
|
|
@@ -5594,68 +7972,116 @@ button.setrow{font-family:inherit}
|
|
|
5594
7972
|
body.appendChild(d);
|
|
5595
7973
|
return;
|
|
5596
7974
|
}
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
7975
|
+
var mode = 'main';
|
|
7976
|
+
var seg = hEl('span', 'seg');
|
|
7977
|
+
seg.style.margin = '0 12px 8px';
|
|
7978
|
+
[['main', '主引擎'], ['fallback', '备用']].forEach(function (p) {
|
|
7979
|
+
var b = hEl('button', 'seg__b' + (mode === p[0] ? ' is-on' : ''), p[1]);
|
|
7980
|
+
b.type = 'button';
|
|
7981
|
+
b.setAttribute('data-v', p[0]);
|
|
7982
|
+
b.onclick = function () { mode = p[0]; markSeg(seg, mode); render(); };
|
|
7983
|
+
seg.appendChild(b);
|
|
5603
7984
|
});
|
|
5604
|
-
/*
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
7985
|
+
/* 两档都不再画说明文字(主/备引擎的说明、建议引擎的说明都不需要)。 */
|
|
7986
|
+
var holder = hEl('div', '');
|
|
7987
|
+
body.appendChild(seg);
|
|
7988
|
+
body.appendChild(holder);
|
|
7989
|
+
function render() {
|
|
7990
|
+
variantRender(holder, avail, mode, cfg, render);
|
|
7991
|
+
}
|
|
7992
|
+
render();
|
|
7993
|
+
if (cfg.footNote) {
|
|
7994
|
+
var nk = hEl('div', 'history__hint', cfg.footNote);
|
|
7995
|
+
nk.style.textAlign = 'left';
|
|
7996
|
+
body.appendChild(nk);
|
|
7997
|
+
}
|
|
5610
7998
|
}, function (e) {
|
|
5611
7999
|
hint.textContent = '拉取引擎清单失败:' + ((e && e.message) || e);
|
|
5612
8000
|
var retry = hEl('button', 'pick__citem', '重试');
|
|
5613
8001
|
retry.type = 'button';
|
|
5614
|
-
retry.onclick = function () {
|
|
8002
|
+
retry.onclick = function () { openPairEngineSheet(title, cfg); };
|
|
5615
8003
|
body.appendChild(retry);
|
|
5616
8004
|
});
|
|
5617
8005
|
});
|
|
5618
8006
|
}
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
:
|
|
8007
|
+
/* 一档的「引擎 → 模型」级联(主 / 备共用)。选完调 cfg.set*,然后整块重画
|
|
8008
|
+
(抽屉不关 —— 用户还要接着配另一档)。 */
|
|
8009
|
+
function variantRender(holder, avail, mode, cfg, again) {
|
|
8010
|
+
holder.innerHTML = '';
|
|
8011
|
+
var isMain = mode === 'main';
|
|
8012
|
+
var prefs = isMain ? cfg.getMain() : cfg.getFallback();
|
|
8013
|
+
buildEngineCascade(holder, avail, {
|
|
8014
|
+
prefs: prefs,
|
|
8015
|
+
pick: function (engine, model) {
|
|
8016
|
+
if (isMain) cfg.setMain(engine, model);
|
|
8017
|
+
else cfg.setFallback(engine, model);
|
|
8018
|
+
again();
|
|
8019
|
+
},
|
|
8020
|
+
offLabel: isMain ? cfg.mainOffLabel : cfg.fbOffLabel,
|
|
8021
|
+
offDesc: isMain ? cfg.mainOffDesc : cfg.fbOffDesc,
|
|
8022
|
+
modelDefaultDesc: isMain ? cfg.modelDesc : cfg.fbModelDesc,
|
|
8023
|
+
/* 「自动 / 引擎默认」只在**主档**按需去掉(备用档要保留「不设置」才能清空降级)。 */
|
|
8024
|
+
hideOff: isMain && !!cfg.hideMainOff,
|
|
8025
|
+
hideModelDefault: isMain && !!cfg.hideMainModelDefault
|
|
8026
|
+
});
|
|
8027
|
+
}
|
|
8028
|
+
function openSuggestEnginePicker() {
|
|
8029
|
+
openPairEngineSheet('建议引擎 / 模型', {
|
|
8030
|
+
sort: function (a, b) {
|
|
8031
|
+
if (a.engine === 'llm') return -1;
|
|
8032
|
+
if (b.engine === 'llm') return 1;
|
|
8033
|
+
return String(a.engine).localeCompare(String(b.engine));
|
|
8034
|
+
},
|
|
8035
|
+
getMain: function () { return { engine: SUGGEST.engine, model: SUGGEST.model }; },
|
|
8036
|
+
getFallback: function () { return { engine: SUGGEST.fbEngine, model: SUGGEST.fbModel }; },
|
|
8037
|
+
setMain: function (engine, model) {
|
|
8038
|
+
SUGGEST.engine = String(engine || '').trim();
|
|
8039
|
+
SUGGEST.model = (SUGGEST.engine && model) ? String(model).trim() : '';
|
|
8040
|
+
saveSuggestPref();
|
|
8041
|
+
applySuggestToLive();
|
|
8042
|
+
},
|
|
8043
|
+
setFallback: function (engine, model) {
|
|
8044
|
+
SUGGEST.fbEngine = String(engine || '').trim();
|
|
8045
|
+
SUGGEST.fbModel = (SUGGEST.fbEngine && model) ? String(model).trim() : '';
|
|
8046
|
+
saveSuggestPref();
|
|
8047
|
+
applySuggestToLive();
|
|
8048
|
+
},
|
|
8049
|
+
mainOffLabel: '不开启',
|
|
8050
|
+
mainOffDesc: '不生成下一轮建议(默认)',
|
|
8051
|
+
fbOffLabel: '不设置',
|
|
8052
|
+
fbOffDesc: '主建议引擎失败时直接落到 llm 兜底',
|
|
8053
|
+
modelDesc: '不指定模型(建议生成用 no-think 变体)',
|
|
8054
|
+
fbModelDesc: '建议备用引擎自己的默认模型'
|
|
8055
|
+
});
|
|
5628
8056
|
}
|
|
5629
8057
|
|
|
5630
8058
|
function openDefaultEnginePicker() {
|
|
5631
|
-
|
|
5632
|
-
function
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
}
|
|
5658
|
-
load();
|
|
8059
|
+
openPairEngineSheet('默认引擎 / 模型', {
|
|
8060
|
+
sort: function (a, b) { return String(a.engine).localeCompare(String(b.engine)); },
|
|
8061
|
+
/* 默认引擎不再提供「自动(首个可用)」与「引擎默认」两项:用户直接选引擎 + 模型
|
|
8062
|
+
(2026-10-24 用户:「引擎不需要自动 模型不需要引擎默认」)。 */
|
|
8063
|
+
hideMainOff: true,
|
|
8064
|
+
hideMainModelDefault: true,
|
|
8065
|
+
getMain: function () { return { engine: RUN_PREF.engine, model: RUN_PREF.model }; },
|
|
8066
|
+
getFallback: function () { return { engine: RUN_PREF.fbEngine, model: RUN_PREF.fbModel }; },
|
|
8067
|
+
setMain: function (engine, model) {
|
|
8068
|
+
RUN_PREF.engine = String(engine || '').trim();
|
|
8069
|
+
RUN_PREF.model = (RUN_PREF.engine && model) ? String(model).trim() : '';
|
|
8070
|
+
saveRunPref();
|
|
8071
|
+
applyRunPrefToLive();
|
|
8072
|
+
},
|
|
8073
|
+
setFallback: function (engine, model) {
|
|
8074
|
+
RUN_PREF.fbEngine = String(engine || '').trim();
|
|
8075
|
+
RUN_PREF.fbModel = (RUN_PREF.fbEngine && model) ? String(model).trim() : '';
|
|
8076
|
+
saveRunPref();
|
|
8077
|
+
applyRunPrefToLive();
|
|
8078
|
+
},
|
|
8079
|
+
mainOffLabel: '自动',
|
|
8080
|
+
mainOffDesc: '首个可用引擎',
|
|
8081
|
+
fbOffLabel: '不设置',
|
|
8082
|
+
fbOffDesc: '主引擎失败时不降级',
|
|
8083
|
+
modelDesc: '不指定模型,由引擎自己挑',
|
|
8084
|
+
fbModelDesc: '备用引擎自己的默认模型'
|
|
5659
8085
|
});
|
|
5660
8086
|
}
|
|
5661
8087
|
/* 二列级联(左引擎 · 右该引擎的模型),与对话框里那套同构:
|
|
@@ -5680,12 +8106,16 @@ button.setrow{font-family:inherit}
|
|
|
5680
8106
|
|
|
5681
8107
|
function paint() {
|
|
5682
8108
|
left.innerHTML = ''; right.innerHTML = '';
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
8109
|
+
/* 引擎列保留「N 个模型」这行小字(它不是主备说明);列宽已在 CSS 里收窄,
|
|
8110
|
+
右列因此能把长模型名显示全。 */
|
|
8111
|
+
if (!o.hideOff) {
|
|
8112
|
+
var off = hEl('button', 'pick__citem' + (!prefs.engine ? ' is-on' : ''));
|
|
8113
|
+
off.type = 'button';
|
|
8114
|
+
off.appendChild(hEl('span', 'pick__cl', offLabel));
|
|
8115
|
+
off.appendChild(hEl('span', 'pick__cd', offDesc));
|
|
8116
|
+
off.onclick = function () { pick('', ''); };
|
|
8117
|
+
left.appendChild(off);
|
|
8118
|
+
}
|
|
5689
8119
|
|
|
5690
8120
|
avail.forEach(function (en) {
|
|
5691
8121
|
var n = (en.models || []).length;
|
|
@@ -5696,19 +8126,35 @@ button.setrow{font-family:inherit}
|
|
|
5696
8126
|
+ (en.engine === cur.engine && prefs.engine !== en.engine ? ' is-cur' : ''));
|
|
5697
8127
|
b.type = 'button';
|
|
5698
8128
|
b.appendChild(hEl('span', 'pick__cl', en.engine));
|
|
5699
|
-
b.appendChild(hEl('span', 'pick__cd',
|
|
8129
|
+
b.appendChild(hEl('span', 'pick__cd', engineSubLine(en)));
|
|
8130
|
+
/* 账号剩余积分(codebuddy 系才有 credit_left,见 core --engines)。 */
|
|
8131
|
+
if (en.credit_left) {
|
|
8132
|
+
b.appendChild(hEl('span', 'pick__cd pick__cd--credit', '剩余 ' + en.credit_left + (en.credit_suffix || ' 积分')));
|
|
8133
|
+
}
|
|
8134
|
+
/* 每日积分:没领就画「领取」(点它只领积分,不切引擎);已领显示状态。 */
|
|
8135
|
+
if (en.checkin_today === false) {
|
|
8136
|
+
var chip2 = hEl('span', 'pick__cd pick__cd--credit pick__claim',
|
|
8137
|
+
en._claimBusy ? '领取中…' : ('领取 ' + (en.checkin_credit || '') + ' 积分'));
|
|
8138
|
+
chip2.addEventListener('click', function (ev) {
|
|
8139
|
+
ev.preventDefault(); ev.stopPropagation();
|
|
8140
|
+
claimCheckinFor(en, paint);
|
|
8141
|
+
});
|
|
8142
|
+
b.appendChild(chip2);
|
|
8143
|
+
}
|
|
5700
8144
|
b.onclick = function () { cur = en; paint(); };
|
|
5701
8145
|
left.appendChild(b);
|
|
5702
8146
|
});
|
|
5703
8147
|
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
8148
|
+
if (!o.hideModelDefault) {
|
|
8149
|
+
var d = hEl('button', 'pick__citem' + (prefs.engine === cur.engine && !prefs.model ? ' is-on' : ''));
|
|
8150
|
+
d.type = 'button';
|
|
8151
|
+
d.appendChild(hEl('span', 'pick__cl', '引擎默认'));
|
|
8152
|
+
d.appendChild(hEl('span', 'pick__cd', modelDefaultDesc));
|
|
8153
|
+
d.onclick = function () { pick(cur.engine, ''); };
|
|
8154
|
+
right.appendChild(d);
|
|
8155
|
+
}
|
|
5710
8156
|
|
|
5711
|
-
(cur.models || []).forEach(function (mm) {
|
|
8157
|
+
qaModelsOrdered(ENGINES, cur.engine, cur.models || []).forEach(function (mm) {
|
|
5712
8158
|
var b = hEl('button', 'pick__citem' + (prefs.engine === cur.engine && prefs.model === mm ? ' is-on' : ''));
|
|
5713
8159
|
b.type = 'button';
|
|
5714
8160
|
/* 显示剥掉 `custom-local:` 前缀(`mm` 本身是完整 id,选中时原样存进 prefs),
|
|
@@ -5727,7 +8173,7 @@ button.setrow{font-family:inherit}
|
|
|
5727
8173
|
applyRunPrefToLive();
|
|
5728
8174
|
closeHostSheet();
|
|
5729
8175
|
toast(RUN_PREF.engine
|
|
5730
|
-
? ('默认引擎:' + RUN_PREF.engine + ' · ' + (qaModelText(RUN_PREF.model) || '引擎默认模型'))
|
|
8176
|
+
? ('默认引擎:' + RUN_PREF.engine + ' · ' + (qaModelText(RUN_PREF.model, RUN_PREF.engine) || '引擎默认模型'))
|
|
5731
8177
|
: '默认引擎:自动(首个可用)');
|
|
5732
8178
|
}
|
|
5733
8179
|
/* 改默认值只同步**还没发过任务的新会话**;历史会话保持它自己的引擎,
|
|
@@ -5736,11 +8182,13 @@ button.setrow{font-family:inherit}
|
|
|
5736
8182
|
if (!conv || !conv.state || conv.state.session) return;
|
|
5737
8183
|
var eng = RUN_PREF.engine;
|
|
5738
8184
|
if (!eng) {
|
|
5739
|
-
var first = ENGINES
|
|
8185
|
+
var first = pickableEngines(ENGINES)[0];
|
|
5740
8186
|
eng = first ? first.engine : '';
|
|
5741
8187
|
}
|
|
5742
8188
|
if (conv.setEngine) conv.setEngine(eng);
|
|
5743
8189
|
if (conv.setModel) conv.setModel(eng ? (RUN_PREF.engine ? RUN_PREF.model : '') : '');
|
|
8190
|
+
/* 主备:备用与主引擎的选择相互独立(只配备用也能生效)。 */
|
|
8191
|
+
if (conv.setFallback) conv.setFallback(RUN_PREF.fbEngine, RUN_PREF.fbModel);
|
|
5744
8192
|
}
|
|
5745
8193
|
/* 会话里点了具体模型/「引擎默认」→ 记住成默认(组件 onPick 回调)。 */
|
|
5746
8194
|
function rememberRunPick(engine, model) {
|
|
@@ -5778,6 +8226,74 @@ button.setrow{font-family:inherit}
|
|
|
5778
8226
|
Promise.all(jobs.map(function (p) { return p.catch(function () {}); })).then(done, done);
|
|
5779
8227
|
}
|
|
5780
8228
|
|
|
8229
|
+
/* ══════════ 重启桌面端服务 ══════════
|
|
8230
|
+
2026-10-24 用户:「在移动端我的中增加1个重启按钮 点击后触发magic-client重启」。
|
|
8231
|
+
|
|
8232
|
+
链路(见 internal/client/restart.go):POST /desk/restart → 桌面端**先立标志、
|
|
8233
|
+
先答 200**(重复点击答 already_pending,不会再起第二个助手),1 秒后由**进程内
|
|
8234
|
+
定时器**拉起一个脱离终端的 `magic-client restart` 助手停旧实例、按状态文件里的
|
|
8235
|
+
参数拉起新实例(端口 / 令牌沿用,手机里存的地址不用改)。
|
|
8236
|
+
|
|
8237
|
+
⚠️ 会话是桌面端起的子进程:重启会把**正在跑的所有会话**一起中断 → 点之前必须确认。
|
|
8238
|
+
⚠️ 判断「好了没」:旧实例认账时 /desk/health 会带 restarting=true(它马上要下线,
|
|
8239
|
+
可当成「看到下线」),再探到**不带** restarting 的 200 才算新实例起来了。 */
|
|
8240
|
+
function restartClient() {
|
|
8241
|
+
if (!TOKEN && !BASE) { toast('还没连接桌面'); return; }
|
|
8242
|
+
if (!window.confirm('重启桌面端服务?\n\n正在运行的会话会被中断(状态会变成「中断」,重新打开会话可继续)。')) return;
|
|
8243
|
+
var btn = document.getElementById('btnRestartClient');
|
|
8244
|
+
var val = document.getElementById('restartClientState');
|
|
8245
|
+
if (btn) btn.disabled = true;
|
|
8246
|
+
if (val) val.textContent = '重启中…';
|
|
8247
|
+
var hdr = TOKEN ? { 'X-Magic-Token': TOKEN } : {};
|
|
8248
|
+
var done = function (label, msg) {
|
|
8249
|
+
if (val) val.textContent = label;
|
|
8250
|
+
if (btn) btn.disabled = false;
|
|
8251
|
+
if (msg) toast(msg);
|
|
8252
|
+
};
|
|
8253
|
+
fetch(BASE + '/desk/restart', { method: 'POST', headers: hdr })
|
|
8254
|
+
.then(function (r) {
|
|
8255
|
+
if (r.ok) return null;
|
|
8256
|
+
return r.json().catch(function () { return {}; }).then(function (d) {
|
|
8257
|
+
throw new Error((d && d.error) || ('HTTP ' + r.status));
|
|
8258
|
+
});
|
|
8259
|
+
})
|
|
8260
|
+
.then(function () { return waitRestartBack(); })
|
|
8261
|
+
.then(function () {
|
|
8262
|
+
done('重启', '桌面端已重启');
|
|
8263
|
+
loadSessions(true); // 新实例的会话表(状态已按重启后的实况归一过)
|
|
8264
|
+
})
|
|
8265
|
+
.catch(function (e) {
|
|
8266
|
+
done('重启', '重启未确认:' + (e && e.message ? e.message : e));
|
|
8267
|
+
});
|
|
8268
|
+
}
|
|
8269
|
+
/* 等桌面端**真的换了一代**:旧实例认账(health.restarting=true)或连不上都算「看到
|
|
8270
|
+
下线」,再看到 200(不带 restarting 标志)才算新实例起来。60 秒等不到就当失败 ——
|
|
8271
|
+
实测正常 3-7 秒;但客户端与 watchdog 抢着拉起时见过 30 秒以上(两个实例抢端口,
|
|
8272
|
+
输的那个要等 ready 超时),别让按钮永远停在「重启中…」。 */
|
|
8273
|
+
function waitRestartBack() {
|
|
8274
|
+
var hdr = TOKEN ? { 'X-Magic-Token': TOKEN } : {};
|
|
8275
|
+
var deadline = Date.now() + 60000;
|
|
8276
|
+
var sawDown = false, settled = false;
|
|
8277
|
+
return new Promise(function (resolve, reject) {
|
|
8278
|
+
(function tick() {
|
|
8279
|
+
if (settled) return;
|
|
8280
|
+
fetch(BASE + '/desk/health', { headers: hdr, cache: 'no-store' })
|
|
8281
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
8282
|
+
.then(function (d) {
|
|
8283
|
+
if (!d) { sawDown = true; return; } // 连不上:旧实例真的走了
|
|
8284
|
+
if (d.restarting) { sawDown = true; return; } // 旧实例认账(restarting 标志):定时器到点就下线
|
|
8285
|
+
if (sawDown) { settled = true; return resolve(d); } // 不带标志的 200 = 新一代
|
|
8286
|
+
})
|
|
8287
|
+
.catch(function () { sawDown = true; })
|
|
8288
|
+
.then(function () {
|
|
8289
|
+
if (settled) return; // 已确认恢复:不再空转探活
|
|
8290
|
+
if (Date.now() > deadline) { settled = true; return reject(new Error('等桌面端恢复超时')); }
|
|
8291
|
+
setTimeout(tick, 400);
|
|
8292
|
+
});
|
|
8293
|
+
})();
|
|
8294
|
+
});
|
|
8295
|
+
}
|
|
8296
|
+
|
|
5781
8297
|
/* ══════════ 接线 ══════════ */
|
|
5782
8298
|
document.getElementById('btnConnect').onclick = function () {
|
|
5783
8299
|
var target = parseTarget(addrEl.value);
|
|
@@ -5810,6 +8326,7 @@ button.setrow{font-family:inherit}
|
|
|
5810
8326
|
on('btnRsWorkspace', function () { openWorkspacePicker(); });
|
|
5811
8327
|
/* 权限 / 团队沿用会话组件里那唯一一份选择器(浮层盖在本页之上);选完就地重画。 */
|
|
5812
8328
|
on('btnRsPermission', function () { if (conv && conv.openPermissionPicker) conv.openPermissionPicker(); });
|
|
8329
|
+
on('btnRsReasoning', openReasoningPicker);
|
|
5813
8330
|
on('btnRsTeam', function () { if (conv && conv.openTeamPicker) conv.openTeamPicker(); });
|
|
5814
8331
|
/* 会话托管(2026-10-23,按会话):只有开 / 关两档,直接翻当前这条会话。 */
|
|
5815
8332
|
on('btnRsHosting', toggleHostingHere);
|
|
@@ -5845,10 +8362,82 @@ button.setrow{font-family:inherit}
|
|
|
5845
8362
|
getBase: function () { return BASE; },
|
|
5846
8363
|
getToken: function () { return TOKEN; },
|
|
5847
8364
|
getRootDirs: function () { return ROOT_DIRS; },
|
|
5848
|
-
|
|
8365
|
+
/* 空间清单给的是**路径数组**(需求页只拿它判「这个空间还在不在」);
|
|
8366
|
+
别名另开一个口子(getSpaceAlias),免得需求页去拆对象。 */
|
|
8367
|
+
getCustomSpaces: function () { return CUSTOM_SPACES.map(function (s) { return s.path; }); },
|
|
8368
|
+
getSpaceAlias: function (dir) { var it = customSpaceOf(dir); return it ? (it.alias || '') : ''; },
|
|
5849
8369
|
getRunPref: function () { return RUN_PREF; },
|
|
5850
8370
|
isRealSpaceDir: isRealSpaceDir,
|
|
5851
|
-
topLevelSessions: topLevelSessions
|
|
8371
|
+
topLevelSessions: topLevelSessions,
|
|
8372
|
+
/* core 不支持截图能力位时,相机徽标整个不出现(不给「点了必然 501」的死入口)。 */
|
|
8373
|
+
canShot: hasReqShotFeature,
|
|
8374
|
+
/* 「页面完成情况」截图的入口(reqshot.js 用)。core 不支持这个能力位时
|
|
8375
|
+
徽标干脆不出现 —— 与其给一个点了必然 501 的按钮,不如不给。 */
|
|
8376
|
+
openShot: function (task) {
|
|
8377
|
+
if (!hasReqShotFeature()) { toast('当前桌面端不支持需求截图,请升级'); return; }
|
|
8378
|
+
if (window.MagicReqShot && window.MagicReqShot.open) window.MagicReqShot.open(task);
|
|
8379
|
+
},
|
|
8380
|
+
/* 存完截图后刷新需求清单(徽标上的「待截图 / N 图」要跟着变)。 */
|
|
8381
|
+
refreshReqs: function () { if (window.MagicReqs && window.MagicReqs.show) window.MagicReqs.show(); }
|
|
8382
|
+
});
|
|
8383
|
+
}
|
|
8384
|
+
/* 需求页的「页面完成情况」截图与标注(reqshot.js):宿主只给这几件能力 ——
|
|
8385
|
+
浏览器(必须走中转,截图能力靠中转注入的 relay)、抽屉、令牌、以及清单刷新。 */
|
|
8386
|
+
if (window.MagicReqShot && window.MagicReqShot.mount) {
|
|
8387
|
+
window.MagicReqShot.mount({
|
|
8388
|
+
hEl: hEl,
|
|
8389
|
+
elSpan: elSpan,
|
|
8390
|
+
toast: toast,
|
|
8391
|
+
openSheet: openHostSheet,
|
|
8392
|
+
closeSheet: closeHostSheet,
|
|
8393
|
+
getBase: function () { return BASE; },
|
|
8394
|
+
getToken: function () { return TOKEN; },
|
|
8395
|
+
/* 打开页面(走中转):截图能力**只存在于中转链路** ——
|
|
8396
|
+
手机直连的外站页面里没有 relay,截不了。所以这里强制走 brProxyURL。 */
|
|
8397
|
+
openBrowseProxy: function (u) {
|
|
8398
|
+
brStack = [u];
|
|
8399
|
+
brFrame.src = u;
|
|
8400
|
+
brPaint();
|
|
8401
|
+
brResetFloat();
|
|
8402
|
+
brEl.hidden = false;
|
|
8403
|
+
},
|
|
8404
|
+
proxyURL: function (u) { return brProxyURL(u); },
|
|
8405
|
+
/* 强制打开中转开关(截图的前提);用完可以由用户自己关回去。 */
|
|
8406
|
+
setRelayOn: function (on) {
|
|
8407
|
+
try { localStorage.setItem(BR_RELAY_KEY, on ? '1' : '0'); } catch (e) {}
|
|
8408
|
+
brPaintRelay();
|
|
8409
|
+
},
|
|
8410
|
+
/* 当前中转开关的状态(截图流程结束时还原成用户自己的选择)。 */
|
|
8411
|
+
relayOn: function () { return brRelayOn(); },
|
|
8412
|
+
/* 浏览器工具栏上的「截图」按钮:只在截图流程里出现。 */
|
|
8413
|
+
setShotBtn: function (on) { if (brShotBtn) brShotBtn.hidden = !on; }
|
|
8414
|
+
});
|
|
8415
|
+
}
|
|
8416
|
+
/* 脚本页(scripts.js):内容与逻辑不在本文件,这里只注入宿主能力并挂载一次。
|
|
8417
|
+
getter 形式传 BASE/TOKEN —— 切环境后这些值会变,不能传快照。
|
|
8418
|
+
⚠️ openSheet / closeSheet 借宿主的抽屉(.field / .btn 样式都在宿主那一套里,
|
|
8419
|
+
自己再造一套会出现「两种输入框」)。 */
|
|
8420
|
+
if (window.MagicScripts && window.MagicScripts.mount) {
|
|
8421
|
+
window.MagicScripts.mount({
|
|
8422
|
+
rootEl: scriptsEl,
|
|
8423
|
+
detailEl: scriptDetailEl,
|
|
8424
|
+
hEl: hEl,
|
|
8425
|
+
elSpan: elSpan,
|
|
8426
|
+
svgUse: svgUse,
|
|
8427
|
+
toast: toast,
|
|
8428
|
+
hideAll: hideAll,
|
|
8429
|
+
setActive: setActive,
|
|
8430
|
+
showRoot: function () {
|
|
8431
|
+
hideAll();
|
|
8432
|
+
scriptsEl.hidden = false;
|
|
8433
|
+
tabbarEl.hidden = true;
|
|
8434
|
+
},
|
|
8435
|
+
showDetail: showScriptDetail,
|
|
8436
|
+
backToDiscover: showDiscover,
|
|
8437
|
+
openSheet: openHostSheet,
|
|
8438
|
+
closeSheet: closeHostSheet,
|
|
8439
|
+
getBase: function () { return BASE; },
|
|
8440
|
+
getToken: function () { return TOKEN; }
|
|
5852
8441
|
});
|
|
5853
8442
|
}
|
|
5854
8443
|
Array.prototype.forEach.call(tabbarEl.querySelectorAll('.tab'), function (b) {
|
|
@@ -5868,6 +8457,10 @@ button.setrow{font-family:inherit}
|
|
|
5868
8457
|
on('dscBrowser', function () { openBrowser(HOME_URL); });
|
|
5869
8458
|
on('dscApps', function () { openComingSheet('应用', '暂无可用应用。桌面端发布的应用会出现在这里。'); });
|
|
5870
8459
|
on('dscPlugins', function () { showPlugins(); });
|
|
8460
|
+
on('dscScripts', function () { showScripts(); });
|
|
8461
|
+
on('dscKnowledge', function () { showKnowledge(); });
|
|
8462
|
+
on('btnKnowledgeBack', function () { showDiscover(); });
|
|
8463
|
+
on('btnKnowledgeDetailBack', function () { showKnowledge(); });
|
|
5871
8464
|
on('btnPluginsBack', function () { showDiscover(); });
|
|
5872
8465
|
on('btnPluginDetailBack', function () { showPlugins(); });
|
|
5873
8466
|
on('dscSkill', function () { openComingSheet('SKILL', '暂无可用 SKILL。技能包上线后将在此展示。'); });
|
|
@@ -5879,21 +8472,27 @@ button.setrow{font-family:inherit}
|
|
|
5879
8472
|
on('brRefresh', brReload);
|
|
5880
8473
|
on('brShare', brShare);
|
|
5881
8474
|
on('brStar', brToggleStar);
|
|
8475
|
+
on('brRelay', brToggleRelay);
|
|
8476
|
+
/* 截图按钮:需求页「页面完成情况」流程专用(reqshot.js 发指令、收图)。
|
|
8477
|
+
浏览器关掉时收尾 —— 别留一个半开的截图通道。 */
|
|
8478
|
+
on('brShot', function () {
|
|
8479
|
+
if (window.MagicReqShot && window.MagicReqShot.capture) window.MagicReqShot.capture();
|
|
8480
|
+
});
|
|
8481
|
+
brPaintRelay();
|
|
5882
8482
|
on('btnNotify', openNotifySettings);
|
|
5883
8483
|
on('btnDefaultEngine', openDefaultEnginePicker);
|
|
5884
|
-
on('btnDefaultModel', openDefaultEnginePicker);
|
|
5885
8484
|
on('btnSuggestEngine', openSuggestEnginePicker);
|
|
5886
|
-
on('btnSuggestModel', openSuggestEnginePicker);
|
|
5887
8485
|
/* 团队模式配置(「我的」→ 子页面):计划 + 7 个角色,行由 paintTeamRoles 动态渲染,
|
|
5888
8486
|
行的点击在渲染时绑定(见 addTeamRouteRow / addTeamRuleRow),这里只留入口与返回。 */
|
|
5889
8487
|
on('btnTeamPage', showTeamPage);
|
|
5890
8488
|
on('btnTeamBack', showMine);
|
|
5891
8489
|
on('btnLogout', disconnect);
|
|
5892
8490
|
on('btnUpgrade', reloadApp);
|
|
8491
|
+
on('btnRestartClient', restartClient);
|
|
5893
8492
|
on('btnMineVersion', checkAppUpdate);
|
|
5894
8493
|
if (segTheme) takeSeg(segTheme, 'magic.theme', function (v) { PREFS.theme = v; applyThemePref(v); });
|
|
5895
8494
|
if (segFont) takeSeg(segFont, 'magic.font', function (v) { PREFS.font = v; applyFontPref(v); });
|
|
5896
|
-
if (segMode) takeSeg(segMode, 'magic.taskmode', function (v) { TASK_MODE = v; });
|
|
8495
|
+
if (segMode) takeSeg(segMode, 'magic.taskmode', function (v) { TASK_MODE = v; applyTaskModeToQueued(v); });
|
|
5897
8496
|
|
|
5898
8497
|
/* 会话托管(2026-10-23 起按会话):入口在会话页 → 任务运行设置(见 btnRsHosting)。
|
|
5899
8498
|
「我的」里不再有全局开关 —— 曾经那枚 `tglHosting` 写的是设备级 `magic.hosting`,
|
|
@@ -5903,7 +8502,7 @@ button.setrow{font-family:inherit}
|
|
|
5903
8502
|
把整机流量经隧道送到桌面端的 SOCKS5 出口:TUN → 内核 → 桌面端 → 互联网。
|
|
5904
8503
|
只在 App 壳里可用(需要原生 VpnService);网页环境如实说不支持。 */
|
|
5905
8504
|
var tglVpn = document.getElementById('tglVpn');
|
|
5906
|
-
var VPN_ON = false; //
|
|
8505
|
+
var VPN_ON = false; // 网络代理是否真的在跑(开关状态;浏览器默认直连,见 brLoad)
|
|
5907
8506
|
function vpnBase() {
|
|
5908
8507
|
var b = BASE;
|
|
5909
8508
|
if (!b && /^https?:$/i.test(location.protocol)) b = location.origin;
|