Skip to content

Commit d39e6df

Browse files
committed
feat: add DBX as the default database client plugin
1 parent 48e0b9b commit d39e6df

22 files changed

Lines changed: 321 additions & 18 deletions

‎docs/plugins/DESIGN.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ my-terminal-plugin/
149149

150150
| type | 说明 | 典型场景 |
151151
| -------- | -------------------------------------------- | ----------------------- |
152-
| `args` | 模板替换后作为命令行参数 | PuTTY、DBeaver |
152+
| `args` | 模板替换后作为命令行参数 | PuTTY、DBX、DBeaver |
153153
| `script` | 执行 `scripts/` 下平台脚本,传入 JSON 上下文 | iTerm2、复杂 GUI 自动化 |
154154
| `url` | 构建 URL Scheme 并 `open` | Navicat |
155155
| `file` | 先写临时文件再打开 | RDP `.rdp` |
@@ -168,6 +168,7 @@ my-terminal-plugin/
168168
| `{port}` | 端口 |
169169
| `{file}` | 临时文件路径(`file` 类型) |
170170
| `{dbname}` | 数据库名 |
171+
| `{dbx_url}` | URL 编码的 DBX 一次性连接链接 |
171172
| `{use_ssl}` | 是否 SSL |
172173
| `{allow_invalid_cert}` | 是否允许无效证书 |
173174

@@ -180,7 +181,7 @@ my-terminal-plugin/
180181
"version": 1,
181182
"selections": {
182183
"terminal:ssh": "builtin.putty",
183-
"databases:mysql": "builtin.dbeaver"
184+
"databases:mysql": "windows.dbx"
184185
},
185186
"plugins": {
186187
"com.example.xshell": {
@@ -320,6 +321,6 @@ sequenceDiagram
320321
| macOS | remotedesktop | 系统 RDP(open) |
321322
| Linux | terminal | 系统 terminal |
322323
| Linux | remotedesktop | xfreerdp, tigervnc |
323-
| 全平台 | databases | dbeaver(需用户配路径) |
324+
| 全平台 | databases | dbx(默认)、dbeaver(可选,需配置本机路径) |
324325

325326
其余(XShell、Navicat、MobaXterm 等)以**可选插件**形式提供下载或用户自行打包安装。

‎electron/src/apps/local-app-launcher.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,26 @@ function navicatUrl(payload) {
2626
return `navicat://conn.${protocol}?Conn.Host=${payload.endpoint.host}&Conn.Name=${sanitizedName(payload.name)}&Conn.Port=${payload.endpoint.port}&Conn.Username=${username(payload)}`;
2727
}
2828

29+
function dbxUrl(payload) {
30+
const aliases = { postgresql: "postgres", sqlserver: "mssql", dameng: "dm", mariadb: "mysql" };
31+
const query = new URLSearchParams({
32+
name: sanitizedName(payload.name),
33+
type: aliases[payload.protocol] || payload.protocol,
34+
host: String(payload.endpoint?.host || ""),
35+
port: String(payload.endpoint?.port || ""),
36+
user: username(payload),
37+
password: String(payload.token?.value || ""),
38+
// Magnus routes Oracle connections by the token supplied as the service name.
39+
database: payload.protocol === "oracle" ? username(payload) : String(payload.asset?.info?.db_name || ""),
40+
one_time: "true"
41+
});
42+
if (payload.protocol === "mongodb") {
43+
query.set("url_params", "authSource=admin&loadBalanced=true&retryWrites=false");
44+
}
45+
// The legacy argument splitter treats '*' as an argument separator, even inside quotes.
46+
return `dbx://connection/new?${query.toString().replaceAll("*", "%2A")}`;
47+
}
48+
2949
function valuesFor(payload): Record<string, string> {
3050
return {
3151
name: sanitizedName(payload.name),
@@ -36,6 +56,7 @@ function valuesFor(payload): Record<string, string> {
3656
port: String(payload.endpoint?.port || ""),
3757
dbname: payload.protocol === "oracle" ? username(payload) : String(payload.asset?.info?.db_name || ""),
3858
dbeaver_protocol: payload.protocol === "sqlserver" ? "mssql_jdbc_ms_new" : payload.protocol,
59+
dbx_url: dbxUrl(payload),
3960
url: navicatUrl(payload)
4061
};
4162
}

‎electron/tests/application-config.test.ts‎

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import assert from "node:assert/strict";
2+
import { once } from "node:events";
23
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4+
import { createServer } from "node:net";
35
import os from "node:os";
46
import path from "node:path";
57
import test from "node:test";
@@ -11,6 +13,132 @@ import { systemFontInternals } from "../src/apps/system-fonts.ts";
1113

1214
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
1315

16+
test(
17+
"installed DBX initiates a database connection through the plugin launcher",
18+
{
19+
skip: !process.env.JMS_TEST_DBX_EXECUTABLE
20+
},
21+
async (context) => {
22+
const appData = await mkdtemp(path.join(os.tmpdir(), "jms-dbx-native-"));
23+
context.after(() => rm(appData, { recursive: true, force: true }));
24+
const server = createServer((socket) => socket.destroy());
25+
context.after(() => server.close());
26+
server.listen(0, "127.0.0.1");
27+
await once(server, "listening");
28+
const app = { getPath: () => appData, isPackaged: false };
29+
const service = new ApplicationConfigService(app, projectRoot);
30+
await service.initialize();
31+
await service.updateSelection({
32+
category: "databases",
33+
protocol: "mysql",
34+
name: "dbx",
35+
pluginId: undefined,
36+
path: process.env.JMS_TEST_DBX_EXECUTABLE
37+
});
38+
const received = once(server, "connection", { signal: AbortSignal.timeout(8000) });
39+
const payload = {
40+
protocol: "mysql",
41+
client: "dbx",
42+
name: `Luna DBX connection probe ${Date.now()}`,
43+
endpoint: { host: "127.0.0.1", port: (server.address() as { port: number }).port },
44+
token: { id: "luna-probe", value: "test-only" },
45+
asset: { info: { db_name: "probe" } }
46+
};
47+
const launcher = new LocalApplicationLauncher(app, projectRoot, service, null);
48+
await Promise.all([received, launcher.launch(`jms2://${Buffer.from(JSON.stringify(payload)).toString("base64")}`)]);
49+
}
50+
);
51+
52+
for (const platform of ["macos", "linux", "windows"]) {
53+
test(`${platform} defaults to DBX and launches encoded, one-time database connections`, async (context) => {
54+
const appData = await mkdtemp(path.join(os.tmpdir(), "jms-dbx-"));
55+
context.after(() => rm(appData, { recursive: true, force: true }));
56+
const app = { getPath: () => appData, isPackaged: false };
57+
const service = new ApplicationConfigService(app, projectRoot);
58+
service.builtInDir = async () => path.join(projectRoot, "plugins", platform);
59+
await service.initialize();
60+
const config = await service.getConfig();
61+
const dbx = config.databases.find((item) => item.name === "dbx");
62+
assert.ok(dbx);
63+
assert.ok(config.databases.some((item) => item.name === "dbeaver"));
64+
assert.equal(dbx.launch_type, "args");
65+
assert.equal(dbx.executable_type, "user_path");
66+
const types = {
67+
mysql: "mysql",
68+
mariadb: "mysql",
69+
postgresql: "postgres",
70+
oracle: "oracle",
71+
sqlserver: "mssql",
72+
dameng: "dm",
73+
clickhouse: "clickhouse",
74+
mongodb: "mongodb",
75+
redis: "redis"
76+
};
77+
assert.deepEqual(new Set(dbx.protocol), new Set(Object.keys(types)));
78+
assert.deepEqual(dbx.match_first, dbx.protocol);
79+
const launcher = new LocalApplicationLauncher(app, projectRoot, service, null);
80+
for (const [protocol, type] of Object.entries(types)) {
81+
const payload = {
82+
protocol,
83+
name: "测试 DBX &连接",
84+
endpoint: { host: "2001:db8::1", port: 5525 },
85+
token: { id: "token&+/*'\"\\中文", value: " secret &one_time=false+?#%/*'\"\\中文 ", protocol },
86+
asset: { info: { db_name: protocol === "redis" ? "0" : "库 名&+/*'\"\\" } }
87+
};
88+
let launches = 0;
89+
launcher.launchExecutable = async (application, argumentString) => {
90+
assert.equal(application.name, "dbx");
91+
const args = localAppLauncherInternals.splitArguments(argumentString);
92+
assert.equal(args.length, 1);
93+
const url = new URL(args[0]);
94+
assert.equal(url.protocol, "dbx:");
95+
assert.equal(url.hostname, "connection");
96+
assert.equal(url.pathname, "/new");
97+
assert.deepEqual(Object.fromEntries(url.searchParams), {
98+
name: "测试DBX&连接",
99+
type,
100+
host: payload.endpoint.host,
101+
port: "5525",
102+
user: payload.token.id,
103+
password: payload.token.value,
104+
database: protocol === "oracle" ? payload.token.id : payload.asset.info.db_name,
105+
one_time: "true",
106+
...(protocol === "mongodb" ? { url_params: "authSource=admin&loadBalanced=true&retryWrites=false" } : {})
107+
});
108+
launches++;
109+
};
110+
for (const client of [undefined, "dbx"]) {
111+
await launcher.launch(`jms2://${Buffer.from(JSON.stringify({ ...payload, client })).toString("base64")}`);
112+
}
113+
if (protocol === "mariadb") {
114+
await launcher.launch(
115+
`jms2://${Buffer.from(JSON.stringify({ ...payload, protocol: "mysql" })).toString("base64")}`
116+
);
117+
}
118+
assert.equal(launches, protocol === "mariadb" ? 3 : 2);
119+
}
120+
121+
// Upgrades retain saved DBeaver paths, other defaults, and explicitly disabled protocols.
122+
const preferences = {
123+
version: 1,
124+
selections: {
125+
"databases:mysql": `${platform}.dbeaver`,
126+
"databases:postgresql": `${platform}.${platform === "windows" ? "navicat17" : "terminal-db"}`,
127+
"databases:redis": ""
128+
},
129+
enabled_selections: { "databases:mysql": [`${platform}.dbeaver`] },
130+
plugins: { [`${platform}.dbeaver`]: { path: process.execPath, enabled: true } }
131+
};
132+
await service.saveState(preferences);
133+
const state = await service.loadState();
134+
for (const [key, value] of Object.entries(preferences.selections)) assert.equal(state.selections[key], value);
135+
assert.deepEqual(state.plugins, preferences.plugins);
136+
assert.deepEqual(state.enabled_selections, preferences.enabled_selections);
137+
assert.equal((await launcher.resolveApplication({ protocol: "mysql" })).name, "dbeaver");
138+
await assert.rejects(launcher.resolveApplication({ protocol: "redis" }), /no configured application/);
139+
});
140+
}
141+
14142
for (const [platform, protocol, expectedClient] of [
15143
["macos", "ssh", "terminal"],
16144
["linux", "ssh", "terminal"],

‎plugins/README.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,27 @@ macos.tigervnc/
2929
应用发现、选择和启动均由 Electron/Node 直接读取这里的插件配置。SSH helper 使用
3030
Electron 自带的 Node 运行时,不再维护应用启动配置的副本。
3131

32+
## 数据库客户端
33+
34+
新安装默认选择 DBX,DBeaver Community 继续作为可选插件提供。已有用户保存的客户端选择、
35+
禁用状态和可执行文件路径保持不变。使用 DBX 前请先安装桌面端,并在设置中确认其可执行文件路径;
36+
便携版或自定义安装目录需要手动选择路径,Oracle、达梦需在 DBX 中安装对应驱动。
37+
38+
DBX 使用 `dbx://connection/new` 启动,连接参数采用 URL 编码,并设置 `one_time=true`
39+
自动连接、断开后删除连接信息。用户名和密码使用 JumpServer 连接 Token,端点使用 Magnus 代理地址。
40+
Oracle 的 service name 沿用 Token ID,以便 Magnus 路由;协议别名与服务端 DBX applet 保持一致。
41+
MongoDB 沿用现有客户端的 `authSource=admin&loadBalanced=true&retryWrites=false` 代理参数。
42+
浏览器页面仍通过现有的 JumpServer 客户端唤起流程连接,由 Electron 启动本机 DBX。
43+
44+
可对已安装的 DBX 运行原生连接检查(macOS 示例):
45+
46+
```sh
47+
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
48+
```
49+
50+
该检查通过完整插件启动链路发送一次性测试连接,使用临时配置和本地 TCP 监听器确认 DBX
51+
确实发起连接,不需要 Magnus。它不验证代理鉴权或真实数据库查询。
52+
3253
## 文档
3354

3455
- [架构设计](../docs/plugins/DESIGN.md)

‎plugins/linux/index.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
{
22
"version": 1,
33
"plugins": [
4+
{
5+
"id": "linux.dbx",
6+
"name": "dbx",
7+
"category": "databases"
8+
},
49
{
510
"id": "linux.dbeaver",
611
"name": "dbeaver",
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"platform": "linux",
3+
"match_first": [],
4+
"is_default": true,
5+
"is_set": false,
6+
"is_internal": false,
7+
"executable": {
8+
"type": "user_path",
9+
"default": "/usr/bin/dbx",
10+
"required": true
11+
},
12+
"launch": {
13+
"type": "args",
14+
"template": "\"{dbx_url}\""
15+
},
16+
"display_name": "DBX"
17+
}

‎plugins/linux/linux.dbx/icon.png‎

86.4 KB
Loading
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"id": "linux.dbx",
3+
"name": "dbx",
4+
"display_name": "DBX",
5+
"version": "1.0.0",
6+
"min_client_version": "5.0.0",
7+
"author": "JumpServer",
8+
"homepage": "https://dbxio.com",
9+
"download_url": "https://dbxio.com",
10+
"category": "databases",
11+
"protocols": ["mysql", "mariadb", "postgresql", "oracle", "sqlserver", "dameng", "clickhouse", "mongodb", "redis"],
12+
"builtin": true,
13+
"comment": {
14+
"zh": "轻量级跨平台数据库客户端,集成连接管理、SQL 编辑、数据表格和结构工具。请先安装 DBX,确认可执行文件路径后启用;Oracle、达梦需要在 DBX 中安装对应驱动。",
15+
"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."
16+
}
17+
}

‎plugins/linux/plugins-state.defaults.json‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
"terminal:ssh": "linux.terminal",
55
"terminal:telnet": "linux.terminal",
66
"remotedesktop:vnc": "linux.tigervnc",
7-
"databases:oracle": "linux.terminal-db",
8-
"databases:mysql": "linux.terminal-db",
9-
"databases:postgresql": "linux.terminal-db",
10-
"databases:mariadb": "linux.terminal-db",
11-
"databases:redis": "linux.terminal-db",
12-
"databases:mongodb": "linux.terminal-db"
7+
"databases:oracle": "linux.dbx",
8+
"databases:mysql": "linux.dbx",
9+
"databases:postgresql": "linux.dbx",
10+
"databases:mariadb": "linux.dbx",
11+
"databases:redis": "linux.dbx",
12+
"databases:mongodb": "linux.dbx",
13+
"databases:sqlserver": "linux.dbx",
14+
"databases:dameng": "linux.dbx",
15+
"databases:clickhouse": "linux.dbx"
1316
},
1417
"plugins": {}
1518
}

‎plugins/macos/index.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
"name": "another_redis",
77
"category": "databases"
88
},
9+
{
10+
"id": "macos.dbx",
11+
"name": "dbx",
12+
"category": "databases"
13+
},
914
{
1015
"id": "macos.dbeaver",
1116
"name": "dbeaver",

0 commit comments

Comments
 (0)