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
package/src/host-core.js
CHANGED
|
@@ -1,27 +1,36 @@
|
|
|
1
1
|
// dsh-dock · 宿主共享内核:各功能模块(features/*/host.js)共用的常量与工具。
|
|
2
2
|
// 放在 src/ 而非 features/:不属于任何单个功能;提取功能模块独立成包时按需随包复制。
|
|
3
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { homedir } from 'node:os'
|
|
3
6
|
import z from '@deepseek-ai/schemastery'
|
|
4
7
|
|
|
5
|
-
/** dsh-dock 自有 settings
|
|
8
|
+
/** dsh-dock 自有 settings 命名空间 = profile 条目 id(cordis.patch.yml 的 id)。 */
|
|
6
9
|
export const DOCK_NS = 'dsh-dock'
|
|
7
10
|
|
|
8
11
|
/** 自有命名空间的 schema(功能开关 + 图片理解代理 + 任务动画/任务通知配置 + 远程访问账号)。
|
|
9
|
-
* 运行状态模块只读共享追踪、不写配置,因此没有自己的段。
|
|
12
|
+
* 运行状态模块只读共享追踪、不写配置,因此没有自己的段。
|
|
13
|
+
*
|
|
14
|
+
* dsh ≥ 0.1.7-alpha.1:settings 服务改为 SettingsForms——
|
|
15
|
+
* ① 插件必须导出同名 Config(否则 mutate 报 No configurable plugin entry);
|
|
16
|
+
* ② ns 是 profile 条目 id(此处 DOCK_NS=cordis.patch.yml 的 id);
|
|
17
|
+
* ③ 活编辑路径必须落在 .volatile() 字段上,否则 write 抛 not volatile。
|
|
18
|
+
* 每个顶层段整体 volatile:段内任意 path 可被面板 mutate,无需逐字段标记。 */
|
|
10
19
|
export const DockConfig = z.object({
|
|
11
20
|
// 宿主侧功能开关(id -> boolean):客户端面板 toggle 即时同步过来,
|
|
12
21
|
// 重启 dsh 后按此表决定宿主半部 setup 哪些功能(路由注册、事件订阅等)。
|
|
13
|
-
features: z.dict(z.boolean()).default({}),
|
|
22
|
+
features: z.dict(z.boolean()).default({}).volatile(),
|
|
14
23
|
remoteAuth: z.object({
|
|
15
24
|
/** 远程访问登录账号(明文用户名;密码只存加盐哈希)。 */
|
|
16
25
|
username: z.string().default(''),
|
|
17
26
|
passwordHash: z.string().default(''),
|
|
18
27
|
salt: z.string().default(''),
|
|
19
|
-
}).default({}),
|
|
28
|
+
}).default({}).volatile(),
|
|
20
29
|
visionProxy: z.object({
|
|
21
30
|
enabled: z.boolean().default(false),
|
|
22
31
|
provider: z.string().default(''),
|
|
23
32
|
model: z.string().default(''),
|
|
24
|
-
}).default({}),
|
|
33
|
+
}).default({}).volatile(),
|
|
25
34
|
// 【用量记录】模块的单价配置:官网抓取的刊例价常与实际计费不符(第三方/中转、
|
|
26
35
|
// 折扣、自建端点等),这里保存用户自填单价并优先用于费用估算。
|
|
27
36
|
// 详见 features/tokenlog/host.js 的 resolvePricing(优先级:自定义 > 官网 > 内置 > 兜底)。
|
|
@@ -42,7 +51,9 @@ export const DockConfig = z.object({
|
|
|
42
51
|
cacheRead: z.number().default(0),
|
|
43
52
|
cacheWrite: z.number().default(0),
|
|
44
53
|
/** 分时段价(可多段,如 DeepSeek 的高峰/优惠时段):命中某段用该段价,否则用上面的基准价。
|
|
45
|
-
* start/end 为本地小时数 0~24;start>end 表示跨零点;单段等价旧的 peak。
|
|
54
|
+
* start/end 为本地小时数 0~24;start>end 表示跨零点;单段等价旧的 peak。
|
|
55
|
+
* days:日期类型——all=每天(默认);workday=仅工作日(非周末且非下方 holidays);
|
|
56
|
+
* nonworkday=仅周末或节假日。用于「周末/节假日谷价、仅工作日高峰」这类计价。 */
|
|
46
57
|
peaks: z.array(z.object({
|
|
47
58
|
start: z.number().default(0),
|
|
48
59
|
end: z.number().default(0),
|
|
@@ -50,8 +61,12 @@ export const DockConfig = z.object({
|
|
|
50
61
|
output: z.number().default(0),
|
|
51
62
|
cacheRead: z.number().default(0),
|
|
52
63
|
cacheWrite: z.number().default(0),
|
|
64
|
+
days: z.string().default('all'),
|
|
53
65
|
})).default([]),
|
|
54
66
|
})).default([]),
|
|
67
|
+
/** 法定节假日:单日 YYYY-MM-DD 或区间 YYYY-MM-DD~YYYY-MM-DD(含两端)。
|
|
68
|
+
* 供 peaks.days=workday/nonworkday 判断;周末(周六/周日)始终视为非工作日。 */
|
|
69
|
+
holidays: z.array(z.string()).default([]),
|
|
55
70
|
/** 未匹配任何条目的模型所用兜底单价。 */
|
|
56
71
|
fallback: z.object({
|
|
57
72
|
input: z.number().default(2.16),
|
|
@@ -59,7 +74,7 @@ export const DockConfig = z.object({
|
|
|
59
74
|
cacheRead: z.number().default(0.43),
|
|
60
75
|
cacheWrite: z.number().default(4.32),
|
|
61
76
|
}).default({}),
|
|
62
|
-
}).default({}),
|
|
77
|
+
}).default({}).volatile(),
|
|
63
78
|
// 【任务动画】模块:只放动画本身(动效开关/模式/桌面伙伴大小)。
|
|
64
79
|
// 通知相关的字段已全部迁往下面的 notify 段(见 migrateNotifyConfig);
|
|
65
80
|
// 旧值不在 schema 里也会被 schemastery 原样保留,迁移读得到。
|
|
@@ -68,7 +83,7 @@ export const DockConfig = z.object({
|
|
|
68
83
|
effectMode: z.string().default('flow'),
|
|
69
84
|
// 桌面伙伴场景缩放(0.85~2.2;浮层右下角也可直接拖动缩放)
|
|
70
85
|
robotScale: z.number().default(1.35),
|
|
71
|
-
}).default({}),
|
|
86
|
+
}).default({}).volatile(),
|
|
72
87
|
// 【任务通知】模块:页内卡片 / 提示音 / 浏览器系统通知 / 群机器人推送。
|
|
73
88
|
// 模块自身的启停由 features.notify 开关负责(独立菜单项),这里只放通知行为配置。
|
|
74
89
|
notify: z.object({
|
|
@@ -92,9 +107,99 @@ export const DockConfig = z.object({
|
|
|
92
107
|
feishuWebhook: z.string().default(''),
|
|
93
108
|
// 一次性迁移标记:animation 段的旧通知配置已搬到本段(搬完置 true,避免每次启动重复写)
|
|
94
109
|
migratedFromAnimation: z.boolean().default(false),
|
|
95
|
-
}).default({}),
|
|
110
|
+
}).default({}).volatile(),
|
|
96
111
|
})
|
|
97
112
|
|
|
113
|
+
// ── 本地配置镜像(替代已移除的 settings.get) ─────────────────────────────
|
|
114
|
+
// apply(ctx, config) 绑定带 volatile 引用的 Config;每次操作前 plainify 一次取快照。
|
|
115
|
+
// settings.mutate 成功后由调用方/ volatile-update 刷新。测试桩可用 seedDockConfig 注入。
|
|
116
|
+
|
|
117
|
+
/** @type {null | Record<string, unknown>} */
|
|
118
|
+
let dockConfigBound = null
|
|
119
|
+
|
|
120
|
+
/** 是否为 cosmokit Volatile 引用(跨副本用 Symbol 协议识别,不依赖包导入)。 */
|
|
121
|
+
function isVolatileRef(value) {
|
|
122
|
+
return typeof value === 'object' && value !== null && Symbol.for('cosmokit.volatile.write') in value
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 递归解开 volatile 引用 → 可 JSON 化的普通对象。 */
|
|
126
|
+
export function plainifyDockConfig(value) {
|
|
127
|
+
if (isVolatileRef(value)) return plainifyDockConfig(value.get())
|
|
128
|
+
if (Array.isArray(value)) return value.map(plainifyDockConfig)
|
|
129
|
+
if (value !== null && typeof value === 'object') {
|
|
130
|
+
const out = {}
|
|
131
|
+
for (const [k, v] of Object.entries(value)) out[k] = plainifyDockConfig(v)
|
|
132
|
+
return out
|
|
133
|
+
}
|
|
134
|
+
return value
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 由 index.js apply 挂载/刷新:持有 Config 引用并在 loader/volatile-update 后重读。 */
|
|
138
|
+
export function bindDockConfig(config) {
|
|
139
|
+
dockConfigBound = config === undefined || config === null ? null : plainifyDockConfig(config)
|
|
140
|
+
return dockConfigBound
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 测试/本地桩:直接写入镜像(绕过宿主 settings)。 */
|
|
144
|
+
export function seedDockConfig(value) {
|
|
145
|
+
dockConfigBound = value === undefined || value === null ? null : plainifyDockConfig(value)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 读自有命名空间根对象。优先级:
|
|
150
|
+
* 1) apply 绑定的 Config 镜像(新宿主活值)
|
|
151
|
+
* 2) 测试/旧宿主的 settings.get(若仍存在)
|
|
152
|
+
* 3) settings.describe 里 ns=DOCK_NS 的 value(SettingsForms 读路径)
|
|
153
|
+
* 4) null(调用方回退默认)
|
|
154
|
+
*/
|
|
155
|
+
export function readDockRoot(ctx) {
|
|
156
|
+
if (dockConfigBound) return dockConfigBound
|
|
157
|
+
try {
|
|
158
|
+
const settings = ctx && typeof ctx.get === 'function' ? ctx.get('settings') : null
|
|
159
|
+
if (settings) {
|
|
160
|
+
if (typeof settings.get === 'function') {
|
|
161
|
+
const v = settings.get(DOCK_NS)
|
|
162
|
+
if (v && typeof v === 'object') return v
|
|
163
|
+
}
|
|
164
|
+
if (typeof settings.describe === 'function') {
|
|
165
|
+
const views = settings.describe({ redactSecrets: false }) || []
|
|
166
|
+
const hit = views.find((d) => d && d.ns === DOCK_NS && d.value && typeof d.value === 'object')
|
|
167
|
+
if (hit) return hit.value
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch { /* settings 未挂载 */ }
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 写自有命名空间一段(走 SettingsForms.mutate;成功后同步本地镜像)。
|
|
176
|
+
* @param {object} ctx Cordis context
|
|
177
|
+
* @param {string[]} path 段路径(如 ['animation'])
|
|
178
|
+
* @param {unknown} value 整段值
|
|
179
|
+
* @param {number} [expectedRevision] describe 的 revision(乐观锁,可选)
|
|
180
|
+
*/
|
|
181
|
+
export async function mutateDockSection(ctx, path, value, expectedRevision) {
|
|
182
|
+
const settings = ctx && typeof ctx.get === 'function' ? ctx.get('settings') : null
|
|
183
|
+
if (!settings || typeof settings.mutate !== 'function') {
|
|
184
|
+
throw new Error('settings 服务不可用,配置无法持久化')
|
|
185
|
+
}
|
|
186
|
+
await settings.mutate(DOCK_NS, [{ op: 'set', path, value }], expectedRevision)
|
|
187
|
+
if (dockConfigBound) {
|
|
188
|
+
const next = { ...dockConfigBound }
|
|
189
|
+
let node = next
|
|
190
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
191
|
+
const key = path[i]
|
|
192
|
+
const child = node[key] && typeof node[key] === 'object' && !Array.isArray(node[key]) ? { ...node[key] } : {}
|
|
193
|
+
node[key] = child
|
|
194
|
+
node = child
|
|
195
|
+
}
|
|
196
|
+
if (path.length > 0) next[path[path.length - 1]] = value
|
|
197
|
+
else Object.assign(next, value)
|
|
198
|
+
dockConfigBound = next
|
|
199
|
+
}
|
|
200
|
+
return value
|
|
201
|
+
}
|
|
202
|
+
|
|
98
203
|
/** 通知配置字段清单(schema、迁移、客户端默认值三处共用同一份键名)。 */
|
|
99
204
|
export const NOTIFY_FIELDS = [
|
|
100
205
|
'notifyOnComplete', 'notifyOnError', 'notifyOnConfirm', 'notifyStayMs',
|
|
@@ -112,9 +217,7 @@ export const NOTIFY_FIELDS = [
|
|
|
112
217
|
*/
|
|
113
218
|
export async function migrateNotifyConfig(ctx) {
|
|
114
219
|
try {
|
|
115
|
-
const
|
|
116
|
-
if (!settings || typeof settings.get !== 'function' || typeof settings.mutate !== 'function') return
|
|
117
|
-
const root = settings.get(DOCK_NS)
|
|
220
|
+
const root = readDockRoot(ctx)
|
|
118
221
|
const notify = root && typeof root === 'object' && root.notify && typeof root.notify === 'object' ? root.notify : null
|
|
119
222
|
if (notify && notify.migratedFromAnimation === true) return
|
|
120
223
|
const animation = root && typeof root === 'object' && root.animation && typeof root.animation === 'object' ? root.animation : null
|
|
@@ -127,7 +230,7 @@ export async function migrateNotifyConfig(ctx) {
|
|
|
127
230
|
// 避免 settings 命名空间尚未生效时误把「空迁移」当完成(那会丢掉旧配置)。
|
|
128
231
|
if (copied === 0) return
|
|
129
232
|
const next = Object.assign({}, notify || {}, legacy, { migratedFromAnimation: true })
|
|
130
|
-
await
|
|
233
|
+
await mutateDockSection(ctx, ['notify'], next)
|
|
131
234
|
console.log('[dsh-dock] notify config migrated from animation section; fields copied:', copied)
|
|
132
235
|
} catch (e) {
|
|
133
236
|
console.warn('[dsh-dock] notify config migration skipped:', (e && e.message) || String(e))
|
|
@@ -144,20 +247,55 @@ export async function migrateNotifyConfig(ctx) {
|
|
|
144
247
|
*/
|
|
145
248
|
export async function migrateModelsFeatureId(ctx) {
|
|
146
249
|
try {
|
|
147
|
-
const
|
|
148
|
-
if (!settings || typeof settings.get !== 'function' || typeof settings.mutate !== 'function') return
|
|
149
|
-
const root = settings.get(DOCK_NS)
|
|
250
|
+
const root = readDockRoot(ctx)
|
|
150
251
|
const features = root && typeof root === 'object' && root.features && typeof root.features === 'object' ? root.features : null
|
|
151
252
|
if (!features || typeof features.models !== 'boolean' || typeof features.modelconfig === 'boolean') return
|
|
152
253
|
const next = Object.assign({}, features, { modelconfig: features.models })
|
|
153
254
|
delete next.models
|
|
154
|
-
await
|
|
255
|
+
await mutateDockSection(ctx, ['features'], next)
|
|
155
256
|
console.log('[dsh-dock] feature id migrated: features.models -> features.modelconfig =', features.models)
|
|
156
257
|
} catch (e) {
|
|
157
258
|
console.warn('[dsh-dock] models feature id migration skipped:', (e && e.message) || String(e))
|
|
158
259
|
}
|
|
159
260
|
}
|
|
160
261
|
|
|
262
|
+
/**
|
|
263
|
+
* 一次性迁移:旧 settings.yaml 已被宿主改名为 settings.yaml.imported;
|
|
264
|
+
* 当时 dsh-dock 尚未导出 Config,importLegacyDocument 对本段 update 会失败,
|
|
265
|
+
* 功能开关表停在 imported 文件里。配置可写后把 features 补写进 profile。
|
|
266
|
+
*/
|
|
267
|
+
export async function migrateImportedFeatures(ctx) {
|
|
268
|
+
try {
|
|
269
|
+
const root = readDockRoot(ctx)
|
|
270
|
+
const features = root && typeof root === 'object' && root.features && typeof root.features === 'object' ? root.features : null
|
|
271
|
+
if (features && Object.keys(features).length > 0) return
|
|
272
|
+
const home = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
273
|
+
const file = join(home, 'settings.yaml.imported')
|
|
274
|
+
if (!existsSync(file)) return
|
|
275
|
+
const text = readFileSync(file, 'utf8')
|
|
276
|
+
// 极简 YAML:仅取 dsh-dock.features 下的 `key: true|false` 行(避免引入完整解析器)
|
|
277
|
+
const lines = text.split(/\r?\n/)
|
|
278
|
+
let inDock = false
|
|
279
|
+
let inFeatures = false
|
|
280
|
+
const imported = {}
|
|
281
|
+
for (const line of lines) {
|
|
282
|
+
if (/^dsh-dock:\s*$/.test(line)) { inDock = true; inFeatures = false; continue }
|
|
283
|
+
if (inDock && /^\S/.test(line) && !/^dsh-dock:/.test(line)) { inDock = false; inFeatures = false }
|
|
284
|
+
if (inDock && /^\s{2}features:\s*$/.test(line)) { inFeatures = true; continue }
|
|
285
|
+
if (inDock && inFeatures && /^\s{2}\S/.test(line)) { inFeatures = false }
|
|
286
|
+
if (inDock && inFeatures) {
|
|
287
|
+
const m = /^\s{4}([A-Za-z0-9_-]+):\s*(true|false)\s*$/.exec(line)
|
|
288
|
+
if (m) imported[m[1]] = m[2] === 'true'
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (Object.keys(imported).length === 0) return
|
|
292
|
+
await mutateDockSection(ctx, ['features'], imported)
|
|
293
|
+
console.log('[dsh-dock] features imported from settings.yaml.imported:', imported)
|
|
294
|
+
} catch (e) {
|
|
295
|
+
console.warn('[dsh-dock] imported features migration skipped:', (e && e.message) || String(e))
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
161
299
|
/** 任务动画 effectMode 合法值(客户端动画模式)。 */
|
|
162
300
|
export const ANIMATION_MODES = [
|
|
163
301
|
'flow', 'breathe', 'ring', 'orbit', 'robot', 'matrix', 'stars', 'aurora', 'space',
|