mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-28 01:11:31 +08:00
🐛 fix(driver-manager): 修复驱动管理网络告警重复并强化代理引导
- 新增下载链路域名探测,区分“GitHub可达但驱动下载链路不可达” - 网络不可达场景仅保留红色强提醒,移除重复二级告警 - 强提醒增加“打开全局代理设置”入口,优先引导使用 GoNavi 全局代理 - 统一网络检测与目录说明提示图标尺寸,修复加载期视觉不一致 - refs #141
This commit is contained in:
@@ -979,6 +979,7 @@ function App() {
|
||||
<DriverManagerModal
|
||||
open={isDriverModalOpen}
|
||||
onClose={() => setIsDriverModalOpen(false)}
|
||||
onOpenGlobalProxySettings={() => setIsProxyModalOpen(true)}
|
||||
/>
|
||||
<Modal
|
||||
title="关于 GoNavi"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Alert, Button, Collapse, Input, Modal, Progress, Select, Space, Switch, Table, Tag, Typography, message } from 'antd';
|
||||
import { DeleteOutlined, DownloadOutlined, FileSearchOutlined, FolderOpenOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { DeleteOutlined, DownloadOutlined, FileSearchOutlined, FolderOpenOutlined, InfoCircleFilled, ReloadOutlined } from '@ant-design/icons';
|
||||
import { EventsOn } from '../../wailsjs/runtime/runtime';
|
||||
import { useStore } from '../store';
|
||||
import { normalizeOpacityForPlatform } from '../utils/appearance';
|
||||
@@ -63,6 +63,9 @@ type DriverNetworkProbe = {
|
||||
reachable: boolean;
|
||||
httpStatus?: number;
|
||||
latencyMs?: number;
|
||||
tcpLatencyMs?: number;
|
||||
httpLatencyMs?: number;
|
||||
method?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
@@ -71,12 +74,22 @@ type DriverNetworkStatus = {
|
||||
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;
|
||||
@@ -90,8 +103,15 @@ type DriverVersionOption = {
|
||||
const buildVersionOptionKey = (option: DriverVersionOption) => `${option.version}@@${option.downloadUrl}`;
|
||||
const buildVersionSizeLoadingKey = (driverType: string, optionKey: string) => `${driverType}@@${optionKey}`;
|
||||
const DRIVER_TABLE_SCROLL_X = 1450;
|
||||
const DRIVER_STATUS_CACHE_TTL_MS = 60 * 1000;
|
||||
const DRIVER_NETWORK_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
const normalizeDriverSearchText = (value: string) => String(value || '').trim().toLowerCase();
|
||||
|
||||
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[] };
|
||||
@@ -138,7 +158,11 @@ const buildVersionSelectOptions = (options: DriverVersionOption[]) => {
|
||||
return grouped;
|
||||
};
|
||||
|
||||
const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, onClose }) => {
|
||||
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';
|
||||
@@ -166,6 +190,11 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
const [versionLoadingMap, setVersionLoadingMap] = useState<Record<string, boolean>>({});
|
||||
const [versionSizeLoadingMap, setVersionSizeLoadingMap] = useState<Record<string, boolean>>({});
|
||||
const [horizontalScrollWidth, setHorizontalScrollWidth] = useState(DRIVER_TABLE_SCROLL_X);
|
||||
const downloadDirRef = useRef(downloadDir);
|
||||
|
||||
useEffect(() => {
|
||||
downloadDirRef.current = downloadDir;
|
||||
}, [downloadDir]);
|
||||
|
||||
const appendOperationLog = useCallback((
|
||||
driverType: string,
|
||||
@@ -283,10 +312,16 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
horizontalSyncSourceRef.current = '';
|
||||
}, []);
|
||||
|
||||
const refreshStatus = useCallback(async (toastOnError = true) => {
|
||||
setLoading(true);
|
||||
const refreshStatus = useCallback(async (
|
||||
toastOnError = true,
|
||||
options?: { showLoading?: boolean },
|
||||
) => {
|
||||
const showLoading = options?.showLoading ?? true;
|
||||
if (showLoading) {
|
||||
setLoading(true);
|
||||
}
|
||||
try {
|
||||
const res = await GetDriverStatusList(downloadDir, '');
|
||||
const res = await GetDriverStatusList(downloadDirRef.current, '');
|
||||
if (!res?.success) {
|
||||
if (toastOnError) {
|
||||
message.error(res?.message || '拉取驱动状态失败');
|
||||
@@ -298,6 +333,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
const resolvedDir = String(data.downloadDir || '').trim();
|
||||
const drivers = Array.isArray(data.drivers) ? data.drivers : [];
|
||||
|
||||
const effectiveDownloadDir = resolvedDir || downloadDirRef.current;
|
||||
if (resolvedDir) {
|
||||
setDownloadDir(resolvedDir);
|
||||
}
|
||||
@@ -320,17 +356,30 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
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 {
|
||||
setLoading(false);
|
||||
if (showLoading) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [downloadDir]);
|
||||
}, []);
|
||||
|
||||
const checkNetworkStatus = useCallback(async (toastOnError = false) => {
|
||||
setNetworkChecking(true);
|
||||
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) {
|
||||
@@ -345,26 +394,40 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
name: String(item.name || '').trim(),
|
||||
url: String(item.url || '').trim(),
|
||||
reachable: !!item.reachable,
|
||||
httpStatus: Number(item.httpStatus || 0) || undefined,
|
||||
latencyMs: Number(item.latencyMs || 0) || undefined,
|
||||
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,
|
||||
}));
|
||||
setNetworkStatus({
|
||||
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 {
|
||||
setNetworkChecking(false);
|
||||
if (showLoading) {
|
||||
setNetworkChecking(false);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -523,8 +586,29 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
tableScrollTargetsRef.current = [];
|
||||
return;
|
||||
}
|
||||
refreshStatus(false);
|
||||
checkNetworkStatus(false);
|
||||
|
||||
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(() => {
|
||||
@@ -1106,6 +1190,18 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
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)})`;
|
||||
@@ -1156,15 +1252,43 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<Text type="secondary">除 MySQL / Redis / Oracle / PostgreSQL 外,其他数据源需先安装启用后再连接。</Text>
|
||||
{networkStatus ? (
|
||||
<Alert
|
||||
type={networkStatus.reachable ? 'success' : 'warning'}
|
||||
showIcon
|
||||
message={networkStatus.summary}
|
||||
description={(
|
||||
<Space direction="vertical" size={6} style={{ width: '100%' }}>
|
||||
<Text type="secondary">
|
||||
驱动下载依赖 GitHub 与 Go 模块代理网络。若检测失败,建议先启用 HTTP/HTTPS/SOCKS5 代理后重试。
|
||||
</Text>
|
||||
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={[
|
||||
@@ -1173,11 +1297,11 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
label: '查看网络检测明细',
|
||||
children: (
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
{networkStatus.checks.map((item) => (
|
||||
<Text key={`${item.name}-${item.url}`} type={item.reachable ? 'secondary' : 'danger'}>
|
||||
{item.name}:{item.reachable ? '可达' : '不可达'}{item.httpStatus ? `,HTTP ${item.httpStatus}` : ''}{item.latencyMs ? `,${item.latencyMs}ms` : ''}{item.error ? `,${item.error}` : ''}
|
||||
</Text>
|
||||
))}
|
||||
<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('、')}
|
||||
@@ -1190,30 +1314,47 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Alert type="info" showIcon message={networkChecking ? '正在检测驱动下载网络...' : '尚未完成网络检测'} />
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
icon={sharedInfoAlertIcon}
|
||||
message={networkChecking ? '正在检测驱动下载网络...' : '尚未完成网络检测'}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
icon={sharedInfoAlertIcon}
|
||||
message="驱动目录与复用说明"
|
||||
description={(
|
||||
<Space direction="vertical" size={6} style={{ width: '100%' }}>
|
||||
<Text type="secondary">自动下载和手动导入的驱动都会落盘到以下目录;后续版本升级可重复复用已下载驱动。</Text>
|
||||
<Text type="secondary">行内“本地导入”仅用于单个驱动文件/总包(如 `mariadb-driver-agent`、`mariadb-driver-agent.exe`、`GoNavi-DriverAgents.zip`);批量导入请使用上方“导入驱动目录”。</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>
|
||||
<Collapse
|
||||
size="small"
|
||||
items={[
|
||||
{
|
||||
key: 'driver-directory',
|
||||
label: '查看驱动目录与复用说明',
|
||||
children: (
|
||||
<Space direction="vertical" size={6} style={{ width: '100%' }}>
|
||||
<Text type="secondary">自动下载和手动导入的驱动都会落盘到以下目录;后续版本升级可重复复用已下载驱动。</Text>
|
||||
<Text type="secondary">行内“本地导入”仅用于单个驱动文件/总包(如 `mariadb-driver-agent`、`mariadb-driver-agent.exe`、`GoNavi-DriverAgents.zip`);批量导入请使用上方“导入驱动目录”。</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>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
@@ -83,12 +83,15 @@ type driverDownloadProgressPayload struct {
|
||||
}
|
||||
|
||||
type driverNetworkProbeItem struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Reachable bool `json:"reachable"`
|
||||
HTTPStatus int `json:"httpStatus,omitempty"`
|
||||
LatencyMs int64 `json:"latencyMs,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Reachable bool `json:"reachable"`
|
||||
HTTPStatus int `json:"httpStatus,omitempty"`
|
||||
LatencyMs int64 `json:"latencyMs,omitempty"`
|
||||
TCPLatency int64 `json:"tcpLatencyMs,omitempty"`
|
||||
HTTPLatency int64 `json:"httpLatencyMs,omitempty"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type pinnedDriverPackage struct {
|
||||
@@ -201,6 +204,7 @@ const (
|
||||
driverBundleIndexMaxSize = 1 << 20
|
||||
driverManifestMaxSize = 2 << 20
|
||||
driverNetworkProbeTimeout = 4 * time.Second
|
||||
driverNetworkProbeTCPTimeout = 3 * time.Second
|
||||
localDriverDirectoryScanMaxEntries = 20000
|
||||
driverChecksumPolicyStrict = "strict"
|
||||
driverChecksumPolicyWarn = "warn"
|
||||
@@ -647,24 +651,43 @@ func (a *App) CheckDriverNetworkStatus() connection.QueryResult {
|
||||
Name: "GitHub 驱动发布",
|
||||
URL: fmt.Sprintf("https://github.com/%s/releases/latest/download/%s", updateRepo, optionalDriverBundleAssetName),
|
||||
},
|
||||
{
|
||||
Name: "GitHub Release 资产域名",
|
||||
URL: "https://release-assets.githubusercontent.com/",
|
||||
},
|
||||
{
|
||||
Name: "Go 模块代理",
|
||||
URL: "https://proxy.golang.org/github.com/go-sql-driver/mysql/@v/list",
|
||||
},
|
||||
}
|
||||
|
||||
client := newHTTPClientWithGlobalProxy(driverNetworkProbeTimeout)
|
||||
allReachable := true
|
||||
for index := range checks {
|
||||
checks[index] = probeDriverNetworkEndpoint(checks[index])
|
||||
checks[index] = probeDriverNetworkEndpoint(client, checks[index])
|
||||
if !checks[index].Reachable {
|
||||
allReachable = false
|
||||
}
|
||||
}
|
||||
findProbe := func(name string) (driverNetworkProbeItem, bool) {
|
||||
for _, item := range checks {
|
||||
if strings.EqualFold(strings.TrimSpace(item.Name), strings.TrimSpace(name)) {
|
||||
return item, true
|
||||
}
|
||||
}
|
||||
return driverNetworkProbeItem{}, false
|
||||
}
|
||||
githubAPICheck, _ := findProbe("GitHub API")
|
||||
githubReleaseCheck, _ := findProbe("GitHub 驱动发布")
|
||||
releaseAssetsCheck, _ := findProbe("GitHub Release 资产域名")
|
||||
downloadChainReachable := githubReleaseCheck.Reachable && releaseAssetsCheck.Reachable
|
||||
|
||||
proxyEnv := collectDriverProxyEnv()
|
||||
proxyConfigured := len(proxyEnv) > 0
|
||||
summary := "驱动下载网络检测通过,可直接安装驱动。"
|
||||
if !allReachable {
|
||||
if githubAPICheck.Reachable && !downloadChainReachable {
|
||||
summary = "重要提醒:GitHub API 可达,但驱动下载链路不可达。请优先在 GoNavi 启用全局代理(填写代理应用本地地址和端口),并在代理规则中放行 github.com、api.github.com、release-assets.githubusercontent.com、objects.githubusercontent.com、raw.githubusercontent.com;若仍失败,再考虑开启 TUN 模式。"
|
||||
} else if !allReachable {
|
||||
if proxyConfigured {
|
||||
summary = "检测到部分驱动下载地址不可达,请确认系统代理配置有效后重试。"
|
||||
} else {
|
||||
@@ -678,6 +701,14 @@ func (a *App) CheckDriverNetworkStatus() connection.QueryResult {
|
||||
"recommendedProxy": !allReachable,
|
||||
"proxyConfigured": proxyConfigured,
|
||||
"proxyEnv": proxyEnv,
|
||||
"downloadChainReachable": downloadChainReachable,
|
||||
"downloadRequiredHosts": []string{
|
||||
"github.com",
|
||||
"api.github.com",
|
||||
"release-assets.githubusercontent.com",
|
||||
"objects.githubusercontent.com",
|
||||
"raw.githubusercontent.com",
|
||||
},
|
||||
"checkedAt": time.Now().Format(time.RFC3339),
|
||||
"checks": checks,
|
||||
}
|
||||
@@ -890,12 +921,15 @@ func (a *App) emitDriverDownloadProgress(driverType string, status string, downl
|
||||
runtime.EventsEmit(a.ctx, driverDownloadProgressEvent, payload)
|
||||
}
|
||||
|
||||
func probeDriverNetworkEndpoint(item driverNetworkProbeItem) driverNetworkProbeItem {
|
||||
func probeDriverNetworkEndpoint(client *http.Client, item driverNetworkProbeItem) driverNetworkProbeItem {
|
||||
probed := item
|
||||
probed.Reachable = false
|
||||
probed.HTTPStatus = 0
|
||||
probed.Error = ""
|
||||
probed.LatencyMs = 0
|
||||
probed.TCPLatency = 0
|
||||
probed.HTTPLatency = 0
|
||||
probed.Method = ""
|
||||
|
||||
urlText := strings.TrimSpace(item.URL)
|
||||
if urlText == "" {
|
||||
@@ -903,33 +937,34 @@ func probeDriverNetworkEndpoint(item driverNetworkProbeItem) driverNetworkProbeI
|
||||
return probed
|
||||
}
|
||||
|
||||
client := newHTTPClientWithGlobalProxy(driverNetworkProbeTimeout)
|
||||
start := time.Now()
|
||||
req, err := http.NewRequest(http.MethodHead, urlText, nil)
|
||||
if err != nil {
|
||||
probed.Error = normalizeErrorMessage(err)
|
||||
return probed
|
||||
if tcpLatency, tcpErr := probeDriverTCPLatency(urlText); tcpErr == nil {
|
||||
probed.TCPLatency = tcpLatency
|
||||
probed.LatencyMs = tcpLatency
|
||||
}
|
||||
req.Header.Set("User-Agent", "GoNavi-DriverManager")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
// 某些网关不支持 HEAD,请回退为 GET(不读取正文)。
|
||||
reqGet, reqErr := http.NewRequest(http.MethodGet, urlText, nil)
|
||||
if reqErr != nil {
|
||||
probed.Error = normalizeErrorMessage(reqErr)
|
||||
probed.LatencyMs = time.Since(start).Milliseconds()
|
||||
return probed
|
||||
}
|
||||
reqGet.Header.Set("User-Agent", "GoNavi-DriverManager")
|
||||
resp, err = client.Do(reqGet)
|
||||
if client == nil {
|
||||
client = newHTTPClientWithGlobalProxy(driverNetworkProbeTimeout)
|
||||
}
|
||||
start := time.Now()
|
||||
resp, method, err := doDriverProbeRequest(client, urlText, http.MethodGet)
|
||||
if err != nil || shouldFallbackHeadProbe(resp) {
|
||||
if resp != nil {
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
// 回退到 HEAD 时重置计时,避免把失败重试耗时累计到最终延迟指标里。
|
||||
start = time.Now()
|
||||
resp, method, err = doDriverProbeRequest(client, urlText, http.MethodHead)
|
||||
}
|
||||
probed.HTTPLatency = time.Since(start).Milliseconds()
|
||||
if probed.LatencyMs <= 0 {
|
||||
probed.LatencyMs = probed.HTTPLatency
|
||||
}
|
||||
probed.LatencyMs = time.Since(start).Milliseconds()
|
||||
if err != nil {
|
||||
probed.Error = normalizeDriverNetworkError(err)
|
||||
return probed
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
probed.Method = method
|
||||
|
||||
probed.HTTPStatus = resp.StatusCode
|
||||
if resp.StatusCode >= 500 {
|
||||
@@ -940,6 +975,121 @@ func probeDriverNetworkEndpoint(item driverNetworkProbeItem) driverNetworkProbeI
|
||||
return probed
|
||||
}
|
||||
|
||||
func probeDriverTCPLatency(rawURL string) (int64, error) {
|
||||
dialAddr, err := resolveDriverProbeDialAddress(rawURL)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
start := time.Now()
|
||||
conn, err := net.DialTimeout("tcp", dialAddr, driverNetworkProbeTCPTimeout)
|
||||
elapsed := time.Since(start)
|
||||
latency := elapsed.Milliseconds()
|
||||
if elapsed > 0 && latency <= 0 {
|
||||
latency = 1
|
||||
}
|
||||
if err != nil {
|
||||
return latency, err
|
||||
}
|
||||
_ = conn.Close()
|
||||
return latency, nil
|
||||
}
|
||||
|
||||
func resolveDriverProbeDialAddress(rawURL string) (string, error) {
|
||||
urlText := strings.TrimSpace(rawURL)
|
||||
if urlText == "" {
|
||||
return "", fmt.Errorf("检测地址为空")
|
||||
}
|
||||
parsed, err := url.Parse(urlText)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
targetHost := strings.TrimSpace(parsed.Hostname())
|
||||
if targetHost == "" {
|
||||
return "", fmt.Errorf("检测地址缺少主机")
|
||||
}
|
||||
targetPort := strings.TrimSpace(parsed.Port())
|
||||
if targetPort == "" {
|
||||
if strings.EqualFold(parsed.Scheme, "http") {
|
||||
targetPort = "80"
|
||||
} else {
|
||||
targetPort = "443"
|
||||
}
|
||||
}
|
||||
|
||||
if proxyURL := resolveDriverProbeProxyURL(parsed); proxyURL != nil {
|
||||
proxyHost := strings.TrimSpace(proxyURL.Hostname())
|
||||
if proxyHost == "" {
|
||||
return net.JoinHostPort(targetHost, targetPort), nil
|
||||
}
|
||||
proxyPort := strings.TrimSpace(proxyURL.Port())
|
||||
if proxyPort == "" {
|
||||
proxyPort = defaultPortForScheme(proxyURL.Scheme)
|
||||
}
|
||||
return net.JoinHostPort(proxyHost, proxyPort), nil
|
||||
}
|
||||
|
||||
return net.JoinHostPort(targetHost, targetPort), nil
|
||||
}
|
||||
|
||||
func resolveDriverProbeProxyURL(target *url.URL) *url.URL {
|
||||
if target == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
snapshot := currentGlobalProxyConfig()
|
||||
if snapshot.Enabled {
|
||||
proxyURL, err := buildProxyURLFromConfig(snapshot.Proxy)
|
||||
if err == nil {
|
||||
return proxyURL
|
||||
}
|
||||
}
|
||||
|
||||
req := &http.Request{URL: target}
|
||||
proxyURL, err := http.ProxyFromEnvironment(req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return proxyURL
|
||||
}
|
||||
|
||||
func defaultPortForScheme(scheme string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(scheme)) {
|
||||
case "https":
|
||||
return "443"
|
||||
case "socks5", "socks5h":
|
||||
return "1080"
|
||||
case "http":
|
||||
fallthrough
|
||||
default:
|
||||
return "80"
|
||||
}
|
||||
}
|
||||
|
||||
func doDriverProbeRequest(client *http.Client, urlText string, method string) (*http.Response, string, error) {
|
||||
req, err := http.NewRequest(method, urlText, nil)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
req.Header.Set("User-Agent", "GoNavi-DriverManager")
|
||||
// 用 GET+Range 探测可更接近真实下载链路,同时避免下载正文。
|
||||
if strings.EqualFold(method, http.MethodGet) {
|
||||
req.Header.Set("Range", "bytes=0-0")
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, method, err
|
||||
}
|
||||
return resp, method, nil
|
||||
}
|
||||
|
||||
func shouldFallbackHeadProbe(resp *http.Response) bool {
|
||||
if resp == nil {
|
||||
return false
|
||||
}
|
||||
return resp.StatusCode == http.StatusMethodNotAllowed || resp.StatusCode == http.StatusNotImplemented
|
||||
}
|
||||
|
||||
func normalizeDriverNetworkError(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
|
||||
Reference in New Issue
Block a user