Files
MyGoNavi/frontend/src/components/DriverManagerModal.tsx
Syngnat c99cdb5d0d 🐛 fix(driver-manager/mongodb): 恢复旧版 MongoDB 连接兼容
- 默认版本:将 MongoDB 推荐 driver-agent 切到 1.17.9
- 安装策略:v1 兼容驱动优先尝试发布资产,不再强制源码构建
- 状态提示:已安装 2.x 时提示重装以兼容 MongoDB 4.0
- 前端交互:重装驱动优先选择推荐兼容版本
- 测试覆盖:补充版本选择、下载候选和重装传参回归
2026-06-12 15:18:57 +08:00

1734 lines
66 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Alert, Button, Collapse, Empty, Input, Modal, Progress, Select, Space, Switch, Tag, Typography, message } from 'antd';
import { DeleteOutlined, DownloadOutlined, FileSearchOutlined, FolderOpenOutlined, InfoCircleFilled, ReloadOutlined } from '@ant-design/icons';
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,
DRIVER_LOCAL_IMPORT_DIRECTORY_HELP,
DRIVER_LOCAL_IMPORT_SINGLE_FILE_HELP,
} from '../utils/driverImportGuidance';
import {
CheckDriverNetworkStatus,
DownloadDriverPackage,
GetDriverVersionList,
GetDriverVersionPackageSize,
GetDriverStatusList,
InstallLocalDriverPackage,
OpenDriverDownloadDirectory,
RemoveDriverPackage,
SelectDriverPackageDirectory,
SelectDriverPackageFile,
} from '../../wailsjs/go/app/App';
const { Paragraph, Text } = Typography;
type DriverStatusRow = {
type: string;
name: string;
builtIn: boolean;
pinnedVersion?: string;
installedVersion?: string;
packageSizeText?: string;
runtimeAvailable: boolean;
packageInstalled: boolean;
connectable: boolean;
defaultDownloadUrl?: string;
installDir?: string;
packagePath?: string;
executablePath?: string;
downloadedAt?: string;
agentRevision?: string;
expectedRevision?: string;
needsUpdate?: boolean;
updateReason?: string;
affectedConnections?: number;
message?: string;
};
type DriverProgressEvent = {
driverType?: string;
status?: 'start' | 'downloading' | 'done' | 'error';
message?: string;
percent?: number;
};
type DriverActionKind = '' | 'install' | 'remove' | 'local';
type DriverBatchActionKind = '' | 'install-all' | 'reinstall-updates' | 'remove-all';
type DriverBatchProgressState = {
total: number;
completed: number;
success: number;
failed: number;
skipped: number;
currentDriverType: string;
currentDriverName: string;
currentMessage: string;
};
type DriverLogEntry = {
time: string;
text: string;
signature: string;
};
type DriverNetworkProbe = {
name: string;
url: string;
reachable: boolean;
httpStatus?: number;
latencyMs?: number;
tcpLatencyMs?: number;
httpLatencyMs?: number;
method?: string;
error?: string;
};
type DriverNetworkStatus = {
reachable: boolean;
summary: string;
recommendedProxy: boolean;
proxyConfigured: boolean;
downloadChainReachable?: boolean;
downloadRequiredHosts?: string[];
proxyEnv?: Record<string, string>;
checks: DriverNetworkProbe[];
checkedAt?: string;
logPath?: string;
};
const parseOptionalLatency = (value: unknown): number | undefined => {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed < 0) return undefined;
return parsed;
};
const sharedInfoAlertIcon = <InfoCircleFilled style={{ fontSize: 24 }} />;
type DriverVersionOption = {
version: string;
downloadUrl: string;
packageSizeText?: string;
recommended?: boolean;
source?: string;
year?: string;
displayLabel?: string;
};
const buildVersionOptionKey = (option: DriverVersionOption) => `${option.version}@@${option.downloadUrl}`;
const buildVersionSizeLoadingKey = (driverType: string, optionKey: string) => `${driverType}@@${optionKey}`;
const DRIVER_STATUS_CACHE_TTL_MS = 60 * 1000;
const DRIVER_NETWORK_CACHE_TTL_MS = 5 * 60 * 1000;
const DRIVER_INSTALL_WATCHDOG_MS = 12 * 60 * 1000;
const normalizeDriverSearchText = (value: string) => String(value || '').trim().toLowerCase();
const isSlimBuildInstallUnavailable = (row: DriverStatusRow) => (row.message || '').includes('精简构建') && !row.packageInstalled;
const resolveDriverBatchActionLabel = (actionKind: DriverBatchActionKind) => {
switch (actionKind) {
case 'install-all':
return '安装所有驱动';
case 'reinstall-updates':
return '重装需更新驱动';
case 'remove-all':
return '删除所有驱动';
default:
return '批量操作';
}
};
const createDriverBatchProgress = (total: number, currentMessage: string): DriverBatchProgressState => ({
total,
completed: 0,
success: 0,
failed: 0,
skipped: 0,
currentDriverType: '',
currentDriverName: '',
currentMessage,
});
let driverStatusSnapshotCache: { rows: DriverStatusRow[]; downloadDir: string; cachedAt: number } | null = null;
let driverNetworkSnapshotCache: { status: DriverNetworkStatus; cachedAt: number } | null = null;
const isFreshCache = (cachedAt: number, ttlMs: number): boolean => Date.now() - cachedAt <= ttlMs;
const buildVersionSelectOptions = (options: DriverVersionOption[]) => {
type SelectOption = { value: string; label: string };
type SelectGroup = { label: string; options: SelectOption[] };
if (options.length === 0) {
return [] as Array<SelectOption | SelectGroup>;
}
const yearGroups = new Map<string, SelectOption[]>();
const others: SelectOption[] = [];
options.forEach((option) => {
const selectOption: SelectOption = {
value: buildVersionOptionKey(option),
label: option.displayLabel || option.version || '默认版本',
};
const year = String(option.year || '').trim();
if (!year) {
others.push(selectOption);
return;
}
const group = yearGroups.get(year) || [];
group.push(selectOption);
yearGroups.set(year, group);
});
const sortedYears = Array.from(yearGroups.keys()).sort((a, b) => {
const left = Number.parseInt(a, 10);
const right = Number.parseInt(b, 10);
const leftValid = Number.isFinite(left);
const rightValid = Number.isFinite(right);
if (leftValid && rightValid) {
return right - left;
}
return b.localeCompare(a);
});
const grouped: SelectGroup[] = sortedYears.map((year) => ({
label: `${year}`,
options: yearGroups.get(year) || [],
}));
if (others.length > 0) {
grouped.push({ label: '其他', options: others });
}
return grouped;
};
const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenGlobalProxySettings?: () => void }> = ({
open,
onClose,
onOpenGlobalProxySettings,
}) => {
const theme = useStore((state) => state.theme);
const appearance = useStore((state) => state.appearance);
const darkMode = theme === 'dark';
const resolvedAppearance = resolveAppearanceValues(appearance);
const opacity = normalizeOpacityForPlatform(resolvedAppearance.opacity);
const driverManagerTheme = useMemo(
() => buildDriverManagerWorkbenchTheme(darkMode, opacity),
[darkMode, opacity, appearance.uiVersion],
);
const [loading, setLoading] = useState(false);
const [downloadDir, setDownloadDir] = useState('');
const [networkChecking, setNetworkChecking] = useState(false);
const [networkStatus, setNetworkStatus] = useState<DriverNetworkStatus | null>(null);
const [searchKeyword, setSearchKeyword] = useState('');
const [rows, setRows] = useState<DriverStatusRow[]>([]);
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, DriverProgressState>>({});
const [operationLogMap, setOperationLogMap] = useState<Record<string, DriverLogEntry[]>>({});
const [logDriverType, setLogDriverType] = useState('');
const [logModalOpen, setLogModalOpen] = useState(false);
const [batchDirectoryImporting, setBatchDirectoryImporting] = useState(false);
const [forceOverwriteInstalled, setForceOverwriteInstalled] = useState(false);
const [versionMap, setVersionMap] = useState<Record<string, DriverVersionOption[]>>({});
const [selectedVersionMap, setSelectedVersionMap] = useState<Record<string, string>>({});
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 !== '';
const installMutatingBusy = batchBusy || 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',
overflowX: 'hidden',
paddingRight: 18,
background: driverManagerTheme.pageBg,
color: driverManagerTheme.titleText,
}), [driverManagerTheme]);
const managerSectionStyle = useMemo<React.CSSProperties>(() => ({
border: driverManagerTheme.sectionBorder,
borderRadius: 8,
background: driverManagerTheme.sectionBg,
}), [driverManagerTheme]);
const managerStatStyle = useMemo<React.CSSProperties>(() => ({
border: driverManagerTheme.statBorder,
borderRadius: 8,
background: driverManagerTheme.statBg,
}), [driverManagerTheme]);
const managerUpdateNoteStyle = useMemo<React.CSSProperties>(() => ({
border: driverManagerTheme.updateNoteBorder,
borderRadius: 8,
background: driverManagerTheme.updateNoteBg,
}), [driverManagerTheme]);
const appendOperationLog = useCallback((
driverType: string,
text: string,
signature?: string,
mode: 'append' | 'update-last' = 'append',
) => {
const normalized = String(driverType || '').trim().toLowerCase();
const content = String(text || '').trim();
if (!normalized || !content) {
return;
}
const sign = String(signature || content).trim() || content;
const now = new Date().toLocaleTimeString();
setOperationLogMap((prev) => {
const history = prev[normalized] || [];
if (history.length > 0) {
const last = history[history.length - 1];
if (last.signature === sign) {
if (mode === 'update-last') {
if (last.text === content) {
return prev;
}
const nextHistory = [...history];
nextHistory[nextHistory.length - 1] = {
...last,
text: content,
time: now,
};
return { ...prev, [normalized]: nextHistory };
}
return prev;
}
}
const nextHistory = [
...history,
{
time: now,
text: content,
signature: sign,
},
];
const sliced = nextHistory.length > 200 ? nextHistory.slice(nextHistory.length - 200) : nextHistory;
return { ...prev, [normalized]: sliced };
});
}, []);
const refreshStatus = useCallback(async (
toastOnError = true,
options?: { showLoading?: boolean },
) => {
const showLoading = options?.showLoading ?? true;
if (showLoading) {
setLoading(true);
}
try {
const res = await GetDriverStatusList(downloadDirRef.current, '');
if (!res?.success) {
if (toastOnError) {
message.error(res?.message || '拉取驱动状态失败');
}
return;
}
const data = (res?.data || {}) as any;
const resolvedDir = String(data.downloadDir || '').trim();
const drivers = Array.isArray(data.drivers) ? data.drivers : [];
const effectiveDownloadDir = resolvedDir || downloadDirRef.current;
if (resolvedDir) {
setDownloadDir(resolvedDir);
}
const nextRows: DriverStatusRow[] = drivers.map((item: any) => ({
type: String(item.type || '').trim(),
name: String(item.name || item.type || '').trim(),
builtIn: !!item.builtIn,
pinnedVersion: String(item.pinnedVersion || '').trim() || undefined,
installedVersion: String(item.installedVersion || '').trim() || undefined,
packageSizeText: String(item.packageSizeText || '').trim() || undefined,
runtimeAvailable: !!item.runtimeAvailable,
packageInstalled: !!item.packageInstalled,
connectable: !!item.connectable,
defaultDownloadUrl: String(item.defaultDownloadUrl || '').trim() || undefined,
installDir: String(item.installDir || '').trim() || undefined,
packagePath: String(item.packagePath || '').trim() || undefined,
executablePath: String(item.executablePath || '').trim() || undefined,
downloadedAt: String(item.downloadedAt || '').trim() || undefined,
agentRevision: String(item.agentRevision || '').trim() || undefined,
expectedRevision: String(item.expectedRevision || '').trim() || undefined,
needsUpdate: !!item.needsUpdate,
updateReason: String(item.updateReason || '').trim() || undefined,
affectedConnections: Number.isFinite(Number(item.affectedConnections))
? Number(item.affectedConnections)
: undefined,
message: String(item.message || '').trim() || undefined,
}));
setRows(nextRows);
driverStatusSnapshotCache = {
rows: nextRows,
downloadDir: effectiveDownloadDir,
cachedAt: Date.now(),
};
} catch (err: any) {
if (toastOnError) {
message.error(`拉取驱动状态失败:${err?.message || String(err)}`);
}
} finally {
if (showLoading) {
setLoading(false);
}
}
}, []);
const checkNetworkStatus = useCallback(async (
toastOnError = false,
options?: { showLoading?: boolean },
) => {
const showLoading = options?.showLoading ?? true;
if (showLoading) {
setNetworkChecking(true);
}
try {
const res = await CheckDriverNetworkStatus();
if (!res?.success) {
if (toastOnError) {
message.error(res?.message || '驱动网络检测失败');
}
return;
}
const data = (res?.data || {}) as any;
const checks = Array.isArray(data.checks) ? data.checks : [];
const normalizedChecks: DriverNetworkProbe[] = checks.map((item: any) => ({
name: String(item.name || '').trim(),
url: String(item.url || '').trim(),
reachable: !!item.reachable,
httpStatus: parseOptionalLatency(item.httpStatus),
latencyMs: parseOptionalLatency(item.latencyMs),
tcpLatencyMs: parseOptionalLatency(item.tcpLatencyMs),
httpLatencyMs: parseOptionalLatency(item.httpLatencyMs),
method: String(item.method || '').trim().toUpperCase() || undefined,
error: String(item.error || '').trim() || undefined,
}));
const nextStatus: DriverNetworkStatus = {
reachable: !!data.reachable,
summary: String(data.summary || '').trim() || '驱动网络检测已完成',
recommendedProxy: !!data.recommendedProxy,
proxyConfigured: !!data.proxyConfigured,
downloadChainReachable: typeof data.downloadChainReachable === 'boolean' ? data.downloadChainReachable : undefined,
downloadRequiredHosts: Array.isArray(data.downloadRequiredHosts)
? data.downloadRequiredHosts.map((item: unknown) => String(item || '').trim()).filter(Boolean)
: undefined,
proxyEnv: (data.proxyEnv || {}) as Record<string, string>,
checkedAt: String(data.checkedAt || '').trim() || undefined,
checks: normalizedChecks,
logPath: String(data.logPath || '').trim() || undefined,
};
setNetworkStatus(nextStatus);
driverNetworkSnapshotCache = {
status: nextStatus,
cachedAt: Date.now(),
};
} catch (err: any) {
if (toastOnError) {
message.error(`驱动网络检测失败:${err?.message || String(err)}`);
}
} finally {
if (showLoading) {
setNetworkChecking(false);
}
}
}, []);
const loadVersionOptions = useCallback(async (row: DriverStatusRow, toastOnError = false) => {
if (row.builtIn) {
return [] as DriverVersionOption[];
}
const driverType = String(row.type || '').trim();
if (!driverType) {
return [] as DriverVersionOption[];
}
setVersionLoadingMap((prev) => ({ ...prev, [driverType]: true }));
try {
const res = await GetDriverVersionList(driverType, '');
if (!res?.success) {
if (toastOnError) {
message.error(res?.message || `${row.name} 版本列表加载失败`);
}
return [] as DriverVersionOption[];
}
const data = (res?.data || {}) as any;
const rawVersions = Array.isArray(data.versions) ? data.versions : [];
const options: DriverVersionOption[] = rawVersions
.map((item: any) => {
const version = String(item.version || '').trim();
const downloadUrl = String(item.downloadUrl || '').trim();
if (!version && !downloadUrl) {
return null;
}
return {
version,
downloadUrl,
packageSizeText: String(item.packageSizeText || '').trim() || undefined,
recommended: !!item.recommended,
source: String(item.source || '').trim() || undefined,
year: String(item.year || '').trim() || undefined,
displayLabel: String(item.displayLabel || '').trim() || undefined,
} as DriverVersionOption;
})
.filter((item: DriverVersionOption | null): item is DriverVersionOption => !!item);
if (options.length === 0) {
const fallbackVersion = String(row.pinnedVersion || '').trim();
const fallbackURL = String(row.defaultDownloadUrl || '').trim();
if (fallbackVersion || fallbackURL) {
options.push({
version: fallbackVersion,
downloadUrl: fallbackURL,
recommended: true,
source: 'fallback',
displayLabel: fallbackVersion || '默认版本',
});
}
}
setVersionMap((prev) => ({ ...prev, [driverType]: options }));
setSelectedVersionMap((prev) => {
const currentKey = prev[driverType];
if (currentKey && options.some((option) => buildVersionOptionKey(option) === currentKey)) {
return prev;
}
const preferred =
(row.needsUpdate ? options.find((option) => option.version === row.pinnedVersion) : undefined) ||
(row.needsUpdate ? options.find((option) => option.recommended) : undefined) ||
options.find((option) => option.version === row.installedVersion) ||
options.find((option) => option.version === row.pinnedVersion) ||
options.find((option) => option.recommended) ||
options[0];
if (!preferred) {
return prev;
}
return { ...prev, [driverType]: buildVersionOptionKey(preferred) };
});
return options;
} catch (err: any) {
if (toastOnError) {
message.error(`加载 ${row.name} 版本列表失败:${err?.message || String(err)}`);
}
return [] as DriverVersionOption[];
} finally {
setVersionLoadingMap((prev) => ({ ...prev, [driverType]: false }));
}
}, []);
const loadVersionPackageSize = useCallback(async (row: DriverStatusRow, optionKey: string) => {
if (row.builtIn) {
return;
}
const driverType = String(row.type || '').trim();
if (!driverType || !optionKey) {
return;
}
const options = versionMap[driverType] || [];
const selectedOption = options.find((item) => buildVersionOptionKey(item) === optionKey);
if (!selectedOption) {
return;
}
if (String(selectedOption.packageSizeText || '').trim()) {
return;
}
const versionText = String(selectedOption.version || '').trim();
if (!versionText) {
return;
}
const loadingKey = buildVersionSizeLoadingKey(driverType, optionKey);
if (versionSizeLoadingMap[loadingKey]) {
return;
}
setVersionSizeLoadingMap((prev) => ({ ...prev, [loadingKey]: true }));
try {
const res = await GetDriverVersionPackageSize(driverType, versionText);
if (!res?.success) {
return;
}
const data = (res?.data || {}) as any;
const sizeText = String(data.packageSizeText || '').trim();
if (!sizeText) {
return;
}
setVersionMap((prev) => {
const current = prev[driverType] || [];
let changed = false;
const next = current.map((item) => {
if (buildVersionOptionKey(item) !== optionKey) {
return item;
}
if (String(item.packageSizeText || '').trim() === sizeText) {
return item;
}
changed = true;
return { ...item, packageSizeText: sizeText };
});
if (!changed) {
return prev;
}
return { ...prev, [driverType]: next };
});
} finally {
setVersionSizeLoadingMap((prev) => {
if (!prev[loadingKey]) {
return prev;
}
const next = { ...prev };
delete next[loadingKey];
return next;
});
}
}, [versionMap, versionSizeLoadingMap]);
useEffect(() => {
if (!open) {
return;
}
const cachedStatus = driverStatusSnapshotCache;
const hasCachedStatus = !!cachedStatus;
if (cachedStatus) {
setRows(cachedStatus.rows);
if (cachedStatus.downloadDir) {
setDownloadDir(cachedStatus.downloadDir);
}
}
const shouldRefreshStatus = !cachedStatus || !isFreshCache(cachedStatus.cachedAt, DRIVER_STATUS_CACHE_TTL_MS);
if (shouldRefreshStatus) {
void refreshStatus(false, { showLoading: !hasCachedStatus });
}
const cachedNetwork = driverNetworkSnapshotCache;
const hasCachedNetwork = !!cachedNetwork;
if (cachedNetwork) {
setNetworkStatus(cachedNetwork.status);
}
const shouldRefreshNetwork = !cachedNetwork || !isFreshCache(cachedNetwork.cachedAt, DRIVER_NETWORK_CACHE_TTL_MS);
if (shouldRefreshNetwork) {
void checkNetworkStatus(false, { showLoading: !hasCachedNetwork });
}
}, [checkNetworkStatus, open, refreshStatus]);
useEffect(() => {
let off: (() => void) | undefined;
try {
off = EventsOn('driver:download-progress', (event: DriverProgressEvent) => {
if (!event) {
return;
}
const driverType = String(event.driverType || '').trim().toLowerCase();
const status = event.status;
if (!driverType || !status) {
return;
}
const messageText = String(event.message || '').trim();
const percent = Math.max(0, Math.min(100, Number(event.percent || 0)));
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) {
console.warn('Wails API: EventsOn unavailable', error);
}
return () => {
if (off) {
off();
}
};
}, [appendOperationLog, updateDriverProgress]);
const resolveLocalImportVersion = useCallback((row: DriverStatusRow) => {
const options = versionMap[row.type] || [];
const selectedKey = selectedVersionMap[row.type];
const selectedOption =
options.find((item) => buildVersionOptionKey(item) === selectedKey) ||
options.find((item) => item.recommended) ||
options[0];
return selectedOption?.version || row.pinnedVersion || '';
}, [selectedVersionMap, versionMap]);
const installDriver = useCallback(async (
row: DriverStatusRow,
actionOptions?: { silentToast?: boolean; skipRefresh?: boolean },
) => {
setActionState({ driverType: row.type, kind: 'install' });
updateDriverProgress(row.type, {
status: 'start',
message: '开始安装',
percent: 0,
});
appendOperationLog(row.type, '[START] 开始自动安装');
let watchdogId: ReturnType<typeof setTimeout> | undefined;
try {
let versionOptions = versionMap[row.type] || [];
if (versionOptions.length === 0) {
versionOptions = await loadVersionOptions(row, true);
}
const selectedKey = selectedVersionMap[row.type];
const selectedOption =
(row.needsUpdate ? versionOptions.find((item) => item.version === row.pinnedVersion) : undefined) ||
(row.needsUpdate ? versionOptions.find((item) => item.recommended) : undefined) ||
versionOptions.find((item) => buildVersionOptionKey(item) === selectedKey) ||
versionOptions.find((item) => item.recommended) ||
versionOptions[0];
const selectedVersion = selectedOption?.version || row.pinnedVersion || '';
const selectedDownloadURL = selectedOption?.downloadUrl || row.defaultDownloadUrl || '';
const installWatchdog = new Promise<never>((_, reject) => {
watchdogId = setTimeout(() => {
reject(new Error(`安装 ${row.name} 超过 ${Math.round(DRIVER_INSTALL_WATCHDOG_MS / 60000)} 分钟仍未完成。后台任务可能仍在下载或构建,请稍后刷新状态;如多次出现,请检查代理或改用本地驱动包导入。`));
}, DRIVER_INSTALL_WATCHDOG_MS);
});
const result = await Promise.race([
DownloadDriverPackage(row.type, selectedVersion, selectedDownloadURL, downloadDir),
installWatchdog,
]);
if (watchdogId) {
clearTimeout(watchdogId);
}
if (!result?.success) {
const errText = result?.message || `安装 ${row.name} 失败`;
appendOperationLog(row.type, `[ERROR] ${errText}`);
if (!actionOptions?.silentToast) {
message.error(errText);
}
return false;
}
const versionTip = selectedVersion ? `${selectedVersion}` : '';
appendOperationLog(row.type, `[DONE] 自动安装完成 ${versionTip}`);
if (!actionOptions?.silentToast) {
message.success(`${row.name}${versionTip} 已安装启用`);
}
if (!actionOptions?.skipRefresh) {
await refreshStatus(false);
}
return true;
} catch (error) {
const errText = error instanceof Error ? error.message : String(error || `安装 ${row.name} 失败`);
appendOperationLog(row.type, `[ERROR] ${errText}`);
updateDriverProgress(row.type, {
status: 'error',
message: errText,
percent: 0,
});
if (!actionOptions?.silentToast) {
message.error(errText);
}
return false;
} finally {
if (watchdogId) {
clearTimeout(watchdogId);
}
setActionState({ driverType: '', kind: '' });
}
}, [appendOperationLog, downloadDir, loadVersionOptions, refreshStatus, selectedVersionMap, updateDriverProgress, versionMap]);
const installDriverFromLocalPath = useCallback(async (
row: DriverStatusRow,
sourcePath: string,
sourceLabel: '文件' | '目录',
options?: { silentToast?: boolean; skipRefresh?: boolean },
) => {
const pathText = String(sourcePath || '').trim();
if (!pathText) {
if (!options?.silentToast) {
message.error(`未选择有效的本地导入${sourceLabel}`);
}
return false;
}
setActionState({ driverType: row.type, kind: 'local' });
updateDriverProgress(row.type, {
status: 'start',
message: '开始导入本地驱动包',
percent: 0,
});
const selectedVersion = resolveLocalImportVersion(row);
const versionTip = selectedVersion ? `${selectedVersion}` : '';
appendOperationLog(row.type, `[START] 开始本地导入${versionTip}${sourceLabel}${pathText}`);
try {
const result = await InstallLocalDriverPackage(row.type, pathText, downloadDir, selectedVersion);
if (!result?.success) {
const errText = result?.message || `导入 ${row.name} 本地驱动包失败`;
appendOperationLog(row.type, `[ERROR] ${errText}`);
if (!options?.silentToast) {
message.error(errText);
}
return false;
}
appendOperationLog(row.type, `[DONE] 本地导入安装完成 ${versionTip}`.trim());
if (!options?.silentToast) {
message.success(`${row.name}${versionTip} 本地驱动包已安装启用`);
}
if (!options?.skipRefresh) {
await refreshStatus(false);
}
return true;
} finally {
setActionState({ driverType: '', kind: '' });
}
}, [appendOperationLog, downloadDir, refreshStatus, resolveLocalImportVersion, updateDriverProgress]);
const installDriverFromLocalFile = useCallback(async (row: DriverStatusRow) => {
const fileRes = await SelectDriverPackageFile(downloadDir);
if (!fileRes?.success) {
if (String(fileRes?.message || '') !== '已取消') {
message.error(fileRes?.message || '选择本地驱动包文件失败');
}
return;
}
const filePath = String((fileRes?.data as any)?.path || '').trim();
if (!filePath) {
message.error('未选择有效的驱动包文件');
return;
}
await installDriverFromLocalPath(row, filePath, '文件');
}, [downloadDir, installDriverFromLocalPath]);
const installDriversFromDirectory = useCallback(async () => {
const directoryRes = await SelectDriverPackageDirectory(downloadDir);
if (!directoryRes?.success) {
if (String(directoryRes?.message || '') !== '已取消') {
message.error(directoryRes?.message || '选择本地驱动包目录失败');
}
return;
}
const directoryPath = String((directoryRes?.data as any)?.path || '').trim();
if (!directoryPath) {
message.error('未选择有效的驱动包目录');
return;
}
const optionalRows = rows.filter((item) => !item.builtIn);
if (optionalRows.length === 0) {
message.info('当前没有可导入的外置驱动');
return;
}
let successCount = 0;
let failCount = 0;
let dedupeSkipCount = 0;
let slimSkipCount = 0;
setBatchDirectoryImporting(true);
try {
for (const row of optionalRows) {
const alreadyInstalled = row.packageInstalled || row.connectable;
if (alreadyInstalled && !forceOverwriteInstalled) {
dedupeSkipCount += 1;
appendOperationLog(row.type, '[SKIP] 已检测到驱动已安装,目录导入去重跳过');
continue;
}
if (alreadyInstalled && forceOverwriteInstalled) {
appendOperationLog(row.type, '[INFO] 已启用覆盖已安装模式,执行重装导入');
}
const isSlimBuildUnavailable = (row.message || '').includes('精简构建') && !row.packageInstalled;
if (isSlimBuildUnavailable) {
slimSkipCount += 1;
appendOperationLog(row.type, '[WARN] 当前发行包为精简构建,已跳过目录导入');
continue;
}
const ok = await installDriverFromLocalPath(row, directoryPath, '目录', { silentToast: true, skipRefresh: true });
if (ok) {
successCount += 1;
} else {
failCount += 1;
}
}
await refreshStatus(false);
} finally {
setBatchDirectoryImporting(false);
}
const skipParts: string[] = [];
if (dedupeSkipCount > 0) {
skipParts.push(`去重跳过 ${dedupeSkipCount}`);
}
if (slimSkipCount > 0) {
skipParts.push(`精简版跳过 ${slimSkipCount}`);
}
const skipTip = skipParts.length > 0 ? `${skipParts.join('')}` : '';
const forceTip = forceOverwriteInstalled ? '(覆盖已安装)' : '';
if (failCount === 0) {
message.success(`目录导入完成${forceTip}:成功 ${successCount}${skipTip}`);
return;
}
if (successCount > 0) {
message.warning(`目录导入完成${forceTip}:成功 ${successCount},失败 ${failCount}${skipTip}`);
return;
}
message.error(`目录导入失败${forceTip}:失败 ${failCount}${skipTip}`);
}, [appendOperationLog, downloadDir, forceOverwriteInstalled, installDriverFromLocalPath, refreshStatus, rows]);
const openDriverDirectory = useCallback(async () => {
try {
const res = await OpenDriverDownloadDirectory(downloadDir);
if (!res?.success) {
throw new Error(res?.message || '打开驱动目录失败');
}
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error || '未知错误');
message.error(`打开驱动目录失败: ${errMsg}`);
}
}, [downloadDir]);
const openDriverLog = useCallback((driverType: string) => {
const normalized = String(driverType || '').trim().toLowerCase();
if (!normalized) {
return;
}
setLogDriverType(normalized);
setLogModalOpen(true);
}, []);
const removeDriver = useCallback(async (
row: DriverStatusRow,
options?: { silentToast?: boolean; skipRefresh?: boolean },
) => {
setActionState({ driverType: row.type, kind: 'remove' });
appendOperationLog(row.type, '[START] 开始移除驱动');
try {
const result = await RemoveDriverPackage(row.type, downloadDir);
if (!result?.success) {
const errText = result?.message || `移除 ${row.name} 失败`;
appendOperationLog(row.type, `[ERROR] ${errText}`);
if (!options?.silentToast) {
message.error(errText);
}
return false;
}
appendOperationLog(row.type, '[DONE] 驱动移除完成');
if (!options?.silentToast) {
message.success(`${row.name} 已移除`);
}
clearDriverProgress(row.type);
if (!options?.skipRefresh) {
await refreshStatus(false);
}
return true;
} finally {
setActionState({ driverType: '', kind: '' });
}
}, [appendOperationLog, clearDriverProgress, downloadDir, refreshStatus]);
const resolvePackageSizeText = (row: DriverStatusRow): string => {
if (row.builtIn) {
return row.packageSizeText || '-';
}
const options = versionMap[row.type] || [];
const selectedKey = selectedVersionMap[row.type];
const loadingKey = buildVersionSizeLoadingKey(row.type, selectedKey || '');
const selectedOption =
options.find((item) => buildVersionOptionKey(item) === selectedKey) ||
options.find((item) => item.recommended) ||
options[0];
const anyKnownSize = options.find((item) => String(item.packageSizeText || '').trim())?.packageSizeText;
if (selectedKey && versionSizeLoadingMap[loadingKey]) {
return '计算中...';
}
return selectedOption?.packageSizeText || anyKnownSize || row.packageSizeText || '-';
};
const resolveDriverStatusTag = (row: DriverStatusRow) => {
if (row.builtIn) {
return <Tag color="success"></Tag>;
}
const progress = progressMap[row.type];
if (progress && (progress.status === 'start' || progress.status === 'downloading')) {
return <Tag color="processing"> {Math.round(progress.percent)}%</Tag>;
}
if (row.needsUpdate) {
return <Tag color="warning"></Tag>;
}
if (row.connectable) {
return <Tag color="success"></Tag>;
}
if (row.packageInstalled) {
return <Tag color="warning"></Tag>;
}
return <Tag></Tag>;
};
const resolveDriverProgress = (row: DriverStatusRow) => {
const progress = progressMap[row.type];
let percent = 0;
let status: 'normal' | 'exception' | 'active' | 'success' = 'normal';
if (progress?.status === 'error') {
percent = Math.max(0, Math.min(100, Math.round(progress.percent || 0)));
status = 'exception';
} else if (progress && (progress.status === 'start' || progress.status === 'downloading')) {
percent = Math.max(1, Math.min(99, Math.round(progress.percent || 0)));
status = 'active';
} else if (row.connectable || row.packageInstalled) {
percent = 100;
status = 'success';
}
return { percent, status };
};
const renderVersionControl = (row: DriverStatusRow) => {
if (row.builtIn) {
return <Text type="secondary"></Text>;
}
const versionLocked = row.packageInstalled || row.connectable;
if (versionLocked) {
const installedVersion = String(row.installedVersion || '').trim();
const revisionHint = row.needsUpdate ? ',需重装' : '';
return (
<Text type="secondary" className="driver-manager-version-lock">
{installedVersion ? `${installedVersion}(已安装${revisionHint}` : `已安装${row.needsUpdate ? ',需重装' : ''}`}
</Text>
);
}
const options = versionMap[row.type] || [];
const selectedKey = selectedVersionMap[row.type];
const selectOptions = buildVersionSelectOptions(options);
const mongoHint = row.type === 'mongodb'
? 'MongoDB 4.0 请使用 1.17.x 兼容驱动2.x 驱动要求 MongoDB 4.2+。'
: '';
return (
<div className="driver-manager-version-control">
<Select
size="small"
style={{ width: '100%' }}
loading={!!versionLoadingMap[row.type]}
disabled={batchBusy || actionState.driverType === row.type}
placeholder={options.length > 0 ? '选择驱动版本' : '点击加载版本'}
value={selectedKey}
options={selectOptions as any}
onOpenChange={(open) => {
if (open && options.length === 0 && !versionLoadingMap[row.type]) {
void loadVersionOptions(row, true);
return;
}
if (open && selectedKey) {
void loadVersionPackageSize(row, selectedKey);
}
}}
onChange={(value) => {
setSelectedVersionMap((prev) => ({ ...prev, [row.type]: value }));
void loadVersionPackageSize(row, value);
}}
/>
{mongoHint ? <Text type="secondary" className="driver-manager-small-text">{mongoHint}</Text> : null}
</div>
);
};
const renderDriverActions = (row: DriverStatusRow) => {
if (row.builtIn) {
return null;
}
const isSlimBuildUnavailable = isSlimBuildInstallUnavailable(row);
const loadingInstallOrRemove =
actionState.driverType === row.type && (actionState.kind === 'install' || actionState.kind === 'remove');
const loadingLocal = actionState.driverType === row.type && actionState.kind === 'local';
const logs = operationLogMap[row.type] || [];
const hasLogs = logs.length > 0;
if (isSlimBuildUnavailable && !row.packageInstalled) {
return <Text type="secondary">使 Full </Text>;
}
const mainAction = row.needsUpdate ? (
<Button type="primary" icon={<DownloadOutlined />} disabled={batchBusy} loading={loadingInstallOrRemove} onClick={() => installDriver(row)}>
</Button>
) : row.connectable ? (
<Button danger icon={<DeleteOutlined />} disabled={batchBusy} loading={loadingInstallOrRemove} onClick={() => removeDriver(row)}>
</Button>
) : (
<Button type="primary" icon={<DownloadOutlined />} disabled={batchBusy} loading={loadingInstallOrRemove} onClick={() => installDriver(row)}>
</Button>
);
return (
<Space size={8} wrap className="driver-manager-card-actions">
{mainAction}
<Button icon={<FileSearchOutlined />} disabled={batchBusy} loading={loadingLocal} onClick={() => installDriverFromLocalFile(row)}>
{DRIVER_LOCAL_IMPORT_BUTTON_LABEL}
</Button>
<Button type={hasLogs ? 'default' : 'text'} disabled={!hasLogs} onClick={() => openDriverLog(row.type)}>
</Button>
</Space>
);
};
const activeLogRow = useMemo(() => {
if (!logDriverType) {
return undefined;
}
return rows.find((item) => item.type === logDriverType);
}, [logDriverType, rows]);
const normalizedSearchKeyword = useMemo(() => normalizeDriverSearchText(searchKeyword), [searchKeyword]);
const filteredRows = useMemo(() => {
if (!normalizedSearchKeyword) {
return rows;
}
return rows.filter((row) => {
const searchableParts = [
row.name,
row.type,
row.pinnedVersion,
row.installedVersion,
row.updateReason,
row.message,
row.builtIn ? '内置' : '外置',
row.needsUpdate ? '强烈建议重装' : row.connectable ? '已启用' : row.packageInstalled ? '已安装' : '未启用',
];
const searchableText = normalizeDriverSearchText(searchableParts.filter(Boolean).join(' '));
return searchableText.includes(normalizedSearchKeyword);
});
}, [normalizedSearchKeyword, rows]);
const filterSummaryText = useMemo(() => {
if (normalizedSearchKeyword) {
return `匹配 ${filteredRows.length} / ${rows.length}`;
}
return `${rows.length} 个驱动`;
}, [filteredRows.length, normalizedSearchKeyword, rows.length]);
const statusSummary = useMemo(() => {
const optionalRows = rows.filter((row) => !row.builtIn);
return {
total: rows.length,
enabled: optionalRows.filter((row) => row.connectable).length,
needsUpdate: optionalRows.filter((row) => row.needsUpdate).length,
notEnabled: optionalRows.filter((row) => !row.connectable && !row.packageInstalled).length,
};
}, [rows]);
const reinstallableRows = useMemo(() => rows.filter((row) => !row.builtIn && row.needsUpdate), [rows]);
const installableRows = useMemo(
() => rows.filter((row) => !row.builtIn && !row.connectable),
[rows],
);
const removableRows = useMemo(
() => rows.filter((row) => !row.builtIn && (row.connectable || row.packageInstalled)),
[rows],
);
const batchProgressPercent = useMemo(() => {
if (!batchProgress || batchProgress.total <= 0) {
return 0;
}
const currentProgress = batchProgress.currentDriverType
? progressMap[batchProgress.currentDriverType]
: undefined;
const shouldUseCurrentProgress = batchAction === 'install-all' || batchAction === 'reinstall-updates';
const currentContribution = shouldUseCurrentProgress && currentProgress && currentProgress.status !== 'error'
? Math.max(0, Math.min(100, Number(currentProgress.percent || 0))) / 100
: 0;
const completed = Math.max(0, Math.min(batchProgress.completed, batchProgress.total));
const percent = ((completed + currentContribution) / batchProgress.total) * 100;
return Math.max(0, Math.min(100, Math.round(percent)));
}, [batchAction, batchProgress, progressMap]);
const activeBatchDriverProgress = (batchAction === 'install-all' || batchAction === 'reinstall-updates') && batchProgress?.currentDriverType
? progressMap[batchProgress.currentDriverType]
: undefined;
const batchProgressMessage = activeBatchDriverProgress?.message || batchProgress?.currentMessage || '';
const runBatchInstall = useCallback(async (
targetRows: DriverStatusRow[],
actionKind: DriverBatchActionKind,
emptyMessage: string,
successLabel: string,
) => {
if (targetRows.length === 0) {
message.info(emptyMessage);
return;
}
setBatchAction(actionKind);
setBatchProgress(createDriverBatchProgress(targetRows.length, `准备${successLabel}`));
let successCount = 0;
let failCount = 0;
let slimSkipCount = 0;
try {
for (const row of targetRows) {
if (isSlimBuildInstallUnavailable(row)) {
slimSkipCount += 1;
appendOperationLog(row.type, '[WARN] 当前发行包为精简构建,已跳过自动安装');
setBatchProgress((prev) => {
if (!prev) {
return prev;
}
const completed = Math.min(prev.total, prev.completed + 1);
return {
...prev,
completed,
skipped: prev.skipped + 1,
currentDriverType: '',
currentDriverName: '',
currentMessage: `已跳过 ${row.name}`,
};
});
continue;
}
setBatchProgress((prev) => {
if (!prev) {
return prev;
}
return {
...prev,
currentDriverType: row.type,
currentDriverName: row.name,
currentMessage: `正在${successLabel}${row.name}`,
};
});
const ok = await installDriver(row, { silentToast: true, skipRefresh: true });
if (ok) {
successCount += 1;
await refreshStatus(false, { showLoading: false });
} else {
failCount += 1;
}
setBatchProgress((prev) => {
if (!prev) {
return prev;
}
const completed = Math.min(prev.total, prev.completed + 1);
return {
...prev,
completed,
success: prev.success + (ok ? 1 : 0),
failed: prev.failed + (ok ? 0 : 1),
currentDriverType: '',
currentDriverName: '',
currentMessage: ok ? `已完成 ${row.name}` : `失败 ${row.name}`,
};
});
}
await refreshStatus(false);
} finally {
setBatchAction('');
setBatchProgress(null);
}
const skipTip = slimSkipCount > 0 ? `,精简版跳过 ${slimSkipCount}` : '';
if (failCount === 0) {
message.success(`${successLabel}完成:成功 ${successCount}${skipTip}`);
return;
}
if (successCount > 0) {
message.warning(`${successLabel}完成:成功 ${successCount},失败 ${failCount}${skipTip}`);
return;
}
message.error(`${successLabel}失败:失败 ${failCount}${skipTip}`);
}, [appendOperationLog, installDriver, refreshStatus]);
const reinstallNeededDrivers = useCallback(async () => {
await runBatchInstall(reinstallableRows, 'reinstall-updates', '当前没有需要重装的外置驱动', '重装需要更新的驱动');
}, [reinstallableRows, runBatchInstall]);
const installAllDrivers = useCallback(async () => {
await runBatchInstall(installableRows, 'install-all', '当前没有需要安装或启用的外置驱动', '安装所有驱动');
}, [installableRows, runBatchInstall]);
const removeAllDrivers = useCallback(() => {
if (removableRows.length === 0) {
message.info('当前没有可删除的外置驱动');
return;
}
Modal.confirm({
title: '删除所有已安装外置驱动?',
content: `将移除 ${removableRows.length} 个外置驱动包,后续连接对应数据源前需要重新安装。`,
okText: '删除所有',
okButtonProps: { danger: true },
cancelText: '取消',
onOk: async () => {
setBatchAction('remove-all');
setBatchProgress(createDriverBatchProgress(removableRows.length, '准备删除所有驱动'));
let successCount = 0;
let failCount = 0;
try {
for (const row of removableRows) {
setBatchProgress((prev) => {
if (!prev) {
return prev;
}
return {
...prev,
currentDriverType: row.type,
currentDriverName: row.name,
currentMessage: `正在删除:${row.name}`,
};
});
const ok = await removeDriver(row, { silentToast: true, skipRefresh: true });
if (ok) {
successCount += 1;
await refreshStatus(false, { showLoading: false });
} else {
failCount += 1;
}
setBatchProgress((prev) => {
if (!prev) {
return prev;
}
const completed = Math.min(prev.total, prev.completed + 1);
return {
...prev,
completed,
success: prev.success + (ok ? 1 : 0),
failed: prev.failed + (ok ? 0 : 1),
currentDriverType: '',
currentDriverName: '',
currentMessage: ok ? `已完成 ${row.name}` : `删除失败 ${row.name}`,
};
});
}
await refreshStatus(false);
} finally {
setBatchAction('');
setBatchProgress(null);
}
if (failCount === 0) {
message.success(`删除所有驱动完成:成功 ${successCount}`);
return;
}
if (successCount > 0) {
message.warning(`删除所有驱动完成:成功 ${successCount},失败 ${failCount}`);
return;
}
message.error(`删除所有驱动失败:失败 ${failCount}`);
},
});
}, [refreshStatus, removableRows, removeDriver]);
const renderDriverCard = (row: DriverStatusRow) => {
const progress = resolveDriverProgress(row);
const hasActiveProgress = !!progressMap[row.type] || row.connectable || row.packageInstalled;
const issueText = String(row.updateReason || row.message || '').trim();
const affectedText = row.affectedConnections && row.affectedConnections > 0
? `影响 ${row.affectedConnections} 个已保存连接`
: '';
return (
<div
key={row.type}
className={[
'driver-manager-card',
row.needsUpdate ? 'driver-manager-card-warning' : '',
row.connectable ? 'driver-manager-card-ready' : '',
].filter(Boolean).join(' ')}
style={{
border: row.needsUpdate
? driverManagerTheme.cardWarningBorder
: (row.connectable ? driverManagerTheme.cardReadyBorder : driverManagerTheme.cardBorder),
background: driverManagerTheme.cardBg,
}}
>
<div className="driver-manager-card-main">
<div className="driver-manager-card-info">
<div className="driver-manager-title-row">
<Text strong className="driver-manager-driver-name">{row.name}</Text>
<Tag>{row.type}</Tag>
{resolveDriverStatusTag(row)}
</div>
<div className="driver-manager-meta-row">
<Text type="secondary">{resolvePackageSizeText(row)}</Text>
<Text type="secondary">{row.installedVersion || row.pinnedVersion || '-'}</Text>
{affectedText ? <Text type="secondary">{affectedText}</Text> : null}
</div>
{row.needsUpdate && issueText ? (
<div className="driver-manager-update-note" style={managerUpdateNoteStyle}>
<Text strong type="warning"></Text>
<Paragraph
className="driver-manager-note-text"
ellipsis={{ rows: 2, expandable: true, symbol: '展开原因' }}
>
{issueText}
</Paragraph>
</div>
) : issueText ? (
<Paragraph
className="driver-manager-muted-message"
type="secondary"
ellipsis={{ rows: 2, expandable: true, symbol: '展开' }}
>
{issueText}
</Paragraph>
) : null}
</div>
<div className="driver-manager-card-controls">
<div className="driver-manager-control-block">
<Text type="secondary" className="driver-manager-control-label"></Text>
{renderVersionControl(row)}
</div>
<div className="driver-manager-control-block">
<Text type="secondary" className="driver-manager-control-label"></Text>
{row.builtIn ? (
<Text type="secondary"></Text>
) : hasActiveProgress ? (
<Progress percent={progress.percent} status={progress.status} size="small" />
) : (
<Progress percent={0} size="small" />
)}
</div>
{renderDriverActions(row)}
</div>
</div>
</div>
);
};
const activeDriverLogs = operationLogMap[logDriverType] || [];
const activeDriverLogLines = activeDriverLogs.map((item) => `[${item.time}] ${item.text}`);
const proxyEnvEntries = Object.entries(networkStatus?.proxyEnv || {});
const downloadRequiredHosts = (networkStatus?.downloadRequiredHosts || []).filter(Boolean);
const showDownloadChainAlert = networkStatus?.downloadChainReachable === false;
const networkUnreachable = networkStatus?.reachable === false;
const downloadRequiredHostText = (downloadRequiredHosts.length > 0
? downloadRequiredHosts
: ['github.com', 'api.github.com', 'release-assets.githubusercontent.com', 'objects.githubusercontent.com', 'raw.githubusercontent.com']).join('、');
const githubConnectivityProbe = networkStatus?.checks.find((item) => item.name === 'GitHub API')
|| networkStatus?.checks.find((item) => item.name === 'GitHub 驱动发布')
|| null;
const githubConnectivityLatencyMs = githubConnectivityProbe
? (githubConnectivityProbe.httpLatencyMs ?? githubConnectivityProbe.latencyMs ?? githubConnectivityProbe.tcpLatencyMs)
: undefined;
const logBlockBackground = darkMode
? `rgba(28, 28, 28, ${Math.max(opacity, 0.82)})`
: `rgba(255, 255, 255, ${Math.max(opacity, 0.92)})`;
const logBlockBorderColor = darkMode ? 'rgba(255, 255, 255, 0.16)' : 'rgba(0, 0, 0, 0.12)';
const logBlockTextColor = darkMode ? 'rgba(255, 255, 255, 0.88)' : 'rgba(0, 0, 0, 0.88)';
return (
<Modal
title="驱动管理"
open={open}
onCancel={onClose}
width={1120}
style={{ top: 24 }}
className="driver-manager-modal"
styles={{
body: modalBodyStyle,
}}
destroyOnHidden
footer={(
<Space className="driver-manager-footer-actions" size={8}>
<Button key="refresh" icon={<ReloadOutlined />} onClick={() => refreshStatus(true)} loading={loading}>
</Button>
<Button key="network" onClick={() => checkNetworkStatus(true)} loading={networkChecking}>
</Button>
<Button key="close" type="primary" onClick={onClose}>
{installMutatingBusy ? '后台运行' : '关闭'}
</Button>
</Space>
)}
>
<div className="driver-manager-shell" data-driver-theme={driverManagerTheme.isDark ? 'dark' : 'light'}>
<div className="driver-manager-header" style={managerSectionStyle}>
<div className="driver-manager-heading">
<Text type="secondary"> MySQL / Redis / Oracle / PostgreSQL </Text>
<Text type="secondary">GoNavi agent </Text>
</div>
<div className="driver-manager-stats">
<div className="driver-manager-stat" style={managerStatStyle}>
<span>{statusSummary.total}</span>
<Text type="secondary"></Text>
</div>
<div className="driver-manager-stat" style={managerStatStyle}>
<span>{statusSummary.enabled}</span>
<Text type="secondary"></Text>
</div>
<div className="driver-manager-stat driver-manager-stat-warning" style={managerStatStyle}>
<span style={{ color: driverManagerTheme.warningText }}>{statusSummary.needsUpdate}</span>
<Text type="secondary"></Text>
</div>
<div className="driver-manager-stat" style={managerStatStyle}>
<span>{statusSummary.notEnabled}</span>
<Text type="secondary"></Text>
</div>
</div>
</div>
<Space direction="vertical" size={12} style={{ width: '100%' }}>
{networkStatus ? (
networkUnreachable ? (
<Alert
type="error"
showIcon
message={showDownloadChainAlert ? '重要提醒:驱动下载链路域名不可达' : '重要提醒:驱动下载网络不可达'}
description={(
<Space direction="vertical" size={8} style={{ width: '100%' }}>
{showDownloadChainAlert ? (
<>
<Text>
访 GitHub
GoNavi
</Text>
{onOpenGlobalProxySettings ? (
<Button size="small" onClick={onOpenGlobalProxySettings}></Button>
) : null}
<Text>
{downloadRequiredHostText} TUN
</Text>
</>
) : (
<Text>{networkStatus.summary}</Text>
)}
{proxyEnvEntries.length > 0 ? (
<Text type="secondary">
{proxyEnvEntries.map(([key]) => key).join('、')}
</Text>
) : null}
</Space>
)}
/>
) : (
<Alert
type="success"
showIcon
message={networkStatus.summary}
description={(
<Collapse
size="small"
items={[
{
key: 'checks',
label: '查看网络检测明细',
children: (
<Space direction="vertical" size={4} style={{ width: '100%' }}>
<Text type="secondary">
GitHub {githubConnectivityProbe ? (githubConnectivityProbe.reachable ? '可达' : '不可达') : '暂无结果'}
{githubConnectivityLatencyMs !== undefined ? `${githubConnectivityLatencyMs}ms` : ''}
{githubConnectivityProbe?.error ? `${githubConnectivityProbe.error}` : ''}
</Text>
{proxyEnvEntries.length > 0 ? (
<Text type="secondary">
{proxyEnvEntries.map(([key]) => key).join('、')}
</Text>
) : (
<Text type="secondary"></Text>
)}
</Space>
),
},
]}
/>
)}
/>
)
) : (
<Alert
type="info"
showIcon
icon={sharedInfoAlertIcon}
message={networkChecking ? '正在检测驱动下载网络...' : '尚未完成网络检测'}
/>
)}
<div className="driver-manager-directory-panel" style={managerSectionStyle}>
<Collapse
size="small"
ghost
items={[
{
key: 'driver-directory',
label: '驱动目录与手动导入说明',
children: (
<Space direction="vertical" size={6} style={{ width: '100%' }}>
<Text type="secondary"></Text>
<Text type="secondary">{DRIVER_LOCAL_IMPORT_DIRECTORY_HELP}</Text>
<Text type="secondary">{DRIVER_LOCAL_IMPORT_SINGLE_FILE_HELP}</Text>
<Paragraph copyable={{ text: downloadDir || '-' }} style={{ marginBottom: 0 }}>
{downloadDir || '-'}
</Paragraph>
{networkStatus?.logPath ? (
<Paragraph copyable={{ text: networkStatus.logPath }} style={{ marginBottom: 0 }}>
{networkStatus.logPath}
</Paragraph>
) : null}
</Space>
),
},
]}
/>
</div>
<div className="driver-manager-toolbar">
<Input.Search
allowClear
placeholder="搜索驱动名称/类型(如 DuckDB、clickhouse"
value={searchKeyword}
onChange={(event) => setSearchKeyword(event.target.value)}
className="driver-manager-search"
/>
<Space size={8} wrap className="driver-manager-toolbar-actions">
<Text type="secondary"></Text>
<Switch
checked={forceOverwriteInstalled}
onChange={(checked) => setForceOverwriteInstalled(checked)}
disabled={batchDirectoryImporting}
/>
<Button
type="primary"
icon={<DownloadOutlined />}
disabled={installMutatingBusy || installableRows.length === 0}
loading={batchAction === 'install-all'}
onClick={() => void installAllDrivers()}
>
</Button>
<Button
type="primary"
icon={<DownloadOutlined />}
disabled={installMutatingBusy || reinstallableRows.length === 0}
loading={batchAction === 'reinstall-updates'}
onClick={() => void reinstallNeededDrivers()}
>
</Button>
<Button
danger
icon={<DeleteOutlined />}
disabled={installMutatingBusy || removableRows.length === 0}
loading={batchAction === 'remove-all'}
onClick={() => void removeAllDrivers()}
>
</Button>
<Button
icon={<FolderOpenOutlined />}
onClick={() => void openDriverDirectory()}
>
</Button>
<Button
icon={<FolderOpenOutlined />}
loading={batchDirectoryImporting}
disabled={batchDirectoryImporting}
onClick={() => void installDriversFromDirectory()}
>
</Button>
</Space>
</div>
{batchProgress ? (
<div className="driver-manager-batch-progress-panel" style={managerSectionStyle}>
<div className="driver-manager-batch-progress-header">
<Text strong>{resolveDriverBatchActionLabel(batchAction)}</Text>
<Text type="secondary">{batchProgressMessage || '批量任务运行中'}</Text>
</div>
<Progress percent={batchProgressPercent} status="active" />
<div className="driver-manager-batch-progress-meta">
<Text type="secondary"> {batchProgress.completed} / {batchProgress.total}</Text>
<Text type="secondary"> {batchProgress.success}</Text>
{batchProgress.failed > 0 ? <Text type="danger"> {batchProgress.failed}</Text> : null}
{batchProgress.skipped > 0 ? <Text type="secondary"> {batchProgress.skipped}</Text> : null}
{batchProgress.currentDriverName ? <Text type="secondary">{batchProgress.currentDriverName}</Text> : null}
</div>
</div>
) : null}
<div className="driver-manager-list-head">
<Text type="secondary">{filterSummaryText}</Text>
{loading ? <Text type="secondary">...</Text> : null}
</div>
<div className="driver-manager-list">
{filteredRows.length > 0 ? (
filteredRows.map(renderDriverCard)
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={normalizedSearchKeyword ? `未找到匹配“${String(searchKeyword || '').trim()}”的驱动` : '暂无驱动数据'}
/>
)}
</div>
</Space>
</div>
<Modal
title={`驱动日志 - ${activeLogRow?.name || logDriverType}`}
open={logModalOpen}
onCancel={() => setLogModalOpen(false)}
footer={[
<Button key="close-log" type="primary" onClick={() => setLogModalOpen(false)}>
</Button>,
]}
width={780}
>
<Space direction="vertical" size={8} style={{ width: '100%' }}>
{activeLogRow?.installDir ? (
<Paragraph copyable={{ text: activeLogRow.installDir }} style={{ marginBottom: 0 }}>
{activeLogRow.installDir}
</Paragraph>
) : null}
{activeLogRow?.executablePath ? (
<Paragraph copyable={{ text: activeLogRow.executablePath }} style={{ marginBottom: 0 }}>
{activeLogRow.executablePath}
</Paragraph>
) : null}
{activeDriverLogLines.length > 0 ? (
<pre style={{ margin: 0, maxHeight: 360, overflow: 'auto', padding: 12, background: logBlockBackground, color: logBlockTextColor, borderRadius: 8, border: `1px solid ${logBlockBorderColor}`, whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontFamily: 'var(--gn-font-mono)' }}>
{activeDriverLogLines.join('\n')}
</pre>
) : (
<Text type="secondary"></Text>
)}
</Space>
</Modal>
</Modal>
);
};
export default DriverManagerModal;