dsh-dock 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -15
- package/client.js +578 -707
- package/features/animation/view.jsx +26 -12
- package/features/mobile-relay/host.js +3 -4
- package/features/mobile-relay/view.jsx +1 -2
- package/features/notify/host.js +18 -3
- package/features/notify/view.jsx +393 -68
- package/features/runstate/host.js +1 -11
- package/features/runstate/view.jsx +32 -15
- package/index.js +0 -3
- package/package.json +2 -2
- package/src/client.jsx +2 -3
- package/src/host-core.js +14 -2
- package/src/shared.js +86 -0
- package/src/task-track.js +135 -3
- package/features/mobile/host.js +0 -315
- package/features/mobile/view.js +0 -530
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
//
|
|
15
15
|
// Host 通信:fetch('dsh-dock/animation/<method>')(见 features/animation/host.js)。
|
|
16
16
|
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
|
17
|
-
import { openPanel, panelNav, subscribePanel } from "../../src/shared.js";
|
|
17
|
+
import { openPanel, panelNav, subscribePanel, taskWaiting, taskApprovals, taskStalled } from "../../src/shared.js";
|
|
18
18
|
|
|
19
19
|
// ---------- Host RPC 桥接 ----------
|
|
20
20
|
function rpcCall(method, args) {
|
|
@@ -721,7 +721,11 @@ function MissionDeparture(props) {
|
|
|
721
721
|
}
|
|
722
722
|
function ProgrammingStuck(props) {
|
|
723
723
|
const waiting = !!props.waiting;
|
|
724
|
-
|
|
724
|
+
// 等人的性质要说清:等批准(要不要执行)与等回答(选哪个答案)是两件事
|
|
725
|
+
const task = props.task;
|
|
726
|
+
const label = !waiting ? "编程等待中"
|
|
727
|
+
: (taskApprovals(task).length > 0 ? "等待确认" : "等待回答");
|
|
728
|
+
return <div className={"dkan-space-stuck" + (waiting ? " waiting" : "")} aria-hidden="true"><FreighterHull units={3} empty /><span className="dkan-stuck-code"><i />{`{ }`}</span><strong>{label}</strong><small>{truncate((props.task && props.task.title) || "工具暂无新响应", 28)}</small></div>;
|
|
725
729
|
}
|
|
726
730
|
|
|
727
731
|
// ---------- 全局浮层:轮询 + 动效(功能启用即常驻;通知在 features/notify/) ----------
|
|
@@ -889,9 +893,13 @@ export function AnimationOverlay(props) {
|
|
|
889
893
|
const phase = active.length > 0
|
|
890
894
|
? active.reduce((best, x) => (!best || (x.phaseAt || 0) > (best.phaseAt || 0) ? x : best), null).phase
|
|
891
895
|
: "think";
|
|
892
|
-
//
|
|
896
|
+
// 有会话在等用户介入时,状态文字让位给"等待确认 / 等待回答"(优先级最高:不动手就停住)。
|
|
893
897
|
// 会话级卡片(等待确认/编程等待中)只对"当前正在看的会话"出现,别的会话等待不打扰。
|
|
894
|
-
|
|
898
|
+
// 等人介入的口径统一走 src/shared.js 的 taskWaiting——只认 approvals 会把「提问等回答」
|
|
899
|
+
// 漏成"编程等待中",甚至被卡住判定误报(2026-11-01 用户实测)。
|
|
900
|
+
const waitingTask = active.find((x) => taskWaiting(x)) || null;
|
|
901
|
+
// 等的是批准还是回答:徽标文字跟着变,别一律说「等待确认」把提问吞进去
|
|
902
|
+
const waitWord = waitingTask && taskApprovals(waitingTask).length === 0 ? "等待回答" : "等待确认";
|
|
895
903
|
const sessionTitleRef = useRef({ at: 0, title: "" });
|
|
896
904
|
const currentTitle = useMemo(() => {
|
|
897
905
|
// 标题查询要 getBoundingClientRect(强制布局),限频 1s;面板页/任务行为不受此限制
|
|
@@ -903,7 +911,7 @@ export function AnimationOverlay(props) {
|
|
|
903
911
|
return c.title;
|
|
904
912
|
}, [snap.status]);
|
|
905
913
|
const onScreenTasks = useMemo(() => active.filter((x) => taskOnScreen(x, currentTitle)), [active, currentTitle]);
|
|
906
|
-
const waitingTaskOnScreen = onScreenTasks.find((x) =>
|
|
914
|
+
const waitingTaskOnScreen = onScreenTasks.find((x) => taskWaiting(x)) || null;
|
|
907
915
|
useTicker(ctx, animOn);
|
|
908
916
|
// 徽标计时:以最早开始的任务为基准
|
|
909
917
|
const now = Date.now();
|
|
@@ -936,7 +944,13 @@ export function AnimationOverlay(props) {
|
|
|
936
944
|
const spaceGalaxies = spaceModeOn && spaceDocks ? spaceGalaxyLayout() : null;
|
|
937
945
|
// Host 在编程阶段持续 18 秒没有 chunk/tool 活动,才把它视为卡住;正常长任务不会误触发。
|
|
938
946
|
// 卡住卡片同样只对当前会话出现。
|
|
939
|
-
|
|
947
|
+
// 停在原地等人的会话不算卡住(等批准、等回答都是合法停点,会话零事件是必然的)——
|
|
948
|
+
// 与【任务通知】的卡住判定同一口径:两处都直接调 src/shared.js 的 taskStalled,
|
|
949
|
+
// 它已经把「有在飞的工具调用」「正在压缩上下文」这两种正在干活的安静排除在外
|
|
950
|
+
//(2026-11-02 实测:25 分钟里 18 次误报全落在 compaction 窗口内,最长 96.7 秒)。
|
|
951
|
+
// 别在这里另写一份表达式,否则动画报卡住而通知不报,自相矛盾。
|
|
952
|
+
const stuckTask = onScreenTasks.find((task) => task.phase === "code"
|
|
953
|
+
&& taskStalled(task, now, 18000)) || null;
|
|
940
954
|
const robotScaleFromConfig = Math.max(.85, Math.min(2.2, Number(cfg && cfg.robotScale) || 1.35));
|
|
941
955
|
const speedStyle = { "--dkan-speed": Number(speed).toFixed(2) };
|
|
942
956
|
const saveRobotScale = useCallback((robotScale) => {
|
|
@@ -1050,7 +1064,7 @@ export function AnimationOverlay(props) {
|
|
|
1050
1064
|
? <AmbientLayer mode={mode} phase={phase} speed={Number(speed).toFixed(2)} tasks={active} galaxies={spaceGalaxies} /> : null}
|
|
1051
1065
|
{/* 星际模式:空闲时在输入框上方待命;任务卡住时在中央呈现编程等待状态。 */}
|
|
1052
1066
|
{spaceModeOn && spaceDocks && active.length === 0 && deliveries.length === 0 ? <ReadyFreighter dock={spaceDocks.ready} /> : null}
|
|
1053
|
-
{spaceModeOn && stuckTask ? <ProgrammingStuck task={stuckTask} waiting={
|
|
1067
|
+
{spaceModeOn && stuckTask ? <ProgrammingStuck task={stuckTask} waiting={taskWaiting(stuckTask)} /> : null}
|
|
1054
1068
|
{!panelOpen ? departures.map((departure) => <MissionDeparture key={departure.id} departure={departure} />) : null}
|
|
1055
1069
|
{/* 环屏巡航:一颗光点沿屏幕边缘转圈(orbit 模式) */}
|
|
1056
1070
|
{ambientOn && mode === "orbit" ? <div className="dkan-orbit" style={speedStyle} aria-hidden="true" /> : null}
|
|
@@ -1059,7 +1073,7 @@ export function AnimationOverlay(props) {
|
|
|
1059
1073
|
{/* 桌面伙伴:背侧视角紧凑双工位人物,阶段与任务同步;整卡可拖到任意位置(robot 模式) */}
|
|
1060
1074
|
{ambientOn && mode === "robot" ? (
|
|
1061
1075
|
<div className="dkan-botcard" role="button" tabIndex={0} style={Object.assign({}, botCardStyle, waitingTask ? { "--dkan-phase": "#b45309" } : null)}
|
|
1062
|
-
title={active.length + " 个任务 · " + (waitingTask ?
|
|
1076
|
+
title={active.length + " 个任务 · " + (waitingTask ? waitWord : phaseLabel(phase)) + " · 点击查看任务动画页 · 可拖动到任意位置"}
|
|
1063
1077
|
onPointerDown={onBotPointerDown}
|
|
1064
1078
|
onPointerMove={onBotPointerMove}
|
|
1065
1079
|
onPointerUp={onBotPointerUp}
|
|
@@ -1068,7 +1082,7 @@ export function AnimationOverlay(props) {
|
|
|
1068
1082
|
onKeyDown={(e) => { if (e.target === e.currentTarget && (e.key === "Enter" || e.key === " ")) { e.preventDefault(); onBotClick(); } }}>
|
|
1069
1083
|
<RobotScene phase={phase} tasks={active} />
|
|
1070
1084
|
<span className="dkan-bot-cap">
|
|
1071
|
-
<span className="n">{active.length}</span> 个任务{elapsed ? " · " + elapsed : ""} · {waitingTask ?
|
|
1085
|
+
<span className="n">{active.length}</span> 个任务{elapsed ? " · " + elapsed : ""} · {waitingTask ? waitWord : phaseLabel(phase)}
|
|
1072
1086
|
</span>
|
|
1073
1087
|
<button type="button" className="dkan-bot-resize" aria-label="调整桌面伙伴大小"
|
|
1074
1088
|
title="拖动调整桌面伙伴大小"
|
|
@@ -1089,14 +1103,14 @@ export function AnimationOverlay(props) {
|
|
|
1089
1103
|
{ambientOn && mode !== "robot" ? (
|
|
1090
1104
|
<button type="button" className={"dkan-badge" + (mode === "breathe" ? " dkan-badge-breathe" : "")}
|
|
1091
1105
|
style={Object.assign({}, speedStyle, { "--dkan-phase": phaseColor(waitingTask ? "code" : phase) })}
|
|
1092
|
-
title={active.length + " 个任务进行中 · " + (waitingTask ?
|
|
1106
|
+
title={active.length + " 个任务进行中 · " + (waitingTask ? waitWord : phaseLabel(phase)) + " · 点击查看任务动画页"}
|
|
1093
1107
|
onClick={() => openPanel("animation")}>
|
|
1094
1108
|
<span className="dkan-dotwrap">
|
|
1095
1109
|
<span className="dkan-dot" />
|
|
1096
1110
|
{mode === "breathe" ? <span className="dkan-halo" /> : null}
|
|
1097
1111
|
{mode === "ring" ? <span className="dkan-ring" /> : null}
|
|
1098
1112
|
</span>
|
|
1099
|
-
<span className="dkan-badge-txt"><span className="n">{active.length}</span> 个任务{elapsed ? " · " + elapsed : ""} · {waitingTask ?
|
|
1113
|
+
<span className="dkan-badge-txt"><span className="n">{active.length}</span> 个任务{elapsed ? " · " + elapsed : ""} · {waitingTask ? waitWord : phaseLabel(phase)}</span>
|
|
1100
1114
|
</button>
|
|
1101
1115
|
) : null}
|
|
1102
1116
|
{/* 完成瞬间的一次性流光(成功绿 / 异常红),动画结束自动清场 */}
|
|
@@ -1261,7 +1275,7 @@ function AnimationView(props) {
|
|
|
1261
1275
|
// 运行状态(进行中任务 / 最近完成 / 等待确认)已独立成左侧菜单项,这里只留一句指路
|
|
1262
1276
|
rows.push(
|
|
1263
1277
|
<div key="hint" className="dkan-note">
|
|
1264
|
-
任务运行状态(进行中任务、阶段、Token
|
|
1278
|
+
任务运行状态(进行中任务、阶段、Token、等人介入(等批准 / 等回答)与最近完成)已独立成左侧「运行状态」菜单;本页只负责动效。
|
|
1265
1279
|
</div>
|
|
1266
1280
|
);
|
|
1267
1281
|
return <div className="dkan-root">{rows}</div>;
|
|
@@ -125,10 +125,9 @@ function removeRemotePatches(list) {
|
|
|
125
125
|
// 回环/安全上下文下 randomUUID 原生存在,脚本自我短路零副作用。
|
|
126
126
|
const LAN_COMPAT_JS = "(function(){var g=typeof globalThis!=='undefined'?globalThis:(typeof window!=='undefined'?window:(typeof self!=='undefined'?self:undefined));if(!g)return;if(!g.crypto){try{g.crypto={}}catch(e){return}}var c=g.crypto;var nativeRng=typeof c.getRandomValues==='function'?c.getRandomValues.bind(c):null;function rng(bytes){if(nativeRng){nativeRng(bytes);return bytes}for(var i=0;i<bytes.length;i++)bytes[i]=Math.floor(Math.random()*256)&255;return bytes}function uuid(){var b=rng(new Uint8Array(16));b[6]=(b[6]&15)|64;b[8]=(b[8]&63)|128;var h=[];for(var i=0;i<16;i++)h.push((b[i]+256).toString(16).slice(1));return h.slice(0,4).join('')+'-'+h.slice(4,6).join('')+'-'+h.slice(6,8).join('')+'-'+h.slice(8,10).join('')+'-'+h.slice(10).join('')}function fill(array){rng(array);return array}function define(obj,name,value){if(!obj||typeof obj[name]==='function')return;try{Object.defineProperty(obj,name,{value:value,configurable:true})}catch(e){try{obj[name]=value}catch(e2){}}}define(c,'randomUUID',uuid);if(!nativeRng)define(c,'getRandomValues',fill);define(g.Crypto&&g.Crypto.prototype||null,'randomUUID',uuid);if(!nativeRng)define(g.Crypto&&g.Crypto.prototype||null,'getRandomValues',fill);if(typeof c.randomUUID!=='function'){var fresh={getRandomValues:fill,randomUUID:uuid};if(c.subtle)fresh.subtle=c.subtle;var keyOrigin=Object.create(null);for(var k in c){try{keyOrigin[k]=c[k]}catch(e3){}}for(var k2 in keyOrigin){if(typeof fresh[k2]==='undefined')fresh[k2]=keyOrigin[k2]}try{Object.defineProperty(g,'crypto',{value:fresh,configurable:true})}catch(e4){try{g.crypto=fresh}catch(e5){}}}})()"
|
|
127
127
|
const LAN_COMPAT_MARKER = 'data-dsh-lan-compat'
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
// 访问时缺该 API,DSH 客户端引导会整片失败)。
|
|
128
|
+
// 远程访问只注入 LAN 非安全上下文的 crypto.randomUUID 兜底(远程设备经
|
|
129
|
+
// http://<局域网IP> 访问时缺该 API,DSH 客户端引导会整片失败)。
|
|
130
|
+
// 手机端排版/行为(旧【手机适配】)已整体移除,本模块不再注入任何窄屏 chrome。
|
|
132
131
|
/** 把 LAN 兼容脚本内联注入 index.html 的 <head> 之后(幂等)。 */
|
|
133
132
|
function injectLanCompat(html) {
|
|
134
133
|
let out = html
|
|
@@ -159,8 +159,7 @@ export function MobileRelayView() {
|
|
|
159
159
|
* 【远程访问】不再挂 Overlay:「预览版说明」自动确认已迁到插件级
|
|
160
160
|
* (src/welcome-ack.js 经 index.js 注册 tapIndex,不随任何功能开关启停)——
|
|
161
161
|
* 关掉远程访问、主实例直接绑 0.0.0.0 的直连形态同样每次重弹,归属不能在本模块。
|
|
162
|
-
*
|
|
163
|
-
* (features/mobile/view.js + features/mobile/host.js)。远程访问只负责「远程入口」本身。
|
|
162
|
+
* 手机端排版/行为(旧【手机适配】)已整体移除,远程访问只负责「远程入口」本身。
|
|
164
163
|
*/
|
|
165
164
|
|
|
166
165
|
export function MobileRelayHomeStat() {
|
package/features/notify/host.js
CHANGED
|
@@ -23,6 +23,12 @@ function defaultConfig() {
|
|
|
23
23
|
notifyOnComplete: true,
|
|
24
24
|
notifyOnError: true,
|
|
25
25
|
notifyOnConfirm: true,
|
|
26
|
+
// 卡住提醒卡片(与 voiceOnStuck 同口径 18 秒无活动,但卡片与语音各自独立开关)
|
|
27
|
+
notifyOnStuck: true,
|
|
28
|
+
// 页内卡片总开关(关闭 → 右侧所有类型的通知卡片都不再弹出)
|
|
29
|
+
notifyCards: true,
|
|
30
|
+
// 卡片口径三挡(正文写多少):默认 detailed =历史内容,未设置时卡片不缩水
|
|
31
|
+
cardMode: 'detailed',
|
|
26
32
|
notifyStayMs: 8000,
|
|
27
33
|
systemNotify: false,
|
|
28
34
|
soundNotify: true,
|
|
@@ -31,7 +37,8 @@ function defaultConfig() {
|
|
|
31
37
|
dingtalkWebhook: '',
|
|
32
38
|
feishuEnabled: false,
|
|
33
39
|
feishuWebhook: '',
|
|
34
|
-
// 语音播报(浏览器 speechSynthesis
|
|
40
|
+
// 语音播报(浏览器 speechSynthesis 短句播报):服务端总开关,默认关闭。
|
|
41
|
+
// 客户端另有按浏览器保存的「本机播报」开关;服务端开 + 本机开才出声。
|
|
35
42
|
voiceNotify: false,
|
|
36
43
|
voiceMode: 'brief',
|
|
37
44
|
voiceOnComplete: true,
|
|
@@ -60,7 +67,8 @@ function readConfig(ctx) {
|
|
|
60
67
|
if (n[key] !== undefined) cfg[key] = n[key]
|
|
61
68
|
}
|
|
62
69
|
if (!SOUND_EFFECTS.includes(cfg.soundEffect)) cfg.soundEffect = 'chime'
|
|
63
|
-
if (cfg.voiceMode !== 'brief' && cfg.voiceMode !== 'detailed') cfg.voiceMode = 'brief'
|
|
70
|
+
if (cfg.voiceMode !== 'brief' && cfg.voiceMode !== 'medium' && cfg.voiceMode !== 'detailed') cfg.voiceMode = 'brief'
|
|
71
|
+
if (cfg.cardMode !== 'brief' && cfg.cardMode !== 'medium' && cfg.cardMode !== 'detailed') cfg.cardMode = 'detailed'
|
|
64
72
|
}
|
|
65
73
|
} catch { /* settings 未挂载,用默认 */ }
|
|
66
74
|
return cfg
|
|
@@ -277,6 +285,13 @@ export const feature = {
|
|
|
277
285
|
if (typeof p.notifyOnComplete === 'boolean') cfg.notifyOnComplete = p.notifyOnComplete
|
|
278
286
|
if (typeof p.notifyOnError === 'boolean') cfg.notifyOnError = p.notifyOnError
|
|
279
287
|
if (typeof p.notifyOnConfirm === 'boolean') cfg.notifyOnConfirm = p.notifyOnConfirm
|
|
288
|
+
// 卡住提醒卡 + 页内卡片总开关(总开关只管卡片弹不弹,不管语音/提示音/系统通知)
|
|
289
|
+
if (typeof p.notifyOnStuck === 'boolean') cfg.notifyOnStuck = p.notifyOnStuck
|
|
290
|
+
if (typeof p.notifyCards === 'boolean') cfg.notifyCards = p.notifyCards
|
|
291
|
+
// 卡片口径三挡(正文写多少);非法值忽略(保持已存的值)
|
|
292
|
+
if (typeof p.cardMode === 'string' && (p.cardMode === 'brief' || p.cardMode === 'medium' || p.cardMode === 'detailed')) {
|
|
293
|
+
cfg.cardMode = p.cardMode
|
|
294
|
+
}
|
|
280
295
|
if (typeof p.notifyStayMs === 'number' && Number.isFinite(p.notifyStayMs)) {
|
|
281
296
|
cfg.notifyStayMs = Math.max(0, Math.min(600000, Math.round(p.notifyStayMs)))
|
|
282
297
|
}
|
|
@@ -305,7 +320,7 @@ export const feature = {
|
|
|
305
320
|
}
|
|
306
321
|
// ===== 语音播报字段 =====
|
|
307
322
|
if (typeof p.voiceNotify === 'boolean') cfg.voiceNotify = p.voiceNotify
|
|
308
|
-
if (typeof p.voiceMode === 'string' && (p.voiceMode === 'brief' || p.voiceMode === 'detailed')) {
|
|
323
|
+
if (typeof p.voiceMode === 'string' && (p.voiceMode === 'brief' || p.voiceMode === 'medium' || p.voiceMode === 'detailed')) {
|
|
309
324
|
cfg.voiceMode = p.voiceMode
|
|
310
325
|
}
|
|
311
326
|
for (const key of ['voiceOnComplete', 'voiceOnError', 'voiceOnConfirm', 'voiceOnStuck']) {
|