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
@@ -0,0 +1,95 @@
1
+ /**
2
+ * workspaces 分区 adapter(设计 §3.3/§11):
3
+ * 数据源 = ~/.dsh/storages/workspace.json 的 tables.workspaces(经 ctx.workspace 门面)。
4
+ * workspace.path 为绝对路径 → 跨设备必须 PathMapping(analyzeImport 生成 PathMapping 项,
5
+ * 映射由引擎 PathMapper 先行应用到 sections,applyItem 拿到的已是映射后数据)。
6
+ */
7
+ import { isDeepStrictEqual } from 'node:util';
8
+ import type { WorkspaceRecord, WorkspacesSection } from '../schema/types.ts';
9
+ import type {
10
+ ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext,
11
+ ImportContext, PlanItem, ValidationResult,
12
+ } from '../core/types.ts';
13
+
14
+ export class WorkspacesAdapter implements ConfigAdapter<WorkspacesSection> {
15
+ readonly id = 'workspaces' as const;
16
+ readonly displayName = 'Workspaces';
17
+ readonly defaultIncluded = true;
18
+ readonly portability = 'platformSpecific' as const;
19
+
20
+ async export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<WorkspacesSection>> {
21
+ const records = await ctx.workspace.listRecords();
22
+ return {
23
+ sectionId: 'workspaces',
24
+ data: { version: 1, workspaces: records },
25
+ counts: { workspaces: records.length },
26
+ warnings: [],
27
+ };
28
+ }
29
+
30
+ async analyzeImport(data: WorkspacesSection, ctx: ImportContext): Promise<PlanItem[]> {
31
+ const items: PlanItem[] = [];
32
+ for (const rec of data.workspaces) {
33
+ const id = `workspace:${rec.id}`;
34
+ const existing = (await ctx.target.workspace.listRecords()).find((r) => r.id === rec.id);
35
+ if (!existing) {
36
+ items.push({
37
+ id, kind: 'Create', adapter: 'workspaces',
38
+ description: `创建工作区 ${rec.title ?? rec.id}(${rec.path})`, severity: 'info',
39
+ target: { adapter: 'workspaces', ref: rec.id },
40
+ });
41
+ } else if (isDeepStrictEqual(existing, rec)) {
42
+ items.push({ id, kind: 'Skip', adapter: 'workspaces', description: `工作区 ${rec.id} 已一致`, severity: 'info' });
43
+ } else {
44
+ items.push({
45
+ id, kind: 'Conflict', adapter: 'workspaces',
46
+ description: `工作区 ${rec.id} 与目标不同`,
47
+ detail: `current=${JSON.stringify(existing)} imported=${JSON.stringify(rec)}`.slice(0, 200),
48
+ severity: 'warning', target: { adapter: 'workspaces', ref: rec.id },
49
+ });
50
+ }
51
+ // 绝对路径 → 路径映射提示(oldPrefix 先用整条路径占位,newPrefix 由 UI 确认)
52
+ if (rec.path.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(rec.path)) {
53
+ items.push({
54
+ id: `${id}:path`, kind: 'PathMapping', adapter: 'workspaces',
55
+ description: `路径需映射: ${rec.path}`, severity: 'warning',
56
+ pathMapping: { oldPrefix: rec.path, newPrefix: '', appliesTo: ['workspaces'] },
57
+ target: { adapter: 'workspaces', ref: rec.id },
58
+ });
59
+ }
60
+ }
61
+ return items;
62
+ }
63
+
64
+ async applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult> {
65
+ // PathMapping 项:数据已由 PathMapper 应用到 sections,无需额外写入
66
+ if (item.kind === 'PathMapping') return { ok: true };
67
+ const ref = item.target?.ref;
68
+ if (!ref) return { ok: false, message: '缺少 target.ref' };
69
+ const data = ctx.sections.get('workspaces') as WorkspacesSection | undefined;
70
+ const rec = data?.workspaces.find((r) => r.id === ref);
71
+ if (!rec) return { ok: false, message: `导入数据缺少工作区 ${ref}` };
72
+ await ctx.target.workspace.writeRecord(rec);
73
+ return { ok: true };
74
+ }
75
+
76
+ async validate(data: WorkspacesSection): Promise<ValidationResult> {
77
+ const issues: ValidationResult['issues'] = [];
78
+ if (data === null || typeof data !== 'object') {
79
+ return { valid: false, issues: [{ path: '$', message: 'workspaces 数据必须是对象', severity: 'error' }] };
80
+ }
81
+ if (data.version !== 1) {
82
+ issues.push({ path: 'version', message: `version 必须为 1(收到 ${String(data.version)})`, severity: 'error' });
83
+ }
84
+ if (!Array.isArray(data.workspaces)) {
85
+ issues.push({ path: 'workspaces', message: 'workspaces 必须是数组', severity: 'error' });
86
+ } else {
87
+ for (const w of data.workspaces as WorkspaceRecord[]) {
88
+ if (w === null || typeof w !== 'object' || typeof w.id !== 'string' || typeof w.path !== 'string') {
89
+ issues.push({ path: 'workspaces[]', message: 'workspace 记录必须含字符串 id 与 path', severity: 'error' });
90
+ }
91
+ }
92
+ }
93
+ return { valid: issues.filter((i) => i.severity === 'error').length === 0, issues };
94
+ }
95
+ }
@@ -0,0 +1,101 @@
1
+ # Client 半(src/client)构建依赖清单 — 给 p3 的输入
2
+
3
+ > 本文件由 p1(client 半)产出,只描述 `src/client/**` 需要的依赖与 tsconfig 变更;
4
+ > **p1 未修改 package.json / tsconfig**(按任务约定,构建配置由 p3 负责)。
5
+
6
+ ## 1. 必装依赖
7
+
8
+ ### runtime peerDependencies(`dsh.client.inject` 声明,参考 dsh-ssh package.json)
9
+
10
+ | 包 | 版本基线 | 用途 |
11
+ |---|---|---|
12
+ | `@deepseek-ai/dsh-client-runtime` | `^0.1.0-rc.6` | ClientContext 类型、slots 运行时 |
13
+ | `@deepseek-ai/dsh-client-locale` | `^0.1.0-rc.6` | `ctx.locale` 声明合并(index.ts 用) |
14
+ | `@deepseek-ai/dsh-client-ui-settings` | `^0.1.0-rc.6` | `settings.section` SlotMap 声明合并(index.ts 用) |
15
+ | `@deepseek-ai/dsh-client-ui-slots` | `^0.1.0-rc.6` | SlotMap / LocaleNamespaceMap / PropsRuntime / TranslateNS |
16
+ | `@deepseek-ai/cordis` | `^4.0.1` | Context 基础类型 |
17
+ | `react` / `react-dom` | `^18.2.0`(dsh-ssh 同款) | 组件渲染 |
18
+ | `@types/react` / `@types/react-dom` | `~18.3.1` / `^18.3.5` | dev 类型 |
19
+
20
+ `dsh.client` 声明(对照 dsh-ssh):
21
+
22
+ ```jsonc
23
+ "dsh": {
24
+ "bundle": { "patch": "./cordis.patch.yml" },
25
+ "client": {
26
+ "inject": [
27
+ "@deepseek-ai/dsh-client-runtime",
28
+ "@deepseek-ai/dsh-client-connection",
29
+ "@deepseek-ai/dsh-client-ui-settings"
30
+ ],
31
+ "platform": "web"
32
+ }
33
+ }
34
+ ```
35
+
36
+ ### devDependencies(构建)
37
+
38
+ | 包 | 用途 |
39
+ |---|---|
40
+ | `lightningcss` | `^1.32.0`(dsh-ssh 同款)— CSS Modules 编译 |
41
+ | `tsdown` | `^0.22.2`(dsh-ssh 同款)— host+client bundle |
42
+ | `typescript` | `~5.7.2` 或现有 `^5.9.0` 均可 |
43
+
44
+ ## 2. tsconfig 变更(p3 需加)
45
+
46
+ 现有 `tsconfig.json`:`lib: ["ES2023"]` + `types: ["node"]` + include `src/**/*.ts`。
47
+
48
+ client 需要:
49
+
50
+ ```jsonc
51
+ {
52
+ "compilerOptions": {
53
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
54
+ "jsx": "react-jsx",
55
+ "types": ["node", "react", "react-dom"] // 或经 @types 自动解析
56
+ },
57
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts"]
58
+ }
59
+ ```
60
+
61
+ > 说明:`src/client/**` 的 `.tsx` 文件当前不在 `include`(`src/**/*.ts` 不匹配 `.tsx`),
62
+ > 不影响现有 `npm run typecheck`(core/ui 仍全绿);p3 打开 `.tsx` 后
63
+ > 需一并补齐 DOM lib 与 jsx,否则 client 类型不通过。
64
+
65
+ ## 3. Host 半 `/api/dsh-config-manager/*` 路由契约(client 按此调用)
66
+
67
+ p1 只做 client 侧 fetch 封装(`src/client/api.ts`),Host 半(`src/index.ts`)按此实现:
68
+
69
+ ```
70
+ GET /api/dsh-config-manager/status → ServiceStatus { ready, pluginVersion, dshVersion, platform, arch }
71
+ POST /api/dsh-config-manager/export body: { includeSecrets, only?, password? } → ExportResponse { zipPath, manifest, report }
72
+ GET /api/dsh-config-manager/download?path → 文件流(content-disposition: filename="...")
73
+ POST /api/dsh-config-manager/upload?name body: 原始字节 → UploadResponse { zipPath, name, sizeBytes }
74
+ POST /api/dsh-config-manager/analyze body: { zipPath } → ImportAnalysis
75
+ POST /api/dsh-config-manager/plan body: { zipPath, decisions } → ImportPlan
76
+ POST /api/dsh-config-manager/execute body: { zipPath, plan, opts: { confirm, secretInputs, rollbackOnError } } → ImportResult
77
+ ```
78
+
79
+ - `password` 仅加密备份模式传入(Host 侧 `security/encryption.ts` scrypt+AES-256-GCM),
80
+ **绝不写入 manifest / 任何配置**;
81
+ - `upload` 落 Host 受控临时目录(如 `~/.dsh/dsh-config-manager/tmp/`),返回的 zipPath
82
+ 供 analyze/plan/execute 引用;
83
+ - 错误响应统一 `{ error: string }`(client `readJson` 依赖该形状);
84
+ - Host 侧路由安全围栏参考 dsh-ssh `isLoopbackRequest`(仅 127.0.0.1 + 同源)。
85
+
86
+ ## 4. 与 src/ui 的绑定点(p1 实现摘要)
87
+
88
+ | src/ui 模块 | client 消费 |
89
+ |---|---|
90
+ | `export-flow.ts` 的 `ExportFlow` / `DEFAULT_CATEGORIES` | `export/ExportView.tsx`(port=api) |
91
+ | `types.ts` 的 `EXPORT_GROUPS` / `ProgressEvent` / `ImportPreviewSummary` | 导出勾选目录 / 进度条 / 预览摘要 |
92
+ | `import-wizard.ts` 的 `ImportWizard` | `import/ImportWizardView.tsx`(port=api) |
93
+ | `conflict-view.ts` 的 `ConflictCollector` | `import/ConflictList.tsx` |
94
+ | `report.ts` 的 `renderExportReport` / `renderImportReport` / `renderRollbackReport` / `suggestedActions` / `importSectionStats` | `common/ReportView.tsx` |
95
+ | `errors.ts` 的 `toActionableError` / `formatActionableError` | `common/ErrorBanner.tsx` |
96
+ | `progress.ts` 的 `stageText` / `ProgressTracker` | `common/ProgressBar.tsx` |
97
+ | `security/redaction.ts` 的 `redact` | 全部文本展示前的强制脱敏(双保险) |
98
+
99
+ > 例外:`ui/path-mapping.ts` 的 `PathMappingEditor` 依赖 `utils/paths.ts`(`node:path`),
100
+ > 浏览器 bundle 不可用 —— `import/PathMappingForm.tsx` 做轻量等价实现(输入输出形状
101
+ > 与 core `PathMapping` 一致,实际前缀替换由 Host 侧 core 在 `createImportPlan` 阶段执行)。
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Config Manager 设置页(settings.section 入口的主页面容器)。
3
+ *
4
+ * owner props 由 settings shell 传入(close);业务面(api)由注册时的
5
+ * inject face 注入;t 由 locale seat 注入。内部两个主视图:
6
+ * Export(Quick/Custom)与 Import(九步向导)。
7
+ */
8
+ import { useState } from 'react'
9
+ import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
10
+ import type { ConfigManagerSectionInjected, TranslateNS } from './client-types.ts'
11
+ import { ExportView } from './export/ExportView.tsx'
12
+ import { ImportWizardView } from './import/ImportWizardView.tsx'
13
+ import css from './config-manager.module.css'
14
+
15
+ export type ConfigManagerSectionProps =
16
+ & PropsRuntime<'settings.section'>
17
+ & ConfigManagerSectionInjected
18
+ & { t: TranslateNS<'config-manager'> }
19
+
20
+ type MainView = 'export' | 'import'
21
+
22
+ /**
23
+ * 设置页容器:Export / Import 双视图切换。
24
+ */
25
+ export function ConfigManagerSection({ close, api, t }: ConfigManagerSectionProps) {
26
+ const [view, setView] = useState<MainView>('export')
27
+
28
+ return (
29
+ <div className={css.section}>
30
+ <div className={css.sectionHeader}>
31
+ <div className={css.viewTabs} role="tablist">
32
+ <button
33
+ type="button"
34
+ role="tab"
35
+ aria-selected={view === 'export'}
36
+ data-active={view === 'export' ? '' : undefined}
37
+ className={css.viewTab}
38
+ onClick={() => { setView('export') }}
39
+ >
40
+ {t('view.export')}
41
+ </button>
42
+ <button
43
+ type="button"
44
+ role="tab"
45
+ aria-selected={view === 'import'}
46
+ data-active={view === 'import' ? '' : undefined}
47
+ className={css.viewTab}
48
+ onClick={() => { setView('import') }}
49
+ >
50
+ {t('view.import')}
51
+ </button>
52
+ </div>
53
+ <button type="button" className={css.iconButton} title={t('common.close')} aria-label={t('common.close')} onClick={close}>×</button>
54
+ </div>
55
+ <div className={css.sectionBody}>
56
+ {view === 'export' ? <ExportView api={api} t={t} /> : <ImportWizardView api={api} t={t} />}
57
+ </div>
58
+ </div>
59
+ )
60
+ }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Config Manager 浏览器半 —— `/api/dsh-config-manager/*` 的类型化 fetch 封装。
3
+ *
4
+ * 设计对齐 dsh-ssh 的 `src/client/api.ts` 范式(同源 fetch、readJson、query helper),
5
+ * 但方法签名直接实现 `src/ui/types.ts` 的 `ExportPort` / `ImportPort` 端口契约:
6
+ * 客户端控制器(ExportFlow / ImportWizard)把本类实例当作 port 注入即可,
7
+ * 无需任何额外适配层 —— 这是「直接绑定 src/ui,不重复实现」的关键。
8
+ *
9
+ * 端点契约(Host 半 `src/index.ts` 按此实现,见 CLIENT_DEPENDENCIES.md 的接口说明):
10
+ * ```
11
+ * GET /api/dsh-config-manager/status → ServiceStatus (健康/版本检查)
12
+ * POST /api/dsh-config-manager/export → ExportResponse (body: ExportOptions)
13
+ * GET /api/dsh-config-manager/download?path=… → 文件流(content-disposition 文件名)
14
+ * POST /api/dsh-config-manager/upload?name=… → UploadResponse (body: 原始字节)
15
+ * POST /api/dsh-config-manager/analyze → ImportAnalysis (body: { zipPath })
16
+ * POST /api/dsh-config-manager/plan → ImportPlan (body: { zipPath, decisions })
17
+ * POST /api/dsh-config-manager/execute → ImportResult (body: { zipPath, plan, opts })
18
+ * ```
19
+ * 安全约束:
20
+ * - 所有响应/错误文本在进入 UI 前经 `redact()`(见 common/ErrorBanner.tsx);
21
+ * - secretInputs 只存在于请求体内,绝不落日志;
22
+ * - 本文件不 import 任何 node 模块(纯浏览器 bundle)。
23
+ */
24
+
25
+ import type { ImportAnalysis, ImportDecisions, ImportPlan, ImportResult } from '../core/types.ts';
26
+ import type { ExportOptions, ExportReport } from '../core/types.ts';
27
+ import type { Manifest } from '../schema/types.ts';
28
+
29
+ /** Host 半健康检查响应(plugin 版本 / DSH 版本 / 平台,用于主页横幅与兼容性说明) */
30
+ export interface ServiceStatus {
31
+ ready: boolean;
32
+ pluginVersion: string;
33
+ dshVersion: string;
34
+ platform: string;
35
+ arch: string;
36
+ }
37
+
38
+ /** export 端点响应(对齐 ExportFlow 的 ExportRunResult 前半部分) */
39
+ export interface ExportResponse {
40
+ zipPath: string;
41
+ manifest: Manifest;
42
+ report: ExportReport;
43
+ }
44
+
45
+ /** upload 端点响应:Host 把用户上传的 ZIP 存入受控临时目录,返回可被 analyze/plan/execute 引用的路径 */
46
+ export interface UploadResponse {
47
+ zipPath: string;
48
+ name: string;
49
+ sizeBytes: number;
50
+ }
51
+
52
+ /** execute 端点请求体(对齐 ImportWizard.execute 的 opts) */
53
+ export interface ExecutePayload {
54
+ zipPath: string;
55
+ plan: ImportPlan;
56
+ opts: {
57
+ /** 安全阀:非 true 拒绝执行(core ImportNotConfirmedError) */
58
+ confirm: boolean;
59
+ /** 秘密补录值(仅内存,经 HTTPS 传输,Host 端不落盘) */
60
+ secretInputs: Record<string, string>;
61
+ /** true=任一项失败整体回滚(场景 E);false=单项失败继续 */
62
+ rollbackOnError: boolean;
63
+ };
64
+ }
65
+
66
+ /** 路由族常量(集中管理,与 Host 半的路由前缀保持一致) */
67
+ export const CONFIG_MANAGER_API = {
68
+ base: '/api/dsh-config-manager',
69
+ status: '/api/dsh-config-manager/status',
70
+ export: '/api/dsh-config-manager/export',
71
+ download: '/api/dsh-config-manager/download',
72
+ upload: '/api/dsh-config-manager/upload',
73
+ analyze: '/api/dsh-config-manager/analyze',
74
+ plan: '/api/dsh-config-manager/plan',
75
+ execute: '/api/dsh-config-manager/execute',
76
+ } as const;
77
+
78
+ /** 携带路由 JSON error 消息的错误类型 */
79
+ export class ConfigManagerApiError extends Error {
80
+ constructor(message: string) {
81
+ super(message);
82
+ this.name = 'ConfigManagerApiError';
83
+ }
84
+ }
85
+
86
+ /** 解析 JSON 响应;非 2xx 时抛出带路由 error 消息的 ConfigManagerApiError */
87
+ async function readJson<T>(response: Response): Promise<T> {
88
+ let body: unknown;
89
+ try {
90
+ body = await response.json();
91
+ } catch {
92
+ throw new ConfigManagerApiError(`HTTP ${response.status}: invalid JSON response`);
93
+ }
94
+ if (!response.ok) {
95
+ const message =
96
+ typeof body === 'object' && body !== null && typeof (body as { error?: unknown }).error === 'string'
97
+ ? (body as { error: string }).error
98
+ : `HTTP ${response.status}`;
99
+ throw new ConfigManagerApiError(message);
100
+ }
101
+ return body as T;
102
+ }
103
+
104
+ /** query-string 辅助(跳过 undefined/空串,与 dsh-ssh 一致) */
105
+ function query(params: Record<string, string | number | undefined>): string {
106
+ const search = new URLSearchParams();
107
+ for (const [key, value] of Object.entries(params)) {
108
+ if (value !== undefined && value !== '') search.set(key, String(value));
109
+ }
110
+ const text = search.toString();
111
+ return text === '' ? '' : '?' + text;
112
+ }
113
+
114
+ /** 浏览器 File System Access API 的最小表面(非全部 lib.dom 版本都有) */
115
+ interface WindowWithFileSystemAccess {
116
+ showSaveFilePicker?: (options: { suggestedName?: string }) => Promise<{
117
+ createWritable: () => Promise<{ write: (data: Uint8Array) => Promise<void>; close: () => Promise<void> }>;
118
+ }>;
119
+ }
120
+
121
+ /** 导出下载结果(流式落盘或内存 Blob 兜底) */
122
+ export interface DownloadResult {
123
+ blob?: Blob;
124
+ filename: string;
125
+ streamed: boolean;
126
+ bytes: number;
127
+ }
128
+
129
+ /**
130
+ * Config Manager 浏览器半的唯一数据入口。
131
+ * 同时实现 `ExportPort`(export-flow 用)与 `ImportPort`(import-wizard 用)。
132
+ */
133
+ export class ConfigManagerApi {
134
+ /**
135
+ * 加密备份密码(仅内存,默认 null = 普通备份)。
136
+ * ExportView 在 run 前设置;export 请求体携带给 Host 半做 AES-256-GCM 加密。
137
+ * 绝不写入 manifest / 任何 DSH 配置 / localStorage。
138
+ */
139
+ exportPassword: string | null = null
140
+
141
+ // ------------------------------------------------------------- status
142
+ /** 健康/版本检查(主页横幅:插件版本 / DSH 版本 / 平台) */
143
+ async status(): Promise<ServiceStatus> {
144
+ const response = await fetch(CONFIG_MANAGER_API.status);
145
+ return readJson<ServiceStatus>(response);
146
+ }
147
+
148
+ // ------------------------------------------------------------- export
149
+ /** ExportPort.export:调用 Host 侧导出编排(core Exporter)。加密密码随请求体传输(仅内存)。 */
150
+ async export(options: ExportOptions): Promise<ExportResponse> {
151
+ const body: ExportOptions & { password?: string } = {
152
+ ...options,
153
+ password: this.exportPassword ?? undefined,
154
+ };
155
+ const response = await fetch(CONFIG_MANAGER_API.export, {
156
+ method: 'POST',
157
+ headers: { 'content-type': 'application/json' },
158
+ body: JSON.stringify(body),
159
+ });
160
+ return readJson<ExportResponse>(response);
161
+ }
162
+
163
+ /**
164
+ * 把导出的 ZIP 下载到本机。优先 File System Access API 流式落盘
165
+ * (不占整文件内存),不可用时回退内存 Blob(dsh-ssh downloadFile 同款策略)。
166
+ */
167
+ async download(
168
+ zipPath: string,
169
+ onProgress?: (received: number, total: number) => void,
170
+ ): Promise<DownloadResult> {
171
+ const response = await fetch(CONFIG_MANAGER_API.download + query({ path: zipPath }));
172
+ if (!response.ok || response.body === null) {
173
+ const text = await response.text().catch(() => '');
174
+ throw new ConfigManagerApiError(text !== '' ? text : `download failed: HTTP ${response.status}`);
175
+ }
176
+ const total = Number(response.headers.get('content-length') ?? '0');
177
+ const disposition = response.headers.get('content-disposition') ?? '';
178
+ const match = /filename="([^"]+)"/.exec(disposition);
179
+ const filename = match?.[1] ?? zipPath.split(/[\\/]/).pop() ?? 'dsh-config.zip';
180
+ const reader = response.body.getReader();
181
+ const picker =
182
+ typeof window !== 'undefined'
183
+ ? (window as WindowWithFileSystemAccess).showSaveFilePicker
184
+ : undefined;
185
+ let streamed = false;
186
+ let writable: { write: (data: Uint8Array) => Promise<void>; close: () => Promise<void> } | undefined;
187
+ const chunks: Uint8Array<ArrayBuffer>[] = [];
188
+ let received = 0;
189
+ try {
190
+ if (picker !== undefined) {
191
+ const handle = await picker.call(window, { suggestedName: filename });
192
+ writable = await handle.createWritable();
193
+ streamed = true;
194
+ }
195
+ } catch {
196
+ // 用户取消保存对话框或 API 不可用:回退内存 Blob。
197
+ }
198
+ for (;;) {
199
+ const { done, value } = await reader.read();
200
+ if (done) break;
201
+ if (writable !== undefined) {
202
+ await writable.write(value as Uint8Array);
203
+ } else {
204
+ chunks.push(value as Uint8Array<ArrayBuffer>);
205
+ }
206
+ received += value.length;
207
+ onProgress?.(received, total);
208
+ }
209
+ if (writable !== undefined) await writable.close();
210
+ return {
211
+ blob: streamed ? undefined : new Blob(chunks),
212
+ filename,
213
+ streamed,
214
+ bytes: received,
215
+ };
216
+ }
217
+
218
+ // ------------------------------------------------------------- import
219
+ /** 上传用户选择的 ZIP 到 Host 受控临时目录,返回可引用的 zipPath(dsh-ssh 同款原始字节上传) */
220
+ async upload(file: File): Promise<UploadResponse> {
221
+ const response = await fetch(CONFIG_MANAGER_API.upload + query({ name: file.name }), {
222
+ method: 'POST',
223
+ body: file,
224
+ });
225
+ return readJson<UploadResponse>(response);
226
+ }
227
+
228
+ /** ImportPort.analyzeImport:零写入分析(校验/兼容性/差异/路径/秘密检测) */
229
+ async analyzeImport(zipPath: string): Promise<ImportAnalysis> {
230
+ const response = await fetch(CONFIG_MANAGER_API.analyze, {
231
+ method: 'POST',
232
+ headers: { 'content-type': 'application/json' },
233
+ body: JSON.stringify({ zipPath }),
234
+ });
235
+ return readJson<ImportAnalysis>(response);
236
+ }
237
+
238
+ /** ImportPort.createImportPlan:用用户决策(冲突/路径映射/策略)生成最终计划(Dry Run 零写入) */
239
+ async createImportPlan(zipPath: string, decisions: ImportDecisions): Promise<ImportPlan> {
240
+ const response = await fetch(CONFIG_MANAGER_API.plan, {
241
+ method: 'POST',
242
+ headers: { 'content-type': 'application/json' },
243
+ body: JSON.stringify({ zipPath, decisions }),
244
+ });
245
+ return readJson<ImportPlan>(response);
246
+ }
247
+
248
+ /** ImportPort.executeImportPlan:快照→分阶段 apply→validate→commit/rollback */
249
+ async executeImportPlan(
250
+ zipPath: string,
251
+ plan: ImportPlan,
252
+ opts: { confirm: boolean; secretInputs?: Record<string, string>; rollbackOnError: boolean },
253
+ ): Promise<ImportResult> {
254
+ const payload: ExecutePayload = {
255
+ zipPath,
256
+ plan,
257
+ opts: {
258
+ confirm: opts.confirm,
259
+ secretInputs: opts.secretInputs ?? {},
260
+ rollbackOnError: opts.rollbackOnError,
261
+ },
262
+ };
263
+ const response = await fetch(CONFIG_MANAGER_API.execute, {
264
+ method: 'POST',
265
+ headers: { 'content-type': 'application/json' },
266
+ body: JSON.stringify(payload),
267
+ });
268
+ return readJson<ImportResult>(response);
269
+ }
270
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Client 半的类型集中出口:把对 @deepseek-ai 运行时包的类型依赖收敛到本文件,
3
+ * 其余组件只从 `../client-types.ts` 引用,避免类型散落与误用值导入。
4
+ *
5
+ * p3 构建前需安装的依赖(见 CLIENT_DEPENDENCIES.md):
6
+ * - @deepseek-ai/dsh-client-runtime(ClientContext 声明合并)
7
+ * - @deepseek-ai/dsh-client-locale(ctx.locale 声明合并)
8
+ * - @deepseek-ai/dsh-client-ui-settings(settings.section SlotMap 声明合并)
9
+ * - @deepseek-ai/dsh-client-ui-slots(SlotMap / LocaleNamespaceMap / TranslateNS)
10
+ * - @deepseek-ai/cordis(Context 基础类型)
11
+ */
12
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
13
+ import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
14
+
15
+ export type { ClientContext }
16
+ export type { TranslateNS }
17
+
18
+ /** settings.section 注册项的 owner props(shell 传入 close) */
19
+ export interface SettingsSectionProps {
20
+ /** 关闭设置面板(shell 拥有 open 状态) */
21
+ close: () => void
22
+ }
23
+
24
+ /** 本插件 Client 半的注入业务面:每个 settings.section 注册项都拿到同一个 api 实例 */
25
+ export interface ConfigManagerSectionInjected {
26
+ api: import('./api.ts').ConfigManagerApi
27
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * 可操作错误展示(规范 §23,绑 src/ui/errors.ts)。
3
+ *
4
+ * 安全约束:**强制 redact 不泄 Secret** —— 展示前对错误消息、关联项文本
5
+ * 统一过 `redact()`(字段名黑名单 + sk-/JWT/PEM/Bearer 等值形状模式),
6
+ * 渲染结果只含 Reason / Suggested action / Item,绝不出现密钥原文。
7
+ */
8
+ import { useState } from 'react'
9
+ import { formatActionableError, toActionableError } from '../../ui/errors.ts'
10
+ import { redact } from '../../security/redaction.ts'
11
+ import { Button } from './ui.tsx'
12
+ import css from '../config-manager.module.css'
13
+
14
+ export interface ErrorBannerProps {
15
+ /** 任意未知错误(Error / 字符串 / API 错误) */
16
+ error: unknown
17
+ /** 可重试时显示的重试按钮(缺省不显示) */
18
+ onRetry?: () => void
19
+ /** 重试中的 loading 态 */
20
+ retrying?: boolean
21
+ }
22
+
23
+ /**
24
+ * 错误横幅:toActionableError 解析为标题 + 原因 + 建议动作,
25
+ * 文本在渲染前再经 redact() 兜底(双保险),Reason 以等宽块展示。
26
+ */
27
+ export function ErrorBanner({ error, onRetry, retrying }: ErrorBannerProps) {
28
+ const [actionable] = useState(() => toActionableError(error))
29
+ const reason = redact(actionable.reason)
30
+ const item = actionable.item !== undefined ? redact(actionable.item) : undefined
31
+
32
+ return (
33
+ <div className={css.errorBanner} role="alert">
34
+ <div className={css.errorTitle}>{redact(actionable.title)}</div>
35
+ <pre className={css.errorReason}>{reason}</pre>
36
+ {actionable.suggestedAction !== undefined && (
37
+ <div className={css.errorAction}>
38
+ <span className={css.errorActionLabel}>→</span> {redact(actionable.suggestedAction)}
39
+ </div>
40
+ )}
41
+ {item !== undefined && <div className={css.errorItem}>{item}</div>}
42
+ {actionable.retryable && onRetry !== undefined && (
43
+ <div className={css.errorFooter}>
44
+ <Button variant="primary" onClick={onRetry} disabled={retrying === true}>
45
+ {retrying === true ? '…' : 'Retry'}
46
+ </Button>
47
+ </div>
48
+ )}
49
+ </div>
50
+ )
51
+ }
52
+
53
+ /** 多行错误文本展示(Wizard.errors 数组用;同样强制 redact) */
54
+ export function ErrorList({ errors }: { errors: readonly string[] }) {
55
+ if (errors.length === 0) return null
56
+ return (
57
+ <div className={css.errorList}>
58
+ {errors.map((line, i) => (
59
+ <pre key={i} className={css.errorLine}>{redact(line)}</pre>
60
+ ))}
61
+ </div>
62
+ )
63
+ }
64
+
65
+ /** formatActionableError 的文本化出口(供非 React 渲染场景复用) */
66
+ export function formatErrorText(error: unknown): string {
67
+ return formatActionableError(toActionableError(error))
68
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * 进度条(规范 §29,绑 src/ui/progress.ts)。
3
+ *
4
+ * 控制器(ExportFlow / ImportWizard)在调用 core 前后发出 ProgressEvent,
5
+ * 本组件按 stage 文案 + step/total 渲染进度条;未知阶段回退显示 id 本身。
6
+ */
7
+ import { stageText } from '../../ui/progress.ts'
8
+ import type { ProgressEvent } from '../../ui/types.ts'
9
+ import css from '../config-manager.module.css'
10
+
11
+ export interface ProgressBarProps {
12
+ /** 当前阶段事件(null = 未开始) */
13
+ event: ProgressEvent | null
14
+ /** 是否正在执行(false 时显示为完成态) */
15
+ active: boolean
16
+ }
17
+
18
+ /**
19
+ * 进度条:阶段文字 + 百分比(有 step/total 时计算,否则不定态动画)。
20
+ */
21
+ export function ProgressBar({ event, active }: ProgressBarProps) {
22
+ const percent =
23
+ event !== null && event.step !== undefined && event.total !== undefined && event.total > 0
24
+ ? Math.round((event.step / event.total) * 100)
25
+ : null
26
+ const label = event !== null ? stageText(event.stage) : ''
27
+
28
+ return (
29
+ <div className={css.progressBlock}>
30
+ <div className={css.progressMeta}>
31
+ <span className={css.progressLabel}>{label}</span>
32
+ {event?.detail !== undefined && event.detail !== '' && (
33
+ <span className={css.progressDetail}>{event.detail}</span>
34
+ )}
35
+ {percent !== null && <span className={css.progressPercent}>{percent}%</span>}
36
+ </div>
37
+ <div className={css.progressTrack}>
38
+ {percent !== null ? (
39
+ <div
40
+ className={`${css.progressBar} ${active ? '' : css.progressBarDone}`}
41
+ style={{ width: `${percent}%` }}
42
+ />
43
+ ) : (
44
+ <div className={`${css.progressBar} ${css.progressIndeterminate}`} />
45
+ )}
46
+ </div>
47
+ </div>
48
+ )
49
+ }