diff --git a/docs/plugins/DESIGN.md b/docs/plugins/DESIGN.md index 1d7adf1ba..f26a2c080 100644 --- a/docs/plugins/DESIGN.md +++ b/docs/plugins/DESIGN.md @@ -149,7 +149,7 @@ my-terminal-plugin/ | type | 说明 | 典型场景 | | -------- | -------------------------------------------- | ----------------------- | -| `args` | 模板替换后作为命令行参数 | PuTTY、DBeaver | +| `args` | 模板替换后作为命令行参数 | PuTTY、DBX、DBeaver | | `script` | 执行 `scripts/` 下平台脚本,传入 JSON 上下文 | iTerm2、复杂 GUI 自动化 | | `url` | 构建 URL Scheme 并 `open` | Navicat | | `file` | 先写临时文件再打开 | RDP `.rdp` | @@ -168,6 +168,7 @@ my-terminal-plugin/ | `{port}` | 端口 | | `{file}` | 临时文件路径(`file` 类型) | | `{dbname}` | 数据库名 | +| `{dbx_url}` | URL 编码的 DBX 一次性连接链接 | | `{use_ssl}` | 是否 SSL | | `{allow_invalid_cert}` | 是否允许无效证书 | @@ -180,7 +181,7 @@ my-terminal-plugin/ "version": 1, "selections": { "terminal:ssh": "builtin.putty", - "databases:mysql": "builtin.dbeaver" + "databases:mysql": "windows.dbx" }, "plugins": { "com.example.xshell": { @@ -320,6 +321,6 @@ sequenceDiagram | macOS | remotedesktop | 系统 RDP(open) | | Linux | terminal | 系统 terminal | | Linux | remotedesktop | xfreerdp, tigervnc | -| 全平台 | databases | dbeaver(需用户配路径) | +| 全平台 | databases | dbx(默认)、dbeaver(可选,需配置本机路径) | 其余(XShell、Navicat、MobaXterm 等)以**可选插件**形式提供下载或用户自行打包安装。 diff --git a/electron/src/apps/local-app-launcher.ts b/electron/src/apps/local-app-launcher.ts index 7b29d6b33..dfe27e9f4 100644 --- a/electron/src/apps/local-app-launcher.ts +++ b/electron/src/apps/local-app-launcher.ts @@ -26,6 +26,26 @@ function navicatUrl(payload) { return `navicat://conn.${protocol}?Conn.Host=${payload.endpoint.host}&Conn.Name=${sanitizedName(payload.name)}&Conn.Port=${payload.endpoint.port}&Conn.Username=${username(payload)}`; } +function dbxUrl(payload) { + const aliases = { postgresql: "postgres", sqlserver: "mssql", dameng: "dm", mariadb: "mysql" }; + const query = new URLSearchParams({ + name: sanitizedName(payload.name), + type: aliases[payload.protocol] || payload.protocol, + host: String(payload.endpoint?.host || ""), + port: String(payload.endpoint?.port || ""), + user: username(payload), + password: String(payload.token?.value || ""), + // Magnus routes Oracle connections by the token supplied as the service name. + database: payload.protocol === "oracle" ? username(payload) : String(payload.asset?.info?.db_name || ""), + one_time: "true" + }); + if (payload.protocol === "mongodb") { + query.set("url_params", "authSource=admin&loadBalanced=true&retryWrites=false"); + } + // The legacy argument splitter treats '*' as an argument separator, even inside quotes. + return `dbx://connection/new?${query.toString().replaceAll("*", "%2A")}`; +} + function valuesFor(payload): Record { return { name: sanitizedName(payload.name), @@ -36,6 +56,7 @@ function valuesFor(payload): Record { port: String(payload.endpoint?.port || ""), dbname: payload.protocol === "oracle" ? username(payload) : String(payload.asset?.info?.db_name || ""), dbeaver_protocol: payload.protocol === "sqlserver" ? "mssql_jdbc_ms_new" : payload.protocol, + dbx_url: dbxUrl(payload), url: navicatUrl(payload) }; } diff --git a/electron/tests/application-config.test.ts b/electron/tests/application-config.test.ts index 814b4e2d8..835d315cf 100644 --- a/electron/tests/application-config.test.ts +++ b/electron/tests/application-config.test.ts @@ -1,5 +1,7 @@ import assert from "node:assert/strict"; +import { once } from "node:events"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { createServer } from "node:net"; import os from "node:os"; import path from "node:path"; import test from "node:test"; @@ -11,6 +13,132 @@ import { systemFontInternals } from "../src/apps/system-fonts.ts"; const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +test( + "installed DBX initiates a database connection through the plugin launcher", + { + skip: !process.env.JMS_TEST_DBX_EXECUTABLE + }, + async (context) => { + const appData = await mkdtemp(path.join(os.tmpdir(), "jms-dbx-native-")); + context.after(() => rm(appData, { recursive: true, force: true })); + const server = createServer((socket) => socket.destroy()); + context.after(() => server.close()); + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + const app = { getPath: () => appData, isPackaged: false }; + const service = new ApplicationConfigService(app, projectRoot); + await service.initialize(); + await service.updateSelection({ + category: "databases", + protocol: "mysql", + name: "dbx", + pluginId: undefined, + path: process.env.JMS_TEST_DBX_EXECUTABLE + }); + const received = once(server, "connection", { signal: AbortSignal.timeout(8000) }); + const payload = { + protocol: "mysql", + client: "dbx", + name: `Luna DBX connection probe ${Date.now()}`, + endpoint: { host: "127.0.0.1", port: (server.address() as { port: number }).port }, + token: { id: "luna-probe", value: "test-only" }, + asset: { info: { db_name: "probe" } } + }; + const launcher = new LocalApplicationLauncher(app, projectRoot, service, null); + await Promise.all([received, launcher.launch(`jms2://${Buffer.from(JSON.stringify(payload)).toString("base64")}`)]); + } +); + +for (const platform of ["macos", "linux", "windows"]) { + test(`${platform} defaults to DBX and launches encoded, one-time database connections`, async (context) => { + const appData = await mkdtemp(path.join(os.tmpdir(), "jms-dbx-")); + context.after(() => rm(appData, { recursive: true, force: true })); + const app = { getPath: () => appData, isPackaged: false }; + const service = new ApplicationConfigService(app, projectRoot); + service.builtInDir = async () => path.join(projectRoot, "plugins", platform); + await service.initialize(); + const config = await service.getConfig(); + const dbx = config.databases.find((item) => item.name === "dbx"); + assert.ok(dbx); + assert.ok(config.databases.some((item) => item.name === "dbeaver")); + assert.equal(dbx.launch_type, "args"); + assert.equal(dbx.executable_type, "user_path"); + const types = { + mysql: "mysql", + mariadb: "mysql", + postgresql: "postgres", + oracle: "oracle", + sqlserver: "mssql", + dameng: "dm", + clickhouse: "clickhouse", + mongodb: "mongodb", + redis: "redis" + }; + assert.deepEqual(new Set(dbx.protocol), new Set(Object.keys(types))); + assert.deepEqual(dbx.match_first, dbx.protocol); + const launcher = new LocalApplicationLauncher(app, projectRoot, service, null); + for (const [protocol, type] of Object.entries(types)) { + const payload = { + protocol, + name: "测试 DBX &连接", + endpoint: { host: "2001:db8::1", port: 5525 }, + token: { id: "token&+/*'\"\\中文", value: " secret &one_time=false+?#%/*'\"\\中文 ", protocol }, + asset: { info: { db_name: protocol === "redis" ? "0" : "库 名&+/*'\"\\" } } + }; + let launches = 0; + launcher.launchExecutable = async (application, argumentString) => { + assert.equal(application.name, "dbx"); + const args = localAppLauncherInternals.splitArguments(argumentString); + assert.equal(args.length, 1); + const url = new URL(args[0]); + assert.equal(url.protocol, "dbx:"); + assert.equal(url.hostname, "connection"); + assert.equal(url.pathname, "/new"); + assert.deepEqual(Object.fromEntries(url.searchParams), { + name: "测试DBX&连接", + type, + host: payload.endpoint.host, + port: "5525", + user: payload.token.id, + password: payload.token.value, + database: protocol === "oracle" ? payload.token.id : payload.asset.info.db_name, + one_time: "true", + ...(protocol === "mongodb" ? { url_params: "authSource=admin&loadBalanced=true&retryWrites=false" } : {}) + }); + launches++; + }; + for (const client of [undefined, "dbx"]) { + await launcher.launch(`jms2://${Buffer.from(JSON.stringify({ ...payload, client })).toString("base64")}`); + } + if (protocol === "mariadb") { + await launcher.launch( + `jms2://${Buffer.from(JSON.stringify({ ...payload, protocol: "mysql" })).toString("base64")}` + ); + } + assert.equal(launches, protocol === "mariadb" ? 3 : 2); + } + + // Upgrades retain saved DBeaver paths, other defaults, and explicitly disabled protocols. + const preferences = { + version: 1, + selections: { + "databases:mysql": `${platform}.dbeaver`, + "databases:postgresql": `${platform}.${platform === "windows" ? "navicat17" : "terminal-db"}`, + "databases:redis": "" + }, + enabled_selections: { "databases:mysql": [`${platform}.dbeaver`] }, + plugins: { [`${platform}.dbeaver`]: { path: process.execPath, enabled: true } } + }; + await service.saveState(preferences); + const state = await service.loadState(); + for (const [key, value] of Object.entries(preferences.selections)) assert.equal(state.selections[key], value); + assert.deepEqual(state.plugins, preferences.plugins); + assert.deepEqual(state.enabled_selections, preferences.enabled_selections); + assert.equal((await launcher.resolveApplication({ protocol: "mysql" })).name, "dbeaver"); + await assert.rejects(launcher.resolveApplication({ protocol: "redis" }), /no configured application/); + }); +} + for (const [platform, protocol, expectedClient] of [ ["macos", "ssh", "terminal"], ["linux", "ssh", "terminal"], diff --git a/plugins/README.md b/plugins/README.md index ca524703c..b835158e0 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -29,6 +29,27 @@ macos.tigervnc/ 应用发现、选择和启动均由 Electron/Node 直接读取这里的插件配置。SSH helper 使用 Electron 自带的 Node 运行时,不再维护应用启动配置的副本。 +## 数据库客户端 + +新安装默认选择 DBX,DBeaver Community 继续作为可选插件提供。已有用户保存的客户端选择、 +禁用状态和可执行文件路径保持不变。使用 DBX 前请先安装桌面端,并在设置中确认其可执行文件路径; +便携版或自定义安装目录需要手动选择路径,Oracle、达梦需在 DBX 中安装对应驱动。 + +DBX 使用 `dbx://connection/new` 启动,连接参数采用 URL 编码,并设置 `one_time=true` +自动连接、断开后删除连接信息。用户名和密码使用 JumpServer 连接 Token,端点使用 Magnus 代理地址。 +Oracle 的 service name 沿用 Token ID,以便 Magnus 路由;协议别名与服务端 DBX applet 保持一致。 +MongoDB 沿用现有客户端的 `authSource=admin&loadBalanced=true&retryWrites=false` 代理参数。 +浏览器页面仍通过现有的 JumpServer 客户端唤起流程连接,由 Electron 启动本机 DBX。 + +可对已安装的 DBX 运行原生连接检查(macOS 示例): + +```sh +JMS_TEST_DBX_EXECUTABLE=/Applications/DBX.app/Contents/MacOS/dbx pnpm --dir electron exec node --import tsx --test --test-name-pattern='installed DBX' tests/application-config.test.ts +``` + +该检查通过完整插件启动链路发送一次性测试连接,使用临时配置和本地 TCP 监听器确认 DBX +确实发起连接,不需要 Magnus。它不验证代理鉴权或真实数据库查询。 + ## 文档 - [架构设计](../docs/plugins/DESIGN.md) diff --git a/plugins/linux/index.json b/plugins/linux/index.json index ddac84002..fe925e6a1 100644 --- a/plugins/linux/index.json +++ b/plugins/linux/index.json @@ -1,6 +1,11 @@ { "version": 1, "plugins": [ + { + "id": "linux.dbx", + "name": "dbx", + "category": "databases" + }, { "id": "linux.dbeaver", "name": "dbeaver", diff --git a/plugins/linux/linux.dbx/connect.json b/plugins/linux/linux.dbx/connect.json new file mode 100644 index 000000000..9049f019f --- /dev/null +++ b/plugins/linux/linux.dbx/connect.json @@ -0,0 +1,17 @@ +{ + "platform": "linux", + "match_first": [], + "is_default": true, + "is_set": false, + "is_internal": false, + "executable": { + "type": "user_path", + "default": "/usr/bin/dbx", + "required": true + }, + "launch": { + "type": "args", + "template": "\"{dbx_url}\"" + }, + "display_name": "DBX" +} diff --git a/plugins/linux/linux.dbx/icon.png b/plugins/linux/linux.dbx/icon.png new file mode 100644 index 000000000..23c91bcfa Binary files /dev/null and b/plugins/linux/linux.dbx/icon.png differ diff --git a/plugins/linux/linux.dbx/manifest.json b/plugins/linux/linux.dbx/manifest.json new file mode 100644 index 000000000..63859601d --- /dev/null +++ b/plugins/linux/linux.dbx/manifest.json @@ -0,0 +1,17 @@ +{ + "id": "linux.dbx", + "name": "dbx", + "display_name": "DBX", + "version": "1.0.0", + "min_client_version": "5.0.0", + "author": "JumpServer", + "homepage": "https://dbxio.com", + "download_url": "https://dbxio.com", + "category": "databases", + "protocols": ["mysql", "mariadb", "postgresql", "oracle", "sqlserver", "dameng", "clickhouse", "mongodb", "redis"], + "builtin": true, + "comment": { + "zh": "轻量级跨平台数据库客户端,集成连接管理、SQL 编辑、数据表格和结构工具。请先安装 DBX,确认可执行文件路径后启用;Oracle、达梦需要在 DBX 中安装对应驱动。", + "en": "Lightweight cross-platform database client with connection management, SQL editing, data grids and schema tools. Install DBX and verify the executable path before enabling. Oracle and Dameng require their drivers in DBX." + } +} diff --git a/plugins/linux/plugins-state.defaults.json b/plugins/linux/plugins-state.defaults.json index 3d2a84277..5bc25edc6 100644 --- a/plugins/linux/plugins-state.defaults.json +++ b/plugins/linux/plugins-state.defaults.json @@ -4,12 +4,15 @@ "terminal:ssh": "linux.terminal", "terminal:telnet": "linux.terminal", "remotedesktop:vnc": "linux.tigervnc", - "databases:oracle": "linux.terminal-db", - "databases:mysql": "linux.terminal-db", - "databases:postgresql": "linux.terminal-db", - "databases:mariadb": "linux.terminal-db", - "databases:redis": "linux.terminal-db", - "databases:mongodb": "linux.terminal-db" + "databases:oracle": "linux.dbx", + "databases:mysql": "linux.dbx", + "databases:postgresql": "linux.dbx", + "databases:mariadb": "linux.dbx", + "databases:redis": "linux.dbx", + "databases:mongodb": "linux.dbx", + "databases:sqlserver": "linux.dbx", + "databases:dameng": "linux.dbx", + "databases:clickhouse": "linux.dbx" }, "plugins": {} } diff --git a/plugins/macos/index.json b/plugins/macos/index.json index c2a8537b0..4f53db498 100644 --- a/plugins/macos/index.json +++ b/plugins/macos/index.json @@ -6,6 +6,11 @@ "name": "another_redis", "category": "databases" }, + { + "id": "macos.dbx", + "name": "dbx", + "category": "databases" + }, { "id": "macos.dbeaver", "name": "dbeaver", diff --git a/plugins/macos/macos.dbx/connect.json b/plugins/macos/macos.dbx/connect.json new file mode 100644 index 000000000..6e7ec3176 --- /dev/null +++ b/plugins/macos/macos.dbx/connect.json @@ -0,0 +1,17 @@ +{ + "platform": "macos", + "match_first": [], + "is_default": true, + "is_set": false, + "is_internal": false, + "executable": { + "type": "user_path", + "default": "/Applications/DBX.app/Contents/MacOS/dbx", + "required": true + }, + "launch": { + "type": "args", + "template": "\"{dbx_url}\"" + }, + "display_name": "DBX" +} diff --git a/plugins/macos/macos.dbx/icon.png b/plugins/macos/macos.dbx/icon.png new file mode 100644 index 000000000..23c91bcfa Binary files /dev/null and b/plugins/macos/macos.dbx/icon.png differ diff --git a/plugins/macos/macos.dbx/manifest.json b/plugins/macos/macos.dbx/manifest.json new file mode 100644 index 000000000..e8fd1bbc4 --- /dev/null +++ b/plugins/macos/macos.dbx/manifest.json @@ -0,0 +1,17 @@ +{ + "id": "macos.dbx", + "name": "dbx", + "display_name": "DBX", + "version": "1.0.0", + "min_client_version": "5.0.0", + "author": "JumpServer", + "homepage": "https://dbxio.com", + "download_url": "https://dbxio.com", + "category": "databases", + "protocols": ["mysql", "mariadb", "postgresql", "oracle", "sqlserver", "dameng", "clickhouse", "mongodb", "redis"], + "builtin": true, + "comment": { + "zh": "轻量级跨平台数据库客户端,集成连接管理、SQL 编辑、数据表格和结构工具。请先安装 DBX,确认可执行文件路径后启用;Oracle、达梦需要在 DBX 中安装对应驱动。", + "en": "Lightweight cross-platform database client with connection management, SQL editing, data grids and schema tools. Install DBX and verify the executable path before enabling. Oracle and Dameng require their drivers in DBX." + } +} diff --git a/plugins/macos/plugins-state.defaults.json b/plugins/macos/plugins-state.defaults.json index 150980bd3..2021226de 100644 --- a/plugins/macos/plugins-state.defaults.json +++ b/plugins/macos/plugins-state.defaults.json @@ -6,13 +6,15 @@ "remotedesktop:rdp": "macos.mstsc", "remotedesktop:vnc": "macos.tigervnc", "filetransfer:sftp": "macos.iterm-sftp", - "databases:oracle": "macos.terminal-db", - "databases:mysql": "macos.terminal-db", - "databases:postgresql": "macos.terminal-db", - "databases:mariadb": "macos.terminal-db", - "databases:redis": "macos.terminal-db", - "databases:sqlserver": "macos.terminal-db", - "databases:mongodb": "macos.terminal-db" + "databases:oracle": "macos.dbx", + "databases:mysql": "macos.dbx", + "databases:postgresql": "macos.dbx", + "databases:mariadb": "macos.dbx", + "databases:redis": "macos.dbx", + "databases:sqlserver": "macos.dbx", + "databases:mongodb": "macos.dbx", + "databases:dameng": "macos.dbx", + "databases:clickhouse": "macos.dbx" }, "plugins": {} } diff --git a/plugins/schema/manifest.schema.json b/plugins/schema/manifest.schema.json index 1491b6258..a4d97f9ff 100644 --- a/plugins/schema/manifest.schema.json +++ b/plugins/schema/manifest.schema.json @@ -18,7 +18,7 @@ "id": { "type": "string", "pattern": "^[a-z][a-z0-9]*(\\.[a-z][a-z0-9]*)+$", - "description": "Globally unique plugin id, e.g. builtin.dbeaver (built-in) or com.example.xshell (third-party)" + "description": "Globally unique plugin id, e.g. windows.dbx (built-in) or com.example.xshell (third-party)" }, "name": { "type": "string", diff --git a/plugins/windows/index.json b/plugins/windows/index.json index 82531ee53..9792bebad 100644 --- a/plugins/windows/index.json +++ b/plugins/windows/index.json @@ -6,6 +6,11 @@ "name": "another_redis", "category": "databases" }, + { + "id": "windows.dbx", + "name": "dbx", + "category": "databases" + }, { "id": "windows.dbeaver", "name": "dbeaver", diff --git a/plugins/windows/plugins-state.defaults.json b/plugins/windows/plugins-state.defaults.json index c05b4d359..9c9e4dcd2 100644 --- a/plugins/windows/plugins-state.defaults.json +++ b/plugins/windows/plugins-state.defaults.json @@ -3,7 +3,16 @@ "selections": { "terminal:ssh": "windows.putty", "terminal:telnet": "windows.putty", - "remotedesktop:rdp": "windows.mstsc" + "remotedesktop:rdp": "windows.mstsc", + "databases:mysql": "windows.dbx", + "databases:mariadb": "windows.dbx", + "databases:postgresql": "windows.dbx", + "databases:oracle": "windows.dbx", + "databases:sqlserver": "windows.dbx", + "databases:dameng": "windows.dbx", + "databases:clickhouse": "windows.dbx", + "databases:mongodb": "windows.dbx", + "databases:redis": "windows.dbx" }, "plugins": {} } diff --git a/plugins/windows/windows.dbx/connect.json b/plugins/windows/windows.dbx/connect.json new file mode 100644 index 000000000..720b9f082 --- /dev/null +++ b/plugins/windows/windows.dbx/connect.json @@ -0,0 +1,17 @@ +{ + "platform": "windows", + "match_first": [], + "is_default": true, + "is_set": false, + "is_internal": false, + "executable": { + "type": "user_path", + "default": "C:\\Program Files\\DBX\\DBX.exe", + "required": true + }, + "launch": { + "type": "args", + "template": "\"{dbx_url}\"" + }, + "display_name": "DBX" +} diff --git a/plugins/windows/windows.dbx/icon.png b/plugins/windows/windows.dbx/icon.png new file mode 100644 index 000000000..23c91bcfa Binary files /dev/null and b/plugins/windows/windows.dbx/icon.png differ diff --git a/plugins/windows/windows.dbx/manifest.json b/plugins/windows/windows.dbx/manifest.json new file mode 100644 index 000000000..66459482c --- /dev/null +++ b/plugins/windows/windows.dbx/manifest.json @@ -0,0 +1,17 @@ +{ + "id": "windows.dbx", + "name": "dbx", + "display_name": "DBX", + "version": "1.0.0", + "min_client_version": "5.0.0", + "author": "JumpServer", + "homepage": "https://dbxio.com", + "download_url": "https://dbxio.com", + "category": "databases", + "protocols": ["mysql", "mariadb", "postgresql", "oracle", "sqlserver", "dameng", "clickhouse", "mongodb", "redis"], + "builtin": true, + "comment": { + "zh": "轻量级跨平台数据库客户端,集成连接管理、SQL 编辑、数据表格和结构工具。请先安装 DBX,确认可执行文件路径后启用;Oracle、达梦需要在 DBX 中安装对应驱动。", + "en": "Lightweight cross-platform database client with connection management, SQL editing, data grids and schema tools. Install DBX and verify the executable path before enabling. Oracle and Dameng require their drivers in DBX." + } +} diff --git a/ui/assets/images/dbx.png b/ui/assets/images/dbx.png new file mode 100644 index 000000000..23c91bcfa Binary files /dev/null and b/ui/assets/images/dbx.png differ diff --git a/ui/components/SettingItems/settingItems.vue b/ui/components/SettingItems/settingItems.vue index f53dd4c53..06ce891df 100644 --- a/ui/components/SettingItems/settingItems.vue +++ b/ui/components/SettingItems/settingItems.vue @@ -27,6 +27,7 @@ const imagesMap: Record = { builtin_client: getImageByName("terminal"), iterm: getImageByName("item2"), dbeaver: getImageByName("dbeaver"), + dbx: getImageByName("dbx"), heidisql: getImageByName("heidisql"), mstsc: getImageByName("mstsc"), terminal: getImageByName("terminal"),