dsh-dock 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -11
- package/client.js +2124 -243
- package/features/animation/host.js +642 -0
- package/features/animation/view.jsx +1631 -0
- package/features/balance/view.js +66 -41
- package/features/modelconfig/view.js +62 -18
- package/features/theme/view.js +35 -10
- package/features/tokenlog/view.jsx +3 -1
- package/features/visionproxy/host.js +24 -10
- package/index.js +5 -9
- package/package.json +2 -2
- package/screenshots/dock-animation.png +0 -0
- package/screenshots/dock-robot.png +0 -0
- package/src/client.jsx +130 -62
- package/src/host-core.js +31 -1
- package/src/shared.js +24 -3
package/src/client.jsx
CHANGED
|
@@ -23,19 +23,16 @@ import { feature as fModelconfig } from "../features/modelconfig/view.js";
|
|
|
23
23
|
import { feature as fHeartbeat } from "../features/heartbeat/view.js";
|
|
24
24
|
import { feature as fTheme } from "../features/theme/view.js";
|
|
25
25
|
import { feature as fBalance } from "../features/balance/view.js";
|
|
26
|
+
import { feature as fAnimation } from "../features/animation/view.jsx";
|
|
26
27
|
|
|
27
28
|
const name = "dsh-dock";
|
|
28
|
-
const DOCK_VERSION = "0.
|
|
29
|
+
const DOCK_VERSION = "0.5.0";
|
|
29
30
|
|
|
30
31
|
// ---- 内置功能注册表:新功能 = features/<id>/ 加模块 + 这里 import 一行 ----
|
|
31
|
-
const BUILTIN_FEATURES = [fTokenlog, fModelconfig, fHeartbeat, fTheme, fBalance];
|
|
32
|
+
const BUILTIN_FEATURES = [fTokenlog, fModelconfig, fHeartbeat, fTheme, fBalance, fAnimation];
|
|
32
33
|
// 规划占位(路线图):接入后移除并建 features/<id>/ 模块
|
|
33
|
-
const PLANNED_FEATURES = [
|
|
34
|
-
|
|
35
|
-
];
|
|
36
|
-
const PLANNED_NOTES = {
|
|
37
|
-
animation: "待接入(路线图 0.5.0):任务进度动画与完成通知。",
|
|
38
|
-
};
|
|
34
|
+
const PLANNED_FEATURES = [];
|
|
35
|
+
const PLANNED_NOTES = {};
|
|
39
36
|
|
|
40
37
|
// ---- 外部功能注册表(dockBridge 回装通道) ----
|
|
41
38
|
const externalDefs = [];
|
|
@@ -45,7 +42,7 @@ function notifyExternal() {
|
|
|
45
42
|
}
|
|
46
43
|
/**
|
|
47
44
|
* 外部功能桥:独立功能包经 require("dsh-dock").dockBridge 注册视图。
|
|
48
|
-
* def: { id, name, order?, accent?, description?, css?, View, HomeStat?, package? }
|
|
45
|
+
* def: { id, name, order?, accent?, description?, css?, View, HomeStat?, Overlay?, package? }
|
|
49
46
|
* 返回注销函数。重复 id 视为更新(HMR/重注册场景)。
|
|
50
47
|
*/
|
|
51
48
|
const dockBridge = {
|
|
@@ -64,6 +61,7 @@ const dockBridge = {
|
|
|
64
61
|
css: typeof def.css === "string" ? def.css : "",
|
|
65
62
|
View: def.View,
|
|
66
63
|
HomeStat: typeof def.HomeStat === "function" ? def.HomeStat : null,
|
|
64
|
+
Overlay: typeof def.Overlay === "function" ? def.Overlay : null,
|
|
67
65
|
external: true,
|
|
68
66
|
package: typeof def.package === "string" ? def.package : "",
|
|
69
67
|
};
|
|
@@ -109,9 +107,13 @@ const SHELL_CSS = [
|
|
|
109
107
|
".dock-badge{flex:none;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:3px 10px;font-size:12px;}",
|
|
110
108
|
".dock-body{border-top:1px solid var(--dsw-alias-border-l1);padding-top:8px;color:var(--dsw-alias-label-secondary);display:flex;flex-direction:column;gap:4px;}",
|
|
111
109
|
// 侧栏入口按钮(docke2- 前缀)+ 功能坞弹出面板(dockm- 前缀,仿 dsh 设置的居中模态:遮罩 + 对话框,左导航 + 右内容)
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
// 侧栏入口按钮(docke2- 前缀):与 dsh 设置按钮(.VOzbGW_trigger)几何逐条对齐——
|
|
111
|
+
// 宽栏同为整宽左对齐行按钮(高 42、margin 4/-2、padding 0 10px 0 8px、圆角 12),窄栏同为 36 圆钮居中,
|
|
112
|
+
// 使两按钮上下成列、左右边缘完全对齐(此前右对齐/叠放都会错位)。
|
|
113
|
+
".docke2-btn{box-sizing:border-box;cursor:pointer;display:flex;align-items:center;gap:8px;border:none;background:transparent;color:var(--dsw-alias-label-primary);border-radius:12px;width:calc(100% + 4px);height:42px;margin:4px -2px;padding:0 10px 0 8px;font-family:inherit;font-size:14px;line-height:22px;transition:background .15s var(--ds-ease-in-out),color .15s var(--ds-ease-in-out);}",
|
|
114
|
+
".docke2-btn:hover{background:var(--dsw-alias-interactive-bg-hover);}",
|
|
114
115
|
".docke2-btn.docke2-on{color:var(--dsw-alias-accent,#4d9fff);background:var(--dsw-alias-accent-soft,color-mix(in srgb,var(--dsw-alias-accent,#4d9fff) 12%,transparent));}",
|
|
116
|
+
".docke2-btn.docke2-rail{width:36px;height:36px;margin:8px 0 10px;padding:0;border-radius:50%;justify-content:center;gap:0;}",
|
|
115
117
|
".docke2-label{white-space:nowrap;overflow:hidden;}",
|
|
116
118
|
".dockm-backdrop{position:fixed;inset:0;z-index:80;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--dsw-alias-bg-layer-1) 55%,transparent);backdrop-filter:blur(4px);pointer-events:auto;animation:dockm-fade .15s var(--ds-ease-in-out);}",
|
|
117
119
|
"@keyframes dockm-fade{from{opacity:0}to{opacity:1}}",
|
|
@@ -155,16 +157,36 @@ const SHELL_CSS = [
|
|
|
155
157
|
".dockh-card:hover,.dockh-card:focus-visible{border-color:var(--dsw-alias-accent,#4d9fff);outline:none;}",
|
|
156
158
|
".dockh-head{display:flex;align-items:center;gap:8px;}",
|
|
157
159
|
".dockh-name{font-weight:600;flex:none;}",
|
|
158
|
-
".dockh-badge{
|
|
159
|
-
|
|
160
|
-
".dockh-
|
|
160
|
+
".dockh-badge{flex:none;font-size:11px;border-radius:999px;padding:0 8px;color:var(--dsw-alias-label-tertiary);border:1px solid var(--dsw-alias-border-l2);}",
|
|
161
|
+
// 状态标识(非交互:圆点+文字,与可点的开关一眼区分)
|
|
162
|
+
".dockh-status{margin-left:auto;flex:none;display:inline-flex;align-items:center;gap:5px;font-size:11px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;}",
|
|
163
|
+
".dockh-sdot{width:7px;height:7px;border-radius:50%;flex:none;background:var(--dsw-alias-state-success-primary,#34d399);box-shadow:0 0 4px color-mix(in srgb,var(--dsw-alias-state-success-primary,#34d399) 55%,transparent);}",
|
|
164
|
+
".dockh-status.off .dockh-sdot{background:transparent;border:1.5px solid var(--dsw-alias-label-tertiary);box-shadow:none;opacity:.75;}",
|
|
165
|
+
".dockh-status.plan .dockh-sdot{background:transparent;border:1.5px dashed var(--dsw-alias-label-tertiary);box-shadow:none;}",
|
|
166
|
+
// iOS 风滑动开关(首页卡片 / 弹层页脚 / 设置页共用的启停控件)
|
|
167
|
+
".dock-sw{flex:none;position:relative;width:34px;height:19px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);cursor:pointer;padding:0;transition:background .18s var(--ds-ease-in-out),border-color .18s var(--ds-ease-in-out);}",
|
|
168
|
+
".dock-sw::after{content:\"\";position:absolute;top:2px;left:2px;width:13px;height:13px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgb(0 0 0 / .35);transition:transform .18s var(--ds-ease-in-out);}",
|
|
169
|
+
".dock-sw:hover{border-color:var(--dsw-alias-accent,#4d9fff);}",
|
|
170
|
+
".dock-sw.on{background:var(--dsw-alias-state-success-primary,#34d399);border-color:transparent;}",
|
|
171
|
+
".dock-sw.on::after{transform:translateX(15px);}",
|
|
172
|
+
// 弹层页脚:文字标签 + 开关 成组
|
|
173
|
+
".dockm-foot-sw{margin-left:auto;flex:none;display:inline-flex;align-items:center;gap:8px;}",
|
|
174
|
+
".dockm-foot-swlabel{font-size:11px;color:var(--dsw-alias-label-tertiary);}",
|
|
175
|
+
".dockm-foot-swlabel.on{color:var(--dsw-alias-state-success-primary);}",
|
|
161
176
|
".dockh-desc{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5;}",
|
|
162
177
|
".dockh-stat{color:var(--dsw-alias-label-tertiary);font-size:12px;border-top:1px solid var(--dsw-alias-border-l1);padding-top:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
163
178
|
".dockh-foot{display:flex;align-items:center;gap:8px;}",
|
|
164
179
|
".dockh-go{color:var(--dsw-alias-label-tertiary);font-size:11px;}",
|
|
165
180
|
// 会话输入区工具行 chips(dockchip- 前缀):余额/用量随身小控件,挂在模型选择器左侧
|
|
166
|
-
|
|
167
|
-
|
|
181
|
+
// 溢出兼容(两层防护,缺一不可):
|
|
182
|
+
// 1) 宽度上限:宿主 .row 是 flex-wrap:wrap 且换行优先于收缩,chips 一长 tools 整行变宽,
|
|
183
|
+
// 右侧模型选择器/发送键被挤到第二行(左右错位)。宿主 .row 带 container-type:inline-size,
|
|
184
|
+
// 用容器单位 cqw 让上限随输入卡宽度自适应(窄卡少占、宽卡多占);不支持 cqw 的老内核退回固定 280px。
|
|
185
|
+
// 2) 截断:超限部分用省略号截断(完整数值在 title 悬浮提示里),防 chips 凸出输入卡圆角(悬空)。
|
|
186
|
+
".dockchip-row{display:inline-flex;align-items:center;gap:4px;min-width:0;flex:0 1 auto;overflow:hidden;max-width:280px;}",
|
|
187
|
+
"@supports (width:1cqw){.dockchip-row{max-width:min(280px,36cqw);}}",
|
|
188
|
+
".dockchip{display:inline-flex;align-items:center;gap:5px;cursor:pointer;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);border-radius:8px;padding:2px 8px;font-family:inherit;font-size:11px;line-height:18px;white-space:nowrap;min-width:0;overflow:hidden;transition:background .15s var(--ds-ease-in-out),color .15s var(--ds-ease-in-out);}",
|
|
189
|
+
".dockchip > span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}",
|
|
168
190
|
".dockchip:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
|
|
169
191
|
".dockchip .dockchip-dot{width:6px;height:6px;border-radius:50%;flex:none;}",
|
|
170
192
|
".dockchip.err{color:var(--dsw-alias-state-error-primary);}"
|
|
@@ -210,8 +232,9 @@ function useExternalVersion() {
|
|
|
210
232
|
// ---- 弹层窗口几何记忆(页面生命周期内):拖动/缩放后记住位置与尺寸,重开还原 ----
|
|
211
233
|
let lastGeom = { x: null, y: null, w: null, h: null };
|
|
212
234
|
|
|
213
|
-
function DockIcon() {
|
|
214
|
-
|
|
235
|
+
function DockIcon(props) {
|
|
236
|
+
const size = (props && props.size) || 16;
|
|
237
|
+
return react.createElement("svg", { width: size, height: size, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.3, "aria-hidden": true },
|
|
215
238
|
react.createElement("rect", { x: 2.5, y: 2.5, width: 4.4, height: 4.4, rx: 1.2 }),
|
|
216
239
|
react.createElement("rect", { x: 9, y: 2.5, width: 4.4, height: 4.4, rx: 1.2 }),
|
|
217
240
|
react.createElement("rect", { x: 2.5, y: 9, width: 4.4, height: 4.4, rx: 1.2 }),
|
|
@@ -224,18 +247,12 @@ function DockEntry(props) {
|
|
|
224
247
|
react.useEffect(() => subscribePanel(() => setOpen(panelNav.open)), []);
|
|
225
248
|
return react.createElement("button", {
|
|
226
249
|
type: "button",
|
|
227
|
-
className: "docke2-btn" + (open ? " docke2-on" : ""),
|
|
228
|
-
style: wide
|
|
229
|
-
// 与设置按钮精确同框:设置 trigger 宽栏 = 42px 高、margin-top 4 → 下移 46px、高度同为 42,
|
|
230
|
-
// 使本按钮的四条边与设置按钮完全重合(同一水平线);rail = 36px、margin-top 8 → 下移 44px、高 36 亦然。
|
|
231
|
-
// zIndex 保证盖在满宽设置按钮之上(其右侧为空白区,不遮齿轮图标)
|
|
232
|
-
? { marginLeft: "auto", transform: "translateY(46px)", zIndex: 1, height: 42, lineHeight: "42px", padding: "0 12px" }
|
|
233
|
-
: { transform: "translateY(44px)", zIndex: 1, width: 36, height: 36, justifyContent: "center", padding: 0 },
|
|
250
|
+
className: "docke2-btn" + (open ? " docke2-on" : "") + (wide ? "" : " docke2-rail"),
|
|
234
251
|
title: "功能坞",
|
|
235
252
|
"aria-label": "功能坞",
|
|
236
253
|
"aria-expanded": open,
|
|
237
254
|
onClick: () => setPanelOpen(!open)
|
|
238
|
-
}, react.createElement(DockIcon,
|
|
255
|
+
}, react.createElement(DockIcon, { size: wide ? 16 : 18 }), wide ? react.createElement("span", { className: "docke2-label" }, "功能坞") : null);
|
|
239
256
|
}
|
|
240
257
|
|
|
241
258
|
// ---- 功能坞弹出面板:shell.overlay(仿 dsh 设置:居中模态 = 遮罩 + 对话框) ----
|
|
@@ -379,34 +396,46 @@ function DockModal() {
|
|
|
379
396
|
? react.createElement("div", { className: "dockm-note" }, PLANNED_NOTES[mod.id] || "待接入:见 README 路线图")
|
|
380
397
|
: st && st.enabled && viewNode
|
|
381
398
|
? viewNode
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
399
|
+
: st && st.error
|
|
400
|
+
? react.createElement("div", { className: "dockm-note dockm-err" }, "功能出错:" + st.error)
|
|
401
|
+
: react.createElement("div", { className: "dockm-note" }, "该功能当前为停用状态(开关已持久化,重启后保持)"),
|
|
385
402
|
react.createElement("div", { className: "dockm-foot" },
|
|
386
403
|
react.createElement("span", null, isHome
|
|
387
404
|
? "功能坞 v" + DOCK_VERSION + " · 共 " + MODULES.length + " 个功能模块," + enabledCount + " 个已启用"
|
|
388
405
|
: "功能坞 v" + DOCK_VERSION + " · 新功能按路线图追加"),
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
406
|
+
!isHome && mod && !mod.planned && st
|
|
407
|
+
? react.createElement("span", { className: "dockm-foot-sw" },
|
|
408
|
+
react.createElement("span", { className: "dockm-foot-swlabel" + (st.enabled ? " on" : "") }, st.enabled ? "已启用" : "已停用"),
|
|
409
|
+
react.createElement("button", {
|
|
410
|
+
type: "button",
|
|
411
|
+
className: "dock-sw" + (st.enabled ? " on" : ""),
|
|
412
|
+
role: "switch",
|
|
413
|
+
"aria-checked": !!st.enabled,
|
|
414
|
+
"aria-label": (st.enabled ? "停用" : "启用") + mod.name,
|
|
415
|
+
title: st.enabled ? "停用「" + mod.name + "」" : "启用「" + mod.name + "」",
|
|
416
|
+
onClick: () => { toggleFeature(mod.id); force(); }
|
|
417
|
+
}))
|
|
418
|
+
: null,
|
|
419
|
+
!isHome && mod && typeof mod.Chip === "function"
|
|
420
|
+
? react.createElement("span", { className: "dockm-foot-sw" },
|
|
421
|
+
react.createElement("span", { className: "dockm-foot-swlabel" + (chipShown(mod.id) ? " on" : "") }, "会话页小控件"),
|
|
422
|
+
react.createElement("button", {
|
|
423
|
+
type: "button",
|
|
424
|
+
className: "dock-sw" + (chipShown(mod.id) ? " on" : ""),
|
|
425
|
+
role: "switch",
|
|
426
|
+
"aria-checked": chipShown(mod.id),
|
|
427
|
+
"aria-label": (chipShown(mod.id) ? "隐藏" : "显示") + mod.name + "的会话页小控件",
|
|
428
|
+
title: "控制会话输入区(模型选择器左侧)是否显示本功能的随身小控件",
|
|
429
|
+
onClick: () => { setChipShown(mod.id, !chipShown(mod.id)); force(); }
|
|
430
|
+
}))
|
|
431
|
+
: null))),
|
|
404
432
|
win.mode === "normal"
|
|
405
433
|
? react.createElement("div", { className: "dockm-resize", onPointerDown: (e) => beginDrag(e, "size") })
|
|
406
434
|
: null));
|
|
407
435
|
}
|
|
408
436
|
|
|
409
|
-
// ----
|
|
437
|
+
// ---- 首页总揽:每个功能模块一张卡片(状态标识 + 运行概要 + 启停开关),点卡片进对应功能 ----
|
|
438
|
+
// 交互约定:状态是「圆点+文字」纯标识(不可点);启停是 iOS 滑动开关(明显可点);点卡片其余区域跳详情页。
|
|
410
439
|
function HomeView(props) {
|
|
411
440
|
const ctx = props && props.ctx;
|
|
412
441
|
const [, force] = react.useReducer((n) => n + 1, 0);
|
|
@@ -435,15 +464,18 @@ function HomeView(props) {
|
|
|
435
464
|
react.createElement("span", { className: "dockm-dot", style: { background: m.accent } }),
|
|
436
465
|
react.createElement("span", { className: "dockh-name" }, m.name),
|
|
437
466
|
m.external ? react.createElement("span", { className: "dockh-badge", title: m.package || undefined }, "外部") : null,
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
react.createElement("span", { className: "dockh-go" }, "查看详情 →"),
|
|
467
|
+
// 状态标识:圆点 + 文字(纯展示,与开关视觉区分)
|
|
468
|
+
react.createElement("span", { className: "dockh-status" + (m.planned ? " plan" : enabled ? "" : " off") },
|
|
469
|
+
react.createElement("span", { className: "dockh-sdot" }),
|
|
470
|
+
react.createElement("span", null, m.planned ? "规划中" : enabled ? "运行中" : "已停用")),
|
|
471
|
+
// 启停开关(规划中的功能不显示)
|
|
444
472
|
m.planned ? null : react.createElement("button", {
|
|
445
473
|
type: "button",
|
|
446
|
-
className: "
|
|
474
|
+
className: "dock-sw" + (enabled ? " on" : ""),
|
|
475
|
+
role: "switch",
|
|
476
|
+
"aria-checked": enabled,
|
|
477
|
+
"aria-label": (enabled ? "停用" : "启用") + m.name,
|
|
478
|
+
title: enabled ? "停用「" + m.name + "」" : "启用「" + m.name + "」",
|
|
447
479
|
onClick: (e) => {
|
|
448
480
|
e.stopPropagation();
|
|
449
481
|
toggleFeature(m.id);
|
|
@@ -451,7 +483,11 @@ function HomeView(props) {
|
|
|
451
483
|
if (props && typeof props.onToggle === "function") props.onToggle();
|
|
452
484
|
force();
|
|
453
485
|
}
|
|
454
|
-
}
|
|
486
|
+
})),
|
|
487
|
+
react.createElement("div", { className: "dockh-desc" }, m.description),
|
|
488
|
+
react.createElement("div", { className: "dockh-stat" }, statNode),
|
|
489
|
+
react.createElement("div", { className: "dockh-foot" },
|
|
490
|
+
react.createElement("span", { className: "dockh-go" }, "查看详情 →")));
|
|
455
491
|
}));
|
|
456
492
|
}
|
|
457
493
|
|
|
@@ -484,16 +520,27 @@ function DockPanel() {
|
|
|
484
520
|
f.planned
|
|
485
521
|
? react.createElement("span", { className: "dock-badge" }, "规划中")
|
|
486
522
|
: react.createElement("button", {
|
|
487
|
-
|
|
523
|
+
type: "button",
|
|
524
|
+
className: "dock-sw" + (st.enabled ? " on" : ""),
|
|
525
|
+
role: "switch",
|
|
526
|
+
"aria-checked": st.enabled,
|
|
527
|
+
"aria-label": (st.enabled ? "停用" : "启用") + f.name,
|
|
528
|
+
title: st.enabled ? "停用「" + f.name + "」" : "启用「" + f.name + "」",
|
|
488
529
|
onClick: () => toggle(f.id)
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
530
|
+
}),
|
|
531
|
+
!f.planned && typeof f.Chip === "function"
|
|
532
|
+
? react.createElement("span", { className: "dockm-foot-sw" },
|
|
533
|
+
react.createElement("span", { className: "dockm-foot-swlabel" + (chipShown(f.id) ? " on" : "") }, "会话页小控件"),
|
|
534
|
+
react.createElement("button", {
|
|
535
|
+
type: "button",
|
|
536
|
+
className: "dock-sw" + (chipShown(f.id) ? " on" : ""),
|
|
537
|
+
role: "switch",
|
|
538
|
+
"aria-checked": chipShown(f.id),
|
|
539
|
+
"aria-label": (chipShown(f.id) ? "隐藏" : "显示") + f.name + "的会话页小控件",
|
|
540
|
+
title: "控制会话输入区(模型选择器左侧)是否显示本功能的随身小控件",
|
|
541
|
+
onClick: () => { setChipShown(f.id, !chipShown(f.id)); force(); }
|
|
542
|
+
}))
|
|
543
|
+
: null),
|
|
497
544
|
f.planned
|
|
498
545
|
? react.createElement("div", { className: "dock-body" }, PLANNED_NOTES[f.id] || "待接入:见 README 路线图")
|
|
499
546
|
: st.error ? react.createElement("div", { className: "dock-body dockm-err" }, "功能出错:" + st.error) : null,
|
|
@@ -519,6 +566,23 @@ function DockChips(props) {
|
|
|
519
566
|
return react.createElement("div", { className: "dockchip-row" }, items);
|
|
520
567
|
}
|
|
521
568
|
|
|
569
|
+
// ---- 功能全局浮层:模块可在描述符上挂 Overlay 组件(props: { ctx, feature }) ----
|
|
570
|
+
// 与 Chip(会话输入区小控件)不同,Overlay 是常驻整页挂载的全局 UI(如任务动画的动效与通知栈),
|
|
571
|
+
// 只要所属功能启用就渲染(功能停用即卸载,浮层自身负责按数据状态决定显示什么)。
|
|
572
|
+
function FeatureOverlays() {
|
|
573
|
+
const [, force] = react.useReducer((n) => n + 1, 0);
|
|
574
|
+
useExternalVersion();
|
|
575
|
+
react.useEffect(() => subscribeFeatureState(() => force()), []);
|
|
576
|
+
const items = [];
|
|
577
|
+
for (const f of allModules()) {
|
|
578
|
+
if (f.planned || !stateOf(f.id).enabled || typeof f.Overlay !== "function") continue;
|
|
579
|
+
items.push(react.createElement(FeatureBoundary, { key: f.id },
|
|
580
|
+
react.createElement(f.Overlay, { ctx: ctxRef.current, feature: f })));
|
|
581
|
+
}
|
|
582
|
+
if (items.length === 0) return null;
|
|
583
|
+
return react.createElement(react.Fragment, null, items);
|
|
584
|
+
}
|
|
585
|
+
|
|
522
586
|
// ---- 插件应用:注册四处 UI(入口按钮 / 弹层 / 设置页 / 会话区 chips),注入样式 ----
|
|
523
587
|
// ctx 以 ref 供视图组件使用(timer/theme 等服务按需自取;重装/HMR 时更新)
|
|
524
588
|
const ctxRef = { current: null };
|
|
@@ -536,6 +600,10 @@ export function apply(ctx) {
|
|
|
536
600
|
slots.inject("shell.overlay", () => slots.register(
|
|
537
601
|
{ name: "shell.overlay", id: "dsh-dock-panel", order: 21, label: "功能坞面板" },
|
|
538
602
|
() => react.createElement(DockModal, null)));
|
|
603
|
+
// 功能全局浮层(任务动画的动效与通知等):已启用功能的 Overlay 常驻挂载
|
|
604
|
+
slots.inject("shell.overlay", () => slots.register(
|
|
605
|
+
{ name: "shell.overlay", id: "dsh-dock-feature-overlays", order: 22, label: "功能坞全局浮层" },
|
|
606
|
+
() => react.createElement(FeatureOverlays, null)));
|
|
539
607
|
slots.inject("settings.section", () => slots.register(
|
|
540
608
|
{ name: "settings.section", id: "dsh-dock", order: 90, label: "功能坞" },
|
|
541
609
|
() => react.createElement(DockPanel, null)));
|
package/src/host-core.js
CHANGED
|
@@ -5,15 +5,45 @@ import z from '@deepseek-ai/schemastery'
|
|
|
5
5
|
/** dsh-dock 自有 settings 命名空间(插件级配置,如图片理解代理)。 */
|
|
6
6
|
export const DOCK_NS = 'dsh-dock'
|
|
7
7
|
|
|
8
|
-
/** 自有命名空间的 schema
|
|
8
|
+
/** 自有命名空间的 schema(图片理解代理 + 任务动画配置)。 */
|
|
9
9
|
export const DockConfig = z.object({
|
|
10
10
|
visionProxy: z.object({
|
|
11
11
|
enabled: z.boolean().default(false),
|
|
12
12
|
provider: z.string().default(''),
|
|
13
13
|
model: z.string().default(''),
|
|
14
14
|
}).default({}),
|
|
15
|
+
animation: z.object({
|
|
16
|
+
// 动画与通知两个独立开关(可只开其一)
|
|
17
|
+
animationEnabled: z.boolean().default(true),
|
|
18
|
+
effectMode: z.string().default('flow'),
|
|
19
|
+
// 桌面伙伴场景缩放(0.85~2.2;浮层右下角也可直接拖动缩放)
|
|
20
|
+
robotScale: z.number().default(1.35),
|
|
21
|
+
notifyEnabled: z.boolean().default(true),
|
|
22
|
+
notifyOnComplete: z.boolean().default(true),
|
|
23
|
+
notifyOnError: z.boolean().default(true),
|
|
24
|
+
// 通知停留毫秒数(0 = 常驻直到手动关闭)
|
|
25
|
+
notifyStayMs: z.number().default(8000),
|
|
26
|
+
// 浏览器系统通知(页面后台时推送)
|
|
27
|
+
systemNotify: z.boolean().default(false),
|
|
28
|
+
// 任务结束提示音(WebAudio 合成,macOS/Windows 通用,无音频文件依赖)
|
|
29
|
+
soundNotify: z.boolean().default(true),
|
|
30
|
+
// 提示音音效(完成场景音名;异常音由音效包内配套)
|
|
31
|
+
soundEffect: z.string().default('chime'),
|
|
32
|
+
// 钉钉群机器人推送(宿主侧直发,浏览器关着也能推;事件跟随 notifyOnComplete/notifyOnError)
|
|
33
|
+
dingtalkEnabled: z.boolean().default(false),
|
|
34
|
+
dingtalkWebhook: z.string().default(''),
|
|
35
|
+
// 飞书群机器人推送(宿主侧直发,浏览器关着也能推;事件跟随 notifyOnComplete/notifyOnError)
|
|
36
|
+
feishuEnabled: z.boolean().default(false),
|
|
37
|
+
feishuWebhook: z.string().default(''),
|
|
38
|
+
}).default({}),
|
|
15
39
|
})
|
|
16
40
|
|
|
41
|
+
/** 任务动画 effectMode 合法值(客户端动画模式)。 */
|
|
42
|
+
export const ANIMATION_MODES = ['flow', 'breathe', 'ring', 'orbit', 'robot', 'matrix', 'stars', 'aurora']
|
|
43
|
+
|
|
44
|
+
/** 提示音 soundEffect 合法值(客户端音效库键名)。 */
|
|
45
|
+
export const SOUND_EFFECTS = ['chime', 'ding', 'coin', 'bell', 'pulse', 'arp']
|
|
46
|
+
|
|
17
47
|
/** 沿 settingsPath 走一层对象(user/base/value 都可用)。 */
|
|
18
48
|
export function walkPath(node, path) {
|
|
19
49
|
for (const key of path || []) {
|
package/src/shared.js
CHANGED
|
@@ -23,16 +23,35 @@ export class FeatureBoundary extends react.Component {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
// ---- 功能开关状态(外壳面板与会话区 chips
|
|
26
|
+
// ---- 功能开关状态(外壳面板与会话区 chips 共用;每功能开关 localStorage 持久化,重启后保持) ----
|
|
27
27
|
const featureState = new Map();
|
|
28
28
|
const stateListeners = new Set();
|
|
29
29
|
function notifyState() {
|
|
30
30
|
for (const fn of stateListeners) fn();
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
// 开关持久化(dsh-dock/features/v1):id -> boolean;未记录过的功能用模块默认值
|
|
33
|
+
const FEATURE_STORE_KEY = "dsh-dock/features/v1";
|
|
34
|
+
const featurePersist = { map: {} };
|
|
35
|
+
try {
|
|
36
|
+
if (typeof localStorage !== "undefined") {
|
|
37
|
+
const raw = localStorage.getItem(FEATURE_STORE_KEY);
|
|
38
|
+
const obj = raw ? JSON.parse(raw) : null;
|
|
39
|
+
if (obj && typeof obj === "object") featurePersist.map = obj;
|
|
40
|
+
}
|
|
41
|
+
} catch { /* localStorage 不可用时用默认值 */ }
|
|
42
|
+
function persistFeatureEnabled() {
|
|
43
|
+
try {
|
|
44
|
+
if (typeof localStorage !== "undefined") localStorage.setItem(FEATURE_STORE_KEY, JSON.stringify(featurePersist.map));
|
|
45
|
+
} catch { /* 持久化失败静默 */ }
|
|
46
|
+
}
|
|
47
|
+
/** 外壳 apply 时初始化内置功能的默认开关(持久化过的值优先于模块默认)。 */
|
|
33
48
|
export function initFeatureState(defs) {
|
|
34
49
|
for (const f of defs) {
|
|
35
|
-
if (
|
|
50
|
+
if (featureState.has(f.id)) continue;
|
|
51
|
+
let enabled = !f.planned && f.defaultEnabled !== false;
|
|
52
|
+
const saved = featurePersist.map[f.id];
|
|
53
|
+
if (typeof saved === "boolean") enabled = saved;
|
|
54
|
+
featureState.set(f.id, { enabled, error: null });
|
|
36
55
|
}
|
|
37
56
|
}
|
|
38
57
|
/** 取功能开关(外部功能默认启用;未登记的 id 惰性建项)。 */
|
|
@@ -44,6 +63,8 @@ export function stateOf(id) {
|
|
|
44
63
|
export function toggleFeature(id) {
|
|
45
64
|
const st = stateOf(id);
|
|
46
65
|
st.enabled = !st.enabled;
|
|
66
|
+
featurePersist.map[id] = st.enabled;
|
|
67
|
+
persistFeatureEnabled();
|
|
47
68
|
notifyState();
|
|
48
69
|
}
|
|
49
70
|
export function subscribeFeatureState(fn) {
|