feat(driver-manager): 增强驱动管理网络诊断与本地导入能力

- 新增 CheckDriverNetworkStatus,探测 GitHub API/Release/Go Proxy 可达性并返回代理环境信息。
- 驱动管理弹窗新增网络检测结果、驱动目录复用说明、本地导入入口与日志查看。
- 操作日志支持同签名进度覆盖更新,下载百分比动态刷新,不再重复新增日志行。
- 修正弹窗滚动行为与表格滚动体验。
- refs #128
This commit is contained in:
Syngnat
2026-02-27 12:29:54 +08:00
parent 53b5802add
commit 7350a011e3
4 changed files with 706 additions and 54 deletions

View File

@@ -1,16 +1,19 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Button, Modal, Progress, Select, Space, Table, Tag, Typography, message } from 'antd';
import { DeleteOutlined, DownloadOutlined, ReloadOutlined } from '@ant-design/icons';
import { Alert, Button, Collapse, Modal, Progress, Select, Space, Table, Tag, Typography, message } from 'antd';
import { DeleteOutlined, DownloadOutlined, FileSearchOutlined, ReloadOutlined } from '@ant-design/icons';
import { EventsOn } from '../../wailsjs/runtime/runtime';
import {
CheckDriverNetworkStatus,
DownloadDriverPackage,
GetDriverVersionList,
GetDriverVersionPackageSize,
GetDriverStatusList,
InstallLocalDriverPackage,
RemoveDriverPackage,
SelectDriverPackageFile,
} from '../../wailsjs/go/app/App';
const { Text } = Typography;
const { Paragraph, Text } = Typography;
type DriverStatusRow = {
type: string;
@@ -23,6 +26,10 @@ type DriverStatusRow = {
packageInstalled: boolean;
connectable: boolean;
defaultDownloadUrl?: string;
installDir?: string;
packagePath?: string;
executablePath?: string;
downloadedAt?: string;
message?: string;
};
@@ -39,6 +46,32 @@ type ProgressState = {
percent: number;
};
type DriverLogEntry = {
time: string;
text: string;
signature: string;
};
type DriverNetworkProbe = {
name: string;
url: string;
reachable: boolean;
httpStatus?: number;
latencyMs?: number;
error?: string;
};
type DriverNetworkStatus = {
reachable: boolean;
summary: string;
recommendedProxy: boolean;
proxyConfigured: boolean;
proxyEnv?: Record<string, string>;
checks: DriverNetworkProbe[];
checkedAt?: string;
logPath?: string;
};
type DriverVersionOption = {
version: string;
downloadUrl: string;
@@ -101,14 +134,65 @@ const buildVersionSelectOptions = (options: DriverVersionOption[]) => {
const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, onClose }) => {
const [loading, setLoading] = useState(false);
const [downloadDir, setDownloadDir] = useState('');
const [networkChecking, setNetworkChecking] = useState(false);
const [networkStatus, setNetworkStatus] = useState<DriverNetworkStatus | null>(null);
const [rows, setRows] = useState<DriverStatusRow[]>([]);
const [actionDriver, setActionDriver] = useState('');
const [progressMap, setProgressMap] = useState<Record<string, ProgressState>>({});
const [operationLogMap, setOperationLogMap] = useState<Record<string, DriverLogEntry[]>>({});
const [logDriverType, setLogDriverType] = useState('');
const [logModalOpen, setLogModalOpen] = 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 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) => {
setLoading(true);
try {
@@ -139,6 +223,10 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
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,
message: String(item.message || '').trim() || undefined,
}));
setRows(nextRows);
@@ -151,6 +239,45 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
}
}, [downloadDir]);
const checkNetworkStatus = useCallback(async (toastOnError = false) => {
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: Number(item.httpStatus || 0) || undefined,
latencyMs: Number(item.latencyMs || 0) || undefined,
error: String(item.error || '').trim() || undefined,
}));
setNetworkStatus({
reachable: !!data.reachable,
summary: String(data.summary || '').trim() || '驱动网络检测已完成',
recommendedProxy: !!data.recommendedProxy,
proxyConfigured: !!data.proxyConfigured,
proxyEnv: (data.proxyEnv || {}) as Record<string, string>,
checkedAt: String(data.checkedAt || '').trim() || undefined,
checks: normalizedChecks,
logPath: String(data.logPath || '').trim() || undefined,
});
} catch (err: any) {
if (toastOnError) {
message.error(`驱动网络检测失败:${err?.message || String(err)}`);
}
} finally {
setNetworkChecking(false);
}
}, []);
const loadVersionOptions = useCallback(async (row: DriverStatusRow, toastOnError = false) => {
if (row.builtIn) {
return [] as DriverVersionOption[];
@@ -305,7 +432,8 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
return;
}
refreshStatus(false);
}, [open, refreshStatus]);
checkNetworkStatus(false);
}, [checkNetworkStatus, open, refreshStatus]);
useEffect(() => {
if (!open) {
@@ -330,11 +458,16 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
percent,
},
}));
const progressText = `${Math.round(percent)}%`;
const statusText = String(status || '').toUpperCase();
const lineText = `[${statusText}] ${messageText || '-'} (${progressText})`;
const lineSignature = `${statusText}|${messageText || '-'}`;
appendOperationLog(driverType, lineText, lineSignature, 'update-last');
});
return () => {
off();
};
}, [open]);
}, [appendOperationLog, open]);
const installDriver = useCallback(async (row: DriverStatusRow) => {
setActionDriver(row.type);
@@ -346,6 +479,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
percent: 0,
},
}));
appendOperationLog(row.type, '[START] 开始自动安装');
try {
let options = versionMap[row.type] || [];
if (options.length === 0) {
@@ -361,25 +495,81 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
const result = await DownloadDriverPackage(row.type, selectedVersion, selectedDownloadURL, downloadDir);
if (!result?.success) {
message.error(result?.message || `安装 ${row.name} 失败`);
const errText = result?.message || `安装 ${row.name} 失败`;
appendOperationLog(row.type, `[ERROR] ${errText}`);
message.error(errText);
return;
}
const versionTip = selectedVersion ? `${selectedVersion}` : '';
appendOperationLog(row.type, `[DONE] 自动安装完成 ${versionTip}`);
message.success(`${row.name}${versionTip} 已安装启用`);
refreshStatus(false);
} finally {
setActionDriver('');
}
}, [downloadDir, loadVersionOptions, refreshStatus, selectedVersionMap, versionMap]);
}, [appendOperationLog, downloadDir, loadVersionOptions, refreshStatus, selectedVersionMap, versionMap]);
const installDriverFromLocalFile = useCallback(async (row: DriverStatusRow) => {
const fileRes = await SelectDriverPackageFile(downloadDir);
if (!fileRes?.success) {
if (String(fileRes?.message || '') !== 'Cancelled') {
message.error(fileRes?.message || '选择本地驱动包失败');
}
return;
}
const filePath = String((fileRes?.data as any)?.path || '').trim();
if (!filePath) {
message.error('未选择有效的驱动包文件');
return;
}
setActionDriver(row.type);
setProgressMap((prev) => ({
...prev,
[row.type]: {
status: 'start',
message: '开始导入本地驱动包',
percent: 0,
},
}));
appendOperationLog(row.type, `[START] 开始本地导入:${filePath}`);
try {
const result = await InstallLocalDriverPackage(row.type, filePath, downloadDir);
if (!result?.success) {
const errText = result?.message || `导入 ${row.name} 本地驱动包失败`;
appendOperationLog(row.type, `[ERROR] ${errText}`);
message.error(errText);
return;
}
appendOperationLog(row.type, '[DONE] 本地导入安装完成');
message.success(`${row.name} 本地驱动包已安装启用`);
refreshStatus(false);
} finally {
setActionDriver('');
}
}, [appendOperationLog, downloadDir, refreshStatus]);
const openDriverLog = useCallback((driverType: string) => {
const normalized = String(driverType || '').trim().toLowerCase();
if (!normalized) {
return;
}
setLogDriverType(normalized);
setLogModalOpen(true);
}, []);
const removeDriver = useCallback(async (row: DriverStatusRow) => {
setActionDriver(row.type);
appendOperationLog(row.type, '[START] 开始移除驱动');
try {
const result = await RemoveDriverPackage(row.type, downloadDir);
if (!result?.success) {
message.error(result?.message || `移除 ${row.name} 失败`);
const errText = result?.message || `移除 ${row.name} 失败`;
appendOperationLog(row.type, `[ERROR] ${errText}`);
message.error(errText);
return;
}
appendOperationLog(row.type, '[DONE] 驱动移除完成');
message.success(`${row.name} 已移除`);
setProgressMap((prev) => {
const next = { ...prev };
@@ -390,7 +580,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
} finally {
setActionDriver('');
}
}, [downloadDir, refreshStatus]);
}, [appendOperationLog, downloadDir, refreshStatus]);
const columns = useMemo(() => {
return [
@@ -400,6 +590,25 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
key: 'name',
width: 150,
},
{
title: '安装位置',
key: 'installPath',
width: 260,
render: (_: string, row: DriverStatusRow) => {
if (row.builtIn) {
return <Text type="secondary"></Text>;
}
const installPath = row.executablePath || row.installDir || '-';
if (installPath === '-') {
return <Text type="secondary">-</Text>;
}
return (
<Text copyable={{ text: installPath }} style={{ fontSize: 12 }}>
{installPath}
</Text>
);
},
},
{
title: '安装包大小',
dataIndex: 'packageSizeText',
@@ -511,7 +720,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
{
title: '操作',
key: 'actions',
width: 190,
width: 320,
render: (_: string, row: DriverStatusRow) => {
if (row.builtIn) {
return <Text type="secondary">-</Text>;
@@ -521,19 +730,20 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
if (isSlimBuildUnavailable && !row.packageInstalled) {
return <Text type="secondary"> Full </Text>;
}
if (row.connectable) {
return (
<Button
danger
icon={<DeleteOutlined />}
loading={loadingAction}
onClick={() => removeDriver(row)}
>
</Button>
);
}
return (
const logs = operationLogMap[row.type] || [];
const hasLogs = logs.length > 0;
const mainAction = row.connectable ? (
<Button
danger
icon={<DeleteOutlined />}
loading={loadingAction}
onClick={() => removeDriver(row)}
>
</Button>
) : (
<Button
type="primary"
icon={<DownloadOutlined />}
@@ -543,10 +753,41 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
</Button>
);
return (
<Space size={8} wrap>
{mainAction}
<Button
icon={<FileSearchOutlined />}
loading={loadingAction}
onClick={() => installDriverFromLocalFile(row)}
>
</Button>
<Button
type={hasLogs ? 'default' : 'text'}
disabled={!hasLogs}
onClick={() => openDriverLog(row.type)}
>
</Button>
</Space>
);
},
},
];
}, [actionDriver, installDriver, loadVersionOptions, loadVersionPackageSize, progressMap, removeDriver, selectedVersionMap, versionLoadingMap, versionMap, versionSizeLoadingMap]);
}, [actionDriver, installDriver, installDriverFromLocalFile, loadVersionOptions, loadVersionPackageSize, openDriverLog, operationLogMap, progressMap, removeDriver, selectedVersionMap, versionLoadingMap, versionMap, versionSizeLoadingMap]);
const activeLogRow = useMemo(() => {
if (!logDriverType) {
return undefined;
}
return rows.find((item) => item.type === logDriverType);
}, [logDriverType, rows]);
const activeDriverLogs = operationLogMap[logDriverType] || [];
const activeDriverLogLines = activeDriverLogs.map((item) => `[${item.time}] ${item.text}`);
const proxyEnvEntries = Object.entries(networkStatus?.proxyEnv || {});
return (
<Modal
@@ -554,11 +795,23 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
open={open}
onCancel={onClose}
width={980}
style={{ top: 24 }}
styles={{
body: {
maxHeight: 'calc(100vh - 220px)',
overflowY: 'auto',
overflowX: 'hidden',
paddingRight: 18,
},
}}
destroyOnClose
footer={[
<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}>
</Button>,
@@ -566,6 +819,67 @@ 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>
<Collapse
size="small"
items={[
{
key: 'checks',
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>
))}
{proxyEnvEntries.length > 0 ? (
<Text type="secondary">
{proxyEnvEntries.map(([key]) => key).join('、')}
</Text>
) : (
<Text type="secondary"></Text>
)}
</Space>
),
},
]}
/>
</Space>
)}
/>
) : (
<Alert type="info" showIcon message={networkChecking ? '正在检测驱动下载网络...' : '尚未完成网络检测'} />
)}
<Alert
type="info"
showIcon
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>
)}
/>
<Table
rowKey="type"
@@ -574,8 +888,40 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
dataSource={rows}
pagination={false}
size="middle"
scroll={{ x: 1450 }}
/>
</Space>
<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: '#fafafa', borderRadius: 8, border: '1px solid #f0f0f0', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
{activeDriverLogLines.join('\n')}
</pre>
) : (
<Text type="secondary"></Text>
)}
</Space>
</Modal>
</Modal>
);
};

View File

@@ -6,6 +6,8 @@ import {redis} from '../models';
export function ApplyChanges(arg1:connection.ConnectionConfig,arg2:string,arg3:string,arg4:connection.ChangeSet):Promise<connection.QueryResult>;
export function CheckDriverNetworkStatus():Promise<connection.QueryResult>;
export function CheckForUpdates():Promise<connection.QueryResult>;
export function ConfigureDriverRuntimeDirectory(arg1:string):Promise<connection.QueryResult>;

View File

@@ -6,6 +6,10 @@ export function ApplyChanges(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['ApplyChanges'](arg1, arg2, arg3, arg4);
}
export function CheckDriverNetworkStatus() {
return window['go']['app']['App']['CheckDriverNetworkStatus']();
}
export function CheckForUpdates() {
return window['go']['app']['App']['CheckForUpdates']();
}

View File

@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
@@ -21,6 +22,7 @@ import (
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/db"
"GoNavi-Wails/internal/logger"
"github.com/wailsapp/wails/v2/pkg/runtime"
"golang.org/x/mod/semver"
@@ -79,6 +81,15 @@ type driverDownloadProgressPayload struct {
Message string `json:"message,omitempty"`
}
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"`
}
type pinnedDriverPackage struct {
Version string
DownloadURL string
@@ -188,6 +199,7 @@ const (
driverVersionWarmupMinInterval = 30 * time.Second
driverBundleIndexMaxSize = 1 << 20
driverManifestMaxSize = 2 << 20
driverNetworkProbeTimeout = 4 * time.Second
driverChecksumPolicyStrict = "strict"
driverChecksumPolicyWarn = "warn"
driverChecksumPolicyOff = "off"
@@ -592,6 +604,59 @@ func (a *App) GetDriverStatusList(downloadDir string, manifestURL string) connec
}
}
func (a *App) CheckDriverNetworkStatus() connection.QueryResult {
checks := []driverNetworkProbeItem{
{
Name: "GitHub API",
URL: "https://api.github.com/rate_limit",
},
{
Name: "GitHub 驱动发布",
URL: fmt.Sprintf("https://github.com/%s/releases/latest/download/%s", updateRepo, optionalDriverBundleAssetName),
},
{
Name: "Go 模块代理",
URL: "https://proxy.golang.org/github.com/go-sql-driver/mysql/@v/list",
},
}
allReachable := true
for index := range checks {
checks[index] = probeDriverNetworkEndpoint(checks[index])
if !checks[index].Reachable {
allReachable = false
}
}
proxyEnv := collectDriverProxyEnv()
proxyConfigured := len(proxyEnv) > 0
summary := "驱动下载网络检测通过,可直接安装驱动。"
if !allReachable {
if proxyConfigured {
summary = "检测到部分驱动下载地址不可达,请确认系统代理配置有效后重试。"
} else {
summary = "检测到部分驱动下载地址不可达,建议先配置 HTTP/HTTPS/SOCKS5 代理后再安装驱动。"
}
}
data := map[string]interface{}{
"reachable": allReachable,
"summary": summary,
"recommendedProxy": !allReachable,
"proxyConfigured": proxyConfigured,
"proxyEnv": proxyEnv,
"checkedAt": time.Now().Format(time.RFC3339),
"checks": checks,
}
if logPath := strings.TrimSpace(logger.Path()); logPath != "" {
data["logPath"] = logPath
}
return connection.QueryResult{
Success: true,
Data: data,
}
}
func (a *App) InstallLocalDriverPackage(driverType string, filePath string, downloadDir string) connection.QueryResult {
definition, ok := resolveDriverDefinition(driverType)
if !ok {
@@ -614,28 +679,27 @@ func (a *App) InstallLocalDriverPackage(driverType string, filePath string, down
}
db.SetExternalDriverDownloadDirectory(resolvedDir)
hash := ""
if pathText := strings.TrimSpace(filePath); pathText != "" {
if fileHash, hashErr := hashFileSHA256(pathText); hashErr == nil {
hash = fileHash
a.emitDriverDownloadProgress(definition.Type, "start", 0, 100, "开始安装本地驱动包")
selectedVersion := resolveDriverInstallVersion(definition.PinnedVersion, "local://manual", definition)
meta, installErr := installOptionalDriverAgentFromLocalFile(definition, filePath, resolvedDir, selectedVersion)
if installErr != nil {
errText := normalizeErrorMessage(installErr)
a.emitDriverDownloadProgress(definition.Type, "error", 0, 0, errText)
return connection.QueryResult{
Success: false,
Message: logDriverOperationError(installErr, "导入本地驱动包失败driver=%s file=%s", definition.Type, strings.TrimSpace(filePath)),
}
}
a.emitDriverDownloadProgress(definition.Type, "start", 0, 0, "开始安装")
meta := installedDriverPackage{
DriverType: definition.Type,
Version: resolveDriverInstallVersion(definition.PinnedVersion, "local://activate", definition),
FilePath: "",
FileName: "embedded-go-driver",
DownloadURL: "local://activate",
SHA256: hash,
DownloadedAt: time.Now().Format(time.RFC3339),
}
a.emitDriverDownloadProgress(definition.Type, "downloading", 90, 100, "写入驱动元数据")
if err := writeInstalledDriverPackage(resolvedDir, definition.Type, meta); err != nil {
a.emitDriverDownloadProgress(definition.Type, "error", 0, 0, err.Error())
return connection.QueryResult{Success: false, Message: err.Error()}
errText := normalizeErrorMessage(err)
a.emitDriverDownloadProgress(definition.Type, "error", 0, 0, errText)
return connection.QueryResult{
Success: false,
Message: logDriverOperationError(err, "写入本地驱动元数据失败driver=%s", definition.Type),
}
}
a.emitDriverDownloadProgress(definition.Type, "done", 1, 1, "安装完成(纯 Go 驱动已启用)")
a.emitDriverDownloadProgress(definition.Type, "done", 100, 100, "本地驱动包导入完成")
return connection.QueryResult{Success: true, Message: "驱动安装成功", Data: map[string]interface{}{
"driverType": definition.Type,
@@ -683,13 +747,21 @@ func (a *App) DownloadDriverPackage(driverType string, version string, downloadU
a.emitDriverDownloadProgress(definition.Type, "start", 0, 100, fmt.Sprintf("开始安装 %s 驱动代理", displayName))
meta, installErr := installOptionalDriverAgentPackage(a, definition, selectedVersion, resolvedDir, urlText)
if installErr != nil {
a.emitDriverDownloadProgress(definition.Type, "error", 0, 0, installErr.Error())
return connection.QueryResult{Success: false, Message: installErr.Error()}
errText := normalizeErrorMessage(installErr)
a.emitDriverDownloadProgress(definition.Type, "error", 0, 0, errText)
return connection.QueryResult{
Success: false,
Message: logDriverOperationError(installErr, "驱动下载安装失败driver=%s version=%s url=%s", definition.Type, selectedVersion, urlText),
}
}
a.emitDriverDownloadProgress(definition.Type, "downloading", 95, 100, "写入驱动元数据")
if writeErr := writeInstalledDriverPackage(resolvedDir, definition.Type, meta); writeErr != nil {
a.emitDriverDownloadProgress(definition.Type, "error", 0, 0, writeErr.Error())
return connection.QueryResult{Success: false, Message: writeErr.Error()}
errText := normalizeErrorMessage(writeErr)
a.emitDriverDownloadProgress(definition.Type, "error", 0, 0, errText)
return connection.QueryResult{
Success: false,
Message: logDriverOperationError(writeErr, "写入驱动元数据失败driver=%s version=%s", definition.Type, selectedVersion),
}
}
a.emitDriverDownloadProgress(definition.Type, "done", 100, 100, fmt.Sprintf("%s 驱动代理安装完成", displayName))
return connection.QueryResult{Success: true, Message: "驱动安装成功", Data: map[string]interface{}{
@@ -710,8 +782,12 @@ func (a *App) DownloadDriverPackage(driverType string, version string, downloadU
DownloadedAt: time.Now().Format(time.RFC3339),
}
if err := writeInstalledDriverPackage(resolvedDir, definition.Type, meta); err != nil {
a.emitDriverDownloadProgress(definition.Type, "error", 0, 0, err.Error())
return connection.QueryResult{Success: false, Message: err.Error()}
errText := normalizeErrorMessage(err)
a.emitDriverDownloadProgress(definition.Type, "error", 0, 0, errText)
return connection.QueryResult{
Success: false,
Message: logDriverOperationError(err, "写入驱动元数据失败driver=%s version=%s", definition.Type, selectedVersion),
}
}
a.emitDriverDownloadProgress(definition.Type, "done", 1, 1, "安装完成(纯 Go 驱动已启用)")
@@ -781,6 +857,100 @@ func (a *App) emitDriverDownloadProgress(driverType string, status string, downl
runtime.EventsEmit(a.ctx, driverDownloadProgressEvent, payload)
}
func probeDriverNetworkEndpoint(item driverNetworkProbeItem) driverNetworkProbeItem {
probed := item
probed.Reachable = false
probed.HTTPStatus = 0
probed.Error = ""
probed.LatencyMs = 0
urlText := strings.TrimSpace(item.URL)
if urlText == "" {
probed.Error = "检测地址为空"
return probed
}
client := &http.Client{Timeout: driverNetworkProbeTimeout}
start := time.Now()
req, err := http.NewRequest(http.MethodHead, urlText, nil)
if err != nil {
probed.Error = normalizeErrorMessage(err)
return probed
}
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)
}
probed.LatencyMs = time.Since(start).Milliseconds()
if err != nil {
probed.Error = normalizeDriverNetworkError(err)
return probed
}
defer resp.Body.Close()
probed.HTTPStatus = resp.StatusCode
if resp.StatusCode >= 500 {
probed.Error = fmt.Sprintf("HTTP %d", resp.StatusCode)
return probed
}
probed.Reachable = true
return probed
}
func normalizeDriverNetworkError(err error) string {
if err == nil {
return ""
}
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
return "网络连接超时"
}
return normalizeErrorMessage(err)
}
func collectDriverProxyEnv() map[string]string {
keys := []string{
"HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY",
"http_proxy", "https_proxy", "all_proxy", "no_proxy",
}
result := make(map[string]string)
for _, key := range keys {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
continue
}
result[key] = value
}
return result
}
func driverLogHint() string {
path := strings.TrimSpace(logger.Path())
if path == "" {
return ""
}
return fmt.Sprintf("(详细日志:%s", path)
}
func logDriverOperationError(err error, format string, args ...interface{}) string {
message := normalizeErrorMessage(err)
if strings.TrimSpace(message) == "" {
message = "未知错误"
}
logger.Error(err, format, args...)
return strings.TrimSpace(message) + driverLogHint()
}
func defaultDriverDownloadDirectory() string {
root, err := db.ResolveExternalDriverRoot("")
if err == nil && strings.TrimSpace(root) != "" {
@@ -1378,12 +1548,7 @@ func fetchGoModuleVersionMetas(modulePath string) ([]goModuleVersionMeta, error)
}
endpoint := fmt.Sprintf("https://proxy.golang.org/%s/@v/list", escapeGoModulePathForProxy(trimmed))
client := &http.Client{
Timeout: driverModuleLatestProbeTimeout,
Transport: &http.Transport{
Proxy: nil,
},
}
client := &http.Client{Timeout: driverModuleLatestProbeTimeout}
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
@@ -2025,6 +2190,141 @@ func installOptionalDriverAgentPackage(a *App, definition driverDefinition, sele
}, nil
}
func installOptionalDriverAgentFromLocalFile(definition driverDefinition, filePath string, resolvedDir string, selectedVersion string) (installedDriverPackage, error) {
driverType := normalizeDriverType(definition.Type)
displayName := resolveDriverDisplayName(definition)
pathText := strings.TrimSpace(filePath)
if pathText == "" {
return installedDriverPackage{}, fmt.Errorf("本地驱动包路径为空")
}
if absPath, absErr := filepath.Abs(pathText); absErr == nil {
pathText = absPath
}
info, statErr := os.Stat(pathText)
if statErr != nil {
return installedDriverPackage{}, fmt.Errorf("读取本地驱动包失败:%w", statErr)
}
if info.IsDir() {
return installedDriverPackage{}, fmt.Errorf("本地驱动包路径为目录:%s", pathText)
}
executablePath, err := db.ResolveOptionalDriverAgentExecutablePath(resolvedDir, driverType)
if err != nil {
return installedDriverPackage{}, err
}
if mkErr := os.MkdirAll(filepath.Dir(executablePath), 0o755); mkErr != nil {
return installedDriverPackage{}, fmt.Errorf("创建 %s 驱动目录失败:%w", displayName, mkErr)
}
downloadSource := fmt.Sprintf("local://manual/%s", filepath.Base(pathText))
if strings.EqualFold(filepath.Ext(pathText), ".zip") {
entryName, extractErr := installOptionalDriverAgentFromLocalZip(pathText, definition, executablePath)
if extractErr != nil {
return installedDriverPackage{}, extractErr
}
if strings.TrimSpace(entryName) != "" {
downloadSource = downloadSource + "#" + entryName
}
} else {
if copyErr := copyAgentBinary(pathText, executablePath); copyErr != nil {
return installedDriverPackage{}, fmt.Errorf("导入本地驱动代理失败:%w", copyErr)
}
}
hash, hashErr := hashFileSHA256(executablePath)
if hashErr != nil {
return installedDriverPackage{}, fmt.Errorf("计算 %s 驱动代理摘要失败:%w", displayName, hashErr)
}
return installedDriverPackage{
DriverType: driverType,
Version: strings.TrimSpace(selectedVersion),
FilePath: pathText,
FileName: filepath.Base(pathText),
ExecutablePath: executablePath,
DownloadURL: downloadSource,
SHA256: hash,
DownloadedAt: time.Now().Format(time.RFC3339),
}, nil
}
func installOptionalDriverAgentFromLocalZip(zipPath string, definition driverDefinition, executablePath string) (string, error) {
driverType := normalizeDriverType(definition.Type)
displayName := resolveDriverDisplayName(definition)
reader, err := zip.OpenReader(zipPath)
if err != nil {
return "", fmt.Errorf("打开本地驱动包失败:%w", err)
}
defer reader.Close()
entryPath := optionalDriverBundleEntryPath(driverType)
expectedBaseName := optionalDriverReleaseAssetName(driverType)
findEntry := func() *zip.File {
for _, file := range reader.File {
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
if name == entryPath {
return file
}
}
for _, file := range reader.File {
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
if strings.EqualFold(name, entryPath) {
return file
}
}
for _, file := range reader.File {
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
if strings.EqualFold(filepath.Base(name), expectedBaseName) {
return file
}
}
return nil
}
entry := findEntry()
if entry == nil {
return "", fmt.Errorf("本地驱动包内未找到 %s 代理文件(期望路径 %s", displayName, entryPath)
}
src, err := entry.Open()
if err != nil {
return "", fmt.Errorf("读取本地驱动包条目失败:%w", err)
}
defer src.Close()
tempPath := executablePath + ".tmp"
_ = os.Remove(tempPath)
dst, err := os.Create(tempPath)
if err != nil {
return "", fmt.Errorf("创建驱动代理临时文件失败:%w", err)
}
if _, err := io.Copy(dst, src); err != nil {
dst.Close()
_ = os.Remove(tempPath)
return "", fmt.Errorf("写入驱动代理失败:%w", err)
}
if err := dst.Sync(); err != nil {
dst.Close()
_ = os.Remove(tempPath)
return "", fmt.Errorf("落盘驱动代理失败:%w", err)
}
if err := dst.Close(); err != nil {
_ = os.Remove(tempPath)
return "", fmt.Errorf("关闭驱动代理文件失败:%w", err)
}
if chmodErr := os.Chmod(tempPath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
_ = os.Remove(tempPath)
return "", fmt.Errorf("设置驱动代理权限失败:%w", chmodErr)
}
if err := os.Rename(tempPath, executablePath); err != nil {
_ = os.Remove(tempPath)
return "", fmt.Errorf("替换驱动代理失败:%w", err)
}
if chmodErr := os.Chmod(executablePath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
return "", fmt.Errorf("设置驱动代理权限失败:%w", chmodErr)
}
return filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(entry.Name), "./")), nil
}
func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, executablePath string, downloadURL string) (string, string, error) {
driverType := normalizeDriverType(definition.Type)
displayName := resolveDriverDisplayName(definition)