dsh-config-manager 0.1.76 → 0.1.77

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 (71) hide show
  1. package/lib/adapters/plugin-data-declared.d.ts +12 -0
  2. package/lib/adapters/plugin-data-declared.js +57 -0
  3. package/lib/adapters/plugin-data.d.ts +4 -6
  4. package/lib/adapters/plugin-data.js +8 -36
  5. package/lib/adapters/plugins.js +92 -12
  6. package/lib/client.d.ts +104 -9
  7. package/lib/client.js +1796 -714
  8. package/lib/core/messages.d.ts +1 -0
  9. package/lib/core/messages.js +2 -0
  10. package/lib/core/rollback-attribution.d.ts +81 -0
  11. package/lib/core/rollback-attribution.js +236 -0
  12. package/lib/core/rollback-merge.d.ts +22 -0
  13. package/lib/core/rollback-merge.js +203 -0
  14. package/lib/core/rollback-point.d.ts +114 -0
  15. package/lib/core/rollback-point.js +534 -17
  16. package/lib/core/types.d.ts +18 -0
  17. package/lib/index.d.ts +26 -3
  18. package/lib/index.js +129 -16
  19. package/lib/routes/recovery.js +11 -0
  20. package/lib/schema/section-registry.js +5 -5
  21. package/lib/ui/badge-tone.d.ts +16 -0
  22. package/lib/ui/badge-tone.js +20 -0
  23. package/lib/ui/i18n.d.ts +4 -4
  24. package/lib/ui/i18n.js +8 -8
  25. package/lib/ui/rollback-view.d.ts +95 -0
  26. package/lib/ui/rollback-view.js +55 -0
  27. package/lib/ui/selection-model.d.ts +140 -1
  28. package/lib/ui/selection-model.js +241 -9
  29. package/package.json +1 -1
  30. package/src/adapters/plugin-data-declared.ts +60 -0
  31. package/src/adapters/plugin-data.ts +7 -33
  32. package/src/adapters/plugins.ts +98 -13
  33. package/src/adapters/units.test.ts +26 -0
  34. package/src/client/chat/ChatEntryView.tsx +147 -46
  35. package/src/client/chat/chat-entry-view.test.ts +277 -1
  36. package/src/client/chat/chat-entry-view.ts +98 -4
  37. package/src/client/chat/chat-node-registration.ts +71 -0
  38. package/src/client/chat/chat-slots.ts +30 -12
  39. package/src/client/common/ContentPicker.tsx +581 -175
  40. package/src/client/common/host-primitives.ts +64 -1
  41. package/src/client/common/ui.tsx +17 -3
  42. package/src/client/config-manager.module.css +630 -397
  43. package/src/client/css-scale-guard.test.ts +41 -3
  44. package/src/client/css-token-guard.test.ts +10 -4
  45. package/src/client/export/ExportView.tsx +2 -0
  46. package/src/client/index.ts +15 -3
  47. package/src/client/locales.ts +54 -10
  48. package/src/client/recovery/RollbackCard.tsx +56 -2
  49. package/src/client/recovery/RollbackDiffPane.tsx +5 -0
  50. package/src/client/recovery/RollbackFileChanges.tsx +46 -2
  51. package/src/client/recovery/recovery-locales.ts +22 -0
  52. package/src/client/recovery/rollback-file-changes.test.ts +1 -1
  53. package/src/core/messages.ts +2 -0
  54. package/src/core/rollback-attribution.test.ts +120 -0
  55. package/src/core/rollback-attribution.ts +267 -0
  56. package/src/core/rollback-merge.test.ts +133 -0
  57. package/src/core/rollback-merge.ts +213 -0
  58. package/src/core/rollback-point.test.ts +412 -0
  59. package/src/core/rollback-point.ts +560 -20
  60. package/src/core/types.ts +19 -0
  61. package/src/index.facade.test.ts +60 -0
  62. package/src/index.ts +133 -19
  63. package/src/routes/recovery.ts +11 -0
  64. package/src/schema/section-registry.ts +5 -5
  65. package/src/ui/badge-tone.test.ts +23 -0
  66. package/src/ui/badge-tone.ts +27 -0
  67. package/src/ui/i18n.ts +8 -8
  68. package/src/ui/rollback-view.test.ts +27 -0
  69. package/src/ui/rollback-view.ts +139 -0
  70. package/src/ui/selection-model.test.ts +240 -2
  71. package/src/ui/selection-model.ts +349 -1
@@ -0,0 +1,12 @@
1
+ import type { PluginDataSource } from '../core/plugin-data-registry.ts';
2
+ import type { HostContext } from '../core/types.ts';
3
+ /** 单次扫描最多读取多少个已安装插件的 manifest(防御性上限:profile 依赖数量异常时不无限读) */
4
+ export declare const MAX_DECLARED_PACKAGES = 300;
5
+ /**
6
+ * 解析声明时用的 profile 目录名。
7
+ *
8
+ * 与 DSH 的 profile 解析同口径:空 / undefined = \`web\`(与改造前的私有实现一致)。
9
+ */
10
+ export declare function declaredProfileName(profile: string | undefined): string;
11
+ /** 扫描本机已安装插件自述的数据目录声明(零副作用;读不到的一律跳过)。 */
12
+ export declare function scanDeclaredDataSources(ctx: HostContext, maxPackages?: number): Promise<PluginDataSource[]>;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 「已安装插件自述数据目录(dsh.dataDirs)」的**单一扫描实现**。
3
+ *
4
+ * 为什么单独一个模块:这份扫描有两个消费者 ——
5
+ * - \`pluginData\` / \`pluginAssets\` adapter(决定要收集哪些区域);
6
+ * - \`plugins\` adapter(导出选择器「按插件看」需要知道**每个插件自己声明的数据区域**,
7
+ * 才能在不读任何数据目录的前提下画出树骨架)。
8
+ * 两处各写一份必然漂移(同一条铁律:同一事实只有一份实现)。
9
+ *
10
+ * 语义(与改造前 plugin-data.ts 的私有实现逐字一致):
11
+ * - 读的是 **profile 实际安装**的那份 manifest(\`profiles/<profile>/node_modules/<pkg>/package.json\`),
12
+ * 不是全局目录 —— 同一个包在不同 profile 可以是不同版本,声明必须取本 profile 装的;
13
+ * - 读不到(未安装 / 无声明 / 形状非法)一律跳过:本机制是**附加**信息,绝不因为一个包读不出来
14
+ * 就影响内置来源或整次导出;
15
+ * - 本函数**不缓存**(缓存策略由调用方决定:pluginData 有实例级 TTL 缓存,plugins 有模块级缓存)。
16
+ */
17
+ import { parseDeclaredDataDirs } from '../core/plugin-data-registry.js';
18
+ /** 单次扫描最多读取多少个已安装插件的 manifest(防御性上限:profile 依赖数量异常时不无限读) */
19
+ export const MAX_DECLARED_PACKAGES = 300;
20
+ /**
21
+ * 解析声明时用的 profile 目录名。
22
+ *
23
+ * 与 DSH 的 profile 解析同口径:空 / undefined = \`web\`(与改造前的私有实现一致)。
24
+ */
25
+ export function declaredProfileName(profile) {
26
+ return profile !== undefined && profile !== '' ? profile : 'web';
27
+ }
28
+ /** 扫描本机已安装插件自述的数据目录声明(零副作用;读不到的一律跳过)。 */
29
+ export async function scanDeclaredDataSources(ctx, maxPackages = MAX_DECLARED_PACKAGES) {
30
+ const base = 'profiles/' + declaredProfileName(ctx.profile);
31
+ const out = [];
32
+ let deps = [];
33
+ try {
34
+ const raw = await ctx.fs.readFile(base + '/package.json');
35
+ const parsed = JSON.parse(new TextDecoder().decode(raw));
36
+ const dep = parsed?.dependencies;
37
+ if (dep !== null && typeof dep === 'object')
38
+ deps = Object.keys(dep).slice(0, maxPackages);
39
+ }
40
+ catch {
41
+ deps = [];
42
+ }
43
+ for (const name of deps) {
44
+ try {
45
+ const raw = await ctx.fs.readFile(base + '/node_modules/' + name + '/package.json');
46
+ const manifest = JSON.parse(new TextDecoder().decode(raw));
47
+ const source = parseDeclaredDataDirs(name, manifest);
48
+ if (source !== null)
49
+ out.push(source);
50
+ }
51
+ catch {
52
+ // 未安装 / 无 manifest → 无声明,跳过
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+ //# sourceMappingURL=plugin-data-declared.js.map
@@ -47,13 +47,11 @@ export declare class PluginDataAdapter extends FileCollectionAdapter {
47
47
  private allowedEntries;
48
48
  private allSources;
49
49
  /**
50
- * 已安装插件的自述声明(dsh.dataDirs)。
50
+ * 已安装插件的自述声明(dsh.dataDirs)—— 本方法只做**缓存**,扫描实现见 plugin-data-declared.ts。
51
51
  *
52
- * 为什么读 profile 的 node_modules 而不是全局目录:DSH 的插件装在 \`profiles/<profile>/node_modules\`
53
- * 下(pnpm 隔离布局),且同一个包在不同 profile 可以是不同版本 —— 声明必须取**本 profile 实际装的**那份。
54
- *
55
- * 读不到(未安装 / 无声明 / 形状非法)一律跳过:本机制是**附加**信息,绝不因为一个包读不出来
56
- * 就影响内置来源或整次导出。
52
+ * 为什么缓存必须带 ctx 键(homeDir + profile):DSH 的插件装在 \`profiles/<profile>/node_modules\`
53
+ * 下(pnpm 隔离布局),而「这台机器装了某插件」不代表另一台装了 —— 缓存若按实例共享,
54
+ * 导入到「没装该插件」的目标机时会拿源机的声明去放行写入,白名单就变成「谁先导入谁说了算」。
57
55
  */
58
56
  private declaredSources;
59
57
  /**
@@ -30,13 +30,12 @@ import { normalizePath } from '../utils/paths.js';
30
30
  import { engineMsg } from './messages.js';
31
31
  import { listFilesDetailed } from './link-report.js';
32
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';
33
+ import { BUILTIN_PLUGIN_DATA_SOURCES, areaOfPath, areasForSection, isExcludedRel, normalizeHomeRel, } from '../core/plugin-data-registry.js';
34
+ import { declaredProfileName, scanDeclaredDataSources } from './plugin-data-declared.js';
34
35
  import { osHomeDirOf } from '../core/backup.js';
35
36
  import { FileCollectionAdapter } from './file-collection.js';
36
37
  /** 声明来源缓存的 TTL(毫秒):一次导出/导入/执行里会多次解析,逐次读 15+ 个 package.json 是净开销 */
37
38
  const DECLARED_TTL_MS = 5_000;
38
- /** 单次解析里最多读取多少个已安装插件的 manifest(防御性上限:profile 依赖数量异常时不无限读) */
39
- const MAX_DECLARED_PACKAGES = 300;
40
39
  export class PluginDataAdapter extends FileCollectionAdapter {
41
40
  id;
42
41
  displayName;
@@ -75,48 +74,21 @@ export class PluginDataAdapter extends FileCollectionAdapter {
75
74
  return [...this.builtin, ...declared];
76
75
  }
77
76
  /**
78
- * 已安装插件的自述声明(dsh.dataDirs)。
77
+ * 已安装插件的自述声明(dsh.dataDirs)—— 本方法只做**缓存**,扫描实现见 plugin-data-declared.ts。
79
78
  *
80
- * 为什么读 profile 的 node_modules 而不是全局目录:DSH 的插件装在 \`profiles/<profile>/node_modules\`
81
- * 下(pnpm 隔离布局),且同一个包在不同 profile 可以是不同版本 —— 声明必须取**本 profile 实际装的**那份。
82
- *
83
- * 读不到(未安装 / 无声明 / 形状非法)一律跳过:本机制是**附加**信息,绝不因为一个包读不出来
84
- * 就影响内置来源或整次导出。
79
+ * 为什么缓存必须带 ctx 键(homeDir + profile):DSH 的插件装在 \`profiles/<profile>/node_modules\`
80
+ * 下(pnpm 隔离布局),而「这台机器装了某插件」不代表另一台装了 —— 缓存若按实例共享,
81
+ * 导入到「没装该插件」的目标机时会拿源机的声明去放行写入,白名单就变成「谁先导入谁说了算」。
85
82
  */
86
83
  async declaredSources(ctx) {
87
84
  const now = Date.now();
88
- const profile = ctx.profile !== undefined && ctx.profile !== '' ? ctx.profile : 'web';
89
- const memoKey = ctx.homeDir + '|' + profile;
85
+ const memoKey = ctx.homeDir + '|' + declaredProfileName(ctx.profile);
90
86
  if (this.declaredMemo !== undefined
91
87
  && this.declaredMemo.key === memoKey
92
88
  && now - this.declaredMemo.at < this.declaredTtlMs) {
93
89
  return this.declaredMemo.sources;
94
90
  }
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
- }
91
+ const out = await scanDeclaredDataSources(ctx);
120
92
  this.declaredMemo = { key: memoKey, at: now, sources: out };
121
93
  return out;
122
94
  }
@@ -23,6 +23,8 @@ import { isENOENT } from '../utils/guards.js';
23
23
  import { PLUGIN_PATCH_REF_PREFIX } from '../core/backup.js';
24
24
  import { readEffectivePatchLines, resolveWriteLayer, USER_PATCH_FILE } from '../core/patch-layers.js';
25
25
  import { parsePnpmPatchedDependencies, sanitizePnpmWorkspacePatches } from './pnpm-workspace.js';
26
+ import { BUILTIN_PLUGIN_DATA_SOURCES, areasForSection } from '../core/plugin-data-registry.js';
27
+ import { declaredProfileName, scanDeclaredDataSources } from './plugin-data-declared.js';
26
28
  import { sectionMeta } from '../schema/section-registry.js';
27
29
  import { validateJsonSection } from './json-section.js';
28
30
  /**
@@ -126,6 +128,66 @@ function entriesOfRaw(raw) {
126
128
  * 导出产物(真实 base64)走另一条分支,行为逐字不变。
127
129
  */
128
130
  const previewTarballBytes = new WeakMap();
131
+ /**
132
+ * 「插件包名 → 它声明的数据区域」映射(只有 \`plugin:<包名>\` 单元用;导出选择器「按插件看」的树骨架)。
133
+ *
134
+ * 为什么挂 WeakMap 而不是给 PluginsSection 加字段:这是**展示用旁路信息** —— 既不是备份格式的
135
+ * 一部分(schema 校验与市场防线都不该看见它),也与「导出真的带什么」无关;挂在载荷对象上随对象
136
+ * 一起回收,载荷本身逐字节不变(与 previewTarballBytes 同一套理由)。
137
+ */
138
+ const relatedAreasByData = new WeakMap();
139
+ /** 内置来源 + 自述声明的扫描缓存(TTL 内的同一 home/profile 只扫一次;/export-preview 一趟会问两次) */
140
+ const declaredSourcesMemo = { key: '', at: 0, sources: [] };
141
+ const DECLARED_MEMO_TTL_MS = 5_000;
142
+ /**
143
+ * 本机可归属的插件数据来源 = 内置来源表 + 已安装插件自述声明。
144
+ *
145
+ * 缓存键必须带 homeDir + profile:同一台机器换 profile 就是另一批已安装插件,拿旧声明画树
146
+ * 会让用户看到不属于当前 profile 的数据区域。
147
+ */
148
+ async function pluginDataSources(ctx) {
149
+ const key = ctx.homeDir + '|' + declaredProfileName(ctx.profile);
150
+ const now = Date.now();
151
+ if (declaredSourcesMemo.key === key && now - declaredSourcesMemo.at < DECLARED_MEMO_TTL_MS) {
152
+ return declaredSourcesMemo.sources;
153
+ }
154
+ const sources = [...BUILTIN_PLUGIN_DATA_SOURCES, ...await scanDeclaredDataSources(ctx)];
155
+ declaredSourcesMemo.key = key;
156
+ declaredSourcesMemo.at = now;
157
+ declaredSourcesMemo.sources = sources;
158
+ return sources;
159
+ }
160
+ /**
161
+ * 包名 → 该插件的数据区域引用。
162
+ *
163
+ * 口径与 pluginData / pluginAssets 的收集面**同源**(同一个 areasForSection):role=config 进
164
+ * pluginData、role=assets 进 pluginAssets;role=cache 的区域不属于任何分区,因此**绝不进树**
165
+ * (缓存是可再生产物,列出来只会诱导用户把它带走)。
166
+ *
167
+ * 只产出「注册表说它应该在哪儿」,不代表磁盘上真有 —— 界面必须把两者分开渲染。
168
+ */
169
+ async function relatedAreasOf(ctx) {
170
+ const sources = await pluginDataSources(ctx);
171
+ const map = new Map();
172
+ for (const source of sources) {
173
+ const refs = [];
174
+ for (const section of ['pluginData', 'pluginAssets']) {
175
+ for (const entry of areasForSection([source], section)) {
176
+ refs.push({ section, relPath: entry.area.relPath, role: entry.area.role === 'assets' ? 'assets' : 'config' });
177
+ }
178
+ }
179
+ if (refs.length === 0)
180
+ continue;
181
+ for (const pkg of source.packages) {
182
+ const cur = map.get(pkg);
183
+ if (cur === undefined)
184
+ map.set(pkg, [...refs]);
185
+ else
186
+ cur.push(...refs);
187
+ }
188
+ }
189
+ return map;
190
+ }
129
191
  /**
130
192
  * 零 spawn 推导本地源插件的体积(预览专用)。
131
193
  *
@@ -179,7 +241,7 @@ async function measureLocalSources(plugins, ctx) {
179
241
  * `tarballBytes` = 预览路径登记到的本地源体积(见 measureLocalSources,0 = 体积未知但仍随包携带);
180
242
  * 缺省时体积与文件数完全按导出产物里的 base64 计算 —— 与改造前逐字一致。
181
243
  */
182
- function unitsOf(data, tarballBytes) {
244
+ function unitsOf(data, tarballBytes, related) {
183
245
  const wsId = 'plugins:pnpm-workspace';
184
246
  const patchFileIds = (data.patchFiles ?? []).map((pf) => `plugins:patch:${normalizePath(pf.relativePath)}`);
185
247
  const group = [wsId, ...patchFileIds];
@@ -187,6 +249,7 @@ function unitsOf(data, tarballBytes) {
187
249
  for (const pl of data.plugins) {
188
250
  const tarball = tarballOf(data, pl.name);
189
251
  const measured = tarballBytes?.get(pl.name);
252
+ const areas = related?.get(pl.name);
190
253
  units.push({
191
254
  id: `plugin:${pl.name}`,
192
255
  label: pl.name,
@@ -194,6 +257,8 @@ function unitsOf(data, tarballBytes) {
194
257
  // base64 长度 → 原始字节数(仅用于展示体积,不求精确);预览路径没有字节,用 stat 量到的源体积
195
258
  sizeBytes: tarball !== undefined ? Math.floor((tarball.base64.length * 3) / 4) : measured ?? 0,
196
259
  ...(tarball !== undefined || measured !== undefined ? { fileCount: 1 } : {}),
260
+ // 「按插件看」的树骨架:该插件声明的数据区域(零数据目录 I/O)
261
+ ...(areas !== undefined && areas.length > 0 ? { relatedAreas: areas } : {}),
197
262
  });
198
263
  }
199
264
  for (const line of data.patch) {
@@ -260,7 +325,7 @@ export class PluginsAdapter {
260
325
  section: collected,
261
326
  // 与 src/index.ts 对 JSON 分区现算体积的口径一致(utf8 字节数)
262
327
  sizeBytes: new TextEncoder().encode(stringifyJsonSafe(data)).length,
263
- items: unitsOf(data, tarballBytes),
328
+ items: unitsOf(data, tarballBytes, relatedAreasByData.get(data)),
264
329
  };
265
330
  }
266
331
  /**
@@ -401,16 +466,30 @@ export class PluginsAdapter {
401
466
  const alive = new Set(keptPlugins.map((pl) => pl.name));
402
467
  keptTarballs = localTarballs?.filter((t) => alive.has(t.packageName));
403
468
  }
469
+ const data = {
470
+ version: 1,
471
+ plugins: keptPlugins,
472
+ patch: keptPatch,
473
+ pnpmWorkspace: keptWorkspace,
474
+ ...(keptTarballs !== undefined ? { localTarballs: keptTarballs } : {}),
475
+ ...(keptPatchFiles !== undefined ? { patchFiles: keptPatchFiles } : {}),
476
+ };
477
+ /**
478
+ * 「按插件看」的树骨架(导出选择器):登记每个插件声明的数据区域。
479
+ * 只登记「至少有一个区域」的插件 —— 其余插件单元保持无该字段,界面据此不显示空壳节点。
480
+ * 归属信息是**附加**信息:扫描失败不得影响导出/预览本身(与 pluginData 同一纪律)。
481
+ */
482
+ try {
483
+ const related = await relatedAreasOf(ctx);
484
+ if (related.size > 0)
485
+ relatedAreasByData.set(data, related);
486
+ }
487
+ catch {
488
+ // 读不到声明就不登记(界面按「没有已知关联」渲染,绝不猜)
489
+ }
404
490
  return {
405
491
  sectionId: 'plugins',
406
- data: {
407
- version: 1,
408
- plugins: keptPlugins,
409
- patch: keptPatch,
410
- pnpmWorkspace: keptWorkspace,
411
- ...(keptTarballs !== undefined ? { localTarballs: keptTarballs } : {}),
412
- ...(keptPatchFiles !== undefined ? { patchFiles: keptPatchFiles } : {}),
413
- },
492
+ data,
414
493
  counts: {
415
494
  plugins: keptPlugins.length,
416
495
  patchLines: keptPatch.length,
@@ -429,8 +508,9 @@ export class PluginsAdapter {
429
508
  * 由选择模型保证勾选/取消同步。
430
509
  */
431
510
  listUnits(section) {
432
- // 预览产物没有 tarball 字节,体积来自 measureLocalSources 挂上的统计(导出产物走 base64 分支)
433
- return unitsOf(section.data, previewTarballBytes.get(section.data));
511
+ // 预览产物没有 tarball 字节,体积来自 measureLocalSources 挂上的统计(导出产物走 base64 分支);
512
+ // 「按插件看」的数据区域骨架同样由 collect() 登记在载荷对象上(两条路径逐字一致,units.test 钉住)
513
+ return unitsOf(section.data, previewTarballBytes.get(section.data), relatedAreasByData.get(section.data));
434
514
  }
435
515
  /**
436
516
  * 导入后目标机**可能**存在的 patch 文件集合(相对 profile 目录)——issue #35。
package/lib/client.d.ts CHANGED
@@ -185,6 +185,24 @@ interface ExportUnit {
185
185
  * 只下发**事实**,不下发文案 —— 界面按当前语言显示(与 DSH 同口径),绝不用 uuid 冒充标题。
186
186
  */
187
187
  blank?: boolean;
188
+ /**
189
+ * 该插件**声明的第三方数据区域**(只有 \`plugin:<包名>\` 单元可能有值;导出选择器「按插件看」用)。
190
+ *
191
+ * 用途:选择器要在**不读任何数据目录**的前提下画出「插件 → 数据目录 / 大文件」的树骨架 ——
192
+ * 区域清单来自来源注册表(内置来源表 + 插件自述 dsh.dataDirs),是静态知识,零额外 I/O;
193
+ * 体积与「实际有没有内容」等该分区的清单到货后再填。
194
+ *
195
+ * 语义:**纯展示**,不参与勾选/导出契约(契约只认 id)。这里是「注册表说它应该在哪儿」,
196
+ * **不是**「磁盘上确实有」—— 界面必须把两者分开渲染(unread / 无内容 / 已读到)。
197
+ */
198
+ relatedAreas?: readonly PluginAreaRef[];
199
+ }
200
+ /** 插件数据区域引用(pluginData / pluginAssets;只读展示用,不参与勾选契约)。 */
201
+ interface PluginAreaRef {
202
+ section: 'pluginData' | 'pluginAssets';
203
+ /** home 相对 POSIX 路径 —— 就是该分区单元的 id 后缀(\`pluginData:.mnemon/runtime\` 的末段) */
204
+ relPath: string;
205
+ role: 'config' | 'assets';
188
206
  }
189
207
  type PlanItemKind = 'Create' | 'Update' | 'Skip' | 'Conflict' | 'Install' | 'MissingSecret' | 'MissingDependency' | 'PathMapping' | 'Warning' | 'Error';
190
208
  type ItemResolution = 'keepCurrent' | 'useImported' | 'review';
@@ -1954,10 +1972,10 @@ declare const uiZh: {
1954
1972
  readonly 'export.category.settings': 'DSH 全局设置(namespace 分区,redacted)';
1955
1973
  readonly 'export.category.providers': 'LLM Provider / Model / 默认模型 / BaseURL';
1956
1974
  readonly 'export.category.plugins': '已安装插件清单与启用状态(不含二进制)';
1957
- readonly 'export.category.pluginFiles': '其他插件放在 ~/.dsh 根下的零散配置文件(白名单 + plugin-config/ 目录,整文件复制)';
1958
- readonly 'export.category.self': '本插件自身配置(同步/自动同步/分区选择/UI 偏好/市场;sync-*.json 等,不含凭据值)';
1959
- readonly 'export.category.pluginData': '其他插件在系统主目录下的数据(如 ~/.dsh-wallpaper-engine、~/.mnemon 的配置与状态;可再生成缓存绝不收录)';
1960
- readonly 'export.category.pluginAssets': '其他插件的大体积文件(上传的壁纸、本地数据库等;默认不勾选)';
1975
+ readonly 'export.category.pluginFiles': '插件放在 ~/.dsh 根下的配置文件(白名单 + plugin-config/ 目录,整文件复制)';
1976
+ readonly 'export.category.self': '配置管理器自身的设置(同步/自动同步/分区选择/UI 偏好/市场;sync-*.json 等,不含凭据值)';
1977
+ readonly 'export.category.pluginData': '插件在系统主目录下的数据(如 ~/.dsh-wallpaper-engine、~/.mnemon 的配置与状态;可再生成缓存绝不收录)';
1978
+ readonly 'export.category.pluginAssets': '插件的大体积文件(上传的壁纸、本地数据库等;默认不勾选)';
1961
1979
  readonly 'export.category.mcp': 'MCP 服务器组合配置(需重启生效)';
1962
1980
  readonly 'export.category.prompts': 'System Prompt / Plan Mode 提示';
1963
1981
  readonly 'export.category.skills': '用户技能文件(~/.dsh/skills)';
@@ -4164,6 +4182,23 @@ interface RollbackPreviewFile {
4164
4182
  removed: number | null;
4165
4183
  /** true = 本次没有可信行数(added/removed 均为 null)。**宁可说「不知道」,也绝不谎报行数。** */
4166
4184
  truncated: boolean;
4185
+ /**
4186
+ * true = 该文件在留点**之后**是**别的会话**写的(写入归属台账证明;见 core/rollback-attribution.ts)。
4187
+ * 界面据此默认不勾选它(用户仍可勾上)。缺字段 / false = **没有归属证据**,
4188
+ * 而不是「本会话写的」——「未判定」与「确知是本会话」必须分开。
4189
+ */
4190
+ foreign?: boolean;
4191
+ /** 那些「别的会话」的会话 id(仅当 foreign 为 true) */
4192
+ foreignSessionId?: string;
4193
+ /** 那次写入的时间(毫秒;仅当 foreign 为 true) */
4194
+ foreignAt?: number;
4195
+ /**
4196
+ * true = 这次回退**不会改动它**(与其它对话的改动冲突 / 无法安全合并)。行级统计为 null ——
4197
+ * 「不改动」不是「0 行变化」,界面必须分开说。
4198
+ */
4199
+ skipped?: boolean;
4200
+ /** 不改动的机器原因(界面映射成字典键,绝不裸渲染) */
4201
+ mergeReason?: string;
4167
4202
  }
4168
4203
  interface RollbackWorkspacePreview {
4169
4204
  /** 将被写回(覆盖)的条目数 */
@@ -4285,6 +4320,12 @@ interface RollbackFileDiffResult {
4285
4320
  identical: boolean;
4286
4321
  degraded: boolean;
4287
4322
  reason?: RollbackFileDiffReason;
4323
+ /** true = 「回退后」一侧显示的是三方合并的结果(不是记录里的原文) */
4324
+ merged?: boolean;
4325
+ /** true = 这次回退不会改动它(冲突 / 无法安全合并)——此时两侧相同 */
4326
+ mergeSkipped?: boolean;
4327
+ /** 不改动的机器原因(界面映射成字典键) */
4328
+ mergeReason?: string;
4288
4329
  }
4289
4330
  interface RollbackSegmentResult {
4290
4331
  state: string;
@@ -4310,6 +4351,22 @@ interface RollbackSegmentResult {
4310
4351
  createdRefusedReason?: string | null;
4311
4352
  /** 工作区段:实际写回的字节数(宿主回传才有) */
4312
4353
  writtenBytes?: number;
4354
+ /** 因归属隔离(B)跳过的记录条目数:该文件在留点后被**别的会话**写过(宿主回传才有) */
4355
+ foreignSkipped?: number;
4356
+ /** 因路径选择(A)跳过的记录条目数(宿主回传才有) */
4357
+ selectionSkipped?: number;
4358
+ /** 三方合并成功、写回「撤销本会话 + 保留对方改动」的条目数(宿主回传才有) */
4359
+ mergedCount?: number;
4360
+ mergedPaths?: string[];
4361
+ /** 冲突:两侧改到同一处 ⇒ 未写(宿主回传才有) */
4362
+ conflictCount?: number;
4363
+ conflictPaths?: string[];
4364
+ /** 无法安全撤销 ⇒ 未写(宿主回传才有) */
4365
+ unavailableCount?: number;
4366
+ unavailablePaths?: string[];
4367
+ /** 写入前发现又被改过(并发)⇒ 未写(宿主回传才有) */
4368
+ concurrentCount?: number;
4369
+ concurrentPaths?: string[];
4313
4370
  /** 兼容形态:宿主只回传计数时也算得出来 */
4314
4371
  createdAfter?: RollbackCount;
4315
4372
  createdAfterTruncated?: boolean;
@@ -4348,6 +4405,13 @@ interface RollbackRewindOptions {
4348
4405
  segments?: readonly RollbackState[];
4349
4406
  allowPartial?: boolean;
4350
4407
  preRewindGuard?: RollbackGuardPolicy;
4408
+ /**
4409
+ * 路径级选择性恢复(2026-10-17 定案 A):只恢复这些路径。
4410
+ *
4411
+ * 缺省 = 全量口径(一字不变)。**空数组是无效输入**(宿主 400)—— 「一个都没勾」由界面拦住,
4412
+ * 绝不让它变成「整次回退静默 noop」。
4413
+ */
4414
+ onlyPaths?: readonly string[];
4351
4415
  }
4352
4416
  /** 端口:由 src/client/recovery/recovery-api.ts 实现(Partial 消费,缺失时界面如实说能力缺失)。 */
4353
4417
  interface RollbackPointPort {
@@ -4689,20 +4753,20 @@ declare const zh$4: {
4689
4753
  readonly 'section.settings': '设置';
4690
4754
  readonly 'section.ui': 'UI 偏好';
4691
4755
  readonly 'section.providers': 'Provider 与模型';
4692
- readonly 'section.plugins': '插件';
4756
+ readonly 'section.plugins': '插件清单';
4693
4757
  readonly 'section.mcp': 'MCP 服务器';
4694
4758
  readonly 'section.prompts': '提示词';
4695
4759
  readonly 'section.skills': '技能';
4696
4760
  readonly 'section.agentPresets': 'Agent 预设';
4697
4761
  readonly 'section.agentInstructions': '全局指令';
4698
4762
  readonly 'section.workspaces': '工作区';
4699
- readonly 'section.pluginFiles': '其他插件零散文件';
4763
+ readonly 'section.pluginFiles': '插件散落配置';
4700
4764
  readonly 'section.credentialsStatus': '凭据状态';
4701
4765
  readonly 'section.secrets': '密钥';
4702
4766
  readonly 'section.sessions': '历史会话';
4703
- readonly 'section.self': '本插件自身配置';
4704
- readonly 'section.pluginData': '其他插件数据';
4705
- readonly 'section.pluginAssets': '其他插件大文件';
4767
+ readonly 'section.self': '配置管理器设置';
4768
+ readonly 'section.pluginData': '插件数据目录';
4769
+ readonly 'section.pluginAssets': '插件大文件';
4706
4770
  readonly 'view.about': '关于';
4707
4771
  readonly 'about.snippet.environment': '### 环境 / Environment';
4708
4772
  readonly 'about.releaseNotes.rateLimited': 'GitHub API 速率限制(Rate limit exceeded),请稍后再试或直接在 GitHub 上查看。';
@@ -4842,6 +4906,26 @@ declare const zh$4: {
4842
4906
  readonly 'picker.sensitive': '敏感';
4843
4907
  readonly 'picker.showAll': '显示全部(共 {count} 条)';
4844
4908
  readonly 'picker.summaryImport': '将导入 {sections} 个分区 · {units} 个条目';
4909
+ readonly 'picker.pluginRoot': '插件';
4910
+ readonly 'picker.pluginLeafPlugin': '插件清单条目';
4911
+ readonly 'picker.pluginLeafConfig': '数据目录(配置)';
4912
+ readonly 'picker.pluginLeafAssets': '大文件(壁纸 / 数据库)';
4913
+ readonly 'picker.pluginAreaUnread': '未读取';
4914
+ readonly 'picker.pluginAreaAbsent': '无内容';
4915
+ readonly 'picker.pluginViewEmpty': '插件清单还没读到,先读取「插件」分区。';
4916
+ readonly 'picker.bucketPlugins': '插件分区的其他条目(补丁行 / pnpm-workspace.yaml)';
4917
+ readonly 'picker.bucketLooseFiles': '未归属到插件的配置(~/.dsh 根)';
4918
+ readonly 'picker.bucketHint': '这些条目没有可证明的插件归属,不猜;展开可逐条勾选。';
4919
+ readonly 'picker.groupHint.extensions': '这一组里的插件分区合并成一棵按插件分组的树:默认只显示一个「插件」区块,展开它才列出各个插件,插件名下面是它的清单条目、数据目录与大文件。只显示能证明的归属(内置来源表 + 插件自述 dsh.dataDirs)——「插件散落配置」没有包名信息,因此留在「未归属」里;带「设备相关」的分区默认不勾选,跨设备导入时可能不适用。';
4920
+ readonly 'group.general': '通用';
4921
+ readonly 'group.ai': 'AI';
4922
+ readonly 'group.extensions': '插件与扩展';
4923
+ readonly 'group.mcp': 'MCP / 工具';
4924
+ readonly 'group.customization': '定制';
4925
+ readonly 'group.automation': '自动化';
4926
+ readonly 'group.workspace': '工作区';
4927
+ readonly 'group.ui': '界面';
4928
+ readonly 'group.optional': '可选数据';
4845
4929
  readonly 'import.nothingSelected': '当前没有勾选任何要导入的内容,请把需要导入的分区或条目勾上。';
4846
4930
  readonly 'import.excludedByUser': '本次有 {count} 项被你取消勾选:未导入,也未进快照。';
4847
4931
  readonly 'import.consult.hint': '只读分析:先看结论与依据,确认后再选择要导入的内容。';
@@ -6529,6 +6613,14 @@ declare const zh$1: {
6529
6613
  readonly 'rollback.preview.notice.skipped': '{n} 项在留点时被跳过(未记录;已计入证明名单 ⇒ 不参与删除判据)';
6530
6614
  readonly 'rollback.preview.notice.truncated': '该点捕获被上限截断(超单文件上限 / 字节预算 / 文件数预算);删除判据另看证明名单(路径索引)是否完整';
6531
6615
  readonly 'rollback.preview.files.title': '将被回退的文件:';
6616
+ readonly 'rollback.preview.select.foreign': '其它对话改过';
6617
+ readonly 'rollback.preview.select.toggle': '「{path}」:勾上则这次回退恢复它';
6618
+ readonly 'rollback.result.foreignSkipped': '{n} 个文件在留点后被其它对话改过,这次没有回退它们';
6619
+ readonly 'rollback.result.merged': '{n} 个文件已合并:撤销本次对话的改动,保留其它对话的改动';
6620
+ readonly 'rollback.result.conflict': '{n} 个文件两侧改到同一处,已保持原样 —— 需要你手工处理';
6621
+ readonly 'rollback.result.unavailable': '{n} 个文件无法安全撤销(缺少中间版本 / 二进制 / 差异过大),已保持原样';
6622
+ readonly 'rollback.result.concurrent': '{n} 个文件在回退过程中又被改过,已保持原样';
6623
+ readonly 'rollback.result.selectionSkipped': '{n} 个文件按你的勾选跳过了';
6532
6624
  readonly 'rollback.preview.files.empty': '工作区与该回退点一致,无需恢复';
6533
6625
  readonly 'rollback.preview.files.unavailable': '该回退点的工作区态没有可用的记录(原因:{reason}),无需恢复';
6534
6626
  readonly 'rollback.preview.files.unavailableSession': '该回退点的工作区态没有可用的记录(原因:{reason});会话仍会回退到该点之前';
@@ -6538,6 +6630,9 @@ declare const zh$1: {
6538
6630
  readonly 'rollback.preview.file.removed': '−{n}';
6539
6631
  /** 整文件删除(用户 2026-10-12 第 6 条):写「删除」而不是 `+0 −N` —— 那是两件事。 */
6540
6632
  readonly 'rollback.preview.file.deleted': '删除';
6633
+ readonly 'rollback.preview.file.skipped': '不会改动(与其它对话的改动冲突,或无法安全合并)';
6634
+ readonly 'rollback.diff.merged': '下面是合并后的结果:撤销本对话的改动、保留其它对话的改动';
6635
+ readonly 'rollback.diff.skipped': '这次回退不会改动它(与其它对话的改动冲突,或无法安全合并)';
6541
6636
  readonly 'rollback.preview.file.unknown': '行数未统计';
6542
6637
  readonly 'rollback.preview.file.unknownHint': '这些文件的行数没有统计出来(文件过大、不是文本或内容副本读不出来)—— 回退仍会照常覆盖它们。';
6543
6638
  readonly 'rollback.diff.viewHint': '查看差异:{path}';