diff --git a/frontend/src/components/DriverManagerModal.tsx b/frontend/src/components/DriverManagerModal.tsx index 6bbadd3..d4e7190 100644 --- a/frontend/src/components/DriverManagerModal.tsx +++ b/frontend/src/components/DriverManagerModal.tsx @@ -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; + 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(null); const [rows, setRows] = useState([]); const [actionDriver, setActionDriver] = useState(''); const [progressMap, setProgressMap] = useState>({}); + const [operationLogMap, setOperationLogMap] = useState>({}); + const [logDriverType, setLogDriverType] = useState(''); + const [logModalOpen, setLogModalOpen] = useState(false); const [versionMap, setVersionMap] = useState>({}); const [selectedVersionMap, setSelectedVersionMap] = useState>({}); const [versionLoadingMap, setVersionLoadingMap] = useState>({}); const [versionSizeLoadingMap, setVersionSizeLoadingMap] = useState>({}); + 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, + 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 内置; + } + const installPath = row.executablePath || row.installDir || '-'; + if (installPath === '-') { + return -; + } + return ( + + {installPath} + + ); + }, + }, { 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 -; @@ -521,19 +730,20 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({ if (isSlimBuildUnavailable && !row.packageInstalled) { return 需 Full 版; } - if (row.connectable) { - return ( - - ); - } - return ( + + const logs = operationLogMap[row.type] || []; + const hasLogs = logs.length > 0; + + const mainAction = row.connectable ? ( + + ) : ( ); + + return ( + + {mainAction} + + + + ); }, }, ]; - }, [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 ( void }> = ({ open={open} onCancel={onClose} width={980} + style={{ top: 24 }} + styles={{ + body: { + maxHeight: 'calc(100vh - 220px)', + overflowY: 'auto', + overflowX: 'hidden', + paddingRight: 18, + }, + }} destroyOnClose footer={[ , + , , @@ -566,6 +819,67 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({ > 除 MySQL / Redis / Oracle / PostgreSQL 外,其他数据源需先安装启用后再连接。 + {networkStatus ? ( + + + 驱动下载依赖 GitHub 与 Go 模块代理网络。若检测失败,建议先启用 HTTP/HTTPS/SOCKS5 代理后重试。 + + + {networkStatus.checks.map((item) => ( + + {item.name}:{item.reachable ? '可达' : '不可达'}{item.httpStatus ? `,HTTP ${item.httpStatus}` : ''}{item.latencyMs ? `,${item.latencyMs}ms` : ''}{item.error ? `,${item.error}` : ''} + + ))} + {proxyEnvEntries.length > 0 ? ( + + 检测到代理环境变量:{proxyEnvEntries.map(([key]) => key).join('、')} + + ) : ( + 未检测到系统代理环境变量。 + )} + + ), + }, + ]} + /> + + )} + /> + ) : ( + + )} + + + 自动下载和手动导入的驱动都会落盘到以下目录;后续版本升级可重复复用已下载驱动。 + 手动导入支持单个驱动代理文件(如 `mariadb-driver-agent` / `mariadb-driver-agent.exe`)或驱动总包 `GoNavi-DriverAgents.zip`。 + + 驱动根目录:{downloadDir || '-'} + + {networkStatus?.logPath ? ( + + 运行日志文件:{networkStatus.logPath} + + ) : null} + + )} + /> void }> = ({ dataSource={rows} pagination={false} size="middle" + scroll={{ x: 1450 }} /> + setLogModalOpen(false)} + footer={[ + , + ]} + width={780} + > + + {activeLogRow?.installDir ? ( + + 安装目录:{activeLogRow.installDir} + + ) : null} + {activeLogRow?.executablePath ? ( + + 驱动可执行文件:{activeLogRow.executablePath} + + ) : null} + {activeDriverLogLines.length > 0 ? ( +
+              {activeDriverLogLines.join('\n')}
+            
+ ) : ( + 当前驱动暂无操作日志。 + )} +
+
); }; diff --git a/frontend/wailsjs/go/app/App.d.ts b/frontend/wailsjs/go/app/App.d.ts index e177d59..8c38771 100755 --- a/frontend/wailsjs/go/app/App.d.ts +++ b/frontend/wailsjs/go/app/App.d.ts @@ -6,6 +6,8 @@ import {redis} from '../models'; export function ApplyChanges(arg1:connection.ConnectionConfig,arg2:string,arg3:string,arg4:connection.ChangeSet):Promise; +export function CheckDriverNetworkStatus():Promise; + export function CheckForUpdates():Promise; export function ConfigureDriverRuntimeDirectory(arg1:string):Promise; diff --git a/frontend/wailsjs/go/app/App.js b/frontend/wailsjs/go/app/App.js index 120f0bf..3b1a185 100755 --- a/frontend/wailsjs/go/app/App.js +++ b/frontend/wailsjs/go/app/App.js @@ -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'](); } diff --git a/internal/app/methods_driver.go b/internal/app/methods_driver.go index 46268e0..9fe2e56 100644 --- a/internal/app/methods_driver.go +++ b/internal/app/methods_driver.go @@ -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)