diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9691743..f8b6e0f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -203,7 +203,9 @@ jobs: APP_NAME=$(basename "$APP_PATH") echo "🔏 正在进行 Ad-hoc 签名..." - codesign --force --options runtime --deep --sign - "$APP_NAME" + # 注意:Ad-hoc + hardened runtime(--options runtime)在未配置 entitlements 时, + # 可能导致部分 macOS 机型上应用双击无响应。这里保持 Ad-hoc 深签名但禁用 runtime hardened。 + codesign --force --deep --sign - "$APP_NAME" DMG_NAME="${{ matrix.build_name }}.dmg" FINAL_NAME="GoNavi-$VERSION-${{ matrix.os_name }}-${{ matrix.arch_name }}${{ matrix.artifact_suffix }}.dmg" diff --git a/docs/driver-manifest.json b/docs/driver-manifest.json index ae4b5c9..a1c0c7c 100644 --- a/docs/driver-manifest.json +++ b/docs/driver-manifest.json @@ -3,79 +3,79 @@ "drivers": { "mariadb": { "engine": "go", - "version": "go-embedded", + "version": "1.9.3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/mariadb" }, "diros": { "engine": "go", - "version": "go-embedded", + "version": "1.9.3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/diros" }, "sphinx": { "engine": "go", - "version": "go-embedded", + "version": "1.9.3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/sphinx" }, "sqlserver": { "engine": "go", - "version": "go-embedded", + "version": "1.9.6", "checksumPolicy": "off", "downloadUrl": "builtin://activate/sqlserver" }, "sqlite": { "engine": "go", - "version": "go-embedded", + "version": "1.44.3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/sqlite" }, "duckdb": { "engine": "go", - "version": "go-embedded", + "version": "2.5.5", "checksumPolicy": "off", "downloadUrl": "builtin://activate/duckdb" }, "dameng": { "engine": "go", - "version": "go-embedded", + "version": "1.8.22", "checksumPolicy": "off", "downloadUrl": "builtin://activate/dameng" }, "kingbase": { "engine": "go", - "version": "go-embedded", + "version": "0.0.0-20201021123113-29bd62a876c3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/kingbase" }, "highgo": { "engine": "go", - "version": "go-embedded", + "version": "0.0.0-local", "checksumPolicy": "off", "downloadUrl": "builtin://activate/highgo" }, "vastbase": { "engine": "go", - "version": "go-embedded", + "version": "1.11.1", "checksumPolicy": "off", "downloadUrl": "builtin://activate/vastbase" }, "mongodb": { "engine": "go", - "version": "go-embedded", + "version": "2.5.0", "checksumPolicy": "off", "downloadUrl": "builtin://activate/mongodb" }, "tdengine": { "engine": "go", - "version": "go-embedded", + "version": "3.7.8", "checksumPolicy": "off", "downloadUrl": "builtin://activate/tdengine" }, "postgres": { "engine": "go", - "version": "go-embedded", + "version": "1.11.1", "checksumPolicy": "off", "downloadUrl": "builtin://activate/postgres" } diff --git a/frontend/src/components/ConnectionModal.tsx b/frontend/src/components/ConnectionModal.tsx index 0224149..b6cb4e3 100644 --- a/frontend/src/components/ConnectionModal.tsx +++ b/frontend/src/components/ConnectionModal.tsx @@ -3,7 +3,7 @@ import { Modal, Form, Input, InputNumber, Button, message, Checkbox, Divider, Se import { DatabaseOutlined, ConsoleSqlOutlined, FileTextOutlined, CloudServerOutlined, AppstoreAddOutlined, CloudOutlined, CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons'; import { useStore } from '../store'; import { DBGetDatabases, GetDriverStatusList, MongoDiscoverMembers, TestConnection, RedisConnect, SelectSSHKeyFile } from '../../wailsjs/go/app/App'; -import { MongoMemberInfo, SavedConnection } from '../types'; +import { ConnectionConfig, MongoMemberInfo, SavedConnection } from '../types'; const { Meta } = Card; const { Text } = Typography; @@ -58,6 +58,7 @@ const ConnectionModal: React.FC<{ const [form] = Form.useForm(); const [loading, setLoading] = useState(false); const [useSSH, setUseSSH] = useState(false); + const [useProxy, setUseProxy] = useState(false); const [dbType, setDbType] = useState('mysql'); const [step, setStep] = useState(1); // 1: Select Type, 2: Configure const [activeGroup, setActiveGroup] = useState(0); // Active category index in step 1 @@ -655,6 +656,12 @@ const ConnectionModal: React.FC<{ sshUser: config.ssh?.user, sshPassword: config.ssh?.password, sshKeyPath: config.ssh?.keyPath, + useProxy: config.useProxy, + proxyType: config.proxy?.type || 'socks5', + proxyHost: config.proxy?.host, + proxyPort: config.proxy?.port, + proxyUser: config.proxy?.user, + proxyPassword: config.proxy?.password, driver: config.driver, dsn: config.dsn, timeout: config.timeout || 30, @@ -674,6 +681,7 @@ const ConnectionModal: React.FC<{ mongoReplicaPassword: config.mongoReplicaPassword || '' }); setUseSSH(config.useSSH || false); + setUseProxy(config.useProxy || false); setDbType(configType); // 如果是 Redis 编辑模式,设置已保存的 Redis 数据库列表 if (configType === 'redis') { @@ -684,6 +692,7 @@ const ConnectionModal: React.FC<{ setStep(1); form.resetFields(); setUseSSH(false); + setUseProxy(false); setDbType('mysql'); setActiveGroup(0); } @@ -733,6 +742,7 @@ const ConnectionModal: React.FC<{ setLoading(false); form.resetFields(); setUseSSH(false); + setUseProxy(false); setDbType('mysql'); setStep(1); onClose(); @@ -852,7 +862,7 @@ const ConnectionModal: React.FC<{ } }; - const buildConfig = async (values: any, forPersist: boolean) => { + const buildConfig = async (values: any, forPersist: boolean): Promise => { const mergedValues = { ...values }; const parsedUriValues = parseUriToValues(mergedValues.uri, mergedValues.type); const isEmptyField = (value: unknown) => ( @@ -951,6 +961,22 @@ const ConnectionModal: React.FC<{ password: mergedValues.sshPassword || "", keyPath: mergedValues.sshKeyPath || "" } : { host: "", port: 22, user: "", password: "", keyPath: "" }; + const effectiveUseProxy = !isFileDbType && !!mergedValues.useProxy; + const proxyTypeRaw = String(mergedValues.proxyType || 'socks5').toLowerCase(); + const proxyType: 'socks5' | 'http' = proxyTypeRaw === 'http' ? 'http' : 'socks5'; + const proxyConfig: NonNullable = effectiveUseProxy ? { + type: proxyType, + host: String(mergedValues.proxyHost || '').trim(), + port: Number(mergedValues.proxyPort || (proxyTypeRaw === 'http' ? 8080 : 1080)), + user: String(mergedValues.proxyUser || '').trim(), + password: mergedValues.proxyPassword || "", + } : { + type: 'socks5', + host: '', + port: 1080, + user: '', + password: '', + }; const keepPassword = !forPersist || savePassword; @@ -964,6 +990,8 @@ const ConnectionModal: React.FC<{ database: mergedValues.database || "", useSSH: !!mergedValues.useSSH, ssh: sshConfig, + useProxy: effectiveUseProxy, + proxy: proxyConfig, driver: mergedValues.driver, dsn: mergedValues.dsn, timeout: Number(mergedValues.timeout || 30), @@ -997,6 +1025,7 @@ const ConnectionModal: React.FC<{ const defaultPort = getDefaultPortByType(type); if (isFileDatabaseType(type)) { setUseSSH(false); + setUseProxy(false); form.setFieldsValue({ host: '', port: 0, @@ -1009,6 +1038,12 @@ const ConnectionModal: React.FC<{ sshUser: '', sshPassword: '', sshKeyPath: '', + useProxy: false, + proxyType: 'socks5', + proxyHost: '', + proxyPort: 1080, + proxyUser: '', + proxyPassword: '', mysqlTopology: 'single', mongoTopology: 'single', mongoSrv: false, @@ -1026,6 +1061,7 @@ const ConnectionModal: React.FC<{ }); } else if (type !== 'custom') { form.setFieldsValue({ + database: '', port: defaultPort, mysqlTopology: 'single', mongoTopology: 'single', @@ -1164,9 +1200,13 @@ const ConnectionModal: React.FC<{ type: 'mysql', host: 'localhost', port: 3306, + database: '', user: 'root', useSSH: false, sshPort: 22, + useProxy: false, + proxyType: 'socks5', + proxyPort: 1080, timeout: 30, uri: '', mysqlTopology: 'single', @@ -1191,6 +1231,21 @@ const ConnectionModal: React.FC<{ setUriFeedback(null); } if (changed.useSSH !== undefined) setUseSSH(changed.useSSH); + if (changed.useProxy !== undefined) setUseProxy(changed.useProxy); + if (changed.proxyType !== undefined) { + const nextType = String(changed.proxyType || 'socks5').toLowerCase(); + if (nextType === 'http') { + const currentPort = Number(form.getFieldValue('proxyPort') || 0); + if (!currentPort || currentPort === 1080) { + form.setFieldValue('proxyPort', 8080); + } + } else { + const currentPort = Number(form.getFieldValue('proxyPort') || 0); + if (!currentPort || currentPort === 8080) { + form.setFieldValue('proxyPort', 1080); + } + } + } // Type change handled by step 1, but keep sync if select changes (hidden now) if (changed.type !== undefined) setDbType(changed.type); if ( @@ -1285,6 +1340,16 @@ const ConnectionModal: React.FC<{ )} + {(dbType === 'postgres' || dbType === 'kingbase' || dbType === 'highgo' || dbType === 'vastbase') && ( + + + + )} + {(dbType === 'mysql' || dbType === 'mariadb' || dbType === 'diros' || dbType === 'sphinx') && ( <> @@ -1531,6 +1596,38 @@ const ConnectionModal: React.FC<{ )} + + + 使用代理 (SOCKS5 / HTTP CONNECT) + + + {useProxy && ( +
+
+ + + + + + +
+
+ + + + + + +
+
+ )} + `${option.version}@@${option.downloadUrl}`; +const buildVersionSizeLoadingKey = (driverType: string, optionKey: string) => `${driverType}@@${optionKey}`; + +const buildVersionSelectOptions = (options: DriverVersionOption[]) => { + type SelectOption = { value: string; label: string }; + type SelectGroup = { label: string; options: SelectOption[] }; + + if (options.length === 0) { + return [] as Array; + } + + const yearGroups = new Map(); + const others: SelectOption[] = []; + options.forEach((option) => { + const selectOption: SelectOption = { + value: buildVersionOptionKey(option), + label: option.displayLabel || option.version || '默认版本', + }; + const year = String(option.year || '').trim(); + if (!year) { + others.push(selectOption); + return; + } + const group = yearGroups.get(year) || []; + group.push(selectOption); + yearGroups.set(year, group); + }); + + const sortedYears = Array.from(yearGroups.keys()).sort((a, b) => { + const left = Number.parseInt(a, 10); + const right = Number.parseInt(b, 10); + const leftValid = Number.isFinite(left); + const rightValid = Number.isFinite(right); + if (leftValid && rightValid) { + return right - left; + } + return b.localeCompare(a); + }); + + const grouped: SelectGroup[] = sortedYears.map((year) => ({ + label: `${year} 年`, + options: yearGroups.get(year) || [], + })); + if (others.length > 0) { + grouped.push({ label: '其他', options: others }); + } + return grouped; +}; + const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, onClose }) => { const [loading, setLoading] = useState(false); const [downloadDir, setDownloadDir] = useState(''); const [rows, setRows] = useState([]); const [actionDriver, setActionDriver] = useState(''); const [progressMap, setProgressMap] = useState>({}); + const [versionMap, setVersionMap] = useState>({}); + const [selectedVersionMap, setSelectedVersionMap] = useState>({}); + const [versionLoadingMap, setVersionLoadingMap] = useState>({}); + const [versionSizeLoadingMap, setVersionSizeLoadingMap] = useState>({}); const refreshStatus = useCallback(async (toastOnError = true) => { setLoading(true); @@ -65,6 +132,8 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({ type: String(item.type || '').trim(), name: String(item.name || item.type || '').trim(), builtIn: !!item.builtIn, + pinnedVersion: String(item.pinnedVersion || '').trim() || undefined, + installedVersion: String(item.installedVersion || '').trim() || undefined, packageSizeText: String(item.packageSizeText || '').trim() || undefined, runtimeAvailable: !!item.runtimeAvailable, packageInstalled: !!item.packageInstalled, @@ -82,6 +151,155 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({ } }, [downloadDir]); + const loadVersionOptions = useCallback(async (row: DriverStatusRow, toastOnError = false) => { + if (row.builtIn) { + return [] as DriverVersionOption[]; + } + const driverType = String(row.type || '').trim(); + if (!driverType) { + return [] as DriverVersionOption[]; + } + setVersionLoadingMap((prev) => ({ ...prev, [driverType]: true })); + try { + const res = await GetDriverVersionList(driverType, ''); + if (!res?.success) { + if (toastOnError) { + message.error(res?.message || `${row.name} 版本列表加载失败`); + } + return [] as DriverVersionOption[]; + } + const data = (res?.data || {}) as any; + const rawVersions = Array.isArray(data.versions) ? data.versions : []; + const options: DriverVersionOption[] = rawVersions + .map((item: any) => { + const version = String(item.version || '').trim(); + const downloadUrl = String(item.downloadUrl || '').trim(); + if (!version && !downloadUrl) { + return null; + } + return { + version, + downloadUrl, + packageSizeText: String(item.packageSizeText || '').trim() || undefined, + recommended: !!item.recommended, + source: String(item.source || '').trim() || undefined, + year: String(item.year || '').trim() || undefined, + displayLabel: String(item.displayLabel || '').trim() || undefined, + } as DriverVersionOption; + }) + .filter((item: DriverVersionOption | null): item is DriverVersionOption => !!item); + + if (options.length === 0) { + const fallbackVersion = String(row.pinnedVersion || '').trim(); + const fallbackURL = String(row.defaultDownloadUrl || '').trim(); + if (fallbackVersion || fallbackURL) { + options.push({ + version: fallbackVersion, + downloadUrl: fallbackURL, + recommended: true, + source: 'fallback', + displayLabel: fallbackVersion || '默认版本', + }); + } + } + + setVersionMap((prev) => ({ ...prev, [driverType]: options })); + setSelectedVersionMap((prev) => { + const currentKey = prev[driverType]; + if (currentKey && options.some((option) => buildVersionOptionKey(option) === currentKey)) { + return prev; + } + const preferred = + options.find((option) => option.version === row.installedVersion) || + options.find((option) => option.version === row.pinnedVersion) || + options.find((option) => option.recommended) || + options[0]; + if (!preferred) { + return prev; + } + return { ...prev, [driverType]: buildVersionOptionKey(preferred) }; + }); + return options; + } catch (err: any) { + if (toastOnError) { + message.error(`加载 ${row.name} 版本列表失败:${err?.message || String(err)}`); + } + return [] as DriverVersionOption[]; + } finally { + setVersionLoadingMap((prev) => ({ ...prev, [driverType]: false })); + } + }, []); + + const loadVersionPackageSize = useCallback(async (row: DriverStatusRow, optionKey: string) => { + if (row.builtIn) { + return; + } + const driverType = String(row.type || '').trim(); + if (!driverType || !optionKey) { + return; + } + + const options = versionMap[driverType] || []; + const selectedOption = options.find((item) => buildVersionOptionKey(item) === optionKey); + if (!selectedOption) { + return; + } + if (String(selectedOption.packageSizeText || '').trim()) { + return; + } + + const versionText = String(selectedOption.version || '').trim(); + if (!versionText) { + return; + } + + const loadingKey = buildVersionSizeLoadingKey(driverType, optionKey); + if (versionSizeLoadingMap[loadingKey]) { + return; + } + + setVersionSizeLoadingMap((prev) => ({ ...prev, [loadingKey]: true })); + try { + const res = await GetDriverVersionPackageSize(driverType, versionText); + if (!res?.success) { + return; + } + const data = (res?.data || {}) as any; + const sizeText = String(data.packageSizeText || '').trim(); + if (!sizeText) { + return; + } + + setVersionMap((prev) => { + const current = prev[driverType] || []; + let changed = false; + const next = current.map((item) => { + if (buildVersionOptionKey(item) !== optionKey) { + return item; + } + if (String(item.packageSizeText || '').trim() === sizeText) { + return item; + } + changed = true; + return { ...item, packageSizeText: sizeText }; + }); + if (!changed) { + return prev; + } + return { ...prev, [driverType]: next }; + }); + } finally { + setVersionSizeLoadingMap((prev) => { + if (!prev[loadingKey]) { + return prev; + } + const next = { ...prev }; + delete next[loadingKey]; + return next; + }); + } + }, [versionMap, versionSizeLoadingMap]); + useEffect(() => { if (!open) { return; @@ -129,17 +347,30 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({ }, })); try { - const result = await DownloadDriverPackage(row.type, '', downloadDir); + let options = versionMap[row.type] || []; + if (options.length === 0) { + options = await loadVersionOptions(row, true); + } + const selectedKey = selectedVersionMap[row.type]; + const selectedOption = + options.find((item) => buildVersionOptionKey(item) === selectedKey) || + options.find((item) => item.recommended) || + options[0]; + const selectedVersion = selectedOption?.version || row.pinnedVersion || ''; + const selectedDownloadURL = selectedOption?.downloadUrl || row.defaultDownloadUrl || ''; + + const result = await DownloadDriverPackage(row.type, selectedVersion, selectedDownloadURL, downloadDir); if (!result?.success) { message.error(result?.message || `安装 ${row.name} 失败`); return; } - message.success(`${row.name} 已安装启用`); + const versionTip = selectedVersion ? `(${selectedVersion})` : ''; + message.success(`${row.name}${versionTip} 已安装启用`); refreshStatus(false); } finally { setActionDriver(''); } - }, [downloadDir, refreshStatus]); + }, [downloadDir, loadVersionOptions, refreshStatus, selectedVersionMap, versionMap]); const removeDriver = useCallback(async (row: DriverStatusRow) => { setActionDriver(row.type); @@ -174,7 +405,23 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({ dataIndex: 'packageSizeText', key: 'packageSizeText', width: 120, - render: (_: string | undefined, row: DriverStatusRow) => row.packageSizeText || '-', + render: (_: string | undefined, row: DriverStatusRow) => { + if (row.builtIn) { + return row.packageSizeText || '-'; + } + const options = versionMap[row.type] || []; + const selectedKey = selectedVersionMap[row.type]; + const loadingKey = buildVersionSizeLoadingKey(row.type, selectedKey || ''); + const selectedOption = + options.find((item) => buildVersionOptionKey(item) === selectedKey) || + options.find((item) => item.recommended) || + options[0]; + const anyKnownSize = options.find((item) => String(item.packageSizeText || '').trim())?.packageSizeText; + if (selectedKey && versionSizeLoadingMap[loadingKey]) { + return '计算中...'; + } + return selectedOption?.packageSizeText || anyKnownSize || row.packageSizeText || '-'; + }, }, { title: '状态', @@ -224,6 +471,43 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({ return ; }, }, + { + title: '驱动版本', + key: 'driverVersion', + width: 230, + render: (_: string, row: DriverStatusRow) => { + if (row.builtIn) { + return -; + } + const options = versionMap[row.type] || []; + const selectedKey = selectedVersionMap[row.type]; + const selectOptions = buildVersionSelectOptions(options); + return ( +