From 455813e53ce76663806c9c28d5b8a48e673b7864 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Thu, 4 Jun 2026 16:57:00 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(driver):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E9=A9=B1=E5=8A=A8=E5=AE=89=E8=A3=85=E8=BF=9B=E5=BA=A6?= =?UTF-8?q?=E5=9B=9E=E9=80=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 驱动安装进度按单次会话做单调归一,避免旧下载事件覆盖新进度。 有单文件预编译资产时跳过驱动总包兜底,减少进度回退和安装失败面。 补充前端进度状态机与后端总包兜底回归测试。 --- .../src/components/DriverManagerModal.tsx | 100 ++++++++++-------- frontend/src/utils/driverProgress.test.ts | 82 ++++++++++++++ frontend/src/utils/driverProgress.ts | 59 +++++++++++ internal/app/methods_driver.go | 12 ++- internal/app/methods_driver_version_test.go | 15 +++ 5 files changed, 223 insertions(+), 45 deletions(-) create mode 100644 frontend/src/utils/driverProgress.test.ts create mode 100644 frontend/src/utils/driverProgress.ts diff --git a/frontend/src/components/DriverManagerModal.tsx b/frontend/src/components/DriverManagerModal.tsx index 4bdbbbf..8efb601 100644 --- a/frontend/src/components/DriverManagerModal.tsx +++ b/frontend/src/components/DriverManagerModal.tsx @@ -4,6 +4,7 @@ import { DeleteOutlined, DownloadOutlined, FileSearchOutlined, FolderOpenOutline import { EventsOn } from '../../wailsjs/runtime/runtime'; import { useStore } from '../store'; import { normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance'; +import { normalizeDriverProgressUpdate, type DriverProgressState } from '../utils/driverProgress'; import { buildDriverManagerWorkbenchTheme } from '../utils/driverManagerWorkbenchTheme'; import { DRIVER_LOCAL_IMPORT_BUTTON_LABEL, @@ -55,12 +56,6 @@ type DriverProgressEvent = { percent?: number; }; -type ProgressState = { - status: 'start' | 'downloading' | 'done' | 'error'; - message: string; - percent: number; -}; - type DriverActionKind = '' | 'install' | 'remove' | 'local'; type DriverBatchActionKind = '' | 'install-all' | 'reinstall-updates' | 'remove-all'; @@ -227,7 +222,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG const [actionState, setActionState] = useState<{ driverType: string; kind: DriverActionKind }>({ driverType: '', kind: '' }); const [batchAction, setBatchAction] = useState(''); const [batchProgress, setBatchProgress] = useState(null); - const [progressMap, setProgressMap] = useState>({}); + const [progressMap, setProgressMap] = useState>({}); const [operationLogMap, setOperationLogMap] = useState>({}); const [logDriverType, setLogDriverType] = useState(''); const [logModalOpen, setLogModalOpen] = useState(false); @@ -238,12 +233,38 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG const [versionLoadingMap, setVersionLoadingMap] = useState>({}); const [versionSizeLoadingMap, setVersionSizeLoadingMap] = useState>({}); const downloadDirRef = useRef(downloadDir); + const progressMapRef = useRef>({}); const batchBusy = batchDirectoryImporting || batchAction !== '' || actionState.kind !== ''; useEffect(() => { downloadDirRef.current = downloadDir; }, [downloadDir]); + const updateDriverProgress = useCallback((driverType: string, incoming: DriverProgressState) => { + const normalized = String(driverType || '').trim().toLowerCase(); + if (!normalized) { + return undefined; + } + const nextProgress = normalizeDriverProgressUpdate(progressMapRef.current[normalized], incoming); + progressMapRef.current = { + ...progressMapRef.current, + [normalized]: nextProgress, + }; + setProgressMap(progressMapRef.current); + return nextProgress; + }, []); + + const clearDriverProgress = useCallback((driverType: string) => { + const normalized = String(driverType || '').trim().toLowerCase(); + if (!normalized) { + return; + } + const next = { ...progressMapRef.current }; + delete next[normalized]; + progressMapRef.current = next; + setProgressMap(next); + }, []); + const modalBodyStyle = useMemo(() => ({ maxHeight: 'calc(100vh - 220px)', overflowY: 'auto', @@ -635,18 +656,19 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG } const messageText = String(event.message || '').trim(); const percent = Math.max(0, Math.min(100, Number(event.percent || 0))); - setProgressMap((prev) => ({ - ...prev, - [driverType]: { - status, - message: messageText, - percent, - }, - })); - const progressText = `${Math.round(percent)}%`; - const statusText = String(status || '').toUpperCase(); - const lineText = `[${statusText}] ${messageText || '-'} (${progressText})`; - const lineSignature = `${statusText}|${messageText || '-'}`; + const nextProgress = updateDriverProgress(driverType, { + status, + message: messageText, + percent, + }); + if (!nextProgress) { + return; + } + const progressText = `${Math.round(nextProgress.percent)}%`; + const statusText = String(nextProgress.status || '').toUpperCase(); + const logMessageText = nextProgress.message || '-'; + const lineText = `[${statusText}] ${logMessageText} (${progressText})`; + const lineSignature = `${statusText}|${logMessageText}`; appendOperationLog(driverType, lineText, lineSignature, 'update-last'); }); } catch (error) { @@ -657,7 +679,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG off(); } }; - }, [appendOperationLog]); + }, [appendOperationLog, updateDriverProgress]); const resolveLocalImportVersion = useCallback((row: DriverStatusRow) => { const options = versionMap[row.type] || []; @@ -674,14 +696,11 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG actionOptions?: { silentToast?: boolean; skipRefresh?: boolean }, ) => { setActionState({ driverType: row.type, kind: 'install' }); - setProgressMap((prev) => ({ - ...prev, - [row.type]: { - status: 'start', - message: '开始安装', - percent: 0, - }, - })); + updateDriverProgress(row.type, { + status: 'start', + message: '开始安装', + percent: 0, + }); appendOperationLog(row.type, '[START] 开始自动安装'); try { let versionOptions = versionMap[row.type] || []; @@ -717,7 +736,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG } finally { setActionState({ driverType: '', kind: '' }); } - }, [appendOperationLog, downloadDir, loadVersionOptions, refreshStatus, selectedVersionMap, versionMap]); + }, [appendOperationLog, downloadDir, loadVersionOptions, refreshStatus, selectedVersionMap, updateDriverProgress, versionMap]); const installDriverFromLocalPath = useCallback(async ( row: DriverStatusRow, @@ -734,14 +753,11 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG } setActionState({ driverType: row.type, kind: 'local' }); - setProgressMap((prev) => ({ - ...prev, - [row.type]: { - status: 'start', - message: '开始导入本地驱动包', - percent: 0, - }, - })); + updateDriverProgress(row.type, { + status: 'start', + message: '开始导入本地驱动包', + percent: 0, + }); const selectedVersion = resolveLocalImportVersion(row); const versionTip = selectedVersion ? `(${selectedVersion})` : ''; appendOperationLog(row.type, `[START] 开始本地导入${versionTip}(${sourceLabel}):${pathText}`); @@ -766,7 +782,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG } finally { setActionState({ driverType: '', kind: '' }); } - }, [appendOperationLog, downloadDir, refreshStatus, resolveLocalImportVersion]); + }, [appendOperationLog, downloadDir, refreshStatus, resolveLocalImportVersion, updateDriverProgress]); const installDriverFromLocalFile = useCallback(async (row: DriverStatusRow) => { const fileRes = await SelectDriverPackageFile(downloadDir); @@ -901,11 +917,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG if (!options?.silentToast) { message.success(`${row.name} 已移除`); } - setProgressMap((prev) => { - const next = { ...prev }; - delete next[row.type]; - return next; - }); + clearDriverProgress(row.type); if (!options?.skipRefresh) { await refreshStatus(false); } @@ -913,7 +925,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG } finally { setActionState({ driverType: '', kind: '' }); } - }, [appendOperationLog, downloadDir, refreshStatus]); + }, [appendOperationLog, clearDriverProgress, downloadDir, refreshStatus]); const resolvePackageSizeText = (row: DriverStatusRow): string => { if (row.builtIn) { diff --git a/frontend/src/utils/driverProgress.test.ts b/frontend/src/utils/driverProgress.test.ts new file mode 100644 index 0000000..262f1f8 --- /dev/null +++ b/frontend/src/utils/driverProgress.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from 'vitest'; + +import { normalizeDriverProgressUpdate, type DriverProgressState } from './driverProgress'; + +describe('normalizeDriverProgressUpdate', () => { + it('keeps downloading progress monotonic within one install session', () => { + const previous: DriverProgressState = { + status: 'downloading', + message: '写入驱动元数据', + percent: 90, + }; + + const actual = normalizeDriverProgressUpdate(previous, { + status: 'downloading', + message: '下载驱动总包', + percent: 30, + }); + + expect(actual).toEqual({ + status: 'downloading', + message: '下载驱动总包', + percent: 90, + }); + }); + + it('allows start to reset progress for a new install session', () => { + const actual = normalizeDriverProgressUpdate({ + status: 'error', + message: '安装失败', + percent: 90, + }, { + status: 'start', + message: '开始安装', + percent: 0, + }); + + expect(actual).toEqual({ + status: 'start', + message: '开始安装', + percent: 0, + }); + }); + + it('does not let stale downloading events overwrite terminal states', () => { + const done = normalizeDriverProgressUpdate({ + status: 'downloading', + message: '写入驱动元数据', + percent: 95, + }, { + status: 'done', + message: '驱动代理安装完成', + percent: 100, + }); + + expect(normalizeDriverProgressUpdate(done, { + status: 'downloading', + message: '下载驱动总包', + percent: 40, + })).toBe(done); + + const failed = normalizeDriverProgressUpdate({ + status: 'downloading', + message: '写入驱动元数据', + percent: 95, + }, { + status: 'error', + message: '安装失败', + percent: 0, + }); + + expect(failed).toEqual({ + status: 'error', + message: '安装失败', + percent: 95, + }); + expect(normalizeDriverProgressUpdate(failed, { + status: 'downloading', + message: '下载驱动总包', + percent: 40, + })).toBe(failed); + }); +}); diff --git a/frontend/src/utils/driverProgress.ts b/frontend/src/utils/driverProgress.ts new file mode 100644 index 0000000..86cdfd5 --- /dev/null +++ b/frontend/src/utils/driverProgress.ts @@ -0,0 +1,59 @@ +export type DriverProgressStatus = 'start' | 'downloading' | 'done' | 'error'; + +export type DriverProgressState = { + status: DriverProgressStatus; + message: string; + percent: number; +}; + +const clampDriverProgressPercent = (value: number): number => { + if (!Number.isFinite(value)) { + return 0; + } + return Math.max(0, Math.min(100, value)); +}; + +export const normalizeDriverProgressUpdate = ( + previous: DriverProgressState | undefined, + incoming: DriverProgressState, +): DriverProgressState => { + const next: DriverProgressState = { + status: incoming.status, + message: String(incoming.message || '').trim(), + percent: clampDriverProgressPercent(Number(incoming.percent || 0)), + }; + + if (next.status === 'start') { + return { + ...next, + percent: 0, + }; + } + + if (next.status === 'done') { + return { + ...next, + percent: 100, + }; + } + + if (next.status === 'error') { + return { + ...next, + percent: Math.max(clampDriverProgressPercent(previous?.percent || 0), next.percent), + }; + } + + if (previous?.status === 'done' || previous?.status === 'error') { + return previous; + } + + if (previous?.status === 'start' || previous?.status === 'downloading') { + return { + ...next, + percent: Math.max(clampDriverProgressPercent(previous.percent || 0), next.percent), + }; + } + + return next; +}; diff --git a/internal/app/methods_driver.go b/internal/app/methods_driver.go index 022f0bb..ed94eb0 100644 --- a/internal/app/methods_driver.go +++ b/internal/app/methods_driver.go @@ -3346,7 +3346,7 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut bundleURLs := []string{} if !forceSourceBuild { downloadURLs = resolveOptionalDriverAgentDownloadURLs(definition, downloadURL, selectedVersion) - if !restrictToExplicitArtifact { + if shouldUseOptionalDriverBundleFallback(driverType, restrictToExplicitArtifact, len(downloadURLs)) { bundleURLs = resolveOptionalDriverBundleDownloadURLs() } } @@ -3501,6 +3501,16 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut return "", "", errors.New(strings.Join(parts, ";")) } +func shouldUseOptionalDriverBundleFallback(driverType string, restrictToExplicitArtifact bool, directURLCount int) bool { + if restrictToExplicitArtifact { + return false + } + if shouldSkipDirectOptionalDriverDownloads(driverType) { + return true + } + return directURLCount == 0 +} + func downloadOptionalDriverAgentBinary(a *App, definition driverDefinition, urlText string, executablePath string) (string, error) { driverType := normalizeDriverType(definition.Type) displayName := resolveDriverDisplayName(definition) diff --git a/internal/app/methods_driver_version_test.go b/internal/app/methods_driver_version_test.go index 5b24161..16269fa 100644 --- a/internal/app/methods_driver_version_test.go +++ b/internal/app/methods_driver_version_test.go @@ -212,6 +212,21 @@ func TestResolveOptionalDriverAgentDownloadURLsSkipsBundleOnlyDamengAsset(t *tes } } +func TestShouldUseOptionalDriverBundleFallbackSkipsWhenDirectAssetExists(t *testing.T) { + if shouldUseOptionalDriverBundleFallback("sqlserver", false, 1) { + t.Fatal("expected published single-file driver asset to avoid 497MB bundle fallback") + } +} + +func TestShouldUseOptionalDriverBundleFallbackKeepsBundleWhenDirectAssetMissing(t *testing.T) { + if !shouldUseOptionalDriverBundleFallback("dameng", false, 0) { + t.Fatal("expected missing single-file driver asset to keep bundle fallback") + } + if shouldUseOptionalDriverBundleFallback("dameng", true, 0) { + t.Fatal("expected explicit version artifact installs to skip bundle fallback") + } +} + func TestResolveDriverInstallVersionUsesPinnedVersionForBuiltinActivateURL(t *testing.T) { definition, ok := resolveDriverDefinition("sqlserver") if !ok {