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.
- package/README.md +3 -3
- package/client.js +154 -121
- package/features/animation/host.js +5 -10
- package/features/animation/view.jsx +10 -10
- package/features/games/view.jsx +2 -2
- package/features/mobile-relay/host.js +13 -14
- package/features/modelconfig/host.js +4 -4
- package/features/notify/host.js +5 -10
- package/features/notify/view.jsx +1 -1
- package/features/tokenlog/host.js +103 -23
- package/features/tokenlog/view.jsx +71 -24
- package/features/visionproxy/host.js +2 -3
- package/index.js +60 -29
- package/package.json +9 -7
- package/src/client.jsx +16 -6
- package/src/host-core.js +155 -17
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
//
|
|
11
11
|
// 配置模型(schemastery schema 见 src/host-core.js DockConfig.animation):
|
|
12
12
|
// animationEnabled / effectMode / robotScale
|
|
13
|
-
import {
|
|
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
|
|
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
|
-
//
|
|
25
|
+
// 读本地 Config 镜像里的 animation 段(resolved 值已含 schema 默认),异常时回退默认
|
|
26
26
|
function readConfig(ctx) {
|
|
27
27
|
const cfg = defaultConfig()
|
|
28
28
|
try {
|
|
29
|
-
const
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
1338
|
-
".dkan-space-stuck{position:fixed;right:20px;top:88px;width:244px;height:160px;z-index:
|
|
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:
|
|
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:
|
|
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:
|
|
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;}",
|
package/features/games/view.jsx
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
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
|
|
417
|
-
|
|
418
|
-
|
|
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
|
|
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
|
|
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 || []) {
|
package/features/notify/host.js
CHANGED
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
// systemNotify / soundNotify / soundEffect / dingtalkEnabled / dingtalkWebhook /
|
|
15
15
|
// feishuEnabled / feishuWebhook
|
|
16
16
|
// 模块启停由功能坞的 features.notify 开关负责(独立菜单项),本模块不再有第二个总开关。
|
|
17
|
-
import {
|
|
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
|
|
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
|
-
//
|
|
40
|
+
// 读本地 Config 镜像里的 notify 配置(resolved 值已含 schema 默认),异常时回退默认
|
|
41
41
|
function readConfig(ctx) {
|
|
42
42
|
const cfg = defaultConfig()
|
|
43
43
|
try {
|
|
44
|
-
const
|
|
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
|
|
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
|
package/features/notify/view.jsx
CHANGED
|
@@ -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:
|
|
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:
|
|
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 {
|
|
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
|
-
//
|
|
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: [
|
|
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: [
|
|
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
|
|
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
|
-
//
|
|
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
|
-
// 分时段判断(本地时间,
|
|
236
|
+
// 分时段判断(本地时间, 支持多段/跨零点/days 日期类型; 用小数小时以便 08:30 这类半点边界); 未命中用基准价
|
|
185
237
|
let h = -1
|
|
238
|
+
let d = null
|
|
186
239
|
if (typeof timeMs === 'number' && Number.isFinite(timeMs)) {
|
|
187
|
-
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
-
//
|
|
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.
|
|
493
|
-
const
|
|
494
|
-
const
|
|
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 }
|