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/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.4.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
- { id: "animation", name: "任务动画", order: 130, accent: "#f472b6", planned: true, description: "接入路线图 0.5.0:任务进度动画与通知" },
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
- ".docke2-btn{box-sizing:border-box;cursor:pointer;display:inline-flex;align-items:center;gap:6px;border:none;background:transparent;color:var(--dsw-alias-label-secondary);border-radius:10px;height:32px;font-family:inherit;font-size:13px;line-height:32px;transition:background .15s var(--ds-ease-in-out),color .15s var(--ds-ease-in-out);}",
113
- ".docke2-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
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{margin-left:auto;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);}",
159
- ".dockh-badge.on{color:var(--dsw-alias-state-success-primary);border-color:currentColor;}",
160
- ".dockh-badge.off{border-style:dashed;}",
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
- ".dockchip-row{display:inline-flex;align-items:center;gap:4px;min-width:0;}",
167
- ".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;transition:background .15s var(--ds-ease-in-out),color .15s var(--ds-ease-in-out);}",
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
- return react.createElement("svg", { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.3, "aria-hidden": true },
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, null), wide ? react.createElement("span", { className: "docke2-label" }, "功能坞") : null);
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
- : st && st.error
383
- ? react.createElement("div", { className: "dockm-note dockm-err" }, "功能出错:" + st.error)
384
- : react.createElement("div", { className: "dockm-note" }, "该功能当前为停用状态(记忆态随页面生命周期,0.5.0 起持久化)"),
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
- !isHome && mod && !mod.planned && st
390
- ? react.createElement("button", {
391
- type: "button",
392
- className: "dockm-switch" + (st.enabled ? " on" : ""),
393
- onClick: () => { toggleFeature(mod.id); force(); }
394
- }, st.enabled ? "已启用(点击停用)" : "已停用(点击启用)")
395
- : null,
396
- !isHome && mod && typeof mod.Chip === "function"
397
- ? react.createElement("button", {
398
- type: "button",
399
- className: "dockm-switch" + (chipShown(mod.id) ? " on" : ""),
400
- title: "控制会话输入区(模型选择器左侧)是否显示本功能的随身小控件",
401
- onClick: () => { setChipShown(mod.id, !chipShown(mod.id)); force(); }
402
- }, chipShown(mod.id) ? "会话页显示中" : "会话页已隐藏")
403
- : null))),
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
- react.createElement("span", { className: "dockh-badge" + (m.planned ? "" : enabled ? " on" : " off") },
439
- m.planned ? "规划中" : enabled ? "已启用" : "已停用")),
440
- react.createElement("div", { className: "dockh-desc" }, m.description),
441
- react.createElement("div", { className: "dockh-stat" }, statNode),
442
- react.createElement("div", { className: "dockh-foot" },
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: "dockm-switch" + (enabled ? " on" : ""),
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
- }, enabled ? "停用" : "启用")));
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
- className: "dock-switch" + (st.enabled ? " on" : ""),
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
- }, st.enabled ? "已启用" : "已停用"),
490
- !f.planned && typeof f.Chip === "function"
491
- ? react.createElement("button", {
492
- className: "dock-switch" + (chipShown(f.id) ? " on" : ""),
493
- title: "控制会话输入区(模型选择器左侧)是否显示本功能的随身小控件",
494
- onClick: () => { setChipShown(f.id, !chipShown(f.id)); force(); }
495
- }, chipShown(f.id) ? "会话页显示" : "会话页隐藏")
496
- : null),
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
- /** 外壳 apply 时初始化内置功能的默认开关。 */
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 (!featureState.has(f.id)) featureState.set(f.id, { enabled: !f.planned && f.defaultEnabled !== false, error: null });
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) {