dsh-dock 0.9.6 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/LICENSE +18 -18
  2. package/README.md +325 -325
  3. package/client.js +42 -8
  4. package/cordis.patch.yml +2 -2
  5. package/features/animation/host.js +660 -660
  6. package/features/animation/view.jsx +2247 -2247
  7. package/features/balance/host.js +494 -494
  8. package/features/balance/view.js +351 -351
  9. package/features/games/games/breakout.jsx +227 -227
  10. package/features/games/games/gomoku.jsx +244 -244
  11. package/features/games/games/racer.jsx +169 -169
  12. package/features/games/games/shared.jsx +118 -118
  13. package/features/games/games/snake.jsx +144 -144
  14. package/features/games/games/sokoban.jsx +180 -180
  15. package/features/games/games/tank.jsx +128 -128
  16. package/features/games/games/tetris.jsx +308 -308
  17. package/features/games/games/xiangqi.jsx +277 -277
  18. package/features/games/tank-core.js +152 -152
  19. package/features/games/view.jsx +639 -639
  20. package/features/games/xiangqi-core.js +299 -299
  21. package/features/heartbeat/view.js +35 -35
  22. package/features/mobile-relay/gateway.js +472 -472
  23. package/features/mobile-relay/host.js +570 -569
  24. package/features/mobile-relay/view.jsx +300 -300
  25. package/features/modelconfig/host.js +403 -403
  26. package/features/modelconfig/view.js +490 -490
  27. package/features/theme/view.js +61 -61
  28. package/features/tokenlog/host.js +678 -678
  29. package/features/tokenlog/view.jsx +561 -561
  30. package/features/visionproxy/host.js +315 -315
  31. package/index.js +175 -118
  32. package/package.json +59 -59
  33. package/src/client.jsx +668 -668
  34. package/src/host-core.js +98 -95
  35. package/src/shared.js +167 -130
@@ -1,2247 +1,2247 @@
1
- // dsh-dock · 功能模块【任务动画】· 客户端视图(v0.5.0,参照 @wycto/dsh-task-pulse client 会话追踪消费,同作者 MIT)
2
- //
3
- // 三部分:
4
- // 1. View —— 功能坞面板页:动画/通知两组独立开关 + 动画模式选择(带缩微预览)+ 运行状态列表;
5
- // 2. HomeStat —— 首页总揽概要(N 个任务进行中 / 空闲);
6
- // 3. Overlay —— 全局浮层(shell.overlay 常驻,功能启用即挂载):轮询 Host 状态,
7
- // 任务进行中渲染克制的动效(顶部流光细线 / 呼吸光点 / 轨道光环)+ 右下角状态徽标,
8
- // 任务结束时弹通知卡片(可选浏览器系统通知),完成瞬间一缕流光掠过。
9
- //
10
- // 设计取向:不做满屏粒子彩带,动效全部走主题变量(暗/亮色自适应)、低透明度、慢节奏。
11
- // 动画与通知是两个独立开关(可只开其一);配置全部经 Host settings 持久化,重启后恢复。
12
- //
13
- // Host 通信:fetch('/dsh-dock/animation/<method>')(见 features/animation/host.js)。
14
- import { useState, useEffect, useRef, useCallback, useMemo } from "react";
15
- import { openPanel, panelNav, subscribePanel } from "../../src/shared.js";
16
-
17
- // ---------- Host RPC 桥接 ----------
18
- function rpcCall(method, args) {
19
- return fetch("/dsh-dock/animation/" + method, {
20
- method: "POST",
21
- headers: { "content-type": "application/json" },
22
- body: JSON.stringify(args === undefined ? {} : args),
23
- })
24
- .then(async (res) => {
25
- const data = await res.json().catch(() => ({}));
26
- if (res.ok && data && data.ok === true) return data.data;
27
- // 旧宿主进程没有动画路由(405/404):给出可操作提示而非裸状态码
28
- if (res.status === 405 || res.status === 404) {
29
- throw new Error("宿主进程是旧版本(没有任务动画路由),重启 dsh web 后重试");
30
- }
31
- if (data && data.ok === false) throw new Error((data.error && data.error.message) || ("HTTP " + res.status));
32
- throw new Error("HTTP " + res.status + (data && data.error && data.error.message ? ": " + data.error.message : ""));
33
- });
34
- }
35
-
36
- // ---------- 动画模式 ----------
37
- const EFFECT_MODES = [
38
- { id: "flow", name: "流光细线", desc: "顶部细线流光往返,速度随任务吞吐加快" },
39
- { id: "breathe", name: "呼吸光点", desc: "醒目呼吸光点带扩散光晕:颜色随任务阶段(思考蓝/输出绿/代码橙/查资料青),呼吸随吞吐加快" },
40
- { id: "ring", name: "轨道光环", desc: "细环绕圆点旋转,转速随任务速度" },
41
- { id: "orbit", name: "环屏巡航", desc: "一颗光点沿屏幕边缘巡航整圈,醒目不遮挡" },
42
- { id: "robot", name: "桌面伙伴", desc: "真人化双工位:查资料看左屏、写入/改代码移到右屏;座椅和转头随实时任务节奏加速" },
43
- { id: "matrix", name: "代码雨", desc: "字符沿屏幕缓落如数据流,速度随任务吞吐,克制的低透明度" },
44
- { id: "stars", name: "星野", desc: "细碎星点缓慢飘移闪烁,安静耐看的背景氛围" },
45
- { id: "aurora", name: "极光", desc: "屏幕顶部柔光带缓慢呼吸流动,像极光拂过" },
46
- { id: "space", name: "星际远征", desc: "任务中星球与飞船巡航;完成后货运旗舰携实际输出量停靠输入框" },
47
- { id: "nebula", name: "星云潮汐", desc: "四团彩色星云在屏幕边缘缓慢潮汐,阶段色随任务改变" },
48
- { id: "warp", name: "曲速航道", desc: "光束从屏幕中心向外拉伸,吞吐越高曲速越快" },
49
- { id: "radar", name: "量子雷达", desc: "三座边缘雷达持续扫描,任务活动化作跳动信标" },
50
- { id: "constellation", name: "星座网络", desc: "节点与连线逐段点亮,像任务知识图谱在生长" },
51
- { id: "fireflies", name: "数据萤火", desc: "轻盈光点在空白区游弋,活跃阶段会更明亮" },
52
- { id: "ocean", name: "深海脉动", desc: "低透明度波层与气泡缓慢上浮,安静但富有生命感" },
53
- { id: "prism", name: "棱镜光谱", desc: "彩色光束从屏幕边缘折射穿行,形成克制的玻璃光感" },
54
- { id: "circuit", name: "神经电路", desc: "电路路径依次通电,脉冲速度跟随代码与工具活动" },
55
- { id: "gravity", name: "引力涟漪", desc: "多个引力源向外扩散波纹,任务越快涟漪越紧密" },
56
- { id: "lantern", name: "灵感天灯", desc: "微型灵感灯从屏幕底部缓慢升起,留下温暖光迹" },
57
- ];
58
-
59
- const AMBIENT_EFFECT_MODES = new Set(["matrix", "stars", "aurora", "space", "nebula", "warp", "radar", "constellation", "fireflies", "ocean", "prism", "circuit", "gravity", "lantern"]);
60
-
61
- // 任务阶段 → 机器人行为/文案(host 按 chunk/事件实时推导:
62
- // reasoning-delta=think、text-delta=write、tool 名分类=search/code)
63
- const PHASE_LABELS = { think: "思考中", write: "输出中", code: "编写代码", search: "查资料" };
64
- function phaseLabel(p) { return PHASE_LABELS[p] || "工作中"; }
65
- // 阶段配色(徽标呼吸点/光环随任务阶段变色,--dkan-phase 下发到 CSS)
66
- const PHASE_COLORS = { think: "#2f6fed", write: "#0d9488", code: "#b45309", search: "#0e7490" };
67
- function phaseColor(p) { return PHASE_COLORS[p] || "#2f6fed"; }
68
-
69
- // 工具名 → 中文场景("需确认"通知里说清楚是什么在等确认)
70
- function toolLabel(name) {
71
- const n = String(name || "").toLowerCase();
72
- if (/bash|shell|terminal|cmd|pwsh|powershell|exec/.test(n)) return "执行命令";
73
- if (/write|edit|patch|apply|create|mkdir|remove|delete/.test(n)) return "修改文件";
74
- if (/web|fetch|browser|navigate|search/.test(n)) return "访问网页";
75
- return "";
76
- }
77
-
78
- // ---------- 结束原因 ----------
79
- const END_LABELS = {
80
- completed: { label: "完成", cls: "ok" },
81
- error: { label: "出错", cls: "err" },
82
- aborted: { label: "已中止", cls: "warn" },
83
- blocked: { label: "受阻", cls: "warn" },
84
- "max-tokens": { label: "达输出上限", cls: "warn" },
85
- interrupted: { label: "中断", cls: "warn" },
86
- };
87
- function endInfo(reason) {
88
- return END_LABELS[reason] || END_LABELS.completed;
89
- }
90
- function isSuccessReason(reason) {
91
- return !reason || reason === "completed";
92
- }
93
-
94
- // ---------- 格式化 ----------
95
- function fmtNum(n) { return (Number(n) || 0).toLocaleString("en-US"); }
96
- function fmtCompact(n) {
97
- n = Number(n) || 0;
98
- if (n >= 1e6) return (n / 1e6).toFixed(2) + "M";
99
- if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
100
- return String(Math.round(n));
101
- }
102
- // 毫秒 → "3分21秒" / "1小时2分"
103
- function fmtDur(ms) {
104
- const s = Math.max(0, Math.round((ms || 0) / 1000));
105
- const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
106
- if (h > 0) return h + "小时" + m + "分" + sec + "秒";
107
- if (m > 0) return m + "分" + sec + "秒";
108
- return sec + "秒";
109
- }
110
- // 毫秒 → "12:34" / "1:02:11"(徽标计时)
111
- function fmtClock(ms) {
112
- const s = Math.max(0, Math.floor((ms || 0) / 1000));
113
- const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
114
- const p = (x) => String(x).padStart(2, "0");
115
- return h > 0 ? h + ":" + p(m) + ":" + p(sec) : p(m) + ":" + p(sec);
116
- }
117
- function fmtTime(ts) {
118
- if (!ts) return "";
119
- const d = new Date(ts);
120
- const p = (x) => String(x).padStart(2, "0");
121
- return p(d.getHours()) + ":" + p(d.getMinutes()) + ":" + p(d.getSeconds());
122
- }
123
- function truncate(str, max) {
124
- if (!str) return "";
125
- return String(str).length > max ? String(str).slice(0, max) + "…" : str;
126
- }
127
-
128
- // ---------- 共享快照:浮层(轮询) / 面板页 / 首页概要共用一份数据 ----------
129
- const animationStore = {
130
- snap: { status: null, loading: false, error: null },
131
- listeners: new Set(),
132
- subscribe(fn) { this.listeners.add(fn); return () => { this.listeners.delete(fn); }; },
133
- emit() { for (const fn of this.listeners) fn(); },
134
- applyConfig(cfg) {
135
- if (this.snap.status) {
136
- this.snap = Object.assign({}, this.snap, { status: Object.assign({}, this.snap.status, { config: cfg }) });
137
- this.emit();
138
- }
139
- },
140
- refresh() {
141
- if (this.snap.loading) return Promise.resolve();
142
- this.snap = Object.assign({}, this.snap, { loading: true });
143
- this.emit();
144
- return rpcCall("status")
145
- .then((data) => {
146
- this.snap = { status: data, loading: false, error: null };
147
- this.emit();
148
- })
149
- .catch((e) => {
150
- this.snap = Object.assign({}, this.snap, { loading: false, error: (e && e.message) || String(e) });
151
- this.emit();
152
- });
153
- },
154
- };
155
- function useAnimation() {
156
- const [snap, setSnap] = useState(animationStore.snap);
157
- useEffect(() => animationStore.subscribe(() => setSnap(animationStore.snap)), []);
158
- return snap;
159
- }
160
-
161
- // 秒级滴答(徽标计时用;ctx.interval 优先,退回 setInterval)
162
- function useTicker(ctx, active) {
163
- const [, bump] = useState(0);
164
- useEffect(() => {
165
- if (!active) return;
166
- if (ctx && typeof ctx.interval === "function") return ctx.interval(bump, 1000);
167
- const t = setInterval(bump, 1000);
168
- return () => clearInterval(t);
169
- }, [active]);
170
- }
171
-
172
- // ---------- 桌面场景(CSS 3D:长方体拼装的 3D 动漫人物,侧身面向镜头、面朝三屏) ----------
173
- // data-phase 驱动四态(think/write/code/search,host 由 assistant/chunk 流级事件实时同步);
174
- // --dkan-speed 随任务吞吐加速;卡片可整卡自由拖拽(位置 localStorage 持久化)。
175
- // Box3 = 六面长方体(w×h×d),faces: 前(+Z)/后/右(+X)/左/顶/底;世界层统一转角取 3/4 视角。
176
- function Box3(props) {
177
- const w = props.w, h = props.h, d = props.d;
178
- const faces = [
179
- [w, h, "translate(-50%,-50%) translateZ(" + (d / 2) + "px)"],
180
- [w, h, "translate(-50%,-50%) rotateY(180deg) translateZ(" + (d / 2) + "px)"],
181
- [d, h, "translate(-50%,-50%) rotateY(90deg) translateZ(" + (w / 2) + "px)"],
182
- [d, h, "translate(-50%,-50%) rotateY(-90deg) translateZ(" + (w / 2) + "px)"],
183
- [w, d, "translate(-50%,-50%) rotateX(90deg) translateZ(" + (h / 2) + "px)"],
184
- [w, d, "translate(-50%,-50%) rotateX(-90deg) translateZ(" + (h / 2) + "px)"],
185
- ];
186
- return (
187
- <span className={"dk3-box " + (props.cls || "")}
188
- style={Object.assign({ width: w, height: h, left: props.x - w / 2, top: props.y - h / 2 },
189
- props.z ? { transform: "translateZ(" + props.z + "px)" } : null, props.style)}>
190
- {faces.map((f, i) => (
191
- <span key={i} className="dk3-face" style={{ width: f[0], height: f[1], transform: f[2] }} />
192
- ))}
193
- {props.children}
194
- </span>
195
- );
196
- }
197
-
198
- function Monitor3(props) {
199
- const w = props.w, h = props.h;
200
- const searchScreen = props.mode === "search";
201
- // 屏幕内容:10 行代码(5 行×2 循环)+ 光标,整体向上滚动(速度随 --dkan-speed)
202
- const lines = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5];
203
- return (
204
- <span className={"dk3-mon3 " + (props.cls || "")}
205
- style={{ left: props.x - w / 2, top: props.y - h / 2, width: w, height: h, transform: "translateZ(" + (props.z || -3) + "px) rotateY(" + (props.ry || 0) + "deg)" }}>
206
- <Box3 w={w} h={h} d={5} cls="dk3-frame" x={w / 2} y={h / 2} />
207
- <span className="dk3-screen" style={{ width: w - 4, height: h - 4, transform: "translate(-50%,-50%) translateZ(3.1px)" }}>
208
- {searchScreen ? <span className="dk3-search-ui">
209
- <span className="dk3-search-box"><b /></span>
210
- <span className="dk3-search-results">{lines.slice(0, 6).map((n, i) => <i key={i} />)}</span>
211
- </span> : <span className="dk3-code">
212
- {lines.map((n, i) => <i key={i} />)}
213
- <span className="dk3-cur" />
214
- </span>}
215
- </span>
216
- </span>
217
- );
218
- }
219
-
220
- // 3D 动漫人物:更拟人的比例与细节(圆角头/ volumetric 头发/五官/脖颈/弯肘/腿脚鞋)。
221
- // 紧凑双工位:左侧专做资料检索,右侧专做写作/改文件;顶部小屏放任务状态。
222
- // 人物坐滚轮椅在两侧工位间滑动,思考时停在两屏之间。
223
- function RobotScene(props) {
224
- const phase = props && props.phase ? props.phase : "code";
225
- const tasks = Array.isArray(props && props.tasks) ? props.tasks : null;
226
- const n = tasks ? Math.max(1, Math.min(3, tasks.length)) : 1;
227
- // 左工位只做资料检索;正文输出和工具写入都在右工位,思考回到中位。
228
- const station = phase === "search" ? "left" : (phase === "write" || phase === "code" ? "right" : "center");
229
- return (
230
- <div className="dkan-bot-scene" data-phase={phase} data-station={station} data-tasks={tasks ? String(n) : undefined} aria-hidden="true">
231
- <div className="dk3-world">
232
- {/* 紧凑单人书桌:只留左右两个真实工作位,消除左侧空桌面。 */}
233
- <Box3 w={134} h={7} d={38} cls="dk3-desk" x={145} y={76} />
234
- <Box3 w={5} h={24} d={30} cls="dk3-metal dk3-leg" x={84} y={89} />
235
- <Box3 w={5} h={24} d={30} cls="dk3-metal dk3-leg" x={206} y={89} />
236
- {/* 左检索、右编码,顶部小屏显示任务进度。 */}
237
- <Monitor3 w={34} h={24} x={140} y={59} ry={20} cls="left" mode="search" />
238
- <Monitor3 w={42} h={30} x={180} y={57} ry={-16} cls="right" />
239
- <Monitor3 w={28} h={18} x={160} y={31} ry={0} cls="top" />
240
- {/* 两套键鼠紧贴相应屏幕,中央仅保留咖啡杯。 */}
241
- <Box3 w={16} h={2.5} d={9} cls="dk3-metal dk3-kb3" x={130} y={71} z={14} />
242
- <Box3 w={4} h={2.5} d={6} cls="dk3-metal dk3-mouse3" x={141} y={71} z={14} />
243
- <Box3 w={4} h={5} d={4} cls="dk3-mug" x={154} y={69} z={14} />
244
- <Box3 w={16} h={2.5} d={9} cls="dk3-metal dk3-kb3" x={170} y={71} z={14} />
245
- <Box3 w={4} h={2.5} d={6} cls="dk3-metal dk3-mouse3" x={181} y={71} z={14} />
246
- {/* 人物组:按当前任务在左检索/中思考/右编码两个工位间平滑移动。 */}
247
- <div className="dk3-person">
248
- {/* 椅子:靠背只到肩胛、降低支柱和底盘,贴近真实坐姿。 */}
249
- <Box3 w={4} h={20} d={20} cls="dk3-chairback" x={2} y={47} />
250
- <Box3 w={18} h={4} d={20} cls="dk3-chairseat" x={12} y={59} />
251
- <Box3 w={3} h={9} d={3} cls="dk3-metal" x={12} y={66} />
252
- <Box3 w={13} h={2} d={13} cls="dk3-metal" x={12} y={71} />
253
- {/* 椅子滚轮(左右各一,滑动时可见) */}
254
- <Box3 w={3} h={3} d={3} cls="dk3-wheel" x={6} y={74} />
255
- <Box3 w={3} h={3} d={3} cls="dk3-wheel" x={18} y={74} />
256
- {/* 腿:大腿/小腿/鞋 */}
257
- <Box3 w={12} h={5} d={9} cls="dk3-pants" x={20} y={54} />
258
- <Box3 w={4} h={10} d={4} cls="dk3-pants" x={26} y={62} />
259
- <Box3 w={6} h={3} d={5} cls="dk3-shoe" x={28} y={70} />
260
- {/* 上身整体始终面向桌面;阶段动作只改变前倾、肩肘腕和视线。 */}
261
- <div className="dk3-upper3">
262
- {/* 肩线、胸腔和腰部做成上宽下窄,摆脱直筒积木感。 */}
263
- <Box3 w={18} h={5} d={12} cls="dk3-hood dk3-shoulders3" x={13} y={30} />
264
- <Box3 w={15} h={14} d={11} cls="dk3-hood dk3-torso dk3-chest3" x={12} y={38} />
265
- <Box3 w={12} h={7} d={10} cls="dk3-hood dk3-torso dk3-waist3" x={12} y={47} />
266
- <Box3 w={5} h={8} d={9} cls="dk3-hood dk3-hoodbump" x={5} y={32} />
267
- {/* 颈部加长并与下颌、衣领重叠,避免悬空。 */}
268
- <Box3 w={5} h={7} d={5} cls="dk3-skin dk3-neck3" x={14} y={24} />
269
- <Box3 w={8} h={3} d={8} cls="dk3-collar3" x={14} y={28} />
270
- {/* 头组与颈部同轴:颅骨 + 收窄下颌 + 短侧渐层发型。 */}
271
- <div className="dk3-head3">
272
- <Box3 w={12} h={11} d={11} cls="dk3-skin dk3-cranium" x={8} y={8}>
273
- <span className="dk3-brows" style={{ width: 9, height: 4, transform: "translate(-50%,-50%) rotateY(90deg) translateZ(5.8px)" }}><i /><i /></span>
274
- <span className="dk3-eyes" style={{ width: 9, height: 6, transform: "translate(-50%,-50%) rotateY(90deg) translateZ(5.7px)" }}><i /><i /></span>
275
- <span className="dk3-nose" style={{ transform: "translate(-50%,-50%) rotateY(90deg) translateZ(6.1px)" }} />
276
- </Box3>
277
- <Box3 w={9} h={7} d={9} cls="dk3-skin dk3-jaw3" x={8.5} y={14}>
278
- <span className="dk3-mouth" style={{ transform: "translate(-50%,-50%) rotateY(90deg) translateZ(4.7px)" }} />
279
- </Box3>
280
- <Box3 w={12} h={5} d={12} cls="dk3-hair dk3-hair-crown" x={8} y={3.5} />
281
- <Box3 w={4} h={8} d={11} cls="dk3-hair dk3-hair-back" x={3} y={7.5} />
282
- <span className="dk3-fringe3"><i /><i /><i /></span>
283
- <span className="dk3-ear" />
284
- </div>
285
- {/* 近侧手臂:肩、肘、腕、手指分段动作。 */}
286
- <div className="dk3-arm3 dk3-near">
287
- <Box3 w={4} h={11} d={4} cls="dk3-hood dk3-uarm" x={2} y={6} />
288
- <div className="dk3-elbow">
289
- <Box3 w={10} h={3.5} d={3.5} cls="dk3-hood dk3-farm" x={5.5} y={2} />
290
- <div className="dk3-wrist3">
291
- <Box3 w={3.5} h={3} d={3} cls="dk3-skin dk3-hand" x={2} y={2} />
292
- <span className="dk3-fingers3"><i /><i /></span>
293
- </div>
294
- </div>
295
- </div>
296
- {/* 远侧手臂:在桌面深处,和近侧手错开节奏。 */}
297
- <div className="dk3-arm3 dk3-far">
298
- <Box3 w={4} h={11} d={4} cls="dk3-hood dk3-uarm" x={2} y={6} />
299
- <div className="dk3-elbow">
300
- <Box3 w={10} h={3.5} d={3.5} cls="dk3-hood dk3-farm" x={5.5} y={2} />
301
- <div className="dk3-wrist3">
302
- <Box3 w={3.5} h={3} d={3} cls="dk3-skin dk3-hand" x={2} y={2} />
303
- <span className="dk3-fingers3"><i /><i /></span>
304
- </div>
305
- </div>
306
- </div>
307
- </div>
308
- </div>
309
- </div>
310
- {/* 思考泡泡(贴镜头的 2D 层,think 阶段显示) */}
311
- <span className="dkan-bubble"><i /><i /><i /></span>
312
- </div>
313
- );
314
- }
315
-
316
- // ---------- 提示音(WebAudio 合成;macOS/Windows 通用,无音频文件) ----------
317
- // 音效库:每种含完成音(done)、异常音(err)与确认音(ask)三套音符序列。
318
- // 音符 [频率, 起始秒, 时长秒, 波形?],波形缺省 sine。确认音双音上扬、偏长,
319
- // 听感是"来人搭话",和完成音的收束感、异常音的下坠感区分开。
320
- const SOUND_LIBRARY = {
321
- chime: { name: "清脆双音", notes: { done: [[659.25, 0, .14], [880, .13, .24]], err: [[220, 0, .16], [164.81, .15, .3]], ask: [[587.33, 0, .16], [880, .19, .34]] } },
322
- ding: { name: "叮", notes: { done: [[987.77, 0, .35]], err: [[246.94, 0, .4]], ask: [[783.99, 0, .18], [1174.66, .2, .42]] } },
323
- coin: { name: "金币", notes: { done: [[988, 0, .08], [1319, .08, .35], [988, 0, .08, "square"], [1319, .08, .3, "square"]], err: [[196, 0, .12], [147, .11, .35]], ask: [[880, 0, .09], [1108.73, .1, .12], [1318.51, .22, .3]] } },
324
- bell: { name: "钟声", notes: { done: [[523.25, 0, .5], [659.25, .02, .45], [783.99, .04, .4]], err: [[174.61, 0, .5], [130.81, .05, .5]], ask: [[659.25, 0, .4], [523.25, .05, .55]] } },
325
- pulse: { name: "脉冲", notes: { done: [[440, 0, .09], [440, .14, .09], [440, .28, .16]], err: [[174.61, 0, .1], [174.61, .14, .1], [174.61, .28, .18]], ask: [[523.25, 0, .09], [523.25, .15, .09], [659.25, .3, .22]] } },
326
- arp: { name: "琶音", notes: { done: [[523.25, 0, .12], [659.25, .09, .12], [783.99, .18, .12], [1046.5, .27, .3]], err: [[392, 0, .12], [329.63, .1, .12], [261.63, .2, .12], [196, .3, .32]], ask: [[440, 0, .11], [554.37, .12, .11], [659.25, .24, .11], [880, .36, .34]] } },
327
- };
328
- // AudioContext 按需创建(浏览器自动播放策略:首次用户交互后才能出声,静默失败不报错)
329
- let soundCtx = null;
330
- function playTone(seq) {
331
- try {
332
- if (typeof window === "undefined" || !window.AudioContext && !window.webkitAudioContext) return;
333
- const AC = window.AudioContext || window.webkitAudioContext;
334
- if (!soundCtx) soundCtx = new AC();
335
- if (soundCtx.state === "suspended") { soundCtx.resume().catch(() => {}); }
336
- const t0 = soundCtx.currentTime;
337
- for (const [f, at, dur, wave] of seq) {
338
- const osc = soundCtx.createOscillator();
339
- const gain = soundCtx.createGain();
340
- osc.type = wave || "sine";
341
- osc.frequency.value = f;
342
- gain.gain.setValueAtTime(0, t0 + at);
343
- gain.gain.linearRampToValueAtTime(0.18, t0 + at + 0.015);
344
- gain.gain.exponentialRampToValueAtTime(0.0001, t0 + at + dur);
345
- osc.connect(gain).connect(soundCtx.destination);
346
- osc.start(t0 + at);
347
- osc.stop(t0 + at + dur + 0.05);
348
- }
349
- } catch { /* 音频不可用静默 */ }
350
- }
351
- // 按音效播放:完成音 / 异常音 / 确认音(未知音效回退 chime)
352
- function playDoneSound(success, effect) {
353
- const lib = SOUND_LIBRARY[effect] || SOUND_LIBRARY.chime;
354
- playTone(success ? lib.notes.done : lib.notes.err);
355
- }
356
- function playAskSound(effect) {
357
- const lib = SOUND_LIBRARY[effect] || SOUND_LIBRARY.chime;
358
- playTone(lib.notes.ask || lib.notes.done);
359
- }
360
- // 试听:完成音 + 异常音 连播(间隔 0.55s)
361
- function previewSound(effect) {
362
- const lib = SOUND_LIBRARY[effect] || SOUND_LIBRARY.chime;
363
- playTone(lib.notes.done);
364
- const delayed = lib.notes.err.map(([f, at, dur, wave]) => [f, at + 0.55, dur, wave]);
365
- playTone(delayed);
366
- }
367
-
368
-
369
- function Toast(props) {
370
- const t = props.t;
371
- const [closing, setClosing] = useState(false);
372
- const close = useCallback(() => { if (!closing) setClosing(true); }, [closing]);
373
- // 停留时长(stayMs=0 常驻,仅手动关闭)
374
- useEffect(() => {
375
- if (!t.stayMs) return;
376
- const timer = setTimeout(close, t.stayMs);
377
- return () => clearTimeout(timer);
378
- }, []);
379
- useEffect(() => {
380
- if (!closing) return;
381
- const timer = setTimeout(() => props.onClose(t.id), 240);
382
- return () => clearTimeout(timer);
383
- }, [closing]);
384
- const markColor = t.kind === "success"
385
- ? "var(--dk-ok)"
386
- : t.kind === "error"
387
- ? "var(--dk-err)"
388
- : t.kind === "confirm"
389
- ? "var(--dk-warn)"
390
- : "var(--dk-warn)";
391
- return (
392
- <div className={"dkan-toast" + (t.kind === "confirm" ? " dkan-toast-confirm" : "") + (closing ? " out" : "")}>
393
- <div className="dkan-toast-head">
394
- <span className="dkan-toast-mark" style={{ background: markColor }} />
395
- <span className="dkan-toast-title" title={t.title}>{t.kind === "confirm" ? "✋ " : ""}{t.title}</span>
396
- <button type="button" className="dkan-toast-close" aria-label="关闭" onClick={close}>✕</button>
397
- </div>
398
- {t.body ? <div className="dkan-toast-body">{t.body}</div> : null}
399
- </div>
400
- );
401
- }
402
-
403
- // ---------- 交互反馈层:对用户操作即时回应(纸飞机/火花/流光/微光/光涌/彩带) ----------
404
- // 事件来源:document 点击监听(发送/新会话/切会话/切工作目录)+ 任务状态变化(开始/完成)。
405
- // 每个 burst = { id, type, x, y, bits },bits 为预生成的随机粒子参数(内联样式驱动 CSS 动画)。
406
- function makeBits(n, fn) {
407
- const arr = [];
408
- for (let i = 0; i < n; i++) arr.push(fn(i));
409
- return arr;
410
- }
411
- function BurstLayer(props) {
412
- const bursts = props.bursts || [];
413
- return (
414
- <div className="dkan-fxwrap" aria-hidden="true">
415
- {bursts.map((b) => {
416
- if (b.type === "plane") {
417
- return <span key={b.id} className="dkan-fx dkan-fx-plane" style={{ left: b.x, top: b.y }}>➤</span>;
418
- }
419
- if (b.type === "spark") {
420
- return (
421
- <span key={b.id} className="dkan-fx dkan-fx-spark" style={{ left: b.x, top: b.y }}>
422
- {b.bits.map((p, i) => <i key={i} style={{ "--dx": p.dx + "px", "--dy": p.dy + "px", background: p.c, boxShadow: "0 0 6px " + p.c }} />)}
423
- </span>
424
- );
425
- }
426
- if (b.type === "streak") return <span key={b.id} className="dkan-fx dkan-fx-streak" />;
427
- if (b.type === "flash") return <span key={b.id} className="dkan-fx dkan-fx-flash" />;
428
- if (b.type === "surge") return <span key={b.id} className="dkan-fx dkan-fx-surge" />;
429
- if (b.type === "confetti") {
430
- return (
431
- <span key={b.id} className="dkan-fx dkan-fx-confetti">
432
- {b.bits.map((p, i) => <i key={i} style={{ left: p.l + "%", "--dx": p.dx + "px", "--r": p.r + "deg", background: p.c, boxShadow: "0 0 4px " + p.c, animationDelay: p.d + "s" }} />)}
433
- </span>
434
- );
435
- }
436
- return null;
437
- })}
438
- </div>
439
- );
440
- }
441
-
442
- // ---------- 氛围动效:代码雨 / 星野 / 极光(粒子配置 useMemo 稳定,避免重渲染抖动) ----------
443
- const MATRIX_CHARS = "01</>;{}=+*#";
444
- function AmbientLayer(props) {
445
- const mode = props.mode;
446
- const speed = props.speed;
447
- // 一个模式只创建自己需要的粒子;星际模式不再额外生成代码雨和星野的 114 个无用节点。
448
- const particles = useMemo(() => {
449
- if (mode === "matrix") return { matrix: makeBits(34, () => ({
450
- l: Math.random() * 100, d: 4 + Math.random() * 6, delay: -Math.random() * 8, o: 0.28 + Math.random() * 0.34, s: 11 + Math.round(Math.random() * 4),
451
- c: MATRIX_CHARS[Math.floor(Math.random() * MATRIX_CHARS.length)] + MATRIX_CHARS[Math.floor(Math.random() * MATRIX_CHARS.length)] + MATRIX_CHARS[Math.floor(Math.random() * MATRIX_CHARS.length)],
452
- })) };
453
- if (mode === "stars") return { stars: makeBits(80, () => ({
454
- l: Math.random() * 100, t: Math.random() * 100, sz: 1.5 + Math.random() * 2, d: 2 + Math.random() * 4, delay: -Math.random() * 6, dx: (Math.random() - 0.5) * 40, dy: (Math.random() - 0.5) * 24,
455
- })) };
456
- if (mode === "space") return { dust: makeBits(22, () => ({
457
- l: Math.random() * 100, t: Math.random() * 82, sz: 1 + Math.random() * 2.2, d: 2.4 + Math.random() * 3.8, delay: -Math.random() * 6,
458
- })) };
459
- if (mode === "warp") return { warp: makeBits(28, () => ({
460
- a: Math.random() * 360, len: 54 + Math.random() * 130, d: 1.8 + Math.random() * 2.4, delay: -Math.random() * 4,
461
- })) };
462
- if (mode === "fireflies") return { fireflies: makeBits(28, () => ({
463
- l: 4 + Math.random() * 92, t: 8 + Math.random() * 78, sz: 3 + Math.random() * 5, dx: -42 + Math.random() * 84, dy: -32 + Math.random() * 64, d: 4.8 + Math.random() * 5.4, delay: -Math.random() * 8,
464
- })) };
465
- if (mode === "ocean") return { bubbles: makeBits(18, () => ({
466
- l: 3 + Math.random() * 94, sz: 4 + Math.random() * 12, d: 5 + Math.random() * 6, delay: -Math.random() * 9, drift: -34 + Math.random() * 68,
467
- })) };
468
- if (mode === "lantern") return { lanterns: makeBits(18, () => ({
469
- l: 4 + Math.random() * 92, sz: 8 + Math.random() * 9, d: 7 + Math.random() * 7, delay: -Math.random() * 12, drift: -48 + Math.random() * 96,
470
- })) };
471
- return {};
472
- }, [mode]);
473
- const ambientStyle = { "--dkan-speed": speed, "--dkan-phase": phaseColor(props.phase) };
474
- if (mode === "matrix") {
475
- return (
476
- <div className="dkan-amb dkan-matrix" style={{ "--dkan-speed": speed }} aria-hidden="true">
477
- {particles.matrix.map((p, i) => (
478
- <span key={i} style={{ left: p.l + "%", animationDuration: "calc(" + p.d + "s / var(--dkan-speed,1))", animationDelay: p.delay + "s", opacity: p.o, fontSize: p.s }}>{p.c}</span>
479
- ))}
480
- </div>
481
- );
482
- }
483
- if (mode === "stars") {
484
- return (
485
- <div className="dkan-amb dkan-stars" style={{ "--dkan-speed": speed }} aria-hidden="true">
486
- {particles.stars.map((p, i) => (
487
- <i key={i} className="dkan-star" style={{ left: p.l + "%", top: p.t + "%", width: p.sz, height: p.sz, animationDuration: "calc(" + p.d + "s / var(--dkan-speed,1))", animationDelay: p.delay + "s", "--dx": p.dx + "px", "--dy": p.dy + "px" }} />
488
- ))}
489
- <span className="dkan-moon" />
490
- <i className="dkan-meteor" style={{ top: "12%", left: "72%", animationDelay: "0s" }} />
491
- <i className="dkan-meteor" style={{ top: "28%", left: "88%", animationDelay: "2.8s" }} />
492
- <i className="dkan-meteor" style={{ top: "6%", left: "42%", animationDelay: "5.6s" }} />
493
- </div>
494
- );
495
- }
496
- if (mode === "aurora") {
497
- return (
498
- <div className="dkan-amb dkan-aurora" style={{ "--dkan-speed": speed }} aria-hidden="true">
499
- <i /><i /><i />
500
- </div>
501
- );
502
- }
503
- if (mode === "nebula") {
504
- return <div className="dkan-amb dkan-nebula" style={ambientStyle} aria-hidden="true"><i /><i /><i /><i /></div>;
505
- }
506
- if (mode === "warp") {
507
- return <div className="dkan-amb dkan-warp" style={ambientStyle} aria-hidden="true">
508
- {particles.warp.map((p, i) => <i key={i} style={{ "--a": p.a + "deg", "--len": p.len + "px", animationDuration: "calc(" + p.d + "s / var(--dkan-speed,1))", animationDelay: p.delay + "s" }} />)}
509
- </div>;
510
- }
511
- if (mode === "radar") {
512
- return <div className="dkan-amb dkan-radar" style={ambientStyle} aria-hidden="true">
513
- {["a", "b", "c"].map((id) => <span className={"dkan-radar-station " + id} key={id}><i /><b /></span>)}
514
- </div>;
515
- }
516
- if (mode === "constellation") {
517
- return <div className="dkan-amb dkan-constellation" style={ambientStyle} aria-hidden="true">
518
- <svg viewBox="0 0 100 100" preserveAspectRatio="none">
519
- <path pathLength="1" d="M6 68 L18 30 L31 48 L45 18 L58 42 L73 25 L91 58 L78 78 L58 42 L31 48 L18 76 L6 68" />
520
- {[[6,68],[18,30],[31,48],[45,18],[58,42],[73,25],[91,58],[78,78],[18,76]].map(([cx, cy], i) => <circle key={i} cx={cx} cy={cy} r={i % 3 === 0 ? 1.1 : .7} style={{ animationDelay: (-i * .24) + "s" }} />)}
521
- </svg>
522
- </div>;
523
- }
524
- if (mode === "fireflies") {
525
- return <div className="dkan-amb dkan-fireflies" style={ambientStyle} aria-hidden="true">
526
- {particles.fireflies.map((p, i) => <i key={i} style={{ left: p.l + "%", top: p.t + "%", width: p.sz, height: p.sz, "--dx": p.dx + "px", "--dy": p.dy + "px", animationDuration: "calc(" + p.d + "s / var(--dkan-speed,1))", animationDelay: p.delay + "s" }} />)}
527
- </div>;
528
- }
529
- if (mode === "ocean") {
530
- return <div className="dkan-amb dkan-ocean" style={ambientStyle} aria-hidden="true">
531
- <span className="dkan-ocean-waves">{Array.from({ length: 5 }, (_, i) => <i key={i} />)}</span>
532
- {particles.bubbles.map((p, i) => <b key={i} style={{ left: p.l + "%", width: p.sz, height: p.sz, "--drift": p.drift + "px", animationDuration: "calc(" + p.d + "s / var(--dkan-speed,1))", animationDelay: p.delay + "s" }} />)}
533
- </div>;
534
- }
535
- if (mode === "prism") {
536
- return <div className="dkan-amb dkan-prism" style={ambientStyle} aria-hidden="true">{Array.from({ length: 7 }, (_, i) => <i key={i} />)}</div>;
537
- }
538
- if (mode === "circuit") {
539
- return <div className="dkan-amb dkan-circuit" style={ambientStyle} aria-hidden="true">
540
- <svg viewBox="0 0 1000 600" preserveAspectRatio="none">
541
- {["M0 92 H180 V210 H390 V128 H610 V260 H820 V104 H1000", "M0 486 H144 V358 H334 V470 H520 V338 H748 V448 H1000", "M82 0 V126 H264 V286 H472 V196 H694 V320 H914 V600", "M936 0 V148 H776 V278 H566 V410 H350 V548 H112 V600"].map((d, i) => <path key={i} pathLength="1" d={d} style={{ animationDelay: (-i * .7) + "s" }} />)}
542
- {[[180,210],[390,128],[610,260],[144,358],[520,338],[264,286],[694,320],[566,410]].map(([cx, cy], i) => <circle key={i} cx={cx} cy={cy} r="5" />)}
543
- </svg>
544
- </div>;
545
- }
546
- if (mode === "gravity") {
547
- return <div className="dkan-amb dkan-gravity" style={ambientStyle} aria-hidden="true">
548
- {["a", "b", "c"].map((id) => <span className={"dkan-gravity-source " + id} key={id}><b />{Array.from({ length: 4 }, (_, i) => <i key={i} style={{ animationDelay: "calc(" + (-i * .9) + "s / var(--dkan-speed,1))" }} />)}</span>)}
549
- </div>;
550
- }
551
- if (mode === "lantern") {
552
- return <div className="dkan-amb dkan-lantern" style={ambientStyle} aria-hidden="true">
553
- {particles.lanterns.map((p, i) => <i key={i} style={{ left: p.l + "%", width: p.sz, height: p.sz * 1.28, "--drift": p.drift + "px", animationDuration: "calc(" + p.d + "s / var(--dkan-speed,1))", animationDelay: p.delay + "s" }} />)}
554
- </div>;
555
- }
556
- if (mode === "space") {
557
- const taskCount = Math.max(1, Math.min(3, Array.isArray(props.tasks) ? props.tasks.length : 1));
558
- // 任务期间每条航线至少有两艘巡航船,三座星系之间始终有可见往返;并发任务越多,舰队越密。
559
- // 所有飞船仍只使用 transform/opacity 合成动画,速度跟随任务活动速率。
560
- const runnerCount = Math.max(6, Math.min(9, taskCount * 3));
561
- const galaxies = props.galaxies;
562
- if (!galaxies) return null;
563
- return (
564
- <div className="dkan-amb dkan-space" style={{
565
- "--dkan-speed": speed, "--dkan-space-ships": taskCount,
566
- "--dkan-alpha-x": galaxies.alpha.cx + "px", "--dkan-alpha-y": galaxies.alpha.cy + "px",
567
- "--dkan-beta-x": galaxies.beta.cx + "px", "--dkan-beta-y": galaxies.beta.cy + "px",
568
- "--dkan-gamma-x": galaxies.gamma.cx + "px", "--dkan-gamma-y": galaxies.gamma.cy + "px",
569
- }} aria-hidden="true">
570
- {particles.dust.map((p, i) => <i key={i} className="dkan-space-dust" style={{ left: p.l + "%", top: p.t + "%", width: p.sz, height: p.sz, animationDuration: "calc(" + p.d + "s / var(--dkan-speed,1))", animationDelay: p.delay + "s" }} />)}
571
- {["alpha", "beta", "gamma"].map((galaxy) => <span className={"dkan-space-system " + galaxy} key={galaxy} style={{ left: galaxies[galaxy].x, top: galaxies[galaxy].y, "--dkan-system-scale": galaxies[galaxy].scale }}>
572
- <i className="dkan-space-sun" />
573
- <span className="dkan-space-orbit a"><i /></span>
574
- <span className="dkan-space-orbit b"><i /></span>
575
- <span className="dkan-space-orbit c"><i /></span>
576
- </span>)}
577
- {Array.from({ length: runnerCount }, (_, i) => <span className={"dkan-space-runner route-" + (i % 3)} key={i} style={{ "--dkan-run-delay": (-i * 1.8) + "s" }}><FreighterHull units={1} empty /></span>)}
578
- </div>
579
- );
580
- }
581
- return null;
582
- }
583
-
584
- // ---------- 完成货运舰:从任务航线归航到会话输入框上方,货舱量使用真实输出 Token ----------
585
- function outputCargo(outputTokens) {
586
- const tokens = Math.max(0, Number(outputTokens) || 0);
587
- // 每个可见货舱约代表 900 输出 Token;保留 1 个基础舱,防止无 usage 时舰体塌陷。
588
- return { tokens, units: Math.max(1, Math.min(8, Math.ceil(tokens / 900))) };
589
- }
590
- function fallbackComposerDockPoint(side = "right") {
591
- const vw = typeof window === "undefined" ? 1280 : window.innerWidth;
592
- const vh = typeof window === "undefined" ? 900 : window.innerHeight;
593
- return {
594
- // 待命位贴近输入框右上;右侧远端只保留给旧的通用停靠计算。
595
- x: side === "left" ? Math.max(304, Math.min(520, Math.round(vw * .18)))
596
- : side === "ready" ? Math.max(316, vw - 640)
597
- : Math.max(16, vw - 232),
598
- y: Math.max(32, vh - 218),
599
- };
600
- }
601
- function visibleRect(el) {
602
- if (!el || typeof window === "undefined") return null;
603
- const r = el.getBoundingClientRect();
604
- return r.width > 0 && r.height > 0 && r.bottom > 0 && r.top < window.innerHeight ? r : null;
605
- }
606
- // 当前正在看的会话标题(侧栏高亮行)。dsh 会话树行 = role="treeitem" + aria-selected,
607
- // 行文本即会话标题;据此把"需确认/卡住"这类会话级卡片限定到当前会话,别的会话等待时不打扰。
608
- function currentSessionTitle() {
609
- if (typeof document === "undefined") return "";
610
- for (const el of document.querySelectorAll('[role="treeitem"][aria-selected="true"]')) {
611
- const r = el.getBoundingClientRect();
612
- // 侧栏在左:跳过正文区可能出现的同名高亮节点(右上搜索结果等)
613
- if (r.width > 0 && r.height > 0 && r.right <= window.innerWidth * .35) {
614
- const text = (el.textContent || "").trim();
615
- if (text) return text;
616
- }
617
- }
618
- return "";
619
- }
620
- // 指定任务是否属于当前正在看的会话(按标题对齐;侧栏不可见时退回"是"避免整层失效)
621
- function taskOnScreen(task, titleCache) {
622
- const title = task && (task.title || "");
623
- if (!title) return true;
624
- const current = titleCache !== undefined ? titleCache : currentSessionTitle();
625
- if (!current) return true;
626
- return current === title || current.indexOf(title) !== -1 || title.indexOf(current) !== -1;
627
- }
628
- const COMPOSER_SEND_SELECTOR = 'button[aria-label="Send message"],button[aria-label*="发送"],button[data-testid*="send"],button[data-testid*="submit"],button[type="submit"]';
629
- function conversationComposer() {
630
- if (typeof document === "undefined" || typeof window === "undefined") return null;
631
- // 发送键只会位于真实会话输入卡内。由它逐层向上找卡片,比遍历历史消息的编辑节点可靠。
632
- const sendButtons = document.querySelectorAll(COMPOSER_SEND_SELECTOR);
633
- for (const button of sendButtons) {
634
- let el = button.parentElement;
635
- for (let depth = 0; el && depth < 9; depth += 1, el = el.parentElement) {
636
- const r = visibleRect(el);
637
- if (r && r.width >= 360 && r.height >= 72 && r.height <= 360 && r.top > window.innerHeight * .42) return { element: el, rect: r };
638
- }
639
- }
640
- return null;
641
- }
642
- function conversationComposerRect() {
643
- const composer = conversationComposer();
644
- return composer ? composer.rect : null;
645
- }
646
- function composerPointFromRect(composer, side) {
647
- return {
648
- x: side === "ready" ? Math.max(composer.left + 16, composer.right - 210) : fallbackComposerDockPoint(side).x,
649
- y: Math.max(22, composer.top - 92),
650
- };
651
- }
652
- function composerDockPoint(side = "right") {
653
- if (typeof document === "undefined" || typeof window === "undefined") return fallbackComposerDockPoint(side);
654
- const composer = conversationComposerRect();
655
- if (composer) return composerPointFromRect(composer, side);
656
- const selectors = [
657
- '[data-testid*="composer"]', '[data-testid*="conversation-input"]', '[data-slot*="conversation.input"]',
658
- 'textarea[placeholder*="Message"]', 'textarea[placeholder*="消息"]', 'textarea',
659
- '[contenteditable="true"][data-testid*="composer"]', '[contenteditable="true"][aria-label*="消息"]', '[contenteditable="true"][data-slot*="conversation.input"]',
660
- ];
661
- const seen = new Set();
662
- const candidates = [];
663
- for (const selector of selectors) {
664
- for (const el of document.querySelectorAll(selector)) {
665
- if (seen.has(el)) continue;
666
- seen.add(el);
667
- const r = el.getBoundingClientRect();
668
- if (r.width > 80 && r.height > 16 && r.bottom > window.innerHeight * .48) candidates.push({ el, r });
669
- }
670
- }
671
- const best = candidates.sort((a, b) => b.r.bottom - a.r.bottom)[0];
672
- if (!best) return fallbackComposerDockPoint(side);
673
- const r = best.r;
674
- // 候补输入节点没有卡片边界时,仅待命位贴近其右侧;其他位置仍由视口锚点决定。
675
- const x = side === "ready" ? Math.max(r.left + 16, r.right - 210) : fallbackComposerDockPoint(side).x;
676
- return {
677
- // 纵向以真实输入控件上沿停靠,飞船底部和输入框保留间隙。
678
- x,
679
- y: Math.max(22, r.top - 92),
680
- };
681
- }
682
- // 刷新时先让会话本身完成首帧绘制,再在空闲期读取输入框布局。
683
- // 避免星际模式恢复时 querySelectorAll + getBoundingClientRect 抢占主线程。
684
- function useSpaceDocks(enabled) {
685
- const [docks, setDocks] = useState(null);
686
- useEffect(() => {
687
- if (!enabled || typeof window === "undefined") { setDocks(null); return undefined; }
688
- let disposed = false;
689
- let frame = 0;
690
- let idle = 0;
691
- let timer = 0;
692
- let retries = 0;
693
- let pending = false;
694
- let observedComposer = null;
695
- let resizeObserver = null;
696
- const samePoint = (a, b) => !!a && !!b && Math.abs(a.x - b.x) < 1 && Math.abs(a.y - b.y) < 1;
697
- const bindComposerResize = (element) => {
698
- if (!resizeObserver || observedComposer === element) return;
699
- if (observedComposer) resizeObserver.unobserve(observedComposer);
700
- observedComposer = element;
701
- if (observedComposer) resizeObserver.observe(observedComposer);
702
- };
703
- const measure = () => {
704
- if (disposed) return;
705
- const found = conversationComposer();
706
- // 历史会话切换时输入卡可能比浮层更晚挂载;没有真实卡片就不显示,绝不猜到正文中。
707
- if (!found) {
708
- bindComposerResize(null);
709
- setDocks(null);
710
- if (retries++ < 20) schedule(140);
711
- return;
712
- }
713
- retries = 0;
714
- bindComposerResize(found.element);
715
- const composer = found.rect;
716
- const right = composerPointFromRect(composer, "right");
717
- // 三个停泊位共用同一输入框上沿,避免重复扫描整个页面读取布局。
718
- const next = {
719
- right,
720
- left: composerPointFromRect(composer, "left"),
721
- ready: composerPointFromRect(composer, "ready"),
722
- };
723
- setDocks((current) => current && samePoint(current.right, next.right) && samePoint(current.left, next.left) && samePoint(current.ready, next.ready) ? current : next);
724
- };
725
- function schedule(delay = 0, deferUntilIdle = false) {
726
- if (disposed || pending) return;
727
- pending = true;
728
- const queueFrame = () => {
729
- frame = window.requestAnimationFrame(() => {
730
- pending = false;
731
- measure();
732
- });
733
- };
734
- if (delay > 0) timer = window.setTimeout(queueFrame, delay);
735
- else if (deferUntilIdle && typeof window.requestIdleCallback === "function") idle = window.requestIdleCallback(queueFrame, { timeout: 350 });
736
- else queueFrame();
737
- };
738
- if (typeof window.ResizeObserver === "function") resizeObserver = new window.ResizeObserver(() => schedule());
739
- // 会话是 SPA 切换:输入框节点会被整体替换,但 effect 本身不会重新挂载。仅在包含发送键的子树增删时重测。
740
- const mutationObserver = typeof window.MutationObserver === "function" ? new window.MutationObserver((mutations) => {
741
- const hasComposerChange = mutations.some((mutation) => Array.from(mutation.addedNodes).concat(Array.from(mutation.removedNodes)).some((node) => {
742
- if (!node || node.nodeType !== 1) return false;
743
- return (node.matches && node.matches(COMPOSER_SEND_SELECTOR)) || (node.querySelector && node.querySelector(COMPOSER_SEND_SELECTOR));
744
- }));
745
- if (hasComposerChange) schedule(40);
746
- }) : null;
747
- if (mutationObserver && document.body) mutationObserver.observe(document.body, { childList: true, subtree: true });
748
- schedule(0, true);
749
- // 部分会话会复用同一输入框节点、只改变祖先布局;低频校准覆盖这种无 DOM 替换的位移。
750
- // 校准只在任务活动时才值得跑:空闲期每 700ms 一次全页 querySelectorAll + getBoundingClientRect
751
- // 是持续掉帧的来源(每次都强制布局)。空闲 20s 后降频到 5s,任务恢复立即回到快节奏。
752
- let calibrationInterval = 700;
753
- let lastActivityAt = Date.now();
754
- const markActivity = () => { lastActivityAt = Date.now(); if (calibrationInterval !== 700) { calibrationInterval = 700; restartPoll(); } };
755
- const unsubscribeActivity = animationStore.subscribe(markActivity);
756
- let pollTimer = 0;
757
- function restartPoll() {
758
- if (pollTimer) window.clearInterval(pollTimer);
759
- pollTimer = window.setInterval(() => {
760
- if (Date.now() - lastActivityAt > 20000 && calibrationInterval !== 5000) calibrationInterval = 5000;
761
- schedule();
762
- }, calibrationInterval);
763
- }
764
- restartPoll();
765
- const onResize = () => {
766
- if (idle && typeof window.cancelIdleCallback === "function") window.cancelIdleCallback(idle);
767
- if (timer) window.clearTimeout(timer);
768
- pending = false;
769
- schedule();
770
- };
771
- window.addEventListener("resize", onResize, { passive: true });
772
- return () => {
773
- disposed = true;
774
- window.cancelAnimationFrame(frame);
775
- if (idle && typeof window.cancelIdleCallback === "function") window.cancelIdleCallback(idle);
776
- if (timer) window.clearTimeout(timer);
777
- if (pollTimer) window.clearInterval(pollTimer);
778
- if (mutationObserver) mutationObserver.disconnect();
779
- if (resizeObserver) resizeObserver.disconnect();
780
- unsubscribeActivity();
781
- window.removeEventListener("resize", onResize);
782
- };
783
- }, [enabled]);
784
- return docks;
785
- }
786
- // 三个星系分处内容区右上、左上、左下,路线有足够的飞行距离且避开侧栏和输入框。
787
- // 返回中心点还会被巡航线路和归航飞船共用,避免“从空气里飞回来”。
788
- function spaceGalaxyLayout() {
789
- const vw = typeof window === "undefined" ? 1280 : window.innerWidth;
790
- const vh = typeof window === "undefined" ? 900 : window.innerHeight;
791
- const contentLeft = 316;
792
- const make = (x, y, scale) => ({ x, y, scale, cx: x + 123, cy: y + 123 });
793
- return {
794
- alpha: make(Math.max(contentLeft + 420, vw - 360), 72, .50),
795
- beta: make(contentLeft, Math.max(142, Math.min(232, vh * .18)), .48),
796
- gamma: make(contentLeft + 18, Math.max(420, vh - 382), .48),
797
- };
798
- }
799
- function freighterAnchorAt(point) {
800
- // 飞船外框为 210×88,飞船主体中心约在 (88, 32),对齐到星系中心而不是左上角。
801
- return { x: Math.round(point.x - 88), y: Math.round(point.y - 32) };
802
- }
803
- function flightAngle(from, to) {
804
- return Math.round(Math.atan2(to.y - from.y, to.x - from.x) * 180 / Math.PI);
805
- }
806
- function FreighterHull(props) {
807
- const units = Math.max(1, Number(props.units) || 1);
808
- return <span className="dkan-freighter">
809
- <i className="dkan-freighter-engine"><i /><i /></i>
810
- <i className="dkan-freighter-wing left" /><i className="dkan-freighter-wing right" /><i className="dkan-freighter-fin" />
811
- <i className="dkan-freighter-hull" /><i className="dkan-freighter-canopy" /><i className="dkan-freighter-nose" />
812
- <span className={"dkan-freighter-cargo" + (props.empty ? " empty" : "")}>{Array.from({ length: units }, (_, i) => <i key={i} />)}</span>
813
- </span>;
814
- }
815
- function DeliveryFreighter(props) {
816
- const d = props.delivery;
817
- return (
818
- <div className="dkan-delivery" style={{
819
- "--dkan-from-x": d.from.x + "px", "--dkan-from-y": d.from.y + "px",
820
- "--dkan-to-x": d.to.x + "px", "--dkan-to-y": d.to.y + "px",
821
- "--dkan-delivery-duration": d.duration + "ms",
822
- }} aria-hidden="true">
823
- <span className="dkan-delivery-trail" />
824
- <FreighterHull units={d.units} />
825
- <span className="dkan-delivery-label"><b>归航货运舰</b><span>输出 {fmtCompact(d.tokens)} Tokens · {d.units} 舱</span></span>
826
- </div>
827
- );
828
- }
829
- function ReadyFreighter(props) {
830
- const dock = props.dock || fallbackComposerDockPoint("ready");
831
- return <div className="dkan-ready-freighter" style={{ left: dock.x, top: dock.y }} aria-hidden="true"><FreighterHull units={3} empty /><span>待命 · 等待下一条任务</span></div>;
832
- }
833
- function MissionDeparture(props) {
834
- const d = props.departure;
835
- return <div className="dkan-departure" style={{
836
- "--dkan-from-x": d.from.x + "px", "--dkan-from-y": d.from.y + "px",
837
- "--dkan-to-x": d.to.x + "px", "--dkan-to-y": d.to.y + "px",
838
- "--dkan-flight-angle": d.angle + "deg", "--dkan-departure-duration": d.duration + "ms",
839
- }} aria-hidden="true"><FreighterHull units={3} empty /></div>;
840
- }
841
- function ProgrammingStuck(props) {
842
- const waiting = !!props.waiting;
843
- return <div className={"dkan-space-stuck" + (waiting ? " waiting" : "")} aria-hidden="true"><FreighterHull units={3} empty /><span className="dkan-stuck-code"><i />{`{ }`}</span><strong>{waiting ? "等待确认" : "编程等待中"}</strong><small>{truncate((props.task && props.task.title) || "工具暂无新响应", 28)}</small></div>;
844
- }
845
-
846
- // ---------- 全局浮层:轮询 + 动效 + 通知(功能启用即常驻) ----------
847
- export function AnimationOverlay(props) {
848
- const ctx = props && props.ctx;
849
- const snap = useAnimation();
850
- const [toasts, setToasts] = useState([]);
851
- const [flourish, setFlourish] = useState(null); // { key, err } 完成瞬间的一次性流光
852
- const [bursts, setBursts] = useState([]); // 交互反馈动画队列
853
- const [deliveries, setDeliveries] = useState([]); // 星际模式完成时停泊在输入框上方的归航货运舰
854
- const [departures, setDepartures] = useState([]); // 从输入框起飞的出航舰
855
- const prevActiveRef = useRef(null);
856
- const burstIdRef = useRef(0);
857
- const deliveryIdRef = useRef(0);
858
- const departureTimersRef = useRef(new Map());
859
- const lastImmediateDepartureRef = useRef(0);
860
-
861
- useEffect(() => () => {
862
- for (const timer of departureTimersRef.current.values()) clearTimeout(timer);
863
- departureTimersRef.current.clear();
864
- }, []);
865
-
866
- // 触发一个交互反馈动画(自动清理)
867
- const pushBurst = useCallback((type, x, y, bits) => {
868
- const id = ++burstIdRef.current;
869
- setBursts((prev) => prev.concat([{ id, type, x, y, bits }]).slice(-6));
870
- setTimeout(() => setBursts((prev) => prev.filter((b) => b.id !== id)), 2600);
871
- }, []);
872
- // 发送即出发:从左侧归航位先缓慢滑行,再逐段加速,最终并入右上角星系。
873
- const launchMission = useCallback(() => {
874
- const cfg = animationStore.snap.status && animationStore.snap.status.config;
875
- if (!cfg || !cfg.animationEnabled || cfg.effectMode !== "space") return false;
876
- const id = ++deliveryIdRef.current;
877
- const from = composerDockPoint("left");
878
- const targetGalaxy = spaceGalaxyLayout().alpha;
879
- const to = freighterAnchorAt({ x: targetGalaxy.cx, y: targetGalaxy.cy });
880
- const departure = {
881
- id, from, to,
882
- angle: flightAngle(from, to),
883
- duration: 8600,
884
- };
885
- lastImmediateDepartureRef.current = Date.now();
886
- setDeliveries([]); // 满载归港舰重新出航前离开停泊位
887
- setDepartures((prev) => prev.concat([departure]).slice(-3));
888
- departureTimersRef.current.set(id, setTimeout(() => {
889
- departureTimersRef.current.delete(id);
890
- setDepartures((prev) => prev.filter((item) => item.id !== id));
891
- }, departure.duration));
892
- return true;
893
- }, []);
894
-
895
- // 点击监听:识别用户操作 → 即时反馈(发送/新会话/切会话/切工作目录)
896
- useEffect(() => {
897
- if (typeof document === "undefined") return;
898
- const onClick = (e) => {
899
- const t = e.target;
900
- if (!t || !t.closest) return;
901
- const sendBtn = t.closest('button[aria-label="Send message"], button[aria-label*="发送"]');
902
- const newSession = t.closest('button[aria-label*="New session"], button[aria-label*="新会话"], button[aria-label*="New Session"]');
903
- const workspace = t.closest('button[aria-label*="Choose workspace"], button[aria-label*="工作目录"], button[aria-label*="workspace"]');
904
- const sessionItem = t.closest('[role="treeitem"]');
905
- if (sendBtn) {
906
- pushBurst("plane", e.clientX, e.clientY);
907
- launchMission();
908
- } else if (newSession) {
909
- pushBurst("spark", e.clientX, e.clientY, makeBits(10, () => ({
910
- dx: (Math.random() - 0.5) * 70, dy: (Math.random() - 0.5) * 70,
911
- c: ["#2563eb", "#0d9488", "#b45309", "#be185d"][Math.floor(Math.random() * 4)],
912
- })));
913
- } else if (workspace) {
914
- pushBurst("flash");
915
- } else if (sessionItem) {
916
- pushBurst("streak");
917
- }
918
- };
919
- document.addEventListener("click", onClick, true);
920
- return () => document.removeEventListener("click", onClick, true);
921
- }, [pushBurst, launchMission]);
922
-
923
- // 任务状态变化:没有直接发送事件时,仍由状态变化兜底从左侧起飞。
924
- const taskCountRef = useRef(0);
925
- useEffect(() => {
926
- const st = snap.status;
927
- if (!st || !st.active) return;
928
- const n = st.active.length;
929
- if (n > taskCountRef.current) {
930
- pushBurst("surge");
931
- const cfg = st.config || {};
932
- if (cfg.animationEnabled && cfg.effectMode === "space" && Date.now() - lastImmediateDepartureRef.current > 10000) launchMission();
933
- }
934
- taskCountRef.current = n;
935
- }, [snap.status, pushBurst, launchMission]);
936
-
937
- // 轮询:任务中 850ms,实时捕捉 chunk/工具活动;空闲 6s / 出错 15s;页面切回立即刷新。
938
- useEffect(() => {
939
- let stopped = false;
940
- let timer = null;
941
- const loop = async () => {
942
- await animationStore.refresh();
943
- if (stopped) return;
944
- const s = animationStore.snap;
945
- timer = setTimeout(loop, s.error ? 15000 : (s.status && s.status.active && s.status.active.length > 0 ? 850 : 6000));
946
- };
947
- loop();
948
- const onVisible = () => {
949
- if (!stopped && typeof document !== "undefined" && document.visibilityState === "visible") animationStore.refresh();
950
- };
951
- document.addEventListener("visibilitychange", onVisible);
952
- return () => { stopped = true; if (timer) clearTimeout(timer); document.removeEventListener("visibilitychange", onVisible); };
953
- }, []);
954
-
955
- // 任务结束检测:上一轮活跃、本轮消失 → 通知 + 流光(找最近完成记录补全信息)
956
- useEffect(() => {
957
- const st = snap.status;
958
- if (!st || !st.active) return;
959
- const prev = prevActiveRef.current;
960
- if (prev) {
961
- const currIds = new Set(st.active.map((x) => x.sessionId));
962
- for (const task of prev) {
963
- if (currIds.has(task.sessionId)) continue;
964
- const record = (st.recent || []).find((r) => r.sessionId === task.sessionId) || null;
965
- handleTaskEnd(task, record, st.config || {});
966
- }
967
- }
968
- prevActiveRef.current = st.active.slice();
969
- }, [snap.status]);
970
-
971
- // ===== 需确认检测:工具等待用户批准时通知(页内卡片 + 确认音 + 后台系统通知) =====
972
- // host 从会话流 approval/asked 收集待确认项,approval/decided 后移除;
973
- // 这里对"任务首次出现待确认"触发一次提醒,已提醒过的会话不再重复打扰。
974
- const remindedApprovalsRef = useRef(new Map()); // sessionId -> 提醒过的 approvalId 集合
975
- useEffect(() => {
976
- const st = snap.status;
977
- if (!st || !st.active) return;
978
- const cfg = st.config || {};
979
- const activeIds = new Set(st.active.map((x) => x.sessionId));
980
- for (const [sid, seen] of remindedApprovalsRef.current) {
981
- if (!activeIds.has(sid)) remindedApprovalsRef.current.delete(sid);
982
- }
983
- for (const task of st.active) {
984
- const approvals = Array.isArray(task.approvals) ? task.approvals : [];
985
- if (approvals.length === 0) continue;
986
- let seen = remindedApprovalsRef.current.get(task.sessionId);
987
- if (!seen) { seen = new Set(); remindedApprovalsRef.current.set(task.sessionId, seen); }
988
- const fresh = approvals.filter((a) => a && !seen.has(a.id));
989
- if (fresh.length === 0) continue;
990
- for (const a of fresh) seen.add(a.id);
991
- // 提醒开关在"收到新确认项"时检查,保证开关中途打开对下一项生效
992
- if (!cfg.notifyEnabled || cfg.notifyOnConfirm === false) continue;
993
- const toolName = fresh[0].toolName || "";
994
- const scene = toolLabel(toolName);
995
- const lines = [
996
- "任务:" + (task.title || "(无标题)"),
997
- "工具:" + (toolName || "未知") + (scene ? "(" + scene + ")" : ""),
998
- ];
999
- if (fresh[0].reason) lines.push("说明:" + truncate(fresh[0].reason, 140));
1000
- lines.push(fresh.length > 1 ? "共 " + fresh.length + " 项等待你的确认" : "请在会话里确认后继续");
1001
- const toast = {
1002
- id: Date.now() + Math.random(),
1003
- kind: "confirm",
1004
- title: "任务需要确认",
1005
- body: lines.join("\n"),
1006
- // 需确认常驻到用户处理(notifyStayMs=0 同款语义),关掉开关的间隙也不会自动消失
1007
- stayMs: 0,
1008
- };
1009
- setToasts((prev) => prev.concat([toast]).slice(-4));
1010
- if (cfg.soundNotify !== false) playAskSound(cfg.soundEffect);
1011
- if (cfg.systemNotify && typeof document !== "undefined" && document.hidden
1012
- && typeof Notification !== "undefined" && Notification.permission === "granted") {
1013
- try {
1014
- new Notification("dsh 任务需要确认", {
1015
- body: (task.title || "(无标题)") + " · " + (toolName || "工具") + " 等待确认",
1016
- tag: "dsh-dock-approval-" + task.sessionId,
1017
- });
1018
- } catch { /* 系统通知失败不影响页面内通知 */ }
1019
- }
1020
- }
1021
- }, [snap.status]);
1022
-
1023
- const handleTaskEnd = (task, record, cfg) => {
1024
- const reason = (record && record.endReason) || "";
1025
- const success = isSuccessReason(reason);
1026
- const info = endInfo(reason);
1027
- // 动画侧:完成瞬间一缕流光掠过 + 成功时顶部彩带庆祝(动画关时不渲染)
1028
- if (cfg.animationEnabled) {
1029
- setFlourish({ key: Date.now(), err: !success });
1030
- // 星际远征把完成结果送回用户正在输入的位置。速度继承最后一帧任务节奏;
1031
- // 货舱数量及标签只取 Host 已记录的真实 outputTokens。
1032
- if (cfg.effectMode === "space") {
1033
- const cargo = outputCargo((record && record.outputTokens) || task.outputTokens);
1034
- const id = ++deliveryIdRef.current;
1035
- const taskSpeed = Math.max(.9, Number(speed) || 1);
1036
- const duration = Math.round(Math.max(8000, Math.min(9400, 10000 / Math.sqrt(taskSpeed))));
1037
- // 从三个星系中随机挑选归航起点,任务完成时才能知道本次货舰从哪里回来。
1038
- const galaxies = spaceGalaxyLayout();
1039
- const returnGalaxy = galaxies[["alpha", "beta", "gamma"][Math.floor(Math.random() * 3)]];
1040
- const from = freighterAnchorAt({ x: returnGalaxy.cx, y: returnGalaxy.cy });
1041
- const to = composerDockPoint("left");
1042
- // 满载舰最终停入左侧归航位,和右上出发星系形成清晰的往返方向。
1043
- const delivery = Object.assign({ id, from, to, duration }, cargo);
1044
- setDeliveries([delivery]);
1045
- }
1046
- if (success) {
1047
- pushBurst("confetti", 0, 0, makeBits(18, (i) => ({
1048
- l: 4 + (i / 18) * 92 + Math.random() * 3,
1049
- dx: (Math.random() - 0.5) * 60,
1050
- r: Math.random() * 720 - 360,
1051
- d: Math.random() * 0.35,
1052
- c: ["#2563eb", "#0d9488", "#b45309", "#be185d", "#7c3aed"][i % 5],
1053
- })));
1054
- }
1055
- }
1056
- // 通知侧:与动画开关完全独立
1057
- if (!cfg.notifyEnabled) return;
1058
- const wanted = success ? cfg.notifyOnComplete : cfg.notifyOnError;
1059
- if (!wanted) return;
1060
- const models = record && record.models && record.models.length ? record.models.join(", ")
1061
- : (task.models && task.models.length ? task.models.join(", ") : "未知模型");
1062
- const provider = (record && record.provider) || task.provider || "";
1063
- const startTs = (record && record.startTime) || task.startTime;
1064
- const endTs = (record && record.endTime) || Date.now();
1065
- const lines = [
1066
- "任务:" + ((record && record.title) || task.title || "(无标题)"),
1067
- "模型:" + models + (provider ? "(" + provider + ")" : ""),
1068
- "耗时:" + fmtDur((record && record.duration) || (endTs - startTs))
1069
- + "(" + fmtTime(startTs) + " → " + fmtTime(endTs) + ")",
1070
- "回合 " + ((record && record.turns) || task.turns || 0)
1071
- + " · 步骤 " + ((record && record.steps) || task.steps || 0)
1072
- + (((record && record.toolCalls) || task.toolCalls) ? " · 工具 " + ((record && record.toolCalls) || task.toolCalls) + " 次" : ""),
1073
- "Token:输入 " + fmtNum((record && record.inputTokens) || task.inputTokens)
1074
- + " / 输出 " + fmtNum((record && record.outputTokens) || task.outputTokens),
1075
- ];
1076
- if (record && record.lastText) lines.push("摘要:" + truncate(record.lastText, 140));
1077
- if (!success && record && record.errorMessage) lines.push("错误:" + truncate(record.errorMessage, 120));
1078
- const title = success ? "任务完成" : "任务" + info.label;
1079
- const toast = {
1080
- id: Date.now() + Math.random(),
1081
- kind: success ? "success" : (info.cls === "err" ? "error" : "warn"),
1082
- title,
1083
- body: lines.join("\n"),
1084
- stayMs: typeof cfg.notifyStayMs === "number" ? cfg.notifyStayMs : 8000,
1085
- };
1086
- setToasts((prev) => prev.concat([toast]).slice(-4));
1087
- // 提示音:任务结束时播放(完成/异常配套音);与系统通知独立开关
1088
- if (cfg.soundNotify !== false) playDoneSound(success, cfg.soundEffect);
1089
- // 系统通知:仅页面处于后台时推送,避免前台重复打扰
1090
- if (cfg.systemNotify && typeof document !== "undefined" && document.hidden
1091
- && typeof Notification !== "undefined" && Notification.permission === "granted") {
1092
- try {
1093
- new Notification("dsh " + title, {
1094
- body: ((record && record.title) || task.title || "(无标题)") + " · " + fmtDur((record && record.duration) || (endTs - startTs)),
1095
- tag: "dsh-dock-animation-" + task.sessionId,
1096
- });
1097
- } catch { /* 系统通知失败不影响页面内通知 */ }
1098
- }
1099
- };
1100
-
1101
- const st = snap.status;
1102
- const cfg = st && st.config;
1103
- const active = st && st.active ? st.active : [];
1104
- const animOn = !!(cfg && cfg.animationEnabled && active.length > 0);
1105
- const mode = cfg && cfg.effectMode ? cfg.effectMode : "flow";
1106
- // 机器人阶段:取最近发生阶段变化的活跃任务。
1107
- const phase = active.length > 0
1108
- ? active.reduce((best, x) => (!best || (x.phaseAt || 0) > (best.phaseAt || 0) ? x : best), null).phase
1109
- : "think";
1110
- // 有会话在等用户确认时,状态文字让位给"等待确认"(优先级最高:不动手就卡住)。
1111
- // 会话级卡片(等待确认/编程等待中)只对"当前正在看的会话"出现,别的会话等待不打扰。
1112
- const waitingTask = active.find((x) => Array.isArray(x.approvals) && x.approvals.length > 0) || null;
1113
- const sessionTitleRef = useRef({ at: 0, title: "" });
1114
- const currentTitle = useMemo(() => {
1115
- // 标题查询要 getBoundingClientRect(强制布局),限频 1s;面板页/任务行为不受此限制
1116
- const c = sessionTitleRef.current;
1117
- if (typeof window === "undefined") return "";
1118
- if (Date.now() - c.at < 1000) return c.title;
1119
- c.at = Date.now();
1120
- c.title = currentSessionTitle();
1121
- return c.title;
1122
- }, [snap.status]);
1123
- const onScreenTasks = useMemo(() => active.filter((x) => taskOnScreen(x, currentTitle)), [active, currentTitle]);
1124
- const waitingTaskOnScreen = onScreenTasks.find((x) => Array.isArray(x.approvals) && x.approvals.length > 0) || null;
1125
- useTicker(ctx, animOn);
1126
- // 徽标计时:以最早开始的任务为基准
1127
- const now = Date.now();
1128
- const elapsed = active.length > 0 ? fmtClock(now - Math.min(...active.map((x) => x.startTime || now))) : "";
1129
- // 面板打开时隐藏环境动效(徽标/细线/巡航/机器人),避免与弹窗重叠错位
1130
- const [panelOpen, setPanelOpen] = useState(false);
1131
- useEffect(() => {
1132
- setPanelOpen(panelNav.open);
1133
- return subscribePanel(() => setPanelOpen(panelNav.open));
1134
- }, []);
1135
- // 任务速度:优先使用 host 从 chunk/tool 实时累积的活动脉冲;
1136
- // token usage 只在完整消息后才到达,不能再作为唯一速度来源。
1137
- const [speed, setSpeed] = useState(1);
1138
- const speedRef = useRef({ ticks: -1, at: 0 });
1139
- useEffect(() => {
1140
- if (!st || active.length === 0) { speedRef.current = { ticks: -1, at: 0 }; setSpeed(1); return; }
1141
- const ticks = active.reduce((a, x) => a + (x.motionTicks || 0), 0);
1142
- const t = Date.now();
1143
- const prev = speedRef.current;
1144
- if (prev.ticks < 0 || t - prev.at < 300) { speedRef.current = { ticks, at: t }; return; }
1145
- const rate = Math.max(0, (ticks - prev.ticks) / ((t - prev.at) / 1000));
1146
- const phaseBoost = phase === "write" || phase === "code" ? .28 : (phase === "search" ? .18 : 0);
1147
- const next = Math.max(.9, Math.min(4, Number((.9 + rate / 14 + phaseBoost).toFixed(2))));
1148
- setSpeed((current) => Math.abs(current - next) < .03 ? current : next);
1149
- speedRef.current = { ticks, at: t };
1150
- }, [snap.status]);
1151
- const ambientOn = animOn && !panelOpen;
1152
- const spaceModeOn = !!(cfg && cfg.animationEnabled && mode === "space" && !panelOpen);
1153
- const spaceDocks = useSpaceDocks(spaceModeOn);
1154
- const spaceGalaxies = spaceModeOn && spaceDocks ? spaceGalaxyLayout() : null;
1155
- // Host 在编程阶段持续 18 秒没有 chunk/tool 活动,才把它视为卡住;正常长任务不会误触发。
1156
- // 卡住卡片同样只对当前会话出现。
1157
- const stuckTask = onScreenTasks.find((task) => task.phase === "code" && now - (task.lastActivityAt || task.phaseAt || now) >= 18000) || null;
1158
- const robotScaleFromConfig = Math.max(.85, Math.min(2.2, Number(cfg && cfg.robotScale) || 1.35));
1159
- const speedStyle = { "--dkan-speed": Number(speed).toFixed(2) };
1160
- const saveRobotScale = useCallback((robotScale) => {
1161
- rpcCall("config", { robotScale })
1162
- .then((d) => animationStore.applyConfig(d && d.config))
1163
- .catch(() => { /* 浮层保存失败不打断任务动画;面板页会显示可操作的错误 */ });
1164
- }, []);
1165
-
1166
- // ===== 机器人卡片自由拖拽:整卡可拖到屏幕任意位置(localStorage 持久化),防止固定遮挡 =====
1167
- const [botPos, setBotPos] = useState(null); // null = 停泊默认位(右下、输入卡上方)
1168
- const botPosRef = useRef(null);
1169
- const botDragRef = useRef(null); // { sx, sy, ox, oy, moved }
1170
- const botResizeRef = useRef(null); // { sx, sy, scale }
1171
- const [botScaleDraft, setBotScaleDraft] = useState(null);
1172
- const botClickBlockRef = useRef(false);
1173
- useEffect(() => {
1174
- try {
1175
- if (typeof localStorage === "undefined") return;
1176
- const raw = localStorage.getItem("dsh-dock/anim/robot-pos/v1");
1177
- const p = raw ? JSON.parse(raw) : null;
1178
- if (p && typeof p.x === "number" && typeof p.y === "number" && typeof window !== "undefined") {
1179
- // 恢复时钳制回视口内(窗口可能变小了)
1180
- const card = document.querySelector(".dkan-botcard");
1181
- const w = card ? card.offsetWidth : 200, h = card ? card.offsetHeight : 160;
1182
- const x = Math.min(Math.max(8, p.x), Math.max(8, window.innerWidth - w - 8));
1183
- const y = Math.min(Math.max(8, p.y), Math.max(8, window.innerHeight - h - 8));
1184
- botPosRef.current = { x, y };
1185
- setBotPos({ x, y });
1186
- }
1187
- } catch { /* localStorage 不可用时停默认位 */ }
1188
- }, []);
1189
- const onBotPointerDown = (e) => {
1190
- if (e.button !== 0) return;
1191
- const r = e.currentTarget.getBoundingClientRect();
1192
- botDragRef.current = { sx: e.clientX, sy: e.clientY, ox: r.left, oy: r.top, moved: false };
1193
- try { e.currentTarget.setPointerCapture(e.pointerId); } catch { /* 无指针捕获也能拖 */ }
1194
- };
1195
- const onBotPointerMove = (e) => {
1196
- const d = botDragRef.current;
1197
- if (!d || typeof window === "undefined") return;
1198
- const dx = e.clientX - d.sx, dy = e.clientY - d.sy;
1199
- if (!d.moved && Math.hypot(dx, dy) < 4) return;
1200
- d.moved = true;
1201
- const el = e.currentTarget;
1202
- const w = el.offsetWidth || 200, h = el.offsetHeight || 160;
1203
- const x = Math.min(Math.max(8, d.ox + dx), window.innerWidth - w - 8);
1204
- const y = Math.min(Math.max(8, d.oy + dy), window.innerHeight - h - 8);
1205
- botPosRef.current = { x, y };
1206
- setBotPos({ x, y });
1207
- };
1208
- const onBotPointerUp = () => {
1209
- const d = botDragRef.current;
1210
- botDragRef.current = null;
1211
- if (d && d.moved) {
1212
- try { if (typeof localStorage !== "undefined") localStorage.setItem("dsh-dock/anim/robot-pos/v1", JSON.stringify(botPosRef.current)); } catch { /* 持久化失败静默 */ }
1213
- botClickBlockRef.current = true; // 拖动结束的 pointerup 不触发面板跳转
1214
- setTimeout(() => { botClickBlockRef.current = false; }, 0);
1215
- }
1216
- };
1217
- const onBotResizePointerDown = (e) => {
1218
- if (e.button !== 0) return;
1219
- e.preventDefault();
1220
- e.stopPropagation();
1221
- botResizeRef.current = { sx: e.clientX, sy: e.clientY, scale: botScaleDraft == null ? robotScaleFromConfig : botScaleDraft };
1222
- try { e.currentTarget.setPointerCapture(e.pointerId); } catch { /* 无指针捕获也能缩放 */ }
1223
- };
1224
- const onBotResizePointerMove = (e) => {
1225
- const d = botResizeRef.current;
1226
- if (!d) return;
1227
- e.preventDefault();
1228
- e.stopPropagation();
1229
- // 保持场景比例;以右下角手柄向外拖动放大,向内拖动缩小。
1230
- const delta = ((e.clientX - d.sx) + (e.clientY - d.sy)) / 2;
1231
- const next = Math.max(.85, Math.min(2.2, Math.round((d.scale + delta / 220) * 100) / 100));
1232
- setBotScaleDraft(next);
1233
- // 已拖到自定义位置时,放大也始终留在可视区域内。
1234
- if (botPosRef.current && typeof window !== "undefined") {
1235
- const w = 220 * next + 24, h = 132 * next + 46;
1236
- const x = Math.min(Math.max(8, botPosRef.current.x), Math.max(8, window.innerWidth - w - 8));
1237
- const y = Math.min(Math.max(8, botPosRef.current.y), Math.max(8, window.innerHeight - h - 8));
1238
- botPosRef.current = { x, y };
1239
- setBotPos({ x, y });
1240
- }
1241
- };
1242
- const onBotResizePointerUp = (e) => {
1243
- if (!botResizeRef.current) return;
1244
- e.preventDefault();
1245
- e.stopPropagation();
1246
- const next = botScaleDraft == null ? robotScaleFromConfig : botScaleDraft;
1247
- botResizeRef.current = null;
1248
- saveRobotScale(next);
1249
- try { if (botPosRef.current && typeof localStorage !== "undefined") localStorage.setItem("dsh-dock/anim/robot-pos/v1", JSON.stringify(botPosRef.current)); } catch { /* 位置保存失败不影响缩放 */ }
1250
- setBotScaleDraft(null);
1251
- };
1252
- const onBotClick = () => {
1253
- if (botClickBlockRef.current) return;
1254
- openPanel("animation");
1255
- };
1256
- const displayRobotScale = botScaleDraft == null ? robotScaleFromConfig : botScaleDraft;
1257
- const botCardStyle = botPos
1258
- ? Object.assign({}, speedStyle, { "--dkan-bot-scale": displayRobotScale, left: botPos.x, top: botPos.y, right: "auto", bottom: "auto" })
1259
- : Object.assign({}, speedStyle, { "--dkan-bot-scale": displayRobotScale });
1260
-
1261
- // Host 不可用(旧宿主未重启等):浮层整体静默,面板页会给提示
1262
- if (!st) return null;
1263
- return (<>
1264
- {/* 顶部流光细线(flow 模式 / 任务进行中) */}
1265
- {ambientOn && mode === "flow" ? <div className="dkan-line" style={speedStyle} aria-hidden="true" /> : null}
1266
- {/* 氛围动效(代码雨/星野/极光/星际远征,任务进行中渲染,速度随吞吐) */}
1267
- {ambientOn && AMBIENT_EFFECT_MODES.has(mode) && (mode !== "space" || spaceGalaxies)
1268
- ? <AmbientLayer mode={mode} phase={phase} speed={Number(speed).toFixed(2)} tasks={active} galaxies={spaceGalaxies} /> : null}
1269
- {/* 星际模式:空闲时在输入框上方待命;任务卡住时在中央呈现编程等待状态。 */}
1270
- {spaceModeOn && spaceDocks && active.length === 0 && deliveries.length === 0 ? <ReadyFreighter dock={spaceDocks.ready} /> : null}
1271
- {spaceModeOn && stuckTask ? <ProgrammingStuck task={stuckTask} waiting={!!(stuckTask.approvals && stuckTask.approvals.length)} /> : null}
1272
- {!panelOpen ? departures.map((departure) => <MissionDeparture key={departure.id} departure={departure} />) : null}
1273
- {/* 环屏巡航:一颗光点沿屏幕边缘转圈(orbit 模式) */}
1274
- {ambientOn && mode === "orbit" ? <div className="dkan-orbit" style={speedStyle} aria-hidden="true" /> : null}
1275
- {/* 交互反馈层:纸飞机/火花/流光/微光/光涌/彩带(与动画开关独立,只要功能启用即回应操作) */}
1276
- <BurstLayer bursts={bursts} />
1277
- {/* 桌面伙伴:背侧视角紧凑双工位人物,阶段与任务同步;整卡可拖到任意位置(robot 模式) */}
1278
- {ambientOn && mode === "robot" ? (
1279
- <div className="dkan-botcard" role="button" tabIndex={0} style={Object.assign({}, botCardStyle, waitingTask ? { "--dkan-phase": "#b45309" } : null)}
1280
- title={active.length + " 个任务 · " + (waitingTask ? "等待确认" : phaseLabel(phase)) + " · 点击查看任务动画页 · 可拖动到任意位置"}
1281
- onPointerDown={onBotPointerDown}
1282
- onPointerMove={onBotPointerMove}
1283
- onPointerUp={onBotPointerUp}
1284
- onPointerCancel={onBotPointerUp}
1285
- onClick={onBotClick}
1286
- onKeyDown={(e) => { if (e.target === e.currentTarget && (e.key === "Enter" || e.key === " ")) { e.preventDefault(); onBotClick(); } }}>
1287
- <RobotScene phase={phase} tasks={active} />
1288
- <span className="dkan-bot-cap">
1289
- <span className="n">{active.length}</span> 个任务{elapsed ? " · " + elapsed : ""} · {waitingTask ? "等待确认" : phaseLabel(phase)}
1290
- </span>
1291
- <button type="button" className="dkan-bot-resize" aria-label="调整桌面伙伴大小"
1292
- title="拖动调整桌面伙伴大小"
1293
- onPointerDown={onBotResizePointerDown}
1294
- onPointerMove={onBotResizePointerMove}
1295
- onPointerUp={onBotResizePointerUp}
1296
- onPointerCancel={onBotResizePointerUp}
1297
- onClick={(e) => e.stopPropagation()}
1298
- onKeyDown={(e) => {
1299
- if (e.key !== "ArrowUp" && e.key !== "ArrowRight" && e.key !== "ArrowDown" && e.key !== "ArrowLeft") return;
1300
- e.preventDefault(); e.stopPropagation();
1301
- const d = e.key === "ArrowUp" || e.key === "ArrowRight" ? .05 : -.05;
1302
- saveRobotScale(Math.max(.85, Math.min(2.2, Math.round((displayRobotScale + d) * 100) / 100)));
1303
- }} />
1304
- </div>
1305
- ) : null}
1306
- {/* 右下角状态徽标:dot 随模式变化(breathe/ring 装饰),颜色随任务阶段、呼吸随吞吐;点击进功能坞动画页 */}
1307
- {ambientOn && mode !== "robot" ? (
1308
- <button type="button" className={"dkan-badge" + (mode === "breathe" ? " dkan-badge-breathe" : "")}
1309
- style={Object.assign({}, speedStyle, { "--dkan-phase": phaseColor(waitingTask ? "code" : phase) })}
1310
- title={active.length + " 个任务进行中 · " + (waitingTask ? "等待确认" : phaseLabel(phase)) + " · 点击查看任务动画页"}
1311
- onClick={() => openPanel("animation")}>
1312
- <span className="dkan-dotwrap">
1313
- <span className="dkan-dot" />
1314
- {mode === "breathe" ? <span className="dkan-halo" /> : null}
1315
- {mode === "ring" ? <span className="dkan-ring" /> : null}
1316
- </span>
1317
- <span className="dkan-badge-txt"><span className="n">{active.length}</span> 个任务{elapsed ? " · " + elapsed : ""} · {waitingTask ? "等待确认" : phaseLabel(phase)}</span>
1318
- </button>
1319
- ) : null}
1320
- {/* 完成瞬间的一次性流光(成功绿 / 异常红),动画结束自动清场 */}
1321
- {flourish ? (
1322
- <div key={flourish.key} className={"dkan-done" + (flourish.err ? " err" : "")}
1323
- onAnimationEnd={() => setFlourish(null)} aria-hidden="true" />
1324
- ) : null}
1325
- {/* 星际任务完成:货运舰沿航线飞到输入框上方并保持停泊,直到下一次任务出航。 */}
1326
- {!panelOpen ? deliveries.map((delivery) => <DeliveryFreighter key={delivery.id} delivery={delivery} />) : null}
1327
- {/* 通知卡片栈(右下→右上不遮 dsh 自身 UI;右上角更常规) */}
1328
- {toasts.length > 0 ? (
1329
- <div className="dkan-toasts">
1330
- {toasts.map((t) => <Toast key={t.id} t={t} onClose={(id) => setToasts((prev) => prev.filter((x) => x.id !== id))} />)}
1331
- </div>
1332
- ) : null}
1333
- </>);
1334
- }
1335
-
1336
- // ---------- 面板页 ----------
1337
- function ModePreview({ id }) {
1338
- if (id === "flow") return <span className="dkan-prev"><span className="dkan-prev-flow" /></span>;
1339
- if (id === "ring") {
1340
- return (
1341
- <span className="dkan-prev">
1342
- <span className="dkan-dotwrap"><span className="dkan-dot" /><span className="dkan-ring" /></span>
1343
- </span>
1344
- );
1345
- }
1346
- if (id === "orbit") {
1347
- return (
1348
- <span className="dkan-prev dkan-prev-box">
1349
- <span className="dkan-prev-orbit" />
1350
- </span>
1351
- );
1352
- }
1353
- if (id === "matrix") {
1354
- return (
1355
- <span className="dkan-prev dkan-prev-matrix">
1356
- <i /><i /><i /><i /><i />
1357
- </span>
1358
- );
1359
- }
1360
- if (id === "stars") {
1361
- return (
1362
- <span className="dkan-prev dkan-prev-stars">
1363
- <i /><i /><i /><i /><i /><i />
1364
- </span>
1365
- );
1366
- }
1367
- if (id === "space") {
1368
- return <span className="dkan-prev dkan-prev-space"><i /><b /><em /></span>;
1369
- }
1370
- if (id === "aurora") {
1371
- return (
1372
- <span className="dkan-prev dkan-prev-aurora">
1373
- <i /><i />
1374
- </span>
1375
- );
1376
- }
1377
- if (["nebula", "warp", "radar", "constellation", "fireflies", "ocean", "prism", "circuit", "gravity", "lantern"].includes(id)) {
1378
- return <span className={"dkan-prev dkan-prev-new dkan-prev-" + id}>{Array.from({ length: id === "warp" ? 8 : 6 }, (_, i) => <i key={i} />)}</span>;
1379
- }
1380
- if (id === "robot") {
1381
- return (
1382
- <span className="dkan-prev dkan-prev-bot">
1383
- <RobotScene phase="code" />
1384
- </span>
1385
- );
1386
- }
1387
- return (
1388
- <span className="dkan-prev">
1389
- <span className="dkan-dotwrap dkan-breathe"><span className="dkan-dot" /><span className="dkan-halo" /></span>
1390
- </span>
1391
- );
1392
- }
1393
-
1394
- function TaskRow(props) {
1395
- const t = props.t;
1396
- const info = endInfo(t.endReason);
1397
- const waiting = !props.done && Array.isArray(t.approvals) && t.approvals.length > 0;
1398
- return (
1399
- <div className="dkan-task">
1400
- <div className="dkan-task-head">
1401
- <span className="dkan-task-title" title={t.title}>{t.title || "(无标题)"}</span>
1402
- {props.done ? <span className={"dkan-tag " + info.cls}>{info.label}</span>
1403
- : waiting ? <span className="dkan-tag warn">等待确认</span>
1404
- : <span className="dkan-tag on">{phaseLabel(t.phase)}</span>}
1405
- </div>
1406
- <div className="dkan-task-meta">
1407
- <span>{(t.models && t.models.length ? t.models.join(", ") : "未知") + (t.provider ? "(" + t.provider + ")" : "")}</span>
1408
- <span>{props.done ? fmtDur(t.duration) : fmtClock(t.elapsed)}</span>
1409
- {"turns" in t ? <span>{"回合 " + (t.turns || 0) + " · 步骤 " + (t.steps || 0) + (t.toolCalls ? " · 工具 " + t.toolCalls : "")}</span> : null}
1410
- {t.totalTokens ? <span>{"↧" + fmtCompact(t.inputTokens) + " ↥" + fmtCompact(t.outputTokens)}</span> : null}
1411
- {props.done && t.endTime ? <span>{fmtTime(t.endTime)}</span> : null}
1412
- </div>
1413
- {waiting ? (
1414
- <div className="dkan-task-err">
1415
- {t.approvals.map((a, i) => (a && a.toolName ? a.toolName : "未知工具") + (a && a.reason ? ":" + truncate(a.reason, 80) : "")).join(";")}
1416
- </div>
1417
- ) : null}
1418
- {props.done && t.errorMessage ? <div className="dkan-task-err">{truncate(t.errorMessage, 120)}</div> : null}
1419
- </div>
1420
- );
1421
- }
1422
-
1423
- function AnimationView(props) {
1424
- const ctx = props && props.ctx;
1425
- const snap = useAnimation(ctx);
1426
- const [cfg, setCfg] = useState(null); // null = 尚未加载
1427
- const [saveErr, setSaveErr] = useState("");
1428
- const [testing, setTesting] = useState(false);
1429
- const [testState, setTestState] = useState(null); // { ok, msg } 机器人测试结果
1430
- const [feishuTestState, setFeishuTestState] = useState(null); // { ok, msg } 飞书测试结果
1431
- const [feishuTesting, setFeishuTesting] = useState(false);
1432
- const cfgRef = useRef(null);
1433
- const pendingSavesRef = useRef(0); // 进行中的保存(轮询回包不覆盖乐观值)
1434
- const editingWebhookRef = useRef(false); // 钉钉 Webhook 输入中(轮询不覆盖草稿)
1435
- const editingFeishuRef = useRef(false); // 飞书 Webhook 输入中(轮询不覆盖草稿)
1436
- // 拉到新配置(含保存回包)后同步本地编辑态;保存中/输入中不覆盖
1437
- useEffect(() => {
1438
- const c = snap.status && snap.status.config;
1439
- if (!c) return;
1440
- if (pendingSavesRef.current > 0 || editingWebhookRef.current || editingFeishuRef.current) return;
1441
- if (c !== cfgRef.current) {
1442
- cfgRef.current = c;
1443
- setCfg(Object.assign({}, c));
1444
- }
1445
- }, [snap.status]);
1446
- // 面板打开时兜底拉一次(浮层通常已在轮询)
1447
- useEffect(() => {
1448
- if (!animationStore.snap.status) animationStore.refresh();
1449
- }, []);
1450
- // 乐观更新 + 立即持久化(每个开关独立保存);返回 Promise 供需要串联的操作使用
1451
- const patch = (p) => {
1452
- setSaveErr("");
1453
- setCfg(Object.assign({}, cfg, p));
1454
- pendingSavesRef.current++;
1455
- return rpcCall("config", p)
1456
- .then((d) => {
1457
- pendingSavesRef.current--;
1458
- animationStore.applyConfig(d && d.config);
1459
- })
1460
- .catch((e) => {
1461
- pendingSavesRef.current--;
1462
- setSaveErr("保存失败:" + ((e && e.message) || String(e)));
1463
- });
1464
- };
1465
- // Webhook 草稿保存:清编辑态后立即对齐一次(同步 effect 在编辑期被跳过)
1466
- const saveWebhook = async () => {
1467
- if (!editingWebhookRef.current) return;
1468
- const hook = String(cfg.dingtalkWebhook || "").trim();
1469
- await patch({ dingtalkWebhook: hook });
1470
- editingWebhookRef.current = false;
1471
- const c = animationStore.snap.status && animationStore.snap.status.config;
1472
- if (c && c !== cfgRef.current) {
1473
- cfgRef.current = c;
1474
- setCfg(Object.assign({}, c));
1475
- }
1476
- };
1477
- // 飞书 Webhook 草稿保存
1478
- const saveFeishuWebhook = async () => {
1479
- if (!editingFeishuRef.current) return;
1480
- const hook = String(cfg.feishuWebhook || "").trim();
1481
- await patch({ feishuWebhook: hook });
1482
- editingFeishuRef.current = false;
1483
- const c = animationStore.snap.status && animationStore.snap.status.config;
1484
- if (c && c !== cfgRef.current) {
1485
- cfgRef.current = c;
1486
- setCfg(Object.assign({}, c));
1487
- }
1488
- };
1489
- // 钉钉测试:草稿未保存先保存,再发测试消息
1490
- const runDingtalkTest = async () => {
1491
- setTesting(true);
1492
- setTestState(null);
1493
- try {
1494
- if (editingWebhookRef.current) {
1495
- const hook = String(cfg.dingtalkWebhook || "").trim();
1496
- if (!hook) throw new Error("请先填写 Webhook 地址");
1497
- const d = await rpcCall("config", { dingtalkWebhook: hook });
1498
- animationStore.applyConfig(d && d.config);
1499
- editingWebhookRef.current = false;
1500
- cfgRef.current = (d && d.config) || cfgRef.current;
1501
- setCfg(Object.assign({}, cfg, { dingtalkWebhook: hook }));
1502
- }
1503
- const r = await rpcCall("test");
1504
- setTestState(r && r.sent
1505
- ? { ok: true, msg: "测试消息已发送,去群里看看" }
1506
- : { ok: false, msg: (r && r.error) || "发送失败" });
1507
- } catch (e) {
1508
- setTestState({ ok: false, msg: (e && e.message) || String(e) });
1509
- } finally {
1510
- setTesting(false);
1511
- }
1512
- };
1513
- // 飞书测试:草稿未保存先保存,再发测试消息
1514
- const runFeishuTest = async () => {
1515
- setFeishuTesting(true);
1516
- setFeishuTestState(null);
1517
- try {
1518
- if (editingFeishuRef.current) {
1519
- const hook = String(cfg.feishuWebhook || "").trim();
1520
- if (!hook) throw new Error("请先填写 Webhook 地址");
1521
- const d = await rpcCall("config", { feishuWebhook: hook });
1522
- animationStore.applyConfig(d && d.config);
1523
- editingFeishuRef.current = false;
1524
- cfgRef.current = (d && d.config) || cfgRef.current;
1525
- setCfg(Object.assign({}, cfg, { feishuWebhook: hook }));
1526
- }
1527
- const r = await rpcCall("test", { target: "feishu" });
1528
- setFeishuTestState(r && r.sent
1529
- ? { ok: true, msg: "测试消息已发送,去群里看看" }
1530
- : { ok: false, msg: (r && r.error) || "发送失败" });
1531
- } catch (e) {
1532
- setFeishuTestState({ ok: false, msg: (e && e.message) || String(e) });
1533
- } finally {
1534
- setFeishuTesting(false);
1535
- }
1536
- };
1537
- const enableSystemNotify = async (next) => {
1538
- if (next && typeof Notification !== "undefined" && Notification.permission !== "granted") {
1539
- try {
1540
- const perm = await Notification.requestPermission(); // 开关点击即用户手势
1541
- if (perm !== "granted") {
1542
- setSaveErr("浏览器未授权系统通知(可在地址栏权限设置里重新允许)");
1543
- return;
1544
- }
1545
- } catch {
1546
- setSaveErr("浏览器不支持系统通知");
1547
- return;
1548
- }
1549
- }
1550
- patch({ systemNotify: next });
1551
- };
1552
-
1553
- const st = snap.status;
1554
- const active = st && st.active ? st.active : [];
1555
- const recent = st && st.recent ? st.recent.slice(0, 6) : [];
1556
- const permNote = typeof Notification === "undefined"
1557
- ? "当前浏览器不支持系统通知"
1558
- : Notification.permission === "granted" ? "已授权 · 仅页面后台时推送"
1559
- : Notification.permission === "denied" ? "已被浏览器拒绝(需在浏览器权限设置里重新允许)" : "未授权 · 开启时会请求授权,仅页面后台时推送";
1560
-
1561
- const rows = [];
1562
- if (!cfg) {
1563
- rows.push(<div key="load" className="dkan-note">{snap.error ? "状态不可用:" + snap.error : (snap.loading ? "正在拉取任务状态…" : "等待任务状态")}</div>);
1564
- } else {
1565
- const waitingCount = active.reduce((n, t) => n + (Array.isArray(t.approvals) ? t.approvals.length : 0), 0);
1566
- rows.push(
1567
- <div key="anim" className="dkan-sec">
1568
- <div className="dkan-sec-head">
1569
- <span className="dkan-sec-title">运行动画</span>
1570
- <span className="dkan-sec-sub">任务进行中才出现;克制的动效,暗/亮色自适应</span>
1571
- <span className="dkan-sec-sw">
1572
- <span className={"dkan-sec-swlabel" + (cfg.animationEnabled ? " on" : "")}>{cfg.animationEnabled ? "已开启" : "已关闭"}</span>
1573
- <button type="button" className={"dock-sw" + (cfg.animationEnabled ? " on" : "")}
1574
- role="switch" aria-checked={cfg.animationEnabled} aria-label="开关运行动画"
1575
- title={cfg.animationEnabled ? "关闭运行动画" : "开启运行动画"}
1576
- onClick={() => patch({ animationEnabled: !cfg.animationEnabled })} />
1577
- </span>
1578
- </div>
1579
- {cfg.animationEnabled ? (
1580
- <>
1581
- <div className="dkan-modes">
1582
- {EFFECT_MODES.map((m) => (
1583
- <button type="button" key={m.id}
1584
- className={"dkan-mode" + (cfg.effectMode === m.id ? " on" : "")}
1585
- onClick={() => patch({ effectMode: m.id })}>
1586
- <span className="dkan-mode-name">{m.name}</span>
1587
- <ModePreview id={m.id} />
1588
- <span className="dkan-mode-desc">{m.desc}</span>
1589
- </button>
1590
- ))}
1591
- </div>
1592
- {cfg.effectMode === "robot" ? <div className="dkan-robot-controls">
1593
- <div className="dkan-robot-controls-head">
1594
- <span>桌面伙伴大小</span>
1595
- <strong>{Math.round((Number(cfg.robotScale) || 1.35) * 100)}%</strong>
1596
- </div>
1597
- <div className="dkan-robot-size-options" role="group" aria-label="桌面伙伴大小预设">
1598
- {[[1, "紧凑"], [1.35, "默认"], [1.7, "加大"], [2.1, "特大"]].map(([scale, label]) => (
1599
- <button type="button" key={scale} className={Math.abs((Number(cfg.robotScale) || 1.35) - scale) < .03 ? "on" : ""}
1600
- onClick={() => patch({ robotScale: scale })}>{label}</button>
1601
- ))}
1602
- </div>
1603
- <div className="dkan-robot-slider-row">
1604
- <input type="range" min="0.85" max="2.2" step="0.05" value={Number(cfg.robotScale) || 1.35}
1605
- aria-label="桌面伙伴大小百分比"
1606
- onChange={(e) => patch({ robotScale: Number(e.target.value) })} />
1607
- <span>浮层右下角也可直接拖动缩放,大小会自动保存。</span>
1608
- </div>
1609
- </div> : null}
1610
- </>
1611
- ) : <div className="dkan-note">动画已关闭——只保留通知(或全部关闭)时,页面不会有任何动效。</div>}
1612
- </div>,
1613
- <div key="notify" className="dkan-sec">
1614
- <div className="dkan-sec-head">
1615
- <span className="dkan-sec-title">完成通知</span>
1616
- <span className="dkan-sec-sub">与动画互不依赖,可单独开启</span>
1617
- <span className="dkan-sec-sw">
1618
- <span className={"dkan-sec-swlabel" + (cfg.notifyEnabled ? " on" : "")}>{cfg.notifyEnabled ? "已开启" : "已关闭"}</span>
1619
- <button type="button" className={"dock-sw" + (cfg.notifyEnabled ? " on" : "")}
1620
- role="switch" aria-checked={cfg.notifyEnabled} aria-label="开关完成通知"
1621
- title={cfg.notifyEnabled ? "关闭完成通知" : "开启完成通知"}
1622
- onClick={() => patch({ notifyEnabled: !cfg.notifyEnabled })} />
1623
- </span>
1624
- </div>
1625
- {cfg.notifyEnabled ? <div className="dkan-rows-narrow">
1626
- <div className="dkan-row">
1627
- <span className="dkan-row-label">完成通知</span>
1628
- <button type="button" className={"dkm-miniswitch" + (cfg.notifyOnComplete ? " on" : "")}
1629
- onClick={() => patch({ notifyOnComplete: !cfg.notifyOnComplete })}>
1630
- {cfg.notifyOnComplete ? "开" : "关"}
1631
- </button>
1632
- <span className="dkan-row-sub">任务正常完成时通知</span>
1633
- </div>
1634
- <div className="dkan-row">
1635
- <span className="dkan-row-label">异常通知</span>
1636
- <button type="button" className={"dkm-miniswitch" + (cfg.notifyOnError ? " on" : "")}
1637
- onClick={() => patch({ notifyOnError: !cfg.notifyOnError })}>
1638
- {cfg.notifyOnError ? "开" : "关"}
1639
- </button>
1640
- <span className="dkan-row-sub">出错 / 中止 / 达输出上限时通知</span>
1641
- </div>
1642
- <div className="dkan-row">
1643
- <span className="dkan-row-label">需确认提醒</span>
1644
- <button type="button" className={"dkm-miniswitch" + (cfg.notifyOnConfirm !== false ? " on" : "")}
1645
- onClick={() => patch({ notifyOnConfirm: cfg.notifyOnConfirm === false })}>
1646
- {cfg.notifyOnConfirm !== false ? "开" : "关"}
1647
- </button>
1648
- <span className="dkan-row-sub">工具等待你批准时弹卡片并响确认音(常驻不自动消失)</span>
1649
- </div>
1650
- <div className="dkan-row">
1651
- <span className="dkan-row-label">停留时长</span>
1652
- <select className="dkan-select" value={String(cfg.notifyStayMs)}
1653
- onChange={(e) => patch({ notifyStayMs: Number(e.target.value) })}>
1654
- <option value="4000">4 秒</option>
1655
- <option value="8000">8 秒</option>
1656
- <option value="15000">15 秒</option>
1657
- <option value="30000">30 秒</option>
1658
- <option value="0">常驻(手动关闭)</option>
1659
- </select>
1660
- </div>
1661
- <div className="dkan-row">
1662
- <span className="dkan-row-label">系统通知</span>
1663
- <button type="button" className={"dkm-miniswitch" + (cfg.systemNotify ? " on" : "")}
1664
- onClick={() => enableSystemNotify(!cfg.systemNotify)}>
1665
- {cfg.systemNotify ? "开" : "关"}
1666
- </button>
1667
- <span className="dkan-row-sub">{permNote}</span>
1668
- </div>
1669
- <div className="dkan-row">
1670
- <span className="dkan-row-label">提示音</span>
1671
- <button type="button" className={"dkm-miniswitch" + (cfg.soundNotify !== false ? " on" : "")}
1672
- onClick={() => patch({ soundNotify: cfg.soundNotify === false })}>
1673
- {cfg.soundNotify !== false ? "开" : "关"}
1674
- </button>
1675
- <span className="dkan-row-sub">任务结束/需确认时播放(试听为先播完成音、后播异常音)</span>
1676
- </div>
1677
- {cfg.soundNotify !== false ? (
1678
- <div className="dkan-sounds">
1679
- {Object.keys(SOUND_LIBRARY).map((key) => (
1680
- <button type="button" key={key}
1681
- className={"dkan-sound" + (cfg.soundEffect === key ? " on" : "")}
1682
- onClick={() => patch({ soundEffect: key })}>
1683
- <span className="dkan-sound-name">
1684
- {SOUND_LIBRARY[key].name}
1685
- {cfg.soundEffect === key ? <span className="dkan-sound-cur">✓</span> : null}
1686
- </span>
1687
- <span className="dkan-sound-play" role="button" tabIndex={0}
1688
- title={"试听 " + SOUND_LIBRARY[key].name}
1689
- onClick={(e) => { e.stopPropagation(); previewSound(key); }}
1690
- onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); e.stopPropagation(); previewSound(key); } }}>▶</span>
1691
- </button>
1692
- ))}
1693
- </div>
1694
- ) : null}
1695
- </div> : <div className="dkan-note">通知已关闭——任务结束时既不弹卡片也不推系统通知。</div>}
1696
- </div>,
1697
- <div key="dingtalk" className="dkan-sec">
1698
- <div className="dkan-sec-head">
1699
- <span className="dkan-sec-title">钉钉推送</span>
1700
- <span className="dkan-sec-sub">任务结束推送到钉钉群机器人(宿主直发,浏览器关着也能推;事件跟随上方完成/异常开关)</span>
1701
- <span className="dkan-sec-sw">
1702
- <span className={"dkan-sec-swlabel" + (cfg.dingtalkEnabled ? " on" : "")}>{cfg.dingtalkEnabled ? "已开启" : "已关闭"}</span>
1703
- <button type="button" className={"dock-sw" + (cfg.dingtalkEnabled ? " on" : "")}
1704
- role="switch" aria-checked={cfg.dingtalkEnabled} aria-label="开关钉钉推送"
1705
- title={cfg.dingtalkEnabled ? "关闭钉钉推送" : "开启钉钉推送"}
1706
- onClick={() => patch({ dingtalkEnabled: !cfg.dingtalkEnabled })} />
1707
- </span>
1708
- </div>
1709
- {cfg.dingtalkEnabled ? <div className="dkan-rows-narrow">
1710
- <div className="dkan-row dkan-row-webhook">
1711
- <span className="dkan-row-label">Webhook</span>
1712
- <input type="text" className="dkan-input" spellCheck={false}
1713
- value={cfg.dingtalkWebhook || ""}
1714
- placeholder="https://oapi.dingtalk.com/robot/send?access_token=…"
1715
- onChange={(e) => {
1716
- editingWebhookRef.current = true;
1717
- setCfg(Object.assign({}, cfg, { dingtalkWebhook: e.target.value }));
1718
- }} />
1719
- <button type="button" className="dkan-btn" disabled={!editingWebhookRef.current}
1720
- onClick={saveWebhook}>
1721
- {editingWebhookRef.current ? "保存" : "已保存"}
1722
- </button>
1723
- </div>
1724
- <div className="dkan-row">
1725
- <span className="dkan-row-label">连通测试</span>
1726
- <button type="button" className="dkan-btn" disabled={testing} onClick={runDingtalkTest}>
1727
- {testing ? "发送中…" : "发送测试消息"}
1728
- </button>
1729
- {testState ? <span className={"dkan-row-sub" + (testState.ok ? " dkan-ok" : " dkan-err")}>
1730
- {testState.ok ? "✓ " : "✗ "}{testState.msg}
1731
- </span> : <span className="dkan-row-sub">用当前保存的 Webhook 发一条测试消息</span>}
1732
- </div>
1733
- <div className="dkan-note">机器人创建:钉钉群 → 设置 → 智能群助手 → 添加机器人 → 自定义(Webhook),
1734
- 安全设置选「自定义关键词」填「任务」或「dsh」(推送标题含「任务」即可命中)。</div>
1735
- </div> : <div className="dkan-note">未开启——任务结束不推送钉钉。</div>}
1736
- </div>,
1737
- <div key="feishu" className="dkan-sec">
1738
- <div className="dkan-sec-head">
1739
- <span className="dkan-sec-title">飞书推送</span>
1740
- <span className="dkan-sec-sub">任务结束推送到飞书群机器人(宿主直发,浏览器关着也能推;事件跟随上方完成/异常开关)</span>
1741
- <span className="dkan-sec-sw">
1742
- <span className={"dkan-sec-swlabel" + (cfg.feishuEnabled ? " on" : "")}>{cfg.feishuEnabled ? "已开启" : "已关闭"}</span>
1743
- <button type="button" className={"dock-sw" + (cfg.feishuEnabled ? " on" : "")}
1744
- role="switch" aria-checked={cfg.feishuEnabled} aria-label="开关飞书推送"
1745
- title={cfg.feishuEnabled ? "关闭飞书推送" : "开启飞书推送"}
1746
- onClick={() => patch({ feishuEnabled: !cfg.feishuEnabled })} />
1747
- </span>
1748
- </div>
1749
- {cfg.feishuEnabled ? <div className="dkan-rows-narrow">
1750
- <div className="dkan-row dkan-row-webhook">
1751
- <span className="dkan-row-label">Webhook</span>
1752
- <input type="text" className="dkan-input" spellCheck={false}
1753
- value={cfg.feishuWebhook || ""}
1754
- placeholder="https://open.feishu.cn/open-apis/bot/v2/hook/…"
1755
- onChange={(e) => {
1756
- editingFeishuRef.current = true;
1757
- setCfg(Object.assign({}, cfg, { feishuWebhook: e.target.value }));
1758
- }} />
1759
- <button type="button" className="dkan-btn" disabled={!editingFeishuRef.current}
1760
- onClick={saveFeishuWebhook}>
1761
- {editingFeishuRef.current ? "保存" : "已保存"}
1762
- </button>
1763
- </div>
1764
- <div className="dkan-row">
1765
- <span className="dkan-row-label">连通测试</span>
1766
- <button type="button" className="dkan-btn" disabled={feishuTesting} onClick={runFeishuTest}>
1767
- {feishuTesting ? "发送中…" : "发送测试消息"}
1768
- </button>
1769
- {feishuTestState ? <span className={"dkan-row-sub" + (feishuTestState.ok ? " dkan-ok" : " dkan-err")}>
1770
- {feishuTestState.ok ? "✓ " : "✗ "}{feishuTestState.msg}
1771
- </span> : <span className="dkan-row-sub">用当前保存的 Webhook 发一条测试消息</span>}
1772
- </div>
1773
- <div className="dkan-note">机器人创建:飞书群 → 设置 → 群机器人 → 添加机器人 → 自定义机器人(获取 Webhook 地址);
1774
- 安全设置如选「自定义关键词」填「任务」或「dsh」(推送标题含「任务」即可命中)。</div>
1775
- </div> : <div className="dkan-note">未开启——任务结束不推送飞书。</div>}
1776
- </div>
1777
- );
1778
- }
1779
- if (saveErr) rows.push(<div key="err" className="dkan-note dkan-err">{saveErr}</div>);
1780
- rows.push(
1781
- <div key="state" className="dkan-sec">
1782
- <div className="dkan-sec-head">
1783
- <span className="dkan-sec-title">运行状态</span>
1784
- <span className="dkan-sec-sub">
1785
- {snap.error ? "状态拉取失败:" + snap.error
1786
- : waitingCount > 0 ? waitingCount + " 项等待确认 · " + active.length + " 个任务进行中"
1787
- : active.length > 0 ? active.length + " 个任务进行中"
1788
- : recent.length > 0 ? "空闲 · 显示最近完成" : "空闲 · 暂无任务记录"}
1789
- </span>
1790
- <button type="button" className="dkan-refresh" onClick={() => animationStore.refresh()}>
1791
- {snap.loading ? "刷新中…" : "刷新"}
1792
- </button>
1793
- </div>
1794
- {active.length > 0 ? <div className="dkan-tasks">{active.map((t) => <TaskRow key={t.sessionId} t={t} />)}</div> : null}
1795
- {active.length === 0 && recent.length === 0
1796
- ? <div className="dkan-note">发起新会话任务后,这里会显示进行中与最近完成的任务;动画与通知同时在页面生效。</div> : null}
1797
- {recent.length > 0 ? <div className="dkan-tasks dkan-tasks-done">{recent.map((t) => <TaskRow key={t.sessionId + ":" + t.endTime} t={t} done />)}</div> : null}
1798
- </div>
1799
- );
1800
- return <div className="dkan-root">{rows}</div>;
1801
- }
1802
-
1803
- // ---------- 首页总揽概要 ----------
1804
- function AnimationStat(props) {
1805
- const snap = useAnimation(props && props.ctx);
1806
- const st = snap.status;
1807
- if (snap.error) return <span className="dkan-err">任务状态不可用(宿主需重启加载动画路由)</span>;
1808
- if (!st) return <span>等待任务状态…</span>;
1809
- if (st.active && st.active.length > 0) {
1810
- return <span>{st.active.length + " 个任务进行中 · " + fmtDur(Math.max(...st.active.map((x) => x.elapsed || 0)))}</span>;
1811
- }
1812
- const last = st.recent && st.recent[0];
1813
- return <span>{last ? "空闲 · 最近完成 " + truncate(last.title, 24) : "空闲 · 暂无任务记录"}</span>;
1814
- }
1815
-
1816
- // ---------- 样式(dkan- 前缀;全部走主题变量,暗/亮色自适应;动效时长 = 基准 / --dkan-speed) ----------
1817
- const css = [
1818
- // 顶部流光细线(3px,比首版更醒目)
1819
- ".dkan-line{position:fixed;top:0;left:0;right:0;height:3px;z-index:9990;pointer-events:none;background:color-mix(in srgb,var(--dk-accent) 30%,transparent);}",
1820
- ".dkan-line::after{content:\"\";position:absolute;top:0;bottom:0;left:-42%;width:42%;background:linear-gradient(90deg,transparent,var(--dk-accent) 60%,#fff);box-shadow:0 0 10px 1px color-mix(in srgb,var(--dk-accent) 70%,transparent);animation:dkan-flow calc(1.9s / var(--dkan-speed,1)) linear infinite;}",
1821
- "@keyframes dkan-flow{to{left:100%}}",
1822
- // 完成瞬间的一次性流光(成功绿 / 异常红),动画结束自动清场
1823
- ".dkan-done{position:fixed;top:0;left:0;right:0;height:3px;z-index:9991;pointer-events:none;background:linear-gradient(90deg,transparent,var(--dsw-alias-state-success-primary,#34d399) 50%,#fff);background-size:50% 100%;background-repeat:no-repeat;animation:dkan-done 1.1s var(--ds-ease-in-out) forwards;}",
1824
- ".dkan-done.err{background-image:linear-gradient(90deg,transparent,var(--dsw-alias-state-error-primary,#f87171) 50%,#fff);}",
1825
- "@keyframes dkan-done{0%{background-position:-60% 0;opacity:0}25%{opacity:1}100%{background-position:160% 0;opacity:0}}",
1826
- // 环屏巡航:光点沿屏幕边缘转一整圈
1827
- ".dkan-orbit{position:fixed;top:0;left:0;width:12px;height:12px;z-index:9990;pointer-events:none;border-radius:50%;background:var(--dk-accent);box-shadow:0 0 14px 4px color-mix(in srgb,var(--dk-accent) 60%,transparent),0 0 30px 8px color-mix(in srgb,var(--dk-accent) 25%,transparent);animation:dkan-orbit calc(14s / var(--dkan-speed,1)) linear infinite;}",
1828
- "@keyframes dkan-orbit{0%{top:0;left:0}25%{top:0;left:calc(100vw - 12px)}50%{top:calc(100vh - 12px);left:calc(100vw - 12px)}75%{top:calc(100vh - 12px);left:0}100%{top:0;left:0}}",
1829
- // ===== 氛围动效 =====
1830
- ".dkan-amb{position:fixed;inset:0;z-index:9989;pointer-events:none;overflow:hidden;}",
1831
- // pointer-events 不继承:全屏装饰层的子粒子(星点/流星/极光/火花/彩带)必须各自透明于点击
1832
- ".dkan-amb *,.dkan-fxwrap *{pointer-events:none;}",
1833
- // 代码雨:字符列缓落(提亮加大,速度随吞吐)
1834
- ".dkan-matrix span{position:absolute;top:-12%;writing-mode:vertical-rl;font-family:var(--ds-font-family-code,monospace);color:var(--dk-accent);text-shadow:0 0 6px color-mix(in srgb,var(--dk-accent) 60%,transparent);animation:dkan-fall linear infinite;will-change:transform;}",
1835
- "@keyframes dkan-fall{to{transform:translateY(125vh)}}",
1836
- // 星野:星点缓慢飘移 + 闪烁(加亮加发光)+ 月亮 + 流星
1837
- ".dkan-stars .dkan-star{position:absolute;border-radius:50%;background:#cfe3ff;box-shadow:0 0 4px 1px color-mix(in srgb,#8ab6ff 55%,transparent);animation:dkan-star ease-in-out infinite alternate;}",
1838
- "@keyframes dkan-star{0%{opacity:.3;transform:translate(0,0)}50%{opacity:1}100%{opacity:.45;transform:translate(var(--dx),var(--dy))}}",
1839
- // 月亮:右上弯月(radial-gradient 咬出月牙,透明区不遮挡背景;外圈柔光晕)
1840
- ".dkan-moon{position:absolute;top:9%;right:10%;width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 66% 34%,transparent 0 44%,#fdf6d8 46%,#e8d9a0 78%,#d8c78a);box-shadow:0 0 18px 4px color-mix(in srgb,#fdf6d8 35%,transparent),0 0 44px 12px color-mix(in srgb,#fdf6d8 15%,transparent);}",
1841
- // 流星:斜向划过的光带(亮头 + 渐隐尾),周期出现
1842
- ".dkan-meteor{position:absolute;width:110px;height:2px;border-radius:2px;background:linear-gradient(90deg,#fff,color-mix(in srgb,#8ab6ff 60%,transparent) 40%,transparent);transform:rotate(35deg);transform-origin:left center;opacity:0;animation:dkan-meteor 8.4s linear infinite;}",
1843
- ".dkan-meteor::before{content:\"\";position:absolute;left:-2px;top:-2px;width:6px;height:6px;border-radius:50%;background:#fff;box-shadow:0 0 8px 2px color-mix(in srgb,#cfe3ff 70%,transparent);}",
1844
- "@keyframes dkan-meteor{0%{opacity:0;transform:rotate(35deg) translateX(0)}3%{opacity:1}10%{opacity:0;transform:rotate(35deg) translateX(46vh)}100%{opacity:0;transform:rotate(35deg) translateX(46vh)}}",
1845
- // 星际远征:星尘、行星轨道和任务飞船都只在任务运行时出现,统一由 --dkan-speed 加速。
1846
- ".dkan-space{background:radial-gradient(ellipse 34% 28% at 84% 11%,color-mix(in srgb,#426aaf 15%,transparent),transparent 72%),radial-gradient(ellipse 26% 22% at 14% 58%,color-mix(in srgb,#7c4dff 10%,transparent),transparent 74%);}",
1847
- ".dkan-space-dust{position:absolute;border-radius:50%;background:#dbeafe;box-shadow:0 0 5px color-mix(in srgb,#93c5fd 62%,transparent);opacity:.2;animation:dkan-space-dust ease-in-out infinite alternate;will-change:transform,opacity;}",
1848
- "@keyframes dkan-space-dust{0%{opacity:.14;transform:translate3d(0,0,0) scale(.7)}55%{opacity:.78}100%{opacity:.26;transform:translate3d(22px,-8px,0) scale(1.15)}}",
1849
- ".dkan-space-system{position:fixed;width:246px;height:246px;border-radius:50%;opacity:.78;transform:translateZ(0) scale(var(--dkan-system-scale,1));transform-origin:center;contain:layout paint style;}.dkan-space-system.alpha{opacity:.76}.dkan-space-system.beta{opacity:.68}.dkan-space-system.gamma{opacity:.64}",
1850
- ".dkan-space-sun{position:absolute;left:50%;top:50%;width:34px;height:34px;border-radius:50%;transform:translate(-50%,-50%);background:radial-gradient(circle at 32% 28%,#fff7c2 0 9%,#ffc85a 30%,#e76f2e 74%);box-shadow:0 0 12px 4px color-mix(in srgb,#ffc85a 58%,transparent),0 0 36px 12px color-mix(in srgb,#f97316 20%,transparent);}",
1851
- ".dkan-space-orbit{position:absolute;left:50%;top:50%;border:1px solid color-mix(in srgb,#93c5fd 28%,transparent);border-radius:50%;animation:dkan-space-orbit linear infinite;transform-origin:center;}",
1852
- ".dkan-space-orbit i{position:absolute;top:50%;left:-5px;width:10px;height:10px;border-radius:50%;transform:translateY(-50%);box-shadow:0 0 9px 2px currentColor;}",
1853
- ".dkan-space-orbit.a{width:82px;height:50px;margin:-25px 0 0 -41px;animation-duration:calc(5.4s / var(--dkan-speed,1));}.dkan-space-orbit.a i{color:#67e8f9;background:#22d3ee;}",
1854
- ".dkan-space-orbit.b{width:142px;height:86px;margin:-43px 0 0 -71px;animation-duration:calc(9.2s / var(--dkan-speed,1));animation-direction:reverse;}.dkan-space-orbit.b i{width:15px;height:15px;color:#c4b5fd;background:radial-gradient(circle at 30% 28%,#ede9fe,#8b5cf6 72%);}",
1855
- ".dkan-space-orbit.c{width:212px;height:130px;margin:-65px 0 0 -106px;animation-duration:calc(15.6s / var(--dkan-speed,1));}.dkan-space-orbit.c i{width:18px;height:18px;color:#fda4af;background:radial-gradient(circle at 32% 28%,#ffe4e6,#e11d48 74%);}",
1856
- "@keyframes dkan-space-orbit{to{transform:rotate(360deg)}}",
1857
- ".dkan-space-runner{position:fixed;left:0;top:0;width:108px;height:56px;opacity:0;animation-duration:calc(10.8s / var(--dkan-speed,1));animation-timing-function:linear;animation-iteration-count:infinite;animation-delay:var(--dkan-run-delay,0s);will-change:transform,opacity;contain:layout paint style;backface-visibility:hidden;}.dkan-space-runner .dkan-freighter{left:0;top:0;transform:translateZ(0) scale(.52);transform-origin:top left;filter:none}.dkan-space-runner .dkan-freighter-engine i{animation:none;opacity:.76;box-shadow:-6px 0 8px #22d3ee}.dkan-space-runner .dkan-freighter-nose{filter:none}.dkan-space-runner.route-0{animation-name:dkan-space-route-a}.dkan-space-runner.route-1{animation-name:dkan-space-route-b}.dkan-space-runner.route-2{animation-name:dkan-space-route-c}",
1858
- "@keyframes dkan-space-route-a{0%{opacity:0;transform:translate3d(var(--dkan-alpha-x),var(--dkan-alpha-y),0) rotate(151deg)}7%,86%{opacity:.9}100%{opacity:0;transform:translate3d(var(--dkan-beta-x),var(--dkan-beta-y),0) rotate(151deg)}}@keyframes dkan-space-route-b{0%{opacity:0;transform:translate3d(var(--dkan-beta-x),var(--dkan-beta-y),0) rotate(-90deg)}7%,86%{opacity:.9}100%{opacity:0;transform:translate3d(var(--dkan-gamma-x),var(--dkan-gamma-y),0) rotate(-90deg)}}@keyframes dkan-space-route-c{0%{opacity:0;transform:translate3d(var(--dkan-gamma-x),var(--dkan-gamma-y),0) rotate(-18deg)}7%,86%{opacity:.9}100%{opacity:0;transform:translate3d(var(--dkan-alpha-x),var(--dkan-alpha-y),0) rotate(-18deg)}}",
1859
- // 完成归航:一艘货运旗舰从航线飞到真实会话输入框上方,货舱逐格表达实际输出量。
1860
- ".dkan-delivery{position:fixed;left:0;top:0;width:210px;height:88px;z-index:9994;pointer-events:none;will-change:transform,opacity;contain:layout paint style;isolation:isolate;animation:dkan-delivery-flight var(--dkan-delivery-duration,8600ms) cubic-bezier(.22,.61,.36,1) forwards;}",
1861
- "@keyframes dkan-delivery-flight{0%{opacity:0;transform:translate3d(var(--dkan-from-x),var(--dkan-from-y),0) scale(.5) rotate(-12deg)}6%{opacity:1;transform:translate3d(var(--dkan-from-x),var(--dkan-from-y),0) scale(.56) rotate(-10deg)}100%{opacity:1;transform:translate3d(var(--dkan-to-x),var(--dkan-to-y),0) scale(1) rotate(0)}}",
1862
- ".dkan-delivery-trail{position:absolute;left:-132px;top:26px;width:154px;height:4px;border-radius:999px;background:linear-gradient(90deg,transparent,#34d399 48%,#d9f99d 78%,transparent);opacity:.9;animation:dkan-delivery-trail var(--dkan-delivery-duration,8600ms) linear forwards;}@keyframes dkan-delivery-trail{0%,82%{opacity:.9}100%{opacity:0}}.dkan-freighter{position:absolute;left:10px;top:10px;width:176px;height:45px;display:block;filter:drop-shadow(0 0 9px color-mix(in srgb,#7dd3fc 62%,transparent));}.dkan-delivery .dkan-freighter,.dkan-departure .dkan-freighter{filter:none}.dkan-delivery .dkan-freighter-engine i,.dkan-departure .dkan-freighter-engine i{animation:none;opacity:.76;box-shadow:-6px 0 8px #22d3ee}.dkan-delivery .dkan-freighter-nose,.dkan-departure .dkan-freighter-nose{filter:none}.dkan-freighter-hull{position:absolute;z-index:3;left:21px;top:7px;width:126px;height:31px;background:linear-gradient(150deg,#f8fbff 0%,#9cc8ff 30%,#385d91 72%,#1d3150);clip-path:polygon(0 30%,43% 0,83% 17%,100% 50%,83% 83%,43% 100%,0 70%,12% 50%);box-shadow:inset 0 1px 0 rgb(255 255 255 / .7),inset -7px -5px 9px rgb(8 24 49 / .38);}.dkan-freighter-nose{position:absolute;z-index:4;right:10px;top:16px;width:28px;height:14px;background:linear-gradient(90deg,#a9d1ff,#e8f5ff);clip-path:polygon(0 0,100% 50%,0 100%,22% 50%);filter:drop-shadow(5px 0 5px color-mix(in srgb,#93c5fd 62%,transparent));}.dkan-freighter-canopy{position:absolute;z-index:5;left:77px;top:10px;width:39px;height:11px;border-radius:70% 48% 42% 32%;background:linear-gradient(135deg,#ecfeff 0%,#67e8f9 32%,#2563eb 75%);transform:skewX(-18deg);box-shadow:inset 0 2px 2px rgb(255 255 255 / .7),0 0 8px color-mix(in srgb,#67e8f9 42%,transparent);}.dkan-freighter-fin{position:absolute;z-index:2;left:48px;top:3px;width:24px;height:17px;background:linear-gradient(135deg,#31557f,#a7caf7);clip-path:polygon(0 100%,58% 0,100% 100%);}.dkan-freighter-wing{position:absolute;z-index:1;left:48px;width:60px;height:18px;background:linear-gradient(135deg,#24436d,#8ebaf4);}.dkan-freighter-wing.left{top:2px;clip-path:polygon(0 100%,100% 0,68% 100%)}.dkan-freighter-wing.right{top:26px;clip-path:polygon(0 0,68% 0,100% 100%)}",
1863
- ".dkan-freighter-cargo{position:absolute;z-index:6;left:37px;top:31px;width:74px;height:7px;display:flex;align-items:center;gap:2px;padding:1px 3px;border-radius:4px;background:#172842;border:1px solid color-mix(in srgb,#b8d8ff 48%,transparent);box-shadow:inset 0 1px 0 rgb(255 255 255 / .2);}.dkan-freighter-cargo i{display:block;flex:1;min-width:4px;height:5px;border-radius:1px;background:linear-gradient(135deg,#fef3c7,#f59e0b 58%,#b45309);box-shadow:0 0 4px color-mix(in srgb,#fbbf24 58%,transparent);}.dkan-freighter-cargo.empty i{background:linear-gradient(135deg,#dbeafe,#38bdf8 58%,#1d4ed8);box-shadow:0 0 4px color-mix(in srgb,#38bdf8 55%,transparent);}.dkan-freighter-engine{position:absolute;z-index:0;left:5px;top:19px;display:flex;gap:3px;align-items:center;height:11px;}.dkan-freighter-engine i{width:18px;height:5px;border-radius:60% 0 0 60%;background:linear-gradient(90deg,transparent,#67e8f9);box-shadow:-9px 0 12px #22d3ee;animation:dkan-engine .34s ease-in-out infinite alternate;}.dkan-freighter-engine i:nth-child(2){animation-delay:-.14s}",
1864
- "@keyframes dkan-engine{to{transform:scaleX(.55);opacity:.5}}",
1865
- ".dkan-delivery-label{position:absolute;left:21px;top:58px;display:flex;align-items:baseline;gap:6px;padding:4px 8px;border:1px solid color-mix(in srgb,#93c5fd 30%,var(--dsw-alias-border-l1));border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 82%,transparent);backdrop-filter:blur(10px);color:var(--dsw-alias-label-secondary);font:10px/1.2 var(--ds-font-family,system-ui,sans-serif);white-space:nowrap;}.dkan-delivery-label b{color:var(--dsw-alias-label-primary);font-weight:650}.dkan-delivery-label span{color:var(--dk-accent);}",
1866
- "@media (max-width:1100px){.dkan-space-system,.dkan-space-runner{display:none}}",
1867
- ".dkan-ready-freighter{position:fixed;width:210px;height:88px;z-index:9994;pointer-events:none;filter:drop-shadow(0 10px 14px rgb(0 0 0 / .18));}.dkan-ready-freighter>span:last-child{position:absolute;left:21px;top:58px;padding:4px 8px;border:1px solid color-mix(in srgb,#93c5fd 28%,var(--dsw-alias-border-l1));border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 80%,transparent);backdrop-filter:blur(10px);color:var(--dsw-alias-label-secondary);font:10px/1.2 var(--ds-font-family,system-ui,sans-serif);white-space:nowrap;}.dkan-departure{position:fixed;left:0;top:0;width:210px;height:88px;z-index:9994;pointer-events:none;will-change:transform,opacity;contain:layout paint style;isolation:isolate;animation:dkan-departure-flight var(--dkan-departure-duration,8600ms) cubic-bezier(.42,0,.85,.45) forwards;}@keyframes dkan-departure-flight{0%{opacity:0;transform:translate3d(var(--dkan-from-x),var(--dkan-from-y),0) scale(1) rotate(var(--dkan-flight-angle,0deg))}6%{opacity:1;transform:translate3d(var(--dkan-from-x),var(--dkan-from-y),0) scale(1) rotate(var(--dkan-flight-angle,0deg))}100%{opacity:0;transform:translate3d(var(--dkan-to-x),var(--dkan-to-y),0) scale(.24) rotate(var(--dkan-flight-angle,0deg))}}",
1868
- ".dkan-space-stuck{position:fixed;right:20px;top:88px;width:244px;height:160px;z-index:9993;pointer-events:none;border:1px solid color-mix(in srgb,#f59e0b 48%,var(--dsw-alias-border-l1));border-radius:20px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 88%,transparent);backdrop-filter:blur(16px);box-shadow:0 18px 54px rgb(0 0 0 / .28),0 0 30px color-mix(in srgb,#f59e0b 15%,transparent);}.dkan-space-stuck .dkan-freighter{left:37px;top:11px;transform:scale(.9);transform-origin:top left;}.dkan-stuck-code{position:absolute;left:17px;right:17px;top:60px;height:36px;border-radius:7px;background:linear-gradient(90deg,color-mix(in srgb,#f59e0b 18%,transparent),transparent);color:var(--dk-warn);font:700 14px/36px var(--ds-font-family-code,monospace);letter-spacing:.12em;text-align:center;overflow:hidden;}.dkan-stuck-code::after{content:'';position:absolute;inset:0;background:linear-gradient(90deg,transparent,color-mix(in srgb,#fef3c7 70%,transparent),transparent);transform:translateX(-110%);animation:dkan-stuck-scan 1.25s linear infinite;}.dkan-stuck-code i{display:inline-block;width:7px;height:7px;margin-right:8px;border-radius:50%;background:#f59e0b;box-shadow:0 0 9px #f59e0b;animation:dkan-stuck-pulse .8s ease-in-out infinite alternate;}.dkan-space-stuck strong{position:absolute;left:0;right:0;top:108px;color:var(--dsw-alias-label-primary);font:650 13px/1.2 var(--ds-font-family,system-ui,sans-serif);text-align:center;}.dkan-space-stuck small{position:absolute;left:18px;right:18px;top:128px;color:var(--dsw-alias-label-secondary);font:11px/1.25 var(--ds-font-family,system-ui,sans-serif);text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}@keyframes dkan-stuck-scan{to{transform:translateX(110%)}}@keyframes dkan-stuck-pulse{to{transform:scale(.55);opacity:.45}}",
1869
- // 窄屏(手机/分屏)贴右会溢出:回落到水平居中、屏幕上方,避开正文重心
1870
- "@media (max-width:760px){.dkan-space-stuck{right:50%;transform:translateX(50%);top:64px;}}",
1871
- // 极光:顶部柔光带呼吸流动(加浓)
1872
- ".dkan-aurora i{position:absolute;top:-40%;left:-20%;width:70%;height:80%;border-radius:50%;filter:blur(50px);opacity:.32;animation:dkan-aurora ease-in-out infinite alternate;}",
1873
- ".dkan-aurora i:nth-child(1){background:#4d9fff;}",
1874
- ".dkan-aurora i:nth-child(2){background:#34d399;left:20%;animation-delay:-2s;}",
1875
- ".dkan-aurora i:nth-child(3){background:#a78bfa;left:55%;animation-delay:-4s;}",
1876
- "@keyframes dkan-aurora{0%{transform:translateX(-8%) scaleY(1)}100%{transform:translateX(10%) scaleY(1.3)}}",
1877
- // 星云潮汐:无动态模糊,柔边由径向渐变生成,移动只发生在合成层。
1878
- ".dkan-nebula{contain:strict}.dkan-nebula i{position:absolute;width:48vw;aspect-ratio:1;border-radius:50%;opacity:.22;background:radial-gradient(circle,color-mix(in srgb,var(--dkan-phase,#60a5fa) 48%,transparent),color-mix(in srgb,#8b5cf6 18%,transparent) 42%,transparent 70%);animation:dkan-nebula-drift calc(18s / var(--dkan-speed,1)) cubic-bezier(.45,0,.55,1) infinite alternate;will-change:transform,opacity}.dkan-nebula i:nth-child(1){left:-18%;top:-24%}.dkan-nebula i:nth-child(2){right:-18%;top:4%;background:radial-gradient(circle,color-mix(in srgb,#22d3ee 34%,transparent),color-mix(in srgb,#3b82f6 15%,transparent) 45%,transparent 72%);animation-delay:-5s}.dkan-nebula i:nth-child(3){left:4%;bottom:-34%;background:radial-gradient(circle,color-mix(in srgb,#f472b6 28%,transparent),color-mix(in srgb,#8b5cf6 14%,transparent) 48%,transparent 72%);animation-delay:-10s}.dkan-nebula i:nth-child(4){right:8%;bottom:-26%;width:36vw;animation-delay:-14s}",
1879
- "@keyframes dkan-nebula-drift{0%{opacity:.14;transform:translate3d(-3vw,-2vh,0) scale(.92)}100%{opacity:.3;transform:translate3d(5vw,4vh,0) scale(1.12)}}",
1880
- // 曲速航道:从视口中心放射的独立光束。
1881
- ".dkan-warp{background:radial-gradient(circle at center,color-mix(in srgb,var(--dkan-phase,#60a5fa) 10%,transparent),transparent 34%);contain:strict}.dkan-warp i{position:absolute;left:50%;top:50%;width:var(--len);height:1px;border-radius:999px;transform-origin:left center;background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--dkan-phase,#60a5fa) 48%,transparent),#e0f2fe);opacity:0;animation-name:dkan-warp-ray;animation-timing-function:cubic-bezier(.3,.1,.8,1);animation-iteration-count:infinite;will-change:transform,opacity}",
1882
- "@keyframes dkan-warp-ray{0%{opacity:0;transform:rotate(var(--a)) translate3d(18px,0,0) scaleX(.05)}18%{opacity:.6}100%{opacity:0;transform:rotate(var(--a)) translate3d(58vw,0,0) scaleX(1.55)}}",
1883
- // 量子雷达:三个边缘站点各自扫描,避免占据会话正文中心。
1884
- ".dkan-radar{contain:strict}.dkan-radar-station{position:absolute;width:176px;height:176px;border-radius:50%;opacity:.44;background:repeating-radial-gradient(circle,color-mix(in srgb,var(--dkan-phase,#60a5fa) 28%,transparent) 0 1px,transparent 1px 29px);border:1px solid color-mix(in srgb,var(--dkan-phase,#60a5fa) 22%,transparent)}.dkan-radar-station.a{right:4%;top:8%}.dkan-radar-station.b{left:1%;top:38%;transform:scale(.72)}.dkan-radar-station.c{right:7%;bottom:9%;transform:scale(.58)}.dkan-radar-station i{position:absolute;inset:0;border-radius:50%;background:conic-gradient(from 0deg,transparent 0 76%,color-mix(in srgb,var(--dkan-phase,#60a5fa) 44%,transparent) 92%,transparent);animation:dkan-radar-sweep calc(4.8s / var(--dkan-speed,1)) linear infinite;will-change:transform}.dkan-radar-station b{position:absolute;left:67%;top:31%;width:7px;height:7px;border-radius:50%;background:var(--dkan-phase,#60a5fa);box-shadow:0 0 12px 4px color-mix(in srgb,var(--dkan-phase,#60a5fa) 58%,transparent);animation:dkan-radar-blip calc(2.4s / var(--dkan-speed,1)) ease-in-out infinite}",
1885
- "@keyframes dkan-radar-sweep{to{transform:rotate(360deg)}}@keyframes dkan-radar-blip{0%,70%,100%{opacity:.15;transform:scale(.65)}78%{opacity:1;transform:scale(1.25)}}",
1886
- // 星座网络:轻量 SVG 线网分段点亮。
1887
- ".dkan-constellation svg{position:absolute;left:8%;top:7%;width:86%;height:78%;overflow:visible}.dkan-constellation path{fill:none;stroke:color-mix(in srgb,var(--dkan-phase,#60a5fa) 42%,transparent);stroke-width:.16;vector-effect:non-scaling-stroke;stroke-dasharray:.08 .025;animation:dkan-constellation-line calc(7s / var(--dkan-speed,1)) linear infinite}.dkan-constellation circle{fill:var(--dkan-phase,#60a5fa);filter:drop-shadow(0 0 2px var(--dkan-phase,#60a5fa));animation:dkan-constellation-star calc(2.8s / var(--dkan-speed,1)) ease-in-out infinite alternate;transform-box:fill-box;transform-origin:center}",
1888
- "@keyframes dkan-constellation-line{to{stroke-dashoffset:-1}}@keyframes dkan-constellation-star{0%{opacity:.28;transform:scale(.68)}100%{opacity:.92;transform:scale(1.22)}}",
1889
- // 数据萤火:二维漂移和明暗呼吸组合在同一个 transform 动画中。
1890
- ".dkan-fireflies{contain:strict}.dkan-fireflies i{position:absolute;border-radius:50%;background:color-mix(in srgb,var(--dkan-phase,#60a5fa) 78%,#fff);box-shadow:0 0 8px 2px color-mix(in srgb,var(--dkan-phase,#60a5fa) 46%,transparent);opacity:.2;animation-name:dkan-firefly;animation-timing-function:cubic-bezier(.45,0,.55,1);animation-iteration-count:infinite;animation-direction:alternate;will-change:transform,opacity}",
1891
- "@keyframes dkan-firefly{0%{opacity:.12;transform:translate3d(0,0,0) scale(.65)}48%{opacity:.86}100%{opacity:.26;transform:translate3d(var(--dx),var(--dy),0) scale(1.18)}}",
1892
- // 深海脉动:波层在底部横向漂移,气泡只做向上合成位移。
1893
- ".dkan-ocean{background:linear-gradient(0deg,color-mix(in srgb,#0284c7 12%,transparent),transparent 46%);contain:strict}.dkan-ocean-waves{position:absolute;left:-12%;right:-12%;bottom:0;height:42%}.dkan-ocean-waves i{position:absolute;left:0;width:112%;height:52%;border-top:1px solid color-mix(in srgb,#67e8f9 28%,transparent);border-radius:50%;animation:dkan-ocean-wave calc(9s / var(--dkan-speed,1)) ease-in-out infinite alternate;will-change:transform}.dkan-ocean-waves i:nth-child(1){top:2%}.dkan-ocean-waves i:nth-child(2){top:18%;animation-delay:-2s}.dkan-ocean-waves i:nth-child(3){top:34%;animation-delay:-4s}.dkan-ocean-waves i:nth-child(4){top:50%;animation-delay:-6s}.dkan-ocean-waves i:nth-child(5){top:66%;animation-delay:-8s}.dkan-ocean>b{position:absolute;bottom:-20px;border:1px solid color-mix(in srgb,#a5f3fc 54%,transparent);border-radius:50%;opacity:0;animation-name:dkan-ocean-bubble;animation-timing-function:linear;animation-iteration-count:infinite;will-change:transform,opacity}",
1894
- "@keyframes dkan-ocean-wave{to{transform:translate3d(7%,5px,0) scaleY(1.08)}}@keyframes dkan-ocean-bubble{0%{opacity:0;transform:translate3d(0,0,0) scale(.7)}14%{opacity:.58}100%{opacity:0;transform:translate3d(var(--drift),-82vh,0) scale(1.18)}}",
1895
- // 棱镜光谱:七条低透明度光带错峰穿过视口。
1896
- ".dkan-prism{contain:strict}.dkan-prism i{position:absolute;left:-32vw;width:160vw;height:2px;border-radius:999px;opacity:0;background:linear-gradient(90deg,transparent,#ef4444,#f59e0b,#facc15,#22c55e,#22d3ee,#6366f1,#a855f7,transparent);animation:dkan-prism-ray calc(8s / var(--dkan-speed,1)) cubic-bezier(.45,0,.55,1) infinite;will-change:transform,opacity}.dkan-prism i:nth-child(1){top:12%;animation-delay:0s}.dkan-prism i:nth-child(2){top:25%;animation-delay:-1.1s}.dkan-prism i:nth-child(3){top:38%;animation-delay:-2.2s}.dkan-prism i:nth-child(4){top:51%;animation-delay:-3.3s}.dkan-prism i:nth-child(5){top:64%;animation-delay:-4.4s}.dkan-prism i:nth-child(6){top:77%;animation-delay:-5.5s}.dkan-prism i:nth-child(7){top:90%;animation-delay:-6.6s}",
1897
- "@keyframes dkan-prism-ray{0%{opacity:0;transform:translate3d(-12vw,0,0) rotate(-7deg) scaleX(.7)}34%{opacity:.2}66%{opacity:.36}100%{opacity:0;transform:translate3d(20vw,0,0) rotate(-7deg) scaleX(1.05)}}",
1898
- // 神经电路:少量 SVG 路径的虚线脉冲,节点保持低透明度。
1899
- ".dkan-circuit svg{position:absolute;inset:5% 3% 9% 8%;width:89%;height:86%;overflow:visible}.dkan-circuit path{fill:none;stroke:color-mix(in srgb,var(--dkan-phase,#60a5fa) 40%,transparent);stroke-width:1.2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:.045 .12;animation:dkan-circuit-pulse calc(5.6s / var(--dkan-speed,1)) linear infinite}.dkan-circuit circle{fill:var(--dkan-phase,#60a5fa);opacity:.58;filter:drop-shadow(0 0 4px var(--dkan-phase,#60a5fa));animation:dkan-circuit-node calc(2.2s / var(--dkan-speed,1)) ease-in-out infinite alternate}",
1900
- "@keyframes dkan-circuit-pulse{to{stroke-dashoffset:-1}}@keyframes dkan-circuit-node{to{opacity:.16}}",
1901
- // 引力涟漪:三个边缘引力源按错峰节奏扩散。
1902
- ".dkan-gravity{contain:strict}.dkan-gravity-source{position:absolute;width:260px;height:260px}.dkan-gravity-source.a{right:2%;top:5%}.dkan-gravity-source.b{left:-4%;top:39%;transform:scale(.76)}.dkan-gravity-source.c{right:17%;bottom:-10%;transform:scale(.62)}.dkan-gravity-source b{position:absolute;left:50%;top:50%;width:12px;height:12px;margin:-6px;border-radius:50%;background:var(--dkan-phase,#60a5fa);box-shadow:0 0 16px 5px color-mix(in srgb,var(--dkan-phase,#60a5fa) 42%,transparent)}.dkan-gravity-source i{position:absolute;left:50%;top:50%;width:26px;height:26px;margin:-13px;border:1px solid color-mix(in srgb,var(--dkan-phase,#60a5fa) 42%,transparent);border-radius:50%;opacity:0;animation:dkan-gravity-wave calc(4.6s / var(--dkan-speed,1)) cubic-bezier(.2,.55,.35,1) infinite;will-change:transform,opacity}",
1903
- "@keyframes dkan-gravity-wave{0%{opacity:.64;transform:scale(.3)}100%{opacity:0;transform:scale(9)}}",
1904
- // 灵感天灯:暖色灯体从屏幕底部缓慢升起,尾迹由伪元素提供。
1905
- ".dkan-lantern{contain:strict}.dkan-lantern i{position:absolute;bottom:-30px;border-radius:45% 45% 34% 34%;background:linear-gradient(150deg,#fff7c2 0%,#fbbf24 46%,#f97316 100%);box-shadow:0 0 10px 3px color-mix(in srgb,#fbbf24 36%,transparent);opacity:0;animation-name:dkan-lantern-rise;animation-timing-function:cubic-bezier(.34,.02,.6,1);animation-iteration-count:infinite;will-change:transform,opacity}.dkan-lantern i::after{content:'';position:absolute;left:42%;top:100%;width:16%;height:13px;background:linear-gradient(#f59e0b,transparent)}",
1906
- "@keyframes dkan-lantern-rise{0%{opacity:0;transform:translate3d(0,0,0) rotate(-3deg) scale(.72)}12%{opacity:.72}78%{opacity:.48}100%{opacity:0;transform:translate3d(var(--drift),-104vh,0) rotate(4deg) scale(1.04)}}",
1907
- // ===== 交互反馈层 =====
1908
- ".dkan-fxwrap{position:fixed;inset:0;z-index:9992;pointer-events:none;overflow:hidden;}",
1909
- ".dkan-fx{position:absolute;}",
1910
- // 纸飞机:从点击处向右上飞出淡出(加大加亮)
1911
- ".dkan-fx-plane{color:var(--dk-accent);font-size:24px;text-shadow:0 0 8px color-mix(in srgb,var(--dk-accent) 70%,transparent);animation:dkan-plane 1.2s var(--ds-ease-in-out) forwards;}",
1912
- "@keyframes dkan-plane{0%{opacity:0;transform:translate(0,0) rotate(-20deg) scale(.6)}15%{opacity:1;transform:translate(20px,-14px) rotate(-20deg) scale(1.1)}100%{opacity:0;transform:translate(240px,-160px) rotate(-20deg) scale(1)}}",
1913
- // 火花:粒子四散(加大加多散得更开)
1914
- ".dkan-fx-spark i{position:absolute;width:7px;height:7px;border-radius:50%;box-shadow:0 0 6px currentColor;animation:dkan-spark .9s var(--ds-ease-in-out) forwards;}",
1915
- "@keyframes dkan-spark{0%{opacity:1;transform:translate(0,0) scale(1)}100%{opacity:0;transform:translate(var(--dx),var(--dy)) scale(.4)}}",
1916
- // 流光:横向扫过(切会话,加高加亮)
1917
- ".dkan-fx-streak{top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,transparent,var(--dk-accent) 50%,#fff);background-size:45% 100%;background-repeat:no-repeat;box-shadow:0 0 12px 2px color-mix(in srgb,var(--dk-accent) 50%,transparent);animation:dkan-streak 1s var(--ds-ease-in-out) forwards;}",
1918
- "@keyframes dkan-streak{0%{background-position:-50% 0;opacity:0}20%{opacity:1}100%{background-position:150% 0;opacity:0}}",
1919
- // 微光:全屏柔光一闪(切工作目录,加浓)
1920
- ".dkan-fx-flash{inset:0;background:radial-gradient(ellipse at center,color-mix(in srgb,var(--dk-accent) 26%,transparent),transparent 75%);animation:dkan-flash .8s var(--ds-ease-in-out) forwards;}",
1921
- "@keyframes dkan-flash{0%{opacity:0}30%{opacity:1}100%{opacity:0}}",
1922
- // 光涌:底部向上涌起(任务开始,加高加浓)
1923
- ".dkan-fx-surge{left:0;right:0;bottom:0;height:42%;background:linear-gradient(0deg,color-mix(in srgb,var(--dsw-alias-state-success-primary,#34d399) 38%,transparent),transparent);animation:dkan-surge 1.2s var(--ds-ease-in-out) forwards;}",
1924
- "@keyframes dkan-surge{0%{opacity:0;transform:translateY(45%)}30%{opacity:1}100%{opacity:0;transform:translateY(-35%)}}",
1925
- // 彩带:顶部飘落(任务完成庆祝,加大加多)
1926
- ".dkan-fx-confetti{inset:0;}",
1927
- ".dkan-fx-confetti i{position:absolute;top:-14px;width:8px;height:13px;border-radius:2px;box-shadow:0 0 4px currentColor;animation:dkan-confetti 2s var(--ds-ease-in-out) forwards;}",
1928
- "@keyframes dkan-confetti{0%{opacity:1;transform:translateY(0) rotate(0)}100%{opacity:0;transform:translateY(75vh) translateX(var(--dx)) rotate(var(--r))}}",
1929
- // 右下角状态徽标(玻璃拟态,可点击);抬到 dsh 输入卡上方避免重叠错位
1930
- ".dkan-badge{position:fixed;right:20px;bottom:calc(var(--dsh-composer-height,152px) + 20px);z-index:9990;display:inline-flex;align-items:center;gap:8px;padding:7px 14px;border-radius:999px;cursor:pointer;border:1px solid var(--dsw-alias-border-l1);background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 78%,transparent);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);box-shadow:0 6px 24px rgb(0 0 0 / .16);color:var(--dsw-alias-label-secondary);font-family:inherit;font-size:12px;line-height:18px;pointer-events:auto;animation:dkan-rise .3s var(--ds-ease-in-out);transition:color .15s var(--ds-ease-in-out);}",
1931
- ".dkan-badge:hover{color:var(--dsw-alias-label-primary);}",
1932
- ".dkan-badge .n{color:var(--dsw-alias-label-primary);font-weight:600;}",
1933
- "@keyframes dkan-rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}",
1934
- // 徽标圆点与两种动效(呼吸 / 轨道环)
1935
- ".dkan-dotwrap{position:relative;width:20px;height:20px;flex:none;display:inline-flex;align-items:center;justify-content:center;}",
1936
- ".dkan-dot{width:12px;height:12px;border-radius:50%;background:var(--dkan-phase,var(--dk-accent));box-shadow:0 0 10px color-mix(in srgb,var(--dkan-phase,var(--dk-accent)) 70%,transparent);transition:background .4s var(--ds-ease-in-out),box-shadow .4s var(--ds-ease-in-out);}",
1937
- ".dkan-breathe .dkan-dot{animation:dkan-breathe calc(2.2s / var(--dkan-speed,1)) ease-in-out infinite;}",
1938
- "@keyframes dkan-breathe{0%,100%{transform:scale(1);opacity:.5}50%{transform:scale(1.7);opacity:1}}",
1939
- // 呼吸光晕:向外扩散的波纹环,与圆点同频(颜色随阶段)
1940
- ".dkan-halo{position:absolute;inset:0;border-radius:50%;border:2px solid var(--dkan-phase,var(--dk-accent));animation:dkan-halo calc(2.2s / var(--dkan-speed,1)) ease-out infinite;}",
1941
- "@keyframes dkan-halo{0%{transform:scale(.5);opacity:.9}70%{transform:scale(1.5);opacity:.25}100%{transform:scale(1.7);opacity:0}}",
1942
- // breathe 模式徽标更醒目:阶段色描边 + 呼吸辉光
1943
- ".dkan-badge-breathe{border-color:color-mix(in srgb,var(--dkan-phase,#4d9fff) 55%,transparent);animation:dkan-rise .3s var(--ds-ease-in-out),dkan-badge-breathe calc(2.2s / var(--dkan-speed,1)) ease-in-out infinite;}",
1944
- "@keyframes dkan-badge-breathe{0%,100%{box-shadow:0 0 6px color-mix(in srgb,var(--dkan-phase,#4d9fff) 22%,transparent),0 6px 24px rgb(0 0 0 / .16)}50%{box-shadow:0 0 18px color-mix(in srgb,var(--dkan-phase,#4d9fff) 55%,transparent),0 6px 24px rgb(0 0 0 / .16)}}",
1945
- ".dkan-ring{position:absolute;inset:0;border-radius:50%;background:conic-gradient(from 0deg,transparent 0 68%,var(--dk-accent) 92%,#fff);-webkit-mask:radial-gradient(farthest-side,transparent calc(100% - 2px),#000 calc(100% - 2px));mask:radial-gradient(farthest-side,transparent calc(100% - 2px),#000 calc(100% - 2px));animation:dkan-spin calc(2.2s / var(--dkan-speed,1)) linear infinite;opacity:.9;}",
1946
- "@keyframes dkan-spin{to{transform:rotate(360deg)}}",
1947
- // 通知卡片栈(右上角)
1948
- ".dkan-toasts{position:fixed;top:16px;right:16px;z-index:9995;display:flex;flex-direction:column;gap:8px;width:min(380px,calc(100vw - 32px));}",
1949
- ".dkan-toast{border-radius:12px;padding:12px 14px;pointer-events:auto;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 88%,transparent);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--dsw-alias-border-l1);box-shadow:0 10px 36px rgb(0 0 0 / .22);animation:dkan-toast-in .32s var(--ds-ease-in-out);}",
1950
- ".dkan-toast.out{animation:dkan-toast-out .24s var(--ds-ease-in-out) forwards;}",
1951
- "@keyframes dkan-toast-in{from{opacity:0;transform:translateX(14px)}to{opacity:1;transform:none}}",
1952
- "@keyframes dkan-toast-out{to{opacity:0;transform:translateX(10px)}}",
1953
- ".dkan-toast-head{display:flex;align-items:center;gap:8px;margin-bottom:4px;}",
1954
- ".dkan-toast-mark{width:8px;height:8px;border-radius:50%;flex:none;}",
1955
- ".dkan-toast-title{font-weight:600;font-size:13px;color:var(--dsw-alias-label-primary);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
1956
- ".dkan-toast-close{cursor:pointer;flex:none;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);border-radius:6px;width:22px;height:22px;font-size:11px;line-height:1;display:inline-flex;align-items:center;justify-content:center;font-family:inherit;}",
1957
- ".dkan-toast-close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
1958
- ".dkan-toast-body{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.7;white-space:pre-line;word-break:break-word;}",
1959
- // 需确认卡片:琥珀描边 + 缓慢呼吸光晕,常驻提醒直到用户处理
1960
- ".dkan-toast-confirm{border-color:color-mix(in srgb,var(--dk-warn) 55%,transparent);animation:dkan-toast-in .32s var(--ds-ease-in-out),dkan-confirm-glow 2.4s ease-in-out 0.4s infinite;}",
1961
- "@keyframes dkan-confirm-glow{0%,100%{box-shadow:0 10px 36px rgb(0 0 0 / .22),0 0 0 0 color-mix(in srgb,var(--dk-warn) 30%,transparent)}50%{box-shadow:0 10px 36px rgb(0 0 0 / .22),0 0 14px 2px color-mix(in srgb,var(--dk-warn) 32%,transparent)}}",
1962
- // 面板页布局
1963
- ".dkan-root{display:flex;flex-direction:column;gap:10px;}",
1964
- ".dkan-note{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.6;}",
1965
- ".dkan-err{color:var(--dsw-alias-state-error-primary);}",
1966
- ".dkan-sec{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:8px;}",
1967
- ".dkan-sec-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
1968
- ".dkan-sec-title{font-weight:600;font-size:13px;color:var(--dsw-alias-label-primary);flex:none;}",
1969
- ".dkan-sec-sub{font-size:11px;color:var(--dsw-alias-label-tertiary);flex:1;min-width:120px;}",
1970
- ".dkan-sec-sw{margin-left:auto;flex:none;display:inline-flex;align-items:center;gap:8px;}",
1971
- ".dkan-sec-swlabel{font-size:11px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;}",
1972
- ".dkan-sec-swlabel.on{color:var(--dsw-alias-state-success-primary);}",
1973
- ".dkan-refresh{cursor:pointer;flex:none;color:var(--dsw-alias-label-primary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:2px 10px;font-family:inherit;font-size:12px;}",
1974
- ".dkan-refresh:hover{background:var(--dsw-alias-interactive-bg-hover);}",
1975
- // 模式选择卡(带缩微预览)
1976
- ".dkan-modes{display:flex;gap:8px;flex-wrap:wrap;}",
1977
- ".dkan-mode{flex:1;min-width:150px;display:flex;flex-direction:column;gap:6px;padding:10px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-2);cursor:pointer;font-family:inherit;text-align:left;transition:border-color .15s var(--ds-ease-in-out);}",
1978
- ".dkan-mode:hover{border-color:var(--dk-accent);}",
1979
- ".dkan-mode.on{border-color:var(--dk-accent);box-shadow:0 0 0 1px color-mix(in srgb,var(--dk-accent) 40%,transparent);}",
1980
- ".dkan-mode-name{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:6px;}",
1981
- ".dkan-mode.on .dkan-mode-name::after{content:\"✓\";color:var(--dk-accent);}",
1982
- ".dkan-mode-desc{font-size:11px;color:var(--dsw-alias-label-tertiary);line-height:1.5;}",
1983
- ".dkan-robot-controls{display:flex;flex-direction:column;gap:8px;padding:9px 10px;border:1px solid color-mix(in srgb,var(--dk-accent) 28%,var(--dsw-alias-border-l1));border-radius:9px;background:color-mix(in srgb,var(--dk-accent) 5%,var(--dsw-alias-bg-layer-2));}",
1984
- ".dkan-robot-controls-head{display:flex;align-items:center;justify-content:space-between;font-size:12px;color:var(--dsw-alias-label-primary);}",
1985
- ".dkan-robot-controls-head strong{font-variant-numeric:tabular-nums;color:var(--dk-accent);}",
1986
- ".dkan-robot-size-options{display:flex;gap:6px;flex-wrap:wrap;}",
1987
- ".dkan-robot-size-options button{cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;padding:3px 10px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;}",
1988
- ".dkan-robot-size-options button:hover,.dkan-robot-size-options button.on{border-color:var(--dk-accent);color:var(--dsw-alias-label-primary);}",
1989
- ".dkan-robot-slider-row{display:flex;align-items:center;gap:9px;flex-wrap:wrap;font-size:11px;color:var(--dsw-alias-label-tertiary);}",
1990
- ".dkan-robot-slider-row input{accent-color:var(--dk-accent);width:min(240px,100%);}",
1991
- ".dkan-prev{height:30px;border-radius:6px;background:var(--dsw-alias-bg-layer-1);position:relative;overflow:hidden;display:flex;align-items:center;justify-content:center;}",
1992
- ".dkan-prev-flow{position:absolute;top:0;left:0;right:0;height:3px;background:color-mix(in srgb,var(--dk-accent) 30%,transparent);}",
1993
- ".dkan-prev-flow::after{content:\"\";position:absolute;top:0;bottom:0;left:-40%;width:40%;background:linear-gradient(90deg,transparent,var(--dk-accent));animation:dkan-flow 1.9s linear infinite;}",
1994
- ".dkan-prev-box{--dkan-speed:1;}",
1995
- ".dkan-prev-orbit{position:absolute;top:3px;left:3px;width:7px;height:7px;border-radius:50%;background:var(--dk-accent);box-shadow:0 0 7px 2px color-mix(in srgb,var(--dk-accent) 55%,transparent);animation:dkan-prev-orbit 3s linear infinite;}",
1996
- "@keyframes dkan-prev-orbit{0%{top:3px;left:3px}25%{top:3px;left:calc(100% - 10px)}50%{top:calc(100% - 10px);left:calc(100% - 10px)}75%{top:calc(100% - 10px);left:3px}100%{top:3px;left:3px}}",
1997
- // 预览:代码雨(字符列下落)
1998
- ".dkan-prev-matrix i{position:absolute;top:-100%;writing-mode:vertical-rl;font-size:8px;font-family:var(--ds-font-family-code,monospace);color:var(--dk-accent);opacity:.5;animation:dkan-prev-fall 2.2s linear infinite;}",
1999
- ".dkan-prev-matrix i:nth-child(1){left:12%;}",
2000
- ".dkan-prev-matrix i:nth-child(2){left:32%;animation-delay:-.6s;}",
2001
- ".dkan-prev-matrix i:nth-child(3){left:52%;animation-delay:-1.2s;}",
2002
- ".dkan-prev-matrix i:nth-child(4){left:72%;animation-delay:-.3s;}",
2003
- ".dkan-prev-matrix i:nth-child(5){left:88%;animation-delay:-1.6s;}",
2004
- ".dkan-prev-matrix i::after{content:\"01</>\";}",
2005
- "@keyframes dkan-prev-fall{to{transform:translateY(300%)}}",
2006
- // 预览:星野(星点闪烁)
2007
- ".dkan-prev-stars i{position:absolute;width:2px;height:2px;border-radius:50%;background:var(--dsw-alias-label-secondary);animation:dkan-prev-star 1.6s ease-in-out infinite alternate;}",
2008
- ".dkan-prev-stars i:nth-child(1){left:15%;top:25%;}",
2009
- ".dkan-prev-stars i:nth-child(2){left:40%;top:60%;animation-delay:-.4s;}",
2010
- ".dkan-prev-stars i:nth-child(3){left:60%;top:30%;animation-delay:-.8s;}",
2011
- ".dkan-prev-stars i:nth-child(4){left:80%;top:55%;animation-delay:-1.2s;}",
2012
- ".dkan-prev-stars i:nth-child(5){left:28%;top:70%;animation-delay:-.6s;}",
2013
- ".dkan-prev-stars i:nth-child(6){left:70%;top:75%;animation-delay:-1s;}",
2014
- "@keyframes dkan-prev-star{0%{opacity:.2}100%{opacity:.9}}",
2015
- // 预览:星际远征(行星、轨道与飞船)
2016
- ".dkan-prev-space i{position:absolute;left:49%;top:50%;width:9px;height:9px;border-radius:50%;background:#fbbf24;box-shadow:0 0 6px #fbbf24;}.dkan-prev-space b{position:absolute;left:21%;top:23%;width:15px;height:8px;border:1px solid #93c5fd;border-radius:50%;animation:dkan-prev-space-orbit 2.4s linear infinite;}.dkan-prev-space b::after{content:'';position:absolute;left:-3px;top:0;width:5px;height:5px;border-radius:50%;background:#a78bfa;box-shadow:0 0 4px #a78bfa;}.dkan-prev-space em{position:absolute;left:5%;top:65%;width:19px;height:4px;border-radius:50% 0 0 50%;background:linear-gradient(90deg,#22d3ee,#dbeafe);animation:dkan-prev-space-ship 1.8s linear infinite;}.dkan-prev-space em::after{content:'';position:absolute;right:-7px;top:-3px;border-left:9px solid #93c5fd;border-top:5px solid transparent;border-bottom:5px solid transparent;}.dkan-prev-space{background:radial-gradient(ellipse at 78% 20%,color-mix(in srgb,#6366f1 18%,transparent),transparent 58%);}.dkan-prev-space i,.dkan-prev-space b,.dkan-prev-space em{font-style:normal;}.dkan-prev-space b{transform-origin:34px 11px;}@keyframes dkan-prev-space-orbit{to{transform:rotate(360deg)}}@keyframes dkan-prev-space-ship{to{transform:translateX(105px)}}",
2017
- // 预览:极光(柔光带呼吸)
2018
- ".dkan-prev-aurora i{position:absolute;top:-30%;width:60%;height:120%;border-radius:50%;filter:blur(8px);opacity:.35;animation:dkan-prev-aurora 2.4s ease-in-out infinite alternate;}",
2019
- ".dkan-prev-aurora i:nth-child(1){left:5%;background:#4d9fff;}",
2020
- ".dkan-prev-aurora i:nth-child(2){left:45%;background:#34d399;animation-delay:-1.2s;}",
2021
- "@keyframes dkan-prev-aurora{0%{transform:translateX(-10%)}100%{transform:translateX(15%)}}",
2022
- // 十种新增模式的缩微预览:复用主体运动语言,但把节点数和振幅压到卡片内。
2023
- ".dkan-prev-new{--dkan-phase:var(--dk-accent)}.dkan-prev-new i{position:absolute;display:block}",
2024
- ".dkan-prev-nebula i{width:48px;height:48px;border-radius:50%;background:radial-gradient(circle,color-mix(in srgb,var(--dkan-phase) 48%,transparent),transparent 70%);animation:dkan-prev-nebula 3s ease-in-out infinite alternate}.dkan-prev-nebula i:nth-child(1){left:2%;top:-60%}.dkan-prev-nebula i:nth-child(2){right:4%;bottom:-65%;background:radial-gradient(circle,color-mix(in srgb,#a855f7 42%,transparent),transparent 70%);animation-delay:-1.4s}.dkan-prev-nebula i:nth-child(n+3){display:none}@keyframes dkan-prev-nebula{to{transform:translateX(24px) scale(1.18)}}",
2025
- ".dkan-prev-warp i{left:50%;top:50%;width:36px;height:1px;transform-origin:left;background:linear-gradient(90deg,transparent,var(--dkan-phase),#fff);animation:dkan-prev-warp 1.8s linear infinite;opacity:0}.dkan-prev-warp i:nth-child(1){--a:0deg}.dkan-prev-warp i:nth-child(2){--a:45deg;animation-delay:-.2s}.dkan-prev-warp i:nth-child(3){--a:90deg;animation-delay:-.4s}.dkan-prev-warp i:nth-child(4){--a:135deg;animation-delay:-.6s}.dkan-prev-warp i:nth-child(5){--a:180deg;animation-delay:-.8s}.dkan-prev-warp i:nth-child(6){--a:225deg;animation-delay:-1s}.dkan-prev-warp i:nth-child(7){--a:270deg;animation-delay:-1.2s}.dkan-prev-warp i:nth-child(8){--a:315deg;animation-delay:-1.4s}@keyframes dkan-prev-warp{0%{opacity:0;transform:rotate(var(--a)) translateX(2px) scaleX(.1)}35%{opacity:.8}100%{opacity:0;transform:rotate(var(--a)) translateX(40px) scaleX(1.3)}}",
2026
- ".dkan-prev-radar i{left:50%;top:50%;width:24px;height:24px;margin:-12px;border:1px solid color-mix(in srgb,var(--dkan-phase) 48%,transparent);border-radius:50%;animation:dkan-prev-radar 2.6s ease-out infinite;opacity:0}.dkan-prev-radar i:nth-child(2){animation-delay:-.85s}.dkan-prev-radar i:nth-child(3){animation-delay:-1.7s}.dkan-prev-radar i:nth-child(n+4){display:none}@keyframes dkan-prev-radar{0%{opacity:.8;transform:scale(.2)}100%{opacity:0;transform:scale(1.25)}}",
2027
- ".dkan-prev-constellation::after{content:'';position:absolute;left:12%;right:12%;top:50%;height:1px;background:linear-gradient(90deg,transparent,var(--dkan-phase),transparent);transform:rotate(-9deg)}.dkan-prev-constellation i,.dkan-prev-fireflies i{width:4px;height:4px;border-radius:50%;background:var(--dkan-phase);box-shadow:0 0 5px var(--dkan-phase);animation:dkan-prev-twinkle 1.8s ease-in-out infinite alternate}.dkan-prev-constellation i:nth-child(1),.dkan-prev-fireflies i:nth-child(1){left:12%;top:62%}.dkan-prev-constellation i:nth-child(2),.dkan-prev-fireflies i:nth-child(2){left:28%;top:26%;animation-delay:-.3s}.dkan-prev-constellation i:nth-child(3),.dkan-prev-fireflies i:nth-child(3){left:45%;top:52%;animation-delay:-.6s}.dkan-prev-constellation i:nth-child(4),.dkan-prev-fireflies i:nth-child(4){left:61%;top:18%;animation-delay:-.9s}.dkan-prev-constellation i:nth-child(5),.dkan-prev-fireflies i:nth-child(5){left:76%;top:58%;animation-delay:-1.2s}.dkan-prev-constellation i:nth-child(6),.dkan-prev-fireflies i:nth-child(6){left:88%;top:34%;animation-delay:-1.5s}@keyframes dkan-prev-twinkle{to{opacity:.2;transform:translateY(4px) scale(.65)}}",
2028
- ".dkan-prev-ocean i{left:-10%;width:120%;height:18px;border-top:1px solid color-mix(in srgb,#22d3ee 52%,transparent);border-radius:50%;animation:dkan-prev-wave 2.8s ease-in-out infinite alternate}.dkan-prev-ocean i:nth-child(1){top:15%}.dkan-prev-ocean i:nth-child(2){top:35%;animation-delay:-.5s}.dkan-prev-ocean i:nth-child(3){top:55%;animation-delay:-1s}.dkan-prev-ocean i:nth-child(4){top:75%;animation-delay:-1.5s}.dkan-prev-ocean i:nth-child(n+5){display:none}@keyframes dkan-prev-wave{to{transform:translateX(12px) scaleY(1.2)}}",
2029
- ".dkan-prev-prism i{left:-10%;width:120%;height:1px;background:linear-gradient(90deg,transparent,#ef4444,#fbbf24,#22c55e,#22d3ee,#8b5cf6,transparent);transform:rotate(-6deg);animation:dkan-prev-prism 2.8s ease-in-out infinite}.dkan-prev-prism i:nth-child(1){top:18%}.dkan-prev-prism i:nth-child(2){top:34%;animation-delay:-.4s}.dkan-prev-prism i:nth-child(3){top:50%;animation-delay:-.8s}.dkan-prev-prism i:nth-child(4){top:66%;animation-delay:-1.2s}.dkan-prev-prism i:nth-child(5){top:82%;animation-delay:-1.6s}.dkan-prev-prism i:nth-child(n+6){display:none}@keyframes dkan-prev-prism{0%,100%{opacity:.12;transform:translateX(-8px) rotate(-6deg)}50%{opacity:.72;transform:translateX(10px) rotate(-6deg)}}",
2030
- ".dkan-prev-circuit i{height:1px;background:var(--dkan-phase);box-shadow:0 0 4px var(--dkan-phase);animation:dkan-prev-circuit 2.4s linear infinite}.dkan-prev-circuit i:nth-child(1){left:5%;top:30%;width:42%}.dkan-prev-circuit i:nth-child(2){left:47%;top:30%;width:1px;height:45%}.dkan-prev-circuit i:nth-child(3){left:47%;top:74%;width:38%}.dkan-prev-circuit i:nth-child(4){left:68%;top:15%;width:1px;height:60%}.dkan-prev-circuit i:nth-child(5){left:68%;top:15%;width:26%}.dkan-prev-circuit i:nth-child(6){left:12%;top:62%;width:35%}@keyframes dkan-prev-circuit{0%,100%{opacity:.18}50%{opacity:.9}}",
2031
- ".dkan-prev-gravity i{left:50%;top:50%;width:18px;height:18px;margin:-9px;border:1px solid var(--dkan-phase);border-radius:50%;opacity:0;animation:dkan-prev-gravity 2.6s ease-out infinite}.dkan-prev-gravity i:nth-child(2){animation-delay:-.65s}.dkan-prev-gravity i:nth-child(3){animation-delay:-1.3s}.dkan-prev-gravity i:nth-child(4){animation-delay:-1.95s}.dkan-prev-gravity i:nth-child(n+5){display:none}@keyframes dkan-prev-gravity{0%{opacity:.8;transform:scale(.15)}100%{opacity:0;transform:scale(2.4)}}",
2032
- ".dkan-prev-lantern i{bottom:-8px;width:6px;height:8px;border-radius:3px;background:#fbbf24;box-shadow:0 0 5px #f59e0b;animation:dkan-prev-lantern 3.2s linear infinite}.dkan-prev-lantern i:nth-child(1){left:12%}.dkan-prev-lantern i:nth-child(2){left:27%;animation-delay:-.6s}.dkan-prev-lantern i:nth-child(3){left:43%;animation-delay:-1.2s}.dkan-prev-lantern i:nth-child(4){left:61%;animation-delay:-1.8s}.dkan-prev-lantern i:nth-child(5){left:76%;animation-delay:-2.4s}.dkan-prev-lantern i:nth-child(6){left:90%;animation-delay:-2.8s}@keyframes dkan-prev-lantern{0%{opacity:0;transform:translateY(0)}20%{opacity:.9}100%{opacity:0;transform:translateY(-38px)}}",
2033
- ".dkan-prev-bot{height:92px;justify-content:center;}",
2034
- ".dkan-prev-bot .dkan-bot-scene{--dkan-bot-scale:1;transform:scale(.56);transform-origin:center;}",
2035
- // ===== 桌面伙伴:具象人物 + 紧凑双工位(尺寸可调,整卡可拖拽) =====
2036
- // 卡片:玻璃拟态;默认停泊右下(dsh 输入卡上方),拖动后位置持久化、可放屏幕任意处
2037
- ".dkan-botcard{--dkan-bot-scale:1.35;position:fixed;right:20px;bottom:calc(var(--dsh-composer-height,152px) + 20px);z-index:9990;display:flex;flex-direction:column;gap:6px;padding:10px 12px;border-radius:16px;cursor:grab;border:1px solid var(--dsw-alias-border-l1);background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 86%,transparent);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 12px 36px rgb(0 0 0 / .22);color:var(--dsw-alias-label-secondary);font-family:inherit;font-size:12px;pointer-events:auto;touch-action:none;user-select:none;animation:dkan-rise .3s var(--ds-ease-in-out);}",
2038
- ".dkan-botcard:hover,.dkan-botcard:focus-visible{border-color:var(--dk-accent);outline:none;}",
2039
- ".dkan-botcard.dkan-dragging{cursor:grabbing;animation:none;}",
2040
- ".dkan-botcard .n{color:var(--dsw-alias-label-primary);font-weight:600;}",
2041
- ".dkan-bot-cap{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-right:18px;}",
2042
- ".dkan-bot-resize{position:absolute;right:7px;bottom:7px;width:15px;height:15px;padding:0;border:0;border-radius:4px;background:linear-gradient(135deg,transparent 0 42%,var(--dsw-alias-label-tertiary) 44% 51%,transparent 53% 62%,var(--dsw-alias-label-tertiary) 64% 71%,transparent 73%);cursor:nwse-resize;touch-action:none;opacity:.72;}",
2043
- ".dkan-bot-resize:hover,.dkan-bot-resize:focus-visible{opacity:1;outline:1px solid var(--dk-accent);outline-offset:1px;}",
2044
- // ===== 3D 场景(CSS 长方体拼装:世界层统一 3/4 视角——俯视 + 侧身面向镜头) =====
2045
- ".dkan-bot-scene{position:relative;width:calc(220px * var(--dkan-bot-scale,1));height:calc(132px * var(--dkan-bot-scale,1));perspective:calc(620px * var(--dkan-bot-scale,1));perspective-origin:55% 18%;--dk3-side:#7c8ba0;--dk3-side-hi:#a9b8ca;--dk3-side-dk:#4c5870;--dk3-screen:#0d1526;--dkan-code-a:#4ade80;--dkan-code-b:#60a5fa;--dkan-code-c:#fbbf24;}",
2046
- ".dk3-world{position:absolute;left:0;top:0;width:220px;height:132px;transform-style:preserve-3d;transform-origin:top left;transform:scale(var(--dkan-bot-scale,1)) rotateX(-15deg) rotateY(-30deg);}",
2047
- ".dk3-box{position:absolute;transform-style:preserve-3d;}",
2048
- ".dk3-face{position:absolute;left:50%;top:50%;display:block;background:var(--dk3-side);backface-visibility:hidden;border-radius:1px;}",
2049
- // 金属件着色:顶面亮、底面暗,正/侧面中调(3D 光感)
2050
- ".dk3-metal .dk3-face{background:linear-gradient(180deg,#93a5ba,#71809a);}",
2051
- ".dk3-metal .dk3-face:nth-child(5){background:linear-gradient(180deg,var(--dk3-side-hi),#9dadc2);}",
2052
- ".dk3-metal .dk3-face:nth-child(6){background:var(--dk3-side-dk);}",
2053
- // 书桌与桌腿
2054
- ".dk3-desk .dk3-face{background:linear-gradient(180deg,#9db0c6,#7f92aa);}",
2055
- ".dk3-desk .dk3-face:nth-child(5){background:#c3d2e2;}",
2056
- // 显示器(薄盒 + 贴在前面的屏幕;组内扇形微转,整组后撤出桌沿)
2057
- ".dk3-mon3{position:absolute;transform-style:preserve-3d;}",
2058
- ".dk3-frame .dk3-face{background:#3a465b;}",
2059
- ".dk3-frame .dk3-face:nth-child(5){background:#55647d;}",
2060
- ".dk3-screen{position:absolute;left:50%;top:50%;background:var(--dk3-screen);border-radius:2px;overflow:hidden;transition:opacity .4s var(--ds-ease-in-out),box-shadow .4s var(--ds-ease-in-out);}",
2061
- // 屏幕内容:代码块整体向上滚动(10 行=5 行×2 循环,滚半程无缝接回)+ 闪烁光标,速度随 --dkan-speed
2062
- ".dk3-code{position:absolute;left:0;top:0;right:0;display:block;animation:dk3-scroll calc(5s / var(--dkan-speed,1)) linear infinite;}",
2063
- ".dk3-code i{display:block;height:2px;border-radius:1px;margin:2px 3px 0;background:var(--dkan-code-b);opacity:.55;}",
2064
- ".dk3-code i:nth-child(5n+1){width:58%;background:var(--dkan-code-a);}",
2065
- ".dk3-code i:nth-child(5n+2){width:82%;}",
2066
- ".dk3-code i:nth-child(5n+3){width:46%;background:var(--dkan-code-c);}",
2067
- ".dk3-code i:nth-child(5n+4){width:72%;}",
2068
- ".dk3-code i:nth-child(5n){width:54%;background:var(--dkan-code-a);}",
2069
- ".dk3-cur{position:absolute;left:3px;bottom:2px;width:4px;height:2px;background:var(--dkan-code-b);animation:dkan-blink3 1s steps(1) infinite;}",
2070
- // 左屏检索视图:搜索框、放大镜与结果条,和右侧代码编辑器明确区分
2071
- ".dk3-search-ui{position:absolute;inset:3px;display:block;overflow:hidden;}",
2072
- ".dk3-search-box{display:block;height:5px;margin:0 1px 3px;border:1px solid color-mix(in srgb,var(--dkan-code-a) 72%,#d7fff4);border-radius:2px;box-sizing:border-box;}",
2073
- ".dk3-search-box b{display:block;width:2px;height:2px;margin:1px 0 0 2px;border:1px solid var(--dkan-code-a);border-radius:50%;position:relative;}",
2074
- ".dk3-search-box b:after{content:'';position:absolute;width:2px;height:1px;background:var(--dkan-code-a);right:-2px;bottom:-1px;transform:rotate(45deg);transform-origin:left center;}",
2075
- ".dk3-search-results{display:block;animation:dk3-search-scroll calc(2.1s / var(--dkan-speed,1)) linear infinite;}",
2076
- ".dk3-search-results i{display:block;height:2px;margin:2px 1px 0;border-radius:1px;background:var(--dkan-code-a);opacity:.72;}",
2077
- ".dk3-search-results i:nth-child(2n){width:74%;background:var(--dkan-code-b);}",
2078
- ".dk3-search-results i:nth-child(3n){width:48%;background:var(--dkan-code-c);}",
2079
- "@keyframes dk3-search-scroll{to{transform:translateY(-5px)}}",
2080
- "@keyframes dk3-scroll{to{transform:translateY(-50%)}}",
2081
- // 人物与椅子始终正对桌面:工位切换只横移,不再旋转整个人和座椅。
2082
- ".dk3-person{--dk3-seat-x:22px;position:absolute;left:98px;top:14px;width:44px;height:61px;transform-style:preserve-3d;transform:translateZ(30px) translateX(var(--dk3-seat-x));transition:transform max(.18s,calc(.38s / var(--dkan-speed,1))) cubic-bezier(.23,1,.32,1);will-change:transform;}",
2083
- ".dkan-bot-scene[data-station=left] .dk3-person{--dk3-seat-x:0px;}",
2084
- ".dkan-bot-scene[data-station=center] .dk3-person{--dk3-seat-x:22px;}",
2085
- ".dkan-bot-scene[data-station=right] .dk3-person{--dk3-seat-x:44px;}",
2086
- // 上身以髋部为旋转原点;只做前后倾,始终朝向桌面。
2087
- ".dk3-upper3{position:absolute;inset:0;transform-style:preserve-3d;transform-origin:12px 52px;transform:translateZ(0);transition:transform .24s cubic-bezier(.77,0,.175,1);will-change:transform;}",
2088
- // 动漫人物配色:皮肤/头发/卫衣/裤子
2089
- ".dk3-skin .dk3-face{background:linear-gradient(180deg,#ffd9b8,#f3b98d);}",
2090
- ".dk3-skin .dk3-face:nth-child(5){background:#ffe3c9;}",
2091
- ".dk3-hair .dk3-face{background:linear-gradient(180deg,#5b4632,#3f3021);}",
2092
- ".dk3-hair .dk3-face:nth-child(5){background:#6b543c;}",
2093
- ".dk3-hood .dk3-face{background:linear-gradient(180deg,#7c8ba0,#5b6a80);}",
2094
- ".dk3-hood .dk3-face:nth-child(5){background:#93a5ba;}",
2095
- ".dk3-pants .dk3-face{background:linear-gradient(180deg,#3d4a5c,#2c3646);}",
2096
- ".dk3-pants .dk3-face:nth-child(5){background:#4a5a70;}",
2097
- // 人物细节:收窄下颌、连续肩颈、短侧渐层发型
2098
- ".dk3-cranium .dk3-face{border-radius:42% 46% 38% 36%;}",
2099
- ".dk3-jaw3 .dk3-face{border-radius:30% 30% 48% 48%;}",
2100
- ".dk3-neck3 .dk3-face{border-radius:2px;}",
2101
- ".dk3-collar3 .dk3-face{background:linear-gradient(180deg,#516176,#374459);border-radius:3px;}",
2102
- ".dk3-shoulders3 .dk3-face{border-radius:4px;}",
2103
- ".dk3-chest3 .dk3-face{border-radius:4px;}",
2104
- ".dk3-waist3 .dk3-face{border-radius:3px;}",
2105
- ".dk3-hair .dk3-face{border-radius:2px;}",
2106
- ".dk3-hair-crown .dk3-face{border-radius:48% 42% 28% 30%;}",
2107
- ".dk3-hair-back .dk3-face{background:linear-gradient(180deg,#493727,#2f241b);border-radius:46% 24% 38% 44%;}",
2108
- ".dk3-shoe .dk3-face{background:#20262f;border-radius:2px;}",
2109
- ".dk3-mug .dk3-face{background:#c2703d;border-radius:1px;}",
2110
- ".dk3-mug .dk3-face:nth-child(5){background:#e08a52;}",
2111
- ".dk3-mouth{position:absolute;left:50%;top:64%;width:3.5px;height:1px;border-radius:2px;background:#a9635c;}",
2112
- // 椅子
2113
- ".dk3-chairback .dk3-face{background:linear-gradient(180deg,#3f4c60,#2c3648);}",
2114
- ".dk3-chairseat .dk3-face{background:#33415a;}",
2115
- // 头组与颈部中心均为 x=14;只让头部观察左右屏,身体和椅子不转向。
2116
- ".dk3-head3{--dk3-head-turn:rotateY(0deg);position:absolute;left:6px;top:3px;width:17px;height:21px;transform-style:preserve-3d;transform-origin:8px 18px;transform:var(--dk3-head-turn);transition:transform max(.18s,calc(.22s / var(--dkan-speed,1))) cubic-bezier(.23,1,.32,1);will-change:transform;}",
2117
- ".dkan-bot-scene[data-station=left] .dk3-head3{--dk3-head-turn:rotateY(-18deg);}",
2118
- ".dkan-bot-scene[data-station=center] .dk3-head3{--dk3-head-turn:rotateY(0deg);}",
2119
- ".dkan-bot-scene[data-station=right] .dk3-head3{--dk3-head-turn:rotateY(14deg);}",
2120
- ".dk3-brows{position:absolute;left:50%;top:29%;display:flex;gap:2.5px;align-items:center;justify-content:center;}",
2121
- ".dk3-brows i{width:3.5px;height:1px;border-radius:2px;background:#3a2a21;transform:rotate(-7deg);}",
2122
- ".dk3-brows i+ i{transform:rotate(7deg);}",
2123
- ".dk3-eyes{position:absolute;left:50%;top:51%;display:flex;gap:2.5px;align-items:center;justify-content:center;}",
2124
- ".dk3-eyes i{width:3.5px;height:2.5px;border-radius:50%;background:radial-gradient(circle at 62% 30%,#fff 0 .5px,#263141 .8px 1.7px,#111827 1.9px);animation:dkan-blink3 4.8s infinite;}",
2125
- ".dk3-nose{position:absolute;left:50%;top:66%;width:2px;height:3px;border-radius:60% 40% 55% 45%;background:linear-gradient(90deg,#e7a67e,#c77f67);}",
2126
- ".dk3-ear{position:absolute;left:1px;top:10px;width:3px;height:5px;border-radius:52% 44% 48% 52%;background:linear-gradient(90deg,#edaa82,#ffd2ad);transform:translateZ(2px);}",
2127
- ".dk3-fringe3{position:absolute;left:13px;top:4px;width:5px;height:7px;transform-style:preserve-3d;transform:rotateY(90deg) translateZ(5.8px);}",
2128
- ".dk3-fringe3 i{position:absolute;top:0;width:2px;height:6px;border-radius:70% 25% 60% 30%;background:linear-gradient(180deg,#6b5239,#392b20);transform-origin:top center;}",
2129
- ".dk3-fringe3 i:nth-child(1){left:0;transform:rotate(-18deg);height:5px;}",
2130
- ".dk3-fringe3 i:nth-child(2){left:1.8px;transform:rotate(-6deg);height:6px;}",
2131
- ".dk3-fringe3 i:nth-child(3){left:3.5px;transform:rotate(12deg);height:4px;}",
2132
- "@keyframes dkan-blink3{0%,91%,100%{transform:scaleY(1)}94%{transform:scaleY(.15)}}",
2133
- // 手臂按肩→肘→腕分段,旋转原点位于真实关节连接处。
2134
- ".dk3-arm3{position:absolute;left:20px;top:30px;width:16px;height:16px;transform-style:preserve-3d;transform-origin:2px 2px;will-change:transform;}",
2135
- ".dk3-elbow{position:absolute;left:0;top:10px;width:15px;height:5px;transform-style:preserve-3d;transform-origin:1px 2px;will-change:transform;}",
2136
- ".dk3-wrist3{position:absolute;left:9.5px;top:0;width:6px;height:5px;transform-style:preserve-3d;transform-origin:1px 2px;will-change:transform;}",
2137
- ".dk3-fingers3{position:absolute;left:3px;top:1px;width:4px;height:3px;transform:translateZ(2px);}",
2138
- ".dk3-fingers3 i{position:absolute;left:0;width:4px;height:.8px;border-radius:2px;background:#f2b58d;}",
2139
- ".dk3-fingers3 i+ i{top:1.4px;width:3.5px;}",
2140
- ".dk3-far{transform:translateZ(-7px);filter:brightness(.72);}",
2141
- // 思考泡泡(贴镜头 2D 层,think 阶段显示)
2142
- ".dkan-bubble{position:absolute;left:80px;top:4px;display:none;gap:3px;padding:4px 6px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);box-shadow:0 2px 6px rgb(0 0 0 / .15);z-index:3;}",
2143
- ".dkan-bubble i{width:4px;height:4px;border-radius:50%;background:var(--dsw-alias-label-tertiary);animation:dkan-bubdot 1.2s ease-in-out infinite;}",
2144
- ".dkan-bubble i:nth-child(2){animation-delay:.2s;}",
2145
- ".dkan-bubble i:nth-child(3){animation-delay:.4s;}",
2146
- "@keyframes dkan-bubdot{0%,100%{opacity:.25;transform:translateY(0)}50%{opacity:1;transform:translateY(-2px)}}",
2147
- // ===== 多任务工位(data-tasks=活跃任务数,上限 3) =====
2148
- // 椅子滚轮
2149
- ".dk3-wheel .dk3-face{background:#1c232e;border-radius:50%;}",
2150
- // 有活跃任务时,未轮到的侧工位稍暗;多任务则两个工位均保持可见。
2151
- ".dkan-bot-scene[data-tasks] .dk3-mon3.left .dk3-screen,.dkan-bot-scene[data-tasks] .dk3-mon3.right .dk3-screen{opacity:.3;}",
2152
- // 2 个及以上任务:左右工位同时点亮(各自带辉光、代码滚动加速)。
2153
- ".dkan-bot-scene[data-tasks=\"2\"] .dk3-mon3.left .dk3-screen,.dkan-bot-scene[data-tasks=\"2\"] .dk3-mon3.right .dk3-screen,.dkan-bot-scene[data-tasks=\"3\"] .dk3-mon3.left .dk3-screen,.dkan-bot-scene[data-tasks=\"3\"] .dk3-mon3.right .dk3-screen{opacity:1;box-shadow:0 0 8px color-mix(in srgb,var(--dkan-code-b,#60a5fa) 42%,transparent);}",
2154
- ".dkan-bot-scene[data-tasks=\"2\"] .dk3-mon3.left .dk3-code,.dkan-bot-scene[data-tasks=\"2\"] .dk3-mon3.right .dk3-code,.dkan-bot-scene[data-tasks=\"3\"] .dk3-mon3.left .dk3-code,.dkan-bot-scene[data-tasks=\"3\"] .dk3-mon3.right .dk3-code{animation-duration:calc(2.6s / var(--dkan-speed,1));}",
2155
- // 座椅移动期间滚轮跟随任务节奏转动;思考中停下,避免无意义的常驻运动。
2156
- ".dkan-bot-scene[data-station=left] .dk3-wheel,.dkan-bot-scene[data-station=right] .dk3-wheel{animation:dkan-wheel calc(.46s / var(--dkan-speed,1)) linear infinite;transform-origin:center;}",
2157
- "@keyframes dkan-wheel{to{transform:rotateZ(360deg)}}",
2158
- // ===== 阶段驱动(data-phase 四态,host 由 chunk 流实时同步) =====
2159
- // think:上身轻靠、近侧手托下巴、远侧手留在桌面;动作有停顿而不是机械往返。
2160
- ".dkan-bot-scene[data-phase=think] .dk3-screen{opacity:.32;}",
2161
- ".dkan-bot-scene[data-phase=think] .dkan-bubble{display:inline-flex;}",
2162
- ".dkan-bot-scene[data-phase=think] .dk3-upper3{animation:dkan-think-upper max(1.8s,calc(2.4s / var(--dkan-speed,1))) cubic-bezier(.77,0,.175,1) infinite;}",
2163
- ".dkan-bot-scene[data-phase=think] .dk3-head3{animation:dkan-think-head max(1.8s,calc(2.4s / var(--dkan-speed,1))) cubic-bezier(.77,0,.175,1) infinite;}",
2164
- ".dkan-bot-scene[data-phase=think] .dk3-arm3.dk3-near{animation:dkan-think-near max(1.8s,calc(2.4s / var(--dkan-speed,1))) cubic-bezier(.77,0,.175,1) infinite;}",
2165
- ".dkan-bot-scene[data-phase=think] .dk3-arm3.dk3-far{transform:translateZ(-7px) rotateZ(3deg);}",
2166
- ".dkan-bot-scene[data-phase=think] .dk3-near .dk3-elbow{transform:rotateZ(-28deg) translateY(-1px);}",
2167
- ".dkan-bot-scene[data-phase=think] .dk3-near .dk3-wrist3{transform:rotateZ(-18deg) translateY(-1px);}",
2168
- "@keyframes dkan-think-upper{0%,44%{transform:rotateZ(-1deg) translateY(0)}62%,100%{transform:rotateZ(-1.8deg) translateY(-.5px)}}",
2169
- "@keyframes dkan-think-head{0%,44%{transform:var(--dk3-head-turn) rotateZ(-3deg)}62%,100%{transform:var(--dk3-head-turn) rotateZ(-6deg) translateY(-.5px)}}",
2170
- "@keyframes dkan-think-near{0%,44%{transform:rotateZ(-31deg) translate(-8px,-14.5px)}62%,100%{transform:rotateZ(-34deg) translate(-8.5px,-15px)}}",
2171
- // write/code:身体仍面向桌面,仅轻微前倾;主要由手腕和手指敲击。
2172
- ".dkan-bot-scene[data-phase=write] .dk3-mon3.right .dk3-screen,.dkan-bot-scene[data-phase=code] .dk3-mon3.right .dk3-screen{opacity:1;box-shadow:0 0 11px color-mix(in srgb,var(--dkan-code-b,#60a5fa) 52%,transparent);}",
2173
- ".dkan-bot-scene[data-phase=write] .dk3-mon3.right .dk3-code,.dkan-bot-scene[data-phase=code] .dk3-mon3.right .dk3-code{animation-duration:calc(1.35s / var(--dkan-speed,1));}",
2174
- ".dkan-bot-scene[data-phase=write] .dk3-upper3,.dkan-bot-scene[data-phase=code] .dk3-upper3{animation:dkan-code-upper max(1.2s,calc(2.4s / var(--dkan-speed,1))) cubic-bezier(.77,0,.175,1) infinite;}",
2175
- ".dkan-bot-scene[data-phase=write] .dk3-arm3.dk3-near,.dkan-bot-scene[data-phase=code] .dk3-arm3.dk3-near{transform:rotateZ(3deg);}",
2176
- ".dkan-bot-scene[data-phase=write] .dk3-arm3.dk3-far,.dkan-bot-scene[data-phase=code] .dk3-arm3.dk3-far{transform:translateZ(-7px) rotateZ(1deg);}",
2177
- ".dkan-bot-scene[data-phase=write] .dk3-elbow,.dkan-bot-scene[data-phase=code] .dk3-elbow{animation:dkan-type-elbow max(.28s,calc(.58s / var(--dkan-speed,1))) cubic-bezier(.77,0,.175,1) infinite;}",
2178
- ".dkan-bot-scene[data-phase=write] .dk3-wrist3,.dkan-bot-scene[data-phase=code] .dk3-wrist3{animation:dkan-type-wrist max(.18s,calc(.29s / var(--dkan-speed,1))) cubic-bezier(.77,0,.175,1) infinite;}",
2179
- ".dkan-bot-scene[data-phase=write] .dk3-far .dk3-elbow,.dkan-bot-scene[data-phase=code] .dk3-far .dk3-elbow{animation-delay:-.09s;animation-duration:max(.3s,calc(.63s / var(--dkan-speed,1)));}",
2180
- ".dkan-bot-scene[data-phase=write] .dk3-far .dk3-wrist3,.dkan-bot-scene[data-phase=code] .dk3-far .dk3-wrist3{animation-delay:-.09s;animation-duration:max(.2s,calc(.33s / var(--dkan-speed,1)));}",
2181
- ".dkan-bot-scene[data-phase=write] .dk3-head3,.dkan-bot-scene[data-phase=code] .dk3-head3{animation:dkan-output-head max(1.2s,calc(2.4s / var(--dkan-speed,1))) cubic-bezier(.77,0,.175,1) infinite;}",
2182
- "@keyframes dkan-code-upper{0%,38%{transform:rotateZ(2deg) translate(1px,0)}62%,100%{transform:rotateZ(3deg) translate(1px,1px)}}",
2183
- "@keyframes dkan-type-elbow{0%,38%{transform:rotateZ(-4deg)}62%,100%{transform:rotateZ(5deg) translateY(1px)}}",
2184
- "@keyframes dkan-type-wrist{0%,34%{transform:rotateZ(-2deg) translateY(0)}62%,100%{transform:rotateZ(3deg) translateY(1.5px)}}",
2185
- "@keyframes dkan-output-head{0%,46%,100%{transform:var(--dk3-head-turn) rotateZ(3deg)}58%{transform:var(--dk3-head-turn) rotateZ(5deg) translateY(1px)}}",
2186
- // search:身体正对桌面,头看左屏,近侧手腕小幅操作鼠标。
2187
- ".dkan-bot-scene[data-phase=search] .dk3-mon3.left .dk3-screen{opacity:1;box-shadow:0 0 10px color-mix(in srgb,var(--dkan-code-a,#4ade80) 52%,transparent);}",
2188
- ".dkan-bot-scene[data-phase=search] .dk3-mon3.left .dk3-search-results{animation-duration:calc(1.05s / var(--dkan-speed,1));}",
2189
- ".dkan-bot-scene[data-phase=search] .dk3-upper3{transform:rotateZ(2deg) translateY(1px);}",
2190
- ".dkan-bot-scene[data-phase=search] .dk3-head3{animation:dkan-search-head max(.72s,calc(1.15s / var(--dkan-speed,1))) cubic-bezier(.77,0,.175,1) infinite;}",
2191
- ".dkan-bot-scene[data-phase=search] .dk3-near .dk3-wrist3{animation:dkan-search-wrist max(.45s,calc(.72s / var(--dkan-speed,1))) cubic-bezier(.77,0,.175,1) infinite;}",
2192
- ".dkan-bot-scene[data-phase=search] .dk3-far{transform:translateZ(-7px) rotateZ(2deg);}",
2193
- "@keyframes dkan-search-head{0%,28%{transform:var(--dk3-head-turn) rotateZ(1deg)}48%,68%{transform:var(--dk3-head-turn) rotateZ(6deg) translate(1px,1px)}88%,100%{transform:var(--dk3-head-turn) rotateZ(2deg)}}",
2194
- "@keyframes dkan-search-wrist{0%,34%{transform:translateX(-1px)}62%,100%{transform:translateX(1.5px)}}",
2195
- // 减少动态时停止空间位移,保留屏幕亮暗与思考状态的淡入反馈。
2196
- "@media (prefers-reduced-motion:reduce){.dkan-amb *,.dkan-prev *,.dkan-space-system,.dkan-space-dust,.dkan-space-runner,.dkan-delivery,.dkan-departure,.dkan-freighter-engine i,.dkan-stuck-code::after,.dkan-stuck-code i{animation:none!important}.dkan-space-runner,.dkan-departure{display:none}.dkan-delivery{opacity:1;transform:translate3d(var(--dkan-to-x),var(--dkan-to-y),0)}.dkan-delivery-trail{display:none}.dk3-person,.dk3-upper3,.dk3-head3,.dk3-arm3,.dk3-elbow,.dk3-wrist3,.dk3-wheel,.dk3-code,.dk3-search-results{animation:none!important;transition:none!important}.dk3-screen,.dkan-bubble{transition:opacity .2s cubic-bezier(.23,1,.32,1)!important}}",
2197
- // 通知子选项行
2198
- ".dkan-rows-narrow{display:flex;flex-direction:column;gap:6px;}",
2199
- ".dkan-row{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary);flex-wrap:wrap;}",
2200
- ".dkan-row-label{flex:none;min-width:60px;color:var(--dsw-alias-label-primary);}",
2201
- ".dkan-row-sub{font-size:11px;color:var(--dsw-alias-label-tertiary);}",
2202
- ".dkm-miniswitch{cursor:pointer;flex:none;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 12px;font-family:inherit;font-size:11px;line-height:18px;}",
2203
- ".dkm-miniswitch.on{color:var(--dsw-alias-state-success-primary);border-color:currentColor;}",
2204
- ".dkan-select{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:3px 8px;font-size:12px;font-family:inherit;}",
2205
- // 音效选择卡(名称 + 播放键;选中态描边)
2206
- ".dkan-sounds{display:flex;gap:6px;flex-wrap:wrap;}",
2207
- ".dkan-sound{flex:1;min-width:104px;display:flex;align-items:center;justify-content:space-between;gap:6px;padding:6px 10px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-2);cursor:pointer;font-family:inherit;transition:border-color .15s var(--ds-ease-in-out);}",
2208
- ".dkan-sound:hover{border-color:var(--dk-accent);}",
2209
- ".dkan-sound.on{border-color:var(--dk-accent);box-shadow:0 0 0 1px color-mix(in srgb,var(--dk-accent) 40%,transparent);}",
2210
- ".dkan-sound-name{font-size:12px;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:5px;}",
2211
- ".dkan-sound-cur{color:var(--dk-accent);font-size:11px;}",
2212
- ".dkan-sound-play{flex:none;cursor:pointer;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:1;border-radius:50%;width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--dsw-alias-border-l2);}",
2213
- ".dkan-sound-play:hover{color:var(--dsw-alias-label-primary);border-color:currentColor;}",
2214
- ".dkan-input{flex:1;min-width:220px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:4px 8px;font-size:12px;font-family:inherit;}",
2215
- ".dkan-input:focus{outline:none;border-color:var(--dk-accent);}",
2216
- ".dkan-row-webhook{flex-wrap:nowrap;}",
2217
- ".dkan-row-webhook .dkan-input{min-width:0;}",
2218
- ".dkan-btn{cursor:pointer;flex:none;color:var(--dsw-alias-label-primary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:4px 12px;font-family:inherit;font-size:12px;}",
2219
- ".dkan-btn:hover{background:var(--dsw-alias-interactive-bg-hover);}",
2220
- ".dkan-btn[disabled]{opacity:.5;cursor:default;}",
2221
- ".dkan-ok{color:var(--dsw-alias-state-success-primary);}",
2222
- // 任务列表
2223
- ".dkan-tasks{display:flex;flex-direction:column;gap:6px;}",
2224
- ".dkan-tasks-done{border-top:1px dashed var(--dsw-alias-border-l2);padding-top:6px;}",
2225
- ".dkan-task{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:6px 10px;display:flex;flex-direction:column;gap:3px;}",
2226
- ".dkan-task-head{display:flex;align-items:center;gap:8px;min-width:0;}",
2227
- ".dkan-task-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
2228
- ".dkan-task-meta{display:flex;gap:10px;flex-wrap:wrap;font-size:11px;color:var(--dsw-alias-label-tertiary);}",
2229
- ".dkan-task-err{font-size:11px;color:var(--dsw-alias-state-error-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
2230
- ".dkan-tag{flex:none;font-size:10px;border-radius:999px;padding:0 8px;color:var(--dsw-alias-label-tertiary);border:1px solid var(--dsw-alias-border-l2);}",
2231
- ".dkan-tag.on{color:var(--dk-accent);border-color:currentColor;}",
2232
- ".dkan-tag.ok{color:var(--dsw-alias-state-success-primary);border-color:currentColor;}",
2233
- ".dkan-tag.err{color:var(--dsw-alias-state-error-primary);border-color:currentColor;}",
2234
- ".dkan-tag.warn{color:var(--dk-warn);border-color:currentColor;}",
2235
- ].join("\n");
2236
-
2237
- export const feature = {
2238
- id: "animation",
2239
- name: "任务动画",
2240
- order: 130,
2241
- accent: "#f472b6",
2242
- description: "19 种任务运行动画与完成通知:速度随任务活动联动,两组开关独立、配置持久化",
2243
- css,
2244
- View: AnimationView,
2245
- HomeStat: AnimationStat,
2246
- Overlay: AnimationOverlay,
2247
- };
1
+ // dsh-dock · 功能模块【任务动画】· 客户端视图(v0.5.0,参照 @wycto/dsh-task-pulse client 会话追踪消费,同作者 MIT)
2
+ //
3
+ // 三部分:
4
+ // 1. View —— 功能坞面板页:动画/通知两组独立开关 + 动画模式选择(带缩微预览)+ 运行状态列表;
5
+ // 2. HomeStat —— 首页总揽概要(N 个任务进行中 / 空闲);
6
+ // 3. Overlay —— 全局浮层(shell.overlay 常驻,功能启用即挂载):轮询 Host 状态,
7
+ // 任务进行中渲染克制的动效(顶部流光细线 / 呼吸光点 / 轨道光环)+ 右下角状态徽标,
8
+ // 任务结束时弹通知卡片(可选浏览器系统通知),完成瞬间一缕流光掠过。
9
+ //
10
+ // 设计取向:不做满屏粒子彩带,动效全部走主题变量(暗/亮色自适应)、低透明度、慢节奏。
11
+ // 动画与通知是两个独立开关(可只开其一);配置全部经 Host settings 持久化,重启后恢复。
12
+ //
13
+ // Host 通信:fetch('/dsh-dock/animation/<method>')(见 features/animation/host.js)。
14
+ import { useState, useEffect, useRef, useCallback, useMemo } from "react";
15
+ import { openPanel, panelNav, subscribePanel } from "../../src/shared.js";
16
+
17
+ // ---------- Host RPC 桥接 ----------
18
+ function rpcCall(method, args) {
19
+ return fetch("/dsh-dock/animation/" + method, {
20
+ method: "POST",
21
+ headers: { "content-type": "application/json" },
22
+ body: JSON.stringify(args === undefined ? {} : args),
23
+ })
24
+ .then(async (res) => {
25
+ const data = await res.json().catch(() => ({}));
26
+ if (res.ok && data && data.ok === true) return data.data;
27
+ // 旧宿主进程没有动画路由(405/404):给出可操作提示而非裸状态码
28
+ if (res.status === 405 || res.status === 404) {
29
+ throw new Error("宿主进程是旧版本(没有任务动画路由),重启 dsh web 后重试");
30
+ }
31
+ if (data && data.ok === false) throw new Error((data.error && data.error.message) || ("HTTP " + res.status));
32
+ throw new Error("HTTP " + res.status + (data && data.error && data.error.message ? ": " + data.error.message : ""));
33
+ });
34
+ }
35
+
36
+ // ---------- 动画模式 ----------
37
+ const EFFECT_MODES = [
38
+ { id: "flow", name: "流光细线", desc: "顶部细线流光往返,速度随任务吞吐加快" },
39
+ { id: "breathe", name: "呼吸光点", desc: "醒目呼吸光点带扩散光晕:颜色随任务阶段(思考蓝/输出绿/代码橙/查资料青),呼吸随吞吐加快" },
40
+ { id: "ring", name: "轨道光环", desc: "细环绕圆点旋转,转速随任务速度" },
41
+ { id: "orbit", name: "环屏巡航", desc: "一颗光点沿屏幕边缘巡航整圈,醒目不遮挡" },
42
+ { id: "robot", name: "桌面伙伴", desc: "真人化双工位:查资料看左屏、写入/改代码移到右屏;座椅和转头随实时任务节奏加速" },
43
+ { id: "matrix", name: "代码雨", desc: "字符沿屏幕缓落如数据流,速度随任务吞吐,克制的低透明度" },
44
+ { id: "stars", name: "星野", desc: "细碎星点缓慢飘移闪烁,安静耐看的背景氛围" },
45
+ { id: "aurora", name: "极光", desc: "屏幕顶部柔光带缓慢呼吸流动,像极光拂过" },
46
+ { id: "space", name: "星际远征", desc: "任务中星球与飞船巡航;完成后货运旗舰携实际输出量停靠输入框" },
47
+ { id: "nebula", name: "星云潮汐", desc: "四团彩色星云在屏幕边缘缓慢潮汐,阶段色随任务改变" },
48
+ { id: "warp", name: "曲速航道", desc: "光束从屏幕中心向外拉伸,吞吐越高曲速越快" },
49
+ { id: "radar", name: "量子雷达", desc: "三座边缘雷达持续扫描,任务活动化作跳动信标" },
50
+ { id: "constellation", name: "星座网络", desc: "节点与连线逐段点亮,像任务知识图谱在生长" },
51
+ { id: "fireflies", name: "数据萤火", desc: "轻盈光点在空白区游弋,活跃阶段会更明亮" },
52
+ { id: "ocean", name: "深海脉动", desc: "低透明度波层与气泡缓慢上浮,安静但富有生命感" },
53
+ { id: "prism", name: "棱镜光谱", desc: "彩色光束从屏幕边缘折射穿行,形成克制的玻璃光感" },
54
+ { id: "circuit", name: "神经电路", desc: "电路路径依次通电,脉冲速度跟随代码与工具活动" },
55
+ { id: "gravity", name: "引力涟漪", desc: "多个引力源向外扩散波纹,任务越快涟漪越紧密" },
56
+ { id: "lantern", name: "灵感天灯", desc: "微型灵感灯从屏幕底部缓慢升起,留下温暖光迹" },
57
+ ];
58
+
59
+ const AMBIENT_EFFECT_MODES = new Set(["matrix", "stars", "aurora", "space", "nebula", "warp", "radar", "constellation", "fireflies", "ocean", "prism", "circuit", "gravity", "lantern"]);
60
+
61
+ // 任务阶段 → 机器人行为/文案(host 按 chunk/事件实时推导:
62
+ // reasoning-delta=think、text-delta=write、tool 名分类=search/code)
63
+ const PHASE_LABELS = { think: "思考中", write: "输出中", code: "编写代码", search: "查资料" };
64
+ function phaseLabel(p) { return PHASE_LABELS[p] || "工作中"; }
65
+ // 阶段配色(徽标呼吸点/光环随任务阶段变色,--dkan-phase 下发到 CSS)
66
+ const PHASE_COLORS = { think: "#2f6fed", write: "#0d9488", code: "#b45309", search: "#0e7490" };
67
+ function phaseColor(p) { return PHASE_COLORS[p] || "#2f6fed"; }
68
+
69
+ // 工具名 → 中文场景("需确认"通知里说清楚是什么在等确认)
70
+ function toolLabel(name) {
71
+ const n = String(name || "").toLowerCase();
72
+ if (/bash|shell|terminal|cmd|pwsh|powershell|exec/.test(n)) return "执行命令";
73
+ if (/write|edit|patch|apply|create|mkdir|remove|delete/.test(n)) return "修改文件";
74
+ if (/web|fetch|browser|navigate|search/.test(n)) return "访问网页";
75
+ return "";
76
+ }
77
+
78
+ // ---------- 结束原因 ----------
79
+ const END_LABELS = {
80
+ completed: { label: "完成", cls: "ok" },
81
+ error: { label: "出错", cls: "err" },
82
+ aborted: { label: "已中止", cls: "warn" },
83
+ blocked: { label: "受阻", cls: "warn" },
84
+ "max-tokens": { label: "达输出上限", cls: "warn" },
85
+ interrupted: { label: "中断", cls: "warn" },
86
+ };
87
+ function endInfo(reason) {
88
+ return END_LABELS[reason] || END_LABELS.completed;
89
+ }
90
+ function isSuccessReason(reason) {
91
+ return !reason || reason === "completed";
92
+ }
93
+
94
+ // ---------- 格式化 ----------
95
+ function fmtNum(n) { return (Number(n) || 0).toLocaleString("en-US"); }
96
+ function fmtCompact(n) {
97
+ n = Number(n) || 0;
98
+ if (n >= 1e6) return (n / 1e6).toFixed(2) + "M";
99
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
100
+ return String(Math.round(n));
101
+ }
102
+ // 毫秒 → "3分21秒" / "1小时2分"
103
+ function fmtDur(ms) {
104
+ const s = Math.max(0, Math.round((ms || 0) / 1000));
105
+ const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
106
+ if (h > 0) return h + "小时" + m + "分" + sec + "秒";
107
+ if (m > 0) return m + "分" + sec + "秒";
108
+ return sec + "秒";
109
+ }
110
+ // 毫秒 → "12:34" / "1:02:11"(徽标计时)
111
+ function fmtClock(ms) {
112
+ const s = Math.max(0, Math.floor((ms || 0) / 1000));
113
+ const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
114
+ const p = (x) => String(x).padStart(2, "0");
115
+ return h > 0 ? h + ":" + p(m) + ":" + p(sec) : p(m) + ":" + p(sec);
116
+ }
117
+ function fmtTime(ts) {
118
+ if (!ts) return "";
119
+ const d = new Date(ts);
120
+ const p = (x) => String(x).padStart(2, "0");
121
+ return p(d.getHours()) + ":" + p(d.getMinutes()) + ":" + p(d.getSeconds());
122
+ }
123
+ function truncate(str, max) {
124
+ if (!str) return "";
125
+ return String(str).length > max ? String(str).slice(0, max) + "…" : str;
126
+ }
127
+
128
+ // ---------- 共享快照:浮层(轮询) / 面板页 / 首页概要共用一份数据 ----------
129
+ const animationStore = {
130
+ snap: { status: null, loading: false, error: null },
131
+ listeners: new Set(),
132
+ subscribe(fn) { this.listeners.add(fn); return () => { this.listeners.delete(fn); }; },
133
+ emit() { for (const fn of this.listeners) fn(); },
134
+ applyConfig(cfg) {
135
+ if (this.snap.status) {
136
+ this.snap = Object.assign({}, this.snap, { status: Object.assign({}, this.snap.status, { config: cfg }) });
137
+ this.emit();
138
+ }
139
+ },
140
+ refresh() {
141
+ if (this.snap.loading) return Promise.resolve();
142
+ this.snap = Object.assign({}, this.snap, { loading: true });
143
+ this.emit();
144
+ return rpcCall("status")
145
+ .then((data) => {
146
+ this.snap = { status: data, loading: false, error: null };
147
+ this.emit();
148
+ })
149
+ .catch((e) => {
150
+ this.snap = Object.assign({}, this.snap, { loading: false, error: (e && e.message) || String(e) });
151
+ this.emit();
152
+ });
153
+ },
154
+ };
155
+ function useAnimation() {
156
+ const [snap, setSnap] = useState(animationStore.snap);
157
+ useEffect(() => animationStore.subscribe(() => setSnap(animationStore.snap)), []);
158
+ return snap;
159
+ }
160
+
161
+ // 秒级滴答(徽标计时用;ctx.interval 优先,退回 setInterval)
162
+ function useTicker(ctx, active) {
163
+ const [, bump] = useState(0);
164
+ useEffect(() => {
165
+ if (!active) return;
166
+ if (ctx && typeof ctx.interval === "function") return ctx.interval(bump, 1000);
167
+ const t = setInterval(bump, 1000);
168
+ return () => clearInterval(t);
169
+ }, [active]);
170
+ }
171
+
172
+ // ---------- 桌面场景(CSS 3D:长方体拼装的 3D 动漫人物,侧身面向镜头、面朝三屏) ----------
173
+ // data-phase 驱动四态(think/write/code/search,host 由 assistant/chunk 流级事件实时同步);
174
+ // --dkan-speed 随任务吞吐加速;卡片可整卡自由拖拽(位置 localStorage 持久化)。
175
+ // Box3 = 六面长方体(w×h×d),faces: 前(+Z)/后/右(+X)/左/顶/底;世界层统一转角取 3/4 视角。
176
+ function Box3(props) {
177
+ const w = props.w, h = props.h, d = props.d;
178
+ const faces = [
179
+ [w, h, "translate(-50%,-50%) translateZ(" + (d / 2) + "px)"],
180
+ [w, h, "translate(-50%,-50%) rotateY(180deg) translateZ(" + (d / 2) + "px)"],
181
+ [d, h, "translate(-50%,-50%) rotateY(90deg) translateZ(" + (w / 2) + "px)"],
182
+ [d, h, "translate(-50%,-50%) rotateY(-90deg) translateZ(" + (w / 2) + "px)"],
183
+ [w, d, "translate(-50%,-50%) rotateX(90deg) translateZ(" + (h / 2) + "px)"],
184
+ [w, d, "translate(-50%,-50%) rotateX(-90deg) translateZ(" + (h / 2) + "px)"],
185
+ ];
186
+ return (
187
+ <span className={"dk3-box " + (props.cls || "")}
188
+ style={Object.assign({ width: w, height: h, left: props.x - w / 2, top: props.y - h / 2 },
189
+ props.z ? { transform: "translateZ(" + props.z + "px)" } : null, props.style)}>
190
+ {faces.map((f, i) => (
191
+ <span key={i} className="dk3-face" style={{ width: f[0], height: f[1], transform: f[2] }} />
192
+ ))}
193
+ {props.children}
194
+ </span>
195
+ );
196
+ }
197
+
198
+ function Monitor3(props) {
199
+ const w = props.w, h = props.h;
200
+ const searchScreen = props.mode === "search";
201
+ // 屏幕内容:10 行代码(5 行×2 循环)+ 光标,整体向上滚动(速度随 --dkan-speed)
202
+ const lines = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5];
203
+ return (
204
+ <span className={"dk3-mon3 " + (props.cls || "")}
205
+ style={{ left: props.x - w / 2, top: props.y - h / 2, width: w, height: h, transform: "translateZ(" + (props.z || -3) + "px) rotateY(" + (props.ry || 0) + "deg)" }}>
206
+ <Box3 w={w} h={h} d={5} cls="dk3-frame" x={w / 2} y={h / 2} />
207
+ <span className="dk3-screen" style={{ width: w - 4, height: h - 4, transform: "translate(-50%,-50%) translateZ(3.1px)" }}>
208
+ {searchScreen ? <span className="dk3-search-ui">
209
+ <span className="dk3-search-box"><b /></span>
210
+ <span className="dk3-search-results">{lines.slice(0, 6).map((n, i) => <i key={i} />)}</span>
211
+ </span> : <span className="dk3-code">
212
+ {lines.map((n, i) => <i key={i} />)}
213
+ <span className="dk3-cur" />
214
+ </span>}
215
+ </span>
216
+ </span>
217
+ );
218
+ }
219
+
220
+ // 3D 动漫人物:更拟人的比例与细节(圆角头/ volumetric 头发/五官/脖颈/弯肘/腿脚鞋)。
221
+ // 紧凑双工位:左侧专做资料检索,右侧专做写作/改文件;顶部小屏放任务状态。
222
+ // 人物坐滚轮椅在两侧工位间滑动,思考时停在两屏之间。
223
+ function RobotScene(props) {
224
+ const phase = props && props.phase ? props.phase : "code";
225
+ const tasks = Array.isArray(props && props.tasks) ? props.tasks : null;
226
+ const n = tasks ? Math.max(1, Math.min(3, tasks.length)) : 1;
227
+ // 左工位只做资料检索;正文输出和工具写入都在右工位,思考回到中位。
228
+ const station = phase === "search" ? "left" : (phase === "write" || phase === "code" ? "right" : "center");
229
+ return (
230
+ <div className="dkan-bot-scene" data-phase={phase} data-station={station} data-tasks={tasks ? String(n) : undefined} aria-hidden="true">
231
+ <div className="dk3-world">
232
+ {/* 紧凑单人书桌:只留左右两个真实工作位,消除左侧空桌面。 */}
233
+ <Box3 w={134} h={7} d={38} cls="dk3-desk" x={145} y={76} />
234
+ <Box3 w={5} h={24} d={30} cls="dk3-metal dk3-leg" x={84} y={89} />
235
+ <Box3 w={5} h={24} d={30} cls="dk3-metal dk3-leg" x={206} y={89} />
236
+ {/* 左检索、右编码,顶部小屏显示任务进度。 */}
237
+ <Monitor3 w={34} h={24} x={140} y={59} ry={20} cls="left" mode="search" />
238
+ <Monitor3 w={42} h={30} x={180} y={57} ry={-16} cls="right" />
239
+ <Monitor3 w={28} h={18} x={160} y={31} ry={0} cls="top" />
240
+ {/* 两套键鼠紧贴相应屏幕,中央仅保留咖啡杯。 */}
241
+ <Box3 w={16} h={2.5} d={9} cls="dk3-metal dk3-kb3" x={130} y={71} z={14} />
242
+ <Box3 w={4} h={2.5} d={6} cls="dk3-metal dk3-mouse3" x={141} y={71} z={14} />
243
+ <Box3 w={4} h={5} d={4} cls="dk3-mug" x={154} y={69} z={14} />
244
+ <Box3 w={16} h={2.5} d={9} cls="dk3-metal dk3-kb3" x={170} y={71} z={14} />
245
+ <Box3 w={4} h={2.5} d={6} cls="dk3-metal dk3-mouse3" x={181} y={71} z={14} />
246
+ {/* 人物组:按当前任务在左检索/中思考/右编码两个工位间平滑移动。 */}
247
+ <div className="dk3-person">
248
+ {/* 椅子:靠背只到肩胛、降低支柱和底盘,贴近真实坐姿。 */}
249
+ <Box3 w={4} h={20} d={20} cls="dk3-chairback" x={2} y={47} />
250
+ <Box3 w={18} h={4} d={20} cls="dk3-chairseat" x={12} y={59} />
251
+ <Box3 w={3} h={9} d={3} cls="dk3-metal" x={12} y={66} />
252
+ <Box3 w={13} h={2} d={13} cls="dk3-metal" x={12} y={71} />
253
+ {/* 椅子滚轮(左右各一,滑动时可见) */}
254
+ <Box3 w={3} h={3} d={3} cls="dk3-wheel" x={6} y={74} />
255
+ <Box3 w={3} h={3} d={3} cls="dk3-wheel" x={18} y={74} />
256
+ {/* 腿:大腿/小腿/鞋 */}
257
+ <Box3 w={12} h={5} d={9} cls="dk3-pants" x={20} y={54} />
258
+ <Box3 w={4} h={10} d={4} cls="dk3-pants" x={26} y={62} />
259
+ <Box3 w={6} h={3} d={5} cls="dk3-shoe" x={28} y={70} />
260
+ {/* 上身整体始终面向桌面;阶段动作只改变前倾、肩肘腕和视线。 */}
261
+ <div className="dk3-upper3">
262
+ {/* 肩线、胸腔和腰部做成上宽下窄,摆脱直筒积木感。 */}
263
+ <Box3 w={18} h={5} d={12} cls="dk3-hood dk3-shoulders3" x={13} y={30} />
264
+ <Box3 w={15} h={14} d={11} cls="dk3-hood dk3-torso dk3-chest3" x={12} y={38} />
265
+ <Box3 w={12} h={7} d={10} cls="dk3-hood dk3-torso dk3-waist3" x={12} y={47} />
266
+ <Box3 w={5} h={8} d={9} cls="dk3-hood dk3-hoodbump" x={5} y={32} />
267
+ {/* 颈部加长并与下颌、衣领重叠,避免悬空。 */}
268
+ <Box3 w={5} h={7} d={5} cls="dk3-skin dk3-neck3" x={14} y={24} />
269
+ <Box3 w={8} h={3} d={8} cls="dk3-collar3" x={14} y={28} />
270
+ {/* 头组与颈部同轴:颅骨 + 收窄下颌 + 短侧渐层发型。 */}
271
+ <div className="dk3-head3">
272
+ <Box3 w={12} h={11} d={11} cls="dk3-skin dk3-cranium" x={8} y={8}>
273
+ <span className="dk3-brows" style={{ width: 9, height: 4, transform: "translate(-50%,-50%) rotateY(90deg) translateZ(5.8px)" }}><i /><i /></span>
274
+ <span className="dk3-eyes" style={{ width: 9, height: 6, transform: "translate(-50%,-50%) rotateY(90deg) translateZ(5.7px)" }}><i /><i /></span>
275
+ <span className="dk3-nose" style={{ transform: "translate(-50%,-50%) rotateY(90deg) translateZ(6.1px)" }} />
276
+ </Box3>
277
+ <Box3 w={9} h={7} d={9} cls="dk3-skin dk3-jaw3" x={8.5} y={14}>
278
+ <span className="dk3-mouth" style={{ transform: "translate(-50%,-50%) rotateY(90deg) translateZ(4.7px)" }} />
279
+ </Box3>
280
+ <Box3 w={12} h={5} d={12} cls="dk3-hair dk3-hair-crown" x={8} y={3.5} />
281
+ <Box3 w={4} h={8} d={11} cls="dk3-hair dk3-hair-back" x={3} y={7.5} />
282
+ <span className="dk3-fringe3"><i /><i /><i /></span>
283
+ <span className="dk3-ear" />
284
+ </div>
285
+ {/* 近侧手臂:肩、肘、腕、手指分段动作。 */}
286
+ <div className="dk3-arm3 dk3-near">
287
+ <Box3 w={4} h={11} d={4} cls="dk3-hood dk3-uarm" x={2} y={6} />
288
+ <div className="dk3-elbow">
289
+ <Box3 w={10} h={3.5} d={3.5} cls="dk3-hood dk3-farm" x={5.5} y={2} />
290
+ <div className="dk3-wrist3">
291
+ <Box3 w={3.5} h={3} d={3} cls="dk3-skin dk3-hand" x={2} y={2} />
292
+ <span className="dk3-fingers3"><i /><i /></span>
293
+ </div>
294
+ </div>
295
+ </div>
296
+ {/* 远侧手臂:在桌面深处,和近侧手错开节奏。 */}
297
+ <div className="dk3-arm3 dk3-far">
298
+ <Box3 w={4} h={11} d={4} cls="dk3-hood dk3-uarm" x={2} y={6} />
299
+ <div className="dk3-elbow">
300
+ <Box3 w={10} h={3.5} d={3.5} cls="dk3-hood dk3-farm" x={5.5} y={2} />
301
+ <div className="dk3-wrist3">
302
+ <Box3 w={3.5} h={3} d={3} cls="dk3-skin dk3-hand" x={2} y={2} />
303
+ <span className="dk3-fingers3"><i /><i /></span>
304
+ </div>
305
+ </div>
306
+ </div>
307
+ </div>
308
+ </div>
309
+ </div>
310
+ {/* 思考泡泡(贴镜头的 2D 层,think 阶段显示) */}
311
+ <span className="dkan-bubble"><i /><i /><i /></span>
312
+ </div>
313
+ );
314
+ }
315
+
316
+ // ---------- 提示音(WebAudio 合成;macOS/Windows 通用,无音频文件) ----------
317
+ // 音效库:每种含完成音(done)、异常音(err)与确认音(ask)三套音符序列。
318
+ // 音符 [频率, 起始秒, 时长秒, 波形?],波形缺省 sine。确认音双音上扬、偏长,
319
+ // 听感是"来人搭话",和完成音的收束感、异常音的下坠感区分开。
320
+ const SOUND_LIBRARY = {
321
+ chime: { name: "清脆双音", notes: { done: [[659.25, 0, .14], [880, .13, .24]], err: [[220, 0, .16], [164.81, .15, .3]], ask: [[587.33, 0, .16], [880, .19, .34]] } },
322
+ ding: { name: "叮", notes: { done: [[987.77, 0, .35]], err: [[246.94, 0, .4]], ask: [[783.99, 0, .18], [1174.66, .2, .42]] } },
323
+ coin: { name: "金币", notes: { done: [[988, 0, .08], [1319, .08, .35], [988, 0, .08, "square"], [1319, .08, .3, "square"]], err: [[196, 0, .12], [147, .11, .35]], ask: [[880, 0, .09], [1108.73, .1, .12], [1318.51, .22, .3]] } },
324
+ bell: { name: "钟声", notes: { done: [[523.25, 0, .5], [659.25, .02, .45], [783.99, .04, .4]], err: [[174.61, 0, .5], [130.81, .05, .5]], ask: [[659.25, 0, .4], [523.25, .05, .55]] } },
325
+ pulse: { name: "脉冲", notes: { done: [[440, 0, .09], [440, .14, .09], [440, .28, .16]], err: [[174.61, 0, .1], [174.61, .14, .1], [174.61, .28, .18]], ask: [[523.25, 0, .09], [523.25, .15, .09], [659.25, .3, .22]] } },
326
+ arp: { name: "琶音", notes: { done: [[523.25, 0, .12], [659.25, .09, .12], [783.99, .18, .12], [1046.5, .27, .3]], err: [[392, 0, .12], [329.63, .1, .12], [261.63, .2, .12], [196, .3, .32]], ask: [[440, 0, .11], [554.37, .12, .11], [659.25, .24, .11], [880, .36, .34]] } },
327
+ };
328
+ // AudioContext 按需创建(浏览器自动播放策略:首次用户交互后才能出声,静默失败不报错)
329
+ let soundCtx = null;
330
+ function playTone(seq) {
331
+ try {
332
+ if (typeof window === "undefined" || !window.AudioContext && !window.webkitAudioContext) return;
333
+ const AC = window.AudioContext || window.webkitAudioContext;
334
+ if (!soundCtx) soundCtx = new AC();
335
+ if (soundCtx.state === "suspended") { soundCtx.resume().catch(() => {}); }
336
+ const t0 = soundCtx.currentTime;
337
+ for (const [f, at, dur, wave] of seq) {
338
+ const osc = soundCtx.createOscillator();
339
+ const gain = soundCtx.createGain();
340
+ osc.type = wave || "sine";
341
+ osc.frequency.value = f;
342
+ gain.gain.setValueAtTime(0, t0 + at);
343
+ gain.gain.linearRampToValueAtTime(0.18, t0 + at + 0.015);
344
+ gain.gain.exponentialRampToValueAtTime(0.0001, t0 + at + dur);
345
+ osc.connect(gain).connect(soundCtx.destination);
346
+ osc.start(t0 + at);
347
+ osc.stop(t0 + at + dur + 0.05);
348
+ }
349
+ } catch { /* 音频不可用静默 */ }
350
+ }
351
+ // 按音效播放:完成音 / 异常音 / 确认音(未知音效回退 chime)
352
+ function playDoneSound(success, effect) {
353
+ const lib = SOUND_LIBRARY[effect] || SOUND_LIBRARY.chime;
354
+ playTone(success ? lib.notes.done : lib.notes.err);
355
+ }
356
+ function playAskSound(effect) {
357
+ const lib = SOUND_LIBRARY[effect] || SOUND_LIBRARY.chime;
358
+ playTone(lib.notes.ask || lib.notes.done);
359
+ }
360
+ // 试听:完成音 + 异常音 连播(间隔 0.55s)
361
+ function previewSound(effect) {
362
+ const lib = SOUND_LIBRARY[effect] || SOUND_LIBRARY.chime;
363
+ playTone(lib.notes.done);
364
+ const delayed = lib.notes.err.map(([f, at, dur, wave]) => [f, at + 0.55, dur, wave]);
365
+ playTone(delayed);
366
+ }
367
+
368
+
369
+ function Toast(props) {
370
+ const t = props.t;
371
+ const [closing, setClosing] = useState(false);
372
+ const close = useCallback(() => { if (!closing) setClosing(true); }, [closing]);
373
+ // 停留时长(stayMs=0 常驻,仅手动关闭)
374
+ useEffect(() => {
375
+ if (!t.stayMs) return;
376
+ const timer = setTimeout(close, t.stayMs);
377
+ return () => clearTimeout(timer);
378
+ }, []);
379
+ useEffect(() => {
380
+ if (!closing) return;
381
+ const timer = setTimeout(() => props.onClose(t.id), 240);
382
+ return () => clearTimeout(timer);
383
+ }, [closing]);
384
+ const markColor = t.kind === "success"
385
+ ? "var(--dk-ok)"
386
+ : t.kind === "error"
387
+ ? "var(--dk-err)"
388
+ : t.kind === "confirm"
389
+ ? "var(--dk-warn)"
390
+ : "var(--dk-warn)";
391
+ return (
392
+ <div className={"dkan-toast" + (t.kind === "confirm" ? " dkan-toast-confirm" : "") + (closing ? " out" : "")}>
393
+ <div className="dkan-toast-head">
394
+ <span className="dkan-toast-mark" style={{ background: markColor }} />
395
+ <span className="dkan-toast-title" title={t.title}>{t.kind === "confirm" ? "✋ " : ""}{t.title}</span>
396
+ <button type="button" className="dkan-toast-close" aria-label="关闭" onClick={close}>✕</button>
397
+ </div>
398
+ {t.body ? <div className="dkan-toast-body">{t.body}</div> : null}
399
+ </div>
400
+ );
401
+ }
402
+
403
+ // ---------- 交互反馈层:对用户操作即时回应(纸飞机/火花/流光/微光/光涌/彩带) ----------
404
+ // 事件来源:document 点击监听(发送/新会话/切会话/切工作目录)+ 任务状态变化(开始/完成)。
405
+ // 每个 burst = { id, type, x, y, bits },bits 为预生成的随机粒子参数(内联样式驱动 CSS 动画)。
406
+ function makeBits(n, fn) {
407
+ const arr = [];
408
+ for (let i = 0; i < n; i++) arr.push(fn(i));
409
+ return arr;
410
+ }
411
+ function BurstLayer(props) {
412
+ const bursts = props.bursts || [];
413
+ return (
414
+ <div className="dkan-fxwrap" aria-hidden="true">
415
+ {bursts.map((b) => {
416
+ if (b.type === "plane") {
417
+ return <span key={b.id} className="dkan-fx dkan-fx-plane" style={{ left: b.x, top: b.y }}>➤</span>;
418
+ }
419
+ if (b.type === "spark") {
420
+ return (
421
+ <span key={b.id} className="dkan-fx dkan-fx-spark" style={{ left: b.x, top: b.y }}>
422
+ {b.bits.map((p, i) => <i key={i} style={{ "--dx": p.dx + "px", "--dy": p.dy + "px", background: p.c, boxShadow: "0 0 6px " + p.c }} />)}
423
+ </span>
424
+ );
425
+ }
426
+ if (b.type === "streak") return <span key={b.id} className="dkan-fx dkan-fx-streak" />;
427
+ if (b.type === "flash") return <span key={b.id} className="dkan-fx dkan-fx-flash" />;
428
+ if (b.type === "surge") return <span key={b.id} className="dkan-fx dkan-fx-surge" />;
429
+ if (b.type === "confetti") {
430
+ return (
431
+ <span key={b.id} className="dkan-fx dkan-fx-confetti">
432
+ {b.bits.map((p, i) => <i key={i} style={{ left: p.l + "%", "--dx": p.dx + "px", "--r": p.r + "deg", background: p.c, boxShadow: "0 0 4px " + p.c, animationDelay: p.d + "s" }} />)}
433
+ </span>
434
+ );
435
+ }
436
+ return null;
437
+ })}
438
+ </div>
439
+ );
440
+ }
441
+
442
+ // ---------- 氛围动效:代码雨 / 星野 / 极光(粒子配置 useMemo 稳定,避免重渲染抖动) ----------
443
+ const MATRIX_CHARS = "01</>;{}=+*#";
444
+ function AmbientLayer(props) {
445
+ const mode = props.mode;
446
+ const speed = props.speed;
447
+ // 一个模式只创建自己需要的粒子;星际模式不再额外生成代码雨和星野的 114 个无用节点。
448
+ const particles = useMemo(() => {
449
+ if (mode === "matrix") return { matrix: makeBits(34, () => ({
450
+ l: Math.random() * 100, d: 4 + Math.random() * 6, delay: -Math.random() * 8, o: 0.28 + Math.random() * 0.34, s: 11 + Math.round(Math.random() * 4),
451
+ c: MATRIX_CHARS[Math.floor(Math.random() * MATRIX_CHARS.length)] + MATRIX_CHARS[Math.floor(Math.random() * MATRIX_CHARS.length)] + MATRIX_CHARS[Math.floor(Math.random() * MATRIX_CHARS.length)],
452
+ })) };
453
+ if (mode === "stars") return { stars: makeBits(80, () => ({
454
+ l: Math.random() * 100, t: Math.random() * 100, sz: 1.5 + Math.random() * 2, d: 2 + Math.random() * 4, delay: -Math.random() * 6, dx: (Math.random() - 0.5) * 40, dy: (Math.random() - 0.5) * 24,
455
+ })) };
456
+ if (mode === "space") return { dust: makeBits(22, () => ({
457
+ l: Math.random() * 100, t: Math.random() * 82, sz: 1 + Math.random() * 2.2, d: 2.4 + Math.random() * 3.8, delay: -Math.random() * 6,
458
+ })) };
459
+ if (mode === "warp") return { warp: makeBits(28, () => ({
460
+ a: Math.random() * 360, len: 54 + Math.random() * 130, d: 1.8 + Math.random() * 2.4, delay: -Math.random() * 4,
461
+ })) };
462
+ if (mode === "fireflies") return { fireflies: makeBits(28, () => ({
463
+ l: 4 + Math.random() * 92, t: 8 + Math.random() * 78, sz: 3 + Math.random() * 5, dx: -42 + Math.random() * 84, dy: -32 + Math.random() * 64, d: 4.8 + Math.random() * 5.4, delay: -Math.random() * 8,
464
+ })) };
465
+ if (mode === "ocean") return { bubbles: makeBits(18, () => ({
466
+ l: 3 + Math.random() * 94, sz: 4 + Math.random() * 12, d: 5 + Math.random() * 6, delay: -Math.random() * 9, drift: -34 + Math.random() * 68,
467
+ })) };
468
+ if (mode === "lantern") return { lanterns: makeBits(18, () => ({
469
+ l: 4 + Math.random() * 92, sz: 8 + Math.random() * 9, d: 7 + Math.random() * 7, delay: -Math.random() * 12, drift: -48 + Math.random() * 96,
470
+ })) };
471
+ return {};
472
+ }, [mode]);
473
+ const ambientStyle = { "--dkan-speed": speed, "--dkan-phase": phaseColor(props.phase) };
474
+ if (mode === "matrix") {
475
+ return (
476
+ <div className="dkan-amb dkan-matrix" style={{ "--dkan-speed": speed }} aria-hidden="true">
477
+ {particles.matrix.map((p, i) => (
478
+ <span key={i} style={{ left: p.l + "%", animationDuration: "calc(" + p.d + "s / var(--dkan-speed,1))", animationDelay: p.delay + "s", opacity: p.o, fontSize: p.s }}>{p.c}</span>
479
+ ))}
480
+ </div>
481
+ );
482
+ }
483
+ if (mode === "stars") {
484
+ return (
485
+ <div className="dkan-amb dkan-stars" style={{ "--dkan-speed": speed }} aria-hidden="true">
486
+ {particles.stars.map((p, i) => (
487
+ <i key={i} className="dkan-star" style={{ left: p.l + "%", top: p.t + "%", width: p.sz, height: p.sz, animationDuration: "calc(" + p.d + "s / var(--dkan-speed,1))", animationDelay: p.delay + "s", "--dx": p.dx + "px", "--dy": p.dy + "px" }} />
488
+ ))}
489
+ <span className="dkan-moon" />
490
+ <i className="dkan-meteor" style={{ top: "12%", left: "72%", animationDelay: "0s" }} />
491
+ <i className="dkan-meteor" style={{ top: "28%", left: "88%", animationDelay: "2.8s" }} />
492
+ <i className="dkan-meteor" style={{ top: "6%", left: "42%", animationDelay: "5.6s" }} />
493
+ </div>
494
+ );
495
+ }
496
+ if (mode === "aurora") {
497
+ return (
498
+ <div className="dkan-amb dkan-aurora" style={{ "--dkan-speed": speed }} aria-hidden="true">
499
+ <i /><i /><i />
500
+ </div>
501
+ );
502
+ }
503
+ if (mode === "nebula") {
504
+ return <div className="dkan-amb dkan-nebula" style={ambientStyle} aria-hidden="true"><i /><i /><i /><i /></div>;
505
+ }
506
+ if (mode === "warp") {
507
+ return <div className="dkan-amb dkan-warp" style={ambientStyle} aria-hidden="true">
508
+ {particles.warp.map((p, i) => <i key={i} style={{ "--a": p.a + "deg", "--len": p.len + "px", animationDuration: "calc(" + p.d + "s / var(--dkan-speed,1))", animationDelay: p.delay + "s" }} />)}
509
+ </div>;
510
+ }
511
+ if (mode === "radar") {
512
+ return <div className="dkan-amb dkan-radar" style={ambientStyle} aria-hidden="true">
513
+ {["a", "b", "c"].map((id) => <span className={"dkan-radar-station " + id} key={id}><i /><b /></span>)}
514
+ </div>;
515
+ }
516
+ if (mode === "constellation") {
517
+ return <div className="dkan-amb dkan-constellation" style={ambientStyle} aria-hidden="true">
518
+ <svg viewBox="0 0 100 100" preserveAspectRatio="none">
519
+ <path pathLength="1" d="M6 68 L18 30 L31 48 L45 18 L58 42 L73 25 L91 58 L78 78 L58 42 L31 48 L18 76 L6 68" />
520
+ {[[6,68],[18,30],[31,48],[45,18],[58,42],[73,25],[91,58],[78,78],[18,76]].map(([cx, cy], i) => <circle key={i} cx={cx} cy={cy} r={i % 3 === 0 ? 1.1 : .7} style={{ animationDelay: (-i * .24) + "s" }} />)}
521
+ </svg>
522
+ </div>;
523
+ }
524
+ if (mode === "fireflies") {
525
+ return <div className="dkan-amb dkan-fireflies" style={ambientStyle} aria-hidden="true">
526
+ {particles.fireflies.map((p, i) => <i key={i} style={{ left: p.l + "%", top: p.t + "%", width: p.sz, height: p.sz, "--dx": p.dx + "px", "--dy": p.dy + "px", animationDuration: "calc(" + p.d + "s / var(--dkan-speed,1))", animationDelay: p.delay + "s" }} />)}
527
+ </div>;
528
+ }
529
+ if (mode === "ocean") {
530
+ return <div className="dkan-amb dkan-ocean" style={ambientStyle} aria-hidden="true">
531
+ <span className="dkan-ocean-waves">{Array.from({ length: 5 }, (_, i) => <i key={i} />)}</span>
532
+ {particles.bubbles.map((p, i) => <b key={i} style={{ left: p.l + "%", width: p.sz, height: p.sz, "--drift": p.drift + "px", animationDuration: "calc(" + p.d + "s / var(--dkan-speed,1))", animationDelay: p.delay + "s" }} />)}
533
+ </div>;
534
+ }
535
+ if (mode === "prism") {
536
+ return <div className="dkan-amb dkan-prism" style={ambientStyle} aria-hidden="true">{Array.from({ length: 7 }, (_, i) => <i key={i} />)}</div>;
537
+ }
538
+ if (mode === "circuit") {
539
+ return <div className="dkan-amb dkan-circuit" style={ambientStyle} aria-hidden="true">
540
+ <svg viewBox="0 0 1000 600" preserveAspectRatio="none">
541
+ {["M0 92 H180 V210 H390 V128 H610 V260 H820 V104 H1000", "M0 486 H144 V358 H334 V470 H520 V338 H748 V448 H1000", "M82 0 V126 H264 V286 H472 V196 H694 V320 H914 V600", "M936 0 V148 H776 V278 H566 V410 H350 V548 H112 V600"].map((d, i) => <path key={i} pathLength="1" d={d} style={{ animationDelay: (-i * .7) + "s" }} />)}
542
+ {[[180,210],[390,128],[610,260],[144,358],[520,338],[264,286],[694,320],[566,410]].map(([cx, cy], i) => <circle key={i} cx={cx} cy={cy} r="5" />)}
543
+ </svg>
544
+ </div>;
545
+ }
546
+ if (mode === "gravity") {
547
+ return <div className="dkan-amb dkan-gravity" style={ambientStyle} aria-hidden="true">
548
+ {["a", "b", "c"].map((id) => <span className={"dkan-gravity-source " + id} key={id}><b />{Array.from({ length: 4 }, (_, i) => <i key={i} style={{ animationDelay: "calc(" + (-i * .9) + "s / var(--dkan-speed,1))" }} />)}</span>)}
549
+ </div>;
550
+ }
551
+ if (mode === "lantern") {
552
+ return <div className="dkan-amb dkan-lantern" style={ambientStyle} aria-hidden="true">
553
+ {particles.lanterns.map((p, i) => <i key={i} style={{ left: p.l + "%", width: p.sz, height: p.sz * 1.28, "--drift": p.drift + "px", animationDuration: "calc(" + p.d + "s / var(--dkan-speed,1))", animationDelay: p.delay + "s" }} />)}
554
+ </div>;
555
+ }
556
+ if (mode === "space") {
557
+ const taskCount = Math.max(1, Math.min(3, Array.isArray(props.tasks) ? props.tasks.length : 1));
558
+ // 任务期间每条航线至少有两艘巡航船,三座星系之间始终有可见往返;并发任务越多,舰队越密。
559
+ // 所有飞船仍只使用 transform/opacity 合成动画,速度跟随任务活动速率。
560
+ const runnerCount = Math.max(6, Math.min(9, taskCount * 3));
561
+ const galaxies = props.galaxies;
562
+ if (!galaxies) return null;
563
+ return (
564
+ <div className="dkan-amb dkan-space" style={{
565
+ "--dkan-speed": speed, "--dkan-space-ships": taskCount,
566
+ "--dkan-alpha-x": galaxies.alpha.cx + "px", "--dkan-alpha-y": galaxies.alpha.cy + "px",
567
+ "--dkan-beta-x": galaxies.beta.cx + "px", "--dkan-beta-y": galaxies.beta.cy + "px",
568
+ "--dkan-gamma-x": galaxies.gamma.cx + "px", "--dkan-gamma-y": galaxies.gamma.cy + "px",
569
+ }} aria-hidden="true">
570
+ {particles.dust.map((p, i) => <i key={i} className="dkan-space-dust" style={{ left: p.l + "%", top: p.t + "%", width: p.sz, height: p.sz, animationDuration: "calc(" + p.d + "s / var(--dkan-speed,1))", animationDelay: p.delay + "s" }} />)}
571
+ {["alpha", "beta", "gamma"].map((galaxy) => <span className={"dkan-space-system " + galaxy} key={galaxy} style={{ left: galaxies[galaxy].x, top: galaxies[galaxy].y, "--dkan-system-scale": galaxies[galaxy].scale }}>
572
+ <i className="dkan-space-sun" />
573
+ <span className="dkan-space-orbit a"><i /></span>
574
+ <span className="dkan-space-orbit b"><i /></span>
575
+ <span className="dkan-space-orbit c"><i /></span>
576
+ </span>)}
577
+ {Array.from({ length: runnerCount }, (_, i) => <span className={"dkan-space-runner route-" + (i % 3)} key={i} style={{ "--dkan-run-delay": (-i * 1.8) + "s" }}><FreighterHull units={1} empty /></span>)}
578
+ </div>
579
+ );
580
+ }
581
+ return null;
582
+ }
583
+
584
+ // ---------- 完成货运舰:从任务航线归航到会话输入框上方,货舱量使用真实输出 Token ----------
585
+ function outputCargo(outputTokens) {
586
+ const tokens = Math.max(0, Number(outputTokens) || 0);
587
+ // 每个可见货舱约代表 900 输出 Token;保留 1 个基础舱,防止无 usage 时舰体塌陷。
588
+ return { tokens, units: Math.max(1, Math.min(8, Math.ceil(tokens / 900))) };
589
+ }
590
+ function fallbackComposerDockPoint(side = "right") {
591
+ const vw = typeof window === "undefined" ? 1280 : window.innerWidth;
592
+ const vh = typeof window === "undefined" ? 900 : window.innerHeight;
593
+ return {
594
+ // 待命位贴近输入框右上;右侧远端只保留给旧的通用停靠计算。
595
+ x: side === "left" ? Math.max(304, Math.min(520, Math.round(vw * .18)))
596
+ : side === "ready" ? Math.max(316, vw - 640)
597
+ : Math.max(16, vw - 232),
598
+ y: Math.max(32, vh - 218),
599
+ };
600
+ }
601
+ function visibleRect(el) {
602
+ if (!el || typeof window === "undefined") return null;
603
+ const r = el.getBoundingClientRect();
604
+ return r.width > 0 && r.height > 0 && r.bottom > 0 && r.top < window.innerHeight ? r : null;
605
+ }
606
+ // 当前正在看的会话标题(侧栏高亮行)。dsh 会话树行 = role="treeitem" + aria-selected,
607
+ // 行文本即会话标题;据此把"需确认/卡住"这类会话级卡片限定到当前会话,别的会话等待时不打扰。
608
+ function currentSessionTitle() {
609
+ if (typeof document === "undefined") return "";
610
+ for (const el of document.querySelectorAll('[role="treeitem"][aria-selected="true"]')) {
611
+ const r = el.getBoundingClientRect();
612
+ // 侧栏在左:跳过正文区可能出现的同名高亮节点(右上搜索结果等)
613
+ if (r.width > 0 && r.height > 0 && r.right <= window.innerWidth * .35) {
614
+ const text = (el.textContent || "").trim();
615
+ if (text) return text;
616
+ }
617
+ }
618
+ return "";
619
+ }
620
+ // 指定任务是否属于当前正在看的会话(按标题对齐;侧栏不可见时退回"是"避免整层失效)
621
+ function taskOnScreen(task, titleCache) {
622
+ const title = task && (task.title || "");
623
+ if (!title) return true;
624
+ const current = titleCache !== undefined ? titleCache : currentSessionTitle();
625
+ if (!current) return true;
626
+ return current === title || current.indexOf(title) !== -1 || title.indexOf(current) !== -1;
627
+ }
628
+ const COMPOSER_SEND_SELECTOR = 'button[aria-label="Send message"],button[aria-label*="发送"],button[data-testid*="send"],button[data-testid*="submit"],button[type="submit"]';
629
+ function conversationComposer() {
630
+ if (typeof document === "undefined" || typeof window === "undefined") return null;
631
+ // 发送键只会位于真实会话输入卡内。由它逐层向上找卡片,比遍历历史消息的编辑节点可靠。
632
+ const sendButtons = document.querySelectorAll(COMPOSER_SEND_SELECTOR);
633
+ for (const button of sendButtons) {
634
+ let el = button.parentElement;
635
+ for (let depth = 0; el && depth < 9; depth += 1, el = el.parentElement) {
636
+ const r = visibleRect(el);
637
+ if (r && r.width >= 360 && r.height >= 72 && r.height <= 360 && r.top > window.innerHeight * .42) return { element: el, rect: r };
638
+ }
639
+ }
640
+ return null;
641
+ }
642
+ function conversationComposerRect() {
643
+ const composer = conversationComposer();
644
+ return composer ? composer.rect : null;
645
+ }
646
+ function composerPointFromRect(composer, side) {
647
+ return {
648
+ x: side === "ready" ? Math.max(composer.left + 16, composer.right - 210) : fallbackComposerDockPoint(side).x,
649
+ y: Math.max(22, composer.top - 92),
650
+ };
651
+ }
652
+ function composerDockPoint(side = "right") {
653
+ if (typeof document === "undefined" || typeof window === "undefined") return fallbackComposerDockPoint(side);
654
+ const composer = conversationComposerRect();
655
+ if (composer) return composerPointFromRect(composer, side);
656
+ const selectors = [
657
+ '[data-testid*="composer"]', '[data-testid*="conversation-input"]', '[data-slot*="conversation.input"]',
658
+ 'textarea[placeholder*="Message"]', 'textarea[placeholder*="消息"]', 'textarea',
659
+ '[contenteditable="true"][data-testid*="composer"]', '[contenteditable="true"][aria-label*="消息"]', '[contenteditable="true"][data-slot*="conversation.input"]',
660
+ ];
661
+ const seen = new Set();
662
+ const candidates = [];
663
+ for (const selector of selectors) {
664
+ for (const el of document.querySelectorAll(selector)) {
665
+ if (seen.has(el)) continue;
666
+ seen.add(el);
667
+ const r = el.getBoundingClientRect();
668
+ if (r.width > 80 && r.height > 16 && r.bottom > window.innerHeight * .48) candidates.push({ el, r });
669
+ }
670
+ }
671
+ const best = candidates.sort((a, b) => b.r.bottom - a.r.bottom)[0];
672
+ if (!best) return fallbackComposerDockPoint(side);
673
+ const r = best.r;
674
+ // 候补输入节点没有卡片边界时,仅待命位贴近其右侧;其他位置仍由视口锚点决定。
675
+ const x = side === "ready" ? Math.max(r.left + 16, r.right - 210) : fallbackComposerDockPoint(side).x;
676
+ return {
677
+ // 纵向以真实输入控件上沿停靠,飞船底部和输入框保留间隙。
678
+ x,
679
+ y: Math.max(22, r.top - 92),
680
+ };
681
+ }
682
+ // 刷新时先让会话本身完成首帧绘制,再在空闲期读取输入框布局。
683
+ // 避免星际模式恢复时 querySelectorAll + getBoundingClientRect 抢占主线程。
684
+ function useSpaceDocks(enabled) {
685
+ const [docks, setDocks] = useState(null);
686
+ useEffect(() => {
687
+ if (!enabled || typeof window === "undefined") { setDocks(null); return undefined; }
688
+ let disposed = false;
689
+ let frame = 0;
690
+ let idle = 0;
691
+ let timer = 0;
692
+ let retries = 0;
693
+ let pending = false;
694
+ let observedComposer = null;
695
+ let resizeObserver = null;
696
+ const samePoint = (a, b) => !!a && !!b && Math.abs(a.x - b.x) < 1 && Math.abs(a.y - b.y) < 1;
697
+ const bindComposerResize = (element) => {
698
+ if (!resizeObserver || observedComposer === element) return;
699
+ if (observedComposer) resizeObserver.unobserve(observedComposer);
700
+ observedComposer = element;
701
+ if (observedComposer) resizeObserver.observe(observedComposer);
702
+ };
703
+ const measure = () => {
704
+ if (disposed) return;
705
+ const found = conversationComposer();
706
+ // 历史会话切换时输入卡可能比浮层更晚挂载;没有真实卡片就不显示,绝不猜到正文中。
707
+ if (!found) {
708
+ bindComposerResize(null);
709
+ setDocks(null);
710
+ if (retries++ < 20) schedule(140);
711
+ return;
712
+ }
713
+ retries = 0;
714
+ bindComposerResize(found.element);
715
+ const composer = found.rect;
716
+ const right = composerPointFromRect(composer, "right");
717
+ // 三个停泊位共用同一输入框上沿,避免重复扫描整个页面读取布局。
718
+ const next = {
719
+ right,
720
+ left: composerPointFromRect(composer, "left"),
721
+ ready: composerPointFromRect(composer, "ready"),
722
+ };
723
+ setDocks((current) => current && samePoint(current.right, next.right) && samePoint(current.left, next.left) && samePoint(current.ready, next.ready) ? current : next);
724
+ };
725
+ function schedule(delay = 0, deferUntilIdle = false) {
726
+ if (disposed || pending) return;
727
+ pending = true;
728
+ const queueFrame = () => {
729
+ frame = window.requestAnimationFrame(() => {
730
+ pending = false;
731
+ measure();
732
+ });
733
+ };
734
+ if (delay > 0) timer = window.setTimeout(queueFrame, delay);
735
+ else if (deferUntilIdle && typeof window.requestIdleCallback === "function") idle = window.requestIdleCallback(queueFrame, { timeout: 350 });
736
+ else queueFrame();
737
+ };
738
+ if (typeof window.ResizeObserver === "function") resizeObserver = new window.ResizeObserver(() => schedule());
739
+ // 会话是 SPA 切换:输入框节点会被整体替换,但 effect 本身不会重新挂载。仅在包含发送键的子树增删时重测。
740
+ const mutationObserver = typeof window.MutationObserver === "function" ? new window.MutationObserver((mutations) => {
741
+ const hasComposerChange = mutations.some((mutation) => Array.from(mutation.addedNodes).concat(Array.from(mutation.removedNodes)).some((node) => {
742
+ if (!node || node.nodeType !== 1) return false;
743
+ return (node.matches && node.matches(COMPOSER_SEND_SELECTOR)) || (node.querySelector && node.querySelector(COMPOSER_SEND_SELECTOR));
744
+ }));
745
+ if (hasComposerChange) schedule(40);
746
+ }) : null;
747
+ if (mutationObserver && document.body) mutationObserver.observe(document.body, { childList: true, subtree: true });
748
+ schedule(0, true);
749
+ // 部分会话会复用同一输入框节点、只改变祖先布局;低频校准覆盖这种无 DOM 替换的位移。
750
+ // 校准只在任务活动时才值得跑:空闲期每 700ms 一次全页 querySelectorAll + getBoundingClientRect
751
+ // 是持续掉帧的来源(每次都强制布局)。空闲 20s 后降频到 5s,任务恢复立即回到快节奏。
752
+ let calibrationInterval = 700;
753
+ let lastActivityAt = Date.now();
754
+ const markActivity = () => { lastActivityAt = Date.now(); if (calibrationInterval !== 700) { calibrationInterval = 700; restartPoll(); } };
755
+ const unsubscribeActivity = animationStore.subscribe(markActivity);
756
+ let pollTimer = 0;
757
+ function restartPoll() {
758
+ if (pollTimer) window.clearInterval(pollTimer);
759
+ pollTimer = window.setInterval(() => {
760
+ if (Date.now() - lastActivityAt > 20000 && calibrationInterval !== 5000) calibrationInterval = 5000;
761
+ schedule();
762
+ }, calibrationInterval);
763
+ }
764
+ restartPoll();
765
+ const onResize = () => {
766
+ if (idle && typeof window.cancelIdleCallback === "function") window.cancelIdleCallback(idle);
767
+ if (timer) window.clearTimeout(timer);
768
+ pending = false;
769
+ schedule();
770
+ };
771
+ window.addEventListener("resize", onResize, { passive: true });
772
+ return () => {
773
+ disposed = true;
774
+ window.cancelAnimationFrame(frame);
775
+ if (idle && typeof window.cancelIdleCallback === "function") window.cancelIdleCallback(idle);
776
+ if (timer) window.clearTimeout(timer);
777
+ if (pollTimer) window.clearInterval(pollTimer);
778
+ if (mutationObserver) mutationObserver.disconnect();
779
+ if (resizeObserver) resizeObserver.disconnect();
780
+ unsubscribeActivity();
781
+ window.removeEventListener("resize", onResize);
782
+ };
783
+ }, [enabled]);
784
+ return docks;
785
+ }
786
+ // 三个星系分处内容区右上、左上、左下,路线有足够的飞行距离且避开侧栏和输入框。
787
+ // 返回中心点还会被巡航线路和归航飞船共用,避免“从空气里飞回来”。
788
+ function spaceGalaxyLayout() {
789
+ const vw = typeof window === "undefined" ? 1280 : window.innerWidth;
790
+ const vh = typeof window === "undefined" ? 900 : window.innerHeight;
791
+ const contentLeft = 316;
792
+ const make = (x, y, scale) => ({ x, y, scale, cx: x + 123, cy: y + 123 });
793
+ return {
794
+ alpha: make(Math.max(contentLeft + 420, vw - 360), 72, .50),
795
+ beta: make(contentLeft, Math.max(142, Math.min(232, vh * .18)), .48),
796
+ gamma: make(contentLeft + 18, Math.max(420, vh - 382), .48),
797
+ };
798
+ }
799
+ function freighterAnchorAt(point) {
800
+ // 飞船外框为 210×88,飞船主体中心约在 (88, 32),对齐到星系中心而不是左上角。
801
+ return { x: Math.round(point.x - 88), y: Math.round(point.y - 32) };
802
+ }
803
+ function flightAngle(from, to) {
804
+ return Math.round(Math.atan2(to.y - from.y, to.x - from.x) * 180 / Math.PI);
805
+ }
806
+ function FreighterHull(props) {
807
+ const units = Math.max(1, Number(props.units) || 1);
808
+ return <span className="dkan-freighter">
809
+ <i className="dkan-freighter-engine"><i /><i /></i>
810
+ <i className="dkan-freighter-wing left" /><i className="dkan-freighter-wing right" /><i className="dkan-freighter-fin" />
811
+ <i className="dkan-freighter-hull" /><i className="dkan-freighter-canopy" /><i className="dkan-freighter-nose" />
812
+ <span className={"dkan-freighter-cargo" + (props.empty ? " empty" : "")}>{Array.from({ length: units }, (_, i) => <i key={i} />)}</span>
813
+ </span>;
814
+ }
815
+ function DeliveryFreighter(props) {
816
+ const d = props.delivery;
817
+ return (
818
+ <div className="dkan-delivery" style={{
819
+ "--dkan-from-x": d.from.x + "px", "--dkan-from-y": d.from.y + "px",
820
+ "--dkan-to-x": d.to.x + "px", "--dkan-to-y": d.to.y + "px",
821
+ "--dkan-delivery-duration": d.duration + "ms",
822
+ }} aria-hidden="true">
823
+ <span className="dkan-delivery-trail" />
824
+ <FreighterHull units={d.units} />
825
+ <span className="dkan-delivery-label"><b>归航货运舰</b><span>输出 {fmtCompact(d.tokens)} Tokens · {d.units} 舱</span></span>
826
+ </div>
827
+ );
828
+ }
829
+ function ReadyFreighter(props) {
830
+ const dock = props.dock || fallbackComposerDockPoint("ready");
831
+ return <div className="dkan-ready-freighter" style={{ left: dock.x, top: dock.y }} aria-hidden="true"><FreighterHull units={3} empty /><span>待命 · 等待下一条任务</span></div>;
832
+ }
833
+ function MissionDeparture(props) {
834
+ const d = props.departure;
835
+ return <div className="dkan-departure" style={{
836
+ "--dkan-from-x": d.from.x + "px", "--dkan-from-y": d.from.y + "px",
837
+ "--dkan-to-x": d.to.x + "px", "--dkan-to-y": d.to.y + "px",
838
+ "--dkan-flight-angle": d.angle + "deg", "--dkan-departure-duration": d.duration + "ms",
839
+ }} aria-hidden="true"><FreighterHull units={3} empty /></div>;
840
+ }
841
+ function ProgrammingStuck(props) {
842
+ const waiting = !!props.waiting;
843
+ return <div className={"dkan-space-stuck" + (waiting ? " waiting" : "")} aria-hidden="true"><FreighterHull units={3} empty /><span className="dkan-stuck-code"><i />{`{ }`}</span><strong>{waiting ? "等待确认" : "编程等待中"}</strong><small>{truncate((props.task && props.task.title) || "工具暂无新响应", 28)}</small></div>;
844
+ }
845
+
846
+ // ---------- 全局浮层:轮询 + 动效 + 通知(功能启用即常驻) ----------
847
+ export function AnimationOverlay(props) {
848
+ const ctx = props && props.ctx;
849
+ const snap = useAnimation();
850
+ const [toasts, setToasts] = useState([]);
851
+ const [flourish, setFlourish] = useState(null); // { key, err } 完成瞬间的一次性流光
852
+ const [bursts, setBursts] = useState([]); // 交互反馈动画队列
853
+ const [deliveries, setDeliveries] = useState([]); // 星际模式完成时停泊在输入框上方的归航货运舰
854
+ const [departures, setDepartures] = useState([]); // 从输入框起飞的出航舰
855
+ const prevActiveRef = useRef(null);
856
+ const burstIdRef = useRef(0);
857
+ const deliveryIdRef = useRef(0);
858
+ const departureTimersRef = useRef(new Map());
859
+ const lastImmediateDepartureRef = useRef(0);
860
+
861
+ useEffect(() => () => {
862
+ for (const timer of departureTimersRef.current.values()) clearTimeout(timer);
863
+ departureTimersRef.current.clear();
864
+ }, []);
865
+
866
+ // 触发一个交互反馈动画(自动清理)
867
+ const pushBurst = useCallback((type, x, y, bits) => {
868
+ const id = ++burstIdRef.current;
869
+ setBursts((prev) => prev.concat([{ id, type, x, y, bits }]).slice(-6));
870
+ setTimeout(() => setBursts((prev) => prev.filter((b) => b.id !== id)), 2600);
871
+ }, []);
872
+ // 发送即出发:从左侧归航位先缓慢滑行,再逐段加速,最终并入右上角星系。
873
+ const launchMission = useCallback(() => {
874
+ const cfg = animationStore.snap.status && animationStore.snap.status.config;
875
+ if (!cfg || !cfg.animationEnabled || cfg.effectMode !== "space") return false;
876
+ const id = ++deliveryIdRef.current;
877
+ const from = composerDockPoint("left");
878
+ const targetGalaxy = spaceGalaxyLayout().alpha;
879
+ const to = freighterAnchorAt({ x: targetGalaxy.cx, y: targetGalaxy.cy });
880
+ const departure = {
881
+ id, from, to,
882
+ angle: flightAngle(from, to),
883
+ duration: 8600,
884
+ };
885
+ lastImmediateDepartureRef.current = Date.now();
886
+ setDeliveries([]); // 满载归港舰重新出航前离开停泊位
887
+ setDepartures((prev) => prev.concat([departure]).slice(-3));
888
+ departureTimersRef.current.set(id, setTimeout(() => {
889
+ departureTimersRef.current.delete(id);
890
+ setDepartures((prev) => prev.filter((item) => item.id !== id));
891
+ }, departure.duration));
892
+ return true;
893
+ }, []);
894
+
895
+ // 点击监听:识别用户操作 → 即时反馈(发送/新会话/切会话/切工作目录)
896
+ useEffect(() => {
897
+ if (typeof document === "undefined") return;
898
+ const onClick = (e) => {
899
+ const t = e.target;
900
+ if (!t || !t.closest) return;
901
+ const sendBtn = t.closest('button[aria-label="Send message"], button[aria-label*="发送"]');
902
+ const newSession = t.closest('button[aria-label*="New session"], button[aria-label*="新会话"], button[aria-label*="New Session"]');
903
+ const workspace = t.closest('button[aria-label*="Choose workspace"], button[aria-label*="工作目录"], button[aria-label*="workspace"]');
904
+ const sessionItem = t.closest('[role="treeitem"]');
905
+ if (sendBtn) {
906
+ pushBurst("plane", e.clientX, e.clientY);
907
+ launchMission();
908
+ } else if (newSession) {
909
+ pushBurst("spark", e.clientX, e.clientY, makeBits(10, () => ({
910
+ dx: (Math.random() - 0.5) * 70, dy: (Math.random() - 0.5) * 70,
911
+ c: ["#2563eb", "#0d9488", "#b45309", "#be185d"][Math.floor(Math.random() * 4)],
912
+ })));
913
+ } else if (workspace) {
914
+ pushBurst("flash");
915
+ } else if (sessionItem) {
916
+ pushBurst("streak");
917
+ }
918
+ };
919
+ document.addEventListener("click", onClick, true);
920
+ return () => document.removeEventListener("click", onClick, true);
921
+ }, [pushBurst, launchMission]);
922
+
923
+ // 任务状态变化:没有直接发送事件时,仍由状态变化兜底从左侧起飞。
924
+ const taskCountRef = useRef(0);
925
+ useEffect(() => {
926
+ const st = snap.status;
927
+ if (!st || !st.active) return;
928
+ const n = st.active.length;
929
+ if (n > taskCountRef.current) {
930
+ pushBurst("surge");
931
+ const cfg = st.config || {};
932
+ if (cfg.animationEnabled && cfg.effectMode === "space" && Date.now() - lastImmediateDepartureRef.current > 10000) launchMission();
933
+ }
934
+ taskCountRef.current = n;
935
+ }, [snap.status, pushBurst, launchMission]);
936
+
937
+ // 轮询:任务中 850ms,实时捕捉 chunk/工具活动;空闲 6s / 出错 15s;页面切回立即刷新。
938
+ useEffect(() => {
939
+ let stopped = false;
940
+ let timer = null;
941
+ const loop = async () => {
942
+ await animationStore.refresh();
943
+ if (stopped) return;
944
+ const s = animationStore.snap;
945
+ timer = setTimeout(loop, s.error ? 15000 : (s.status && s.status.active && s.status.active.length > 0 ? 850 : 6000));
946
+ };
947
+ loop();
948
+ const onVisible = () => {
949
+ if (!stopped && typeof document !== "undefined" && document.visibilityState === "visible") animationStore.refresh();
950
+ };
951
+ document.addEventListener("visibilitychange", onVisible);
952
+ return () => { stopped = true; if (timer) clearTimeout(timer); document.removeEventListener("visibilitychange", onVisible); };
953
+ }, []);
954
+
955
+ // 任务结束检测:上一轮活跃、本轮消失 → 通知 + 流光(找最近完成记录补全信息)
956
+ useEffect(() => {
957
+ const st = snap.status;
958
+ if (!st || !st.active) return;
959
+ const prev = prevActiveRef.current;
960
+ if (prev) {
961
+ const currIds = new Set(st.active.map((x) => x.sessionId));
962
+ for (const task of prev) {
963
+ if (currIds.has(task.sessionId)) continue;
964
+ const record = (st.recent || []).find((r) => r.sessionId === task.sessionId) || null;
965
+ handleTaskEnd(task, record, st.config || {});
966
+ }
967
+ }
968
+ prevActiveRef.current = st.active.slice();
969
+ }, [snap.status]);
970
+
971
+ // ===== 需确认检测:工具等待用户批准时通知(页内卡片 + 确认音 + 后台系统通知) =====
972
+ // host 从会话流 approval/asked 收集待确认项,approval/decided 后移除;
973
+ // 这里对"任务首次出现待确认"触发一次提醒,已提醒过的会话不再重复打扰。
974
+ const remindedApprovalsRef = useRef(new Map()); // sessionId -> 提醒过的 approvalId 集合
975
+ useEffect(() => {
976
+ const st = snap.status;
977
+ if (!st || !st.active) return;
978
+ const cfg = st.config || {};
979
+ const activeIds = new Set(st.active.map((x) => x.sessionId));
980
+ for (const [sid, seen] of remindedApprovalsRef.current) {
981
+ if (!activeIds.has(sid)) remindedApprovalsRef.current.delete(sid);
982
+ }
983
+ for (const task of st.active) {
984
+ const approvals = Array.isArray(task.approvals) ? task.approvals : [];
985
+ if (approvals.length === 0) continue;
986
+ let seen = remindedApprovalsRef.current.get(task.sessionId);
987
+ if (!seen) { seen = new Set(); remindedApprovalsRef.current.set(task.sessionId, seen); }
988
+ const fresh = approvals.filter((a) => a && !seen.has(a.id));
989
+ if (fresh.length === 0) continue;
990
+ for (const a of fresh) seen.add(a.id);
991
+ // 提醒开关在"收到新确认项"时检查,保证开关中途打开对下一项生效
992
+ if (!cfg.notifyEnabled || cfg.notifyOnConfirm === false) continue;
993
+ const toolName = fresh[0].toolName || "";
994
+ const scene = toolLabel(toolName);
995
+ const lines = [
996
+ "任务:" + (task.title || "(无标题)"),
997
+ "工具:" + (toolName || "未知") + (scene ? "(" + scene + ")" : ""),
998
+ ];
999
+ if (fresh[0].reason) lines.push("说明:" + truncate(fresh[0].reason, 140));
1000
+ lines.push(fresh.length > 1 ? "共 " + fresh.length + " 项等待你的确认" : "请在会话里确认后继续");
1001
+ const toast = {
1002
+ id: Date.now() + Math.random(),
1003
+ kind: "confirm",
1004
+ title: "任务需要确认",
1005
+ body: lines.join("\n"),
1006
+ // 需确认常驻到用户处理(notifyStayMs=0 同款语义),关掉开关的间隙也不会自动消失
1007
+ stayMs: 0,
1008
+ };
1009
+ setToasts((prev) => prev.concat([toast]).slice(-4));
1010
+ if (cfg.soundNotify !== false) playAskSound(cfg.soundEffect);
1011
+ if (cfg.systemNotify && typeof document !== "undefined" && document.hidden
1012
+ && typeof Notification !== "undefined" && Notification.permission === "granted") {
1013
+ try {
1014
+ new Notification("dsh 任务需要确认", {
1015
+ body: (task.title || "(无标题)") + " · " + (toolName || "工具") + " 等待确认",
1016
+ tag: "dsh-dock-approval-" + task.sessionId,
1017
+ });
1018
+ } catch { /* 系统通知失败不影响页面内通知 */ }
1019
+ }
1020
+ }
1021
+ }, [snap.status]);
1022
+
1023
+ const handleTaskEnd = (task, record, cfg) => {
1024
+ const reason = (record && record.endReason) || "";
1025
+ const success = isSuccessReason(reason);
1026
+ const info = endInfo(reason);
1027
+ // 动画侧:完成瞬间一缕流光掠过 + 成功时顶部彩带庆祝(动画关时不渲染)
1028
+ if (cfg.animationEnabled) {
1029
+ setFlourish({ key: Date.now(), err: !success });
1030
+ // 星际远征把完成结果送回用户正在输入的位置。速度继承最后一帧任务节奏;
1031
+ // 货舱数量及标签只取 Host 已记录的真实 outputTokens。
1032
+ if (cfg.effectMode === "space") {
1033
+ const cargo = outputCargo((record && record.outputTokens) || task.outputTokens);
1034
+ const id = ++deliveryIdRef.current;
1035
+ const taskSpeed = Math.max(.9, Number(speed) || 1);
1036
+ const duration = Math.round(Math.max(8000, Math.min(9400, 10000 / Math.sqrt(taskSpeed))));
1037
+ // 从三个星系中随机挑选归航起点,任务完成时才能知道本次货舰从哪里回来。
1038
+ const galaxies = spaceGalaxyLayout();
1039
+ const returnGalaxy = galaxies[["alpha", "beta", "gamma"][Math.floor(Math.random() * 3)]];
1040
+ const from = freighterAnchorAt({ x: returnGalaxy.cx, y: returnGalaxy.cy });
1041
+ const to = composerDockPoint("left");
1042
+ // 满载舰最终停入左侧归航位,和右上出发星系形成清晰的往返方向。
1043
+ const delivery = Object.assign({ id, from, to, duration }, cargo);
1044
+ setDeliveries([delivery]);
1045
+ }
1046
+ if (success) {
1047
+ pushBurst("confetti", 0, 0, makeBits(18, (i) => ({
1048
+ l: 4 + (i / 18) * 92 + Math.random() * 3,
1049
+ dx: (Math.random() - 0.5) * 60,
1050
+ r: Math.random() * 720 - 360,
1051
+ d: Math.random() * 0.35,
1052
+ c: ["#2563eb", "#0d9488", "#b45309", "#be185d", "#7c3aed"][i % 5],
1053
+ })));
1054
+ }
1055
+ }
1056
+ // 通知侧:与动画开关完全独立
1057
+ if (!cfg.notifyEnabled) return;
1058
+ const wanted = success ? cfg.notifyOnComplete : cfg.notifyOnError;
1059
+ if (!wanted) return;
1060
+ const models = record && record.models && record.models.length ? record.models.join(", ")
1061
+ : (task.models && task.models.length ? task.models.join(", ") : "未知模型");
1062
+ const provider = (record && record.provider) || task.provider || "";
1063
+ const startTs = (record && record.startTime) || task.startTime;
1064
+ const endTs = (record && record.endTime) || Date.now();
1065
+ const lines = [
1066
+ "任务:" + ((record && record.title) || task.title || "(无标题)"),
1067
+ "模型:" + models + (provider ? "(" + provider + ")" : ""),
1068
+ "耗时:" + fmtDur((record && record.duration) || (endTs - startTs))
1069
+ + "(" + fmtTime(startTs) + " → " + fmtTime(endTs) + ")",
1070
+ "回合 " + ((record && record.turns) || task.turns || 0)
1071
+ + " · 步骤 " + ((record && record.steps) || task.steps || 0)
1072
+ + (((record && record.toolCalls) || task.toolCalls) ? " · 工具 " + ((record && record.toolCalls) || task.toolCalls) + " 次" : ""),
1073
+ "Token:输入 " + fmtNum((record && record.inputTokens) || task.inputTokens)
1074
+ + " / 输出 " + fmtNum((record && record.outputTokens) || task.outputTokens),
1075
+ ];
1076
+ if (record && record.lastText) lines.push("摘要:" + truncate(record.lastText, 140));
1077
+ if (!success && record && record.errorMessage) lines.push("错误:" + truncate(record.errorMessage, 120));
1078
+ const title = success ? "任务完成" : "任务" + info.label;
1079
+ const toast = {
1080
+ id: Date.now() + Math.random(),
1081
+ kind: success ? "success" : (info.cls === "err" ? "error" : "warn"),
1082
+ title,
1083
+ body: lines.join("\n"),
1084
+ stayMs: typeof cfg.notifyStayMs === "number" ? cfg.notifyStayMs : 8000,
1085
+ };
1086
+ setToasts((prev) => prev.concat([toast]).slice(-4));
1087
+ // 提示音:任务结束时播放(完成/异常配套音);与系统通知独立开关
1088
+ if (cfg.soundNotify !== false) playDoneSound(success, cfg.soundEffect);
1089
+ // 系统通知:仅页面处于后台时推送,避免前台重复打扰
1090
+ if (cfg.systemNotify && typeof document !== "undefined" && document.hidden
1091
+ && typeof Notification !== "undefined" && Notification.permission === "granted") {
1092
+ try {
1093
+ new Notification("dsh " + title, {
1094
+ body: ((record && record.title) || task.title || "(无标题)") + " · " + fmtDur((record && record.duration) || (endTs - startTs)),
1095
+ tag: "dsh-dock-animation-" + task.sessionId,
1096
+ });
1097
+ } catch { /* 系统通知失败不影响页面内通知 */ }
1098
+ }
1099
+ };
1100
+
1101
+ const st = snap.status;
1102
+ const cfg = st && st.config;
1103
+ const active = st && st.active ? st.active : [];
1104
+ const animOn = !!(cfg && cfg.animationEnabled && active.length > 0);
1105
+ const mode = cfg && cfg.effectMode ? cfg.effectMode : "flow";
1106
+ // 机器人阶段:取最近发生阶段变化的活跃任务。
1107
+ const phase = active.length > 0
1108
+ ? active.reduce((best, x) => (!best || (x.phaseAt || 0) > (best.phaseAt || 0) ? x : best), null).phase
1109
+ : "think";
1110
+ // 有会话在等用户确认时,状态文字让位给"等待确认"(优先级最高:不动手就卡住)。
1111
+ // 会话级卡片(等待确认/编程等待中)只对"当前正在看的会话"出现,别的会话等待不打扰。
1112
+ const waitingTask = active.find((x) => Array.isArray(x.approvals) && x.approvals.length > 0) || null;
1113
+ const sessionTitleRef = useRef({ at: 0, title: "" });
1114
+ const currentTitle = useMemo(() => {
1115
+ // 标题查询要 getBoundingClientRect(强制布局),限频 1s;面板页/任务行为不受此限制
1116
+ const c = sessionTitleRef.current;
1117
+ if (typeof window === "undefined") return "";
1118
+ if (Date.now() - c.at < 1000) return c.title;
1119
+ c.at = Date.now();
1120
+ c.title = currentSessionTitle();
1121
+ return c.title;
1122
+ }, [snap.status]);
1123
+ const onScreenTasks = useMemo(() => active.filter((x) => taskOnScreen(x, currentTitle)), [active, currentTitle]);
1124
+ const waitingTaskOnScreen = onScreenTasks.find((x) => Array.isArray(x.approvals) && x.approvals.length > 0) || null;
1125
+ useTicker(ctx, animOn);
1126
+ // 徽标计时:以最早开始的任务为基准
1127
+ const now = Date.now();
1128
+ const elapsed = active.length > 0 ? fmtClock(now - Math.min(...active.map((x) => x.startTime || now))) : "";
1129
+ // 面板打开时隐藏环境动效(徽标/细线/巡航/机器人),避免与弹窗重叠错位
1130
+ const [panelOpen, setPanelOpen] = useState(false);
1131
+ useEffect(() => {
1132
+ setPanelOpen(panelNav.open);
1133
+ return subscribePanel(() => setPanelOpen(panelNav.open));
1134
+ }, []);
1135
+ // 任务速度:优先使用 host 从 chunk/tool 实时累积的活动脉冲;
1136
+ // token usage 只在完整消息后才到达,不能再作为唯一速度来源。
1137
+ const [speed, setSpeed] = useState(1);
1138
+ const speedRef = useRef({ ticks: -1, at: 0 });
1139
+ useEffect(() => {
1140
+ if (!st || active.length === 0) { speedRef.current = { ticks: -1, at: 0 }; setSpeed(1); return; }
1141
+ const ticks = active.reduce((a, x) => a + (x.motionTicks || 0), 0);
1142
+ const t = Date.now();
1143
+ const prev = speedRef.current;
1144
+ if (prev.ticks < 0 || t - prev.at < 300) { speedRef.current = { ticks, at: t }; return; }
1145
+ const rate = Math.max(0, (ticks - prev.ticks) / ((t - prev.at) / 1000));
1146
+ const phaseBoost = phase === "write" || phase === "code" ? .28 : (phase === "search" ? .18 : 0);
1147
+ const next = Math.max(.9, Math.min(4, Number((.9 + rate / 14 + phaseBoost).toFixed(2))));
1148
+ setSpeed((current) => Math.abs(current - next) < .03 ? current : next);
1149
+ speedRef.current = { ticks, at: t };
1150
+ }, [snap.status]);
1151
+ const ambientOn = animOn && !panelOpen;
1152
+ const spaceModeOn = !!(cfg && cfg.animationEnabled && mode === "space" && !panelOpen);
1153
+ const spaceDocks = useSpaceDocks(spaceModeOn);
1154
+ const spaceGalaxies = spaceModeOn && spaceDocks ? spaceGalaxyLayout() : null;
1155
+ // Host 在编程阶段持续 18 秒没有 chunk/tool 活动,才把它视为卡住;正常长任务不会误触发。
1156
+ // 卡住卡片同样只对当前会话出现。
1157
+ const stuckTask = onScreenTasks.find((task) => task.phase === "code" && now - (task.lastActivityAt || task.phaseAt || now) >= 18000) || null;
1158
+ const robotScaleFromConfig = Math.max(.85, Math.min(2.2, Number(cfg && cfg.robotScale) || 1.35));
1159
+ const speedStyle = { "--dkan-speed": Number(speed).toFixed(2) };
1160
+ const saveRobotScale = useCallback((robotScale) => {
1161
+ rpcCall("config", { robotScale })
1162
+ .then((d) => animationStore.applyConfig(d && d.config))
1163
+ .catch(() => { /* 浮层保存失败不打断任务动画;面板页会显示可操作的错误 */ });
1164
+ }, []);
1165
+
1166
+ // ===== 机器人卡片自由拖拽:整卡可拖到屏幕任意位置(localStorage 持久化),防止固定遮挡 =====
1167
+ const [botPos, setBotPos] = useState(null); // null = 停泊默认位(右下、输入卡上方)
1168
+ const botPosRef = useRef(null);
1169
+ const botDragRef = useRef(null); // { sx, sy, ox, oy, moved }
1170
+ const botResizeRef = useRef(null); // { sx, sy, scale }
1171
+ const [botScaleDraft, setBotScaleDraft] = useState(null);
1172
+ const botClickBlockRef = useRef(false);
1173
+ useEffect(() => {
1174
+ try {
1175
+ if (typeof localStorage === "undefined") return;
1176
+ const raw = localStorage.getItem("dsh-dock/anim/robot-pos/v1");
1177
+ const p = raw ? JSON.parse(raw) : null;
1178
+ if (p && typeof p.x === "number" && typeof p.y === "number" && typeof window !== "undefined") {
1179
+ // 恢复时钳制回视口内(窗口可能变小了)
1180
+ const card = document.querySelector(".dkan-botcard");
1181
+ const w = card ? card.offsetWidth : 200, h = card ? card.offsetHeight : 160;
1182
+ const x = Math.min(Math.max(8, p.x), Math.max(8, window.innerWidth - w - 8));
1183
+ const y = Math.min(Math.max(8, p.y), Math.max(8, window.innerHeight - h - 8));
1184
+ botPosRef.current = { x, y };
1185
+ setBotPos({ x, y });
1186
+ }
1187
+ } catch { /* localStorage 不可用时停默认位 */ }
1188
+ }, []);
1189
+ const onBotPointerDown = (e) => {
1190
+ if (e.button !== 0) return;
1191
+ const r = e.currentTarget.getBoundingClientRect();
1192
+ botDragRef.current = { sx: e.clientX, sy: e.clientY, ox: r.left, oy: r.top, moved: false };
1193
+ try { e.currentTarget.setPointerCapture(e.pointerId); } catch { /* 无指针捕获也能拖 */ }
1194
+ };
1195
+ const onBotPointerMove = (e) => {
1196
+ const d = botDragRef.current;
1197
+ if (!d || typeof window === "undefined") return;
1198
+ const dx = e.clientX - d.sx, dy = e.clientY - d.sy;
1199
+ if (!d.moved && Math.hypot(dx, dy) < 4) return;
1200
+ d.moved = true;
1201
+ const el = e.currentTarget;
1202
+ const w = el.offsetWidth || 200, h = el.offsetHeight || 160;
1203
+ const x = Math.min(Math.max(8, d.ox + dx), window.innerWidth - w - 8);
1204
+ const y = Math.min(Math.max(8, d.oy + dy), window.innerHeight - h - 8);
1205
+ botPosRef.current = { x, y };
1206
+ setBotPos({ x, y });
1207
+ };
1208
+ const onBotPointerUp = () => {
1209
+ const d = botDragRef.current;
1210
+ botDragRef.current = null;
1211
+ if (d && d.moved) {
1212
+ try { if (typeof localStorage !== "undefined") localStorage.setItem("dsh-dock/anim/robot-pos/v1", JSON.stringify(botPosRef.current)); } catch { /* 持久化失败静默 */ }
1213
+ botClickBlockRef.current = true; // 拖动结束的 pointerup 不触发面板跳转
1214
+ setTimeout(() => { botClickBlockRef.current = false; }, 0);
1215
+ }
1216
+ };
1217
+ const onBotResizePointerDown = (e) => {
1218
+ if (e.button !== 0) return;
1219
+ e.preventDefault();
1220
+ e.stopPropagation();
1221
+ botResizeRef.current = { sx: e.clientX, sy: e.clientY, scale: botScaleDraft == null ? robotScaleFromConfig : botScaleDraft };
1222
+ try { e.currentTarget.setPointerCapture(e.pointerId); } catch { /* 无指针捕获也能缩放 */ }
1223
+ };
1224
+ const onBotResizePointerMove = (e) => {
1225
+ const d = botResizeRef.current;
1226
+ if (!d) return;
1227
+ e.preventDefault();
1228
+ e.stopPropagation();
1229
+ // 保持场景比例;以右下角手柄向外拖动放大,向内拖动缩小。
1230
+ const delta = ((e.clientX - d.sx) + (e.clientY - d.sy)) / 2;
1231
+ const next = Math.max(.85, Math.min(2.2, Math.round((d.scale + delta / 220) * 100) / 100));
1232
+ setBotScaleDraft(next);
1233
+ // 已拖到自定义位置时,放大也始终留在可视区域内。
1234
+ if (botPosRef.current && typeof window !== "undefined") {
1235
+ const w = 220 * next + 24, h = 132 * next + 46;
1236
+ const x = Math.min(Math.max(8, botPosRef.current.x), Math.max(8, window.innerWidth - w - 8));
1237
+ const y = Math.min(Math.max(8, botPosRef.current.y), Math.max(8, window.innerHeight - h - 8));
1238
+ botPosRef.current = { x, y };
1239
+ setBotPos({ x, y });
1240
+ }
1241
+ };
1242
+ const onBotResizePointerUp = (e) => {
1243
+ if (!botResizeRef.current) return;
1244
+ e.preventDefault();
1245
+ e.stopPropagation();
1246
+ const next = botScaleDraft == null ? robotScaleFromConfig : botScaleDraft;
1247
+ botResizeRef.current = null;
1248
+ saveRobotScale(next);
1249
+ try { if (botPosRef.current && typeof localStorage !== "undefined") localStorage.setItem("dsh-dock/anim/robot-pos/v1", JSON.stringify(botPosRef.current)); } catch { /* 位置保存失败不影响缩放 */ }
1250
+ setBotScaleDraft(null);
1251
+ };
1252
+ const onBotClick = () => {
1253
+ if (botClickBlockRef.current) return;
1254
+ openPanel("animation");
1255
+ };
1256
+ const displayRobotScale = botScaleDraft == null ? robotScaleFromConfig : botScaleDraft;
1257
+ const botCardStyle = botPos
1258
+ ? Object.assign({}, speedStyle, { "--dkan-bot-scale": displayRobotScale, left: botPos.x, top: botPos.y, right: "auto", bottom: "auto" })
1259
+ : Object.assign({}, speedStyle, { "--dkan-bot-scale": displayRobotScale });
1260
+
1261
+ // Host 不可用(旧宿主未重启等):浮层整体静默,面板页会给提示
1262
+ if (!st) return null;
1263
+ return (<>
1264
+ {/* 顶部流光细线(flow 模式 / 任务进行中) */}
1265
+ {ambientOn && mode === "flow" ? <div className="dkan-line" style={speedStyle} aria-hidden="true" /> : null}
1266
+ {/* 氛围动效(代码雨/星野/极光/星际远征,任务进行中渲染,速度随吞吐) */}
1267
+ {ambientOn && AMBIENT_EFFECT_MODES.has(mode) && (mode !== "space" || spaceGalaxies)
1268
+ ? <AmbientLayer mode={mode} phase={phase} speed={Number(speed).toFixed(2)} tasks={active} galaxies={spaceGalaxies} /> : null}
1269
+ {/* 星际模式:空闲时在输入框上方待命;任务卡住时在中央呈现编程等待状态。 */}
1270
+ {spaceModeOn && spaceDocks && active.length === 0 && deliveries.length === 0 ? <ReadyFreighter dock={spaceDocks.ready} /> : null}
1271
+ {spaceModeOn && stuckTask ? <ProgrammingStuck task={stuckTask} waiting={!!(stuckTask.approvals && stuckTask.approvals.length)} /> : null}
1272
+ {!panelOpen ? departures.map((departure) => <MissionDeparture key={departure.id} departure={departure} />) : null}
1273
+ {/* 环屏巡航:一颗光点沿屏幕边缘转圈(orbit 模式) */}
1274
+ {ambientOn && mode === "orbit" ? <div className="dkan-orbit" style={speedStyle} aria-hidden="true" /> : null}
1275
+ {/* 交互反馈层:纸飞机/火花/流光/微光/光涌/彩带(与动画开关独立,只要功能启用即回应操作) */}
1276
+ <BurstLayer bursts={bursts} />
1277
+ {/* 桌面伙伴:背侧视角紧凑双工位人物,阶段与任务同步;整卡可拖到任意位置(robot 模式) */}
1278
+ {ambientOn && mode === "robot" ? (
1279
+ <div className="dkan-botcard" role="button" tabIndex={0} style={Object.assign({}, botCardStyle, waitingTask ? { "--dkan-phase": "#b45309" } : null)}
1280
+ title={active.length + " 个任务 · " + (waitingTask ? "等待确认" : phaseLabel(phase)) + " · 点击查看任务动画页 · 可拖动到任意位置"}
1281
+ onPointerDown={onBotPointerDown}
1282
+ onPointerMove={onBotPointerMove}
1283
+ onPointerUp={onBotPointerUp}
1284
+ onPointerCancel={onBotPointerUp}
1285
+ onClick={onBotClick}
1286
+ onKeyDown={(e) => { if (e.target === e.currentTarget && (e.key === "Enter" || e.key === " ")) { e.preventDefault(); onBotClick(); } }}>
1287
+ <RobotScene phase={phase} tasks={active} />
1288
+ <span className="dkan-bot-cap">
1289
+ <span className="n">{active.length}</span> 个任务{elapsed ? " · " + elapsed : ""} · {waitingTask ? "等待确认" : phaseLabel(phase)}
1290
+ </span>
1291
+ <button type="button" className="dkan-bot-resize" aria-label="调整桌面伙伴大小"
1292
+ title="拖动调整桌面伙伴大小"
1293
+ onPointerDown={onBotResizePointerDown}
1294
+ onPointerMove={onBotResizePointerMove}
1295
+ onPointerUp={onBotResizePointerUp}
1296
+ onPointerCancel={onBotResizePointerUp}
1297
+ onClick={(e) => e.stopPropagation()}
1298
+ onKeyDown={(e) => {
1299
+ if (e.key !== "ArrowUp" && e.key !== "ArrowRight" && e.key !== "ArrowDown" && e.key !== "ArrowLeft") return;
1300
+ e.preventDefault(); e.stopPropagation();
1301
+ const d = e.key === "ArrowUp" || e.key === "ArrowRight" ? .05 : -.05;
1302
+ saveRobotScale(Math.max(.85, Math.min(2.2, Math.round((displayRobotScale + d) * 100) / 100)));
1303
+ }} />
1304
+ </div>
1305
+ ) : null}
1306
+ {/* 右下角状态徽标:dot 随模式变化(breathe/ring 装饰),颜色随任务阶段、呼吸随吞吐;点击进功能坞动画页 */}
1307
+ {ambientOn && mode !== "robot" ? (
1308
+ <button type="button" className={"dkan-badge" + (mode === "breathe" ? " dkan-badge-breathe" : "")}
1309
+ style={Object.assign({}, speedStyle, { "--dkan-phase": phaseColor(waitingTask ? "code" : phase) })}
1310
+ title={active.length + " 个任务进行中 · " + (waitingTask ? "等待确认" : phaseLabel(phase)) + " · 点击查看任务动画页"}
1311
+ onClick={() => openPanel("animation")}>
1312
+ <span className="dkan-dotwrap">
1313
+ <span className="dkan-dot" />
1314
+ {mode === "breathe" ? <span className="dkan-halo" /> : null}
1315
+ {mode === "ring" ? <span className="dkan-ring" /> : null}
1316
+ </span>
1317
+ <span className="dkan-badge-txt"><span className="n">{active.length}</span> 个任务{elapsed ? " · " + elapsed : ""} · {waitingTask ? "等待确认" : phaseLabel(phase)}</span>
1318
+ </button>
1319
+ ) : null}
1320
+ {/* 完成瞬间的一次性流光(成功绿 / 异常红),动画结束自动清场 */}
1321
+ {flourish ? (
1322
+ <div key={flourish.key} className={"dkan-done" + (flourish.err ? " err" : "")}
1323
+ onAnimationEnd={() => setFlourish(null)} aria-hidden="true" />
1324
+ ) : null}
1325
+ {/* 星际任务完成:货运舰沿航线飞到输入框上方并保持停泊,直到下一次任务出航。 */}
1326
+ {!panelOpen ? deliveries.map((delivery) => <DeliveryFreighter key={delivery.id} delivery={delivery} />) : null}
1327
+ {/* 通知卡片栈(右下→右上不遮 dsh 自身 UI;右上角更常规) */}
1328
+ {toasts.length > 0 ? (
1329
+ <div className="dkan-toasts">
1330
+ {toasts.map((t) => <Toast key={t.id} t={t} onClose={(id) => setToasts((prev) => prev.filter((x) => x.id !== id))} />)}
1331
+ </div>
1332
+ ) : null}
1333
+ </>);
1334
+ }
1335
+
1336
+ // ---------- 面板页 ----------
1337
+ function ModePreview({ id }) {
1338
+ if (id === "flow") return <span className="dkan-prev"><span className="dkan-prev-flow" /></span>;
1339
+ if (id === "ring") {
1340
+ return (
1341
+ <span className="dkan-prev">
1342
+ <span className="dkan-dotwrap"><span className="dkan-dot" /><span className="dkan-ring" /></span>
1343
+ </span>
1344
+ );
1345
+ }
1346
+ if (id === "orbit") {
1347
+ return (
1348
+ <span className="dkan-prev dkan-prev-box">
1349
+ <span className="dkan-prev-orbit" />
1350
+ </span>
1351
+ );
1352
+ }
1353
+ if (id === "matrix") {
1354
+ return (
1355
+ <span className="dkan-prev dkan-prev-matrix">
1356
+ <i /><i /><i /><i /><i />
1357
+ </span>
1358
+ );
1359
+ }
1360
+ if (id === "stars") {
1361
+ return (
1362
+ <span className="dkan-prev dkan-prev-stars">
1363
+ <i /><i /><i /><i /><i /><i />
1364
+ </span>
1365
+ );
1366
+ }
1367
+ if (id === "space") {
1368
+ return <span className="dkan-prev dkan-prev-space"><i /><b /><em /></span>;
1369
+ }
1370
+ if (id === "aurora") {
1371
+ return (
1372
+ <span className="dkan-prev dkan-prev-aurora">
1373
+ <i /><i />
1374
+ </span>
1375
+ );
1376
+ }
1377
+ if (["nebula", "warp", "radar", "constellation", "fireflies", "ocean", "prism", "circuit", "gravity", "lantern"].includes(id)) {
1378
+ return <span className={"dkan-prev dkan-prev-new dkan-prev-" + id}>{Array.from({ length: id === "warp" ? 8 : 6 }, (_, i) => <i key={i} />)}</span>;
1379
+ }
1380
+ if (id === "robot") {
1381
+ return (
1382
+ <span className="dkan-prev dkan-prev-bot">
1383
+ <RobotScene phase="code" />
1384
+ </span>
1385
+ );
1386
+ }
1387
+ return (
1388
+ <span className="dkan-prev">
1389
+ <span className="dkan-dotwrap dkan-breathe"><span className="dkan-dot" /><span className="dkan-halo" /></span>
1390
+ </span>
1391
+ );
1392
+ }
1393
+
1394
+ function TaskRow(props) {
1395
+ const t = props.t;
1396
+ const info = endInfo(t.endReason);
1397
+ const waiting = !props.done && Array.isArray(t.approvals) && t.approvals.length > 0;
1398
+ return (
1399
+ <div className="dkan-task">
1400
+ <div className="dkan-task-head">
1401
+ <span className="dkan-task-title" title={t.title}>{t.title || "(无标题)"}</span>
1402
+ {props.done ? <span className={"dkan-tag " + info.cls}>{info.label}</span>
1403
+ : waiting ? <span className="dkan-tag warn">等待确认</span>
1404
+ : <span className="dkan-tag on">{phaseLabel(t.phase)}</span>}
1405
+ </div>
1406
+ <div className="dkan-task-meta">
1407
+ <span>{(t.models && t.models.length ? t.models.join(", ") : "未知") + (t.provider ? "(" + t.provider + ")" : "")}</span>
1408
+ <span>{props.done ? fmtDur(t.duration) : fmtClock(t.elapsed)}</span>
1409
+ {"turns" in t ? <span>{"回合 " + (t.turns || 0) + " · 步骤 " + (t.steps || 0) + (t.toolCalls ? " · 工具 " + t.toolCalls : "")}</span> : null}
1410
+ {t.totalTokens ? <span>{"↧" + fmtCompact(t.inputTokens) + " ↥" + fmtCompact(t.outputTokens)}</span> : null}
1411
+ {props.done && t.endTime ? <span>{fmtTime(t.endTime)}</span> : null}
1412
+ </div>
1413
+ {waiting ? (
1414
+ <div className="dkan-task-err">
1415
+ {t.approvals.map((a, i) => (a && a.toolName ? a.toolName : "未知工具") + (a && a.reason ? ":" + truncate(a.reason, 80) : "")).join(";")}
1416
+ </div>
1417
+ ) : null}
1418
+ {props.done && t.errorMessage ? <div className="dkan-task-err">{truncate(t.errorMessage, 120)}</div> : null}
1419
+ </div>
1420
+ );
1421
+ }
1422
+
1423
+ function AnimationView(props) {
1424
+ const ctx = props && props.ctx;
1425
+ const snap = useAnimation(ctx);
1426
+ const [cfg, setCfg] = useState(null); // null = 尚未加载
1427
+ const [saveErr, setSaveErr] = useState("");
1428
+ const [testing, setTesting] = useState(false);
1429
+ const [testState, setTestState] = useState(null); // { ok, msg } 机器人测试结果
1430
+ const [feishuTestState, setFeishuTestState] = useState(null); // { ok, msg } 飞书测试结果
1431
+ const [feishuTesting, setFeishuTesting] = useState(false);
1432
+ const cfgRef = useRef(null);
1433
+ const pendingSavesRef = useRef(0); // 进行中的保存(轮询回包不覆盖乐观值)
1434
+ const editingWebhookRef = useRef(false); // 钉钉 Webhook 输入中(轮询不覆盖草稿)
1435
+ const editingFeishuRef = useRef(false); // 飞书 Webhook 输入中(轮询不覆盖草稿)
1436
+ // 拉到新配置(含保存回包)后同步本地编辑态;保存中/输入中不覆盖
1437
+ useEffect(() => {
1438
+ const c = snap.status && snap.status.config;
1439
+ if (!c) return;
1440
+ if (pendingSavesRef.current > 0 || editingWebhookRef.current || editingFeishuRef.current) return;
1441
+ if (c !== cfgRef.current) {
1442
+ cfgRef.current = c;
1443
+ setCfg(Object.assign({}, c));
1444
+ }
1445
+ }, [snap.status]);
1446
+ // 面板打开时兜底拉一次(浮层通常已在轮询)
1447
+ useEffect(() => {
1448
+ if (!animationStore.snap.status) animationStore.refresh();
1449
+ }, []);
1450
+ // 乐观更新 + 立即持久化(每个开关独立保存);返回 Promise 供需要串联的操作使用
1451
+ const patch = (p) => {
1452
+ setSaveErr("");
1453
+ setCfg(Object.assign({}, cfg, p));
1454
+ pendingSavesRef.current++;
1455
+ return rpcCall("config", p)
1456
+ .then((d) => {
1457
+ pendingSavesRef.current--;
1458
+ animationStore.applyConfig(d && d.config);
1459
+ })
1460
+ .catch((e) => {
1461
+ pendingSavesRef.current--;
1462
+ setSaveErr("保存失败:" + ((e && e.message) || String(e)));
1463
+ });
1464
+ };
1465
+ // Webhook 草稿保存:清编辑态后立即对齐一次(同步 effect 在编辑期被跳过)
1466
+ const saveWebhook = async () => {
1467
+ if (!editingWebhookRef.current) return;
1468
+ const hook = String(cfg.dingtalkWebhook || "").trim();
1469
+ await patch({ dingtalkWebhook: hook });
1470
+ editingWebhookRef.current = false;
1471
+ const c = animationStore.snap.status && animationStore.snap.status.config;
1472
+ if (c && c !== cfgRef.current) {
1473
+ cfgRef.current = c;
1474
+ setCfg(Object.assign({}, c));
1475
+ }
1476
+ };
1477
+ // 飞书 Webhook 草稿保存
1478
+ const saveFeishuWebhook = async () => {
1479
+ if (!editingFeishuRef.current) return;
1480
+ const hook = String(cfg.feishuWebhook || "").trim();
1481
+ await patch({ feishuWebhook: hook });
1482
+ editingFeishuRef.current = false;
1483
+ const c = animationStore.snap.status && animationStore.snap.status.config;
1484
+ if (c && c !== cfgRef.current) {
1485
+ cfgRef.current = c;
1486
+ setCfg(Object.assign({}, c));
1487
+ }
1488
+ };
1489
+ // 钉钉测试:草稿未保存先保存,再发测试消息
1490
+ const runDingtalkTest = async () => {
1491
+ setTesting(true);
1492
+ setTestState(null);
1493
+ try {
1494
+ if (editingWebhookRef.current) {
1495
+ const hook = String(cfg.dingtalkWebhook || "").trim();
1496
+ if (!hook) throw new Error("请先填写 Webhook 地址");
1497
+ const d = await rpcCall("config", { dingtalkWebhook: hook });
1498
+ animationStore.applyConfig(d && d.config);
1499
+ editingWebhookRef.current = false;
1500
+ cfgRef.current = (d && d.config) || cfgRef.current;
1501
+ setCfg(Object.assign({}, cfg, { dingtalkWebhook: hook }));
1502
+ }
1503
+ const r = await rpcCall("test");
1504
+ setTestState(r && r.sent
1505
+ ? { ok: true, msg: "测试消息已发送,去群里看看" }
1506
+ : { ok: false, msg: (r && r.error) || "发送失败" });
1507
+ } catch (e) {
1508
+ setTestState({ ok: false, msg: (e && e.message) || String(e) });
1509
+ } finally {
1510
+ setTesting(false);
1511
+ }
1512
+ };
1513
+ // 飞书测试:草稿未保存先保存,再发测试消息
1514
+ const runFeishuTest = async () => {
1515
+ setFeishuTesting(true);
1516
+ setFeishuTestState(null);
1517
+ try {
1518
+ if (editingFeishuRef.current) {
1519
+ const hook = String(cfg.feishuWebhook || "").trim();
1520
+ if (!hook) throw new Error("请先填写 Webhook 地址");
1521
+ const d = await rpcCall("config", { feishuWebhook: hook });
1522
+ animationStore.applyConfig(d && d.config);
1523
+ editingFeishuRef.current = false;
1524
+ cfgRef.current = (d && d.config) || cfgRef.current;
1525
+ setCfg(Object.assign({}, cfg, { feishuWebhook: hook }));
1526
+ }
1527
+ const r = await rpcCall("test", { target: "feishu" });
1528
+ setFeishuTestState(r && r.sent
1529
+ ? { ok: true, msg: "测试消息已发送,去群里看看" }
1530
+ : { ok: false, msg: (r && r.error) || "发送失败" });
1531
+ } catch (e) {
1532
+ setFeishuTestState({ ok: false, msg: (e && e.message) || String(e) });
1533
+ } finally {
1534
+ setFeishuTesting(false);
1535
+ }
1536
+ };
1537
+ const enableSystemNotify = async (next) => {
1538
+ if (next && typeof Notification !== "undefined" && Notification.permission !== "granted") {
1539
+ try {
1540
+ const perm = await Notification.requestPermission(); // 开关点击即用户手势
1541
+ if (perm !== "granted") {
1542
+ setSaveErr("浏览器未授权系统通知(可在地址栏权限设置里重新允许)");
1543
+ return;
1544
+ }
1545
+ } catch {
1546
+ setSaveErr("浏览器不支持系统通知");
1547
+ return;
1548
+ }
1549
+ }
1550
+ patch({ systemNotify: next });
1551
+ };
1552
+
1553
+ const st = snap.status;
1554
+ const active = st && st.active ? st.active : [];
1555
+ const recent = st && st.recent ? st.recent.slice(0, 6) : [];
1556
+ const permNote = typeof Notification === "undefined"
1557
+ ? "当前浏览器不支持系统通知"
1558
+ : Notification.permission === "granted" ? "已授权 · 仅页面后台时推送"
1559
+ : Notification.permission === "denied" ? "已被浏览器拒绝(需在浏览器权限设置里重新允许)" : "未授权 · 开启时会请求授权,仅页面后台时推送";
1560
+
1561
+ const rows = [];
1562
+ if (!cfg) {
1563
+ rows.push(<div key="load" className="dkan-note">{snap.error ? "状态不可用:" + snap.error : (snap.loading ? "正在拉取任务状态…" : "等待任务状态")}</div>);
1564
+ } else {
1565
+ const waitingCount = active.reduce((n, t) => n + (Array.isArray(t.approvals) ? t.approvals.length : 0), 0);
1566
+ rows.push(
1567
+ <div key="anim" className="dkan-sec">
1568
+ <div className="dkan-sec-head">
1569
+ <span className="dkan-sec-title">运行动画</span>
1570
+ <span className="dkan-sec-sub">任务进行中才出现;克制的动效,暗/亮色自适应</span>
1571
+ <span className="dkan-sec-sw">
1572
+ <span className={"dkan-sec-swlabel" + (cfg.animationEnabled ? " on" : "")}>{cfg.animationEnabled ? "已开启" : "已关闭"}</span>
1573
+ <button type="button" className={"dock-sw" + (cfg.animationEnabled ? " on" : "")}
1574
+ role="switch" aria-checked={cfg.animationEnabled} aria-label="开关运行动画"
1575
+ title={cfg.animationEnabled ? "关闭运行动画" : "开启运行动画"}
1576
+ onClick={() => patch({ animationEnabled: !cfg.animationEnabled })} />
1577
+ </span>
1578
+ </div>
1579
+ {cfg.animationEnabled ? (
1580
+ <>
1581
+ <div className="dkan-modes">
1582
+ {EFFECT_MODES.map((m) => (
1583
+ <button type="button" key={m.id}
1584
+ className={"dkan-mode" + (cfg.effectMode === m.id ? " on" : "")}
1585
+ onClick={() => patch({ effectMode: m.id })}>
1586
+ <span className="dkan-mode-name">{m.name}</span>
1587
+ <ModePreview id={m.id} />
1588
+ <span className="dkan-mode-desc">{m.desc}</span>
1589
+ </button>
1590
+ ))}
1591
+ </div>
1592
+ {cfg.effectMode === "robot" ? <div className="dkan-robot-controls">
1593
+ <div className="dkan-robot-controls-head">
1594
+ <span>桌面伙伴大小</span>
1595
+ <strong>{Math.round((Number(cfg.robotScale) || 1.35) * 100)}%</strong>
1596
+ </div>
1597
+ <div className="dkan-robot-size-options" role="group" aria-label="桌面伙伴大小预设">
1598
+ {[[1, "紧凑"], [1.35, "默认"], [1.7, "加大"], [2.1, "特大"]].map(([scale, label]) => (
1599
+ <button type="button" key={scale} className={Math.abs((Number(cfg.robotScale) || 1.35) - scale) < .03 ? "on" : ""}
1600
+ onClick={() => patch({ robotScale: scale })}>{label}</button>
1601
+ ))}
1602
+ </div>
1603
+ <div className="dkan-robot-slider-row">
1604
+ <input type="range" min="0.85" max="2.2" step="0.05" value={Number(cfg.robotScale) || 1.35}
1605
+ aria-label="桌面伙伴大小百分比"
1606
+ onChange={(e) => patch({ robotScale: Number(e.target.value) })} />
1607
+ <span>浮层右下角也可直接拖动缩放,大小会自动保存。</span>
1608
+ </div>
1609
+ </div> : null}
1610
+ </>
1611
+ ) : <div className="dkan-note">动画已关闭——只保留通知(或全部关闭)时,页面不会有任何动效。</div>}
1612
+ </div>,
1613
+ <div key="notify" className="dkan-sec">
1614
+ <div className="dkan-sec-head">
1615
+ <span className="dkan-sec-title">完成通知</span>
1616
+ <span className="dkan-sec-sub">与动画互不依赖,可单独开启</span>
1617
+ <span className="dkan-sec-sw">
1618
+ <span className={"dkan-sec-swlabel" + (cfg.notifyEnabled ? " on" : "")}>{cfg.notifyEnabled ? "已开启" : "已关闭"}</span>
1619
+ <button type="button" className={"dock-sw" + (cfg.notifyEnabled ? " on" : "")}
1620
+ role="switch" aria-checked={cfg.notifyEnabled} aria-label="开关完成通知"
1621
+ title={cfg.notifyEnabled ? "关闭完成通知" : "开启完成通知"}
1622
+ onClick={() => patch({ notifyEnabled: !cfg.notifyEnabled })} />
1623
+ </span>
1624
+ </div>
1625
+ {cfg.notifyEnabled ? <div className="dkan-rows-narrow">
1626
+ <div className="dkan-row">
1627
+ <span className="dkan-row-label">完成通知</span>
1628
+ <button type="button" className={"dkm-miniswitch" + (cfg.notifyOnComplete ? " on" : "")}
1629
+ onClick={() => patch({ notifyOnComplete: !cfg.notifyOnComplete })}>
1630
+ {cfg.notifyOnComplete ? "开" : "关"}
1631
+ </button>
1632
+ <span className="dkan-row-sub">任务正常完成时通知</span>
1633
+ </div>
1634
+ <div className="dkan-row">
1635
+ <span className="dkan-row-label">异常通知</span>
1636
+ <button type="button" className={"dkm-miniswitch" + (cfg.notifyOnError ? " on" : "")}
1637
+ onClick={() => patch({ notifyOnError: !cfg.notifyOnError })}>
1638
+ {cfg.notifyOnError ? "开" : "关"}
1639
+ </button>
1640
+ <span className="dkan-row-sub">出错 / 中止 / 达输出上限时通知</span>
1641
+ </div>
1642
+ <div className="dkan-row">
1643
+ <span className="dkan-row-label">需确认提醒</span>
1644
+ <button type="button" className={"dkm-miniswitch" + (cfg.notifyOnConfirm !== false ? " on" : "")}
1645
+ onClick={() => patch({ notifyOnConfirm: cfg.notifyOnConfirm === false })}>
1646
+ {cfg.notifyOnConfirm !== false ? "开" : "关"}
1647
+ </button>
1648
+ <span className="dkan-row-sub">工具等待你批准时弹卡片并响确认音(常驻不自动消失)</span>
1649
+ </div>
1650
+ <div className="dkan-row">
1651
+ <span className="dkan-row-label">停留时长</span>
1652
+ <select className="dkan-select" value={String(cfg.notifyStayMs)}
1653
+ onChange={(e) => patch({ notifyStayMs: Number(e.target.value) })}>
1654
+ <option value="4000">4 秒</option>
1655
+ <option value="8000">8 秒</option>
1656
+ <option value="15000">15 秒</option>
1657
+ <option value="30000">30 秒</option>
1658
+ <option value="0">常驻(手动关闭)</option>
1659
+ </select>
1660
+ </div>
1661
+ <div className="dkan-row">
1662
+ <span className="dkan-row-label">系统通知</span>
1663
+ <button type="button" className={"dkm-miniswitch" + (cfg.systemNotify ? " on" : "")}
1664
+ onClick={() => enableSystemNotify(!cfg.systemNotify)}>
1665
+ {cfg.systemNotify ? "开" : "关"}
1666
+ </button>
1667
+ <span className="dkan-row-sub">{permNote}</span>
1668
+ </div>
1669
+ <div className="dkan-row">
1670
+ <span className="dkan-row-label">提示音</span>
1671
+ <button type="button" className={"dkm-miniswitch" + (cfg.soundNotify !== false ? " on" : "")}
1672
+ onClick={() => patch({ soundNotify: cfg.soundNotify === false })}>
1673
+ {cfg.soundNotify !== false ? "开" : "关"}
1674
+ </button>
1675
+ <span className="dkan-row-sub">任务结束/需确认时播放(试听为先播完成音、后播异常音)</span>
1676
+ </div>
1677
+ {cfg.soundNotify !== false ? (
1678
+ <div className="dkan-sounds">
1679
+ {Object.keys(SOUND_LIBRARY).map((key) => (
1680
+ <button type="button" key={key}
1681
+ className={"dkan-sound" + (cfg.soundEffect === key ? " on" : "")}
1682
+ onClick={() => patch({ soundEffect: key })}>
1683
+ <span className="dkan-sound-name">
1684
+ {SOUND_LIBRARY[key].name}
1685
+ {cfg.soundEffect === key ? <span className="dkan-sound-cur">✓</span> : null}
1686
+ </span>
1687
+ <span className="dkan-sound-play" role="button" tabIndex={0}
1688
+ title={"试听 " + SOUND_LIBRARY[key].name}
1689
+ onClick={(e) => { e.stopPropagation(); previewSound(key); }}
1690
+ onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); e.stopPropagation(); previewSound(key); } }}>▶</span>
1691
+ </button>
1692
+ ))}
1693
+ </div>
1694
+ ) : null}
1695
+ </div> : <div className="dkan-note">通知已关闭——任务结束时既不弹卡片也不推系统通知。</div>}
1696
+ </div>,
1697
+ <div key="dingtalk" className="dkan-sec">
1698
+ <div className="dkan-sec-head">
1699
+ <span className="dkan-sec-title">钉钉推送</span>
1700
+ <span className="dkan-sec-sub">任务结束推送到钉钉群机器人(宿主直发,浏览器关着也能推;事件跟随上方完成/异常开关)</span>
1701
+ <span className="dkan-sec-sw">
1702
+ <span className={"dkan-sec-swlabel" + (cfg.dingtalkEnabled ? " on" : "")}>{cfg.dingtalkEnabled ? "已开启" : "已关闭"}</span>
1703
+ <button type="button" className={"dock-sw" + (cfg.dingtalkEnabled ? " on" : "")}
1704
+ role="switch" aria-checked={cfg.dingtalkEnabled} aria-label="开关钉钉推送"
1705
+ title={cfg.dingtalkEnabled ? "关闭钉钉推送" : "开启钉钉推送"}
1706
+ onClick={() => patch({ dingtalkEnabled: !cfg.dingtalkEnabled })} />
1707
+ </span>
1708
+ </div>
1709
+ {cfg.dingtalkEnabled ? <div className="dkan-rows-narrow">
1710
+ <div className="dkan-row dkan-row-webhook">
1711
+ <span className="dkan-row-label">Webhook</span>
1712
+ <input type="text" className="dkan-input" spellCheck={false}
1713
+ value={cfg.dingtalkWebhook || ""}
1714
+ placeholder="https://oapi.dingtalk.com/robot/send?access_token=…"
1715
+ onChange={(e) => {
1716
+ editingWebhookRef.current = true;
1717
+ setCfg(Object.assign({}, cfg, { dingtalkWebhook: e.target.value }));
1718
+ }} />
1719
+ <button type="button" className="dkan-btn" disabled={!editingWebhookRef.current}
1720
+ onClick={saveWebhook}>
1721
+ {editingWebhookRef.current ? "保存" : "已保存"}
1722
+ </button>
1723
+ </div>
1724
+ <div className="dkan-row">
1725
+ <span className="dkan-row-label">连通测试</span>
1726
+ <button type="button" className="dkan-btn" disabled={testing} onClick={runDingtalkTest}>
1727
+ {testing ? "发送中…" : "发送测试消息"}
1728
+ </button>
1729
+ {testState ? <span className={"dkan-row-sub" + (testState.ok ? " dkan-ok" : " dkan-err")}>
1730
+ {testState.ok ? "✓ " : "✗ "}{testState.msg}
1731
+ </span> : <span className="dkan-row-sub">用当前保存的 Webhook 发一条测试消息</span>}
1732
+ </div>
1733
+ <div className="dkan-note">机器人创建:钉钉群 → 设置 → 智能群助手 → 添加机器人 → 自定义(Webhook),
1734
+ 安全设置选「自定义关键词」填「任务」或「dsh」(推送标题含「任务」即可命中)。</div>
1735
+ </div> : <div className="dkan-note">未开启——任务结束不推送钉钉。</div>}
1736
+ </div>,
1737
+ <div key="feishu" className="dkan-sec">
1738
+ <div className="dkan-sec-head">
1739
+ <span className="dkan-sec-title">飞书推送</span>
1740
+ <span className="dkan-sec-sub">任务结束推送到飞书群机器人(宿主直发,浏览器关着也能推;事件跟随上方完成/异常开关)</span>
1741
+ <span className="dkan-sec-sw">
1742
+ <span className={"dkan-sec-swlabel" + (cfg.feishuEnabled ? " on" : "")}>{cfg.feishuEnabled ? "已开启" : "已关闭"}</span>
1743
+ <button type="button" className={"dock-sw" + (cfg.feishuEnabled ? " on" : "")}
1744
+ role="switch" aria-checked={cfg.feishuEnabled} aria-label="开关飞书推送"
1745
+ title={cfg.feishuEnabled ? "关闭飞书推送" : "开启飞书推送"}
1746
+ onClick={() => patch({ feishuEnabled: !cfg.feishuEnabled })} />
1747
+ </span>
1748
+ </div>
1749
+ {cfg.feishuEnabled ? <div className="dkan-rows-narrow">
1750
+ <div className="dkan-row dkan-row-webhook">
1751
+ <span className="dkan-row-label">Webhook</span>
1752
+ <input type="text" className="dkan-input" spellCheck={false}
1753
+ value={cfg.feishuWebhook || ""}
1754
+ placeholder="https://open.feishu.cn/open-apis/bot/v2/hook/…"
1755
+ onChange={(e) => {
1756
+ editingFeishuRef.current = true;
1757
+ setCfg(Object.assign({}, cfg, { feishuWebhook: e.target.value }));
1758
+ }} />
1759
+ <button type="button" className="dkan-btn" disabled={!editingFeishuRef.current}
1760
+ onClick={saveFeishuWebhook}>
1761
+ {editingFeishuRef.current ? "保存" : "已保存"}
1762
+ </button>
1763
+ </div>
1764
+ <div className="dkan-row">
1765
+ <span className="dkan-row-label">连通测试</span>
1766
+ <button type="button" className="dkan-btn" disabled={feishuTesting} onClick={runFeishuTest}>
1767
+ {feishuTesting ? "发送中…" : "发送测试消息"}
1768
+ </button>
1769
+ {feishuTestState ? <span className={"dkan-row-sub" + (feishuTestState.ok ? " dkan-ok" : " dkan-err")}>
1770
+ {feishuTestState.ok ? "✓ " : "✗ "}{feishuTestState.msg}
1771
+ </span> : <span className="dkan-row-sub">用当前保存的 Webhook 发一条测试消息</span>}
1772
+ </div>
1773
+ <div className="dkan-note">机器人创建:飞书群 → 设置 → 群机器人 → 添加机器人 → 自定义机器人(获取 Webhook 地址);
1774
+ 安全设置如选「自定义关键词」填「任务」或「dsh」(推送标题含「任务」即可命中)。</div>
1775
+ </div> : <div className="dkan-note">未开启——任务结束不推送飞书。</div>}
1776
+ </div>
1777
+ );
1778
+ }
1779
+ if (saveErr) rows.push(<div key="err" className="dkan-note dkan-err">{saveErr}</div>);
1780
+ rows.push(
1781
+ <div key="state" className="dkan-sec">
1782
+ <div className="dkan-sec-head">
1783
+ <span className="dkan-sec-title">运行状态</span>
1784
+ <span className="dkan-sec-sub">
1785
+ {snap.error ? "状态拉取失败:" + snap.error
1786
+ : waitingCount > 0 ? waitingCount + " 项等待确认 · " + active.length + " 个任务进行中"
1787
+ : active.length > 0 ? active.length + " 个任务进行中"
1788
+ : recent.length > 0 ? "空闲 · 显示最近完成" : "空闲 · 暂无任务记录"}
1789
+ </span>
1790
+ <button type="button" className="dkan-refresh" onClick={() => animationStore.refresh()}>
1791
+ {snap.loading ? "刷新中…" : "刷新"}
1792
+ </button>
1793
+ </div>
1794
+ {active.length > 0 ? <div className="dkan-tasks">{active.map((t) => <TaskRow key={t.sessionId} t={t} />)}</div> : null}
1795
+ {active.length === 0 && recent.length === 0
1796
+ ? <div className="dkan-note">发起新会话任务后,这里会显示进行中与最近完成的任务;动画与通知同时在页面生效。</div> : null}
1797
+ {recent.length > 0 ? <div className="dkan-tasks dkan-tasks-done">{recent.map((t) => <TaskRow key={t.sessionId + ":" + t.endTime} t={t} done />)}</div> : null}
1798
+ </div>
1799
+ );
1800
+ return <div className="dkan-root">{rows}</div>;
1801
+ }
1802
+
1803
+ // ---------- 首页总揽概要 ----------
1804
+ function AnimationStat(props) {
1805
+ const snap = useAnimation(props && props.ctx);
1806
+ const st = snap.status;
1807
+ if (snap.error) return <span className="dkan-err">任务状态不可用(宿主需重启加载动画路由)</span>;
1808
+ if (!st) return <span>等待任务状态…</span>;
1809
+ if (st.active && st.active.length > 0) {
1810
+ return <span>{st.active.length + " 个任务进行中 · " + fmtDur(Math.max(...st.active.map((x) => x.elapsed || 0)))}</span>;
1811
+ }
1812
+ const last = st.recent && st.recent[0];
1813
+ return <span>{last ? "空闲 · 最近完成 " + truncate(last.title, 24) : "空闲 · 暂无任务记录"}</span>;
1814
+ }
1815
+
1816
+ // ---------- 样式(dkan- 前缀;全部走主题变量,暗/亮色自适应;动效时长 = 基准 / --dkan-speed) ----------
1817
+ const css = [
1818
+ // 顶部流光细线(3px,比首版更醒目)
1819
+ ".dkan-line{position:fixed;top:0;left:0;right:0;height:3px;z-index:9990;pointer-events:none;background:color-mix(in srgb,var(--dk-accent) 30%,transparent);}",
1820
+ ".dkan-line::after{content:\"\";position:absolute;top:0;bottom:0;left:-42%;width:42%;background:linear-gradient(90deg,transparent,var(--dk-accent) 60%,#fff);box-shadow:0 0 10px 1px color-mix(in srgb,var(--dk-accent) 70%,transparent);animation:dkan-flow calc(1.9s / var(--dkan-speed,1)) linear infinite;}",
1821
+ "@keyframes dkan-flow{to{left:100%}}",
1822
+ // 完成瞬间的一次性流光(成功绿 / 异常红),动画结束自动清场
1823
+ ".dkan-done{position:fixed;top:0;left:0;right:0;height:3px;z-index:9991;pointer-events:none;background:linear-gradient(90deg,transparent,var(--dsw-alias-state-success-primary,#34d399) 50%,#fff);background-size:50% 100%;background-repeat:no-repeat;animation:dkan-done 1.1s var(--ds-ease-in-out) forwards;}",
1824
+ ".dkan-done.err{background-image:linear-gradient(90deg,transparent,var(--dsw-alias-state-error-primary,#f87171) 50%,#fff);}",
1825
+ "@keyframes dkan-done{0%{background-position:-60% 0;opacity:0}25%{opacity:1}100%{background-position:160% 0;opacity:0}}",
1826
+ // 环屏巡航:光点沿屏幕边缘转一整圈
1827
+ ".dkan-orbit{position:fixed;top:0;left:0;width:12px;height:12px;z-index:9990;pointer-events:none;border-radius:50%;background:var(--dk-accent);box-shadow:0 0 14px 4px color-mix(in srgb,var(--dk-accent) 60%,transparent),0 0 30px 8px color-mix(in srgb,var(--dk-accent) 25%,transparent);animation:dkan-orbit calc(14s / var(--dkan-speed,1)) linear infinite;}",
1828
+ "@keyframes dkan-orbit{0%{top:0;left:0}25%{top:0;left:calc(100vw - 12px)}50%{top:calc(100vh - 12px);left:calc(100vw - 12px)}75%{top:calc(100vh - 12px);left:0}100%{top:0;left:0}}",
1829
+ // ===== 氛围动效 =====
1830
+ ".dkan-amb{position:fixed;inset:0;z-index:9989;pointer-events:none;overflow:hidden;}",
1831
+ // pointer-events 不继承:全屏装饰层的子粒子(星点/流星/极光/火花/彩带)必须各自透明于点击
1832
+ ".dkan-amb *,.dkan-fxwrap *{pointer-events:none;}",
1833
+ // 代码雨:字符列缓落(提亮加大,速度随吞吐)
1834
+ ".dkan-matrix span{position:absolute;top:-12%;writing-mode:vertical-rl;font-family:var(--ds-font-family-code,monospace);color:var(--dk-accent);text-shadow:0 0 6px color-mix(in srgb,var(--dk-accent) 60%,transparent);animation:dkan-fall linear infinite;will-change:transform;}",
1835
+ "@keyframes dkan-fall{to{transform:translateY(125vh)}}",
1836
+ // 星野:星点缓慢飘移 + 闪烁(加亮加发光)+ 月亮 + 流星
1837
+ ".dkan-stars .dkan-star{position:absolute;border-radius:50%;background:#cfe3ff;box-shadow:0 0 4px 1px color-mix(in srgb,#8ab6ff 55%,transparent);animation:dkan-star ease-in-out infinite alternate;}",
1838
+ "@keyframes dkan-star{0%{opacity:.3;transform:translate(0,0)}50%{opacity:1}100%{opacity:.45;transform:translate(var(--dx),var(--dy))}}",
1839
+ // 月亮:右上弯月(radial-gradient 咬出月牙,透明区不遮挡背景;外圈柔光晕)
1840
+ ".dkan-moon{position:absolute;top:9%;right:10%;width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 66% 34%,transparent 0 44%,#fdf6d8 46%,#e8d9a0 78%,#d8c78a);box-shadow:0 0 18px 4px color-mix(in srgb,#fdf6d8 35%,transparent),0 0 44px 12px color-mix(in srgb,#fdf6d8 15%,transparent);}",
1841
+ // 流星:斜向划过的光带(亮头 + 渐隐尾),周期出现
1842
+ ".dkan-meteor{position:absolute;width:110px;height:2px;border-radius:2px;background:linear-gradient(90deg,#fff,color-mix(in srgb,#8ab6ff 60%,transparent) 40%,transparent);transform:rotate(35deg);transform-origin:left center;opacity:0;animation:dkan-meteor 8.4s linear infinite;}",
1843
+ ".dkan-meteor::before{content:\"\";position:absolute;left:-2px;top:-2px;width:6px;height:6px;border-radius:50%;background:#fff;box-shadow:0 0 8px 2px color-mix(in srgb,#cfe3ff 70%,transparent);}",
1844
+ "@keyframes dkan-meteor{0%{opacity:0;transform:rotate(35deg) translateX(0)}3%{opacity:1}10%{opacity:0;transform:rotate(35deg) translateX(46vh)}100%{opacity:0;transform:rotate(35deg) translateX(46vh)}}",
1845
+ // 星际远征:星尘、行星轨道和任务飞船都只在任务运行时出现,统一由 --dkan-speed 加速。
1846
+ ".dkan-space{background:radial-gradient(ellipse 34% 28% at 84% 11%,color-mix(in srgb,#426aaf 15%,transparent),transparent 72%),radial-gradient(ellipse 26% 22% at 14% 58%,color-mix(in srgb,#7c4dff 10%,transparent),transparent 74%);}",
1847
+ ".dkan-space-dust{position:absolute;border-radius:50%;background:#dbeafe;box-shadow:0 0 5px color-mix(in srgb,#93c5fd 62%,transparent);opacity:.2;animation:dkan-space-dust ease-in-out infinite alternate;will-change:transform,opacity;}",
1848
+ "@keyframes dkan-space-dust{0%{opacity:.14;transform:translate3d(0,0,0) scale(.7)}55%{opacity:.78}100%{opacity:.26;transform:translate3d(22px,-8px,0) scale(1.15)}}",
1849
+ ".dkan-space-system{position:fixed;width:246px;height:246px;border-radius:50%;opacity:.78;transform:translateZ(0) scale(var(--dkan-system-scale,1));transform-origin:center;contain:layout paint style;}.dkan-space-system.alpha{opacity:.76}.dkan-space-system.beta{opacity:.68}.dkan-space-system.gamma{opacity:.64}",
1850
+ ".dkan-space-sun{position:absolute;left:50%;top:50%;width:34px;height:34px;border-radius:50%;transform:translate(-50%,-50%);background:radial-gradient(circle at 32% 28%,#fff7c2 0 9%,#ffc85a 30%,#e76f2e 74%);box-shadow:0 0 12px 4px color-mix(in srgb,#ffc85a 58%,transparent),0 0 36px 12px color-mix(in srgb,#f97316 20%,transparent);}",
1851
+ ".dkan-space-orbit{position:absolute;left:50%;top:50%;border:1px solid color-mix(in srgb,#93c5fd 28%,transparent);border-radius:50%;animation:dkan-space-orbit linear infinite;transform-origin:center;}",
1852
+ ".dkan-space-orbit i{position:absolute;top:50%;left:-5px;width:10px;height:10px;border-radius:50%;transform:translateY(-50%);box-shadow:0 0 9px 2px currentColor;}",
1853
+ ".dkan-space-orbit.a{width:82px;height:50px;margin:-25px 0 0 -41px;animation-duration:calc(5.4s / var(--dkan-speed,1));}.dkan-space-orbit.a i{color:#67e8f9;background:#22d3ee;}",
1854
+ ".dkan-space-orbit.b{width:142px;height:86px;margin:-43px 0 0 -71px;animation-duration:calc(9.2s / var(--dkan-speed,1));animation-direction:reverse;}.dkan-space-orbit.b i{width:15px;height:15px;color:#c4b5fd;background:radial-gradient(circle at 30% 28%,#ede9fe,#8b5cf6 72%);}",
1855
+ ".dkan-space-orbit.c{width:212px;height:130px;margin:-65px 0 0 -106px;animation-duration:calc(15.6s / var(--dkan-speed,1));}.dkan-space-orbit.c i{width:18px;height:18px;color:#fda4af;background:radial-gradient(circle at 32% 28%,#ffe4e6,#e11d48 74%);}",
1856
+ "@keyframes dkan-space-orbit{to{transform:rotate(360deg)}}",
1857
+ ".dkan-space-runner{position:fixed;left:0;top:0;width:108px;height:56px;opacity:0;animation-duration:calc(10.8s / var(--dkan-speed,1));animation-timing-function:linear;animation-iteration-count:infinite;animation-delay:var(--dkan-run-delay,0s);will-change:transform,opacity;contain:layout paint style;backface-visibility:hidden;}.dkan-space-runner .dkan-freighter{left:0;top:0;transform:translateZ(0) scale(.52);transform-origin:top left;filter:none}.dkan-space-runner .dkan-freighter-engine i{animation:none;opacity:.76;box-shadow:-6px 0 8px #22d3ee}.dkan-space-runner .dkan-freighter-nose{filter:none}.dkan-space-runner.route-0{animation-name:dkan-space-route-a}.dkan-space-runner.route-1{animation-name:dkan-space-route-b}.dkan-space-runner.route-2{animation-name:dkan-space-route-c}",
1858
+ "@keyframes dkan-space-route-a{0%{opacity:0;transform:translate3d(var(--dkan-alpha-x),var(--dkan-alpha-y),0) rotate(151deg)}7%,86%{opacity:.9}100%{opacity:0;transform:translate3d(var(--dkan-beta-x),var(--dkan-beta-y),0) rotate(151deg)}}@keyframes dkan-space-route-b{0%{opacity:0;transform:translate3d(var(--dkan-beta-x),var(--dkan-beta-y),0) rotate(-90deg)}7%,86%{opacity:.9}100%{opacity:0;transform:translate3d(var(--dkan-gamma-x),var(--dkan-gamma-y),0) rotate(-90deg)}}@keyframes dkan-space-route-c{0%{opacity:0;transform:translate3d(var(--dkan-gamma-x),var(--dkan-gamma-y),0) rotate(-18deg)}7%,86%{opacity:.9}100%{opacity:0;transform:translate3d(var(--dkan-alpha-x),var(--dkan-alpha-y),0) rotate(-18deg)}}",
1859
+ // 完成归航:一艘货运旗舰从航线飞到真实会话输入框上方,货舱逐格表达实际输出量。
1860
+ ".dkan-delivery{position:fixed;left:0;top:0;width:210px;height:88px;z-index:9994;pointer-events:none;will-change:transform,opacity;contain:layout paint style;isolation:isolate;animation:dkan-delivery-flight var(--dkan-delivery-duration,8600ms) cubic-bezier(.22,.61,.36,1) forwards;}",
1861
+ "@keyframes dkan-delivery-flight{0%{opacity:0;transform:translate3d(var(--dkan-from-x),var(--dkan-from-y),0) scale(.5) rotate(-12deg)}6%{opacity:1;transform:translate3d(var(--dkan-from-x),var(--dkan-from-y),0) scale(.56) rotate(-10deg)}100%{opacity:1;transform:translate3d(var(--dkan-to-x),var(--dkan-to-y),0) scale(1) rotate(0)}}",
1862
+ ".dkan-delivery-trail{position:absolute;left:-132px;top:26px;width:154px;height:4px;border-radius:999px;background:linear-gradient(90deg,transparent,#34d399 48%,#d9f99d 78%,transparent);opacity:.9;animation:dkan-delivery-trail var(--dkan-delivery-duration,8600ms) linear forwards;}@keyframes dkan-delivery-trail{0%,82%{opacity:.9}100%{opacity:0}}.dkan-freighter{position:absolute;left:10px;top:10px;width:176px;height:45px;display:block;filter:drop-shadow(0 0 9px color-mix(in srgb,#7dd3fc 62%,transparent));}.dkan-delivery .dkan-freighter,.dkan-departure .dkan-freighter{filter:none}.dkan-delivery .dkan-freighter-engine i,.dkan-departure .dkan-freighter-engine i{animation:none;opacity:.76;box-shadow:-6px 0 8px #22d3ee}.dkan-delivery .dkan-freighter-nose,.dkan-departure .dkan-freighter-nose{filter:none}.dkan-freighter-hull{position:absolute;z-index:3;left:21px;top:7px;width:126px;height:31px;background:linear-gradient(150deg,#f8fbff 0%,#9cc8ff 30%,#385d91 72%,#1d3150);clip-path:polygon(0 30%,43% 0,83% 17%,100% 50%,83% 83%,43% 100%,0 70%,12% 50%);box-shadow:inset 0 1px 0 rgb(255 255 255 / .7),inset -7px -5px 9px rgb(8 24 49 / .38);}.dkan-freighter-nose{position:absolute;z-index:4;right:10px;top:16px;width:28px;height:14px;background:linear-gradient(90deg,#a9d1ff,#e8f5ff);clip-path:polygon(0 0,100% 50%,0 100%,22% 50%);filter:drop-shadow(5px 0 5px color-mix(in srgb,#93c5fd 62%,transparent));}.dkan-freighter-canopy{position:absolute;z-index:5;left:77px;top:10px;width:39px;height:11px;border-radius:70% 48% 42% 32%;background:linear-gradient(135deg,#ecfeff 0%,#67e8f9 32%,#2563eb 75%);transform:skewX(-18deg);box-shadow:inset 0 2px 2px rgb(255 255 255 / .7),0 0 8px color-mix(in srgb,#67e8f9 42%,transparent);}.dkan-freighter-fin{position:absolute;z-index:2;left:48px;top:3px;width:24px;height:17px;background:linear-gradient(135deg,#31557f,#a7caf7);clip-path:polygon(0 100%,58% 0,100% 100%);}.dkan-freighter-wing{position:absolute;z-index:1;left:48px;width:60px;height:18px;background:linear-gradient(135deg,#24436d,#8ebaf4);}.dkan-freighter-wing.left{top:2px;clip-path:polygon(0 100%,100% 0,68% 100%)}.dkan-freighter-wing.right{top:26px;clip-path:polygon(0 0,68% 0,100% 100%)}",
1863
+ ".dkan-freighter-cargo{position:absolute;z-index:6;left:37px;top:31px;width:74px;height:7px;display:flex;align-items:center;gap:2px;padding:1px 3px;border-radius:4px;background:#172842;border:1px solid color-mix(in srgb,#b8d8ff 48%,transparent);box-shadow:inset 0 1px 0 rgb(255 255 255 / .2);}.dkan-freighter-cargo i{display:block;flex:1;min-width:4px;height:5px;border-radius:1px;background:linear-gradient(135deg,#fef3c7,#f59e0b 58%,#b45309);box-shadow:0 0 4px color-mix(in srgb,#fbbf24 58%,transparent);}.dkan-freighter-cargo.empty i{background:linear-gradient(135deg,#dbeafe,#38bdf8 58%,#1d4ed8);box-shadow:0 0 4px color-mix(in srgb,#38bdf8 55%,transparent);}.dkan-freighter-engine{position:absolute;z-index:0;left:5px;top:19px;display:flex;gap:3px;align-items:center;height:11px;}.dkan-freighter-engine i{width:18px;height:5px;border-radius:60% 0 0 60%;background:linear-gradient(90deg,transparent,#67e8f9);box-shadow:-9px 0 12px #22d3ee;animation:dkan-engine .34s ease-in-out infinite alternate;}.dkan-freighter-engine i:nth-child(2){animation-delay:-.14s}",
1864
+ "@keyframes dkan-engine{to{transform:scaleX(.55);opacity:.5}}",
1865
+ ".dkan-delivery-label{position:absolute;left:21px;top:58px;display:flex;align-items:baseline;gap:6px;padding:4px 8px;border:1px solid color-mix(in srgb,#93c5fd 30%,var(--dsw-alias-border-l1));border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 82%,transparent);backdrop-filter:blur(10px);color:var(--dsw-alias-label-secondary);font:10px/1.2 var(--ds-font-family,system-ui,sans-serif);white-space:nowrap;}.dkan-delivery-label b{color:var(--dsw-alias-label-primary);font-weight:650}.dkan-delivery-label span{color:var(--dk-accent);}",
1866
+ "@media (max-width:1100px){.dkan-space-system,.dkan-space-runner{display:none}}",
1867
+ ".dkan-ready-freighter{position:fixed;width:210px;height:88px;z-index:9994;pointer-events:none;filter:drop-shadow(0 10px 14px rgb(0 0 0 / .18));}.dkan-ready-freighter>span:last-child{position:absolute;left:21px;top:58px;padding:4px 8px;border:1px solid color-mix(in srgb,#93c5fd 28%,var(--dsw-alias-border-l1));border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 80%,transparent);backdrop-filter:blur(10px);color:var(--dsw-alias-label-secondary);font:10px/1.2 var(--ds-font-family,system-ui,sans-serif);white-space:nowrap;}.dkan-departure{position:fixed;left:0;top:0;width:210px;height:88px;z-index:9994;pointer-events:none;will-change:transform,opacity;contain:layout paint style;isolation:isolate;animation:dkan-departure-flight var(--dkan-departure-duration,8600ms) cubic-bezier(.42,0,.85,.45) forwards;}@keyframes dkan-departure-flight{0%{opacity:0;transform:translate3d(var(--dkan-from-x),var(--dkan-from-y),0) scale(1) rotate(var(--dkan-flight-angle,0deg))}6%{opacity:1;transform:translate3d(var(--dkan-from-x),var(--dkan-from-y),0) scale(1) rotate(var(--dkan-flight-angle,0deg))}100%{opacity:0;transform:translate3d(var(--dkan-to-x),var(--dkan-to-y),0) scale(.24) rotate(var(--dkan-flight-angle,0deg))}}",
1868
+ ".dkan-space-stuck{position:fixed;right:20px;top:88px;width:244px;height:160px;z-index:9993;pointer-events:none;border:1px solid color-mix(in srgb,#f59e0b 48%,var(--dsw-alias-border-l1));border-radius:20px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 88%,transparent);backdrop-filter:blur(16px);box-shadow:0 18px 54px rgb(0 0 0 / .28),0 0 30px color-mix(in srgb,#f59e0b 15%,transparent);}.dkan-space-stuck .dkan-freighter{left:37px;top:11px;transform:scale(.9);transform-origin:top left;}.dkan-stuck-code{position:absolute;left:17px;right:17px;top:60px;height:36px;border-radius:7px;background:linear-gradient(90deg,color-mix(in srgb,#f59e0b 18%,transparent),transparent);color:var(--dk-warn);font:700 14px/36px var(--ds-font-family-code,monospace);letter-spacing:.12em;text-align:center;overflow:hidden;}.dkan-stuck-code::after{content:'';position:absolute;inset:0;background:linear-gradient(90deg,transparent,color-mix(in srgb,#fef3c7 70%,transparent),transparent);transform:translateX(-110%);animation:dkan-stuck-scan 1.25s linear infinite;}.dkan-stuck-code i{display:inline-block;width:7px;height:7px;margin-right:8px;border-radius:50%;background:#f59e0b;box-shadow:0 0 9px #f59e0b;animation:dkan-stuck-pulse .8s ease-in-out infinite alternate;}.dkan-space-stuck strong{position:absolute;left:0;right:0;top:108px;color:var(--dsw-alias-label-primary);font:650 13px/1.2 var(--ds-font-family,system-ui,sans-serif);text-align:center;}.dkan-space-stuck small{position:absolute;left:18px;right:18px;top:128px;color:var(--dsw-alias-label-secondary);font:11px/1.25 var(--ds-font-family,system-ui,sans-serif);text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}@keyframes dkan-stuck-scan{to{transform:translateX(110%)}}@keyframes dkan-stuck-pulse{to{transform:scale(.55);opacity:.45}}",
1869
+ // 窄屏(手机/分屏)贴右会溢出:回落到水平居中、屏幕上方,避开正文重心
1870
+ "@media (max-width:760px){.dkan-space-stuck{right:50%;transform:translateX(50%);top:64px;}}",
1871
+ // 极光:顶部柔光带呼吸流动(加浓)
1872
+ ".dkan-aurora i{position:absolute;top:-40%;left:-20%;width:70%;height:80%;border-radius:50%;filter:blur(50px);opacity:.32;animation:dkan-aurora ease-in-out infinite alternate;}",
1873
+ ".dkan-aurora i:nth-child(1){background:#4d9fff;}",
1874
+ ".dkan-aurora i:nth-child(2){background:#34d399;left:20%;animation-delay:-2s;}",
1875
+ ".dkan-aurora i:nth-child(3){background:#a78bfa;left:55%;animation-delay:-4s;}",
1876
+ "@keyframes dkan-aurora{0%{transform:translateX(-8%) scaleY(1)}100%{transform:translateX(10%) scaleY(1.3)}}",
1877
+ // 星云潮汐:无动态模糊,柔边由径向渐变生成,移动只发生在合成层。
1878
+ ".dkan-nebula{contain:strict}.dkan-nebula i{position:absolute;width:48vw;aspect-ratio:1;border-radius:50%;opacity:.22;background:radial-gradient(circle,color-mix(in srgb,var(--dkan-phase,#60a5fa) 48%,transparent),color-mix(in srgb,#8b5cf6 18%,transparent) 42%,transparent 70%);animation:dkan-nebula-drift calc(18s / var(--dkan-speed,1)) cubic-bezier(.45,0,.55,1) infinite alternate;will-change:transform,opacity}.dkan-nebula i:nth-child(1){left:-18%;top:-24%}.dkan-nebula i:nth-child(2){right:-18%;top:4%;background:radial-gradient(circle,color-mix(in srgb,#22d3ee 34%,transparent),color-mix(in srgb,#3b82f6 15%,transparent) 45%,transparent 72%);animation-delay:-5s}.dkan-nebula i:nth-child(3){left:4%;bottom:-34%;background:radial-gradient(circle,color-mix(in srgb,#f472b6 28%,transparent),color-mix(in srgb,#8b5cf6 14%,transparent) 48%,transparent 72%);animation-delay:-10s}.dkan-nebula i:nth-child(4){right:8%;bottom:-26%;width:36vw;animation-delay:-14s}",
1879
+ "@keyframes dkan-nebula-drift{0%{opacity:.14;transform:translate3d(-3vw,-2vh,0) scale(.92)}100%{opacity:.3;transform:translate3d(5vw,4vh,0) scale(1.12)}}",
1880
+ // 曲速航道:从视口中心放射的独立光束。
1881
+ ".dkan-warp{background:radial-gradient(circle at center,color-mix(in srgb,var(--dkan-phase,#60a5fa) 10%,transparent),transparent 34%);contain:strict}.dkan-warp i{position:absolute;left:50%;top:50%;width:var(--len);height:1px;border-radius:999px;transform-origin:left center;background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--dkan-phase,#60a5fa) 48%,transparent),#e0f2fe);opacity:0;animation-name:dkan-warp-ray;animation-timing-function:cubic-bezier(.3,.1,.8,1);animation-iteration-count:infinite;will-change:transform,opacity}",
1882
+ "@keyframes dkan-warp-ray{0%{opacity:0;transform:rotate(var(--a)) translate3d(18px,0,0) scaleX(.05)}18%{opacity:.6}100%{opacity:0;transform:rotate(var(--a)) translate3d(58vw,0,0) scaleX(1.55)}}",
1883
+ // 量子雷达:三个边缘站点各自扫描,避免占据会话正文中心。
1884
+ ".dkan-radar{contain:strict}.dkan-radar-station{position:absolute;width:176px;height:176px;border-radius:50%;opacity:.44;background:repeating-radial-gradient(circle,color-mix(in srgb,var(--dkan-phase,#60a5fa) 28%,transparent) 0 1px,transparent 1px 29px);border:1px solid color-mix(in srgb,var(--dkan-phase,#60a5fa) 22%,transparent)}.dkan-radar-station.a{right:4%;top:8%}.dkan-radar-station.b{left:1%;top:38%;transform:scale(.72)}.dkan-radar-station.c{right:7%;bottom:9%;transform:scale(.58)}.dkan-radar-station i{position:absolute;inset:0;border-radius:50%;background:conic-gradient(from 0deg,transparent 0 76%,color-mix(in srgb,var(--dkan-phase,#60a5fa) 44%,transparent) 92%,transparent);animation:dkan-radar-sweep calc(4.8s / var(--dkan-speed,1)) linear infinite;will-change:transform}.dkan-radar-station b{position:absolute;left:67%;top:31%;width:7px;height:7px;border-radius:50%;background:var(--dkan-phase,#60a5fa);box-shadow:0 0 12px 4px color-mix(in srgb,var(--dkan-phase,#60a5fa) 58%,transparent);animation:dkan-radar-blip calc(2.4s / var(--dkan-speed,1)) ease-in-out infinite}",
1885
+ "@keyframes dkan-radar-sweep{to{transform:rotate(360deg)}}@keyframes dkan-radar-blip{0%,70%,100%{opacity:.15;transform:scale(.65)}78%{opacity:1;transform:scale(1.25)}}",
1886
+ // 星座网络:轻量 SVG 线网分段点亮。
1887
+ ".dkan-constellation svg{position:absolute;left:8%;top:7%;width:86%;height:78%;overflow:visible}.dkan-constellation path{fill:none;stroke:color-mix(in srgb,var(--dkan-phase,#60a5fa) 42%,transparent);stroke-width:.16;vector-effect:non-scaling-stroke;stroke-dasharray:.08 .025;animation:dkan-constellation-line calc(7s / var(--dkan-speed,1)) linear infinite}.dkan-constellation circle{fill:var(--dkan-phase,#60a5fa);filter:drop-shadow(0 0 2px var(--dkan-phase,#60a5fa));animation:dkan-constellation-star calc(2.8s / var(--dkan-speed,1)) ease-in-out infinite alternate;transform-box:fill-box;transform-origin:center}",
1888
+ "@keyframes dkan-constellation-line{to{stroke-dashoffset:-1}}@keyframes dkan-constellation-star{0%{opacity:.28;transform:scale(.68)}100%{opacity:.92;transform:scale(1.22)}}",
1889
+ // 数据萤火:二维漂移和明暗呼吸组合在同一个 transform 动画中。
1890
+ ".dkan-fireflies{contain:strict}.dkan-fireflies i{position:absolute;border-radius:50%;background:color-mix(in srgb,var(--dkan-phase,#60a5fa) 78%,#fff);box-shadow:0 0 8px 2px color-mix(in srgb,var(--dkan-phase,#60a5fa) 46%,transparent);opacity:.2;animation-name:dkan-firefly;animation-timing-function:cubic-bezier(.45,0,.55,1);animation-iteration-count:infinite;animation-direction:alternate;will-change:transform,opacity}",
1891
+ "@keyframes dkan-firefly{0%{opacity:.12;transform:translate3d(0,0,0) scale(.65)}48%{opacity:.86}100%{opacity:.26;transform:translate3d(var(--dx),var(--dy),0) scale(1.18)}}",
1892
+ // 深海脉动:波层在底部横向漂移,气泡只做向上合成位移。
1893
+ ".dkan-ocean{background:linear-gradient(0deg,color-mix(in srgb,#0284c7 12%,transparent),transparent 46%);contain:strict}.dkan-ocean-waves{position:absolute;left:-12%;right:-12%;bottom:0;height:42%}.dkan-ocean-waves i{position:absolute;left:0;width:112%;height:52%;border-top:1px solid color-mix(in srgb,#67e8f9 28%,transparent);border-radius:50%;animation:dkan-ocean-wave calc(9s / var(--dkan-speed,1)) ease-in-out infinite alternate;will-change:transform}.dkan-ocean-waves i:nth-child(1){top:2%}.dkan-ocean-waves i:nth-child(2){top:18%;animation-delay:-2s}.dkan-ocean-waves i:nth-child(3){top:34%;animation-delay:-4s}.dkan-ocean-waves i:nth-child(4){top:50%;animation-delay:-6s}.dkan-ocean-waves i:nth-child(5){top:66%;animation-delay:-8s}.dkan-ocean>b{position:absolute;bottom:-20px;border:1px solid color-mix(in srgb,#a5f3fc 54%,transparent);border-radius:50%;opacity:0;animation-name:dkan-ocean-bubble;animation-timing-function:linear;animation-iteration-count:infinite;will-change:transform,opacity}",
1894
+ "@keyframes dkan-ocean-wave{to{transform:translate3d(7%,5px,0) scaleY(1.08)}}@keyframes dkan-ocean-bubble{0%{opacity:0;transform:translate3d(0,0,0) scale(.7)}14%{opacity:.58}100%{opacity:0;transform:translate3d(var(--drift),-82vh,0) scale(1.18)}}",
1895
+ // 棱镜光谱:七条低透明度光带错峰穿过视口。
1896
+ ".dkan-prism{contain:strict}.dkan-prism i{position:absolute;left:-32vw;width:160vw;height:2px;border-radius:999px;opacity:0;background:linear-gradient(90deg,transparent,#ef4444,#f59e0b,#facc15,#22c55e,#22d3ee,#6366f1,#a855f7,transparent);animation:dkan-prism-ray calc(8s / var(--dkan-speed,1)) cubic-bezier(.45,0,.55,1) infinite;will-change:transform,opacity}.dkan-prism i:nth-child(1){top:12%;animation-delay:0s}.dkan-prism i:nth-child(2){top:25%;animation-delay:-1.1s}.dkan-prism i:nth-child(3){top:38%;animation-delay:-2.2s}.dkan-prism i:nth-child(4){top:51%;animation-delay:-3.3s}.dkan-prism i:nth-child(5){top:64%;animation-delay:-4.4s}.dkan-prism i:nth-child(6){top:77%;animation-delay:-5.5s}.dkan-prism i:nth-child(7){top:90%;animation-delay:-6.6s}",
1897
+ "@keyframes dkan-prism-ray{0%{opacity:0;transform:translate3d(-12vw,0,0) rotate(-7deg) scaleX(.7)}34%{opacity:.2}66%{opacity:.36}100%{opacity:0;transform:translate3d(20vw,0,0) rotate(-7deg) scaleX(1.05)}}",
1898
+ // 神经电路:少量 SVG 路径的虚线脉冲,节点保持低透明度。
1899
+ ".dkan-circuit svg{position:absolute;inset:5% 3% 9% 8%;width:89%;height:86%;overflow:visible}.dkan-circuit path{fill:none;stroke:color-mix(in srgb,var(--dkan-phase,#60a5fa) 40%,transparent);stroke-width:1.2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:.045 .12;animation:dkan-circuit-pulse calc(5.6s / var(--dkan-speed,1)) linear infinite}.dkan-circuit circle{fill:var(--dkan-phase,#60a5fa);opacity:.58;filter:drop-shadow(0 0 4px var(--dkan-phase,#60a5fa));animation:dkan-circuit-node calc(2.2s / var(--dkan-speed,1)) ease-in-out infinite alternate}",
1900
+ "@keyframes dkan-circuit-pulse{to{stroke-dashoffset:-1}}@keyframes dkan-circuit-node{to{opacity:.16}}",
1901
+ // 引力涟漪:三个边缘引力源按错峰节奏扩散。
1902
+ ".dkan-gravity{contain:strict}.dkan-gravity-source{position:absolute;width:260px;height:260px}.dkan-gravity-source.a{right:2%;top:5%}.dkan-gravity-source.b{left:-4%;top:39%;transform:scale(.76)}.dkan-gravity-source.c{right:17%;bottom:-10%;transform:scale(.62)}.dkan-gravity-source b{position:absolute;left:50%;top:50%;width:12px;height:12px;margin:-6px;border-radius:50%;background:var(--dkan-phase,#60a5fa);box-shadow:0 0 16px 5px color-mix(in srgb,var(--dkan-phase,#60a5fa) 42%,transparent)}.dkan-gravity-source i{position:absolute;left:50%;top:50%;width:26px;height:26px;margin:-13px;border:1px solid color-mix(in srgb,var(--dkan-phase,#60a5fa) 42%,transparent);border-radius:50%;opacity:0;animation:dkan-gravity-wave calc(4.6s / var(--dkan-speed,1)) cubic-bezier(.2,.55,.35,1) infinite;will-change:transform,opacity}",
1903
+ "@keyframes dkan-gravity-wave{0%{opacity:.64;transform:scale(.3)}100%{opacity:0;transform:scale(9)}}",
1904
+ // 灵感天灯:暖色灯体从屏幕底部缓慢升起,尾迹由伪元素提供。
1905
+ ".dkan-lantern{contain:strict}.dkan-lantern i{position:absolute;bottom:-30px;border-radius:45% 45% 34% 34%;background:linear-gradient(150deg,#fff7c2 0%,#fbbf24 46%,#f97316 100%);box-shadow:0 0 10px 3px color-mix(in srgb,#fbbf24 36%,transparent);opacity:0;animation-name:dkan-lantern-rise;animation-timing-function:cubic-bezier(.34,.02,.6,1);animation-iteration-count:infinite;will-change:transform,opacity}.dkan-lantern i::after{content:'';position:absolute;left:42%;top:100%;width:16%;height:13px;background:linear-gradient(#f59e0b,transparent)}",
1906
+ "@keyframes dkan-lantern-rise{0%{opacity:0;transform:translate3d(0,0,0) rotate(-3deg) scale(.72)}12%{opacity:.72}78%{opacity:.48}100%{opacity:0;transform:translate3d(var(--drift),-104vh,0) rotate(4deg) scale(1.04)}}",
1907
+ // ===== 交互反馈层 =====
1908
+ ".dkan-fxwrap{position:fixed;inset:0;z-index:9992;pointer-events:none;overflow:hidden;}",
1909
+ ".dkan-fx{position:absolute;}",
1910
+ // 纸飞机:从点击处向右上飞出淡出(加大加亮)
1911
+ ".dkan-fx-plane{color:var(--dk-accent);font-size:24px;text-shadow:0 0 8px color-mix(in srgb,var(--dk-accent) 70%,transparent);animation:dkan-plane 1.2s var(--ds-ease-in-out) forwards;}",
1912
+ "@keyframes dkan-plane{0%{opacity:0;transform:translate(0,0) rotate(-20deg) scale(.6)}15%{opacity:1;transform:translate(20px,-14px) rotate(-20deg) scale(1.1)}100%{opacity:0;transform:translate(240px,-160px) rotate(-20deg) scale(1)}}",
1913
+ // 火花:粒子四散(加大加多散得更开)
1914
+ ".dkan-fx-spark i{position:absolute;width:7px;height:7px;border-radius:50%;box-shadow:0 0 6px currentColor;animation:dkan-spark .9s var(--ds-ease-in-out) forwards;}",
1915
+ "@keyframes dkan-spark{0%{opacity:1;transform:translate(0,0) scale(1)}100%{opacity:0;transform:translate(var(--dx),var(--dy)) scale(.4)}}",
1916
+ // 流光:横向扫过(切会话,加高加亮)
1917
+ ".dkan-fx-streak{top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,transparent,var(--dk-accent) 50%,#fff);background-size:45% 100%;background-repeat:no-repeat;box-shadow:0 0 12px 2px color-mix(in srgb,var(--dk-accent) 50%,transparent);animation:dkan-streak 1s var(--ds-ease-in-out) forwards;}",
1918
+ "@keyframes dkan-streak{0%{background-position:-50% 0;opacity:0}20%{opacity:1}100%{background-position:150% 0;opacity:0}}",
1919
+ // 微光:全屏柔光一闪(切工作目录,加浓)
1920
+ ".dkan-fx-flash{inset:0;background:radial-gradient(ellipse at center,color-mix(in srgb,var(--dk-accent) 26%,transparent),transparent 75%);animation:dkan-flash .8s var(--ds-ease-in-out) forwards;}",
1921
+ "@keyframes dkan-flash{0%{opacity:0}30%{opacity:1}100%{opacity:0}}",
1922
+ // 光涌:底部向上涌起(任务开始,加高加浓)
1923
+ ".dkan-fx-surge{left:0;right:0;bottom:0;height:42%;background:linear-gradient(0deg,color-mix(in srgb,var(--dsw-alias-state-success-primary,#34d399) 38%,transparent),transparent);animation:dkan-surge 1.2s var(--ds-ease-in-out) forwards;}",
1924
+ "@keyframes dkan-surge{0%{opacity:0;transform:translateY(45%)}30%{opacity:1}100%{opacity:0;transform:translateY(-35%)}}",
1925
+ // 彩带:顶部飘落(任务完成庆祝,加大加多)
1926
+ ".dkan-fx-confetti{inset:0;}",
1927
+ ".dkan-fx-confetti i{position:absolute;top:-14px;width:8px;height:13px;border-radius:2px;box-shadow:0 0 4px currentColor;animation:dkan-confetti 2s var(--ds-ease-in-out) forwards;}",
1928
+ "@keyframes dkan-confetti{0%{opacity:1;transform:translateY(0) rotate(0)}100%{opacity:0;transform:translateY(75vh) translateX(var(--dx)) rotate(var(--r))}}",
1929
+ // 右下角状态徽标(玻璃拟态,可点击);抬到 dsh 输入卡上方避免重叠错位
1930
+ ".dkan-badge{position:fixed;right:20px;bottom:calc(var(--dsh-composer-height,152px) + 20px);z-index:9990;display:inline-flex;align-items:center;gap:8px;padding:7px 14px;border-radius:999px;cursor:pointer;border:1px solid var(--dsw-alias-border-l1);background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 78%,transparent);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);box-shadow:0 6px 24px rgb(0 0 0 / .16);color:var(--dsw-alias-label-secondary);font-family:inherit;font-size:12px;line-height:18px;pointer-events:auto;animation:dkan-rise .3s var(--ds-ease-in-out);transition:color .15s var(--ds-ease-in-out);}",
1931
+ ".dkan-badge:hover{color:var(--dsw-alias-label-primary);}",
1932
+ ".dkan-badge .n{color:var(--dsw-alias-label-primary);font-weight:600;}",
1933
+ "@keyframes dkan-rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}",
1934
+ // 徽标圆点与两种动效(呼吸 / 轨道环)
1935
+ ".dkan-dotwrap{position:relative;width:20px;height:20px;flex:none;display:inline-flex;align-items:center;justify-content:center;}",
1936
+ ".dkan-dot{width:12px;height:12px;border-radius:50%;background:var(--dkan-phase,var(--dk-accent));box-shadow:0 0 10px color-mix(in srgb,var(--dkan-phase,var(--dk-accent)) 70%,transparent);transition:background .4s var(--ds-ease-in-out),box-shadow .4s var(--ds-ease-in-out);}",
1937
+ ".dkan-breathe .dkan-dot{animation:dkan-breathe calc(2.2s / var(--dkan-speed,1)) ease-in-out infinite;}",
1938
+ "@keyframes dkan-breathe{0%,100%{transform:scale(1);opacity:.5}50%{transform:scale(1.7);opacity:1}}",
1939
+ // 呼吸光晕:向外扩散的波纹环,与圆点同频(颜色随阶段)
1940
+ ".dkan-halo{position:absolute;inset:0;border-radius:50%;border:2px solid var(--dkan-phase,var(--dk-accent));animation:dkan-halo calc(2.2s / var(--dkan-speed,1)) ease-out infinite;}",
1941
+ "@keyframes dkan-halo{0%{transform:scale(.5);opacity:.9}70%{transform:scale(1.5);opacity:.25}100%{transform:scale(1.7);opacity:0}}",
1942
+ // breathe 模式徽标更醒目:阶段色描边 + 呼吸辉光
1943
+ ".dkan-badge-breathe{border-color:color-mix(in srgb,var(--dkan-phase,#4d9fff) 55%,transparent);animation:dkan-rise .3s var(--ds-ease-in-out),dkan-badge-breathe calc(2.2s / var(--dkan-speed,1)) ease-in-out infinite;}",
1944
+ "@keyframes dkan-badge-breathe{0%,100%{box-shadow:0 0 6px color-mix(in srgb,var(--dkan-phase,#4d9fff) 22%,transparent),0 6px 24px rgb(0 0 0 / .16)}50%{box-shadow:0 0 18px color-mix(in srgb,var(--dkan-phase,#4d9fff) 55%,transparent),0 6px 24px rgb(0 0 0 / .16)}}",
1945
+ ".dkan-ring{position:absolute;inset:0;border-radius:50%;background:conic-gradient(from 0deg,transparent 0 68%,var(--dk-accent) 92%,#fff);-webkit-mask:radial-gradient(farthest-side,transparent calc(100% - 2px),#000 calc(100% - 2px));mask:radial-gradient(farthest-side,transparent calc(100% - 2px),#000 calc(100% - 2px));animation:dkan-spin calc(2.2s / var(--dkan-speed,1)) linear infinite;opacity:.9;}",
1946
+ "@keyframes dkan-spin{to{transform:rotate(360deg)}}",
1947
+ // 通知卡片栈(右上角)
1948
+ ".dkan-toasts{position:fixed;top:16px;right:16px;z-index:9995;display:flex;flex-direction:column;gap:8px;width:min(380px,calc(100vw - 32px));}",
1949
+ ".dkan-toast{border-radius:12px;padding:12px 14px;pointer-events:auto;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 88%,transparent);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--dsw-alias-border-l1);box-shadow:0 10px 36px rgb(0 0 0 / .22);animation:dkan-toast-in .32s var(--ds-ease-in-out);}",
1950
+ ".dkan-toast.out{animation:dkan-toast-out .24s var(--ds-ease-in-out) forwards;}",
1951
+ "@keyframes dkan-toast-in{from{opacity:0;transform:translateX(14px)}to{opacity:1;transform:none}}",
1952
+ "@keyframes dkan-toast-out{to{opacity:0;transform:translateX(10px)}}",
1953
+ ".dkan-toast-head{display:flex;align-items:center;gap:8px;margin-bottom:4px;}",
1954
+ ".dkan-toast-mark{width:8px;height:8px;border-radius:50%;flex:none;}",
1955
+ ".dkan-toast-title{font-weight:600;font-size:13px;color:var(--dsw-alias-label-primary);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
1956
+ ".dkan-toast-close{cursor:pointer;flex:none;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);border-radius:6px;width:22px;height:22px;font-size:11px;line-height:1;display:inline-flex;align-items:center;justify-content:center;font-family:inherit;}",
1957
+ ".dkan-toast-close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
1958
+ ".dkan-toast-body{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.7;white-space:pre-line;word-break:break-word;}",
1959
+ // 需确认卡片:琥珀描边 + 缓慢呼吸光晕,常驻提醒直到用户处理
1960
+ ".dkan-toast-confirm{border-color:color-mix(in srgb,var(--dk-warn) 55%,transparent);animation:dkan-toast-in .32s var(--ds-ease-in-out),dkan-confirm-glow 2.4s ease-in-out 0.4s infinite;}",
1961
+ "@keyframes dkan-confirm-glow{0%,100%{box-shadow:0 10px 36px rgb(0 0 0 / .22),0 0 0 0 color-mix(in srgb,var(--dk-warn) 30%,transparent)}50%{box-shadow:0 10px 36px rgb(0 0 0 / .22),0 0 14px 2px color-mix(in srgb,var(--dk-warn) 32%,transparent)}}",
1962
+ // 面板页布局
1963
+ ".dkan-root{display:flex;flex-direction:column;gap:10px;}",
1964
+ ".dkan-note{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.6;}",
1965
+ ".dkan-err{color:var(--dsw-alias-state-error-primary);}",
1966
+ ".dkan-sec{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:8px;}",
1967
+ ".dkan-sec-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
1968
+ ".dkan-sec-title{font-weight:600;font-size:13px;color:var(--dsw-alias-label-primary);flex:none;}",
1969
+ ".dkan-sec-sub{font-size:11px;color:var(--dsw-alias-label-tertiary);flex:1;min-width:120px;}",
1970
+ ".dkan-sec-sw{margin-left:auto;flex:none;display:inline-flex;align-items:center;gap:8px;}",
1971
+ ".dkan-sec-swlabel{font-size:11px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;}",
1972
+ ".dkan-sec-swlabel.on{color:var(--dsw-alias-state-success-primary);}",
1973
+ ".dkan-refresh{cursor:pointer;flex:none;color:var(--dsw-alias-label-primary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:2px 10px;font-family:inherit;font-size:12px;}",
1974
+ ".dkan-refresh:hover{background:var(--dsw-alias-interactive-bg-hover);}",
1975
+ // 模式选择卡(带缩微预览)
1976
+ ".dkan-modes{display:flex;gap:8px;flex-wrap:wrap;}",
1977
+ ".dkan-mode{flex:1;min-width:150px;display:flex;flex-direction:column;gap:6px;padding:10px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-2);cursor:pointer;font-family:inherit;text-align:left;transition:border-color .15s var(--ds-ease-in-out);}",
1978
+ ".dkan-mode:hover{border-color:var(--dk-accent);}",
1979
+ ".dkan-mode.on{border-color:var(--dk-accent);box-shadow:0 0 0 1px color-mix(in srgb,var(--dk-accent) 40%,transparent);}",
1980
+ ".dkan-mode-name{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:6px;}",
1981
+ ".dkan-mode.on .dkan-mode-name::after{content:\"✓\";color:var(--dk-accent);}",
1982
+ ".dkan-mode-desc{font-size:11px;color:var(--dsw-alias-label-tertiary);line-height:1.5;}",
1983
+ ".dkan-robot-controls{display:flex;flex-direction:column;gap:8px;padding:9px 10px;border:1px solid color-mix(in srgb,var(--dk-accent) 28%,var(--dsw-alias-border-l1));border-radius:9px;background:color-mix(in srgb,var(--dk-accent) 5%,var(--dsw-alias-bg-layer-2));}",
1984
+ ".dkan-robot-controls-head{display:flex;align-items:center;justify-content:space-between;font-size:12px;color:var(--dsw-alias-label-primary);}",
1985
+ ".dkan-robot-controls-head strong{font-variant-numeric:tabular-nums;color:var(--dk-accent);}",
1986
+ ".dkan-robot-size-options{display:flex;gap:6px;flex-wrap:wrap;}",
1987
+ ".dkan-robot-size-options button{cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;padding:3px 10px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;}",
1988
+ ".dkan-robot-size-options button:hover,.dkan-robot-size-options button.on{border-color:var(--dk-accent);color:var(--dsw-alias-label-primary);}",
1989
+ ".dkan-robot-slider-row{display:flex;align-items:center;gap:9px;flex-wrap:wrap;font-size:11px;color:var(--dsw-alias-label-tertiary);}",
1990
+ ".dkan-robot-slider-row input{accent-color:var(--dk-accent);width:min(240px,100%);}",
1991
+ ".dkan-prev{height:30px;border-radius:6px;background:var(--dsw-alias-bg-layer-1);position:relative;overflow:hidden;display:flex;align-items:center;justify-content:center;}",
1992
+ ".dkan-prev-flow{position:absolute;top:0;left:0;right:0;height:3px;background:color-mix(in srgb,var(--dk-accent) 30%,transparent);}",
1993
+ ".dkan-prev-flow::after{content:\"\";position:absolute;top:0;bottom:0;left:-40%;width:40%;background:linear-gradient(90deg,transparent,var(--dk-accent));animation:dkan-flow 1.9s linear infinite;}",
1994
+ ".dkan-prev-box{--dkan-speed:1;}",
1995
+ ".dkan-prev-orbit{position:absolute;top:3px;left:3px;width:7px;height:7px;border-radius:50%;background:var(--dk-accent);box-shadow:0 0 7px 2px color-mix(in srgb,var(--dk-accent) 55%,transparent);animation:dkan-prev-orbit 3s linear infinite;}",
1996
+ "@keyframes dkan-prev-orbit{0%{top:3px;left:3px}25%{top:3px;left:calc(100% - 10px)}50%{top:calc(100% - 10px);left:calc(100% - 10px)}75%{top:calc(100% - 10px);left:3px}100%{top:3px;left:3px}}",
1997
+ // 预览:代码雨(字符列下落)
1998
+ ".dkan-prev-matrix i{position:absolute;top:-100%;writing-mode:vertical-rl;font-size:8px;font-family:var(--ds-font-family-code,monospace);color:var(--dk-accent);opacity:.5;animation:dkan-prev-fall 2.2s linear infinite;}",
1999
+ ".dkan-prev-matrix i:nth-child(1){left:12%;}",
2000
+ ".dkan-prev-matrix i:nth-child(2){left:32%;animation-delay:-.6s;}",
2001
+ ".dkan-prev-matrix i:nth-child(3){left:52%;animation-delay:-1.2s;}",
2002
+ ".dkan-prev-matrix i:nth-child(4){left:72%;animation-delay:-.3s;}",
2003
+ ".dkan-prev-matrix i:nth-child(5){left:88%;animation-delay:-1.6s;}",
2004
+ ".dkan-prev-matrix i::after{content:\"01</>\";}",
2005
+ "@keyframes dkan-prev-fall{to{transform:translateY(300%)}}",
2006
+ // 预览:星野(星点闪烁)
2007
+ ".dkan-prev-stars i{position:absolute;width:2px;height:2px;border-radius:50%;background:var(--dsw-alias-label-secondary);animation:dkan-prev-star 1.6s ease-in-out infinite alternate;}",
2008
+ ".dkan-prev-stars i:nth-child(1){left:15%;top:25%;}",
2009
+ ".dkan-prev-stars i:nth-child(2){left:40%;top:60%;animation-delay:-.4s;}",
2010
+ ".dkan-prev-stars i:nth-child(3){left:60%;top:30%;animation-delay:-.8s;}",
2011
+ ".dkan-prev-stars i:nth-child(4){left:80%;top:55%;animation-delay:-1.2s;}",
2012
+ ".dkan-prev-stars i:nth-child(5){left:28%;top:70%;animation-delay:-.6s;}",
2013
+ ".dkan-prev-stars i:nth-child(6){left:70%;top:75%;animation-delay:-1s;}",
2014
+ "@keyframes dkan-prev-star{0%{opacity:.2}100%{opacity:.9}}",
2015
+ // 预览:星际远征(行星、轨道与飞船)
2016
+ ".dkan-prev-space i{position:absolute;left:49%;top:50%;width:9px;height:9px;border-radius:50%;background:#fbbf24;box-shadow:0 0 6px #fbbf24;}.dkan-prev-space b{position:absolute;left:21%;top:23%;width:15px;height:8px;border:1px solid #93c5fd;border-radius:50%;animation:dkan-prev-space-orbit 2.4s linear infinite;}.dkan-prev-space b::after{content:'';position:absolute;left:-3px;top:0;width:5px;height:5px;border-radius:50%;background:#a78bfa;box-shadow:0 0 4px #a78bfa;}.dkan-prev-space em{position:absolute;left:5%;top:65%;width:19px;height:4px;border-radius:50% 0 0 50%;background:linear-gradient(90deg,#22d3ee,#dbeafe);animation:dkan-prev-space-ship 1.8s linear infinite;}.dkan-prev-space em::after{content:'';position:absolute;right:-7px;top:-3px;border-left:9px solid #93c5fd;border-top:5px solid transparent;border-bottom:5px solid transparent;}.dkan-prev-space{background:radial-gradient(ellipse at 78% 20%,color-mix(in srgb,#6366f1 18%,transparent),transparent 58%);}.dkan-prev-space i,.dkan-prev-space b,.dkan-prev-space em{font-style:normal;}.dkan-prev-space b{transform-origin:34px 11px;}@keyframes dkan-prev-space-orbit{to{transform:rotate(360deg)}}@keyframes dkan-prev-space-ship{to{transform:translateX(105px)}}",
2017
+ // 预览:极光(柔光带呼吸)
2018
+ ".dkan-prev-aurora i{position:absolute;top:-30%;width:60%;height:120%;border-radius:50%;filter:blur(8px);opacity:.35;animation:dkan-prev-aurora 2.4s ease-in-out infinite alternate;}",
2019
+ ".dkan-prev-aurora i:nth-child(1){left:5%;background:#4d9fff;}",
2020
+ ".dkan-prev-aurora i:nth-child(2){left:45%;background:#34d399;animation-delay:-1.2s;}",
2021
+ "@keyframes dkan-prev-aurora{0%{transform:translateX(-10%)}100%{transform:translateX(15%)}}",
2022
+ // 十种新增模式的缩微预览:复用主体运动语言,但把节点数和振幅压到卡片内。
2023
+ ".dkan-prev-new{--dkan-phase:var(--dk-accent)}.dkan-prev-new i{position:absolute;display:block}",
2024
+ ".dkan-prev-nebula i{width:48px;height:48px;border-radius:50%;background:radial-gradient(circle,color-mix(in srgb,var(--dkan-phase) 48%,transparent),transparent 70%);animation:dkan-prev-nebula 3s ease-in-out infinite alternate}.dkan-prev-nebula i:nth-child(1){left:2%;top:-60%}.dkan-prev-nebula i:nth-child(2){right:4%;bottom:-65%;background:radial-gradient(circle,color-mix(in srgb,#a855f7 42%,transparent),transparent 70%);animation-delay:-1.4s}.dkan-prev-nebula i:nth-child(n+3){display:none}@keyframes dkan-prev-nebula{to{transform:translateX(24px) scale(1.18)}}",
2025
+ ".dkan-prev-warp i{left:50%;top:50%;width:36px;height:1px;transform-origin:left;background:linear-gradient(90deg,transparent,var(--dkan-phase),#fff);animation:dkan-prev-warp 1.8s linear infinite;opacity:0}.dkan-prev-warp i:nth-child(1){--a:0deg}.dkan-prev-warp i:nth-child(2){--a:45deg;animation-delay:-.2s}.dkan-prev-warp i:nth-child(3){--a:90deg;animation-delay:-.4s}.dkan-prev-warp i:nth-child(4){--a:135deg;animation-delay:-.6s}.dkan-prev-warp i:nth-child(5){--a:180deg;animation-delay:-.8s}.dkan-prev-warp i:nth-child(6){--a:225deg;animation-delay:-1s}.dkan-prev-warp i:nth-child(7){--a:270deg;animation-delay:-1.2s}.dkan-prev-warp i:nth-child(8){--a:315deg;animation-delay:-1.4s}@keyframes dkan-prev-warp{0%{opacity:0;transform:rotate(var(--a)) translateX(2px) scaleX(.1)}35%{opacity:.8}100%{opacity:0;transform:rotate(var(--a)) translateX(40px) scaleX(1.3)}}",
2026
+ ".dkan-prev-radar i{left:50%;top:50%;width:24px;height:24px;margin:-12px;border:1px solid color-mix(in srgb,var(--dkan-phase) 48%,transparent);border-radius:50%;animation:dkan-prev-radar 2.6s ease-out infinite;opacity:0}.dkan-prev-radar i:nth-child(2){animation-delay:-.85s}.dkan-prev-radar i:nth-child(3){animation-delay:-1.7s}.dkan-prev-radar i:nth-child(n+4){display:none}@keyframes dkan-prev-radar{0%{opacity:.8;transform:scale(.2)}100%{opacity:0;transform:scale(1.25)}}",
2027
+ ".dkan-prev-constellation::after{content:'';position:absolute;left:12%;right:12%;top:50%;height:1px;background:linear-gradient(90deg,transparent,var(--dkan-phase),transparent);transform:rotate(-9deg)}.dkan-prev-constellation i,.dkan-prev-fireflies i{width:4px;height:4px;border-radius:50%;background:var(--dkan-phase);box-shadow:0 0 5px var(--dkan-phase);animation:dkan-prev-twinkle 1.8s ease-in-out infinite alternate}.dkan-prev-constellation i:nth-child(1),.dkan-prev-fireflies i:nth-child(1){left:12%;top:62%}.dkan-prev-constellation i:nth-child(2),.dkan-prev-fireflies i:nth-child(2){left:28%;top:26%;animation-delay:-.3s}.dkan-prev-constellation i:nth-child(3),.dkan-prev-fireflies i:nth-child(3){left:45%;top:52%;animation-delay:-.6s}.dkan-prev-constellation i:nth-child(4),.dkan-prev-fireflies i:nth-child(4){left:61%;top:18%;animation-delay:-.9s}.dkan-prev-constellation i:nth-child(5),.dkan-prev-fireflies i:nth-child(5){left:76%;top:58%;animation-delay:-1.2s}.dkan-prev-constellation i:nth-child(6),.dkan-prev-fireflies i:nth-child(6){left:88%;top:34%;animation-delay:-1.5s}@keyframes dkan-prev-twinkle{to{opacity:.2;transform:translateY(4px) scale(.65)}}",
2028
+ ".dkan-prev-ocean i{left:-10%;width:120%;height:18px;border-top:1px solid color-mix(in srgb,#22d3ee 52%,transparent);border-radius:50%;animation:dkan-prev-wave 2.8s ease-in-out infinite alternate}.dkan-prev-ocean i:nth-child(1){top:15%}.dkan-prev-ocean i:nth-child(2){top:35%;animation-delay:-.5s}.dkan-prev-ocean i:nth-child(3){top:55%;animation-delay:-1s}.dkan-prev-ocean i:nth-child(4){top:75%;animation-delay:-1.5s}.dkan-prev-ocean i:nth-child(n+5){display:none}@keyframes dkan-prev-wave{to{transform:translateX(12px) scaleY(1.2)}}",
2029
+ ".dkan-prev-prism i{left:-10%;width:120%;height:1px;background:linear-gradient(90deg,transparent,#ef4444,#fbbf24,#22c55e,#22d3ee,#8b5cf6,transparent);transform:rotate(-6deg);animation:dkan-prev-prism 2.8s ease-in-out infinite}.dkan-prev-prism i:nth-child(1){top:18%}.dkan-prev-prism i:nth-child(2){top:34%;animation-delay:-.4s}.dkan-prev-prism i:nth-child(3){top:50%;animation-delay:-.8s}.dkan-prev-prism i:nth-child(4){top:66%;animation-delay:-1.2s}.dkan-prev-prism i:nth-child(5){top:82%;animation-delay:-1.6s}.dkan-prev-prism i:nth-child(n+6){display:none}@keyframes dkan-prev-prism{0%,100%{opacity:.12;transform:translateX(-8px) rotate(-6deg)}50%{opacity:.72;transform:translateX(10px) rotate(-6deg)}}",
2030
+ ".dkan-prev-circuit i{height:1px;background:var(--dkan-phase);box-shadow:0 0 4px var(--dkan-phase);animation:dkan-prev-circuit 2.4s linear infinite}.dkan-prev-circuit i:nth-child(1){left:5%;top:30%;width:42%}.dkan-prev-circuit i:nth-child(2){left:47%;top:30%;width:1px;height:45%}.dkan-prev-circuit i:nth-child(3){left:47%;top:74%;width:38%}.dkan-prev-circuit i:nth-child(4){left:68%;top:15%;width:1px;height:60%}.dkan-prev-circuit i:nth-child(5){left:68%;top:15%;width:26%}.dkan-prev-circuit i:nth-child(6){left:12%;top:62%;width:35%}@keyframes dkan-prev-circuit{0%,100%{opacity:.18}50%{opacity:.9}}",
2031
+ ".dkan-prev-gravity i{left:50%;top:50%;width:18px;height:18px;margin:-9px;border:1px solid var(--dkan-phase);border-radius:50%;opacity:0;animation:dkan-prev-gravity 2.6s ease-out infinite}.dkan-prev-gravity i:nth-child(2){animation-delay:-.65s}.dkan-prev-gravity i:nth-child(3){animation-delay:-1.3s}.dkan-prev-gravity i:nth-child(4){animation-delay:-1.95s}.dkan-prev-gravity i:nth-child(n+5){display:none}@keyframes dkan-prev-gravity{0%{opacity:.8;transform:scale(.15)}100%{opacity:0;transform:scale(2.4)}}",
2032
+ ".dkan-prev-lantern i{bottom:-8px;width:6px;height:8px;border-radius:3px;background:#fbbf24;box-shadow:0 0 5px #f59e0b;animation:dkan-prev-lantern 3.2s linear infinite}.dkan-prev-lantern i:nth-child(1){left:12%}.dkan-prev-lantern i:nth-child(2){left:27%;animation-delay:-.6s}.dkan-prev-lantern i:nth-child(3){left:43%;animation-delay:-1.2s}.dkan-prev-lantern i:nth-child(4){left:61%;animation-delay:-1.8s}.dkan-prev-lantern i:nth-child(5){left:76%;animation-delay:-2.4s}.dkan-prev-lantern i:nth-child(6){left:90%;animation-delay:-2.8s}@keyframes dkan-prev-lantern{0%{opacity:0;transform:translateY(0)}20%{opacity:.9}100%{opacity:0;transform:translateY(-38px)}}",
2033
+ ".dkan-prev-bot{height:92px;justify-content:center;}",
2034
+ ".dkan-prev-bot .dkan-bot-scene{--dkan-bot-scale:1;transform:scale(.56);transform-origin:center;}",
2035
+ // ===== 桌面伙伴:具象人物 + 紧凑双工位(尺寸可调,整卡可拖拽) =====
2036
+ // 卡片:玻璃拟态;默认停泊右下(dsh 输入卡上方),拖动后位置持久化、可放屏幕任意处
2037
+ ".dkan-botcard{--dkan-bot-scale:1.35;position:fixed;right:20px;bottom:calc(var(--dsh-composer-height,152px) + 20px);z-index:9990;display:flex;flex-direction:column;gap:6px;padding:10px 12px;border-radius:16px;cursor:grab;border:1px solid var(--dsw-alias-border-l1);background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 86%,transparent);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 12px 36px rgb(0 0 0 / .22);color:var(--dsw-alias-label-secondary);font-family:inherit;font-size:12px;pointer-events:auto;touch-action:none;user-select:none;animation:dkan-rise .3s var(--ds-ease-in-out);}",
2038
+ ".dkan-botcard:hover,.dkan-botcard:focus-visible{border-color:var(--dk-accent);outline:none;}",
2039
+ ".dkan-botcard.dkan-dragging{cursor:grabbing;animation:none;}",
2040
+ ".dkan-botcard .n{color:var(--dsw-alias-label-primary);font-weight:600;}",
2041
+ ".dkan-bot-cap{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-right:18px;}",
2042
+ ".dkan-bot-resize{position:absolute;right:7px;bottom:7px;width:15px;height:15px;padding:0;border:0;border-radius:4px;background:linear-gradient(135deg,transparent 0 42%,var(--dsw-alias-label-tertiary) 44% 51%,transparent 53% 62%,var(--dsw-alias-label-tertiary) 64% 71%,transparent 73%);cursor:nwse-resize;touch-action:none;opacity:.72;}",
2043
+ ".dkan-bot-resize:hover,.dkan-bot-resize:focus-visible{opacity:1;outline:1px solid var(--dk-accent);outline-offset:1px;}",
2044
+ // ===== 3D 场景(CSS 长方体拼装:世界层统一 3/4 视角——俯视 + 侧身面向镜头) =====
2045
+ ".dkan-bot-scene{position:relative;width:calc(220px * var(--dkan-bot-scale,1));height:calc(132px * var(--dkan-bot-scale,1));perspective:calc(620px * var(--dkan-bot-scale,1));perspective-origin:55% 18%;--dk3-side:#7c8ba0;--dk3-side-hi:#a9b8ca;--dk3-side-dk:#4c5870;--dk3-screen:#0d1526;--dkan-code-a:#4ade80;--dkan-code-b:#60a5fa;--dkan-code-c:#fbbf24;}",
2046
+ ".dk3-world{position:absolute;left:0;top:0;width:220px;height:132px;transform-style:preserve-3d;transform-origin:top left;transform:scale(var(--dkan-bot-scale,1)) rotateX(-15deg) rotateY(-30deg);}",
2047
+ ".dk3-box{position:absolute;transform-style:preserve-3d;}",
2048
+ ".dk3-face{position:absolute;left:50%;top:50%;display:block;background:var(--dk3-side);backface-visibility:hidden;border-radius:1px;}",
2049
+ // 金属件着色:顶面亮、底面暗,正/侧面中调(3D 光感)
2050
+ ".dk3-metal .dk3-face{background:linear-gradient(180deg,#93a5ba,#71809a);}",
2051
+ ".dk3-metal .dk3-face:nth-child(5){background:linear-gradient(180deg,var(--dk3-side-hi),#9dadc2);}",
2052
+ ".dk3-metal .dk3-face:nth-child(6){background:var(--dk3-side-dk);}",
2053
+ // 书桌与桌腿
2054
+ ".dk3-desk .dk3-face{background:linear-gradient(180deg,#9db0c6,#7f92aa);}",
2055
+ ".dk3-desk .dk3-face:nth-child(5){background:#c3d2e2;}",
2056
+ // 显示器(薄盒 + 贴在前面的屏幕;组内扇形微转,整组后撤出桌沿)
2057
+ ".dk3-mon3{position:absolute;transform-style:preserve-3d;}",
2058
+ ".dk3-frame .dk3-face{background:#3a465b;}",
2059
+ ".dk3-frame .dk3-face:nth-child(5){background:#55647d;}",
2060
+ ".dk3-screen{position:absolute;left:50%;top:50%;background:var(--dk3-screen);border-radius:2px;overflow:hidden;transition:opacity .4s var(--ds-ease-in-out),box-shadow .4s var(--ds-ease-in-out);}",
2061
+ // 屏幕内容:代码块整体向上滚动(10 行=5 行×2 循环,滚半程无缝接回)+ 闪烁光标,速度随 --dkan-speed
2062
+ ".dk3-code{position:absolute;left:0;top:0;right:0;display:block;animation:dk3-scroll calc(5s / var(--dkan-speed,1)) linear infinite;}",
2063
+ ".dk3-code i{display:block;height:2px;border-radius:1px;margin:2px 3px 0;background:var(--dkan-code-b);opacity:.55;}",
2064
+ ".dk3-code i:nth-child(5n+1){width:58%;background:var(--dkan-code-a);}",
2065
+ ".dk3-code i:nth-child(5n+2){width:82%;}",
2066
+ ".dk3-code i:nth-child(5n+3){width:46%;background:var(--dkan-code-c);}",
2067
+ ".dk3-code i:nth-child(5n+4){width:72%;}",
2068
+ ".dk3-code i:nth-child(5n){width:54%;background:var(--dkan-code-a);}",
2069
+ ".dk3-cur{position:absolute;left:3px;bottom:2px;width:4px;height:2px;background:var(--dkan-code-b);animation:dkan-blink3 1s steps(1) infinite;}",
2070
+ // 左屏检索视图:搜索框、放大镜与结果条,和右侧代码编辑器明确区分
2071
+ ".dk3-search-ui{position:absolute;inset:3px;display:block;overflow:hidden;}",
2072
+ ".dk3-search-box{display:block;height:5px;margin:0 1px 3px;border:1px solid color-mix(in srgb,var(--dkan-code-a) 72%,#d7fff4);border-radius:2px;box-sizing:border-box;}",
2073
+ ".dk3-search-box b{display:block;width:2px;height:2px;margin:1px 0 0 2px;border:1px solid var(--dkan-code-a);border-radius:50%;position:relative;}",
2074
+ ".dk3-search-box b:after{content:'';position:absolute;width:2px;height:1px;background:var(--dkan-code-a);right:-2px;bottom:-1px;transform:rotate(45deg);transform-origin:left center;}",
2075
+ ".dk3-search-results{display:block;animation:dk3-search-scroll calc(2.1s / var(--dkan-speed,1)) linear infinite;}",
2076
+ ".dk3-search-results i{display:block;height:2px;margin:2px 1px 0;border-radius:1px;background:var(--dkan-code-a);opacity:.72;}",
2077
+ ".dk3-search-results i:nth-child(2n){width:74%;background:var(--dkan-code-b);}",
2078
+ ".dk3-search-results i:nth-child(3n){width:48%;background:var(--dkan-code-c);}",
2079
+ "@keyframes dk3-search-scroll{to{transform:translateY(-5px)}}",
2080
+ "@keyframes dk3-scroll{to{transform:translateY(-50%)}}",
2081
+ // 人物与椅子始终正对桌面:工位切换只横移,不再旋转整个人和座椅。
2082
+ ".dk3-person{--dk3-seat-x:22px;position:absolute;left:98px;top:14px;width:44px;height:61px;transform-style:preserve-3d;transform:translateZ(30px) translateX(var(--dk3-seat-x));transition:transform max(.18s,calc(.38s / var(--dkan-speed,1))) cubic-bezier(.23,1,.32,1);will-change:transform;}",
2083
+ ".dkan-bot-scene[data-station=left] .dk3-person{--dk3-seat-x:0px;}",
2084
+ ".dkan-bot-scene[data-station=center] .dk3-person{--dk3-seat-x:22px;}",
2085
+ ".dkan-bot-scene[data-station=right] .dk3-person{--dk3-seat-x:44px;}",
2086
+ // 上身以髋部为旋转原点;只做前后倾,始终朝向桌面。
2087
+ ".dk3-upper3{position:absolute;inset:0;transform-style:preserve-3d;transform-origin:12px 52px;transform:translateZ(0);transition:transform .24s cubic-bezier(.77,0,.175,1);will-change:transform;}",
2088
+ // 动漫人物配色:皮肤/头发/卫衣/裤子
2089
+ ".dk3-skin .dk3-face{background:linear-gradient(180deg,#ffd9b8,#f3b98d);}",
2090
+ ".dk3-skin .dk3-face:nth-child(5){background:#ffe3c9;}",
2091
+ ".dk3-hair .dk3-face{background:linear-gradient(180deg,#5b4632,#3f3021);}",
2092
+ ".dk3-hair .dk3-face:nth-child(5){background:#6b543c;}",
2093
+ ".dk3-hood .dk3-face{background:linear-gradient(180deg,#7c8ba0,#5b6a80);}",
2094
+ ".dk3-hood .dk3-face:nth-child(5){background:#93a5ba;}",
2095
+ ".dk3-pants .dk3-face{background:linear-gradient(180deg,#3d4a5c,#2c3646);}",
2096
+ ".dk3-pants .dk3-face:nth-child(5){background:#4a5a70;}",
2097
+ // 人物细节:收窄下颌、连续肩颈、短侧渐层发型
2098
+ ".dk3-cranium .dk3-face{border-radius:42% 46% 38% 36%;}",
2099
+ ".dk3-jaw3 .dk3-face{border-radius:30% 30% 48% 48%;}",
2100
+ ".dk3-neck3 .dk3-face{border-radius:2px;}",
2101
+ ".dk3-collar3 .dk3-face{background:linear-gradient(180deg,#516176,#374459);border-radius:3px;}",
2102
+ ".dk3-shoulders3 .dk3-face{border-radius:4px;}",
2103
+ ".dk3-chest3 .dk3-face{border-radius:4px;}",
2104
+ ".dk3-waist3 .dk3-face{border-radius:3px;}",
2105
+ ".dk3-hair .dk3-face{border-radius:2px;}",
2106
+ ".dk3-hair-crown .dk3-face{border-radius:48% 42% 28% 30%;}",
2107
+ ".dk3-hair-back .dk3-face{background:linear-gradient(180deg,#493727,#2f241b);border-radius:46% 24% 38% 44%;}",
2108
+ ".dk3-shoe .dk3-face{background:#20262f;border-radius:2px;}",
2109
+ ".dk3-mug .dk3-face{background:#c2703d;border-radius:1px;}",
2110
+ ".dk3-mug .dk3-face:nth-child(5){background:#e08a52;}",
2111
+ ".dk3-mouth{position:absolute;left:50%;top:64%;width:3.5px;height:1px;border-radius:2px;background:#a9635c;}",
2112
+ // 椅子
2113
+ ".dk3-chairback .dk3-face{background:linear-gradient(180deg,#3f4c60,#2c3648);}",
2114
+ ".dk3-chairseat .dk3-face{background:#33415a;}",
2115
+ // 头组与颈部中心均为 x=14;只让头部观察左右屏,身体和椅子不转向。
2116
+ ".dk3-head3{--dk3-head-turn:rotateY(0deg);position:absolute;left:6px;top:3px;width:17px;height:21px;transform-style:preserve-3d;transform-origin:8px 18px;transform:var(--dk3-head-turn);transition:transform max(.18s,calc(.22s / var(--dkan-speed,1))) cubic-bezier(.23,1,.32,1);will-change:transform;}",
2117
+ ".dkan-bot-scene[data-station=left] .dk3-head3{--dk3-head-turn:rotateY(-18deg);}",
2118
+ ".dkan-bot-scene[data-station=center] .dk3-head3{--dk3-head-turn:rotateY(0deg);}",
2119
+ ".dkan-bot-scene[data-station=right] .dk3-head3{--dk3-head-turn:rotateY(14deg);}",
2120
+ ".dk3-brows{position:absolute;left:50%;top:29%;display:flex;gap:2.5px;align-items:center;justify-content:center;}",
2121
+ ".dk3-brows i{width:3.5px;height:1px;border-radius:2px;background:#3a2a21;transform:rotate(-7deg);}",
2122
+ ".dk3-brows i+ i{transform:rotate(7deg);}",
2123
+ ".dk3-eyes{position:absolute;left:50%;top:51%;display:flex;gap:2.5px;align-items:center;justify-content:center;}",
2124
+ ".dk3-eyes i{width:3.5px;height:2.5px;border-radius:50%;background:radial-gradient(circle at 62% 30%,#fff 0 .5px,#263141 .8px 1.7px,#111827 1.9px);animation:dkan-blink3 4.8s infinite;}",
2125
+ ".dk3-nose{position:absolute;left:50%;top:66%;width:2px;height:3px;border-radius:60% 40% 55% 45%;background:linear-gradient(90deg,#e7a67e,#c77f67);}",
2126
+ ".dk3-ear{position:absolute;left:1px;top:10px;width:3px;height:5px;border-radius:52% 44% 48% 52%;background:linear-gradient(90deg,#edaa82,#ffd2ad);transform:translateZ(2px);}",
2127
+ ".dk3-fringe3{position:absolute;left:13px;top:4px;width:5px;height:7px;transform-style:preserve-3d;transform:rotateY(90deg) translateZ(5.8px);}",
2128
+ ".dk3-fringe3 i{position:absolute;top:0;width:2px;height:6px;border-radius:70% 25% 60% 30%;background:linear-gradient(180deg,#6b5239,#392b20);transform-origin:top center;}",
2129
+ ".dk3-fringe3 i:nth-child(1){left:0;transform:rotate(-18deg);height:5px;}",
2130
+ ".dk3-fringe3 i:nth-child(2){left:1.8px;transform:rotate(-6deg);height:6px;}",
2131
+ ".dk3-fringe3 i:nth-child(3){left:3.5px;transform:rotate(12deg);height:4px;}",
2132
+ "@keyframes dkan-blink3{0%,91%,100%{transform:scaleY(1)}94%{transform:scaleY(.15)}}",
2133
+ // 手臂按肩→肘→腕分段,旋转原点位于真实关节连接处。
2134
+ ".dk3-arm3{position:absolute;left:20px;top:30px;width:16px;height:16px;transform-style:preserve-3d;transform-origin:2px 2px;will-change:transform;}",
2135
+ ".dk3-elbow{position:absolute;left:0;top:10px;width:15px;height:5px;transform-style:preserve-3d;transform-origin:1px 2px;will-change:transform;}",
2136
+ ".dk3-wrist3{position:absolute;left:9.5px;top:0;width:6px;height:5px;transform-style:preserve-3d;transform-origin:1px 2px;will-change:transform;}",
2137
+ ".dk3-fingers3{position:absolute;left:3px;top:1px;width:4px;height:3px;transform:translateZ(2px);}",
2138
+ ".dk3-fingers3 i{position:absolute;left:0;width:4px;height:.8px;border-radius:2px;background:#f2b58d;}",
2139
+ ".dk3-fingers3 i+ i{top:1.4px;width:3.5px;}",
2140
+ ".dk3-far{transform:translateZ(-7px);filter:brightness(.72);}",
2141
+ // 思考泡泡(贴镜头 2D 层,think 阶段显示)
2142
+ ".dkan-bubble{position:absolute;left:80px;top:4px;display:none;gap:3px;padding:4px 6px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);box-shadow:0 2px 6px rgb(0 0 0 / .15);z-index:3;}",
2143
+ ".dkan-bubble i{width:4px;height:4px;border-radius:50%;background:var(--dsw-alias-label-tertiary);animation:dkan-bubdot 1.2s ease-in-out infinite;}",
2144
+ ".dkan-bubble i:nth-child(2){animation-delay:.2s;}",
2145
+ ".dkan-bubble i:nth-child(3){animation-delay:.4s;}",
2146
+ "@keyframes dkan-bubdot{0%,100%{opacity:.25;transform:translateY(0)}50%{opacity:1;transform:translateY(-2px)}}",
2147
+ // ===== 多任务工位(data-tasks=活跃任务数,上限 3) =====
2148
+ // 椅子滚轮
2149
+ ".dk3-wheel .dk3-face{background:#1c232e;border-radius:50%;}",
2150
+ // 有活跃任务时,未轮到的侧工位稍暗;多任务则两个工位均保持可见。
2151
+ ".dkan-bot-scene[data-tasks] .dk3-mon3.left .dk3-screen,.dkan-bot-scene[data-tasks] .dk3-mon3.right .dk3-screen{opacity:.3;}",
2152
+ // 2 个及以上任务:左右工位同时点亮(各自带辉光、代码滚动加速)。
2153
+ ".dkan-bot-scene[data-tasks=\"2\"] .dk3-mon3.left .dk3-screen,.dkan-bot-scene[data-tasks=\"2\"] .dk3-mon3.right .dk3-screen,.dkan-bot-scene[data-tasks=\"3\"] .dk3-mon3.left .dk3-screen,.dkan-bot-scene[data-tasks=\"3\"] .dk3-mon3.right .dk3-screen{opacity:1;box-shadow:0 0 8px color-mix(in srgb,var(--dkan-code-b,#60a5fa) 42%,transparent);}",
2154
+ ".dkan-bot-scene[data-tasks=\"2\"] .dk3-mon3.left .dk3-code,.dkan-bot-scene[data-tasks=\"2\"] .dk3-mon3.right .dk3-code,.dkan-bot-scene[data-tasks=\"3\"] .dk3-mon3.left .dk3-code,.dkan-bot-scene[data-tasks=\"3\"] .dk3-mon3.right .dk3-code{animation-duration:calc(2.6s / var(--dkan-speed,1));}",
2155
+ // 座椅移动期间滚轮跟随任务节奏转动;思考中停下,避免无意义的常驻运动。
2156
+ ".dkan-bot-scene[data-station=left] .dk3-wheel,.dkan-bot-scene[data-station=right] .dk3-wheel{animation:dkan-wheel calc(.46s / var(--dkan-speed,1)) linear infinite;transform-origin:center;}",
2157
+ "@keyframes dkan-wheel{to{transform:rotateZ(360deg)}}",
2158
+ // ===== 阶段驱动(data-phase 四态,host 由 chunk 流实时同步) =====
2159
+ // think:上身轻靠、近侧手托下巴、远侧手留在桌面;动作有停顿而不是机械往返。
2160
+ ".dkan-bot-scene[data-phase=think] .dk3-screen{opacity:.32;}",
2161
+ ".dkan-bot-scene[data-phase=think] .dkan-bubble{display:inline-flex;}",
2162
+ ".dkan-bot-scene[data-phase=think] .dk3-upper3{animation:dkan-think-upper max(1.8s,calc(2.4s / var(--dkan-speed,1))) cubic-bezier(.77,0,.175,1) infinite;}",
2163
+ ".dkan-bot-scene[data-phase=think] .dk3-head3{animation:dkan-think-head max(1.8s,calc(2.4s / var(--dkan-speed,1))) cubic-bezier(.77,0,.175,1) infinite;}",
2164
+ ".dkan-bot-scene[data-phase=think] .dk3-arm3.dk3-near{animation:dkan-think-near max(1.8s,calc(2.4s / var(--dkan-speed,1))) cubic-bezier(.77,0,.175,1) infinite;}",
2165
+ ".dkan-bot-scene[data-phase=think] .dk3-arm3.dk3-far{transform:translateZ(-7px) rotateZ(3deg);}",
2166
+ ".dkan-bot-scene[data-phase=think] .dk3-near .dk3-elbow{transform:rotateZ(-28deg) translateY(-1px);}",
2167
+ ".dkan-bot-scene[data-phase=think] .dk3-near .dk3-wrist3{transform:rotateZ(-18deg) translateY(-1px);}",
2168
+ "@keyframes dkan-think-upper{0%,44%{transform:rotateZ(-1deg) translateY(0)}62%,100%{transform:rotateZ(-1.8deg) translateY(-.5px)}}",
2169
+ "@keyframes dkan-think-head{0%,44%{transform:var(--dk3-head-turn) rotateZ(-3deg)}62%,100%{transform:var(--dk3-head-turn) rotateZ(-6deg) translateY(-.5px)}}",
2170
+ "@keyframes dkan-think-near{0%,44%{transform:rotateZ(-31deg) translate(-8px,-14.5px)}62%,100%{transform:rotateZ(-34deg) translate(-8.5px,-15px)}}",
2171
+ // write/code:身体仍面向桌面,仅轻微前倾;主要由手腕和手指敲击。
2172
+ ".dkan-bot-scene[data-phase=write] .dk3-mon3.right .dk3-screen,.dkan-bot-scene[data-phase=code] .dk3-mon3.right .dk3-screen{opacity:1;box-shadow:0 0 11px color-mix(in srgb,var(--dkan-code-b,#60a5fa) 52%,transparent);}",
2173
+ ".dkan-bot-scene[data-phase=write] .dk3-mon3.right .dk3-code,.dkan-bot-scene[data-phase=code] .dk3-mon3.right .dk3-code{animation-duration:calc(1.35s / var(--dkan-speed,1));}",
2174
+ ".dkan-bot-scene[data-phase=write] .dk3-upper3,.dkan-bot-scene[data-phase=code] .dk3-upper3{animation:dkan-code-upper max(1.2s,calc(2.4s / var(--dkan-speed,1))) cubic-bezier(.77,0,.175,1) infinite;}",
2175
+ ".dkan-bot-scene[data-phase=write] .dk3-arm3.dk3-near,.dkan-bot-scene[data-phase=code] .dk3-arm3.dk3-near{transform:rotateZ(3deg);}",
2176
+ ".dkan-bot-scene[data-phase=write] .dk3-arm3.dk3-far,.dkan-bot-scene[data-phase=code] .dk3-arm3.dk3-far{transform:translateZ(-7px) rotateZ(1deg);}",
2177
+ ".dkan-bot-scene[data-phase=write] .dk3-elbow,.dkan-bot-scene[data-phase=code] .dk3-elbow{animation:dkan-type-elbow max(.28s,calc(.58s / var(--dkan-speed,1))) cubic-bezier(.77,0,.175,1) infinite;}",
2178
+ ".dkan-bot-scene[data-phase=write] .dk3-wrist3,.dkan-bot-scene[data-phase=code] .dk3-wrist3{animation:dkan-type-wrist max(.18s,calc(.29s / var(--dkan-speed,1))) cubic-bezier(.77,0,.175,1) infinite;}",
2179
+ ".dkan-bot-scene[data-phase=write] .dk3-far .dk3-elbow,.dkan-bot-scene[data-phase=code] .dk3-far .dk3-elbow{animation-delay:-.09s;animation-duration:max(.3s,calc(.63s / var(--dkan-speed,1)));}",
2180
+ ".dkan-bot-scene[data-phase=write] .dk3-far .dk3-wrist3,.dkan-bot-scene[data-phase=code] .dk3-far .dk3-wrist3{animation-delay:-.09s;animation-duration:max(.2s,calc(.33s / var(--dkan-speed,1)));}",
2181
+ ".dkan-bot-scene[data-phase=write] .dk3-head3,.dkan-bot-scene[data-phase=code] .dk3-head3{animation:dkan-output-head max(1.2s,calc(2.4s / var(--dkan-speed,1))) cubic-bezier(.77,0,.175,1) infinite;}",
2182
+ "@keyframes dkan-code-upper{0%,38%{transform:rotateZ(2deg) translate(1px,0)}62%,100%{transform:rotateZ(3deg) translate(1px,1px)}}",
2183
+ "@keyframes dkan-type-elbow{0%,38%{transform:rotateZ(-4deg)}62%,100%{transform:rotateZ(5deg) translateY(1px)}}",
2184
+ "@keyframes dkan-type-wrist{0%,34%{transform:rotateZ(-2deg) translateY(0)}62%,100%{transform:rotateZ(3deg) translateY(1.5px)}}",
2185
+ "@keyframes dkan-output-head{0%,46%,100%{transform:var(--dk3-head-turn) rotateZ(3deg)}58%{transform:var(--dk3-head-turn) rotateZ(5deg) translateY(1px)}}",
2186
+ // search:身体正对桌面,头看左屏,近侧手腕小幅操作鼠标。
2187
+ ".dkan-bot-scene[data-phase=search] .dk3-mon3.left .dk3-screen{opacity:1;box-shadow:0 0 10px color-mix(in srgb,var(--dkan-code-a,#4ade80) 52%,transparent);}",
2188
+ ".dkan-bot-scene[data-phase=search] .dk3-mon3.left .dk3-search-results{animation-duration:calc(1.05s / var(--dkan-speed,1));}",
2189
+ ".dkan-bot-scene[data-phase=search] .dk3-upper3{transform:rotateZ(2deg) translateY(1px);}",
2190
+ ".dkan-bot-scene[data-phase=search] .dk3-head3{animation:dkan-search-head max(.72s,calc(1.15s / var(--dkan-speed,1))) cubic-bezier(.77,0,.175,1) infinite;}",
2191
+ ".dkan-bot-scene[data-phase=search] .dk3-near .dk3-wrist3{animation:dkan-search-wrist max(.45s,calc(.72s / var(--dkan-speed,1))) cubic-bezier(.77,0,.175,1) infinite;}",
2192
+ ".dkan-bot-scene[data-phase=search] .dk3-far{transform:translateZ(-7px) rotateZ(2deg);}",
2193
+ "@keyframes dkan-search-head{0%,28%{transform:var(--dk3-head-turn) rotateZ(1deg)}48%,68%{transform:var(--dk3-head-turn) rotateZ(6deg) translate(1px,1px)}88%,100%{transform:var(--dk3-head-turn) rotateZ(2deg)}}",
2194
+ "@keyframes dkan-search-wrist{0%,34%{transform:translateX(-1px)}62%,100%{transform:translateX(1.5px)}}",
2195
+ // 减少动态时停止空间位移,保留屏幕亮暗与思考状态的淡入反馈。
2196
+ "@media (prefers-reduced-motion:reduce){.dkan-amb *,.dkan-prev *,.dkan-space-system,.dkan-space-dust,.dkan-space-runner,.dkan-delivery,.dkan-departure,.dkan-freighter-engine i,.dkan-stuck-code::after,.dkan-stuck-code i{animation:none!important}.dkan-space-runner,.dkan-departure{display:none}.dkan-delivery{opacity:1;transform:translate3d(var(--dkan-to-x),var(--dkan-to-y),0)}.dkan-delivery-trail{display:none}.dk3-person,.dk3-upper3,.dk3-head3,.dk3-arm3,.dk3-elbow,.dk3-wrist3,.dk3-wheel,.dk3-code,.dk3-search-results{animation:none!important;transition:none!important}.dk3-screen,.dkan-bubble{transition:opacity .2s cubic-bezier(.23,1,.32,1)!important}}",
2197
+ // 通知子选项行
2198
+ ".dkan-rows-narrow{display:flex;flex-direction:column;gap:6px;}",
2199
+ ".dkan-row{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary);flex-wrap:wrap;}",
2200
+ ".dkan-row-label{flex:none;min-width:60px;color:var(--dsw-alias-label-primary);}",
2201
+ ".dkan-row-sub{font-size:11px;color:var(--dsw-alias-label-tertiary);}",
2202
+ ".dkm-miniswitch{cursor:pointer;flex:none;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 12px;font-family:inherit;font-size:11px;line-height:18px;}",
2203
+ ".dkm-miniswitch.on{color:var(--dsw-alias-state-success-primary);border-color:currentColor;}",
2204
+ ".dkan-select{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:3px 8px;font-size:12px;font-family:inherit;}",
2205
+ // 音效选择卡(名称 + 播放键;选中态描边)
2206
+ ".dkan-sounds{display:flex;gap:6px;flex-wrap:wrap;}",
2207
+ ".dkan-sound{flex:1;min-width:104px;display:flex;align-items:center;justify-content:space-between;gap:6px;padding:6px 10px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-2);cursor:pointer;font-family:inherit;transition:border-color .15s var(--ds-ease-in-out);}",
2208
+ ".dkan-sound:hover{border-color:var(--dk-accent);}",
2209
+ ".dkan-sound.on{border-color:var(--dk-accent);box-shadow:0 0 0 1px color-mix(in srgb,var(--dk-accent) 40%,transparent);}",
2210
+ ".dkan-sound-name{font-size:12px;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:5px;}",
2211
+ ".dkan-sound-cur{color:var(--dk-accent);font-size:11px;}",
2212
+ ".dkan-sound-play{flex:none;cursor:pointer;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:1;border-radius:50%;width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--dsw-alias-border-l2);}",
2213
+ ".dkan-sound-play:hover{color:var(--dsw-alias-label-primary);border-color:currentColor;}",
2214
+ ".dkan-input{flex:1;min-width:220px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:4px 8px;font-size:12px;font-family:inherit;}",
2215
+ ".dkan-input:focus{outline:none;border-color:var(--dk-accent);}",
2216
+ ".dkan-row-webhook{flex-wrap:nowrap;}",
2217
+ ".dkan-row-webhook .dkan-input{min-width:0;}",
2218
+ ".dkan-btn{cursor:pointer;flex:none;color:var(--dsw-alias-label-primary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:4px 12px;font-family:inherit;font-size:12px;}",
2219
+ ".dkan-btn:hover{background:var(--dsw-alias-interactive-bg-hover);}",
2220
+ ".dkan-btn[disabled]{opacity:.5;cursor:default;}",
2221
+ ".dkan-ok{color:var(--dsw-alias-state-success-primary);}",
2222
+ // 任务列表
2223
+ ".dkan-tasks{display:flex;flex-direction:column;gap:6px;}",
2224
+ ".dkan-tasks-done{border-top:1px dashed var(--dsw-alias-border-l2);padding-top:6px;}",
2225
+ ".dkan-task{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:6px 10px;display:flex;flex-direction:column;gap:3px;}",
2226
+ ".dkan-task-head{display:flex;align-items:center;gap:8px;min-width:0;}",
2227
+ ".dkan-task-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
2228
+ ".dkan-task-meta{display:flex;gap:10px;flex-wrap:wrap;font-size:11px;color:var(--dsw-alias-label-tertiary);}",
2229
+ ".dkan-task-err{font-size:11px;color:var(--dsw-alias-state-error-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
2230
+ ".dkan-tag{flex:none;font-size:10px;border-radius:999px;padding:0 8px;color:var(--dsw-alias-label-tertiary);border:1px solid var(--dsw-alias-border-l2);}",
2231
+ ".dkan-tag.on{color:var(--dk-accent);border-color:currentColor;}",
2232
+ ".dkan-tag.ok{color:var(--dsw-alias-state-success-primary);border-color:currentColor;}",
2233
+ ".dkan-tag.err{color:var(--dsw-alias-state-error-primary);border-color:currentColor;}",
2234
+ ".dkan-tag.warn{color:var(--dk-warn);border-color:currentColor;}",
2235
+ ].join("\n");
2236
+
2237
+ export const feature = {
2238
+ id: "animation",
2239
+ name: "任务动画",
2240
+ order: 130,
2241
+ accent: "#f472b6",
2242
+ description: "19 种任务运行动画与完成通知:速度随任务活动联动,两组开关独立、配置持久化",
2243
+ css,
2244
+ View: AnimationView,
2245
+ HomeStat: AnimationStat,
2246
+ Overlay: AnimationOverlay,
2247
+ };