dsh-config-manager 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/README.md +216 -0
  2. package/README.zh-CN.md +218 -0
  3. package/cordis.patch.yml +45 -0
  4. package/dsh.bundle.patch +21 -0
  5. package/dsh.client +39 -0
  6. package/lib/adapters/agent-presets.d.ts +13 -0
  7. package/lib/adapters/agent-presets.js +14 -0
  8. package/lib/adapters/agent-presets.js.map +1 -0
  9. package/lib/adapters/credentials.d.ts +36 -0
  10. package/lib/adapters/credentials.js +121 -0
  11. package/lib/adapters/credentials.js.map +1 -0
  12. package/lib/adapters/file-collection.d.ts +14 -0
  13. package/lib/adapters/file-collection.js +102 -0
  14. package/lib/adapters/file-collection.js.map +1 -0
  15. package/lib/adapters/index.d.ts +38 -0
  16. package/lib/adapters/index.js +46 -0
  17. package/lib/adapters/index.js.map +1 -0
  18. package/lib/adapters/mcp.d.ts +27 -0
  19. package/lib/adapters/mcp.js +175 -0
  20. package/lib/adapters/mcp.js.map +1 -0
  21. package/lib/adapters/plugin-files.d.ts +15 -0
  22. package/lib/adapters/plugin-files.js +91 -0
  23. package/lib/adapters/plugin-files.js.map +1 -0
  24. package/lib/adapters/plugins.d.ts +13 -0
  25. package/lib/adapters/plugins.js +177 -0
  26. package/lib/adapters/plugins.js.map +1 -0
  27. package/lib/adapters/prompts.d.ts +29 -0
  28. package/lib/adapters/prompts.js +228 -0
  29. package/lib/adapters/prompts.js.map +1 -0
  30. package/lib/adapters/providers.d.ts +33 -0
  31. package/lib/adapters/providers.js +143 -0
  32. package/lib/adapters/providers.js.map +1 -0
  33. package/lib/adapters/sessions.d.ts +14 -0
  34. package/lib/adapters/sessions.js +15 -0
  35. package/lib/adapters/sessions.js.map +1 -0
  36. package/lib/adapters/settings.d.ts +23 -0
  37. package/lib/adapters/settings.js +142 -0
  38. package/lib/adapters/settings.js.map +1 -0
  39. package/lib/adapters/skills.d.ts +13 -0
  40. package/lib/adapters/skills.js +14 -0
  41. package/lib/adapters/skills.js.map +1 -0
  42. package/lib/adapters/test-helpers.d.ts +99 -0
  43. package/lib/adapters/test-helpers.js +192 -0
  44. package/lib/adapters/test-helpers.js.map +1 -0
  45. package/lib/adapters/ui.d.ts +26 -0
  46. package/lib/adapters/ui.js +88 -0
  47. package/lib/adapters/ui.js.map +1 -0
  48. package/lib/adapters/workspaces.d.ts +12 -0
  49. package/lib/adapters/workspaces.js +92 -0
  50. package/lib/adapters/workspaces.js.map +1 -0
  51. package/lib/client.d.ts +377 -0
  52. package/lib/client.js +2598 -0
  53. package/lib/client.js.map +1 -0
  54. package/lib/core/analyzer.d.ts +35 -0
  55. package/lib/core/analyzer.js +563 -0
  56. package/lib/core/analyzer.js.map +1 -0
  57. package/lib/core/backup.d.ts +29 -0
  58. package/lib/core/backup.js +184 -0
  59. package/lib/core/backup.js.map +1 -0
  60. package/lib/core/exporter.d.ts +37 -0
  61. package/lib/core/exporter.js +222 -0
  62. package/lib/core/exporter.js.map +1 -0
  63. package/lib/core/importer.d.ts +42 -0
  64. package/lib/core/importer.js +25 -0
  65. package/lib/core/importer.js.map +1 -0
  66. package/lib/core/index.d.ts +12 -0
  67. package/lib/core/index.js +11 -0
  68. package/lib/core/index.js.map +1 -0
  69. package/lib/core/rollback.d.ts +9 -0
  70. package/lib/core/rollback.js +136 -0
  71. package/lib/core/rollback.js.map +1 -0
  72. package/lib/core/types.d.ts +331 -0
  73. package/lib/core/types.js +18 -0
  74. package/lib/core/types.js.map +1 -0
  75. package/lib/core/validator.d.ts +18 -0
  76. package/lib/core/validator.js +77 -0
  77. package/lib/core/validator.js.map +1 -0
  78. package/lib/index.d.ts +51 -0
  79. package/lib/index.js +875 -0
  80. package/lib/index.js.map +1 -0
  81. package/lib/migrations/index.d.ts +29 -0
  82. package/lib/migrations/index.js +56 -0
  83. package/lib/migrations/index.js.map +1 -0
  84. package/lib/migrations/v1-to-v2.d.ts +9 -0
  85. package/lib/migrations/v1-to-v2.js +10 -0
  86. package/lib/migrations/v1-to-v2.js.map +1 -0
  87. package/lib/profiles/index.d.ts +4 -0
  88. package/lib/profiles/index.js +5 -0
  89. package/lib/profiles/index.js.map +1 -0
  90. package/lib/profiles/profile-manager.d.ts +102 -0
  91. package/lib/profiles/profile-manager.js +522 -0
  92. package/lib/profiles/profile-manager.js.map +1 -0
  93. package/lib/schema/config.d.ts +28 -0
  94. package/lib/schema/config.js +136 -0
  95. package/lib/schema/config.js.map +1 -0
  96. package/lib/schema/manifest.d.ts +28 -0
  97. package/lib/schema/manifest.js +109 -0
  98. package/lib/schema/manifest.js.map +1 -0
  99. package/lib/schema/types.d.ts +169 -0
  100. package/lib/schema/types.js +8 -0
  101. package/lib/schema/types.js.map +1 -0
  102. package/lib/schema/versions.d.ts +27 -0
  103. package/lib/schema/versions.js +50 -0
  104. package/lib/schema/versions.js.map +1 -0
  105. package/lib/security/encryption.d.ts +41 -0
  106. package/lib/security/encryption.js +152 -0
  107. package/lib/security/encryption.js.map +1 -0
  108. package/lib/security/index.d.ts +16 -0
  109. package/lib/security/index.js +17 -0
  110. package/lib/security/index.js.map +1 -0
  111. package/lib/security/integrity.d.ts +30 -0
  112. package/lib/security/integrity.js +78 -0
  113. package/lib/security/integrity.js.map +1 -0
  114. package/lib/security/redaction.d.ts +26 -0
  115. package/lib/security/redaction.js +95 -0
  116. package/lib/security/redaction.js.map +1 -0
  117. package/lib/security/secret-scanner.d.ts +48 -0
  118. package/lib/security/secret-scanner.js +253 -0
  119. package/lib/security/secret-scanner.js.map +1 -0
  120. package/lib/security/zip-security.d.ts +27 -0
  121. package/lib/security/zip-security.js +154 -0
  122. package/lib/security/zip-security.js.map +1 -0
  123. package/lib/ui/conflict-view.d.ts +28 -0
  124. package/lib/ui/conflict-view.js +49 -0
  125. package/lib/ui/conflict-view.js.map +1 -0
  126. package/lib/ui/errors.d.ts +17 -0
  127. package/lib/ui/errors.js +76 -0
  128. package/lib/ui/errors.js.map +1 -0
  129. package/lib/ui/export-flow.d.ts +62 -0
  130. package/lib/ui/export-flow.js +79 -0
  131. package/lib/ui/export-flow.js.map +1 -0
  132. package/lib/ui/import-wizard.d.ts +67 -0
  133. package/lib/ui/import-wizard.js +169 -0
  134. package/lib/ui/import-wizard.js.map +1 -0
  135. package/lib/ui/path-mapping.d.ts +38 -0
  136. package/lib/ui/path-mapping.js +75 -0
  137. package/lib/ui/path-mapping.js.map +1 -0
  138. package/lib/ui/progress.d.ts +27 -0
  139. package/lib/ui/progress.js +76 -0
  140. package/lib/ui/progress.js.map +1 -0
  141. package/lib/ui/report.d.ts +21 -0
  142. package/lib/ui/report.js +146 -0
  143. package/lib/ui/report.js.map +1 -0
  144. package/lib/ui/test-helpers.d.ts +62 -0
  145. package/lib/ui/test-helpers.js +147 -0
  146. package/lib/ui/test-helpers.js.map +1 -0
  147. package/lib/ui/types.d.ts +129 -0
  148. package/lib/ui/types.js +21 -0
  149. package/lib/ui/types.js.map +1 -0
  150. package/lib/utils/hashing.d.ts +17 -0
  151. package/lib/utils/hashing.js +41 -0
  152. package/lib/utils/hashing.js.map +1 -0
  153. package/lib/utils/json.d.ts +24 -0
  154. package/lib/utils/json.js +76 -0
  155. package/lib/utils/json.js.map +1 -0
  156. package/lib/utils/logger.d.ts +39 -0
  157. package/lib/utils/logger.js +82 -0
  158. package/lib/utils/logger.js.map +1 -0
  159. package/lib/utils/paths.d.ts +31 -0
  160. package/lib/utils/paths.js +132 -0
  161. package/lib/utils/paths.js.map +1 -0
  162. package/lib/utils/zip.d.ts +54 -0
  163. package/lib/utils/zip.js +285 -0
  164. package/lib/utils/zip.js.map +1 -0
  165. package/package.json +109 -0
  166. package/src/adapters/agent-presets.ts +14 -0
  167. package/src/adapters/credentials.test.ts +90 -0
  168. package/src/adapters/credentials.ts +141 -0
  169. package/src/adapters/file-collection.ts +110 -0
  170. package/src/adapters/files.test.ts +113 -0
  171. package/src/adapters/index.ts +69 -0
  172. package/src/adapters/mcp.test.ts +96 -0
  173. package/src/adapters/mcp.ts +181 -0
  174. package/src/adapters/plugin-files.ts +96 -0
  175. package/src/adapters/plugins.test.ts +72 -0
  176. package/src/adapters/plugins.ts +176 -0
  177. package/src/adapters/prompts.test.ts +110 -0
  178. package/src/adapters/prompts.ts +226 -0
  179. package/src/adapters/providers.test.ts +125 -0
  180. package/src/adapters/providers.ts +161 -0
  181. package/src/adapters/roundtrip.test.ts +188 -0
  182. package/src/adapters/sessions.ts +15 -0
  183. package/src/adapters/settings.test.ts +93 -0
  184. package/src/adapters/settings.ts +165 -0
  185. package/src/adapters/skills.ts +14 -0
  186. package/src/adapters/test-helpers.ts +207 -0
  187. package/src/adapters/ui.test.ts +56 -0
  188. package/src/adapters/ui.ts +106 -0
  189. package/src/adapters/workspaces.test.ts +79 -0
  190. package/src/adapters/workspaces.ts +95 -0
  191. package/src/client/CLIENT_DEPENDENCIES.md +101 -0
  192. package/src/client/ConfigManagerSection.tsx +60 -0
  193. package/src/client/api.ts +270 -0
  194. package/src/client/client-types.ts +27 -0
  195. package/src/client/common/ErrorBanner.tsx +68 -0
  196. package/src/client/common/ProgressBar.tsx +49 -0
  197. package/src/client/common/ReportView.tsx +117 -0
  198. package/src/client/common/ui.tsx +165 -0
  199. package/src/client/config-manager.module.css +759 -0
  200. package/src/client/css-modules.d.ts +8 -0
  201. package/src/client/export/ExportView.tsx +222 -0
  202. package/src/client/import/ConflictList.tsx +72 -0
  203. package/src/client/import/ImportWizardView.tsx +406 -0
  204. package/src/client/import/PathMappingForm.tsx +77 -0
  205. package/src/client/index.ts +65 -0
  206. package/src/client/locales.ts +182 -0
  207. package/src/core/analyzer.ts +631 -0
  208. package/src/core/backup.ts +212 -0
  209. package/src/core/exporter.ts +246 -0
  210. package/src/core/importer.ts +66 -0
  211. package/src/core/index.ts +39 -0
  212. package/src/core/rollback.ts +140 -0
  213. package/src/core/smoke.test.ts +717 -0
  214. package/src/core/types.ts +343 -0
  215. package/src/core/validator.ts +84 -0
  216. package/src/index.ts +942 -0
  217. package/src/migrations/index.ts +71 -0
  218. package/src/migrations/v1-to-v2.ts +17 -0
  219. package/src/profiles/index.ts +8 -0
  220. package/src/profiles/profile-manager.test.ts +280 -0
  221. package/src/profiles/profile-manager.ts +606 -0
  222. package/src/schema/config.ts +137 -0
  223. package/src/schema/manifest.ts +124 -0
  224. package/src/schema/types.ts +179 -0
  225. package/src/schema/versions.ts +55 -0
  226. package/src/security/encryption.ts +180 -0
  227. package/src/security/index.ts +16 -0
  228. package/src/security/integrity.ts +102 -0
  229. package/src/security/redaction.ts +101 -0
  230. package/src/security/secret-scanner.ts +275 -0
  231. package/src/security/security.test.ts +796 -0
  232. package/src/security/zip-security.ts +180 -0
  233. package/src/ui/conflict-view.test.ts +62 -0
  234. package/src/ui/conflict-view.ts +65 -0
  235. package/src/ui/errors.test.ts +44 -0
  236. package/src/ui/errors.ts +95 -0
  237. package/src/ui/export-flow.test.ts +75 -0
  238. package/src/ui/export-flow.ts +128 -0
  239. package/src/ui/import-wizard.test.ts +128 -0
  240. package/src/ui/import-wizard.ts +208 -0
  241. package/src/ui/path-mapping.test.ts +67 -0
  242. package/src/ui/path-mapping.ts +103 -0
  243. package/src/ui/progress.test.ts +42 -0
  244. package/src/ui/progress.ts +90 -0
  245. package/src/ui/report.test.ts +93 -0
  246. package/src/ui/report.ts +140 -0
  247. package/src/ui/test-helpers.ts +177 -0
  248. package/src/ui/types.ts +193 -0
  249. package/src/utils/hashing.ts +53 -0
  250. package/src/utils/json.ts +77 -0
  251. package/src/utils/logger.ts +114 -0
  252. package/src/utils/paths.ts +122 -0
  253. package/src/utils/zip.ts +332 -0
package/lib/client.js ADDED
@@ -0,0 +1,2598 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-config-manager",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+
7
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
8
+ let react = require("react");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ //#region src/client/api.ts
11
+ /** 路由族常量(集中管理,与 Host 半的路由前缀保持一致) */
12
+ const CONFIG_MANAGER_API = {
13
+ base: "/api/dsh-config-manager",
14
+ status: "/api/dsh-config-manager/status",
15
+ export: "/api/dsh-config-manager/export",
16
+ download: "/api/dsh-config-manager/download",
17
+ upload: "/api/dsh-config-manager/upload",
18
+ analyze: "/api/dsh-config-manager/analyze",
19
+ plan: "/api/dsh-config-manager/plan",
20
+ execute: "/api/dsh-config-manager/execute"
21
+ };
22
+ /** 携带路由 JSON error 消息的错误类型 */
23
+ var ConfigManagerApiError = class extends Error {
24
+ constructor(message) {
25
+ super(message);
26
+ this.name = "ConfigManagerApiError";
27
+ }
28
+ };
29
+ /** 解析 JSON 响应;非 2xx 时抛出带路由 error 消息的 ConfigManagerApiError */
30
+ async function readJson(response) {
31
+ let body;
32
+ try {
33
+ body = await response.json();
34
+ } catch {
35
+ throw new ConfigManagerApiError(`HTTP ${response.status}: invalid JSON response`);
36
+ }
37
+ if (!response.ok) throw new ConfigManagerApiError(typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : `HTTP ${response.status}`);
38
+ return body;
39
+ }
40
+ /** query-string 辅助(跳过 undefined/空串,与 dsh-ssh 一致) */
41
+ function query(params) {
42
+ const search = new URLSearchParams();
43
+ for (const [key, value] of Object.entries(params)) if (value !== void 0 && value !== "") search.set(key, String(value));
44
+ const text = search.toString();
45
+ return text === "" ? "" : "?" + text;
46
+ }
47
+ /**
48
+ * Config Manager 浏览器半的唯一数据入口。
49
+ * 同时实现 `ExportPort`(export-flow 用)与 `ImportPort`(import-wizard 用)。
50
+ */
51
+ var ConfigManagerApi = class {
52
+ /**
53
+ * 加密备份密码(仅内存,默认 null = 普通备份)。
54
+ * ExportView 在 run 前设置;export 请求体携带给 Host 半做 AES-256-GCM 加密。
55
+ * 绝不写入 manifest / 任何 DSH 配置 / localStorage。
56
+ */
57
+ exportPassword = null;
58
+ /** 健康/版本检查(主页横幅:插件版本 / DSH 版本 / 平台) */
59
+ async status() {
60
+ return readJson(await fetch(CONFIG_MANAGER_API.status));
61
+ }
62
+ /** ExportPort.export:调用 Host 侧导出编排(core Exporter)。加密密码随请求体传输(仅内存)。 */
63
+ async export(options) {
64
+ const body = {
65
+ ...options,
66
+ password: this.exportPassword ?? void 0
67
+ };
68
+ return readJson(await fetch(CONFIG_MANAGER_API.export, {
69
+ method: "POST",
70
+ headers: { "content-type": "application/json" },
71
+ body: JSON.stringify(body)
72
+ }));
73
+ }
74
+ /**
75
+ * 把导出的 ZIP 下载到本机。优先 File System Access API 流式落盘
76
+ * (不占整文件内存),不可用时回退内存 Blob(dsh-ssh downloadFile 同款策略)。
77
+ */
78
+ async download(zipPath, onProgress) {
79
+ const response = await fetch(CONFIG_MANAGER_API.download + query({ path: zipPath }));
80
+ if (!response.ok || response.body === null) {
81
+ const text = await response.text().catch(() => "");
82
+ throw new ConfigManagerApiError(text !== "" ? text : `download failed: HTTP ${response.status}`);
83
+ }
84
+ const total = Number(response.headers.get("content-length") ?? "0");
85
+ const disposition = response.headers.get("content-disposition") ?? "";
86
+ const filename = /filename="([^"]+)"/.exec(disposition)?.[1] ?? zipPath.split(/[\\/]/).pop() ?? "dsh-config.zip";
87
+ const reader = response.body.getReader();
88
+ const picker = typeof window !== "undefined" ? window.showSaveFilePicker : void 0;
89
+ let streamed = false;
90
+ let writable;
91
+ const chunks = [];
92
+ let received = 0;
93
+ try {
94
+ if (picker !== void 0) {
95
+ writable = await (await picker.call(window, { suggestedName: filename })).createWritable();
96
+ streamed = true;
97
+ }
98
+ } catch {}
99
+ for (;;) {
100
+ const { done, value } = await reader.read();
101
+ if (done) break;
102
+ if (writable !== void 0) await writable.write(value);
103
+ else chunks.push(value);
104
+ received += value.length;
105
+ onProgress?.(received, total);
106
+ }
107
+ if (writable !== void 0) await writable.close();
108
+ return {
109
+ blob: streamed ? void 0 : new Blob(chunks),
110
+ filename,
111
+ streamed,
112
+ bytes: received
113
+ };
114
+ }
115
+ /** 上传用户选择的 ZIP 到 Host 受控临时目录,返回可引用的 zipPath(dsh-ssh 同款原始字节上传) */
116
+ async upload(file) {
117
+ return readJson(await fetch(CONFIG_MANAGER_API.upload + query({ name: file.name }), {
118
+ method: "POST",
119
+ body: file
120
+ }));
121
+ }
122
+ /** ImportPort.analyzeImport:零写入分析(校验/兼容性/差异/路径/秘密检测) */
123
+ async analyzeImport(zipPath) {
124
+ return readJson(await fetch(CONFIG_MANAGER_API.analyze, {
125
+ method: "POST",
126
+ headers: { "content-type": "application/json" },
127
+ body: JSON.stringify({ zipPath })
128
+ }));
129
+ }
130
+ /** ImportPort.createImportPlan:用用户决策(冲突/路径映射/策略)生成最终计划(Dry Run 零写入) */
131
+ async createImportPlan(zipPath, decisions) {
132
+ return readJson(await fetch(CONFIG_MANAGER_API.plan, {
133
+ method: "POST",
134
+ headers: { "content-type": "application/json" },
135
+ body: JSON.stringify({
136
+ zipPath,
137
+ decisions
138
+ })
139
+ }));
140
+ }
141
+ /** ImportPort.executeImportPlan:快照→分阶段 apply→validate→commit/rollback */
142
+ async executeImportPlan(zipPath, plan, opts) {
143
+ const payload = {
144
+ zipPath,
145
+ plan,
146
+ opts: {
147
+ confirm: opts.confirm,
148
+ secretInputs: opts.secretInputs ?? {},
149
+ rollbackOnError: opts.rollbackOnError
150
+ }
151
+ };
152
+ return readJson(await fetch(CONFIG_MANAGER_API.execute, {
153
+ method: "POST",
154
+ headers: { "content-type": "application/json" },
155
+ body: JSON.stringify(payload)
156
+ }));
157
+ }
158
+ };
159
+ //#endregion
160
+ //#region src/ui/types.ts
161
+ /** Custom Export 分组目录(规范 §1;automation 组 DSH 无对应分区,仅说明) */
162
+ const EXPORT_GROUPS = [
163
+ {
164
+ id: "general",
165
+ label: "General"
166
+ },
167
+ {
168
+ id: "ai",
169
+ label: "AI"
170
+ },
171
+ {
172
+ id: "extensions",
173
+ label: "Extensions"
174
+ },
175
+ {
176
+ id: "mcp",
177
+ label: "MCP / Tools"
178
+ },
179
+ {
180
+ id: "customization",
181
+ label: "Customization"
182
+ },
183
+ {
184
+ id: "automation",
185
+ label: "Automation",
186
+ note: "DSH 当前无 Workflows / Commands 配置文件(运行时注册),无迁移内容"
187
+ },
188
+ {
189
+ id: "workspace",
190
+ label: "Workspace"
191
+ },
192
+ {
193
+ id: "ui",
194
+ label: "UI"
195
+ },
196
+ {
197
+ id: "optional",
198
+ label: "Optional Data"
199
+ }
200
+ ];
201
+ /** Quick Export 推荐项 = defaultIncluded 且非 deviceSpecific(设计 §11.1) */
202
+ function isQuickRecommended(c) {
203
+ return c.defaultIncluded && c.portability !== "deviceSpecific";
204
+ }
205
+ //#endregion
206
+ //#region src/ui/progress.ts
207
+ /** 导出阶段文案(规范 §29 Export 示例) */
208
+ const EXPORT_STAGES = [
209
+ "analyzing",
210
+ "exporting-settings",
211
+ "scanning-secrets",
212
+ "exporting-plugins",
213
+ "creating-archive",
214
+ "calculating-checksums",
215
+ "done"
216
+ ];
217
+ /** 导入阶段文案(规范 §29 Import 示例 + 快照/回滚阶段) */
218
+ const IMPORT_STAGES = [
219
+ "validating",
220
+ "checking-compatibility",
221
+ "creating-snapshot",
222
+ "restoring-settings",
223
+ "restoring-plugins",
224
+ "restoring-mcp",
225
+ "validating-config",
226
+ "rolling-back",
227
+ "done"
228
+ ];
229
+ const STAGE_TEXTS = {
230
+ "analyzing": "Analyzing configuration...",
231
+ "exporting-settings": "Exporting settings...",
232
+ "scanning-secrets": "Scanning secrets...",
233
+ "exporting-plugins": "Exporting plugins...",
234
+ "creating-archive": "Creating archive...",
235
+ "calculating-checksums": "Calculating checksums...",
236
+ "validating": "Validating backup...",
237
+ "checking-compatibility": "Checking compatibility...",
238
+ "creating-snapshot": "Creating safety snapshot...",
239
+ "restoring-settings": "Restoring settings...",
240
+ "restoring-plugins": "Restoring plugins...",
241
+ "restoring-mcp": "Restoring MCP...",
242
+ "validating-config": "Validating configuration...",
243
+ "rolling-back": "Rolling back...",
244
+ "done": "Done"
245
+ };
246
+ /** 阶段 id → 用户可读文案(未知阶段回退 id) */
247
+ function stageText(stage) {
248
+ return STAGE_TEXTS[stage] ?? stage;
249
+ }
250
+ /**
251
+ * 进度追踪器:按给定阶段序列在回调时附带 step/total 序号。
252
+ * 控制器调用 core 前后 emit(),UI 侧渲染进度条/阶段文字。
253
+ */
254
+ var ProgressTracker = class {
255
+ listener;
256
+ stages;
257
+ emitted = [];
258
+ constructor(stages, listener) {
259
+ this.stages = stages;
260
+ this.listener = listener;
261
+ }
262
+ /** 发出某阶段事件(可携带 detail);未在序列中的阶段 step/total 缺省 */
263
+ emit(stage, detail) {
264
+ const idx = this.stages.indexOf(stage);
265
+ const event = {
266
+ stage,
267
+ detail,
268
+ step: idx >= 0 ? idx + 1 : void 0,
269
+ total: idx >= 0 ? this.stages.length : void 0
270
+ };
271
+ this.emitted.push(stage);
272
+ this.listener?.(event);
273
+ }
274
+ /** 已发出阶段的快照(测试与日志用) */
275
+ get events() {
276
+ return [...this.emitted];
277
+ }
278
+ };
279
+ //#endregion
280
+ //#region src/ui/report.ts
281
+ function renderExportReport(report) {
282
+ const lines = ["Backup Created", ""];
283
+ lines.push("Included:");
284
+ for (const { section, counts } of report.included) {
285
+ const detail = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(", ");
286
+ lines.push(` ✓ ${section}${detail !== "" ? ` (${detail})` : ""}`);
287
+ }
288
+ lines.push("");
289
+ if (report.excluded.length > 0) {
290
+ lines.push("Excluded:");
291
+ for (const s of report.excluded) lines.push(` ○ ${s}`);
292
+ lines.push("");
293
+ }
294
+ lines.push("Security:");
295
+ lines.push(` ✓ API Keys excluded: ${report.security.secretsExcluded ? "yes" : "no"}`);
296
+ lines.push(` ✓ Contains secrets: ${report.security.containsSecrets ? "yes (encrypted)" : "no"}`);
297
+ lines.push(` ✓ Encrypted: ${report.security.encrypted ? "yes" : "no"}`);
298
+ if (report.security.redactedHits > 0) lines.push(` ⚠ ${report.security.redactedHits} sensitive field(s) redacted`);
299
+ lines.push("");
300
+ lines.push(`File: ${report.file.name} (${formatBytes(report.file.sizeBytes)})`);
301
+ for (const w of report.warnings) lines.push(` ⚠ ${w}`);
302
+ return lines.join("\n");
303
+ }
304
+ /** 从 itemId 前缀推断所属分区(与 adapters id 规则对齐;未知归 'other') */
305
+ function sectionFromItemId(itemId) {
306
+ if (itemId.startsWith("settings:")) return "settings";
307
+ if (itemId.startsWith("ui:")) return "ui";
308
+ if (itemId.startsWith("provider:")) return "providers";
309
+ if (itemId.startsWith("plugin:") || itemId.startsWith("patch:")) return "plugins";
310
+ if (itemId.startsWith("mcp:")) return "mcp";
311
+ if (itemId.startsWith("prompt:")) return "prompts";
312
+ if (itemId.startsWith("workspace:")) return "workspaces";
313
+ if (itemId.startsWith("secret:")) return "credentialsStatus";
314
+ if (itemId.startsWith("skills:")) return "skills";
315
+ if (itemId.startsWith("agentPresets:")) return "agentPresets";
316
+ if (itemId.startsWith("pluginFiles:")) return "pluginFiles";
317
+ if (itemId.startsWith("sessions:")) return "sessions";
318
+ return "other";
319
+ }
320
+ /** 按分区聚合执行结果(统计 + 明细) */
321
+ function importSectionStats(executed) {
322
+ const bySection = /* @__PURE__ */ new Map();
323
+ for (const e of executed) {
324
+ const section = sectionFromItemId(e.itemId);
325
+ let stat = bySection.get(section);
326
+ if (!stat) {
327
+ stat = {
328
+ section,
329
+ ok: 0,
330
+ skipped: 0,
331
+ failed: 0,
332
+ items: []
333
+ };
334
+ bySection.set(section, stat);
335
+ }
336
+ if (e.status === "ok") stat.ok += 1;
337
+ else if (e.status === "skipped") stat.skipped += 1;
338
+ else stat.failed += 1;
339
+ stat.items.push(e);
340
+ }
341
+ return [...bySection.values()];
342
+ }
343
+ /** 导入报告渲染(含回滚状态;§22 动作按钮由 suggestedActions 给出) */
344
+ function renderImportReport(result) {
345
+ const lines = [result.ok ? "Import Complete" : "Import Failed", ""];
346
+ const stats = importSectionStats(result.executed);
347
+ for (const s of stats) {
348
+ const parts = [];
349
+ if (s.ok > 0) parts.push(`✓ ${s.ok} imported/restored`);
350
+ if (s.skipped > 0) parts.push(`- ${s.skipped} skipped`);
351
+ if (s.failed > 0) parts.push(`✗ ${s.failed} failed`);
352
+ lines.push(`${s.section}: ${parts.join(" ") || "no changes"}`);
353
+ for (const it of s.items) if (it.status === "failed") lines.push(` Reason: ${it.message ?? "未知原因"}`);
354
+ }
355
+ if (result.missingSecrets.length > 0) lines.push(`Secrets: ⚠ ${result.missingSecrets.length} credentials need to be entered`);
356
+ if (result.needsRestart) lines.push("Restart required: 插件/MCP 变更将在重启 DSH 后生效");
357
+ for (const w of result.warnings) lines.push(`⚠ ${w}`);
358
+ if (result.rollback) {
359
+ lines.push("");
360
+ lines.push(renderRollbackReport(result.rollback));
361
+ }
362
+ return lines.join("\n");
363
+ }
364
+ /** 结果页动作按钮(§22:Fix Issues / View Details / Done) */
365
+ function suggestedActions(result) {
366
+ const actions = [];
367
+ if (result.executed.some((e) => e.status === "failed")) actions.push("fixIssues");
368
+ if (result.executed.length > 0 || result.warnings.length > 0) actions.push("viewDetails");
369
+ actions.push("done");
370
+ return actions;
371
+ }
372
+ /** 回滚报告渲染(full / partial + 人工恢复清单) */
373
+ function renderRollbackReport(rr) {
374
+ if (rr.full) return "Rollback: 已完整恢复导入前的配置。";
375
+ const lines = ["Rollback partially completed."];
376
+ lines.push("These items may require manual recovery:");
377
+ for (const f of rr.failed) lines.push(` - ${f.item}: ${f.reason}${f.manualHint ? ` (${f.manualHint})` : ""}`);
378
+ if (rr.restored.length > 0) lines.push(`Restored: ${rr.restored.length} item(s)`);
379
+ return lines.join("\n");
380
+ }
381
+ function formatBytes(n) {
382
+ if (n < 1024) return `${n} B`;
383
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
384
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
385
+ }
386
+ //#endregion
387
+ //#region src/ui/export-flow.ts
388
+ /** 内置分类目录(与 src/adapters/* 的 displayName/defaultIncluded/portability 对齐,研究报告 §2.2) */
389
+ const DEFAULT_CATEGORIES = [
390
+ {
391
+ id: "settings",
392
+ label: "Settings",
393
+ description: "DSH 全局设置(namespace 分区,redacted)",
394
+ defaultIncluded: true,
395
+ portability: "portable",
396
+ group: "general"
397
+ },
398
+ {
399
+ id: "providers",
400
+ label: "Providers & Models",
401
+ description: "LLM Provider / Model / 默认模型 / BaseURL",
402
+ defaultIncluded: true,
403
+ portability: "portable",
404
+ group: "ai"
405
+ },
406
+ {
407
+ id: "plugins",
408
+ label: "Plugins",
409
+ description: "已安装插件清单与启用状态(不含二进制)",
410
+ defaultIncluded: true,
411
+ portability: "portable",
412
+ group: "extensions"
413
+ },
414
+ {
415
+ id: "pluginFiles",
416
+ label: "Plugin Files",
417
+ description: "插件自有配置文件(dsh-ssh.json 等,白名单)",
418
+ defaultIncluded: false,
419
+ portability: "deviceSpecific",
420
+ group: "extensions"
421
+ },
422
+ {
423
+ id: "mcp",
424
+ label: "MCP Servers",
425
+ description: "MCP 服务器组合配置(需重启生效)",
426
+ defaultIncluded: true,
427
+ portability: "platformSpecific",
428
+ group: "mcp"
429
+ },
430
+ {
431
+ id: "prompts",
432
+ label: "Prompts",
433
+ description: "System Prompt / Plan Mode 提示",
434
+ defaultIncluded: true,
435
+ portability: "portable",
436
+ group: "customization"
437
+ },
438
+ {
439
+ id: "skills",
440
+ label: "Skills",
441
+ description: "用户技能文件(~/.dsh/skills)",
442
+ defaultIncluded: true,
443
+ portability: "portable",
444
+ group: "customization"
445
+ },
446
+ {
447
+ id: "agentPresets",
448
+ label: "Agent Presets",
449
+ description: "Agent 预设(~/.dsh/.agent-presets)",
450
+ defaultIncluded: true,
451
+ portability: "portable",
452
+ group: "customization"
453
+ },
454
+ {
455
+ id: "workspaces",
456
+ label: "Workspaces",
457
+ description: "工作区记录(含绝对路径,需路径映射)",
458
+ defaultIncluded: true,
459
+ portability: "platformSpecific",
460
+ group: "workspace"
461
+ },
462
+ {
463
+ id: "ui",
464
+ label: "UI Preferences",
465
+ description: "UI 类 settings namespace(localStorage 项仅说明)",
466
+ defaultIncluded: true,
467
+ portability: "portable",
468
+ group: "ui"
469
+ },
470
+ {
471
+ id: "credentialsStatus",
472
+ label: "Credentials Status",
473
+ description: "凭据状态(configured 标记,永不导出值)",
474
+ defaultIncluded: true,
475
+ portability: "deviceSpecific",
476
+ group: "optional",
477
+ sensitive: true
478
+ },
479
+ {
480
+ id: "sessions",
481
+ label: "Sessions",
482
+ description: "历史会话(默认关闭,含敏感内容)",
483
+ defaultIncluded: false,
484
+ portability: "deviceSpecific",
485
+ group: "optional"
486
+ }
487
+ ];
488
+ var ExportFlow = class {
489
+ categories;
490
+ port;
491
+ onProgress;
492
+ constructor(opts) {
493
+ this.port = opts.port;
494
+ this.categories = opts.categories ?? DEFAULT_CATEGORIES;
495
+ this.onProgress = opts.onProgress;
496
+ }
497
+ /** Quick Export 推荐分区(defaultIncluded 且非 deviceSpecific) */
498
+ quickSelection() {
499
+ return this.categories.filter(isQuickRecommended).map((c) => c.id);
500
+ }
501
+ /** 按 §1 分组返回分类目录(Custom Export 树) */
502
+ groupedCatalog() {
503
+ return EXPORT_GROUPS.map((g) => ({
504
+ group: g.id,
505
+ label: g.label,
506
+ note: g.note,
507
+ categories: this.categories.filter((c) => c.group === g.id)
508
+ }));
509
+ }
510
+ /** 校验 Custom 勾选:未知分区 = invalid;deviceSpecific 分区给提示警告(仍可继续) */
511
+ validateSelection(selection) {
512
+ const warnings = [];
513
+ let valid = true;
514
+ const known = new Set(this.categories.map((c) => c.id));
515
+ for (const id of selection) if (!known.has(id)) {
516
+ warnings.push(`未知分区:${id}`);
517
+ valid = false;
518
+ }
519
+ for (const c of this.categories) if (selection.includes(c.id) && c.portability === "deviceSpecific") warnings.push(`${c.label} 为设备相关数据(${c.portability}),跨设备导入时可能不适用`);
520
+ return {
521
+ valid,
522
+ warnings
523
+ };
524
+ }
525
+ /** 执行导出:发进度事件 → 调 core → 渲染 §21 报告 */
526
+ async run(mode, selection, opts = {}) {
527
+ const tracker = new ProgressTracker(EXPORT_STAGES, this.onProgress);
528
+ const only = mode === "quick" ? this.quickSelection() : [...selection];
529
+ if (mode === "quick" && opts.includeSecrets === void 0) opts = {
530
+ ...opts,
531
+ includeSecrets: false
532
+ };
533
+ tracker.emit("analyzing");
534
+ tracker.emit("exporting-settings");
535
+ tracker.emit("scanning-secrets");
536
+ tracker.emit("exporting-plugins");
537
+ tracker.emit("creating-archive");
538
+ tracker.emit("calculating-checksums");
539
+ const result = await this.port.export({
540
+ includeSecrets: opts.includeSecrets ?? false,
541
+ only
542
+ });
543
+ tracker.emit("done");
544
+ return {
545
+ ...result,
546
+ text: renderExportReport(result.report)
547
+ };
548
+ }
549
+ };
550
+ //#endregion
551
+ //#region \0config-manager-css:C:\Repository\DSH Config Manager\src\client\config-manager.module.css.js
552
+ const css = ".fjdJpq_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}.fjdJpq_sectionHeader{flex:none;align-items:center;gap:10px;display:flex}.fjdJpq_viewTabs{border-bottom:1px solid var(--dsw-alias-border-l1);flex:1;gap:2px;display:flex}.fjdJpq_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}.fjdJpq_viewTab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.fjdJpq_viewTab[data-active]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:600}.fjdJpq_sectionBody{flex-direction:column;flex:1;min-height:0;display:flex;overflow-y:auto}.fjdJpq_viewBody{flex-direction:column;gap:12px;padding:4px 2px 16px;display:flex}.fjdJpq_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}.fjdJpq_iconButton:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fjdJpq_sectionTitleBlock{flex-direction:column;gap:2px;display:flex}.fjdJpq_sectionTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:15px;font-weight:700}.fjdJpq_sectionSubtitle{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:1.5}.fjdJpq_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}.fjdJpq_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}.fjdJpq_primaryButton:disabled{opacity:.5;cursor:default}.fjdJpq_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}.fjdJpq_ghostButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.fjdJpq_ghostButton:disabled{opacity:.45;cursor:default}.fjdJpq_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}.fjdJpq_dangerButton:hover:not(:disabled){background:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-label-primary-foreground)}.fjdJpq_dangerButton:disabled{opacity:.45;cursor:default}.fjdJpq_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}.fjdJpq_badgeInfo{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}.fjdJpq_badgeOk{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.fjdJpq_badgeWarn{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.fjdJpq_badgeError{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.fjdJpq_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}.fjdJpq_banner[data-kind=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.fjdJpq_banner[data-kind=error]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.fjdJpq_banner[data-kind=info]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}.fjdJpq_banner[data-kind=warn]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.fjdJpq_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}.fjdJpq_field{flex-direction:column;gap:5px;display:flex}.fjdJpq_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}.fjdJpq_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}.fjdJpq_input:focus{border-color:var(--dsw-alias-state-business-primary)}.fjdJpq_input::placeholder{color:var(--dsw-alias-label-tertiary)}.fjdJpq_input:disabled{opacity:.55}.fjdJpq_hint{color:var(--dsw-alias-label-tertiary);font-size:11.5px;line-height:1.5}.fjdJpq_formError{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}.fjdJpq_checkboxRow{color:var(--dsw-alias-label-primary);cursor:pointer;align-items:flex-start;gap:8px;font-size:13px;display:flex}.fjdJpq_checkboxRow input{flex:none;margin-top:2px}.fjdJpq_radioLabel{color:var(--dsw-alias-label-primary);cursor:pointer;align-items:center;gap:6px;font-size:13px;display:inline-flex}.fjdJpq_secretFields{grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px 12px;padding-top:4px;display:grid}.fjdJpq_hiddenFile{display:none}.fjdJpq_modeTabs{border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;gap:2px;display:flex}.fjdJpq_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}.fjdJpq_modeTab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.fjdJpq_modeTab[data-active]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:600}.fjdJpq_modeHint{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}.fjdJpq_groupList{flex-direction:column;gap:10px;display:flex}.fjdJpq_groupCard{gap:8px}.fjdJpq_groupHeader{flex-wrap:wrap;align-items:baseline;gap:8px;display:flex}.fjdJpq_groupLabel{color:var(--dsw-alias-label-primary);font-size:12.5px;font-weight:700}.fjdJpq_groupNote{color:var(--dsw-alias-label-tertiary);font-size:11.5px}.fjdJpq_groupItems{flex-direction:column;gap:6px;display:flex}.fjdJpq_categoryItem{flex-wrap:wrap;align-items:center;gap:8px;display:inline-flex}.fjdJpq_categoryName{font-weight:500}.fjdJpq_categoryDesc{color:var(--dsw-alias-label-tertiary);font-size:11.5px}.fjdJpq_optionsCard{gap:8px}.fjdJpq_warnList{margin:4px 0 0;padding-left:18px}.fjdJpq_actionRow{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.fjdJpq_progressBlock{flex-direction:column;flex:none;gap:6px;display:flex}.fjdJpq_progressMeta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:12px;font-size:12px;display:flex}.fjdJpq_progressLabel{font-weight:600}.fjdJpq_progressDetail{color:var(--dsw-alias-label-tertiary)}.fjdJpq_progressPercent{font-variant-numeric:tabular-nums;margin-left:auto}.fjdJpq_progressTrack{background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;height:8px;overflow:hidden}.fjdJpq_progressBar{background:var(--dsw-alias-state-business-primary);border-radius:999px;height:100%;transition:width .12s linear}.fjdJpq_progressBarDone{background:var(--dsw-alias-state-success-primary)}.fjdJpq_progressIndeterminate{width:40%;animation:1s ease-in-out infinite fjdJpq_dshCmIndeterminate}@keyframes fjdJpq_dshCmIndeterminate{0%{margin-left:-40%}to{margin-left:100%}}.fjdJpq_reportView{flex-direction:column;gap:10px;display:flex}.fjdJpq_statRow{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.fjdJpq_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}.fjdJpq_reportFooter{justify-content:flex-end;gap:8px;display:flex}.fjdJpq_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}.fjdJpq_rollbackBox .fjdJpq_reportText{border-color:var(--dsw-alias-border-l2)}.fjdJpq_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}.fjdJpq_errorTitle{color:var(--dsw-alias-state-error-primary);font-size:13px;font-weight:700}.fjdJpq_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}.fjdJpq_errorAction{color:var(--dsw-alias-label-secondary);font-size:12.5px}.fjdJpq_errorActionLabel{color:var(--dsw-alias-state-business-primary)}.fjdJpq_errorItem{color:var(--dsw-alias-label-tertiary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px}.fjdJpq_errorFooter{justify-content:flex-end;display:flex}.fjdJpq_errorList{flex-direction:column;gap:4px;display:flex}.fjdJpq_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}.fjdJpq_conflictList{flex-direction:column;gap:10px;display:flex}.fjdJpq_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}.fjdJpq_conflictHead{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.fjdJpq_conflictId{color:var(--dsw-alias-label-primary);font-size:12.5px;font-weight:600}.fjdJpq_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}.fjdJpq_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}.fjdJpq_conflictOptions{flex-wrap:wrap;gap:14px;display:flex}.fjdJpq_pathMappingList{flex-direction:column;gap:8px;display:flex}.fjdJpq_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}.fjdJpq_pathOld,.fjdJpq_pathNew{flex-direction:column;gap:4px;min-width:0;display:flex}.fjdJpq_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}.fjdJpq_pathIssueKind{color:var(--dsw-alias-state-warn-primary);font-size:10.5px}.fjdJpq_secretsList{flex-direction:column;gap:10px;display:flex}.fjdJpq_empty{text-align:center;color:var(--dsw-alias-label-tertiary);padding:22px 12px;font-size:12.5px}.fjdJpq_spinnerWrap{align-items:center;gap:6px;display:inline-flex}.fjdJpq_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 fjdJpq_dshCmSpin;display:inline-block}.fjdJpq_spinnerLabel{color:var(--dsw-alias-label-secondary);font-size:12px}@keyframes fjdJpq_dshCmSpin{to{transform:rotate(360deg)}}";
553
+ const tagId = "dsh-config-manager/config-manager.module.css";
554
+ if (typeof document !== "undefined" && document.querySelector("style[data-tag=\"dsh-config-manager/config-manager.module.css\"]") === null) {
555
+ const tag = document.createElement("style");
556
+ tag.dataset.plugin = "dsh-config-manager";
557
+ tag.dataset.tag = tagId;
558
+ tag.textContent = css;
559
+ document.head.appendChild(tag);
560
+ }
561
+ var config_manager_module_css_default = {
562
+ "reportText": "fjdJpq_reportText",
563
+ "empty": "fjdJpq_empty",
564
+ "dshCmSpin": "fjdJpq_dshCmSpin",
565
+ "radioLabel": "fjdJpq_radioLabel",
566
+ "progressIndeterminate": "fjdJpq_progressIndeterminate",
567
+ "progressBar": "fjdJpq_progressBar",
568
+ "secretsList": "fjdJpq_secretsList",
569
+ "progressLabel": "fjdJpq_progressLabel",
570
+ "spinnerWrap": "fjdJpq_spinnerWrap",
571
+ "warnList": "fjdJpq_warnList",
572
+ "conflictId": "fjdJpq_conflictId",
573
+ "card": "fjdJpq_card",
574
+ "categoryItem": "fjdJpq_categoryItem",
575
+ "checkboxRow": "fjdJpq_checkboxRow",
576
+ "hint": "fjdJpq_hint",
577
+ "sectionTitle": "fjdJpq_sectionTitle",
578
+ "reportFooter": "fjdJpq_reportFooter",
579
+ "errorReason": "fjdJpq_errorReason",
580
+ "errorAction": "fjdJpq_errorAction",
581
+ "errorBanner": "fjdJpq_errorBanner",
582
+ "pathNew": "fjdJpq_pathNew",
583
+ "errorLine": "fjdJpq_errorLine",
584
+ "sectionHeader": "fjdJpq_sectionHeader",
585
+ "errorFooter": "fjdJpq_errorFooter",
586
+ "statRow": "fjdJpq_statRow",
587
+ "conflictDetail": "fjdJpq_conflictDetail",
588
+ "input": "fjdJpq_input",
589
+ "progressTrack": "fjdJpq_progressTrack",
590
+ "primaryButton": "fjdJpq_primaryButton",
591
+ "badgeError": "fjdJpq_badgeError",
592
+ "viewTabs": "fjdJpq_viewTabs",
593
+ "groupList": "fjdJpq_groupList",
594
+ "section": "fjdJpq_section",
595
+ "groupHeader": "fjdJpq_groupHeader",
596
+ "reportView": "fjdJpq_reportView",
597
+ "pathIssueKind": "fjdJpq_pathIssueKind",
598
+ "categoryDesc": "fjdJpq_categoryDesc",
599
+ "rollbackBox": "fjdJpq_rollbackBox",
600
+ "conflictList": "fjdJpq_conflictList",
601
+ "pathValue": "fjdJpq_pathValue",
602
+ "badgeOk": "fjdJpq_badgeOk",
603
+ "formError": "fjdJpq_formError",
604
+ "conflictHead": "fjdJpq_conflictHead",
605
+ "groupLabel": "fjdJpq_groupLabel",
606
+ "banner": "fjdJpq_banner",
607
+ "severityError": "fjdJpq_severityError",
608
+ "conflictOptions": "fjdJpq_conflictOptions",
609
+ "ghostButton": "fjdJpq_ghostButton",
610
+ "actionRow": "fjdJpq_actionRow",
611
+ "spinnerLabel": "fjdJpq_spinnerLabel",
612
+ "field": "fjdJpq_field",
613
+ "badgeWarn": "fjdJpq_badgeWarn",
614
+ "secretFields": "fjdJpq_secretFields",
615
+ "groupCard": "fjdJpq_groupCard",
616
+ "badgeInfo": "fjdJpq_badgeInfo",
617
+ "pathMappingList": "fjdJpq_pathMappingList",
618
+ "viewBody": "fjdJpq_viewBody",
619
+ "progressBlock": "fjdJpq_progressBlock",
620
+ "dshCmIndeterminate": "fjdJpq_dshCmIndeterminate",
621
+ "sectionBody": "fjdJpq_sectionBody",
622
+ "sectionTitleBlock": "fjdJpq_sectionTitleBlock",
623
+ "hiddenFile": "fjdJpq_hiddenFile",
624
+ "modeTabs": "fjdJpq_modeTabs",
625
+ "fieldLabel": "fjdJpq_fieldLabel",
626
+ "categoryName": "fjdJpq_categoryName",
627
+ "optionsCard": "fjdJpq_optionsCard",
628
+ "progressDetail": "fjdJpq_progressDetail",
629
+ "progressPercent": "fjdJpq_progressPercent",
630
+ "spinner": "fjdJpq_spinner",
631
+ "groupItems": "fjdJpq_groupItems",
632
+ "progressMeta": "fjdJpq_progressMeta",
633
+ "iconButton": "fjdJpq_iconButton",
634
+ "badge": "fjdJpq_badge",
635
+ "viewTab": "fjdJpq_viewTab",
636
+ "modeHint": "fjdJpq_modeHint",
637
+ "groupNote": "fjdJpq_groupNote",
638
+ "pathRow": "fjdJpq_pathRow",
639
+ "errorActionLabel": "fjdJpq_errorActionLabel",
640
+ "errorTitle": "fjdJpq_errorTitle",
641
+ "conflictItem": "fjdJpq_conflictItem",
642
+ "errorList": "fjdJpq_errorList",
643
+ "dangerButton": "fjdJpq_dangerButton",
644
+ "modeTab": "fjdJpq_modeTab",
645
+ "progressBarDone": "fjdJpq_progressBarDone",
646
+ "sectionSubtitle": "fjdJpq_sectionSubtitle",
647
+ "errorItem": "fjdJpq_errorItem",
648
+ "pathOld": "fjdJpq_pathOld"
649
+ };
650
+ //#endregion
651
+ //#region src/client/common/ui.tsx
652
+ /** 统一按钮(primary=主操作 / ghost=次操作 / danger=危险操作) */
653
+ function Button({ variant = "ghost", disabled, onClick, children, title, className }) {
654
+ const cls = variant === "primary" ? config_manager_module_css_default.primaryButton : variant === "danger" ? config_manager_module_css_default.dangerButton : config_manager_module_css_default.ghostButton;
655
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
656
+ type: "button",
657
+ className: className !== void 0 ? `${cls} ${className}` : cls,
658
+ disabled,
659
+ title,
660
+ onClick,
661
+ children
662
+ });
663
+ }
664
+ /** 状态徽章(info=业务色 / ok=成功 / warn=警告 / error=错误) */
665
+ function Badge({ kind = "info", children }) {
666
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
667
+ className: `${config_manager_module_css_default.badge} ${config_manager_module_css_default[`badge${kind[0].toUpperCase()}${kind.slice(1)}`] ?? ""}`,
668
+ children
669
+ });
670
+ }
671
+ /** 说明横幅(ok/error/info/warn 四态) */
672
+ function Banner({ kind = "info", children }) {
673
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
674
+ className: config_manager_module_css_default.banner,
675
+ "data-kind": kind,
676
+ children
677
+ });
678
+ }
679
+ /** 卡片容器(bg-layer-2 + 圆角 + 细边框) */
680
+ function Card({ children, className }) {
681
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
682
+ className: className !== void 0 ? `${config_manager_module_css_default.card} ${className}` : config_manager_module_css_default.card,
683
+ children
684
+ });
685
+ }
686
+ /** 加载指示(旋转环 + 可选文案) */
687
+ function Spinner({ label }) {
688
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
689
+ className: config_manager_module_css_default.spinnerWrap,
690
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
691
+ className: config_manager_module_css_default.spinner,
692
+ "aria-hidden": "true"
693
+ }), label !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
694
+ className: config_manager_module_css_default.spinnerLabel,
695
+ children: label
696
+ })]
697
+ });
698
+ }
699
+ /** 区块标题(页面内二级标题 + 可选副标题) */
700
+ function SectionTitle({ title, subtitle }) {
701
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
702
+ className: config_manager_module_css_default.sectionTitleBlock,
703
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
704
+ className: config_manager_module_css_default.sectionTitle,
705
+ children: title
706
+ }), subtitle !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
707
+ className: config_manager_module_css_default.sectionSubtitle,
708
+ children: subtitle
709
+ })]
710
+ });
711
+ }
712
+ /** 空状态占位 */
713
+ function Empty({ children }) {
714
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
715
+ className: config_manager_module_css_default.empty,
716
+ children
717
+ });
718
+ }
719
+ /** 复选框行(勾选 + 标签) */
720
+ function Checkbox({ checked, onChange, label, disabled }) {
721
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
722
+ className: config_manager_module_css_default.checkboxRow,
723
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
724
+ type: "checkbox",
725
+ checked,
726
+ disabled,
727
+ onChange: (event) => {
728
+ onChange(event.target.checked);
729
+ }
730
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
731
+ });
732
+ }
733
+ //#endregion
734
+ //#region src/security/secret-scanner.ts
735
+ /**
736
+ * 敏感字段名单(规范化名:小写、无分隔符)。
737
+ * 覆盖规范 §6 原始清单 + 设计 §7.2 + core logger 名单,并保持克制以防误伤
738
+ * (不收录 `key`/`session` 等过宽子串——`sessionIds`/`monkey` 不得中招)。
739
+ */
740
+ const DEFAULT_SECRET_FIELD_NAMES = [
741
+ "password",
742
+ "passwd",
743
+ "token",
744
+ "accesstoken",
745
+ "refreshtoken",
746
+ "apikey",
747
+ "secret",
748
+ "credential",
749
+ "authorization",
750
+ "cookie",
751
+ "privatekey",
752
+ "clientsecret",
753
+ "pwd",
754
+ "passphrase",
755
+ "authtoken",
756
+ "sessionkey",
757
+ "apisecret",
758
+ "authheader",
759
+ "bearer",
760
+ "webhooksecret"
761
+ ];
762
+ /** 敏感后缀(规范化后以这些结尾 → 命中;`key` 故意不收,避免 monkey/whiskey 误伤) */
763
+ const SENSITIVE_SUFFIXES = [
764
+ "token",
765
+ "secret",
766
+ "password",
767
+ "passwd",
768
+ "credential"
769
+ ];
770
+ /** 敏感前缀(规范化后以这些开头 → 命中;token/secret 等过宽词不收入) */
771
+ const SENSITIVE_PREFIXES = [
772
+ "apikey",
773
+ "accesstoken",
774
+ "refreshtoken",
775
+ "authtoken",
776
+ "clientsecret",
777
+ "privatekey",
778
+ "authorization",
779
+ "authheader",
780
+ "webhooksecret",
781
+ "apisecret"
782
+ ];
783
+ /** 规范化字段名:小写 + 去 `_-. ` 分隔符(用于名单匹配) */
784
+ function normalizeFieldName(name) {
785
+ return name.toLowerCase().replace(/[^a-z0-9]/g, "");
786
+ }
787
+ /** 字段名是否命中敏感名单(规范化后精确 / 敏感后缀 / 敏感前缀) */
788
+ function isSensitiveFieldName(field, extra = []) {
789
+ const norm = normalizeFieldName(field);
790
+ if (norm === "") return false;
791
+ if ([...DEFAULT_SECRET_FIELD_NAMES, ...extra].includes(norm)) return true;
792
+ if (SENSITIVE_SUFFIXES.some((s) => norm.endsWith(s) && norm.length > s.length)) return true;
793
+ return SENSITIVE_PREFIXES.some((p) => norm.startsWith(p));
794
+ }
795
+ //#endregion
796
+ //#region src/security/redaction.ts
797
+ /**
798
+ * 日志/文本脱敏(规范 §24 / 设计 §9.8 / core utils/logger.ts 的强化层)。
799
+ *
800
+ * 与 core logger.redact 的分工:core 版按固定字段名单做子串替换(不感知字段边界);
801
+ * 本模块为强化版,两模式:
802
+ * 1. **结构化字段形态**:JSON `"field": "value"` / `field=value` / `field: value`,
803
+ * 字段名命中敏感名单(复用 secret-scanner 的 `isSensitiveFieldName`,单一来源防漂移)→ 值替换。
804
+ * 2. **值形状模式**:与字段名无关,文本任意位置出现 sk- / JWT / AKIA / GitHub PAT /
805
+ * PEM 私钥 / Bearer / URL query 的敏感参数值 → 替换。
806
+ *
807
+ * 幂等性:替换产物 `***REDACTED***` 不匹配任何模式,重复 redact 结果不变;
808
+ * JSON 行的引号结构保留(输出仍是合法 JSON)。
809
+ */
810
+ const REDACTED = "***REDACTED***";
811
+ const JSON_FIELD_RE = /"([A-Za-z0-9_.\-]+)"\s*:\s*"([^"]*)"/g;
812
+ /** kv 值到空白/逗号/分号/&(& 截断:避免吞掉 URL query 的后续参数) */
813
+ const KV_FIELD_RE = /([A-Za-z0-9_.\-]+)\s*=\s*([^\s,;&]+)/g;
814
+ /** colon 值允许空格(如 `Authorization: Bearer xyz`),到逗号/分号/右大括号截断 */
815
+ const COLON_FIELD_RE = /([A-Za-z0-9_.\-]+)\s*:\s*([^,;}]+)/g;
816
+ const VALUE_PATTERNS = [
817
+ {
818
+ name: "openai-key",
819
+ re: /sk-[A-Za-z0-9_-]{8,}/g
820
+ },
821
+ {
822
+ name: "jwt",
823
+ re: /eyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g
824
+ },
825
+ {
826
+ name: "aws-access-key",
827
+ re: /AKIA[0-9A-Z]{16}/g
828
+ },
829
+ {
830
+ name: "github-token",
831
+ re: /gh[pousr]_[A-Za-z0-9]{20,}/g
832
+ },
833
+ {
834
+ name: "github-pat",
835
+ re: /github_pat_[A-Za-z0-9_]{20,}/g
836
+ },
837
+ {
838
+ name: "pem-private-key",
839
+ re: /-----BEGIN [A-Za-z0-9 ]*PRIVATE KEY-----/g
840
+ },
841
+ {
842
+ name: "bearer-token",
843
+ re: /Bearer [A-Za-z0-9._~+/=-]{8,}/g
844
+ }
845
+ ];
846
+ /** URL query 中的敏感参数值(保留参数名,只替换值) */
847
+ const URL_QUERY_RE = /([?&](?:token|api[_-]?key|key|secret|access[_-]?token|password|auth)=)([^&\s"']+)/gi;
848
+ function replaceSensitiveField(text, re, blacklist) {
849
+ return text.replace(re, (match, field, value) => {
850
+ if (!isSensitiveFieldName(field, blacklist)) return match;
851
+ if (value === "") return match;
852
+ const idx = match.lastIndexOf(value);
853
+ return match.slice(0, idx) + REDACTED + match.slice(idx + value.length);
854
+ });
855
+ }
856
+ /**
857
+ * 文本脱敏(幂等)。blacklist 为**附加**规范化字段名(小写无分隔符,如 'clientsecret')。
858
+ * 输出仍保留结构化形态(JSON 行合法)。
859
+ */
860
+ function redact(text, blacklist = []) {
861
+ let out = text;
862
+ out = replaceSensitiveField(out, JSON_FIELD_RE, blacklist);
863
+ out = replaceSensitiveField(out, KV_FIELD_RE, blacklist);
864
+ out = replaceSensitiveField(out, COLON_FIELD_RE, blacklist);
865
+ for (const { re } of VALUE_PATTERNS) {
866
+ re.lastIndex = 0;
867
+ out = out.replace(re, REDACTED);
868
+ }
869
+ out = out.replace(URL_QUERY_RE, `$1${REDACTED}`);
870
+ return out;
871
+ }
872
+ //#endregion
873
+ //#region src/ui/errors.ts
874
+ /**
875
+ * 可操作错误模型(规范 §23,m6-ui)。
876
+ *
877
+ * 绝不只有 "Something went wrong.":
878
+ * - 解析错误 → Reason + Suggested action(可操作);
879
+ * - 输出前强制脱敏(redact),Secret 永不进入 UI/日志。
880
+ */
881
+ const ERROR_RULES = [
882
+ {
883
+ match: (m) => /SETTINGS_CONFLICT|revision.*conflict/i.test(m),
884
+ title: "配置已被并发修改",
885
+ suggestedAction: "目标 DSH 的该配置在导入期间被其他进程修改,为避免覆盖请重新导出或手动核对后再试。",
886
+ retryable: true
887
+ },
888
+ {
889
+ match: (m) => /ImportNotConfirmed|未确认/i.test(m),
890
+ title: "导入未确认",
891
+ suggestedAction: "请在预览页确认导入内容后重试。",
892
+ retryable: true
893
+ },
894
+ {
895
+ match: (m) => /backup integrity|完整性校验失败|checksum/i.test(m),
896
+ title: "备份完整性校验失败",
897
+ suggestedAction: "备份文件可能已损坏或被篡改,请重新导出备份后再试。",
898
+ retryable: false
899
+ },
900
+ {
901
+ match: (m) => /schema.*(不支持|超出)|无法导入|Unsupported/i.test(m),
902
+ title: "备份格式版本不受支持",
903
+ suggestedAction: "请升级 DSH Config Manager 或使用相同版本的备份文件。",
904
+ retryable: false
905
+ },
906
+ {
907
+ match: (m) => /ENOENT|not found|无法读取|不存在/i.test(m),
908
+ title: "文件或路径不存在",
909
+ suggestedAction: "请检查文件路径是否正确、文件是否已被移动或删除。",
910
+ retryable: true
911
+ },
912
+ {
913
+ match: (m) => /EACCES|permission|权限/i.test(m),
914
+ title: "权限不足",
915
+ suggestedAction: "请检查目标目录的读写权限后重试。",
916
+ retryable: true
917
+ },
918
+ {
919
+ match: (m) => /install.*(plugin|插件)|needsRestart|重启/i.test(m),
920
+ title: "插件安装需要重启生效",
921
+ suggestedAction: "导入已完成,插件将在重启 DSH 后生效;请在 DSH Desktop 中重启服务。",
922
+ retryable: false
923
+ }
924
+ ];
925
+ /** 将任意错误转换为可操作错误(消息已脱敏,不泄漏 Secret) */
926
+ function toActionableError(err, opts) {
927
+ const message = redact(err instanceof Error ? err.message : String(err ?? "未知错误"));
928
+ const rule = ERROR_RULES.find((r) => r.match(message));
929
+ return {
930
+ title: rule?.title ?? opts?.fallbackTitle ?? "操作失败",
931
+ reason: message,
932
+ suggestedAction: rule?.suggestedAction,
933
+ item: opts?.item !== void 0 ? redact(opts.item) : void 0,
934
+ retryable: rule?.retryable ?? true
935
+ };
936
+ }
937
+ /** 格式化为用户可读的多行文本(Reason / Suggested action) */
938
+ function formatActionableError(e) {
939
+ const lines = [e.title];
940
+ lines.push(`Reason: ${e.reason}`);
941
+ if (e.suggestedAction) lines.push(`Suggested action: ${e.suggestedAction}`);
942
+ if (e.item) lines.push(`Item: ${e.item}`);
943
+ return lines.join("\n");
944
+ }
945
+ //#endregion
946
+ //#region src/client/common/ErrorBanner.tsx
947
+ /**
948
+ * 可操作错误展示(规范 §23,绑 src/ui/errors.ts)。
949
+ *
950
+ * 安全约束:**强制 redact 不泄 Secret** —— 展示前对错误消息、关联项文本
951
+ * 统一过 `redact()`(字段名黑名单 + sk-/JWT/PEM/Bearer 等值形状模式),
952
+ * 渲染结果只含 Reason / Suggested action / Item,绝不出现密钥原文。
953
+ */
954
+ /**
955
+ * 错误横幅:toActionableError 解析为标题 + 原因 + 建议动作,
956
+ * 文本在渲染前再经 redact() 兜底(双保险),Reason 以等宽块展示。
957
+ */
958
+ function ErrorBanner({ error, onRetry, retrying }) {
959
+ const [actionable] = (0, react.useState)(() => toActionableError(error));
960
+ const reason = redact(actionable.reason);
961
+ const item = actionable.item !== void 0 ? redact(actionable.item) : void 0;
962
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
963
+ className: config_manager_module_css_default.errorBanner,
964
+ role: "alert",
965
+ children: [
966
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
967
+ className: config_manager_module_css_default.errorTitle,
968
+ children: redact(actionable.title)
969
+ }),
970
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
971
+ className: config_manager_module_css_default.errorReason,
972
+ children: reason
973
+ }),
974
+ actionable.suggestedAction !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
975
+ className: config_manager_module_css_default.errorAction,
976
+ children: [
977
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
978
+ className: config_manager_module_css_default.errorActionLabel,
979
+ children: "→"
980
+ }),
981
+ " ",
982
+ redact(actionable.suggestedAction)
983
+ ]
984
+ }),
985
+ item !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
986
+ className: config_manager_module_css_default.errorItem,
987
+ children: item
988
+ }),
989
+ actionable.retryable && onRetry !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
990
+ className: config_manager_module_css_default.errorFooter,
991
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
992
+ variant: "primary",
993
+ onClick: onRetry,
994
+ disabled: retrying === true,
995
+ children: retrying === true ? "…" : "Retry"
996
+ })
997
+ })
998
+ ]
999
+ });
1000
+ }
1001
+ /** 多行错误文本展示(Wizard.errors 数组用;同样强制 redact) */
1002
+ function ErrorList({ errors }) {
1003
+ if (errors.length === 0) return null;
1004
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1005
+ className: config_manager_module_css_default.errorList,
1006
+ children: errors.map((line, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1007
+ className: config_manager_module_css_default.errorLine,
1008
+ children: redact(line)
1009
+ }, i))
1010
+ });
1011
+ }
1012
+ //#endregion
1013
+ //#region src/client/common/ProgressBar.tsx
1014
+ /**
1015
+ * 进度条(规范 §29,绑 src/ui/progress.ts)。
1016
+ *
1017
+ * 控制器(ExportFlow / ImportWizard)在调用 core 前后发出 ProgressEvent,
1018
+ * 本组件按 stage 文案 + step/total 渲染进度条;未知阶段回退显示 id 本身。
1019
+ */
1020
+ /**
1021
+ * 进度条:阶段文字 + 百分比(有 step/total 时计算,否则不定态动画)。
1022
+ */
1023
+ function ProgressBar({ event, active }) {
1024
+ const percent = event !== null && event.step !== void 0 && event.total !== void 0 && event.total > 0 ? Math.round(event.step / event.total * 100) : null;
1025
+ const label = event !== null ? stageText(event.stage) : "";
1026
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1027
+ className: config_manager_module_css_default.progressBlock,
1028
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1029
+ className: config_manager_module_css_default.progressMeta,
1030
+ children: [
1031
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1032
+ className: config_manager_module_css_default.progressLabel,
1033
+ children: label
1034
+ }),
1035
+ event?.detail !== void 0 && event.detail !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1036
+ className: config_manager_module_css_default.progressDetail,
1037
+ children: event.detail
1038
+ }),
1039
+ percent !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1040
+ className: config_manager_module_css_default.progressPercent,
1041
+ children: [percent, "%"]
1042
+ })
1043
+ ]
1044
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1045
+ className: config_manager_module_css_default.progressTrack,
1046
+ children: percent !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1047
+ className: `${config_manager_module_css_default.progressBar} ${active ? "" : config_manager_module_css_default.progressBarDone}`,
1048
+ style: { width: `${percent}%` }
1049
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: `${config_manager_module_css_default.progressBar} ${config_manager_module_css_default.progressIndeterminate}` })
1050
+ })]
1051
+ });
1052
+ }
1053
+ //#endregion
1054
+ //#region src/client/common/ReportView.tsx
1055
+ /** 导入分区的统计徽章(由 report.importSectionStats 计算) */
1056
+ function SectionStatBadges({ result }) {
1057
+ const stats = importSectionStats(result.executed);
1058
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1059
+ className: config_manager_module_css_default.statRow,
1060
+ children: stats.map((s) => {
1061
+ let kind = "ok";
1062
+ if (s.failed > 0) kind = "error";
1063
+ else if (s.skipped > 0) kind = "warn";
1064
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Badge, {
1065
+ kind,
1066
+ children: [
1067
+ s.section,
1068
+ ": ",
1069
+ s.ok,
1070
+ "✓",
1071
+ s.skipped > 0 ? ` ${s.skipped}≈` : "",
1072
+ s.failed > 0 ? ` ${s.failed}✗` : ""
1073
+ ]
1074
+ }, s.section);
1075
+ })
1076
+ });
1077
+ }
1078
+ /** 导出报告的安全摘要(Included / Excluded / Security 徽章) */
1079
+ function ExportSummary({ report }) {
1080
+ const included = report.included.length;
1081
+ const excluded = report.excluded.length;
1082
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1083
+ className: config_manager_module_css_default.statRow,
1084
+ children: [
1085
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Badge, {
1086
+ kind: "ok",
1087
+ children: [included, " included"]
1088
+ }),
1089
+ excluded > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Badge, {
1090
+ kind: "warn",
1091
+ children: [excluded, " excluded"]
1092
+ }),
1093
+ report.security.containsSecrets && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
1094
+ kind: "warn",
1095
+ children: "encrypted"
1096
+ }),
1097
+ !report.security.containsSecrets && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
1098
+ kind: "ok",
1099
+ children: "no secrets"
1100
+ }),
1101
+ report.security.redactedHits > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Badge, {
1102
+ kind: "error",
1103
+ children: [report.security.redactedHits, " redacted"]
1104
+ })
1105
+ ]
1106
+ });
1107
+ }
1108
+ /**
1109
+ * 结果报告视图。
1110
+ * 文本详情 = report.ts 渲染器的输出(已脱敏),展示前再过 redact() 双保险;
1111
+ * 以 <pre> 等宽块呈现保持对齐。
1112
+ */
1113
+ function ReportView({ kind, exportReport, importResult, onAction, onDownload }) {
1114
+ const actions = kind === "import" && importResult !== void 0 ? suggestedActions(importResult) : [];
1115
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1116
+ className: config_manager_module_css_default.reportView,
1117
+ children: [kind === "export" && exportReport !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1118
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExportSummary, { report: exportReport }),
1119
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1120
+ className: config_manager_module_css_default.reportText,
1121
+ children: redact(renderExportReport(exportReport))
1122
+ }),
1123
+ onDownload !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1124
+ className: config_manager_module_css_default.reportFooter,
1125
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
1126
+ variant: "primary",
1127
+ onClick: onDownload,
1128
+ children: "Download"
1129
+ })
1130
+ })
1131
+ ] }), kind === "import" && importResult !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1132
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionStatBadges, { result: importResult }),
1133
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1134
+ className: config_manager_module_css_default.reportText,
1135
+ children: redact(renderImportReport(importResult))
1136
+ }),
1137
+ importResult.rollback !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1138
+ className: config_manager_module_css_default.rollbackBox,
1139
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Rollback" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1140
+ className: config_manager_module_css_default.reportText,
1141
+ children: redact(renderRollbackReport(importResult.rollback))
1142
+ })]
1143
+ }),
1144
+ actions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1145
+ className: config_manager_module_css_default.reportFooter,
1146
+ children: actions.map((a) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
1147
+ variant: a === "done" ? "primary" : "ghost",
1148
+ onClick: () => onAction?.(a),
1149
+ children: a
1150
+ }, a))
1151
+ })
1152
+ ] })]
1153
+ });
1154
+ }
1155
+ //#endregion
1156
+ //#region src/client/export/ExportView.tsx
1157
+ /**
1158
+ * 导出视图(规范 §1 / §21,绑 src/ui/export-flow.ts 的 ExportFlow 控制器)。
1159
+ *
1160
+ * - Quick:一键导出推荐分区(ExportFlow.quickSelection());
1161
+ * - Custom:按 §1 分组目录(EXPORT_GROUPS × DEFAULT_CATEGORIES)逐项勾选,
1162
+ * ExportFlow.validateSelection() 给出设备相关分区警告;
1163
+ * - Include secrets:可选加密备份(需设置加密密码,密码仅本次内存使用,
1164
+ * 经 api.exportPassword 随请求体传给 Host 半,绝不落盘/入 manifest);
1165
+ * - 进度:ExportFlow.run 发出 ProgressEvent → ProgressBar;
1166
+ * - 结果:ReportView(export) + 下载按钮(File System Access API 流式落盘)。
1167
+ */
1168
+ /** Custom 模式的初始勾选 = 推荐分区(可再调整) */
1169
+ function defaultCustomSelection() {
1170
+ return DEFAULT_CATEGORIES.filter((c) => c.defaultIncluded).map((c) => c.id);
1171
+ }
1172
+ /**
1173
+ * 导出主视图:Quick/Custom 切换 → 勾选/密码 → 执行 → 进度 → 报告 → 下载。
1174
+ */
1175
+ function ExportView({ api, t }) {
1176
+ const [mode, setMode] = (0, react.useState)("quick");
1177
+ const [selection, setSelection] = (0, react.useState)(defaultCustomSelection);
1178
+ const [includeSecrets, setIncludeSecrets] = (0, react.useState)(false);
1179
+ const [password, setPassword] = (0, react.useState)("");
1180
+ const [passwordConfirm, setPasswordConfirm] = (0, react.useState)("");
1181
+ const [running, setRunning] = (0, react.useState)(false);
1182
+ const [progress, setProgress] = (0, react.useState)(null);
1183
+ const [result, setResult] = (0, react.useState)(null);
1184
+ const [error, setError] = (0, react.useState)(null);
1185
+ const [downloaded, setDownloaded] = (0, react.useState)(false);
1186
+ const [flow] = (0, react.useState)(() => new ExportFlow({
1187
+ port: api,
1188
+ onProgress: (event) => {
1189
+ setProgress(event);
1190
+ }
1191
+ }));
1192
+ const toggleSection = (0, react.useCallback)((id, checked) => {
1193
+ setSelection((prev) => {
1194
+ const has = prev.includes(id);
1195
+ if (checked && !has) return [...prev, id];
1196
+ if (!checked && has) return prev.filter((s) => s !== id);
1197
+ return prev;
1198
+ });
1199
+ }, []);
1200
+ const passwordInvalid = includeSecrets && (password === "" || password !== passwordConfirm);
1201
+ /** 执行导出(Quick 或 Custom) */
1202
+ const runExport = async () => {
1203
+ if (passwordInvalid) return;
1204
+ setRunning(true);
1205
+ setError(null);
1206
+ setResult(null);
1207
+ setDownloaded(false);
1208
+ setProgress(null);
1209
+ try {
1210
+ api.exportPassword = includeSecrets ? password : null;
1211
+ const run = await flow.run(mode, selection, { includeSecrets });
1212
+ setResult(run);
1213
+ } catch (err) {
1214
+ setError(err);
1215
+ } finally {
1216
+ setRunning(false);
1217
+ setProgress(null);
1218
+ }
1219
+ };
1220
+ /** 下载导出的 ZIP 到本机 */
1221
+ const download = async () => {
1222
+ if (result === null) return;
1223
+ try {
1224
+ setError(null);
1225
+ await api.download(result.zipPath);
1226
+ setDownloaded(true);
1227
+ } catch (err) {
1228
+ setError(err);
1229
+ }
1230
+ };
1231
+ const customWarnings = mode === "custom" ? flow.validateSelection(selection).warnings : [];
1232
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1233
+ className: config_manager_module_css_default.viewBody,
1234
+ children: [
1235
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionTitle, {
1236
+ title: t("view.export"),
1237
+ subtitle: t("section.description")
1238
+ }),
1239
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1240
+ className: config_manager_module_css_default.modeTabs,
1241
+ role: "tablist",
1242
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1243
+ type: "button",
1244
+ role: "tab",
1245
+ "aria-selected": mode === "quick",
1246
+ "data-active": mode === "quick" ? "" : void 0,
1247
+ className: config_manager_module_css_default.modeTab,
1248
+ onClick: () => {
1249
+ setMode("quick");
1250
+ },
1251
+ children: t("export.mode.quick")
1252
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1253
+ type: "button",
1254
+ role: "tab",
1255
+ "aria-selected": mode === "custom",
1256
+ "data-active": mode === "custom" ? "" : void 0,
1257
+ className: config_manager_module_css_default.modeTab,
1258
+ onClick: () => {
1259
+ setMode("custom");
1260
+ },
1261
+ children: t("export.mode.custom")
1262
+ })]
1263
+ }),
1264
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1265
+ className: config_manager_module_css_default.modeHint,
1266
+ children: mode === "quick" ? t("export.mode.quickHint") : t("export.mode.customHint")
1267
+ }),
1268
+ mode === "custom" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1269
+ className: config_manager_module_css_default.groupList,
1270
+ children: EXPORT_GROUPS.map((group) => {
1271
+ const categories = flow.categories.filter((c) => c.group === group.id);
1272
+ if (categories.length === 0) return null;
1273
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
1274
+ className: config_manager_module_css_default.groupCard,
1275
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1276
+ className: config_manager_module_css_default.groupHeader,
1277
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1278
+ className: config_manager_module_css_default.groupLabel,
1279
+ children: group.label
1280
+ }), group.note !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1281
+ className: config_manager_module_css_default.groupNote,
1282
+ children: group.note
1283
+ })]
1284
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1285
+ className: config_manager_module_css_default.groupItems,
1286
+ children: categories.map((cat) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Checkbox, {
1287
+ checked: selection.includes(cat.id),
1288
+ onChange: (checked) => {
1289
+ toggleSection(cat.id, checked);
1290
+ },
1291
+ label: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1292
+ className: config_manager_module_css_default.categoryItem,
1293
+ children: [
1294
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1295
+ className: config_manager_module_css_default.categoryName,
1296
+ children: cat.label
1297
+ }),
1298
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1299
+ className: config_manager_module_css_default.categoryDesc,
1300
+ children: cat.description
1301
+ }),
1302
+ cat.portability !== "portable" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
1303
+ kind: cat.portability === "deviceSpecific" ? "warn" : "info",
1304
+ children: cat.portability
1305
+ }),
1306
+ cat.sensitive === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
1307
+ kind: "warn",
1308
+ children: "secret"
1309
+ })
1310
+ ]
1311
+ })
1312
+ }, cat.id))
1313
+ })]
1314
+ }, group.id);
1315
+ })
1316
+ }),
1317
+ customWarnings.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Banner, {
1318
+ kind: "warn",
1319
+ children: [t("export.selectionWarnings"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1320
+ className: config_manager_module_css_default.warnList,
1321
+ children: customWarnings.map((w, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: w }, i))
1322
+ })]
1323
+ }),
1324
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
1325
+ className: config_manager_module_css_default.optionsCard,
1326
+ children: [
1327
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Checkbox, {
1328
+ checked: includeSecrets,
1329
+ onChange: setIncludeSecrets,
1330
+ label: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1331
+ className: config_manager_module_css_default.categoryName,
1332
+ children: t("export.includeSecrets")
1333
+ })
1334
+ }),
1335
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1336
+ className: config_manager_module_css_default.hint,
1337
+ children: t("export.includeSecretsHint")
1338
+ }),
1339
+ includeSecrets && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1340
+ className: config_manager_module_css_default.secretFields,
1341
+ children: [
1342
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1343
+ className: config_manager_module_css_default.field,
1344
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1345
+ className: config_manager_module_css_default.fieldLabel,
1346
+ children: t("export.password")
1347
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1348
+ type: "password",
1349
+ className: config_manager_module_css_default.input,
1350
+ value: password,
1351
+ onChange: (e) => {
1352
+ setPassword(e.target.value);
1353
+ }
1354
+ })]
1355
+ }),
1356
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1357
+ className: config_manager_module_css_default.field,
1358
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1359
+ className: config_manager_module_css_default.fieldLabel,
1360
+ children: t("export.passwordConfirm")
1361
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1362
+ type: "password",
1363
+ className: config_manager_module_css_default.input,
1364
+ value: passwordConfirm,
1365
+ onChange: (e) => {
1366
+ setPasswordConfirm(e.target.value);
1367
+ }
1368
+ })]
1369
+ }),
1370
+ password !== "" && password !== passwordConfirm && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1371
+ className: config_manager_module_css_default.formError,
1372
+ children: t("export.passwordMismatch")
1373
+ }),
1374
+ password === "" && includeSecrets && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1375
+ className: config_manager_module_css_default.formError,
1376
+ children: t("export.passwordRequired")
1377
+ })
1378
+ ]
1379
+ })
1380
+ ]
1381
+ }),
1382
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1383
+ className: config_manager_module_css_default.actionRow,
1384
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
1385
+ variant: "primary",
1386
+ disabled: running || passwordInvalid,
1387
+ onClick: () => {
1388
+ runExport();
1389
+ },
1390
+ children: running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: t("export.running") }) : t("export.run")
1391
+ })
1392
+ }),
1393
+ running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProgressBar, {
1394
+ event: progress,
1395
+ active: true
1396
+ }),
1397
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, {
1398
+ error,
1399
+ onRetry: () => {
1400
+ runExport();
1401
+ },
1402
+ retrying: running
1403
+ }),
1404
+ result !== null && !running && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReportView, {
1405
+ kind: "export",
1406
+ exportReport: result.report,
1407
+ onDownload: () => {
1408
+ download();
1409
+ }
1410
+ }), downloaded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Banner, {
1411
+ kind: "ok",
1412
+ children: ["Saved: ", result.report.file.name]
1413
+ })] })
1414
+ ]
1415
+ });
1416
+ }
1417
+ //#endregion
1418
+ //#region src/ui/import-wizard.ts
1419
+ var ImportWizard = class {
1420
+ port;
1421
+ onProgress;
1422
+ tracker;
1423
+ step = "select";
1424
+ zipPath = null;
1425
+ analysis = null;
1426
+ plan = null;
1427
+ result = null;
1428
+ rollbackOnError;
1429
+ errors = [];
1430
+ decisions = {
1431
+ strategy: "merge",
1432
+ resolutions: {},
1433
+ pathMappings: []
1434
+ };
1435
+ secretInputs = {};
1436
+ constructor(opts) {
1437
+ this.port = opts.port;
1438
+ this.onProgress = opts.onProgress;
1439
+ this.tracker = new ProgressTracker(IMPORT_STAGES, this.onProgress);
1440
+ this.rollbackOnError = opts.defaultRollbackOnError ?? true;
1441
+ }
1442
+ /** 当前状态快照(React 绑定 / 测试断言用) */
1443
+ snapshot() {
1444
+ return {
1445
+ step: this.step,
1446
+ zipPath: this.zipPath,
1447
+ analysis: this.analysis,
1448
+ plan: this.plan,
1449
+ result: this.result,
1450
+ rollbackOnError: this.rollbackOnError,
1451
+ errors: [...this.errors]
1452
+ };
1453
+ }
1454
+ get currentStep() {
1455
+ return this.step;
1456
+ }
1457
+ /** 步骤 1-2:选 ZIP → Analyzing → Compatibility(analyzeImport 零写入) */
1458
+ async selectZip(path) {
1459
+ this.zipPath = path;
1460
+ this.step = "analyzing";
1461
+ this.tracker.emit("validating");
1462
+ this.errors = [];
1463
+ try {
1464
+ this.analysis = await this.port.analyzeImport(path);
1465
+ this.tracker.emit("checking-compatibility");
1466
+ if (!this.analysis.valid) {
1467
+ this.errors.push(...this.analysis.errors.map((e) => formatActionableError(toActionableError(new Error(e)))));
1468
+ throw new Error(this.analysis.errors.join("; ") || "备份分析失败");
1469
+ }
1470
+ this.step = "compatibility";
1471
+ return this.analysis;
1472
+ } catch (err) {
1473
+ if (this.errors.length === 0) this.errors.push(formatActionableError(toActionableError(err)));
1474
+ throw err;
1475
+ }
1476
+ }
1477
+ /** 步骤 3→4:用户确认兼容性后进入 Preview(Dry Run:用当前决策生成计划摘要,零写入) */
1478
+ async confirmCompatibility() {
1479
+ if (this.analysis === null || this.zipPath === null) throw new Error("尚未完成分析,请先选择备份文件");
1480
+ this.plan = await this.port.createImportPlan(this.zipPath, this.decisions);
1481
+ this.step = "preview";
1482
+ return this.plan;
1483
+ }
1484
+ /** 更新全局冲突策略(merge/replace/skipExisting,规范 §11) */
1485
+ setStrategy(strategy) {
1486
+ this.decisions = {
1487
+ ...this.decisions,
1488
+ strategy
1489
+ };
1490
+ }
1491
+ /** 设置逐项冲突决策(keepCurrent/useImported/review) */
1492
+ setResolutions(resolutions) {
1493
+ this.decisions = {
1494
+ ...this.decisions,
1495
+ resolutions
1496
+ };
1497
+ }
1498
+ /** 设置路径映射(§12) */
1499
+ setPathMappings(mappings) {
1500
+ this.decisions = {
1501
+ ...this.decisions,
1502
+ pathMappings: mappings
1503
+ };
1504
+ }
1505
+ /** 设置秘密补录值(仅内存,绝不持久化) */
1506
+ setSecretInputs(inputs) {
1507
+ this.secretInputs = inputs;
1508
+ }
1509
+ /** Preview 摘要(规范 §10 数值化;基于当前 plan 与 analysis) */
1510
+ previewSummary() {
1511
+ const items = this.plan?.items ?? [];
1512
+ const analysis = this.analysis;
1513
+ const count = (kinds) => items.filter((i) => kinds.includes(i.kind)).length;
1514
+ return {
1515
+ willChange: count([
1516
+ "Create",
1517
+ "Update",
1518
+ "Install",
1519
+ "Conflict"
1520
+ ]),
1521
+ unchanged: count(["Skip"]),
1522
+ settingsUpdates: count(["Create", "Update"]),
1523
+ pluginsInstalled: analysis?.pluginSummary.installed ?? 0,
1524
+ pluginsToInstall: count(["Install"]),
1525
+ mcpAdds: items.filter((i) => i.adapter === "mcp" && i.kind === "Create").length,
1526
+ prompts: items.filter((i) => i.adapter === "prompts" && i.kind !== "Skip").length,
1527
+ pathMappingsNeeded: analysis?.pathIssues.length ?? 0,
1528
+ secretsNeeded: this.plan?.missingSecrets.length ?? analysis?.secretCount ?? 0,
1529
+ conflicts: count(["Conflict"]),
1530
+ needsRestart: this.plan?.needsRestart ?? false
1531
+ };
1532
+ }
1533
+ /** 冲突项(供 ConflictCollector 使用) */
1534
+ conflictItems() {
1535
+ return (this.plan?.items ?? []).filter((i) => i.kind === "Conflict");
1536
+ }
1537
+ /** 设置回滚策略(场景 E 选择;执行前调用) */
1538
+ setRollbackOnError(enable) {
1539
+ this.rollbackOnError = enable;
1540
+ }
1541
+ /**
1542
+ * 步骤 10-14:确认导入 → 快照 → 执行 → 校验 → 结果。
1543
+ * 用最终决策重建计划(与预览一致),显式传 rollbackOnError。
1544
+ */
1545
+ async execute(opts) {
1546
+ if (this.zipPath === null || this.analysis === null) throw new Error("尚未完成分析,请先选择备份文件");
1547
+ if (opts.confirm !== true) throw new Error("导入未确认:必须确认后才允许修改任何数据");
1548
+ const rollbackOnError = opts.rollbackOnError ?? this.rollbackOnError;
1549
+ this.step = "importing";
1550
+ this.tracker.emit("creating-snapshot");
1551
+ try {
1552
+ this.plan = await this.port.createImportPlan(this.zipPath, this.decisions);
1553
+ this.tracker.emit("restoring-settings");
1554
+ this.tracker.emit("restoring-plugins");
1555
+ this.tracker.emit("restoring-mcp");
1556
+ this.tracker.emit("validating-config");
1557
+ this.result = await this.port.executeImportPlan(this.zipPath, this.plan, {
1558
+ confirm: true,
1559
+ secretInputs: this.secretInputs,
1560
+ rollbackOnError
1561
+ });
1562
+ if (!this.result.ok && this.result.rollback) this.tracker.emit("rolling-back");
1563
+ this.tracker.emit("done");
1564
+ this.step = "result";
1565
+ return this.result;
1566
+ } catch (err) {
1567
+ this.errors.push(formatActionableError(toActionableError(err)));
1568
+ throw err;
1569
+ }
1570
+ }
1571
+ /** 重置向导(可复用实例开始新导入) */
1572
+ reset() {
1573
+ this.step = "select";
1574
+ this.zipPath = null;
1575
+ this.analysis = null;
1576
+ this.plan = null;
1577
+ this.result = null;
1578
+ this.errors = [];
1579
+ this.decisions = {
1580
+ strategy: "merge",
1581
+ resolutions: {},
1582
+ pathMappings: []
1583
+ };
1584
+ this.secretInputs = {};
1585
+ }
1586
+ };
1587
+ //#endregion
1588
+ //#region src/ui/conflict-view.ts
1589
+ var ConflictCollector = class {
1590
+ plan;
1591
+ decisions = /* @__PURE__ */ new Map();
1592
+ constructor(plan) {
1593
+ this.plan = plan;
1594
+ }
1595
+ /** 需要用户决策的冲突项(kind === 'Conflict') */
1596
+ get conflicts() {
1597
+ return this.plan.items.filter((i) => i.kind === "Conflict");
1598
+ }
1599
+ /** 设置某项决策;未出现在冲突列表中的 id 忽略并返回 false */
1600
+ resolve(itemId, resolution) {
1601
+ if (!this.conflicts.some((i) => i.id === itemId)) return false;
1602
+ this.decisions.set(itemId, resolution);
1603
+ return true;
1604
+ }
1605
+ /** 单项当前决策(未决策返回 null) */
1606
+ decisionOf(itemId) {
1607
+ return this.decisions.get(itemId) ?? null;
1608
+ }
1609
+ /** 仍未解决(review 或未决策)的冲突项 */
1610
+ unresolved() {
1611
+ return this.conflicts.filter((i) => {
1612
+ const d = this.decisions.get(i.id);
1613
+ return d === void 0 || d === "review";
1614
+ });
1615
+ }
1616
+ get hasUnresolved() {
1617
+ return this.unresolved().length > 0;
1618
+ }
1619
+ /** 转 core 决策表(供 createImportPlan / executeImportPlan 使用) */
1620
+ toResolutions() {
1621
+ const out = {};
1622
+ for (const [id, res] of this.decisions) out[id] = res;
1623
+ return out;
1624
+ }
1625
+ /** 构造视图项列表(React 绑定用):含当前/导入摘要占位 */
1626
+ viewItems() {
1627
+ return this.conflicts.map((item) => ({
1628
+ item,
1629
+ currentSummary: item.detail?.split("\n")[0],
1630
+ importedSummary: void 0,
1631
+ resolution: this.decisionOf(item.id)
1632
+ }));
1633
+ }
1634
+ };
1635
+ //#endregion
1636
+ //#region src/client/import/ConflictList.tsx
1637
+ /**
1638
+ * 冲突决策列表(规范 §11,绑 src/ui/conflict-view.ts 的 ConflictCollector)。
1639
+ *
1640
+ * 渲染 plan 中 kind === 'Conflict' 的项,用户逐项选择
1641
+ * Keep Current / Use Imported / Review;决策实时写入 collector,
1642
+ * 完成时父组件调用 collector.toResolutions() → wizard.setResolutions()。
1643
+ */
1644
+ const RESOLUTION_OPTIONS = [
1645
+ {
1646
+ value: "keepCurrent",
1647
+ key: "import.conflicts.keepCurrent"
1648
+ },
1649
+ {
1650
+ value: "useImported",
1651
+ key: "import.conflicts.useImported"
1652
+ },
1653
+ {
1654
+ value: "review",
1655
+ key: "import.conflicts.review"
1656
+ }
1657
+ ];
1658
+ /** 冲突项决策列表 */
1659
+ function ConflictList({ collector, t, onChanged }) {
1660
+ const [tick, setTick] = (0, react.useState)(0);
1661
+ const items = collector.viewItems();
1662
+ const unresolved = collector.unresolved().length;
1663
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1664
+ className: config_manager_module_css_default.conflictList,
1665
+ children: [
1666
+ unresolved > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
1667
+ kind: "warn",
1668
+ children: t("import.conflicts.unresolved", { count: String(unresolved) })
1669
+ }),
1670
+ items.map((view) => {
1671
+ const item = view.item;
1672
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1673
+ className: config_manager_module_css_default.conflictItem,
1674
+ children: [
1675
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1676
+ className: config_manager_module_css_default.conflictHead,
1677
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1678
+ className: config_manager_module_css_default.conflictId,
1679
+ children: [
1680
+ item.adapter,
1681
+ ": ",
1682
+ item.description
1683
+ ]
1684
+ }), item.severity === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1685
+ className: config_manager_module_css_default.severityError,
1686
+ children: "error"
1687
+ })]
1688
+ }),
1689
+ item.detail !== void 0 && item.detail !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1690
+ className: config_manager_module_css_default.conflictDetail,
1691
+ children: item.detail
1692
+ }),
1693
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1694
+ className: config_manager_module_css_default.conflictOptions,
1695
+ children: RESOLUTION_OPTIONS.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1696
+ className: config_manager_module_css_default.radioLabel,
1697
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1698
+ type: "radio",
1699
+ name: `conflict-${item.id}`,
1700
+ checked: view.resolution === opt.value,
1701
+ onChange: () => {
1702
+ collector.resolve(item.id, opt.value);
1703
+ setTick((v) => v + 1);
1704
+ onChanged();
1705
+ }
1706
+ }), t(opt.key)]
1707
+ }, opt.value))
1708
+ })
1709
+ ]
1710
+ }, item.id);
1711
+ }),
1712
+ items.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1713
+ className: config_manager_module_css_default.empty,
1714
+ children: "No conflicts"
1715
+ }),
1716
+ void 0
1717
+ ]
1718
+ });
1719
+ }
1720
+ //#endregion
1721
+ //#region src/client/import/PathMappingForm.tsx
1722
+ /**
1723
+ * 路径映射表单(规范 §12,绑 src/ui/types.ts 的 PathMappingDraft 形状)。
1724
+ *
1725
+ * 注意:src/ui/path-mapping.ts 的 PathMappingEditor 依赖 utils/paths.ts 的
1726
+ * applyPrefixMappings(node:path)——浏览器 bundle 不可用,故此处做轻量等价实现:
1727
+ * 输入输出形状与 core 的 PathMapping 完全一致(oldPrefix/newPrefix/appliesTo),
1728
+ * 实际前缀替换由 Host 侧 core 在 createImportPlan 阶段执行,本组件只负责收集用户输入。
1729
+ */
1730
+ /**
1731
+ * 路径映射表单:每条 PathIssue 一行(原路径 → 新路径输入框),
1732
+ * 留空 = 该路径不映射(unresolved);填写 = 输出 core PathMapping[]。
1733
+ */
1734
+ function PathMappingForm({ issues, initial, t, onChange }) {
1735
+ const [drafts, setDrafts] = (0, react.useState)(() => {
1736
+ const out = {};
1737
+ for (const m of initial ?? []) out[m.oldPrefix] = m.newPrefix;
1738
+ return out;
1739
+ });
1740
+ const setNewPrefix = (oldPrefix, value) => {
1741
+ const next = {
1742
+ ...drafts,
1743
+ [oldPrefix]: value
1744
+ };
1745
+ setDrafts(next);
1746
+ onChange(Object.entries(next).filter(([, v]) => v !== "").map(([oldPrefix, newPrefix]) => ({
1747
+ oldPrefix,
1748
+ newPrefix,
1749
+ appliesTo: []
1750
+ })));
1751
+ };
1752
+ const unresolved = (0, react.useMemo)(() => {
1753
+ return issues.filter((issue) => (drafts[issue.value] ?? "") === "");
1754
+ }, [issues, drafts]);
1755
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1756
+ className: config_manager_module_css_default.pathMappingList,
1757
+ children: [
1758
+ unresolved.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
1759
+ kind: "warn",
1760
+ children: t("import.paths.unresolved", { count: String(unresolved.length) })
1761
+ }),
1762
+ issues.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1763
+ className: config_manager_module_css_default.empty,
1764
+ children: "No paths to map"
1765
+ }),
1766
+ issues.map((issue) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1767
+ className: config_manager_module_css_default.pathRow,
1768
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1769
+ className: config_manager_module_css_default.pathOld,
1770
+ children: [
1771
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1772
+ className: config_manager_module_css_default.fieldLabel,
1773
+ children: t("import.paths.old")
1774
+ }),
1775
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1776
+ className: config_manager_module_css_default.pathValue,
1777
+ children: issue.value
1778
+ }),
1779
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1780
+ className: config_manager_module_css_default.pathIssueKind,
1781
+ children: issue.kind
1782
+ })
1783
+ ]
1784
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1785
+ className: config_manager_module_css_default.pathNew,
1786
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1787
+ className: config_manager_module_css_default.fieldLabel,
1788
+ children: t("import.paths.new")
1789
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1790
+ className: config_manager_module_css_default.input,
1791
+ value: drafts[issue.value] ?? "",
1792
+ placeholder: issue.mappedTo ?? "",
1793
+ onChange: (e) => {
1794
+ setNewPrefix(issue.value, e.target.value);
1795
+ }
1796
+ })]
1797
+ })]
1798
+ }, issue.value))
1799
+ ]
1800
+ });
1801
+ }
1802
+ //#endregion
1803
+ //#region src/client/import/ImportWizardView.tsx
1804
+ /**
1805
+ * 导入九步向导(规范 §9 / §10 / §28,绑 src/ui/import-wizard.ts 的 ImportWizard 控制器)。
1806
+ *
1807
+ * 步骤(对齐 ui/types.ts 的 ImportStep):
1808
+ * Select ZIP → Analyzing → Compatibility → Preview
1809
+ * → Resolve Conflicts(若有)→ Path Mapping(若有)→ Secrets 补录(若有)
1810
+ * → Confirm → Importing → Result
1811
+ *
1812
+ * 安全/正确性约束(来自 ImportWizard 与 core):
1813
+ * - analyzeImport / createImportPlan 零写入(Dry Run 复用);
1814
+ * - executeImportPlan 必须 confirm=true(core 安全阀);
1815
+ * - 秘密补录值仅内存(secretInputs),经 HTTPS 请求体传给 Host,绝不落日志/落盘;
1816
+ * - 默认整体回滚(rollbackOnError=true),用户在 Confirm 步可切换。
1817
+ *
1818
+ * 数据流:本地文件 → api.upload → zipPath → wizard.selectZip/confirmCompatibility/
1819
+ * setResolutions/setPathMappings/setSecretInputs → wizard.execute。
1820
+ * 中间阶段(conflicts/path-mapping/secrets)是 UI 层流程页,wizard 的 decisions 由
1821
+ * 对应组件收集后写入。
1822
+ */
1823
+ const SCORE_LABEL = {
1824
+ excellent: "Excellent",
1825
+ good: "Good",
1826
+ partial: "Partial",
1827
+ unsupported: "Unsupported"
1828
+ };
1829
+ /** 密钥补录表单(仅内存收集,值不外泄) */
1830
+ function SecretsForm({ missing, t, onChange }) {
1831
+ const [inputs, setInputs] = (0, react.useState)({});
1832
+ const setRef = (ref, value) => {
1833
+ const next = {
1834
+ ...inputs,
1835
+ [ref]: value
1836
+ };
1837
+ setInputs(next);
1838
+ onChange(next);
1839
+ };
1840
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1841
+ className: config_manager_module_css_default.secretsList,
1842
+ children: [
1843
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1844
+ className: config_manager_module_css_default.hint,
1845
+ children: t("import.secrets.hint")
1846
+ }),
1847
+ missing.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Empty, { children: "No secrets required" }),
1848
+ missing.map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1849
+ className: config_manager_module_css_default.field,
1850
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1851
+ className: config_manager_module_css_default.fieldLabel,
1852
+ children: [
1853
+ s.ref,
1854
+ " ",
1855
+ s.required ? t("import.secrets.required") : t("import.secrets.optional")
1856
+ ]
1857
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1858
+ type: "password",
1859
+ className: config_manager_module_css_default.input,
1860
+ autoComplete: "off",
1861
+ value: inputs[s.ref] ?? "",
1862
+ onChange: (e) => {
1863
+ setRef(s.ref, e.target.value);
1864
+ }
1865
+ })]
1866
+ }, s.ref))
1867
+ ]
1868
+ });
1869
+ }
1870
+ /**
1871
+ * 导入向导主视图。
1872
+ */
1873
+ function ImportWizardView({ api, t }) {
1874
+ const [wizard] = (0, react.useState)(() => new ImportWizard({
1875
+ port: api,
1876
+ onProgress: (event) => {
1877
+ setProgress(event);
1878
+ },
1879
+ defaultRollbackOnError: true
1880
+ }));
1881
+ const [, setTick] = (0, react.useState)(0);
1882
+ const refresh = () => {
1883
+ setTick((v) => v + 1);
1884
+ };
1885
+ const [progress, setProgress] = (0, react.useState)(null);
1886
+ const [error, setError] = (0, react.useState)(null);
1887
+ const [phase, setPhase] = (0, react.useState)("preview");
1888
+ const [uploading, setUploading] = (0, react.useState)(false);
1889
+ const [rollbackOnError, setRollbackOnError] = (0, react.useState)(true);
1890
+ const [conflictCollector, setConflictCollector] = (0, react.useState)(null);
1891
+ const [pathMappings, setPathMappings] = (0, react.useState)([]);
1892
+ const [secretInputs, setSecretInputs] = (0, react.useState)({});
1893
+ const fileInput = (0, react.useRef)(null);
1894
+ const snapshot = wizard.snapshot();
1895
+ const step = snapshot.step;
1896
+ const hasConflicts = (snapshot.plan?.items ?? []).some((i) => i.kind === "Conflict");
1897
+ const hasPathIssues = (snapshot.analysis?.pathIssues.length ?? 0) > 0;
1898
+ const hasSecrets = (snapshot.plan?.missingSecrets.length ?? 0) > 0;
1899
+ /** 从某阶段完成后进入的下一个阶段(跳过空阶段;from 之前的阶段不再进入) */
1900
+ const nextPhase = (from) => {
1901
+ if (from !== "conflicts" && hasConflicts) return "conflicts";
1902
+ if (from !== "path-mapping" && hasPathIssues) return "path-mapping";
1903
+ if (from !== "secrets" && hasSecrets) return "secrets";
1904
+ return "confirm";
1905
+ };
1906
+ /** 选择并上传 ZIP → wizard.selectZip(analyzing → compatibility) */
1907
+ const onPickFile = async (file) => {
1908
+ if (file === void 0) return;
1909
+ setUploading(true);
1910
+ setError(null);
1911
+ try {
1912
+ const uploaded = await api.upload(file);
1913
+ await wizard.selectZip(uploaded.zipPath);
1914
+ refresh();
1915
+ } catch (err) {
1916
+ setError(err);
1917
+ refresh();
1918
+ } finally {
1919
+ setUploading(false);
1920
+ }
1921
+ };
1922
+ /** Compatibility → Preview */
1923
+ const goPreview = async () => {
1924
+ setError(null);
1925
+ try {
1926
+ await wizard.confirmCompatibility();
1927
+ refresh();
1928
+ } catch (err) {
1929
+ setError(err);
1930
+ refresh();
1931
+ }
1932
+ };
1933
+ /** 进入 conflicts 阶段(先创建 collector) */
1934
+ const enterConflicts = () => {
1935
+ const plan = snapshot.plan;
1936
+ if (plan !== null && conflictCollector === null) setConflictCollector(new ConflictCollector(plan));
1937
+ setPhase("conflicts");
1938
+ };
1939
+ /** Conflicts 完成:写入决策 → 下一阶段 */
1940
+ const finishConflicts = () => {
1941
+ if (conflictCollector !== null) wizard.setResolutions(conflictCollector.toResolutions());
1942
+ setPhase(nextPhase("conflicts"));
1943
+ };
1944
+ /** Path Mapping 完成:写入映射 → 下一阶段 */
1945
+ const finishPathMapping = () => {
1946
+ wizard.setPathMappings(pathMappings);
1947
+ setPhase(nextPhase("path-mapping"));
1948
+ };
1949
+ /** Secrets 完成:写入补录值 → Confirm */
1950
+ const finishSecrets = () => {
1951
+ wizard.setSecretInputs(secretInputs);
1952
+ setPhase("confirm");
1953
+ };
1954
+ /** Confirm 执行:confirm=true(安全阀)+ 用户回滚策略 */
1955
+ const execute = async () => {
1956
+ setError(null);
1957
+ try {
1958
+ await wizard.execute({
1959
+ confirm: true,
1960
+ rollbackOnError
1961
+ });
1962
+ refresh();
1963
+ } catch (err) {
1964
+ setError(err);
1965
+ refresh();
1966
+ }
1967
+ };
1968
+ /** 重置向导(重新导入) */
1969
+ const resetWizard = () => {
1970
+ wizard.reset();
1971
+ setError(null);
1972
+ setPhase("preview");
1973
+ setConflictCollector(null);
1974
+ setPathMappings([]);
1975
+ setSecretInputs({});
1976
+ refresh();
1977
+ };
1978
+ if (step === "select") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1979
+ className: config_manager_module_css_default.viewBody,
1980
+ children: [
1981
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionTitle, {
1982
+ title: t("import.select.title"),
1983
+ subtitle: t("import.select.hint")
1984
+ }),
1985
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1986
+ ref: fileInput,
1987
+ type: "file",
1988
+ accept: ".zip,application/zip",
1989
+ className: config_manager_module_css_default.hiddenFile,
1990
+ onChange: (e) => {
1991
+ onPickFile(e.target.files?.[0]);
1992
+ }
1993
+ }),
1994
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1995
+ className: config_manager_module_css_default.actionRow,
1996
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
1997
+ variant: "primary",
1998
+ disabled: uploading,
1999
+ onClick: () => {
2000
+ fileInput.current?.click();
2001
+ },
2002
+ children: uploading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: t("import.analyzing") }) : t("import.select.browse")
2003
+ })
2004
+ }),
2005
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, {
2006
+ error,
2007
+ onRetry: resetWizard
2008
+ })
2009
+ ]
2010
+ });
2011
+ if (step === "analyzing") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2012
+ className: config_manager_module_css_default.viewBody,
2013
+ children: [
2014
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProgressBar, {
2015
+ event: progress,
2016
+ active: true
2017
+ }),
2018
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, {
2019
+ error,
2020
+ onRetry: resetWizard
2021
+ }),
2022
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorList, { errors: snapshot.errors })
2023
+ ]
2024
+ });
2025
+ if (step === "compatibility") {
2026
+ const analysis = snapshot.analysis;
2027
+ if (analysis === null) return null;
2028
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2029
+ className: config_manager_module_css_default.viewBody,
2030
+ children: [
2031
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionTitle, { title: t("import.compatibility.title") }),
2032
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2033
+ className: config_manager_module_css_default.statRow,
2034
+ children: [
2035
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
2036
+ kind: analysis.compatibility === "unsupported" ? "error" : analysis.compatibility === "partial" ? "warn" : "ok",
2037
+ children: t("import.compatibility.score", { score: SCORE_LABEL[analysis.compatibility] ?? analysis.compatibility })
2038
+ }),
2039
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Badge, {
2040
+ kind: "info",
2041
+ children: [analysis.sectionsInZip.length, " sections"]
2042
+ }),
2043
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Badge, {
2044
+ kind: "info",
2045
+ children: [
2046
+ "plugins: ",
2047
+ analysis.pluginSummary.installed,
2048
+ "✓ / ",
2049
+ analysis.pluginSummary.toInstall,
2050
+ "✗"
2051
+ ]
2052
+ }),
2053
+ analysis.pathIssues.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Badge, {
2054
+ kind: "warn",
2055
+ children: [analysis.pathIssues.length, " paths"]
2056
+ }),
2057
+ analysis.secretCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Badge, {
2058
+ kind: "warn",
2059
+ children: [analysis.secretCount, " secrets"]
2060
+ })
2061
+ ]
2062
+ }),
2063
+ analysis.warnings.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
2064
+ kind: "warn",
2065
+ children: analysis.warnings.map((w, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: w }, i))
2066
+ }),
2067
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, {
2068
+ error,
2069
+ onRetry: () => {
2070
+ goPreview();
2071
+ }
2072
+ }),
2073
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2074
+ className: config_manager_module_css_default.actionRow,
2075
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2076
+ variant: "primary",
2077
+ onClick: () => {
2078
+ goPreview();
2079
+ },
2080
+ children: t("common.next")
2081
+ })
2082
+ })
2083
+ ]
2084
+ });
2085
+ }
2086
+ if (step === "preview" && phase === "preview") {
2087
+ const summary = wizard.previewSummary();
2088
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2089
+ className: config_manager_module_css_default.viewBody,
2090
+ children: [
2091
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionTitle, { title: t("import.preview.title") }),
2092
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2093
+ className: config_manager_module_css_default.statRow,
2094
+ children: [
2095
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
2096
+ kind: summary.willChange > 0 ? "info" : "ok",
2097
+ children: t("import.preview.willChange", { count: String(summary.willChange) })
2098
+ }),
2099
+ summary.unchanged > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
2100
+ kind: "ok",
2101
+ children: t("import.preview.unchanged", { count: String(summary.unchanged) })
2102
+ }),
2103
+ summary.settingsUpdates > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
2104
+ kind: "info",
2105
+ children: t("import.preview.settings", { count: String(summary.settingsUpdates) })
2106
+ }),
2107
+ summary.pluginsToInstall > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
2108
+ kind: "info",
2109
+ children: t("import.preview.plugins", { count: String(summary.pluginsToInstall) })
2110
+ }),
2111
+ summary.mcpAdds > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
2112
+ kind: "info",
2113
+ children: t("import.preview.mcp", { count: String(summary.mcpAdds) })
2114
+ }),
2115
+ summary.pathMappingsNeeded > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
2116
+ kind: "warn",
2117
+ children: t("import.preview.paths", { count: String(summary.pathMappingsNeeded) })
2118
+ }),
2119
+ summary.secretsNeeded > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
2120
+ kind: "warn",
2121
+ children: t("import.preview.secrets", { count: String(summary.secretsNeeded) })
2122
+ }),
2123
+ summary.conflicts > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
2124
+ kind: "error",
2125
+ children: t("import.preview.conflicts", { count: String(summary.conflicts) })
2126
+ })
2127
+ ]
2128
+ }),
2129
+ summary.needsRestart && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
2130
+ kind: "warn",
2131
+ children: t("import.preview.restart")
2132
+ }),
2133
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, {
2134
+ error,
2135
+ onRetry: () => {
2136
+ goPreview();
2137
+ }
2138
+ }),
2139
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2140
+ className: config_manager_module_css_default.actionRow,
2141
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2142
+ variant: "primary",
2143
+ onClick: () => {
2144
+ const next = nextPhase("preview");
2145
+ setPhase(next);
2146
+ if (next === "conflicts") enterConflicts();
2147
+ },
2148
+ children: t("common.next")
2149
+ })
2150
+ })
2151
+ ]
2152
+ });
2153
+ }
2154
+ if (phase === "conflicts") {
2155
+ if (conflictCollector === null || snapshot.plan === null) return null;
2156
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2157
+ className: config_manager_module_css_default.viewBody,
2158
+ children: [
2159
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionTitle, {
2160
+ title: t("import.conflicts.title"),
2161
+ subtitle: t("import.conflicts.hint")
2162
+ }),
2163
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConflictList, {
2164
+ collector: conflictCollector,
2165
+ t,
2166
+ onChanged: refresh
2167
+ }),
2168
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2169
+ className: config_manager_module_css_default.actionRow,
2170
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2171
+ variant: "ghost",
2172
+ onClick: () => {
2173
+ setPhase("preview");
2174
+ },
2175
+ children: t("common.back")
2176
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2177
+ variant: "primary",
2178
+ disabled: conflictCollector.hasUnresolved,
2179
+ onClick: finishConflicts,
2180
+ children: t("common.next")
2181
+ })]
2182
+ })
2183
+ ]
2184
+ });
2185
+ }
2186
+ if (phase === "path-mapping") {
2187
+ const issues = snapshot.analysis?.pathIssues ?? [];
2188
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2189
+ className: config_manager_module_css_default.viewBody,
2190
+ children: [
2191
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionTitle, {
2192
+ title: t("import.paths.title"),
2193
+ subtitle: t("import.paths.hint")
2194
+ }),
2195
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PathMappingForm, {
2196
+ issues,
2197
+ initial: pathMappings,
2198
+ t,
2199
+ onChange: setPathMappings
2200
+ }),
2201
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2202
+ className: config_manager_module_css_default.actionRow,
2203
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2204
+ variant: "ghost",
2205
+ onClick: () => {
2206
+ setPhase("preview");
2207
+ },
2208
+ children: t("common.back")
2209
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2210
+ variant: "primary",
2211
+ onClick: finishPathMapping,
2212
+ children: t("common.next")
2213
+ })]
2214
+ })
2215
+ ]
2216
+ });
2217
+ }
2218
+ if (phase === "secrets") {
2219
+ const missing = snapshot.plan?.missingSecrets ?? [];
2220
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2221
+ className: config_manager_module_css_default.viewBody,
2222
+ children: [
2223
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionTitle, { title: t("import.secrets.title") }),
2224
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SecretsForm, {
2225
+ missing,
2226
+ t,
2227
+ onChange: setSecretInputs
2228
+ }),
2229
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2230
+ className: config_manager_module_css_default.actionRow,
2231
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2232
+ variant: "ghost",
2233
+ onClick: () => {
2234
+ setPhase("preview");
2235
+ },
2236
+ children: t("common.back")
2237
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2238
+ variant: "primary",
2239
+ onClick: finishSecrets,
2240
+ children: t("common.next")
2241
+ })]
2242
+ })
2243
+ ]
2244
+ });
2245
+ }
2246
+ if (phase === "confirm") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2247
+ className: config_manager_module_css_default.viewBody,
2248
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
2249
+ className: config_manager_module_css_default.optionsCard,
2250
+ children: [
2251
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
2252
+ kind: "info",
2253
+ children: t("import.confirm.warning")
2254
+ }),
2255
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Checkbox, {
2256
+ checked: rollbackOnError,
2257
+ onChange: setRollbackOnError,
2258
+ label: t("import.rollbackOnError")
2259
+ }),
2260
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2261
+ className: config_manager_module_css_default.actionRow,
2262
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2263
+ variant: "ghost",
2264
+ onClick: () => {
2265
+ setPhase("preview");
2266
+ },
2267
+ children: t("common.back")
2268
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2269
+ variant: "primary",
2270
+ onClick: () => {
2271
+ execute();
2272
+ },
2273
+ children: t("import.confirm.execute")
2274
+ })]
2275
+ })
2276
+ ]
2277
+ }), error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, {
2278
+ error,
2279
+ onRetry: () => {
2280
+ execute();
2281
+ }
2282
+ })]
2283
+ });
2284
+ if (step === "importing") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2285
+ className: config_manager_module_css_default.viewBody,
2286
+ children: [
2287
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProgressBar, {
2288
+ event: progress,
2289
+ active: true
2290
+ }),
2291
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2292
+ className: config_manager_module_css_default.hint,
2293
+ children: t("import.importing")
2294
+ }),
2295
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, { error }),
2296
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorList, { errors: snapshot.errors })
2297
+ ]
2298
+ });
2299
+ if (step === "result") {
2300
+ const result = snapshot.result;
2301
+ if (result === null) return null;
2302
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2303
+ className: config_manager_module_css_default.viewBody,
2304
+ children: [
2305
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionTitle, { title: t("report.import.title") }),
2306
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReportView, {
2307
+ kind: "import",
2308
+ importResult: result,
2309
+ onAction: (action) => {
2310
+ if (action === "done") resetWizard();
2311
+ }
2312
+ }),
2313
+ result.needsRestart && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
2314
+ kind: "warn",
2315
+ children: t("report.needsRestart")
2316
+ }),
2317
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, { error }),
2318
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2319
+ className: config_manager_module_css_default.actionRow,
2320
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2321
+ variant: "ghost",
2322
+ onClick: resetWizard,
2323
+ children: t("common.done")
2324
+ })
2325
+ })
2326
+ ]
2327
+ });
2328
+ }
2329
+ return null;
2330
+ }
2331
+ //#endregion
2332
+ //#region src/client/ConfigManagerSection.tsx
2333
+ /**
2334
+ * Config Manager 设置页(settings.section 入口的主页面容器)。
2335
+ *
2336
+ * owner props 由 settings shell 传入(close);业务面(api)由注册时的
2337
+ * inject face 注入;t 由 locale seat 注入。内部两个主视图:
2338
+ * Export(Quick/Custom)与 Import(九步向导)。
2339
+ */
2340
+ /**
2341
+ * 设置页容器:Export / Import 双视图切换。
2342
+ */
2343
+ function ConfigManagerSection({ close, api, t }) {
2344
+ const [view, setView] = (0, react.useState)("export");
2345
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2346
+ className: config_manager_module_css_default.section,
2347
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2348
+ className: config_manager_module_css_default.sectionHeader,
2349
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2350
+ className: config_manager_module_css_default.viewTabs,
2351
+ role: "tablist",
2352
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2353
+ type: "button",
2354
+ role: "tab",
2355
+ "aria-selected": view === "export",
2356
+ "data-active": view === "export" ? "" : void 0,
2357
+ className: config_manager_module_css_default.viewTab,
2358
+ onClick: () => {
2359
+ setView("export");
2360
+ },
2361
+ children: t("view.export")
2362
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2363
+ type: "button",
2364
+ role: "tab",
2365
+ "aria-selected": view === "import",
2366
+ "data-active": view === "import" ? "" : void 0,
2367
+ className: config_manager_module_css_default.viewTab,
2368
+ onClick: () => {
2369
+ setView("import");
2370
+ },
2371
+ children: t("view.import")
2372
+ })]
2373
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2374
+ type: "button",
2375
+ className: config_manager_module_css_default.iconButton,
2376
+ title: t("common.close"),
2377
+ "aria-label": t("common.close"),
2378
+ onClick: close,
2379
+ children: "×"
2380
+ })]
2381
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2382
+ className: config_manager_module_css_default.sectionBody,
2383
+ children: view === "export" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExportView, {
2384
+ api,
2385
+ t
2386
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImportWizardView, {
2387
+ api,
2388
+ t
2389
+ })
2390
+ })]
2391
+ });
2392
+ }
2393
+ //#endregion
2394
+ //#region src/client/locales.ts
2395
+ /**
2396
+ * Config Manager 表面文案:zh 为源语言,en 镜像每个键。
2397
+ * 字典 namespace 由 client/index.ts 注册(declare module 合并进 LocaleNamespaceMap)。
2398
+ * 键集合通过 `ConfigManagerKey` 类型在注册处做编译期校验(缺键/多键即编译错误)。
2399
+ */
2400
+ const zh = {
2401
+ "section.label": "备份与迁移",
2402
+ "section.description": "导出 / 导入 / 迁移 DSH 配置(备份不含密钥,加密备份可选)",
2403
+ "view.export": "导出备份",
2404
+ "view.import": "导入恢复",
2405
+ "common.close": "关闭",
2406
+ "common.cancel": "取消",
2407
+ "common.back": "上一步",
2408
+ "common.next": "下一步",
2409
+ "common.done": "完成",
2410
+ "common.retry": "重试",
2411
+ "common.confirm": "确认",
2412
+ "common.loading": "加载中…",
2413
+ "common.unknownError": "未知错误",
2414
+ "status.ready": "Config Manager 已就绪(插件 {version} · DSH {dsh} · {platform})",
2415
+ "status.unavailable": "Config Manager 服务未就绪,请确认插件已正确挂载。",
2416
+ "export.mode.quick": "快速导出",
2417
+ "export.mode.quickHint": "一键导出推荐分区(可迁移、不含设备专属数据),适合常规备份。",
2418
+ "export.mode.custom": "自定义导出",
2419
+ "export.mode.customHint": "按分组逐项勾选要导出的分区。",
2420
+ "export.includeSecrets": "包含真实密钥(加密备份)",
2421
+ "export.includeSecretsHint": "勾选后导出将包含密钥,并以密码加密(AES-256-GCM)。密码仅用于本次加密,不会写入备份或任何配置文件。",
2422
+ "export.password": "备份加密密码",
2423
+ "export.passwordConfirm": "确认密码",
2424
+ "export.passwordMismatch": "两次输入的密码不一致",
2425
+ "export.passwordRequired": "包含密钥时必须设置加密密码",
2426
+ "export.run": "开始导出",
2427
+ "export.running": "正在导出…",
2428
+ "export.download": "下载备份文件",
2429
+ "export.selectionWarnings": "以下分区为设备相关数据,跨设备导入时可能不适用:",
2430
+ "export.group.unknown": "其他",
2431
+ "export.included": "已包含",
2432
+ "export.excluded": "未包含",
2433
+ "import.select.title": "选择备份文件",
2434
+ "import.select.hint": "选择本机导出的 dsh-config-*.zip 备份文件开始导入。分析阶段只读,不会修改任何配置。",
2435
+ "import.select.browse": "选择 ZIP 文件",
2436
+ "import.select.file": "已选择:{name}",
2437
+ "import.analyzing": "正在分析备份…",
2438
+ "import.compatibility.title": "兼容性",
2439
+ "import.compatibility.score": "兼容性评分:{score}",
2440
+ "import.compatibility.ok": "备份可导入",
2441
+ "import.compatibility.fail": "备份分析失败:{reason}",
2442
+ "import.preview.title": "导入预览",
2443
+ "import.preview.willChange": "将变更 {count} 项",
2444
+ "import.preview.unchanged": "已一致跳过 {count} 项",
2445
+ "import.preview.settings": "设置更新 {count}",
2446
+ "import.preview.plugins": "插件待安装 {count}",
2447
+ "import.preview.mcp": "MCP 新增 {count}",
2448
+ "import.preview.prompts": "提示词 {count}",
2449
+ "import.preview.paths": "路径映射 {count}",
2450
+ "import.preview.secrets": "密钥待补录 {count}",
2451
+ "import.preview.conflicts": "冲突 {count}",
2452
+ "import.preview.restart": "导入后需重启 DSH 生效",
2453
+ "import.confirm.execute": "确认导入",
2454
+ "import.confirm.warning": "导入将修改当前 DSH 配置;执行前会自动创建安全快照,失败时整体回滚。",
2455
+ "import.rollbackOnError": "失败时整体回滚(推荐)",
2456
+ "import.importing": "正在导入…",
2457
+ "import.secrets.title": "补录密钥",
2458
+ "import.secrets.hint": "以下凭据未随备份导出(安全设计)。如需在目标 DSH 使用,请手动补录;留空将跳过。",
2459
+ "import.secrets.optional": "(可选)",
2460
+ "import.secrets.required": "(必需)",
2461
+ "import.conflicts.title": "解决冲突",
2462
+ "import.conflicts.hint": "以下配置项在目标 DSH 已存在且与备份不同,请逐项决策。",
2463
+ "import.conflicts.keepCurrent": "保留当前",
2464
+ "import.conflicts.useImported": "使用备份",
2465
+ "import.conflicts.review": "稍后决定",
2466
+ "import.conflicts.unresolved": "还有 {count} 项未决策",
2467
+ "import.paths.title": "路径映射",
2468
+ "import.paths.hint": "备份中的绝对路径在目标机器可能不存在,请为每条路径指定新位置(留空将跳过该路径)。",
2469
+ "import.paths.old": "原路径",
2470
+ "import.paths.new": "新路径",
2471
+ "import.paths.unresolved": "还有 {count} 条路径未映射",
2472
+ "report.export.title": "导出完成",
2473
+ "report.import.title": "导入结果",
2474
+ "report.rollback.title": "回滚报告",
2475
+ "report.action.fixIssues": "查看失败项",
2476
+ "report.action.viewDetails": "查看详情",
2477
+ "report.action.done": "完成",
2478
+ "report.needsRestart": "插件 / MCP 变更将在重启 DSH 后生效。",
2479
+ "error.title": "操作失败",
2480
+ "error.hint": "错误消息已自动脱敏,不会泄露任何密钥信息。"
2481
+ };
2482
+ const en = {
2483
+ "section.label": "Backup & Migration",
2484
+ "section.description": "Export / import / migrate DSH configuration (backups exclude secrets; encrypted backups optional)",
2485
+ "view.export": "Export",
2486
+ "view.import": "Import",
2487
+ "common.close": "Close",
2488
+ "common.cancel": "Cancel",
2489
+ "common.back": "Back",
2490
+ "common.next": "Next",
2491
+ "common.done": "Done",
2492
+ "common.retry": "Retry",
2493
+ "common.confirm": "Confirm",
2494
+ "common.loading": "Loading…",
2495
+ "common.unknownError": "Unknown error",
2496
+ "status.ready": "Config Manager ready (plugin {version} · DSH {dsh} · {platform})",
2497
+ "status.unavailable": "Config Manager service unavailable — verify the plugin is mounted.",
2498
+ "export.mode.quick": "Quick Export",
2499
+ "export.mode.quickHint": "One-click export of recommended sections (portable, no device-specific data) for routine backups.",
2500
+ "export.mode.custom": "Custom Export",
2501
+ "export.mode.customHint": "Select sections per group.",
2502
+ "export.includeSecrets": "Include real secrets (encrypted backup)",
2503
+ "export.includeSecretsHint": "When enabled the backup contains secrets, encrypted with a password (AES-256-GCM). The password is used once and never written into the backup or any config file.",
2504
+ "export.password": "Backup encryption password",
2505
+ "export.passwordConfirm": "Confirm password",
2506
+ "export.passwordMismatch": "Passwords do not match",
2507
+ "export.passwordRequired": "An encryption password is required when including secrets",
2508
+ "export.run": "Start Export",
2509
+ "export.running": "Exporting…",
2510
+ "export.download": "Download backup file",
2511
+ "export.selectionWarnings": "Device-specific sections may not apply on another machine:",
2512
+ "export.group.unknown": "Other",
2513
+ "export.included": "Included",
2514
+ "export.excluded": "Excluded",
2515
+ "import.select.title": "Select Backup File",
2516
+ "import.select.hint": "Pick a dsh-config-*.zip backup exported from DSH to begin. Analysis is read-only and changes nothing.",
2517
+ "import.select.browse": "Choose ZIP file",
2518
+ "import.select.file": "Selected: {name}",
2519
+ "import.analyzing": "Analyzing backup…",
2520
+ "import.compatibility.title": "Compatibility",
2521
+ "import.compatibility.score": "Compatibility score: {score}",
2522
+ "import.compatibility.ok": "The backup can be imported",
2523
+ "import.compatibility.fail": "Backup analysis failed: {reason}",
2524
+ "import.preview.title": "Import Preview",
2525
+ "import.preview.willChange": "{count} item(s) will change",
2526
+ "import.preview.unchanged": "{count} item(s) already identical (skipped)",
2527
+ "import.preview.settings": "{count} settings update(s)",
2528
+ "import.preview.plugins": "{count} plugin(s) to install",
2529
+ "import.preview.mcp": "{count} MCP server(s) added",
2530
+ "import.preview.prompts": "{count} prompt(s)",
2531
+ "import.preview.paths": "{count} path mapping(s) needed",
2532
+ "import.preview.secrets": "{count} secret(s) to re-enter",
2533
+ "import.preview.conflicts": "{count} conflict(s)",
2534
+ "import.preview.restart": "A DSH restart is required after import",
2535
+ "import.confirm.execute": "Confirm Import",
2536
+ "import.confirm.warning": "Import modifies the current DSH configuration; a safety snapshot is created first and everything rolls back on failure.",
2537
+ "import.rollbackOnError": "Roll back everything on failure (recommended)",
2538
+ "import.importing": "Importing…",
2539
+ "import.secrets.title": "Re-enter Secrets",
2540
+ "import.secrets.hint": "These credentials were not exported with the backup (security by design). Enter them manually to use them here; leave blank to skip.",
2541
+ "import.secrets.optional": "(optional)",
2542
+ "import.secrets.required": "(required)",
2543
+ "import.conflicts.title": "Resolve Conflicts",
2544
+ "import.conflicts.hint": "These items already exist on this machine and differ from the backup. Decide per item.",
2545
+ "import.conflicts.keepCurrent": "Keep current",
2546
+ "import.conflicts.useImported": "Use backup",
2547
+ "import.conflicts.review": "Decide later",
2548
+ "import.conflicts.unresolved": "{count} item(s) unresolved",
2549
+ "import.paths.title": "Path Mapping",
2550
+ "import.paths.hint": "Absolute paths in the backup may not exist on this machine. Map each path to a new location (leave blank to skip).",
2551
+ "import.paths.old": "Original path",
2552
+ "import.paths.new": "New path",
2553
+ "import.paths.unresolved": "{count} path(s) unmapped",
2554
+ "report.export.title": "Export Complete",
2555
+ "report.import.title": "Import Result",
2556
+ "report.rollback.title": "Rollback Report",
2557
+ "report.action.fixIssues": "View failures",
2558
+ "report.action.viewDetails": "View details",
2559
+ "report.action.done": "Done",
2560
+ "report.needsRestart": "Plugin / MCP changes take effect after restarting DSH.",
2561
+ "error.title": "Operation failed",
2562
+ "error.hint": "Error messages are auto-redacted and never leak secrets."
2563
+ };
2564
+ //#endregion
2565
+ //#region src/client/index.ts
2566
+ /** 本插件拥有的 locale namespace。 */
2567
+ const NS = "config-manager";
2568
+ /** 必需服务(fiber inject 等待 —— slots/locale 必须先就绪)。 */
2569
+ const inject = ["slots", "locale"];
2570
+ /**
2571
+ * 注册 Config Manager 设置页。
2572
+ * @param ctx - client root context(slots + locale 服务)。
2573
+ */
2574
+ function apply(ctx) {
2575
+ ctx.effect(() => ctx.locale.register(NS, {
2576
+ zh,
2577
+ en
2578
+ }), "config-manager: dictionaries");
2579
+ const t = ctx.locale.bind(NS);
2580
+ const api = new ConfigManagerApi();
2581
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
2582
+ name: "settings.section",
2583
+ id: "config-manager",
2584
+ order: 60,
2585
+ label: () => t("section.label"),
2586
+ locale: NS,
2587
+ inject: () => ({ api })
2588
+ }, ConfigManagerSection));
2589
+ }
2590
+ //#endregion
2591
+ exports.apply = apply;
2592
+ exports.inject = inject;
2593
+
2594
+
2595
+ return module.exports;
2596
+ }
2597
+ });
2598
+ //# sourceMappingURL=client.js.map