dsh-dock 0.9.9 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -34
- package/client.js +1818 -1460
- package/features/animation/host.js +16 -557
- package/features/animation/view.jsx +57 -645
- package/features/modelconfig/host.js +4 -1
- package/features/notify/host.js +374 -0
- package/features/notify/view.jsx +751 -0
- package/features/runstate/host.js +64 -0
- package/features/runstate/view.jsx +269 -0
- package/index.js +25 -5
- package/package.json +4 -3
- package/screenshots/dock-animation.png +0 -0
- package/screenshots/dock-balance.png +0 -0
- package/screenshots/dock-chips.png +0 -0
- package/screenshots/dock-games.png +0 -0
- package/screenshots/dock-home.png +0 -0
- package/screenshots/dock-modelconfig.png +0 -0
- package/screenshots/dock-notify.png +0 -0
- package/screenshots/dock-runstate.png +0 -0
- package/screenshots/dock-settings.png +0 -0
- package/screenshots/dock-tokenlog.png +0 -0
- package/src/client.jsx +5 -3
- package/src/host-core.js +74 -3
- package/src/shared.js +25 -8
- package/src/task-track.js +359 -0
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
// dsh-dock · 功能模块【任务动画】·
|
|
1
|
+
// dsh-dock · 功能模块【任务动画】· 客户端视图
|
|
2
2
|
//
|
|
3
3
|
// 三部分:
|
|
4
|
-
// 1. View ——
|
|
5
|
-
// 2. HomeStat ——
|
|
4
|
+
// 1. View —— 功能坞面板页:动画开关 + 19 种模式选择(带缩微预览);
|
|
5
|
+
// 2. HomeStat —— 首页总揽概要(当前动效模式 + 开关状态);
|
|
6
6
|
// 3. Overlay —— 全局浮层(shell.overlay 常驻,功能启用即挂载):轮询 Host 状态,
|
|
7
7
|
// 任务进行中渲染克制的动效(顶部流光细线 / 呼吸光点 / 轨道光环)+ 右下角状态徽标,
|
|
8
|
-
//
|
|
8
|
+
// 完成瞬间一缕流光掠过(星际远征另有货运舰归航)。
|
|
9
9
|
//
|
|
10
10
|
// 设计取向:不做满屏粒子彩带,动效全部走主题变量(暗/亮色自适应)、低透明度、慢节奏。
|
|
11
|
-
//
|
|
11
|
+
// 通知(页内卡片 / 提示音 / 系统通知 / 钉钉飞书推送)独立在 features/notify/,
|
|
12
|
+
// 运行状态(进行中任务与最近完成)独立在 features/runstate/:三个功能各自启停、互不依赖,
|
|
13
|
+
// 会话级任务追踪在宿主侧共用一份。
|
|
12
14
|
//
|
|
13
15
|
// Host 通信:fetch('/dsh-dock/animation/<method>')(见 features/animation/host.js)。
|
|
14
16
|
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
|
@@ -24,9 +26,9 @@ function rpcCall(method, args) {
|
|
|
24
26
|
.then(async (res) => {
|
|
25
27
|
const data = await res.json().catch(() => ({}));
|
|
26
28
|
if (res.ok && data && data.ok === true) return data.data;
|
|
27
|
-
//
|
|
29
|
+
// 宿主侧没有动画路由(405/404):宿主进程可能是旧版本,或本功能在宿主侧未 setup
|
|
28
30
|
if (res.status === 405 || res.status === 404) {
|
|
29
|
-
throw new Error("
|
|
31
|
+
throw new Error("宿主侧没有「任务动画」接口:宿主进程可能是旧版本,或该功能在宿主侧未启用(重启 dsh web,或把功能坞里的开关关掉再打开)");
|
|
30
32
|
}
|
|
31
33
|
if (data && data.ok === false) throw new Error((data.error && data.error.message) || ("HTTP " + res.status));
|
|
32
34
|
throw new Error("HTTP " + res.status + (data && data.error && data.error.message ? ": " + data.error.message : ""));
|
|
@@ -66,47 +68,19 @@ function phaseLabel(p) { return PHASE_LABELS[p] || "工作中"; }
|
|
|
66
68
|
const PHASE_COLORS = { think: "#2f6fed", write: "#0d9488", code: "#b45309", search: "#0e7490" };
|
|
67
69
|
function phaseColor(p) { return PHASE_COLORS[p] || "#2f6fed"; }
|
|
68
70
|
|
|
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
71
|
// ---------- 结束原因 ----------
|
|
79
|
-
|
|
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
|
-
}
|
|
72
|
+
// (结束原因的展示文案随「运行状态」页一起搬去 features/runstate/,这里只保留动效判断用的成功判定)
|
|
90
73
|
function isSuccessReason(reason) {
|
|
91
74
|
return !reason || reason === "completed";
|
|
92
75
|
}
|
|
93
76
|
|
|
94
77
|
// ---------- 格式化 ----------
|
|
95
|
-
function fmtNum(n) { return (Number(n) || 0).toLocaleString("en-US"); }
|
|
96
78
|
function fmtCompact(n) {
|
|
97
79
|
n = Number(n) || 0;
|
|
98
80
|
if (n >= 1e6) return (n / 1e6).toFixed(2) + "M";
|
|
99
81
|
if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
|
|
100
82
|
return String(Math.round(n));
|
|
101
83
|
}
|
|
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
84
|
// 毫秒 → "12:34" / "1:02:11"(徽标计时)
|
|
111
85
|
function fmtClock(ms) {
|
|
112
86
|
const s = Math.max(0, Math.floor((ms || 0) / 1000));
|
|
@@ -114,12 +88,6 @@ function fmtClock(ms) {
|
|
|
114
88
|
const p = (x) => String(x).padStart(2, "0");
|
|
115
89
|
return h > 0 ? h + ":" + p(m) + ":" + p(sec) : p(m) + ":" + p(sec);
|
|
116
90
|
}
|
|
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
91
|
function truncate(str, max) {
|
|
124
92
|
if (!str) return "";
|
|
125
93
|
return String(str).length > max ? String(str).slice(0, max) + "…" : str;
|
|
@@ -313,93 +281,6 @@ function RobotScene(props) {
|
|
|
313
281
|
);
|
|
314
282
|
}
|
|
315
283
|
|
|
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
284
|
// ---------- 交互反馈层:对用户操作即时回应(纸飞机/火花/流光/微光/光涌/彩带) ----------
|
|
404
285
|
// 事件来源:document 点击监听(发送/新会话/切会话/切工作目录)+ 任务状态变化(开始/完成)。
|
|
405
286
|
// 每个 burst = { id, type, x, y, bits },bits 为预生成的随机粒子参数(内联样式驱动 CSS 动画)。
|
|
@@ -843,11 +724,10 @@ function ProgrammingStuck(props) {
|
|
|
843
724
|
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
725
|
}
|
|
845
726
|
|
|
846
|
-
// ---------- 全局浮层:轮询 +
|
|
727
|
+
// ---------- 全局浮层:轮询 + 动效(功能启用即常驻;通知在 features/notify/) ----------
|
|
847
728
|
export function AnimationOverlay(props) {
|
|
848
729
|
const ctx = props && props.ctx;
|
|
849
730
|
const snap = useAnimation();
|
|
850
|
-
const [toasts, setToasts] = useState([]);
|
|
851
731
|
const [flourish, setFlourish] = useState(null); // { key, err } 完成瞬间的一次性流光
|
|
852
732
|
const [bursts, setBursts] = useState([]); // 交互反馈动画队列
|
|
853
733
|
const [deliveries, setDeliveries] = useState([]); // 星际模式完成时停泊在输入框上方的归航货运舰
|
|
@@ -952,7 +832,7 @@ export function AnimationOverlay(props) {
|
|
|
952
832
|
return () => { stopped = true; if (timer) clearTimeout(timer); document.removeEventListener("visibilitychange", onVisible); };
|
|
953
833
|
}, []);
|
|
954
834
|
|
|
955
|
-
// 任务结束检测:上一轮活跃、本轮消失 →
|
|
835
|
+
// 任务结束检测:上一轮活跃、本轮消失 → 完成瞬间的流光(找最近完成记录补全信息)
|
|
956
836
|
useEffect(() => {
|
|
957
837
|
const st = snap.status;
|
|
958
838
|
if (!st || !st.active) return;
|
|
@@ -968,133 +848,35 @@ export function AnimationOverlay(props) {
|
|
|
968
848
|
prevActiveRef.current = st.active.slice();
|
|
969
849
|
}, [snap.status]);
|
|
970
850
|
|
|
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
851
|
const handleTaskEnd = (task, record, cfg) => {
|
|
1024
|
-
|
|
1025
|
-
const success = isSuccessReason(
|
|
1026
|
-
|
|
1027
|
-
//
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
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
|
-
}
|
|
852
|
+
if (!cfg.animationEnabled) return;
|
|
853
|
+
const success = isSuccessReason((record && record.endReason) || "");
|
|
854
|
+
setFlourish({ key: Date.now(), err: !success });
|
|
855
|
+
// 星际远征把完成结果送回用户正在输入的位置。速度继承最后一帧任务节奏;
|
|
856
|
+
// 货舱数量及标签只取 Host 已记录的真实 outputTokens。
|
|
857
|
+
if (cfg.effectMode === "space") {
|
|
858
|
+
const cargo = outputCargo((record && record.outputTokens) || task.outputTokens);
|
|
859
|
+
const id = ++deliveryIdRef.current;
|
|
860
|
+
const taskSpeed = Math.max(.9, Number(speed) || 1);
|
|
861
|
+
const duration = Math.round(Math.max(8000, Math.min(9400, 10000 / Math.sqrt(taskSpeed))));
|
|
862
|
+
// 从三个星系中随机挑选归航起点,任务完成时才能知道本次货舰从哪里回来。
|
|
863
|
+
const galaxies = spaceGalaxyLayout();
|
|
864
|
+
const returnGalaxy = galaxies[["alpha", "beta", "gamma"][Math.floor(Math.random() * 3)]];
|
|
865
|
+
const from = freighterAnchorAt({ x: returnGalaxy.cx, y: returnGalaxy.cy });
|
|
866
|
+
const to = composerDockPoint("left");
|
|
867
|
+
// 满载舰最终停入左侧归航位,和右上出发星系形成清晰的往返方向。
|
|
868
|
+
const delivery = Object.assign({ id, from, to, duration }, cargo);
|
|
869
|
+
setDeliveries([delivery]);
|
|
1055
870
|
}
|
|
1056
|
-
//
|
|
1057
|
-
if (
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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 { /* 系统通知失败不影响页面内通知 */ }
|
|
871
|
+
// 成功时顶部彩带庆祝(通知卡片由【任务通知】模块负责,与本模块无关)
|
|
872
|
+
if (success) {
|
|
873
|
+
pushBurst("confetti", 0, 0, makeBits(18, (i) => ({
|
|
874
|
+
l: 4 + (i / 18) * 92 + Math.random() * 3,
|
|
875
|
+
dx: (Math.random() - 0.5) * 60,
|
|
876
|
+
r: Math.random() * 720 - 360,
|
|
877
|
+
d: Math.random() * 0.35,
|
|
878
|
+
c: ["#2563eb", "#0d9488", "#b45309", "#be185d", "#7c3aed"][i % 5],
|
|
879
|
+
})));
|
|
1098
880
|
}
|
|
1099
881
|
};
|
|
1100
882
|
|
|
@@ -1324,12 +1106,6 @@ export function AnimationOverlay(props) {
|
|
|
1324
1106
|
) : null}
|
|
1325
1107
|
{/* 星际任务完成:货运舰沿航线飞到输入框上方并保持停泊,直到下一次任务出航。 */}
|
|
1326
1108
|
{!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
1109
|
</>);
|
|
1334
1110
|
}
|
|
1335
1111
|
|
|
@@ -1391,53 +1167,18 @@ function ModePreview({ id }) {
|
|
|
1391
1167
|
);
|
|
1392
1168
|
}
|
|
1393
1169
|
|
|
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
1170
|
function AnimationView(props) {
|
|
1424
1171
|
const ctx = props && props.ctx;
|
|
1425
1172
|
const snap = useAnimation(ctx);
|
|
1426
1173
|
const [cfg, setCfg] = useState(null); // null = 尚未加载
|
|
1427
1174
|
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
1175
|
const cfgRef = useRef(null);
|
|
1433
1176
|
const pendingSavesRef = useRef(0); // 进行中的保存(轮询回包不覆盖乐观值)
|
|
1434
|
-
|
|
1435
|
-
const editingFeishuRef = useRef(false); // 飞书 Webhook 输入中(轮询不覆盖草稿)
|
|
1436
|
-
// 拉到新配置(含保存回包)后同步本地编辑态;保存中/输入中不覆盖
|
|
1177
|
+
// 拉到新配置(含保存回包)后同步本地编辑态;保存中不覆盖
|
|
1437
1178
|
useEffect(() => {
|
|
1438
1179
|
const c = snap.status && snap.status.config;
|
|
1439
1180
|
if (!c) return;
|
|
1440
|
-
if (pendingSavesRef.current > 0
|
|
1181
|
+
if (pendingSavesRef.current > 0) return;
|
|
1441
1182
|
if (c !== cfgRef.current) {
|
|
1442
1183
|
cfgRef.current = c;
|
|
1443
1184
|
setCfg(Object.assign({}, c));
|
|
@@ -1447,7 +1188,7 @@ function AnimationView(props) {
|
|
|
1447
1188
|
useEffect(() => {
|
|
1448
1189
|
if (!animationStore.snap.status) animationStore.refresh();
|
|
1449
1190
|
}, []);
|
|
1450
|
-
// 乐观更新 +
|
|
1191
|
+
// 乐观更新 + 立即持久化(每个开关独立保存)
|
|
1451
1192
|
const patch = (p) => {
|
|
1452
1193
|
setSaveErr("");
|
|
1453
1194
|
setCfg(Object.assign({}, cfg, p));
|
|
@@ -1462,105 +1203,6 @@ function AnimationView(props) {
|
|
|
1462
1203
|
setSaveErr("保存失败:" + ((e && e.message) || String(e)));
|
|
1463
1204
|
});
|
|
1464
1205
|
};
|
|
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
|
-
// 等待确认的审批项总数:运行状态标题与页脚都用,必须在 if/else 之外声明。
|
|
1557
|
-
// ⚠️ 曾误写在 if(!cfg) 的 else 块内、却在块外引用 → ReferenceError: waitingCount is not defined,
|
|
1558
|
-
// 整个视图渲染抛错、宿主把面板插槽换成空 div(点「任务动画」界面直接消失)。
|
|
1559
|
-
const waitingCount = active.reduce((n, t) => n + (Array.isArray(t.approvals) ? t.approvals.length : 0), 0);
|
|
1560
|
-
const permNote = typeof Notification === "undefined"
|
|
1561
|
-
? "当前浏览器不支持系统通知"
|
|
1562
|
-
: Notification.permission === "granted" ? "已授权 · 仅页面后台时推送"
|
|
1563
|
-
: Notification.permission === "denied" ? "已被浏览器拒绝(需在浏览器权限设置里重新允许)" : "未授权 · 开启时会请求授权,仅页面后台时推送";
|
|
1564
1206
|
|
|
1565
1207
|
const rows = [];
|
|
1566
1208
|
if (!cfg) {
|
|
@@ -1611,209 +1253,34 @@ function AnimationView(props) {
|
|
|
1611
1253
|
</div>
|
|
1612
1254
|
</div> : null}
|
|
1613
1255
|
</>
|
|
1614
|
-
) : <div className="dkan-note"
|
|
1615
|
-
</div>,
|
|
1616
|
-
<div key="notify" className="dkan-sec">
|
|
1617
|
-
<div className="dkan-sec-head">
|
|
1618
|
-
<span className="dkan-sec-title">完成通知</span>
|
|
1619
|
-
<span className="dkan-sec-sub">与动画互不依赖,可单独开启</span>
|
|
1620
|
-
<span className="dkan-sec-sw">
|
|
1621
|
-
<span className={"dkan-sec-swlabel" + (cfg.notifyEnabled ? " on" : "")}>{cfg.notifyEnabled ? "已开启" : "已关闭"}</span>
|
|
1622
|
-
<button type="button" className={"dock-sw" + (cfg.notifyEnabled ? " on" : "")}
|
|
1623
|
-
role="switch" aria-checked={cfg.notifyEnabled} aria-label="开关完成通知"
|
|
1624
|
-
title={cfg.notifyEnabled ? "关闭完成通知" : "开启完成通知"}
|
|
1625
|
-
onClick={() => patch({ notifyEnabled: !cfg.notifyEnabled })} />
|
|
1626
|
-
</span>
|
|
1627
|
-
</div>
|
|
1628
|
-
{cfg.notifyEnabled ? <div className="dkan-rows-narrow">
|
|
1629
|
-
<div className="dkan-row">
|
|
1630
|
-
<span className="dkan-row-label">完成通知</span>
|
|
1631
|
-
<button type="button" className={"dkm-miniswitch" + (cfg.notifyOnComplete ? " on" : "")}
|
|
1632
|
-
onClick={() => patch({ notifyOnComplete: !cfg.notifyOnComplete })}>
|
|
1633
|
-
{cfg.notifyOnComplete ? "开" : "关"}
|
|
1634
|
-
</button>
|
|
1635
|
-
<span className="dkan-row-sub">任务正常完成时通知</span>
|
|
1636
|
-
</div>
|
|
1637
|
-
<div className="dkan-row">
|
|
1638
|
-
<span className="dkan-row-label">异常通知</span>
|
|
1639
|
-
<button type="button" className={"dkm-miniswitch" + (cfg.notifyOnError ? " on" : "")}
|
|
1640
|
-
onClick={() => patch({ notifyOnError: !cfg.notifyOnError })}>
|
|
1641
|
-
{cfg.notifyOnError ? "开" : "关"}
|
|
1642
|
-
</button>
|
|
1643
|
-
<span className="dkan-row-sub">出错 / 中止 / 达输出上限时通知</span>
|
|
1644
|
-
</div>
|
|
1645
|
-
<div className="dkan-row">
|
|
1646
|
-
<span className="dkan-row-label">需确认提醒</span>
|
|
1647
|
-
<button type="button" className={"dkm-miniswitch" + (cfg.notifyOnConfirm !== false ? " on" : "")}
|
|
1648
|
-
onClick={() => patch({ notifyOnConfirm: cfg.notifyOnConfirm === false })}>
|
|
1649
|
-
{cfg.notifyOnConfirm !== false ? "开" : "关"}
|
|
1650
|
-
</button>
|
|
1651
|
-
<span className="dkan-row-sub">工具等待你批准时弹卡片并响确认音(常驻不自动消失)</span>
|
|
1652
|
-
</div>
|
|
1653
|
-
<div className="dkan-row">
|
|
1654
|
-
<span className="dkan-row-label">停留时长</span>
|
|
1655
|
-
<select className="dkan-select" value={String(cfg.notifyStayMs)}
|
|
1656
|
-
onChange={(e) => patch({ notifyStayMs: Number(e.target.value) })}>
|
|
1657
|
-
<option value="4000">4 秒</option>
|
|
1658
|
-
<option value="8000">8 秒</option>
|
|
1659
|
-
<option value="15000">15 秒</option>
|
|
1660
|
-
<option value="30000">30 秒</option>
|
|
1661
|
-
<option value="0">常驻(手动关闭)</option>
|
|
1662
|
-
</select>
|
|
1663
|
-
</div>
|
|
1664
|
-
<div className="dkan-row">
|
|
1665
|
-
<span className="dkan-row-label">系统通知</span>
|
|
1666
|
-
<button type="button" className={"dkm-miniswitch" + (cfg.systemNotify ? " on" : "")}
|
|
1667
|
-
onClick={() => enableSystemNotify(!cfg.systemNotify)}>
|
|
1668
|
-
{cfg.systemNotify ? "开" : "关"}
|
|
1669
|
-
</button>
|
|
1670
|
-
<span className="dkan-row-sub">{permNote}</span>
|
|
1671
|
-
</div>
|
|
1672
|
-
<div className="dkan-row">
|
|
1673
|
-
<span className="dkan-row-label">提示音</span>
|
|
1674
|
-
<button type="button" className={"dkm-miniswitch" + (cfg.soundNotify !== false ? " on" : "")}
|
|
1675
|
-
onClick={() => patch({ soundNotify: cfg.soundNotify === false })}>
|
|
1676
|
-
{cfg.soundNotify !== false ? "开" : "关"}
|
|
1677
|
-
</button>
|
|
1678
|
-
<span className="dkan-row-sub">任务结束/需确认时播放(试听为先播完成音、后播异常音)</span>
|
|
1679
|
-
</div>
|
|
1680
|
-
{cfg.soundNotify !== false ? (
|
|
1681
|
-
<div className="dkan-sounds">
|
|
1682
|
-
{Object.keys(SOUND_LIBRARY).map((key) => (
|
|
1683
|
-
<button type="button" key={key}
|
|
1684
|
-
className={"dkan-sound" + (cfg.soundEffect === key ? " on" : "")}
|
|
1685
|
-
onClick={() => patch({ soundEffect: key })}>
|
|
1686
|
-
<span className="dkan-sound-name">
|
|
1687
|
-
{SOUND_LIBRARY[key].name}
|
|
1688
|
-
{cfg.soundEffect === key ? <span className="dkan-sound-cur">✓</span> : null}
|
|
1689
|
-
</span>
|
|
1690
|
-
<span className="dkan-sound-play" role="button" tabIndex={0}
|
|
1691
|
-
title={"试听 " + SOUND_LIBRARY[key].name}
|
|
1692
|
-
onClick={(e) => { e.stopPropagation(); previewSound(key); }}
|
|
1693
|
-
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); e.stopPropagation(); previewSound(key); } }}>▶</span>
|
|
1694
|
-
</button>
|
|
1695
|
-
))}
|
|
1696
|
-
</div>
|
|
1697
|
-
) : null}
|
|
1698
|
-
</div> : <div className="dkan-note">通知已关闭——任务结束时既不弹卡片也不推系统通知。</div>}
|
|
1699
|
-
</div>,
|
|
1700
|
-
<div key="dingtalk" className="dkan-sec">
|
|
1701
|
-
<div className="dkan-sec-head">
|
|
1702
|
-
<span className="dkan-sec-title">钉钉推送</span>
|
|
1703
|
-
<span className="dkan-sec-sub">任务结束推送到钉钉群机器人(宿主直发,浏览器关着也能推;事件跟随上方完成/异常开关)</span>
|
|
1704
|
-
<span className="dkan-sec-sw">
|
|
1705
|
-
<span className={"dkan-sec-swlabel" + (cfg.dingtalkEnabled ? " on" : "")}>{cfg.dingtalkEnabled ? "已开启" : "已关闭"}</span>
|
|
1706
|
-
<button type="button" className={"dock-sw" + (cfg.dingtalkEnabled ? " on" : "")}
|
|
1707
|
-
role="switch" aria-checked={cfg.dingtalkEnabled} aria-label="开关钉钉推送"
|
|
1708
|
-
title={cfg.dingtalkEnabled ? "关闭钉钉推送" : "开启钉钉推送"}
|
|
1709
|
-
onClick={() => patch({ dingtalkEnabled: !cfg.dingtalkEnabled })} />
|
|
1710
|
-
</span>
|
|
1711
|
-
</div>
|
|
1712
|
-
{cfg.dingtalkEnabled ? <div className="dkan-rows-narrow">
|
|
1713
|
-
<div className="dkan-row dkan-row-webhook">
|
|
1714
|
-
<span className="dkan-row-label">Webhook</span>
|
|
1715
|
-
<input type="text" className="dkan-input" spellCheck={false}
|
|
1716
|
-
value={cfg.dingtalkWebhook || ""}
|
|
1717
|
-
placeholder="https://oapi.dingtalk.com/robot/send?access_token=…"
|
|
1718
|
-
onChange={(e) => {
|
|
1719
|
-
editingWebhookRef.current = true;
|
|
1720
|
-
setCfg(Object.assign({}, cfg, { dingtalkWebhook: e.target.value }));
|
|
1721
|
-
}} />
|
|
1722
|
-
<button type="button" className="dkan-btn" disabled={!editingWebhookRef.current}
|
|
1723
|
-
onClick={saveWebhook}>
|
|
1724
|
-
{editingWebhookRef.current ? "保存" : "已保存"}
|
|
1725
|
-
</button>
|
|
1726
|
-
</div>
|
|
1727
|
-
<div className="dkan-row">
|
|
1728
|
-
<span className="dkan-row-label">连通测试</span>
|
|
1729
|
-
<button type="button" className="dkan-btn" disabled={testing} onClick={runDingtalkTest}>
|
|
1730
|
-
{testing ? "发送中…" : "发送测试消息"}
|
|
1731
|
-
</button>
|
|
1732
|
-
{testState ? <span className={"dkan-row-sub" + (testState.ok ? " dkan-ok" : " dkan-err")}>
|
|
1733
|
-
{testState.ok ? "✓ " : "✗ "}{testState.msg}
|
|
1734
|
-
</span> : <span className="dkan-row-sub">用当前保存的 Webhook 发一条测试消息</span>}
|
|
1735
|
-
</div>
|
|
1736
|
-
<div className="dkan-note">机器人创建:钉钉群 → 设置 → 智能群助手 → 添加机器人 → 自定义(Webhook),
|
|
1737
|
-
安全设置选「自定义关键词」填「任务」或「dsh」(推送标题含「任务」即可命中)。</div>
|
|
1738
|
-
</div> : <div className="dkan-note">未开启——任务结束不推送钉钉。</div>}
|
|
1739
|
-
</div>,
|
|
1740
|
-
<div key="feishu" className="dkan-sec">
|
|
1741
|
-
<div className="dkan-sec-head">
|
|
1742
|
-
<span className="dkan-sec-title">飞书推送</span>
|
|
1743
|
-
<span className="dkan-sec-sub">任务结束推送到飞书群机器人(宿主直发,浏览器关着也能推;事件跟随上方完成/异常开关)</span>
|
|
1744
|
-
<span className="dkan-sec-sw">
|
|
1745
|
-
<span className={"dkan-sec-swlabel" + (cfg.feishuEnabled ? " on" : "")}>{cfg.feishuEnabled ? "已开启" : "已关闭"}</span>
|
|
1746
|
-
<button type="button" className={"dock-sw" + (cfg.feishuEnabled ? " on" : "")}
|
|
1747
|
-
role="switch" aria-checked={cfg.feishuEnabled} aria-label="开关飞书推送"
|
|
1748
|
-
title={cfg.feishuEnabled ? "关闭飞书推送" : "开启飞书推送"}
|
|
1749
|
-
onClick={() => patch({ feishuEnabled: !cfg.feishuEnabled })} />
|
|
1750
|
-
</span>
|
|
1751
|
-
</div>
|
|
1752
|
-
{cfg.feishuEnabled ? <div className="dkan-rows-narrow">
|
|
1753
|
-
<div className="dkan-row dkan-row-webhook">
|
|
1754
|
-
<span className="dkan-row-label">Webhook</span>
|
|
1755
|
-
<input type="text" className="dkan-input" spellCheck={false}
|
|
1756
|
-
value={cfg.feishuWebhook || ""}
|
|
1757
|
-
placeholder="https://open.feishu.cn/open-apis/bot/v2/hook/…"
|
|
1758
|
-
onChange={(e) => {
|
|
1759
|
-
editingFeishuRef.current = true;
|
|
1760
|
-
setCfg(Object.assign({}, cfg, { feishuWebhook: e.target.value }));
|
|
1761
|
-
}} />
|
|
1762
|
-
<button type="button" className="dkan-btn" disabled={!editingFeishuRef.current}
|
|
1763
|
-
onClick={saveFeishuWebhook}>
|
|
1764
|
-
{editingFeishuRef.current ? "保存" : "已保存"}
|
|
1765
|
-
</button>
|
|
1766
|
-
</div>
|
|
1767
|
-
<div className="dkan-row">
|
|
1768
|
-
<span className="dkan-row-label">连通测试</span>
|
|
1769
|
-
<button type="button" className="dkan-btn" disabled={feishuTesting} onClick={runFeishuTest}>
|
|
1770
|
-
{feishuTesting ? "发送中…" : "发送测试消息"}
|
|
1771
|
-
</button>
|
|
1772
|
-
{feishuTestState ? <span className={"dkan-row-sub" + (feishuTestState.ok ? " dkan-ok" : " dkan-err")}>
|
|
1773
|
-
{feishuTestState.ok ? "✓ " : "✗ "}{feishuTestState.msg}
|
|
1774
|
-
</span> : <span className="dkan-row-sub">用当前保存的 Webhook 发一条测试消息</span>}
|
|
1775
|
-
</div>
|
|
1776
|
-
<div className="dkan-note">机器人创建:飞书群 → 设置 → 群机器人 → 添加机器人 → 自定义机器人(获取 Webhook 地址);
|
|
1777
|
-
安全设置如选「自定义关键词」填「任务」或「dsh」(推送标题含「任务」即可命中)。</div>
|
|
1778
|
-
</div> : <div className="dkan-note">未开启——任务结束不推送飞书。</div>}
|
|
1256
|
+
) : <div className="dkan-note">动画已关闭——任务进行中不会有任何动效(通知设置见左侧「任务通知」)。</div>}
|
|
1779
1257
|
</div>
|
|
1780
1258
|
);
|
|
1781
1259
|
}
|
|
1782
1260
|
if (saveErr) rows.push(<div key="err" className="dkan-note dkan-err">{saveErr}</div>);
|
|
1261
|
+
// 运行状态(进行中任务 / 最近完成 / 等待确认)已独立成左侧菜单项,这里只留一句指路
|
|
1783
1262
|
rows.push(
|
|
1784
|
-
<div key="
|
|
1785
|
-
|
|
1786
|
-
<span className="dkan-sec-title">运行状态</span>
|
|
1787
|
-
<span className="dkan-sec-sub">
|
|
1788
|
-
{snap.error ? "状态拉取失败:" + snap.error
|
|
1789
|
-
: waitingCount > 0 ? waitingCount + " 项等待确认 · " + active.length + " 个任务进行中"
|
|
1790
|
-
: active.length > 0 ? active.length + " 个任务进行中"
|
|
1791
|
-
: recent.length > 0 ? "空闲 · 显示最近完成" : "空闲 · 暂无任务记录"}
|
|
1792
|
-
</span>
|
|
1793
|
-
<button type="button" className="dkan-refresh" onClick={() => animationStore.refresh()}>
|
|
1794
|
-
{snap.loading ? "刷新中…" : "刷新"}
|
|
1795
|
-
</button>
|
|
1796
|
-
</div>
|
|
1797
|
-
{active.length > 0 ? <div className="dkan-tasks">{active.map((t) => <TaskRow key={t.sessionId} t={t} />)}</div> : null}
|
|
1798
|
-
{active.length === 0 && recent.length === 0
|
|
1799
|
-
? <div className="dkan-note">发起新会话任务后,这里会显示进行中与最近完成的任务;动画与通知同时在页面生效。</div> : null}
|
|
1800
|
-
{recent.length > 0 ? <div className="dkan-tasks dkan-tasks-done">{recent.map((t) => <TaskRow key={t.sessionId + ":" + t.endTime} t={t} done />)}</div> : null}
|
|
1263
|
+
<div key="hint" className="dkan-note">
|
|
1264
|
+
任务运行状态(进行中任务、阶段、Token、等待确认与最近完成)已独立成左侧「运行状态」菜单;本页只负责动效。
|
|
1801
1265
|
</div>
|
|
1802
1266
|
);
|
|
1803
1267
|
return <div className="dkan-root">{rows}</div>;
|
|
1804
1268
|
}
|
|
1805
1269
|
|
|
1806
1270
|
// ---------- 首页总揽概要 ----------
|
|
1271
|
+
// 任务进行中 / 空闲这类任务信息归【运行状态】模块,这里只报动画自身的状态:
|
|
1272
|
+
// 动效开关 + 当前模式(桌面伙伴另报大小),与面板页开关一一对应。
|
|
1807
1273
|
function AnimationStat(props) {
|
|
1808
1274
|
const snap = useAnimation(props && props.ctx);
|
|
1809
1275
|
const st = snap.status;
|
|
1810
|
-
if (snap.error) return <span className="dkan-err"
|
|
1811
|
-
if (!st) return <span
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
const
|
|
1816
|
-
|
|
1276
|
+
if (snap.error) return <span className="dkan-err">动画状态不可用(宿主需重启加载动画路由)</span>;
|
|
1277
|
+
if (!st) return <span>等待动画配置…</span>;
|
|
1278
|
+
const cfg = st.config;
|
|
1279
|
+
if (!cfg) return <span>等待动画配置…</span>;
|
|
1280
|
+
if (!cfg.animationEnabled) return <span>动效已关闭 · 任务进行中不显示动画</span>;
|
|
1281
|
+
const mode = EFFECT_MODES.find((m) => m.id === cfg.effectMode) || EFFECT_MODES[0];
|
|
1282
|
+
const scale = Math.round((Number(cfg.robotScale) || 1.35) * 100);
|
|
1283
|
+
return <span>{"动效:" + mode.name + (cfg.effectMode === "robot" ? " · 大小 " + scale + "%" : "")}</span>;
|
|
1817
1284
|
}
|
|
1818
1285
|
|
|
1819
1286
|
// ---------- 样式(dkan- 前缀;全部走主题变量,暗/亮色自适应;动效时长 = 基准 / --dkan-speed) ----------
|
|
@@ -1947,21 +1414,6 @@ const css = [
|
|
|
1947
1414
|
"@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)}}",
|
|
1948
1415
|
".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;}",
|
|
1949
1416
|
"@keyframes dkan-spin{to{transform:rotate(360deg)}}",
|
|
1950
|
-
// 通知卡片栈(右上角)
|
|
1951
|
-
".dkan-toasts{position:fixed;top:16px;right:16px;z-index:9995;display:flex;flex-direction:column;gap:8px;width:min(380px,calc(100vw - 32px));}",
|
|
1952
|
-
".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);}",
|
|
1953
|
-
".dkan-toast.out{animation:dkan-toast-out .24s var(--ds-ease-in-out) forwards;}",
|
|
1954
|
-
"@keyframes dkan-toast-in{from{opacity:0;transform:translateX(14px)}to{opacity:1;transform:none}}",
|
|
1955
|
-
"@keyframes dkan-toast-out{to{opacity:0;transform:translateX(10px)}}",
|
|
1956
|
-
".dkan-toast-head{display:flex;align-items:center;gap:8px;margin-bottom:4px;}",
|
|
1957
|
-
".dkan-toast-mark{width:8px;height:8px;border-radius:50%;flex:none;}",
|
|
1958
|
-
".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;}",
|
|
1959
|
-
".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;}",
|
|
1960
|
-
".dkan-toast-close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
|
|
1961
|
-
".dkan-toast-body{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.7;white-space:pre-line;word-break:break-word;}",
|
|
1962
|
-
// 需确认卡片:琥珀描边 + 缓慢呼吸光晕,常驻提醒直到用户处理
|
|
1963
|
-
".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;}",
|
|
1964
|
-
"@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)}}",
|
|
1965
1417
|
// 面板页布局
|
|
1966
1418
|
".dkan-root{display:flex;flex-direction:column;gap:10px;}",
|
|
1967
1419
|
".dkan-note{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.6;}",
|
|
@@ -1973,8 +1425,6 @@ const css = [
|
|
|
1973
1425
|
".dkan-sec-sw{margin-left:auto;flex:none;display:inline-flex;align-items:center;gap:8px;}",
|
|
1974
1426
|
".dkan-sec-swlabel{font-size:11px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;}",
|
|
1975
1427
|
".dkan-sec-swlabel.on{color:var(--dsw-alias-state-success-primary);}",
|
|
1976
|
-
".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;}",
|
|
1977
|
-
".dkan-refresh:hover{background:var(--dsw-alias-interactive-bg-hover);}",
|
|
1978
1428
|
// 模式选择卡(带缩微预览)
|
|
1979
1429
|
".dkan-modes{display:flex;gap:8px;flex-wrap:wrap;}",
|
|
1980
1430
|
".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);}",
|
|
@@ -2197,44 +1647,6 @@ const css = [
|
|
|
2197
1647
|
"@keyframes dkan-search-wrist{0%,34%{transform:translateX(-1px)}62%,100%{transform:translateX(1.5px)}}",
|
|
2198
1648
|
// 减少动态时停止空间位移,保留屏幕亮暗与思考状态的淡入反馈。
|
|
2199
1649
|
"@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}}",
|
|
2200
|
-
// 通知子选项行
|
|
2201
|
-
".dkan-rows-narrow{display:flex;flex-direction:column;gap:6px;}",
|
|
2202
|
-
".dkan-row{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary);flex-wrap:wrap;}",
|
|
2203
|
-
".dkan-row-label{flex:none;min-width:60px;color:var(--dsw-alias-label-primary);}",
|
|
2204
|
-
".dkan-row-sub{font-size:11px;color:var(--dsw-alias-label-tertiary);}",
|
|
2205
|
-
".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;}",
|
|
2206
|
-
".dkm-miniswitch.on{color:var(--dsw-alias-state-success-primary);border-color:currentColor;}",
|
|
2207
|
-
".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;}",
|
|
2208
|
-
// 音效选择卡(名称 + 播放键;选中态描边)
|
|
2209
|
-
".dkan-sounds{display:flex;gap:6px;flex-wrap:wrap;}",
|
|
2210
|
-
".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);}",
|
|
2211
|
-
".dkan-sound:hover{border-color:var(--dk-accent);}",
|
|
2212
|
-
".dkan-sound.on{border-color:var(--dk-accent);box-shadow:0 0 0 1px color-mix(in srgb,var(--dk-accent) 40%,transparent);}",
|
|
2213
|
-
".dkan-sound-name{font-size:12px;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:5px;}",
|
|
2214
|
-
".dkan-sound-cur{color:var(--dk-accent);font-size:11px;}",
|
|
2215
|
-
".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);}",
|
|
2216
|
-
".dkan-sound-play:hover{color:var(--dsw-alias-label-primary);border-color:currentColor;}",
|
|
2217
|
-
".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;}",
|
|
2218
|
-
".dkan-input:focus{outline:none;border-color:var(--dk-accent);}",
|
|
2219
|
-
".dkan-row-webhook{flex-wrap:nowrap;}",
|
|
2220
|
-
".dkan-row-webhook .dkan-input{min-width:0;}",
|
|
2221
|
-
".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;}",
|
|
2222
|
-
".dkan-btn:hover{background:var(--dsw-alias-interactive-bg-hover);}",
|
|
2223
|
-
".dkan-btn[disabled]{opacity:.5;cursor:default;}",
|
|
2224
|
-
".dkan-ok{color:var(--dsw-alias-state-success-primary);}",
|
|
2225
|
-
// 任务列表
|
|
2226
|
-
".dkan-tasks{display:flex;flex-direction:column;gap:6px;}",
|
|
2227
|
-
".dkan-tasks-done{border-top:1px dashed var(--dsw-alias-border-l2);padding-top:6px;}",
|
|
2228
|
-
".dkan-task{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:6px 10px;display:flex;flex-direction:column;gap:3px;}",
|
|
2229
|
-
".dkan-task-head{display:flex;align-items:center;gap:8px;min-width:0;}",
|
|
2230
|
-
".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;}",
|
|
2231
|
-
".dkan-task-meta{display:flex;gap:10px;flex-wrap:wrap;font-size:11px;color:var(--dsw-alias-label-tertiary);}",
|
|
2232
|
-
".dkan-task-err{font-size:11px;color:var(--dsw-alias-state-error-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
2233
|
-
".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);}",
|
|
2234
|
-
".dkan-tag.on{color:var(--dk-accent);border-color:currentColor;}",
|
|
2235
|
-
".dkan-tag.ok{color:var(--dsw-alias-state-success-primary);border-color:currentColor;}",
|
|
2236
|
-
".dkan-tag.err{color:var(--dsw-alias-state-error-primary);border-color:currentColor;}",
|
|
2237
|
-
".dkan-tag.warn{color:var(--dk-warn);border-color:currentColor;}",
|
|
2238
1650
|
].join("\n");
|
|
2239
1651
|
|
|
2240
1652
|
export const feature = {
|
|
@@ -2242,7 +1654,7 @@ export const feature = {
|
|
|
2242
1654
|
name: "任务动画",
|
|
2243
1655
|
order: 130,
|
|
2244
1656
|
accent: "#f472b6",
|
|
2245
|
-
description: "19
|
|
1657
|
+
description: "19 种任务运行动画:速度随任务活动联动,配置持久化(通知见「任务通知」,任务状态见「运行状态」)",
|
|
2246
1658
|
css,
|
|
2247
1659
|
View: AnimationView,
|
|
2248
1660
|
HomeStat: AnimationStat,
|