dsh-config-manager 0.1.72 → 0.1.76

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.
Files changed (153) hide show
  1. package/README.md +40 -4
  2. package/README.zh-CN.md +40 -4
  3. package/lib/adapters/index.d.ts +1 -0
  4. package/lib/adapters/index.js +5 -0
  5. package/lib/adapters/messages.d.ts +3 -0
  6. package/lib/adapters/messages.js +7 -0
  7. package/lib/adapters/plugin-data.d.ts +114 -0
  8. package/lib/adapters/plugin-data.js +309 -0
  9. package/lib/adapters/self.d.ts +1 -1
  10. package/lib/adapters/self.js +2 -1
  11. package/lib/adapters/test-helpers.d.ts +9 -0
  12. package/lib/adapters/test-helpers.js +14 -0
  13. package/lib/client.d.ts +103 -10
  14. package/lib/client.js +10748 -10119
  15. package/lib/core/analyzer.d.ts +11 -2
  16. package/lib/core/analyzer.js +31 -8
  17. package/lib/core/backup-plan.d.ts +10 -0
  18. package/lib/core/backup-plan.js +21 -3
  19. package/lib/core/backup.d.ts +20 -1
  20. package/lib/core/backup.js +48 -3
  21. package/lib/core/command-rollback.d.ts +6 -2
  22. package/lib/core/command-rollback.js +6 -2
  23. package/lib/core/messages-core.d.ts +2 -0
  24. package/lib/core/messages-core.js +4 -0
  25. package/lib/core/messages.d.ts +2 -1
  26. package/lib/core/messages.js +4 -2
  27. package/lib/core/plugin-data-registry.d.ts +124 -0
  28. package/lib/core/plugin-data-registry.js +198 -0
  29. package/lib/core/rollback-gate.d.ts +33 -0
  30. package/lib/core/rollback-gate.js +57 -0
  31. package/lib/core/rollback-ledger.d.ts +274 -0
  32. package/lib/core/rollback-ledger.js +1400 -0
  33. package/lib/core/rollback-point.d.ts +406 -5
  34. package/lib/core/rollback-point.js +1453 -306
  35. package/lib/core/rollback.js +6 -4
  36. package/lib/core/types.d.ts +19 -0
  37. package/lib/foreign/registry.d.ts +1 -1
  38. package/lib/foreign/registry.js +1 -1
  39. package/lib/index.d.ts +5 -1
  40. package/lib/index.js +83 -13
  41. package/lib/routes/messages.d.ts +1 -0
  42. package/lib/routes/messages.js +2 -0
  43. package/lib/routes/prefs.js +26 -3
  44. package/lib/routes/recovery.d.ts +21 -0
  45. package/lib/routes/recovery.js +127 -7
  46. package/lib/routes/sync.js +11 -1
  47. package/lib/schema/section-registry.d.ts +1 -1
  48. package/lib/schema/section-registry.js +32 -3
  49. package/lib/schema/types.d.ts +1 -1
  50. package/lib/sync/messages.d.ts +1 -0
  51. package/lib/sync/messages.js +3 -0
  52. package/lib/sync/sync-engine.d.ts +21 -7
  53. package/lib/sync/sync-engine.js +108 -18
  54. package/lib/sync/ui-prefs.d.ts +22 -0
  55. package/lib/sync/ui-prefs.js +28 -0
  56. package/lib/ui/diff-view.d.ts +10 -0
  57. package/lib/ui/diff-view.js +17 -0
  58. package/lib/ui/export-flow.js +2 -0
  59. package/lib/ui/i18n.d.ts +5 -3
  60. package/lib/ui/i18n.js +10 -6
  61. package/lib/ui/rollback-view.d.ts +59 -0
  62. package/lib/ui/rollback-view.js +14 -0
  63. package/lib/ui/test-helpers.js +2 -1
  64. package/lib/utils/bundle-scan.d.ts +5 -3
  65. package/lib/utils/bundle-scan.js +5 -3
  66. package/lib/utils/line-diff.d.ts +7 -1
  67. package/lib/utils/line-diff.js +8 -2
  68. package/package.json +1 -1
  69. package/src/adapters/index.ts +5 -0
  70. package/src/adapters/messages.ts +7 -0
  71. package/src/adapters/plugin-data.test.ts +225 -0
  72. package/src/adapters/plugin-data.ts +351 -0
  73. package/src/adapters/self.ts +2 -1
  74. package/src/adapters/test-helpers.ts +15 -0
  75. package/src/client/chat/ChatEntryView.tsx +59 -10
  76. package/src/client/chat/RollbackDiffTab.tsx +49 -2
  77. package/src/client/chat/RollbackFileIcon.tsx +7 -45
  78. package/src/client/chat/RollbackReviewDialog.tsx +20 -4
  79. package/src/client/chat/chat-entry-view.test.ts +164 -1
  80. package/src/client/chat/chat-entry-view.ts +127 -10
  81. package/src/client/chat/chat-node-registration.ts +40 -6
  82. package/src/client/chat/chat-point-window.ts +11 -1
  83. package/src/client/chat/rollback-diff-tab.ts +13 -3
  84. package/src/client/chat/rollback-entry-view.ts +5 -0
  85. package/src/client/chat/sidebar-slots.ts +24 -2
  86. package/src/client/common/DiffTable.tsx +54 -29
  87. package/src/client/common/client-route-parity.test.ts +6 -0
  88. package/src/client/common/host-primitives.ts +78 -0
  89. package/src/client/common/routes.ts +10 -0
  90. package/src/client/common/section-labels.test.ts +1 -0
  91. package/src/client/common/section-labels.ts +2 -0
  92. package/src/client/config-manager.module.css +138 -14
  93. package/src/client/environment/maintenance/DiskUsageCard.tsx +20 -3
  94. package/src/client/index.ts +54 -2
  95. package/src/client/library/RestorePlanView.tsx +0 -1
  96. package/src/client/locales.ts +8 -6
  97. package/src/client/recovery/RollbackCard.tsx +201 -7
  98. package/src/client/recovery/RollbackDiffPane.tsx +26 -8
  99. package/src/client/recovery/recovery-api.ts +28 -0
  100. package/src/client/recovery/recovery-locales.ts +36 -6
  101. package/src/client/recovery/rollback-enabled.ts +41 -0
  102. package/src/client/recovery/rollback-file-changes.test.ts +77 -0
  103. package/src/client/sync/PushPreviewCard.tsx +57 -0
  104. package/src/client/sync/SyncPanel.tsx +2 -37
  105. package/src/client/sync/SyncSettingsView.test.ts +3 -0
  106. package/src/client/sync/sync-push-preview.test.ts +14 -0
  107. package/src/client/sync/sync-view.test.ts +1 -1
  108. package/src/client/sync/sync-view.ts +9 -1
  109. package/src/core/analyzer.ts +31 -9
  110. package/src/core/backup-plan.test.ts +20 -5
  111. package/src/core/backup-plan.ts +22 -3
  112. package/src/core/backup.ts +50 -4
  113. package/src/core/command-rollback.ts +6 -2
  114. package/src/core/messages-core.ts +4 -0
  115. package/src/core/messages.ts +4 -2
  116. package/src/core/plugin-data-registry.test.ts +113 -0
  117. package/src/core/plugin-data-registry.ts +261 -0
  118. package/src/core/rollback-gate.test.ts +66 -0
  119. package/src/core/rollback-gate.ts +61 -0
  120. package/src/core/rollback-ledger.test.ts +655 -0
  121. package/src/core/rollback-ledger.ts +1430 -0
  122. package/src/core/rollback-point.test.ts +866 -41
  123. package/src/core/rollback-point.ts +1582 -291
  124. package/src/core/rollback.ts +5 -3
  125. package/src/core/types.ts +19 -0
  126. package/src/foreign/registry.test.ts +1 -1
  127. package/src/foreign/registry.ts +1 -1
  128. package/src/index.ts +73 -10
  129. package/src/routes/messages.ts +2 -0
  130. package/src/routes/prefs.ts +31 -2
  131. package/src/routes/recovery.ts +122 -7
  132. package/src/routes/sync.ts +15 -1
  133. package/src/schema/manifest-schema.test.ts +4 -4
  134. package/src/schema/registry.test.ts +9 -2
  135. package/src/schema/section-registry.ts +32 -3
  136. package/src/schema/types.ts +2 -1
  137. package/src/sync/messages.ts +3 -0
  138. package/src/sync/sync-credentials.test.ts +119 -0
  139. package/src/sync/sync-engine.ts +106 -18
  140. package/src/sync/sync-preview-push.test.ts +158 -0
  141. package/src/sync/transport.test.ts +1 -0
  142. package/src/sync/ui-prefs.test.ts +44 -1
  143. package/src/sync/ui-prefs.ts +39 -0
  144. package/src/ui/diff-view.test.ts +9 -1
  145. package/src/ui/diff-view.ts +22 -0
  146. package/src/ui/export-flow.ts +2 -0
  147. package/src/ui/i18n.ts +10 -6
  148. package/src/ui/rollback-view.test.ts +17 -0
  149. package/src/ui/rollback-view.ts +66 -0
  150. package/src/ui/test-helpers.ts +2 -1
  151. package/src/utils/bundle-scan.ts +5 -3
  152. package/src/utils/line-diff.test.ts +12 -0
  153. package/src/utils/line-diff.ts +9 -3
@@ -171,11 +171,20 @@ export declare class Analyzer {
171
171
  * 未采用其导入内容的 Conflict 都不写目标,记录只会保守化 reconcile 而无收益。
172
172
  */
173
173
  private shouldJournalStep;
174
- /** 文件类且可指纹 → 返回 home-relative 目标路径(posix);否则 null(不可指纹外部项)。
174
+ /** 文件类且可指纹 → 返回「根相对路径」(journal 的 ref 身份);否则 null(不可指纹外部项)。
175
175
  * profile 必须传入:plugins 分区的 pnpm-workspace.yaml / patch 文件位于
176
176
  * `profiles/<profile>/` 下,缺了它算出的相对路径不存在 → 指纹恒 null(issue #35)。 */
177
177
  private static fileRelFor;
178
- /** 读目标文件算 sha256(home-relative;经 HostContext.fs 使测试 mock 可注入)。失败返回 null。 */
178
+ /**
179
+ * 文件类且可指纹 → 返回「该分区自己的门面 + 绝对路径」。
180
+ *
181
+ * 为什么不能只拿 home-relative 路径去读:`pluginData` / `pluginAssets` 的根是**系统 home**
182
+ * (见 `core/backup.ts` 的 `fsForSection`),用 `ctx.fs` 读会落到 $DSH_HOME 下的错误位置
183
+ * 或被越界守卫拦下 —— 指纹会静默变成「读不到」,reconcile 因此永远判不出是否已应用。
184
+ * 宿主未提供该门面 / 路径不可解析 → null(保守:不可证明)。
185
+ */
186
+ private static fileTargetFor;
187
+ /** 读目标文件算 sha256(经该分区自己的门面,测试 mock 可注入)。失败返回 null。 */
179
188
  private fileFp;
180
189
  private applyOne;
181
190
  }
@@ -19,7 +19,7 @@ import { isAbsolutePath, applyPrefixMappings } from '../utils/paths.js';
19
19
  import { parseZip } from '../utils/zip.js';
20
20
  import { loadTombstones, isTombstoned } from '../schema/tombstones.js';
21
21
  import { DEFAULT_SENSITIVE_RELS, restoreVaultFiles } from '../security/vault.js';
22
- import { createSnapshot, planItemWritesTarget, resolveFileTarget, resolveFileTargetRel } from './backup.js';
22
+ import { createSnapshot, fsForSection, planItemWritesTarget, resolveFileTarget, resolveFileTargetRel } from './backup.js';
23
23
  import { isCredentialConfigured } from './credential-status.js';
24
24
  import { rollback } from './rollback.js';
25
25
  import { compatibilityReasons, computeCompatibility } from './validator.js';
@@ -790,7 +790,7 @@ export class Analyzer {
790
790
  let beforeFp = null;
791
791
  if (Analyzer.fileRelFor(item, importCtx.target.profile) !== null) {
792
792
  rel = Analyzer.fileRelFor(item, importCtx.target.profile);
793
- beforeFp = await this.fileFp(importCtx, rel);
793
+ beforeFp = await this.fileFp(importCtx, item);
794
794
  }
795
795
  beforeFpByItem.set(item.id, beforeFp);
796
796
  await sb.recordStep({
@@ -896,7 +896,7 @@ export class Analyzer {
896
896
  const rel = Analyzer.fileRelFor(item, importCtx.target.profile);
897
897
  let afterFp = null;
898
898
  if (rel !== null && outcome.executed.status === 'ok') {
899
- afterFp = await this.fileFp(importCtx, rel);
899
+ afterFp = await this.fileFp(importCtx, item);
900
900
  }
901
901
  await sb.recordStep({
902
902
  id: item.id,
@@ -1227,7 +1227,7 @@ export class Analyzer {
1227
1227
  shouldJournalStep(item) {
1228
1228
  return planItemWritesTarget(item);
1229
1229
  }
1230
- /** 文件类且可指纹 → 返回 home-relative 目标路径(posix);否则 null(不可指纹外部项)。
1230
+ /** 文件类且可指纹 → 返回「根相对路径」(journal 的 ref 身份);否则 null(不可指纹外部项)。
1231
1231
  * profile 必须传入:plugins 分区的 pnpm-workspace.yaml / patch 文件位于
1232
1232
  * `profiles/<profile>/` 下,缺了它算出的相对路径不存在 → 指纹恒 null(issue #35)。 */
1233
1233
  static fileRelFor(item, profile) {
@@ -1238,11 +1238,34 @@ export class Analyzer {
1238
1238
  return null;
1239
1239
  return resolveFileTargetRel(item.adapter, ref, profile);
1240
1240
  }
1241
- /** 读目标文件算 sha256(home-relative;经 HostContext.fs 使测试 mock 可注入)。失败返回 null。 */
1242
- async fileFp(ctx, rel) {
1241
+ /**
1242
+ * 文件类且可指纹 → 返回「该分区自己的门面 + 绝对路径」。
1243
+ *
1244
+ * 为什么不能只拿 home-relative 路径去读:`pluginData` / `pluginAssets` 的根是**系统 home**
1245
+ * (见 `core/backup.ts` 的 `fsForSection`),用 `ctx.fs` 读会落到 $DSH_HOME 下的错误位置
1246
+ * 或被越界守卫拦下 —— 指纹会静默变成「读不到」,reconcile 因此永远判不出是否已应用。
1247
+ * 宿主未提供该门面 / 路径不可解析 → null(保守:不可证明)。
1248
+ */
1249
+ static fileTargetFor(ctx, item) {
1250
+ if (!isFileSection(item.adapter))
1251
+ return null;
1252
+ const ref = item.target?.ref;
1253
+ if (ref === undefined || ref === '')
1254
+ return null;
1255
+ try {
1256
+ return { fs: fsForSection(ctx.target, item.adapter), abs: resolveFileTarget(ctx.target, item.adapter, ref) };
1257
+ }
1258
+ catch {
1259
+ return null;
1260
+ }
1261
+ }
1262
+ /** 读目标文件算 sha256(经该分区自己的门面,测试 mock 可注入)。失败返回 null。 */
1263
+ async fileFp(ctx, item) {
1264
+ const target = Analyzer.fileTargetFor(ctx, item);
1265
+ if (target === null)
1266
+ return null;
1243
1267
  try {
1244
- const data = await ctx.target.fs.readFile(rel);
1245
- return sha256Hex(data);
1268
+ return sha256Hex(await target.fs.readFile(target.abs));
1246
1269
  }
1247
1270
  catch {
1248
1271
  return null;
@@ -11,6 +11,16 @@ export declare const DEFAULT_BACKUP_SECTIONS: readonly SectionId[];
11
11
  export declare const OPT_IN_BACKUP_SECTIONS: readonly SectionId[];
12
12
  /** 全部可离线收集的分区(默认 ∪ 显式选中) */
13
13
  export declare const OFFLINE_BACKUP_SECTIONS: readonly SectionId[];
14
+ /**
15
+ * 离线 CLI **明确不收集**的分区(第三方插件数据)。
16
+ *
17
+ * 为什么不做:它的收集目标不是静态路径,而是「本机内置来源注册表 + 已安装插件自述声明」推导出来的
18
+ * 区域(见 `core/plugin-data-registry.ts`),还要尊重环境变量覆盖判定 —— 这套判定住在 adapter 里
19
+ * (走 HostContext 门面),而 CLI 的硬约束是**绝不 import DSH 门面**(peer 缺失也要能跑)。
20
+ * 在这里手抄一份区域表会立刻与注册表漂移(正是本文件开头反复说的那类缺陷),所以选择**显式不适用**:
21
+ * 用户显式 `--sections pluginData` 时给一条可执行的提示(改用 GUI 导出/导入),而不是静默不收。
22
+ */
23
+ export declare const OFFLINE_EXCLUDED_SECTIONS: readonly SectionId[];
14
24
  /**
15
25
  * 离线**无法**收集的分区(需要 DSH Service 门面读值/权威脱敏)。仅用于报告与参数校验,
16
26
  * 绝不写进归档:这些分区的权威形态是 GUI 导出(经 DSH `describe({redactSecrets:true})`
@@ -63,6 +63,16 @@ export const OPT_IN_BACKUP_SECTIONS = ['pluginFiles'];
63
63
  export const OFFLINE_BACKUP_SECTIONS = [
64
64
  ...DEFAULT_BACKUP_SECTIONS, ...OPT_IN_BACKUP_SECTIONS,
65
65
  ];
66
+ /**
67
+ * 离线 CLI **明确不收集**的分区(第三方插件数据)。
68
+ *
69
+ * 为什么不做:它的收集目标不是静态路径,而是「本机内置来源注册表 + 已安装插件自述声明」推导出来的
70
+ * 区域(见 `core/plugin-data-registry.ts`),还要尊重环境变量覆盖判定 —— 这套判定住在 adapter 里
71
+ * (走 HostContext 门面),而 CLI 的硬约束是**绝不 import DSH 门面**(peer 缺失也要能跑)。
72
+ * 在这里手抄一份区域表会立刻与注册表漂移(正是本文件开头反复说的那类缺陷),所以选择**显式不适用**:
73
+ * 用户显式 `--sections pluginData` 时给一条可执行的提示(改用 GUI 导出/导入),而不是静默不收。
74
+ */
75
+ export const OFFLINE_EXCLUDED_SECTIONS = ['pluginData', 'pluginAssets'];
66
76
  /**
67
77
  * 离线**无法**收集的分区(需要 DSH Service 门面读值/权威脱敏)。仅用于报告与参数校验,
68
78
  * 绝不写进归档:这些分区的权威形态是 GUI 导出(经 DSH `describe({redactSecrets:true})`
@@ -74,7 +84,10 @@ export const OFFLINE_UNAVAILABLE_SECTIONS = [
74
84
  // 加载期自检(注册表派生,t30):三个离线清单里的 id 必须都是**已注册分区**。
75
85
  // 「离线可收集性」是注册表未建模的另一条轴(要脱离 DSH 读盘收集),故这三份清单仍是显式声明;
76
86
  // 但拼错 id / 分区改名过去会让该分区「静默永不收集、永不报告」,现在模块加载即失败。
77
- for (const id of [...DEFAULT_BACKUP_SECTIONS, ...OPT_IN_BACKUP_SECTIONS, ...OFFLINE_UNAVAILABLE_SECTIONS]) {
87
+ for (const id of [
88
+ ...DEFAULT_BACKUP_SECTIONS, ...OPT_IN_BACKUP_SECTIONS, ...OFFLINE_UNAVAILABLE_SECTIONS,
89
+ ...OFFLINE_EXCLUDED_SECTIONS,
90
+ ]) {
78
91
  requireSectionMeta(id);
79
92
  }
80
93
  /**
@@ -112,7 +125,7 @@ const SECTION_SPECS = {
112
125
  baseDir: '',
113
126
  files: ['dsh-ssh.json', 'pet.json'],
114
127
  dirs: ['plugin-config'],
115
- label: 'Plugin Files',
128
+ label: "Other Plugins' Loose Files",
116
129
  get risk() { return engineMsg('core.backupPlan.pluginFilesRisk'); },
117
130
  },
118
131
  // adapters/self.ts(baseDir='dsh-config-manager',SELF_CONFIG_FILES 白名单)
@@ -127,7 +140,7 @@ const SECTION_SPECS = {
127
140
  'dsh-config-manager/market/market-config.json',
128
141
  'dsh-config-manager/exports/.backup-notes.json',
129
142
  ],
130
- label: 'Plugin Self Config',
143
+ label: "This Plugin's Own Config",
131
144
  },
132
145
  };
133
146
  /**
@@ -188,6 +201,11 @@ export function parseSectionsArg(raw) {
188
201
  if (parts.length === 0) {
189
202
  return { ok: false, error: engineMsg('core.backupPlan.sectionsMissingValue') };
190
203
  }
204
+ // 显式请求「不支持离线」的分区 → 单独一条可执行提示(不得当成「不存在的分区」)
205
+ const notOffline = parts.filter((p) => OFFLINE_EXCLUDED_SECTIONS.includes(p));
206
+ if (notOffline.length > 0) {
207
+ return { ok: false, error: engineMsg('core.backupPlan.sectionNotOffline', { section: notOffline.join(', ') }) };
208
+ }
191
209
  const unknown = parts.filter((p) => !OFFLINE_BACKUP_SECTIONS.includes(p));
192
210
  if (unknown.length > 0) {
193
211
  return {
@@ -1,5 +1,5 @@
1
1
  import type { SectionId } from '../schema/types.ts';
2
- import type { ConfigAdapter, HostContext, ImportPlan, PlanItem, Snapshot, SnapshotStatus, SnapshotStore } from './types.ts';
2
+ import type { ConfigAdapter, FileSystemFacade, HostContext, ImportPlan, PlanItem, Snapshot, SnapshotStatus, SnapshotStore } from './types.ts';
3
3
  /**
4
4
  * 「会写目标」的**单点事实源**:该计划项是否会被引擎交给 `adapter.applyItem`
5
5
  * (即可能产生目标写入)。
@@ -38,6 +38,25 @@ export declare function planItemWritesTarget(item: PlanItem, exec?: PlanItemExec
38
38
  * 行 id 是任意字符串,无法靠形状判断它是不是一个路径。
39
39
  */
40
40
  export declare const PLUGIN_PATCH_REF_PREFIX = "patchFile:";
41
+ /**
42
+ * 以**系统 home 为根**的文件类分区(ref 是系统 home 相对路径)。
43
+ *
44
+ * 为什么单独一组:`FileSystemFacade` 被限制在 $DSH_HOME 内,而第三方插件的数据常在 $DSH_HOME 之外
45
+ * (`~/.dsh-wallpaper-engine/`、`~/.mnemon/` …)。这两个分区的路径解析与 I/O 门面都换根,
46
+ * 其余分区一字不动 —— 单根不变量只在**这两个分区**上被显式放宽,且仍然只允许白名单区域。
47
+ */
48
+ export declare const OS_HOME_FILE_SECTIONS: ReadonlySet<SectionId>;
49
+ /** 该分区文件目标的根(`dsh` = $DSH_HOME;`osHome` = 系统 home) */
50
+ export declare function sectionFileRoot(adapter: SectionId): 'dsh' | 'osHome';
51
+ /** 系统 home:宿主显式给就用它,否则进程 `homedir()`(`~` 的定义,不是猜) */
52
+ export declare function osHomeDirOf(ctx: HostContext): string;
53
+ /**
54
+ * 该分区文件 I/O 应使用的门面。
55
+ *
56
+ * `osHome` 分区必须用 `homeFs`:缺了它**显式抛错**,绝不退回 `ctx.fs` —— 那会把
57
+ * `~/.mnemon/x.db` 写成 `$DSH_HOME/.mnemon/x.db`(看起来成功、实际写错位置)。
58
+ */
59
+ export declare function fsForSection(ctx: HostContext, adapter: SectionId): FileSystemFacade;
41
60
  /** 解析文件类目标的绝对路径(引擎通用快照与回滚共用) */
42
61
  export declare function resolveFileTarget(ctx: HostContext, adapter: SectionId, ref: string): string;
43
62
  /**
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { engineMsg } from './messages-core.js';
9
9
  import fs from 'node:fs/promises';
10
+ import { homedir } from 'node:os';
10
11
  import path from 'node:path';
11
12
  import crypto from 'node:crypto';
12
13
  import { parseJsonSafe } from '../utils/json.js';
@@ -96,6 +97,10 @@ const FILE_BASES = {
96
97
  agentPresets: '.agent-presets',
97
98
  agentInstructions: '', // homeDir 根(~/.dsh/AGENTS.md)
98
99
  pluginFiles: '',
100
+ // 第三方插件数据:baseDir '' = home 根,ref 是**完整的 home 相对路径**
101
+ // (.dsh-wallpaper-engine/config.json / .mnemon/data/x.db)
102
+ pluginData: '',
103
+ pluginAssets: '',
99
104
  sessions: 'sessions',
100
105
  self: 'dsh-config-manager',
101
106
  };
@@ -118,8 +123,41 @@ function profileDirOf(ctx) {
118
123
  const profile = ctx.profile !== undefined && ctx.profile !== '' ? ctx.profile : 'web';
119
124
  return path.join(ctx.homeDir, 'profiles', profile);
120
125
  }
126
+ /**
127
+ * 以**系统 home 为根**的文件类分区(ref 是系统 home 相对路径)。
128
+ *
129
+ * 为什么单独一组:`FileSystemFacade` 被限制在 $DSH_HOME 内,而第三方插件的数据常在 $DSH_HOME 之外
130
+ * (`~/.dsh-wallpaper-engine/`、`~/.mnemon/` …)。这两个分区的路径解析与 I/O 门面都换根,
131
+ * 其余分区一字不动 —— 单根不变量只在**这两个分区**上被显式放宽,且仍然只允许白名单区域。
132
+ */
133
+ export const OS_HOME_FILE_SECTIONS = new Set(['pluginData', 'pluginAssets']);
134
+ /** 该分区文件目标的根(`dsh` = $DSH_HOME;`osHome` = 系统 home) */
135
+ export function sectionFileRoot(adapter) {
136
+ return OS_HOME_FILE_SECTIONS.has(adapter) ? 'osHome' : 'dsh';
137
+ }
138
+ /** 系统 home:宿主显式给就用它,否则进程 `homedir()`(`~` 的定义,不是猜) */
139
+ export function osHomeDirOf(ctx) {
140
+ return ctx.osHomeDir !== undefined && ctx.osHomeDir !== '' ? ctx.osHomeDir : homedir();
141
+ }
142
+ /**
143
+ * 该分区文件 I/O 应使用的门面。
144
+ *
145
+ * `osHome` 分区必须用 `homeFs`:缺了它**显式抛错**,绝不退回 `ctx.fs` —— 那会把
146
+ * `~/.mnemon/x.db` 写成 `$DSH_HOME/.mnemon/x.db`(看起来成功、实际写错位置)。
147
+ */
148
+ export function fsForSection(ctx, adapter) {
149
+ if (sectionFileRoot(adapter) === 'dsh')
150
+ return ctx.fs;
151
+ if (ctx.homeFs === undefined)
152
+ throw new Error(engineMsg('core.backup.osHomeUnavailable', { section: adapter }));
153
+ return ctx.homeFs;
154
+ }
121
155
  /** 解析文件类目标的绝对路径(引擎通用快照与回滚共用) */
122
156
  export function resolveFileTarget(ctx, adapter, ref) {
157
+ // 系统 home 根的分区:整体换根(plugins 的两个 profile 特例与它们互不相干)
158
+ if (sectionFileRoot(adapter) === 'osHome') {
159
+ return path.join(osHomeDirOf(ctx), FILE_BASES[adapter] ?? '', ref);
160
+ }
123
161
  // plugins 分区的 pnpm-workspace.yaml:位于 profiles/<profile>/ 下(非 FILE_BASES 静态基准)
124
162
  if (adapter === 'plugins' && ref === 'pnpm-workspace.yaml') {
125
163
  return path.join(profileDirOf(ctx), 'pnpm-workspace.yaml');
@@ -143,6 +181,9 @@ export function resolveFileTarget(ctx, adapter, ref) {
143
181
  * 指纹恒为 null,crash 后 reconcile 无法证明该项是否已应用(保守但可避免的精度损失)。
144
182
  */
145
183
  export function resolveFileTargetRel(adapter, ref, profile) {
184
+ // 系统 home 根的分区:ref 本身就是「根相对路径」,原样归一即可
185
+ if (sectionFileRoot(adapter) === 'osHome')
186
+ return normalizePath(ref);
146
187
  if (adapter === 'plugins') {
147
188
  const profileDir = `profiles/${profile !== undefined && profile !== '' ? profile : 'web'}`;
148
189
  if (ref === 'pnpm-workspace.yaml')
@@ -243,13 +284,17 @@ async function engineSnapshotEntry(ctx, target) {
243
284
  case 'agentPresets':
244
285
  case 'agentInstructions':
245
286
  case 'pluginFiles':
287
+ case 'pluginData':
288
+ case 'pluginAssets':
246
289
  case 'sessions':
247
290
  case 'self': {
291
+ // 门面按分区选:pluginData / pluginAssets 在系统 home 根(见 fsForSection 的说明)
292
+ const fsFacade = fsForSection(ctx, target.adapter);
248
293
  const abs = resolveFileTarget(ctx, target.adapter, target.ref);
249
- if (!(await ctx.fs.exists(abs))) {
294
+ if (!(await fsFacade.exists(abs))) {
250
295
  return { kind: 'file', adapter: target.adapter, ref: target.ref, before: null, existed: false };
251
296
  }
252
- const data = await ctx.fs.readFile(abs);
297
+ const data = await fsFacade.readFile(abs);
253
298
  // 文件字节不放进 SnapshotEntry(契约纯净),由 createSnapshot 收集进 blobs Map 统一落盘
254
299
  return {
255
300
  kind: 'file',
@@ -335,7 +380,7 @@ export async function createSnapshot(opts) {
335
380
  if (entry.kind === 'file' && entry.copiedTo && entry.existed) {
336
381
  const abs = resolveFileTarget(ctx, adapterId, target.ref);
337
382
  // 与改动前**完全相同的读调用**,只是延后到被消费时执行(路径语义与异常传播不变)
338
- lazyBlobSources.push({ blobPath: entry.copiedTo, read: () => ctx.fs.readFile(abs) });
383
+ lazyBlobSources.push({ blobPath: entry.copiedTo, read: () => fsForSection(ctx, adapterId).readFile(abs) });
339
384
  }
340
385
  entries.push(entry);
341
386
  }
@@ -10,8 +10,12 @@
10
10
  * **绝不绕过**(命令入口没有「确认按钮」,用户敲下 id 就是那次确认;其余门照旧全量生效)。
11
11
  *
12
12
  * 纪律(与 src/core/model-tools.ts 的 registerModelTools 同口径):
13
- * · 命令服务**惰取**:一律 ctx.get('commands') 调用期读取,绝不属性访问 ctx.commands、
14
- * 绝不放进 inject —— 未组合该服务的部署必须照常启动(命令是可选能力,不是硬依赖);
13
+ * · 命令服务**惰取**:一律 ctx.get('commands'),绝不属性访问 ctx.commands、绝不放进 inject ——
14
+ * 未组合该服务的部署必须照常启动(命令是可选能力,不是硬依赖);
15
+ * **读的时机是装配期一次**(见 registerRollbackCommand):Cordis 的 ctx.get 在 apply 期只能拿到
16
+ * **已 apply** 的服务,所以 `commands` 行晚于本插件时 `/rollback` 不会注册(只 warn 一行、无重试入口)。
17
+ * DSH 内置插件与本插件的实际 apply 顺序**未坐实**(asar 不可直接读、本机 profile 无相关行);
18
+ * 要彻底消除这个窗口需要 `ctx.inject(['commands'])` 式的「服务就绪再注册」回调 —— 未做,如实记在这里。
15
19
  * · 注册失败只 warn:命令挂不上不得影响其它装配(宿主启动 = blocker);
16
20
  * · 用户可见文案一律走宿主字典 engineMsg(key: command.rollback.*),**取文案那一刻**解析语言;
17
21
  * · 本模块不 import @deepseek-ai/dsh-commands 的运行时值(只做结构化窄化),core 保持与 DSH 解耦。
@@ -10,8 +10,12 @@
10
10
  * **绝不绕过**(命令入口没有「确认按钮」,用户敲下 id 就是那次确认;其余门照旧全量生效)。
11
11
  *
12
12
  * 纪律(与 src/core/model-tools.ts 的 registerModelTools 同口径):
13
- * · 命令服务**惰取**:一律 ctx.get('commands') 调用期读取,绝不属性访问 ctx.commands、
14
- * 绝不放进 inject —— 未组合该服务的部署必须照常启动(命令是可选能力,不是硬依赖);
13
+ * · 命令服务**惰取**:一律 ctx.get('commands'),绝不属性访问 ctx.commands、绝不放进 inject ——
14
+ * 未组合该服务的部署必须照常启动(命令是可选能力,不是硬依赖);
15
+ * **读的时机是装配期一次**(见 registerRollbackCommand):Cordis 的 ctx.get 在 apply 期只能拿到
16
+ * **已 apply** 的服务,所以 `commands` 行晚于本插件时 `/rollback` 不会注册(只 warn 一行、无重试入口)。
17
+ * DSH 内置插件与本插件的实际 apply 顺序**未坐实**(asar 不可直接读、本机 profile 无相关行);
18
+ * 要彻底消除这个窗口需要 `ctx.inject(['commands'])` 式的「服务就绪再注册」回调 —— 未做,如实记在这里。
15
19
  * · 注册失败只 warn:命令挂不上不得影响其它装配(宿主启动 = blocker);
16
20
  * · 用户可见文案一律走宿主字典 engineMsg(key: command.rollback.*),**取文案那一刻**解析语言;
17
21
  * · 本模块不 import @deepseek-ai/dsh-commands 的运行时值(只做结构化窄化),core 保持与 DSH 解耦。
@@ -200,7 +200,9 @@ export declare const zh: {
200
200
  'core.backupPlan.depDirSkipped': string;
201
201
  'core.backupPlan.followedLinks': string;
202
202
  'core.backupPlan.sectionsMissingValue': string;
203
+ 'core.backup.osHomeUnavailable': string;
203
204
  'core.backupPlan.unknownSections': string;
205
+ 'core.backupPlan.sectionNotOffline': string;
204
206
  'core.backupPlan.unsafeEntryName': string;
205
207
  'core.backupPlan.statFailed': string;
206
208
  'core.backupPlan.readFailed': string;
@@ -220,7 +220,9 @@ export const zh = {
220
220
  'core.backupPlan.depDirSkipped': '运行时依赖目录未进备份(可重建,目标机需重新安装依赖)/ runtime dependency directory NOT in this backup (rebuildable; reinstall on the target): {path}({files})',
221
221
  'core.backupPlan.followedLinks': '已跟随 {count} 个链接目录收集内容 / followed {count} linked director(ies)',
222
222
  'core.backupPlan.sectionsMissingValue': '`--sections` 缺值 / missing value for `--sections`',
223
+ 'core.backup.osHomeUnavailable': '分区 {section} 的数据在系统主目录下,但宿主未提供该目录的文件门面(旧版本宿主);本次不读也不写 / section {section} stores data under the system home directory but the host provides no file facade for it (older host); nothing is read or written this time',
223
224
  'core.backupPlan.unknownSections': '未知分区 / unknown section(s): {unknown}(可用 / available: {available})',
225
+ 'core.backupPlan.sectionNotOffline': '分区 {section} 不支持离线 CLI 备份(它的收集依赖插件来源注册表与本机插件清单),请用 GUI 的导出/导入迁移它 / section {section} is not supported by the offline CLI backup (its collection depends on the plugin-data source registry and the local plugin manifests); use the GUI export/import instead',
224
226
  'core.backupPlan.unsafeEntryName': '跳过不安全条目名 / unsafe entry name skipped: {name}',
225
227
  'core.backupPlan.statFailed': '探测失败 / stat failed: {path}({reason})',
226
228
  'core.backupPlan.readFailed': '读取失败 / read failed: {path}({reason})',
@@ -512,7 +514,9 @@ export const en = {
512
514
  'core.backupPlan.depDirSkipped': 'runtime dependency directory NOT in this backup (rebuildable; reinstall dependencies on the target): {path} ({files})',
513
515
  'core.backupPlan.followedLinks': 'followed {count} linked director(ies) to collect content',
514
516
  'core.backupPlan.sectionsMissingValue': 'missing value for `--sections`',
517
+ 'core.backup.osHomeUnavailable': 'section {section} stores data under the system home directory but the host provides no file facade for it (older host); nothing is read or written this time',
515
518
  'core.backupPlan.unknownSections': 'unknown section(s): {unknown} (available: {available})',
519
+ 'core.backupPlan.sectionNotOffline': 'section {section} is not supported by the offline CLI backup (its collection depends on the plugin-data source registry and the local plugin manifests); use the GUI export/import instead',
516
520
  'core.backupPlan.unsafeEntryName': 'unsafe entry name skipped: {name}',
517
521
  'core.backupPlan.statFailed': 'stat failed: {path} ({reason})',
518
522
  'core.backupPlan.readFailed': 'read failed: {path} ({reason})',
@@ -333,6 +333,7 @@ export declare const zh: {
333
333
  readonly 'sync.credentialsReadFailed': '读取凭据文件失败,本次快照不含任何凭据值: {reason}';
334
334
  readonly 'sync.credentialsEmpty': '本机凭据文件为空或无法解析,本次快照不含任何凭据值';
335
335
  readonly 'sync.credentialsItemDesc': '凭据 {ref} 随加密快照迁移(确认后写入本机凭据存储)';
336
+ readonly 'sync.credentialsItemSame': '凭据 {ref} 与本机一致,跳过写入(保留本机值)';
336
337
  readonly 'sync.credentialsNeedPassword': '快照 {id} 含加密凭据载荷,需要解密密码才能读取';
337
338
  readonly 'sync.encryptedNeedsPassword': '快照 {id} 已加密,需要解密密码才能读取(自动同步无法解密,请手动同步)';
338
339
  readonly 'sync.noPortableSections': '没有可同步的 portable 分区(全部导出失败)';
@@ -430,7 +431,7 @@ export declare const zh: {
430
431
  readonly 'command.rollback.restorableYes': '可回退';
431
432
  readonly 'command.rollback.restorableNo': '不可回退';
432
433
  readonly 'command.rollback.restorableSummary': '{parts} ⇒ {verdict}';
433
- readonly 'command.rollback.rewindRestored': '已回退到 {id}。{detail}';
434
+ readonly 'command.rollback.rewindRestored': '已回退到 {id}。{detail}当前对话不会切换:回退出的新会话在左侧栏(未分组时排在列表末尾),打开它继续聊。';
434
435
  readonly 'command.rollback.rewindPartial': '回退只完成了一部分(会话段已 fork,工作区段见下)——{id}:{segments}。{detail}';
435
436
  readonly 'command.rollback.segmentFacts': '段:{facts}';
436
437
  readonly 'command.rollback.rewindFailed': '回退失败({code}):{id}。{detail}';
@@ -327,6 +327,7 @@ export const zh = {
327
327
  'sync.credentialsReadFailed': '读取凭据文件失败,本次快照不含任何凭据值: {reason}',
328
328
  'sync.credentialsEmpty': '本机凭据文件为空或无法解析,本次快照不含任何凭据值',
329
329
  'sync.credentialsItemDesc': '凭据 {ref} 随加密快照迁移(确认后写入本机凭据存储)',
330
+ 'sync.credentialsItemSame': '凭据 {ref} 与本机一致,跳过写入(保留本机值)',
330
331
  'sync.credentialsNeedPassword': '快照 {id} 含加密凭据载荷,需要解密密码才能读取',
331
332
  'sync.encryptedNeedsPassword': '快照 {id} 已加密,需要解密密码才能读取(自动同步无法解密,请手动同步)',
332
333
  'sync.noPortableSections': '没有可同步的 portable 分区(全部导出失败)',
@@ -429,7 +430,7 @@ export const zh = {
429
430
  'command.rollback.restorableYes': '可回退',
430
431
  'command.rollback.restorableNo': '不可回退',
431
432
  'command.rollback.restorableSummary': '{parts} ⇒ {verdict}',
432
- 'command.rollback.rewindRestored': '已回退到 {id}。{detail}',
433
+ 'command.rollback.rewindRestored': '已回退到 {id}。{detail}当前对话不会切换:回退出的新会话在左侧栏(未分组时排在列表末尾),打开它继续聊。',
433
434
  'command.rollback.rewindPartial': '回退只完成了一部分(会话段已 fork,工作区段见下)——{id}:{segments}。{detail}',
434
435
  'command.rollback.segmentFacts': '段:{facts}',
435
436
  'command.rollback.rewindFailed': '回退失败({code}):{id}。{detail}',
@@ -763,6 +764,7 @@ export const en = {
763
764
  'sync.credentialsReadFailed': 'Failed to read the credentials file; this snapshot carries no credential value: {reason}',
764
765
  'sync.credentialsEmpty': 'The local credentials file is empty or unparseable; this snapshot carries no credential value',
765
766
  'sync.credentialsItemDesc': 'Credential {ref} travels with the encrypted snapshot (written to the local credential store once confirmed)',
767
+ 'sync.credentialsItemSame': 'Credential {ref} already matches this machine; write skipped (local value kept)',
766
768
  'sync.credentialsNeedPassword': 'Snapshot {id} carries an encrypted credentials payload; a decryption password is required',
767
769
  'sync.encryptedNeedsPassword': 'Snapshot {id} is encrypted; a decryption password is required (auto sync cannot decrypt — sync manually)',
768
770
  'sync.noPortableSections': 'No portable sections to sync (all exports failed)',
@@ -864,7 +866,7 @@ export const en = {
864
866
  'command.rollback.restorableYes': 'restorable',
865
867
  'command.rollback.restorableNo': 'not restorable',
866
868
  'command.rollback.restorableSummary': '{parts} => {verdict}',
867
- 'command.rollback.rewindRestored': 'Rewound to {id}. {detail}',
869
+ 'command.rollback.rewindRestored': 'Rewound to {id}. {detail}This conversation is not switched: the new session is in the left sidebar (at the end of the list when ungrouped) — open it to continue.',
868
870
  'command.rollback.rewindPartial': 'The rewind only completed partially (session segment forked; see workspace segment below) - {id}: {segments}. {detail}',
869
871
  'command.rollback.segmentFacts': 'segments: {facts}',
870
872
  'command.rollback.rewindFailed': 'Rewind failed ({code}): {id}. {detail}',
@@ -0,0 +1,124 @@
1
+ /**
2
+ * 第三方插件「数据目录」来源注册表(pluginData / pluginAssets 两个分区的唯一知识源)。
3
+ *
4
+ * 为什么需要它:DSH 插件的数据目录不由宿主统一提供(2026-10-14 核对 dsh-config-manager /
5
+ * dsh-mnemon / dsh-better-sidebar 的 package.json,均无任何「数据目录」声明字段),而现实里它们
6
+ * 散落在 ~/.dsh-<name>/、~/.<name>/、~/.dsh/<name>/ 三种形态 —— 既有的 pluginFiles 分区只认
7
+ * 「相对 ~/.dsh 根的白名单文件」,够不到其中任何一类。
8
+ *
9
+ * 本模块只做三件事,全部是纯函数 / 静态数据(零 I/O、零 node 内置依赖):
10
+ * ① 冻结来源与「区域(area)」的形状;
11
+ * ② 内置已取证来源(每条如实标注取证强度,与 src/foreign/types.ts 的 ForeignEvidenceKind 同款纪律);
12
+ * ③ 提供「路径 → 区域」的最长前缀匹配与 dsh.dataDirs 自述声明的解析。
13
+ *
14
+ * 三条不变量:
15
+ * - 不猜:路径一律来自插件自己的 README 明示位置或本机实测;不确定的来源宁可不收。
16
+ * - 缓存永不收录:role === 'cache' 的区域只用于报告(「我知道它在那儿,但不带」),绝不进包 ——
17
+ * 缓存是可再生产物,带出去只会撑爆包体。
18
+ * - 环境变量覆盖即跳过且不读值:区域被环境变量覆盖时,绝不去读那个变量指向的目录(目标机的
19
+ * 同名变量可能指向完全不同的盘符,按 home 相对路径写进包,换机后就落到「看起来对」的错误位置)。
20
+ */
21
+ import type { SectionId } from '../schema/types.ts';
22
+ /** 第三方插件数据分区(两个:配置类与资产类) */
23
+ export type PluginDataSection = Extract<SectionId, 'pluginData' | 'pluginAssets'>;
24
+ /**
25
+ * 区域角色 —— 决定它进哪个分区:
26
+ * - config:配置与轻量状态(进 pluginData);
27
+ * - assets:大体积用户数据(上传的壁纸、本地数据库;进 pluginAssets);
28
+ * - cache:可再生产物,两个分区都不进,只出现在报告里。
29
+ */
30
+ export type PluginDataRole = 'config' | 'assets' | 'cache';
31
+ /** 取证强度(字段必填:未取证的来源不可能被悄悄列出来) */
32
+ export type PluginDataEvidence =
33
+ /** 本机实测:目录确实存在,且据此实现 */
34
+ 'measured'
35
+ /** 有可复现取证方式:夹具 + 单测端到端跑同一份布局 */
36
+ | 'fixture'
37
+ /** 只有插件自己的文档(README 明示的位置/环境变量) */
38
+ | 'documented';
39
+ /** 一个数据区域:home 相对 POSIX 路径(文件或目录同形,按磁盘实际形态判定) */
40
+ export interface PluginDataArea {
41
+ readonly relPath: string;
42
+ readonly role: PluginDataRole;
43
+ /**
44
+ * 会整体搬走本区域的环境变量名(任一存在即跳过本区域并报码)。
45
+ *
46
+ * 必须把「祖先区域的环境变量」也列进来:wallpaper-engine 的 DSH_WE_DATA_DIR 搬走的是整个
47
+ * ~/.dsh-wallpaper-engine,只列 DSH_WE_UPLOAD_DIR 会让 uploads 在数据目录已被搬走时仍被当成
48
+ * 「默认位置」去读(读到空目录 → 用户以为「没有上传的壁纸」)。
49
+ */
50
+ readonly envOverrides?: readonly string[];
51
+ /** 仅目录区域:永不收集的**区域相对**子路径(POSIX 前缀),如 cache / bin */
52
+ readonly excludeRel?: readonly string[];
53
+ }
54
+ /** 一个第三方插件来源 */
55
+ export interface PluginDataSource {
56
+ /** 稳定机器 id(进单元 id 与报告;不随文案变化) */
57
+ readonly id: string;
58
+ /** 该来源对应的 npm 包名(自述声明的去重键) */
59
+ readonly packages: readonly string[];
60
+ readonly evidence: PluginDataEvidence;
61
+ readonly areas: readonly PluginDataArea[];
62
+ }
63
+ /**
64
+ * 内置来源 —— 只放已取证的。
65
+ *
66
+ * 收录口径(2026-10-14 调研:按 GitHub star + npm 周下载量排序后逐一核对):
67
+ * - 有自有数据目录(不在 DSH 三个核心存储里,也不在 profile 树里);
68
+ * - 目录里不含凭据(~/.commandcode/auth.json、~/.dsh/agy-accounts 这类命中即整来源不收录 ——
69
+ * 文件类分区的内容不进 SecretScanner 的剥离路径,带出去就是明文);
70
+ * - 位置由插件自己的 README 明示,或本机实测。
71
+ *
72
+ * 未收录但确实有数据目录的插件(如实登记,避免「没列出来 = 不存在」的误读):
73
+ * - dsh-better-sidebar(4175 star):状态全在 DSH settings/storages,已被既有分区覆盖;
74
+ * - @mars-sea/dsh-commandcode-provider(389 star):~/.commandcode/ 含 auth.json(凭据);
75
+ * - dsh-agy-link(111 star):~/.dsh/agy-accounts 为账号池(凭据)。
76
+ */
77
+ export declare const BUILTIN_PLUGIN_DATA_SOURCES: readonly PluginDataSource[];
78
+ /** 角色 → 分区(cache 不属于任何分区) */
79
+ export declare function sectionOfRole(role: PluginDataRole): PluginDataSection | null;
80
+ /** 该分区要收集的区域清单(保序:来源顺序 → 区域内顺序) */
81
+ export declare function areasForSection(sources: readonly PluginDataSource[], section: PluginDataSection): readonly {
82
+ source: PluginDataSource;
83
+ area: PluginDataArea;
84
+ }[];
85
+ /**
86
+ * home 相对路径归一化 + 安全判定。
87
+ *
88
+ * 只接受 POSIX 相对路径:拒绝绝对路径(含 Windows 盘符写法 C:/x)、..、反斜杠、空段、
89
+ * 以 / 结尾、含 NUL 的值。
90
+ * 返回 null = 调用方必须报错/跳过,绝不「尽力而为」地拼一个路径出来。
91
+ */
92
+ export declare function normalizeHomeRel(value: unknown): string | null;
93
+ /**
94
+ * 目录区域的排除判定(issue:**excludeRel 是区域相对路径,rel 是 home 相对路径**)。
95
+ *
96
+ * 必须先把 rel 折成区域相对再比:`excludeRel: ['uploads']` 配 `.dsh-wallpaper-engine` 区域时,
97
+ * 真正的判别对象是 `uploads/a.mp4`(区域相对),不是 `.dsh-wallpaper-engine/uploads/a.mp4`(home 相对)——
98
+ * 直接拿 home 相对路径去 startsWith,排除项永远不命中,缓存与大资产会悄悄跟着配置分区一起进包。
99
+ */
100
+ export declare function isExcludedRel(area: PluginDataArea, rel: string): boolean;
101
+ /** rel 是否属于该区域(文件区域 = 精确相等;目录区域 = 精确相等或前缀) */
102
+ export declare function relInArea(area: PluginDataArea, rel: string): boolean;
103
+ /**
104
+ * 路径 → 区域(最长前缀优先)。
105
+ *
106
+ * 为什么必须最长优先:.dsh-wallpaper-engine 与 .dsh-wallpaper-engine/uploads 同时是区域,
107
+ * 短前缀先匹配会把上传的壁纸算进「配置」分区(用户取消勾选大资产也照样带走几百 MB)。
108
+ */
109
+ export declare function areaOfPath(entries: readonly {
110
+ source: PluginDataSource;
111
+ area: PluginDataArea;
112
+ }[], rel: string): {
113
+ source: PluginDataSource;
114
+ area: PluginDataArea;
115
+ } | null;
116
+ /**
117
+ * 解析一个已安装插件 package.json 里的自述声明(dsh.dataDirs)—— 「让别的插件自己适配」的钩子。
118
+ *
119
+ * 契约(对外文档见 docs/spec/plugin-data-declaration.md):
120
+ * { "dsh": { "dataDirs": [ { "path": ".my-plugin", "role": "config", "env": ["MY_PLUGIN_DIR"] } ] } }
121
+ *
122
+ * 形状不认识就整条丢弃(返回 null),绝不猜;path 非法(绝对路径 / .. / 空段)同样丢弃。
123
+ */
124
+ export declare function parseDeclaredDataDirs(packageName: string, manifest: unknown): PluginDataSource | null;