dsh-dock 0.11.5 → 0.12.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.
@@ -10,10 +10,10 @@
10
10
  //
11
11
  // 配置模型(schemastery schema 见 src/host-core.js DockConfig.animation):
12
12
  // animationEnabled / effectMode / robotScale
13
- import { DOCK_NS, ANIMATION_MODES, sendJson, readBody } from '../../src/host-core.js'
13
+ import { ANIMATION_MODES, sendJson, readBody, readDockRoot, mutateDockSection } from '../../src/host-core.js'
14
14
  import { acquireTaskTracker } from '../../src/task-track.js'
15
15
 
16
- // 默认配置(schema 默认值一致;settings.get 未挂载时的兜底)
16
+ // 默认配置(schema 默认值一致;settings 未挂载时的兜底)
17
17
  function defaultConfig() {
18
18
  return {
19
19
  animationEnabled: true,
@@ -22,12 +22,11 @@ function defaultConfig() {
22
22
  }
23
23
  }
24
24
 
25
- // 读 settings 里的 animation 配置(resolved 值已含 schema 默认),异常时回退默认
25
+ // 读本地 Config 镜像里的 animation 段(resolved 值已含 schema 默认),异常时回退默认
26
26
  function readConfig(ctx) {
27
27
  const cfg = defaultConfig()
28
28
  try {
29
- const settings = ctx.get('settings')
30
- const v = settings && typeof settings.get === 'function' ? settings.get(DOCK_NS) : null
29
+ const v = readDockRoot(ctx)
31
30
  const a = v && typeof v === 'object' && v.animation && typeof v.animation === 'object' ? v.animation : null
32
31
  if (a) {
33
32
  for (const key of Object.keys(cfg)) {
@@ -86,12 +85,8 @@ export const feature = {
86
85
  cfg.robotScale = Math.max(0.85, Math.min(2.2, Math.round(p.robotScale * 100) / 100))
87
86
  }
88
87
 
89
- const settings = ctx.get('settings')
90
- if (!settings || typeof settings.mutate !== 'function') {
91
- throw new Error('settings 服务不可用,配置无法持久化')
92
- }
93
88
  try {
94
- await settings.mutate(DOCK_NS, [{ op: 'set', path: ['animation'], value: cfg }])
89
+ await mutateDockSection(ctx, ['animation'], cfg)
95
90
  } catch (e) {
96
91
  const err = new Error('保存配置被拒绝:' + ((e && e.message) || String(e)))
97
92
  err.statusCode = 400
@@ -1286,18 +1286,18 @@ function AnimationStat(props) {
1286
1286
  // ---------- 样式(dkan- 前缀;全部走主题变量,暗/亮色自适应;动效时长 = 基准 / --dkan-speed) ----------
1287
1287
  const css = [
1288
1288
  // 顶部流光细线(3px,比首版更醒目)
1289
- ".dkan-line{position:fixed;top:0;left:0;right:0;height:3px;z-index:9990;pointer-events:none;background:color-mix(in srgb,var(--dk-accent) 30%,transparent);}",
1289
+ ".dkan-line{position:fixed;top:0;left:0;right:0;height:3px;z-index:2;pointer-events:none;background:color-mix(in srgb,var(--dk-accent) 30%,transparent);}",
1290
1290
  ".dkan-line::after{content:\"\";position:absolute;top:0;bottom:0;left:-42%;width:42%;background:linear-gradient(90deg,transparent,var(--dk-accent) 60%,#fff);box-shadow:0 0 10px 1px color-mix(in srgb,var(--dk-accent) 70%,transparent);animation:dkan-flow calc(1.9s / var(--dkan-speed,1)) linear infinite;}",
1291
1291
  "@keyframes dkan-flow{to{left:100%}}",
1292
1292
  // 完成瞬间的一次性流光(成功绿 / 异常红),动画结束自动清场
1293
- ".dkan-done{position:fixed;top:0;left:0;right:0;height:3px;z-index:9991;pointer-events:none;background:linear-gradient(90deg,transparent,var(--dsw-alias-state-success-primary,#34d399) 50%,#fff);background-size:50% 100%;background-repeat:no-repeat;animation:dkan-done 1.1s var(--ds-ease-in-out) forwards;}",
1293
+ ".dkan-done{position:fixed;top:0;left:0;right:0;height:3px;z-index:3;pointer-events:none;background:linear-gradient(90deg,transparent,var(--dsw-alias-state-success-primary,#34d399) 50%,#fff);background-size:50% 100%;background-repeat:no-repeat;animation:dkan-done 1.1s var(--ds-ease-in-out) forwards;}",
1294
1294
  ".dkan-done.err{background-image:linear-gradient(90deg,transparent,var(--dsw-alias-state-error-primary,#f87171) 50%,#fff);}",
1295
1295
  "@keyframes dkan-done{0%{background-position:-60% 0;opacity:0}25%{opacity:1}100%{background-position:160% 0;opacity:0}}",
1296
1296
  // 环屏巡航:光点沿屏幕边缘转一整圈
1297
- ".dkan-orbit{position:fixed;top:0;left:0;width:12px;height:12px;z-index:9990;pointer-events:none;border-radius:50%;background:var(--dk-accent);box-shadow:0 0 14px 4px color-mix(in srgb,var(--dk-accent) 60%,transparent),0 0 30px 8px color-mix(in srgb,var(--dk-accent) 25%,transparent);animation:dkan-orbit calc(14s / var(--dkan-speed,1)) linear infinite;}",
1297
+ ".dkan-orbit{position:fixed;top:0;left:0;width:12px;height:12px;z-index:2;pointer-events:none;border-radius:50%;background:var(--dk-accent);box-shadow:0 0 14px 4px color-mix(in srgb,var(--dk-accent) 60%,transparent),0 0 30px 8px color-mix(in srgb,var(--dk-accent) 25%,transparent);animation:dkan-orbit calc(14s / var(--dkan-speed,1)) linear infinite;}",
1298
1298
  "@keyframes dkan-orbit{0%{top:0;left:0}25%{top:0;left:calc(100vw - 12px)}50%{top:calc(100vh - 12px);left:calc(100vw - 12px)}75%{top:calc(100vh - 12px);left:0}100%{top:0;left:0}}",
1299
1299
  // ===== 氛围动效 =====
1300
- ".dkan-amb{position:fixed;inset:0;z-index:9989;pointer-events:none;overflow:hidden;}",
1300
+ ".dkan-amb{position:fixed;inset:0;z-index:1;pointer-events:none;overflow:hidden;}",
1301
1301
  // pointer-events 不继承:全屏装饰层的子粒子(星点/流星/极光/火花/彩带)必须各自透明于点击
1302
1302
  ".dkan-amb *,.dkan-fxwrap *{pointer-events:none;}",
1303
1303
  // 代码雨:字符列缓落(提亮加大,速度随吞吐)
@@ -1327,15 +1327,15 @@ const css = [
1327
1327
  ".dkan-space-runner{position:fixed;left:0;top:0;width:108px;height:56px;opacity:0;animation-duration:calc(10.8s / var(--dkan-speed,1));animation-timing-function:linear;animation-iteration-count:infinite;animation-delay:var(--dkan-run-delay,0s);will-change:transform,opacity;contain:layout paint style;backface-visibility:hidden;}.dkan-space-runner .dkan-freighter{left:0;top:0;transform:translateZ(0) scale(.52);transform-origin:top left;filter:none}.dkan-space-runner .dkan-freighter-engine i{animation:none;opacity:.76;box-shadow:-6px 0 8px #22d3ee}.dkan-space-runner .dkan-freighter-nose{filter:none}.dkan-space-runner.route-0{animation-name:dkan-space-route-a}.dkan-space-runner.route-1{animation-name:dkan-space-route-b}.dkan-space-runner.route-2{animation-name:dkan-space-route-c}",
1328
1328
  "@keyframes dkan-space-route-a{0%{opacity:0;transform:translate3d(var(--dkan-alpha-x),var(--dkan-alpha-y),0) rotate(151deg)}7%,86%{opacity:.9}100%{opacity:0;transform:translate3d(var(--dkan-beta-x),var(--dkan-beta-y),0) rotate(151deg)}}@keyframes dkan-space-route-b{0%{opacity:0;transform:translate3d(var(--dkan-beta-x),var(--dkan-beta-y),0) rotate(-90deg)}7%,86%{opacity:.9}100%{opacity:0;transform:translate3d(var(--dkan-gamma-x),var(--dkan-gamma-y),0) rotate(-90deg)}}@keyframes dkan-space-route-c{0%{opacity:0;transform:translate3d(var(--dkan-gamma-x),var(--dkan-gamma-y),0) rotate(-18deg)}7%,86%{opacity:.9}100%{opacity:0;transform:translate3d(var(--dkan-alpha-x),var(--dkan-alpha-y),0) rotate(-18deg)}}",
1329
1329
  // 完成归航:一艘货运旗舰从航线飞到真实会话输入框上方,货舱逐格表达实际输出量。
1330
- ".dkan-delivery{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-delivery-flight var(--dkan-delivery-duration,8600ms) cubic-bezier(.22,.61,.36,1) forwards;}",
1330
+ ".dkan-delivery{position:fixed;left:0;top:0;width:210px;height:88px;z-index:6;pointer-events:none;will-change:transform,opacity;contain:layout paint style;isolation:isolate;animation:dkan-delivery-flight var(--dkan-delivery-duration,8600ms) cubic-bezier(.22,.61,.36,1) forwards;}",
1331
1331
  "@keyframes dkan-delivery-flight{0%{opacity:0;transform:translate3d(var(--dkan-from-x),var(--dkan-from-y),0) scale(.5) rotate(-12deg)}6%{opacity:1;transform:translate3d(var(--dkan-from-x),var(--dkan-from-y),0) scale(.56) rotate(-10deg)}100%{opacity:1;transform:translate3d(var(--dkan-to-x),var(--dkan-to-y),0) scale(1) rotate(0)}}",
1332
1332
  ".dkan-delivery-trail{position:absolute;left:-132px;top:26px;width:154px;height:4px;border-radius:999px;background:linear-gradient(90deg,transparent,#34d399 48%,#d9f99d 78%,transparent);opacity:.9;animation:dkan-delivery-trail var(--dkan-delivery-duration,8600ms) linear forwards;}@keyframes dkan-delivery-trail{0%,82%{opacity:.9}100%{opacity:0}}.dkan-freighter{position:absolute;left:10px;top:10px;width:176px;height:45px;display:block;filter:drop-shadow(0 0 9px color-mix(in srgb,#7dd3fc 62%,transparent));}.dkan-delivery .dkan-freighter,.dkan-departure .dkan-freighter{filter:none}.dkan-delivery .dkan-freighter-engine i,.dkan-departure .dkan-freighter-engine i{animation:none;opacity:.76;box-shadow:-6px 0 8px #22d3ee}.dkan-delivery .dkan-freighter-nose,.dkan-departure .dkan-freighter-nose{filter:none}.dkan-freighter-hull{position:absolute;z-index:3;left:21px;top:7px;width:126px;height:31px;background:linear-gradient(150deg,#f8fbff 0%,#9cc8ff 30%,#385d91 72%,#1d3150);clip-path:polygon(0 30%,43% 0,83% 17%,100% 50%,83% 83%,43% 100%,0 70%,12% 50%);box-shadow:inset 0 1px 0 rgb(255 255 255 / .7),inset -7px -5px 9px rgb(8 24 49 / .38);}.dkan-freighter-nose{position:absolute;z-index:4;right:10px;top:16px;width:28px;height:14px;background:linear-gradient(90deg,#a9d1ff,#e8f5ff);clip-path:polygon(0 0,100% 50%,0 100%,22% 50%);filter:drop-shadow(5px 0 5px color-mix(in srgb,#93c5fd 62%,transparent));}.dkan-freighter-canopy{position:absolute;z-index:5;left:77px;top:10px;width:39px;height:11px;border-radius:70% 48% 42% 32%;background:linear-gradient(135deg,#ecfeff 0%,#67e8f9 32%,#2563eb 75%);transform:skewX(-18deg);box-shadow:inset 0 2px 2px rgb(255 255 255 / .7),0 0 8px color-mix(in srgb,#67e8f9 42%,transparent);}.dkan-freighter-fin{position:absolute;z-index:2;left:48px;top:3px;width:24px;height:17px;background:linear-gradient(135deg,#31557f,#a7caf7);clip-path:polygon(0 100%,58% 0,100% 100%);}.dkan-freighter-wing{position:absolute;z-index:1;left:48px;width:60px;height:18px;background:linear-gradient(135deg,#24436d,#8ebaf4);}.dkan-freighter-wing.left{top:2px;clip-path:polygon(0 100%,100% 0,68% 100%)}.dkan-freighter-wing.right{top:26px;clip-path:polygon(0 0,68% 0,100% 100%)}",
1333
1333
  ".dkan-freighter-cargo{position:absolute;z-index:6;left:37px;top:31px;width:74px;height:7px;display:flex;align-items:center;gap:2px;padding:1px 3px;border-radius:4px;background:#172842;border:1px solid color-mix(in srgb,#b8d8ff 48%,transparent);box-shadow:inset 0 1px 0 rgb(255 255 255 / .2);}.dkan-freighter-cargo i{display:block;flex:1;min-width:4px;height:5px;border-radius:1px;background:linear-gradient(135deg,#fef3c7,#f59e0b 58%,#b45309);box-shadow:0 0 4px color-mix(in srgb,#fbbf24 58%,transparent);}.dkan-freighter-cargo.empty i{background:linear-gradient(135deg,#dbeafe,#38bdf8 58%,#1d4ed8);box-shadow:0 0 4px color-mix(in srgb,#38bdf8 55%,transparent);}.dkan-freighter-engine{position:absolute;z-index:0;left:5px;top:19px;display:flex;gap:3px;align-items:center;height:11px;}.dkan-freighter-engine i{width:18px;height:5px;border-radius:60% 0 0 60%;background:linear-gradient(90deg,transparent,#67e8f9);box-shadow:-9px 0 12px #22d3ee;animation:dkan-engine .34s ease-in-out infinite alternate;}.dkan-freighter-engine i:nth-child(2){animation-delay:-.14s}",
1334
1334
  "@keyframes dkan-engine{to{transform:scaleX(.55);opacity:.5}}",
1335
1335
  ".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);}",
1336
1336
  "@media (max-width:1100px){.dkan-space-system,.dkan-space-runner{display:none}}",
1337
- ".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))}}",
1338
- ".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}}",
1337
+ ".dkan-ready-freighter{position:fixed;width:210px;height:88px;z-index:6;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:6;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))}}",
1338
+ ".dkan-space-stuck{position:fixed;right:20px;top:88px;width:244px;height:160px;z-index:5;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}}",
1339
1339
  // 窄屏(手机/分屏)贴右会溢出:回落到水平居中、屏幕上方,避开正文重心
1340
1340
  "@media (max-width:760px){.dkan-space-stuck{right:50%;transform:translateX(50%);top:64px;}}",
1341
1341
  // 极光:顶部柔光带呼吸流动(加浓)
@@ -1375,7 +1375,7 @@ const css = [
1375
1375
  ".dkan-lantern{contain:strict}.dkan-lantern i{position:absolute;bottom:-30px;border-radius:45% 45% 34% 34%;background:linear-gradient(150deg,#fff7c2 0%,#fbbf24 46%,#f97316 100%);box-shadow:0 0 10px 3px color-mix(in srgb,#fbbf24 36%,transparent);opacity:0;animation-name:dkan-lantern-rise;animation-timing-function:cubic-bezier(.34,.02,.6,1);animation-iteration-count:infinite;will-change:transform,opacity}.dkan-lantern i::after{content:'';position:absolute;left:42%;top:100%;width:16%;height:13px;background:linear-gradient(#f59e0b,transparent)}",
1376
1376
  "@keyframes dkan-lantern-rise{0%{opacity:0;transform:translate3d(0,0,0) rotate(-3deg) scale(.72)}12%{opacity:.72}78%{opacity:.48}100%{opacity:0;transform:translate3d(var(--drift),-104vh,0) rotate(4deg) scale(1.04)}}",
1377
1377
  // ===== 交互反馈层 =====
1378
- ".dkan-fxwrap{position:fixed;inset:0;z-index:9992;pointer-events:none;overflow:hidden;}",
1378
+ ".dkan-fxwrap{position:fixed;inset:0;z-index:4;pointer-events:none;overflow:hidden;}",
1379
1379
  ".dkan-fx{position:absolute;}",
1380
1380
  // 纸飞机:从点击处向右上飞出淡出(加大加亮)
1381
1381
  ".dkan-fx-plane{color:var(--dk-accent);font-size:24px;text-shadow:0 0 8px color-mix(in srgb,var(--dk-accent) 70%,transparent);animation:dkan-plane 1.2s var(--ds-ease-in-out) forwards;}",
@@ -1397,7 +1397,7 @@ const css = [
1397
1397
  ".dkan-fx-confetti i{position:absolute;top:-14px;width:8px;height:13px;border-radius:2px;box-shadow:0 0 4px currentColor;animation:dkan-confetti 2s var(--ds-ease-in-out) forwards;}",
1398
1398
  "@keyframes dkan-confetti{0%{opacity:1;transform:translateY(0) rotate(0)}100%{opacity:0;transform:translateY(75vh) translateX(var(--dx)) rotate(var(--r))}}",
1399
1399
  // 右下角状态徽标(玻璃拟态,可点击);抬到 dsh 输入卡上方避免重叠错位
1400
- ".dkan-badge{position:fixed;right:20px;bottom:calc(var(--dsh-composer-height,152px) + 20px);z-index:9990;display:inline-flex;align-items:center;gap:8px;padding:7px 14px;border-radius:999px;cursor:pointer;border:1px solid var(--dsw-alias-border-l1);background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 78%,transparent);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);box-shadow:0 6px 24px rgb(0 0 0 / .16);color:var(--dsw-alias-label-secondary);font-family:inherit;font-size:12px;line-height:18px;pointer-events:auto;animation:dkan-rise .3s var(--ds-ease-in-out);transition:color .15s var(--ds-ease-in-out);}",
1400
+ ".dkan-badge{position:fixed;right:20px;bottom:calc(var(--dsh-composer-height,152px) + 20px);z-index:2;display:inline-flex;align-items:center;gap:8px;padding:7px 14px;border-radius:999px;cursor:pointer;border:1px solid var(--dsw-alias-border-l1);background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 78%,transparent);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);box-shadow:0 6px 24px rgb(0 0 0 / .16);color:var(--dsw-alias-label-secondary);font-family:inherit;font-size:12px;line-height:18px;pointer-events:auto;animation:dkan-rise .3s var(--ds-ease-in-out);transition:color .15s var(--ds-ease-in-out);}",
1401
1401
  ".dkan-badge:hover{color:var(--dsw-alias-label-primary);}",
1402
1402
  ".dkan-badge .n{color:var(--dsw-alias-label-primary);font-weight:600;}",
1403
1403
  "@keyframes dkan-rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}",
@@ -1487,7 +1487,7 @@ const css = [
1487
1487
  ".dkan-prev-bot .dkan-bot-scene{--dkan-bot-scale:1;transform:scale(.56);transform-origin:center;}",
1488
1488
  // ===== 桌面伙伴:具象人物 + 紧凑双工位(尺寸可调,整卡可拖拽) =====
1489
1489
  // 卡片:玻璃拟态;默认停泊右下(dsh 输入卡上方),拖动后位置持久化、可放屏幕任意处
1490
- ".dkan-botcard{--dkan-bot-scale:1.35;position:fixed;right:20px;bottom:calc(var(--dsh-composer-height,152px) + 20px);z-index:9990;display:flex;flex-direction:column;gap:6px;padding:10px 12px;border-radius:16px;cursor:grab;border:1px solid var(--dsw-alias-border-l1);background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 86%,transparent);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 12px 36px rgb(0 0 0 / .22);color:var(--dsw-alias-label-secondary);font-family:inherit;font-size:12px;pointer-events:auto;touch-action:none;user-select:none;animation:dkan-rise .3s var(--ds-ease-in-out);}",
1490
+ ".dkan-botcard{--dkan-bot-scale:1.35;position:fixed;right:20px;bottom:calc(var(--dsh-composer-height,152px) + 20px);z-index:2;display:flex;flex-direction:column;gap:6px;padding:10px 12px;border-radius:16px;cursor:grab;border:1px solid var(--dsw-alias-border-l1);background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 86%,transparent);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 12px 36px rgb(0 0 0 / .22);color:var(--dsw-alias-label-secondary);font-family:inherit;font-size:12px;pointer-events:auto;touch-action:none;user-select:none;animation:dkan-rise .3s var(--ds-ease-in-out);}",
1491
1491
  ".dkan-botcard:hover,.dkan-botcard:focus-visible{border-color:var(--dk-accent);outline:none;}",
1492
1492
  ".dkan-botcard.dkan-dragging{cursor:grabbing;animation:none;}",
1493
1493
  ".dkan-botcard .n{color:var(--dsw-alias-label-primary);font-weight:600;}",
@@ -588,8 +588,8 @@ const baseCss = `
588
588
  .dgcov-grid{--dgame-accent:var(--dk-games-accent,#7c3aed);display:flex;gap:10px;flex-wrap:wrap}.dgcov{flex:1;min-width:230px;display:flex;flex-direction:column;gap:8px;padding:11px;border:1px solid var(--dsw-alias-border-l1);border-radius:13px;background:var(--dsw-alias-bg-layer-2);cursor:pointer;font-family:inherit;text-align:left;transition:border-color .16s ease,transform .16s ease}.dgcov:hover{border-color:var(--dgame-accent);transform:translateY(-2px)}.dgcov:focus-visible{outline:2px solid var(--dgame-accent);outline-offset:2px}.dgcov.on{border-color:var(--dgame-accent);box-shadow:0 0 0 1px color-mix(in srgb,var(--dgame-accent) 40%,transparent)}.dgcov-name{display:flex;align-items:center;gap:7px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}.dgcov-name i{width:8px;height:8px;border-radius:50%;flex:none}.dgcov-name em{margin-left:auto;font-style:normal;font-size:10px;color:var(--dgame-accent);border:1px solid color-mix(in srgb,var(--dgame-accent) 45%,transparent);border-radius:999px;padding:1px 8px;white-space:nowrap}.dgcov-desc{font-size:11.5px;color:var(--dsw-alias-label-secondary);line-height:1.55}.dgcov-prev{position:relative;display:block;height:96px;border-radius:9px;overflow:hidden;border:1px solid color-mix(in srgb,var(--dgame-accent) 25%,var(--dsw-alias-border-l1))}
589
589
  .dgcov-prev-dash{background:radial-gradient(circle at 70% 20%,color-mix(in srgb,#1d4ed8 30%,transparent),transparent 55%),linear-gradient(180deg,#0d1330,#171233)}.dgcov-prev-dash i{position:absolute;top:-22%;font-style:normal;color:#fb7185;text-shadow:0 0 10px #f43f5e;font-size:15px;animation:dgcov-fall 1.5s linear infinite}.dgcov-prev-dash i::after{content:'◆'}.dgcov-prev-dash i:nth-child(1){left:16%}.dgcov-prev-dash i:nth-child(2){left:50%;animation-delay:-.5s}.dgcov-prev-dash i:nth-child(3){left:82%;animation-delay:-1s}.dgcov-prev-dash b{position:absolute;left:50%;bottom:8px;margin-left:-9px;font-size:17px;color:#a5f3fc;text-shadow:0 0 12px #38bdf8;animation:dgcov-dodge 2.4s ease-in-out infinite}.dgcov-prev-dash b::after{content:'▲'}@keyframes dgcov-fall{to{transform:translateY(140px)}}@keyframes dgcov-dodge{0%,100%{transform:translateX(-46px)}25%,75%{transform:translateX(0)}50%{transform:translateX(46px)}}
590
590
  .dgcov-prev-reactor{display:grid;grid-template-columns:repeat(3,1fr);gap:7px;padding:14px;background:linear-gradient(135deg,#272463,#12102c)}.dgcov-prev-reactor i{display:block;border-radius:9px;background:#191c38;border:1px solid color-mix(in srgb,#ffffff 11%,transparent);animation:dgcov-reactor 3s ease-in-out infinite}.dgcov-prev-reactor i:nth-child(1){animation-delay:0s}.dgcov-prev-reactor i:nth-child(2){animation-delay:-.33s}.dgcov-prev-reactor i:nth-child(3){animation-delay:-.66s}.dgcov-prev-reactor i:nth-child(4){animation-delay:-1s}.dgcov-prev-reactor i:nth-child(5){animation-delay:-1.33s}.dgcov-prev-reactor i:nth-child(6){animation-delay:-1.66s}.dgcov-prev-reactor i:nth-child(7){animation-delay:-2s}.dgcov-prev-reactor i:nth-child(8){animation-delay:-2.33s}.dgcov-prev-reactor i:nth-child(9){animation-delay:-2.66s}@keyframes dgcov-reactor{0%,20%,100%{background:#191c38;box-shadow:none;border-color:color-mix(in srgb,#ffffff 11%,transparent)}40%,64%{background:radial-gradient(circle at 35% 30%,#ecfeff,#67e8f9 40%,#2563eb 85%);box-shadow:0 0 14px color-mix(in srgb,#22d3ee 55%,transparent);border-color:#67e8f9}}
591
- .dgfab{--dgame-accent:var(--dk-games-accent,#7c3aed);position:fixed;z-index:9985;display:flex;flex-direction:column;align-items:center;gap:6px;box-sizing:border-box;width:36px;padding:11px 0;border:1px solid color-mix(in srgb,var(--dgame-accent) 42%,var(--dsw-alias-border-l1));background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 82%,transparent);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);color:var(--dsw-alias-label-primary);font-family:inherit;cursor:grab;user-select:none;touch-action:none;box-shadow:0 6px 24px rgb(0 0 0 / .18);transition:left .22s ease,top .22s ease,border-color .15s ease,opacity .15s ease}.dgfab:hover{border-color:var(--dgame-accent)}.dgfab-side-left{border-radius:0 12px 12px 0;border-left:none}.dgfab-side-right{border-radius:12px 0 0 12px;border-right:none}.dgfab-drag{transition:none;cursor:grabbing;box-shadow:0 10px 34px rgb(0 0 0 / .3);z-index:9986}.dgfab-hide{display:none}.dgfab-ico{display:inline-flex;color:var(--dgame-accent)}.dgfab-label{writing-mode:vertical-rl;letter-spacing:.22em;font-size:11px;line-height:1;color:var(--dsw-alias-label-secondary)}
592
- .dgwin{--dgame-accent:#a78bfa;--dgame-accent-soft:color-mix(in srgb,#a78bfa 16%,transparent);--dsw-alias-label-primary:#e7ecff;--dsw-alias-label-secondary:#aab3e8;--dsw-alias-label-tertiary:#7d86c0;--dsw-alias-border-l1:rgb(255 255 255 / .12);--dsw-alias-border-l2:rgb(255 255 255 / .17);--dsw-alias-bg-layer-1:#0d1230;--dsw-alias-bg-layer-2:#131a3d;--dsw-alias-interactive-bg-hover:rgb(255 255 255 / .08);position:fixed;z-index:9993;display:flex;flex-direction:column;border:1px solid rgb(255 255 255 / .14);border-radius:14px;background:radial-gradient(600px 300px at 80% -10%,color-mix(in srgb,#312e81 30%,transparent),transparent 60%),radial-gradient(500px 340px at -10% 110%,color-mix(in srgb,#0ea5e9 14%,transparent),transparent 55%),linear-gradient(180deg,#0b1024,#12102a 60%,#0a0f22);color:var(--dsw-alias-label-primary);box-shadow:0 24px 72px rgb(0 0 0 / .5);animation:dgwin-in .18s ease-out}.dgwin::before{content:'';position:absolute;inset:0;pointer-events:none;background-image:radial-gradient(1px 1px at 12% 22%,rgb(255 255 255 / .35),transparent),radial-gradient(1px 1px at 34% 68%,rgb(255 255 255 / .26),transparent),radial-gradient(1.5px 1.5px at 58% 14%,rgb(199 210 254 / .35),transparent),radial-gradient(1px 1px at 76% 44%,rgb(255 255 255 / .22),transparent),radial-gradient(1.5px 1.5px at 88% 78%,rgb(165 243 252 / .3),transparent),radial-gradient(1px 1px at 22% 88%,rgb(255 255 255 / .18),transparent),radial-gradient(1px 1px at 45% 38%,rgb(255 255 255 / .15),transparent),radial-gradient(1px 1px at 67% 86%,rgb(255 255 255 / .19),transparent)}@keyframes dgwin-in{from{opacity:0;transform:translateY(8px) scale(.985)}}
591
+ .dgfab{--dgame-accent:var(--dk-games-accent,#7c3aed);position:fixed;z-index:1;display:flex;flex-direction:column;align-items:center;gap:6px;box-sizing:border-box;width:36px;padding:11px 0;border:1px solid color-mix(in srgb,var(--dgame-accent) 42%,var(--dsw-alias-border-l1));background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 82%,transparent);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);color:var(--dsw-alias-label-primary);font-family:inherit;cursor:grab;user-select:none;touch-action:none;box-shadow:0 6px 24px rgb(0 0 0 / .18);transition:left .22s ease,top .22s ease,border-color .15s ease,opacity .15s ease}.dgfab:hover{border-color:var(--dgame-accent)}.dgfab-side-left{border-radius:0 12px 12px 0;border-left:none}.dgfab-side-right{border-radius:12px 0 0 12px;border-right:none}.dgfab-drag{transition:none;cursor:grabbing;box-shadow:0 10px 34px rgb(0 0 0 / .3);z-index:2}.dgfab-hide{display:none}.dgfab-ico{display:inline-flex;color:var(--dgame-accent)}.dgfab-label{writing-mode:vertical-rl;letter-spacing:.22em;font-size:11px;line-height:1;color:var(--dsw-alias-label-secondary)}
592
+ .dgwin{--dgame-accent:#a78bfa;--dgame-accent-soft:color-mix(in srgb,#a78bfa 16%,transparent);--dsw-alias-label-primary:#e7ecff;--dsw-alias-label-secondary:#aab3e8;--dsw-alias-label-tertiary:#7d86c0;--dsw-alias-border-l1:rgb(255 255 255 / .12);--dsw-alias-border-l2:rgb(255 255 255 / .17);--dsw-alias-bg-layer-1:#0d1230;--dsw-alias-bg-layer-2:#131a3d;--dsw-alias-interactive-bg-hover:rgb(255 255 255 / .08);position:fixed;z-index:5;display:flex;flex-direction:column;border:1px solid rgb(255 255 255 / .14);border-radius:14px;background:radial-gradient(600px 300px at 80% -10%,color-mix(in srgb,#312e81 30%,transparent),transparent 60%),radial-gradient(500px 340px at -10% 110%,color-mix(in srgb,#0ea5e9 14%,transparent),transparent 55%),linear-gradient(180deg,#0b1024,#12102a 60%,#0a0f22);color:var(--dsw-alias-label-primary);box-shadow:0 24px 72px rgb(0 0 0 / .5);animation:dgwin-in .18s ease-out}.dgwin::before{content:'';position:absolute;inset:0;pointer-events:none;background-image:radial-gradient(1px 1px at 12% 22%,rgb(255 255 255 / .35),transparent),radial-gradient(1px 1px at 34% 68%,rgb(255 255 255 / .26),transparent),radial-gradient(1.5px 1.5px at 58% 14%,rgb(199 210 254 / .35),transparent),radial-gradient(1px 1px at 76% 44%,rgb(255 255 255 / .22),transparent),radial-gradient(1.5px 1.5px at 88% 78%,rgb(165 243 252 / .3),transparent),radial-gradient(1px 1px at 22% 88%,rgb(255 255 255 / .18),transparent),radial-gradient(1px 1px at 45% 38%,rgb(255 255 255 / .15),transparent),radial-gradient(1px 1px at 67% 86%,rgb(255 255 255 / .19),transparent)}@keyframes dgwin-in{from{opacity:0;transform:translateY(8px) scale(.985)}}
593
593
  .dgwin-min{height:auto!important;border-radius:12px}.dgwin-min .dgwin-body{display:none}.dgwin-min .dgwin-resize{display:none}
594
594
  .dgwin-max{border-radius:12px}.dgwin-max .dgwin-body .dgame-game{align-items:stretch}.dgwin-max .dgwin-body .dgame-dash-stage,.dgwin-max .dgwin-body .dgame-tetris,.dgwin-max .dgwin-body .dgame-tetris-board,.dgwin-max .dgwin-body .dgame-snake-stage,.dgwin-max .dgwin-body .dgame-sokoban-stage,.dgwin-max .dgwin-body .dgame-gomoku-board,.dgwin-max .dgwin-body .dgame-xiangqi-board,.dgwin-max .dgwin-body .dgame-tank-stage,.dgwin-max .dgwin-body .dgame-breakout,.dgwin-max .dgwin-body .dgame-racer,.dgwin-max .dgwin-body .dgame-reactor{width:100%;max-width:none}.dgwin-max .dgwin-body .dgame-controls{width:100%;max-width:none}
595
595
  .dgwin-bar{flex:none;display:flex;align-items:center;gap:9px;padding:10px 12px;border-bottom:1px solid rgb(255 255 255 / .09);background:rgb(7 11 28 / .72);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);color:#cdd6ff;cursor:move;user-select:none;touch-action:none;position:relative;z-index:2}.dgwin-ico{display:inline-flex;color:#a78bfa}.dgwin-title{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1}.dgwin-title b{font-size:13px}.dgwin-title span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;color:#8f98ce}.dgwin-task{display:inline-flex;align-items:center;gap:7px;padding:5px 11px;border-radius:999px;border:1px solid rgb(255 255 255 / .13);background:rgb(255 255 255 / .05);font-size:11px;color:#aab3e8;white-space:nowrap}.dgwin-task i{width:7px;height:7px;border-radius:50%;background:#6b7499;flex:none}.dgwin-task.live{color:#c4f5e4;border-color:color-mix(in srgb,#34d399 33%,transparent);background:color-mix(in srgb,#34d399 8%,transparent)}.dgwin-task.live i{background:#34d399;box-shadow:0 0 0 3px color-mix(in srgb,#34d399 14%,transparent);animation:dgame-pulse 1.8s ease-in-out infinite}.dgwin-ctrls{display:flex;align-items:center;gap:6px;flex:none}.dgwin-btn{cursor:pointer;border:1px solid rgb(255 255 255 / .17);background:rgb(255 255 255 / .07);color:#e6ebff;border-radius:8px;padding:5px 10px;font:inherit;font-size:11px;transition:background .15s ease,border-color .15s ease}.dgwin-btn:hover{background:rgb(255 255 255 / .13);border-color:rgb(255 255 255 / .28)}.dgwin-sq{cursor:pointer;border:1px solid transparent;background:transparent;color:#a9b2e2;border-radius:8px;width:28px;height:26px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;transition:background .15s ease,color .15s ease}.dgwin-sq:hover{background:rgb(255 255 255 / .12);color:#fff}
@@ -20,7 +20,7 @@ import { stat } from 'node:fs/promises'
20
20
  import { homedir } from 'node:os'
21
21
  import { dirname, join } from 'node:path'
22
22
  import yaml from 'js-yaml'
23
- import { DOCK_NS, readBody, sendJson } from '../../src/host-core.js'
23
+ import { DOCK_NS, readBody, sendJson, readDockRoot, mutateDockSection } from '../../src/host-core.js'
24
24
  import { lanAddresses, startProtectedLanGateway } from './gateway.js'
25
25
 
26
26
  function dshHome() { return process.env.DSH_HOME || join(homedir(), '.dsh') }
@@ -324,7 +324,6 @@ export const feature = {
324
324
  const disposers = []
325
325
  let gateway = null
326
326
  let gatewayStarting = null
327
- let settingsCtx = null
328
327
 
329
328
  // 自愈:早期版本把 browse 行平铺写进补丁层——DSH 会把未知 id 的普通补丁行整行
330
329
  // 跳过(且 auto 已被停用),结果 directoryPicker 服务无人提供,/api 整面 404,
@@ -337,8 +336,6 @@ export const feature = {
337
336
  }
338
337
  } catch { /* 补丁层不可读时交给 lanStatus 的错误呈现,不在启动路径上放大 */ }
339
338
 
340
- disposers.push(ctx.inject(['settings'], (sctx) => { settingsCtx = sctx }))
341
-
342
339
  // ── 浏览模式目录选择器的 Windows 跨盘补全 ──
343
340
  // 官方 browse 后端从主目录起步,只能列出当前盘内的子目录,面包屑在主目录之上
344
341
  // 全部折叠——Windows 开着远程访问时,选工作区根本到不了其他磁盘。
@@ -401,23 +398,25 @@ export const feature = {
401
398
  return ba.length > 0 && ba.length === bb.length && timingSafeEqual(ba, bb)
402
399
  }
403
400
  function getAuth() {
404
- if (!settingsCtx) return null
405
- const value = settingsCtx.settings.get(DOCK_NS)
406
- const auth = value && value.remoteAuth
407
- return auth && auth.username && auth.passwordHash && auth.salt ? auth : null
401
+ try {
402
+ const value = readDockRoot(ctx)
403
+ const auth = value && value.remoteAuth
404
+ return auth && auth.username && auth.passwordHash && auth.salt ? auth : null
405
+ } catch {
406
+ return null
407
+ }
408
408
  }
409
409
  async function saveAuth(username, password) {
410
- if (!settingsCtx || typeof settingsCtx.settings.mutate !== 'function') {
410
+ const settings = ctx.get('settings')
411
+ if (!settings || typeof settings.mutate !== 'function') {
411
412
  const error = new Error('settings 服务未就绪,无法保存账号')
412
413
  error.statusCode = 500
413
414
  throw error
414
415
  }
415
416
  const salt = randomBytes(16).toString('hex')
416
- await settingsCtx.settings.mutate(DOCK_NS, [{
417
- op: 'set',
418
- path: ['remoteAuth'],
419
- value: { username, salt, passwordHash: hashPassword(salt, password) },
420
- }])
417
+ await mutateDockSection(ctx, ['remoteAuth'], {
418
+ username, salt, passwordHash: hashPassword(salt, password),
419
+ })
421
420
  // 账号密码变更:作废所有已登录设备,强制重新登录。
422
421
  if (gateway) gateway.revokeAllSessions()
423
422
  }
@@ -9,7 +9,7 @@
9
9
  // - 输入类型官方 schema 仅接受 text/image;「视频」等以 dockTags 标注随配置持久化(不参与请求路由)
10
10
  // - schemastery 保留未知字段(实测),dockTags 对官方校验透明
11
11
  // - POST 也收图片理解代理配置(visionProxy 分支,写 dsh-dock 自有命名空间)
12
- import { DOCK_NS, sendJson, readBody, walkPath } from '../../src/host-core.js'
12
+ import { DOCK_NS, sendJson, readBody, walkPath, readDockRoot, mutateDockSection } from '../../src/host-core.js'
13
13
 
14
14
  /** 官方 schema 接受的输入模态(写回配置的 input 只允许这两个)。 */
15
15
  const HOST_MODALITIES = ['text', 'image']
@@ -294,7 +294,7 @@ async function writeModelConfig(ctx, body) {
294
294
  }
295
295
  const revision = body.revisions && typeof body.revisions[DOCK_NS] === 'number' ? body.revisions[DOCK_NS] : undefined
296
296
  try {
297
- await settings.mutate(DOCK_NS, [{ op: 'set', path: ['visionProxy'], value }], revision)
297
+ await mutateDockSection(ctx, ['visionProxy'], value, revision)
298
298
  } catch (e) {
299
299
  const msg = e && e.message ? e.message : String(e)
300
300
  const err = new Error(`保存图片理解代理配置被拒绝:${msg}`)
@@ -373,11 +373,11 @@ export const feature = {
373
373
  await enrichRuntimeInput(ctx, payload)
374
374
  // 附带图片理解代理配置与自有命名空间 revision(面板保存用)
375
375
  try {
376
- const settings = ctx.get('settings')
377
- const v = settings && typeof settings.get === 'function' ? settings.get(DOCK_NS) : undefined
376
+ const v = readDockRoot(ctx)
378
377
  payload.visionProxy = v && v.visionProxy
379
378
  ? { enabled: !!v.visionProxy.enabled, provider: String(v.visionProxy.provider || ''), model: String(v.visionProxy.model || '') }
380
379
  : { enabled: false, provider: '', model: '' }
380
+ const settings = ctx.get('settings')
381
381
  const desc = settings && typeof settings.describe === 'function'
382
382
  ? settings.describe({ redactSecrets: true }) : []
383
383
  for (const d of desc || []) {
@@ -14,10 +14,10 @@
14
14
  // systemNotify / soundNotify / soundEffect / dingtalkEnabled / dingtalkWebhook /
15
15
  // feishuEnabled / feishuWebhook
16
16
  // 模块启停由功能坞的 features.notify 开关负责(独立菜单项),本模块不再有第二个总开关。
17
- import { DOCK_NS, SOUND_EFFECTS, sendJson, readBody } from '../../src/host-core.js'
17
+ import { SOUND_EFFECTS, sendJson, readBody, readDockRoot, mutateDockSection } from '../../src/host-core.js'
18
18
  import { acquireTaskTracker } from '../../src/task-track.js'
19
19
 
20
- // 默认配置(schema 默认值一致;settings.get 未挂载时的兜底)
20
+ // 默认配置(schema 默认值一致;settings 未挂载时的兜底)
21
21
  function defaultConfig() {
22
22
  return {
23
23
  notifyOnComplete: true,
@@ -37,12 +37,11 @@ function defaultConfig() {
37
37
  }
38
38
  }
39
39
 
40
- // 读 settings 里的 notify 配置(resolved 值已含 schema 默认),异常时回退默认
40
+ // 读本地 Config 镜像里的 notify 配置(resolved 值已含 schema 默认),异常时回退默认
41
41
  function readConfig(ctx) {
42
42
  const cfg = defaultConfig()
43
43
  try {
44
- const settings = ctx.get('settings')
45
- const v = settings && typeof settings.get === 'function' ? settings.get(DOCK_NS) : null
44
+ const v = readDockRoot(ctx)
46
45
  const n = v && typeof v === 'object' && v.notify && typeof v.notify === 'object' ? v.notify : null
47
46
  if (n) {
48
47
  for (const key of Object.keys(cfg)) {
@@ -291,12 +290,8 @@ export const feature = {
291
290
  cfg.feishuWebhook = hook
292
291
  }
293
292
 
294
- const settings = ctx.get('settings')
295
- if (!settings || typeof settings.mutate !== 'function') {
296
- throw new Error('settings 服务不可用,配置无法持久化')
297
- }
298
293
  try {
299
- await settings.mutate(DOCK_NS, [{ op: 'set', path: ['notify'], value: cfg }])
294
+ await mutateDockSection(ctx, ['notify'], cfg)
300
295
  } catch (e) {
301
296
  const err = new Error('保存配置被拒绝:' + ((e && e.message) || String(e)))
302
297
  err.statusCode = 400
@@ -685,7 +685,7 @@ function NotifyStat(props) {
685
685
  // ---------- 样式(dknt- 前缀;全部走主题变量,暗/亮色自适应) ----------
686
686
  const css = [
687
687
  // 通知卡片栈(右上角,不遮 dsh 自身 UI)
688
- ".dknt-toasts{position:fixed;top:16px;right:16px;z-index:9995;display:flex;flex-direction:column;gap:8px;width:min(380px,calc(100vw - 32px));}",
688
+ ".dknt-toasts{position:fixed;top:16px;right:16px;z-index:7;display:flex;flex-direction:column;gap:8px;width:min(380px,calc(100vw - 32px));}",
689
689
  ".dknt-toast{border-radius:12px;padding:12px 14px;pointer-events:auto;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 88%,transparent);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--dsw-alias-border-l1);box-shadow:0 10px 36px rgb(0 0 0 / .22);animation:dknt-toast-in .32s var(--ds-ease-in-out);}",
690
690
  ".dknt-toast.out{animation:dknt-toast-out .24s var(--ds-ease-in-out) forwards;}",
691
691
  "@keyframes dknt-toast-in{from{opacity:0;transform:translateX(14px)}to{opacity:1;transform:none}}",
@@ -21,11 +21,14 @@
21
21
  // fetchOfficial: true
22
22
  // pricing:
23
23
  // - { match: deepseek-v4-flash, input: 1.5, output: 4.5, cacheRead: 0.05, cacheWrite: 0,
24
- // peaks: [{ start: 9, end: 14, input: 3, output: 9, cacheRead: 0.1, cacheWrite: 0 }] }
24
+ // peaks: [{ start: 9, end: 12, days: workday, input: 3, output: 9, cacheRead: 0.1, cacheWrite: 0 },
25
+ // { start: 14, end: 18, days: workday, input: 3, output: 9, cacheRead: 0.1, cacheWrite: 0 }] }
26
+ // holidays: ['2026-10-01~2026-10-07', '2027-01-01'] # 法定节假日(供 days=workday 判定)
25
27
  // 定价优先级:用户自定义单价 > 官网抓取刊例价 > 内置默认表 > 兜底单价。
26
28
  // 分时段价支持多段(start>end 跨零点、start=end 全天、小数小时表示半点);旧单段 peak 自动归一。
29
+ // 每段可带 days=all|workday|nonworkday:workday=非周末且非 holidays;nonworkday=周末或节假日。
27
30
  // 费用在查询/导出时按当前单价即时重算,因此改价无需重扫历史。
28
- import { DOCK_NS, sendJson, readBody } from '../../src/host-core.js'
31
+ import { sendJson, readBody, readDockRoot, mutateDockSection } from '../../src/host-core.js'
29
32
 
30
33
  export const feature = {
31
34
  id: 'tokenlog',
@@ -70,14 +73,22 @@ export const feature = {
70
73
  let usdCnyRate = 7.2
71
74
  let fallbackPricing = { match: '*', ...DEFAULT_FALLBACK }
72
75
  // 内置定价表(人民币元 / 百万 tokens), 按 model 子串匹配; 未匹配走 fallback。
73
- // 支持分时段定价: 条目可带 peaks=[{start,end,input,output,cacheRead,cacheWrite}, ...],
76
+ // 支持分时段定价: 条目可带 peaks=[{start,end,days,input,output,cacheRead,cacheWrite}, ...],
74
77
  // 命中某段(插件运行机器本地时间)用该段价, 其余时段用基准价; 支持多段与跨零点(start>end)。
75
- // 官方来源: DeepSeek-V4 系列 2026-08-17 生效的分时段刊例(高峰每日 9:00-14:00, 空闲为高峰一半)。
78
+ // days 默认 all(每天);workday=仅工作日(非周末且非配置的法定节假日)。
79
+ // 官方来源(按用户口径): DeepSeek 高峰为工作日 9:00-12:00、14:00-18:00 双倍;
80
+ // 周末与节假日全天为谷价(基准价),高峰外时段也回落基准价。
76
81
  const DEFAULT_PRICING = [
77
82
  { match: 'deepseek-v4-flash', input: 1.5, output: 4.5, cacheRead: 0.05, cacheWrite: 0,
78
- peaks: [{ start: 9, end: 14, input: 3.0, output: 9.0, cacheRead: 0.10, cacheWrite: 0 }] },
83
+ peaks: [
84
+ { start: 9, end: 12, days: 'workday', input: 3.0, output: 9.0, cacheRead: 0.10, cacheWrite: 0 },
85
+ { start: 14, end: 18, days: 'workday', input: 3.0, output: 9.0, cacheRead: 0.10, cacheWrite: 0 },
86
+ ] },
79
87
  { match: 'deepseek-v4-pro', input: 4.5, output: 13.5, cacheRead: 0.15, cacheWrite: 0,
80
- peaks: [{ start: 9, end: 14, input: 9.0, output: 27.0, cacheRead: 0.30, cacheWrite: 0 }] },
88
+ peaks: [
89
+ { start: 9, end: 12, days: 'workday', input: 9.0, output: 27.0, cacheRead: 0.30, cacheWrite: 0 },
90
+ { start: 14, end: 18, days: 'workday', input: 9.0, output: 27.0, cacheRead: 0.30, cacheWrite: 0 },
91
+ ] },
81
92
  { match: 'deepseek-v3', input: 3.6, output: 7.2, cacheRead: 0.72, cacheWrite: 7.2 },
82
93
  { match: 'deepseek-chat', input: 1.94, output: 7.92, cacheRead: 0.5, cacheWrite: 7.92 },
83
94
  { match: 'deepseek-reasoner', input: 3.96, output: 15.77, cacheRead: 1.01, cacheWrite: 7.92 },
@@ -87,6 +98,8 @@ export const feature = {
87
98
  let pricingTable = DEFAULT_PRICING
88
99
  // 用户自定义单价(最高优先)与官网抓取开关/地址——均由 loadPricingConfig() 从 settings 刷新。
89
100
  let userPricing = []
101
+ // 法定节假日(YYYY-MM-DD 或 区间 YYYY-MM-DD~YYYY-MM-DD),供 peaks.days 判定工作日。
102
+ let holidays = []
90
103
  let fetchOfficialEnabled = true
91
104
  let pricingUrl = DEFAULT_PRICING_URL
92
105
  let fetchIntervalMs = DEFAULT_FETCH_INTERVAL_HOURS * 3600000
@@ -94,7 +107,7 @@ export const feature = {
94
107
  function normalizePricing(match, e) {
95
108
  const n = (x) => (typeof x === 'number' && Number.isFinite(x) ? x : 0)
96
109
  const row = { match, input: n(e.input), output: n(e.output), cacheRead: n(e.cacheRead), cacheWrite: n(e.cacheWrite) }
97
- // 分时段价支持多段: peaks=[{start,end,input,output,cacheRead,cacheWrite}, ...]。
110
+ // 分时段价支持多段: peaks=[{start,end,days,input,output,cacheRead,cacheWrite}, ...]。
98
111
  // 兼容旧的单段写法 peak={...}(以及旧版本存盘数据), 统一归一到 peaks。
99
112
  const src = Array.isArray(e.peaks) && e.peaks.length ? e.peaks : (e.peak && typeof e.peak === 'object' ? [e.peak] : [])
100
113
  const peaks = []
@@ -102,6 +115,7 @@ export const feature = {
102
115
  if (!s || typeof s !== 'object') continue
103
116
  peaks.push({
104
117
  start: n(s.start), end: n(s.end),
118
+ days: normalizeDays(s.days),
105
119
  input: n(s.input), output: n(s.output),
106
120
  cacheRead: n(s.cacheRead), cacheWrite: n(s.cacheWrite),
107
121
  })
@@ -110,6 +124,11 @@ export const feature = {
110
124
  return row
111
125
  }
112
126
 
127
+ /** 归一日期类型:仅接受 all/workday/nonworkday,其余(含缺失)一律 all(旧数据零迁移)。 */
128
+ function normalizeDays(v) {
129
+ return v === 'workday' || v === 'nonworkday' ? v : 'all'
130
+ }
131
+
113
132
  /** 浅拷贝分时段数组(RPC 下发与落盘都用它,避免调用方改到内存态)。 */
114
133
  function clonePeaks(peaks) {
115
134
  return Array.isArray(peaks) ? peaks.map((s) => Object.assign({}, s)) : []
@@ -119,8 +138,7 @@ export const feature = {
119
138
  // 取值做防御性归一(schema 已给默认, 但 settings 未挂载/异常时仍需兜底)。
120
139
  function loadPricingConfig() {
121
140
  try {
122
- const settings = ctx.get('settings')
123
- const root = settings && typeof settings.get === 'function' ? settings.get(DOCK_NS) : null
141
+ const root = readDockRoot(ctx)
124
142
  const t = root && typeof root === 'object' && root.tokenlog && typeof root.tokenlog === 'object' ? root.tokenlog : null
125
143
  if (!t) return
126
144
  if (typeof t.usdCnyRate === 'number' && t.usdCnyRate > 0) usdCnyRate = t.usdCnyRate
@@ -134,19 +152,53 @@ export const feature = {
134
152
  .filter((e) => e && typeof e === 'object' && typeof e.match === 'string' && e.match)
135
153
  .map((e) => normalizePricing(String(e.match).toLowerCase(), e))
136
154
  : []
155
+ holidays = Array.isArray(t.holidays)
156
+ ? t.holidays.filter((h) => typeof h === 'string' && h)
157
+ : []
137
158
  if (t.fallback && typeof t.fallback === 'object') fallbackPricing = normalizePricing('*', t.fallback)
138
159
  } catch (e) {
139
160
  console.error('[dsh-dock] tokenlog read pricing config failed', e && e.message)
140
161
  }
141
162
  }
142
163
 
143
- // 分时段命中: start<end 为普通区间 [start,end); start>end 表示跨零点(如 23~7 点);
164
+ // 本地日期键 YYYY-MM-DD(供节假日区间比较;字符串序即日期序)。
165
+ function dateKey(d) {
166
+ const p = (n) => (n < 10 ? '0' : '') + n
167
+ return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate())
168
+ }
169
+
170
+ // 是否法定节假日:单日等值,或落入 start~end 区间(含两端)。
171
+ function isHoliday(d, list) {
172
+ const key = dateKey(d)
173
+ for (const h of list || []) {
174
+ const i = h.indexOf('~')
175
+ if (i > 0) {
176
+ if (key >= h.slice(0, i) && key <= h.slice(i + 1)) return true
177
+ } else if (h === key) return true
178
+ }
179
+ return false
180
+ }
181
+
182
+ // 段的日期类型是否匹配:all=每天;workday=非周末且非节假日;nonworkday=周末或节假日。
183
+ function daysMatch(days, d, holidayList) {
184
+ const kind = normalizeDays(days)
185
+ if (kind === 'all') return true
186
+ const w = d.getDay()
187
+ const weekend = w === 0 || w === 6
188
+ const holiday = isHoliday(d, holidayList)
189
+ if (kind === 'workday') return !weekend && !holiday
190
+ return weekend || holiday // nonworkday
191
+ }
192
+
193
+ // 分时段命中: 先过 days(日期类型),再过小时区间。
194
+ // start<end 为普通区间 [start,end); start>end 表示跨零点(如 23~7 点);
144
195
  // start===end 视为全天。多段命中时取第一段(按配置顺序)。
145
- function pickPeakSegment(peaks, hour) {
146
- if (!Array.isArray(peaks) || hour < 0) return null
196
+ function pickPeakSegment(peaks, hour, d, holidayList) {
197
+ if (!Array.isArray(peaks) || hour < 0 || !(d instanceof Date) || Number.isNaN(d.getTime())) return null
147
198
  for (const s of peaks) {
148
199
  const a = Number(s.start), b = Number(s.end)
149
200
  if (!Number.isFinite(a) || !Number.isFinite(b)) continue
201
+ if (!daysMatch(s.days, d, holidayList)) continue
150
202
  const hit = a === b ? true : (a < b ? (hour >= a && hour < b) : (hour >= a || hour < b))
151
203
  if (hit) return s
152
204
  }
@@ -181,13 +233,14 @@ export const feature = {
181
233
  }
182
234
  // 4) 兜底
183
235
  if (!row) { row = fallbackPricing; source = 'fallback' }
184
- // 分时段判断(本地时间, 支持多段与跨零点; 用小数小时以便 08:30 这类半点边界); 未命中用基准价
236
+ // 分时段判断(本地时间, 支持多段/跨零点/days 日期类型; 用小数小时以便 08:30 这类半点边界); 未命中用基准价
185
237
  let h = -1
238
+ let d = null
186
239
  if (typeof timeMs === 'number' && Number.isFinite(timeMs)) {
187
- const d = new Date(timeMs)
240
+ d = new Date(timeMs)
188
241
  h = d.getHours() + d.getMinutes() / 60
189
242
  }
190
- const seg = pickPeakSegment(row.peaks, h)
243
+ const seg = pickPeakSegment(row.peaks, h, d, holidays)
191
244
  const prices = seg
192
245
  ? { input: seg.input, output: seg.output, cacheRead: seg.cacheRead, cacheWrite: seg.cacheWrite }
193
246
  : { input: row.input, output: row.output, cacheRead: row.cacheRead, cacheWrite: row.cacheWrite }
@@ -236,6 +289,7 @@ export const feature = {
236
289
  pricingUrl,
237
290
  pricingFetchIntervalHours: Math.round(fetchIntervalMs / 3600000),
238
291
  pricing: userPricing.map((r) => Object.assign({}, r, { peaks: clonePeaks(r.peaks) })),
292
+ holidays: holidays.slice(),
239
293
  fallback: Object.assign({}, fallbackPricing),
240
294
  builtin: pricingTable.map((r) => ({ match: r.match, input: r.input, output: r.output, cacheRead: r.cacheRead, cacheWrite: r.cacheWrite, peaks: clonePeaks(r.peaks) })),
241
295
  models: [...seen].sort(),
@@ -247,7 +301,7 @@ export const feature = {
247
301
  // 的字段) → 落盘。保存后由调用方 loadPricingConfig() 刷新内存, 使改价立即影响费用估算。
248
302
  async function savePricingConfig(input) {
249
303
  const settings = ctx.get('settings')
250
- if (!settings || typeof settings.get !== 'function' || typeof settings.mutate !== 'function') {
304
+ if (!settings || typeof settings.mutate !== 'function') {
251
305
  throw new Error('settings 服务不可用,单价无法持久化')
252
306
  }
253
307
  const bad = (msg) => { const err = new Error(msg); err.statusCode = 400; return err }
@@ -256,7 +310,7 @@ export const feature = {
256
310
  if (!Number.isFinite(n) || n < 0) throw bad(`「${label}」需为不小于 0 的数字`)
257
311
  return n
258
312
  }
259
- const root = settings.get(DOCK_NS)
313
+ const root = readDockRoot(ctx)
260
314
  const current = root && typeof root === 'object' && root.tokenlog && typeof root.tokenlog === 'object' ? root.tokenlog : {}
261
315
  const next = Object.assign({}, current)
262
316
  if (input.usdCnyRate !== undefined) {
@@ -282,6 +336,7 @@ export const feature = {
282
336
  }
283
337
  // 分时段价(可多段): 每段 start/end 为 0~24 的本地小时(支持 8.5 表示 08:30);
284
338
  // start>end 表示跨零点, start===end 表示全天。
339
+ // days: all|workday|nonworkday —— 周末/节假日谷价时把高峰段标成 workday。
285
340
  const rawPeaks = Array.isArray(raw.peaks) ? raw.peaks : []
286
341
  const peaks = []
287
342
  for (const seg of rawPeaks) {
@@ -290,8 +345,12 @@ export const feature = {
290
345
  if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || start > 24 || end < 0 || end > 24) {
291
346
  throw bad(`「${match}」分时段时刻需为 0~24 的小时数`)
292
347
  }
348
+ const days = seg.days === undefined || seg.days === null || seg.days === '' ? 'all' : String(seg.days)
349
+ if (days !== 'all' && days !== 'workday' && days !== 'nonworkday') {
350
+ throw bad(`「${match}」分时段日期类型需为 all / workday / nonworkday`)
351
+ }
293
352
  peaks.push({
294
- start, end,
353
+ start, end, days,
295
354
  input: nonNeg(seg.input, `${match} 分时段输入单价`),
296
355
  output: nonNeg(seg.output, `${match} 分时段输出单价`),
297
356
  cacheRead: nonNeg(seg.cacheRead, `${match} 分时段缓存命中单价`),
@@ -303,6 +362,25 @@ export const feature = {
303
362
  }
304
363
  next.pricing = rows
305
364
  }
365
+ if (input.holidays !== undefined) {
366
+ if (!Array.isArray(input.holidays)) throw bad('「法定节假日」需为数组')
367
+ const dayRe = /^\d{4}-\d{2}-\d{2}$/
368
+ const rangeRe = /^\d{4}-\d{2}-\d{2}~\d{4}-\d{2}-\d{2}$/
369
+ const list = []
370
+ for (const raw of input.holidays) {
371
+ const h = String(raw == null ? '' : raw).trim()
372
+ if (!h) continue
373
+ if (!dayRe.test(h) && !rangeRe.test(h)) {
374
+ throw bad(`节假日「${h}」格式不符:单日用 2026-10-01,区间用 2026-10-01~2026-10-07`)
375
+ }
376
+ if (rangeRe.test(h)) {
377
+ const [a, b] = h.split('~')
378
+ if (a > b) throw bad(`节假日区间「${h}」起始日期不能晚于结束日期`)
379
+ }
380
+ list.push(h)
381
+ }
382
+ next.holidays = list
383
+ }
306
384
  if (input.fallback !== undefined) {
307
385
  const f = input.fallback
308
386
  if (!f || typeof f !== 'object' || Array.isArray(f)) throw bad('「兜底单价」格式不符')
@@ -314,7 +392,7 @@ export const feature = {
314
392
  }
315
393
  }
316
394
  try {
317
- await settings.mutate(DOCK_NS, [{ op: 'set', path: ['tokenlog'], value: next }])
395
+ await mutateDockSection(ctx, ['tokenlog'], next)
318
396
  } catch (e) {
319
397
  const err = new Error('保存单价被拒绝:' + ((e && e.message) || String(e)))
320
398
  err.statusCode = 400
@@ -481,17 +559,19 @@ export const feature = {
481
559
  }
482
560
  }
483
561
 
484
- // 读取 settings.yaml 的 llm-pi-ai 命名空间, 返回「现有提供商」路由名 -> [模型 id]。
562
+ // 读官方 llm-pi-ai 配置的「现有提供商」路由名 -> [模型 id]。
485
563
  // 用途: 前端提供商/模型下拉 = 现有配置 ∪ 历史记录, 去重合并 ——
486
564
  // - 没有任何记录时仍可选现有提供商;
487
565
  // - 提供商/模型已从配置删除时, 因有历史记录仍可选中(兼容两者)。
566
+ // 0.1.7-alpha.1 起 settings.get 已移除:改走 describe()(SettingsForms 读路径)。
488
567
  function readConfiguredProviders() {
489
568
  const out = {}
490
569
  try {
491
570
  const settings = ctx.get('settings')
492
- if (!settings || typeof settings.get !== 'function') return out
493
- const sec = settings.get('llm-pi-ai')
494
- const providers = sec && typeof sec === 'object' && sec.providers ? sec.providers : {}
571
+ if (!settings || typeof settings.describe !== 'function') return out
572
+ const views = settings.describe({ redactSecrets: true }) || []
573
+ const hit = views.find((d) => d && d.ns === 'llm-pi-ai' && d.value && typeof d.value === 'object')
574
+ const providers = hit && hit.value.providers ? hit.value.providers : {}
495
575
  for (const pid of Object.keys(providers || {})) {
496
576
  const p = providers[pid]
497
577
  if (!p || typeof p !== 'object') { out[pid] = []; continue }