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
|
@@ -3,10 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 独立设置页壳(sectionHeader/close/自身 tab)已移除 —— tab 容器由
|
|
5
5
|
* ConfigManagerSection 统一渲染,本组件只输出内容体:
|
|
6
|
-
* -
|
|
7
|
-
*
|
|
6
|
+
* - **同步通道入口卡**:展示当前通道 + 配置状态 + 凭据徽章;点「配置同步通道」
|
|
7
|
+
* → 弹出**通道配置弹窗**(弹窗体系与市场操作弹窗一致,DESIGN.md §8.12:
|
|
8
|
+
* dialogMask + dialogCard dialogWide + dialogHeaderRow + dialogClose +
|
|
9
|
+
* dialogBodyScroll,零新增样式);
|
|
10
|
+
* - **通道配置弹窗**:通道子 tab(GitHub(git)/ WebDAV)切换,两个通道的
|
|
11
|
+
* 配置表单、自动同步、同步模式、是否加密、远端快照**各自独立**;关闭弹窗
|
|
12
|
+
* = 放弃本次操作(GitHub 登录流程进行中则一并取消,§8.12 约定);
|
|
8
13
|
* - GitHub 子 tab:repoUrl(必填)+ 认证 token(可选,写入 DSH credentials 的提示)
|
|
9
|
-
* + GitHub OAuth device flow
|
|
14
|
+
* + **GitHub OAuth device flow 登录**(登录块跟随 git 通道配置放在弹窗内:
|
|
15
|
+
* 未登录/失效时显示;git 可执行文件固定使用系统 PATH 中的 git);
|
|
10
16
|
* - WebDAV 子 tab:url + username + password(密码写入 DSH credentials)+ 常见服务器预设;
|
|
11
17
|
* - 私有仓库强制提示横幅(仅 git 子 tab 常驻);
|
|
12
18
|
* - 推送按钮 → SyncPushReport;拉取按钮 → SyncPullReport.changes 差异摘要;
|
|
@@ -29,7 +35,7 @@ import type { SyncPullReport, SyncPushReport } from '../../sync/sync-engine.ts'
|
|
|
29
35
|
import type { SectionId } from '../../schema/types.ts'
|
|
30
36
|
import { Badge, Banner, Button, Card, Checkbox, SectionTitle, Spinner } from '../common/ui.tsx'
|
|
31
37
|
import { ErrorBanner } from '../common/ErrorBanner.tsx'
|
|
32
|
-
import { runStore, toSyncStoreSlice, type SyncStoreSlice } from '../run-store.ts'
|
|
38
|
+
import { runStore, toSyncStoreSlice, type SyncConfirmDecisions, type SyncStoreSlice } from '../run-store.ts'
|
|
33
39
|
import { SYNC_CREDENTIAL_REF, SYNC_WEBDAV_CREDENTIAL_REF } from './sync-api.ts'
|
|
34
40
|
import type {
|
|
35
41
|
AutosyncInterval, AutosyncStatusResponse, SyncApi, SyncPushPayload, SyncSectionInfo, SyncSnapshotLite,
|
|
@@ -37,7 +43,7 @@ import type {
|
|
|
37
43
|
} from './sync-api.ts'
|
|
38
44
|
import {
|
|
39
45
|
autosyncIntervalMs, autosyncStatusText, channelTabModels, computeAutosyncCountdown,
|
|
40
|
-
computeGithubLoginView, computeRemoteReady, computeSyncButtons,
|
|
46
|
+
computeGithubLoginView, computeRemoteReady, computeSyncButtons,
|
|
41
47
|
defaultChannelSyncState, formatIntervalDuration, githubPollMessage, kindLabel, presetById,
|
|
42
48
|
presetIdForUrl, privateRepoHint, pullReportView, pushReportView, readStoredChannel,
|
|
43
49
|
recommendedSyncSections, severityLabel, syncSectionGroups, syncSectionOptions,
|
|
@@ -84,11 +90,20 @@ interface SyncUiState {
|
|
|
84
90
|
pullReport: SyncPullReport | null
|
|
85
91
|
/** 一键同步差异确认会话(POST /sync/sync 结果;非空时渲染 SyncConfirmView) */
|
|
86
92
|
confirmSession: SyncStartResponse | null
|
|
93
|
+
/** 一键同步差异确认的逐项决策(adopted/resolution;与 confirmSession 生命周期绑定,切 tab/刷新不丢) */
|
|
94
|
+
confirmDecisions: SyncConfirmDecisions | null
|
|
87
95
|
/** 最近一次一键同步执行结果(回滚入口) */
|
|
88
96
|
lastRestoreId: string | null
|
|
89
97
|
error: string | null
|
|
90
98
|
/** GitHub OAuth device flow 状态(flowId/userCode 仅内存,token 只存宿主) */
|
|
91
99
|
github: GithubUiState
|
|
100
|
+
/**
|
|
101
|
+
* GitHub token 是否有效(「已登录」判定:token 存在且 GitHub API 接受)。
|
|
102
|
+
* null = 尚未校验(不显示登录块,避免已登录用户看到闪烁);true = 已登录
|
|
103
|
+
* (隐藏 GitHub 登录块);false = 未配置或已失效(显示登录块)。
|
|
104
|
+
* 仅内存瞬态(不进 store 切片):切 tab/刷新后重新校验,保证新鲜。
|
|
105
|
+
*/
|
|
106
|
+
githubSignedIn: boolean | null
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
interface GithubUiState {
|
|
@@ -128,9 +143,11 @@ const initial: SyncUiState = {
|
|
|
128
143
|
pushReport: null,
|
|
129
144
|
pullReport: null,
|
|
130
145
|
confirmSession: null,
|
|
146
|
+
confirmDecisions: null,
|
|
131
147
|
lastRestoreId: null,
|
|
132
148
|
error: null,
|
|
133
149
|
github: initialGithub,
|
|
150
|
+
githubSignedIn: null,
|
|
134
151
|
}
|
|
135
152
|
|
|
136
153
|
/**
|
|
@@ -162,6 +179,7 @@ function initFromStore(): SyncUiState {
|
|
|
162
179
|
pushReport: s.pushReport,
|
|
163
180
|
pullReport: s.pullReport,
|
|
164
181
|
confirmSession: s.confirmSession,
|
|
182
|
+
confirmDecisions: s.confirmDecisions,
|
|
165
183
|
lastRestoreId: s.lastRestoreId,
|
|
166
184
|
error: s.error,
|
|
167
185
|
loadError: s.loadError,
|
|
@@ -175,6 +193,8 @@ export function SyncSettingsView({ api, t }: SyncSettingsViewProps) {
|
|
|
175
193
|
const stateRef = useRef<SyncUiState>(state)
|
|
176
194
|
/** 挂载守卫:卸载后不再 setState(store 镜像仍执行,异步结果照常落库) */
|
|
177
195
|
const mountedRef = useRef(true)
|
|
196
|
+
/** 通道配置弹窗开关(瞬态 UI:切 tab/刷新不持久化,弹窗不自动重开;DESIGN.md §8.12 约定) */
|
|
197
|
+
const [channelOpen, setChannelOpen] = useState(false)
|
|
178
198
|
|
|
179
199
|
/**
|
|
180
200
|
* 统一提交入口:更新 stateRef → 挂载时 setState → **总是**镜像进 runStore。
|
|
@@ -275,6 +295,8 @@ export function SyncSettingsView({ api, t }: SyncSettingsViewProps) {
|
|
|
275
295
|
if (info.configured && preset.trim() !== '') {
|
|
276
296
|
void loadSnapshots(preset, activeCh)
|
|
277
297
|
}
|
|
298
|
+
// 校验 GitHub token 有效性:已登录(有效)→ 隐藏 GitHub 登录块;未配置/失效 → 显示
|
|
299
|
+
void validateGithub()
|
|
278
300
|
} catch (err) {
|
|
279
301
|
patch({ loading: false, loadError: err instanceof Error ? err.message : String(err) })
|
|
280
302
|
}
|
|
@@ -436,6 +458,10 @@ export function SyncSettingsView({ api, t }: SyncSettingsViewProps) {
|
|
|
436
458
|
next.statusInfo = info
|
|
437
459
|
}
|
|
438
460
|
commit(next)
|
|
461
|
+
// 手动填入的 git token 保存成功 → 校验有效性(有效则隐藏 GitHub 登录块)
|
|
462
|
+
if (payloadToSave.transport !== 'webdav' && saved.credentialConfigured) {
|
|
463
|
+
void validateGithub()
|
|
464
|
+
}
|
|
439
465
|
} catch (err) {
|
|
440
466
|
patch({ savingConfig: false, error: err instanceof Error ? err.message : String(err) })
|
|
441
467
|
} finally {
|
|
@@ -515,8 +541,8 @@ export function SyncSettingsView({ api, t }: SyncSettingsViewProps) {
|
|
|
515
541
|
}
|
|
516
542
|
const message = githubPollMessage(poll, uiT)
|
|
517
543
|
if (poll.status === 'success') {
|
|
518
|
-
|
|
519
|
-
|
|
544
|
+
// token 已由宿主写入 DSH credentials:标记已登录(隐藏登录块)+ 刷新状态/凭据徽章
|
|
545
|
+
patch({ githubSignedIn: true, github: { ...stateRef.current.github, phase: 'success', error: null } })
|
|
520
546
|
void loadStatus()
|
|
521
547
|
} else {
|
|
522
548
|
patchGithub({ phase: 'error', error: message })
|
|
@@ -539,6 +565,43 @@ export function SyncSettingsView({ api, t }: SyncSettingsViewProps) {
|
|
|
539
565
|
}
|
|
540
566
|
}
|
|
541
567
|
|
|
568
|
+
/**
|
|
569
|
+
* 校验 GitHub token 是否有效(「已登录」判定 → 决定登录块显隐)。
|
|
570
|
+
* 挂载 / 登录成功 / 保存凭据后调用;已确认登录(githubSignedIn===true)时跳过
|
|
571
|
+
* (避免反复网络调用)。401 → 未登录:显示登录块并提示重新登录;网络等其余
|
|
572
|
+
* 错误 → 保持现状(不误判登出,已登录用户不被打扰;下次进入页面会再校验)。
|
|
573
|
+
*/
|
|
574
|
+
const validateGithub = async (): Promise<void> => {
|
|
575
|
+
if (stateRef.current.githubSignedIn === true) return
|
|
576
|
+
try {
|
|
577
|
+
const res = await api.githubValidate()
|
|
578
|
+
patch({ githubSignedIn: res.configured && res.valid })
|
|
579
|
+
} catch {
|
|
580
|
+
// 校验失败(网络/限流等):不可知 → 维持现状(null 隐藏 / 既有值不变)
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/* ------------------------------------------------ 通道配置弹窗(弹窗驱动,DESIGN.md §8.12 同体系) */
|
|
585
|
+
|
|
586
|
+
/** 打开通道配置弹窗:登录态尚未校验时补一次校验(决定 Git 子 tab 登录块显隐)。 */
|
|
587
|
+
const openChannelDialog = (): void => {
|
|
588
|
+
setChannelOpen(true)
|
|
589
|
+
if (stateRef.current.githubSignedIn === null) void validateGithub()
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* 关闭通道配置弹窗 = 放弃本次操作(§8.12 约定):GitHub 登录流程进行中则取消
|
|
594
|
+
* (停轮询 + 通知宿主丢弃设备码登记),保存中(savingConfig)时禁止关闭。
|
|
595
|
+
*/
|
|
596
|
+
const closeChannelDialog = (): void => {
|
|
597
|
+
if (stateRef.current.savingConfig) return
|
|
598
|
+
const phase = stateRef.current.github.phase
|
|
599
|
+
if (phase === 'starting' || phase === 'waiting' || phase === 'polling') {
|
|
600
|
+
void runGithubCancel()
|
|
601
|
+
}
|
|
602
|
+
setChannelOpen(false)
|
|
603
|
+
}
|
|
604
|
+
|
|
542
605
|
const runPush = async (): Promise<void> => {
|
|
543
606
|
patch({ busy: 'push', error: null, pushReport: null, pullReport: null })
|
|
544
607
|
try {
|
|
@@ -647,7 +710,7 @@ export function SyncSettingsView({ api, t }: SyncSettingsViewProps) {
|
|
|
647
710
|
if (state.confirmSession !== null) {
|
|
648
711
|
try { await api.cancel(state.confirmSession.syncSessionId) } catch { /* 尽力清理 */ }
|
|
649
712
|
}
|
|
650
|
-
patch({ busy: 'sync', error: null, confirmSession: null, lastRestoreId: null })
|
|
713
|
+
patch({ busy: 'sync', error: null, confirmSession: null, confirmDecisions: null, lastRestoreId: null })
|
|
651
714
|
try {
|
|
652
715
|
// 解密密码(可选):一键同步拉取加密快照时提供;仅内存传输
|
|
653
716
|
const decrypt =
|
|
@@ -661,7 +724,7 @@ export function SyncSettingsView({ api, t }: SyncSettingsViewProps) {
|
|
|
661
724
|
patch({ busy: null, error: session.message ?? t('syncflow.syncFailed') })
|
|
662
725
|
return
|
|
663
726
|
}
|
|
664
|
-
patch({ busy: null, confirmSession: session, token: '', webdavPassword: '' })
|
|
727
|
+
patch({ busy: null, confirmSession: session, confirmDecisions: null, token: '', webdavPassword: '' })
|
|
665
728
|
patchChannel({ decryptPassword: '' })
|
|
666
729
|
void loadSnapshots()
|
|
667
730
|
} catch (err) {
|
|
@@ -671,7 +734,7 @@ export function SyncSettingsView({ api, t }: SyncSettingsViewProps) {
|
|
|
671
734
|
|
|
672
735
|
/** 用户取消差异确认:清除会话,复位到空闲。 */
|
|
673
736
|
const cancelConfirm = (): void => {
|
|
674
|
-
patch({ confirmSession: null })
|
|
737
|
+
patch({ confirmSession: null, confirmDecisions: null })
|
|
675
738
|
}
|
|
676
739
|
|
|
677
740
|
/** 从 SyncConfirmView 透传的一键回滚完成信号。 */
|
|
@@ -715,7 +778,6 @@ export function SyncSettingsView({ api, t }: SyncSettingsViewProps) {
|
|
|
715
778
|
}
|
|
716
779
|
}
|
|
717
780
|
|
|
718
|
-
const status = computeSyncStatus(state.statusInfo, state.loading, state.loadError, uiT)
|
|
719
781
|
/** 活动通道的远端地址是否就绪(git=repoUrl 非空;webdav=webdavUrl 非空) */
|
|
720
782
|
const remoteReady = computeRemoteReady(state.channel, state.repoUrl, state.webdavUrl)
|
|
721
783
|
const buttons = computeSyncButtons(state.busy, remoteReady, uiT)
|
|
@@ -746,9 +808,59 @@ export function SyncSettingsView({ api, t }: SyncSettingsViewProps) {
|
|
|
746
808
|
<div className={css.viewBody}>
|
|
747
809
|
<SectionTitle title={t('section.label')} subtitle={t('section.description')} />
|
|
748
810
|
|
|
749
|
-
{/*
|
|
811
|
+
{/* 同步通道入口卡:通道配置改为弹窗驱动(点按钮 → 弹窗内配置 Git/WebDAV 通道;
|
|
812
|
+
弹窗样式复用市场操作弹窗体系,DESIGN.md §8.12) */}
|
|
813
|
+
<Card>
|
|
814
|
+
<span className={css.groupLabel}>{t('channel.title')}</span>
|
|
815
|
+
<span className={css.hint}>{t('channel.openHint')}</span>
|
|
816
|
+
<div className={css.statRow}>
|
|
817
|
+
<Badge kind="info">{state.channel === 'webdav' ? t('channel.webdav') : t('channel.git')}</Badge>
|
|
818
|
+
<Badge kind={remoteReady ? 'ok' : 'warn'}>
|
|
819
|
+
{remoteReady ? t('channel.configured') : t('channel.notConfigured')}
|
|
820
|
+
</Badge>
|
|
821
|
+
{state.channel === 'git' && state.statusInfo?.credentialConfigured === true && (
|
|
822
|
+
<Badge kind="ok">{t('config.tokenSaved')}</Badge>
|
|
823
|
+
)}
|
|
824
|
+
{state.channel === 'webdav' && state.statusInfo?.webdav?.passwordConfigured === true && (
|
|
825
|
+
<Badge kind="ok">{t('webdav.passwordSaved')}</Badge>
|
|
826
|
+
)}
|
|
827
|
+
</div>
|
|
828
|
+
{remoteReady && (
|
|
829
|
+
<span className={css.hint}>
|
|
830
|
+
{t('channel.currentUrl')}:{state.channel === 'webdav' ? state.webdavUrl : state.repoUrl}
|
|
831
|
+
</span>
|
|
832
|
+
)}
|
|
833
|
+
<div className={css.actionRow}>
|
|
834
|
+
<Button variant="primary" onClick={openChannelDialog}>
|
|
835
|
+
{t('channel.open')}
|
|
836
|
+
</Button>
|
|
837
|
+
</div>
|
|
838
|
+
</Card>
|
|
839
|
+
|
|
840
|
+
{/* 通道配置弹窗(操作弹窗体系:dialogMask + dialogCard dialogWide + 标题行 + 关闭 × + 正文限高内滚) */}
|
|
841
|
+
{channelOpen && (
|
|
842
|
+
<div
|
|
843
|
+
className={css.dialogMask}
|
|
844
|
+
onMouseDown={(e) => { if (e.target === e.currentTarget && !state.savingConfig) closeChannelDialog() }}
|
|
845
|
+
>
|
|
846
|
+
<div className={`${css.dialogCard} ${css.dialogWide}`} role="dialog" aria-modal="true" aria-label={t('channel.title')}>
|
|
847
|
+
<div className={css.dialogHeaderRow}>
|
|
848
|
+
<span className={css.dialogHeader}>{t('channel.title')}</span>
|
|
849
|
+
<button
|
|
850
|
+
type="button"
|
|
851
|
+
className={css.dialogClose}
|
|
852
|
+
aria-label={t('common.close')}
|
|
853
|
+
disabled={state.savingConfig}
|
|
854
|
+
onClick={closeChannelDialog}
|
|
855
|
+
>
|
|
856
|
+
×
|
|
857
|
+
</button>
|
|
858
|
+
</div>
|
|
859
|
+
<div className={css.dialogBodyScroll}>
|
|
860
|
+
|
|
861
|
+
{/* 通道子 tab:GitHub / WebDAV(modeTabs 样式;两通道设置各自独立) */}
|
|
750
862
|
<div className={css.modeTabs} role="tablist">
|
|
751
|
-
{channelTabModels(state.channel, state.busy !== null).map((tab) => (
|
|
863
|
+
{channelTabModels(state.channel, state.busy !== null || state.savingConfig).map((tab) => (
|
|
752
864
|
<button
|
|
753
865
|
key={tab.channel}
|
|
754
866
|
type="button"
|
|
@@ -768,11 +880,6 @@ export function SyncSettingsView({ api, t }: SyncSettingsViewProps) {
|
|
|
768
880
|
{/* 私有仓库强制提示:仅 git 通道适用 */}
|
|
769
881
|
{state.channel === 'git' && <Banner kind="warn">{privateRepoHint(uiT)}</Banner>}
|
|
770
882
|
|
|
771
|
-
{/* 通道配置(当前子 tab 的表单) */}
|
|
772
|
-
<Card>
|
|
773
|
-
<span className={css.groupLabel}>{t('channel.title')}</span>
|
|
774
|
-
<span className={css.hint}>{t('channel.hint')}</span>
|
|
775
|
-
|
|
776
883
|
{/* git 通道分支 */}
|
|
777
884
|
{state.channel === 'git' && (
|
|
778
885
|
<>
|
|
@@ -813,44 +920,51 @@ export function SyncSettingsView({ api, t }: SyncSettingsViewProps) {
|
|
|
813
920
|
<span className={css.hint}>{t('config.tokenHint', { ref: SYNC_CREDENTIAL_REF })}</span>
|
|
814
921
|
</label>
|
|
815
922
|
|
|
816
|
-
{/* GitHub OAuth 登录(device flow
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
<
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
923
|
+
{/* GitHub OAuth 登录(device flow):弹窗内仅 git 通道显示;已登录(token 有效)时整块隐藏 */}
|
|
924
|
+
{state.githubSignedIn === false && (
|
|
925
|
+
<>
|
|
926
|
+
{state.statusInfo?.credentialConfigured === true && (
|
|
927
|
+
<Banner kind="warn">{t('github.tokenInvalid')}</Banner>
|
|
928
|
+
)}
|
|
929
|
+
<span className={css.groupLabel}>{t('github.title')}</span>
|
|
930
|
+
<span className={css.hint}>{t('github.description')}</span>
|
|
931
|
+
{githubView.showCode && (
|
|
932
|
+
<div className={css.statRow}>
|
|
933
|
+
<Badge kind="info">{t('github.userCode')}:<strong>{githubView.userCode}</strong></Badge>
|
|
934
|
+
<a
|
|
935
|
+
className={css.ghostButton}
|
|
936
|
+
href={githubView.verificationUri}
|
|
937
|
+
target="_blank"
|
|
938
|
+
rel="noreferrer"
|
|
939
|
+
style={{ textDecoration: 'none' }}
|
|
940
|
+
>
|
|
941
|
+
{t('github.openAuth')}
|
|
942
|
+
</a>
|
|
943
|
+
</div>
|
|
944
|
+
)}
|
|
945
|
+
<div className={css.actionRow}>
|
|
946
|
+
<Button
|
|
947
|
+
variant="primary"
|
|
948
|
+
disabled={!githubView.canStart || state.busy !== null}
|
|
949
|
+
onClick={() => { void runGithubStart() }}
|
|
950
|
+
>
|
|
951
|
+
{githubView.startLabel}
|
|
952
|
+
</Button>
|
|
953
|
+
{githubView.canCancel && (
|
|
954
|
+
<Button disabled={state.busy !== null} onClick={() => { void runGithubCancel() }}>
|
|
955
|
+
{t('github.cancel')}
|
|
956
|
+
</Button>
|
|
957
|
+
)}
|
|
958
|
+
</div>
|
|
959
|
+
<div className={css.statRow}>
|
|
960
|
+
<Badge kind={githubView.phase === 'success' ? 'ok' : githubView.phase === 'error' ? 'error' : 'warn'}>
|
|
961
|
+
{githubView.statusText}
|
|
962
|
+
</Badge>
|
|
963
|
+
</div>
|
|
964
|
+
{githubView.phase === 'error' && (
|
|
965
|
+
<span className={css.hint}>{t('config.tokenHint', { ref: SYNC_CREDENTIAL_REF })}</span>
|
|
966
|
+
)}
|
|
967
|
+
</>
|
|
854
968
|
)}
|
|
855
969
|
</>
|
|
856
970
|
)}
|
|
@@ -944,16 +1058,10 @@ export function SyncSettingsView({ api, t }: SyncSettingsViewProps) {
|
|
|
944
1058
|
</Button>
|
|
945
1059
|
</div>
|
|
946
1060
|
<span className={css.hint}>{t('config.saveHint')}</span>
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
{/* 同步状态(当前子 tab 通道) */}
|
|
950
|
-
<Card>
|
|
951
|
-
<span className={css.groupLabel}>{t('status.title')}</span>
|
|
952
|
-
<div className={css.statRow}>
|
|
953
|
-
<Badge kind={status.kind === 'ready' ? 'ok' : status.kind === 'error' ? 'error' : 'warn'}>{status.text}</Badge>
|
|
954
|
-
<Badge kind="info">{state.channel === 'webdav' ? t('channel.webdav') : t('channel.git')}</Badge>
|
|
1061
|
+
</div>
|
|
1062
|
+
</div>
|
|
955
1063
|
</div>
|
|
956
|
-
|
|
1064
|
+
)}
|
|
957
1065
|
|
|
958
1066
|
{/* 同步模式(当前通道):默认(快速导出)/ 高级(自定义导出) */}
|
|
959
1067
|
<Card>
|
|
@@ -1154,6 +1262,8 @@ export function SyncSettingsView({ api, t }: SyncSettingsViewProps) {
|
|
|
1154
1262
|
needsReview={state.confirmSession.needsReview}
|
|
1155
1263
|
compatibility={state.confirmSession.compatibility}
|
|
1156
1264
|
t={t}
|
|
1265
|
+
decisions={state.confirmDecisions}
|
|
1266
|
+
onDecisionsChange={(d) => { patch({ confirmDecisions: d }) }}
|
|
1157
1267
|
onCancel={cancelConfirm}
|
|
1158
1268
|
onRollbackDone={onRollbackApplied}
|
|
1159
1269
|
/>
|
|
@@ -37,6 +37,7 @@ export const SYNC_API = {
|
|
|
37
37
|
githubStart: '/api/dsh-config-manager/sync/github/start',
|
|
38
38
|
githubPoll: '/api/dsh-config-manager/sync/github/poll',
|
|
39
39
|
githubCancel: '/api/dsh-config-manager/sync/github/cancel',
|
|
40
|
+
githubValidate: '/api/dsh-config-manager/sync/github/validate',
|
|
40
41
|
history: '/api/dsh-config-manager/sync/history',
|
|
41
42
|
snapshotsList: '/api/dsh-config-manager/sync/snapshots-list',
|
|
42
43
|
sync: '/api/dsh-config-manager/sync/sync',
|
|
@@ -377,6 +378,18 @@ export interface GithubPollResponse {
|
|
|
377
378
|
credentialConfigured?: boolean;
|
|
378
379
|
}
|
|
379
380
|
|
|
381
|
+
/** POST /sync/github/validate 响应:token 是否已配置 + 是否有效(GET /user;
|
|
382
|
+
* 401 → 无效)。只回布尔 + 登录名(非敏感),token 值永不回传浏览器。 */
|
|
383
|
+
export interface GithubValidateResponse {
|
|
384
|
+
ok: boolean;
|
|
385
|
+
/** token 是否存在于 DSH credentials */
|
|
386
|
+
configured: boolean;
|
|
387
|
+
/** token 是否有效(GitHub GET /user 成功;configured=false 时恒 false) */
|
|
388
|
+
valid: boolean;
|
|
389
|
+
/** GitHub 登录名(valid=true 时;非敏感展示位) */
|
|
390
|
+
login?: string;
|
|
391
|
+
}
|
|
392
|
+
|
|
380
393
|
/** 同步请求超时(ms):与 Host 半 ROUTE_TIMEOUT_MS 对齐(git 网络操作可能较慢) */
|
|
381
394
|
const SYNC_TIMEOUT_MS = 5 * 60 * 1000;
|
|
382
395
|
|
|
@@ -464,6 +477,12 @@ export class SyncApi {
|
|
|
464
477
|
return postJson<{ ok: boolean }>(SYNC_API.githubCancel, { flowId }, this.t);
|
|
465
478
|
}
|
|
466
479
|
|
|
480
|
+
/** GitHub token 有效性校验(判定「是否已登录」:token 存在且 GitHub API 接受;
|
|
481
|
+
* 401 → valid:false 引导重新登录;非 401 错误向上抛,UI 兜底不误判登出) */
|
|
482
|
+
async githubValidate(): Promise<GithubValidateResponse> {
|
|
483
|
+
return postJson<GithubValidateResponse>(SYNC_API.githubValidate, {}, this.t);
|
|
484
|
+
}
|
|
485
|
+
|
|
467
486
|
/** 同步历史:列出本地祖先快照 + 自动同步执行记录(按 createdAt 倒序合并)。 */
|
|
468
487
|
async history(): Promise<SyncHistoryResponse> {
|
|
469
488
|
const response = await fetch(SYNC_API.history);
|
|
@@ -24,10 +24,15 @@ export const zh = {
|
|
|
24
24
|
'config.saveHint': '表单改动会自动保存(密码/token 安全写入 DSH credentials,不会写入同步文件或日志);也可点击按钮立即保存。',
|
|
25
25
|
// 通道选择器
|
|
26
26
|
'channel.title': '同步通道',
|
|
27
|
-
'channel.hint': '选择远程同步通道:Git 私有仓库或 WebDAV 服务器。',
|
|
28
27
|
'channel.git': 'Git 私有仓库',
|
|
29
28
|
'channel.webdav': 'WebDAV 服务器',
|
|
30
29
|
'channel.perChannelHint': 'GitHub 与 WebDAV 通道的自动同步、同步模式、加密与远端快照各自独立配置。',
|
|
30
|
+
// 通道入口卡(弹窗驱动:点按钮 → 弹窗内配置通道)
|
|
31
|
+
'channel.open': '配置同步通道',
|
|
32
|
+
'channel.openHint': '远程同步通过「同步通道」进行:Git 私有仓库或 WebDAV 服务器。点击按钮在弹窗中配置或修改。',
|
|
33
|
+
'channel.configured': '已配置',
|
|
34
|
+
'channel.notConfigured': '未配置',
|
|
35
|
+
'channel.currentUrl': '当前地址',
|
|
31
36
|
// WebDAV 配置表单
|
|
32
37
|
'webdav.title': 'WebDAV 配置',
|
|
33
38
|
'webdav.url': '服务器地址',
|
|
@@ -45,6 +50,7 @@ export const zh = {
|
|
|
45
50
|
'github.login': '使用 GitHub 登录',
|
|
46
51
|
'github.retry': '重新登录',
|
|
47
52
|
'github.cancel': '取消',
|
|
53
|
+
'github.tokenInvalid': 'GitHub 登录已失效,请重新登录。',
|
|
48
54
|
'github.userCode': '一次性授权代码',
|
|
49
55
|
'github.openAuth': '打开 GitHub 授权页面',
|
|
50
56
|
'github.clientIdHint': '需要插件配置 githubClientId(GitHub OAuth App 的 client_id)才能使用 GitHub 登录。',
|
|
@@ -201,10 +207,15 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
201
207
|
'config.saving': 'Saving…',
|
|
202
208
|
'config.saveHint': 'Form changes are saved automatically (password/token written securely into DSH credentials, never into sync files or logs); you can also save explicitly with the button.',
|
|
203
209
|
'channel.title': 'Sync Channel',
|
|
204
|
-
'channel.hint': 'Choose the remote sync channel: Git private repository or WebDAV server.',
|
|
205
210
|
'channel.git': 'Git private repo',
|
|
206
211
|
'channel.webdav': 'WebDAV server',
|
|
207
212
|
'channel.perChannelHint': 'Auto sync, sync mode, encryption and remote snapshots are configured independently for GitHub and WebDAV channels.',
|
|
213
|
+
// Channel entry card (dialog-driven: click the button → configure the channel in the dialog)
|
|
214
|
+
'channel.open': 'Configure sync channel',
|
|
215
|
+
'channel.openHint': 'Remote sync runs through a sync channel: a private Git repository or a WebDAV server. Click the button to configure or change it in the dialog.',
|
|
216
|
+
'channel.configured': 'Configured',
|
|
217
|
+
'channel.notConfigured': 'Not configured',
|
|
218
|
+
'channel.currentUrl': 'Current URL',
|
|
208
219
|
'webdav.title': 'WebDAV Configuration',
|
|
209
220
|
'webdav.url': 'Server URL',
|
|
210
221
|
'webdav.urlHint': 'WebDAV server root URL (https://…). Sync snapshots and the index are stored under a snapshots/ subdirectory of that URL. Do not include a username/password in the URL.',
|
|
@@ -220,6 +231,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
220
231
|
'github.login': 'Sign in with GitHub',
|
|
221
232
|
'github.retry': 'Sign in again',
|
|
222
233
|
'github.cancel': 'Cancel',
|
|
234
|
+
'github.tokenInvalid': 'GitHub sign-in is no longer valid — please sign in again.',
|
|
223
235
|
'github.userCode': 'One-time code',
|
|
224
236
|
'github.openAuth': 'Open GitHub authorization page',
|
|
225
237
|
'github.clientIdHint': 'Requires the githubClientId plugin config (the client_id of your GitHub OAuth App).',
|