Files
MyGoNavi/internal/app/methods_driver.go
Syngnat 7fd6d78c83 feat(driver): 新增 OceanBase 与 OpenGauss Agent 数据源
- 数据源支持:新增 OceanBase 与 OpenGauss optional driver-agent 实现
- 连接适配:复用 MySQL/PostgreSQL 兼容链路并补齐查询、DDL、同步能力
- 前端入口:补充连接表单、侧边栏、图标、SQL 方言和危险操作识别
- 驱动管理:更新 driver manifest、安装提示和 revision 自动生成链路
- 构建发布:支持多平台 driver-agent 打包并优化 release 构建失败提示
2026-04-30 13:13:01 +08:00

4473 lines
144 KiB
Go
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.
package app
import (
"archive/zip"
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
stdRuntime "runtime"
"sort"
"strings"
"sync"
"time"
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/db"
"GoNavi-Wails/internal/logger"
"github.com/wailsapp/wails/v2/pkg/runtime"
"golang.org/x/mod/semver"
)
var (
goBinaryLookPath = exec.LookPath
goBinaryStat = os.Stat
goBinaryCommand = func(name string, arg ...string) *exec.Cmd {
return exec.Command(name, arg...)
}
goBinaryCommandOutput = func(cmd *exec.Cmd) ([]byte, error) {
return cmd.Output()
}
)
// resolveGoBinaryPath 定位 Go 可执行文件,兼容 macOS 图形应用未继承 shell PATH 的场景 by AI.Coding
func resolveGoBinaryPath() (string, error) {
if goPath, err := goBinaryLookPath("go"); err == nil {
return goPath, nil
}
// 修复点GUI 进程常拿不到终端里的 PATH这里补充常见安装位置兜底。
commonCandidates := []string{
"/opt/homebrew/bin/go",
"/usr/local/go/bin/go",
"/usr/local/bin/go",
}
for _, candidate := range commonCandidates {
if info, err := goBinaryStat(candidate); err == nil && !info.IsDir() {
return candidate, nil
}
}
for _, shell := range candidateShellsForCommandLookup() {
cmd := goBinaryCommand(shell, "-lc", "command -v go")
output, err := goBinaryCommandOutput(cmd)
if err != nil {
continue
}
goPath := resolveExistingPathFromCommandOutput(output)
if goPath == "" {
continue
}
if info, err := goBinaryStat(goPath); err == nil && !info.IsDir() {
return goPath, nil
}
}
return "", exec.ErrNotFound
}
// resolveExistingPathFromCommandOutput 从命令输出中提取真实存在的路径,避免 shell 启动脚本输出污染探测结果 by AI.Coding
func resolveExistingPathFromCommandOutput(value []byte) string {
for _, line := range bytes.Split(value, []byte{'\n'}) {
trimmed := strings.TrimSpace(string(line))
if trimmed != "" {
if info, err := goBinaryStat(trimmed); err == nil && !info.IsDir() {
return trimmed
}
}
}
return ""
}
// candidateShellsForCommandLookup 返回可能可用的 shell用于回收用户登录环境中的 PATH by AI.Coding
func candidateShellsForCommandLookup() []string {
seen := make(map[string]struct{}, 4)
result := make([]string, 0, 4)
appendShell := func(value string) {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return
}
if _, ok := seen[trimmed]; ok {
return
}
seen[trimmed] = struct{}{}
result = append(result, trimmed)
}
appendShell(os.Getenv("SHELL"))
appendShell("/bin/zsh")
appendShell("/bin/bash")
appendShell("/bin/sh")
return result
}
type driverDefinition struct {
Type string `json:"type"`
Name string `json:"name"`
Engine string `json:"engine,omitempty"`
BuiltIn bool `json:"builtIn"`
PinnedVersion string `json:"pinnedVersion,omitempty"`
DefaultDownloadURL string `json:"defaultDownloadUrl,omitempty"`
DownloadSHA256 string `json:"downloadSha256,omitempty"`
ChecksumPolicy string `json:"checksumPolicy,omitempty"`
}
type installedDriverPackage struct {
DriverType string `json:"driverType"`
Version string `json:"version,omitempty"`
AgentRevision string `json:"agentRevision,omitempty"`
FilePath string `json:"filePath"`
FileName string `json:"fileName"`
ExecutablePath string `json:"executablePath,omitempty"`
DownloadURL string `json:"downloadUrl,omitempty"`
SHA256 string `json:"sha256,omitempty"`
DownloadedAt string `json:"downloadedAt"`
}
type driverStatusItem struct {
Type string `json:"type"`
Name string `json:"name"`
Engine string `json:"engine,omitempty"`
BuiltIn bool `json:"builtIn"`
PinnedVersion string `json:"pinnedVersion,omitempty"`
InstalledVersion string `json:"installedVersion,omitempty"`
PackageSizeText string `json:"packageSizeText,omitempty"`
RuntimeAvailable bool `json:"runtimeAvailable"`
PackageInstalled bool `json:"packageInstalled"`
Connectable bool `json:"connectable"`
DefaultDownloadURL string `json:"defaultDownloadUrl,omitempty"`
InstallDir string `json:"installDir,omitempty"`
PackagePath string `json:"packagePath,omitempty"`
PackageFileName string `json:"packageFileName,omitempty"`
ExecutablePath string `json:"executablePath,omitempty"`
DownloadedAt string `json:"downloadedAt,omitempty"`
AgentRevision string `json:"agentRevision,omitempty"`
ExpectedRevision string `json:"expectedRevision,omitempty"`
NeedsUpdate bool `json:"needsUpdate,omitempty"`
UpdateReason string `json:"updateReason,omitempty"`
AffectedConnections int `json:"affectedConnections,omitempty"`
Message string `json:"message,omitempty"`
}
const driverDownloadProgressEvent = "driver:download-progress"
type driverDownloadProgressPayload struct {
DriverType string `json:"driverType"`
Status string `json:"status"`
Percent float64 `json:"percent"`
Downloaded int64 `json:"downloaded"`
Total int64 `json:"total"`
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"`
TCPLatency int64 `json:"tcpLatencyMs,omitempty"`
HTTPLatency int64 `json:"httpLatencyMs,omitempty"`
Method string `json:"method,omitempty"`
Error string `json:"error,omitempty"`
}
type pinnedDriverPackage struct {
Version string
DownloadURL string
SHA256 string
Policy string
Engine string
}
type driverManifestFile struct {
Engine string `json:"engine"`
DefaultEngine string `json:"defaultEngine"`
DefaultEngine2 string `json:"default_engine"`
Drivers map[string]driverManifestItem `json:"drivers"`
}
type driverManifestItem struct {
Version string `json:"version"`
DownloadURL string `json:"downloadUrl"`
DownloadURL2 string `json:"download_url"`
SHA256 string `json:"sha256"`
ChecksumPolicy string `json:"checksumPolicy"`
ChecksumPolicy2 string `json:"checksum_policy"`
Engine string `json:"engine"`
Versions []driverManifestVersionItem `json:"versions"`
VersionList []driverManifestVersionItem `json:"versionList"`
VersionList2 []driverManifestVersionItem `json:"version_list"`
VersionOptions []driverManifestVersionItem `json:"versionOptions"`
VersionOptions2 []driverManifestVersionItem `json:"version_options"`
}
type driverManifestVersionItem struct {
Version string `json:"version"`
DownloadURL string `json:"downloadUrl"`
DownloadURL2 string `json:"download_url"`
SHA256 string `json:"sha256"`
ChecksumPolicy string `json:"checksumPolicy"`
ChecksumPolicy2 string `json:"checksum_policy"`
Engine string `json:"engine"`
}
type driverManifestCacheEntry struct {
LoadedAt time.Time
Packages map[string]pinnedDriverPackage
Versions map[string][]pinnedDriverPackage
Err string
}
type driverVersionOptionItem struct {
Version string `json:"version"`
DownloadURL string `json:"downloadUrl"`
SHA256 string `json:"sha256,omitempty"`
PackageSizeBytes int64 `json:"packageSizeBytes,omitempty"`
PackageSizeText string `json:"packageSizeText,omitempty"`
Recommended bool `json:"recommended,omitempty"`
Source string `json:"source,omitempty"`
Year string `json:"year,omitempty"`
DisplayLabel string `json:"displayLabel,omitempty"`
}
type driverReleaseAssetSizeCacheEntry struct {
LoadedAt time.Time
SizeByKey map[string]int64
PublishedAssets map[string]bool
Err string
}
type goModuleLatestVersionCacheEntry struct {
LoadedAt time.Time
Version string
Err string
}
type goModuleLatestVersionResponse struct {
Version string `json:"Version"`
}
type goModuleVersionListCacheEntry struct {
LoadedAt time.Time
Versions []goModuleVersionMeta
Err string
}
type goModuleVersionMeta struct {
Version string
Year string
}
type driverBundleAssetIndex struct {
Assets map[string]int64 `json:"assets"`
}
const (
// 默认使用内置 manifest避免依赖网络与外部仓库 404。
defaultDriverManifestURLValue = "builtin://manifest"
optionalDriverBundleAssetName = "GoNavi-DriverAgents.zip"
optionalDriverBundleIndexAssetName = "GoNavi-DriverAgents-Index.json"
driverManifestCacheTTL = 5 * time.Minute
driverReleaseAssetSizeCacheTTL = 30 * time.Minute
driverReleaseAssetSizeErrorCacheTTL = 30 * time.Second
driverReleaseAssetSizeProbeTimeout = 4 * time.Second
driverReleaseListProbeTimeout = 6 * time.Second
driverModuleLatestCacheTTL = 6 * time.Hour
driverModuleLatestErrorCacheTTL = 2 * time.Minute
driverModuleLatestProbeTimeout = 4 * time.Second
driverModuleVersionInspectLimit = 30
driverModuleVersionListMaxSize = 4 << 20
driverRecentVersionLimit = 5
driverModuleVersionFetchLimit = 64
driverVersionWarmupMinInterval = 30 * time.Second
driverBundleIndexMaxSize = 1 << 20
driverManifestMaxSize = 2 << 20
driverNetworkProbeTimeout = 4 * time.Second
driverNetworkProbeTCPTimeout = 3 * time.Second
localDriverDirectoryScanMaxEntries = 20000
driverChecksumPolicyStrict = "strict"
driverChecksumPolicyWarn = "warn"
driverChecksumPolicyOff = "off"
driverEngineGo = "go"
driverEngineExternal = "external"
)
const builtinDriverManifestJSON = `{
"engine": "go",
"drivers": {
"mysql": { "engine": "go", "version": "1.9.3", "checksumPolicy": "off" },
"mariadb": { "engine": "go", "version": "1.9.3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/mariadb" },
"oceanbase": { "engine": "go", "version": "1.9.3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/oceanbase" },
"doris": { "engine": "go", "version": "1.9.3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/doris" },
"sphinx": { "engine": "go", "version": "1.9.3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/sphinx" },
"sqlserver": { "engine": "go", "version": "1.9.6", "checksumPolicy": "off", "downloadUrl": "builtin://activate/sqlserver" },
"sqlite": { "engine": "go", "version": "1.44.3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/sqlite" },
"duckdb": { "engine": "go", "version": "2.5.6", "checksumPolicy": "off", "downloadUrl": "builtin://activate/duckdb" },
"dameng": { "engine": "go", "version": "1.8.22", "checksumPolicy": "off", "downloadUrl": "builtin://activate/dameng" },
"kingbase": { "engine": "go", "version": "0.0.0-20201021123113-29bd62a876c3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/kingbase" },
"highgo": { "engine": "go", "version": "0.0.0-local", "checksumPolicy": "off", "downloadUrl": "builtin://activate/highgo" },
"vastbase": { "engine": "go", "version": "1.11.1", "checksumPolicy": "off", "downloadUrl": "builtin://activate/vastbase" },
"opengauss": { "engine": "go", "version": "1.11.1", "checksumPolicy": "off", "downloadUrl": "builtin://activate/opengauss" },
"mongodb": { "engine": "go", "version": "2.5.0", "checksumPolicy": "off", "downloadUrl": "builtin://activate/mongodb" },
"tdengine": { "engine": "go", "version": "3.7.8", "checksumPolicy": "off", "downloadUrl": "builtin://activate/tdengine" },
"clickhouse": { "engine": "go", "version": "2.43.1", "checksumPolicy": "off", "downloadUrl": "builtin://activate/clickhouse" }
}
}`
var (
driverManifestCacheMu sync.RWMutex
driverManifestCache = make(map[string]driverManifestCacheEntry)
driverReleaseSizeMu sync.RWMutex
driverReleaseSizeMap = make(map[string]driverReleaseAssetSizeCacheEntry)
driverReleaseListMu sync.RWMutex
driverReleaseList = driverManifestReleaseListCache{}
driverModuleLatestMu sync.RWMutex
driverModuleLatestMap = make(map[string]goModuleLatestVersionCacheEntry)
driverModuleVersionMu sync.RWMutex
driverModuleVersionMap = make(map[string]goModuleVersionListCacheEntry)
driverVersionWarmupMu sync.Mutex
driverVersionWarmup = driverVersionWarmupState{}
errLocalDriverDirScanLimit = errors.New("local_driver_directory_scan_limit_exceeded")
)
type driverVersionWarmupState struct {
Running bool
LastStarted time.Time
}
type driverManifestReleaseListCache struct {
LoadedAt time.Time
Releases []githubRelease
Err string
}
var pinnedDriverPackageMap = map[string]pinnedDriverPackage{
"postgres": {
Version: "go-embedded",
Policy: driverChecksumPolicyOff,
Engine: driverEngineGo,
},
}
var latestDriverVersionMap = map[string]string{
"mysql": "1.9.3",
"mariadb": "1.9.3",
"oceanbase": "1.9.3",
"diros": "1.9.3",
"sphinx": "1.9.3",
"sqlserver": "1.9.6",
"sqlite": "1.46.1",
"duckdb": "2.5.6",
"dameng": "1.8.22",
"kingbase": "0.0.0-20201021123113-29bd62a876c3",
"highgo": "0.0.0-local",
"vastbase": "1.11.2",
"opengauss": "1.11.1",
"mongodb": "2.5.0",
"tdengine": "3.7.8",
"clickhouse": "2.43.1",
"oracle": "2.9.0",
"postgres": "1.11.2",
"redis": "9.17.3",
}
var driverGoModulePathMap = map[string]string{
"mariadb": "github.com/go-sql-driver/mysql",
"oceanbase": "github.com/go-sql-driver/mysql",
"diros": "github.com/go-sql-driver/mysql",
"sphinx": "github.com/go-sql-driver/mysql",
"sqlserver": "github.com/microsoft/go-mssqldb",
"sqlite": "modernc.org/sqlite",
"duckdb": "github.com/duckdb/duckdb-go/v2",
"dameng": "gitee.com/chunanyong/dm",
"kingbase": "gitea.com/kingbase/gokb",
"highgo": "github.com/highgo/pq-sm3",
"vastbase": "github.com/lib/pq",
"opengauss": "github.com/lib/pq",
"mongodb": "go.mongodb.org/mongo-driver/v2",
"tdengine": "github.com/taosdata/driver-go/v3",
"clickhouse": "github.com/ClickHouse/clickhouse-go/v2",
}
var driverGoModuleAliasPathMap = map[string][]string{
"mongodb": {
"go.mongodb.org/mongo-driver",
},
}
var driverExtraHistoryLimitMap = map[string]int{
"mongodb": 10,
"tdengine": 30,
}
var fallbackRecentDriverVersionsMap = map[string][]goModuleVersionMeta{
"mongodb": {
{Version: "2.5.0"},
{Version: "2.4.2"},
{Version: "2.4.1"},
{Version: "2.4.0"},
{Version: "2.3.1"},
{Version: "1.17.9"},
{Version: "1.17.8"},
{Version: "1.17.7"},
{Version: "1.17.6"},
{Version: "1.17.4"},
{Version: "1.17.3"},
{Version: "1.17.2"},
{Version: "1.17.1"},
{Version: "1.17.0"},
{Version: "1.16.1"},
},
"tdengine": {
{Version: "3.8.0"},
{Version: "3.7.8"},
{Version: "3.7.7"},
{Version: "3.7.6"},
{Version: "3.7.5"},
{Version: "3.7.4"},
{Version: "3.7.3"},
{Version: "3.7.2"},
{Version: "3.7.1"},
{Version: "3.7.0"},
{Version: "3.6.0"},
{Version: "3.5.8"},
{Version: "3.5.7"},
{Version: "3.5.6"},
{Version: "3.5.5"},
{Version: "3.5.4"},
{Version: "3.5.3"},
{Version: "3.5.2"},
{Version: "3.5.1"},
{Version: "3.5.0"},
{Version: "3.3.1"},
{Version: "3.1.0"},
{Version: "3.0.4"},
{Version: "3.0.3"},
{Version: "3.0.2"},
{Version: "3.0.1"},
{Version: "3.0.0"},
},
}
func (a *App) SelectDriverDownloadDirectory(currentDir string) connection.QueryResult {
defaultDir := strings.TrimSpace(currentDir)
if defaultDir == "" {
defaultDir = defaultDriverDownloadDirectory()
} else if !filepath.IsAbs(defaultDir) {
if abs, err := filepath.Abs(defaultDir); err == nil {
defaultDir = abs
}
}
selection, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{
Title: "选择驱动下载目录",
DefaultDirectory: defaultDir,
CanCreateDirectories: true,
})
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if strings.TrimSpace(selection) == "" {
return connection.QueryResult{Success: false, Message: "已取消"}
}
resolved, err := resolveDriverDownloadDirectory(selection)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{
Success: true,
Data: map[string]interface{}{
"path": resolved,
"defaultPath": defaultDriverDownloadDirectory(),
"isDefaultPath": false,
},
}
}
func validateLocalDriverPackagePath(path string) error {
pathText := strings.TrimSpace(path)
if pathText == "" {
return nil
}
if strings.EqualFold(filepath.Ext(pathText), ".jar") {
return fmt.Errorf("当前驱动管理不支持直接导入 JDBC Jar。GoNavi 使用 Go 驱动与可选 driver-agent请改用驱动包/驱动目录如需连接人大金仓请优先使用“Kingbase”连接类型或在自定义连接中填写 kingbase / kingbase8")
}
return nil
}
func (a *App) SelectDriverPackageFile(currentPath string) connection.QueryResult {
defaultDir := strings.TrimSpace(currentPath)
if defaultDir == "" {
defaultDir = defaultDriverDownloadDirectory()
}
if filepath.Ext(defaultDir) != "" {
defaultDir = filepath.Dir(defaultDir)
}
if !filepath.IsAbs(defaultDir) {
if abs, err := filepath.Abs(defaultDir); err == nil {
defaultDir = abs
}
}
selection, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: "选择驱动包文件(非 JDBC Jar",
DefaultDirectory: defaultDir,
})
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if strings.TrimSpace(selection) == "" {
return connection.QueryResult{Success: false, Message: "已取消"}
}
if abs, err := filepath.Abs(selection); err == nil {
selection = abs
}
if err := validateLocalDriverPackagePath(selection); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: map[string]interface{}{"path": selection}}
}
func (a *App) SelectDriverPackageDirectory(currentPath string) connection.QueryResult {
defaultDir := strings.TrimSpace(currentPath)
if defaultDir == "" {
defaultDir = defaultDriverDownloadDirectory()
}
if filepath.Ext(defaultDir) != "" {
defaultDir = filepath.Dir(defaultDir)
}
if !filepath.IsAbs(defaultDir) {
if abs, err := filepath.Abs(defaultDir); err == nil {
defaultDir = abs
}
}
selection, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{
Title: "选择驱动包目录",
DefaultDirectory: defaultDir,
})
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if strings.TrimSpace(selection) == "" {
return connection.QueryResult{Success: false, Message: "已取消"}
}
if abs, err := filepath.Abs(selection); err == nil {
selection = abs
}
return connection.QueryResult{Success: true, Data: map[string]interface{}{"path": selection}}
}
func (a *App) OpenDriverDownloadDirectory(directory string) connection.QueryResult {
resolved, err := resolveDriverDownloadDirectory(directory)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := os.MkdirAll(resolved, 0o755); err != nil {
return connection.QueryResult{Success: false, Message: fmt.Sprintf("创建驱动目录失败:%v", err)}
}
var cmd *exec.Cmd
switch stdRuntime.GOOS {
case "darwin":
cmd = exec.Command("open", resolved)
case "windows":
cmd = exec.Command("explorer", resolved)
case "linux":
cmd = exec.Command("xdg-open", resolved)
default:
return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前平台暂不支持打开目录:%s", stdRuntime.GOOS)}
}
if err := cmd.Start(); err != nil {
logger.Error(err, "打开驱动目录失败")
return connection.QueryResult{Success: false, Message: fmt.Sprintf("打开驱动目录失败:%v", err)}
}
return connection.QueryResult{
Success: true,
Message: fmt.Sprintf("已打开驱动目录:%s", resolved),
Data: map[string]interface{}{"path": resolved},
}
}
func (a *App) ResolveDriverDownloadDirectory(directory string) connection.QueryResult {
resolved, err := resolveDriverDownloadDirectory(directory)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: map[string]interface{}{"path": resolved}}
}
func (a *App) ConfigureDriverRuntimeDirectory(directory string) connection.QueryResult {
resolved, err := resolveDriverDownloadDirectory(directory)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
db.SetExternalDriverDownloadDirectory(resolved)
return connection.QueryResult{
Success: true,
Data: map[string]interface{}{
"path": resolved,
"defaultPath": defaultDriverDownloadDirectory(),
"isDefaultPath": strings.TrimSpace(directory) == "",
},
Message: "驱动运行时目录已生效",
}
}
func (a *App) ResolveDriverRepositoryURL(repositoryURL string) connection.QueryResult {
resolved, err := resolveDriverRepositoryURL(repositoryURL)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: map[string]interface{}{"url": resolved}}
}
func (a *App) ResolveDriverPackageDownloadURL(driverType string, repositoryURL string) connection.QueryResult {
effectivePackages, manifestErr := resolveEffectiveDriverPackages(repositoryURL)
definition, ok := resolveDriverDefinitionWithPackages(driverType, effectivePackages)
if !ok {
return connection.QueryResult{Success: false, Message: "不支持的驱动类型"}
}
engine := effectiveDriverEngine(definition)
if definition.BuiltIn {
return connection.QueryResult{Success: false, Message: "内置驱动无需下载扩展包"}
}
if err := ensureOptionalDriverBuildAvailable(definition); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if engine == driverEngineGo && !definition.BuiltIn {
urlText := strings.TrimSpace(definition.DefaultDownloadURL)
if urlText == "" {
urlText = fmt.Sprintf("builtin://activate/%s", optionalDriverPublicTypeName(definition.Type))
}
data := map[string]interface{}{
"url": urlText,
"driverType": definition.Type,
"driverName": definition.Name,
"engine": engine,
"manifestError": errorMessage(manifestErr),
}
if strings.TrimSpace(definition.DownloadSHA256) != "" {
data["sha256"] = strings.TrimSpace(definition.DownloadSHA256)
}
return connection.QueryResult{Success: true, Data: data}
}
return connection.QueryResult{Success: false, Message: "当前仅支持纯 Go 可选驱动的安装启用"}
}
func (a *App) GetDriverVersionList(driverType string, repositoryURL string) connection.QueryResult {
effectivePackages, manifestErr := resolveEffectiveDriverPackages(repositoryURL)
definition, ok := resolveDriverDefinitionWithPackages(driverType, effectivePackages)
if !ok {
return connection.QueryResult{Success: false, Message: "不支持的驱动类型"}
}
if definition.BuiltIn {
return connection.QueryResult{Success: false, Message: "内置驱动无需选择版本"}
}
if err := ensureOptionalDriverBuildAvailable(definition); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
options, err := resolveDriverVersionOptions(definition, repositoryURL)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{
Success: true,
Data: map[string]interface{}{
"driverType": definition.Type,
"driverName": definition.Name,
"pinnedVersion": definition.PinnedVersion,
"manifestError": errorMessage(manifestErr),
"versions": options,
},
}
}
func (a *App) GetDriverVersionPackageSize(driverType string, version string) connection.QueryResult {
definition, ok := resolveDriverDefinition(driverType)
if !ok {
return connection.QueryResult{Success: false, Message: "不支持的驱动类型"}
}
if definition.BuiltIn {
return connection.QueryResult{Success: false, Message: "内置驱动无需安装包"}
}
normalizedType := normalizeDriverType(definition.Type)
if normalizedType == "" || !db.IsOptionalGoDriver(normalizedType) {
return connection.QueryResult{Success: false, Message: "当前驱动不支持安装包查询"}
}
normalizedVersion := normalizeVersion(strings.TrimSpace(version))
if normalizedVersion == "" {
return connection.QueryResult{Success: false, Message: "版本号为空"}
}
if err := validateDriverSelectedVersion(definition, normalizedVersion); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
assetName := optionalDriverReleaseAssetNameForVersion(normalizedType, normalizedVersion)
if strings.TrimSpace(assetName) == "" {
return connection.QueryResult{Success: false, Message: "驱动资产名称为空"}
}
tag := "v" + normalizedVersion
sizeBytes := int64(0)
sizeSource := ""
if sizeByAsset, _, err := loadReleaseAssetSizesCached("tag:"+tag, func() (*githubRelease, error) {
return fetchReleaseByTag(tag)
}); err == nil {
sizeBytes = resolveOptionalDriverAssetSizeForVersion(sizeByAsset, normalizedType, normalizedVersion)
if sizeBytes > 0 {
sizeSource = "tag"
}
}
allowLatestFallback := sameDriverVersion(normalizedVersion, definition.PinnedVersion) || sameDriverVersion(normalizedVersion, latestDriverVersionMap[normalizedType])
if sizeBytes <= 0 && allowLatestFallback {
if sizeByAsset, _, err := loadReleaseAssetSizesCached("latest", fetchLatestReleaseForDriverAssets); err == nil {
sizeBytes = resolveOptionalDriverAssetSizeForVersion(sizeByAsset, normalizedType, normalizedVersion)
if sizeBytes > 0 {
sizeSource = "latest"
}
}
}
data := map[string]interface{}{
"driverType": normalizedType,
"version": normalizedVersion,
"packageSizeBytes": sizeBytes,
"packageSizeText": "",
"releaseAssetName": assetName,
"releaseAssetTag": tag,
"sizeSource": sizeSource,
}
if sizeBytes > 0 {
data["packageSizeText"] = formatSizeMB(sizeBytes)
}
return connection.QueryResult{Success: true, Data: data}
}
func (a *App) GetDriverStatusList(downloadDir string, manifestURL string) connection.QueryResult {
resolvedDir, err := resolveDriverDownloadDirectory(downloadDir)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
db.SetExternalDriverDownloadDirectory(resolvedDir)
effectivePackages, manifestErr := resolveEffectiveDriverPackages(manifestURL)
definitions := allDriverDefinitionsWithPackages(effectivePackages)
triggerDriverVersionMetadataWarmup(definitions)
packageSizeBytesMap := preloadOptionalDriverPackageSizes(definitions)
usageCounts := a.savedConnectionDriverUsageCounts()
items := make([]driverStatusItem, 0, len(definitions))
for _, definition := range definitions {
engine := effectiveDriverEngine(definition)
runtimeAvailable, runtimeReason := db.DriverRuntimeSupportStatus(definition.Type)
pkg, packageMetaExists := readInstalledDriverPackage(resolvedDir, definition.Type)
needsUpdate, updateReason, expectedRevision := optionalDriverAgentRevisionStatus(definition.Type, pkg, packageMetaExists)
packageInstalled := definition.BuiltIn || packageMetaExists
if runtimeAvailable && db.IsOptionalGoDriver(definition.Type) {
packageInstalled = true
}
item := driverStatusItem{
Type: definition.Type,
Name: definition.Name,
Engine: engine,
BuiltIn: definition.BuiltIn,
PinnedVersion: definition.PinnedVersion,
InstalledVersion: strings.TrimSpace(pkg.Version),
PackageSizeText: resolveDriverPackageSizeText(definition, pkg, packageMetaExists, packageSizeBytesMap),
RuntimeAvailable: runtimeAvailable,
PackageInstalled: packageInstalled,
Connectable: runtimeAvailable,
DefaultDownloadURL: definition.DefaultDownloadURL,
InstallDir: driverInstallDir(resolvedDir, definition.Type),
AgentRevision: strings.TrimSpace(pkg.AgentRevision),
ExpectedRevision: expectedRevision,
NeedsUpdate: needsUpdate,
UpdateReason: updateReason,
AffectedConnections: usageCounts[normalizeDriverType(definition.Type)],
}
if packageMetaExists {
item.PackagePath = pkg.FilePath
item.PackageFileName = pkg.FileName
item.DownloadedAt = pkg.DownloadedAt
item.ExecutablePath = pkg.ExecutablePath
}
switch {
case definition.BuiltIn:
item.Message = "内置驱动,可直接连接"
case needsUpdate:
if item.AffectedConnections > 0 {
item.Message = fmt.Sprintf("%s检测到 %d 个已保存连接正在使用该驱动,请在工具-驱动管理中重装", updateReason, item.AffectedConnections)
} else {
item.Message = updateReason + ",请在工具-驱动管理中重装"
}
case runtimeAvailable:
item.Message = "纯 Go 驱动已启用,可直接连接"
case packageInstalled && strings.TrimSpace(runtimeReason) != "":
item.Message = runtimeReason
case packageInstalled:
if item.InstalledVersion != "" {
item.Message = fmt.Sprintf("驱动已安装(版本:%s待生效", item.InstalledVersion)
} else {
item.Message = "驱动已安装,待生效"
}
case strings.TrimSpace(runtimeReason) != "":
item.Message = runtimeReason
default:
if strings.TrimSpace(definition.PinnedVersion) != "" {
item.Message = fmt.Sprintf("未启用(版本:%s", strings.TrimSpace(definition.PinnedVersion))
} else {
item.Message = "未启用"
}
}
items = append(items, item)
}
return connection.QueryResult{
Success: true,
Data: map[string]interface{}{
"downloadDir": resolvedDir,
"drivers": items,
"manifestURL": resolveManifestURLForView(manifestURL),
"manifestError": errorMessage(manifestErr),
},
}
}
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: "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(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 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 {
summary = "检测到部分驱动下载地址不可达,建议先配置 HTTP/HTTPS/SOCKS5 代理后再安装驱动。"
}
}
data := map[string]interface{}{
"reachable": allReachable,
"summary": summary,
"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,
}
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, version string) connection.QueryResult {
definition, ok := resolveDriverDefinition(driverType)
if !ok {
return connection.QueryResult{Success: false, Message: "不支持的驱动类型"}
}
if definition.BuiltIn {
return connection.QueryResult{Success: false, Message: "内置驱动无需安装扩展包"}
}
if err := validateLocalDriverPackagePath(filePath); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := ensureOptionalDriverBuildAvailable(definition); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
engine := effectiveDriverEngine(definition)
if !(engine == driverEngineGo && !definition.BuiltIn) {
return connection.QueryResult{Success: false, Message: "当前仅支持纯 Go 可选驱动的安装启用"}
}
resolvedDir, err := resolveDriverDownloadDirectory(downloadDir)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
db.SetExternalDriverDownloadDirectory(resolvedDir)
a.emitDriverDownloadProgress(definition.Type, "start", 0, 100, "开始安装本地驱动包")
selectedVersion := resolveDriverInstallVersion(version, "local://manual", definition)
if err := validateDriverSelectedVersion(definition, selectedVersion); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
meta, installErr := installOptionalDriverAgentFromLocalPath(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, "downloading", 90, 100, "写入驱动元数据")
if err := writeInstalledDriverPackage(resolvedDir, definition.Type, meta); err != nil {
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", 100, 100, "本地驱动包导入完成")
return connection.QueryResult{Success: true, Message: "驱动安装成功", Data: map[string]interface{}{
"driverType": definition.Type,
"driverName": definition.Name,
"engine": engine,
}}
}
func (a *App) DownloadDriverPackage(driverType string, version string, downloadURL string, downloadDir string) connection.QueryResult {
definition, ok := resolveDriverDefinition(driverType)
if !ok {
return connection.QueryResult{Success: false, Message: "不支持的驱动类型"}
}
engine := effectiveDriverEngine(definition)
if definition.BuiltIn {
return connection.QueryResult{Success: false, Message: "内置驱动无需下载扩展包"}
}
if err := ensureOptionalDriverBuildAvailable(definition); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if !(engine == driverEngineGo && !definition.BuiltIn) {
return connection.QueryResult{Success: false, Message: "当前仅支持纯 Go 可选驱动的安装启用"}
}
urlText := strings.TrimSpace(downloadURL)
if urlText == "" {
urlText = strings.TrimSpace(definition.DefaultDownloadURL)
}
if urlText == "" {
urlText = fmt.Sprintf("builtin://activate/%s", optionalDriverPublicTypeName(definition.Type))
}
selectedVersion := resolveDriverInstallVersion(version, urlText, definition)
if err := validateDriverSelectedVersion(definition, selectedVersion); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
resolvedDir, err := resolveDriverDownloadDirectory(downloadDir)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
db.SetExternalDriverDownloadDirectory(resolvedDir)
if db.IsOptionalGoDriver(definition.Type) {
displayName := strings.TrimSpace(definition.Name)
if displayName == "" {
displayName = strings.TrimSpace(definition.Type)
}
a.emitDriverDownloadProgress(definition.Type, "start", 0, 100, fmt.Sprintf("开始安装 %s 驱动代理", displayName))
meta, installErr := installOptionalDriverAgentPackage(a, definition, selectedVersion, resolvedDir, urlText)
if installErr != nil {
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 {
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{}{
"driverType": definition.Type,
"driverName": definition.Name,
"engine": engine,
}}
}
a.emitDriverDownloadProgress(definition.Type, "start", 0, 0, "开始安装")
meta := installedDriverPackage{
DriverType: definition.Type,
Version: selectedVersion,
FilePath: "",
FileName: "embedded-go-driver",
DownloadURL: urlText,
SHA256: "",
DownloadedAt: time.Now().Format(time.RFC3339),
}
if err := writeInstalledDriverPackage(resolvedDir, definition.Type, meta); err != nil {
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 驱动已启用)")
return connection.QueryResult{Success: true, Message: "驱动安装成功", Data: map[string]interface{}{
"driverType": definition.Type,
"driverName": definition.Name,
"engine": engine,
}}
}
func (a *App) RemoveDriverPackage(driverType string, downloadDir string) connection.QueryResult {
definition, ok := resolveDriverDefinition(driverType)
if !ok {
return connection.QueryResult{Success: false, Message: "不支持的驱动类型"}
}
if definition.BuiltIn {
return connection.QueryResult{Success: false, Message: "内置驱动不可移除"}
}
resolvedDir, err := resolveDriverDownloadDirectory(downloadDir)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
db.SetExternalDriverDownloadDirectory(resolvedDir)
driverDir := driverInstallDir(resolvedDir, definition.Type)
if err := os.RemoveAll(driverDir); err != nil {
return connection.QueryResult{Success: false, Message: fmt.Sprintf("移除驱动包失败:%v", err)}
}
return connection.QueryResult{Success: true, Message: "驱动包已移除", Data: map[string]interface{}{
"driverType": definition.Type,
"driverName": definition.Name,
}}
}
func (a *App) emitDriverDownloadProgress(driverType string, status string, downloaded, total int64, message string) {
if a.ctx == nil {
return
}
payload := driverDownloadProgressPayload{
DriverType: normalizeDriverType(driverType),
Status: strings.TrimSpace(status),
Percent: 0,
Downloaded: downloaded,
Total: total,
Message: strings.TrimSpace(message),
}
if payload.DriverType == "" {
payload.DriverType = "unknown"
}
if payload.Status == "" {
payload.Status = "downloading"
}
if total > 0 {
payload.Percent = (float64(downloaded) / float64(total)) * 100
if payload.Percent < 0 {
payload.Percent = 0
}
if payload.Percent > 100 {
payload.Percent = 100
}
}
if payload.Status == "done" && payload.Percent < 100 {
payload.Percent = 100
}
runtime.EventsEmit(a.ctx, driverDownloadProgressEvent, payload)
}
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 == "" {
probed.Error = "检测地址为空"
return probed
}
if tcpLatency, tcpErr := probeDriverTCPLatency(urlText); tcpErr == nil {
probed.TCPLatency = tcpLatency
probed.LatencyMs = tcpLatency
}
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
}
if err != nil {
probed.Error = normalizeDriverNetworkError(err)
return probed
}
defer resp.Body.Close()
probed.Method = method
probed.HTTPStatus = resp.StatusCode
if resp.StatusCode >= 500 {
probed.Error = fmt.Sprintf("HTTP %d", resp.StatusCode)
return probed
}
probed.Reachable = true
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 ""
}
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) != "" {
return root
}
return filepath.Join(os.TempDir(), "gonavi-drivers")
}
func resolveDriverDownloadDirectory(directory string) (string, error) {
return db.ResolveExternalDriverRoot(directory)
}
func normalizeDriverType(driverType string) string {
normalized := strings.ToLower(strings.TrimSpace(driverType))
switch normalized {
case "doris":
return "diros"
case "postgresql":
return "postgres"
case "opengauss", "open_gauss", "open-gauss":
return "opengauss"
default:
return normalized
}
}
func normalizeDriverEngine(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case driverEngineGo:
return driverEngineGo
case "jdbc":
return driverEngineExternal
case driverEngineExternal, "exec", "binary":
return driverEngineExternal
default:
return ""
}
}
func normalizeDriverChecksumPolicy(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case driverChecksumPolicyStrict:
return driverChecksumPolicyStrict
case driverChecksumPolicyOff:
return driverChecksumPolicyOff
case driverChecksumPolicyWarn:
return driverChecksumPolicyWarn
default:
return driverChecksumPolicyWarn
}
}
func effectiveDriverEngine(definition driverDefinition) string {
if definition.BuiltIn {
return driverEngineGo
}
engine := normalizeDriverEngine(definition.Engine)
if engine == "" {
return driverEngineExternal
}
return engine
}
func resolveDriverDefinition(driverType string) (driverDefinition, bool) {
effectivePackages, err := resolveEffectiveDriverPackages("")
if err == nil {
return resolveDriverDefinitionWithPackages(driverType, effectivePackages)
}
return resolveDriverDefinitionWithPackages(driverType, nil)
}
func resolveDriverDefinitionWithPackages(driverType string, packages map[string]pinnedDriverPackage) (driverDefinition, bool) {
normalized := normalizeDriverType(driverType)
for _, definition := range allDriverDefinitionsWithPackages(packages) {
if normalizeDriverType(definition.Type) == normalized {
return definition, true
}
}
return driverDefinition{}, false
}
func allDriverDefinitionsWithPackages(packages map[string]pinnedDriverPackage) []driverDefinition {
return []driverDefinition{
{Type: "mysql", Name: "MySQL", Engine: driverEngineGo, BuiltIn: true},
{Type: "oracle", Name: "Oracle", Engine: driverEngineGo, BuiltIn: true},
{Type: "redis", Name: "Redis", Engine: driverEngineGo, BuiltIn: true},
{Type: "postgres", Name: "PostgreSQL", Engine: driverEngineGo, BuiltIn: true},
// 其他数据源需要先在驱动管理中“安装启用”。
buildOptionalGoDriverDefinition("mariadb", "MariaDB", packages),
buildOptionalGoDriverDefinition("oceanbase", "OceanBase", packages),
buildOptionalGoDriverDefinition("diros", "Doris", packages),
buildOptionalGoDriverDefinition("sphinx", "Sphinx", packages),
buildOptionalGoDriverDefinition("sqlserver", "SQL Server", packages),
buildOptionalGoDriverDefinition("sqlite", "SQLite", packages),
buildOptionalGoDriverDefinition("duckdb", "DuckDB", packages),
buildOptionalGoDriverDefinition("dameng", "Dameng", packages),
buildOptionalGoDriverDefinition("kingbase", "Kingbase", packages),
buildOptionalGoDriverDefinition("highgo", "HighGo", packages),
buildOptionalGoDriverDefinition("vastbase", "Vastbase", packages),
buildOptionalGoDriverDefinition("opengauss", "OpenGauss", packages),
buildOptionalGoDriverDefinition("mongodb", "MongoDB", packages),
buildOptionalGoDriverDefinition("tdengine", "TDengine", packages),
buildOptionalGoDriverDefinition("clickhouse", "ClickHouse", packages),
}
}
func buildOptionalGoDriverDefinition(driverType string, driverName string, packages map[string]pinnedDriverPackage) driverDefinition {
spec := resolvedPinnedPackage(driverType, packages)
return driverDefinition{
Type: normalizeDriverType(driverType),
Name: driverName,
Engine: driverEngineGo,
BuiltIn: false,
PinnedVersion: strings.TrimSpace(spec.Version),
DefaultDownloadURL: strings.TrimSpace(spec.DownloadURL),
DownloadSHA256: strings.TrimSpace(spec.SHA256),
ChecksumPolicy: normalizeDriverChecksumPolicy(spec.Policy),
}
}
func ensureOptionalDriverBuildAvailable(definition driverDefinition) error {
driverType := normalizeDriverType(definition.Type)
if !db.IsOptionalGoDriver(driverType) {
return nil
}
if db.IsOptionalGoDriverBuildIncluded(driverType) {
return nil
}
driverName := strings.TrimSpace(definition.Name)
if driverName == "" {
driverName = strings.TrimSpace(definition.Type)
}
return fmt.Errorf("%s 当前发行包为精简构建,未内置该驱动;如需使用请安装 Full 版", driverName)
}
func driverPinnedPackage(driverType string) pinnedDriverPackage {
spec, ok := pinnedDriverPackageMap[normalizeDriverType(driverType)]
if !ok {
return pinnedDriverPackage{}
}
spec.Version = strings.TrimSpace(spec.Version)
spec.DownloadURL = strings.TrimSpace(spec.DownloadURL)
spec.SHA256 = strings.TrimSpace(spec.SHA256)
spec.Policy = normalizeDriverChecksumPolicy(spec.Policy)
spec.Engine = normalizeDriverEngine(spec.Engine)
return spec
}
func resolvedPinnedPackage(driverType string, packages map[string]pinnedDriverPackage) pinnedDriverPackage {
normalizedType := normalizeDriverType(driverType)
spec := driverPinnedPackage(normalizedType)
if packages != nil {
override, ok := packages[normalizedType]
if ok {
if strings.TrimSpace(override.Version) != "" {
spec.Version = strings.TrimSpace(override.Version)
}
if strings.TrimSpace(override.DownloadURL) != "" {
spec.DownloadURL = strings.TrimSpace(override.DownloadURL)
}
if strings.TrimSpace(override.SHA256) != "" {
spec.SHA256 = strings.TrimSpace(override.SHA256)
}
if strings.TrimSpace(override.Policy) != "" {
spec.Policy = normalizeDriverChecksumPolicy(override.Policy)
}
if strings.TrimSpace(override.Engine) != "" {
spec.Engine = normalizeDriverEngine(override.Engine)
}
}
}
if normalizedType == "postgres" {
spec.Engine = driverEngineGo
if strings.TrimSpace(spec.Version) == "" {
spec.Version = "go-embedded"
}
if strings.TrimSpace(spec.Policy) == "" {
spec.Policy = driverChecksumPolicyOff
}
}
return spec
}
func copyPinnedPackageMap(source map[string]pinnedDriverPackage) map[string]pinnedDriverPackage {
if len(source) == 0 {
return map[string]pinnedDriverPackage{}
}
result := make(map[string]pinnedDriverPackage, len(source))
for key, value := range source {
result[key] = pinnedDriverPackage{
Version: strings.TrimSpace(value.Version),
DownloadURL: strings.TrimSpace(value.DownloadURL),
SHA256: strings.TrimSpace(value.SHA256),
Policy: normalizeDriverChecksumPolicy(value.Policy),
Engine: normalizeDriverEngine(value.Engine),
}
}
return result
}
func copyVersionPackageMap(source map[string][]pinnedDriverPackage) map[string][]pinnedDriverPackage {
if len(source) == 0 {
return map[string][]pinnedDriverPackage{}
}
result := make(map[string][]pinnedDriverPackage, len(source))
for key, values := range source {
if len(values) == 0 {
result[key] = []pinnedDriverPackage{}
continue
}
next := make([]pinnedDriverPackage, 0, len(values))
for _, value := range values {
next = append(next, pinnedDriverPackage{
Version: strings.TrimSpace(value.Version),
DownloadURL: strings.TrimSpace(value.DownloadURL),
SHA256: strings.TrimSpace(value.SHA256),
Policy: normalizeDriverChecksumPolicy(value.Policy),
Engine: normalizeDriverEngine(value.Engine),
})
}
result[key] = next
}
return result
}
func resolveEffectiveDriverPackages(manifestURL string) (map[string]pinnedDriverPackage, error) {
effective := copyPinnedPackageMap(pinnedDriverPackageMap)
manifestPackages, err := resolveManifestDriverPackages(manifestURL)
if err != nil {
return effective, err
}
for driverType, item := range manifestPackages {
normalizedType := normalizeDriverType(driverType)
base := effective[normalizedType]
if strings.TrimSpace(item.Version) != "" {
base.Version = strings.TrimSpace(item.Version)
}
if strings.TrimSpace(item.DownloadURL) != "" {
base.DownloadURL = strings.TrimSpace(item.DownloadURL)
}
if strings.TrimSpace(item.SHA256) != "" {
base.SHA256 = strings.TrimSpace(item.SHA256)
}
if strings.TrimSpace(item.Policy) != "" {
base.Policy = normalizeDriverChecksumPolicy(item.Policy)
}
if strings.TrimSpace(item.Engine) != "" {
base.Engine = normalizeDriverEngine(item.Engine)
}
effective[normalizedType] = base
}
return effective, nil
}
func resolveDriverVersionOptions(definition driverDefinition, repositoryURL string) ([]driverVersionOptionItem, error) {
driverType := normalizeDriverType(definition.Type)
if driverType == "" {
return nil, fmt.Errorf("驱动类型为空")
}
optionMap := make(map[string]driverVersionOptionItem)
optionKeys := make([]string, 0, 16)
appendOption := func(version, downloadURL, sha256, source, year string) {
versionText := strings.TrimSpace(version)
urlText := strings.TrimSpace(downloadURL)
if urlText == "" {
urlText = strings.TrimSpace(definition.DefaultDownloadURL)
}
if urlText == "" && effectiveDriverEngine(definition) == driverEngineGo {
urlText = fmt.Sprintf("builtin://activate/%s", optionalDriverPublicTypeName(driverType))
}
if versionText == "" {
versionText = resolveDriverInstallVersion("", urlText, definition)
}
if versionText == "" && urlText == "" {
return
}
if versionText != "" {
if err := validateDriverSelectedVersion(definition, versionText); err != nil {
return
}
}
versionKey := normalizeVersion(versionText)
key := ""
if versionKey != "" {
key = "v:" + strings.ToLower(versionKey)
} else {
key = "u:" + urlText
}
if existing, ok := optionMap[key]; ok {
if existing.Year == "" && strings.TrimSpace(year) != "" {
existing.Year = strings.TrimSpace(year)
optionMap[key] = existing
}
return
}
optionMap[key] = driverVersionOptionItem{
Version: versionText,
DownloadURL: urlText,
SHA256: strings.TrimSpace(sha256),
Source: strings.TrimSpace(source),
Year: strings.TrimSpace(year),
}
optionKeys = append(optionKeys, key)
}
manifestVersions, _ := resolveManifestDriverVersionPackages(repositoryURL)
if values := manifestVersions[driverType]; len(values) > 0 {
expectedEngine := effectiveDriverEngine(definition)
for _, value := range values {
engine := normalizeDriverEngine(value.Engine)
if engine != "" && expectedEngine != "" && engine != expectedEngine {
continue
}
appendOption(value.Version, value.DownloadURL, value.SHA256, "manifest", "")
}
}
appendOption(definition.PinnedVersion, definition.DefaultDownloadURL, definition.DownloadSHA256, "pinned", "")
for _, recent := range resolveRecentDriverVersionOptions(definition, driverRecentVersionLimit) {
if sameDriverVersion(recent.Version, definition.PinnedVersion) {
continue
}
appendOption(recent.Version, recent.DownloadURL, recent.SHA256, recent.Source, recent.Year)
}
if len(optionKeys) == 0 {
return nil, fmt.Errorf("未找到可用驱动版本")
}
recommendedVersion := strings.TrimSpace(definition.PinnedVersion)
recommendedIndex := -1
if recommendedVersion != "" {
for index, key := range optionKeys {
option := optionMap[key]
if strings.EqualFold(strings.TrimSpace(option.Version), recommendedVersion) {
recommendedIndex = index
break
}
}
}
if recommendedIndex == -1 {
recommendedIndex = 0
}
result := make([]driverVersionOptionItem, 0, len(optionKeys))
for index, key := range optionKeys {
option := optionMap[key]
option.Recommended = index == recommendedIndex
sizeBytes := resolveDriverVersionPackageSizeBytes(definition, option)
if sizeBytes > 0 {
option.PackageSizeBytes = sizeBytes
option.PackageSizeText = formatSizeMB(sizeBytes)
}
option.DisplayLabel = buildDriverVersionDisplayLabel(option)
result = append(result, option)
}
return result, nil
}
func buildDriverVersionDisplayLabel(option driverVersionOptionItem) string {
label := strings.TrimSpace(option.Version)
if label == "" {
label = "未标注版本"
}
if strings.EqualFold(strings.TrimSpace(option.Source), "latest") {
label += "(最新)"
}
if option.Recommended {
label += "(推荐)"
}
return label
}
func resolveRecentDriverVersionOptions(definition driverDefinition, limit int) []driverVersionOptionItem {
metas := resolveRecentDriverVersionMetas(definition.Type, limit)
if len(metas) == 0 {
return nil
}
result := make([]driverVersionOptionItem, 0, len(metas))
for index, meta := range metas {
source := "history"
if index == 0 {
source = "latest"
}
versionText, urlText, ok := resolveVersionedDriverOption(definition, meta.Version, source)
if !ok {
continue
}
result = append(result, driverVersionOptionItem{
Version: versionText,
DownloadURL: urlText,
Source: source,
Year: strings.TrimSpace(meta.Year),
})
}
return result
}
func resolveVersionedDriverOption(definition driverDefinition, version string, source string) (string, string, bool) {
driverType := normalizeDriverType(definition.Type)
if driverType == "" {
return "", "", false
}
versionText := normalizeVersion(strings.TrimSpace(version))
if versionText == "" {
return "", "", false
}
if err := validateDriverSelectedVersion(definition, versionText); err != nil {
return "", "", false
}
if publishedURL, ok := resolvePublishedDriverDownloadURL(definition, versionText); ok {
return versionText, publishedURL, true
}
if !optionalDriverSourceBuildAvailable(definition, versionText) {
return "", "", false
}
urlText := strings.TrimSpace(definition.DefaultDownloadURL)
if urlText == "" && effectiveDriverEngine(definition) == driverEngineGo {
urlText = fmt.Sprintf("builtin://activate/%s", optionalDriverPublicTypeName(driverType))
}
if urlText == "" {
return "", "", false
}
parsed, err := url.Parse(urlText)
if err != nil || parsed == nil {
return versionText, urlText, true
}
query := parsed.Query()
channel := strings.TrimSpace(source)
if channel == "" {
channel = "history"
}
query.Set("channel", channel)
query.Set("version", versionText)
parsed.RawQuery = query.Encode()
return versionText, parsed.String(), true
}
func sameDriverVersion(left, right string) bool {
a := normalizeVersion(strings.TrimSpace(left))
b := normalizeVersion(strings.TrimSpace(right))
return a != "" && a == b
}
func validateDriverSelectedVersion(definition driverDefinition, version string) error {
driverType := normalizeDriverType(definition.Type)
versionText := normalizeVersion(strings.TrimSpace(version))
if driverType == "" || versionText == "" {
return nil
}
switch driverType {
case "mongodb":
if strings.HasPrefix(versionText, "2.") {
return nil
}
if strings.HasPrefix(versionText, "1.17.") {
return nil
}
return fmt.Errorf("MongoDB 版本 %s 当前不受支持;仅支持 1.17.x 和 2.x", versionText)
default:
return nil
}
}
func shouldRestrictToExplicitVersionArtifact(definition driverDefinition, selectedVersion string) bool {
versionText := normalizeVersion(strings.TrimSpace(selectedVersion))
if versionText == "" {
return false
}
return !sameDriverVersion(versionText, definition.PinnedVersion)
}
func optionalDriverSourceBuildAvailable(definition driverDefinition, selectedVersion string) bool {
driverType := normalizeDriverType(definition.Type)
if driverType == "" || !db.IsOptionalGoDriver(driverType) {
return false
}
if _, err := optionalDriverBuildTag(driverType, selectedVersion); err != nil {
return false
}
if _, err := exec.LookPath("go"); err != nil {
return false
}
if _, err := locateProjectRootForAgentBuild(); err != nil {
return false
}
return true
}
func resolvePublishedDriverDownloadURL(definition driverDefinition, version string) (string, bool) {
driverType := normalizeDriverType(definition.Type)
versionText := normalizeVersion(strings.TrimSpace(version))
if driverType == "" || versionText == "" {
return "", false
}
tag := "v" + versionText
assetName, ok := resolvePublishedDriverReleaseAssetName(driverType, versionText, tag)
if !ok {
return "", false
}
return fmt.Sprintf("https://github.com/%s/releases/download/%s/%s", updateRepo, tag, assetName), true
}
func resolvePublishedDriverReleaseAssetName(driverType string, version string, tag string) (string, bool) {
assetNames := optionalDriverReleaseAssetNamesForVersion(driverType, version)
if len(assetNames) == 0 {
return "", false
}
cacheKey := "tag:" + strings.TrimSpace(tag)
if sizeByAsset, publishedAssets, ok := readReleaseAssetSizesFromCache(cacheKey); ok {
for _, assetName := range assetNames {
if publishedAssets[assetName] && sizeByAsset[assetName] > 0 {
return assetName, true
}
}
return "", false
}
sizeByAsset, publishedAssets, err := loadReleaseAssetSizesCached(cacheKey, func() (*githubRelease, error) {
return fetchReleaseByTag(tag)
})
if err != nil {
return "", false
}
for _, assetName := range assetNames {
if publishedAssets[assetName] && sizeByAsset[assetName] > 0 {
return assetName, true
}
}
return "", false
}
func resolveDriverVersionPackageSizeBytes(definition driverDefinition, option driverVersionOptionItem) int64 {
driverType := normalizeDriverType(definition.Type)
if driverType == "" || definition.BuiltIn {
return 0
}
if !db.IsOptionalGoDriver(driverType) {
return 0
}
version := normalizeVersion(strings.TrimSpace(option.Version))
if version == "" {
return 0
}
assetNames := optionalDriverReleaseAssetNamesForVersion(driverType, version)
if len(assetNames) == 0 {
return 0
}
tag := "v" + version
if sizeByAsset, _, ok := readReleaseAssetSizesFromCache("tag:" + tag); ok {
return resolveOptionalDriverAssetSizeForVersion(sizeByAsset, driverType, version)
}
// 下拉版本列表要求快速返回:仅复用已有缓存,不在这里触发网络请求。
if strings.EqualFold(strings.TrimSpace(option.Source), "latest") {
if sizeByAsset, _, ok := readReleaseAssetSizesFromCache("latest"); ok {
return resolveOptionalDriverAssetSizeForVersion(sizeByAsset, driverType, version)
}
}
return 0
}
func resolveRecentDriverVersionMetas(driverType string, limit int) []goModuleVersionMeta {
if limit <= 0 {
limit = driverRecentVersionLimit
}
normalized := normalizeDriverType(driverType)
if normalized == "" {
return nil
}
modulePaths := resolveDriverGoModulePaths(normalized)
if len(modulePaths) > 0 {
extraHistoryLimit := resolveDriverExtraHistoryLimit(normalized)
primaryLimit := limit + extraHistoryLimit
if primaryLimit <= 0 {
primaryLimit = limit
}
result := make([]goModuleVersionMeta, 0, primaryLimit)
seen := make(map[string]struct{}, primaryLimit)
appendUnique := func(values []goModuleVersionMeta, maxAppend int) {
if maxAppend <= 0 {
return
}
appended := 0
for _, meta := range values {
version := normalizeVersion(strings.TrimSpace(meta.Version))
if version == "" {
continue
}
key := strings.ToLower(version)
if _, ok := seen[key]; ok {
continue
}
meta.Version = version
result = append(result, meta)
seen[key] = struct{}{}
appended++
if appended >= maxAppend {
return
}
}
}
appendUnique(fetchGoModuleVersionMetasCached(modulePaths[0]), primaryLimit)
extraLimit := extraHistoryLimit
for _, modulePath := range modulePaths[1:] {
if extraLimit <= 0 {
break
}
before := len(result)
appendUnique(fetchGoModuleVersionMetasCached(modulePath), extraLimit)
extraLimit -= len(result) - before
}
if len(result) > 0 {
return result
}
}
fallbackLimit := limit + resolveDriverExtraHistoryLimit(normalized)
if fallbackLimit <= 0 {
fallbackLimit = limit
}
if fallback := fallbackRecentDriverVersionsMap[normalized]; len(fallback) > 0 {
if len(fallback) > fallbackLimit {
return append([]goModuleVersionMeta(nil), fallback[:fallbackLimit]...)
}
return append([]goModuleVersionMeta(nil), fallback...)
}
if fallback := normalizeVersion(strings.TrimSpace(latestDriverVersionMap[normalized])); fallback != "" {
return []goModuleVersionMeta{{Version: fallback}}
}
return nil
}
func triggerDriverVersionMetadataWarmup(definitions []driverDefinition) {
if len(definitions) == 0 {
return
}
modulePaths := make([]string, 0, len(definitions))
seenModule := make(map[string]struct{}, len(definitions))
for _, definition := range definitions {
if definition.BuiltIn {
continue
}
driverType := normalizeDriverType(definition.Type)
if driverType == "" || !db.IsOptionalGoDriver(driverType) {
continue
}
for _, modulePath := range resolveDriverGoModulePaths(driverType) {
if _, ok := seenModule[modulePath]; ok {
continue
}
seenModule[modulePath] = struct{}{}
modulePaths = append(modulePaths, modulePath)
}
}
if len(modulePaths) == 0 {
return
}
if !tryStartDriverVersionMetadataWarmup(time.Now()) {
return
}
go func(paths []string) {
defer finishDriverVersionMetadataWarmup()
// 预热 latest 资产索引,便于版本列表命中大小缓存。
_, _, _ = loadReleaseAssetSizesCached("latest", fetchLatestReleaseForDriverAssets)
for _, modulePath := range paths {
_ = fetchGoModuleVersionMetasCached(modulePath)
}
}(append([]string(nil), modulePaths...))
}
func resolveDriverGoModulePaths(driverType string) []string {
normalized := normalizeDriverType(driverType)
if normalized == "" {
return nil
}
paths := make([]string, 0, 3)
seen := make(map[string]struct{}, 3)
appendPath := func(path string) {
trimmed := strings.TrimSpace(path)
if trimmed == "" {
return
}
if _, ok := seen[trimmed]; ok {
return
}
seen[trimmed] = struct{}{}
paths = append(paths, trimmed)
}
appendPath(driverGoModulePathMap[normalized])
for _, alias := range driverGoModuleAliasPathMap[normalized] {
appendPath(alias)
}
return paths
}
func resolveDriverExtraHistoryLimit(driverType string) int {
limit := driverExtraHistoryLimitMap[normalizeDriverType(driverType)]
if limit < 0 {
return 0
}
return limit
}
func tryStartDriverVersionMetadataWarmup(now time.Time) bool {
driverVersionWarmupMu.Lock()
defer driverVersionWarmupMu.Unlock()
if driverVersionWarmup.Running {
return false
}
if !driverVersionWarmup.LastStarted.IsZero() && now.Sub(driverVersionWarmup.LastStarted) < driverVersionWarmupMinInterval {
return false
}
driverVersionWarmup.Running = true
driverVersionWarmup.LastStarted = now
return true
}
func finishDriverVersionMetadataWarmup() {
driverVersionWarmupMu.Lock()
driverVersionWarmup.Running = false
driverVersionWarmupMu.Unlock()
}
func fetchGoModuleVersionMetasCached(modulePath string) []goModuleVersionMeta {
key := strings.TrimSpace(modulePath)
if key == "" {
return nil
}
driverModuleVersionMu.RLock()
cached, ok := driverModuleVersionMap[key]
driverModuleVersionMu.RUnlock()
if ok {
ttl := driverModuleLatestCacheTTL
if strings.TrimSpace(cached.Err) != "" {
ttl = driverModuleLatestErrorCacheTTL
}
if time.Since(cached.LoadedAt) < ttl {
if strings.TrimSpace(cached.Err) != "" {
return nil
}
return append([]goModuleVersionMeta(nil), cached.Versions...)
}
}
metas, err := fetchGoModuleVersionMetas(key)
entry := goModuleVersionListCacheEntry{
LoadedAt: time.Now(),
Versions: append([]goModuleVersionMeta(nil), metas...),
}
if err != nil {
entry.Err = err.Error()
}
driverModuleVersionMu.Lock()
driverModuleVersionMap[key] = entry
driverModuleVersionMu.Unlock()
if err != nil {
return nil
}
return append([]goModuleVersionMeta(nil), entry.Versions...)
}
func fetchGoModuleVersionMetas(modulePath string) ([]goModuleVersionMeta, error) {
trimmed := strings.TrimSpace(modulePath)
if trimmed == "" {
return nil, fmt.Errorf("模块路径为空")
}
endpoint := fmt.Sprintf("https://proxy.golang.org/%s/@v/list", escapeGoModulePathForProxy(trimmed))
client := newHTTPClientWithGlobalProxy(driverModuleLatestProbeTimeout)
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "GoNavi-DriverManager")
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("拉取模块版本列表失败HTTP %d", resp.StatusCode)
}
body, err := io.ReadAll(io.LimitReader(resp.Body, driverModuleVersionListMaxSize))
if err != nil {
return nil, fmt.Errorf("读取模块版本列表失败:%w", err)
}
lines := strings.Split(strings.TrimSpace(string(body)), "\n")
versions := make([]string, 0, len(lines))
seen := make(map[string]struct{}, len(lines))
for _, line := range lines {
version := normalizeVersion(strings.TrimSpace(line))
if version == "" {
continue
}
normalizedSemver := "v" + version
if !semver.IsValid(normalizedSemver) {
continue
}
if semver.Prerelease(normalizedSemver) != "" {
continue
}
if _, ok := seen[version]; ok {
continue
}
seen[version] = struct{}{}
versions = append(versions, version)
}
if len(versions) == 0 {
return nil, fmt.Errorf("模块版本列表为空")
}
sort.SliceStable(versions, func(i, j int) bool {
left := "v" + versions[i]
right := "v" + versions[j]
return semver.Compare(left, right) > 0
})
if len(versions) > driverModuleVersionFetchLimit {
versions = versions[:driverModuleVersionFetchLimit]
}
metas := make([]goModuleVersionMeta, 0, len(versions))
for _, version := range versions {
metas = append(metas, goModuleVersionMeta{Version: version})
}
return metas, nil
}
func escapeGoModulePathForProxy(modulePath string) string {
parts := strings.Split(modulePath, "/")
for index, part := range parts {
parts[index] = url.PathEscape(strings.TrimSpace(part))
}
return strings.Join(parts, "/")
}
func resolveDriverVersionOptionsFromReleases(definition driverDefinition) []driverVersionOptionItem {
driverType := normalizeDriverType(definition.Type)
if driverType == "" {
return nil
}
releases, err := loadDriverReleaseListCached()
if err != nil {
return nil
}
result := make([]driverVersionOptionItem, 0, len(releases))
for _, release := range releases {
if release.Prerelease {
continue
}
tag := strings.TrimSpace(release.TagName)
version := normalizeVersion(tag)
if tag == "" || version == "" {
continue
}
assetName := optionalDriverReleaseAssetNameForVersion(driverType, version)
assetNames := optionalDriverReleaseAssetNamesForVersion(driverType, version)
if !releaseContainsAnyAsset(release, assetNames) {
continue
}
result = append(result, driverVersionOptionItem{
Version: version,
DownloadURL: fmt.Sprintf("https://github.com/%s/releases/download/%s/%s", updateRepo, tag, assetName),
Source: "release",
})
}
return result
}
func loadDriverReleaseListCached() ([]githubRelease, error) {
driverReleaseListMu.RLock()
cached := driverReleaseList
driverReleaseListMu.RUnlock()
if time.Since(cached.LoadedAt) < driverManifestCacheTTL {
if strings.TrimSpace(cached.Err) != "" {
return nil, errors.New(strings.TrimSpace(cached.Err))
}
return append([]githubRelease(nil), cached.Releases...), nil
}
driverReleaseListMu.Lock()
defer driverReleaseListMu.Unlock()
cached = driverReleaseList
if time.Since(cached.LoadedAt) < driverManifestCacheTTL {
if strings.TrimSpace(cached.Err) != "" {
return nil, errors.New(strings.TrimSpace(cached.Err))
}
return append([]githubRelease(nil), cached.Releases...), nil
}
releases, err := fetchDriverReleaseList()
entry := driverManifestReleaseListCache{
LoadedAt: time.Now(),
Releases: append([]githubRelease(nil), releases...),
}
if err != nil {
entry.Err = err.Error()
}
driverReleaseList = entry
if err != nil {
return nil, err
}
return releases, nil
}
func fetchDriverReleaseList() ([]githubRelease, error) {
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/releases?per_page=30", updateRepo)
client := newHTTPClientWithGlobalProxy(driverReleaseListProbeTimeout)
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "GoNavi-DriverManager")
req.Header.Set("Accept", "application/vnd.github+json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("拉取驱动版本列表失败HTTP %d", resp.StatusCode)
}
decoder := json.NewDecoder(io.LimitReader(resp.Body, 4<<20))
var releases []githubRelease
if err := decoder.Decode(&releases); err != nil {
return nil, fmt.Errorf("解析驱动版本列表失败:%w", err)
}
return releases, nil
}
func releaseContainsAnyAsset(release githubRelease, assetNames []string) bool {
normalizedNames := make([]string, 0, len(assetNames))
for _, assetName := range assetNames {
name := strings.TrimSpace(assetName)
if name == "" {
continue
}
normalizedNames = append(normalizedNames, name)
}
if len(normalizedNames) == 0 {
return false
}
for _, asset := range release.Assets {
assetName := strings.TrimSpace(asset.Name)
for _, expected := range normalizedNames {
if strings.EqualFold(assetName, expected) {
return true
}
}
}
return false
}
func resolveDriverInstallVersion(version, downloadURL string, definition driverDefinition) string {
if selected := strings.TrimSpace(version); selected != "" {
return selected
}
if inferred := inferDriverInstallVersionByDownloadURL(downloadURL); inferred != "" {
return inferred
}
if pinned := strings.TrimSpace(definition.PinnedVersion); pinned != "" {
return pinned
}
if effectiveDriverEngine(definition) == driverEngineGo {
return "go-embedded"
}
return "unknown"
}
func inferDriverInstallVersionByDownloadURL(downloadURL string) string {
urlText := strings.TrimSpace(downloadURL)
if urlText == "" {
return ""
}
parsed, err := url.Parse(urlText)
if err == nil && parsed != nil {
switch strings.ToLower(strings.TrimSpace(parsed.Scheme)) {
case "builtin":
return ""
case "local":
return "local"
case "http", "https":
if queryVersion := normalizeVersion(parsed.Query().Get("version")); queryVersion != "" {
return queryVersion
}
if tag := extractReleaseTagFromPath(parsed.Path); tag != "" {
return normalizeVersion(tag)
}
}
}
if tag := extractReleaseTagFromPath(urlText); tag != "" {
return normalizeVersion(tag)
}
return ""
}
func extractReleaseTagFromPath(pathText string) string {
segments := strings.Split(pathText, "/")
for index := 0; index < len(segments)-1; index++ {
if !strings.EqualFold(strings.TrimSpace(segments[index]), "download") {
continue
}
tag := strings.TrimSpace(segments[index+1])
if tag == "" || strings.EqualFold(tag, "latest") {
continue
}
if decoded, err := url.PathUnescape(tag); err == nil && strings.TrimSpace(decoded) != "" {
tag = strings.TrimSpace(decoded)
}
return tag
}
return ""
}
func resolveDriverRepositoryURL(repositoryURL string) (string, error) {
urlText := strings.TrimSpace(repositoryURL)
if urlText == "" {
return defaultDriverManifestURLValue, nil
}
parsed, err := url.Parse(urlText)
if err == nil && parsed.Scheme != "" {
switch strings.ToLower(parsed.Scheme) {
case "http", "https":
return parsed.String(), nil
case "file":
if parsed.Path == "" {
return "", fmt.Errorf("无效的文件清单地址")
}
return urlText, nil
case "builtin":
if isBuiltinManifestURL(parsed) {
return defaultDriverManifestURLValue, nil
}
return "", fmt.Errorf("不支持的内置清单地址:%s", parsed.String())
default:
return "", fmt.Errorf("不支持的清单地址协议:%s", parsed.Scheme)
}
}
absPath, absErr := filepath.Abs(urlText)
if absErr != nil {
return "", absErr
}
return absPath, nil
}
func resolveManifestURLForView(manifestURL string) string {
resolved, err := resolveDriverRepositoryURL(manifestURL)
if err != nil {
return strings.TrimSpace(manifestURL)
}
return resolved
}
func resolveManifestDriverPackages(manifestURL string) (map[string]pinnedDriverPackage, error) {
resolvedURL, err := resolveDriverRepositoryURL(manifestURL)
if err != nil {
return nil, err
}
driverManifestCacheMu.RLock()
cached, ok := driverManifestCache[resolvedURL]
driverManifestCacheMu.RUnlock()
if ok && time.Since(cached.LoadedAt) < driverManifestCacheTTL {
if strings.TrimSpace(cached.Err) != "" {
return nil, errors.New(cached.Err)
}
return copyPinnedPackageMap(cached.Packages), nil
}
packages, versions, loadErr := loadManifestPackageAndVersions(resolvedURL)
entry := driverManifestCacheEntry{
LoadedAt: time.Now(),
Packages: copyPinnedPackageMap(packages),
Versions: copyVersionPackageMap(versions),
}
if loadErr != nil {
entry.Err = loadErr.Error()
}
driverManifestCacheMu.Lock()
driverManifestCache[resolvedURL] = entry
driverManifestCacheMu.Unlock()
if loadErr != nil {
return nil, loadErr
}
return packages, nil
}
func resolveManifestDriverVersionPackages(manifestURL string) (map[string][]pinnedDriverPackage, error) {
resolvedURL, err := resolveDriverRepositoryURL(manifestURL)
if err != nil {
return nil, err
}
driverManifestCacheMu.RLock()
cached, ok := driverManifestCache[resolvedURL]
driverManifestCacheMu.RUnlock()
if ok && time.Since(cached.LoadedAt) < driverManifestCacheTTL {
if strings.TrimSpace(cached.Err) != "" {
return nil, errors.New(cached.Err)
}
return copyVersionPackageMap(cached.Versions), nil
}
packages, versions, loadErr := loadManifestPackageAndVersions(resolvedURL)
entry := driverManifestCacheEntry{
LoadedAt: time.Now(),
Packages: copyPinnedPackageMap(packages),
Versions: copyVersionPackageMap(versions),
}
if loadErr != nil {
entry.Err = loadErr.Error()
}
driverManifestCacheMu.Lock()
driverManifestCache[resolvedURL] = entry
driverManifestCacheMu.Unlock()
if loadErr != nil {
return nil, loadErr
}
return versions, nil
}
func loadManifestPackageAndVersions(resolvedURL string) (map[string]pinnedDriverPackage, map[string][]pinnedDriverPackage, error) {
content, err := loadManifestContent(resolvedURL)
if err != nil {
return nil, nil, err
}
var manifest driverManifestFile
if err := json.Unmarshal(content, &manifest); err != nil {
return nil, nil, fmt.Errorf("解析驱动清单失败:%w", err)
}
defaultEngine := normalizeDriverEngine(manifest.Engine)
if defaultEngine == "" {
defaultEngine = normalizeDriverEngine(manifest.DefaultEngine)
}
if defaultEngine == "" {
defaultEngine = normalizeDriverEngine(manifest.DefaultEngine2)
}
result := make(map[string]pinnedDriverPackage)
versionResult := make(map[string][]pinnedDriverPackage)
for driverType, item := range manifest.Drivers {
normalizedType := normalizeDriverType(driverType)
if normalizedType == "" {
continue
}
base := normalizeManifestDriverPackage(item.Version, item.DownloadURL, item.DownloadURL2, item.SHA256, item.ChecksumPolicy, item.ChecksumPolicy2, item.Engine, defaultEngine)
result[normalizedType] = base
versions := normalizeManifestDriverVersionList(item, base, defaultEngine)
if len(versions) == 0 {
versions = append(versions, base)
}
versionResult[normalizedType] = versions
}
return result, versionResult, nil
}
func normalizeManifestDriverPackage(version, downloadURL, downloadURL2, sha256, policy, policy2, engine, defaultEngine string) pinnedDriverPackage {
urlText := strings.TrimSpace(downloadURL)
if urlText == "" {
urlText = strings.TrimSpace(downloadURL2)
}
policyText := strings.TrimSpace(policy)
if policyText == "" {
policyText = strings.TrimSpace(policy2)
}
engineText := normalizeDriverEngine(engine)
if engineText == "" {
engineText = defaultEngine
}
return pinnedDriverPackage{
Version: strings.TrimSpace(version),
DownloadURL: urlText,
SHA256: strings.TrimSpace(sha256),
Policy: normalizeDriverChecksumPolicy(policyText),
Engine: engineText,
}
}
func normalizeManifestDriverVersionList(item driverManifestItem, fallback pinnedDriverPackage, defaultEngine string) []pinnedDriverPackage {
rawVersions := make([]driverManifestVersionItem, 0, len(item.Versions)+len(item.VersionList)+len(item.VersionList2)+len(item.VersionOptions)+len(item.VersionOptions2))
rawVersions = append(rawVersions, item.Versions...)
rawVersions = append(rawVersions, item.VersionList...)
rawVersions = append(rawVersions, item.VersionList2...)
rawVersions = append(rawVersions, item.VersionOptions...)
rawVersions = append(rawVersions, item.VersionOptions2...)
if len(rawVersions) == 0 {
return nil
}
result := make([]pinnedDriverPackage, 0, len(rawVersions))
seen := make(map[string]struct{}, len(rawVersions))
for _, versionItem := range rawVersions {
pkg := normalizeManifestDriverPackage(
versionItem.Version,
versionItem.DownloadURL,
versionItem.DownloadURL2,
versionItem.SHA256,
versionItem.ChecksumPolicy,
versionItem.ChecksumPolicy2,
versionItem.Engine,
defaultEngine,
)
if pkg.Version == "" {
pkg.Version = fallback.Version
}
if pkg.DownloadURL == "" {
pkg.DownloadURL = fallback.DownloadURL
}
if pkg.SHA256 == "" {
pkg.SHA256 = fallback.SHA256
}
if pkg.Policy == "" {
pkg.Policy = fallback.Policy
}
if pkg.Engine == "" {
pkg.Engine = fallback.Engine
}
if pkg.Version == "" && pkg.DownloadURL == "" {
continue
}
key := strings.ToLower(strings.TrimSpace(pkg.Version)) + "|" + strings.TrimSpace(pkg.DownloadURL)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
result = append(result, pkg)
}
return result
}
func loadManifestContent(resolvedURL string) ([]byte, error) {
trimmed := strings.TrimSpace(resolvedURL)
if trimmed == "" {
return nil, fmt.Errorf("驱动清单地址为空")
}
parsed, err := url.Parse(trimmed)
if err == nil {
scheme := strings.ToLower(strings.TrimSpace(parsed.Scheme))
switch scheme {
case "http", "https":
client := newHTTPClientWithGlobalProxy(12 * time.Second)
req, reqErr := http.NewRequest(http.MethodGet, parsed.String(), nil)
if reqErr != nil {
return nil, reqErr
}
req.Header.Set("User-Agent", "GoNavi-DriverManifest")
resp, doErr := client.Do(req)
if doErr != nil {
return nil, doErr
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("拉取驱动清单失败HTTP %d", resp.StatusCode)
}
limited := io.LimitReader(resp.Body, driverManifestMaxSize+1)
body, readErr := io.ReadAll(limited)
if readErr != nil {
return nil, readErr
}
if int64(len(body)) > driverManifestMaxSize {
return nil, fmt.Errorf("驱动清单超过大小限制")
}
return body, nil
case "file":
pathText := strings.TrimSpace(parsed.Path)
if pathText == "" {
return nil, fmt.Errorf("无效的本地驱动清单地址")
}
body, readErr := os.ReadFile(pathText)
if readErr != nil {
return nil, readErr
}
if int64(len(body)) > driverManifestMaxSize {
return nil, fmt.Errorf("驱动清单超过大小限制")
}
return body, nil
case "builtin":
if isBuiltinManifestURL(parsed) {
return []byte(builtinDriverManifestJSON), nil
}
return nil, fmt.Errorf("不支持的内置清单地址:%s", parsed.String())
}
}
body, readErr := os.ReadFile(trimmed)
if readErr != nil {
return nil, readErr
}
if int64(len(body)) > driverManifestMaxSize {
return nil, fmt.Errorf("驱动清单超过大小限制")
}
return body, nil
}
func isBuiltinManifestURL(parsed *url.URL) bool {
if parsed == nil {
return false
}
if strings.ToLower(strings.TrimSpace(parsed.Scheme)) != "builtin" {
return false
}
if strings.ToLower(strings.TrimSpace(parsed.Host)) != "manifest" {
return false
}
pathText := strings.TrimSpace(parsed.Path)
return pathText == "" || pathText == "/"
}
func errorMessage(err error) string {
if err == nil {
return ""
}
return strings.TrimSpace(err.Error())
}
func driverInstallDir(downloadDir string, driverType string) string {
root, err := resolveDriverDownloadDirectory(downloadDir)
if err != nil {
root = defaultDriverDownloadDirectory()
}
return filepath.Join(root, normalizeDriverType(driverType))
}
func installedDriverMetaPath(downloadDir string, driverType string) string {
return filepath.Join(driverInstallDir(downloadDir, driverType), "installed.json")
}
func readInstalledDriverPackage(downloadDir string, driverType string) (installedDriverPackage, bool) {
metaPath := installedDriverMetaPath(downloadDir, driverType)
content, err := os.ReadFile(metaPath)
if err != nil {
return installedDriverPackage{}, false
}
var meta installedDriverPackage
if err := json.Unmarshal(content, &meta); err != nil {
return installedDriverPackage{}, false
}
meta.DriverType = normalizeDriverType(meta.DriverType)
if strings.TrimSpace(meta.DriverType) == "" {
meta.DriverType = normalizeDriverType(driverType)
}
return meta, true
}
func optionalDriverAgentRevisionStatus(driverType string, pkg installedDriverPackage, packageMetaExists bool) (bool, string, string) {
expected := db.OptionalDriverAgentRevision(driverType)
if strings.TrimSpace(expected) == "" || !packageMetaExists || !db.IsOptionalGoDriver(driverType) {
return false, "", expected
}
actual := strings.TrimSpace(pkg.AgentRevision)
if actual == expected {
return false, "", expected
}
displayName := resolveDriverDisplayName(driverDefinition{Type: driverType})
updateReason := fmt.Sprintf("当前 GoNavi 版本要求更新后的 %s driver-agentrevision: %s", displayName, expected)
impact := "driver-agent 是独立二进制,不会随主程序自动更新;如果不重装,会继续使用旧 agent 逻辑,驱动侧已修复或优化的行为不会生效,可能继续出现旧版本问题。强烈建议重装对应驱动代理"
if actual == "" {
return true, fmt.Sprintf("原因:%s。影响%s", updateReason, impact), expected
}
return true, fmt.Sprintf("原因:%s。影响%s已安装标记%s当前需要%s", updateReason, impact, actual, expected), expected
}
func (a *App) savedConnectionDriverUsageCounts() map[string]int {
counts := map[string]int{}
if a == nil || strings.TrimSpace(a.configDir) == "" {
return counts
}
items, err := a.savedConnectionRepository().List()
if err != nil {
logger.Warnf("统计驱动连接使用数失败:%v", err)
return counts
}
for _, item := range items {
driverType := normalizeDriverType(item.Config.Type)
if driverType == "custom" {
driverType = normalizeDriverType(item.Config.Driver)
}
if driverType == "" || !db.IsOptionalGoDriver(driverType) {
continue
}
counts[driverType]++
}
return counts
}
func writeInstalledDriverPackage(downloadDir string, driverType string, meta installedDriverPackage) error {
driverDir := driverInstallDir(downloadDir, driverType)
if err := os.MkdirAll(driverDir, 0o755); err != nil {
return fmt.Errorf("创建驱动目录失败:%w", err)
}
meta.DriverType = normalizeDriverType(driverType)
if meta.DownloadedAt == "" {
meta.DownloadedAt = time.Now().Format(time.RFC3339)
}
payload, err := json.MarshalIndent(meta, "", " ")
if err != nil {
return fmt.Errorf("写入驱动元数据失败:%w", err)
}
if err := os.WriteFile(installedDriverMetaPath(downloadDir, driverType), payload, 0o644); err != nil {
return fmt.Errorf("写入驱动元数据失败:%w", err)
}
return nil
}
func hashFileSHA256(filePath string) (string, error) {
pathText := strings.TrimSpace(filePath)
if pathText == "" {
return "", fmt.Errorf("文件路径为空")
}
file, err := os.Open(pathText)
if err != nil {
return "", err
}
defer file.Close()
hasher := sha256.New()
if _, err := io.Copy(hasher, file); err != nil {
return "", err
}
return hex.EncodeToString(hasher.Sum(nil)), nil
}
func installOptionalDriverAgentPackage(a *App, definition driverDefinition, selectedVersion string, resolvedDir string, downloadURL string) (installedDriverPackage, error) {
driverType := normalizeDriverType(definition.Type)
installPath, err := db.ResolveOptionalDriverAgentExecutablePathForVersion(resolvedDir, driverType, selectedVersion)
if err != nil {
return installedDriverPackage{}, err
}
runtimePath, err := db.ResolveOptionalDriverAgentExecutablePath(resolvedDir, driverType)
if err != nil {
return installedDriverPackage{}, err
}
downloadSource, hash, err := ensureOptionalDriverAgentBinary(a, definition, installPath, downloadURL, selectedVersion)
if err != nil {
return installedDriverPackage{}, err
}
if activateErr := activateOptionalDriverAgentBinary(installPath, runtimePath); activateErr != nil {
return installedDriverPackage{}, fmt.Errorf("activate %s driver agent failed: %w", resolveDriverDisplayName(definition), activateErr)
}
if strings.TrimSpace(hash) == "" {
hash, err = hashFileSHA256(installPath)
if err != nil {
return installedDriverPackage{}, fmt.Errorf("计算 %s 驱动代理摘要失败:%w", resolveDriverDisplayName(definition), err)
}
}
if strings.TrimSpace(downloadSource) == "" {
downloadSource = strings.TrimSpace(downloadURL)
}
agentRevision := probeInstalledOptionalDriverAgentRevision(driverType, runtimePath)
return installedDriverPackage{
DriverType: driverType,
Version: strings.TrimSpace(selectedVersion),
AgentRevision: agentRevision,
FilePath: installPath,
FileName: filepath.Base(installPath),
ExecutablePath: runtimePath,
DownloadURL: strings.TrimSpace(downloadSource),
SHA256: hash,
DownloadedAt: time.Now().Format(time.RFC3339),
}, nil
}
func installOptionalDriverAgentFromLocalPath(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)
}
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)
}
sourcePath := pathText
sourceName := filepath.Base(pathText)
downloadSource := fmt.Sprintf("local://manual/%s", filepath.Base(pathText))
if info.IsDir() {
matchedPath, matchedEntry, resolveErr := resolveLocalDriverAgentFromLocalDirectory(pathText, driverType, selectedVersion)
if resolveErr != nil {
return installedDriverPackage{}, resolveErr
}
sourcePath = matchedPath
sourceName = filepath.Base(matchedPath)
downloadSource = fmt.Sprintf("local://manual-dir/%s", filepath.Base(pathText))
if strings.TrimSpace(matchedEntry) != "" {
downloadSource = downloadSource + "#" + matchedEntry
}
}
if !info.IsDir() && strings.EqualFold(filepath.Ext(pathText), ".zip") {
entryName, extractErr := installOptionalDriverAgentFromLocalZip(pathText, definition, executablePath, selectedVersion)
if extractErr != nil {
return installedDriverPackage{}, extractErr
}
if strings.TrimSpace(entryName) != "" {
downloadSource = downloadSource + "#" + entryName
}
} else {
if copyErr := copyAgentBinary(sourcePath, executablePath); copyErr != nil {
return installedDriverPackage{}, fmt.Errorf("导入本地驱动代理失败:%w", copyErr)
}
}
if validateErr := db.ValidateOptionalDriverAgentExecutable(driverType, executablePath); validateErr != nil {
return installedDriverPackage{}, validateErr
}
hash, hashErr := hashFileSHA256(executablePath)
if hashErr != nil {
return installedDriverPackage{}, fmt.Errorf("计算 %s 驱动代理摘要失败:%w", displayName, hashErr)
}
agentRevision := probeInstalledOptionalDriverAgentRevision(driverType, executablePath)
return installedDriverPackage{
DriverType: driverType,
Version: strings.TrimSpace(selectedVersion),
AgentRevision: agentRevision,
FilePath: sourcePath,
FileName: sourceName,
ExecutablePath: executablePath,
DownloadURL: downloadSource,
SHA256: hash,
DownloadedAt: time.Now().Format(time.RFC3339),
}, nil
}
func probeInstalledOptionalDriverAgentRevision(driverType string, executablePath string) string {
expectedRevision := db.OptionalDriverAgentRevision(driverType)
if strings.TrimSpace(expectedRevision) == "" {
return ""
}
metadata, err := db.ProbeOptionalDriverAgentMetadata(driverType, executablePath)
if err != nil {
logger.Warnf("%s 驱动代理未返回版本元数据:%v", resolveDriverDisplayName(driverDefinition{Type: driverType}), err)
return ""
}
return strings.TrimSpace(metadata.AgentRevision)
}
type localDriverCandidate struct {
absPath string
relativePath string
depth int
inPlatformDir bool
}
func resolveLocalDriverAgentFromLocalDirectory(directoryPath string, driverType string, selectedVersion string) (string, string, error) {
root := strings.TrimSpace(directoryPath)
if root == "" {
return "", "", fmt.Errorf("本地驱动目录路径为空")
}
if absPath, absErr := filepath.Abs(root); absErr == nil {
root = absPath
}
info, statErr := os.Stat(root)
if statErr != nil {
return "", "", fmt.Errorf("读取本地驱动目录失败:%w", statErr)
}
if !info.IsDir() {
return "", "", fmt.Errorf("本地驱动目录路径不是目录:%s", root)
}
normalizedType := normalizeDriverType(driverType)
displayDefinition, found := resolveDriverDefinition(normalizedType)
if !found {
displayDefinition = driverDefinition{Type: normalizedType, Name: normalizedType}
}
displayName := resolveDriverDisplayName(displayDefinition)
platformDir := optionalDriverBundlePlatformDir(stdRuntime.GOOS)
assetNameCandidates := optionalDriverReleaseAssetNamesForVersion(normalizedType, selectedVersion)
baseNameCandidates := optionalDriverExecutableBaseNamesForVersion(normalizedType, selectedVersion)
assetName := optionalDriverReleaseAssetNameForVersion(normalizedType, selectedVersion)
exactRelativePath := filepath.ToSlash(filepath.Join(platformDir, assetName))
for _, candidateName := range assetNameCandidates {
exactPath := filepath.Join(root, platformDir, candidateName)
if exactInfo, err := os.Stat(exactPath); err == nil && !exactInfo.IsDir() {
return exactPath, filepath.ToSlash(filepath.Join(platformDir, candidateName)), nil
}
}
for _, candidateName := range assetNameCandidates {
rootAssetPath := filepath.Join(root, candidateName)
if rootAssetInfo, err := os.Stat(rootAssetPath); err == nil && !rootAssetInfo.IsDir() {
return rootAssetPath, filepath.ToSlash(candidateName), nil
}
}
assetCandidates := make([]localDriverCandidate, 0, 8)
baseCandidates := make([]localDriverCandidate, 0, 8)
visited := 0
walkErr := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return nil
}
visited++
if visited > localDriverDirectoryScanMaxEntries {
return errLocalDriverDirScanLimit
}
if d.IsDir() {
return nil
}
name := strings.TrimSpace(d.Name())
if name == "" {
return nil
}
relative, relErr := filepath.Rel(root, path)
if relErr != nil {
relative = name
}
normalizedRelative := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(relative), "./"))
if normalizedRelative == "" {
normalizedRelative = name
}
normalizedLower := strings.ToLower(normalizedRelative)
platformPrefix := strings.ToLower(platformDir) + "/"
inPlatformDir := normalizedLower == strings.ToLower(platformDir) || strings.HasPrefix(normalizedLower, platformPrefix)
depth := strings.Count(normalizedRelative, "/")
candidate := localDriverCandidate{
absPath: path,
relativePath: normalizedRelative,
depth: depth,
inPlatformDir: inPlatformDir,
}
for _, candidateName := range assetNameCandidates {
if strings.EqualFold(name, candidateName) {
assetCandidates = append(assetCandidates, candidate)
return nil
}
}
for _, candidateName := range baseNameCandidates {
if strings.EqualFold(name, candidateName) {
baseCandidates = append(baseCandidates, candidate)
return nil
}
}
return nil
})
if errors.Is(walkErr, errLocalDriverDirScanLimit) {
return "", "", fmt.Errorf("本地驱动目录条目过多(超过 %d请缩小目录范围或直接选择 zip/单文件", localDriverDirectoryScanMaxEntries)
}
if walkErr != nil {
return "", "", fmt.Errorf("扫描本地驱动目录失败:%w", walkErr)
}
selectBest := func(candidates []localDriverCandidate) (localDriverCandidate, bool) {
if len(candidates) == 0 {
return localDriverCandidate{}, false
}
sort.Slice(candidates, func(i, j int) bool {
left := candidates[i]
right := candidates[j]
if left.inPlatformDir != right.inPlatformDir {
return left.inPlatformDir
}
if left.depth != right.depth {
return left.depth < right.depth
}
leftRelative := strings.ToLower(left.relativePath)
rightRelative := strings.ToLower(right.relativePath)
if leftRelative != rightRelative {
return leftRelative < rightRelative
}
return strings.ToLower(left.absPath) < strings.ToLower(right.absPath)
})
return candidates[0], true
}
if candidate, ok := selectBest(assetCandidates); ok {
return candidate.absPath, candidate.relativePath, nil
}
if candidate, ok := selectBest(baseCandidates); ok {
return candidate.absPath, candidate.relativePath, nil
}
return "", "", fmt.Errorf(
"目录中未找到 %s 代理文件(优先路径 %s候选文件名 %s / %s",
displayName,
exactRelativePath,
strings.Join(assetNameCandidates, " | "),
strings.Join(baseNameCandidates, " | "),
)
}
func installOptionalDriverAgentFromLocalZip(zipPath string, definition driverDefinition, executablePath string, selectedVersion 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 := optionalDriverBundleEntryPathForVersion(driverType, selectedVersion)
entryPaths := optionalDriverBundleEntryPathsForVersion(driverType, selectedVersion)
expectedBaseNames := optionalDriverReleaseAssetNamesForVersion(driverType, selectedVersion)
findEntry := func() *zip.File {
for _, file := range reader.File {
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
for _, expectedPath := range entryPaths {
if name == expectedPath {
return file
}
}
}
for _, file := range reader.File {
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
for _, expectedPath := range entryPaths {
if strings.EqualFold(name, expectedPath) {
return file
}
}
}
for _, file := range reader.File {
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
for _, expectedName := range expectedBaseNames {
if strings.EqualFold(filepath.Base(name), expectedName) {
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 buildOptionalDriverInstallPlanMessage(displayName string, selectedVersion string, forceSourceBuild bool, preferSourceBuildBeforeDownload bool, restrictToExplicitArtifact bool, directURLCount int, bundleURLCount int) string {
name := strings.TrimSpace(displayName)
if name == "" {
name = "驱动"
}
versionText := normalizeVersion(strings.TrimSpace(selectedVersion))
if versionText == "" {
versionText = "未标注版本"
}
if forceSourceBuild {
return fmt.Sprintf("准备安装 %s 驱动代理(版本 %s当前版本仅允许本地源码构建", name, versionText)
}
if preferSourceBuildBeforeDownload {
return fmt.Sprintf("准备安装 %s 驱动代理(版本 %s先尝试本地源码构建失败后继续下载兜底", name, versionText)
}
if directURLCount > 0 && !restrictToExplicitArtifact && bundleURLCount > 0 {
return fmt.Sprintf("准备安装 %s 驱动代理(版本 %s先尝试 %d 个预编译直链,失败后转入 %d 个驱动总包源", name, versionText, directURLCount, bundleURLCount)
}
if directURLCount > 0 && restrictToExplicitArtifact {
return fmt.Sprintf("准备安装 %s 驱动代理(版本 %s仅允许显式版本资产先尝试 %d 个预编译直链", name, versionText, directURLCount)
}
if directURLCount > 0 {
return fmt.Sprintf("准备安装 %s 驱动代理(版本 %s先尝试 %d 个预编译直链", name, versionText, directURLCount)
}
if !restrictToExplicitArtifact && bundleURLCount > 0 {
return fmt.Sprintf("准备安装 %s 驱动代理(版本 %s未提供预编译直链直接尝试 %d 个驱动总包源", name, versionText, bundleURLCount)
}
return fmt.Sprintf("准备安装 %s 驱动代理(版本 %s未命中发布资产时将回退到本地源码构建", name, versionText)
}
func buildOptionalDriverFallbackProgressMessage(displayName string, directURLCount int, bundleURLCount int, restrictToExplicitArtifact bool) string {
name := strings.TrimSpace(displayName)
if name == "" {
name = "驱动"
}
if directURLCount > 0 && !restrictToExplicitArtifact && bundleURLCount > 0 {
return fmt.Sprintf("预编译直链未命中,转入驱动总包兜底(%s剩余 %d 个总包源)", name, bundleURLCount)
}
if directURLCount > 0 && restrictToExplicitArtifact {
return fmt.Sprintf("预编译直链未命中;当前版本仅允许显式资产,跳过驱动总包(%s", name)
}
if !restrictToExplicitArtifact && bundleURLCount > 0 {
return fmt.Sprintf("直链不可用,转入驱动总包兜底(%s剩余 %d 个总包源)", name, bundleURLCount)
}
return fmt.Sprintf("发布资产未命中,准备回退到本地源码构建(%s", name)
}
func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, executablePath string, downloadURL string, selectedVersion string) (string, string, error) {
driverType := normalizeDriverType(definition.Type)
displayName := resolveDriverDisplayName(definition)
forceSourceBuild := shouldForceSourceBuildForResolvedDownload(driverType, selectedVersion, downloadURL)
preferSourceBuildBeforeDownload := shouldPreferSourceBuildBeforeDownload(driverType, selectedVersion)
skipReuseCandidate := shouldSkipReusableAgentCandidate(driverType, selectedVersion)
restrictToExplicitArtifact := shouldRestrictToExplicitVersionArtifact(definition, selectedVersion)
downloadURLs := []string{}
bundleURLs := []string{}
if !forceSourceBuild {
downloadURLs = resolveOptionalDriverAgentDownloadURLs(definition, downloadURL, selectedVersion)
if !restrictToExplicitArtifact {
bundleURLs = resolveOptionalDriverBundleDownloadURLs()
}
}
planMessage := buildOptionalDriverInstallPlanMessage(displayName, selectedVersion, forceSourceBuild, preferSourceBuildBeforeDownload, restrictToExplicitArtifact, len(downloadURLs), len(bundleURLs))
logger.Infof("%sdriver=%s version=%s direct_candidates=%d bundle_candidates=%d force_source_build=%v restrict_explicit=%v prefer_source_first=%v", planMessage, driverType, normalizeVersion(selectedVersion), len(downloadURLs), len(bundleURLs), forceSourceBuild, restrictToExplicitArtifact, preferSourceBuildBeforeDownload)
info, err := os.Stat(executablePath)
if err == nil && !info.IsDir() {
if validateErr := db.ValidateOptionalDriverAgentExecutable(driverType, executablePath); validateErr != nil {
_ = os.Remove(executablePath)
} else {
// 用户点击“安装/重装”时应强制刷新驱动代理,避免沿用旧二进制导致修复不生效。
if removeErr := os.Remove(executablePath); removeErr != nil {
return "", "", fmt.Errorf("清理已安装 %s 驱动代理失败:%w", displayName, removeErr)
}
}
}
if err == nil && info.IsDir() {
return "", "", fmt.Errorf("%s 驱动代理路径被目录占用:%s", displayName, executablePath)
}
if mkErr := os.MkdirAll(filepath.Dir(executablePath), 0o755); mkErr != nil {
return "", "", fmt.Errorf("创建 %s 驱动目录失败:%w", displayName, mkErr)
}
if a != nil {
a.emitDriverDownloadProgress(driverType, "downloading", 10, 100, planMessage)
}
if !skipReuseCandidate {
if sourcePath, ok := findExistingOptionalDriverAgentCandidate(definition, executablePath); ok {
if copyErr := copyAgentBinary(sourcePath, executablePath); copyErr != nil {
return "", "", fmt.Errorf("复制预置 %s 驱动代理失败:%w", displayName, copyErr)
}
if validateErr := db.ValidateOptionalDriverAgentExecutable(driverType, executablePath); validateErr != nil {
_ = os.Remove(executablePath)
return "", "", validateErr
}
hash, hashErr := hashFileSHA256(executablePath)
if hashErr != nil {
return "", "", fmt.Errorf("计算预置 %s 驱动代理摘要失败:%w", displayName, hashErr)
}
return "file://" + sourcePath, hash, nil
}
}
var downloadErrs []string
var sourceBuildAttempted bool
var sourceBuildErr error
if !forceSourceBuild && preferSourceBuildBeforeDownload {
sourceBuildAttempted = true
if a != nil {
a.emitDriverDownloadProgress(driverType, "downloading", 16, 100, fmt.Sprintf("优先使用本地源码构建 %s 驱动代理", displayName))
}
hash, buildErr := buildOptionalDriverAgentFromSource(definition, executablePath, selectedVersion)
if buildErr == nil {
return fmt.Sprintf("local://go-build/%s-driver-agent", driverType), hash, nil
}
sourceBuildErr = buildErr
logger.Warnf("预先本地构建 %s 驱动代理失败,将继续尝试下载预编译包:%v", displayName, buildErr)
}
if !forceSourceBuild {
if len(downloadURLs) > 0 {
for _, candidateURL := range downloadURLs {
if a != nil {
a.emitDriverDownloadProgress(driverType, "downloading", 20, 100, fmt.Sprintf("下载预编译 %s 驱动代理", displayName))
}
hash, dlErr := downloadOptionalDriverAgentBinary(a, definition, candidateURL, executablePath)
if dlErr == nil {
return candidateURL, hash, nil
}
logger.Warnf("下载预编译 %s 驱动代理失败url=%s err=%v", displayName, candidateURL, dlErr)
downloadErrs = append(downloadErrs, fmt.Sprintf("%s: %s", candidateURL, strings.TrimSpace(dlErr.Error())))
}
}
if len(bundleURLs) > 0 {
fallbackMessage := buildOptionalDriverFallbackProgressMessage(displayName, len(downloadURLs), len(bundleURLs), restrictToExplicitArtifact)
logger.Infof("%sdriver=%s version=%s", fallbackMessage, driverType, normalizeVersion(selectedVersion))
if a != nil {
a.emitDriverDownloadProgress(driverType, "downloading", 20, 100, fallbackMessage)
}
for _, bundleURL := range bundleURLs {
if a != nil {
a.emitDriverDownloadProgress(driverType, "downloading", 20, 100, fmt.Sprintf("从驱动总包提取 %s 代理", displayName))
}
source, hash, bundleErr := downloadOptionalDriverAgentFromBundle(a, definition, bundleURL, executablePath)
if bundleErr == nil {
return source, hash, nil
}
logger.Warnf("从驱动总包提取 %s 驱动代理失败url=%s err=%v", displayName, bundleURL, bundleErr)
downloadErrs = append(downloadErrs, fmt.Sprintf("%s: %s", bundleURL, strings.TrimSpace(bundleErr.Error())))
}
} else if len(downloadURLs) > 0 || restrictToExplicitArtifact {
fallbackMessage := buildOptionalDriverFallbackProgressMessage(displayName, len(downloadURLs), 0, restrictToExplicitArtifact)
logger.Infof("%sdriver=%s version=%s", fallbackMessage, driverType, normalizeVersion(selectedVersion))
if a != nil {
a.emitDriverDownloadProgress(driverType, "downloading", 20, 100, fallbackMessage)
}
}
}
if a != nil {
a.emitDriverDownloadProgress(driverType, "downloading", 92, 100, "未命中预编译包,尝试开发态本地构建")
}
var buildErr error
if sourceBuildAttempted {
buildErr = sourceBuildErr
} else {
hash, runErr := buildOptionalDriverAgentFromSource(definition, executablePath, selectedVersion)
buildErr = runErr
if buildErr == nil {
return fmt.Sprintf("local://go-build/%s-driver-agent", driverType), hash, nil
}
}
var parts []string
if len(downloadErrs) > 0 {
parts = append(parts, "预编译包下载失败:"+strings.Join(downloadErrs, ""))
}
parts = append(parts, "本地构建失败:"+strings.TrimSpace(buildErr.Error()))
return "", "", errors.New(strings.Join(parts, ""))
}
func downloadOptionalDriverAgentBinary(a *App, definition driverDefinition, urlText string, executablePath string) (string, error) {
driverType := normalizeDriverType(definition.Type)
displayName := resolveDriverDisplayName(definition)
trimmedURL := strings.TrimSpace(urlText)
if trimmedURL == "" {
return "", fmt.Errorf("下载地址为空")
}
tempPath := executablePath + ".tmp"
_ = os.Remove(tempPath)
hash, err := downloadFileWithHash(trimmedURL, tempPath, func(downloaded, total int64) {
if a == nil {
return
}
scaledDownloaded, scaledTotal := scaleProgress(downloaded, total, 20, 90)
a.emitDriverDownloadProgress(driverType, "downloading", scaledDownloaded, scaledTotal, fmt.Sprintf("下载预编译 %s 驱动代理", displayName))
})
if 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 renameErr := os.Rename(tempPath, executablePath); renameErr != nil {
_ = os.Remove(tempPath)
return "", fmt.Errorf("落地代理文件失败:%w", renameErr)
}
if chmodErr := os.Chmod(executablePath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
return "", fmt.Errorf("设置代理权限失败:%w", chmodErr)
}
if validateErr := db.ValidateOptionalDriverAgentExecutable(driverType, executablePath); validateErr != nil {
_ = os.Remove(executablePath)
return "", validateErr
}
return hash, nil
}
func downloadOptionalDriverAgentFromBundle(a *App, definition driverDefinition, bundleURL, executablePath string) (string, string, error) {
driverType := normalizeDriverType(definition.Type)
displayName := resolveDriverDisplayName(definition)
trimmedURL := strings.TrimSpace(bundleURL)
if trimmedURL == "" {
return "", "", fmt.Errorf("驱动总包下载地址为空")
}
bundleTempPath := executablePath + ".bundle.zip.tmp"
_ = os.Remove(bundleTempPath)
_, err := downloadFileWithHash(trimmedURL, bundleTempPath, func(downloaded, total int64) {
if a == nil {
return
}
scaledDownloaded, scaledTotal := scaleProgress(downloaded, total, 20, 78)
a.emitDriverDownloadProgress(driverType, "downloading", scaledDownloaded, scaledTotal, fmt.Sprintf("下载 %s 驱动总包", displayName))
})
if err != nil {
_ = os.Remove(bundleTempPath)
return "", "", fmt.Errorf("下载驱动总包失败:%w", err)
}
defer func() { _ = os.Remove(bundleTempPath) }()
reader, err := zip.OpenReader(bundleTempPath)
if err != nil {
return "", "", fmt.Errorf("打开驱动总包失败:%w", err)
}
defer reader.Close()
entryPath := optionalDriverBundleEntryPath(driverType)
entryPaths := optionalDriverBundleEntryPaths(driverType)
expectedBaseNames := optionalDriverReleaseAssetNames(driverType)
findEntry := func() *zip.File {
for _, file := range reader.File {
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
for _, expectedPath := range entryPaths {
if name == expectedPath {
return file
}
}
}
for _, file := range reader.File {
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
for _, expectedPath := range entryPaths {
if strings.EqualFold(name, expectedPath) {
return file
}
}
}
for _, file := range reader.File {
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
for _, expectedName := range expectedBaseNames {
if strings.EqualFold(filepath.Base(name), expectedName) {
return file
}
}
}
return nil
}
entry := findEntry()
if entry == nil {
return "", "", fmt.Errorf("驱动总包内未找到 %s期望路径 %s", displayName, entryPath)
}
if a != nil {
a.emitDriverDownloadProgress(driverType, "downloading", 84, 100, fmt.Sprintf("解压 %s 驱动代理", displayName))
}
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)
}
if validateErr := db.ValidateOptionalDriverAgentExecutable(driverType, executablePath); validateErr != nil {
_ = os.Remove(executablePath)
return "", "", validateErr
}
hash, err := hashFileSHA256(executablePath)
if err != nil {
return "", "", fmt.Errorf("计算驱动代理摘要失败:%w", err)
}
source := fmt.Sprintf("%s#%s", trimmedURL, filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(entry.Name), "./")))
return source, hash, nil
}
func buildOptionalDriverAgentFromSource(definition driverDefinition, executablePath string, selectedVersion string) (string, error) {
driverType := normalizeDriverType(definition.Type)
displayName := resolveDriverDisplayName(definition)
goPath, lookErr := resolveGoBinaryPath()
if lookErr != nil {
return "", fmt.Errorf("当前环境未安装 Go且未找到可用的 %s 预编译代理包", displayName)
}
tagName, tagErr := optionalDriverBuildTag(driverType, selectedVersion)
if tagErr != nil {
return "", tagErr
}
projectRoot, rootErr := locateProjectRootForAgentBuild()
if rootErr != nil {
return "", rootErr
}
cmd := exec.Command(goPath, "build", "-tags", tagName, "-trimpath", "-ldflags", "-s -w", "-o", executablePath, "./cmd/optional-driver-agent")
cmd.Dir = projectRoot
cmd.Env = append(os.Environ(), "GOTOOLCHAIN=auto")
output, buildErr := cmd.CombinedOutput()
if buildErr != nil {
return "", fmt.Errorf("构建 %s 驱动代理失败:%v输出%s", displayName, buildErr, strings.TrimSpace(string(output)))
}
if chmodErr := os.Chmod(executablePath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
return "", fmt.Errorf("设置 %s 驱动代理权限失败:%w", displayName, chmodErr)
}
hash, hashErr := hashFileSHA256(executablePath)
if hashErr != nil {
return "", fmt.Errorf("计算 %s 驱动代理摘要失败:%w", displayName, hashErr)
}
return hash, nil
}
func resolveMongoDriverMajorFromVersion(version string) int {
trimmed := strings.TrimSpace(version)
trimmed = strings.TrimPrefix(trimmed, "v")
if strings.HasPrefix(trimmed, "1.") || trimmed == "1" {
return 1
}
return 2
}
func shouldForceSourceBuildForVersion(driverType string, selectedVersion string) bool {
if normalizeDriverType(driverType) != "mongodb" {
return false
}
return resolveMongoDriverMajorFromVersion(selectedVersion) == 1
}
func shouldForceSourceBuildForResolvedDownload(driverType string, selectedVersion string, downloadURL string) bool {
if !shouldForceSourceBuildForVersion(driverType, selectedVersion) {
return false
}
parsed, err := url.Parse(strings.TrimSpace(downloadURL))
if err != nil || parsed == nil {
return true
}
switch strings.ToLower(strings.TrimSpace(parsed.Scheme)) {
case "http", "https":
return false
default:
return true
}
}
func shouldPreferSourceBuildBeforeDownload(driverType string, selectedVersion string) bool {
_ = selectedVersion
switch normalizeDriverType(driverType) {
default:
return false
}
}
func shouldSkipReusableAgentCandidate(driverType string, selectedVersion string) bool {
_ = selectedVersion
switch normalizeDriverType(driverType) {
case "mongodb", "kingbase":
return true
default:
return false
}
}
func optionalDriverBuildTag(driverType string, selectedVersion string) (string, error) {
switch normalizeDriverType(driverType) {
case "mysql":
return "gonavi_mysql_driver", nil
case "mariadb":
return "gonavi_mariadb_driver", nil
case "oceanbase":
return "gonavi_oceanbase_driver", nil
case "diros":
return "gonavi_diros_driver", nil
case "sphinx":
return "gonavi_sphinx_driver", nil
case "sqlserver":
return "gonavi_sqlserver_driver", nil
case "sqlite":
return "gonavi_sqlite_driver", nil
case "duckdb":
return "gonavi_duckdb_driver", nil
case "dameng":
return "gonavi_dameng_driver", nil
case "kingbase":
return "gonavi_kingbase_driver", nil
case "highgo":
return "gonavi_highgo_driver", nil
case "vastbase":
return "gonavi_vastbase_driver", nil
case "opengauss":
return "gonavi_opengauss_driver", nil
case "mongodb":
if resolveMongoDriverMajorFromVersion(selectedVersion) == 1 {
return "gonavi_mongodb_driver_v1", nil
}
return "gonavi_mongodb_driver", nil
case "tdengine":
return "gonavi_tdengine_driver", nil
case "clickhouse":
return "gonavi_clickhouse_driver", nil
default:
return "", fmt.Errorf("未配置驱动构建标签:%s", driverType)
}
}
func locateProjectRootForAgentBuild() (string, error) {
wd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("获取当前目录失败:%w", err)
}
dir := wd
for {
if fileExists(filepath.Join(dir, "go.mod")) && fileExists(filepath.Join(dir, "cmd", "optional-driver-agent", "main.go")) {
return dir, nil
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return "", fmt.Errorf("未找到通用驱动代理源码,无法自动构建;请使用已发布版本")
}
func fileExists(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}
func optionalDriverPublicTypeName(driverType string) string {
switch normalizeDriverType(driverType) {
case "diros":
return "doris"
default:
return normalizeDriverType(driverType)
}
}
func optionalDriverExecutableBaseNameForType(typeName string) string {
base := strings.TrimSpace(typeName)
if base == "" {
base = "unknown"
}
name := fmt.Sprintf("%s-driver-agent", base)
if stdRuntime.GOOS == "windows" {
return name + ".exe"
}
return name
}
func optionalDriverReleaseAssetNameForType(typeName string, goos string, goarch string) string {
base := strings.TrimSpace(typeName)
if base == "" {
base = "unknown"
}
name := fmt.Sprintf("%s-driver-agent-%s-%s", base, goos, goarch)
if strings.EqualFold(goos, "windows") {
return name + ".exe"
}
return name
}
func optionalDriverNameStemCandidates(driverType string, selectedVersion string) []string {
candidates := make([]string, 0, 3)
seen := make(map[string]struct{}, 3)
appendStem := func(stem string) {
trimmed := strings.TrimSpace(stem)
if trimmed == "" {
return
}
if _, ok := seen[trimmed]; ok {
return
}
seen[trimmed] = struct{}{}
candidates = append(candidates, trimmed)
}
base := fmt.Sprintf("%s-driver-agent", optionalDriverPublicTypeName(driverType))
if normalizeDriverType(driverType) == "mongodb" {
switch resolveMongoDriverMajorFromVersion(selectedVersion) {
case 1:
appendStem(base + "-v1")
appendStem(base)
case 2:
appendStem(base)
appendStem(base + "-v2")
default:
appendStem(base)
}
return candidates
}
appendStem(base)
return candidates
}
func optionalDriverExecutableBaseNamesForVersion(driverType string, selectedVersion string) []string {
names := make([]string, 0, 2)
seen := make(map[string]struct{}, 2)
appendName := func(stem string) {
name := strings.TrimSpace(stem)
if strings.TrimSpace(name) == "" {
return
}
if stdRuntime.GOOS == "windows" {
name += ".exe"
}
if _, ok := seen[name]; ok {
return
}
seen[name] = struct{}{}
names = append(names, name)
}
for _, stem := range optionalDriverNameStemCandidates(driverType, selectedVersion) {
appendName(stem)
}
return names
}
func optionalDriverExecutableBaseNames(driverType string) []string {
return optionalDriverExecutableBaseNamesForVersion(driverType, "")
}
func optionalDriverReleaseAssetNamesForVersion(driverType string, selectedVersion string) []string {
names := make([]string, 0, 2)
seen := make(map[string]struct{}, 2)
appendName := func(stem string) {
trimmedStem := strings.TrimSpace(stem)
if trimmedStem == "" {
return
}
name := fmt.Sprintf("%s-%s-%s", trimmedStem, stdRuntime.GOOS, stdRuntime.GOARCH)
if strings.EqualFold(stdRuntime.GOOS, "windows") {
name += ".exe"
}
if strings.TrimSpace(name) == "" {
return
}
if _, ok := seen[name]; ok {
return
}
seen[name] = struct{}{}
names = append(names, name)
}
for _, stem := range optionalDriverNameStemCandidates(driverType, selectedVersion) {
appendName(stem)
}
return names
}
func optionalDriverReleaseAssetNames(driverType string) []string {
return optionalDriverReleaseAssetNamesForVersion(driverType, "")
}
func optionalDriverExecutableBaseName(driverType string) string {
names := optionalDriverExecutableBaseNames(driverType)
if len(names) == 0 {
return optionalDriverExecutableBaseNameForType("")
}
return names[0]
}
func optionalDriverReleaseAssetName(driverType string) string {
names := optionalDriverReleaseAssetNames(driverType)
if len(names) == 0 {
return optionalDriverReleaseAssetNameForType("", stdRuntime.GOOS, stdRuntime.GOARCH)
}
return names[0]
}
func optionalDriverReleaseAssetNameForVersion(driverType string, selectedVersion string) string {
names := optionalDriverReleaseAssetNamesForVersion(driverType, selectedVersion)
if len(names) == 0 {
return optionalDriverReleaseAssetNameForType("", stdRuntime.GOOS, stdRuntime.GOARCH)
}
return names[0]
}
func optionalDriverBundlePlatformDir(goos string) string {
switch strings.ToLower(strings.TrimSpace(goos)) {
case "windows":
return "Windows"
case "darwin":
return "MacOS"
case "linux":
return "Linux"
default:
return "Unknown"
}
}
func optionalDriverBundleEntryPathsForVersion(driverType string, selectedVersion string) []string {
platformDir := optionalDriverBundlePlatformDir(stdRuntime.GOOS)
assetNames := optionalDriverReleaseAssetNamesForVersion(driverType, selectedVersion)
result := make([]string, 0, len(assetNames))
seen := make(map[string]struct{}, len(assetNames))
for _, assetName := range assetNames {
entry := filepath.ToSlash(filepath.Join(platformDir, assetName))
if _, ok := seen[entry]; ok {
continue
}
seen[entry] = struct{}{}
result = append(result, entry)
}
return result
}
func optionalDriverBundleEntryPaths(driverType string) []string {
return optionalDriverBundleEntryPathsForVersion(driverType, "")
}
func optionalDriverBundleEntryPathForVersion(driverType string, selectedVersion string) string {
paths := optionalDriverBundleEntryPathsForVersion(driverType, selectedVersion)
if len(paths) == 0 {
return filepath.ToSlash(filepath.Join(optionalDriverBundlePlatformDir(stdRuntime.GOOS), optionalDriverReleaseAssetNameForVersion(driverType, selectedVersion)))
}
return paths[0]
}
func optionalDriverBundleEntryPath(driverType string) string {
return optionalDriverBundleEntryPathForVersion(driverType, "")
}
func resolveOptionalDriverAssetSize(sizeByAsset map[string]int64, driverType string) int64 {
if len(sizeByAsset) == 0 {
return 0
}
for _, assetName := range optionalDriverReleaseAssetNames(driverType) {
sizeBytes := sizeByAsset[assetName]
if sizeBytes > 0 {
return sizeBytes
}
}
return 0
}
func resolveOptionalDriverAssetSizeForVersion(sizeByAsset map[string]int64, driverType string, version string) int64 {
if len(sizeByAsset) == 0 {
return 0
}
for _, assetName := range optionalDriverReleaseAssetNamesForVersion(driverType, version) {
sizeBytes := sizeByAsset[assetName]
if sizeBytes > 0 {
return sizeBytes
}
}
return 0
}
func resolveOptionalDriverBundleDownloadURLs() []string {
candidates := make([]string, 0, 2)
seen := make(map[string]struct{}, 2)
appendURL := func(value string) {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return
}
if _, ok := seen[trimmed]; ok {
return
}
seen[trimmed] = struct{}{}
candidates = append(candidates, trimmed)
}
currentVersion := normalizeVersion(getCurrentVersion())
if currentVersion != "" && currentVersion != "0.0.0" {
appendURL(fmt.Sprintf("https://github.com/Syngnat/GoNavi/releases/download/v%s/%s", currentVersion, optionalDriverBundleAssetName))
}
appendURL(fmt.Sprintf("https://github.com/Syngnat/GoNavi/releases/latest/download/%s", optionalDriverBundleAssetName))
return candidates
}
func resolveOptionalDriverAgentDownloadURLs(definition driverDefinition, rawURL string, selectedVersion string) []string {
candidates := make([]string, 0, 3)
seen := make(map[string]struct{}, 3)
appendURL := func(value string) {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return
}
if _, ok := seen[trimmed]; ok {
return
}
seen[trimmed] = struct{}{}
candidates = append(candidates, trimmed)
}
if parsed, err := url.Parse(strings.TrimSpace(rawURL)); err == nil {
switch strings.ToLower(strings.TrimSpace(parsed.Scheme)) {
case "http", "https":
appendURL(parsed.String())
}
}
if shouldRestrictToExplicitVersionArtifact(definition, selectedVersion) {
return candidates
}
currentVersion := normalizeVersion(getCurrentVersion())
if currentVersion != "" && currentVersion != "0.0.0" {
if publishedURL, ok := resolvePublishedDriverDownloadURL(definition, currentVersion); ok {
appendURL(publishedURL)
}
}
if publishedURL, ok := resolveLatestPublishedDriverDownloadURL(definition); ok {
appendURL(publishedURL)
}
return candidates
}
func findExistingOptionalDriverAgentCandidate(definition driverDefinition, targetPath string) (string, bool) {
driverType := normalizeDriverType(definition.Type)
targetAbs, _ := filepath.Abs(targetPath)
candidates := resolveOptionalDriverAgentCandidatePaths(definition)
for _, candidate := range candidates {
candidate = strings.TrimSpace(candidate)
if candidate == "" {
continue
}
absPath, err := filepath.Abs(candidate)
if err != nil || absPath == "" {
continue
}
if targetAbs != "" && absPath == targetAbs {
continue
}
info, statErr := os.Stat(absPath)
if statErr != nil || info.IsDir() {
continue
}
if validateErr := db.ValidateOptionalDriverAgentExecutable(driverType, absPath); validateErr != nil {
continue
}
return absPath, true
}
return "", false
}
func resolveOptionalDriverAgentCandidatePaths(definition driverDefinition) []string {
driverType := normalizeDriverType(definition.Type)
names := optionalDriverExecutableBaseNames(driverType)
assetNames := optionalDriverReleaseAssetNames(driverType)
pathTypeNames := make([]string, 0, 2)
seenPathType := make(map[string]struct{}, 2)
appendPathType := func(typeName string) {
trimmed := strings.TrimSpace(typeName)
if trimmed == "" {
return
}
if _, ok := seenPathType[trimmed]; ok {
return
}
seenPathType[trimmed] = struct{}{}
pathTypeNames = append(pathTypeNames, trimmed)
}
appendPathType(optionalDriverPublicTypeName(driverType))
candidates := make([]string, 0, 12)
appendPath := func(pathText string) {
trimmed := strings.TrimSpace(pathText)
if trimmed != "" {
candidates = append(candidates, trimmed)
}
}
if exePath, err := os.Executable(); err == nil && strings.TrimSpace(exePath) != "" {
resolved := exePath
if evalPath, evalErr := filepath.EvalSymlinks(exePath); evalErr == nil && strings.TrimSpace(evalPath) != "" {
resolved = evalPath
}
exeDir := filepath.Dir(resolved)
for _, name := range names {
appendPath(filepath.Join(exeDir, name))
}
for _, assetName := range assetNames {
appendPath(filepath.Join(exeDir, assetName))
}
for _, typeName := range pathTypeNames {
for _, name := range names {
appendPath(filepath.Join(exeDir, "drivers", typeName, name))
}
for _, assetName := range assetNames {
appendPath(filepath.Join(exeDir, "drivers", typeName, assetName))
}
}
resourcesDir := filepath.Clean(filepath.Join(exeDir, "..", "Resources"))
for _, typeName := range pathTypeNames {
for _, name := range names {
appendPath(filepath.Join(resourcesDir, "drivers", typeName, name))
}
for _, assetName := range assetNames {
appendPath(filepath.Join(resourcesDir, "drivers", typeName, assetName))
}
}
}
if wd, err := os.Getwd(); err == nil && strings.TrimSpace(wd) != "" {
for _, assetName := range assetNames {
appendPath(filepath.Join(wd, "dist", assetName))
appendPath(filepath.Join(wd, assetName))
}
}
unique := make([]string, 0, len(candidates))
seen := make(map[string]struct{}, len(candidates))
for _, item := range candidates {
if _, ok := seen[item]; ok {
continue
}
seen[item] = struct{}{}
unique = append(unique, item)
}
return unique
}
func resolveDriverDisplayName(definition driverDefinition) string {
if strings.TrimSpace(definition.Name) != "" {
return strings.TrimSpace(definition.Name)
}
if strings.TrimSpace(definition.Type) != "" {
return strings.TrimSpace(definition.Type)
}
return "未知"
}
func activateOptionalDriverAgentBinary(installPath string, runtimePath string) error {
source := strings.TrimSpace(installPath)
target := strings.TrimSpace(runtimePath)
if source == "" || target == "" {
return fmt.Errorf("agent path is empty")
}
if source == target {
return nil
}
absSource := source
absTarget := target
if value, err := filepath.Abs(source); err == nil && strings.TrimSpace(value) != "" {
absSource = value
}
if value, err := filepath.Abs(target); err == nil && strings.TrimSpace(value) != "" {
absTarget = value
}
if strings.EqualFold(absSource, absTarget) {
return nil
}
return copyAgentBinary(source, target)
}
func copyAgentBinary(sourcePath, targetPath string) error {
src, err := os.Open(sourcePath)
if err != nil {
return err
}
defer src.Close()
tempPath := targetPath + ".tmp"
_ = os.Remove(tempPath)
dst, err := os.Create(tempPath)
if err != nil {
return err
}
if _, err := io.Copy(dst, src); err != nil {
dst.Close()
_ = os.Remove(tempPath)
return err
}
if err := dst.Sync(); err != nil {
dst.Close()
_ = os.Remove(tempPath)
return err
}
if err := dst.Close(); err != nil {
_ = os.Remove(tempPath)
return err
}
if chmodErr := os.Chmod(tempPath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
_ = os.Remove(tempPath)
return chmodErr
}
if err := os.Rename(tempPath, targetPath); err != nil {
_ = os.Remove(tempPath)
return err
}
if chmodErr := os.Chmod(targetPath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
return chmodErr
}
return nil
}
func scaleProgress(downloaded, total, start, end int64) (int64, int64) {
if end <= start {
return end, 100
}
if total <= 0 {
return start, 100
}
if downloaded < 0 {
downloaded = 0
}
if downloaded > total {
downloaded = total
}
span := end - start
return start + ((downloaded * span) / total), 100
}
func preloadOptionalDriverPackageSizes(definitions []driverDefinition) map[string]int64 {
result := make(map[string]int64)
if len(definitions) == 0 {
return result
}
needed := make([]string, 0, len(definitions))
for _, definition := range definitions {
normalizedType := normalizeDriverType(definition.Type)
if normalizedType == "" || definition.BuiltIn {
continue
}
if !db.IsOptionalGoDriver(normalizedType) {
continue
}
if !db.IsOptionalGoDriverBuildIncluded(normalizedType) {
continue
}
needed = append(needed, normalizedType)
}
if len(needed) == 0 {
return result
}
currentVersion := normalizeVersion(getCurrentVersion())
tag := ""
if currentVersion != "" && currentVersion != "0.0.0" {
tag = "v" + currentVersion
}
fillFromSizes := func(sizeByAsset map[string]int64, driverTypes []string) []string {
missing := make([]string, 0, len(driverTypes))
for _, driverType := range driverTypes {
sizeBytes := resolveOptionalDriverAssetSize(sizeByAsset, driverType)
if sizeBytes > 0 {
result[driverType] = sizeBytes
continue
}
missing = append(missing, driverType)
}
return missing
}
pending := needed
if tag != "" {
if sizeByAsset, _, err := loadReleaseAssetSizesCached("tag:"+tag, func() (*githubRelease, error) {
return fetchReleaseByTag(tag)
}); err == nil {
pending = fillFromSizes(sizeByAsset, pending)
}
}
if len(pending) == 0 {
return result
}
if sizeByAsset, _, err := loadReleaseAssetSizesCached("latest", fetchLatestReleaseForDriverAssets); err == nil {
_ = fillFromSizes(sizeByAsset, pending)
}
return result
}
func loadReleaseAssetSizesCached(cacheKey string, fetch func() (*githubRelease, error)) (map[string]int64, map[string]bool, error) {
key := strings.TrimSpace(cacheKey)
if key == "" {
return nil, nil, fmt.Errorf("缓存 key 为空")
}
driverReleaseSizeMu.RLock()
cached, ok := driverReleaseSizeMap[key]
driverReleaseSizeMu.RUnlock()
if ok {
ttl := driverReleaseAssetSizeCacheTTL
if strings.TrimSpace(cached.Err) != "" {
ttl = driverReleaseAssetSizeErrorCacheTTL
}
if time.Since(cached.LoadedAt) < ttl {
if strings.TrimSpace(cached.Err) != "" {
return nil, nil, errors.New(strings.TrimSpace(cached.Err))
}
return cached.SizeByKey, cached.PublishedAssets, nil
}
}
release, err := fetch()
entry := driverReleaseAssetSizeCacheEntry{
LoadedAt: time.Now(),
SizeByKey: map[string]int64{},
PublishedAssets: map[string]bool{},
}
if err != nil {
entry.Err = err.Error()
} else {
entry.SizeByKey = buildReleaseAssetSizeMap(release)
entry.PublishedAssets = buildReleaseAssetNameMap(release)
if indexSizes, indexErr := fetchDriverBundleAssetSizeIndex(release); indexErr == nil {
for name, size := range indexSizes {
trimmedName := strings.TrimSpace(name)
if trimmedName == "" || size <= 0 {
continue
}
entry.SizeByKey[trimmedName] = size
}
}
}
driverReleaseSizeMu.Lock()
driverReleaseSizeMap[key] = entry
driverReleaseSizeMu.Unlock()
if err != nil {
return nil, nil, err
}
return entry.SizeByKey, entry.PublishedAssets, nil
}
func readReleaseAssetSizesFromCache(cacheKey string) (map[string]int64, map[string]bool, bool) {
key := strings.TrimSpace(cacheKey)
if key == "" {
return nil, nil, false
}
driverReleaseSizeMu.RLock()
cached, ok := driverReleaseSizeMap[key]
driverReleaseSizeMu.RUnlock()
if !ok {
return nil, nil, false
}
ttl := driverReleaseAssetSizeCacheTTL
if strings.TrimSpace(cached.Err) != "" {
ttl = driverReleaseAssetSizeErrorCacheTTL
}
if time.Since(cached.LoadedAt) >= ttl {
return nil, nil, false
}
if strings.TrimSpace(cached.Err) != "" {
return nil, nil, false
}
return cached.SizeByKey, cached.PublishedAssets, true
}
func buildReleaseAssetSizeMap(release *githubRelease) map[string]int64 {
sizes := make(map[string]int64)
if release == nil {
return sizes
}
for _, asset := range release.Assets {
name := strings.TrimSpace(asset.Name)
if name == "" || asset.Size <= 0 {
continue
}
sizes[name] = asset.Size
}
return sizes
}
func buildReleaseAssetNameMap(release *githubRelease) map[string]bool {
names := make(map[string]bool)
if release == nil {
return names
}
for _, asset := range release.Assets {
name := strings.TrimSpace(asset.Name)
if name == "" {
continue
}
names[name] = true
}
return names
}
func fetchDriverBundleAssetSizeIndex(release *githubRelease) (map[string]int64, error) {
if release == nil {
return nil, fmt.Errorf("release 为空")
}
indexURL := ""
for _, asset := range release.Assets {
if strings.EqualFold(strings.TrimSpace(asset.Name), optionalDriverBundleIndexAssetName) {
indexURL = strings.TrimSpace(asset.BrowserDownloadURL)
break
}
}
if indexURL == "" {
return nil, fmt.Errorf("未找到驱动总包索引资产")
}
client := newHTTPClientWithGlobalProxy(driverReleaseAssetSizeProbeTimeout)
req, err := http.NewRequest(http.MethodGet, indexURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "GoNavi-DriverManager")
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("拉取驱动总包索引失败HTTP %d", resp.StatusCode)
}
limited := io.LimitReader(resp.Body, driverBundleIndexMaxSize)
decoder := json.NewDecoder(limited)
var index driverBundleAssetIndex
if err := decoder.Decode(&index); err != nil {
return nil, fmt.Errorf("解析驱动总包索引失败:%w", err)
}
if len(index.Assets) == 0 {
return nil, fmt.Errorf("驱动总包索引为空")
}
return index.Assets, nil
}
func fetchLatestReleaseForDriverAssets() (*githubRelease, error) {
return fetchDriverReleaseByURL(updateAPIURL)
}
func resolveLatestPublishedDriverDownloadURL(definition driverDefinition) (string, bool) {
driverType := normalizeDriverType(definition.Type)
if driverType == "" {
return "", false
}
assetNames := optionalDriverReleaseAssetNames(driverType)
if len(assetNames) == 0 {
return "", false
}
if sizeByAsset, publishedAssets, ok := readReleaseAssetSizesFromCache("latest"); ok {
for _, assetName := range assetNames {
if publishedAssets[assetName] && sizeByAsset[assetName] > 0 {
return fmt.Sprintf("https://github.com/%s/releases/latest/download/%s", updateRepo, assetName), true
}
}
return "", false
}
sizeByAsset, publishedAssets, err := loadReleaseAssetSizesCached("latest", fetchLatestReleaseForDriverAssets)
if err != nil {
return "", false
}
for _, assetName := range assetNames {
if publishedAssets[assetName] && sizeByAsset[assetName] > 0 {
return fmt.Sprintf("https://github.com/%s/releases/latest/download/%s", updateRepo, assetName), true
}
}
return "", false
}
func fetchReleaseByTag(tag string) (*githubRelease, error) {
tagName := strings.TrimSpace(tag)
if tagName == "" {
return nil, fmt.Errorf("Tag 为空")
}
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/releases/tags/%s", updateRepo, url.PathEscape(tagName))
return fetchDriverReleaseByURL(apiURL)
}
func fetchDriverReleaseByURL(apiURL string) (*githubRelease, error) {
urlText := strings.TrimSpace(apiURL)
if urlText == "" {
return nil, fmt.Errorf("API 地址为空")
}
client := newHTTPClientWithGlobalProxy(driverReleaseAssetSizeProbeTimeout)
req, err := http.NewRequest(http.MethodGet, urlText, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "GoNavi-DriverManager")
req.Header.Set("Accept", "application/vnd.github+json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("拉取 Release 信息失败HTTP %d", resp.StatusCode)
}
var release githubRelease
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
return nil, err
}
return &release, nil
}
func resolveDriverPackageSizeText(definition driverDefinition, pkg installedDriverPackage, packageMetaExists bool, packageSizeBytesMap map[string]int64) string {
if definition.BuiltIn {
return "内置"
}
normalizedType := normalizeDriverType(definition.Type)
if packageMetaExists {
sizeBytes := readInstalledPackageSizeBytes(pkg)
if sizeBytes > 0 {
return formatSizeMB(sizeBytes)
}
}
if sizeBytes, ok := packageSizeBytesMap[normalizedType]; ok && sizeBytes > 0 {
return formatSizeMB(sizeBytes)
}
if !db.IsOptionalGoDriverBuildIncluded(normalizedType) {
return "待发布"
}
return "-"
}
func readInstalledPackageSizeBytes(pkg installedDriverPackage) int64 {
pathText := strings.TrimSpace(pkg.ExecutablePath)
if pathText == "" {
pathText = strings.TrimSpace(pkg.FilePath)
}
if pathText == "" {
return 0
}
info, err := os.Stat(pathText)
if err != nil || info.IsDir() {
return 0
}
return info.Size()
}
func formatSizeMB(sizeBytes int64) string {
if sizeBytes <= 0 {
return "-"
}
sizeMB := float64(sizeBytes) / (1024 * 1024)
return fmt.Sprintf("%.2f MB", sizeMB)
}