feat(driver): 提醒重装旧版驱动代理

- optional-driver-agent 新增 metadata 方法返回 driverType、agentRevision 与协议版本
- 安装和本地导入驱动后记录 agentRevision,并在驱动状态中比对是否需要更新
- 驱动管理、连接表单和已有连接加载入口提示重装旧版 agent
- 补充旧 revision 检测和 custom 连接使用统计回归测试
This commit is contained in:
Syngnat
2026-04-29 17:22:49 +08:00
parent 824aafbdea
commit c927e33c8c
8 changed files with 461 additions and 60 deletions

View File

@@ -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";

View File

@@ -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);

View File

@@ -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 || "",