dsh-dock 0.12.0 → 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/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(''),
@@ -296,6 +306,125 @@ export async function migrateImportedFeatures(ctx) {
296
306
  }
297
307
  }
298
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
+
299
428
  /** 任务动画 effectMode 合法值(客户端动画模式)。 */
300
429
  export const ANIMATION_MODES = [
301
430
  'flow', 'breathe', 'ring', 'orbit', 'robot', 'matrix', 'stars', 'aurora', 'space',