mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-27 03:10:11 +08:00
✨ feat(driver): 提醒重装旧版驱动代理
- optional-driver-agent 新增 metadata 方法返回 driverType、agentRevision 与协议版本 - 安装和本地导入驱动后记录 agentRevision,并在驱动状态中比对是否需要更新 - 驱动管理、连接表单和已有连接加载入口提示重装旧版 agent - 补充旧 revision 检测和 custom 连接使用统计回归测试
This commit is contained in:
@@ -37,6 +37,7 @@ type agentResponse struct {
|
||||
const (
|
||||
agentMethodConnect = "connect"
|
||||
agentMethodClose = "close"
|
||||
agentMethodMetadata = "metadata"
|
||||
agentMethodPing = "ping"
|
||||
agentMethodQuery = "query"
|
||||
agentMethodExec = "exec"
|
||||
@@ -131,6 +132,13 @@ func handleRequest(inst *db.Database, req agentRequest) agentResponse {
|
||||
*inst = nil
|
||||
}
|
||||
return resp
|
||||
case agentMethodMetadata:
|
||||
resp.Data = map[string]string{
|
||||
"driverType": strings.TrimSpace(agentDriverType),
|
||||
"agentRevision": db.OptionalDriverAgentRevision(agentDriverType),
|
||||
"protocolSchema": "json-lines-v1",
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
if *inst == nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
"GoNavi-Wails/internal/db"
|
||||
)
|
||||
|
||||
type duckMapLike map[any]any
|
||||
@@ -66,6 +67,33 @@ func TestNormalizeAgentResponseData_KeepByteSlice(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleRequestMetadataReportsAgentRevision(t *testing.T) {
|
||||
previousDriverType := agentDriverType
|
||||
previousFactory := agentDatabaseFactory
|
||||
t.Cleanup(func() {
|
||||
agentDriverType = previousDriverType
|
||||
agentDatabaseFactory = previousFactory
|
||||
})
|
||||
agentDriverType = "clickhouse"
|
||||
agentDatabaseFactory = func() db.Database { return nil }
|
||||
|
||||
var inst db.Database
|
||||
resp := handleRequest(&inst, agentRequest{ID: 7, Method: agentMethodMetadata})
|
||||
if !resp.Success {
|
||||
t.Fatalf("metadata request failed: %s", resp.Error)
|
||||
}
|
||||
data, ok := resp.Data.(map[string]string)
|
||||
if !ok {
|
||||
t.Fatalf("metadata response data type = %T", resp.Data)
|
||||
}
|
||||
if data["driverType"] != "clickhouse" {
|
||||
t.Fatalf("unexpected driver type: %q", data["driverType"])
|
||||
}
|
||||
if data["agentRevision"] != db.OptionalDriverAgentRevision("clickhouse") {
|
||||
t.Fatalf("unexpected agent revision: %q", data["agentRevision"])
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAgentTimeoutDB struct {
|
||||
queryCalled bool
|
||||
queryContextCalled bool
|
||||
|
||||
@@ -236,6 +236,10 @@ type DriverStatusSnapshot = {
|
||||
type: string;
|
||||
name: string;
|
||||
connectable: boolean;
|
||||
expectedRevision?: string;
|
||||
needsUpdate?: boolean;
|
||||
updateReason?: string;
|
||||
affectedConnections?: number;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
@@ -248,6 +252,14 @@ const normalizeDriverType = (value: string): string => {
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const resolveConnectionDriverType = (type: string, driver?: string): string => {
|
||||
const normalizedType = normalizeDriverType(type);
|
||||
if (normalizedType !== "custom") {
|
||||
return normalizedType;
|
||||
}
|
||||
return normalizeDriverType(driver || "");
|
||||
};
|
||||
|
||||
const ConnectionModal: React.FC<{
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
@@ -320,6 +332,7 @@ const ConnectionModal: React.FC<{
|
||||
const redisTopology = Form.useWatch("redisTopology", form) || "single";
|
||||
const sslMode = Form.useWatch("sslMode", form) || "preferred";
|
||||
const proxyType = Form.useWatch("proxyType", form) || "socks5";
|
||||
const customDriver = Form.useWatch("driver", form) || "";
|
||||
const mongoReadPreference =
|
||||
Form.useWatch("mongoReadPreference", form) || "primary";
|
||||
const mongoAuthMechanism = Form.useWatch("mongoAuthMechanism", form) || "";
|
||||
@@ -851,6 +864,12 @@ const ConnectionModal: React.FC<{
|
||||
type,
|
||||
name: String(item.name || item.type || type).trim(),
|
||||
connectable: !!item.connectable,
|
||||
expectedRevision: String(item.expectedRevision || "").trim() || undefined,
|
||||
needsUpdate: !!item.needsUpdate,
|
||||
updateReason: String(item.updateReason || "").trim() || undefined,
|
||||
affectedConnections: Number.isFinite(Number(item.affectedConnections))
|
||||
? Number(item.affectedConnections)
|
||||
: undefined,
|
||||
message: String(item.message || "").trim() || undefined,
|
||||
};
|
||||
});
|
||||
@@ -868,8 +887,11 @@ const ConnectionModal: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
const resolveDriverUnavailableReason = async (type: string): Promise<string> => {
|
||||
const normalized = normalizeDriverType(type);
|
||||
const resolveDriverUnavailableReason = async (
|
||||
type: string,
|
||||
driver?: string,
|
||||
): Promise<string> => {
|
||||
const normalized = resolveConnectionDriverType(type, driver);
|
||||
if (!normalized || normalized === "custom") {
|
||||
return "";
|
||||
}
|
||||
@@ -2382,10 +2404,16 @@ const ConnectionModal: React.FC<{
|
||||
try {
|
||||
await form.validateFields();
|
||||
const values = form.getFieldsValue(true);
|
||||
const unavailableReason = await resolveDriverUnavailableReason(values.type);
|
||||
const unavailableReason = await resolveDriverUnavailableReason(
|
||||
values.type,
|
||||
values.driver,
|
||||
);
|
||||
if (unavailableReason) {
|
||||
message.warning(unavailableReason);
|
||||
promptInstallDriver(values.type, unavailableReason);
|
||||
promptInstallDriver(
|
||||
resolveConnectionDriverType(values.type, values.driver) || values.type,
|
||||
unavailableReason,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
@@ -2538,7 +2566,10 @@ const ConnectionModal: React.FC<{
|
||||
try {
|
||||
await form.validateFields();
|
||||
const values = form.getFieldsValue(true);
|
||||
const unavailableReason = await resolveDriverUnavailableReason(values.type);
|
||||
const unavailableReason = await resolveDriverUnavailableReason(
|
||||
values.type,
|
||||
values.driver,
|
||||
);
|
||||
if (unavailableReason) {
|
||||
applyTestFailureFeedback(
|
||||
resolveConnectionTestFailureFeedback({
|
||||
@@ -2547,7 +2578,10 @@ const ConnectionModal: React.FC<{
|
||||
fallback: "驱动未安装启用",
|
||||
}),
|
||||
);
|
||||
promptInstallDriver(values.type, unavailableReason);
|
||||
promptInstallDriver(
|
||||
resolveConnectionDriverType(values.type, values.driver) || values.type,
|
||||
unavailableReason,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const blockingSecretClearMessage = getBlockingSecretClearMessage(values);
|
||||
@@ -3326,17 +3360,27 @@ const ConnectionModal: React.FC<{
|
||||
isJVM && hasUnsupportedJvmModeSelection
|
||||
? "当前连接包含未支持的 JVM 模式。此版本只支持 JMX / Endpoint / Agent,请先调整允许模式和首选模式后再继续。"
|
||||
: "";
|
||||
const currentDriverType = normalizeDriverType(dbType);
|
||||
const currentDriverType = resolveConnectionDriverType(dbType, customDriver);
|
||||
const hasCurrentDriverType =
|
||||
currentDriverType !== "" && currentDriverType !== "custom";
|
||||
const currentDriverSnapshot = driverStatusMap[currentDriverType];
|
||||
const currentDriverUnavailableReason =
|
||||
currentDriverType !== "custom" &&
|
||||
hasCurrentDriverType &&
|
||||
currentDriverSnapshot &&
|
||||
!currentDriverSnapshot.connectable
|
||||
? currentDriverSnapshot.message ||
|
||||
`${currentDriverSnapshot.name || dbType} 驱动未安装启用`
|
||||
: "";
|
||||
const currentDriverUpdateReason =
|
||||
hasCurrentDriverType &&
|
||||
currentDriverSnapshot?.connectable &&
|
||||
currentDriverSnapshot.needsUpdate
|
||||
? currentDriverSnapshot.message ||
|
||||
currentDriverSnapshot.updateReason ||
|
||||
`${currentDriverSnapshot.name || dbType} 驱动代理需要重装后才能应用当前版本的驱动侧更新`
|
||||
: "";
|
||||
const driverStatusChecking =
|
||||
currentDriverType !== "custom" && !driverStatusLoaded && step === 2;
|
||||
hasCurrentDriverType && !driverStatusLoaded && step === 2;
|
||||
|
||||
const dbTypeGroups = [
|
||||
{
|
||||
@@ -6055,6 +6099,26 @@ const ConnectionModal: React.FC<{
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{currentDriverUpdateReason && (
|
||||
<Alert
|
||||
showIcon
|
||||
type="warning"
|
||||
style={{ marginBottom: 12 }}
|
||||
message="当前数据源驱动代理建议重装"
|
||||
description={
|
||||
<Space size={8}>
|
||||
<span>{currentDriverUpdateReason}</span>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => onOpenDriverManager?.()}
|
||||
>
|
||||
去驱动管理重装
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{(() => {
|
||||
const sectionItems: Array<{
|
||||
key: "basic" | "network" | "appearance";
|
||||
|
||||
@@ -39,6 +39,11 @@ type DriverStatusRow = {
|
||||
packagePath?: string;
|
||||
executablePath?: string;
|
||||
downloadedAt?: string;
|
||||
agentRevision?: string;
|
||||
expectedRevision?: string;
|
||||
needsUpdate?: boolean;
|
||||
updateReason?: string;
|
||||
affectedConnections?: number;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
@@ -360,6 +365,13 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
|
||||
packagePath: String(item.packagePath || '').trim() || undefined,
|
||||
executablePath: String(item.executablePath || '').trim() || undefined,
|
||||
downloadedAt: String(item.downloadedAt || '').trim() || undefined,
|
||||
agentRevision: String(item.agentRevision || '').trim() || undefined,
|
||||
expectedRevision: String(item.expectedRevision || '').trim() || undefined,
|
||||
needsUpdate: !!item.needsUpdate,
|
||||
updateReason: String(item.updateReason || '').trim() || undefined,
|
||||
affectedConnections: Number.isFinite(Number(item.affectedConnections))
|
||||
? Number(item.affectedConnections)
|
||||
: undefined,
|
||||
message: String(item.message || '').trim() || undefined,
|
||||
}));
|
||||
setRows(nextRows);
|
||||
@@ -1005,7 +1017,17 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
|
||||
title: '数据源',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 150,
|
||||
width: 220,
|
||||
render: (_: string, row: DriverStatusRow) => (
|
||||
<div style={{ display: 'grid', gap: 4 }}>
|
||||
<Text strong>{row.name}</Text>
|
||||
{row.message ? (
|
||||
<Text type={row.needsUpdate ? 'warning' : 'secondary'} style={{ fontSize: 12 }}>
|
||||
{row.message}
|
||||
</Text>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '安装包大小',
|
||||
@@ -1042,6 +1064,9 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
|
||||
if (progress && (progress.status === 'start' || progress.status === 'downloading')) {
|
||||
return <Tag color="processing">安装中 {Math.round(progress.percent)}%</Tag>;
|
||||
}
|
||||
if (row.needsUpdate) {
|
||||
return <Tag color="warning">强烈建议重装</Tag>;
|
||||
}
|
||||
if (row.connectable) {
|
||||
return <Tag color="success">已启用</Tag>;
|
||||
}
|
||||
@@ -1089,10 +1114,11 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
|
||||
const versionLocked = row.packageInstalled || row.connectable;
|
||||
if (versionLocked) {
|
||||
const installedVersion = String(row.installedVersion || '').trim();
|
||||
const revisionHint = row.needsUpdate ? ',需重装' : '';
|
||||
if (installedVersion) {
|
||||
return <Text type="secondary">{installedVersion}(已安装,移除后可更换)</Text>;
|
||||
return <Text type="secondary">{installedVersion}(已安装{revisionHint},移除后可更换)</Text>;
|
||||
}
|
||||
return <Text type="secondary">已安装(移除后可更换)</Text>;
|
||||
return <Text type="secondary">已安装({row.needsUpdate ? '需重装,' : ''}移除后可更换)</Text>;
|
||||
}
|
||||
const options = versionMap[row.type] || [];
|
||||
const selectedKey = selectedVersionMap[row.type];
|
||||
@@ -1148,7 +1174,16 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
|
||||
const logs = operationLogMap[row.type] || [];
|
||||
const hasLogs = logs.length > 0;
|
||||
|
||||
const mainAction = row.connectable ? (
|
||||
const mainAction = row.needsUpdate ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<DownloadOutlined />}
|
||||
loading={loadingInstallOrRemove}
|
||||
onClick={() => installDriver(row)}
|
||||
>
|
||||
重装驱动
|
||||
</Button>
|
||||
) : row.connectable ? (
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
@@ -1209,9 +1244,10 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
|
||||
row.type,
|
||||
row.pinnedVersion,
|
||||
row.installedVersion,
|
||||
row.updateReason,
|
||||
row.message,
|
||||
row.builtIn ? '内置' : '外置',
|
||||
row.connectable ? '已启用' : row.packageInstalled ? '已安装' : '未启用',
|
||||
row.needsUpdate ? '强烈建议重装' : row.connectable ? '已启用' : row.packageInstalled ? '已安装' : '未启用',
|
||||
];
|
||||
const searchableText = normalizeDriverSearchText(searchableParts.filter(Boolean).join(' '));
|
||||
return searchableText.includes(normalizedSearchKeyword);
|
||||
|
||||
@@ -36,9 +36,9 @@ import { Tree, message, Dropdown, MenuProps, Input, Button, Modal, Form, Badge,
|
||||
} from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { buildOverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme';
|
||||
import { SavedConnection, ExternalSQLTreeEntry, JVMCapability, JVMResourceSummary } from '../types';
|
||||
import { SavedConnection, ExternalSQLTreeEntry, JVMCapability, JVMResourceSummary } from '../types';
|
||||
import { getDbIcon } from './DatabaseIcons';
|
||||
import { DBGetDatabases, DBGetTables, DBQuery, DBShowCreateTable, ExportTable, OpenSQLFile, ExecuteSQLFile, CancelSQLFileExecution, CreateDatabase, RenameDatabase, DropDatabase, RenameTable, DropTable, DropView, DropFunction, RenameView, SelectSQLDirectory, ListSQLDirectory, ReadSQLFile, JVMProbeCapabilities } from '../../wailsjs/go/app/App';
|
||||
import { DBGetDatabases, DBGetTables, DBQuery, DBShowCreateTable, ExportTable, OpenSQLFile, ExecuteSQLFile, CancelSQLFileExecution, CreateDatabase, RenameDatabase, DropDatabase, RenameTable, DropTable, DropView, DropFunction, RenameView, SelectSQLDirectory, ListSQLDirectory, ReadSQLFile, JVMProbeCapabilities, GetDriverStatusList } from '../../wailsjs/go/app/App';
|
||||
import { getTableDataDangerActionMeta, supportsTableTruncateAction, type TableDataDangerActionKind } from './tableDataDangerActions';
|
||||
import { EventsOn } from '../../wailsjs/runtime/runtime';
|
||||
import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
|
||||
@@ -81,6 +81,33 @@ interface BatchObjectItem {
|
||||
dataRef: any;
|
||||
}
|
||||
|
||||
type DriverStatusSnapshot = {
|
||||
type: string;
|
||||
name: string;
|
||||
connectable: boolean;
|
||||
expectedRevision?: string;
|
||||
needsUpdate?: boolean;
|
||||
updateReason?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
const DRIVER_STATUS_CACHE_TTL_MS = 30_000;
|
||||
|
||||
const normalizeDriverType = (value: string): string => {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === 'postgresql') return 'postgres';
|
||||
if (normalized === 'doris') return 'diros';
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const resolveSavedConnectionDriverType = (conn: SavedConnection | undefined): string => {
|
||||
const type = normalizeDriverType(conn?.config?.type || '');
|
||||
if (type !== 'custom') {
|
||||
return type;
|
||||
}
|
||||
return normalizeDriverType(conn?.config?.driver || '');
|
||||
};
|
||||
|
||||
const SEARCH_SCOPE_OPTIONS: Array<{ value: SearchScope; label: string }> = [
|
||||
{ value: 'smart', label: '智能' },
|
||||
{ value: 'object', label: '表对象' },
|
||||
@@ -211,10 +238,12 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
const [autoExpandParent, setAutoExpandParent] = useState(true);
|
||||
const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]);
|
||||
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
|
||||
const selectedNodesRef = useRef<any[]>([]);
|
||||
const loadingNodesRef = useRef<Set<string>>(new Set());
|
||||
const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number, y: number, items: MenuProps['items'] } | null>(null);
|
||||
const selectedNodesRef = useRef<any[]>([]);
|
||||
const loadingNodesRef = useRef<Set<string>>(new Set());
|
||||
const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const driverStatusCacheRef = useRef<{ fetchedAt: number; items: Record<string, DriverStatusSnapshot> } | null>(null);
|
||||
const driverUpdateWarningKeysRef = useRef<Set<string>>(new Set());
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number, y: number, items: MenuProps['items'] } | null>(null);
|
||||
|
||||
// Virtual Scroll State
|
||||
const [treeHeight, setTreeHeight] = useState(500);
|
||||
@@ -956,13 +985,72 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
const typeLabel = normalizedType === 'PROCEDURE' ? 'P' : 'F';
|
||||
routines.push({ displayName: `${fullName} [${typeLabel}]`, routineName: fullName, routineType: normalizedType });
|
||||
});
|
||||
});
|
||||
return { routines, supported: hasSuccessfulQuery };
|
||||
};
|
||||
});
|
||||
return { routines, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadDatabases = async (node: any) => {
|
||||
const conn = node.dataRef as SavedConnection;
|
||||
const loadKey = `dbs-${conn.id}`;
|
||||
const fetchDriverStatusMap = async (): Promise<Record<string, DriverStatusSnapshot>> => {
|
||||
const cached = driverStatusCacheRef.current;
|
||||
if (cached && Date.now() - cached.fetchedAt < DRIVER_STATUS_CACHE_TTL_MS) {
|
||||
return cached.items;
|
||||
}
|
||||
const result: Record<string, DriverStatusSnapshot> = {};
|
||||
const res = await GetDriverStatusList('', '');
|
||||
if (!res?.success) {
|
||||
return result;
|
||||
}
|
||||
const data = (res.data || {}) as any;
|
||||
const drivers = Array.isArray(data.drivers) ? data.drivers : [];
|
||||
drivers.forEach((item: any) => {
|
||||
const type = normalizeDriverType(String(item.type || '').trim());
|
||||
if (!type) return;
|
||||
result[type] = {
|
||||
type,
|
||||
name: String(item.name || item.type || type).trim(),
|
||||
connectable: !!item.connectable,
|
||||
expectedRevision: String(item.expectedRevision || '').trim() || undefined,
|
||||
needsUpdate: !!item.needsUpdate,
|
||||
updateReason: String(item.updateReason || '').trim() || undefined,
|
||||
message: String(item.message || '').trim() || undefined,
|
||||
};
|
||||
});
|
||||
driverStatusCacheRef.current = { fetchedAt: Date.now(), items: result };
|
||||
return result;
|
||||
};
|
||||
|
||||
const warnIfConnectionDriverAgentNeedsUpdate = async (conn: SavedConnection) => {
|
||||
try {
|
||||
const driverType = resolveSavedConnectionDriverType(conn);
|
||||
if (!driverType || driverType === 'custom') {
|
||||
return;
|
||||
}
|
||||
const statusMap = await fetchDriverStatusMap();
|
||||
const status = statusMap[driverType];
|
||||
if (!status?.connectable || !status.needsUpdate) {
|
||||
return;
|
||||
}
|
||||
const revisionKey = status.expectedRevision || status.updateReason || status.message || 'unknown';
|
||||
const warningKey = `${conn.id}:${driverType}:${revisionKey}`;
|
||||
if (driverUpdateWarningKeysRef.current.has(warningKey)) {
|
||||
return;
|
||||
}
|
||||
driverUpdateWarningKeysRef.current.add(warningKey);
|
||||
const driverName = status.name || driverType;
|
||||
const reason = status.message || status.updateReason || `${driverName} driver-agent 与当前 GoNavi 版本要求不一致`;
|
||||
message.warning({
|
||||
content: `${driverName} 驱动代理需要重装:${reason}`,
|
||||
key: `driver-agent-update-${conn.id}`,
|
||||
duration: 10,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('检查驱动代理更新状态失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadDatabases = async (node: any) => {
|
||||
const conn = node.dataRef as SavedConnection;
|
||||
void warnIfConnectionDriverAgentNeedsUpdate(conn);
|
||||
const loadKey = `dbs-${conn.id}`;
|
||||
if (loadingNodesRef.current.has(loadKey)) return;
|
||||
loadingNodesRef.current.add(loadKey);
|
||||
const config = {
|
||||
@@ -1845,8 +1933,9 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
setIsBatchModalOpen(true);
|
||||
};
|
||||
|
||||
const loadDatabasesForBatch = async (conn: SavedConnection) => {
|
||||
const config = {
|
||||
const loadDatabasesForBatch = async (conn: SavedConnection) => {
|
||||
void warnIfConnectionDriverAgentNeedsUpdate(conn);
|
||||
const config = {
|
||||
...conn.config,
|
||||
port: Number(conn.config.port),
|
||||
password: conn.config.password || "",
|
||||
@@ -2154,10 +2243,11 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
setIsBatchDbModalOpen(true);
|
||||
};
|
||||
|
||||
const loadDatabasesForDbBatch = async (conn: SavedConnection) => {
|
||||
setBatchConnContext(conn);
|
||||
const loadDatabasesForDbBatch = async (conn: SavedConnection) => {
|
||||
setBatchConnContext(conn);
|
||||
void warnIfConnectionDriverAgentNeedsUpdate(conn);
|
||||
|
||||
const config = {
|
||||
const config = {
|
||||
...conn.config,
|
||||
port: Number(conn.config.port),
|
||||
password: conn.config.password || "",
|
||||
|
||||
@@ -127,6 +127,7 @@ type driverDefinition struct {
|
||||
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"`
|
||||
@@ -136,23 +137,28 @@ type installedDriverPackage struct {
|
||||
}
|
||||
|
||||
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"`
|
||||
Message string `json:"message,omitempty"`
|
||||
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"
|
||||
@@ -758,29 +764,36 @@ func (a *App) GetDriverStatusList(downloadDir string, manifestURL string) connec
|
||||
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),
|
||||
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
|
||||
@@ -792,6 +805,12 @@ func (a *App) GetDriverStatusList(downloadDir string, manifestURL string) connec
|
||||
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) != "":
|
||||
@@ -2702,6 +2721,47 @@ func readInstalledDriverPackage(downloadDir string, driverType string) (installe
|
||||
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-agent(revision: %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 {
|
||||
@@ -2765,9 +2825,11 @@ func installOptionalDriverAgentPackage(a *App, definition driverDefinition, sele
|
||||
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,
|
||||
@@ -2837,9 +2899,11 @@ func installOptionalDriverAgentFromLocalPath(definition driverDefinition, filePa
|
||||
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,
|
||||
@@ -2849,6 +2913,19 @@ func installOptionalDriverAgentFromLocalPath(definition driverDefinition, filePa
|
||||
}, 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
|
||||
|
||||
72
internal/app/methods_driver_agent_revision_test.go
Normal file
72
internal/app/methods_driver_agent_revision_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
)
|
||||
|
||||
func TestOptionalDriverAgentRevisionStatusDetectsStaleClickHouseAgent(t *testing.T) {
|
||||
needsUpdate, reason, expected := optionalDriverAgentRevisionStatus("clickhouse", installedDriverPackage{}, true)
|
||||
if !needsUpdate {
|
||||
t.Fatal("expected missing ClickHouse agent revision to require update")
|
||||
}
|
||||
if expected == "" {
|
||||
t.Fatal("expected ClickHouse to define an agent revision")
|
||||
}
|
||||
if reason == "" {
|
||||
t.Fatal("expected update reason")
|
||||
}
|
||||
if !strings.Contains(reason, "原因:") || !strings.Contains(reason, "影响:") {
|
||||
t.Fatalf("expected reason to explain cause and impact, got %q", reason)
|
||||
}
|
||||
if !strings.Contains(reason, "强烈建议重装") {
|
||||
t.Fatalf("expected reason to strongly recommend reinstall, got %q", reason)
|
||||
}
|
||||
|
||||
current := installedDriverPackage{AgentRevision: expected}
|
||||
needsUpdate, reason, _ = optionalDriverAgentRevisionStatus("clickhouse", current, true)
|
||||
if needsUpdate {
|
||||
t.Fatalf("expected current ClickHouse agent revision to be accepted, reason=%q", reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSavedConnectionDriverUsageCountsIncludesOptionalAndCustomDrivers(t *testing.T) {
|
||||
app := &App{configDir: t.TempDir()}
|
||||
repo := app.savedConnectionRepository()
|
||||
if err := repo.saveAll([]connection.SavedConnectionView{
|
||||
{
|
||||
ID: "conn-clickhouse",
|
||||
Name: "ClickHouse",
|
||||
Config: connection.ConnectionConfig{
|
||||
Type: "clickhouse",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "conn-custom-clickhouse",
|
||||
Name: "Custom ClickHouse",
|
||||
Config: connection.ConnectionConfig{
|
||||
Type: "custom",
|
||||
Driver: "clickhouse",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "conn-mysql",
|
||||
Name: "MySQL",
|
||||
Config: connection.ConnectionConfig{
|
||||
Type: "mysql",
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save connections failed: %v", err)
|
||||
}
|
||||
|
||||
counts := app.savedConnectionDriverUsageCounts()
|
||||
if got := counts["clickhouse"]; got != 2 {
|
||||
t.Fatalf("expected two ClickHouse usages, got %d", got)
|
||||
}
|
||||
if got := counts["mysql"]; got != 0 {
|
||||
t.Fatalf("expected built-in MySQL to be ignored, got %d", got)
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
const (
|
||||
optionalAgentMethodConnect = "connect"
|
||||
optionalAgentMethodClose = "close"
|
||||
optionalAgentMethodMetadata = "metadata"
|
||||
optionalAgentMethodPing = "ping"
|
||||
optionalAgentMethodQuery = "query"
|
||||
optionalAgentMethodExec = "exec"
|
||||
@@ -58,6 +59,12 @@ type optionalAgentResponse struct {
|
||||
RowsAffected int64 `json:"rowsAffected,omitempty"`
|
||||
}
|
||||
|
||||
type OptionalDriverAgentMetadata struct {
|
||||
DriverType string `json:"driverType,omitempty"`
|
||||
AgentRevision string `json:"agentRevision,omitempty"`
|
||||
ProtocolSchema string `json:"protocolSchema,omitempty"`
|
||||
}
|
||||
|
||||
type optionalDriverAgentClient struct {
|
||||
cmd *exec.Cmd
|
||||
stdin io.WriteCloser
|
||||
@@ -69,6 +76,25 @@ type optionalDriverAgentClient struct {
|
||||
driver string
|
||||
}
|
||||
|
||||
func ProbeOptionalDriverAgentMetadata(driverType string, executablePath string) (OptionalDriverAgentMetadata, error) {
|
||||
client, err := newOptionalDriverAgentClient(driverType, executablePath)
|
||||
if err != nil {
|
||||
return OptionalDriverAgentMetadata{}, err
|
||||
}
|
||||
defer func() {
|
||||
_ = client.close()
|
||||
}()
|
||||
|
||||
var metadata OptionalDriverAgentMetadata
|
||||
if err := client.call(optionalAgentRequest{Method: optionalAgentMethodMetadata}, &metadata, nil, nil); err != nil {
|
||||
return OptionalDriverAgentMetadata{}, err
|
||||
}
|
||||
metadata.DriverType = normalizeRuntimeDriverType(metadata.DriverType)
|
||||
metadata.AgentRevision = strings.TrimSpace(metadata.AgentRevision)
|
||||
metadata.ProtocolSchema = strings.TrimSpace(metadata.ProtocolSchema)
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func newOptionalDriverAgentClient(driverType string, executablePath string) (*optionalDriverAgentClient, error) {
|
||||
pathText := strings.TrimSpace(executablePath)
|
||||
if pathText == "" {
|
||||
|
||||
Reference in New Issue
Block a user