mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 18:39:54 +08:00
🐛 fix(driver): 修复驱动安装进度回退
驱动安装进度按单次会话做单调归一,避免旧下载事件覆盖新进度。 有单文件预编译资产时跳过驱动总包兜底,减少进度回退和安装失败面。 补充前端进度状态机与后端总包兜底回归测试。
This commit is contained in:
@@ -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<DriverBatchActionKind>('');
|
||||
const [batchProgress, setBatchProgress] = useState<DriverBatchProgressState | null>(null);
|
||||
const [progressMap, setProgressMap] = useState<Record<string, ProgressState>>({});
|
||||
const [progressMap, setProgressMap] = useState<Record<string, DriverProgressState>>({});
|
||||
const [operationLogMap, setOperationLogMap] = useState<Record<string, DriverLogEntry[]>>({});
|
||||
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<Record<string, boolean>>({});
|
||||
const [versionSizeLoadingMap, setVersionSizeLoadingMap] = useState<Record<string, boolean>>({});
|
||||
const downloadDirRef = useRef(downloadDir);
|
||||
const progressMapRef = useRef<Record<string, DriverProgressState>>({});
|
||||
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<React.CSSProperties>(() => ({
|
||||
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) {
|
||||
|
||||
82
frontend/src/utils/driverProgress.test.ts
Normal file
82
frontend/src/utils/driverProgress.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
59
frontend/src/utils/driverProgress.ts
Normal file
59
frontend/src/utils/driverProgress.ts
Normal file
@@ -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;
|
||||
};
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user