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
@@ -0,0 +1,309 @@
1
+ /**
2
+ * pluginData / pluginAssets 分区 adapter(第三方插件数据迁移)。
3
+ *
4
+ * 数据来源 = \`src/core/plugin-data-registry.ts\` 的来源注册表:内置已取证来源 + 已安装插件在
5
+ * 自己 package.json 里声明的 \`dsh.dataDirs\`。每个「区域(area)」在备份里是**一个可勾选单元**,
6
+ * 单元 id = 区域的 home 相对 POSIX 路径(如 \`.dsh-wallpaper-engine/uploads\`)——
7
+ * 用户能在导出选择器里逐区域看到「几个文件 / 多大」,也能只带走配置、不带几百 MB 的上传壁纸。
8
+ *
9
+ * 为什么两个分区而不是一个:
10
+ * - \`pluginData\` = 配置与轻量状态;\`pluginAssets\` = 大体积用户数据(上传的壁纸、本地数据库);
11
+ * - 「默认不勾选大资产」在既有 UI 选择模型里**只有分区级开关**这一条可靠通道(单元级只有
12
+ * 「默认全选 + 稀疏排除」,没有「默认排除」的概念)—— 拆成两个分区后,「可见但默认不勾选」
13
+ * 就是既有的 \`defaultIncluded=false\` 语义,不需要动共享选择模型,也不会与导入/市场两处选择语义打架。
14
+ *
15
+ * 实现:继承 FileCollectionAdapter(baseDir = '',即 home 根;与 agentInstructions 同一形态),
16
+ * 复用它的单元分组 / 单分区字节闸门 / 预览-导出一致性 / 幂等哈希比对;只覆写三处:
17
+ * ① \`listRelPaths()\` —— 清单来自来源注册表(并如实记录被环境变量覆盖而跳过的区域);
18
+ * ② \`unitIdOf()\` —— 单元 = 区域(最长前缀归属);
19
+ * ③ \`analyzeImport() / applyItem()\` —— **写入路径白名单**(见下)。
20
+ *
21
+ * 安全不变量(本分区是全仓唯一「按 home 相对路径任意写」的分区,必须自己守住):
22
+ * - **导入白名单**:只有「本机内置来源的区域」与「本机已安装插件声明的区域」之下的路径才允许写。
23
+ * 恶意/损坏的包想把 \`.credentials.yaml\` 或 \`AGENTS.md\` 塞进来,在这里被判 Error(计划里可见,
24
+ * 执行期不写);applyItem 再查一遍(纵深防御),内层还有基类的 control-plane namespace 拒绝。
25
+ * - **缓存永不收录**:role === 'cache' 的区域不属于任何分区,导出时根本不进清单。
26
+ * - **环境变量覆盖即跳过**:区域声明的 env 存在且解析结果与默认位置不同 → 报码跳过,绝不猜新位置。
27
+ * - **不产生用户可见散文**:告警与计划项文案一律走消息目录(i18n 铁律)。
28
+ */
29
+ import { normalizePath } from '../utils/paths.js';
30
+ import { engineMsg } from './messages.js';
31
+ import { listFilesDetailed } from './link-report.js';
32
+ import { sectionMeta } from '../schema/section-registry.js';
33
+ import { BUILTIN_PLUGIN_DATA_SOURCES, areaOfPath, areasForSection, isExcludedRel, normalizeHomeRel, parseDeclaredDataDirs, } from '../core/plugin-data-registry.js';
34
+ import { osHomeDirOf } from '../core/backup.js';
35
+ import { FileCollectionAdapter } from './file-collection.js';
36
+ /** 声明来源缓存的 TTL(毫秒):一次导出/导入/执行里会多次解析,逐次读 15+ 个 package.json 是净开销 */
37
+ const DECLARED_TTL_MS = 5_000;
38
+ /** 单次解析里最多读取多少个已安装插件的 manifest(防御性上限:profile 依赖数量异常时不无限读) */
39
+ const MAX_DECLARED_PACKAGES = 300;
40
+ export class PluginDataAdapter extends FileCollectionAdapter {
41
+ id;
42
+ displayName;
43
+ defaultIncluded = false;
44
+ portability = 'deviceSpecific';
45
+ /** home 根(相对路径 = home 相对路径,与 agentInstructions 同形) */
46
+ baseDir = '';
47
+ builtin;
48
+ declaredTtlMs;
49
+ env;
50
+ /**
51
+ * 自述声明缓存。**必须带 ctx 键**(homeDir + profile):这台机器装了某插件,不代表另一台装了 ——
52
+ * 缓存若按实例共享,导入到「没装该插件」的目标机时会拿源机的声明去放行写入,
53
+ * 白名单就变成了「谁先导入谁说了算」。
54
+ */
55
+ declaredMemo;
56
+ /** 本轮清单(listRelPaths 写入;unitIdOf 是同步纯函数,只能从这里拿区域表) */
57
+ active;
58
+ /** 本轮告警(环境变量覆盖跳过等;export/preview 收尾合并进返回的 warnings) */
59
+ pendingWarnings = [];
60
+ constructor(section, options = {}) {
61
+ super();
62
+ this.id = section;
63
+ this.displayName = sectionMeta(section).displayName;
64
+ this.builtin = options.sources ?? BUILTIN_PLUGIN_DATA_SOURCES;
65
+ this.declaredTtlMs = options.declaredTtlMs ?? DECLARED_TTL_MS;
66
+ this.env = options.env ?? process.env;
67
+ }
68
+ /** 本分区可收集区域(内置 + 自述声明) */
69
+ async allowedEntries(ctx) {
70
+ const sources = await this.allSources(ctx);
71
+ return areasForSection(sources, this.id);
72
+ }
73
+ async allSources(ctx) {
74
+ const declared = await this.declaredSources(ctx);
75
+ return [...this.builtin, ...declared];
76
+ }
77
+ /**
78
+ * 已安装插件的自述声明(dsh.dataDirs)。
79
+ *
80
+ * 为什么读 profile 的 node_modules 而不是全局目录:DSH 的插件装在 \`profiles/<profile>/node_modules\`
81
+ * 下(pnpm 隔离布局),且同一个包在不同 profile 可以是不同版本 —— 声明必须取**本 profile 实际装的**那份。
82
+ *
83
+ * 读不到(未安装 / 无声明 / 形状非法)一律跳过:本机制是**附加**信息,绝不因为一个包读不出来
84
+ * 就影响内置来源或整次导出。
85
+ */
86
+ async declaredSources(ctx) {
87
+ const now = Date.now();
88
+ const profile = ctx.profile !== undefined && ctx.profile !== '' ? ctx.profile : 'web';
89
+ const memoKey = ctx.homeDir + '|' + profile;
90
+ if (this.declaredMemo !== undefined
91
+ && this.declaredMemo.key === memoKey
92
+ && now - this.declaredMemo.at < this.declaredTtlMs) {
93
+ return this.declaredMemo.sources;
94
+ }
95
+ const base = 'profiles/' + profile;
96
+ const out = [];
97
+ let deps = [];
98
+ try {
99
+ const raw = await ctx.fs.readFile(base + '/package.json');
100
+ const parsed = JSON.parse(new TextDecoder().decode(raw));
101
+ const dep = parsed?.dependencies;
102
+ if (dep !== null && typeof dep === 'object')
103
+ deps = Object.keys(dep).slice(0, MAX_DECLARED_PACKAGES);
104
+ }
105
+ catch {
106
+ deps = [];
107
+ }
108
+ for (const name of deps) {
109
+ try {
110
+ const raw = await ctx.fs.readFile(base + '/node_modules/' + name + '/package.json');
111
+ const manifest = JSON.parse(new TextDecoder().decode(raw));
112
+ const source = parseDeclaredDataDirs(name, manifest);
113
+ if (source !== null)
114
+ out.push(source);
115
+ }
116
+ catch {
117
+ // 未安装 / 无 manifest → 无声明,跳过
118
+ }
119
+ }
120
+ this.declaredMemo = { key: memoKey, at: now, sources: out };
121
+ return out;
122
+ }
123
+ /**
124
+ * 区域是否被环境变量覆盖(覆盖 → 返回变量名;未覆盖 / 未声明 → null)。
125
+ *
126
+ * 判定口径:变量**存在且解析结果与默认位置不同**才算覆盖 —— 仅仅 \`DSH_WE_DATA_DIR=~/.dsh-wallpaper-engine\`
127
+ * 这种「设成同一个值」的写法不该让区域凭空消失(那会让用户以为「没有数据」)。
128
+ * 解析只做 \`~\` 展开与分隔符归一(不做 realpath:不读盘、不猜)。
129
+ */
130
+ envOverrideOf(ctx, area) {
131
+ for (const name of area.envOverrides ?? []) {
132
+ const raw = this.env[name];
133
+ if (raw === undefined || raw.trim() === '')
134
+ continue;
135
+ const normalized = raw.trim().replace(/\\/g, '/').replace(/\/+$/, '');
136
+ // 默认位置以**系统 home** 为根(本分区的根;见 core/backup.ts 的 OS_HOME_FILE_SECTIONS)
137
+ const home = osHomeDirOf(ctx).replace(/\\/g, '/').replace(/\/+$/, '');
138
+ const expanded = normalized.startsWith('~/') ? home + normalized.slice(1) : normalized;
139
+ const defaultAbs = home + '/' + area.relPath;
140
+ if (expanded !== defaultAbs)
141
+ return name;
142
+ }
143
+ return null;
144
+ }
145
+ /**
146
+ * 区域是否是**普通文件**(而不是目录 / 不存在)。
147
+ *
148
+ * 判据顺序为什么是这样:`statSize` 对「非普通文件」与「不存在」都返回 null,无法单独区分
149
+ * 目录与缺失;所以先判「是文件」,其余一律按目录走遍历 —— 目录不存在时遍历返回空,
150
+ * 结果同样是「没有内容」,不会把缺失误报成文件,也不会把目录误读成空内容而静默丢数据。
151
+ * 门面未实现 statSize(旧宿主)时退回读一次内容:读得出来就是文件。
152
+ */
153
+ async isRegularFile(fsFacade, rel) {
154
+ if (fsFacade.statSize !== undefined) {
155
+ try {
156
+ return (await fsFacade.statSize(rel)) !== null;
157
+ }
158
+ catch {
159
+ return false;
160
+ }
161
+ }
162
+ try {
163
+ await fsFacade.readFile(rel);
164
+ return true;
165
+ }
166
+ catch {
167
+ return false;
168
+ }
169
+ }
170
+ static emptyListing() {
171
+ return { paths: [], skippedLinks: [], skippedDepDirs: [], followedLinks: 0, unreadableDirs: [] };
172
+ }
173
+ /**
174
+ * 清单枚举 = 来源注册表的区域(覆写基类的目录递归;baseDir='' 时基类实现会递归**整个 home**)。
175
+ *
176
+ * **必须覆写**:home 根上递归会遍历 profiles/ sessions/ attachments/ 等全部内容(本机实测
177
+ * 4000+ 文件 / 数百 MB),既慢又会把无关目录当成该分区的「可勾选单元」。
178
+ */
179
+ async listRelPaths(ctx) {
180
+ // 本分区的根是**系统 home**(~/.dsh-wallpaper-engine、~/.mnemon …),不是 $DSH_HOME ——
181
+ // 内容 I/O 一律走 homeFs;宿主没提供就什么都不收集,并如实报码(绝不退回 $DSH_HOME 去读错位置)。
182
+ const homeFs = ctx.homeFs;
183
+ if (homeFs === undefined) {
184
+ this.active = [];
185
+ this.pendingWarnings = [engineMsg('adapter.pluginDataNoOsHome')];
186
+ return PluginDataAdapter.emptyListing();
187
+ }
188
+ const entries = await this.allowedEntries(ctx);
189
+ this.active = entries;
190
+ const warnings = [];
191
+ const paths = [];
192
+ const listing = PluginDataAdapter.emptyListing();
193
+ for (const entry of entries) {
194
+ const rel = entry.area.relPath;
195
+ const env = this.envOverrideOf(ctx, entry.area);
196
+ if (env !== null) {
197
+ warnings.push(engineMsg('adapter.pluginDataEnvOverridden', { path: rel, env }));
198
+ continue;
199
+ }
200
+ if (await this.isRegularFile(homeFs, rel)) {
201
+ paths.push(rel);
202
+ continue;
203
+ }
204
+ let sub;
205
+ try {
206
+ sub = await listFilesDetailed(homeFs, rel);
207
+ }
208
+ catch {
209
+ // 目录不存在 / 读不到 → 视为无内容(存在的空目录与缺失在这里同义:都没有文件可带)
210
+ continue;
211
+ }
212
+ listing.skippedLinks.push(...sub.skippedLinks);
213
+ listing.skippedDepDirs.push(...sub.skippedDepDirs);
214
+ listing.unreadableDirs.push(...sub.unreadableDirs);
215
+ listing.followedLinks += sub.followedLinks;
216
+ for (const p of sub.paths) {
217
+ if (isExcludedRel(entry.area, p))
218
+ continue;
219
+ paths.push(p);
220
+ }
221
+ }
222
+ listing.paths = paths;
223
+ this.pendingWarnings = warnings;
224
+ return listing;
225
+ }
226
+ /**
227
+ * 单元 = 区域(最长前缀归属)。
228
+ *
229
+ * 用「本轮清单写入的区域表」而不是重新解析:listRelPaths 与本函数在同一次 collect 里成对调用,
230
+ * 重算会让两次解析结果不一致时出现「单元 id 不在清单里」(选择器勾不上)。测试/异常路径下
231
+ * \`active\` 可能是 undefined(例如直接调 listUnits),此时退回**内置来源**的最长前缀 —— 与清单口径同源。
232
+ */
233
+ unitIdOf(relativePath) {
234
+ const entries = this.active ?? areasForSection(this.builtin, this.id);
235
+ const hit = areaOfPath(entries, normalizePath(relativePath));
236
+ return hit === null ? normalizePath(relativePath) : hit.area.relPath;
237
+ }
238
+ /**
239
+ * 读一个成员文件:**换根到系统 home 门面**(基类的其余逻辑 —— 体积、哈希、字节闸门 —— 一字不动)。
240
+ *
241
+ * 能走到这里说明 listRelPaths 产出过条目(缺门面时它不产出任何路径),因此缺门面即**抛错**,
242
+ * 不再造一个「空文件」出来(那会把缺通道伪装成空内容)。
243
+ */
244
+ async readMember(ctx, rel, mode, statTimes) {
245
+ const homeFs = ctx.homeFs;
246
+ if (homeFs === undefined)
247
+ throw new Error(engineMsg('adapter.pluginDataNoOsHome'));
248
+ return super.readMember({ ...ctx, fs: homeFs }, rel, mode, statTimes);
249
+ }
250
+ async export(ctx, options) {
251
+ this.pendingWarnings = [];
252
+ const out = await super.export(ctx, options);
253
+ return { ...out, warnings: [...out.warnings, ...this.pendingWarnings] };
254
+ }
255
+ async preview(ctx, options) {
256
+ this.pendingWarnings = [];
257
+ const out = await super.preview(ctx, options);
258
+ return {
259
+ ...out,
260
+ section: { ...out.section, warnings: [...out.section.warnings, ...this.pendingWarnings] },
261
+ };
262
+ }
263
+ /**
264
+ * 导入分析 + **写入路径白名单**(本分区唯一的越界防线)。
265
+ *
266
+ * 判据:路径必须是本机可解析的某个区域(内置来源,或已安装插件的自述声明)之下,且未被该区域
267
+ * 的 excludeRel 排除。不满足 → Error(计划里可见、执行期不写),绝不「尽力而为」地写出去。
268
+ */
269
+ async analyzeImport(data, ctx) {
270
+ // 宿主没有系统 home 门面(旧宿主)→ 白名单为空 + 目标读也换不了根:**全部拒绝**并说明原因。
271
+ // 绝不退回 $DSH_HOME 的 ctx.fs 去比对/写入(那会落到错误位置)。
272
+ const homeFs = ctx.target.homeFs;
273
+ const entries = homeFs === undefined ? [] : await this.allowedEntries(ctx.target);
274
+ const items = await super.analyzeImport(data, homeFs === undefined ? ctx : { ...ctx, target: { ...ctx.target, fs: homeFs } });
275
+ return items.map((item) => {
276
+ const ref = item.target?.ref;
277
+ if (ref === undefined)
278
+ return item;
279
+ const rel = normalizeHomeRel(ref);
280
+ if (rel !== null && areaOfPath(entries, rel) !== null)
281
+ return item;
282
+ return {
283
+ id: item.id,
284
+ ...(item.unitId !== undefined ? { unitId: item.unitId } : {}),
285
+ kind: 'Error',
286
+ adapter: this.id,
287
+ description: engineMsg(homeFs === undefined ? 'adapter.pluginDataNoOsHome' : 'adapter.pluginDataPathNotAllowed', { path: ref }),
288
+ severity: 'error',
289
+ };
290
+ });
291
+ }
292
+ /** 应用前再查一遍白名单(纵深防御:plan 可能来自消费者手工构造/缓存反序列化) */
293
+ async applyItem(item, ctx) {
294
+ const homeFs = ctx.target.homeFs;
295
+ if (homeFs === undefined) {
296
+ return { ok: false, message: engineMsg('adapter.pluginDataNoOsHome') };
297
+ }
298
+ const ref = item.target?.ref;
299
+ if (ref !== undefined) {
300
+ const rel = normalizeHomeRel(ref);
301
+ const entries = await this.allowedEntries(ctx.target);
302
+ if (rel === null || areaOfPath(entries, rel) === null) {
303
+ return { ok: false, message: engineMsg('adapter.pluginDataPathNotAllowed', { path: ref }) };
304
+ }
305
+ }
306
+ return super.applyItem(item, { ...ctx, target: { ...ctx.target, fs: homeFs } });
307
+ }
308
+ }
309
+ //# sourceMappingURL=plugin-data.js.map
@@ -47,7 +47,7 @@ import { FileCollectionAdapter } from './file-collection.ts';
47
47
  export declare const SELF_CONFIG_FILES: readonly string[];
48
48
  export declare class SelfAdapter extends FileCollectionAdapter {
49
49
  readonly id: 'self';
50
- readonly displayName = "Plugin Self Config";
50
+ readonly displayName: string;
51
51
  readonly defaultIncluded = true;
52
52
  readonly portability: 'portable';
53
53
  /** 插件自身配置目录(相对 homeDir;宿主按 dataDir 解析注入,缺省 dsh-config-manager) */
@@ -1,3 +1,4 @@
1
+ import { sectionMeta } from '../schema/section-registry.js';
1
2
  import { FileCollectionAdapter } from './file-collection.js';
2
3
  /** self 分区白名单文件(相对 baseDir,即 $DSH_HOME/dsh-config-manager/)。 */
3
4
  export const SELF_CONFIG_FILES = [
@@ -65,7 +66,7 @@ function canonicalizeWithoutRunState(raw) {
65
66
  }
66
67
  export class SelfAdapter extends FileCollectionAdapter {
67
68
  id = 'self';
68
- displayName = 'Plugin Self Config';
69
+ displayName = sectionMeta('self').displayName;
69
70
  defaultIncluded = true;
70
71
  portability = 'portable';
71
72
  /** 插件自身配置目录(相对 homeDir;宿主按 dataDir 解析注入,缺省 dsh-config-manager) */
@@ -167,6 +167,15 @@ export declare class MockHostContext implements HostContext {
167
167
  sessions?: HostContext['sessions'];
168
168
  /** 可选(issue #71):技能服务端口;测试按需注入(缺省 = 退回纯目录扫描)。 */
169
169
  skills?: HostContext['skills'];
170
+ /**
171
+ * 可选(第三方插件数据):系统 home 与它的文件门面。缺省 undefined = **旧宿主形态**
172
+ * (pluginData / pluginAssets 不收集也不写入,如实报码);需要它们的测试自行注入,
173
+ * 或用 withOsHome() 把系统 home 指向本夹具自己的根。
174
+ */
175
+ osHomeDir?: string;
176
+ homeFs?: HostContext['homeFs'];
177
+ /** 测试助手:把「系统 home」指向本上下文的根(两分区在本夹具里即可直接用) */
178
+ withOsHome(): this;
170
179
  constructor(platform: string, homeDir: string, profile?: string);
171
180
  }
172
181
  export declare function makeContext(platform: string, homeDir: string, profile?: string): MockHostContext;
@@ -261,6 +261,19 @@ export class MockHostContext {
261
261
  sessions;
262
262
  /** 可选(issue #71):技能服务端口;测试按需注入(缺省 = 退回纯目录扫描)。 */
263
263
  skills;
264
+ /**
265
+ * 可选(第三方插件数据):系统 home 与它的文件门面。缺省 undefined = **旧宿主形态**
266
+ * (pluginData / pluginAssets 不收集也不写入,如实报码);需要它们的测试自行注入,
267
+ * 或用 withOsHome() 把系统 home 指向本夹具自己的根。
268
+ */
269
+ osHomeDir;
270
+ homeFs;
271
+ /** 测试助手:把「系统 home」指向本上下文的根(两分区在本夹具里即可直接用) */
272
+ withOsHome() {
273
+ this.osHomeDir = this.homeDir;
274
+ this.homeFs = this.fs;
275
+ return this;
276
+ }
264
277
  constructor(platform, homeDir, profile) {
265
278
  this.platform = platform;
266
279
  this.homeDir = homeDir;
@@ -283,6 +296,7 @@ export function makeManifest(platform = 'win32') {
283
296
  settings: false, ui: false, providers: false, plugins: false, mcp: false, prompts: false,
284
297
  skills: false, agentPresets: false, agentInstructions: false, workspaces: false, pluginFiles: false,
285
298
  credentialsStatus: false, secrets: false, sessions: false, self: false,
299
+ pluginData: false, pluginAssets: false,
286
300
  },
287
301
  security: { containsSecrets: false, encrypted: false, encryption: null },
288
302
  };
package/lib/client.d.ts CHANGED
@@ -20,7 +20,7 @@ type Platform = 'win32' | 'darwin' | 'linux' | 'freebsd' | 'openbsd' | 'aix' | '
20
20
  * 明确不实现的分区(keybindings/workflows/commands)不在此列;
21
21
  * rules 已由 agentInstructions(~/.dsh/AGENTS.md,dsh-agent-instructions 全局指令)承接。
22
22
  */
23
- type SectionId = 'settings' | 'ui' | 'providers' | 'plugins' | 'mcp' | 'prompts' | 'skills' | 'agentPresets' | 'agentInstructions' | 'workspaces' | 'pluginFiles' | 'credentialsStatus' | 'secrets' | 'sessions' | 'self';
23
+ type SectionId = 'settings' | 'ui' | 'providers' | 'plugins' | 'mcp' | 'prompts' | 'skills' | 'agentPresets' | 'agentInstructions' | 'workspaces' | 'pluginFiles' | 'credentialsStatus' | 'secrets' | 'sessions' | 'self' | 'pluginData' | 'pluginAssets';
24
24
  /** 加密备份的算法参数(非秘密:salt/iv/authTag 仅用于解密与完整性校验) */
25
25
  interface EncryptionInfo {
26
26
  algorithm: 'aes-256-gcm';
@@ -647,6 +647,12 @@ interface SnapshotMeta {
647
647
  * (maxLines / maxD)时**降级为整块替换**(全删 + 全增),保证任何输入都有确定
648
648
  * 耗时的输出 —— 大文件不会卡死宿主。
649
649
  *
650
+ * 预算怎么定(2026-10-15 真机实测,本仓 rollback-point.ts 的一对版本):
651
+ * · 时间是 (N+M)·D 量级:D=4000、两侧合计 ~7500 行时 ≈ 70 ms(D=8000 ≈ 115 ms);
652
+ * · 内存是 O(D²)·4B(回溯要逐轮留 V 快照):D=2000 时 RSS +~30 MB、D=4000 +~100 MB、D=8000 +~175 MB。
653
+ * ⇒ 缺省取 2000(覆盖「大文件只改了几处」这一大类);单文件按需查看的调用方可以往上调,
654
+ * 到 4000 仍是一次点击可接受的开销;再大就该换线性空间算法,而不是继续加预算。
655
+ *
650
656
  * 行为约定:
651
657
  * - 行尾 \r\n 归一为 \n;文本末尾的换行不产生"空行"(行数 = 换行符语义)。
652
658
  * - 仅空白差异同样是差异(不做 ignoreWhitespace)。
@@ -1523,7 +1529,7 @@ declare const uiZh: {
1523
1529
  readonly 'error.downloadFailed': '下载失败:HTTP {status}';
1524
1530
  readonly 'error.exportTimeout': '导出超时({minutes} 分钟):导出过程未完成,请重试;若反复超时请检查 DSH 状态';
1525
1531
  readonly 'error.syncTimeout': '同步请求超时({minutes} 分钟):请检查网络与仓库可达性后重试';
1526
- readonly 'error.recoveryTimeout': '恢复请求超时({minutes} 分钟):恢复过程未完成,请重试;若反复超时请检查 DSH 状态';
1532
+ readonly 'error.recoveryTimeout': '恢复请求超时({minutes} 分钟):过程可能仍在后台进行,请先刷新预览确认;确实未完成再重试';
1527
1533
  readonly 'error.settingsConflict.title': '配置已被并发修改';
1528
1534
  readonly 'error.settingsConflict.action': '目标 DSH 的该配置在导入期间被其他进程修改,为避免覆盖请重新导出或手动核对后再试。';
1529
1535
  readonly 'error.notConfirmed.title': '导入未确认';
@@ -1593,7 +1599,7 @@ declare const uiZh: {
1593
1599
  readonly 'sync.kind.skip': '跳过';
1594
1600
  readonly 'sync.kind.conflict': '冲突';
1595
1601
  readonly 'sync.kind.install': '安装';
1596
- readonly 'sync.kind.missingSecret': '缺密钥';
1602
+ readonly 'sync.kind.missingSecret': '凭据迁移';
1597
1603
  readonly 'sync.kind.missingDependency': '缺依赖';
1598
1604
  readonly 'sync.kind.pathMapping': '路径映射';
1599
1605
  readonly 'sync.kind.warning': '警告';
@@ -1948,8 +1954,10 @@ declare const uiZh: {
1948
1954
  readonly 'export.category.settings': 'DSH 全局设置(namespace 分区,redacted)';
1949
1955
  readonly 'export.category.providers': 'LLM Provider / Model / 默认模型 / BaseURL';
1950
1956
  readonly 'export.category.plugins': '已安装插件清单与启用状态(不含二进制)';
1951
- readonly 'export.category.pluginFiles': '插件自有配置文件(白名单 + plugin-config/ 目录,整文件复制)';
1957
+ readonly 'export.category.pluginFiles': '其他插件放在 ~/.dsh 根下的零散配置文件(白名单 + plugin-config/ 目录,整文件复制)';
1952
1958
  readonly 'export.category.self': '本插件自身配置(同步/自动同步/分区选择/UI 偏好/市场;sync-*.json 等,不含凭据值)';
1959
+ readonly 'export.category.pluginData': '其他插件在系统主目录下的数据(如 ~/.dsh-wallpaper-engine、~/.mnemon 的配置与状态;可再生成缓存绝不收录)';
1960
+ readonly 'export.category.pluginAssets': '其他插件的大体积文件(上传的壁纸、本地数据库等;默认不勾选)';
1953
1961
  readonly 'export.category.mcp': 'MCP 服务器组合配置(需重启生效)';
1954
1962
  readonly 'export.category.prompts': 'System Prompt / Plan Mode 提示';
1955
1963
  readonly 'export.category.skills': '用户技能文件(~/.dsh/skills)';
@@ -4062,12 +4070,50 @@ interface RollbackPointSummary {
4062
4070
  spreadMs?: number;
4063
4071
  segments?: RollbackSegmentSummary[];
4064
4072
  }
4073
+ /**
4074
+ * 台账占用(issue #89 P2:可观测)。
4075
+ * `bytes` = records/ + payloads/ 的实际字节数;`budgetBytes` = 触发淘汰的字节预算;
4076
+ * `evicted` = 本进程内被预算淘汰的自动点累计数。缺省(老宿主没这个字段)= 界面不渲染这一行。
4077
+ */
4078
+ interface RollbackLedgerStatus {
4079
+ records: number;
4080
+ bytes: number;
4081
+ budgetBytes: number;
4082
+ evicted: number;
4083
+ /**
4084
+ * 已超预算且**无处可淘汰**(典型:单条点本身就大于预算)。
4085
+ * 老宿主没有这个字段 ⇒ `undefined` 与 `false` 同义,绝不因为读不到就报警。
4086
+ */
4087
+ overBudget?: boolean;
4088
+ }
4065
4089
  interface RollbackStorageStatus {
4066
4090
  available: boolean;
4067
4091
  root?: string;
4068
4092
  detail?: string;
4069
4093
  guidance?: RollbackGuidance[];
4070
4094
  requirements?: readonly string[];
4095
+ /** 磁盘偏好:回合自动留点开关(缺省 / false = 关闭) */
4096
+ rollbackEnabled?: boolean;
4097
+ /** 本进程此刻是否真的在留点(关闭后订阅已解绑)。与 rollbackEnabled 正常一致 */
4098
+ rollbackActive?: boolean;
4099
+ /** 台账占用(老宿主没有这个字段) */
4100
+ ledger?: RollbackLedgerStatus;
4101
+ }
4102
+ /** 开关读 / 写的结果(POST 成功才会改进程 gate)。 */
4103
+ interface RollbackPrefsResult {
4104
+ ok: boolean;
4105
+ rollbackEnabled: boolean;
4106
+ rollbackActive?: boolean;
4107
+ }
4108
+ /** 台账清理结果(只删自动点;guard 与 protected 保留)。 */
4109
+ interface RollbackPurgeResult {
4110
+ ok: boolean;
4111
+ code?: string;
4112
+ detail?: string;
4113
+ removedRecords?: number;
4114
+ removedPayloads?: number;
4115
+ removedObjects?: number;
4116
+ bytes?: number;
4071
4117
  }
4072
4118
  interface RollbackListResult {
4073
4119
  ok?: boolean;
@@ -4316,6 +4362,19 @@ interface RollbackPointPort {
4316
4362
  rollbackPointFileDiff?(id: string, path: string): Promise<RollbackFileDiffResult>;
4317
4363
  restoreRollbackPoint(id: string, opts: RollbackRewindOptions): Promise<RollbackRewindResult>;
4318
4364
  removeRollbackPoint(id: string, userConfirmed: boolean): Promise<RollbackRemoveResult>;
4365
+ /**
4366
+ * 回合自动留点开关(issue #88:它跟配置备份/同步是两件事,必须能关)。
4367
+ * 可选实现:端口缺失时界面**不渲染**开关(绝不给一个点了没反应的入口)。
4368
+ */
4369
+ rollbackPrefs?(): Promise<RollbackPrefsResult>;
4370
+ setRollbackEnabled?(enabled: boolean): Promise<RollbackPrefsResult>;
4371
+ /**
4372
+ * 清理台账(issue #89 P1/P2:把已经涨上去的占用收回来)。
4373
+ * **只删自动点**;`includeGuards` 为 true 时连 guard 也删,protected 永不删。
4374
+ */
4375
+ purgeRollbackPoints?(opts: {
4376
+ includeGuards: boolean;
4377
+ }): Promise<RollbackPurgeResult>;
4319
4378
  }
4320
4379
  //#endregion
4321
4380
  //#region src/client/recovery/recovery-api.d.ts
@@ -4390,6 +4449,25 @@ declare class RecoveryApi implements RecoveryPort, RollbackPointPort {
4390
4449
  * body 必须 `userConfirmed: true`(未确认 → 409 `confirmation-required`);本方法不自动置 true。
4391
4450
  */
4392
4451
  removeRollbackPoint(id: string, userConfirmed: boolean): Promise<RollbackRemoveResult>;
4452
+ /**
4453
+ * GET /recovery/rollback-points/prefs(issue #88):读开关。
4454
+ * `rollbackEnabled` = 磁盘偏好(用户意图,随 self 分区进备份);
4455
+ * `rollbackActive` = 本进程此刻是否真的在留点(关闭后订阅已解绑)。两者正常一致。
4456
+ */
4457
+ rollbackPrefs(): Promise<RollbackPrefsResult>;
4458
+ /**
4459
+ * POST /recovery/rollback-points/prefs(issue #88):写开关。
4460
+ * 宿主写盘成功后才切进程 gate —— 写失败却把功能打开,等于「界面说开着、实际不留点」。
4461
+ */
4462
+ setRollbackEnabled(enabled: boolean): Promise<RollbackPrefsResult>;
4463
+ /**
4464
+ * POST /recovery/rollback-points/cleanup(issue #89):清理台账占用。
4465
+ * **只删自动点**;`includeGuards` 为 true 时连救援点也删(protected 永不删)。
4466
+ * 返回逐类删除计数与释放字节,界面照实说「删了多少 / 还剩多少」。
4467
+ */
4468
+ purgeRollbackPoints(opts: {
4469
+ includeGuards: boolean;
4470
+ }): Promise<RollbackPurgeResult>;
4393
4471
  /** POST /recovery/sessions/rollback(T8):按台账 repairId 回滚(客户端不传路径)。 */
4394
4472
  rollbackSessionRepair(repairId: string): Promise<SessionRepairRollbackResult>;
4395
4473
  /**
@@ -4618,11 +4696,13 @@ declare const zh$4: {
4618
4696
  readonly 'section.agentPresets': 'Agent 预设';
4619
4697
  readonly 'section.agentInstructions': '全局指令';
4620
4698
  readonly 'section.workspaces': '工作区';
4621
- readonly 'section.pluginFiles': '插件文件';
4699
+ readonly 'section.pluginFiles': '其他插件零散文件';
4622
4700
  readonly 'section.credentialsStatus': '凭据状态';
4623
4701
  readonly 'section.secrets': '密钥';
4624
4702
  readonly 'section.sessions': '历史会话';
4625
- readonly 'section.self': '插件自身配置';
4703
+ readonly 'section.self': '本插件自身配置';
4704
+ readonly 'section.pluginData': '其他插件数据';
4705
+ readonly 'section.pluginAssets': '其他插件大文件';
4626
4706
  readonly 'view.about': '关于';
4627
4707
  readonly 'about.snippet.environment': '### 环境 / Environment';
4628
4708
  readonly 'about.releaseNotes.rateLimited': 'GitHub API 速率限制(Rate limit exceeded),请稍后再试或直接在 GitHub 上查看。';
@@ -4923,7 +5003,6 @@ declare const zh$4: {
4923
5003
  readonly 'snapshots.plan.diffEscape': '路径越界,已拒绝读取';
4924
5004
  readonly 'snapshots.plan.diffNotApplicable': '该动作不是文件内容变更,无逐行对照';
4925
5005
  readonly 'snapshots.plan.diffDegraded': '差异过大,已按整块对比显示';
4926
- readonly 'snapshots.plan.diffTruncated': '仅显示前 {shown} 行(共 {total} 行)';
4927
5006
  readonly 'snapshots.plan.statSkipped.tooLarge': '体积超上限,未统计';
4928
5007
  readonly 'snapshots.plan.statSkipped.binary': '二进制';
4929
5008
  readonly 'snapshots.plan.statSkipped.budget': '未统计(文件数上限)';
@@ -6349,6 +6428,20 @@ declare const zh$1: {
6349
6428
  readonly 'rollback.badge.incomplete': '两态不全';
6350
6429
  readonly 'rollback.badge.protected': '受保护(只进不出)';
6351
6430
  readonly 'rollback.apiMissing': '当前宿主没有回滚点能力(插件版本过旧):界面不假装可用';
6431
+ readonly 'rollback.switch.label': '启用回合自动留点';
6432
+ readonly 'rollback.switch.hint': '关闭后不再订阅会话事件、不再写台账,聊天里的「↺ 回退」入口一并收起;已有回滚点仍可查看、回退与清理。';
6433
+ readonly 'rollback.switch.unknown': '正在读取开关状态(还没读到之前不显示成「已关闭」)。';
6434
+ readonly 'rollback.switch.diverged': '磁盘上的偏好是「开启」,但本进程没有启用留点(通常是被另一个 DSH 实例改过)。重新打开一次开关即可让本进程生效。';
6435
+ readonly 'rollback.ledger.summary': '台账 {records} 个点 · {size}';
6436
+ readonly 'rollback.ledger.budget': '字节预算 {budget},已按预算淘汰 {evicted} 个最旧自动点';
6437
+ readonly 'rollback.ledger.overBudget': '台账已到 {size},超过 {budget} 的字节预算,但已经没有可自动淘汰的点了(通常是单个回滚点本身大于预算)—— 再增长请手动清理。';
6438
+ readonly 'rollback.cleanup': '清理台账';
6439
+ readonly 'rollback.cleanup.confirmTitle': '清理回滚点台账';
6440
+ readonly 'rollback.cleanup.confirmMessage': '删除全部自动回滚点(救援点与受保护点按下面的勾选处置),并回收它们的内容副本。工作区文件与会话日志不会被改动。';
6441
+ readonly 'rollback.cleanup.includeGuards': '同时删除救援点(回退前的自动救援点,删了不可撤销)';
6442
+ readonly 'rollback.cleanup.done': '已删除 {records} 个回滚点,释放 {size}';
6443
+ readonly 'rollback.cleanup.none': '没有可删除的回滚点(自动点为空)';
6444
+ readonly 'rollback.switch.saveFailed': '开关保存失败,仍按原状态运行:';
6352
6445
  readonly 'rollback.retention.hint': '自动点:每会话最多 {perSession} 个、全局最多 {total} 个,超出只淘汰最旧的自动点。回退前的救援点不占这两个额度、也不被它们淘汰,但用户删不掉,所以另有自己的两道上限:每会话最多 {guardsPerSession} 个、全局最多 {guards} 个(无论按哪道算,超出都淘汰最旧的救援点)。';
6353
6446
  readonly 'rollback.autopoint.hint': '回滚点由每个回合结束时自动留:工作区快照 + 会话游标(没有「手动拍点」入口)。';
6354
6447
  readonly 'rollback.state.workspace': '工作区';
@@ -6390,13 +6483,14 @@ declare const zh$1: {
6390
6483
  readonly 'rollback.code.session-out-of-root': '会话日志不在本机会话根内,已拒绝';
6391
6484
  readonly 'rollback.code.session-too-large': '会话日志超过上限:只记下了游标(回退不依赖副本)';
6392
6485
  readonly 'rollback.code.session-active': '这条对话正在运行(会话日志刚被写过):此时带文件段的回退会被拒绝、工作区一个字节都不动;只回溯消息不受影响(回退只 fork 子会话,不改写原日志)';
6486
+ readonly 'rollback.code.subagent-session': '这是子代理会话(origin=subagent),插件不为它自动留回滚点:它的工作区与父对话相同,留点只会挤占你自己对话的台账空间。';
6393
6487
  readonly 'rollback.preview.sessionActive': '这条对话正在运行:本轮结束前只回溯消息(不改动工作区),避免与正在写盘的工具抢文件。等本轮结束后重新打开本弹窗,即可连文件一起回退。';
6394
6488
  readonly 'rollback.sessionActive.denied': '这条对话正在运行:本次一个文件都没有改动。请等本轮结束,或改成「只回溯消息」再试。';
6395
6489
  readonly 'rollback.code.session-fork-unavailable': '当前宿主没有提供会话 fork 能力,因此不能回退会话';
6396
6490
  readonly 'rollback.code.fork-failed': 'fork 子会话失败,会话段未回退';
6397
6491
  readonly 'rollback.code.capture-failed': '该态留点失败';
6398
6492
  readonly 'rollback.code.capture-truncated': '该态留点到达上限被截断(已如实标注)';
6399
- readonly 'rollback.code.capture-degraded': '该态留点不完整(有链接或读不到的内容被跳过,已逐条留痕)';
6493
+ readonly 'rollback.code.capture-degraded': '该态留点不完整(有读不到或期间被改写的内容,已逐条留痕)';
6400
6494
  readonly 'rollback.code.no-chunks': '没有给出工作区路径分块';
6401
6495
  readonly 'rollback.code.chunk-not-absolute': '工作区分块必须是绝对路径';
6402
6496
  readonly 'rollback.code.chunk-not-found': '工作区分块不存在或不可用';
@@ -6415,7 +6509,7 @@ declare const zh$1: {
6415
6509
  readonly 'rollback.warn.trackDegraded': '有一态留得不完整(回退可能不完整)';
6416
6510
  readonly 'rollback.warn.protected': '该回滚点受保护(只进不出):不可回退、不可删除';
6417
6511
  readonly 'rollback.warn.guardFailed': '回退前的救援点没留成(已记告警后继续)';
6418
- readonly 'rollback.warn.createdRefused': '本次不删除任何「留点后新建」的路径:捕获时的证明名单不完整(有整目录被忽略 / 路径索引缺失 / 目录读不出),无法证明哪些是新建的';
6512
+ readonly 'rollback.warn.createdRefused': '本次不删除任何「留点后新建」的路径:捕获时的证明名单不完整(有整目录被忽略 / 路径索引缺失 / 目录读不出 / 留点被时间预算中断),无法证明哪些是新建的';
6419
6513
  readonly 'rollback.warn.unknown': '未识别的告警(技术细节见下方)';
6420
6514
  readonly 'rollback.preview': '预览回退';
6421
6515
  readonly 'rollback.preview.title': '回退预览';
@@ -6452,7 +6546,6 @@ declare const zh$1: {
6452
6546
  readonly 'rollback.diff.after': '回退后';
6453
6547
  readonly 'rollback.diff.identical': '该文件与回退点内容一致,回退不会改动它';
6454
6548
  readonly 'rollback.diff.degraded': '差异规模较大,已按整块替换展示(仍是完整结果,只是不是最小的编辑脚本)';
6455
- readonly 'rollback.diff.truncated': '只画了前 {shown} 行(共 {total} 行),数据本身是完整的';
6456
6549
  readonly 'rollback.diff.reason.missingBlob': '这份内容副本读不出来,无法逐行对照(回退仍会尝试写回它)';
6457
6550
  readonly 'rollback.diff.reason.binary': '不是文本文件,没有逐行差异可看';
6458
6551
  readonly 'rollback.diff.reason.tooLarge': '文件超过展示上限,没有逐行差异可看(回退仍会照常覆盖它)';