dsh-dock 0.9.2 → 0.9.5
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 +5 -3
- package/client.js +277 -31
- package/features/animation/host.js +20 -2
- package/features/animation/view.jsx +171 -28
- package/features/balance/host.js +1 -1
- package/features/games/view.jsx +1 -1
- package/features/heartbeat/view.js +1 -0
- package/features/mobile-relay/host.js +92 -2
- package/features/mobile-relay/view.jsx +129 -3
- package/features/modelconfig/host.js +1 -1
- package/features/theme/view.js +1 -0
- package/features/tokenlog/host.js +1 -1
- package/features/visionproxy/host.js +1 -1
- package/package.json +2 -2
- package/src/client.jsx +1 -1
- package/src/host-core.js +2 -0
- package/src/shared.js +3 -2
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
//
|
|
10
10
|
// 配置模型(schemastery schema 见 src/host-core.js DockConfig.animation):
|
|
11
11
|
// animationEnabled / effectMode / notifyEnabled / notifyOnComplete /
|
|
12
|
-
// notifyOnError / notifyStayMs / systemNotify
|
|
12
|
+
// notifyOnError / notifyOnConfirm / notifyStayMs / systemNotify
|
|
13
13
|
// 动画与通知是两个独立开关,可只开其一;全部经 settings 持久化,重启后恢复。
|
|
14
14
|
import { DOCK_NS, ANIMATION_MODES, SOUND_EFFECTS, sendJson, readBody } from '../../src/host-core.js'
|
|
15
15
|
|
|
@@ -22,6 +22,7 @@ function defaultConfig() {
|
|
|
22
22
|
notifyEnabled: true,
|
|
23
23
|
notifyOnComplete: true,
|
|
24
24
|
notifyOnError: true,
|
|
25
|
+
notifyOnConfirm: true,
|
|
25
26
|
notifyStayMs: 8000,
|
|
26
27
|
systemNotify: false,
|
|
27
28
|
soundNotify: true,
|
|
@@ -55,7 +56,7 @@ export const feature = {
|
|
|
55
56
|
id: 'animation',
|
|
56
57
|
name: '任务动画',
|
|
57
58
|
description: '任务运行动画与完成通知(动画/通知独立开关,配置持久化)',
|
|
58
|
-
defaultEnabled:
|
|
59
|
+
defaultEnabled: false,
|
|
59
60
|
setup(ctx) {
|
|
60
61
|
const disposers = []
|
|
61
62
|
const dispose = () => {
|
|
@@ -132,6 +133,8 @@ export const feature = {
|
|
|
132
133
|
errorMessage: '',
|
|
133
134
|
lastText: '',
|
|
134
135
|
firstPrompt: '',
|
|
136
|
+
// 工具等待用户确认(dsh 会话流 approval/asked):浏览器侧据此弹"需确认"通知
|
|
137
|
+
approvals: new Map(), // approvalId -> { toolName, reason, askedAt }
|
|
135
138
|
phase: 'think',
|
|
136
139
|
phaseAt: Date.now(),
|
|
137
140
|
lastActivityAt: Date.now(),
|
|
@@ -393,6 +396,19 @@ export const feature = {
|
|
|
393
396
|
const msg = (reason.error && (reason.error.message || reason.error.code)) || ''
|
|
394
397
|
s.errorMessage = typeof msg === 'string' ? msg : String(msg)
|
|
395
398
|
}
|
|
399
|
+
} else if (event.type === 'approval/asked' && event.data) {
|
|
400
|
+
// 工具等待用户确认/批准:挂在会话记录上,/status 下发给浏览器弹"需确认"通知
|
|
401
|
+
const id = typeof event.data.id === 'string' ? event.data.id : ''
|
|
402
|
+
if (id) {
|
|
403
|
+
s.approvals.set(id, {
|
|
404
|
+
toolName: typeof event.data.toolName === 'string' ? event.data.toolName : '',
|
|
405
|
+
reason: typeof event.data.reason === 'string' ? truncate(event.data.reason, 200) : '',
|
|
406
|
+
askedAt: event.time || Date.now(),
|
|
407
|
+
})
|
|
408
|
+
}
|
|
409
|
+
s.lastActivityAt = now
|
|
410
|
+
} else if (event.type === 'approval/decided' && event.data) {
|
|
411
|
+
s.approvals.delete(typeof event.data.id === 'string' ? event.data.id : '')
|
|
396
412
|
} else if (event.type === 'tool/call') {
|
|
397
413
|
s.toolCalls++
|
|
398
414
|
s.phase = phaseOfToolName(event.data && event.data.name)
|
|
@@ -506,6 +522,7 @@ export const feature = {
|
|
|
506
522
|
lastActivityAt: s.lastActivityAt,
|
|
507
523
|
motionTicks: s.motionTicks,
|
|
508
524
|
lastMotionAt: s.lastMotionAt,
|
|
525
|
+
approvals: [...s.approvals.entries()].map(([id, v]) => ({ id, ...v })),
|
|
509
526
|
})
|
|
510
527
|
}
|
|
511
528
|
return sendJson(res, 200, {
|
|
@@ -531,6 +548,7 @@ export const feature = {
|
|
|
531
548
|
if (typeof p.notifyEnabled === 'boolean') cfg.notifyEnabled = p.notifyEnabled
|
|
532
549
|
if (typeof p.notifyOnComplete === 'boolean') cfg.notifyOnComplete = p.notifyOnComplete
|
|
533
550
|
if (typeof p.notifyOnError === 'boolean') cfg.notifyOnError = p.notifyOnError
|
|
551
|
+
if (typeof p.notifyOnConfirm === 'boolean') cfg.notifyOnConfirm = p.notifyOnConfirm
|
|
534
552
|
if (typeof p.notifyStayMs === 'number' && Number.isFinite(p.notifyStayMs)) {
|
|
535
553
|
cfg.notifyStayMs = Math.max(0, Math.min(600000, Math.round(p.notifyStayMs)))
|
|
536
554
|
}
|
|
@@ -66,6 +66,15 @@ function phaseLabel(p) { return PHASE_LABELS[p] || "工作中"; }
|
|
|
66
66
|
const PHASE_COLORS = { think: "#2f6fed", write: "#0d9488", code: "#b45309", search: "#0e7490" };
|
|
67
67
|
function phaseColor(p) { return PHASE_COLORS[p] || "#2f6fed"; }
|
|
68
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
|
+
|
|
69
78
|
// ---------- 结束原因 ----------
|
|
70
79
|
const END_LABELS = {
|
|
71
80
|
completed: { label: "完成", cls: "ok" },
|
|
@@ -305,15 +314,16 @@ function RobotScene(props) {
|
|
|
305
314
|
}
|
|
306
315
|
|
|
307
316
|
// ---------- 提示音(WebAudio 合成;macOS/Windows 通用,无音频文件) ----------
|
|
308
|
-
// 音效库:每种含完成音(done
|
|
309
|
-
// 音符 [频率, 起始秒, 时长秒, 波形?],波形缺省 sine
|
|
317
|
+
// 音效库:每种含完成音(done)、异常音(err)与确认音(ask)三套音符序列。
|
|
318
|
+
// 音符 [频率, 起始秒, 时长秒, 波形?],波形缺省 sine。确认音双音上扬、偏长,
|
|
319
|
+
// 听感是"来人搭话",和完成音的收束感、异常音的下坠感区分开。
|
|
310
320
|
const SOUND_LIBRARY = {
|
|
311
|
-
chime: { name: "清脆双音", notes: { done: [[659.25, 0, .14], [880, .13, .24]], err: [[220, 0, .16], [164.81, .15, .3]] } },
|
|
312
|
-
ding: { name: "叮", notes: { done: [[987.77, 0, .35]], err: [[246.94, 0, .4]] } },
|
|
313
|
-
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]] } },
|
|
314
|
-
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]] } },
|
|
315
|
-
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]] } },
|
|
316
|
-
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]] } },
|
|
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]] } },
|
|
317
327
|
};
|
|
318
328
|
// AudioContext 按需创建(浏览器自动播放策略:首次用户交互后才能出声,静默失败不报错)
|
|
319
329
|
let soundCtx = null;
|
|
@@ -338,11 +348,15 @@ function playTone(seq) {
|
|
|
338
348
|
}
|
|
339
349
|
} catch { /* 音频不可用静默 */ }
|
|
340
350
|
}
|
|
341
|
-
// 按音效播放:完成音 /
|
|
351
|
+
// 按音效播放:完成音 / 异常音 / 确认音(未知音效回退 chime)
|
|
342
352
|
function playDoneSound(success, effect) {
|
|
343
353
|
const lib = SOUND_LIBRARY[effect] || SOUND_LIBRARY.chime;
|
|
344
354
|
playTone(success ? lib.notes.done : lib.notes.err);
|
|
345
355
|
}
|
|
356
|
+
function playAskSound(effect) {
|
|
357
|
+
const lib = SOUND_LIBRARY[effect] || SOUND_LIBRARY.chime;
|
|
358
|
+
playTone(lib.notes.ask || lib.notes.done);
|
|
359
|
+
}
|
|
346
360
|
// 试听:完成音 + 异常音 连播(间隔 0.55s)
|
|
347
361
|
function previewSound(effect) {
|
|
348
362
|
const lib = SOUND_LIBRARY[effect] || SOUND_LIBRARY.chime;
|
|
@@ -371,12 +385,14 @@ function Toast(props) {
|
|
|
371
385
|
? "var(--dk-ok)"
|
|
372
386
|
: t.kind === "error"
|
|
373
387
|
? "var(--dk-err)"
|
|
374
|
-
: "
|
|
388
|
+
: t.kind === "confirm"
|
|
389
|
+
? "var(--dk-warn)"
|
|
390
|
+
: "var(--dk-warn)";
|
|
375
391
|
return (
|
|
376
|
-
<div className={"dkan-toast" + (closing ? " out" : "")}>
|
|
392
|
+
<div className={"dkan-toast" + (t.kind === "confirm" ? " dkan-toast-confirm" : "") + (closing ? " out" : "")}>
|
|
377
393
|
<div className="dkan-toast-head">
|
|
378
394
|
<span className="dkan-toast-mark" style={{ background: markColor }} />
|
|
379
|
-
<span className="dkan-toast-title" title={t.title}>{t.title}</span>
|
|
395
|
+
<span className="dkan-toast-title" title={t.title}>{t.kind === "confirm" ? "✋ " : ""}{t.title}</span>
|
|
380
396
|
<button type="button" className="dkan-toast-close" aria-label="关闭" onClick={close}>✕</button>
|
|
381
397
|
</div>
|
|
382
398
|
{t.body ? <div className="dkan-toast-body">{t.body}</div> : null}
|
|
@@ -587,6 +603,28 @@ function visibleRect(el) {
|
|
|
587
603
|
const r = el.getBoundingClientRect();
|
|
588
604
|
return r.width > 0 && r.height > 0 && r.bottom > 0 && r.top < window.innerHeight ? r : null;
|
|
589
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
|
+
}
|
|
590
628
|
const COMPOSER_SEND_SELECTOR = 'button[aria-label="Send message"],button[aria-label*="发送"],button[data-testid*="send"],button[data-testid*="submit"],button[type="submit"]';
|
|
591
629
|
function conversationComposer() {
|
|
592
630
|
if (typeof document === "undefined" || typeof window === "undefined") return null;
|
|
@@ -651,7 +689,6 @@ function useSpaceDocks(enabled) {
|
|
|
651
689
|
let frame = 0;
|
|
652
690
|
let idle = 0;
|
|
653
691
|
let timer = 0;
|
|
654
|
-
let pollTimer = 0;
|
|
655
692
|
let retries = 0;
|
|
656
693
|
let pending = false;
|
|
657
694
|
let observedComposer = null;
|
|
@@ -710,7 +747,21 @@ function useSpaceDocks(enabled) {
|
|
|
710
747
|
if (mutationObserver && document.body) mutationObserver.observe(document.body, { childList: true, subtree: true });
|
|
711
748
|
schedule(0, true);
|
|
712
749
|
// 部分会话会复用同一输入框节点、只改变祖先布局;低频校准覆盖这种无 DOM 替换的位移。
|
|
713
|
-
|
|
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();
|
|
714
765
|
const onResize = () => {
|
|
715
766
|
if (idle && typeof window.cancelIdleCallback === "function") window.cancelIdleCallback(idle);
|
|
716
767
|
if (timer) window.clearTimeout(timer);
|
|
@@ -726,6 +777,7 @@ function useSpaceDocks(enabled) {
|
|
|
726
777
|
if (pollTimer) window.clearInterval(pollTimer);
|
|
727
778
|
if (mutationObserver) mutationObserver.disconnect();
|
|
728
779
|
if (resizeObserver) resizeObserver.disconnect();
|
|
780
|
+
unsubscribeActivity();
|
|
729
781
|
window.removeEventListener("resize", onResize);
|
|
730
782
|
};
|
|
731
783
|
}, [enabled]);
|
|
@@ -787,7 +839,8 @@ function MissionDeparture(props) {
|
|
|
787
839
|
}} aria-hidden="true"><FreighterHull units={3} empty /></div>;
|
|
788
840
|
}
|
|
789
841
|
function ProgrammingStuck(props) {
|
|
790
|
-
|
|
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>;
|
|
791
844
|
}
|
|
792
845
|
|
|
793
846
|
// ---------- 全局浮层:轮询 + 动效 + 通知(功能启用即常驻) ----------
|
|
@@ -915,6 +968,58 @@ export function AnimationOverlay(props) {
|
|
|
915
968
|
prevActiveRef.current = st.active.slice();
|
|
916
969
|
}, [snap.status]);
|
|
917
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
|
+
|
|
918
1023
|
const handleTaskEnd = (task, record, cfg) => {
|
|
919
1024
|
const reason = (record && record.endReason) || "";
|
|
920
1025
|
const success = isSuccessReason(reason);
|
|
@@ -1002,6 +1107,21 @@ export function AnimationOverlay(props) {
|
|
|
1002
1107
|
const phase = active.length > 0
|
|
1003
1108
|
? active.reduce((best, x) => (!best || (x.phaseAt || 0) > (best.phaseAt || 0) ? x : best), null).phase
|
|
1004
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;
|
|
1005
1125
|
useTicker(ctx, animOn);
|
|
1006
1126
|
// 徽标计时:以最早开始的任务为基准
|
|
1007
1127
|
const now = Date.now();
|
|
@@ -1033,7 +1153,8 @@ export function AnimationOverlay(props) {
|
|
|
1033
1153
|
const spaceDocks = useSpaceDocks(spaceModeOn);
|
|
1034
1154
|
const spaceGalaxies = spaceModeOn && spaceDocks ? spaceGalaxyLayout() : null;
|
|
1035
1155
|
// Host 在编程阶段持续 18 秒没有 chunk/tool 活动,才把它视为卡住;正常长任务不会误触发。
|
|
1036
|
-
|
|
1156
|
+
// 卡住卡片同样只对当前会话出现。
|
|
1157
|
+
const stuckTask = onScreenTasks.find((task) => task.phase === "code" && now - (task.lastActivityAt || task.phaseAt || now) >= 18000) || null;
|
|
1037
1158
|
const robotScaleFromConfig = Math.max(.85, Math.min(2.2, Number(cfg && cfg.robotScale) || 1.35));
|
|
1038
1159
|
const speedStyle = { "--dkan-speed": Number(speed).toFixed(2) };
|
|
1039
1160
|
const saveRobotScale = useCallback((robotScale) => {
|
|
@@ -1147,7 +1268,7 @@ export function AnimationOverlay(props) {
|
|
|
1147
1268
|
? <AmbientLayer mode={mode} phase={phase} speed={Number(speed).toFixed(2)} tasks={active} galaxies={spaceGalaxies} /> : null}
|
|
1148
1269
|
{/* 星际模式:空闲时在输入框上方待命;任务卡住时在中央呈现编程等待状态。 */}
|
|
1149
1270
|
{spaceModeOn && spaceDocks && active.length === 0 && deliveries.length === 0 ? <ReadyFreighter dock={spaceDocks.ready} /> : null}
|
|
1150
|
-
{spaceModeOn && stuckTask ? <ProgrammingStuck task={stuckTask} /> : null}
|
|
1271
|
+
{spaceModeOn && stuckTask ? <ProgrammingStuck task={stuckTask} waiting={!!(stuckTask.approvals && stuckTask.approvals.length)} /> : null}
|
|
1151
1272
|
{!panelOpen ? departures.map((departure) => <MissionDeparture key={departure.id} departure={departure} />) : null}
|
|
1152
1273
|
{/* 环屏巡航:一颗光点沿屏幕边缘转圈(orbit 模式) */}
|
|
1153
1274
|
{ambientOn && mode === "orbit" ? <div className="dkan-orbit" style={speedStyle} aria-hidden="true" /> : null}
|
|
@@ -1155,8 +1276,8 @@ export function AnimationOverlay(props) {
|
|
|
1155
1276
|
<BurstLayer bursts={bursts} />
|
|
1156
1277
|
{/* 桌面伙伴:背侧视角紧凑双工位人物,阶段与任务同步;整卡可拖到任意位置(robot 模式) */}
|
|
1157
1278
|
{ambientOn && mode === "robot" ? (
|
|
1158
|
-
<div className="dkan-botcard" role="button" tabIndex={0} style={botCardStyle}
|
|
1159
|
-
title={active.length + " 个任务 · " + phaseLabel(phase) + " · 点击查看任务动画页 · 可拖动到任意位置"}
|
|
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)) + " · 点击查看任务动画页 · 可拖动到任意位置"}
|
|
1160
1281
|
onPointerDown={onBotPointerDown}
|
|
1161
1282
|
onPointerMove={onBotPointerMove}
|
|
1162
1283
|
onPointerUp={onBotPointerUp}
|
|
@@ -1165,7 +1286,7 @@ export function AnimationOverlay(props) {
|
|
|
1165
1286
|
onKeyDown={(e) => { if (e.target === e.currentTarget && (e.key === "Enter" || e.key === " ")) { e.preventDefault(); onBotClick(); } }}>
|
|
1166
1287
|
<RobotScene phase={phase} tasks={active} />
|
|
1167
1288
|
<span className="dkan-bot-cap">
|
|
1168
|
-
<span className="n">{active.length}</span> 个任务{elapsed ? " · " + elapsed : ""} · {phaseLabel(phase)}
|
|
1289
|
+
<span className="n">{active.length}</span> 个任务{elapsed ? " · " + elapsed : ""} · {waitingTask ? "等待确认" : phaseLabel(phase)}
|
|
1169
1290
|
</span>
|
|
1170
1291
|
<button type="button" className="dkan-bot-resize" aria-label="调整桌面伙伴大小"
|
|
1171
1292
|
title="拖动调整桌面伙伴大小"
|
|
@@ -1185,15 +1306,15 @@ export function AnimationOverlay(props) {
|
|
|
1185
1306
|
{/* 右下角状态徽标:dot 随模式变化(breathe/ring 装饰),颜色随任务阶段、呼吸随吞吐;点击进功能坞动画页 */}
|
|
1186
1307
|
{ambientOn && mode !== "robot" ? (
|
|
1187
1308
|
<button type="button" className={"dkan-badge" + (mode === "breathe" ? " dkan-badge-breathe" : "")}
|
|
1188
|
-
style={Object.assign({}, speedStyle, { "--dkan-phase": phaseColor(phase) })}
|
|
1189
|
-
title={active.length + " 个任务进行中 · " + phaseLabel(phase) + " · 点击查看任务动画页"}
|
|
1309
|
+
style={Object.assign({}, speedStyle, { "--dkan-phase": phaseColor(waitingTask ? "code" : phase) })}
|
|
1310
|
+
title={active.length + " 个任务进行中 · " + (waitingTask ? "等待确认" : phaseLabel(phase)) + " · 点击查看任务动画页"}
|
|
1190
1311
|
onClick={() => openPanel("animation")}>
|
|
1191
1312
|
<span className="dkan-dotwrap">
|
|
1192
1313
|
<span className="dkan-dot" />
|
|
1193
1314
|
{mode === "breathe" ? <span className="dkan-halo" /> : null}
|
|
1194
1315
|
{mode === "ring" ? <span className="dkan-ring" /> : null}
|
|
1195
1316
|
</span>
|
|
1196
|
-
<span className="dkan-badge-txt"><span className="n">{active.length}</span> 个任务{elapsed ? " · " + elapsed : ""} · {phaseLabel(phase)}</span>
|
|
1317
|
+
<span className="dkan-badge-txt"><span className="n">{active.length}</span> 个任务{elapsed ? " · " + elapsed : ""} · {waitingTask ? "等待确认" : phaseLabel(phase)}</span>
|
|
1197
1318
|
</button>
|
|
1198
1319
|
) : null}
|
|
1199
1320
|
{/* 完成瞬间的一次性流光(成功绿 / 异常红),动画结束自动清场 */}
|
|
@@ -1273,12 +1394,14 @@ function ModePreview({ id }) {
|
|
|
1273
1394
|
function TaskRow(props) {
|
|
1274
1395
|
const t = props.t;
|
|
1275
1396
|
const info = endInfo(t.endReason);
|
|
1397
|
+
const waiting = !props.done && Array.isArray(t.approvals) && t.approvals.length > 0;
|
|
1276
1398
|
return (
|
|
1277
1399
|
<div className="dkan-task">
|
|
1278
1400
|
<div className="dkan-task-head">
|
|
1279
1401
|
<span className="dkan-task-title" title={t.title}>{t.title || "(无标题)"}</span>
|
|
1280
1402
|
{props.done ? <span className={"dkan-tag " + info.cls}>{info.label}</span>
|
|
1281
|
-
: <span className="dkan-tag
|
|
1403
|
+
: waiting ? <span className="dkan-tag warn">等待确认</span>
|
|
1404
|
+
: <span className="dkan-tag on">{phaseLabel(t.phase)}</span>}
|
|
1282
1405
|
</div>
|
|
1283
1406
|
<div className="dkan-task-meta">
|
|
1284
1407
|
<span>{(t.models && t.models.length ? t.models.join(", ") : "未知") + (t.provider ? "(" + t.provider + ")" : "")}</span>
|
|
@@ -1287,6 +1410,11 @@ function TaskRow(props) {
|
|
|
1287
1410
|
{t.totalTokens ? <span>{"↧" + fmtCompact(t.inputTokens) + " ↥" + fmtCompact(t.outputTokens)}</span> : null}
|
|
1288
1411
|
{props.done && t.endTime ? <span>{fmtTime(t.endTime)}</span> : null}
|
|
1289
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}
|
|
1290
1418
|
{props.done && t.errorMessage ? <div className="dkan-task-err">{truncate(t.errorMessage, 120)}</div> : null}
|
|
1291
1419
|
</div>
|
|
1292
1420
|
);
|
|
@@ -1434,6 +1562,7 @@ function AnimationView(props) {
|
|
|
1434
1562
|
if (!cfg) {
|
|
1435
1563
|
rows.push(<div key="load" className="dkan-note">{snap.error ? "状态不可用:" + snap.error : (snap.loading ? "正在拉取任务状态…" : "等待任务状态")}</div>);
|
|
1436
1564
|
} else {
|
|
1565
|
+
const waitingCount = active.reduce((n, t) => n + (Array.isArray(t.approvals) ? t.approvals.length : 0), 0);
|
|
1437
1566
|
rows.push(
|
|
1438
1567
|
<div key="anim" className="dkan-sec">
|
|
1439
1568
|
<div className="dkan-sec-head">
|
|
@@ -1510,6 +1639,14 @@ function AnimationView(props) {
|
|
|
1510
1639
|
</button>
|
|
1511
1640
|
<span className="dkan-row-sub">出错 / 中止 / 达输出上限时通知</span>
|
|
1512
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>
|
|
1513
1650
|
<div className="dkan-row">
|
|
1514
1651
|
<span className="dkan-row-label">停留时长</span>
|
|
1515
1652
|
<select className="dkan-select" value={String(cfg.notifyStayMs)}
|
|
@@ -1535,7 +1672,7 @@ function AnimationView(props) {
|
|
|
1535
1672
|
onClick={() => patch({ soundNotify: cfg.soundNotify === false })}>
|
|
1536
1673
|
{cfg.soundNotify !== false ? "开" : "关"}
|
|
1537
1674
|
</button>
|
|
1538
|
-
<span className="dkan-row-sub"
|
|
1675
|
+
<span className="dkan-row-sub">任务结束/需确认时播放(试听为先播完成音、后播异常音)</span>
|
|
1539
1676
|
</div>
|
|
1540
1677
|
{cfg.soundNotify !== false ? (
|
|
1541
1678
|
<div className="dkan-sounds">
|
|
@@ -1646,8 +1783,9 @@ function AnimationView(props) {
|
|
|
1646
1783
|
<span className="dkan-sec-title">运行状态</span>
|
|
1647
1784
|
<span className="dkan-sec-sub">
|
|
1648
1785
|
{snap.error ? "状态拉取失败:" + snap.error
|
|
1649
|
-
:
|
|
1650
|
-
:
|
|
1786
|
+
: waitingCount > 0 ? waitingCount + " 项等待确认 · " + active.length + " 个任务进行中"
|
|
1787
|
+
: active.length > 0 ? active.length + " 个任务进行中"
|
|
1788
|
+
: recent.length > 0 ? "空闲 · 显示最近完成" : "空闲 · 暂无任务记录"}
|
|
1651
1789
|
</span>
|
|
1652
1790
|
<button type="button" className="dkan-refresh" onClick={() => animationStore.refresh()}>
|
|
1653
1791
|
{snap.loading ? "刷新中…" : "刷新"}
|
|
@@ -1727,7 +1865,9 @@ const css = [
|
|
|
1727
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);}",
|
|
1728
1866
|
"@media (max-width:1100px){.dkan-space-system,.dkan-space-runner{display:none}}",
|
|
1729
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))}}",
|
|
1730
|
-
".dkan-space-stuck{position:fixed;
|
|
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;}}",
|
|
1731
1871
|
// 极光:顶部柔光带呼吸流动(加浓)
|
|
1732
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;}",
|
|
1733
1873
|
".dkan-aurora i:nth-child(1){background:#4d9fff;}",
|
|
@@ -1816,6 +1956,9 @@ const css = [
|
|
|
1816
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;}",
|
|
1817
1957
|
".dkan-toast-close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
|
|
1818
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)}}",
|
|
1819
1962
|
// 面板页布局
|
|
1820
1963
|
".dkan-root{display:flex;flex-direction:column;gap:10px;}",
|
|
1821
1964
|
".dkan-note{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.6;}",
|
package/features/balance/host.js
CHANGED
|
@@ -475,7 +475,7 @@ export const feature = {
|
|
|
475
475
|
id: 'balance',
|
|
476
476
|
name: '模型余额',
|
|
477
477
|
description: '拉取所有模型 Provider 账户余额并展示',
|
|
478
|
-
defaultEnabled:
|
|
478
|
+
defaultEnabled: false,
|
|
479
479
|
setup(ctx) {
|
|
480
480
|
const webServer = ctx.get('webServer')
|
|
481
481
|
if (webServer === undefined) throw new Error('webServer 服务不可用,无法提供余额查询路由')
|
package/features/games/view.jsx
CHANGED
|
@@ -631,7 +631,7 @@ export const feature = {
|
|
|
631
631
|
order: 135,
|
|
632
632
|
accent: "#a78bfa",
|
|
633
633
|
description: "任务等待时可玩的十款小游戏:五子棋、中国象棋(你 vs AI)、俄罗斯方块、推箱子、贪吃蛇、打砖块、极速赛车、坦克大战、星际躲避、反应堆点亮。浮动窗口运行(可拖动、可调大小、可最大化最小化,不遮挡会话),屏幕侧边磁吸快捷入口贴在左侧边栏右缘(不挡工作区目录),可上下挪位、左右换边;均支持键盘",
|
|
634
|
-
defaultEnabled:
|
|
634
|
+
defaultEnabled: false,
|
|
635
635
|
css,
|
|
636
636
|
View: GamesView,
|
|
637
637
|
HomeStat: GamesHomeStat,
|
|
@@ -129,6 +129,9 @@ const LAN_COMPAT_MARKER = 'data-dsh-lan-compat'
|
|
|
129
129
|
// 1) 设置弹窗官方只有"左导航+右内容"两栏,窄屏内容栏被压到不足百像素(一字一行);
|
|
130
130
|
// 改纵向堆叠:导航横排在上、内容占满。
|
|
131
131
|
// 2) 侧边栏展开是 grid 栅格挤占会话区(窄屏会话区只剩 ~110px);改为浮层覆盖。
|
|
132
|
+
// 3) 侧栏栅格归零后,原生「打开侧边栏」按钮随窄轨一起被裁掉——补贴左边缘的抽屉把手
|
|
133
|
+
// (.dsh-mobile-drawer-btn,行为脚本创建,点击转发原生 toggle,位置在趣味游戏
|
|
134
|
+
// 浮标正上方、兜底左侧中部)与抽屉遮罩(.dsh-mobile-scrim,点击即收起,阻断滚动穿透)。
|
|
132
135
|
// 类名用语义后缀匹配(哈希前缀随 DSH 版本会变,后缀稳定);DSH 升级改了结构时
|
|
133
136
|
// 选择器自然失效,不影响其他功能。
|
|
134
137
|
const MOBILE_LAYOUT_CSS = [
|
|
@@ -144,13 +147,100 @@ const MOBILE_LAYOUT_CSS = [
|
|
|
144
147
|
' [class$="frame"]:not([data-sidebar-collapsed]) [class*="sidebarCol"]{position:fixed!important;top:0!important;bottom:0!important;left:0!important;width:min(85vw,320px)!important;z-index:80!important;box-shadow:0 12px 48px rgba(0,0,0,.45)}',
|
|
145
148
|
' [class$="frame"]:not([data-details-collapsed="true"]) [class*="detailsCol"]{position:fixed!important;top:0!important;bottom:0!important;right:0!important;left:auto!important;width:min(85vw,320px)!important;z-index:85!important;box-shadow:-12px 0 48px rgba(0,0,0,.45)}',
|
|
146
149
|
' [class*="overlayLayer"]{z-index:90!important}',
|
|
150
|
+
// 抽屉把手:贴左边缘(同趣味游戏浮标的边缘吸附语言),top 由 place() 动态设定
|
|
151
|
+
// (趣味游戏浮标正上方,兜底左侧中部);z 序 70 < 遮罩 75 < 侧栏 80 < overlayLayer 90,
|
|
152
|
+
// 面板/弹窗打开时自然被盖,无需额外隐藏逻辑。
|
|
153
|
+
' .dsh-mobile-drawer-btn{position:fixed;left:0;top:38vh;z-index:70;box-sizing:border-box;width:30px;height:48px;padding:0;display:flex;align-items:center;justify-content:center;border:1px solid var(--dsw-alias-border-l1,rgba(127,139,161,.35));border-left:none;border-radius:0 12px 12px 0;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2,#1c2230) 88%,transparent);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);color:var(--dsw-alias-label-primary,#e6eaf2);box-shadow:4px 0 18px rgba(0,0,0,.28);cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent}',
|
|
154
|
+
' .dsh-mobile-drawer-btn:active{transform:scale(.94)}',
|
|
155
|
+
' .dsh-mobile-drawer-btn svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}',
|
|
156
|
+
' .dsh-mobile-scrim{position:fixed;inset:0;z-index:75;background:rgba(8,10,14,.45);backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);touch-action:none}',
|
|
147
157
|
'}',
|
|
148
158
|
].join('\n')
|
|
149
159
|
const MOBILE_LAYOUT_MARKER = 'data-dsh-mobile-layout'
|
|
150
160
|
// 窄屏行为补丁:侧边栏浮层化后,原生的"保持展开"会一直挡住半屏——
|
|
151
161
|
// 点会话行/新会话后自动收起;点侧栏外的页面区域也收起(浮层语义)。
|
|
152
162
|
// 侧栏内的其他点击(工作区折叠、搜索、功能坞入口)保持原生行为不收。
|
|
153
|
-
|
|
163
|
+
// 另创建抽屉按钮(窄屏 + 侧栏收起时才显示,点击转发原生「打开侧边栏」toggle;
|
|
164
|
+
// 侧栏展开或设置弹窗打开时自动隐藏)与抽屉遮罩(点击经全局捕获 handler 收起,
|
|
165
|
+
// 单一路径防止同一事件两次 toggle)。注入点在 <head> 后,body 尚未解析,
|
|
166
|
+
// DOM 创建一律推迟到 DOMContentLoaded;显隐同步走 MutationObserver(属性翻转即时)
|
|
167
|
+
// + 低速轮询(覆盖路由重渲等一切边角)。
|
|
168
|
+
const MOBILE_BEHAVIOR_JS = [
|
|
169
|
+
';(function(){',
|
|
170
|
+
'var mq=window.matchMedia?window.matchMedia("(max-width:700px)"):null;',
|
|
171
|
+
'if(!mq)return;',
|
|
172
|
+
// 与 dsh-dock 客户端 Overlay 版(features/mobile-relay/view.jsx,刷新即生效)互斥:
|
|
173
|
+
// 本脚本在 <head> 先执行,存在即接管;客户端版看到旗标自动让位。
|
|
174
|
+
'if(window.__dshDockMobileDrawer)return;',
|
|
175
|
+
'window.__dshDockMobileDrawer="host";',
|
|
176
|
+
'function narrow(){return mq.matches}',
|
|
177
|
+
'function frameEl(){return document.querySelector(\'[class$="frame"]\')}',
|
|
178
|
+
'function sidebarCollapsed(){var f=frameEl();return !!f&&f.hasAttribute("data-sidebar-collapsed")}',
|
|
179
|
+
'function dialogOpen(){return !!document.querySelector(\'[role="dialog"][class*="panel"]\')}',
|
|
180
|
+
'function collapse(){var b=document.querySelector(\'button[aria-label="收起侧边栏"],button[aria-label="Collapse sidebar"]\');if(b)b.click()}',
|
|
181
|
+
'function expand(){var b=document.querySelector(\'button[aria-label="打开侧边栏"],button[aria-label="Open sidebar"]\');if(b)b.click()}',
|
|
182
|
+
'function closeDetails(){var d=document.querySelector(\'[class*="detailsCol"]\');if(!d||!d.getBoundingClientRect().width)return;var c=d.querySelector(\'button[aria-label="关闭详情"]\');if(c)c.click()}',
|
|
183
|
+
'var fab=null,scrim=null;',
|
|
184
|
+
'function ensureChrome(){',
|
|
185
|
+
' if(!fab){fab=document.createElement("button");fab.type="button";fab.className="dsh-mobile-drawer-btn";fab.setAttribute("aria-label","打开会话列表");fab.innerHTML=\'<svg viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h10"/></svg>\';fab.addEventListener("click",function(){expand()});document.body.appendChild(fab)}',
|
|
186
|
+
' if(!scrim){scrim=document.createElement("div");scrim.className="dsh-mobile-scrim";document.body.appendChild(scrim)}',
|
|
187
|
+
'}',
|
|
188
|
+
// 把手定位:贴着趣味游戏浮标(.dgfab,可拖拽)正上方;浮标太靠上时改放它下面
|
|
189
|
+
// (避开顶栏标题),被拖走/隐藏/不存在时兜底左侧中部(38% 视高)。
|
|
190
|
+
'function place(){',
|
|
191
|
+
' if(!fab)return;',
|
|
192
|
+
' var vh=window.innerHeight,top=null;',
|
|
193
|
+
' var g=document.querySelector(".dgfab");',
|
|
194
|
+
' if(g&&!g.classList.contains("dgfab-hide")&&g.getBoundingClientRect){',
|
|
195
|
+
' var r=g.getBoundingClientRect();',
|
|
196
|
+
' if(r&&r.height>0&&r.top>0){',
|
|
197
|
+
' top=r.top-fab.offsetHeight-8;',
|
|
198
|
+
' if(top<64)top=r.bottom+8;',
|
|
199
|
+
' if(top+fab.offsetHeight>vh-16)top=null;',
|
|
200
|
+
' }',
|
|
201
|
+
' }',
|
|
202
|
+
' if(top==null)top=Math.round(vh*.38);',
|
|
203
|
+
' fab.style.top=top+"px";',
|
|
204
|
+
'}',
|
|
205
|
+
'function syncChrome(){',
|
|
206
|
+
' if(!fab||!scrim)return;',
|
|
207
|
+
' var on=narrow()&&!!frameEl()&&!dialogOpen();',
|
|
208
|
+
' var col=sidebarCollapsed();',
|
|
209
|
+
' var showFab=on&&col;',
|
|
210
|
+
' fab.style.display=showFab?"":"none";',
|
|
211
|
+
' if(showFab)place();',
|
|
212
|
+
' scrim.style.display=on&&!col?"":"none";',
|
|
213
|
+
'}',
|
|
214
|
+
'function boot(){',
|
|
215
|
+
' ensureChrome();syncChrome();',
|
|
216
|
+
' if(typeof MutationObserver!=="undefined"){new MutationObserver(function(){syncChrome()}).observe(document.documentElement,{attributes:true,attributeFilter:["data-sidebar-collapsed"],subtree:true})}',
|
|
217
|
+
' setInterval(syncChrome,1500);',
|
|
218
|
+
' var onMq=function(){syncChrome()};',
|
|
219
|
+
' if(mq.addEventListener)mq.addEventListener("change",onMq);else if(mq.addListener)mq.addListener(onMq);',
|
|
220
|
+
' window.addEventListener("resize",place);',
|
|
221
|
+
'}',
|
|
222
|
+
'if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",boot);else boot();',
|
|
223
|
+
'document.addEventListener("click",function(e){',
|
|
224
|
+
' if(!narrow())return;',
|
|
225
|
+
' var t=e.target;',
|
|
226
|
+
' if(!t||!t.closest)return;',
|
|
227
|
+
' if(t.closest(".dsh-mobile-drawer-btn"))return;',
|
|
228
|
+
' var expanded=!!document.querySelector(\'button[aria-label="收起侧边栏"],button[aria-label="Collapse sidebar"]\');',
|
|
229
|
+
' var col=document.querySelector(\'[class*="sidebarCol"]\');',
|
|
230
|
+
' var inSidebar=col&&col.contains(t);',
|
|
231
|
+
' if(!expanded&&!inSidebar)return;',
|
|
232
|
+
' if(inSidebar){',
|
|
233
|
+
' var row=t.closest(\'[role="treeitem"]\');',
|
|
234
|
+
' var leaf=row&&!row.querySelector(\'[role="treeitem"]\')&&row.closest(\'[role="tree"]\');',
|
|
235
|
+
' var fresh=t.closest(\'[class*="newSession"]\');',
|
|
236
|
+
' if(leaf||fresh)setTimeout(function(){collapse();closeDetails()},300);',
|
|
237
|
+
' return',
|
|
238
|
+
' }',
|
|
239
|
+
' if(t.closest(\'[role="dialog"],[class*="dockm"],[class*="dgfab"],[class*="dgwin"],[class*="dgame"],[class*="detailsCol"]\'))return;',
|
|
240
|
+
' collapse()',
|
|
241
|
+
'},true)',
|
|
242
|
+
'})();',
|
|
243
|
+
].join('\n')
|
|
154
244
|
const MOBILE_BEHAVIOR_MARKER = 'data-dsh-mobile-behave'
|
|
155
245
|
/** 把兼容脚本、移动端排版样式与行为脚本内联注入 index.html 的 <head> 之后(幂等)。 */
|
|
156
246
|
function injectLanCompat(html) {
|
|
@@ -174,7 +264,7 @@ export const feature = {
|
|
|
174
264
|
id: 'mobile-relay',
|
|
175
265
|
name: '远程访问',
|
|
176
266
|
description: '账号密码登录的远程入口:局域网/虚拟网设备访问同一个 DSH,任务进度实时一致',
|
|
177
|
-
defaultEnabled:
|
|
267
|
+
defaultEnabled: false,
|
|
178
268
|
setup(ctx) {
|
|
179
269
|
const disposers = []
|
|
180
270
|
let gateway = null
|