dsh-dock 0.11.6 → 0.13.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 +54 -9
- package/client.js +685 -253
- package/features/mobile/view.js +305 -0
- package/features/mobile-relay/gateway.js +187 -11
- package/features/mobile-relay/host.js +175 -50
- package/features/mobile-relay/view.jsx +217 -86
- package/features/tokenlog/host.js +92 -13
- package/features/tokenlog/view.jsx +55 -21
- package/index.js +5 -1
- package/package.json +3 -3
- package/src/client.jsx +3 -2
- package/src/host-core.js +136 -1
|
@@ -21,9 +21,12 @@
|
|
|
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
31
|
import { sendJson, readBody, readDockRoot, mutateDockSection } from '../../src/host-core.js'
|
|
29
32
|
|
|
@@ -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)) : []
|
|
@@ -133,19 +152,53 @@ export const feature = {
|
|
|
133
152
|
.filter((e) => e && typeof e === 'object' && typeof e.match === 'string' && e.match)
|
|
134
153
|
.map((e) => normalizePricing(String(e.match).toLowerCase(), e))
|
|
135
154
|
: []
|
|
155
|
+
holidays = Array.isArray(t.holidays)
|
|
156
|
+
? t.holidays.filter((h) => typeof h === 'string' && h)
|
|
157
|
+
: []
|
|
136
158
|
if (t.fallback && typeof t.fallback === 'object') fallbackPricing = normalizePricing('*', t.fallback)
|
|
137
159
|
} catch (e) {
|
|
138
160
|
console.error('[dsh-dock] tokenlog read pricing config failed', e && e.message)
|
|
139
161
|
}
|
|
140
162
|
}
|
|
141
163
|
|
|
142
|
-
//
|
|
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 点);
|
|
143
195
|
// start===end 视为全天。多段命中时取第一段(按配置顺序)。
|
|
144
|
-
function pickPeakSegment(peaks, hour) {
|
|
145
|
-
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
|
|
146
198
|
for (const s of peaks) {
|
|
147
199
|
const a = Number(s.start), b = Number(s.end)
|
|
148
200
|
if (!Number.isFinite(a) || !Number.isFinite(b)) continue
|
|
201
|
+
if (!daysMatch(s.days, d, holidayList)) continue
|
|
149
202
|
const hit = a === b ? true : (a < b ? (hour >= a && hour < b) : (hour >= a || hour < b))
|
|
150
203
|
if (hit) return s
|
|
151
204
|
}
|
|
@@ -180,13 +233,14 @@ export const feature = {
|
|
|
180
233
|
}
|
|
181
234
|
// 4) 兜底
|
|
182
235
|
if (!row) { row = fallbackPricing; source = 'fallback' }
|
|
183
|
-
// 分时段判断(本地时间,
|
|
236
|
+
// 分时段判断(本地时间, 支持多段/跨零点/days 日期类型; 用小数小时以便 08:30 这类半点边界); 未命中用基准价
|
|
184
237
|
let h = -1
|
|
238
|
+
let d = null
|
|
185
239
|
if (typeof timeMs === 'number' && Number.isFinite(timeMs)) {
|
|
186
|
-
|
|
240
|
+
d = new Date(timeMs)
|
|
187
241
|
h = d.getHours() + d.getMinutes() / 60
|
|
188
242
|
}
|
|
189
|
-
const seg = pickPeakSegment(row.peaks, h)
|
|
243
|
+
const seg = pickPeakSegment(row.peaks, h, d, holidays)
|
|
190
244
|
const prices = seg
|
|
191
245
|
? { input: seg.input, output: seg.output, cacheRead: seg.cacheRead, cacheWrite: seg.cacheWrite }
|
|
192
246
|
: { input: row.input, output: row.output, cacheRead: row.cacheRead, cacheWrite: row.cacheWrite }
|
|
@@ -235,6 +289,7 @@ export const feature = {
|
|
|
235
289
|
pricingUrl,
|
|
236
290
|
pricingFetchIntervalHours: Math.round(fetchIntervalMs / 3600000),
|
|
237
291
|
pricing: userPricing.map((r) => Object.assign({}, r, { peaks: clonePeaks(r.peaks) })),
|
|
292
|
+
holidays: holidays.slice(),
|
|
238
293
|
fallback: Object.assign({}, fallbackPricing),
|
|
239
294
|
builtin: pricingTable.map((r) => ({ match: r.match, input: r.input, output: r.output, cacheRead: r.cacheRead, cacheWrite: r.cacheWrite, peaks: clonePeaks(r.peaks) })),
|
|
240
295
|
models: [...seen].sort(),
|
|
@@ -281,6 +336,7 @@ export const feature = {
|
|
|
281
336
|
}
|
|
282
337
|
// 分时段价(可多段): 每段 start/end 为 0~24 的本地小时(支持 8.5 表示 08:30);
|
|
283
338
|
// start>end 表示跨零点, start===end 表示全天。
|
|
339
|
+
// days: all|workday|nonworkday —— 周末/节假日谷价时把高峰段标成 workday。
|
|
284
340
|
const rawPeaks = Array.isArray(raw.peaks) ? raw.peaks : []
|
|
285
341
|
const peaks = []
|
|
286
342
|
for (const seg of rawPeaks) {
|
|
@@ -289,8 +345,12 @@ export const feature = {
|
|
|
289
345
|
if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || start > 24 || end < 0 || end > 24) {
|
|
290
346
|
throw bad(`「${match}」分时段时刻需为 0~24 的小时数`)
|
|
291
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
|
+
}
|
|
292
352
|
peaks.push({
|
|
293
|
-
start, end,
|
|
353
|
+
start, end, days,
|
|
294
354
|
input: nonNeg(seg.input, `${match} 分时段输入单价`),
|
|
295
355
|
output: nonNeg(seg.output, `${match} 分时段输出单价`),
|
|
296
356
|
cacheRead: nonNeg(seg.cacheRead, `${match} 分时段缓存命中单价`),
|
|
@@ -302,6 +362,25 @@ export const feature = {
|
|
|
302
362
|
}
|
|
303
363
|
next.pricing = rows
|
|
304
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
|
+
}
|
|
305
384
|
if (input.fallback !== undefined) {
|
|
306
385
|
const f = input.fallback
|
|
307
386
|
if (!f || typeof f !== 'object' || Array.isArray(f)) throw bad('「兜底单价」格式不符')
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// - 秒级时间范围查询 + 会话ID/提供商/模型(联动)/状态/推理强度 筛选,条件本地暂存
|
|
5
5
|
// - 9 张 KPI 卡 + 分组统计表 + 明细表(点击表头排序、会话ID点击即筛选、100 行/页上下双分页)
|
|
6
6
|
// - 状态列显示 HTTP 状态码徽章,行内【查看详情】弹窗展示完整信息;CSV 导出(中文表头,列序与明细表一致 + 合计行)
|
|
7
|
-
// -
|
|
7
|
+
// - 独立的「单价设置」子弹窗:按模型配置单价(多段分时价 + 日期类型/法定节假日)并持久化,费用按自填单价重算
|
|
8
8
|
// - 挂载即扫描历史+按暂存条件查询;挂载期间每 5s 静默自动刷新
|
|
9
9
|
// - 查询等待动画:点「查询」后按钮转圈扫光 + 数据区顶部「记账小队清点」banner(轮换俏皮文案,
|
|
10
10
|
// 等久了换语气),旧数据压暗禁点;结果到位时闪一次「✓ 数据已更新」并让 KPI 卡回弹
|
|
@@ -488,6 +488,9 @@ const css = `
|
|
|
488
488
|
.dtok-pseg{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:4px 0;}
|
|
489
489
|
.dtok-pseg-name{font-size:11px;color:var(--dsw-alias-label-secondary);min-width:112px;}
|
|
490
490
|
.dtok-pseg-tilde{font-size:11px;color:var(--dsw-alias-label-secondary);}
|
|
491
|
+
.dtok-price select{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:4px 6px;font-size:12px;font-family:inherit;}
|
|
492
|
+
.dtok-price textarea{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:6px 8px;font-size:12px;font-family:inherit;min-height:64px;resize:vertical;width:100%;box-sizing:border-box;}
|
|
493
|
+
.dtok-days{font-size:11px;}
|
|
491
494
|
.dtok-src{font-size:11px;color:var(--dsw-alias-label-tertiary,#94a3b8);}
|
|
492
495
|
.dtok-src.custom{color:var(--dsw-alias-state-success-primary,#4ade80);}
|
|
493
496
|
.dtok-src.fallback{color:var(--dk-warn);}
|
|
@@ -571,10 +574,12 @@ function Detail({ rec, onClose, rate }) {
|
|
|
571
574
|
// 独立子弹窗(可最大化 / 拖动 / 缩放),与功能坞面板解耦,方便配置较长时段表。
|
|
572
575
|
function numStr(v) { return v === undefined || v === null ? "" : String(v); }
|
|
573
576
|
|
|
574
|
-
/** 时段草稿:一个 { start,end,input,output,cacheRead,cacheWrite } 的可编辑副本。 */
|
|
577
|
+
/** 时段草稿:一个 { start,end,days,input,output,cacheRead,cacheWrite } 的可编辑副本。 */
|
|
575
578
|
function segToDraft(s) {
|
|
579
|
+
const d = s && s.days;
|
|
576
580
|
return {
|
|
577
581
|
start: numStr(s && s.start), end: numStr(s && s.end),
|
|
582
|
+
days: d === "workday" || d === "nonworkday" ? d : "all",
|
|
578
583
|
input: numStr(s && s.input), output: numStr(s && s.output),
|
|
579
584
|
cacheRead: numStr(s && s.cacheRead), cacheWrite: numStr(s && s.cacheWrite),
|
|
580
585
|
};
|
|
@@ -692,24 +697,27 @@ function PricingEditor({ onClose, onSaved, embedded }) {
|
|
|
692
697
|
const [rate, setRate] = useState("7.2");
|
|
693
698
|
const [fetchOn, setFetchOn] = useState(true);
|
|
694
699
|
const [fb, setFb] = useState({ input: "", output: "", cacheRead: "", cacheWrite: "" });
|
|
700
|
+
const [holidaysText, setHolidaysText] = useState("");
|
|
695
701
|
const [newMatch, setNewMatch] = useState("");
|
|
696
702
|
const [msg, setMsg] = useState(null);
|
|
697
703
|
const [saving, setSaving] = useState(false);
|
|
698
704
|
|
|
705
|
+
const applyCfg = (d) => {
|
|
706
|
+
setCfg(d);
|
|
707
|
+
setRate(numStr(d.usdCnyRate));
|
|
708
|
+
setFetchOn(!!d.fetchOfficial);
|
|
709
|
+
setFb({
|
|
710
|
+
input: numStr(d.fallback && d.fallback.input), output: numStr(d.fallback && d.fallback.output),
|
|
711
|
+
cacheRead: numStr(d.fallback && d.fallback.cacheRead), cacheWrite: numStr(d.fallback && d.fallback.cacheWrite),
|
|
712
|
+
});
|
|
713
|
+
setHolidaysText((d.holidays || []).join("\n"));
|
|
714
|
+
setRows((d.pricing || []).map(rowToDraft));
|
|
715
|
+
};
|
|
716
|
+
|
|
699
717
|
useEffect(() => {
|
|
700
718
|
let cancel = false;
|
|
701
719
|
rpcCall("pricing", {})
|
|
702
|
-
.then((d) => {
|
|
703
|
-
if (cancel) return;
|
|
704
|
-
setCfg(d);
|
|
705
|
-
setRate(numStr(d.usdCnyRate));
|
|
706
|
-
setFetchOn(!!d.fetchOfficial);
|
|
707
|
-
setFb({
|
|
708
|
-
input: numStr(d.fallback && d.fallback.input), output: numStr(d.fallback && d.fallback.output),
|
|
709
|
-
cacheRead: numStr(d.fallback && d.fallback.cacheRead), cacheWrite: numStr(d.fallback && d.fallback.cacheWrite),
|
|
710
|
-
});
|
|
711
|
-
setRows((d.pricing || []).map(rowToDraft));
|
|
712
|
-
})
|
|
720
|
+
.then((d) => { if (!cancel) applyCfg(d); })
|
|
713
721
|
.catch((e) => { if (!cancel) setMsg({ ok: false, text: String((e && e.message) || e) }); });
|
|
714
722
|
return () => { cancel = true; };
|
|
715
723
|
}, []);
|
|
@@ -741,12 +749,14 @@ function PricingEditor({ onClose, onSaved, embedded }) {
|
|
|
741
749
|
};
|
|
742
750
|
const peaks = (r.peaks || []).map((s) => ({
|
|
743
751
|
start: Number(s.start || 0), end: Number(s.end || 0),
|
|
752
|
+
days: s.days === "workday" || s.days === "nonworkday" ? s.days : "all",
|
|
744
753
|
input: Number(s.input || 0), output: Number(s.output || 0),
|
|
745
754
|
cacheRead: Number(s.cacheRead || 0), cacheWrite: Number(s.cacheWrite || 0),
|
|
746
755
|
}));
|
|
747
756
|
if (peaks.length) out.peaks = peaks;
|
|
748
757
|
return out;
|
|
749
758
|
});
|
|
759
|
+
payload.holidays = holidaysText.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
750
760
|
payload.fallback = {
|
|
751
761
|
input: Number(fb.input || 0), output: Number(fb.output || 0),
|
|
752
762
|
cacheRead: Number(fb.cacheRead || 0), cacheWrite: Number(fb.cacheWrite || 0),
|
|
@@ -754,8 +764,7 @@ function PricingEditor({ onClose, onSaved, embedded }) {
|
|
|
754
764
|
setSaving(true); setMsg(null);
|
|
755
765
|
rpcCall("setpricing", payload)
|
|
756
766
|
.then((d) => {
|
|
757
|
-
|
|
758
|
-
setRows((d.pricing || []).map(rowToDraft));
|
|
767
|
+
applyCfg(d);
|
|
759
768
|
setMsg({ ok: true, text: "单价已保存,费用已按新价即时重算" });
|
|
760
769
|
if (onSaved) onSaved();
|
|
761
770
|
})
|
|
@@ -778,9 +787,18 @@ function PricingEditor({ onClose, onSaved, embedded }) {
|
|
|
778
787
|
);
|
|
779
788
|
const segLabel = (s) => {
|
|
780
789
|
const a = numStr(s.start), b = numStr(s.end);
|
|
781
|
-
|
|
782
|
-
|
|
790
|
+
const dayTag = s.days === "workday" ? "工作日" : s.days === "nonworkday" ? "周末/节假日" : "";
|
|
791
|
+
const time = a === "" || b === "" ? "时段"
|
|
792
|
+
: a + "~" + b + (Number(a) === Number(b) ? "(全天)" : Number(a) > Number(b) ? "(跨零点)" : "");
|
|
793
|
+
return dayTag ? dayTag + " " + time : time;
|
|
783
794
|
};
|
|
795
|
+
const daysSelect = (value, onChange) => (
|
|
796
|
+
<select className="dtok-days" value={value || "all"} onChange={(e) => onChange(e.target.value)} title="该时段在哪类日期生效">
|
|
797
|
+
<option value="all">每天</option>
|
|
798
|
+
<option value="workday">仅工作日</option>
|
|
799
|
+
<option value="nonworkday">仅周末/节假日</option>
|
|
800
|
+
</select>
|
|
801
|
+
);
|
|
784
802
|
|
|
785
803
|
return (
|
|
786
804
|
<div className={embedded ? "dtok-price emb" : "dtok-price"} onClick={(e) => e.stopPropagation()}>
|
|
@@ -795,6 +813,20 @@ function PricingEditor({ onClose, onSaved, embedded }) {
|
|
|
795
813
|
</label>
|
|
796
814
|
</div>
|
|
797
815
|
|
|
816
|
+
<div>
|
|
817
|
+
<div className="dtok-section-title" style={{ margin: "0 0 4px" }}>法定节假日(供「仅工作日」分时段判定)</div>
|
|
818
|
+
<div className="dtok-price-field" style={{ maxWidth: 420 }}>
|
|
819
|
+
<label>每行一条:单日 2026-10-01,或区间 2026-10-01~2026-10-07(含两端)</label>
|
|
820
|
+
<textarea
|
|
821
|
+
value={holidaysText}
|
|
822
|
+
placeholder={"2026-10-01~2026-10-07\n2027-01-01"}
|
|
823
|
+
onChange={(e) => setHolidaysText(e.target.value)}
|
|
824
|
+
spellCheck={false}
|
|
825
|
+
/>
|
|
826
|
+
</div>
|
|
827
|
+
<div className="dtok-price-hint">周末(周六/周日)与这些节假日视为非工作日;标了「仅工作日」的高峰段在这些日期回落基准价(谷价)。</div>
|
|
828
|
+
</div>
|
|
829
|
+
|
|
798
830
|
<div>
|
|
799
831
|
<div className="dtok-section-title" style={{ margin: "0 0 4px" }}>按模型配置单价({rows.length} 条)</div>
|
|
800
832
|
{onFallback.length > 0 ? (
|
|
@@ -819,12 +851,14 @@ function PricingEditor({ onClose, onSaved, embedded }) {
|
|
|
819
851
|
缓存写入=未命中缓存、本次新建缓存的那部分输入(Claude 等按溢价单独收建缓存费);DeepSeek 不收此项,填 0 即可。
|
|
820
852
|
</div>
|
|
821
853
|
<div className="dtok-price-hint" style={{ marginTop: 2 }}>
|
|
822
|
-
分时段价(可多段,命中哪段用哪段;时段外回落到基准价。start>end 表示跨零点,如 23~7;start=end
|
|
854
|
+
分时段价(可多段,命中哪段用哪段;时段外回落到基准价。start>end 表示跨零点,如 23~7;start=end 表示全天。
|
|
855
|
+
每段可选日期类型:每天 / 仅工作日(非周末且非下方节假日)/ 仅周末或节假日——如 DeepSeek「工作日 9~12、14~18 双倍,周末与节假日全天谷价」)
|
|
823
856
|
</div>
|
|
824
857
|
{r.peaks.length === 0 ? <div className="dtok-price-hint">(未设置分时段,按基准价计费)</div> : null}
|
|
825
858
|
{r.peaks.map((s, k) => (
|
|
826
859
|
<div className="dtok-pseg" key={k}>
|
|
827
860
|
<span className="dtok-pseg-name">{segLabel(s)}</span>
|
|
861
|
+
{daysSelect(s.days, (v) => patchSeg(i, k, { days: v }))}
|
|
828
862
|
{num(s.start, (v) => patchSeg(i, k, { start: v }), { width: 56 })}
|
|
829
863
|
<span className="dtok-pseg-tilde">~</span>
|
|
830
864
|
{num(s.end, (v) => patchSeg(i, k, { end: v }), { width: 56 })}
|
|
@@ -836,7 +870,7 @@ function PricingEditor({ onClose, onSaved, embedded }) {
|
|
|
836
870
|
<button className="dtok-btn tiny" onClick={() => delSeg(i, k)}>删除时段</button>
|
|
837
871
|
</div>
|
|
838
872
|
))}
|
|
839
|
-
<div className="dtok-price-hint" style={{ opacity: .85 }}
|
|
873
|
+
<div className="dtok-price-hint" style={{ opacity: .85 }}>时段价从左到右:日期类型 / 起~止时 / 输入(命中)/ 输入(未命中)/ 输出 / 缓存写入</div>
|
|
840
874
|
<button className="dtok-btn tiny" onClick={() => addSeg(i)}>+ 添加时段</button>
|
|
841
875
|
</div>
|
|
842
876
|
))}
|
|
@@ -871,7 +905,7 @@ function PricingEditor({ onClose, onSaved, embedded }) {
|
|
|
871
905
|
{cfg.builtin.map((b) => (
|
|
872
906
|
<tr key={b.match}>
|
|
873
907
|
<td>{b.match}</td><td>{b.cacheRead}</td><td>{b.input}</td><td>{b.output}</td><td>{b.cacheWrite}</td>
|
|
874
|
-
<td>{b.peaks && b.peaks.length ? b.peaks.map((s) => s.start + "~" + s.end).join("、") + "时" : "—"}</td>
|
|
908
|
+
<td>{b.peaks && b.peaks.length ? b.peaks.map((s) => (s.days === "workday" ? "工作日" : s.days === "nonworkday" ? "周末/节假" : "") + s.start + "~" + s.end).join("、") + "时" : "—"}</td>
|
|
875
909
|
</tr>
|
|
876
910
|
))}
|
|
877
911
|
</tbody>
|
|
@@ -1374,7 +1408,7 @@ export const feature = {
|
|
|
1374
1408
|
name: "用量记录",
|
|
1375
1409
|
order: 110,
|
|
1376
1410
|
accent: "#fbbf24",
|
|
1377
|
-
description: "记录全部 LLM API 调用:秒级时间筛选、Token
|
|
1411
|
+
description: "记录全部 LLM API 调用:秒级时间筛选、Token/费用统计(可配置各模型单价,持久保存并按自填单价计费;多段分时价可限定工作日/周末节假日,内置峰谷计价+官网价目自动同步作兜底)、分组汇总、明细检索与 CSV 导出",
|
|
1378
1412
|
css,
|
|
1379
1413
|
View: TokenLogView,
|
|
1380
1414
|
HomeStat: TokenLogHomeStat,
|
package/index.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// - notify 任务通知:完成/异常/需确认通知 + 提示音/系统通知/钉钉飞书推送(从任务动画拆出)
|
|
17
17
|
// - runstate 运行状态:进行中任务与最近完成一览(从任务动画拆出;只读,无配置段)
|
|
18
18
|
// - mobile-relay 手机接力(未发布):扫码反向代理接力 + 局域网电脑直连(0.0.0.0)
|
|
19
|
-
import { DockConfig, sendJson, readBody, migrateNotifyConfig, migrateModelsFeatureId, migrateImportedFeatures, bindDockConfig, readDockRoot, mutateDockSection } from './src/host-core.js'
|
|
19
|
+
import { DockConfig, sendJson, readBody, migrateNotifyConfig, migrateModelsFeatureId, migrateImportedFeatures, migrateImportedSections, 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'
|
|
@@ -132,6 +132,10 @@ export function apply(ctx, config) {
|
|
|
132
132
|
ctx.inject(['settings'], (sctx) => {
|
|
133
133
|
// 0.1.7-alpha.1 起 settings.register 已移除——不再调用;导出 Config 即注册。
|
|
134
134
|
Promise.resolve()
|
|
135
|
+
// settings.yaml.imported 整段恢复(账号 remoteAuth / notify / animation /
|
|
136
|
+
// visionProxy——Config 导出前导入失败停在 imported 文件里的那批)。必须最先跑:
|
|
137
|
+
// 后面的迁移与功能启用都要读到恢复后的配置(远程访问账号丢了网关就开不起来)。
|
|
138
|
+
.then(() => migrateImportedSections(sctx))
|
|
135
139
|
// 通知配置从 animation 段搬到 notify 段(【任务通知】独立成模块)。
|
|
136
140
|
// 必须在任何面板保存动作之前跑——animation 模块保存时整段写回,旧字段会被覆盖丢失。
|
|
137
141
|
.then(() => migrateNotifyConfig(sctx))
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-dock",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DeepSeek Harness 功能坞插件(dsh-dock):模块化功能中枢——每个功能一个 features/ 模块(可单独提取成包发布,又可经 dockBridge 装回面板)。含用量记录(LLM
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"description": "DeepSeek Harness 功能坞插件(dsh-dock):模块化功能中枢——每个功能一个 features/ 模块(可单独提取成包发布,又可经 dockBridge 装回面板)。含用量记录(LLM 调用记账/费用统计,支持各模型自定义单价、多段分时价与周末/节假日谷价)、模型设置、图片理解代理、模型余额、任务动画(19 种运行动画 + 桌面伙伴 3D 场景)、任务通知(完成/异常/需确认通知、提示音、系统通知、钉钉/飞书推送)、运行状态(进行中任务与最近完成一览);侧栏入口 + 可拖拽弹窗(最大化/最小化/缩放)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"files": [
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build:client": "node scripts/build-client.mjs",
|
|
45
45
|
"test:client": "node scripts/test-client-views.mjs",
|
|
46
|
-
"test:host": "node scripts/test-task-host.mjs && node scripts/test-tokenlog-host.mjs",
|
|
46
|
+
"test:host": "node scripts/test-task-host.mjs && node scripts/test-tokenlog-host.mjs && node scripts/test-legacy-import.mjs && node scripts/test-mobile-relay-host.mjs && node scripts/test-mobile-relay.mjs",
|
|
47
47
|
"dev:client": "node scripts/watch-client.mjs",
|
|
48
48
|
"dev:link": "node scripts/dev-link-deps.mjs"
|
|
49
49
|
},
|
package/src/client.jsx
CHANGED
|
@@ -28,12 +28,13 @@ import { feature as fNotify } from "../features/notify/view.jsx";
|
|
|
28
28
|
import { feature as fRunState } from "../features/runstate/view.jsx";
|
|
29
29
|
import { feature as fGames } from "../features/games/view.jsx";
|
|
30
30
|
import { feature as fMobileRelay } from "../features/mobile-relay/view.jsx";
|
|
31
|
+
import { feature as fMobile } from "../features/mobile/view.js";
|
|
31
32
|
|
|
32
33
|
const name = "dsh-dock";
|
|
33
|
-
const DOCK_VERSION = "0.
|
|
34
|
+
const DOCK_VERSION = "0.13.0";
|
|
34
35
|
|
|
35
36
|
// ---- 内置功能注册表:新功能 = features/<id>/ 加模块 + 这里 import 一行 ----
|
|
36
|
-
const BUILTIN_FEATURES = [fTokenlog, fModelconfig, fHeartbeat, fTheme, fBalance, fAnimation, fNotify, fRunState, fGames, fMobileRelay];
|
|
37
|
+
const BUILTIN_FEATURES = [fTokenlog, fModelconfig, fHeartbeat, fTheme, fBalance, fAnimation, fNotify, fRunState, fGames, fMobileRelay, fMobile];
|
|
37
38
|
// 规划占位(路线图):接入后移除并建 features/<id>/ 模块
|
|
38
39
|
const PLANNED_FEATURES = [];
|
|
39
40
|
const PLANNED_NOTES = {};
|
package/src/host-core.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { readFileSync, existsSync } from 'node:fs'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { homedir } from 'node:os'
|
|
6
|
+
import yaml from 'js-yaml'
|
|
6
7
|
import z from '@deepseek-ai/schemastery'
|
|
7
8
|
|
|
8
9
|
/** dsh-dock 自有 settings 命名空间 = profile 条目 id(cordis.patch.yml 的 id)。 */
|
|
@@ -26,6 +27,15 @@ export const DockConfig = z.object({
|
|
|
26
27
|
passwordHash: z.string().default(''),
|
|
27
28
|
salt: z.string().default(''),
|
|
28
29
|
}).default({}).volatile(),
|
|
30
|
+
/** 远程访问网关的持久化状态:enabled=上次是否在跑;port=自选端口(0=主端口+1)。
|
|
31
|
+
* dsh web 重启后按此自动拉起网关(账号存在才拉),免得每次重启都要进面板点一次。 */
|
|
32
|
+
remoteGateway: z.object({
|
|
33
|
+
enabled: z.boolean().default(false),
|
|
34
|
+
/** 网关端口(0 = 跟随默认的主端口+1);面板「开启远程访问」时回写。 */
|
|
35
|
+
port: z.natural().default(0),
|
|
36
|
+
}).default({}).volatile(),
|
|
37
|
+
/** settings.yaml.imported 全量恢复(migrateImportedSections)是否已执行过的一次性标记。 */
|
|
38
|
+
importedRestored: z.boolean().default(false).volatile(),
|
|
29
39
|
visionProxy: z.object({
|
|
30
40
|
enabled: z.boolean().default(false),
|
|
31
41
|
provider: z.string().default(''),
|
|
@@ -51,7 +61,9 @@ export const DockConfig = z.object({
|
|
|
51
61
|
cacheRead: z.number().default(0),
|
|
52
62
|
cacheWrite: z.number().default(0),
|
|
53
63
|
/** 分时段价(可多段,如 DeepSeek 的高峰/优惠时段):命中某段用该段价,否则用上面的基准价。
|
|
54
|
-
* start/end 为本地小时数 0~24;start>end 表示跨零点;单段等价旧的 peak。
|
|
64
|
+
* start/end 为本地小时数 0~24;start>end 表示跨零点;单段等价旧的 peak。
|
|
65
|
+
* days:日期类型——all=每天(默认);workday=仅工作日(非周末且非下方 holidays);
|
|
66
|
+
* nonworkday=仅周末或节假日。用于「周末/节假日谷价、仅工作日高峰」这类计价。 */
|
|
55
67
|
peaks: z.array(z.object({
|
|
56
68
|
start: z.number().default(0),
|
|
57
69
|
end: z.number().default(0),
|
|
@@ -59,8 +71,12 @@ export const DockConfig = z.object({
|
|
|
59
71
|
output: z.number().default(0),
|
|
60
72
|
cacheRead: z.number().default(0),
|
|
61
73
|
cacheWrite: z.number().default(0),
|
|
74
|
+
days: z.string().default('all'),
|
|
62
75
|
})).default([]),
|
|
63
76
|
})).default([]),
|
|
77
|
+
/** 法定节假日:单日 YYYY-MM-DD 或区间 YYYY-MM-DD~YYYY-MM-DD(含两端)。
|
|
78
|
+
* 供 peaks.days=workday/nonworkday 判断;周末(周六/周日)始终视为非工作日。 */
|
|
79
|
+
holidays: z.array(z.string()).default([]),
|
|
64
80
|
/** 未匹配任何条目的模型所用兜底单价。 */
|
|
65
81
|
fallback: z.object({
|
|
66
82
|
input: z.number().default(2.16),
|
|
@@ -290,6 +306,125 @@ export async function migrateImportedFeatures(ctx) {
|
|
|
290
306
|
}
|
|
291
307
|
}
|
|
292
308
|
|
|
309
|
+
/** 深比较(JSON 化配置快照之间的相等判断)。 */
|
|
310
|
+
function sameValue(a, b) {
|
|
311
|
+
if (a === b) return true
|
|
312
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
313
|
+
return a.length === b.length && a.every((v, i) => sameValue(v, b[i]))
|
|
314
|
+
}
|
|
315
|
+
if (a && b && typeof a === 'object' && typeof b === 'object' && !Array.isArray(a) && !Array.isArray(b)) {
|
|
316
|
+
const ka = Object.keys(a)
|
|
317
|
+
const kb = Object.keys(b)
|
|
318
|
+
return ka.length === kb.length && ka.every((k) => Object.hasOwn(b, k) && sameValue(a[k], b[k]))
|
|
319
|
+
}
|
|
320
|
+
return false
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** schema 缺省值的普通对象快照(用来判断「现网这一段用户从没写过」)。
|
|
324
|
+
* schemastery 没有 parse():走 Standard Schema validate 取值,volatile 读写器交给 plainify 解开。 */
|
|
325
|
+
function dockDefaults() {
|
|
326
|
+
try {
|
|
327
|
+
const standard = DockConfig['~standard'] || DockConfig[Symbol.for('standard.schema')]
|
|
328
|
+
if (!standard || typeof standard.validate !== 'function') return null
|
|
329
|
+
const result = standard.validate({})
|
|
330
|
+
if (!result || result.issues || !result.value) return null
|
|
331
|
+
return plainifyDockConfig(result.value)
|
|
332
|
+
} catch {
|
|
333
|
+
return null
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** 只保留 schema 认识的顶层键:imported 旧文件可能带已废弃字段(如 animation 段里的
|
|
338
|
+
* 旧通知字段),整段原样写会被 mutate 校验拒绝——丢字段会让整次恢复失败。
|
|
339
|
+
* 值原样透传(嵌套结构交 schema 处理),这里只做顶层白名单。 */
|
|
340
|
+
function sanitizeSection(value, template) {
|
|
341
|
+
if (!template || typeof template !== 'object' || Array.isArray(template)) return value
|
|
342
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return value
|
|
343
|
+
const out = {}
|
|
344
|
+
for (const key of Object.keys(template)) {
|
|
345
|
+
if (Object.hasOwn(value, key)) out[key] = value[key]
|
|
346
|
+
}
|
|
347
|
+
return out
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* 一次性全量恢复:`~/.dsh/settings.yaml` 被 dsh 升级改名为 `settings.yaml.imported`
|
|
352
|
+
* 并尝试导入 profile 时,dsh-dock 当时还没导出 Config → 整段导入失败,账号
|
|
353
|
+
* (remoteAuth)、通知、动画、图片代理等配置全部停在 imported 文件里;后来只补迁了
|
|
354
|
+
* features(见 migrateImportedFeatures),于是「远程访问账号丢失、网关开不起来」。
|
|
355
|
+
*
|
|
356
|
+
* 这里把整段补回来,规则保守:
|
|
357
|
+
* - `features`:逐键补缺(现网已有的键绝不覆盖);
|
|
358
|
+
* - 其余 schema 认识的段:仅当现网该段仍等于 schema 缺省值(= 用户从未在此写过)
|
|
359
|
+
* 才整段恢复,现网有任何改动一律以现网为准(不覆盖新配置);
|
|
360
|
+
* - 执行过写入或确认无 imported 文档后置 `importedRestored` 标记,仅跑一次。
|
|
361
|
+
*/
|
|
362
|
+
export async function migrateImportedSections(ctx) {
|
|
363
|
+
try {
|
|
364
|
+
const root = readDockRoot(ctx)
|
|
365
|
+
if (root && root.importedRestored === true) return
|
|
366
|
+
const home = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
367
|
+
const file = join(home, 'settings.yaml.imported')
|
|
368
|
+
if (!existsSync(file)) return
|
|
369
|
+
let data = null
|
|
370
|
+
try {
|
|
371
|
+
data = yaml.load(readFileSync(file, 'utf8'))
|
|
372
|
+
} catch (error) {
|
|
373
|
+
console.warn('[dsh-dock] settings.yaml.imported 不是合法 YAML,跳过恢复:', (error && error.message) || String(error))
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
const section = data && typeof data === 'object' && !Array.isArray(data) ? data[DOCK_NS] : null
|
|
377
|
+
if (!section || typeof section !== 'object' || Array.isArray(section)) {
|
|
378
|
+
await mutateDockSection(ctx, ['importedRestored'], true)
|
|
379
|
+
return
|
|
380
|
+
}
|
|
381
|
+
const defaults = dockDefaults()
|
|
382
|
+
const live = root && typeof root === 'object' ? root : {}
|
|
383
|
+
const writes = []
|
|
384
|
+
const importedFeatures = section.features && typeof section.features === 'object' ? section.features : null
|
|
385
|
+
if (importedFeatures) {
|
|
386
|
+
const merged = live.features && typeof live.features === 'object' ? { ...live.features } : {}
|
|
387
|
+
let added = 0
|
|
388
|
+
for (const [key, value] of Object.entries(importedFeatures)) {
|
|
389
|
+
if (typeof value === 'boolean' && typeof merged[key] !== 'boolean') {
|
|
390
|
+
merged[key] = value
|
|
391
|
+
added++
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (added > 0) writes.push(['features', merged])
|
|
395
|
+
}
|
|
396
|
+
if (defaults) {
|
|
397
|
+
for (const key of Object.keys(section)) {
|
|
398
|
+
if (key === 'features') continue
|
|
399
|
+
if (!Object.hasOwn(defaults, key)) continue
|
|
400
|
+
// 现网缺该段(= 从没写过)或仍等于 schema 缺省 → 恢复;现网有用户改动一律不覆盖。
|
|
401
|
+
if (live[key] !== undefined && !sameValue(live[key], defaults[key])) continue
|
|
402
|
+
writes.push([key, sanitizeSection(section[key], defaults[key])])
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// 逐段独立落盘:一个段被拒(schema 不认/写盘瞬时失败)不拖死其余段——
|
|
406
|
+
// 尤其 remoteAuth 必须尽量恢复,否则远程访问账号一直起不来。
|
|
407
|
+
const restored = []
|
|
408
|
+
const failed = []
|
|
409
|
+
for (const [path, value] of writes) {
|
|
410
|
+
try {
|
|
411
|
+
await mutateDockSection(ctx, [path], value)
|
|
412
|
+
restored.push(path)
|
|
413
|
+
} catch (error) {
|
|
414
|
+
failed.push(path)
|
|
415
|
+
console.warn(`[dsh-dock] 恢复 ${path} 段失败:`, (error && error.message) || String(error))
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// 全部落盘(或本来就无事可做)才置一次性标记;有失败则下次启动重试。
|
|
419
|
+
if (failed.length === 0) await mutateDockSection(ctx, ['importedRestored'], true)
|
|
420
|
+
if (restored.length > 0) {
|
|
421
|
+
console.log('[dsh-dock] restored from settings.yaml.imported:', restored.join(', '))
|
|
422
|
+
}
|
|
423
|
+
} catch (e) {
|
|
424
|
+
console.warn('[dsh-dock] imported section restore skipped:', (e && e.message) || String(e))
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
293
428
|
/** 任务动画 effectMode 合法值(客户端动画模式)。 */
|
|
294
429
|
export const ANIMATION_MODES = [
|
|
295
430
|
'flow', 'breathe', 'ring', 'orbit', 'robot', 'matrix', 'stars', 'aurora', 'space',
|