diff --git a/cmd/optional-driver-agent/main.go b/cmd/optional-driver-agent/main.go index 4c0c5b9..bf24273 100644 --- a/cmd/optional-driver-agent/main.go +++ b/cmd/optional-driver-agent/main.go @@ -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 { diff --git a/cmd/optional-driver-agent/main_test.go b/cmd/optional-driver-agent/main_test.go index 016e520..7e082e1 100644 --- a/cmd/optional-driver-agent/main_test.go +++ b/cmd/optional-driver-agent/main_test.go @@ -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 diff --git a/frontend/src/components/ConnectionModal.tsx b/frontend/src/components/ConnectionModal.tsx index a1f154c..2ea26d2 100644 --- a/frontend/src/components/ConnectionModal.tsx +++ b/frontend/src/components/ConnectionModal.tsx @@ -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 => { - const normalized = normalizeDriverType(type); + const resolveDriverUnavailableReason = async ( + type: string, + driver?: string, + ): Promise => { + 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 && ( + + {currentDriverUpdateReason} + + + } + /> + )} {(() => { const sectionItems: Array<{ key: "basic" | "network" | "appearance"; diff --git a/frontend/src/components/DriverManagerModal.tsx b/frontend/src/components/DriverManagerModal.tsx index 9b8ea05..a4551d7 100644 --- a/frontend/src/components/DriverManagerModal.tsx +++ b/frontend/src/components/DriverManagerModal.tsx @@ -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) => ( +
+ {row.name} + {row.message ? ( + + {row.message} + + ) : null} +
+ ), }, { title: '安装包大小', @@ -1042,6 +1064,9 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG if (progress && (progress.status === 'start' || progress.status === 'downloading')) { return 安装中 {Math.round(progress.percent)}%; } + if (row.needsUpdate) { + return 强烈建议重装; + } if (row.connectable) { return 已启用; } @@ -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 {installedVersion}(已安装,移除后可更换); + return {installedVersion}(已安装{revisionHint},移除后可更换); } - return 已安装(移除后可更换); + return 已安装({row.needsUpdate ? '需重装,' : ''}移除后可更换); } 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 ? ( + + ) : row.connectable ? (