dsh-dock 0.11.5 → 0.11.6

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}}",
@@ -25,7 +25,7 @@
25
25
  // 定价优先级:用户自定义单价 > 官网抓取刊例价 > 内置默认表 > 兜底单价。
26
26
  // 分时段价支持多段(start>end 跨零点、start=end 全天、小数小时表示半点);旧单段 peak 自动归一。
27
27
  // 费用在查询/导出时按当前单价即时重算,因此改价无需重扫历史。
28
- import { DOCK_NS, sendJson, readBody } from '../../src/host-core.js'
28
+ import { sendJson, readBody, readDockRoot, mutateDockSection } from '../../src/host-core.js'
29
29
 
30
30
  export const feature = {
31
31
  id: 'tokenlog',
@@ -119,8 +119,7 @@ export const feature = {
119
119
  // 取值做防御性归一(schema 已给默认, 但 settings 未挂载/异常时仍需兜底)。
120
120
  function loadPricingConfig() {
121
121
  try {
122
- const settings = ctx.get('settings')
123
- const root = settings && typeof settings.get === 'function' ? settings.get(DOCK_NS) : null
122
+ const root = readDockRoot(ctx)
124
123
  const t = root && typeof root === 'object' && root.tokenlog && typeof root.tokenlog === 'object' ? root.tokenlog : null
125
124
  if (!t) return
126
125
  if (typeof t.usdCnyRate === 'number' && t.usdCnyRate > 0) usdCnyRate = t.usdCnyRate
@@ -247,7 +246,7 @@ export const feature = {
247
246
  // 的字段) → 落盘。保存后由调用方 loadPricingConfig() 刷新内存, 使改价立即影响费用估算。
248
247
  async function savePricingConfig(input) {
249
248
  const settings = ctx.get('settings')
250
- if (!settings || typeof settings.get !== 'function' || typeof settings.mutate !== 'function') {
249
+ if (!settings || typeof settings.mutate !== 'function') {
251
250
  throw new Error('settings 服务不可用,单价无法持久化')
252
251
  }
253
252
  const bad = (msg) => { const err = new Error(msg); err.statusCode = 400; return err }
@@ -256,7 +255,7 @@ export const feature = {
256
255
  if (!Number.isFinite(n) || n < 0) throw bad(`「${label}」需为不小于 0 的数字`)
257
256
  return n
258
257
  }
259
- const root = settings.get(DOCK_NS)
258
+ const root = readDockRoot(ctx)
260
259
  const current = root && typeof root === 'object' && root.tokenlog && typeof root.tokenlog === 'object' ? root.tokenlog : {}
261
260
  const next = Object.assign({}, current)
262
261
  if (input.usdCnyRate !== undefined) {
@@ -314,7 +313,7 @@ export const feature = {
314
313
  }
315
314
  }
316
315
  try {
317
- await settings.mutate(DOCK_NS, [{ op: 'set', path: ['tokenlog'], value: next }])
316
+ await mutateDockSection(ctx, ['tokenlog'], next)
318
317
  } catch (e) {
319
318
  const err = new Error('保存单价被拒绝:' + ((e && e.message) || String(e)))
320
319
  err.statusCode = 400
@@ -481,17 +480,19 @@ export const feature = {
481
480
  }
482
481
  }
483
482
 
484
- // 读取 settings.yaml 的 llm-pi-ai 命名空间, 返回「现有提供商」路由名 -> [模型 id]。
483
+ // 读官方 llm-pi-ai 配置的「现有提供商」路由名 -> [模型 id]。
485
484
  // 用途: 前端提供商/模型下拉 = 现有配置 ∪ 历史记录, 去重合并 ——
486
485
  // - 没有任何记录时仍可选现有提供商;
487
486
  // - 提供商/模型已从配置删除时, 因有历史记录仍可选中(兼容两者)。
487
+ // 0.1.7-alpha.1 起 settings.get 已移除:改走 describe()(SettingsForms 读路径)。
488
488
  function readConfiguredProviders() {
489
489
  const out = {}
490
490
  try {
491
491
  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 : {}
492
+ if (!settings || typeof settings.describe !== 'function') return out
493
+ const views = settings.describe({ redactSecrets: true }) || []
494
+ const hit = views.find((d) => d && d.ns === 'llm-pi-ai' && d.value && typeof d.value === 'object')
495
+ const providers = hit && hit.value.providers ? hit.value.providers : {}
495
496
  for (const pid of Object.keys(providers || {})) {
496
497
  const p = providers[pid]
497
498
  if (!p || typeof p !== 'object') { out[pid] = []; continue }
@@ -396,7 +396,7 @@ const css = `
396
396
  .dtok-code.pend{color:var(--dsw-alias-label-tertiary,#94a3b8);}
397
397
  .dtok-detail-link{color:var(--dk-accent);cursor:pointer;font-size:11px;}
398
398
  .dtok-detail-link:hover{text-decoration:underline;}
399
- .dtok-detail{position:fixed;inset:0;z-index:10000;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;}
399
+ .dtok-detail{position:fixed;inset:0;z-index:201;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;}
400
400
  .dtok-detail-card{background:var(--dsw-alias-bg-layer-2,#1c212b);border:1px solid var(--dsw-alias-border-l2,#3a4150);border-radius:12px;padding:20px 24px;max-width:640px;width:92%;max-height:80vh;overflow:auto;color:var(--dsw-alias-label-primary,#e8eaf0);}
401
401
  .dtok-detail-card h3{margin:0 0 12px;font-size:15px;}
402
402
  .dtok-detail-row{display:flex;gap:8px;padding:4px 0;font-size:12px;border-bottom:1px solid var(--dk-tdim);}
@@ -460,7 +460,7 @@ const css = `
460
460
  .dtok-btn.primary:not([disabled]):active{transform:none;}
461
461
  }
462
462
  /* ---- 单价设置(子弹窗) ---- */
463
- .dtok-pm-backdrop{position:fixed;inset:0;z-index:2100;background:rgba(15,17,21,.5);backdrop-filter:blur(3px);display:flex;align-items:center;justify-content:center;}
463
+ .dtok-pm-backdrop{position:fixed;inset:0;z-index:201;background:rgba(15,17,21,.5);backdrop-filter:blur(3px);display:flex;align-items:center;justify-content:center;}
464
464
  .dtok-pm{box-sizing:border-box;width:min(980px,calc(100vw - 32px));height:min(760px,calc(100vh - 32px));display:flex;flex-direction:column;border-radius:14px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2,#1c212b);color:var(--dsw-alias-label-primary);box-shadow:0 20px 64px rgb(0 0 0 / .4);overflow:hidden;}
465
465
  .dtok-pm.max{border-radius:10px;}
466
466
  .dtok-pm-head{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid var(--dsw-alias-border-l1);cursor:move;user-select:none;flex:none;background:var(--dsw-alias-bg-layer-1);}
@@ -641,6 +641,11 @@ function PriceModal({ onClose, onSaved }) {
641
641
  return () => window.removeEventListener("keydown", onKey);
642
642
  }, [onClose]);
643
643
 
644
+ // 遮罩关闭必须「按下也在遮罩上」。
645
+ // 从弹窗内按下、拖选/拖出后在遮罩松开时,click 会派发到共同祖先(遮罩),
646
+ // 若无此标记会把框选误判成点外部关闭(2026-09-23 用户反馈)。
647
+ const backdropDownRef = useRef(false);
648
+
644
649
  const style = maxed
645
650
  ? { position: "fixed", left: 8, top: 8, right: 8, bottom: 8, width: "auto", height: "auto" }
646
651
  : geom
@@ -651,7 +656,15 @@ function PriceModal({ onClose, onSaved }) {
651
656
  // 无 document(测试沙箱)/无 createPortal 时退回就地渲染,行为不变。
652
657
  const portalTarget = typeof document !== "undefined" && document.body ? document.body : null;
653
658
  const overlay = (
654
- <div className="dtok-pm-backdrop" onClick={onClose}>
659
+ <div
660
+ className="dtok-pm-backdrop"
661
+ onPointerDown={(e) => { backdropDownRef.current = e.target === e.currentTarget; }}
662
+ onClick={(e) => {
663
+ if (!backdropDownRef.current) return;
664
+ if (e.target !== e.currentTarget) return;
665
+ onClose();
666
+ }}
667
+ >
655
668
  <div ref={dlgRef} className={"dtok-pm" + (maxed ? " max" : "")} style={style} onClick={(e) => e.stopPropagation()}>
656
669
  <div className="dtok-pm-head" onPointerDown={(e) => startDrag(e, "move")} onDoubleClick={() => setMaxed((v) => !v)}>
657
670
  <b>单价设置</b>
@@ -7,7 +7,7 @@
7
7
  // 配置存自有 settings 命名空间 dsh-dock(visionProxy: enabled/provider/model),
8
8
  // 其编辑 UI 在模型设置页内(见 features/modelconfig/view.js)。
9
9
  import { BlockAssembler } from '@deepseek-ai/dsh-llm'
10
- import { DOCK_NS } from '../../src/host-core.js'
10
+ import { readDockRoot } from '../../src/host-core.js'
11
11
  import { readModelDirectory } from '../modelconfig/host.js'
12
12
 
13
13
  /** 递归判断内容块里是否有图片(含 tool-result 嵌套,与官方 contentHasImage 同构)。 */
@@ -223,8 +223,7 @@ function installVisionProxy(ctx, directory) {
223
223
  const deps = {
224
224
  config: () => {
225
225
  try {
226
- const settings = ctx.get('settings')
227
- const v = settings && typeof settings.get === 'function' ? settings.get(DOCK_NS) : undefined
226
+ const v = readDockRoot(ctx)
228
227
  return v && v.visionProxy ? v.visionProxy : { enabled: false, provider: '', model: '' }
229
228
  } catch {
230
229
  return { enabled: false, provider: '', model: '' }
package/index.js CHANGED
@@ -16,7 +16,7 @@
16
16
  // - notify 任务通知:完成/异常/需确认通知 + 提示音/系统通知/钉钉飞书推送(从任务动画拆出)
17
17
  // - runstate 运行状态:进行中任务与最近完成一览(从任务动画拆出;只读,无配置段)
18
18
  // - mobile-relay 手机接力(未发布):扫码反向代理接力 + 局域网电脑直连(0.0.0.0)
19
- import { DOCK_NS, DockConfig, sendJson, readBody, migrateNotifyConfig, migrateModelsFeatureId } from './src/host-core.js'
19
+ import { DockConfig, sendJson, readBody, migrateNotifyConfig, migrateModelsFeatureId, migrateImportedFeatures, bindDockConfig, readDockRoot, mutateDockSection } from './src/host-core.js'
20
20
  import { feature as fModels } from './features/modelconfig/host.js'
21
21
  import { feature as fVisionProxy } from './features/visionproxy/host.js'
22
22
  import { feature as fBalance } from './features/balance/host.js'
@@ -28,7 +28,10 @@ import { feature as fMobileRelay } from './features/mobile-relay/host.js'
28
28
 
29
29
  export const name = 'dsh-dock'
30
30
 
31
- // 硬依赖 webServer(路由注册)+ llm(图片理解代理包装)+ settings(自有命名空间读写)。
31
+ /** Config schema:dsh ≥ 0.1.7-alpha.1 SettingsForms 要求插件导出同名 Config,否则 mutate 报 No configurable plugin entry。 */
32
+ export const Config = DockConfig
33
+
34
+ // 硬依赖 webServer(路由注册)+ llm(图片理解代理包装)+ settings(SettingsForms 写回)。
32
35
  export const inject = ['webServer', 'llm', 'settings']
33
36
 
34
37
  /** 测试钩子:清空识别缓存(冒烟在用例间隔离;实现在 visionproxy 模块)。 */
@@ -50,7 +53,7 @@ if (typeof process !== 'undefined' && typeof process.on === 'function' && !proce
50
53
  })
51
54
  }
52
55
 
53
- export function apply(ctx) {
56
+ export function apply(ctx, config) {
54
57
  // ---- 功能注册表(Host 侧):每个条目来自对应功能模块的 feature 描述符 ----
55
58
  // defaultEnabled:与 Client 半部保持一致。已接入的功能默认打开,
56
59
  // 规划中的功能缺省 false,等实现后移除 roadmap 并打开。
@@ -65,17 +68,22 @@ export function apply(ctx) {
65
68
  fMobileRelay,
66
69
  ]
67
70
 
71
+ // 绑定 Config 镜像(替代已移除的 settings.get);volatile 字段热更时重绑。
72
+ bindDockConfig(config)
73
+ ctx.on('loader/volatile-update', () => {
74
+ try { bindDockConfig(config) } catch { /* Config 形态异常时保持旧镜像 */ }
75
+ })
76
+
68
77
  const state = new Map()
69
78
  for (const f of FEATURES) state.set(f.id, { enabled: false, dispose: null, error: null })
70
79
 
71
- /** 读宿主侧功能开关表(settings 持久化;settings 未挂载时回退 defaultEnabled)。
72
- * 兼容别名:宿主侧【模型设置】功能 id 曾叫 models(与客户端 modelconfig 对不上,
73
- * 开关永远推不到宿主);迁移落盘前(异步)在这里读时兜底换算,保证启动开关表
74
- * 与 /dsh-dock/features 响应第一时间就是新口径。 */
80
+ // 读宿主侧功能开关表(本地 Config 镜像优先;settings 未挂载时回退 defaultEnabled)。
81
+ // 兼容别名:宿主侧【模型设置】功能 id 曾叫 models(与客户端 modelconfig 对不上,
82
+ // 开关永远推不到宿主);迁移落盘前(异步)在这里读时兜底换算,保证启动开关表
83
+ // 与 /dsh-dock/features 响应第一时间就是新口径。
75
84
  function persistedFeatureMap() {
76
85
  try {
77
- const settings = ctx.get('settings')
78
- const v = settings && typeof settings.get === 'function' ? settings.get(DOCK_NS) : null
86
+ const v = readDockRoot(ctx)
79
87
  if (v && typeof v === 'object' && v.features && typeof v.features === 'object') {
80
88
  const map = v.features
81
89
  if (typeof map.models === 'boolean' && typeof map.modelconfig !== 'boolean') {
@@ -115,25 +123,47 @@ export function apply(ctx) {
115
123
  }
116
124
  }
117
125
 
118
- // 自有 settings 命名空间(dsh-dock):功能开关与各功能配置的持久化;
119
- // 读取走 settings.get(内存 resolved 值,写入经 settings.mutate,均热生效)。
120
- // 宿主侧初始开关也在这里应用:settings 命名空间注册完成后才能读到持久化开关表
121
- // (apply() 同步执行时注册尚未生效,读到的永远是空值)。
126
+ // 自有 Config 段(profile 条目 id dsh-dock):功能开关与各功能配置的持久化。
127
+ // 读走本地镜像(apply 的 config / loader/volatile-update),写经 settings.mutate。
128
+ // 关键顺序:先跑完一次性迁移(可能改写 features),再应用初始开关——
129
+ // 否则首次启动时 bind 的还是空默认值,migrate 尚未落盘就 setEnabled,
130
+ // 面板显示 persisted=true 却全部「未启用」、路由 404/405(2026-09-23 事故)。
122
131
  let initialTogglesApplied = false
123
132
  ctx.inject(['settings'], (sctx) => {
124
- sctx.settings.register(DOCK_NS, DockConfig, {})
125
- // 一次性迁移:通知配置从 animation 段搬到 notify 段(【任务通知】独立成模块)。
126
- // 必须在任何面板保存动作之前跑——animation 模块保存时整段写回,旧字段会被覆盖丢失。
127
- migrateNotifyConfig(sctx)
128
- // 一次性迁移:宿主侧【模型设置】功能 id models → modelconfig(开关表键名对齐客户端)。
129
- migrateModelsFeatureId(sctx)
130
- if (initialTogglesApplied) return
131
- initialTogglesApplied = true
132
- const persisted = persistedFeatureMap()
133
- for (const f of FEATURES) {
134
- const saved = persisted ? persisted[f.id] : undefined
135
- setEnabled(f.id, typeof saved === 'boolean' ? saved : !!f.defaultEnabled)
136
- }
133
+ // 0.1.7-alpha.1 起 settings.register 已移除——不再调用;导出 Config 即注册。
134
+ Promise.resolve()
135
+ // 通知配置从 animation 段搬到 notify 段(【任务通知】独立成模块)。
136
+ // 必须在任何面板保存动作之前跑——animation 模块保存时整段写回,旧字段会被覆盖丢失。
137
+ .then(() => migrateNotifyConfig(sctx))
138
+ // 宿主侧【模型设置】功能 id models → modelconfig(开关表键名对齐客户端)。
139
+ .then(() => migrateModelsFeatureId(sctx))
140
+ // settings.yaml.imported 里停住的功能开关表(Config 导出前 import 失败的那段)。
141
+ .then(() => migrateImportedFeatures(sctx))
142
+ .then(() => {
143
+ // 迁移可能 mutate 过 Config:刷新镜像后再读开关表。
144
+ try { bindDockConfig(config) } catch { /* 保持旧镜像 */ }
145
+ if (initialTogglesApplied) return
146
+ initialTogglesApplied = true
147
+ const persisted = persistedFeatureMap()
148
+ for (const f of FEATURES) {
149
+ const saved = persisted ? persisted[f.id] : undefined
150
+ setEnabled(f.id, typeof saved === 'boolean' ? saved : !!f.defaultEnabled)
151
+ }
152
+ console.log('[dsh-dock] initial toggles applied:', FEATURES
153
+ .filter((f) => state.get(f.id).enabled)
154
+ .map((f) => f.id).join(', ') || '(none)')
155
+ })
156
+ .catch((e) => {
157
+ console.error('[dsh-dock] settings init/migration failed:', (e && e.message) || e)
158
+ // 迁移失败也要尝试按当前镜像启用,避免整坞空白。
159
+ if (initialTogglesApplied) return
160
+ initialTogglesApplied = true
161
+ const persisted = persistedFeatureMap()
162
+ for (const f of FEATURES) {
163
+ const saved = persisted ? persisted[f.id] : undefined
164
+ setEnabled(f.id, typeof saved === 'boolean' ? saved : !!f.defaultEnabled)
165
+ }
166
+ })
137
167
  })
138
168
 
139
169
  // 功能开关管理路由(无条件注册):GET 查询各功能宿主侧状态;POST 同步开关——
@@ -152,10 +182,11 @@ export function apply(ctx) {
152
182
  const enabled = !!(body && body.enabled)
153
183
  if (!state.has(id)) return sendJson(res, 404, { ok: false, error: { message: `未知功能:${id}` } })
154
184
  setEnabled(id, enabled)
155
- const settings = ctx.get('settings')
156
- if (settings && typeof settings.mutate === 'function') {
185
+ try {
157
186
  const features = Object.assign({}, persistedFeatureMap() || {}, { [id]: enabled })
158
- await settings.mutate(DOCK_NS, [{ op: 'set', path: ['features'], value: features }])
187
+ await mutateDockSection(ctx, ['features'], features)
188
+ } catch (e) {
189
+ console.error('[dsh-dock] feature toggle persist failed:', (e && e.message) || e)
159
190
  }
160
191
  }
161
192
  const persisted = persistedFeatureMap() || {}