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.
- package/README.md +216 -0
- package/README.zh-CN.md +218 -0
- package/cordis.patch.yml +45 -0
- package/dsh.bundle.patch +21 -0
- package/dsh.client +39 -0
- package/lib/adapters/agent-presets.d.ts +13 -0
- package/lib/adapters/agent-presets.js +14 -0
- package/lib/adapters/agent-presets.js.map +1 -0
- package/lib/adapters/credentials.d.ts +36 -0
- package/lib/adapters/credentials.js +121 -0
- package/lib/adapters/credentials.js.map +1 -0
- package/lib/adapters/file-collection.d.ts +14 -0
- package/lib/adapters/file-collection.js +102 -0
- package/lib/adapters/file-collection.js.map +1 -0
- package/lib/adapters/index.d.ts +38 -0
- package/lib/adapters/index.js +46 -0
- package/lib/adapters/index.js.map +1 -0
- package/lib/adapters/mcp.d.ts +27 -0
- package/lib/adapters/mcp.js +175 -0
- package/lib/adapters/mcp.js.map +1 -0
- package/lib/adapters/plugin-files.d.ts +15 -0
- package/lib/adapters/plugin-files.js +91 -0
- package/lib/adapters/plugin-files.js.map +1 -0
- package/lib/adapters/plugins.d.ts +13 -0
- package/lib/adapters/plugins.js +177 -0
- package/lib/adapters/plugins.js.map +1 -0
- package/lib/adapters/prompts.d.ts +29 -0
- package/lib/adapters/prompts.js +228 -0
- package/lib/adapters/prompts.js.map +1 -0
- package/lib/adapters/providers.d.ts +33 -0
- package/lib/adapters/providers.js +143 -0
- package/lib/adapters/providers.js.map +1 -0
- package/lib/adapters/sessions.d.ts +14 -0
- package/lib/adapters/sessions.js +15 -0
- package/lib/adapters/sessions.js.map +1 -0
- package/lib/adapters/settings.d.ts +23 -0
- package/lib/adapters/settings.js +142 -0
- package/lib/adapters/settings.js.map +1 -0
- package/lib/adapters/skills.d.ts +13 -0
- package/lib/adapters/skills.js +14 -0
- package/lib/adapters/skills.js.map +1 -0
- package/lib/adapters/test-helpers.d.ts +99 -0
- package/lib/adapters/test-helpers.js +192 -0
- package/lib/adapters/test-helpers.js.map +1 -0
- package/lib/adapters/ui.d.ts +26 -0
- package/lib/adapters/ui.js +88 -0
- package/lib/adapters/ui.js.map +1 -0
- package/lib/adapters/workspaces.d.ts +12 -0
- package/lib/adapters/workspaces.js +92 -0
- package/lib/adapters/workspaces.js.map +1 -0
- package/lib/client.d.ts +377 -0
- package/lib/client.js +2598 -0
- package/lib/client.js.map +1 -0
- package/lib/core/analyzer.d.ts +35 -0
- package/lib/core/analyzer.js +563 -0
- package/lib/core/analyzer.js.map +1 -0
- package/lib/core/backup.d.ts +29 -0
- package/lib/core/backup.js +184 -0
- package/lib/core/backup.js.map +1 -0
- package/lib/core/exporter.d.ts +37 -0
- package/lib/core/exporter.js +222 -0
- package/lib/core/exporter.js.map +1 -0
- package/lib/core/importer.d.ts +42 -0
- package/lib/core/importer.js +25 -0
- package/lib/core/importer.js.map +1 -0
- package/lib/core/index.d.ts +12 -0
- package/lib/core/index.js +11 -0
- package/lib/core/index.js.map +1 -0
- package/lib/core/rollback.d.ts +9 -0
- package/lib/core/rollback.js +136 -0
- package/lib/core/rollback.js.map +1 -0
- package/lib/core/types.d.ts +331 -0
- package/lib/core/types.js +18 -0
- package/lib/core/types.js.map +1 -0
- package/lib/core/validator.d.ts +18 -0
- package/lib/core/validator.js +77 -0
- package/lib/core/validator.js.map +1 -0
- package/lib/index.d.ts +51 -0
- package/lib/index.js +875 -0
- package/lib/index.js.map +1 -0
- package/lib/migrations/index.d.ts +29 -0
- package/lib/migrations/index.js +56 -0
- package/lib/migrations/index.js.map +1 -0
- package/lib/migrations/v1-to-v2.d.ts +9 -0
- package/lib/migrations/v1-to-v2.js +10 -0
- package/lib/migrations/v1-to-v2.js.map +1 -0
- package/lib/profiles/index.d.ts +4 -0
- package/lib/profiles/index.js +5 -0
- package/lib/profiles/index.js.map +1 -0
- package/lib/profiles/profile-manager.d.ts +102 -0
- package/lib/profiles/profile-manager.js +522 -0
- package/lib/profiles/profile-manager.js.map +1 -0
- package/lib/schema/config.d.ts +28 -0
- package/lib/schema/config.js +136 -0
- package/lib/schema/config.js.map +1 -0
- package/lib/schema/manifest.d.ts +28 -0
- package/lib/schema/manifest.js +109 -0
- package/lib/schema/manifest.js.map +1 -0
- package/lib/schema/types.d.ts +169 -0
- package/lib/schema/types.js +8 -0
- package/lib/schema/types.js.map +1 -0
- package/lib/schema/versions.d.ts +27 -0
- package/lib/schema/versions.js +50 -0
- package/lib/schema/versions.js.map +1 -0
- package/lib/security/encryption.d.ts +41 -0
- package/lib/security/encryption.js +152 -0
- package/lib/security/encryption.js.map +1 -0
- package/lib/security/index.d.ts +16 -0
- package/lib/security/index.js +17 -0
- package/lib/security/index.js.map +1 -0
- package/lib/security/integrity.d.ts +30 -0
- package/lib/security/integrity.js +78 -0
- package/lib/security/integrity.js.map +1 -0
- package/lib/security/redaction.d.ts +26 -0
- package/lib/security/redaction.js +95 -0
- package/lib/security/redaction.js.map +1 -0
- package/lib/security/secret-scanner.d.ts +48 -0
- package/lib/security/secret-scanner.js +253 -0
- package/lib/security/secret-scanner.js.map +1 -0
- package/lib/security/zip-security.d.ts +27 -0
- package/lib/security/zip-security.js +154 -0
- package/lib/security/zip-security.js.map +1 -0
- package/lib/ui/conflict-view.d.ts +28 -0
- package/lib/ui/conflict-view.js +49 -0
- package/lib/ui/conflict-view.js.map +1 -0
- package/lib/ui/errors.d.ts +17 -0
- package/lib/ui/errors.js +76 -0
- package/lib/ui/errors.js.map +1 -0
- package/lib/ui/export-flow.d.ts +62 -0
- package/lib/ui/export-flow.js +79 -0
- package/lib/ui/export-flow.js.map +1 -0
- package/lib/ui/import-wizard.d.ts +67 -0
- package/lib/ui/import-wizard.js +169 -0
- package/lib/ui/import-wizard.js.map +1 -0
- package/lib/ui/path-mapping.d.ts +38 -0
- package/lib/ui/path-mapping.js +75 -0
- package/lib/ui/path-mapping.js.map +1 -0
- package/lib/ui/progress.d.ts +27 -0
- package/lib/ui/progress.js +76 -0
- package/lib/ui/progress.js.map +1 -0
- package/lib/ui/report.d.ts +21 -0
- package/lib/ui/report.js +146 -0
- package/lib/ui/report.js.map +1 -0
- package/lib/ui/test-helpers.d.ts +62 -0
- package/lib/ui/test-helpers.js +147 -0
- package/lib/ui/test-helpers.js.map +1 -0
- package/lib/ui/types.d.ts +129 -0
- package/lib/ui/types.js +21 -0
- package/lib/ui/types.js.map +1 -0
- package/lib/utils/hashing.d.ts +17 -0
- package/lib/utils/hashing.js +41 -0
- package/lib/utils/hashing.js.map +1 -0
- package/lib/utils/json.d.ts +24 -0
- package/lib/utils/json.js +76 -0
- package/lib/utils/json.js.map +1 -0
- package/lib/utils/logger.d.ts +39 -0
- package/lib/utils/logger.js +82 -0
- package/lib/utils/logger.js.map +1 -0
- package/lib/utils/paths.d.ts +31 -0
- package/lib/utils/paths.js +132 -0
- package/lib/utils/paths.js.map +1 -0
- package/lib/utils/zip.d.ts +54 -0
- package/lib/utils/zip.js +285 -0
- package/lib/utils/zip.js.map +1 -0
- package/package.json +109 -0
- package/src/adapters/agent-presets.ts +14 -0
- package/src/adapters/credentials.test.ts +90 -0
- package/src/adapters/credentials.ts +141 -0
- package/src/adapters/file-collection.ts +110 -0
- package/src/adapters/files.test.ts +113 -0
- package/src/adapters/index.ts +69 -0
- package/src/adapters/mcp.test.ts +96 -0
- package/src/adapters/mcp.ts +181 -0
- package/src/adapters/plugin-files.ts +96 -0
- package/src/adapters/plugins.test.ts +72 -0
- package/src/adapters/plugins.ts +176 -0
- package/src/adapters/prompts.test.ts +110 -0
- package/src/adapters/prompts.ts +226 -0
- package/src/adapters/providers.test.ts +125 -0
- package/src/adapters/providers.ts +161 -0
- package/src/adapters/roundtrip.test.ts +188 -0
- package/src/adapters/sessions.ts +15 -0
- package/src/adapters/settings.test.ts +93 -0
- package/src/adapters/settings.ts +165 -0
- package/src/adapters/skills.ts +14 -0
- package/src/adapters/test-helpers.ts +207 -0
- package/src/adapters/ui.test.ts +56 -0
- package/src/adapters/ui.ts +106 -0
- package/src/adapters/workspaces.test.ts +79 -0
- package/src/adapters/workspaces.ts +95 -0
- package/src/client/CLIENT_DEPENDENCIES.md +101 -0
- package/src/client/ConfigManagerSection.tsx +60 -0
- package/src/client/api.ts +270 -0
- package/src/client/client-types.ts +27 -0
- package/src/client/common/ErrorBanner.tsx +68 -0
- package/src/client/common/ProgressBar.tsx +49 -0
- package/src/client/common/ReportView.tsx +117 -0
- package/src/client/common/ui.tsx +165 -0
- package/src/client/config-manager.module.css +759 -0
- package/src/client/css-modules.d.ts +8 -0
- package/src/client/export/ExportView.tsx +222 -0
- package/src/client/import/ConflictList.tsx +72 -0
- package/src/client/import/ImportWizardView.tsx +406 -0
- package/src/client/import/PathMappingForm.tsx +77 -0
- package/src/client/index.ts +65 -0
- package/src/client/locales.ts +182 -0
- package/src/core/analyzer.ts +631 -0
- package/src/core/backup.ts +212 -0
- package/src/core/exporter.ts +246 -0
- package/src/core/importer.ts +66 -0
- package/src/core/index.ts +39 -0
- package/src/core/rollback.ts +140 -0
- package/src/core/smoke.test.ts +717 -0
- package/src/core/types.ts +343 -0
- package/src/core/validator.ts +84 -0
- package/src/index.ts +942 -0
- package/src/migrations/index.ts +71 -0
- package/src/migrations/v1-to-v2.ts +17 -0
- package/src/profiles/index.ts +8 -0
- package/src/profiles/profile-manager.test.ts +280 -0
- package/src/profiles/profile-manager.ts +606 -0
- package/src/schema/config.ts +137 -0
- package/src/schema/manifest.ts +124 -0
- package/src/schema/types.ts +179 -0
- package/src/schema/versions.ts +55 -0
- package/src/security/encryption.ts +180 -0
- package/src/security/index.ts +16 -0
- package/src/security/integrity.ts +102 -0
- package/src/security/redaction.ts +101 -0
- package/src/security/secret-scanner.ts +275 -0
- package/src/security/security.test.ts +796 -0
- package/src/security/zip-security.ts +180 -0
- package/src/ui/conflict-view.test.ts +62 -0
- package/src/ui/conflict-view.ts +65 -0
- package/src/ui/errors.test.ts +44 -0
- package/src/ui/errors.ts +95 -0
- package/src/ui/export-flow.test.ts +75 -0
- package/src/ui/export-flow.ts +128 -0
- package/src/ui/import-wizard.test.ts +128 -0
- package/src/ui/import-wizard.ts +208 -0
- package/src/ui/path-mapping.test.ts +67 -0
- package/src/ui/path-mapping.ts +103 -0
- package/src/ui/progress.test.ts +42 -0
- package/src/ui/progress.ts +90 -0
- package/src/ui/report.test.ts +93 -0
- package/src/ui/report.ts +140 -0
- package/src/ui/test-helpers.ts +177 -0
- package/src/ui/types.ts +193 -0
- package/src/utils/hashing.ts +53 -0
- package/src/utils/json.ts +77 -0
- package/src/utils/logger.ts +114 -0
- package/src/utils/paths.ts +122 -0
- package/src/utils/zip.ts +332 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 导入九步向导(规范 §9 / §10 / §28,绑 src/ui/import-wizard.ts 的 ImportWizard 控制器)。
|
|
3
|
+
*
|
|
4
|
+
* 步骤(对齐 ui/types.ts 的 ImportStep):
|
|
5
|
+
* Select ZIP → Analyzing → Compatibility → Preview
|
|
6
|
+
* → Resolve Conflicts(若有)→ Path Mapping(若有)→ Secrets 补录(若有)
|
|
7
|
+
* → Confirm → Importing → Result
|
|
8
|
+
*
|
|
9
|
+
* 安全/正确性约束(来自 ImportWizard 与 core):
|
|
10
|
+
* - analyzeImport / createImportPlan 零写入(Dry Run 复用);
|
|
11
|
+
* - executeImportPlan 必须 confirm=true(core 安全阀);
|
|
12
|
+
* - 秘密补录值仅内存(secretInputs),经 HTTPS 请求体传给 Host,绝不落日志/落盘;
|
|
13
|
+
* - 默认整体回滚(rollbackOnError=true),用户在 Confirm 步可切换。
|
|
14
|
+
*
|
|
15
|
+
* 数据流:本地文件 → api.upload → zipPath → wizard.selectZip/confirmCompatibility/
|
|
16
|
+
* setResolutions/setPathMappings/setSecretInputs → wizard.execute。
|
|
17
|
+
* 中间阶段(conflicts/path-mapping/secrets)是 UI 层流程页,wizard 的 decisions 由
|
|
18
|
+
* 对应组件收集后写入。
|
|
19
|
+
*/
|
|
20
|
+
import { useRef, useState } from 'react'
|
|
21
|
+
import type { ChangeEvent } from 'react'
|
|
22
|
+
import { ImportWizard } from '../../ui/import-wizard.ts'
|
|
23
|
+
import { ConflictCollector } from '../../ui/conflict-view.ts'
|
|
24
|
+
import type { ImportPreviewSummary, ProgressEvent, WizardSnapshot } from '../../ui/types.ts'
|
|
25
|
+
import type { PathMapping } from '../../core/types.ts'
|
|
26
|
+
import type { ConfigManagerApi, UploadResponse } from '../api.ts'
|
|
27
|
+
import type { TranslateNS } from '../client-types.ts'
|
|
28
|
+
import { Badge, Banner, Button, Card, Checkbox, Empty, SectionTitle, Spinner } from '../common/ui.tsx'
|
|
29
|
+
import { ErrorBanner, ErrorList } from '../common/ErrorBanner.tsx'
|
|
30
|
+
import { ProgressBar } from '../common/ProgressBar.tsx'
|
|
31
|
+
import { ReportView } from '../common/ReportView.tsx'
|
|
32
|
+
import { ConflictList } from './ConflictList.tsx'
|
|
33
|
+
import { PathMappingForm } from './PathMappingForm.tsx'
|
|
34
|
+
import css from '../config-manager.module.css'
|
|
35
|
+
|
|
36
|
+
export interface ImportWizardViewProps {
|
|
37
|
+
api: ConfigManagerApi
|
|
38
|
+
t: TranslateNS<'config-manager'>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 中间流程阶段(wizard.step 之外的 UI 层页面) */
|
|
42
|
+
type FlowPhase = 'preview' | 'conflicts' | 'path-mapping' | 'secrets' | 'confirm'
|
|
43
|
+
|
|
44
|
+
const SCORE_LABEL: Record<string, string> = {
|
|
45
|
+
excellent: 'Excellent',
|
|
46
|
+
good: 'Good',
|
|
47
|
+
partial: 'Partial',
|
|
48
|
+
unsupported: 'Unsupported',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 密钥补录表单(仅内存收集,值不外泄) */
|
|
52
|
+
function SecretsForm({
|
|
53
|
+
missing,
|
|
54
|
+
t,
|
|
55
|
+
onChange,
|
|
56
|
+
}: {
|
|
57
|
+
missing: { ref: string; required: boolean }[]
|
|
58
|
+
t: TranslateNS<'config-manager'>
|
|
59
|
+
onChange: (inputs: Record<string, string>) => void
|
|
60
|
+
}) {
|
|
61
|
+
const [inputs, setInputs] = useState<Record<string, string>>({})
|
|
62
|
+
const setRef = (ref: string, value: string): void => {
|
|
63
|
+
const next = { ...inputs, [ref]: value }
|
|
64
|
+
setInputs(next)
|
|
65
|
+
onChange(next)
|
|
66
|
+
}
|
|
67
|
+
return (
|
|
68
|
+
<div className={css.secretsList}>
|
|
69
|
+
<div className={css.hint}>{t('import.secrets.hint')}</div>
|
|
70
|
+
{missing.length === 0 && <Empty>No secrets required</Empty>}
|
|
71
|
+
{missing.map((s) => (
|
|
72
|
+
<label key={s.ref} className={css.field}>
|
|
73
|
+
<span className={css.fieldLabel}>
|
|
74
|
+
{s.ref} {s.required ? t('import.secrets.required') : t('import.secrets.optional')}
|
|
75
|
+
</span>
|
|
76
|
+
<input
|
|
77
|
+
type="password"
|
|
78
|
+
className={css.input}
|
|
79
|
+
autoComplete="off"
|
|
80
|
+
value={inputs[s.ref] ?? ''}
|
|
81
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) => { setRef(s.ref, e.target.value) }}
|
|
82
|
+
/>
|
|
83
|
+
</label>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 导入向导主视图。
|
|
91
|
+
*/
|
|
92
|
+
export function ImportWizardView({ api, t }: ImportWizardViewProps) {
|
|
93
|
+
// ImportWizard 控制器(port=api,onProgress 桥到 React state)
|
|
94
|
+
const [wizard] = useState(() => new ImportWizard({
|
|
95
|
+
port: api,
|
|
96
|
+
onProgress: (event) => { setProgress(event) },
|
|
97
|
+
defaultRollbackOnError: true,
|
|
98
|
+
}))
|
|
99
|
+
const [, setTick] = useState(0)
|
|
100
|
+
const refresh = (): void => { setTick((v) => v + 1) }
|
|
101
|
+
|
|
102
|
+
const [progress, setProgress] = useState<ProgressEvent | null>(null)
|
|
103
|
+
const [error, setError] = useState<unknown>(null)
|
|
104
|
+
const [phase, setPhase] = useState<FlowPhase>('preview')
|
|
105
|
+
const [uploading, setUploading] = useState(false)
|
|
106
|
+
const [rollbackOnError, setRollbackOnError] = useState(true)
|
|
107
|
+
const [conflictCollector, setConflictCollector] = useState<ConflictCollector | null>(null)
|
|
108
|
+
const [pathMappings, setPathMappings] = useState<PathMapping[]>([])
|
|
109
|
+
const [secretInputs, setSecretInputs] = useState<Record<string, string>>({})
|
|
110
|
+
const fileInput = useRef<HTMLInputElement | null>(null)
|
|
111
|
+
|
|
112
|
+
const snapshot: WizardSnapshot = wizard.snapshot()
|
|
113
|
+
const step = snapshot.step
|
|
114
|
+
|
|
115
|
+
/* ---------- 阶段判定 ---------- */
|
|
116
|
+
|
|
117
|
+
const hasConflicts = (snapshot.plan?.items ?? []).some((i) => i.kind === 'Conflict')
|
|
118
|
+
const hasPathIssues = (snapshot.analysis?.pathIssues.length ?? 0) > 0
|
|
119
|
+
const hasSecrets = (snapshot.plan?.missingSecrets.length ?? 0) > 0
|
|
120
|
+
|
|
121
|
+
/** 从某阶段完成后进入的下一个阶段(跳过空阶段;from 之前的阶段不再进入) */
|
|
122
|
+
const nextPhase = (from: FlowPhase): FlowPhase => {
|
|
123
|
+
if (from !== 'conflicts' && hasConflicts) return 'conflicts'
|
|
124
|
+
if (from !== 'path-mapping' && hasPathIssues) return 'path-mapping'
|
|
125
|
+
if (from !== 'secrets' && hasSecrets) return 'secrets'
|
|
126
|
+
return 'confirm'
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* ---------- 动作 ---------- */
|
|
130
|
+
|
|
131
|
+
/** 选择并上传 ZIP → wizard.selectZip(analyzing → compatibility) */
|
|
132
|
+
const onPickFile = async (file: File | undefined): Promise<void> => {
|
|
133
|
+
if (file === undefined) return
|
|
134
|
+
setUploading(true)
|
|
135
|
+
setError(null)
|
|
136
|
+
try {
|
|
137
|
+
const uploaded: UploadResponse = await api.upload(file)
|
|
138
|
+
const analysis = await wizard.selectZip(uploaded.zipPath)
|
|
139
|
+
void analysis
|
|
140
|
+
refresh()
|
|
141
|
+
} catch (err) {
|
|
142
|
+
setError(err)
|
|
143
|
+
refresh()
|
|
144
|
+
} finally {
|
|
145
|
+
setUploading(false)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Compatibility → Preview */
|
|
150
|
+
const goPreview = async (): Promise<void> => {
|
|
151
|
+
setError(null)
|
|
152
|
+
try {
|
|
153
|
+
await wizard.confirmCompatibility()
|
|
154
|
+
refresh()
|
|
155
|
+
} catch (err) {
|
|
156
|
+
setError(err)
|
|
157
|
+
refresh()
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 进入 conflicts 阶段(先创建 collector) */
|
|
162
|
+
const enterConflicts = (): void => {
|
|
163
|
+
const plan = snapshot.plan
|
|
164
|
+
if (plan !== null && conflictCollector === null) {
|
|
165
|
+
setConflictCollector(new ConflictCollector(plan))
|
|
166
|
+
}
|
|
167
|
+
setPhase('conflicts')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Conflicts 完成:写入决策 → 下一阶段 */
|
|
171
|
+
const finishConflicts = (): void => {
|
|
172
|
+
if (conflictCollector !== null) wizard.setResolutions(conflictCollector.toResolutions())
|
|
173
|
+
setPhase(nextPhase('conflicts'))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Path Mapping 完成:写入映射 → 下一阶段 */
|
|
177
|
+
const finishPathMapping = (): void => {
|
|
178
|
+
wizard.setPathMappings(pathMappings)
|
|
179
|
+
setPhase(nextPhase('path-mapping'))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Secrets 完成:写入补录值 → Confirm */
|
|
183
|
+
const finishSecrets = (): void => {
|
|
184
|
+
wizard.setSecretInputs(secretInputs)
|
|
185
|
+
setPhase('confirm')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Confirm 执行:confirm=true(安全阀)+ 用户回滚策略 */
|
|
189
|
+
const execute = async (): Promise<void> => {
|
|
190
|
+
setError(null)
|
|
191
|
+
try {
|
|
192
|
+
await wizard.execute({ confirm: true, rollbackOnError })
|
|
193
|
+
refresh()
|
|
194
|
+
} catch (err) {
|
|
195
|
+
setError(err)
|
|
196
|
+
refresh()
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 重置向导(重新导入) */
|
|
201
|
+
const resetWizard = (): void => {
|
|
202
|
+
wizard.reset()
|
|
203
|
+
setError(null)
|
|
204
|
+
setPhase('preview')
|
|
205
|
+
setConflictCollector(null)
|
|
206
|
+
setPathMappings([])
|
|
207
|
+
setSecretInputs({})
|
|
208
|
+
refresh()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* ---------- 各步骤渲染 ---------- */
|
|
212
|
+
|
|
213
|
+
if (step === 'select') {
|
|
214
|
+
return (
|
|
215
|
+
<div className={css.viewBody}>
|
|
216
|
+
<SectionTitle title={t('import.select.title')} subtitle={t('import.select.hint')} />
|
|
217
|
+
<input
|
|
218
|
+
ref={fileInput}
|
|
219
|
+
type="file"
|
|
220
|
+
accept=".zip,application/zip"
|
|
221
|
+
className={css.hiddenFile}
|
|
222
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) => { void onPickFile(e.target.files?.[0]) }}
|
|
223
|
+
/>
|
|
224
|
+
<div className={css.actionRow}>
|
|
225
|
+
<Button variant="primary" disabled={uploading} onClick={() => { fileInput.current?.click() }}>
|
|
226
|
+
{uploading ? <Spinner label={t('import.analyzing')} /> : t('import.select.browse')}
|
|
227
|
+
</Button>
|
|
228
|
+
</div>
|
|
229
|
+
{error !== null && <ErrorBanner error={error} onRetry={resetWizard} />}
|
|
230
|
+
</div>
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (step === 'analyzing') {
|
|
235
|
+
return (
|
|
236
|
+
<div className={css.viewBody}>
|
|
237
|
+
<ProgressBar event={progress} active />
|
|
238
|
+
{error !== null && <ErrorBanner error={error} onRetry={resetWizard} />}
|
|
239
|
+
<ErrorList errors={snapshot.errors} />
|
|
240
|
+
</div>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (step === 'compatibility') {
|
|
245
|
+
const analysis = snapshot.analysis
|
|
246
|
+
if (analysis === null) return null
|
|
247
|
+
return (
|
|
248
|
+
<div className={css.viewBody}>
|
|
249
|
+
<SectionTitle title={t('import.compatibility.title')} />
|
|
250
|
+
<div className={css.statRow}>
|
|
251
|
+
<Badge kind={analysis.compatibility === 'unsupported' ? 'error' : analysis.compatibility === 'partial' ? 'warn' : 'ok'}>
|
|
252
|
+
{t('import.compatibility.score', { score: SCORE_LABEL[analysis.compatibility] ?? analysis.compatibility })}
|
|
253
|
+
</Badge>
|
|
254
|
+
<Badge kind="info">{analysis.sectionsInZip.length} sections</Badge>
|
|
255
|
+
<Badge kind="info">plugins: {analysis.pluginSummary.installed}✓ / {analysis.pluginSummary.toInstall}✗</Badge>
|
|
256
|
+
{analysis.pathIssues.length > 0 && <Badge kind="warn">{analysis.pathIssues.length} paths</Badge>}
|
|
257
|
+
{analysis.secretCount > 0 && <Badge kind="warn">{analysis.secretCount} secrets</Badge>}
|
|
258
|
+
</div>
|
|
259
|
+
{analysis.warnings.length > 0 && (
|
|
260
|
+
<Banner kind="warn">
|
|
261
|
+
{analysis.warnings.map((w, i) => <div key={i}>{w}</div>)}
|
|
262
|
+
</Banner>
|
|
263
|
+
)}
|
|
264
|
+
{error !== null && <ErrorBanner error={error} onRetry={() => { void goPreview() }} />}
|
|
265
|
+
<div className={css.actionRow}>
|
|
266
|
+
<Button variant="primary" onClick={() => { void goPreview() }}>{t('common.next')}</Button>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (step === 'preview' && phase === 'preview') {
|
|
273
|
+
const summary: ImportPreviewSummary = wizard.previewSummary()
|
|
274
|
+
return (
|
|
275
|
+
<div className={css.viewBody}>
|
|
276
|
+
<SectionTitle title={t('import.preview.title')} />
|
|
277
|
+
<div className={css.statRow}>
|
|
278
|
+
<Badge kind={summary.willChange > 0 ? 'info' : 'ok'}>{t('import.preview.willChange', { count: String(summary.willChange) })}</Badge>
|
|
279
|
+
{summary.unchanged > 0 && <Badge kind="ok">{t('import.preview.unchanged', { count: String(summary.unchanged) })}</Badge>}
|
|
280
|
+
{summary.settingsUpdates > 0 && <Badge kind="info">{t('import.preview.settings', { count: String(summary.settingsUpdates) })}</Badge>}
|
|
281
|
+
{summary.pluginsToInstall > 0 && <Badge kind="info">{t('import.preview.plugins', { count: String(summary.pluginsToInstall) })}</Badge>}
|
|
282
|
+
{summary.mcpAdds > 0 && <Badge kind="info">{t('import.preview.mcp', { count: String(summary.mcpAdds) })}</Badge>}
|
|
283
|
+
{summary.pathMappingsNeeded > 0 && <Badge kind="warn">{t('import.preview.paths', { count: String(summary.pathMappingsNeeded) })}</Badge>}
|
|
284
|
+
{summary.secretsNeeded > 0 && <Badge kind="warn">{t('import.preview.secrets', { count: String(summary.secretsNeeded) })}</Badge>}
|
|
285
|
+
{summary.conflicts > 0 && <Badge kind="error">{t('import.preview.conflicts', { count: String(summary.conflicts) })}</Badge>}
|
|
286
|
+
</div>
|
|
287
|
+
{summary.needsRestart && <Banner kind="warn">{t('import.preview.restart')}</Banner>}
|
|
288
|
+
{error !== null && <ErrorBanner error={error} onRetry={() => { void goPreview() }} />}
|
|
289
|
+
<div className={css.actionRow}>
|
|
290
|
+
<Button
|
|
291
|
+
variant="primary"
|
|
292
|
+
onClick={() => {
|
|
293
|
+
const next = nextPhase('preview')
|
|
294
|
+
setPhase(next)
|
|
295
|
+
if (next === 'conflicts') enterConflicts()
|
|
296
|
+
}}
|
|
297
|
+
>
|
|
298
|
+
{t('common.next')}
|
|
299
|
+
</Button>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (phase === 'conflicts') {
|
|
306
|
+
if (conflictCollector === null || snapshot.plan === null) return null
|
|
307
|
+
return (
|
|
308
|
+
<div className={css.viewBody}>
|
|
309
|
+
<SectionTitle title={t('import.conflicts.title')} subtitle={t('import.conflicts.hint')} />
|
|
310
|
+
<ConflictList collector={conflictCollector} t={t} onChanged={refresh} />
|
|
311
|
+
<div className={css.actionRow}>
|
|
312
|
+
<Button variant="ghost" onClick={() => { setPhase('preview') }}>{t('common.back')}</Button>
|
|
313
|
+
<Button variant="primary" disabled={conflictCollector.hasUnresolved} onClick={finishConflicts}>
|
|
314
|
+
{t('common.next')}
|
|
315
|
+
</Button>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (phase === 'path-mapping') {
|
|
322
|
+
const issues = snapshot.analysis?.pathIssues ?? []
|
|
323
|
+
return (
|
|
324
|
+
<div className={css.viewBody}>
|
|
325
|
+
<SectionTitle title={t('import.paths.title')} subtitle={t('import.paths.hint')} />
|
|
326
|
+
<PathMappingForm issues={issues} initial={pathMappings} t={t} onChange={setPathMappings} />
|
|
327
|
+
<div className={css.actionRow}>
|
|
328
|
+
<Button variant="ghost" onClick={() => { setPhase('preview') }}>{t('common.back')}</Button>
|
|
329
|
+
<Button variant="primary" onClick={finishPathMapping}>{t('common.next')}</Button>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (phase === 'secrets') {
|
|
336
|
+
const missing = snapshot.plan?.missingSecrets ?? []
|
|
337
|
+
return (
|
|
338
|
+
<div className={css.viewBody}>
|
|
339
|
+
<SectionTitle title={t('import.secrets.title')} />
|
|
340
|
+
<SecretsForm missing={missing} t={t} onChange={setSecretInputs} />
|
|
341
|
+
<div className={css.actionRow}>
|
|
342
|
+
<Button variant="ghost" onClick={() => { setPhase('preview') }}>{t('common.back')}</Button>
|
|
343
|
+
<Button variant="primary" onClick={finishSecrets}>{t('common.next')}</Button>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (phase === 'confirm') {
|
|
350
|
+
return (
|
|
351
|
+
<div className={css.viewBody}>
|
|
352
|
+
<Card className={css.optionsCard}>
|
|
353
|
+
<Banner kind="info">{t('import.confirm.warning')}</Banner>
|
|
354
|
+
<Checkbox
|
|
355
|
+
checked={rollbackOnError}
|
|
356
|
+
onChange={setRollbackOnError}
|
|
357
|
+
label={t('import.rollbackOnError')}
|
|
358
|
+
/>
|
|
359
|
+
<div className={css.actionRow}>
|
|
360
|
+
<Button variant="ghost" onClick={() => { setPhase('preview') }}>{t('common.back')}</Button>
|
|
361
|
+
<Button variant="primary" onClick={() => { void execute() }}>
|
|
362
|
+
{t('import.confirm.execute')}
|
|
363
|
+
</Button>
|
|
364
|
+
</div>
|
|
365
|
+
</Card>
|
|
366
|
+
{error !== null && <ErrorBanner error={error} onRetry={() => { void execute() }} />}
|
|
367
|
+
</div>
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (step === 'importing') {
|
|
372
|
+
return (
|
|
373
|
+
<div className={css.viewBody}>
|
|
374
|
+
<ProgressBar event={progress} active />
|
|
375
|
+
<div className={css.hint}>{t('import.importing')}</div>
|
|
376
|
+
{error !== null && <ErrorBanner error={error} />}
|
|
377
|
+
<ErrorList errors={snapshot.errors} />
|
|
378
|
+
</div>
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (step === 'result') {
|
|
383
|
+
const result = snapshot.result
|
|
384
|
+
if (result === null) return null
|
|
385
|
+
return (
|
|
386
|
+
<div className={css.viewBody}>
|
|
387
|
+
<SectionTitle title={t('report.import.title')} />
|
|
388
|
+
<ReportView
|
|
389
|
+
kind="import"
|
|
390
|
+
importResult={result}
|
|
391
|
+
onAction={(action) => {
|
|
392
|
+
if (action === 'done') resetWizard()
|
|
393
|
+
// fixIssues / viewDetails:报告已展示全部失败项与详情,无需额外页面
|
|
394
|
+
}}
|
|
395
|
+
/>
|
|
396
|
+
{result.needsRestart && <Banner kind="warn">{t('report.needsRestart')}</Banner>}
|
|
397
|
+
{error !== null && <ErrorBanner error={error} />}
|
|
398
|
+
<div className={css.actionRow}>
|
|
399
|
+
<Button variant="ghost" onClick={resetWizard}>{t('common.done')}</Button>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return null
|
|
406
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 路径映射表单(规范 §12,绑 src/ui/types.ts 的 PathMappingDraft 形状)。
|
|
3
|
+
*
|
|
4
|
+
* 注意:src/ui/path-mapping.ts 的 PathMappingEditor 依赖 utils/paths.ts 的
|
|
5
|
+
* applyPrefixMappings(node:path)——浏览器 bundle 不可用,故此处做轻量等价实现:
|
|
6
|
+
* 输入输出形状与 core 的 PathMapping 完全一致(oldPrefix/newPrefix/appliesTo),
|
|
7
|
+
* 实际前缀替换由 Host 侧 core 在 createImportPlan 阶段执行,本组件只负责收集用户输入。
|
|
8
|
+
*/
|
|
9
|
+
import { useMemo, useState } from 'react'
|
|
10
|
+
import type { ChangeEvent } from 'react'
|
|
11
|
+
import type { PathIssue, PathMapping } from '../../core/types.ts'
|
|
12
|
+
import type { TranslateNS } from '../client-types.ts'
|
|
13
|
+
import { Banner } from '../common/ui.tsx'
|
|
14
|
+
import css from '../config-manager.module.css'
|
|
15
|
+
|
|
16
|
+
export interface PathMappingFormProps {
|
|
17
|
+
/** 分析结果中的路径问题(每条生成一行映射输入) */
|
|
18
|
+
issues: PathIssue[]
|
|
19
|
+
/** 预置映射(如复用上次导入的映射) */
|
|
20
|
+
initial?: PathMapping[]
|
|
21
|
+
t: TranslateNS<'config-manager'>
|
|
22
|
+
/** 映射变化时上报(仅已填写 newPrefix 的条目进入 core) */
|
|
23
|
+
onChange: (mappings: PathMapping[]) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 路径映射表单:每条 PathIssue 一行(原路径 → 新路径输入框),
|
|
28
|
+
* 留空 = 该路径不映射(unresolved);填写 = 输出 core PathMapping[]。
|
|
29
|
+
*/
|
|
30
|
+
export function PathMappingForm({ issues, initial, t, onChange }: PathMappingFormProps) {
|
|
31
|
+
// draft 状态:issue.value(oldPrefix)→ newPrefix;预置映射合并进 initial 状态
|
|
32
|
+
const [drafts, setDrafts] = useState<Record<string, string>>(() => {
|
|
33
|
+
const out: Record<string, string> = {}
|
|
34
|
+
for (const m of initial ?? []) out[m.oldPrefix] = m.newPrefix
|
|
35
|
+
return out
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const setNewPrefix = (oldPrefix: string, value: string): void => {
|
|
39
|
+
const next = { ...drafts, [oldPrefix]: value }
|
|
40
|
+
setDrafts(next)
|
|
41
|
+
const mappings: PathMapping[] = Object.entries(next)
|
|
42
|
+
.filter(([, v]) => v !== '')
|
|
43
|
+
.map(([oldPrefix, newPrefix]) => ({ oldPrefix, newPrefix, appliesTo: [] }))
|
|
44
|
+
onChange(mappings)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const unresolved = useMemo(() => {
|
|
48
|
+
return issues.filter((issue) => (drafts[issue.value] ?? '') === '')
|
|
49
|
+
}, [issues, drafts])
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className={css.pathMappingList}>
|
|
53
|
+
{unresolved.length > 0 && (
|
|
54
|
+
<Banner kind="warn">{t('import.paths.unresolved', { count: String(unresolved.length) })}</Banner>
|
|
55
|
+
)}
|
|
56
|
+
{issues.length === 0 && <div className={css.empty}>No paths to map</div>}
|
|
57
|
+
{issues.map((issue) => (
|
|
58
|
+
<div key={issue.value} className={css.pathRow}>
|
|
59
|
+
<div className={css.pathOld}>
|
|
60
|
+
<span className={css.fieldLabel}>{t('import.paths.old')}</span>
|
|
61
|
+
<pre className={css.pathValue}>{issue.value}</pre>
|
|
62
|
+
<span className={css.pathIssueKind}>{issue.kind}</span>
|
|
63
|
+
</div>
|
|
64
|
+
<div className={css.pathNew}>
|
|
65
|
+
<span className={css.fieldLabel}>{t('import.paths.new')}</span>
|
|
66
|
+
<input
|
|
67
|
+
className={css.input}
|
|
68
|
+
value={drafts[issue.value] ?? ''}
|
|
69
|
+
placeholder={issue.mappedTo ?? ''}
|
|
70
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) => { setNewPrefix(issue.value, e.target.value) }}
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-config-manager 浏览器半入口 —— 运行在 dsh web GUI 内。
|
|
3
|
+
*
|
|
4
|
+
* 注册:
|
|
5
|
+
* 1. `config-manager` locale 字典(zh 主源 / en 镜像);
|
|
6
|
+
* 2. `settings.section` 设置页(id: config-manager, label: 备份与迁移 / Backup & Migration)。
|
|
7
|
+
*
|
|
8
|
+
* 挂载方式:settings.section 是官方 Slot(list/root,owner props 为 { close }),
|
|
9
|
+
* 经 `ctx.slots.inject(...)` 声明感知注册 —— 与 dsh-plugin-marketplace 注册
|
|
10
|
+
* `settings.plugins.tab`、ui-settings-general 注册 `settings.section` 完全同构
|
|
11
|
+
* (声明未到 ledger 前不注册,声明塌缩时自动移除,重声明后自动重挂)。
|
|
12
|
+
*
|
|
13
|
+
* 导出纪律(dsh-ssh packages/client 规则):/client 面只携带 cordis 装载所需 +
|
|
14
|
+
* 类型;所有值导出保持内部。
|
|
15
|
+
*/
|
|
16
|
+
import type { ClientContext } from './client-types.ts'
|
|
17
|
+
// Type-only:拉入 ctx.locale 的 Context 合并(dsh-client-locale)。
|
|
18
|
+
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
19
|
+
// Type-only:拉入 settings.section 的 SlotMap 合并(dsh-client-ui-settings)。
|
|
20
|
+
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
21
|
+
// Type-only:拉入 SlotMap / LocaleNamespaceMap 合并表(dsh-client-ui-slots)。
|
|
22
|
+
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
23
|
+
import { ConfigManagerApi } from './api.ts'
|
|
24
|
+
import { ConfigManagerSection } from './ConfigManagerSection.tsx'
|
|
25
|
+
import { en, zh, type ConfigManagerKey } from './locales.ts'
|
|
26
|
+
|
|
27
|
+
/** 本插件拥有的 locale namespace。 */
|
|
28
|
+
const NS = 'config-manager'
|
|
29
|
+
|
|
30
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
31
|
+
interface LocaleNamespaceMap {
|
|
32
|
+
/** Config Manager 表面文案。 */
|
|
33
|
+
'config-manager': ConfigManagerKey
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 必需服务(fiber inject 等待 —— slots/locale 必须先就绪)。 */
|
|
38
|
+
export const inject = ['slots', 'locale']
|
|
39
|
+
|
|
40
|
+
/** 类型面(导出纪律:除插件契约外无值导出)。 */
|
|
41
|
+
export type { ConfigManagerSectionProps } from './ConfigManagerSection.tsx'
|
|
42
|
+
export type { ExportViewProps } from './export/ExportView.tsx'
|
|
43
|
+
export type { ImportWizardViewProps } from './import/ImportWizardView.tsx'
|
|
44
|
+
export type { ConfigManagerApiError, DownloadResult, ServiceStatus, UploadResponse } from './api.ts'
|
|
45
|
+
export type { ConfigManagerKey } from './locales.ts'
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 注册 Config Manager 设置页。
|
|
49
|
+
* @param ctx - client root context(slots + locale 服务)。
|
|
50
|
+
*/
|
|
51
|
+
export function apply(ctx: ClientContext): void {
|
|
52
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'config-manager: dictionaries')
|
|
53
|
+
|
|
54
|
+
const t = ctx.locale.bind(NS)
|
|
55
|
+
const api = new ConfigManagerApi()
|
|
56
|
+
|
|
57
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
58
|
+
name: 'settings.section',
|
|
59
|
+
id: 'config-manager',
|
|
60
|
+
order: 60,
|
|
61
|
+
label: () => t('section.label'),
|
|
62
|
+
locale: NS,
|
|
63
|
+
inject: () => ({ api }),
|
|
64
|
+
}, ConfigManagerSection))
|
|
65
|
+
}
|