dsh-config-manager 0.1.12 → 0.1.13

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 (107) hide show
  1. package/README.md +30 -0
  2. package/README.zh-CN.md +30 -0
  3. package/lib/adapters/test-helpers.d.ts +4 -2
  4. package/lib/adapters/test-helpers.js +11 -3
  5. package/lib/adapters/test-helpers.js.map +1 -1
  6. package/lib/cli/index.d.ts +44 -0
  7. package/lib/cli/index.js +230 -0
  8. package/lib/cli/index.js.map +1 -0
  9. package/lib/client.d.ts +260 -5
  10. package/lib/client.js +2519 -820
  11. package/lib/client.js.map +1 -1
  12. package/lib/core/analyzer.d.ts +18 -1
  13. package/lib/core/analyzer.js +29 -0
  14. package/lib/core/analyzer.js.map +1 -1
  15. package/lib/core/backup.d.ts +3 -1
  16. package/lib/core/backup.js +70 -0
  17. package/lib/core/backup.js.map +1 -1
  18. package/lib/core/exporter.d.ts +11 -0
  19. package/lib/core/exporter.js +11 -0
  20. package/lib/core/exporter.js.map +1 -1
  21. package/lib/core/importer.d.ts +3 -0
  22. package/lib/core/importer.js +1 -0
  23. package/lib/core/importer.js.map +1 -1
  24. package/lib/core/index.d.ts +1 -0
  25. package/lib/core/index.js +1 -0
  26. package/lib/core/index.js.map +1 -1
  27. package/lib/core/restore.d.ts +92 -0
  28. package/lib/core/restore.js +505 -0
  29. package/lib/core/restore.js.map +1 -0
  30. package/lib/core/run-registry.d.ts +65 -0
  31. package/lib/core/run-registry.js +120 -0
  32. package/lib/core/run-registry.js.map +1 -0
  33. package/lib/core/types.d.ts +20 -0
  34. package/lib/core/types.js.map +1 -1
  35. package/lib/index.d.ts +45 -0
  36. package/lib/index.js +442 -8
  37. package/lib/index.js.map +1 -1
  38. package/lib/sync/fs.d.ts +18 -0
  39. package/lib/sync/fs.js +47 -0
  40. package/lib/sync/fs.js.map +1 -0
  41. package/lib/sync/git/git-transport.d.ts +87 -0
  42. package/lib/sync/git/git-transport.js +333 -0
  43. package/lib/sync/git/git-transport.js.map +1 -0
  44. package/lib/sync/layout.d.ts +29 -0
  45. package/lib/sync/layout.js +166 -0
  46. package/lib/sync/layout.js.map +1 -0
  47. package/lib/sync/sync-config.d.ts +16 -0
  48. package/lib/sync/sync-config.js +67 -0
  49. package/lib/sync/sync-config.js.map +1 -0
  50. package/lib/sync/sync-engine.d.ts +96 -0
  51. package/lib/sync/sync-engine.js +236 -0
  52. package/lib/sync/sync-engine.js.map +1 -0
  53. package/lib/sync/sync-state.d.ts +31 -0
  54. package/lib/sync/sync-state.js +103 -0
  55. package/lib/sync/sync-state.js.map +1 -0
  56. package/lib/sync/transport.d.ts +50 -0
  57. package/lib/sync/transport.js +22 -0
  58. package/lib/sync/transport.js.map +1 -0
  59. package/package.json +4 -1
  60. package/src/adapters/test-helpers.ts +10 -3
  61. package/src/cli/index.ts +278 -0
  62. package/src/client/ConfigManagerSection.tsx +49 -14
  63. package/src/client/api.ts +53 -5
  64. package/src/client/client-types.ts +0 -6
  65. package/src/client/common/ProgressBar.tsx +31 -19
  66. package/src/client/common/progress-view-edge.test.ts +60 -0
  67. package/src/client/common/progress-view.test.ts +79 -0
  68. package/src/client/common/progress-view.ts +94 -0
  69. package/src/client/config-manager.module.css +99 -20
  70. package/src/client/export/ExportView.tsx +63 -49
  71. package/src/client/import/ImportWizardView.tsx +107 -66
  72. package/src/client/index.ts +22 -0
  73. package/src/client/locales.ts +53 -0
  74. package/src/client/run-store.test.ts +500 -0
  75. package/src/client/run-store.ts +772 -0
  76. package/src/client/snapshots/SnapshotsPanel.tsx +240 -0
  77. package/src/client/sync/SyncSettingsView.tsx +261 -0
  78. package/src/client/sync/sync-api.test.ts +117 -0
  79. package/src/client/sync/sync-api.ts +132 -0
  80. package/src/client/sync/sync-locales.ts +77 -0
  81. package/src/client/sync/sync-view.test.ts +211 -0
  82. package/src/client/sync/sync-view.ts +202 -0
  83. package/src/core/analyzer.ts +50 -1
  84. package/src/core/backup.ts +78 -2
  85. package/src/core/exporter.ts +21 -0
  86. package/src/core/importer.ts +4 -0
  87. package/src/core/index.ts +5 -0
  88. package/src/core/restore.ts +587 -0
  89. package/src/core/run-progress.test.ts +237 -0
  90. package/src/core/run-registry.test.ts +139 -0
  91. package/src/core/run-registry.ts +166 -0
  92. package/src/core/smoke.test.ts +5 -0
  93. package/src/core/types.ts +22 -0
  94. package/src/index.ts +476 -8
  95. package/src/security/security.test.ts +5 -0
  96. package/src/sync/fs.ts +46 -0
  97. package/src/sync/git/git-transport.test.ts +456 -0
  98. package/src/sync/git/git-transport.ts +390 -0
  99. package/src/sync/layout.test.ts +226 -0
  100. package/src/sync/layout.ts +183 -0
  101. package/src/sync/sync-config.ts +72 -0
  102. package/src/sync/sync-engine.test.ts +299 -0
  103. package/src/sync/sync-engine.ts +318 -0
  104. package/src/sync/sync-state.test.ts +118 -0
  105. package/src/sync/sync-state.ts +123 -0
  106. package/src/sync/transport.test.ts +110 -0
  107. package/src/sync/transport.ts +73 -0
package/lib/client.js CHANGED
@@ -17,7 +17,11 @@ const CONFIG_MANAGER_API = {
17
17
  upload: "/api/dsh-config-manager/upload",
18
18
  analyze: "/api/dsh-config-manager/analyze",
19
19
  plan: "/api/dsh-config-manager/plan",
20
- execute: "/api/dsh-config-manager/execute"
20
+ execute: "/api/dsh-config-manager/execute",
21
+ progress: "/api/dsh-config-manager/progress",
22
+ runs: "/api/dsh-config-manager/runs",
23
+ snapshots: "/api/dsh-config-manager/snapshots",
24
+ restore: "/api/dsh-config-manager/restore"
21
25
  };
22
26
  /** 导出请求超时(ms):与 Host 半 ROUTE_TIMEOUT_MS 对齐,防止宿主卡死时 UI 无限等待 */
23
27
  const EXPORT_TIMEOUT_MS = 300 * 1e3;
@@ -29,7 +33,7 @@ var ConfigManagerApiError = class extends Error {
29
33
  }
30
34
  };
31
35
  /** 解析 JSON 响应;非 2xx 时抛出带路由 error 消息的 ConfigManagerApiError */
32
- async function readJson(response) {
36
+ async function readJson$1(response) {
33
37
  const notMountedMessage = "config-manager 服务未挂载(插件未加载):请确认 profile 中已安装 dsh-config-manager 并重启 DSH";
34
38
  let body;
35
39
  try {
@@ -61,7 +65,7 @@ var ConfigManagerApi = class {
61
65
  exportPassword = null;
62
66
  /** 健康/版本检查(主页横幅:插件版本 / DSH 版本 / 平台) */
63
67
  async status() {
64
- return readJson(await fetch(CONFIG_MANAGER_API.status));
68
+ return readJson$1(await fetch(CONFIG_MANAGER_API.status));
65
69
  }
66
70
  /** ExportPort.export:调用 Host 侧导出编排(core Exporter)。加密密码随请求体传输(仅内存)。
67
71
  * 带 AbortController 超时:宿主若卡死,客户端得到明确错误而不是永远停在进度条。 */
@@ -73,7 +77,7 @@ var ConfigManagerApi = class {
73
77
  const controller = new AbortController();
74
78
  const timer = setTimeout(() => controller.abort(), EXPORT_TIMEOUT_MS);
75
79
  try {
76
- return await readJson(await fetch(CONFIG_MANAGER_API.export, {
80
+ return await readJson$1(await fetch(CONFIG_MANAGER_API.export, {
77
81
  method: "POST",
78
82
  headers: { "content-type": "application/json" },
79
83
  body: JSON.stringify(body),
@@ -129,14 +133,14 @@ var ConfigManagerApi = class {
129
133
  }
130
134
  /** 上传用户选择的 ZIP 到 Host 受控临时目录,返回可引用的 zipPath(dsh-ssh 同款原始字节上传) */
131
135
  async upload(file) {
132
- return readJson(await fetch(CONFIG_MANAGER_API.upload + query({ name: file.name }), {
136
+ return readJson$1(await fetch(CONFIG_MANAGER_API.upload + query({ name: file.name }), {
133
137
  method: "POST",
134
138
  body: file
135
139
  }));
136
140
  }
137
141
  /** ImportPort.analyzeImport:零写入分析(校验/兼容性/差异/路径/秘密检测) */
138
142
  async analyzeImport(zipPath) {
139
- return readJson(await fetch(CONFIG_MANAGER_API.analyze, {
143
+ return readJson$1(await fetch(CONFIG_MANAGER_API.analyze, {
140
144
  method: "POST",
141
145
  headers: { "content-type": "application/json" },
142
146
  body: JSON.stringify({ zipPath })
@@ -144,7 +148,7 @@ var ConfigManagerApi = class {
144
148
  }
145
149
  /** ImportPort.createImportPlan:用用户决策(冲突/路径映射/策略)生成最终计划(Dry Run 零写入) */
146
150
  async createImportPlan(zipPath, decisions) {
147
- return readJson(await fetch(CONFIG_MANAGER_API.plan, {
151
+ return readJson$1(await fetch(CONFIG_MANAGER_API.plan, {
148
152
  method: "POST",
149
153
  headers: { "content-type": "application/json" },
150
154
  body: JSON.stringify({
@@ -164,12 +168,83 @@ var ConfigManagerApi = class {
164
168
  rollbackOnError: opts.rollbackOnError
165
169
  }
166
170
  };
167
- return readJson(await fetch(CONFIG_MANAGER_API.execute, {
171
+ return readJson$1(await fetch(CONFIG_MANAGER_API.execute, {
168
172
  method: "POST",
169
173
  headers: { "content-type": "application/json" },
170
174
  body: JSON.stringify(payload)
171
175
  }));
172
176
  }
177
+ /** m1:查询单个 run 的实时状态(执行中轮询 / 刷新恢复用;404 = 已过保留期或不存在) */
178
+ async progress(runId) {
179
+ return readJson$1(await fetch(CONFIG_MANAGER_API.progress + query({ runId })));
180
+ }
181
+ /** m1:列出当前活跃(running)的 run(刷新后重新订阅进行中任务的入口) */
182
+ async runs() {
183
+ return readJson$1(await fetch(CONFIG_MANAGER_API.runs));
184
+ }
185
+ /** 列出全部快照元信息(createdAt 倒序;含 status/条目数/宿主文件数/插件数) */
186
+ async snapshots() {
187
+ return (await readJson$1(await fetch(CONFIG_MANAGER_API.snapshots))).snapshots;
188
+ }
189
+ /** 快照恢复:dryRun=true 只取动作计划(零写入);false 执行并返回诚实报告 */
190
+ async restoreSnapshot(snapshotId, dryRun) {
191
+ return readJson$1(await fetch(CONFIG_MANAGER_API.restore, {
192
+ method: "POST",
193
+ headers: { "content-type": "application/json" },
194
+ body: JSON.stringify({
195
+ snapshotId,
196
+ dryRun
197
+ })
198
+ }));
199
+ }
200
+ };
201
+ //#endregion
202
+ //#region src/ui/conflict-view.ts
203
+ var ConflictCollector = class {
204
+ plan;
205
+ decisions = /* @__PURE__ */ new Map();
206
+ constructor(plan) {
207
+ this.plan = plan;
208
+ }
209
+ /** 需要用户决策的冲突项(kind === 'Conflict') */
210
+ get conflicts() {
211
+ return this.plan.items.filter((i) => i.kind === "Conflict");
212
+ }
213
+ /** 设置某项决策;未出现在冲突列表中的 id 忽略并返回 false */
214
+ resolve(itemId, resolution) {
215
+ if (!this.conflicts.some((i) => i.id === itemId)) return false;
216
+ this.decisions.set(itemId, resolution);
217
+ return true;
218
+ }
219
+ /** 单项当前决策(未决策返回 null) */
220
+ decisionOf(itemId) {
221
+ return this.decisions.get(itemId) ?? null;
222
+ }
223
+ /** 仍未解决(review 或未决策)的冲突项 */
224
+ unresolved() {
225
+ return this.conflicts.filter((i) => {
226
+ const d = this.decisions.get(i.id);
227
+ return d === void 0 || d === "review";
228
+ });
229
+ }
230
+ get hasUnresolved() {
231
+ return this.unresolved().length > 0;
232
+ }
233
+ /** 转 core 决策表(供 createImportPlan / executeImportPlan 使用) */
234
+ toResolutions() {
235
+ const out = {};
236
+ for (const [id, res] of this.decisions) out[id] = res;
237
+ return out;
238
+ }
239
+ /** 构造视图项列表(React 绑定用):含当前/导入摘要占位 */
240
+ viewItems() {
241
+ return this.conflicts.map((item) => ({
242
+ item,
243
+ currentSummary: item.detail?.split("\n")[0],
244
+ importedSummary: void 0,
245
+ resolution: this.decisionOf(item.id)
246
+ }));
247
+ }
173
248
  };
174
249
  //#endregion
175
250
  //#region src/ui/types.ts
@@ -570,189 +645,6 @@ var ExportFlow = class {
570
645
  }
571
646
  };
572
647
  //#endregion
573
- //#region \0config-manager-css:/home/runner/work/dsh-config-manager/dsh-config-manager/src/client/config-manager.module.css.js
574
- const css = "._367UGW_section{height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex-direction:column;gap:10px;font-size:13px;display:flex}._367UGW_sectionHeader{flex:none;align-items:center;gap:10px;display:flex}._367UGW_viewTabs{border-bottom:1px solid var(--dsw-alias-border-l1);flex:1;gap:2px;display:flex}._367UGW_viewTab{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-bottom:2px solid #0000;border-radius:6px 6px 0 0;padding:7px 14px;font-size:13px}._367UGW_viewTab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}._367UGW_viewTab[data-active]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:600}._367UGW_sectionBody{flex-direction:column;flex:1;min-height:0;display:flex;overflow-y:auto}._367UGW_viewBody{flex-direction:column;gap:12px;padding:4px 2px 16px;display:flex}._367UGW_iconButton{width:26px;height:26px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;font-size:15px;display:inline-flex}._367UGW_iconButton:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}._367UGW_sectionTitleBlock{flex-direction:column;gap:2px;display:flex}._367UGW_sectionTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:15px;font-weight:700}._367UGW_sectionSubtitle{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:1.5}._367UGW_primaryButton{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-button-info-fill);cursor:pointer;white-space:nowrap;border:none;border-radius:8px;align-items:center;gap:6px;padding:6px 14px;font-size:13px;font-weight:600;display:inline-flex}._367UGW_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}._367UGW_primaryButton:disabled{opacity:.5;cursor:default}._367UGW_ghostButton{color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;align-items:center;gap:6px;padding:5px 12px;font-size:12px;display:inline-flex}._367UGW_ghostButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}._367UGW_ghostButton:disabled{opacity:.45;cursor:default}._367UGW_dangerButton{color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;padding:5px 12px;font-size:12px}._367UGW_dangerButton:hover:not(:disabled){background:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-label-primary-foreground)}._367UGW_dangerButton:disabled{opacity:.45;cursor:default}._367UGW_badge{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;line-height:1.6;display:inline-block}._367UGW_badgeInfo{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}._367UGW_badgeOk{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}._367UGW_badgeWarn{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}._367UGW_badgeError{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}._367UGW_banner{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;border-radius:8px;padding:8px 12px;font-size:12.5px;line-height:1.5}._367UGW_banner[data-kind=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}._367UGW_banner[data-kind=error]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}._367UGW_banner[data-kind=info]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}._367UGW_banner[data-kind=warn]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}._367UGW_card{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;flex:none;gap:10px;padding:12px;display:flex}._367UGW_field{flex-direction:column;gap:5px;display:flex}._367UGW_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}._367UGW_input{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);resize:vertical;border-radius:8px;outline:none;padding:7px 10px;font-family:inherit;font-size:13px}._367UGW_input:focus{border-color:var(--dsw-alias-state-business-primary)}._367UGW_input::placeholder{color:var(--dsw-alias-label-tertiary)}._367UGW_input:disabled{opacity:.55}._367UGW_hint{color:var(--dsw-alias-label-tertiary);font-size:11.5px;line-height:1.5}._367UGW_formError{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}._367UGW_checkboxRow{color:var(--dsw-alias-label-primary);cursor:pointer;align-items:flex-start;gap:8px;font-size:13px;display:flex}._367UGW_checkboxRow input{flex:none;margin-top:2px}._367UGW_radioLabel{color:var(--dsw-alias-label-primary);cursor:pointer;align-items:center;gap:6px;font-size:13px;display:inline-flex}._367UGW_secretFields{grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px 12px;padding-top:4px;display:grid}._367UGW_hiddenFile{display:none}._367UGW_modeTabs{border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;gap:2px;display:flex}._367UGW_modeTab{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-bottom:2px solid #0000;padding:6px 12px;font-size:12.5px}._367UGW_modeTab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}._367UGW_modeTab[data-active]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:600}._367UGW_modeHint{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}._367UGW_groupList{flex-direction:column;gap:10px;display:flex}._367UGW_groupCard{gap:8px}._367UGW_groupHeader{flex-wrap:wrap;align-items:baseline;gap:8px;display:flex}._367UGW_groupLabel{color:var(--dsw-alias-label-primary);font-size:12.5px;font-weight:700}._367UGW_groupNote{color:var(--dsw-alias-label-tertiary);font-size:11.5px}._367UGW_groupItems{flex-direction:column;gap:6px;display:flex}._367UGW_categoryItem{flex-wrap:wrap;align-items:center;gap:8px;display:inline-flex}._367UGW_categoryName{font-weight:500}._367UGW_categoryDesc{color:var(--dsw-alias-label-tertiary);font-size:11.5px}._367UGW_optionsCard{gap:8px}._367UGW_warnList{margin:4px 0 0;padding-left:18px}._367UGW_actionRow{flex-wrap:wrap;align-items:center;gap:10px;display:flex}._367UGW_progressBlock{flex-direction:column;flex:none;gap:6px;display:flex}._367UGW_progressMeta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:12px;font-size:12px;display:flex}._367UGW_progressLabel{font-weight:600}._367UGW_progressDetail{color:var(--dsw-alias-label-tertiary)}._367UGW_progressPercent{font-variant-numeric:tabular-nums;margin-left:auto}._367UGW_progressTrack{background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;height:8px;overflow:hidden}._367UGW_progressBar{background:var(--dsw-alias-state-business-primary);border-radius:999px;height:100%;transition:width .12s linear}._367UGW_progressBarDone{background:var(--dsw-alias-state-success-primary)}._367UGW_progressIndeterminate{width:40%;animation:1s ease-in-out infinite _367UGW_dshCmIndeterminate}@keyframes _367UGW_dshCmIndeterminate{0%{margin-left:-40%}to{margin-left:100%}}._367UGW_reportView{flex-direction:column;gap:10px;display:flex}._367UGW_statRow{flex-wrap:wrap;align-items:center;gap:6px;display:flex}._367UGW_reportText{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;max-height:260px;color:var(--dsw-alias-label-primary);border-radius:8px;margin:0;padding:10px 12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;line-height:1.6;overflow:auto}._367UGW_reportFooter{justify-content:flex-end;gap:8px;display:flex}._367UGW_rollbackBox{border:1px solid var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-primary);border-radius:8px;flex-direction:column;gap:6px;padding:10px 12px;display:flex}._367UGW_rollbackBox ._367UGW_reportText{border-color:var(--dsw-alias-border-l2)}._367UGW_errorBanner{border:1px solid var(--dsw-alias-state-error-primary);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border-radius:8px;flex-direction:column;gap:6px;padding:10px 12px;display:flex}._367UGW_errorTitle{color:var(--dsw-alias-state-error-primary);font-size:13px;font-weight:700}._367UGW_errorReason{white-space:pre-wrap;word-break:break-all;color:var(--dsw-alias-label-secondary);margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px}._367UGW_errorAction{color:var(--dsw-alias-label-secondary);font-size:12.5px}._367UGW_errorActionLabel{color:var(--dsw-alias-state-business-primary)}._367UGW_errorItem{color:var(--dsw-alias-label-tertiary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px}._367UGW_errorFooter{justify-content:flex-end;display:flex}._367UGW_errorList{flex-direction:column;gap:4px;display:flex}._367UGW_errorLine{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;color:var(--dsw-alias-state-error-primary);border-radius:6px;margin:0;padding:6px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px}._367UGW_conflictList{flex-direction:column;gap:10px;display:flex}._367UGW_conflictItem{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;flex-direction:column;gap:6px;padding:10px 12px;display:flex}._367UGW_conflictHead{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._367UGW_conflictId{color:var(--dsw-alias-label-primary);font-size:12.5px;font-weight:600}._367UGW_severityError{color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary);border-radius:999px;padding:0 6px;font-size:10.5px}._367UGW_conflictDetail{background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;color:var(--dsw-alias-label-secondary);border-radius:6px;margin:0;padding:6px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px}._367UGW_conflictOptions{flex-wrap:wrap;gap:14px;display:flex}._367UGW_pathMappingList{flex-direction:column;gap:8px;display:flex}._367UGW_pathRow{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:10px;padding:10px 12px;display:grid}._367UGW_pathOld,._367UGW_pathNew{flex-direction:column;gap:4px;min-width:0;display:flex}._367UGW_pathValue{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary);margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;overflow:hidden}._367UGW_pathIssueKind{color:var(--dsw-alias-state-warn-primary);font-size:10.5px}._367UGW_secretsList{flex-direction:column;gap:10px;display:flex}._367UGW_empty{text-align:center;color:var(--dsw-alias-label-tertiary);padding:22px 12px;font-size:12.5px}._367UGW_spinnerWrap{align-items:center;gap:6px;display:inline-flex}._367UGW_spinner{border:2px solid var(--dsw-alias-state-business-primary);vertical-align:-1px;border-top-color:#0000;border-radius:50%;flex:none;width:11px;height:11px;animation:.8s linear infinite _367UGW_dshCmSpin;display:inline-block}._367UGW_spinnerLabel{color:var(--dsw-alias-label-secondary);font-size:12px}@keyframes _367UGW_dshCmSpin{to{transform:rotate(360deg)}}";
575
- const tagId = "dsh-config-manager/config-manager.module.css";
576
- if (typeof document !== "undefined" && document.querySelector("style[data-tag=\"dsh-config-manager/config-manager.module.css\"]") === null) {
577
- const tag = document.createElement("style");
578
- tag.dataset.plugin = "dsh-config-manager";
579
- tag.dataset.tag = tagId;
580
- tag.textContent = css;
581
- document.head.appendChild(tag);
582
- }
583
- var config_manager_module_css_default = {
584
- "groupHeader": "_367UGW_groupHeader",
585
- "input": "_367UGW_input",
586
- "errorList": "_367UGW_errorList",
587
- "iconButton": "_367UGW_iconButton",
588
- "primaryButton": "_367UGW_primaryButton",
589
- "errorLine": "_367UGW_errorLine",
590
- "pathRow": "_367UGW_pathRow",
591
- "fieldLabel": "_367UGW_fieldLabel",
592
- "spinner": "_367UGW_spinner",
593
- "groupNote": "_367UGW_groupNote",
594
- "banner": "_367UGW_banner",
595
- "errorTitle": "_367UGW_errorTitle",
596
- "modeHint": "_367UGW_modeHint",
597
- "conflictDetail": "_367UGW_conflictDetail",
598
- "dangerButton": "_367UGW_dangerButton",
599
- "badgeError": "_367UGW_badgeError",
600
- "reportView": "_367UGW_reportView",
601
- "groupLabel": "_367UGW_groupLabel",
602
- "conflictId": "_367UGW_conflictId",
603
- "conflictHead": "_367UGW_conflictHead",
604
- "modeTab": "_367UGW_modeTab",
605
- "groupList": "_367UGW_groupList",
606
- "conflictOptions": "_367UGW_conflictOptions",
607
- "progressLabel": "_367UGW_progressLabel",
608
- "pathIssueKind": "_367UGW_pathIssueKind",
609
- "spinnerWrap": "_367UGW_spinnerWrap",
610
- "dshCmIndeterminate": "_367UGW_dshCmIndeterminate",
611
- "sectionBody": "_367UGW_sectionBody",
612
- "errorBanner": "_367UGW_errorBanner",
613
- "dshCmSpin": "_367UGW_dshCmSpin",
614
- "errorReason": "_367UGW_errorReason",
615
- "badge": "_367UGW_badge",
616
- "progressPercent": "_367UGW_progressPercent",
617
- "errorActionLabel": "_367UGW_errorActionLabel",
618
- "optionsCard": "_367UGW_optionsCard",
619
- "spinnerLabel": "_367UGW_spinnerLabel",
620
- "conflictList": "_367UGW_conflictList",
621
- "progressBar": "_367UGW_progressBar",
622
- "pathValue": "_367UGW_pathValue",
623
- "secretsList": "_367UGW_secretsList",
624
- "progressTrack": "_367UGW_progressTrack",
625
- "checkboxRow": "_367UGW_checkboxRow",
626
- "sectionHeader": "_367UGW_sectionHeader",
627
- "sectionTitleBlock": "_367UGW_sectionTitleBlock",
628
- "badgeWarn": "_367UGW_badgeWarn",
629
- "progressMeta": "_367UGW_progressMeta",
630
- "sectionTitle": "_367UGW_sectionTitle",
631
- "groupItems": "_367UGW_groupItems",
632
- "actionRow": "_367UGW_actionRow",
633
- "progressBarDone": "_367UGW_progressBarDone",
634
- "viewTabs": "_367UGW_viewTabs",
635
- "section": "_367UGW_section",
636
- "sectionSubtitle": "_367UGW_sectionSubtitle",
637
- "modeTabs": "_367UGW_modeTabs",
638
- "progressIndeterminate": "_367UGW_progressIndeterminate",
639
- "categoryDesc": "_367UGW_categoryDesc",
640
- "badgeInfo": "_367UGW_badgeInfo",
641
- "radioLabel": "_367UGW_radioLabel",
642
- "categoryName": "_367UGW_categoryName",
643
- "hiddenFile": "_367UGW_hiddenFile",
644
- "viewBody": "_367UGW_viewBody",
645
- "progressDetail": "_367UGW_progressDetail",
646
- "reportText": "_367UGW_reportText",
647
- "errorAction": "_367UGW_errorAction",
648
- "warnList": "_367UGW_warnList",
649
- "hint": "_367UGW_hint",
650
- "pathMappingList": "_367UGW_pathMappingList",
651
- "badgeOk": "_367UGW_badgeOk",
652
- "pathNew": "_367UGW_pathNew",
653
- "reportFooter": "_367UGW_reportFooter",
654
- "errorFooter": "_367UGW_errorFooter",
655
- "statRow": "_367UGW_statRow",
656
- "ghostButton": "_367UGW_ghostButton",
657
- "empty": "_367UGW_empty",
658
- "pathOld": "_367UGW_pathOld",
659
- "secretFields": "_367UGW_secretFields",
660
- "viewTab": "_367UGW_viewTab",
661
- "card": "_367UGW_card",
662
- "rollbackBox": "_367UGW_rollbackBox",
663
- "progressBlock": "_367UGW_progressBlock",
664
- "errorItem": "_367UGW_errorItem",
665
- "field": "_367UGW_field",
666
- "severityError": "_367UGW_severityError",
667
- "conflictItem": "_367UGW_conflictItem",
668
- "groupCard": "_367UGW_groupCard",
669
- "formError": "_367UGW_formError",
670
- "categoryItem": "_367UGW_categoryItem"
671
- };
672
- //#endregion
673
- //#region src/client/common/ui.tsx
674
- /** 统一按钮(primary=主操作 / ghost=次操作 / danger=危险操作) */
675
- function Button({ variant = "ghost", disabled, onClick, children, title, className }) {
676
- const cls = variant === "primary" ? config_manager_module_css_default.primaryButton : variant === "danger" ? config_manager_module_css_default.dangerButton : config_manager_module_css_default.ghostButton;
677
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
678
- type: "button",
679
- className: className !== void 0 ? `${cls} ${className}` : cls,
680
- disabled,
681
- title,
682
- onClick,
683
- children
684
- });
685
- }
686
- /** 状态徽章(info=业务色 / ok=成功 / warn=警告 / error=错误) */
687
- function Badge({ kind = "info", children }) {
688
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
689
- className: `${config_manager_module_css_default.badge} ${config_manager_module_css_default[`badge${kind[0].toUpperCase()}${kind.slice(1)}`] ?? ""}`,
690
- children
691
- });
692
- }
693
- /** 说明横幅(ok/error/info/warn 四态) */
694
- function Banner({ kind = "info", children }) {
695
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
696
- className: config_manager_module_css_default.banner,
697
- "data-kind": kind,
698
- children
699
- });
700
- }
701
- /** 卡片容器(bg-layer-2 + 圆角 + 细边框) */
702
- function Card({ children, className }) {
703
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
704
- className: className !== void 0 ? `${config_manager_module_css_default.card} ${className}` : config_manager_module_css_default.card,
705
- children
706
- });
707
- }
708
- /** 加载指示(旋转环 + 可选文案) */
709
- function Spinner({ label }) {
710
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
711
- className: config_manager_module_css_default.spinnerWrap,
712
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
713
- className: config_manager_module_css_default.spinner,
714
- "aria-hidden": "true"
715
- }), label !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
716
- className: config_manager_module_css_default.spinnerLabel,
717
- children: label
718
- })]
719
- });
720
- }
721
- /** 区块标题(页面内二级标题 + 可选副标题) */
722
- function SectionTitle({ title, subtitle }) {
723
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
724
- className: config_manager_module_css_default.sectionTitleBlock,
725
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
726
- className: config_manager_module_css_default.sectionTitle,
727
- children: title
728
- }), subtitle !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
729
- className: config_manager_module_css_default.sectionSubtitle,
730
- children: subtitle
731
- })]
732
- });
733
- }
734
- /** 空状态占位 */
735
- function Empty({ children }) {
736
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
737
- className: config_manager_module_css_default.empty,
738
- children
739
- });
740
- }
741
- /** 复选框行(勾选 + 标签) */
742
- function Checkbox({ checked, onChange, label, disabled }) {
743
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
744
- className: config_manager_module_css_default.checkboxRow,
745
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
746
- type: "checkbox",
747
- checked,
748
- disabled,
749
- onChange: (event) => {
750
- onChange(event.target.checked);
751
- }
752
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
753
- });
754
- }
755
- //#endregion
756
648
  //#region src/security/secret-scanner.ts
757
649
  /**
758
650
  * 敏感字段名单(规范化名:小写、无分隔符)。
@@ -965,25 +857,936 @@ function formatActionableError(e) {
965
857
  return lines.join("\n");
966
858
  }
967
859
  //#endregion
968
- //#region src/client/common/ErrorBanner.tsx
969
- /**
970
- * 可操作错误展示(规范 §23,绑 src/ui/errors.ts)。
971
- *
972
- * 安全约束:**强制 redact 不泄 Secret** —— 展示前对错误消息、关联项文本
973
- * 统一过 `redact()`(字段名黑名单 + sk-/JWT/PEM/Bearer 等值形状模式),
974
- * 渲染结果只含 Reason / Suggested action / Item,绝不出现密钥原文。
975
- */
976
- /**
977
- * 错误横幅:toActionableError 解析为标题 + 原因 + 建议动作,
978
- * 文本在渲染前再经 redact() 兜底(双保险),Reason 以等宽块展示。
979
- */
980
- function ErrorBanner({ error, onRetry, retrying }) {
981
- const [actionable] = (0, react.useState)(() => toActionableError(error));
982
- const reason = redact(actionable.reason);
983
- const item = actionable.item !== void 0 ? redact(actionable.item) : void 0;
984
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
985
- className: config_manager_module_css_default.errorBanner,
986
- role: "alert",
860
+ //#region src/ui/import-wizard.ts
861
+ var ImportWizard = class {
862
+ port;
863
+ onProgress;
864
+ tracker;
865
+ step = "select";
866
+ zipPath = null;
867
+ analysis = null;
868
+ plan = null;
869
+ result = null;
870
+ rollbackOnError;
871
+ errors = [];
872
+ decisions = {
873
+ strategy: "merge",
874
+ resolutions: {},
875
+ pathMappings: []
876
+ };
877
+ secretInputs = {};
878
+ constructor(opts) {
879
+ this.port = opts.port;
880
+ this.onProgress = opts.onProgress;
881
+ this.tracker = new ProgressTracker(IMPORT_STAGES, this.onProgress);
882
+ this.rollbackOnError = opts.defaultRollbackOnError ?? true;
883
+ }
884
+ /** 当前状态快照(React 绑定 / 测试断言用) */
885
+ snapshot() {
886
+ return {
887
+ step: this.step,
888
+ zipPath: this.zipPath,
889
+ analysis: this.analysis,
890
+ plan: this.plan,
891
+ result: this.result,
892
+ rollbackOnError: this.rollbackOnError,
893
+ errors: [...this.errors]
894
+ };
895
+ }
896
+ get currentStep() {
897
+ return this.step;
898
+ }
899
+ /** 步骤 1-2:选 ZIP → Analyzing → Compatibility(analyzeImport 零写入) */
900
+ async selectZip(path) {
901
+ this.zipPath = path;
902
+ this.step = "analyzing";
903
+ this.tracker.emit("validating");
904
+ this.errors = [];
905
+ try {
906
+ this.analysis = await this.port.analyzeImport(path);
907
+ this.tracker.emit("checking-compatibility");
908
+ if (!this.analysis.valid) {
909
+ this.errors.push(...this.analysis.errors.map((e) => formatActionableError(toActionableError(new Error(e)))));
910
+ throw new Error(this.analysis.errors.join("; ") || "备份分析失败");
911
+ }
912
+ this.step = "compatibility";
913
+ return this.analysis;
914
+ } catch (err) {
915
+ if (this.errors.length === 0) this.errors.push(formatActionableError(toActionableError(err)));
916
+ throw err;
917
+ }
918
+ }
919
+ /** 步骤 3→4:用户确认兼容性后进入 Preview(Dry Run:用当前决策生成计划摘要,零写入) */
920
+ async confirmCompatibility() {
921
+ if (this.analysis === null || this.zipPath === null) throw new Error("尚未完成分析,请先选择备份文件");
922
+ this.plan = await this.port.createImportPlan(this.zipPath, this.decisions);
923
+ this.step = "preview";
924
+ return this.plan;
925
+ }
926
+ /** 更新全局冲突策略(merge/replace/skipExisting,规范 §11) */
927
+ setStrategy(strategy) {
928
+ this.decisions = {
929
+ ...this.decisions,
930
+ strategy
931
+ };
932
+ }
933
+ /** 设置逐项冲突决策(keepCurrent/useImported/review) */
934
+ setResolutions(resolutions) {
935
+ this.decisions = {
936
+ ...this.decisions,
937
+ resolutions
938
+ };
939
+ }
940
+ /** 设置路径映射(§12) */
941
+ setPathMappings(mappings) {
942
+ this.decisions = {
943
+ ...this.decisions,
944
+ pathMappings: mappings
945
+ };
946
+ }
947
+ /** 设置秘密补录值(仅内存,绝不持久化) */
948
+ setSecretInputs(inputs) {
949
+ this.secretInputs = inputs;
950
+ }
951
+ /** Preview 摘要(规范 §10 数值化;基于当前 plan 与 analysis) */
952
+ previewSummary() {
953
+ const items = this.plan?.items ?? [];
954
+ const analysis = this.analysis;
955
+ const count = (kinds) => items.filter((i) => kinds.includes(i.kind)).length;
956
+ return {
957
+ willChange: count([
958
+ "Create",
959
+ "Update",
960
+ "Install",
961
+ "Conflict"
962
+ ]),
963
+ unchanged: count(["Skip"]),
964
+ settingsUpdates: count(["Create", "Update"]),
965
+ pluginsInstalled: analysis?.pluginSummary.installed ?? 0,
966
+ pluginsToInstall: count(["Install"]),
967
+ mcpAdds: items.filter((i) => i.adapter === "mcp" && i.kind === "Create").length,
968
+ prompts: items.filter((i) => i.adapter === "prompts" && i.kind !== "Skip").length,
969
+ pathMappingsNeeded: analysis?.pathIssues.length ?? 0,
970
+ secretsNeeded: this.plan?.missingSecrets.length ?? analysis?.secretCount ?? 0,
971
+ conflicts: count(["Conflict"]),
972
+ needsRestart: this.plan?.needsRestart ?? false
973
+ };
974
+ }
975
+ /** 冲突项(供 ConflictCollector 使用) */
976
+ conflictItems() {
977
+ return (this.plan?.items ?? []).filter((i) => i.kind === "Conflict");
978
+ }
979
+ /** 设置回滚策略(场景 E 选择;执行前调用) */
980
+ setRollbackOnError(enable) {
981
+ this.rollbackOnError = enable;
982
+ }
983
+ /**
984
+ * 步骤 10-14:确认导入 → 快照 → 执行 → 校验 → 结果。
985
+ * 用最终决策重建计划(与预览一致),显式传 rollbackOnError。
986
+ */
987
+ async execute(opts) {
988
+ if (this.zipPath === null || this.analysis === null) throw new Error("尚未完成分析,请先选择备份文件");
989
+ if (opts.confirm !== true) throw new Error("导入未确认:必须确认后才允许修改任何数据");
990
+ const rollbackOnError = opts.rollbackOnError ?? this.rollbackOnError;
991
+ this.step = "importing";
992
+ this.tracker.emit("creating-snapshot");
993
+ try {
994
+ this.plan = await this.port.createImportPlan(this.zipPath, this.decisions);
995
+ this.tracker.emit(EXECUTING_STAGE);
996
+ this.result = await this.port.executeImportPlan(this.zipPath, this.plan, {
997
+ confirm: true,
998
+ secretInputs: this.secretInputs,
999
+ rollbackOnError
1000
+ });
1001
+ if (!this.result.ok && this.result.rollback) this.tracker.emit("rolling-back");
1002
+ this.tracker.emit("done");
1003
+ this.step = "result";
1004
+ return this.result;
1005
+ } catch (err) {
1006
+ this.errors.push(formatActionableError(toActionableError(err)));
1007
+ throw err;
1008
+ }
1009
+ }
1010
+ /** 重置向导(可复用实例开始新导入) */
1011
+ reset() {
1012
+ this.step = "select";
1013
+ this.zipPath = null;
1014
+ this.analysis = null;
1015
+ this.plan = null;
1016
+ this.result = null;
1017
+ this.errors = [];
1018
+ this.decisions = {
1019
+ strategy: "merge",
1020
+ resolutions: {},
1021
+ pathMappings: []
1022
+ };
1023
+ this.secretInputs = {};
1024
+ }
1025
+ };
1026
+ //#endregion
1027
+ //#region src/client/run-store.ts
1028
+ /**
1029
+ * m2: 模块级 run/UI store —— dsh-config-manager 浏览器半的状态中枢。
1030
+ *
1031
+ * 解决的问题(验收 m2-state / m2-refresh / m2-resume):
1032
+ * - 切 tab / 关面板重开:模块级单例存活于当前 JS 上下文,ExportFlow /
1033
+ * ImportWizard 控制器实例与 UI 状态**不重建**(m2-state);
1034
+ * - 页面刷新:非敏感状态经 sessionStorage(键 dsh.cfgMgr.state.v1)
1035
+ * 序列化/反序列化恢复(m2-refresh);敏感字段
1036
+ * password / passwordConfirm / secretInputs **只存在内存**,序列化白名单
1037
+ * 显式剔除 —— 刷新后自动清空,secrets 阶段要求重输;
1038
+ * - 进行中 run:视图挂载时经 GET /runs 找回活跃 runId,轮询 GET /progress
1039
+ * 直到完成/失败,把 RunState.result 回填到 store(m2-resume)。服务端在
1040
+ * 刷新/关面板期间继续执行,本 store 只负责重新订阅进度。
1041
+ *
1042
+ * 控制器 rehydrate 说明:ImportWizard 没有公开的 hydrate 入口(m2 约束为只改
1043
+ * src/client/、不动 src/ui/),刷新恢复需要把持久化的非敏感快照写回控制器
1044
+ * 私有字段。writeWizardSnapshot() 是唯一的受控访问点:类型层 private 只是
1045
+ * 编译期约束(运行时是普通属性),所有写入值都来自序列化白名单(已剔除
1046
+ * 密码/密钥/secretInputs),且只在 load / hydrate / resume-settle 时调用。
1047
+ *
1048
+ * 安全约束:
1049
+ * - toPersistedState() 用解构白名单剔除敏感字段,即使未来往 LiveState 加字段,
1050
+ * 未显式放行也不会落入 sessionStorage;
1051
+ * - runId 为 32-hex 不可猜标识,可安全持久化(/runs + /progress 的查询键)。
1052
+ */
1053
+ /** sessionStorage 键。 */
1054
+ const STATE_KEY = "dsh.cfgMgr.state.v1";
1055
+ /** Custom 模式的初始勾选 = 推荐分区(可再调整) */
1056
+ function defaultCustomSelection() {
1057
+ return DEFAULT_CATEGORIES.filter((c) => c.defaultIncluded).map((c) => c.id);
1058
+ }
1059
+ function defaultExportState() {
1060
+ return {
1061
+ mode: "quick",
1062
+ selection: defaultCustomSelection(),
1063
+ includeSecrets: false,
1064
+ password: "",
1065
+ passwordConfirm: "",
1066
+ running: false,
1067
+ progress: null,
1068
+ result: null,
1069
+ error: null,
1070
+ downloaded: false,
1071
+ runId: null
1072
+ };
1073
+ }
1074
+ function defaultImportState() {
1075
+ return {
1076
+ step: "select",
1077
+ zipPath: null,
1078
+ analysis: null,
1079
+ plan: null,
1080
+ result: null,
1081
+ rollbackOnError: true,
1082
+ errors: [],
1083
+ phase: "preview",
1084
+ conflictStrategy: "merge",
1085
+ conflictResolutions: {},
1086
+ pathMappings: [],
1087
+ secretInputs: {},
1088
+ uploading: false,
1089
+ running: false,
1090
+ progress: null,
1091
+ error: null,
1092
+ runId: null,
1093
+ conflictCollector: null
1094
+ };
1095
+ }
1096
+ function defaultState() {
1097
+ return {
1098
+ v: 1,
1099
+ view: "export",
1100
+ export: defaultExportState(),
1101
+ import: defaultImportState()
1102
+ };
1103
+ }
1104
+ /**
1105
+ * 持久化白名单:解构剔除敏感字段(password/passwordConfirm/secretInputs)
1106
+ * 与不可序列化的实例字段(conflictCollector),其余原样落入 sessionStorage。
1107
+ * 这是 sessionStorage 的唯一写入路径 —— 敏感值在此被硬性隔离。
1108
+ */
1109
+ function toPersistedState(state) {
1110
+ const { password: _password, passwordConfirm: _passwordConfirm, ...exportRest } = state.export;
1111
+ const { secretInputs: _secretInputs, conflictCollector: _conflictCollector, ...importRest } = state.import;
1112
+ return {
1113
+ v: 1,
1114
+ view: state.view,
1115
+ export: exportRest,
1116
+ import: importRest
1117
+ };
1118
+ }
1119
+ /** 解析 + 轻量校验持久化状态;损坏/版本不符返回 null(调用方回退默认并清键)。 */
1120
+ function parsePersistedState(raw) {
1121
+ let parsed;
1122
+ try {
1123
+ parsed = JSON.parse(raw);
1124
+ } catch {
1125
+ return null;
1126
+ }
1127
+ if (typeof parsed !== "object" || parsed === null) return null;
1128
+ const p = parsed;
1129
+ if (p["v"] !== 1) return null;
1130
+ const view = p["view"];
1131
+ if (view !== "export" && view !== "import") return null;
1132
+ const exp = p["export"];
1133
+ const imp = p["import"];
1134
+ if (typeof exp !== "object" || exp === null || typeof imp !== "object" || imp === null) return null;
1135
+ return {
1136
+ v: 1,
1137
+ view,
1138
+ export: exp,
1139
+ import: imp
1140
+ };
1141
+ }
1142
+ /**
1143
+ * 把 store 的导入快照写回 ImportWizard 控制器私有字段(受控 rehydrate)。
1144
+ * - 只写非敏感字段;secretInputs 恒置空 —— 刷新后 secrets 阶段要求重输;
1145
+ * - 仅在 load / hydrate / resume-settle 时调用,不参与正常交互路径。
1146
+ */
1147
+ function writeWizardSnapshot(wizard, imp) {
1148
+ const internals = wizard;
1149
+ internals.step = imp.step;
1150
+ internals.zipPath = imp.zipPath;
1151
+ internals.analysis = imp.analysis;
1152
+ internals.plan = imp.plan;
1153
+ internals.result = imp.result;
1154
+ internals.rollbackOnError = imp.rollbackOnError;
1155
+ internals.errors = [...imp.errors];
1156
+ internals.decisions = {
1157
+ strategy: imp.conflictStrategy,
1158
+ resolutions: { ...imp.conflictResolutions },
1159
+ pathMappings: imp.pathMappings.map((m) => ({
1160
+ ...m,
1161
+ appliesTo: [...m.appliesTo]
1162
+ }))
1163
+ };
1164
+ internals.secretInputs = {};
1165
+ }
1166
+ /** 由 plan + 已持久化的决策重建 ConflictCollector(刷新恢复用)。 */
1167
+ function rebuildConflictCollector(plan, resolutions) {
1168
+ const collector = new ConflictCollector(plan);
1169
+ for (const [id, resolution] of Object.entries(resolutions)) collector.resolve(id, resolution);
1170
+ return collector;
1171
+ }
1172
+ /**
1173
+ * RunState → RunProgress(m3 轮询进度回填)。
1174
+ * - step/total = 内部计数(百分比条按 item/itemTotal 推进);
1175
+ * - section/sectionTotal 与 item/itemTotal 单独保留给分区徽章/内部计数徽章;
1176
+ * - detail = 当前项名(导出时恒为分区名,ProgressBar 侧会去冗余)。
1177
+ */
1178
+ function mapRunProgress(kind, state) {
1179
+ return {
1180
+ stage: kind === "export" ? "exporting" : "executing",
1181
+ detail: state.detail ?? void 0,
1182
+ step: state.item ?? void 0,
1183
+ total: state.itemTotal ?? void 0,
1184
+ section: state.section,
1185
+ sectionTotal: state.sectionTotal,
1186
+ item: state.item,
1187
+ itemTotal: state.itemTotal
1188
+ };
1189
+ }
1190
+ function defaultStorage() {
1191
+ if (typeof window === "undefined") return null;
1192
+ try {
1193
+ return window.sessionStorage ?? null;
1194
+ } catch {
1195
+ return null;
1196
+ }
1197
+ }
1198
+ /**
1199
+ * 模块级单例 store:控制器实例 + React UI 状态 + sessionStorage 恢复。
1200
+ * 最小接口:subscribe / getSnapshot / load / save / patch / syncWizard /
1201
+ * exportFlow / importWizard / resume / stopResume。
1202
+ */
1203
+ var RunStore = class {
1204
+ storage;
1205
+ pollIntervalMs;
1206
+ listeners = /* @__PURE__ */ new Set();
1207
+ state;
1208
+ exportFlowInst = null;
1209
+ importWizardInst = null;
1210
+ apiRef = null;
1211
+ /** 正在轮询的 runId 集合(stopResume 清空;防重复订阅) */
1212
+ polling = /* @__PURE__ */ new Set();
1213
+ /** 每个 runId 至多一个挂起的轮询定时器(fire 后由下一次调度覆盖) */
1214
+ timers = /* @__PURE__ */ new Map();
1215
+ /** 正在「发现进行中 run」的 kind 集合(watchRunning 活动标记;stopRunWatch/stopResume 清空) */
1216
+ watchActive = /* @__PURE__ */ new Set();
1217
+ constructor(opts = {}) {
1218
+ this.storage = opts.storage !== void 0 ? opts.storage : defaultStorage();
1219
+ this.pollIntervalMs = opts.pollIntervalMs ?? 1e3;
1220
+ this.state = defaultState();
1221
+ this.load();
1222
+ }
1223
+ /** 订阅 store 变化(useSyncExternalStore 的 subscribe)。 */
1224
+ subscribe(listener) {
1225
+ this.listeners.add(listener);
1226
+ return () => {
1227
+ this.listeners.delete(listener);
1228
+ };
1229
+ }
1230
+ /** 当前完整快照(引用稳定:仅在 patch 后替换,适合 useSyncExternalStore)。 */
1231
+ getSnapshot() {
1232
+ return this.state;
1233
+ }
1234
+ notify() {
1235
+ for (const listener of this.listeners) listener();
1236
+ }
1237
+ /** 浅合并补丁并立即持久化(保存走白名单,敏感字段不落盘)。 */
1238
+ patch(patchObj) {
1239
+ this.state = {
1240
+ ...this.state,
1241
+ view: patchObj.view ?? this.state.view,
1242
+ export: {
1243
+ ...this.state.export,
1244
+ ...patchObj.export
1245
+ },
1246
+ import: {
1247
+ ...this.state.import,
1248
+ ...patchObj.import
1249
+ }
1250
+ };
1251
+ this.notify();
1252
+ this.save();
1253
+ }
1254
+ /** 把 ImportWizard 控制器的 snapshot() 镜像进 store(wizard 动作后调用)。 */
1255
+ syncWizard() {
1256
+ const wizard = this.importWizardInst;
1257
+ if (wizard === null) return;
1258
+ const snap = wizard.snapshot();
1259
+ this.patch({ import: {
1260
+ step: snap.step,
1261
+ zipPath: snap.zipPath,
1262
+ analysis: snap.analysis,
1263
+ plan: snap.plan,
1264
+ result: snap.result,
1265
+ rollbackOnError: snap.rollbackOnError,
1266
+ errors: snap.errors
1267
+ } });
1268
+ }
1269
+ /** 从存储恢复非敏感状态(构造时调用一次;损坏数据回退默认并清键)。 */
1270
+ load() {
1271
+ if (this.storage === null) return;
1272
+ let raw = null;
1273
+ try {
1274
+ raw = this.storage.getItem(STATE_KEY);
1275
+ } catch {
1276
+ return;
1277
+ }
1278
+ if (raw === null || raw === "") {
1279
+ this.state = defaultState();
1280
+ return;
1281
+ }
1282
+ const parsed = parsePersistedState(raw);
1283
+ if (parsed === null) {
1284
+ try {
1285
+ this.storage.removeItem(STATE_KEY);
1286
+ } catch {}
1287
+ this.state = defaultState();
1288
+ return;
1289
+ }
1290
+ this.applyPersisted(parsed);
1291
+ }
1292
+ /** 把解析后的持久化状态合并进运行时状态;敏感字段强制回到默认(清空)。 */
1293
+ applyPersisted(parsed) {
1294
+ this.state = {
1295
+ v: 1,
1296
+ view: parsed.view,
1297
+ export: {
1298
+ ...defaultExportState(),
1299
+ ...parsed.export,
1300
+ password: "",
1301
+ passwordConfirm: ""
1302
+ },
1303
+ import: {
1304
+ ...defaultImportState(),
1305
+ ...parsed.import,
1306
+ secretInputs: {},
1307
+ conflictCollector: null
1308
+ }
1309
+ };
1310
+ const missing = this.state.import.plan?.missingSecrets ?? [];
1311
+ if (this.state.import.phase === "confirm" && missing.length > 0) this.state.import.phase = "secrets";
1312
+ const imp = this.state.import;
1313
+ if (imp.phase === "conflicts" && imp.plan !== null) imp.conflictCollector = rebuildConflictCollector(imp.plan, imp.conflictResolutions);
1314
+ }
1315
+ /** 把非敏感状态写入 sessionStorage(白名单序列化;无存储时为空操作)。 */
1316
+ save() {
1317
+ if (this.storage === null) return;
1318
+ try {
1319
+ this.storage.setItem(STATE_KEY, JSON.stringify(toPersistedState(this.state)));
1320
+ } catch {}
1321
+ }
1322
+ /** ExportFlow 控制器实例:懒创建 + 缓存(切 tab/关面板不重建)。 */
1323
+ exportFlow(api) {
1324
+ if (this.exportFlowInst === null) {
1325
+ if (this.apiRef === null) this.apiRef = api;
1326
+ this.exportFlowInst = new ExportFlow({
1327
+ port: api,
1328
+ onProgress: (event) => {
1329
+ this.patch({ export: { progress: event } });
1330
+ }
1331
+ });
1332
+ }
1333
+ return this.exportFlowInst;
1334
+ }
1335
+ /** ImportWizard 控制器实例:懒创建 + 缓存;首次创建时从持久化快照 rehydrate。 */
1336
+ importWizard(api) {
1337
+ if (this.importWizardInst === null) {
1338
+ if (this.apiRef === null) this.apiRef = api;
1339
+ const wizard = new ImportWizard({
1340
+ port: api,
1341
+ onProgress: (event) => {
1342
+ this.patch({ import: { progress: event } });
1343
+ },
1344
+ defaultRollbackOnError: true
1345
+ });
1346
+ writeWizardSnapshot(wizard, this.state.import);
1347
+ this.importWizardInst = wizard;
1348
+ }
1349
+ return this.importWizardInst;
1350
+ }
1351
+ /**
1352
+ * 重新订阅进行中的 run:GET /runs 找回活跃 runId → 轮询 /progress 直到
1353
+ * 完成/失败,把 RunState.result 回填 store(导出结果 / 导入结果均可恢复)。
1354
+ * 返回是否有 run 被恢复。幂等:同一 runId 不会重复轮询。
1355
+ */
1356
+ async resume(api) {
1357
+ if (this.apiRef === null) this.apiRef = api;
1358
+ let active;
1359
+ try {
1360
+ active = await api.runs();
1361
+ } catch {
1362
+ return false;
1363
+ }
1364
+ let resumed = false;
1365
+ const exportRun = active.find((r) => r.kind === "export");
1366
+ if (exportRun !== void 0) {
1367
+ resumed = true;
1368
+ this.patch({ export: {
1369
+ running: true,
1370
+ runId: exportRun.runId,
1371
+ progress: mapRunProgress("export", exportRun)
1372
+ } });
1373
+ this.pollRun(exportRun.runId, "export");
1374
+ } else if (this.state.export.running) this.patch({ export: {
1375
+ running: false,
1376
+ progress: null,
1377
+ runId: null,
1378
+ error: "上次导出任务已结束但结果无法恢复,请重新导出"
1379
+ } });
1380
+ const importRun = active.find((r) => r.kind === "import");
1381
+ if (importRun !== void 0) {
1382
+ resumed = true;
1383
+ this.patch({ import: {
1384
+ running: true,
1385
+ runId: importRun.runId,
1386
+ step: "importing",
1387
+ progress: mapRunProgress("import", importRun)
1388
+ } });
1389
+ this.pollRun(importRun.runId, "import");
1390
+ } else if (this.state.import.step === "importing") {
1391
+ this.importWizardInst?.reset();
1392
+ this.patch({ import: {
1393
+ step: "select",
1394
+ phase: "preview",
1395
+ runId: null,
1396
+ running: false,
1397
+ progress: null,
1398
+ conflictCollector: null,
1399
+ errors: [],
1400
+ error: "上次导入任务已结束或超过保留期,结果无法恢复,请重新导入"
1401
+ } });
1402
+ }
1403
+ return resumed;
1404
+ }
1405
+ /** 停止全部轮询(视图卸载时调用;服务端执行不受影响,重开面板再 resume)。 */
1406
+ stopResume() {
1407
+ this.polling.clear();
1408
+ this.watchActive.clear();
1409
+ for (const timer of this.timers.values()) clearTimeout(timer);
1410
+ this.timers.clear();
1411
+ }
1412
+ /**
1413
+ * 本次会话内启动的 run(POST /export 或 /execute 请求进行期间)的实时进度订阅。
1414
+ * 请求是同步的、响应到达才知 runId,所以先经 GET /runs 发现进行中的 run(500ms),
1415
+ * 发现后转入 /progress 轮询(同一间隔),done/failed 自动停止。
1416
+ *
1417
+ * 与 m2 resume 的收敛:两者共用 pollRun / polling / timers 同一套轮询器,
1418
+ * 只是间隔不同(resume 恢复 1s、本方法实时 500ms)且发现阶段不同
1419
+ * (resume 用 /runs 一次性找回 runId;本方法持续 /runs 直到出现活跃 run)。
1420
+ * 同一 runId 不会重复轮询(polling 集合防重)。
1421
+ */
1422
+ watchRunning(kind, intervalMs) {
1423
+ if (this.watchActive.has(kind)) return;
1424
+ this.watchActive.add(kind);
1425
+ const api = this.apiRef;
1426
+ if (api === null) {
1427
+ this.watchActive.delete(kind);
1428
+ return;
1429
+ }
1430
+ const interval = intervalMs ?? this.pollIntervalMs;
1431
+ const tick = () => {
1432
+ if (!this.watchActive.has(kind)) return;
1433
+ api.runs().then((active) => {
1434
+ if (!this.watchActive.has(kind)) return;
1435
+ const run = active.find((r) => r.kind === kind && r.status === "running");
1436
+ if (run === void 0) {
1437
+ this.scheduleWatch(kind, tick, interval);
1438
+ return;
1439
+ }
1440
+ this.watchActive.delete(kind);
1441
+ this.patchProgress(kind, run);
1442
+ this.pollRun(run.runId, kind, interval);
1443
+ }, () => {
1444
+ if (this.watchActive.has(kind)) this.scheduleWatch(kind, tick, interval);
1445
+ });
1446
+ };
1447
+ this.scheduleWatch(kind, tick, interval);
1448
+ }
1449
+ /** 停止某 kind 的「发现进行中 run」轮询(视图请求结束后调用;/progress 轮询自行结束)。 */
1450
+ stopRunWatch(kind) {
1451
+ this.watchActive.delete(kind);
1452
+ }
1453
+ /** 调度下一轮发现;每 kind 只保留一个挂起定时器。 */
1454
+ scheduleWatch(kind, tick, intervalMs) {
1455
+ const timer = setTimeout(tick, intervalMs);
1456
+ this.timers.set(`watch:${kind}`, timer);
1457
+ }
1458
+ pollRun(runId, kind, intervalMs) {
1459
+ if (this.polling.has(runId)) return;
1460
+ this.polling.add(runId);
1461
+ const api = this.apiRef;
1462
+ if (api === null) return;
1463
+ const interval = intervalMs ?? this.pollIntervalMs;
1464
+ const tick = () => {
1465
+ if (!this.polling.has(runId)) return;
1466
+ api.progress(runId).then((state) => {
1467
+ if (!this.polling.has(runId)) return;
1468
+ if (state.status === "running") {
1469
+ this.patchProgress(kind, state);
1470
+ this.scheduleTick(runId, tick, interval);
1471
+ } else {
1472
+ this.polling.delete(runId);
1473
+ this.applySettled(kind, state);
1474
+ }
1475
+ }, () => {
1476
+ this.polling.delete(runId);
1477
+ this.applyGone(kind);
1478
+ });
1479
+ };
1480
+ this.scheduleTick(runId, tick, interval);
1481
+ }
1482
+ /** 调度下一轮询;每 runId 只保留一个挂起定时器(fire 后由下一次调度覆盖)。 */
1483
+ scheduleTick(runId, tick, intervalMs) {
1484
+ const timer = setTimeout(tick, intervalMs);
1485
+ this.timers.set(runId, timer);
1486
+ }
1487
+ patchProgress(kind, state) {
1488
+ const event = mapRunProgress(kind, state);
1489
+ if (kind === "export") this.patch({ export: { progress: event } });
1490
+ else this.patch({ import: { progress: event } });
1491
+ }
1492
+ /** run 完成/失败:把 RunState.result 回填 store(并镜像回控制器)。 */
1493
+ applySettled(kind, state) {
1494
+ if (kind === "export") {
1495
+ const result = state.result;
1496
+ if (state.status === "done" && result !== void 0 && typeof result.zipPath === "string") this.patch({ export: {
1497
+ running: false,
1498
+ progress: {
1499
+ stage: "done",
1500
+ step: 1,
1501
+ total: 1
1502
+ },
1503
+ result: {
1504
+ ...result,
1505
+ text: renderExportReport(result.report)
1506
+ },
1507
+ downloaded: false
1508
+ } });
1509
+ else this.patch({ export: {
1510
+ running: false,
1511
+ progress: null,
1512
+ result: null,
1513
+ downloaded: false,
1514
+ error: state.error ?? "导出失败(结果不可用)"
1515
+ } });
1516
+ return;
1517
+ }
1518
+ const result = state.result;
1519
+ if (state.status === "done" && result !== void 0 && typeof result === "object") {
1520
+ const wizard = this.importWizardInst;
1521
+ if (wizard !== null) writeWizardSnapshot(wizard, {
1522
+ ...this.state.import,
1523
+ step: "result",
1524
+ result,
1525
+ secretInputs: {},
1526
+ conflictCollector: null
1527
+ });
1528
+ this.patch({ import: {
1529
+ step: "result",
1530
+ result,
1531
+ running: false,
1532
+ progress: {
1533
+ stage: "done",
1534
+ step: 1,
1535
+ total: 1
1536
+ },
1537
+ error: null
1538
+ } });
1539
+ } else {
1540
+ const wizard = this.importWizardInst;
1541
+ if (wizard !== null) {
1542
+ const internals = wizard;
1543
+ internals.errors = [...internals.errors, state.error ?? "导入失败"];
1544
+ }
1545
+ this.patch({ import: {
1546
+ running: false,
1547
+ progress: null,
1548
+ error: state.error ?? "导入失败",
1549
+ errors: [...this.state.import.errors, state.error ?? "导入失败"]
1550
+ } });
1551
+ }
1552
+ }
1553
+ /** run 消失(404/网络错误):停止轮询并提示不可恢复。 */
1554
+ applyGone(kind) {
1555
+ if (kind === "export") this.patch({ export: {
1556
+ running: false,
1557
+ progress: null,
1558
+ runId: null,
1559
+ error: "任务已结束或超过保留期,进度不可恢复"
1560
+ } });
1561
+ else {
1562
+ this.importWizardInst?.reset();
1563
+ this.patch({ import: {
1564
+ running: false,
1565
+ progress: null,
1566
+ runId: null,
1567
+ step: "select",
1568
+ phase: "preview",
1569
+ conflictCollector: null,
1570
+ errors: [],
1571
+ error: "任务已结束或超过保留期,进度不可恢复"
1572
+ } });
1573
+ }
1574
+ }
1575
+ };
1576
+ /** 模块级单例(浏览器半所有视图共享;构造时自动 load())。 */
1577
+ const runStore = new RunStore();
1578
+ //#endregion
1579
+ //#region \0config-manager-css:/home/runner/work/dsh-config-manager/dsh-config-manager/src/client/config-manager.module.css.js
1580
+ const css = "._367UGW_section{height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex-direction:column;gap:10px;font-size:13px;display:flex}._367UGW_sectionHeader{flex:none;align-items:center;gap:10px;display:flex}._367UGW_viewTabs{border-bottom:1px solid var(--dsw-alias-border-l1);flex:1;gap:2px;display:flex}._367UGW_viewTab{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-bottom:2px solid #0000;border-radius:6px 6px 0 0;padding:7px 14px;font-size:13px}._367UGW_viewTab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}._367UGW_viewTab[data-active]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:600}._367UGW_sectionBody{flex-direction:column;flex:1;min-height:0;display:flex;overflow-y:auto}._367UGW_viewBody{flex-direction:column;gap:12px;padding:4px 2px 16px;display:flex}._367UGW_sectionTitleBlock{flex-direction:column;gap:2px;display:flex}._367UGW_sectionTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:15px;font-weight:700}._367UGW_sectionSubtitle{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:1.5}._367UGW_primaryButton{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-button-info-fill);cursor:pointer;white-space:nowrap;border:none;border-radius:8px;align-items:center;gap:6px;padding:6px 14px;font-size:13px;font-weight:600;display:inline-flex}._367UGW_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}._367UGW_primaryButton:disabled{opacity:.5;cursor:default}._367UGW_ghostButton{color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;align-items:center;gap:6px;padding:5px 12px;font-size:12px;display:inline-flex}._367UGW_ghostButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}._367UGW_ghostButton:disabled{opacity:.45;cursor:default}._367UGW_dangerButton{color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;padding:5px 12px;font-size:12px}._367UGW_dangerButton:hover:not(:disabled){background:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-label-primary-foreground)}._367UGW_dangerButton:disabled{opacity:.45;cursor:default}._367UGW_badge{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;line-height:1.6;display:inline-block}._367UGW_badgeInfo{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}._367UGW_badgeOk{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}._367UGW_badgeWarn{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}._367UGW_badgeError{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}._367UGW_banner{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;border-radius:8px;padding:8px 12px;font-size:12.5px;line-height:1.5}._367UGW_banner[data-kind=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}._367UGW_banner[data-kind=error]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}._367UGW_banner[data-kind=info]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}._367UGW_banner[data-kind=warn]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}._367UGW_card{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;flex:none;gap:10px;padding:12px;display:flex}._367UGW_field{flex-direction:column;gap:5px;display:flex}._367UGW_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}._367UGW_input{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);resize:vertical;border-radius:8px;outline:none;padding:7px 10px;font-family:inherit;font-size:13px}._367UGW_input:focus{border-color:var(--dsw-alias-state-business-primary)}._367UGW_input::placeholder{color:var(--dsw-alias-label-tertiary)}._367UGW_input:disabled{opacity:.55}._367UGW_hint{color:var(--dsw-alias-label-tertiary);font-size:11.5px;line-height:1.5}._367UGW_formError{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}._367UGW_checkboxRow{color:var(--dsw-alias-label-primary);cursor:pointer;align-items:flex-start;gap:8px;font-size:13px;display:flex}._367UGW_checkboxRow input{flex:none;margin-top:2px}._367UGW_radioLabel{color:var(--dsw-alias-label-primary);cursor:pointer;align-items:center;gap:6px;font-size:13px;display:inline-flex}._367UGW_secretFields{grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px 12px;padding-top:4px;display:grid}._367UGW_hiddenFile{display:none}._367UGW_modeTabs{border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;gap:2px;display:flex}._367UGW_modeTab{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-bottom:2px solid #0000;padding:6px 12px;font-size:12.5px}._367UGW_modeTab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}._367UGW_modeTab[data-active]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:600}._367UGW_modeHint{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}._367UGW_groupList{flex-direction:column;gap:10px;display:flex}._367UGW_groupCard{gap:8px}._367UGW_groupHeader{flex-wrap:wrap;align-items:baseline;gap:8px;display:flex}._367UGW_groupLabel{color:var(--dsw-alias-label-primary);font-size:12.5px;font-weight:700}._367UGW_groupNote{color:var(--dsw-alias-label-tertiary);font-size:11.5px}._367UGW_groupItems{flex-direction:column;gap:6px;display:flex}._367UGW_categoryItem{flex-wrap:wrap;align-items:center;gap:8px;display:inline-flex}._367UGW_categoryName{font-weight:500}._367UGW_categoryDesc{color:var(--dsw-alias-label-tertiary);font-size:11.5px}._367UGW_optionsCard{gap:8px}._367UGW_warnList{margin:4px 0 0;padding-left:18px}._367UGW_actionRow{flex-wrap:wrap;align-items:center;gap:10px;display:flex}._367UGW_progressBlock{flex-direction:column;flex:none;gap:6px;display:flex}._367UGW_progressMeta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:12px;font-size:12px;display:flex}._367UGW_progressLabel{font-weight:600}._367UGW_progressDetail{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;max-width:320px;overflow:hidden}._367UGW_progressBadge{font-variant-numeric:tabular-nums;white-space:nowrap;border-radius:999px;align-items:center;gap:2px;padding:1px 8px;font-size:11px;line-height:18px;display:inline-flex}._367UGW_progressBadgeSection{background:color-mix(in srgb, var(--dsw-alias-state-business-primary) 14%, transparent);color:var(--dsw-alias-state-business-primary)}._367UGW_progressBadgeCount{background:color-mix(in srgb, var(--dsw-alias-state-info-primary) 14%, transparent);color:var(--dsw-alias-state-info-primary)}._367UGW_progressPercent{font-variant-numeric:tabular-nums;margin-left:auto}._367UGW_progressTrack{background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;height:8px;overflow:hidden}._367UGW_progressBar{background:var(--dsw-alias-state-business-primary);border-radius:999px;height:100%;transition:width .12s linear}._367UGW_progressBarDone{background:var(--dsw-alias-state-success-primary)}._367UGW_progressIndeterminate{width:40%;animation:1s ease-in-out infinite _367UGW_dshCmIndeterminate}@keyframes _367UGW_dshCmIndeterminate{0%{margin-left:-40%}to{margin-left:100%}}._367UGW_reportView{flex-direction:column;gap:10px;display:flex}._367UGW_statRow{flex-wrap:wrap;align-items:center;gap:6px;display:flex}._367UGW_reportText{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;max-height:260px;color:var(--dsw-alias-label-primary);border-radius:8px;margin:0;padding:10px 12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;line-height:1.6;overflow:auto}._367UGW_reportFooter{justify-content:flex-end;gap:8px;display:flex}._367UGW_rollbackBox{border:1px solid var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-primary);border-radius:8px;flex-direction:column;gap:6px;padding:10px 12px;display:flex}._367UGW_rollbackBox ._367UGW_reportText{border-color:var(--dsw-alias-border-l2)}._367UGW_errorBanner{border:1px solid var(--dsw-alias-state-error-primary);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border-radius:8px;flex-direction:column;gap:6px;padding:10px 12px;display:flex}._367UGW_errorTitle{color:var(--dsw-alias-state-error-primary);font-size:13px;font-weight:700}._367UGW_errorReason{white-space:pre-wrap;word-break:break-all;color:var(--dsw-alias-label-secondary);margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px}._367UGW_errorAction{color:var(--dsw-alias-label-secondary);font-size:12.5px}._367UGW_errorActionLabel{color:var(--dsw-alias-state-business-primary)}._367UGW_errorItem{color:var(--dsw-alias-label-tertiary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px}._367UGW_errorFooter{justify-content:flex-end;display:flex}._367UGW_errorList{flex-direction:column;gap:4px;display:flex}._367UGW_errorLine{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;color:var(--dsw-alias-state-error-primary);border-radius:6px;margin:0;padding:6px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px}._367UGW_conflictList{flex-direction:column;gap:10px;display:flex}._367UGW_conflictItem{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;flex-direction:column;gap:6px;padding:10px 12px;display:flex}._367UGW_conflictHead{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._367UGW_conflictId{color:var(--dsw-alias-label-primary);font-size:12.5px;font-weight:600}._367UGW_severityError{color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary);border-radius:999px;padding:0 6px;font-size:10.5px}._367UGW_conflictDetail{background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;color:var(--dsw-alias-label-secondary);border-radius:6px;margin:0;padding:6px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px}._367UGW_conflictOptions{flex-wrap:wrap;gap:14px;display:flex}._367UGW_pathMappingList{flex-direction:column;gap:8px;display:flex}._367UGW_pathRow{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:10px;padding:10px 12px;display:grid}._367UGW_pathOld,._367UGW_pathNew{flex-direction:column;gap:4px;min-width:0;display:flex}._367UGW_pathValue{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary);margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;overflow:hidden}._367UGW_pathIssueKind{color:var(--dsw-alias-state-warn-primary);font-size:10.5px}._367UGW_secretsList{flex-direction:column;gap:10px;display:flex}._367UGW_empty{text-align:center;color:var(--dsw-alias-label-tertiary);padding:22px 12px;font-size:12.5px}._367UGW_spinnerWrap{align-items:center;gap:6px;display:inline-flex}._367UGW_spinner{border:2px solid var(--dsw-alias-state-business-primary);vertical-align:-1px;border-top-color:#0000;border-radius:50%;flex:none;width:11px;height:11px;animation:.8s linear infinite _367UGW_dshCmSpin;display:inline-block}._367UGW_spinnerLabel{color:var(--dsw-alias-label-secondary);font-size:12px}@keyframes _367UGW_dshCmSpin{to{transform:rotate(360deg)}}._367UGW_snapshotList{flex-direction:column;gap:2px;display:flex}._367UGW_snapshotRowHeader,._367UGW_snapshotRow{grid-template-columns:minmax(120px,1.2fr) minmax(140px,1.6fr) 90px 56px 56px;align-items:center;gap:8px;padding:6px 10px;font-size:12px;display:grid}._367UGW_snapshotRowHeader{color:var(--dsw-alias-label-tertiary);border-bottom:1px solid var(--dsw-alias-border-l1);font-size:11px}._367UGW_snapshotRow{text-align:left;width:100%;color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px}._367UGW_snapshotRow:hover{background:var(--dsw-alias-interactive-bg-hover)}._367UGW_snapshotRow[data-active]{border-color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent)}._367UGW_warnText{color:var(--dsw-alias-state-warn-primary)}._367UGW_reportList{color:var(--dsw-alias-label-primary);flex-direction:column;gap:3px;margin:8px 0 4px;padding-left:18px;font-size:12px;display:flex}._367UGW_kindTag{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);border-radius:4px;padding:1px 6px;font-size:10.5px;display:inline-block}._367UGW_rowActions{gap:8px;margin-top:10px;display:flex}";
1581
+ const tagId = "dsh-config-manager/config-manager.module.css";
1582
+ if (typeof document !== "undefined" && document.querySelector("style[data-tag=\"dsh-config-manager/config-manager.module.css\"]") === null) {
1583
+ const tag = document.createElement("style");
1584
+ tag.dataset.plugin = "dsh-config-manager";
1585
+ tag.dataset.tag = tagId;
1586
+ tag.textContent = css;
1587
+ document.head.appendChild(tag);
1588
+ }
1589
+ var config_manager_module_css_default = {
1590
+ "badgeWarn": "_367UGW_badgeWarn",
1591
+ "conflictList": "_367UGW_conflictList",
1592
+ "warnText": "_367UGW_warnText",
1593
+ "groupItems": "_367UGW_groupItems",
1594
+ "errorItem": "_367UGW_errorItem",
1595
+ "groupCard": "_367UGW_groupCard",
1596
+ "progressBlock": "_367UGW_progressBlock",
1597
+ "sectionHeader": "_367UGW_sectionHeader",
1598
+ "sectionTitle": "_367UGW_sectionTitle",
1599
+ "progressDetail": "_367UGW_progressDetail",
1600
+ "reportText": "_367UGW_reportText",
1601
+ "spinnerLabel": "_367UGW_spinnerLabel",
1602
+ "errorActionLabel": "_367UGW_errorActionLabel",
1603
+ "conflictItem": "_367UGW_conflictItem",
1604
+ "hiddenFile": "_367UGW_hiddenFile",
1605
+ "errorAction": "_367UGW_errorAction",
1606
+ "errorBanner": "_367UGW_errorBanner",
1607
+ "severityError": "_367UGW_severityError",
1608
+ "badgeOk": "_367UGW_badgeOk",
1609
+ "viewTab": "_367UGW_viewTab",
1610
+ "sectionTitleBlock": "_367UGW_sectionTitleBlock",
1611
+ "field": "_367UGW_field",
1612
+ "pathNew": "_367UGW_pathNew",
1613
+ "spinner": "_367UGW_spinner",
1614
+ "progressIndeterminate": "_367UGW_progressIndeterminate",
1615
+ "modeHint": "_367UGW_modeHint",
1616
+ "secretFields": "_367UGW_secretFields",
1617
+ "progressLabel": "_367UGW_progressLabel",
1618
+ "categoryName": "_367UGW_categoryName",
1619
+ "progressBadgeCount": "_367UGW_progressBadgeCount",
1620
+ "reportList": "_367UGW_reportList",
1621
+ "banner": "_367UGW_banner",
1622
+ "progressPercent": "_367UGW_progressPercent",
1623
+ "errorReason": "_367UGW_errorReason",
1624
+ "sectionSubtitle": "_367UGW_sectionSubtitle",
1625
+ "errorFooter": "_367UGW_errorFooter",
1626
+ "rowActions": "_367UGW_rowActions",
1627
+ "viewTabs": "_367UGW_viewTabs",
1628
+ "progressBarDone": "_367UGW_progressBarDone",
1629
+ "checkboxRow": "_367UGW_checkboxRow",
1630
+ "hint": "_367UGW_hint",
1631
+ "categoryDesc": "_367UGW_categoryDesc",
1632
+ "progressBadge": "_367UGW_progressBadge",
1633
+ "dangerButton": "_367UGW_dangerButton",
1634
+ "reportFooter": "_367UGW_reportFooter",
1635
+ "pathIssueKind": "_367UGW_pathIssueKind",
1636
+ "secretsList": "_367UGW_secretsList",
1637
+ "groupLabel": "_367UGW_groupLabel",
1638
+ "section": "_367UGW_section",
1639
+ "card": "_367UGW_card",
1640
+ "formError": "_367UGW_formError",
1641
+ "snapshotRow": "_367UGW_snapshotRow",
1642
+ "kindTag": "_367UGW_kindTag",
1643
+ "ghostButton": "_367UGW_ghostButton",
1644
+ "viewBody": "_367UGW_viewBody",
1645
+ "conflictDetail": "_367UGW_conflictDetail",
1646
+ "groupHeader": "_367UGW_groupHeader",
1647
+ "badge": "_367UGW_badge",
1648
+ "errorTitle": "_367UGW_errorTitle",
1649
+ "conflictHead": "_367UGW_conflictHead",
1650
+ "modeTab": "_367UGW_modeTab",
1651
+ "warnList": "_367UGW_warnList",
1652
+ "pathMappingList": "_367UGW_pathMappingList",
1653
+ "pathRow": "_367UGW_pathRow",
1654
+ "primaryButton": "_367UGW_primaryButton",
1655
+ "progressBar": "_367UGW_progressBar",
1656
+ "errorLine": "_367UGW_errorLine",
1657
+ "modeTabs": "_367UGW_modeTabs",
1658
+ "actionRow": "_367UGW_actionRow",
1659
+ "reportView": "_367UGW_reportView",
1660
+ "progressTrack": "_367UGW_progressTrack",
1661
+ "progressBadgeSection": "_367UGW_progressBadgeSection",
1662
+ "dshCmIndeterminate": "_367UGW_dshCmIndeterminate",
1663
+ "progressMeta": "_367UGW_progressMeta",
1664
+ "optionsCard": "_367UGW_optionsCard",
1665
+ "conflictId": "_367UGW_conflictId",
1666
+ "sectionBody": "_367UGW_sectionBody",
1667
+ "badgeInfo": "_367UGW_badgeInfo",
1668
+ "groupNote": "_367UGW_groupNote",
1669
+ "statRow": "_367UGW_statRow",
1670
+ "fieldLabel": "_367UGW_fieldLabel",
1671
+ "categoryItem": "_367UGW_categoryItem",
1672
+ "pathValue": "_367UGW_pathValue",
1673
+ "empty": "_367UGW_empty",
1674
+ "spinnerWrap": "_367UGW_spinnerWrap",
1675
+ "input": "_367UGW_input",
1676
+ "dshCmSpin": "_367UGW_dshCmSpin",
1677
+ "radioLabel": "_367UGW_radioLabel",
1678
+ "pathOld": "_367UGW_pathOld",
1679
+ "badgeError": "_367UGW_badgeError",
1680
+ "rollbackBox": "_367UGW_rollbackBox",
1681
+ "errorList": "_367UGW_errorList",
1682
+ "snapshotList": "_367UGW_snapshotList",
1683
+ "snapshotRowHeader": "_367UGW_snapshotRowHeader",
1684
+ "groupList": "_367UGW_groupList",
1685
+ "conflictOptions": "_367UGW_conflictOptions"
1686
+ };
1687
+ //#endregion
1688
+ //#region src/client/common/ui.tsx
1689
+ /** 统一按钮(primary=主操作 / ghost=次操作 / danger=危险操作) */
1690
+ function Button({ variant = "ghost", disabled, onClick, children, title, className }) {
1691
+ const cls = variant === "primary" ? config_manager_module_css_default.primaryButton : variant === "danger" ? config_manager_module_css_default.dangerButton : config_manager_module_css_default.ghostButton;
1692
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1693
+ type: "button",
1694
+ className: className !== void 0 ? `${cls} ${className}` : cls,
1695
+ disabled,
1696
+ title,
1697
+ onClick,
1698
+ children
1699
+ });
1700
+ }
1701
+ /** 状态徽章(info=业务色 / ok=成功 / warn=警告 / error=错误) */
1702
+ function Badge({ kind = "info", children }) {
1703
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1704
+ className: `${config_manager_module_css_default.badge} ${config_manager_module_css_default[`badge${kind[0].toUpperCase()}${kind.slice(1)}`] ?? ""}`,
1705
+ children
1706
+ });
1707
+ }
1708
+ /** 说明横幅(ok/error/info/warn 四态) */
1709
+ function Banner({ kind = "info", children }) {
1710
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1711
+ className: config_manager_module_css_default.banner,
1712
+ "data-kind": kind,
1713
+ children
1714
+ });
1715
+ }
1716
+ /** 卡片容器(bg-layer-2 + 圆角 + 细边框) */
1717
+ function Card({ children, className }) {
1718
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1719
+ className: className !== void 0 ? `${config_manager_module_css_default.card} ${className}` : config_manager_module_css_default.card,
1720
+ children
1721
+ });
1722
+ }
1723
+ /** 加载指示(旋转环 + 可选文案) */
1724
+ function Spinner({ label }) {
1725
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1726
+ className: config_manager_module_css_default.spinnerWrap,
1727
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1728
+ className: config_manager_module_css_default.spinner,
1729
+ "aria-hidden": "true"
1730
+ }), label !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1731
+ className: config_manager_module_css_default.spinnerLabel,
1732
+ children: label
1733
+ })]
1734
+ });
1735
+ }
1736
+ /** 区块标题(页面内二级标题 + 可选副标题) */
1737
+ function SectionTitle({ title, subtitle }) {
1738
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1739
+ className: config_manager_module_css_default.sectionTitleBlock,
1740
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1741
+ className: config_manager_module_css_default.sectionTitle,
1742
+ children: title
1743
+ }), subtitle !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1744
+ className: config_manager_module_css_default.sectionSubtitle,
1745
+ children: subtitle
1746
+ })]
1747
+ });
1748
+ }
1749
+ /** 空状态占位 */
1750
+ function Empty({ children }) {
1751
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1752
+ className: config_manager_module_css_default.empty,
1753
+ children
1754
+ });
1755
+ }
1756
+ /** 复选框行(勾选 + 标签) */
1757
+ function Checkbox({ checked, onChange, label, disabled }) {
1758
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1759
+ className: config_manager_module_css_default.checkboxRow,
1760
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1761
+ type: "checkbox",
1762
+ checked,
1763
+ disabled,
1764
+ onChange: (event) => {
1765
+ onChange(event.target.checked);
1766
+ }
1767
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
1768
+ });
1769
+ }
1770
+ //#endregion
1771
+ //#region src/client/common/ErrorBanner.tsx
1772
+ /**
1773
+ * 可操作错误展示(规范 §23,绑 src/ui/errors.ts)。
1774
+ *
1775
+ * 安全约束:**强制 redact 不泄 Secret** —— 展示前对错误消息、关联项文本
1776
+ * 统一过 `redact()`(字段名黑名单 + sk-/JWT/PEM/Bearer 等值形状模式),
1777
+ * 渲染结果只含 Reason / Suggested action / Item,绝不出现密钥原文。
1778
+ */
1779
+ /**
1780
+ * 错误横幅:toActionableError 解析为标题 + 原因 + 建议动作,
1781
+ * 文本在渲染前再经 redact() 兜底(双保险),Reason 以等宽块展示。
1782
+ */
1783
+ function ErrorBanner({ error, onRetry, retrying }) {
1784
+ const [actionable] = (0, react.useState)(() => toActionableError(error));
1785
+ const reason = redact(actionable.reason);
1786
+ const item = actionable.item !== void 0 ? redact(actionable.item) : void 0;
1787
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1788
+ className: config_manager_module_css_default.errorBanner,
1789
+ role: "alert",
987
1790
  children: [
988
1791
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
989
1792
  className: config_manager_module_css_default.errorTitle,
@@ -1032,19 +1835,83 @@ function ErrorList({ errors }) {
1032
1835
  });
1033
1836
  }
1034
1837
  //#endregion
1838
+ //#region src/client/common/progress-view.ts
1839
+ /**
1840
+ * m3: 进度展示的纯逻辑层(框架无关,node 可测)。
1841
+ *
1842
+ * 背景:m1 的 RunState 携带真实进度(section/sectionTotal = 当前分区 3/12、
1843
+ * item/itemTotal = 内部计数如插件 6/18、detail = 当前项名);m2 的 store 把
1844
+ * RunState 映射为 ProgressEvent 并存入 progress 字段。本模块定义客户端的
1845
+ * RunProgress(ProgressEvent 超集,额外携带分区/内部计数,JSON 可序列化),
1846
+ * 并提供 computeProgressView() 把进度事件转成渲染模型 —— ProgressBar 只做
1847
+ * 纯渲染,所有判定逻辑集中在此、可单测。
1848
+ *
1849
+ * 兼容路径:无 section/item 的普通 ProgressEvent(控制器 onProgress 发出)
1850
+ * 走原逻辑 —— 阶段文案 + step/total 百分比,无徽章。
1851
+ */
1852
+ /**
1853
+ * 把进度事件换算成渲染模型。
1854
+ * - percent:优先 ProgressEvent.step/total(控制器阶段进度 / 轮询的内部计数);
1855
+ * - sectionBadge(当前分区,如 settings · 3/12):label=分区 id、current=item
1856
+ * (1-based 分区序号,见 core exporter onSection)、total=sectionTotal;
1857
+ * - countBadge(内部计数,如 plugins · 6/18):label=分区 id、current=item、
1858
+ * total=itemTotal;导出时 itemTotal 与 sectionTotal 同值(分区序号即内部计数)
1859
+ * → 置 null,避免「3/12 3/12」重复;
1860
+ * - detail(当前项名):非空且不等于分区 id(导出时 detail 恒为分区 id,去冗余)。
1861
+ */
1862
+ function computeProgressView(event) {
1863
+ if (event === null) return {
1864
+ label: "",
1865
+ percent: null,
1866
+ sectionBadge: null,
1867
+ countBadge: null,
1868
+ detail: null
1869
+ };
1870
+ const section = event.section ?? null;
1871
+ const sectionTotal = event.sectionTotal ?? null;
1872
+ const item = event.item ?? null;
1873
+ const itemTotal = event.itemTotal ?? null;
1874
+ const percent = event.step !== void 0 && event.total !== void 0 && event.total > 0 ? Math.round(event.step / event.total * 100) : item !== null && itemTotal !== null && itemTotal > 0 ? Math.round(item / itemTotal * 100) : null;
1875
+ const sectionBadge = section !== null && item !== null && sectionTotal !== null && sectionTotal > 0 ? {
1876
+ label: section,
1877
+ current: item,
1878
+ total: sectionTotal
1879
+ } : null;
1880
+ let countBadge = section !== null && item !== null && itemTotal !== null && itemTotal > 0 ? {
1881
+ label: section,
1882
+ current: item,
1883
+ total: itemTotal
1884
+ } : null;
1885
+ if (countBadge !== null && sectionBadge !== null && itemTotal === sectionTotal) countBadge = null;
1886
+ const detail = typeof event.detail === "string" && event.detail !== "" && event.detail !== section ? event.detail : null;
1887
+ return {
1888
+ label: stageText(event.stage),
1889
+ percent,
1890
+ sectionBadge,
1891
+ countBadge,
1892
+ detail
1893
+ };
1894
+ }
1895
+ //#endregion
1035
1896
  //#region src/client/common/ProgressBar.tsx
1036
1897
  /**
1037
- * 进度条(规范 §29,绑 src/ui/progress.ts)。
1898
+ * 进度条(规范 §29 + m3 真实进度)。
1038
1899
  *
1039
- * 控制器(ExportFlow / ImportWizard)在调用 core 前后发出 ProgressEvent,
1040
- * 本组件按 stage 文案 + step/total 渲染进度条;未知阶段回退显示 id 本身。
1900
+ * 数据流:
1901
+ * - 控制器(ExportFlow / ImportWizard)onProgress 发出普通 ProgressEvent
1902
+ * (阶段文案 + step/total),走兼容路径;
1903
+ * - m3:store 经 /runs + /progress 轮询得到的 RunState 映射为 RunProgress
1904
+ * (额外携带 section/sectionTotal/item/itemTotal/detail),渲染为
1905
+ * 「分区徽章(settings · 3/12)+ 内部计数徽章(plugins · 6/18)+ 当前项名」。
1906
+ *
1907
+ * 所有换算逻辑在 progress-view.ts 的 computeProgressView()(纯函数,可单测);
1908
+ * 本组件只做渲染。未知阶段回退显示 id 本身。
1041
1909
  */
1042
1910
  /**
1043
- * 进度条:阶段文字 + 百分比(有 step/total 时计算,否则不定态动画)。
1911
+ * 进度条:阶段文字 + 分区/内部计数徽章 + 当前项名 + 百分比。
1044
1912
  */
1045
1913
  function ProgressBar({ event, active }) {
1046
- const percent = event !== null && event.step !== void 0 && event.total !== void 0 && event.total > 0 ? Math.round(event.step / event.total * 100) : null;
1047
- const label = event !== null ? stageText(event.stage) : "";
1914
+ const view = computeProgressView(event);
1048
1915
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1049
1916
  className: config_manager_module_css_default.progressBlock,
1050
1917
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1052,22 +1919,41 @@ function ProgressBar({ event, active }) {
1052
1919
  children: [
1053
1920
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1054
1921
  className: config_manager_module_css_default.progressLabel,
1055
- children: label
1922
+ children: view.label
1923
+ }),
1924
+ view.sectionBadge !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1925
+ className: `${config_manager_module_css_default.progressBadge} ${config_manager_module_css_default.progressBadgeSection}`,
1926
+ children: [
1927
+ view.sectionBadge.label,
1928
+ " · ",
1929
+ view.sectionBadge.current,
1930
+ "/",
1931
+ view.sectionBadge.total
1932
+ ]
1056
1933
  }),
1057
- event?.detail !== void 0 && event.detail !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1934
+ view.countBadge !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1935
+ className: `${config_manager_module_css_default.progressBadge} ${config_manager_module_css_default.progressBadgeCount}`,
1936
+ children: [
1937
+ view.countBadge.label !== "" ? `${view.countBadge.label} · ` : "",
1938
+ view.countBadge.current,
1939
+ "/",
1940
+ view.countBadge.total
1941
+ ]
1942
+ }),
1943
+ view.detail !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1058
1944
  className: config_manager_module_css_default.progressDetail,
1059
- children: event.detail
1945
+ children: view.detail
1060
1946
  }),
1061
- percent !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1947
+ view.percent !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1062
1948
  className: config_manager_module_css_default.progressPercent,
1063
- children: [percent, "%"]
1949
+ children: [view.percent, "%"]
1064
1950
  })
1065
1951
  ]
1066
1952
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1067
1953
  className: config_manager_module_css_default.progressTrack,
1068
- children: percent !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1954
+ children: view.percent !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1069
1955
  className: `${config_manager_module_css_default.progressBar} ${active ? "" : config_manager_module_css_default.progressBarDone}`,
1070
- style: { width: `${percent}%` }
1956
+ style: { width: `${view.percent}%` }
1071
1957
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: `${config_manager_module_css_default.progressBar} ${config_manager_module_css_default.progressIndeterminate}` })
1072
1958
  })]
1073
1959
  });
@@ -1169,488 +2055,291 @@ function ReportView({ kind, exportReport, importResult, onAction, onDownload })
1169
2055
  variant: a === "done" ? "primary" : "ghost",
1170
2056
  onClick: () => onAction?.(a),
1171
2057
  children: a
1172
- }, a))
1173
- })
1174
- ] })]
1175
- });
1176
- }
1177
- //#endregion
1178
- //#region src/client/export/ExportView.tsx
1179
- /**
1180
- * 导出视图(规范 §1 / §21,绑 src/ui/export-flow.ts 的 ExportFlow 控制器)。
1181
- *
1182
- * - Quick:一键导出推荐分区(ExportFlow.quickSelection());
1183
- * - Custom:按 §1 分组目录(EXPORT_GROUPS × DEFAULT_CATEGORIES)逐项勾选,
1184
- * ExportFlow.validateSelection() 给出设备相关分区警告;
1185
- * - Include secrets:可选加密备份(需设置加密密码,密码仅本次内存使用,
1186
- * 经 api.exportPassword 随请求体传给 Host 半,绝不落盘/入 manifest);
1187
- * - 进度:ExportFlow.run 发出 ProgressEvent → ProgressBar;
1188
- * - 结果:ReportView(export) + 下载按钮(File System Access API 流式落盘)。
1189
- */
1190
- /** Custom 模式的初始勾选 = 推荐分区(可再调整) */
1191
- function defaultCustomSelection() {
1192
- return DEFAULT_CATEGORIES.filter((c) => c.defaultIncluded).map((c) => c.id);
1193
- }
1194
- /**
1195
- * 导出主视图:Quick/Custom 切换 → 勾选/密码 → 执行 → 进度 → 报告 → 下载。
1196
- */
1197
- function ExportView({ api, t }) {
1198
- const [mode, setMode] = (0, react.useState)("quick");
1199
- const [selection, setSelection] = (0, react.useState)(defaultCustomSelection);
1200
- const [includeSecrets, setIncludeSecrets] = (0, react.useState)(false);
1201
- const [password, setPassword] = (0, react.useState)("");
1202
- const [passwordConfirm, setPasswordConfirm] = (0, react.useState)("");
1203
- const [running, setRunning] = (0, react.useState)(false);
1204
- const [progress, setProgress] = (0, react.useState)(null);
1205
- const [result, setResult] = (0, react.useState)(null);
1206
- const [error, setError] = (0, react.useState)(null);
1207
- const [downloaded, setDownloaded] = (0, react.useState)(false);
1208
- const [flow] = (0, react.useState)(() => new ExportFlow({
1209
- port: api,
1210
- onProgress: (event) => {
1211
- setProgress(event);
1212
- }
1213
- }));
1214
- const toggleSection = (0, react.useCallback)((id, checked) => {
1215
- setSelection((prev) => {
1216
- const has = prev.includes(id);
1217
- if (checked && !has) return [...prev, id];
1218
- if (!checked && has) return prev.filter((s) => s !== id);
1219
- return prev;
1220
- });
1221
- }, []);
1222
- const passwordInvalid = includeSecrets && (password === "" || password !== passwordConfirm);
1223
- /** 执行导出(Quick 或 Custom) */
1224
- const runExport = async () => {
1225
- if (passwordInvalid) return;
1226
- setRunning(true);
1227
- setError(null);
1228
- setResult(null);
1229
- setDownloaded(false);
1230
- setProgress(null);
1231
- try {
1232
- api.exportPassword = includeSecrets ? password : null;
1233
- const run = await flow.run(mode, selection, { includeSecrets });
1234
- setResult(run);
1235
- } catch (err) {
1236
- setError(err);
1237
- } finally {
1238
- setRunning(false);
1239
- setProgress(null);
1240
- }
1241
- };
1242
- /** 下载导出的 ZIP 到本机 */
1243
- const download = async () => {
1244
- if (result === null) return;
1245
- try {
1246
- setError(null);
1247
- await api.download(result.zipPath);
1248
- setDownloaded(true);
1249
- } catch (err) {
1250
- setError(err);
1251
- }
1252
- };
1253
- const customWarnings = mode === "custom" ? flow.validateSelection(selection).warnings : [];
1254
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1255
- className: config_manager_module_css_default.viewBody,
1256
- children: [
1257
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionTitle, {
1258
- title: t("view.export"),
1259
- subtitle: t("section.description")
1260
- }),
1261
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1262
- className: config_manager_module_css_default.modeTabs,
1263
- role: "tablist",
1264
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1265
- type: "button",
1266
- role: "tab",
1267
- "aria-selected": mode === "quick",
1268
- "data-active": mode === "quick" ? "" : void 0,
1269
- className: config_manager_module_css_default.modeTab,
1270
- onClick: () => {
1271
- setMode("quick");
1272
- },
1273
- children: t("export.mode.quick")
1274
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1275
- type: "button",
1276
- role: "tab",
1277
- "aria-selected": mode === "custom",
1278
- "data-active": mode === "custom" ? "" : void 0,
1279
- className: config_manager_module_css_default.modeTab,
1280
- onClick: () => {
1281
- setMode("custom");
1282
- },
1283
- children: t("export.mode.custom")
1284
- })]
1285
- }),
1286
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1287
- className: config_manager_module_css_default.modeHint,
1288
- children: mode === "quick" ? t("export.mode.quickHint") : t("export.mode.customHint")
1289
- }),
1290
- mode === "custom" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1291
- className: config_manager_module_css_default.groupList,
1292
- children: EXPORT_GROUPS.map((group) => {
1293
- const categories = flow.categories.filter((c) => c.group === group.id);
1294
- if (categories.length === 0) return null;
1295
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
1296
- className: config_manager_module_css_default.groupCard,
1297
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1298
- className: config_manager_module_css_default.groupHeader,
1299
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1300
- className: config_manager_module_css_default.groupLabel,
1301
- children: group.label
1302
- }), group.note !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1303
- className: config_manager_module_css_default.groupNote,
1304
- children: group.note
1305
- })]
1306
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1307
- className: config_manager_module_css_default.groupItems,
1308
- children: categories.map((cat) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Checkbox, {
1309
- checked: selection.includes(cat.id),
1310
- onChange: (checked) => {
1311
- toggleSection(cat.id, checked);
1312
- },
1313
- label: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1314
- className: config_manager_module_css_default.categoryItem,
1315
- children: [
1316
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1317
- className: config_manager_module_css_default.categoryName,
1318
- children: cat.label
1319
- }),
1320
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1321
- className: config_manager_module_css_default.categoryDesc,
1322
- children: cat.description
1323
- }),
1324
- cat.portability !== "portable" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
1325
- kind: cat.portability === "deviceSpecific" ? "warn" : "info",
1326
- children: cat.portability
1327
- }),
1328
- cat.sensitive === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
1329
- kind: "warn",
1330
- children: "secret"
1331
- })
1332
- ]
1333
- })
1334
- }, cat.id))
1335
- })]
1336
- }, group.id);
1337
- })
1338
- }),
1339
- customWarnings.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Banner, {
1340
- kind: "warn",
1341
- children: [t("export.selectionWarnings"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1342
- className: config_manager_module_css_default.warnList,
1343
- children: customWarnings.map((w, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: w }, i))
1344
- })]
1345
- }),
1346
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
1347
- className: config_manager_module_css_default.optionsCard,
1348
- children: [
1349
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Checkbox, {
1350
- checked: includeSecrets,
1351
- onChange: setIncludeSecrets,
1352
- label: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1353
- className: config_manager_module_css_default.categoryName,
1354
- children: t("export.includeSecrets")
1355
- })
1356
- }),
1357
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1358
- className: config_manager_module_css_default.hint,
1359
- children: t("export.includeSecretsHint")
1360
- }),
1361
- includeSecrets && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1362
- className: config_manager_module_css_default.secretFields,
1363
- children: [
1364
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1365
- className: config_manager_module_css_default.field,
1366
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1367
- className: config_manager_module_css_default.fieldLabel,
1368
- children: t("export.password")
1369
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1370
- type: "password",
1371
- className: config_manager_module_css_default.input,
1372
- value: password,
1373
- onChange: (e) => {
1374
- setPassword(e.target.value);
1375
- }
1376
- })]
1377
- }),
1378
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1379
- className: config_manager_module_css_default.field,
1380
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1381
- className: config_manager_module_css_default.fieldLabel,
1382
- children: t("export.passwordConfirm")
1383
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1384
- type: "password",
1385
- className: config_manager_module_css_default.input,
1386
- value: passwordConfirm,
1387
- onChange: (e) => {
1388
- setPasswordConfirm(e.target.value);
1389
- }
1390
- })]
1391
- }),
1392
- password !== "" && password !== passwordConfirm && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1393
- className: config_manager_module_css_default.formError,
1394
- children: t("export.passwordMismatch")
1395
- }),
1396
- password === "" && includeSecrets && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1397
- className: config_manager_module_css_default.formError,
1398
- children: t("export.passwordRequired")
1399
- })
1400
- ]
1401
- })
1402
- ]
1403
- }),
1404
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1405
- className: config_manager_module_css_default.actionRow,
1406
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
1407
- variant: "primary",
1408
- disabled: running || passwordInvalid,
1409
- onClick: () => {
1410
- runExport();
1411
- },
1412
- children: running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: t("export.running") }) : t("export.run")
1413
- })
1414
- }),
1415
- running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProgressBar, {
1416
- event: progress,
1417
- active: true
1418
- }),
1419
- error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, {
1420
- error,
1421
- onRetry: () => {
1422
- runExport();
1423
- },
1424
- retrying: running
1425
- }),
1426
- result !== null && !running && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReportView, {
1427
- kind: "export",
1428
- exportReport: result.report,
1429
- onDownload: () => {
1430
- download();
1431
- }
1432
- }), downloaded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Banner, {
1433
- kind: "ok",
1434
- children: ["Saved: ", result.report.file.name]
1435
- })] })
1436
- ]
2058
+ }, a))
2059
+ })
2060
+ ] })]
1437
2061
  });
1438
2062
  }
1439
2063
  //#endregion
1440
- //#region src/ui/import-wizard.ts
1441
- var ImportWizard = class {
1442
- port;
1443
- onProgress;
1444
- tracker;
1445
- step = "select";
1446
- zipPath = null;
1447
- analysis = null;
1448
- plan = null;
1449
- result = null;
1450
- rollbackOnError;
1451
- errors = [];
1452
- decisions = {
1453
- strategy: "merge",
1454
- resolutions: {},
1455
- pathMappings: []
2064
+ //#region src/client/export/ExportView.tsx
2065
+ /**
2066
+ * 导出视图(规范 §1 / §21,绑 src/ui/export-flow.ts 的 ExportFlow 控制器)。
2067
+ *
2068
+ * - Quick:一键导出推荐分区(ExportFlow.quickSelection());
2069
+ * - Custom:按 §1 分组目录(EXPORT_GROUPS × DEFAULT_CATEGORIES)逐项勾选,
2070
+ * ExportFlow.validateSelection() 给出设备相关分区警告;
2071
+ * - Include secrets:可选加密备份(需设置加密密码,密码仅本次内存使用,
2072
+ * 经 api.exportPassword 随请求体传给 Host 半,绝不落盘/入 manifest,
2073
+ * **也绝不进入 sessionStorage** —— m2 白名单剔除,刷新后要求重输);
2074
+ * - 进度:ExportFlow.run 发出 ProgressEvent → ProgressBar;
2075
+ * - 结果:ReportView(export) + 下载按钮(File System Access API 流式落盘)。
2076
+ *
2077
+ * m2:全部 UI 状态由模块级 runStore 持有(切 tab/关面板不重建、刷新恢复),
2078
+ * 控制器实例(ExportFlow)由 store 缓存复用。
2079
+ */
2080
+ /**
2081
+ * 导出主视图:Quick/Custom 切换 → 勾选/密码 → 执行 → 进度 → 报告 → 下载。
2082
+ */
2083
+ function ExportView({ api, t }) {
2084
+ const exp = (0, react.useSyncExternalStore)(runStore.subscribe, runStore.getSnapshot).export;
2085
+ const flow = runStore.exportFlow(api);
2086
+ const mode = exp.mode;
2087
+ const selection = exp.selection;
2088
+ const includeSecrets = exp.includeSecrets;
2089
+ const password = exp.password;
2090
+ const passwordConfirm = exp.passwordConfirm;
2091
+ const running = exp.running;
2092
+ const progress = exp.progress;
2093
+ const result = exp.result;
2094
+ const error = exp.error;
2095
+ const downloaded = exp.downloaded;
2096
+ const setMode = (next) => {
2097
+ runStore.patch({ export: { mode: next } });
1456
2098
  };
1457
- secretInputs = {};
1458
- constructor(opts) {
1459
- this.port = opts.port;
1460
- this.onProgress = opts.onProgress;
1461
- this.tracker = new ProgressTracker(IMPORT_STAGES, this.onProgress);
1462
- this.rollbackOnError = opts.defaultRollbackOnError ?? true;
1463
- }
1464
- /** 当前状态快照(React 绑定 / 测试断言用) */
1465
- snapshot() {
1466
- return {
1467
- step: this.step,
1468
- zipPath: this.zipPath,
1469
- analysis: this.analysis,
1470
- plan: this.plan,
1471
- result: this.result,
1472
- rollbackOnError: this.rollbackOnError,
1473
- errors: [...this.errors]
1474
- };
1475
- }
1476
- get currentStep() {
1477
- return this.step;
1478
- }
1479
- /** 步骤 1-2:选 ZIP → Analyzing → Compatibility(analyzeImport 零写入) */
1480
- async selectZip(path) {
1481
- this.zipPath = path;
1482
- this.step = "analyzing";
1483
- this.tracker.emit("validating");
1484
- this.errors = [];
2099
+ const setIncludeSecrets = (next) => {
2100
+ runStore.patch({ export: { includeSecrets: next } });
2101
+ };
2102
+ const setPassword = (value) => {
2103
+ runStore.patch({ export: { password: value } });
2104
+ };
2105
+ const setPasswordConfirm = (value) => {
2106
+ runStore.patch({ export: { passwordConfirm: value } });
2107
+ };
2108
+ const toggleSection = (0, react.useCallback)((id, checked) => {
2109
+ const has = selection.includes(id);
2110
+ if (checked && !has) runStore.patch({ export: { selection: [...selection, id] } });
2111
+ if (!checked && has) runStore.patch({ export: { selection: selection.filter((s) => s !== id) } });
2112
+ }, [selection]);
2113
+ const passwordInvalid = includeSecrets && (password === "" || password !== passwordConfirm);
2114
+ /** 执行导出(Quick 或 Custom) */
2115
+ const runExport = async () => {
2116
+ if (passwordInvalid) return;
2117
+ runStore.patch({ export: {
2118
+ running: true,
2119
+ error: null,
2120
+ result: null,
2121
+ downloaded: false,
2122
+ progress: null,
2123
+ runId: null
2124
+ } });
2125
+ runStore.watchRunning("export", 500);
1485
2126
  try {
1486
- this.analysis = await this.port.analyzeImport(path);
1487
- this.tracker.emit("checking-compatibility");
1488
- if (!this.analysis.valid) {
1489
- this.errors.push(...this.analysis.errors.map((e) => formatActionableError(toActionableError(new Error(e)))));
1490
- throw new Error(this.analysis.errors.join("; ") || "备份分析失败");
1491
- }
1492
- this.step = "compatibility";
1493
- return this.analysis;
2127
+ api.exportPassword = includeSecrets ? password : null;
2128
+ const run = await flow.run(mode, selection, { includeSecrets });
2129
+ const runId = run.runId;
2130
+ runStore.patch({ export: {
2131
+ result: run,
2132
+ runId: typeof runId === "string" ? runId : null,
2133
+ progress: {
2134
+ stage: "done",
2135
+ step: 1,
2136
+ total: 1
2137
+ }
2138
+ } });
1494
2139
  } catch (err) {
1495
- if (this.errors.length === 0) this.errors.push(formatActionableError(toActionableError(err)));
1496
- throw err;
2140
+ runStore.patch({ export: { error: err instanceof Error ? err.message : String(err) } });
2141
+ } finally {
2142
+ runStore.stopRunWatch("export");
2143
+ runStore.patch({ export: { running: false } });
1497
2144
  }
1498
- }
1499
- /** 步骤 3→4:用户确认兼容性后进入 Preview(Dry Run:用当前决策生成计划摘要,零写入) */
1500
- async confirmCompatibility() {
1501
- if (this.analysis === null || this.zipPath === null) throw new Error("尚未完成分析,请先选择备份文件");
1502
- this.plan = await this.port.createImportPlan(this.zipPath, this.decisions);
1503
- this.step = "preview";
1504
- return this.plan;
1505
- }
1506
- /** 更新全局冲突策略(merge/replace/skipExisting,规范 §11) */
1507
- setStrategy(strategy) {
1508
- this.decisions = {
1509
- ...this.decisions,
1510
- strategy
1511
- };
1512
- }
1513
- /** 设置逐项冲突决策(keepCurrent/useImported/review) */
1514
- setResolutions(resolutions) {
1515
- this.decisions = {
1516
- ...this.decisions,
1517
- resolutions
1518
- };
1519
- }
1520
- /** 设置路径映射(§12) */
1521
- setPathMappings(mappings) {
1522
- this.decisions = {
1523
- ...this.decisions,
1524
- pathMappings: mappings
1525
- };
1526
- }
1527
- /** 设置秘密补录值(仅内存,绝不持久化) */
1528
- setSecretInputs(inputs) {
1529
- this.secretInputs = inputs;
1530
- }
1531
- /** Preview 摘要(规范 §10 数值化;基于当前 plan 与 analysis) */
1532
- previewSummary() {
1533
- const items = this.plan?.items ?? [];
1534
- const analysis = this.analysis;
1535
- const count = (kinds) => items.filter((i) => kinds.includes(i.kind)).length;
1536
- return {
1537
- willChange: count([
1538
- "Create",
1539
- "Update",
1540
- "Install",
1541
- "Conflict"
1542
- ]),
1543
- unchanged: count(["Skip"]),
1544
- settingsUpdates: count(["Create", "Update"]),
1545
- pluginsInstalled: analysis?.pluginSummary.installed ?? 0,
1546
- pluginsToInstall: count(["Install"]),
1547
- mcpAdds: items.filter((i) => i.adapter === "mcp" && i.kind === "Create").length,
1548
- prompts: items.filter((i) => i.adapter === "prompts" && i.kind !== "Skip").length,
1549
- pathMappingsNeeded: analysis?.pathIssues.length ?? 0,
1550
- secretsNeeded: this.plan?.missingSecrets.length ?? analysis?.secretCount ?? 0,
1551
- conflicts: count(["Conflict"]),
1552
- needsRestart: this.plan?.needsRestart ?? false
1553
- };
1554
- }
1555
- /** 冲突项(供 ConflictCollector 使用) */
1556
- conflictItems() {
1557
- return (this.plan?.items ?? []).filter((i) => i.kind === "Conflict");
1558
- }
1559
- /** 设置回滚策略(场景 E 选择;执行前调用) */
1560
- setRollbackOnError(enable) {
1561
- this.rollbackOnError = enable;
1562
- }
1563
- /**
1564
- * 步骤 10-14:确认导入 → 快照 → 执行 → 校验 → 结果。
1565
- * 用最终决策重建计划(与预览一致),显式传 rollbackOnError。
1566
- */
1567
- async execute(opts) {
1568
- if (this.zipPath === null || this.analysis === null) throw new Error("尚未完成分析,请先选择备份文件");
1569
- if (opts.confirm !== true) throw new Error("导入未确认:必须确认后才允许修改任何数据");
1570
- const rollbackOnError = opts.rollbackOnError ?? this.rollbackOnError;
1571
- this.step = "importing";
1572
- this.tracker.emit("creating-snapshot");
2145
+ };
2146
+ /** 下载导出的 ZIP 到本机 */
2147
+ const download = async () => {
2148
+ if (result === null) return;
1573
2149
  try {
1574
- this.plan = await this.port.createImportPlan(this.zipPath, this.decisions);
1575
- this.tracker.emit(EXECUTING_STAGE);
1576
- this.result = await this.port.executeImportPlan(this.zipPath, this.plan, {
1577
- confirm: true,
1578
- secretInputs: this.secretInputs,
1579
- rollbackOnError
1580
- });
1581
- if (!this.result.ok && this.result.rollback) this.tracker.emit("rolling-back");
1582
- this.tracker.emit("done");
1583
- this.step = "result";
1584
- return this.result;
2150
+ runStore.patch({ export: { error: null } });
2151
+ await api.download(result.zipPath);
2152
+ runStore.patch({ export: { downloaded: true } });
1585
2153
  } catch (err) {
1586
- this.errors.push(formatActionableError(toActionableError(err)));
1587
- throw err;
2154
+ runStore.patch({ export: { error: err instanceof Error ? err.message : String(err) } });
1588
2155
  }
1589
- }
1590
- /** 重置向导(可复用实例开始新导入) */
1591
- reset() {
1592
- this.step = "select";
1593
- this.zipPath = null;
1594
- this.analysis = null;
1595
- this.plan = null;
1596
- this.result = null;
1597
- this.errors = [];
1598
- this.decisions = {
1599
- strategy: "merge",
1600
- resolutions: {},
1601
- pathMappings: []
1602
- };
1603
- this.secretInputs = {};
1604
- }
1605
- };
1606
- //#endregion
1607
- //#region src/ui/conflict-view.ts
1608
- var ConflictCollector = class {
1609
- plan;
1610
- decisions = /* @__PURE__ */ new Map();
1611
- constructor(plan) {
1612
- this.plan = plan;
1613
- }
1614
- /** 需要用户决策的冲突项(kind === 'Conflict') */
1615
- get conflicts() {
1616
- return this.plan.items.filter((i) => i.kind === "Conflict");
1617
- }
1618
- /** 设置某项决策;未出现在冲突列表中的 id 忽略并返回 false */
1619
- resolve(itemId, resolution) {
1620
- if (!this.conflicts.some((i) => i.id === itemId)) return false;
1621
- this.decisions.set(itemId, resolution);
1622
- return true;
1623
- }
1624
- /** 单项当前决策(未决策返回 null) */
1625
- decisionOf(itemId) {
1626
- return this.decisions.get(itemId) ?? null;
1627
- }
1628
- /** 仍未解决(review 或未决策)的冲突项 */
1629
- unresolved() {
1630
- return this.conflicts.filter((i) => {
1631
- const d = this.decisions.get(i.id);
1632
- return d === void 0 || d === "review";
1633
- });
1634
- }
1635
- get hasUnresolved() {
1636
- return this.unresolved().length > 0;
1637
- }
1638
- /** 转 core 决策表(供 createImportPlan / executeImportPlan 使用) */
1639
- toResolutions() {
1640
- const out = {};
1641
- for (const [id, res] of this.decisions) out[id] = res;
1642
- return out;
1643
- }
1644
- /** 构造视图项列表(React 绑定用):含当前/导入摘要占位 */
1645
- viewItems() {
1646
- return this.conflicts.map((item) => ({
1647
- item,
1648
- currentSummary: item.detail?.split("\n")[0],
1649
- importedSummary: void 0,
1650
- resolution: this.decisionOf(item.id)
1651
- }));
1652
- }
1653
- };
2156
+ };
2157
+ const customWarnings = mode === "custom" ? flow.validateSelection(selection).warnings : [];
2158
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2159
+ className: config_manager_module_css_default.viewBody,
2160
+ children: [
2161
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionTitle, {
2162
+ title: t("view.export"),
2163
+ subtitle: t("section.description")
2164
+ }),
2165
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2166
+ className: config_manager_module_css_default.modeTabs,
2167
+ role: "tablist",
2168
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2169
+ type: "button",
2170
+ role: "tab",
2171
+ "aria-selected": mode === "quick",
2172
+ "data-active": mode === "quick" ? "" : void 0,
2173
+ className: config_manager_module_css_default.modeTab,
2174
+ onClick: () => {
2175
+ setMode("quick");
2176
+ },
2177
+ children: t("export.mode.quick")
2178
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2179
+ type: "button",
2180
+ role: "tab",
2181
+ "aria-selected": mode === "custom",
2182
+ "data-active": mode === "custom" ? "" : void 0,
2183
+ className: config_manager_module_css_default.modeTab,
2184
+ onClick: () => {
2185
+ setMode("custom");
2186
+ },
2187
+ children: t("export.mode.custom")
2188
+ })]
2189
+ }),
2190
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2191
+ className: config_manager_module_css_default.modeHint,
2192
+ children: mode === "quick" ? t("export.mode.quickHint") : t("export.mode.customHint")
2193
+ }),
2194
+ mode === "custom" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2195
+ className: config_manager_module_css_default.groupList,
2196
+ children: EXPORT_GROUPS.map((group) => {
2197
+ const categories = flow.categories.filter((c) => c.group === group.id);
2198
+ if (categories.length === 0) return null;
2199
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
2200
+ className: config_manager_module_css_default.groupCard,
2201
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2202
+ className: config_manager_module_css_default.groupHeader,
2203
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2204
+ className: config_manager_module_css_default.groupLabel,
2205
+ children: group.label
2206
+ }), group.note !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2207
+ className: config_manager_module_css_default.groupNote,
2208
+ children: group.note
2209
+ })]
2210
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2211
+ className: config_manager_module_css_default.groupItems,
2212
+ children: categories.map((cat) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Checkbox, {
2213
+ checked: selection.includes(cat.id),
2214
+ onChange: (checked) => {
2215
+ toggleSection(cat.id, checked);
2216
+ },
2217
+ label: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2218
+ className: config_manager_module_css_default.categoryItem,
2219
+ children: [
2220
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2221
+ className: config_manager_module_css_default.categoryName,
2222
+ children: cat.label
2223
+ }),
2224
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2225
+ className: config_manager_module_css_default.categoryDesc,
2226
+ children: cat.description
2227
+ }),
2228
+ cat.portability !== "portable" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
2229
+ kind: cat.portability === "deviceSpecific" ? "warn" : "info",
2230
+ children: cat.portability
2231
+ }),
2232
+ cat.sensitive === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
2233
+ kind: "warn",
2234
+ children: "secret"
2235
+ })
2236
+ ]
2237
+ })
2238
+ }, cat.id))
2239
+ })]
2240
+ }, group.id);
2241
+ })
2242
+ }),
2243
+ customWarnings.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Banner, {
2244
+ kind: "warn",
2245
+ children: [t("export.selectionWarnings"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
2246
+ className: config_manager_module_css_default.warnList,
2247
+ children: customWarnings.map((w, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: w }, i))
2248
+ })]
2249
+ }),
2250
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
2251
+ className: config_manager_module_css_default.optionsCard,
2252
+ children: [
2253
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Checkbox, {
2254
+ checked: includeSecrets,
2255
+ onChange: setIncludeSecrets,
2256
+ label: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2257
+ className: config_manager_module_css_default.categoryName,
2258
+ children: t("export.includeSecrets")
2259
+ })
2260
+ }),
2261
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2262
+ className: config_manager_module_css_default.hint,
2263
+ children: t("export.includeSecretsHint")
2264
+ }),
2265
+ includeSecrets && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2266
+ className: config_manager_module_css_default.secretFields,
2267
+ children: [
2268
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2269
+ className: config_manager_module_css_default.field,
2270
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2271
+ className: config_manager_module_css_default.fieldLabel,
2272
+ children: t("export.password")
2273
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2274
+ type: "password",
2275
+ className: config_manager_module_css_default.input,
2276
+ value: password,
2277
+ onChange: (e) => {
2278
+ setPassword(e.target.value);
2279
+ }
2280
+ })]
2281
+ }),
2282
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2283
+ className: config_manager_module_css_default.field,
2284
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2285
+ className: config_manager_module_css_default.fieldLabel,
2286
+ children: t("export.passwordConfirm")
2287
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2288
+ type: "password",
2289
+ className: config_manager_module_css_default.input,
2290
+ value: passwordConfirm,
2291
+ onChange: (e) => {
2292
+ setPasswordConfirm(e.target.value);
2293
+ }
2294
+ })]
2295
+ }),
2296
+ password !== "" && password !== passwordConfirm && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2297
+ className: config_manager_module_css_default.formError,
2298
+ children: t("export.passwordMismatch")
2299
+ }),
2300
+ password === "" && includeSecrets && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2301
+ className: config_manager_module_css_default.formError,
2302
+ children: t("export.passwordRequired")
2303
+ })
2304
+ ]
2305
+ })
2306
+ ]
2307
+ }),
2308
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2309
+ className: config_manager_module_css_default.actionRow,
2310
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2311
+ variant: "primary",
2312
+ disabled: running || passwordInvalid,
2313
+ onClick: () => {
2314
+ runExport();
2315
+ },
2316
+ children: running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: t("export.running") }) : t("export.run")
2317
+ })
2318
+ }),
2319
+ running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProgressBar, {
2320
+ event: progress,
2321
+ active: true
2322
+ }),
2323
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, {
2324
+ error,
2325
+ onRetry: () => {
2326
+ runExport();
2327
+ },
2328
+ retrying: running
2329
+ }),
2330
+ result !== null && !running && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReportView, {
2331
+ kind: "export",
2332
+ exportReport: result.report,
2333
+ onDownload: () => {
2334
+ download();
2335
+ }
2336
+ }), downloaded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Banner, {
2337
+ kind: "ok",
2338
+ children: ["Saved: ", result.report.file.name]
2339
+ })] })
2340
+ ]
2341
+ });
2342
+ }
1654
2343
  //#endregion
1655
2344
  //#region src/ui/flow.ts
1656
2345
  /**
@@ -1840,13 +2529,18 @@ function PathMappingForm({ issues, initial, t, onChange }) {
1840
2529
  * 安全/正确性约束(来自 ImportWizard 与 core):
1841
2530
  * - analyzeImport / createImportPlan 零写入(Dry Run 复用);
1842
2531
  * - executeImportPlan 必须 confirm=true(core 安全阀);
1843
- * - 秘密补录值仅内存(secretInputs),经 HTTPS 请求体传给 Host,绝不落日志/落盘;
2532
+ * - 秘密补录值仅内存(secretInputs),经 HTTPS 请求体传给 Host,绝不落日志/落盘,
2533
+ * **也绝不进入 sessionStorage**(m2 白名单剔除,刷新后 secrets 阶段要求重输);
1844
2534
  * - 默认整体回滚(rollbackOnError=true),用户在 Confirm 步可切换。
1845
2535
  *
1846
2536
  * 数据流:本地文件 → api.upload → zipPath → wizard.selectZip/confirmCompatibility/
1847
2537
  * setResolutions/setPathMappings/setSecretInputs → wizard.execute。
1848
2538
  * 中间阶段(conflicts/path-mapping/secrets)是 UI 层流程页,wizard 的 decisions 由
1849
2539
  * 对应组件收集后写入。
2540
+ *
2541
+ * m2:全部 UI 状态由模块级 runStore 持有(切 tab/关面板不重建、刷新恢复),
2542
+ * 控制器实例(ImportWizard)由 store 缓存复用;每次 wizard 动作后 syncWizard()
2543
+ * 把控制器快照镜像进 store(非敏感字段持久化)。
1850
2544
  */
1851
2545
  /** 中间流程阶段(wizard.step 之外的 UI 层页面)——定义见 src/ui/flow.ts */
1852
2546
  const SCORE_LABEL = {
@@ -1855,7 +2549,7 @@ const SCORE_LABEL = {
1855
2549
  partial: "Partial",
1856
2550
  unsupported: "Unsupported"
1857
2551
  };
1858
- /** 密钥补录表单(仅内存收集,值不外泄) */
2552
+ /** 密钥补录表单(仅内存收集,值不外泄;onChange 写入 store 的仅内存字段) */
1859
2553
  function SecretsForm({ missing, t, onChange }) {
1860
2554
  const [inputs, setInputs] = (0, react.useState)({});
1861
2555
  const setRef = (ref, value) => {
@@ -1900,31 +2594,25 @@ function SecretsForm({ missing, t, onChange }) {
1900
2594
  * 导入向导主视图。
1901
2595
  */
1902
2596
  function ImportWizardView({ api, t }) {
1903
- const [wizard] = (0, react.useState)(() => new ImportWizard({
1904
- port: api,
1905
- onProgress: (event) => {
1906
- setProgress(event);
1907
- },
1908
- defaultRollbackOnError: true
1909
- }));
1910
- const [, setTick] = (0, react.useState)(0);
1911
- const refresh = () => {
1912
- setTick((v) => v + 1);
1913
- };
1914
- const [progress, setProgress] = (0, react.useState)(null);
1915
- const [error, setError] = (0, react.useState)(null);
1916
- const [phase, setPhase] = (0, react.useState)("preview");
1917
- const [uploading, setUploading] = (0, react.useState)(false);
1918
- const [rollbackOnError, setRollbackOnError] = (0, react.useState)(true);
1919
- const [conflictCollector, setConflictCollector] = (0, react.useState)(null);
1920
- const [pathMappings, setPathMappings] = (0, react.useState)([]);
1921
- const [secretInputs, setSecretInputs] = (0, react.useState)({});
2597
+ const imp = (0, react.useSyncExternalStore)(runStore.subscribe, runStore.getSnapshot).import;
2598
+ const wizard = runStore.importWizard(api);
2599
+ const step = imp.step;
2600
+ const phase = imp.phase;
2601
+ const progress = imp.progress;
2602
+ const error = imp.error;
2603
+ const uploading = imp.uploading;
2604
+ const running = imp.running;
2605
+ const rollbackOnError = imp.rollbackOnError;
2606
+ const conflictCollector = imp.conflictCollector;
2607
+ const pathMappings = imp.pathMappings;
2608
+ const secretInputs = imp.secretInputs;
1922
2609
  const fileInput = (0, react.useRef)(null);
1923
- const snapshot = wizard.snapshot();
1924
- const step = snapshot.step;
1925
- const hasConflicts = (snapshot.plan?.items ?? []).some((i) => i.kind === "Conflict");
1926
- const hasPathIssues = (snapshot.analysis?.pathIssues.length ?? 0) > 0;
1927
- const hasSecrets = (snapshot.plan?.missingSecrets.length ?? 0) > 0;
2610
+ const setPhase = (next) => {
2611
+ runStore.patch({ import: { phase: next } });
2612
+ };
2613
+ const hasConflicts = (imp.plan?.items ?? []).some((i) => i.kind === "Conflict");
2614
+ const hasPathIssues = (imp.analysis?.pathIssues.length ?? 0) > 0;
2615
+ const hasSecrets = (imp.plan?.missingSecrets.length ?? 0) > 0;
1928
2616
  /**
1929
2617
  * 适用阶段的有序列表(仅含需要用户处理 + 确认页)。
1930
2618
  * hasConflicts/hasPathIssues/hasSecrets 基于原始 analysis/plan(Dry Run 产物),
@@ -1944,39 +2632,45 @@ function ImportWizardView({ api, t }) {
1944
2632
  /** 选择并上传 ZIP → wizard.selectZip(analyzing → compatibility) */
1945
2633
  const onPickFile = async (file) => {
1946
2634
  if (file === void 0) return;
1947
- setUploading(true);
1948
- setError(null);
2635
+ runStore.patch({ import: {
2636
+ uploading: true,
2637
+ error: null
2638
+ } });
1949
2639
  try {
1950
2640
  const uploaded = await api.upload(file);
1951
2641
  await wizard.selectZip(uploaded.zipPath);
1952
- refresh();
2642
+ runStore.syncWizard();
1953
2643
  } catch (err) {
1954
- setError(err);
1955
- refresh();
2644
+ runStore.patch({ import: { error: err instanceof Error ? err.message : String(err) } });
2645
+ runStore.syncWizard();
1956
2646
  } finally {
1957
- setUploading(false);
2647
+ runStore.patch({ import: { uploading: false } });
1958
2648
  }
1959
2649
  };
1960
2650
  /** Compatibility → Preview */
1961
2651
  const goPreview = async () => {
1962
- setError(null);
2652
+ runStore.patch({ import: { error: null } });
1963
2653
  try {
1964
2654
  await wizard.confirmCompatibility();
1965
- refresh();
2655
+ runStore.syncWizard();
1966
2656
  } catch (err) {
1967
- setError(err);
1968
- refresh();
2657
+ runStore.patch({ import: { error: err instanceof Error ? err.message : String(err) } });
2658
+ runStore.syncWizard();
1969
2659
  }
1970
2660
  };
1971
2661
  /** 进入 conflicts 阶段(先创建 collector) */
1972
2662
  const enterConflicts = () => {
1973
- const plan = snapshot.plan;
1974
- if (plan !== null && conflictCollector === null) setConflictCollector(new ConflictCollector(plan));
2663
+ const plan = imp.plan;
2664
+ if (plan !== null && imp.conflictCollector === null) runStore.patch({ import: { conflictCollector: new ConflictCollector(plan) } });
1975
2665
  setPhase("conflicts");
1976
2666
  };
1977
- /** Conflicts 完成:写入决策 → 下一阶段 */
2667
+ /** Conflicts 完成:写入决策 → 下一阶段(决策同时持久化,切 tab/刷新可恢复) */
1978
2668
  const finishConflicts = () => {
1979
- if (conflictCollector !== null) wizard.setResolutions(conflictCollector.toResolutions());
2669
+ if (imp.conflictCollector !== null) {
2670
+ const resolutions = imp.conflictCollector.toResolutions();
2671
+ wizard.setResolutions(resolutions);
2672
+ runStore.patch({ import: { conflictResolutions: resolutions } });
2673
+ }
1980
2674
  setPhase(nextPhase("conflicts"));
1981
2675
  };
1982
2676
  /** Path Mapping 完成:写入映射 → 下一阶段 */
@@ -1984,34 +2678,52 @@ function ImportWizardView({ api, t }) {
1984
2678
  wizard.setPathMappings(pathMappings);
1985
2679
  setPhase(nextPhase("path-mapping"));
1986
2680
  };
1987
- /** Secrets 完成:写入补录值 → Confirm */
2681
+ /** Secrets 完成:写入补录值(仅内存)→ Confirm */
1988
2682
  const finishSecrets = () => {
1989
2683
  wizard.setSecretInputs(secretInputs);
1990
2684
  setPhase("confirm");
1991
2685
  };
1992
2686
  /** Confirm 执行:confirm=true(安全阀)+ 用户回滚策略 */
1993
2687
  const execute = async () => {
1994
- setError(null);
2688
+ runStore.patch({ import: {
2689
+ error: null,
2690
+ running: true
2691
+ } });
2692
+ runStore.watchRunning("import", 500);
1995
2693
  try {
1996
- await wizard.execute({
2694
+ const promise = wizard.execute({
1997
2695
  confirm: true,
1998
2696
  rollbackOnError
1999
2697
  });
2000
- refresh();
2698
+ runStore.syncWizard();
2699
+ const runId = (await promise).runId;
2700
+ runStore.patch({ import: { runId: typeof runId === "string" ? runId : null } });
2701
+ runStore.syncWizard();
2001
2702
  } catch (err) {
2002
- setError(err);
2003
- refresh();
2703
+ runStore.patch({ import: { error: err instanceof Error ? err.message : String(err) } });
2704
+ runStore.syncWizard();
2705
+ } finally {
2706
+ runStore.stopRunWatch("import");
2707
+ runStore.patch({ import: { running: false } });
2004
2708
  }
2005
2709
  };
2006
2710
  /** 重置向导(重新导入) */
2007
2711
  const resetWizard = () => {
2008
2712
  wizard.reset();
2009
- setError(null);
2010
- setPhase("preview");
2011
- setConflictCollector(null);
2012
- setPathMappings([]);
2013
- setSecretInputs({});
2014
- refresh();
2713
+ runStore.syncWizard();
2714
+ runStore.patch({ import: {
2715
+ phase: "preview",
2716
+ uploading: false,
2717
+ running: false,
2718
+ progress: null,
2719
+ error: null,
2720
+ runId: null,
2721
+ conflictCollector: null,
2722
+ conflictStrategy: "merge",
2723
+ conflictResolutions: {},
2724
+ pathMappings: [],
2725
+ secretInputs: {}
2726
+ } });
2015
2727
  };
2016
2728
  if (step === "select") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2017
2729
  className: config_manager_module_css_default.viewBody,
@@ -2057,11 +2769,11 @@ function ImportWizardView({ api, t }) {
2057
2769
  error,
2058
2770
  onRetry: resetWizard
2059
2771
  }),
2060
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorList, { errors: snapshot.errors })
2772
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorList, { errors: imp.errors })
2061
2773
  ]
2062
2774
  });
2063
2775
  if (step === "compatibility") {
2064
- const analysis = snapshot.analysis;
2776
+ const analysis = imp.analysis;
2065
2777
  if (analysis === null) return null;
2066
2778
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2067
2779
  className: config_manager_module_css_default.viewBody,
@@ -2190,7 +2902,7 @@ function ImportWizardView({ api, t }) {
2190
2902
  });
2191
2903
  }
2192
2904
  if (phase === "conflicts" && step === "preview") {
2193
- if (conflictCollector === null || snapshot.plan === null) return null;
2905
+ if (conflictCollector === null || imp.plan === null) return null;
2194
2906
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2195
2907
  className: config_manager_module_css_default.viewBody,
2196
2908
  children: [
@@ -2201,7 +2913,9 @@ function ImportWizardView({ api, t }) {
2201
2913
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConflictList, {
2202
2914
  collector: conflictCollector,
2203
2915
  t,
2204
- onChanged: refresh
2916
+ onChanged: () => {
2917
+ if (imp.conflictCollector !== null) runStore.patch({ import: { conflictResolutions: imp.conflictCollector.toResolutions() } });
2918
+ }
2205
2919
  }),
2206
2920
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2207
2921
  className: config_manager_module_css_default.actionRow,
@@ -2222,7 +2936,7 @@ function ImportWizardView({ api, t }) {
2222
2936
  });
2223
2937
  }
2224
2938
  if (phase === "path-mapping" && step === "preview") {
2225
- const issues = snapshot.analysis?.pathIssues ?? [];
2939
+ const issues = imp.analysis?.pathIssues ?? [];
2226
2940
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2227
2941
  className: config_manager_module_css_default.viewBody,
2228
2942
  children: [
@@ -2234,7 +2948,9 @@ function ImportWizardView({ api, t }) {
2234
2948
  issues,
2235
2949
  initial: pathMappings,
2236
2950
  t,
2237
- onChange: setPathMappings
2951
+ onChange: (mappings) => {
2952
+ runStore.patch({ import: { pathMappings: mappings } });
2953
+ }
2238
2954
  }),
2239
2955
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2240
2956
  className: config_manager_module_css_default.actionRow,
@@ -2254,7 +2970,7 @@ function ImportWizardView({ api, t }) {
2254
2970
  });
2255
2971
  }
2256
2972
  if (phase === "secrets" && step === "preview") {
2257
- const missing = snapshot.plan?.missingSecrets ?? [];
2973
+ const missing = imp.plan?.missingSecrets ?? [];
2258
2974
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2259
2975
  className: config_manager_module_css_default.viewBody,
2260
2976
  children: [
@@ -2262,7 +2978,9 @@ function ImportWizardView({ api, t }) {
2262
2978
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SecretsForm, {
2263
2979
  missing,
2264
2980
  t,
2265
- onChange: setSecretInputs
2981
+ onChange: (inputs) => {
2982
+ runStore.patch({ import: { secretInputs: inputs } });
2983
+ }
2266
2984
  }),
2267
2985
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2268
2986
  className: config_manager_module_css_default.actionRow,
@@ -2292,7 +3010,10 @@ function ImportWizardView({ api, t }) {
2292
3010
  }),
2293
3011
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Checkbox, {
2294
3012
  checked: rollbackOnError,
2295
- onChange: setRollbackOnError,
3013
+ onChange: (v) => {
3014
+ wizard.setRollbackOnError(v);
3015
+ runStore.patch({ import: { rollbackOnError: v } });
3016
+ },
2296
3017
  label: t("import.rollbackOnError")
2297
3018
  }),
2298
3019
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2305,6 +3026,7 @@ function ImportWizardView({ api, t }) {
2305
3026
  children: t("common.back")
2306
3027
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2307
3028
  variant: "primary",
3029
+ disabled: running,
2308
3030
  onClick: () => {
2309
3031
  execute();
2310
3032
  },
@@ -2331,86 +3053,368 @@ function ImportWizardView({ api, t }) {
2331
3053
  children: t("import.importing")
2332
3054
  }),
2333
3055
  error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, { error }),
2334
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorList, { errors: snapshot.errors })
3056
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorList, { errors: imp.errors })
3057
+ ]
3058
+ });
3059
+ if (step === "result") {
3060
+ const result = imp.result;
3061
+ if (result === null) return null;
3062
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3063
+ className: config_manager_module_css_default.viewBody,
3064
+ children: [
3065
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionTitle, { title: t("report.import.title") }),
3066
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReportView, {
3067
+ kind: "import",
3068
+ importResult: result,
3069
+ onAction: (action) => {
3070
+ if (action === "done") resetWizard();
3071
+ }
3072
+ }),
3073
+ result.needsRestart && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
3074
+ kind: "warn",
3075
+ children: t("report.needsRestart")
3076
+ }),
3077
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, { error })
3078
+ ]
3079
+ });
3080
+ }
3081
+ return null;
3082
+ }
3083
+ //#endregion
3084
+ //#region src/client/snapshots/SnapshotsPanel.tsx
3085
+ /**
3086
+ * 快照恢复面板(M4):列出快照 → 选择 → dry-run 计划预览 → 确认执行 → 诚实报告。
3087
+ *
3088
+ * 数据流:api.snapshots() 加载列表;选择快照后 api.restoreSnapshot(id, true) 拿
3089
+ * 恢复计划(零写入预览);确认后 api.restoreSnapshot(id, false) 执行并展示报告
3090
+ * (restored / removedPlugins / manualHints(人工项高亮)/ failed / skipped)。
3091
+ * 全部状态组件内自持:恢复是低频显式操作,不进入 sessionStorage(快照列表可随时重载)。
3092
+ */
3093
+ const initial$1 = {
3094
+ status: "loading",
3095
+ error: null,
3096
+ metas: [],
3097
+ selectedId: null,
3098
+ planning: false,
3099
+ plan: null,
3100
+ running: false,
3101
+ report: null,
3102
+ actionError: null
3103
+ };
3104
+ function statusLabel(t, status) {
3105
+ switch (status) {
3106
+ case "pending": return t("snapshots.status.pending");
3107
+ case "done": return t("snapshots.status.done");
3108
+ case "rolled-back": return t("snapshots.status.rolled-back");
3109
+ default: return t("snapshots.status.unknown");
3110
+ }
3111
+ }
3112
+ function statusBadgeKind(status) {
3113
+ switch (status) {
3114
+ case "pending": return "info";
3115
+ case "done": return "ok";
3116
+ case "rolled-back": return "warn";
3117
+ default: return "error";
3118
+ }
3119
+ }
3120
+ /** 计划动作的本地化描述前缀(kind 标签 + 引擎已生成的中文 description) */
3121
+ function actionKindLabel(kind) {
3122
+ switch (kind) {
3123
+ case "hostFileRestore": return "整文件还原";
3124
+ case "hostFileRemove": return "整文件删除";
3125
+ case "pluginRemove": return "卸载插件";
3126
+ case "fileRestore": return "还原文件";
3127
+ case "fileRemove": return "删除文件";
3128
+ case "credentialHint": return "人工提示";
3129
+ case "skip": return "跳过";
3130
+ default: return kind;
3131
+ }
3132
+ }
3133
+ function SnapshotsPanel({ api, t }) {
3134
+ const [state, setState] = (0, react.useState)(initial$1);
3135
+ const load = () => {
3136
+ setState((s) => ({
3137
+ ...s,
3138
+ status: "loading",
3139
+ error: null
3140
+ }));
3141
+ api.snapshots().then((metas) => {
3142
+ setState((s) => ({
3143
+ ...s,
3144
+ status: "ready",
3145
+ metas
3146
+ }));
3147
+ }, (err) => {
3148
+ setState((s) => ({
3149
+ ...s,
3150
+ status: "error",
3151
+ error: err instanceof Error ? err.message : String(err)
3152
+ }));
3153
+ });
3154
+ };
3155
+ (0, react.useEffect)(load, [api]);
3156
+ const select = (id) => {
3157
+ setState((s) => ({
3158
+ ...s,
3159
+ selectedId: id,
3160
+ plan: null,
3161
+ report: null,
3162
+ actionError: null,
3163
+ planning: true
3164
+ }));
3165
+ api.restoreSnapshot(id, true).then((res) => {
3166
+ setState((s) => ({
3167
+ ...s,
3168
+ planning: false,
3169
+ plan: res.plan ?? null
3170
+ }));
3171
+ }, (err) => {
3172
+ setState((s) => ({
3173
+ ...s,
3174
+ planning: false,
3175
+ actionError: err instanceof Error ? err.message : String(err)
3176
+ }));
3177
+ });
3178
+ };
3179
+ const execute = () => {
3180
+ if (state.selectedId === null || state.running) return;
3181
+ setState((s) => ({
3182
+ ...s,
3183
+ running: true,
3184
+ report: null,
3185
+ actionError: null
3186
+ }));
3187
+ api.restoreSnapshot(state.selectedId, false).then((res) => {
3188
+ setState((s) => ({
3189
+ ...s,
3190
+ running: false,
3191
+ report: res.report ?? null
3192
+ }));
3193
+ }, (err) => {
3194
+ setState((s) => ({
3195
+ ...s,
3196
+ running: false,
3197
+ actionError: err instanceof Error ? err.message : String(err)
3198
+ }));
3199
+ });
3200
+ };
3201
+ const summary = () => {
3202
+ const s = state.plan?.summary;
3203
+ if (s === void 0) return "";
3204
+ return `整文件还原 ${s.hostFileRestores} · 整文件删除 ${s.hostFileRemoves} · 插件卸载 ${s.pluginRemoves} · 文件还原 ${s.fileRestores} · 文件删除 ${s.fileRemoves} · 凭据提示 ${s.credentialHints} · 跳过 ${s.skips}`;
3205
+ };
3206
+ const reportLine = (title, items, warn) => {
3207
+ if (items.length === 0) return null;
3208
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
3209
+ className: config_manager_module_css_default.card,
3210
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("strong", {
3211
+ className: warn ? config_manager_module_css_default.warnText : void 0,
3212
+ children: [
3213
+ title,
3214
+ "(",
3215
+ items.length,
3216
+ ")"
3217
+ ]
3218
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
3219
+ className: config_manager_module_css_default.reportList,
3220
+ children: items.map((item, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: item }, `${title}-${i}`))
3221
+ })]
3222
+ });
3223
+ };
3224
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3225
+ className: config_manager_module_css_default.viewBody,
3226
+ children: [
3227
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionTitle, {
3228
+ title: t("snapshots.title"),
3229
+ subtitle: t("snapshots.hint")
3230
+ }),
3231
+ state.status === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: t("snapshots.loading") }),
3232
+ state.status === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Banner, {
3233
+ kind: "error",
3234
+ children: [state.error ?? t("common.unknownError"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
3235
+ variant: "primary",
3236
+ onClick: load,
3237
+ children: t("common.retry")
3238
+ })]
3239
+ }),
3240
+ state.status === "ready" && state.metas.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Empty, { children: t("snapshots.empty") }),
3241
+ state.status === "ready" && state.metas.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3242
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3243
+ className: config_manager_module_css_default.snapshotList,
3244
+ role: "listbox",
3245
+ "aria-label": t("snapshots.selectHint"),
3246
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3247
+ className: config_manager_module_css_default.snapshotRowHeader,
3248
+ children: [
3249
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("snapshots.createdAt") }),
3250
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("snapshots.sourceZip") }),
3251
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("snapshots.status") }),
3252
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("snapshots.entries") }),
3253
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("snapshots.plugins") })
3254
+ ]
3255
+ }), state.metas.map((meta) => {
3256
+ const selected = meta.id === state.selectedId;
3257
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3258
+ type: "button",
3259
+ role: "option",
3260
+ "aria-selected": selected,
3261
+ "data-active": selected ? "" : void 0,
3262
+ className: config_manager_module_css_default.snapshotRow,
3263
+ onClick: () => {
3264
+ select(meta.id);
3265
+ },
3266
+ children: [
3267
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3268
+ title: meta.id,
3269
+ children: new Date(meta.createdAt).toLocaleString()
3270
+ }),
3271
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3272
+ title: meta.sourceZip,
3273
+ children: meta.sourceZip
3274
+ }),
3275
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
3276
+ kind: statusBadgeKind(meta.status),
3277
+ children: statusLabel(t, meta.status)
3278
+ }) }),
3279
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: meta.entryCount }),
3280
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: meta.beforePluginCount })
3281
+ ]
3282
+ }, meta.id);
3283
+ })]
3284
+ }),
3285
+ state.selectedId !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [state.planning && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: t("common.loading") }), state.plan !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, { children: [
3286
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionTitle, {
3287
+ title: t("snapshots.planTitle"),
3288
+ subtitle: summary()
3289
+ }),
3290
+ state.plan.actions.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Empty, { children: t("snapshots.noActions") }),
3291
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
3292
+ className: config_manager_module_css_default.reportList,
3293
+ children: state.plan.actions.map((action, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
3294
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3295
+ className: config_manager_module_css_default.kindTag,
3296
+ children: actionKindLabel(action.kind)
3297
+ }),
3298
+ " ",
3299
+ action.description,
3300
+ action.detail !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3301
+ className: config_manager_module_css_default.hint,
3302
+ children: [
3303
+ "(",
3304
+ action.detail,
3305
+ ")"
3306
+ ]
3307
+ })
3308
+ ] }, `plan-${i}`))
3309
+ }),
3310
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3311
+ className: config_manager_module_css_default.rowActions,
3312
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
3313
+ variant: "danger",
3314
+ disabled: state.running || state.plan.actions.every((a) => a.kind === "skip"),
3315
+ onClick: execute,
3316
+ title: t("snapshots.confirmRestore"),
3317
+ children: state.running ? t("snapshots.executing") : t("snapshots.execute")
3318
+ })
3319
+ }),
3320
+ state.actionError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
3321
+ kind: "error",
3322
+ children: state.actionError
3323
+ })
3324
+ ] })] }),
3325
+ state.report !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3326
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionTitle, { title: t("snapshots.reportTitle") }),
3327
+ reportLine(t("snapshots.restored"), state.report.restored, false),
3328
+ reportLine(t("snapshots.removedPlugins"), state.report.removedPlugins, false),
3329
+ reportLine(t("snapshots.manualHints"), state.report.manualHints, true),
3330
+ reportLine(t("snapshots.failed"), state.report.failed.map((f) => `${f.item}: ${f.reason}`), true),
3331
+ reportLine(t("snapshots.skipped"), state.report.skipped, false)
3332
+ ] })
3333
+ ] })
2335
3334
  ]
2336
3335
  });
2337
- if (step === "result") {
2338
- const result = snapshot.result;
2339
- if (result === null) return null;
2340
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2341
- className: config_manager_module_css_default.viewBody,
2342
- children: [
2343
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionTitle, { title: t("report.import.title") }),
2344
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReportView, {
2345
- kind: "import",
2346
- importResult: result,
2347
- onAction: (action) => {
2348
- if (action === "done") resetWizard();
2349
- }
2350
- }),
2351
- result.needsRestart && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
2352
- kind: "warn",
2353
- children: t("report.needsRestart")
2354
- }),
2355
- error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, { error })
2356
- ]
2357
- });
2358
- }
2359
- return null;
2360
3336
  }
2361
3337
  //#endregion
2362
3338
  //#region src/client/ConfigManagerSection.tsx
2363
3339
  /**
2364
3340
  * Config Manager 设置页(settings.section 入口的主页面容器)。
2365
3341
  *
2366
- * owner props 由 settings shell 传入(close);业务面(api)由注册时的
2367
- * inject face 注入;t 由 locale seat 注入。内部两个主视图:
3342
+ * 业务面(api)由注册时的 inject face 注入;t 由 locale seat 注入。
3343
+ * 关闭按钮由 settings shell 自带,本页不再渲染。内部两个主视图:
2368
3344
  * Export(Quick/Custom)与 Import(九步向导)。
3345
+ *
3346
+ * m2:主视图 tab(view)与全部子视图状态统一由模块级 runStore 持有
3347
+ * (sessionStorage 持久化 + 切 tab/关面板不重建控制器实例);挂载时
3348
+ * 经 GET /runs + 轮询 /progress 恢复进行中的 run(刷新/重开面板后)。
2369
3349
  */
2370
3350
  /**
2371
- * 设置页容器:Export / Import 双视图切换。
3351
+ * 设置页容器:Export / Import / Snapshots 三视图切换。
3352
+ * Export/Import 状态在模块级 store(切 tab 不丢失);快照恢复面板为低频显式
3353
+ * 操作,其状态组件内自持(local state,不进 sessionStorage)。
2372
3354
  */
2373
- function ConfigManagerSection({ close, api, t }) {
2374
- const [view, setView] = (0, react.useState)("export");
3355
+ function ConfigManagerSection({ api, t }) {
3356
+ const view = (0, react.useSyncExternalStore)(runStore.subscribe, runStore.getSnapshot).view;
3357
+ const [snapshotsOpen, setSnapshotsOpen] = (0, react.useState)(false);
3358
+ (0, react.useEffect)(() => {
3359
+ runStore.resume(api);
3360
+ return () => {
3361
+ runStore.stopResume();
3362
+ };
3363
+ }, [api]);
3364
+ const setView = (next) => {
3365
+ setSnapshotsOpen(false);
3366
+ runStore.patch({ view: next });
3367
+ };
3368
+ const activeTab = snapshotsOpen ? "snapshots" : view;
2375
3369
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2376
3370
  className: config_manager_module_css_default.section,
2377
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3371
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2378
3372
  className: config_manager_module_css_default.sectionHeader,
2379
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3373
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2380
3374
  className: config_manager_module_css_default.viewTabs,
2381
3375
  role: "tablist",
2382
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2383
- type: "button",
2384
- role: "tab",
2385
- "aria-selected": view === "export",
2386
- "data-active": view === "export" ? "" : void 0,
2387
- className: config_manager_module_css_default.viewTab,
2388
- onClick: () => {
2389
- setView("export");
2390
- },
2391
- children: t("view.export")
2392
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2393
- type: "button",
2394
- role: "tab",
2395
- "aria-selected": view === "import",
2396
- "data-active": view === "import" ? "" : void 0,
2397
- className: config_manager_module_css_default.viewTab,
2398
- onClick: () => {
2399
- setView("import");
2400
- },
2401
- children: t("view.import")
2402
- })]
2403
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2404
- type: "button",
2405
- className: config_manager_module_css_default.iconButton,
2406
- title: t("common.close"),
2407
- "aria-label": t("common.close"),
2408
- onClick: close,
2409
- children: "×"
2410
- })]
3376
+ children: [
3377
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3378
+ type: "button",
3379
+ role: "tab",
3380
+ "aria-selected": activeTab === "export",
3381
+ "data-active": activeTab === "export" ? "" : void 0,
3382
+ className: config_manager_module_css_default.viewTab,
3383
+ onClick: () => {
3384
+ setView("export");
3385
+ },
3386
+ children: t("view.export")
3387
+ }),
3388
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3389
+ type: "button",
3390
+ role: "tab",
3391
+ "aria-selected": activeTab === "import",
3392
+ "data-active": activeTab === "import" ? "" : void 0,
3393
+ className: config_manager_module_css_default.viewTab,
3394
+ onClick: () => {
3395
+ setView("import");
3396
+ },
3397
+ children: t("view.import")
3398
+ }),
3399
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3400
+ type: "button",
3401
+ role: "tab",
3402
+ "aria-selected": activeTab === "snapshots",
3403
+ "data-active": snapshotsOpen ? "" : void 0,
3404
+ className: config_manager_module_css_default.viewTab,
3405
+ onClick: () => {
3406
+ setSnapshotsOpen(true);
3407
+ },
3408
+ children: t("view.snapshots")
3409
+ })
3410
+ ]
3411
+ })
2411
3412
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2412
3413
  className: config_manager_module_css_default.sectionBody,
2413
- children: view === "export" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExportView, {
3414
+ children: snapshotsOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SnapshotsPanel, {
3415
+ api,
3416
+ t
3417
+ }) : view === "export" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExportView, {
2414
3418
  api,
2415
3419
  t
2416
3420
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImportWizardView, {
@@ -2427,11 +3431,12 @@ function ConfigManagerSection({ close, api, t }) {
2427
3431
  * 字典 namespace 由 client/index.ts 注册(declare module 合并进 LocaleNamespaceMap)。
2428
3432
  * 键集合通过 `ConfigManagerKey` 类型在注册处做编译期校验(缺键/多键即编译错误)。
2429
3433
  */
2430
- const zh = {
3434
+ const zh$1 = {
2431
3435
  "section.label": "备份与迁移",
2432
3436
  "section.description": "导出 / 导入 / 迁移 DSH 配置(备份不含密钥,加密备份可选)",
2433
3437
  "view.export": "导出备份",
2434
3438
  "view.import": "导入恢复",
3439
+ "view.snapshots": "快照恢复",
2435
3440
  "common.close": "关闭",
2436
3441
  "common.cancel": "取消",
2437
3442
  "common.back": "上一步",
@@ -2499,6 +3504,31 @@ const zh = {
2499
3504
  "import.paths.old": "原路径",
2500
3505
  "import.paths.new": "新路径",
2501
3506
  "import.paths.unresolved": "还有 {count} 条路径未映射",
3507
+ "snapshots.title": "快照恢复",
3508
+ "snapshots.hint": "恢复到导入前状态:整文件还原 settings/patch、卸载导入期间新增插件、补偿被修改文件。执行前会把当前文件备份到快照的 pre-restore 目录。",
3509
+ "snapshots.empty": "暂无快照。执行导入时会自动创建安全快照。",
3510
+ "snapshots.loading": "加载快照中…",
3511
+ "snapshots.selectHint": "选择快照以预览恢复计划(dry-run,零写入):",
3512
+ "snapshots.createdAt": "创建时间",
3513
+ "snapshots.sourceZip": "来源备份",
3514
+ "snapshots.status": "状态",
3515
+ "snapshots.entries": "条目",
3516
+ "snapshots.plugins": "插件",
3517
+ "snapshots.status.pending": "进行中",
3518
+ "snapshots.status.done": "已完成",
3519
+ "snapshots.status.rolled-back": "已回滚",
3520
+ "snapshots.status.unknown": "未知",
3521
+ "snapshots.planTitle": "恢复计划预览",
3522
+ "snapshots.noActions": "该快照无可用恢复动作(或全部跳过)。",
3523
+ "snapshots.execute": "执行恢复",
3524
+ "snapshots.executing": "恢复执行中…",
3525
+ "snapshots.confirmRestore": "确认执行恢复?当前文件会先备份到快照的 pre-restore 目录,插件通过官方 dsh plugin remove 卸载。",
3526
+ "snapshots.reportTitle": "恢复报告",
3527
+ "snapshots.restored": "已还原",
3528
+ "snapshots.removedPlugins": "已卸载插件",
3529
+ "snapshots.manualHints": "需人工处理",
3530
+ "snapshots.failed": "失败",
3531
+ "snapshots.skipped": "跳过",
2502
3532
  "report.export.title": "导出完成",
2503
3533
  "report.import.title": "导入结果",
2504
3534
  "report.rollback.title": "回滚报告",
@@ -2509,11 +3539,12 @@ const zh = {
2509
3539
  "error.title": "操作失败",
2510
3540
  "error.hint": "错误消息已自动脱敏,不会泄露任何密钥信息。"
2511
3541
  };
2512
- const en = {
3542
+ const en$1 = {
2513
3543
  "section.label": "Backup & Migration",
2514
3544
  "section.description": "Export / import / migrate DSH configuration (backups exclude secrets; encrypted backups optional)",
2515
3545
  "view.export": "Export",
2516
3546
  "view.import": "Import",
3547
+ "view.snapshots": "Snapshots & Restore",
2517
3548
  "common.close": "Close",
2518
3549
  "common.cancel": "Cancel",
2519
3550
  "common.back": "Back",
@@ -2581,6 +3612,31 @@ const en = {
2581
3612
  "import.paths.old": "Original path",
2582
3613
  "import.paths.new": "New path",
2583
3614
  "import.paths.unresolved": "{count} path(s) unmapped",
3615
+ "snapshots.title": "Snapshot Restore",
3616
+ "snapshots.hint": "Restore to the pre-import state: whole-file restore of settings/patch, uninstall plugins added during import, compensate modified files. Current files are backed up to the snapshot's pre-restore folder before any change.",
3617
+ "snapshots.empty": "No snapshots yet. A safety snapshot is created automatically on import.",
3618
+ "snapshots.loading": "Loading snapshots…",
3619
+ "snapshots.selectHint": "Select a snapshot to preview its restore plan (dry-run, zero writes):",
3620
+ "snapshots.createdAt": "Created",
3621
+ "snapshots.sourceZip": "Source",
3622
+ "snapshots.status": "Status",
3623
+ "snapshots.entries": "Entries",
3624
+ "snapshots.plugins": "Plugins",
3625
+ "snapshots.status.pending": "Pending",
3626
+ "snapshots.status.done": "Done",
3627
+ "snapshots.status.rolled-back": "Rolled back",
3628
+ "snapshots.status.unknown": "Unknown",
3629
+ "snapshots.planTitle": "Restore Plan Preview",
3630
+ "snapshots.noActions": "No restorable actions for this snapshot (or all skipped).",
3631
+ "snapshots.execute": "Restore now",
3632
+ "snapshots.executing": "Restoring…",
3633
+ "snapshots.confirmRestore": "Confirm restore? Current files are backed up to the snapshot's pre-restore folder first; plugins are removed via the official dsh plugin remove.",
3634
+ "snapshots.reportTitle": "Restore Report",
3635
+ "snapshots.restored": "Restored",
3636
+ "snapshots.removedPlugins": "Removed plugins",
3637
+ "snapshots.manualHints": "Manual action needed",
3638
+ "snapshots.failed": "Failed",
3639
+ "snapshots.skipped": "Skipped",
2584
3640
  "report.export.title": "Export Complete",
2585
3641
  "report.import.title": "Import Result",
2586
3642
  "report.rollback.title": "Rollback Report",
@@ -2592,9 +3648,638 @@ const en = {
2592
3648
  "error.hint": "Error messages are auto-redacted and never leak secrets."
2593
3649
  };
2594
3650
  //#endregion
3651
+ //#region src/client/sync/sync-api.ts
3652
+ /** 同步端点常量(与 Host 半 src/index.ts API 常量保持一致) */
3653
+ const SYNC_API = {
3654
+ base: "/api/dsh-config-manager/sync",
3655
+ status: "/api/dsh-config-manager/sync/status",
3656
+ push: "/api/dsh-config-manager/sync/push",
3657
+ pull: "/api/dsh-config-manager/sync/pull"
3658
+ };
3659
+ /** DSH credentials 中的同步 token 引用名(Host 半同值;仅供提示文案使用,值由 Host 读写) */
3660
+ const SYNC_CREDENTIAL_REF = "DSH_CONFIG_MANAGER_SYNC_TOKEN";
3661
+ /** 同步请求超时(ms):与 Host 半 ROUTE_TIMEOUT_MS 对齐(git 网络操作可能较慢) */
3662
+ const SYNC_TIMEOUT_MS = 300 * 1e3;
3663
+ /** 解析 JSON 响应;非 2xx 时抛出带路由 error 消息的 ConfigManagerApiError(与 api.ts 同款) */
3664
+ async function readJson(response) {
3665
+ const notMountedMessage = "config-manager 服务未挂载(插件未加载):请确认 profile 中已安装 dsh-config-manager 并重启 DSH";
3666
+ let body;
3667
+ try {
3668
+ body = await response.json();
3669
+ } catch {
3670
+ if (response.status === 404) throw new ConfigManagerApiError(notMountedMessage);
3671
+ throw new ConfigManagerApiError(`HTTP ${response.status}: invalid JSON response`);
3672
+ }
3673
+ if (!response.ok) throw new ConfigManagerApiError(typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : response.status === 404 ? notMountedMessage : `HTTP ${response.status}`);
3674
+ return body;
3675
+ }
3676
+ /** POST JSON 请求(带超时:宿主卡死时 UI 拿到明确错误而不是永远转圈) */
3677
+ async function postJson(path, body) {
3678
+ const controller = new AbortController();
3679
+ const timer = setTimeout(() => controller.abort(), SYNC_TIMEOUT_MS);
3680
+ try {
3681
+ return await readJson(await fetch(path, {
3682
+ method: "POST",
3683
+ headers: { "content-type": "application/json" },
3684
+ body: JSON.stringify(body),
3685
+ signal: controller.signal
3686
+ }));
3687
+ } catch (err) {
3688
+ if (controller.signal.aborted) throw new ConfigManagerApiError(`同步请求超时(${Math.round(SYNC_TIMEOUT_MS / 6e4)} 分钟):请检查网络与仓库可达性后重试`);
3689
+ throw err;
3690
+ } finally {
3691
+ clearTimeout(timer);
3692
+ }
3693
+ }
3694
+ /** 远程同步浏览器半数据入口(settings.section: config-manager-sync 的注入业务面) */
3695
+ var SyncApi = class {
3696
+ /** 读取同步状态(配置 / 凭据 / 上次同步时间 / 分区数) */
3697
+ async status() {
3698
+ return readJson(await fetch(SYNC_API.status));
3699
+ }
3700
+ /** 推送:导出 portable 分区 → 提交到私有 Git 仓库 → 更新 sync-state */
3701
+ async push(payload) {
3702
+ return postJson(SYNC_API.push, payload);
3703
+ }
3704
+ /** 拉取差异预览:拉取远端最新快照 → 只读分析(绝不执行导入) */
3705
+ async pull(payload) {
3706
+ return postJson(SYNC_API.pull, payload);
3707
+ }
3708
+ };
3709
+ //#endregion
3710
+ //#region src/client/sync/sync-view.ts
3711
+ /**
3712
+ * 私有仓库强制提示文案(Settings 区块常驻警示横幅)。
3713
+ * 安全约束:同步内容为可移植配置,public 仓库会公开配置 → 必须私有;
3714
+ * token 仅用于认证,绝不写入同步文件/提交内容/日志。
3715
+ */
3716
+ const PRIVATE_REPO_HINT = "安全要求:同步仓库必须为私有仓库(public 仓库会公开你的配置内容)。认证 token 仅用于仓库访问,绝不写入同步文件、提交内容或日志。";
3717
+ /** 需要人工决策的 PlanItem 类型(与 SyncEngine.pull 的 needsReview 判定一致) */
3718
+ const REVIEW_KINDS = /* @__PURE__ */ new Set([
3719
+ "Conflict",
3720
+ "MissingSecret",
3721
+ "MissingDependency",
3722
+ "Install",
3723
+ "Error"
3724
+ ]);
3725
+ /** 差异摘要:按 severity 计数 + 需人工决策标记(UI 统计徽章与警示横幅的数据源) */
3726
+ function summarizePullChanges(changes) {
3727
+ let info = 0;
3728
+ let warning = 0;
3729
+ let error = 0;
3730
+ let needsReview = false;
3731
+ for (const c of changes) {
3732
+ if (c.severity === "error") error += 1;
3733
+ else if (c.severity === "warning") warning += 1;
3734
+ else info += 1;
3735
+ if (REVIEW_KINDS.has(c.kind)) needsReview = true;
3736
+ }
3737
+ return {
3738
+ total: changes.length,
3739
+ info,
3740
+ warning,
3741
+ error,
3742
+ needsReview,
3743
+ items: [...changes]
3744
+ };
3745
+ }
3746
+ /** PlanItemKind → 短标签(列表徽章) */
3747
+ function kindLabel(kind) {
3748
+ switch (kind) {
3749
+ case "Create": return "新增";
3750
+ case "Update": return "更新";
3751
+ case "Skip": return "跳过";
3752
+ case "Conflict": return "冲突";
3753
+ case "Install": return "安装";
3754
+ case "MissingSecret": return "缺密钥";
3755
+ case "MissingDependency": return "缺依赖";
3756
+ case "PathMapping": return "路径映射";
3757
+ case "Warning": return "警告";
3758
+ case "Error": return "错误";
3759
+ default: return kind;
3760
+ }
3761
+ }
3762
+ /** severity → 短标签 */
3763
+ function severityLabel(severity) {
3764
+ switch (severity) {
3765
+ case "error": return "错误";
3766
+ case "warning": return "警告";
3767
+ default: return "信息";
3768
+ }
3769
+ }
3770
+ /**
3771
+ * 按钮可用性与文案:
3772
+ * - 任一操作进行中(busy)→ 两个按钮都禁用(防并发 push/pull);
3773
+ * - 仓库地址为空 → 禁用(无仓库无从同步);
3774
+ * - busy 时按钮文案切换为「正在推送/拉取…」(配 Spinner)。
3775
+ */
3776
+ function computeSyncButtons(busy, repoUrl) {
3777
+ const idle = busy === null;
3778
+ const repoOk = repoUrl.trim() !== "";
3779
+ const enabled = idle && repoOk;
3780
+ return {
3781
+ canPush: enabled,
3782
+ canPull: enabled,
3783
+ pushLabel: busy === "push" ? "正在推送…" : "推送到远端",
3784
+ pullLabel: busy === "pull" ? "正在拉取…" : "拉取差异预览"
3785
+ };
3786
+ }
3787
+ /** 状态行渲染模型:加载 / 未配置 / 就绪(凭据 + 上次同步 + 通道)/ 错误 */
3788
+ function computeSyncStatus(statusInfo, loading, error) {
3789
+ if (loading) return {
3790
+ kind: "loading",
3791
+ text: "正在读取同步状态…"
3792
+ };
3793
+ if (error !== null) return {
3794
+ kind: "error",
3795
+ text: error
3796
+ };
3797
+ if (statusInfo === null || !statusInfo.configured) return {
3798
+ kind: "unconfigured",
3799
+ text: "尚未配置同步仓库(请填写仓库地址并推送一次以保存配置)"
3800
+ };
3801
+ return {
3802
+ kind: "ready",
3803
+ text: `${statusInfo.credentialConfigured ? "凭据已配置" : "未配置凭据(token 将在首次推送/拉取时写入 DSH credentials)"} · ${statusInfo.lastSyncAt !== void 0 && statusInfo.lastSyncAt !== "" ? `上次同步:${formatDateTime(statusInfo.lastSyncAt)}` : "尚未同步过"}${statusInfo.transport !== void 0 ? ` · ${statusInfo.transport.type}${statusInfo.transport.ref !== "" ? `/${statusInfo.transport.ref}` : ""}` : ""}`
3804
+ };
3805
+ }
3806
+ /** ISO-8601 → 本地可读时间(YYYY-MM-DD HH:mm;非法输入原样返回) */
3807
+ function formatDateTime(iso) {
3808
+ const d = new Date(iso);
3809
+ if (Number.isNaN(d.getTime())) return iso;
3810
+ const pad = (n) => String(n).padStart(2, "0");
3811
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
3812
+ }
3813
+ /** push 报告 → 渲染模型(ok 头部带快照 id;失败显示引擎 message;分区与告警透传) */
3814
+ function pushReportView(report) {
3815
+ if (report === null) return null;
3816
+ if (!report.ok) return {
3817
+ kind: "error",
3818
+ headline: report.message ?? "推送失败",
3819
+ sections: report.sections,
3820
+ warnings: report.warnings
3821
+ };
3822
+ return {
3823
+ kind: "ok",
3824
+ headline: `推送成功(快照 ${report.snapshotId})`,
3825
+ sections: report.sections,
3826
+ warnings: report.warnings
3827
+ };
3828
+ }
3829
+ /** pull 报告 → 渲染模型(差异预览;empty = 无变更;error = 拉取失败) */
3830
+ function pullReportView(report) {
3831
+ if (report === null) return null;
3832
+ if (!report.ok) return {
3833
+ kind: "error",
3834
+ headline: report.message ?? "拉取失败",
3835
+ summary: null,
3836
+ previewHint: ""
3837
+ };
3838
+ if (report.changes.length === 0) return {
3839
+ kind: "empty",
3840
+ headline: report.message ?? "远端快照与本地一致(无变更)",
3841
+ summary: null,
3842
+ previewHint: ""
3843
+ };
3844
+ return {
3845
+ kind: "ok",
3846
+ headline: `远端快照 ${report.snapshotId} 差异预览:共 ${report.changes.length} 项变更`,
3847
+ summary: summarizePullChanges(report.changes),
3848
+ previewHint: "以上为只读差异预览,不会执行导入。v1 暂不提供一键导入接线;如需应用远端配置,请使用「导入恢复」向导手动导入导出的备份。"
3849
+ };
3850
+ }
3851
+ //#endregion
3852
+ //#region src/client/sync/SyncSettingsView.tsx
3853
+ /**
3854
+ * 远程同步设置区块(settings.section: config-manager-sync)。
3855
+ *
3856
+ * 独立 settings 页(不触碰并行会话的 ConfigManagerSection.tsx 三 tab 容器):
3857
+ * - 仓库配置表单:repoUrl(必填)+ 认证 token(可选,写入 DSH credentials 的提示)
3858
+ * + gitBin(可选);
3859
+ * - 私有仓库强制提示横幅(常驻);
3860
+ * - 推送按钮 → SyncPushReport(快照 id / 分区 / 告警);
3861
+ * - 拉取按钮 → SyncPullReport.changes 差异摘要(description/kind/severity)+
3862
+ * 「预览不执行导入」提示;needsReview 高亮(v1 不做完整导入接线);
3863
+ * - 状态行:凭据配置 + 上次同步时间 + 通道(来自 GET /sync/status,组件挂载时加载)。
3864
+ *
3865
+ * 全部渲染模型来自 ./sync-view.ts 纯函数(node 单测覆盖),组件只做装配;
3866
+ * 状态组件内自持(低频显式操作,同 SnapshotsPanel 策略,不进 sessionStorage);
3867
+ * token 仅内存(state),成功后清空(已写入 DSH credentials),绝不持久化。
3868
+ */
3869
+ const initial = {
3870
+ loading: true,
3871
+ loadError: null,
3872
+ statusInfo: null,
3873
+ repoUrl: "",
3874
+ gitBin: "",
3875
+ token: "",
3876
+ busy: null,
3877
+ pushReport: null,
3878
+ pullReport: null,
3879
+ error: null
3880
+ };
3881
+ function SyncSettingsView({ close, api, t }) {
3882
+ const [state, setState] = (0, react.useState)(initial);
3883
+ const patch = (p) => setState((s) => ({
3884
+ ...s,
3885
+ ...p
3886
+ }));
3887
+ /** 挂载时读取同步状态(配置回填 + 上次同步时间 + 凭据状态) */
3888
+ const loadStatus = async () => {
3889
+ patch({
3890
+ loading: true,
3891
+ loadError: null
3892
+ });
3893
+ try {
3894
+ const info = await api.status();
3895
+ patch({
3896
+ loading: false,
3897
+ statusInfo: info,
3898
+ repoUrl: info.repoUrl ?? "",
3899
+ gitBin: info.gitBin ?? ""
3900
+ });
3901
+ } catch (err) {
3902
+ patch({
3903
+ loading: false,
3904
+ loadError: err instanceof Error ? err.message : String(err)
3905
+ });
3906
+ }
3907
+ };
3908
+ (0, react.useEffect)(() => {
3909
+ loadStatus();
3910
+ }, []);
3911
+ /** 表单快照 → 请求体(token 为空串不携带;gitBin 空串不携带) */
3912
+ const payload = () => ({
3913
+ repoUrl: state.repoUrl.trim(),
3914
+ gitBin: state.gitBin.trim() !== "" ? state.gitBin.trim() : void 0,
3915
+ token: state.token.trim() !== "" ? state.token : void 0
3916
+ });
3917
+ const runPush = async () => {
3918
+ patch({
3919
+ busy: "push",
3920
+ error: null,
3921
+ pushReport: null,
3922
+ pullReport: null
3923
+ });
3924
+ try {
3925
+ const report = await api.push(payload());
3926
+ patch({
3927
+ busy: null,
3928
+ pushReport: report,
3929
+ token: report.ok ? "" : state.token
3930
+ });
3931
+ } catch (err) {
3932
+ patch({
3933
+ busy: null,
3934
+ error: err instanceof Error ? err.message : String(err)
3935
+ });
3936
+ }
3937
+ };
3938
+ const runPull = async () => {
3939
+ patch({
3940
+ busy: "pull",
3941
+ error: null,
3942
+ pullReport: null,
3943
+ pushReport: null
3944
+ });
3945
+ try {
3946
+ const report = await api.pull(payload());
3947
+ patch({
3948
+ busy: null,
3949
+ pullReport: report,
3950
+ token: ""
3951
+ });
3952
+ } catch (err) {
3953
+ patch({
3954
+ busy: null,
3955
+ error: err instanceof Error ? err.message : String(err)
3956
+ });
3957
+ }
3958
+ };
3959
+ const status = computeSyncStatus(state.statusInfo, state.loading, state.loadError);
3960
+ const buttons = computeSyncButtons(state.busy, state.repoUrl);
3961
+ const pushView = pushReportView(state.pushReport);
3962
+ const pullView = pullReportView(state.pullReport);
3963
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3964
+ className: config_manager_module_css_default.section,
3965
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3966
+ className: config_manager_module_css_default.sectionHeader,
3967
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3968
+ className: config_manager_module_css_default.viewTabs,
3969
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3970
+ role: "tab",
3971
+ "aria-selected": "true",
3972
+ "data-active": true,
3973
+ className: config_manager_module_css_default.viewTab,
3974
+ children: t("section.label")
3975
+ })
3976
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3977
+ type: "button",
3978
+ className: config_manager_module_css_default.iconButton,
3979
+ title: t("common.close"),
3980
+ "aria-label": t("common.close"),
3981
+ onClick: close,
3982
+ children: "×"
3983
+ })]
3984
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3985
+ className: config_manager_module_css_default.sectionBody,
3986
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3987
+ className: config_manager_module_css_default.viewBody,
3988
+ children: [
3989
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionTitle, {
3990
+ title: t("section.label"),
3991
+ subtitle: t("section.description")
3992
+ }),
3993
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
3994
+ kind: "warn",
3995
+ children: PRIVATE_REPO_HINT
3996
+ }),
3997
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, { children: [
3998
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3999
+ className: config_manager_module_css_default.groupLabel,
4000
+ children: t("config.title")
4001
+ }),
4002
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4003
+ className: config_manager_module_css_default.field,
4004
+ children: [
4005
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4006
+ className: config_manager_module_css_default.fieldLabel,
4007
+ children: t("config.repoUrl")
4008
+ }),
4009
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4010
+ type: "text",
4011
+ className: config_manager_module_css_default.input,
4012
+ value: state.repoUrl,
4013
+ placeholder: "https://github.com/user/private-repo.git",
4014
+ disabled: state.busy !== null,
4015
+ onChange: (e) => {
4016
+ patch({ repoUrl: e.target.value });
4017
+ }
4018
+ }),
4019
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4020
+ className: config_manager_module_css_default.hint,
4021
+ children: t("config.repoUrlHint")
4022
+ })
4023
+ ]
4024
+ }),
4025
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4026
+ className: config_manager_module_css_default.field,
4027
+ children: [
4028
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4029
+ className: config_manager_module_css_default.fieldLabel,
4030
+ children: [
4031
+ t("config.token"),
4032
+ " ",
4033
+ state.statusInfo?.credentialConfigured === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
4034
+ kind: "ok",
4035
+ children: t("config.tokenSaved")
4036
+ })
4037
+ ]
4038
+ }),
4039
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4040
+ type: "password",
4041
+ className: config_manager_module_css_default.input,
4042
+ value: state.token,
4043
+ autoComplete: "off",
4044
+ placeholder: "ghp_…(可选)",
4045
+ disabled: state.busy !== null,
4046
+ onChange: (e) => {
4047
+ patch({ token: e.target.value });
4048
+ }
4049
+ }),
4050
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4051
+ className: config_manager_module_css_default.hint,
4052
+ children: t("config.tokenHint", { ref: SYNC_CREDENTIAL_REF })
4053
+ })
4054
+ ]
4055
+ }),
4056
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4057
+ className: config_manager_module_css_default.field,
4058
+ children: [
4059
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4060
+ className: config_manager_module_css_default.fieldLabel,
4061
+ children: t("config.gitBin")
4062
+ }),
4063
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4064
+ type: "text",
4065
+ className: config_manager_module_css_default.input,
4066
+ value: state.gitBin,
4067
+ placeholder: "git",
4068
+ disabled: state.busy !== null,
4069
+ onChange: (e) => {
4070
+ patch({ gitBin: e.target.value });
4071
+ }
4072
+ }),
4073
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4074
+ className: config_manager_module_css_default.hint,
4075
+ children: t("config.gitBinHint")
4076
+ })
4077
+ ]
4078
+ })
4079
+ ] }),
4080
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4081
+ className: config_manager_module_css_default.groupLabel,
4082
+ children: t("status.title")
4083
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4084
+ className: config_manager_module_css_default.statRow,
4085
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
4086
+ kind: status.kind === "ready" ? "ok" : status.kind === "error" ? "error" : "warn",
4087
+ children: status.text
4088
+ })
4089
+ })] }),
4090
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4091
+ className: config_manager_module_css_default.actionRow,
4092
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
4093
+ variant: "primary",
4094
+ disabled: !buttons.canPush,
4095
+ onClick: () => {
4096
+ runPush();
4097
+ },
4098
+ children: state.busy === "push" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: buttons.pushLabel }) : buttons.pushLabel
4099
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
4100
+ disabled: !buttons.canPull,
4101
+ onClick: () => {
4102
+ runPull();
4103
+ },
4104
+ children: state.busy === "pull" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: buttons.pullLabel }) : buttons.pullLabel
4105
+ })]
4106
+ }),
4107
+ state.error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, { error: state.error }),
4108
+ pushView !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, { children: [
4109
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4110
+ className: config_manager_module_css_default.groupLabel,
4111
+ children: t("push.title")
4112
+ }),
4113
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
4114
+ kind: pushView.kind === "ok" ? "ok" : "error",
4115
+ children: pushView.headline
4116
+ }),
4117
+ pushView.sections.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4118
+ className: config_manager_module_css_default.fieldLabel,
4119
+ children: t("sections.title")
4120
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4121
+ className: config_manager_module_css_default.statRow,
4122
+ children: pushView.sections.map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
4123
+ kind: "info",
4124
+ children: s
4125
+ }, s))
4126
+ })] }),
4127
+ pushView.warnings.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4128
+ className: config_manager_module_css_default.fieldLabel,
4129
+ children: t("warnings.title")
4130
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
4131
+ className: config_manager_module_css_default.warnList,
4132
+ children: pushView.warnings.map((w, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: w }, i))
4133
+ })] })
4134
+ ] }),
4135
+ pullView !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, { children: [
4136
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4137
+ className: config_manager_module_css_default.groupLabel,
4138
+ children: t("pull.title")
4139
+ }),
4140
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
4141
+ kind: pullView.kind === "ok" ? "info" : pullView.kind === "empty" ? "ok" : "error",
4142
+ children: pullView.headline
4143
+ }),
4144
+ pullView.summary !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4145
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4146
+ className: config_manager_module_css_default.statRow,
4147
+ children: [
4148
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
4149
+ kind: "info",
4150
+ children: t("change.total", { total: pullView.summary.total })
4151
+ }),
4152
+ pullView.summary.error > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Badge, {
4153
+ kind: "error",
4154
+ children: [
4155
+ severityLabel("error"),
4156
+ " × ",
4157
+ pullView.summary.error
4158
+ ]
4159
+ }),
4160
+ pullView.summary.warning > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Badge, {
4161
+ kind: "warn",
4162
+ children: [
4163
+ severityLabel("warning"),
4164
+ " × ",
4165
+ pullView.summary.warning
4166
+ ]
4167
+ }),
4168
+ pullView.summary.info > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Badge, {
4169
+ kind: "info",
4170
+ children: [
4171
+ severityLabel("info"),
4172
+ " × ",
4173
+ pullView.summary.info
4174
+ ]
4175
+ })
4176
+ ]
4177
+ }),
4178
+ pullView.summary.needsReview && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
4179
+ kind: "warn",
4180
+ children: t("pull.needsReview")
4181
+ }),
4182
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4183
+ className: config_manager_module_css_default.reportList,
4184
+ children: pullView.summary.items.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4185
+ className: config_manager_module_css_default.statRow,
4186
+ children: [
4187
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4188
+ className: config_manager_module_css_default.kindTag,
4189
+ children: kindLabel(c.kind)
4190
+ }),
4191
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
4192
+ kind: c.severity === "error" ? "error" : c.severity === "warning" ? "warn" : "info",
4193
+ children: severityLabel(c.severity)
4194
+ }),
4195
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: c.description })
4196
+ ]
4197
+ }, c.id))
4198
+ })
4199
+ ] }),
4200
+ pullView.previewHint !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
4201
+ kind: "info",
4202
+ children: pullView.previewHint
4203
+ })
4204
+ ] })
4205
+ ]
4206
+ })
4207
+ })]
4208
+ });
4209
+ }
4210
+ //#endregion
4211
+ //#region src/client/sync/sync-locales.ts
4212
+ /**
4213
+ * 远程同步设置区块(config-manager-sync)表面文案:zh 为源语言,en 镜像每个键。
4214
+ * 独立命名空间、独立文件:不触碰共享的 locales.ts(并行会话已改),零冲突。
4215
+ * 键集合经 `SyncKey` 类型在 client/index.ts 注册处做编译期校验。
4216
+ */
4217
+ const zh = {
4218
+ "section.label": "远程同步",
4219
+ "section.description": "通过 Git 私有仓库在设备间同步可移植配置(secret 永不参与同步)",
4220
+ "privateRepoHint": "安全要求:同步仓库必须为私有仓库(public 仓库会公开你的配置内容)。认证 token 仅用于仓库访问,绝不写入同步文件、提交内容或日志。",
4221
+ "config.title": "仓库配置",
4222
+ "config.repoUrl": "仓库地址",
4223
+ "config.repoUrlHint": "Git 私有仓库地址(https / ssh / 本地路径)。认证 token 请使用下方凭据字段,不要拼入地址。",
4224
+ "config.token": "认证 token",
4225
+ "config.tokenHint": "将安全写入 DSH credentials(引用名 {ref}),不会写入同步文件或日志。留空表示沿用已保存的凭据。",
4226
+ "config.tokenSaved": "凭据已配置",
4227
+ "config.gitBin": "git 可执行文件(可选)",
4228
+ "config.gitBinHint": "缺省使用 PATH 中的 git。",
4229
+ "status.title": "同步状态",
4230
+ "status.never": "从未同步",
4231
+ "action.push": "推送到远端",
4232
+ "action.pull": "拉取差异预览",
4233
+ "action.pushing": "正在推送…",
4234
+ "action.pulling": "正在拉取…",
4235
+ "push.title": "推送结果",
4236
+ "pull.title": "拉取差异预览",
4237
+ "pull.previewHint": "以上为只读差异预览,不会执行导入。v1 暂不提供一键导入接线;如需应用远端配置,请使用「导入恢复」向导手动导入导出的备份。",
4238
+ "pull.needsReview": "包含需要人工决策的项(冲突 / 密钥 / 依赖 / 安装)",
4239
+ "pull.empty": "远端快照与本地一致(无变更)",
4240
+ "change.total": "共 {total} 项变更",
4241
+ "sections.title": "同步分区",
4242
+ "warnings.title": "分区告警",
4243
+ "common.close": "关闭",
4244
+ "common.retry": "重试",
4245
+ "common.loading": "加载中…"
4246
+ };
4247
+ const en = {
4248
+ "section.label": "Remote Sync",
4249
+ "section.description": "Sync portable configuration across devices via a private Git repository (secrets never sync)",
4250
+ "privateRepoHint": "Security requirement: the sync repository MUST be private (a public repo would expose your configuration). The auth token is only used for repository access and is never written into sync files, commit content, or logs.",
4251
+ "config.title": "Repository",
4252
+ "config.repoUrl": "Repository URL",
4253
+ "config.repoUrlHint": "Private Git repository URL (https / ssh / local path). Use the credential field below for the auth token; never embed it in the URL.",
4254
+ "config.token": "Auth token",
4255
+ "config.tokenHint": "Securely written into DSH credentials (ref {ref}); never written into sync files or logs. Leave blank to reuse the saved credential.",
4256
+ "config.tokenSaved": "Credential configured",
4257
+ "config.gitBin": "git executable (optional)",
4258
+ "config.gitBinHint": "Defaults to git on PATH.",
4259
+ "status.title": "Sync Status",
4260
+ "status.never": "Never synced",
4261
+ "action.push": "Push to remote",
4262
+ "action.pull": "Pull diff preview",
4263
+ "action.pushing": "Pushing…",
4264
+ "action.pulling": "Pulling…",
4265
+ "push.title": "Push Result",
4266
+ "pull.title": "Pull Diff Preview",
4267
+ "pull.previewHint": "Read-only diff preview above; nothing is imported. v1 does not wire one-click import yet — use the Import wizard to apply a downloaded backup if needed.",
4268
+ "pull.needsReview": "Contains items that need human decisions (conflicts / secrets / dependencies / installs)",
4269
+ "pull.empty": "Remote snapshot matches local (no changes)",
4270
+ "change.total": "{total} change(s)",
4271
+ "sections.title": "Synced Sections",
4272
+ "warnings.title": "Section Warnings",
4273
+ "common.close": "Close",
4274
+ "common.retry": "Retry",
4275
+ "common.loading": "Loading…"
4276
+ };
4277
+ //#endregion
2595
4278
  //#region src/client/index.ts
2596
4279
  /** 本插件拥有的 locale namespace。 */
2597
4280
  const NS = "config-manager";
4281
+ /** 远程同步设置区块的独立 locale namespace(独立字典文件,不与共享 locales.ts 冲突)。 */
4282
+ const SYNC_NS = "config-manager-sync";
2598
4283
  /** 必需服务(fiber inject 等待 —— slots/locale 必须先就绪)。 */
2599
4284
  const inject = ["slots", "locale"];
2600
4285
  /**
@@ -2603,11 +4288,17 @@ const inject = ["slots", "locale"];
2603
4288
  */
2604
4289
  function apply(ctx) {
2605
4290
  ctx.effect(() => ctx.locale.register(NS, {
4291
+ zh: zh$1,
4292
+ en: en$1
4293
+ }), "config-manager: dictionaries");
4294
+ ctx.effect(() => ctx.locale.register(SYNC_NS, {
2606
4295
  zh,
2607
4296
  en
2608
- }), "config-manager: dictionaries");
4297
+ }), "config-manager: sync dictionaries");
2609
4298
  const t = ctx.locale.bind(NS);
2610
4299
  const api = new ConfigManagerApi();
4300
+ const syncT = ctx.locale.bind(SYNC_NS);
4301
+ const syncApi = new SyncApi();
2611
4302
  ctx.slots.inject("settings.section", () => ctx.slots.register({
2612
4303
  name: "settings.section",
2613
4304
  id: "config-manager",
@@ -2616,6 +4307,14 @@ function apply(ctx) {
2616
4307
  locale: NS,
2617
4308
  inject: () => ({ api })
2618
4309
  }, ConfigManagerSection));
4310
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
4311
+ name: "settings.section",
4312
+ id: "config-manager-sync",
4313
+ order: 70,
4314
+ label: () => syncT("section.label"),
4315
+ locale: SYNC_NS,
4316
+ inject: () => ({ api: syncApi })
4317
+ }, SyncSettingsView));
2619
4318
  }
2620
4319
  //#endregion
2621
4320
  exports.apply = apply;