dsh-config-manager 0.1.39 → 0.1.41
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/lib/client.d.ts +90 -8
- package/lib/client.js +1904 -1086
- package/lib/client.js.map +1 -1
- package/lib/core/cache-cleaner.d.ts +39 -0
- package/lib/core/cache-cleaner.js +164 -0
- package/lib/core/cache-cleaner.js.map +1 -0
- package/lib/index.js +172 -3
- package/lib/index.js.map +1 -1
- package/lib/market/github-repos.d.ts +19 -8
- package/lib/market/github-repos.js +62 -33
- package/lib/market/github-repos.js.map +1 -1
- package/lib/market/my-repo.d.ts +111 -3
- package/lib/market/my-repo.js +309 -26
- package/lib/market/my-repo.js.map +1 -1
- package/lib/market/prepare.js +3 -2
- package/lib/market/prepare.js.map +1 -1
- package/lib/market/types.d.ts +2 -0
- package/lib/security/secret-scanner.d.ts +18 -3
- package/lib/security/secret-scanner.js +144 -20
- package/lib/security/secret-scanner.js.map +1 -1
- package/package.json +1 -1
- package/src/client/common/ConfirmDialog.tsx +135 -0
- package/src/client/config-manager.module.css +101 -0
- package/src/client/market/MarketPanel.tsx +138 -45
- package/src/client/market/MyConfigsView.tsx +515 -132
- package/src/client/market/disclaimer.test.ts +57 -0
- package/src/client/market/disclaimer.ts +43 -0
- package/src/client/market/market-api.test.ts +3 -0
- package/src/client/market/market-locales.ts +37 -13
- package/src/client/market/my-configs-api.ts +20 -2
- package/src/client/market/my-configs-view.test.ts +137 -2
- package/src/client/market/my-configs-view.ts +174 -2
- package/src/client/run-store.test.ts +40 -2
- package/src/client/run-store.ts +70 -10
- package/src/client/sync/SyncConfirmView.tsx +28 -22
- package/src/client/sync/SyncSettingsView.tsx +175 -65
- package/src/client/sync/sync-api.ts +19 -0
- package/src/client/sync/sync-locales.ts +14 -2
- package/src/core/cache-cleaner.test.ts +333 -0
- package/src/core/cache-cleaner.ts +196 -0
- package/src/index.ts +165 -5
- package/src/market/github-repos.test.ts +99 -8
- package/src/market/github-repos.ts +67 -35
- package/src/market/my-repo.test.ts +218 -27
- package/src/market/my-repo.ts +403 -30
- package/src/market/prepare.test.ts +71 -1
- package/src/market/prepare.ts +3 -2
- package/src/market/types.ts +2 -0
- package/src/security/secret-scanner.ts +139 -22
- package/src/security/security.test.ts +111 -2
|
@@ -24,20 +24,25 @@ import type { TranslateNS } from '../client-types.ts'
|
|
|
24
24
|
import type { ConfigManagerApi } from '../api.ts'
|
|
25
25
|
import type { ImportResult, ImportPlan } from '../../core/types.ts'
|
|
26
26
|
import { Badge, Banner, Button, Card, Empty, SectionTitle, Spinner } from '../common/ui.tsx'
|
|
27
|
-
import { BUILTIN_MARKET_URL
|
|
27
|
+
import { BUILTIN_MARKET_URL } from '../../market/builtin.ts'
|
|
28
28
|
import type { MarketApi } from './market-api.ts'
|
|
29
29
|
import type { MyConfigsApi } from './my-configs-api.ts'
|
|
30
30
|
import type { SyncApi } from '../sync/sync-api.ts'
|
|
31
31
|
import { MyConfigsView } from './MyConfigsView.tsx'
|
|
32
32
|
import type { MyItemEntry } from './my-configs-api.ts'
|
|
33
33
|
import type {
|
|
34
|
-
MarketBrowseResponse, MarketDownloadResult, MarketListItem,
|
|
34
|
+
MarketBrowseResponse, MarketDownloadResult, MarketListItem, MarketStatusResponse,
|
|
35
35
|
} from '../../market/types.ts'
|
|
36
36
|
import {
|
|
37
37
|
approvalRows, approvedAdapterSummary, buildApprovedPlan, collectCategories, defaultApprovals,
|
|
38
|
-
filterMarketItems, marketDetailView, marketListSummary,
|
|
38
|
+
filterMarketItems, marketDetailView, marketListSummary, sourceBadgeKind,
|
|
39
39
|
} from './market-view.ts'
|
|
40
40
|
import type { MarketApprovals } from './market-view.ts'
|
|
41
|
+
import type { MyInstallSlice, MyWizardSlice } from './my-configs-view.ts'
|
|
42
|
+
import { readDisclaimerDismissed, writeDisclaimerDismissed } from './disclaimer.ts'
|
|
43
|
+
import type { DisclaimerKey } from './disclaimer.ts'
|
|
44
|
+
import { ConfirmDialog } from '../common/ConfirmDialog.tsx'
|
|
45
|
+
import { redact } from '../../security/redaction.ts'
|
|
41
46
|
import { runStore, toMarketStoreSlice, type MarketStoreSlice } from '../run-store.ts'
|
|
42
47
|
import css from '../config-manager.module.css'
|
|
43
48
|
|
|
@@ -59,10 +64,14 @@ interface MarketUiState {
|
|
|
59
64
|
myItems: MyItemEntry[] | null
|
|
60
65
|
/** 我的配置:列表加载错误(已 redact;null = 无) */
|
|
61
66
|
myItemsError: string | null
|
|
67
|
+
/** 我的配置:上传/更新向导持久化切片(非敏感;null = 未开始;镜像 runStore,切 tab/刷新不丢) */
|
|
68
|
+
myWizard: MyWizardSlice | null
|
|
69
|
+
/** 我的配置:装回本地(下载+逐分区批准+导入结果)持久化切片(非敏感;null = 未开始/已关闭) */
|
|
70
|
+
myInstall: MyInstallSlice | null
|
|
71
|
+
/** 我的配置:删除确认弹窗目标条目 id(非敏感;镜像 runStore,切 tab/刷新不丢) */
|
|
72
|
+
myConfirmDeleteId: string | null
|
|
62
73
|
loading: boolean
|
|
63
74
|
loadError: string | null
|
|
64
|
-
/** 内置市场摘要(status 返回;条目数 / 名称 / 最近拉取) */
|
|
65
|
-
market: MarketSummary | null
|
|
66
75
|
refreshing: boolean
|
|
67
76
|
browsing: boolean
|
|
68
77
|
items: MarketListItem[]
|
|
@@ -86,9 +95,11 @@ const initial: MarketUiState = {
|
|
|
86
95
|
subView: 'browse',
|
|
87
96
|
myItems: null,
|
|
88
97
|
myItemsError: null,
|
|
98
|
+
myWizard: null,
|
|
99
|
+
myInstall: null,
|
|
100
|
+
myConfirmDeleteId: null,
|
|
89
101
|
loading: true,
|
|
90
102
|
loadError: null,
|
|
91
|
-
market: null,
|
|
92
103
|
refreshing: false,
|
|
93
104
|
browsing: false,
|
|
94
105
|
items: [],
|
|
@@ -114,6 +125,9 @@ function initFromStore(): MarketUiState {
|
|
|
114
125
|
subView: s.subView,
|
|
115
126
|
myItems: s.myItems,
|
|
116
127
|
myItemsError: s.myItemsError,
|
|
128
|
+
myWizard: s.myWizard,
|
|
129
|
+
myInstall: s.myInstall,
|
|
130
|
+
myConfirmDeleteId: s.myConfirmDeleteId,
|
|
117
131
|
search: s.search,
|
|
118
132
|
category: s.category,
|
|
119
133
|
items: s.items,
|
|
@@ -145,30 +159,83 @@ export function MarketPanel({ api, myConfigsApi, importApi, syncApi, t }: Market
|
|
|
145
159
|
}
|
|
146
160
|
const patch = (p: Partial<MarketUiState>): void => commit({ ...stateRef.current, ...p })
|
|
147
161
|
|
|
162
|
+
/* ---------------- 下载弹窗 + 免责前置(2026-08-21) ---------------- */
|
|
163
|
+
/** 下载详情弹窗开关(瞬态 UI,不持久化) */
|
|
164
|
+
const [downloadOpen, setDownloadOpen] = useState(false)
|
|
165
|
+
/** 当前展示的免责弹窗操作(null = 无) */
|
|
166
|
+
const [disclaimerKey, setDisclaimerKey] = useState<DisclaimerKey | null>(null)
|
|
167
|
+
/** 免责弹窗「不再提示」勾选(每次打开重置) */
|
|
168
|
+
const [dontAsk, setDontAsk] = useState(false)
|
|
169
|
+
/** localStorage(浏览器环境;免责「不再提示」跨会话持久化) */
|
|
170
|
+
const storage: Pick<Storage, 'getItem' | 'setItem'> = window.localStorage
|
|
171
|
+
|
|
172
|
+
/** 待下载条目(免责确认后取用;避免免责流程中闭包过期) */
|
|
173
|
+
const pendingDownloadItem = useRef<MarketListItem | null>(null)
|
|
174
|
+
/** 点条目「查看详情」:未勾「不再提示」→ 先弹免责,确认后下载并打开详情弹窗 */
|
|
175
|
+
const openDownload = (item: MarketListItem): void => {
|
|
176
|
+
pendingDownloadItem.current = item
|
|
177
|
+
if (readDisclaimerDismissed('download', storage)) {
|
|
178
|
+
setDownloadOpen(true)
|
|
179
|
+
void runDownload(item)
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
setDontAsk(false)
|
|
183
|
+
setDisclaimerKey('download')
|
|
184
|
+
}
|
|
185
|
+
/** 免责弹窗确认:勾选则记录「不再提示」→ 关闭免责 → 打开下载详情弹窗并启动下载 */
|
|
186
|
+
const confirmDownloadDisclaimer = (): void => {
|
|
187
|
+
if (disclaimerKey !== 'download') return
|
|
188
|
+
if (dontAsk) writeDisclaimerDismissed('download', storage)
|
|
189
|
+
setDisclaimerKey(null)
|
|
190
|
+
const item = pendingDownloadItem.current
|
|
191
|
+
setDownloadOpen(true)
|
|
192
|
+
if (item !== null) void runDownload(item)
|
|
193
|
+
}
|
|
194
|
+
/** 关闭下载详情弹窗:清 detail(弹窗即会话,关闭即放弃) */
|
|
195
|
+
const closeDownload = (): void => {
|
|
196
|
+
setDownloadOpen(false)
|
|
197
|
+
patch({ detail: null, importResult: null, error: null, approvals: {} })
|
|
198
|
+
}
|
|
199
|
+
|
|
148
200
|
/** 卸载时置挂载守卫 + 最后镜像一次(防止「最后一次改动后立即切 tab」时丢状态)。 */
|
|
149
201
|
useEffect(() => () => {
|
|
150
202
|
mountedRef.current = false
|
|
151
203
|
runStore.patch({ market: toMarketStoreSlice(stateRef.current) })
|
|
152
204
|
}, [])
|
|
153
205
|
|
|
154
|
-
/** 内置市场 URL(单一权威;来自 Host
|
|
206
|
+
/** 内置市场 URL(单一权威;来自 Host 内置常量;用于条目来源判定与详情供应链警示) */
|
|
155
207
|
const marketUrl: string = BUILTIN_MARKET_URL
|
|
156
|
-
const official: boolean = isOfficialMarket(marketUrl)
|
|
157
208
|
|
|
158
|
-
/**
|
|
159
|
-
const loadStatus = useCallback(async (): Promise<
|
|
209
|
+
/** 挂载时读取内置市场状态;返回响应供「首次打开自动更新」判据(bootAutoRefreshed) */
|
|
210
|
+
const loadStatus = useCallback(async (): Promise<MarketStatusResponse | null> => {
|
|
160
211
|
patch({ loading: true, loadError: null })
|
|
161
212
|
try {
|
|
162
213
|
const info = await api.status()
|
|
163
|
-
patch({ loading: false
|
|
214
|
+
patch({ loading: false })
|
|
215
|
+
return info
|
|
164
216
|
} catch (err) {
|
|
165
217
|
patch({ loading: false, loadError: err instanceof Error ? err.message : String(err) })
|
|
218
|
+
return null
|
|
166
219
|
}
|
|
167
220
|
}, [api])
|
|
168
221
|
|
|
222
|
+
/**
|
|
223
|
+
* 启动后首次打开市场页 → 自动拉取一次最新 index(需求:dsh 启动后第一次打开市场页面自动更新一次市场)。
|
|
224
|
+
* 判据是 Host 侧进程内存标记 bootAutoRefreshed(dsh 重启后归零;refresh 成功后置位),
|
|
225
|
+
* 因此「每次打开都刷新」/「刷新失败后下次打开重试」都自然成立;无需客户端持久化。
|
|
226
|
+
* bootAutoChecked 同步置位防 StrictMode 双执行/竞态重复触发(同一组件实例只自动刷一次)。
|
|
227
|
+
*/
|
|
228
|
+
const bootAutoChecked = useRef(false)
|
|
169
229
|
useEffect(() => {
|
|
170
|
-
|
|
171
|
-
|
|
230
|
+
if (bootAutoChecked.current) return
|
|
231
|
+
bootAutoChecked.current = true
|
|
232
|
+
void (async () => {
|
|
233
|
+
const info = await loadStatus()
|
|
234
|
+
if (info !== null && info.bootAutoRefreshed !== true) {
|
|
235
|
+
await runRefresh()
|
|
236
|
+
}
|
|
237
|
+
})()
|
|
238
|
+
// api 为注入单例(注册时创建),生命周期内稳定;仅挂载时执行一次
|
|
172
239
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
173
240
|
}, [])
|
|
174
241
|
|
|
@@ -233,8 +300,6 @@ export function MarketPanel({ api, myConfigsApi, importApi, syncApi, t }: Market
|
|
|
233
300
|
}
|
|
234
301
|
|
|
235
302
|
// ---- 渲染模型装配(全部纯函数,node 已测) ----
|
|
236
|
-
// 状态行(共享 marketStatusText:恒 configured 单市场);loading/error 由 spinner/banner 单独呈现
|
|
237
|
-
const statusText = marketStatusText({ count: 1 }, uiT)
|
|
238
303
|
const filtered = filterMarketItems(state.items, state.search, state.category)
|
|
239
304
|
const summary = marketListSummary(state.items, uiT)
|
|
240
305
|
const categories = collectCategories(state.items)
|
|
@@ -285,29 +350,20 @@ export function MarketPanel({ api, myConfigsApi, importApi, syncApi, t }: Market
|
|
|
285
350
|
myItems={state.myItems}
|
|
286
351
|
myItemsError={state.myItemsError}
|
|
287
352
|
onMyItemsChange={(items, error) => { patch({ myItems: items, myItemsError: error }) }}
|
|
288
|
-
|
|
353
|
+
myWizard={state.myWizard}
|
|
354
|
+
onMyWizardChange={(wizard) => { patch({ myWizard: wizard }) }}
|
|
355
|
+
myInstall={state.myInstall}
|
|
356
|
+
onMyInstallChange={(install) => { patch({ myInstall: install }) }}
|
|
357
|
+
myConfirmDeleteId={state.myConfirmDeleteId}
|
|
358
|
+
onMyConfirmDeleteChange={(id) => { patch({ myConfirmDeleteId: id }) }}
|
|
289
359
|
/>
|
|
290
360
|
) : (
|
|
291
361
|
<>
|
|
292
362
|
<SectionTitle title={t('section.label')} subtitle={t('section.description')} />
|
|
293
363
|
|
|
294
|
-
{/*
|
|
364
|
+
{/* 内置市场操作卡:保留面板标题;移除 URL / 官方徽章 / 名称 / 条目数 / 状态行等文字展示 */}
|
|
295
365
|
<Card>
|
|
296
366
|
<span className={css.groupLabel}>{t('config.title')}</span>
|
|
297
|
-
<span className={css.hint}>{t('config.builtinHint')}</span>
|
|
298
|
-
<div className={css.statRow} style={{ paddingTop: 4 }}>
|
|
299
|
-
<Badge kind="info">{marketUrl}</Badge>
|
|
300
|
-
{official && <Badge kind="ok">{t('config.official')}</Badge>}
|
|
301
|
-
{!official && <Badge kind="warn">{t('config.custom')}</Badge>}
|
|
302
|
-
</div>
|
|
303
|
-
{state.market !== null && (state.market.name || state.market.itemCount !== undefined) && (
|
|
304
|
-
<div className={css.statRow}>
|
|
305
|
-
{state.market.name !== undefined && <Badge kind="info">{state.market.name}</Badge>}
|
|
306
|
-
{state.market.itemCount !== undefined && (
|
|
307
|
-
<Badge kind="info">{t('list.count', { count: String(state.market.itemCount) })}</Badge>
|
|
308
|
-
)}
|
|
309
|
-
</div>
|
|
310
|
-
)}
|
|
311
367
|
<div className={css.actionRow}>
|
|
312
368
|
<Button variant="primary" disabled={state.refreshing || state.importing} onClick={() => { void runRefresh() }}>
|
|
313
369
|
{state.refreshing ? <Spinner label={t('config.refreshing')} /> : t('config.refresh')}
|
|
@@ -316,21 +372,36 @@ export function MarketPanel({ api, myConfigsApi, importApi, syncApi, t }: Market
|
|
|
316
372
|
{state.browsing ? <Spinner label={t('list.loading')} /> : t('list.browse')}
|
|
317
373
|
</Button>
|
|
318
374
|
</div>
|
|
319
|
-
<div className={css.statRow}>
|
|
320
|
-
<Badge kind={state.loadError !== null ? 'error' : 'ok'}>{statusText}</Badge>
|
|
321
|
-
</div>
|
|
322
375
|
</Card>
|
|
323
376
|
|
|
324
377
|
{state.error !== null && <Banner kind="error">{state.error}</Banner>}
|
|
325
378
|
|
|
326
|
-
{/*
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
379
|
+
{/* 条目详情弹窗(下载 + 校验 + dry-run 预览;点「查看详情」→ 免责 → 弹窗;
|
|
380
|
+
下载完成前 detail 为 null → 显示 loading) */}
|
|
381
|
+
{downloadOpen && (
|
|
382
|
+
<div
|
|
383
|
+
className={css.dialogMask}
|
|
384
|
+
onMouseDown={(e) => { if (e.target === e.currentTarget && !state.importing) closeDownload() }}
|
|
385
|
+
>
|
|
386
|
+
<div className={`${css.dialogCard} ${css.dialogWide}`} role="dialog" aria-modal="true" aria-label={t('detail.title')}>
|
|
387
|
+
<div className={css.dialogHeaderRow}>
|
|
388
|
+
<span className={css.dialogHeader}>{t('detail.title')}:{state.detail !== null ? state.detail.name : (state.downloadingId ?? '')}</span>
|
|
389
|
+
<button
|
|
390
|
+
type="button"
|
|
391
|
+
className={css.dialogClose}
|
|
392
|
+
aria-label={t('common.close')}
|
|
393
|
+
disabled={state.importing}
|
|
394
|
+
onClick={closeDownload}
|
|
395
|
+
>
|
|
396
|
+
×
|
|
397
|
+
</button>
|
|
398
|
+
</div>
|
|
399
|
+
<div className={css.dialogBodyScroll}>
|
|
400
|
+
{state.error !== null && <Banner kind="error">{redact(state.error)}</Banner>}
|
|
401
|
+
{state.detail === null && (
|
|
402
|
+
<div className={css.statRow}><Spinner label={t('common.loading')} /></div>
|
|
403
|
+
)}
|
|
404
|
+
{state.detail !== null && detailView !== null && (<>
|
|
334
405
|
{/* 供应链警示:恒展示(硬约束),确认导入前必经 */}
|
|
335
406
|
<Banner kind="warn">
|
|
336
407
|
<strong>{t('detail.needReview')}</strong>
|
|
@@ -423,11 +494,14 @@ export function MarketPanel({ api, myConfigsApi, importApi, syncApi, t }: Market
|
|
|
423
494
|
{state.importResult.needsRestart && ' · 部分改动需重启 DSH 后生效'}
|
|
424
495
|
</Banner>
|
|
425
496
|
)}
|
|
426
|
-
|
|
497
|
+
</>)}
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
427
501
|
)}
|
|
428
502
|
|
|
429
503
|
{/* 条目列表(浏览) */}
|
|
430
|
-
{
|
|
504
|
+
{!downloadOpen && (
|
|
431
505
|
<Card>
|
|
432
506
|
{state.loadError !== null && <Empty>{t('list.empty')}</Empty>}
|
|
433
507
|
{state.loadError === null && (
|
|
@@ -489,7 +563,7 @@ export function MarketPanel({ api, myConfigsApi, importApi, syncApi, t }: Market
|
|
|
489
563
|
</Badge>
|
|
490
564
|
</div>
|
|
491
565
|
</div>
|
|
492
|
-
<Button disabled={state.downloadingId === it.id} onClick={() => {
|
|
566
|
+
<Button disabled={state.downloadingId === it.id} onClick={() => { openDownload(it) }}>
|
|
493
567
|
{state.downloadingId === it.id ? <Spinner label={t('common.loading')} /> : t('list.download')}
|
|
494
568
|
</Button>
|
|
495
569
|
</div>
|
|
@@ -499,6 +573,25 @@ export function MarketPanel({ api, myConfigsApi, importApi, syncApi, t }: Market
|
|
|
499
573
|
)}
|
|
500
574
|
</Card>
|
|
501
575
|
)}
|
|
576
|
+
{/* 免责弹窗(复用 ConfirmDialog + 「不再提示」勾选;download 操作) */}
|
|
577
|
+
<ConfirmDialog
|
|
578
|
+
open={disclaimerKey === 'download'}
|
|
579
|
+
title={t('disclaimer.title')}
|
|
580
|
+
message={t('disclaimer.download.text')}
|
|
581
|
+
confirmLabel={t('disclaimer.confirm')}
|
|
582
|
+
cancelLabel={t('common.cancel')}
|
|
583
|
+
onConfirm={confirmDownloadDisclaimer}
|
|
584
|
+
onCancel={() => { setDisclaimerKey(null) }}
|
|
585
|
+
>
|
|
586
|
+
<label className={css.checkboxRow}>
|
|
587
|
+
<input
|
|
588
|
+
type="checkbox"
|
|
589
|
+
checked={dontAsk}
|
|
590
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) => { setDontAsk(e.target.checked) }}
|
|
591
|
+
/>
|
|
592
|
+
<span>{t('disclaimer.dontAsk')}</span>
|
|
593
|
+
</label>
|
|
594
|
+
</ConfirmDialog>
|
|
502
595
|
</>
|
|
503
596
|
)}
|
|
504
597
|
</div>
|