dsh-db-tool 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,9 +38,29 @@ dsh plugin --profile web add "link:E:\path\to\dsh-db-tool"
38
38
 
39
39
  GaussDB 官方驱动未发布 npm,需先构建 vendor:`npm run build:gaussdb`(PowerShell)或 `bash scripts/build-gaussdb.sh`;运行要求 Node ≥ 22.12(产物依赖 p-limit@7 纯 ESM)。oracledb 安装脚本需 `npm approve-scripts oracledb`。mongodb 驱动已 bundle 进发布包(`vendor/mongodb-driver.cjs`,esbuild 构建,`npm run build:mongodb` 可重建),依赖树中不含 mongodb/punycode,详见 [docs/install.md](docs/install.md)。
40
40
 
41
+ ### 桌面端安装
42
+
43
+ DSH 桌面端 0.2.0-rc.2+ 捆绑了 `dsh` 命令,全程无需另装 Node 或 pnpm:
44
+
45
+ 1. **首次使用**:打开桌面端菜单栏,点 **"Manage dsh command"(管理 dsh 命令)**,安装捆绑的 CLI——该操作把 `dsh` 命令注册到系统 PATH。
46
+ 2. **安装插件**(必须钉精确版本:`@latest` 会因 release-age 校验回落到旧版):
47
+
48
+ ```bash
49
+ dsh plugin --profile desktop add dsh-db-tool@1.3.1 --registry=https://registry.npmjs.org/
50
+ ```
51
+
52
+ 已有 npm 镜像源偏好的用户,可把 `--registry` 替换为自己的镜像地址。
53
+ 3. **重启桌面端**生效。
54
+
55
+ 桌面端补充说明:
56
+
57
+ - **免补丁路径**:桌面端宿主代码打包在 `app.asar` 内,无法应用 `patch:dsh` 热补丁;但本插件 0.1.9+ 走免补丁路径(mongodb 驱动已 bundle 为 `vendor/mongodb-driver.cjs` 随包发布,依赖树不含 punycode),桌面端可直接安装加载,无需任何补丁。
58
+ - **GaussDB**:npm 发布包不含 GaussDB vendor 驱动。需要 GaussDB 的用户请改从源码安装(`dsh plugin --profile desktop add "dsh-db-tool@github:mengqi1436/dsh-db-tool"`),并在插件目录自行 `npm run build:gaussdb`(要求 Node ≥ 22.12)。
59
+ - **Oracle**:安装后需 `npm approve-scripts oracledb` 放行 oracledb 安装脚本(或按 DSH 插件安装界面的脚本审批提示放行)。
60
+
41
61
  ## 安全模型
42
62
 
43
- - HTTP API 仅同源(`ctx.webServer` prefix `/dsh-db-tool/api`)+ loopback/Origin trust 校验(等价 ssh-tunnel `http-trust`),body 限 2MB
63
+ - HTTP API 仅同源(`ctx.webServer` prefix `/dsh-db-tool/api`)+ 宿主 Connection 栅栏(`ctx.connection.requestRejection`:Host/Origin + 浏览器 token 鉴权。0.2.0-rc.1 起插件前缀路由不经宿主栅栏,故在 handler 顶层显式应用;宿主无 connection 服务时回退 loopback/Origin trust 校验,等价 ssh-tunnel `http-trust`),body 限 2MB
44
64
  - ro 双保险:服务层拦截 + 驱动会话级 `SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY`(pg/gaussdb)、readonly 打开(SQLite)
45
65
  - SQL 参数绑定 + 标识符白名单(`[A-Za-z0-9_$]+` + 引用包裹);MySQL `multipleStatements:false`;Redis 元数据走 `SCAN`(禁 `KEYS`);Mongo 递归拒 `$where`
46
66
  - `run_script`:node:vm 独立 context、60s 超时、无 require/process/网络/文件系统,仅注入受限 `db.{query,execute}` 句柄
@@ -52,7 +72,7 @@ GaussDB 官方驱动未发布 npm,需先构建 vendor:`npm run build:gaussdb
52
72
 
53
73
  症状:DSH 启动后插件加载报 `failed to import`,伴随 `TypeError: Cannot read properties of null (reading 'Symbol(Symbol.iterator)')`,栈指向 `dsh-app-boot` 的 `routeScoped`。
54
74
 
55
- 根因:上游 `@deepseek-ai/dsh-app-boot` 对 `createRequire(parent).resolve.paths(name)` 直接做 `for..of`,而 Node 对 core-module 同名包(`punycode` 等)返回 `null`,hoisted profile 下凡依赖树含此类 npm 包的插件都会炸。`0.1.7-rc.2` 中共两处同型缺陷:`routeScoped()` 与 `nativePackageDir()`,本补丁一并修复(两处分别独立判定,已打过旧版单处补丁的机器只会补上缺的那处)。
75
+ 根因:上游 `@deepseek-ai/dsh-app-boot` 对 `createRequire(parent).resolve.paths(name)` 直接做 `for..of`,而 Node 对 core-module 同名包(`punycode` 等)返回 `null`,hoisted profile 下凡依赖树含此类 npm 包的插件都会炸。`0.1.7-rc.2`、`0.2.0-rc.1` 与 `0.2.0-rc.2` 中同型缺陷两处(`0.2.0-rc.2` 的 `lib/index.js` 与 `0.2.0-rc.1` 逐字节相同,尚未修复;锚点行均与 `0.1.7-rc.2` 一致,仅整体行号偏移 1 行):`routeScoped()` 与 `nativePackageDir()`,本补丁一并修复(两处分别独立判定,已打过旧版单处补丁的机器只会补上缺的那处)。
56
76
 
57
77
  一键修复(幂等,应用前自动备份为 `index.js.bak-hotfix`;`--revert` 可还原):
58
78
 
@@ -61,7 +81,7 @@ npm run patch:dsh # Windows(PowerShell)
61
81
  npm run patch:dsh:sh # macOS / Linux
62
82
  ```
63
83
 
64
- 脚本自动探测 DSH 安装根(`--dsh-root` 可显式指定);补丁文件见 `patches/dsh-app-boot-route-scoped-hotfix.patch`,仅对 `0.1.7-rc.2` 声明兼容,其他版本会警告(`--force` 覆盖)。上游 issue:<https://github.com/mengqi1436/dsh-db-tool/issues>(占位,待上游仓库开放后替换)。
84
+ 脚本自动探测 DSH 安装根(`--dsh-root` 可显式指定);补丁文件按安装版本自动选择:`patches/dsh-app-boot-route-scoped-hotfix.patch`(`0.1.7-rc.2`)、`patches/dsh-app-boot-route-scoped-hotfix-0.2.0-rc.1.patch`(`0.2.0-rc.1`)、`patches/dsh-app-boot-route-scoped-hotfix-0.2.0-rc.2.patch`(`0.2.0-rc.2`,其 `lib/index.js` 与 `0.2.0-rc.1` 逐字节相同,故 hunk 完全一致;均与内建替换路径逐字节等价已验证);不支持的版本会警告(`--force` 覆盖)。上游 issue:<https://github.com/mengqi1436/dsh-db-tool/issues>(占位,待上游仓库开放后替换)。
65
85
 
66
86
  **本插件的免补丁路径(0.1.9+)**:该 bug 的触发条件是依赖树中出现 core-module 同名包,mongodb 链(`mongodb-connection-string-url → whatwg-url → tr46 → punycode`)正是元凶。0.1.9 起 mongodb 驱动经 esbuild bundle 为 `vendor/mongodb-driver.cjs` 随包发布,生产依赖树不再含 mongodb 与 punycode——在**未修复宿主**(含桌面版 app.asar,无法打补丁)上也能正常安装加载。守卫测试 `tests/guard/deps-core-collision.spec.ts` 断言依赖树永不回退(dependencies 白名单 + lockfile 生产树 core 同名包零交集)。`patch:dsh` 仍保留,用于修复**其他**含同类依赖的插件或宿主自身报错。
67
87
 
package/dist/index.js CHANGED
@@ -4,15 +4,17 @@
4
4
  * - 注册 DatabaseManager 模型工具(单工具多 action),projectPath 取自
5
5
  * session header.cwd(不信任模型自报),业务操作经 GrantStore 授权。
6
6
  * - HTTP API 挂载在 DSH webServer(prefix /dsh-db-tool/api):前端同源
7
- * 相对路径 fetch('/dsh-db-tool/api/...') 即可达;trust 校验移植
8
- * http-trust 逻辑(Host loopback / trustedHosts + Origin 同源校验)。
7
+ * 相对路径 fetch('/dsh-db-tool/api/...') 即可达;0.2.0-rc.1 起插件前缀路由
8
+ * 不经宿主 Connection 栅栏,故在 handler 顶层先应用 ctx.connection.requestRejection
9
+ * (Host/Origin + 浏览器 token 鉴权,401/403);宿主无 connection 服务时退回
10
+ * http-trust 本地校验(Host loopback / trustedHosts + Origin 同源校验)。
9
11
  * - ctx.effect 收尾:注销 HTTP 路由、关适配器、停 challenge 清理。
10
12
  */
11
13
  import { DbToolStore, normalizeProjectKey } from './store/index.js';
12
14
  import { DbToolService, handleToolAction } from './manager.js';
13
15
  import { handleDbToolRequest } from './http/index.js';
14
- /** cordis 注入清单(服务名以 dsh-ssh-tunnel 为准) */
15
- export const inject = ['webServer', 'sessions', 'tools', 'webRuntime'];
16
+ /** cordis 注入清单(服务名以 dsh-ssh-tunnel 为准;connection 用于宿主鉴权栅栏) */
17
+ export const inject = ['webServer', 'sessions', 'tools', 'webRuntime', 'connection'];
16
18
  const TOOL_DESCRIPTION = `数据库管理工具(dsh-db-tool):在已配置的数据库连接上执行查询/写操作/脚本。
17
19
 
18
20
  权限模型(务必遵守):
@@ -50,6 +52,21 @@ export function apply(ctx) {
50
52
  kind: 'prefix',
51
53
  path: '/dsh-db-tool/api',
52
54
  handler: async (req, res) => {
55
+ // 宿主 Connection 栅栏优先:0.2.0-rc.1 起插件前缀路由不经宿主鉴权,
56
+ // 必须显式应用 requestRejection(Host/Origin + 浏览器 token);
57
+ // 401/403 时由本处兜底写出 JSON 响应(宿主只返回状态码)。
58
+ const rejection = ctx.connection?.requestRejection?.(req);
59
+ if (rejection !== undefined) {
60
+ const httpRes = res;
61
+ httpRes.statusCode = rejection;
62
+ httpRes.setHeader('Content-Type', 'application/json; charset=utf-8');
63
+ httpRes.end(JSON.stringify({
64
+ ok: false,
65
+ error: rejection === 401 ? 'unauthorized: browser authentication required' : 'forbidden: untrusted request',
66
+ code: rejection === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
67
+ }));
68
+ return;
69
+ }
53
70
  await handleDbToolRequest(req, res, service, {
54
71
  trustedHosts: trustedHosts(),
55
72
  // 项目上下文权威解析:会话 header.cwd 优先,回退前端 cwd(对齐 ssh-tunnel getProjectContext)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-db-tool",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "DSH community plugin: chat-operated multi-database admin tool with sidebar management, project-scoped grants, ro/rw modes and dangerous-operation confirmation",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -14,7 +14,7 @@
14
14
  "homepage": "https://github.com/mengqi1436/dsh-db-tool#readme",
15
15
  "engines": {
16
16
  "node": ">=20",
17
- "dsh": ">=0.1.0-rc.6"
17
+ "dsh": ">=0.1.0-rc.6 <0.2.0-0 || 0.1.7-rc.2 || >=0.2.0-rc.1 <0.3.0-0"
18
18
  },
19
19
  "main": "dist/index.js",
20
20
  "exports": {
@@ -55,10 +55,12 @@
55
55
  "redis": "^6.2.1"
56
56
  },
57
57
  "devDependencies": {
58
+ "@deepseek-ai/cordis": "^4.0.4",
58
59
  "@stryker-mutator/core": "^10.0.0",
59
60
  "@stryker-mutator/vitest-runner": "^10.0.0",
60
61
  "@types/better-sqlite3": "latest",
61
62
  "@types/node": "latest",
63
+ "dsh-better-sidebar": "^0.24.1",
62
64
  "esbuild": "^0.28.0",
63
65
  "mongodb": "^7.6.0",
64
66
  "stryker-cli": "^1.1.0",
@@ -0,0 +1,40 @@
1
+ # DSH dsh-app-boot resolve.paths hotfix (upstream bug: createRequire(parent).resolve.paths(name)
2
+ # returns null for core-module names like "punycode", breaking hoisted profile installs).
3
+ # Covers BOTH unguarded for..of sites in 0.2.0-rc.1: routeScoped() (@@ -1423,14) and
4
+ # nativePackageDir() (@@ -1245,10). Anchor lines are byte-identical to 0.1.7-rc.2 (overall +1 shift).
5
+ # Target-Version: 0.2.0-rc.1
6
+ # Apply manually: git apply -p1 -d <dsh-app-boot package root> <this file>
7
+ # Or one command: npm run patch:dsh (scripts/apply-dsh-hotfix.ps1)
8
+ # Generated from @deepseek-ai/dsh-app-boot@0.2.0-rc.1 npm tarball vs patched local copy.
9
+ diff --git a/lib/index.js b/lib/index.js
10
+ index e60aac4..fcd1a38 100644
11
+ --- a/lib/index.js
12
+ +++ b/lib/index.js
13
+ @@ -1245,7 +1245,10 @@ function profileChild(path, treePrefix) {
14
+ return treePrefix + (end < 0 ? rest : rest.slice(0, end));
15
+ }
16
+ function nativePackageDir(parent, name) {
17
+ - for (const searchPath of createRequire(parent).resolve.paths(name)) {
18
+ + /* local hotfix: nativePackageDir: resolve.paths returns null for core-module names
19
+ + (fs, node:fs, ...); an empty list means no local candidate, matching the
20
+ + guard in profile.ts:596. */
21
+ + for (const searchPath of createRequire(parent).resolve.paths(name) ?? []) {
22
+ const candidate = join(searchPath, name);
23
+ if (existsSync(join(candidate, "package.json"))) return candidate;
24
+ }
25
+ @@ -1420,7 +1423,14 @@ var ResolutionRouter = class {
26
+ const target = resolution.entries.get(name);
27
+ const candidates = [];
28
+ const localSearchPaths = [];
29
+ - for (const searchPath of createRequire(parent).resolve.paths(name)) {
30
+ + /* local hotfix: resolve.paths returns null for core-module names (e.g. "punycode"),
31
+ + which made the for..of throw TypeError and broke every hoisted profile install
32
+ + whose dependency tree requires such an npm package. Fall back to the standard
33
+ + node_modules search chain so the local candidate can still be found. */
34
+ + const _hotReq = createRequire(parent);
35
+ + const _hotPaths = _hotReq.resolve.paths(name)
36
+ + ?? _hotReq("node:module")._nodeModulePaths(_hotReq("node:path").dirname(parent));
37
+ + for (const searchPath of _hotPaths) {
38
+ if (!searchPath.startsWith(layer.localPrefix)) break;
39
+ localSearchPaths.push(searchPath);
40
+ const candidate = localPackageCandidate(searchPath, name, flavor);
@@ -0,0 +1,41 @@
1
+ # DSH dsh-app-boot resolve.paths hotfix (upstream bug: createRequire(parent).resolve.paths(name)
2
+ # returns null for core-module names like "punycode", breaking hoisted profile installs).
3
+ # Covers BOTH unguarded for..of sites in 0.2.0-rc.2: routeScoped() (@@ -1423,14) and
4
+ # nativePackageDir() (@@ -1245,10). lib/index.js is byte-identical to 0.2.0-rc.1
5
+ # (anchor lines identical to 0.1.7-rc.2, overall +1 shift) — hunks shared verbatim.
6
+ # Target-Version: 0.2.0-rc.2
7
+ # Apply manually: git apply -p1 -d <dsh-app-boot package root> <this file>
8
+ # Or one command: npm run patch:dsh (scripts/apply-dsh-hotfix.ps1)
9
+ # Generated from @deepseek-ai/dsh-app-boot@0.2.0-rc.2 npm tarball vs patched local copy.
10
+ diff --git a/lib/index.js b/lib/index.js
11
+ index e60aac4..fcd1a38 100644
12
+ --- a/lib/index.js
13
+ +++ b/lib/index.js
14
+ @@ -1245,7 +1245,10 @@ function profileChild(path, treePrefix) {
15
+ return treePrefix + (end < 0 ? rest : rest.slice(0, end));
16
+ }
17
+ function nativePackageDir(parent, name) {
18
+ - for (const searchPath of createRequire(parent).resolve.paths(name)) {
19
+ + /* local hotfix: nativePackageDir: resolve.paths returns null for core-module names
20
+ + (fs, node:fs, ...); an empty list means no local candidate, matching the
21
+ + guard in profile.ts:596. */
22
+ + for (const searchPath of createRequire(parent).resolve.paths(name) ?? []) {
23
+ const candidate = join(searchPath, name);
24
+ if (existsSync(join(candidate, "package.json"))) return candidate;
25
+ }
26
+ @@ -1420,7 +1423,14 @@ var ResolutionRouter = class {
27
+ const target = resolution.entries.get(name);
28
+ const candidates = [];
29
+ const localSearchPaths = [];
30
+ - for (const searchPath of createRequire(parent).resolve.paths(name)) {
31
+ + /* local hotfix: resolve.paths returns null for core-module names (e.g. "punycode"),
32
+ + which made the for..of throw TypeError and broke every hoisted profile install
33
+ + whose dependency tree requires such an npm package. Fall back to the standard
34
+ + node_modules search chain so the local candidate can still be found. */
35
+ + const _hotReq = createRequire(parent);
36
+ + const _hotPaths = _hotReq.resolve.paths(name)
37
+ + ?? _hotReq("node:module")._nodeModulePaths(_hotReq("node:path").dirname(parent));
38
+ + for (const searchPath of _hotPaths) {
39
+ if (!searchPath.startsWith(layer.localPrefix)) break;
40
+ localSearchPaths.push(searchPath);
41
+ const candidate = localPackageCandidate(searchPath, name, flavor);
@@ -2,6 +2,8 @@
2
2
  * 类型声明:scripts/hotfix-core.mjs(无 allowJs,为 tests/scripts/hotfix.spec.ts 提供 import 类型)。
3
3
  */
4
4
  export declare const TARGET_VERSION: string;
5
+ export declare const TARGET_VERSIONS: string[];
6
+ export declare const PATCH_BY_VERSION: Record<string, string>;
5
7
  export declare const HOTFIX_MARKER: string;
6
8
  export declare const BOOT_INDEX_REL: string;
7
9
  export declare const BACKUP_SUFFIX: string;
@@ -2,12 +2,15 @@
2
2
  /**
3
3
  * DSH dsh-app-boot resolve.paths hotfix —— 核心逻辑(纯 Node、零依赖、可被测试 import)。
4
4
  *
5
- * 修复的 bug:@deepseek-ai/dsh-app-boot@0.1.7-rc.2 对
5
+ * 修复的 bug:@deepseek-ai/dsh-app-boot 对
6
6
  * `createRequire(parent).resolve.paths(name)` 的 for..of 无保护,而 Node 对
7
7
  * core-module 同名包(punycode 等)返回 null,for..of null 直接抛 TypeError,
8
- * 导致 hoisted profile 下 npm 安装插件导入失败。rc.2 中共两处同型缺陷:
9
- * 1. routeScoped()(L1419 区域)—— 症状直接暴露;
10
- * 2. nativePackageDir()(L1246 区域)—— 第二处,症状相同,一并修复。
8
+ * 导致 hoisted profile 下 npm 安装插件导入失败。0.1.7-rc.2 起共两处同型缺陷:
9
+ * 1. routeScoped()(0.1.7-rc.2 L1419 区域 / 0.2.0-rc.1 与 0.2.0-rc.2 L1423 区域)—— 症状直接暴露;
10
+ * 2. nativePackageDir()(0.1.7-rc.2 L1244 区域 / 0.2.0-rc.1 与 0.2.0-rc.2 L1248 区域)—— 症状相同,一并修复。
11
+ *
12
+ * 版本支持:锚点行在 TARGET_VERSIONS 列出的版本中逐字节一致(仅整体行号偏移),
13
+ * 内建替换路径与版本无关;git-apply 路径按安装版本选择 patches/ 下行号匹配的补丁。
11
14
  *
12
15
  * 用法:node scripts/hotfix-core.mjs [--dsh-root <dir>] [--patch-file <file>]
13
16
  * [--force] [--revert] [--dry-run]
@@ -20,6 +23,14 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
20
23
 
21
24
  /** 补丁文件里的目标版本声明行(patches/*.patch 注释头)。 */
22
25
  export const TARGET_VERSION = '0.1.7-rc.2';
26
+ /** 锚点行逐字节一致的受支持版本列表(仅整体行号偏移;0.2.0-rc.1/0.2.0-rc.2 的 lib/index.js 逐字节相同)。 */
27
+ export const TARGET_VERSIONS = ['0.1.7-rc.2', '0.2.0-rc.1', '0.2.0-rc.2'];
28
+ /** 各受支持版本对应的默认补丁文件名(patches/ 下;git-apply 路径按版本选择)。 */
29
+ export const PATCH_BY_VERSION = {
30
+ '0.1.7-rc.2': 'dsh-app-boot-route-scoped-hotfix.patch',
31
+ '0.2.0-rc.1': 'dsh-app-boot-route-scoped-hotfix-0.2.0-rc.1.patch',
32
+ '0.2.0-rc.2': 'dsh-app-boot-route-scoped-hotfix-0.2.0-rc.2.patch',
33
+ };
23
34
  /** 判断目标文件是否已打补丁的标识串(hotfix 注释首行片段)。 */
24
35
  export const HOTFIX_MARKER = 'local hotfix: resolve.paths returns null';
25
36
  /** dsh-app-boot 内 lib/index.js 相对包根的路径。 */
@@ -287,15 +298,18 @@ function isGitApplyable(indexFile, patchFile) {
287
298
  /** 命令行主入口。@returns {number} 退出码 */
288
299
  export function run(argv = process.argv.slice(2)) {
289
300
  let dshRoot;
290
- let patchFile = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'patches', 'dsh-app-boot-route-scoped-hotfix.patch');
301
+ let patchFile = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'patches', PATCH_BY_VERSION[TARGET_VERSION]);
302
+ let patchFileExplicit = false;
291
303
  let force = false;
292
304
  let revert = false;
293
305
  let dryRun = false;
294
306
  for (let i = 0; i < argv.length; i++) {
295
307
  const arg = argv[i];
296
308
  if (arg === '--dsh-root') dshRoot = argv[++i];
297
- else if (arg === '--patch-file') patchFile = argv[++i];
298
- else if (arg === '--force') force = true;
309
+ else if (arg === '--patch-file') {
310
+ patchFile = argv[++i];
311
+ patchFileExplicit = true;
312
+ } else if (arg === '--force') force = true;
299
313
  else if (arg === '--revert') revert = true;
300
314
  else if (arg === '--dry-run') dryRun = true;
301
315
  else if (arg === '--help' || arg === '-h') {
@@ -324,11 +338,25 @@ export function run(argv = process.argv.slice(2)) {
324
338
  } catch {
325
339
  /* 读不到版本号时跳过比对 */
326
340
  }
341
+ // 按安装版本选择行号匹配的补丁(未显式指定 --patch-file 时);
342
+ // 锚点内容各版本一致,选错补丁只会让 git-apply 路径失配回退内建替换,但优先选对。
343
+ if (!patchFileExplicit && actualVersion && PATCH_BY_VERSION[actualVersion]) {
344
+ patchFile = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'patches', PATCH_BY_VERSION[actualVersion]);
345
+ }
327
346
  const patchText = fs.existsSync(patchFile) ? fs.readFileSync(patchFile, 'utf8') : '';
328
- const declared = parsePatchTargetVersion(patchText) ?? TARGET_VERSION;
329
- if (actualVersion && actualVersion !== declared && !force) {
330
- console.warn(`[dsh-hotfix] version mismatch: installed ${actualVersion} != patch target ${declared}. Use --force to apply anyway.`);
331
- return 2;
347
+ const declared = parsePatchTargetVersion(patchText);
348
+ if (actualVersion && !force) {
349
+ if (declared) {
350
+ if (declared !== actualVersion) {
351
+ console.warn(`[dsh-hotfix] version mismatch: installed ${actualVersion} != patch target ${declared}. Use --force to apply anyway.`);
352
+ return 2;
353
+ }
354
+ } else if (!TARGET_VERSIONS.includes(actualVersion)) {
355
+ console.warn(
356
+ `[dsh-hotfix] version mismatch: installed ${actualVersion} is not supported (supported: ${TARGET_VERSIONS.join(', ')}) and patch declares no Target-Version. Use --force to apply anyway.`,
357
+ );
358
+ return 2;
359
+ }
332
360
  }
333
361
 
334
362
  let content;
@@ -1,76 +0,0 @@
1
- # dsh-db-tool HTTP API 契约(v1)
2
-
3
- 侧边栏 client 与 lib/http 实现共同遵守。API 仅监听 loopback(127.0.0.1),校验 `Origin`/`Referer` 必须来自 DSH 本地 web(同源 fenced)。所有请求/响应 JSON。
4
-
5
- ## 通用约定
6
-
7
- - 成功:`200 { "ok": true, "data": ... }`
8
- - 失败:`{ "ok": false, "error": string, "code": string }`,code ∈ `UNAUTHORIZED_PROJECT`(项目未授权该连接)/ `READ_ONLY`(ro 连接拒绝 execute)/ `NEEDS_CONFIRMATION`(危险操作待确认)/ `INVALID_CHALLENGE` / `NOT_FOUND` / `INVALID_ARGUMENT`(参数非法;插件 dispose 后的新请求同样返回此 code,error 为「服务已关闭」)/ `DRIVER_ERROR`
9
- - `projectPath`:前端传入当前 workspace 绝对路径,服务端用 `normalizeProjectKey` 归一后校验 grants;**不传视为匿名项目**(仅连接管理可用,业务操作一律拒)
10
- - 人工确认通道:SQL 控制台是人工操作,危险语句同样走 challenge 流程——前端展示确认对话框后携 `challengeId` 重发
11
- - 除注明外,业务端点均需 projectPath 且该连接已授权
12
-
13
- ## 路由
14
-
15
- ### 连接管理(无需 projectPath)
16
-
17
- | 方法/路径 | 说明 |
18
- |---|---|
19
- | `GET /api/connections` | 全量连接列表(脱敏 ConnectionMeta[],含每库 kind) |
20
- | `POST /api/connections` | 创建 `{ id, kind, name?, url?, fields?, ssl? }`;密码自动拆入 secrets |
21
- | `PUT /api/connections/:id` | 更新(同上字段可选) |
22
- | `DELETE /api/connections/:id` | 删除(级联 secrets + 全项目 grants) |
23
- | `POST /api/connections/:id/test` | 测试连接 → `TestConnectResult` |
24
-
25
- ### 项目授权
26
-
27
- | 方法/路径 | 说明 |
28
- |---|---|
29
- | `GET /api/grants?project=<path>` | 该项目授权列表 `{ connId, mode }[]` |
30
- | `PUT /api/grants` | `{ projectPath, connId, mode: "ro"\|"rw" }` 授权/改模式 |
31
- | `DELETE /api/grants` | `{ projectPath, connId }` 撤销 |
32
-
33
- ### 浏览(需授权,ro 即可)
34
-
35
- | 方法/路径 | 说明 |
36
- |---|---|
37
- | `GET /api/databases?project=<path>&connId=` | 库/schema 清单 string[] |
38
- | `GET /api/tables?project=<path>&connId=&database=` | TableInfo[] |
39
- | `GET /api/schema?project=<path>&connId=&database=&table=` | ColumnInfo[] |
40
- | `GET /api/preview?project=<path>&connId=&database=&table=&limit=&offset=` | QueryResult(limit≤50 强制;offset 为行偏移,翻页用,默认 0) |
41
-
42
- ### SQL 控制台 / 脚本(需授权,execute 需 rw)
43
-
44
- | 方法/路径 | 说明 |
45
- |---|---|
46
- | `POST /api/query` | `{ projectPath, connId, sql, params?, challengeId? }` → QueryResult;危险读(如 KEYS)同样触发 NEEDS_CONFIRMATION |
47
- | `POST /api/execute` | `{ projectPath, connId, statement, params?, challengeId? }` → ExecResult 或 `NEEDS_CONFIRMATION { challengeId, statement, danger, reason }` |
48
- | `POST /api/script` | `{ projectPath, connId, code, challengeId? }` → 子进程沙箱执行(Node permission model 禁 fs + 新 vm realm 双层隔离),60s 强超时;db 句柄经 IPC 走完整 guard/审计链路 |
49
-
50
- ### 审计
51
-
52
- | 方法/路径 | 说明 |
53
- |---|---|
54
- | `GET /api/audit?project=<path>&limit=` | AuditEntry[](最近 n 条,默认 50) |
55
-
56
- ### 状态
57
-
58
- | 方法/路径 | 说明 |
59
- |---|---|
60
- | `GET /api/state?project=<path>` | 面板初始化:`{ connections, grants, auditTail }` |
61
-
62
- ## NEEDS_CONFIRMATION 语义
63
-
64
- ```json
65
- {
66
- "ok": false,
67
- "code": "NEEDS_CONFIRMATION",
68
- "challengeId": "c_9f3a…",
69
- "statement": "DROP TABLE users",
70
- "danger": "danger",
71
- "reason": "DDL 不可回滚(隐式提交)"
72
- }
73
- ```
74
-
75
- - `challengeId` 一次性、绑定语句 hash、5 分钟过期;重发请求带同 `challengeId` 且语句 hash 一致才放行
76
- - 模型工具(对话内)用同一 guard/challenge 实现,确认经 ask_user 完成
@@ -1,80 +0,0 @@
1
- # dsh-db-tool 侧边栏 Apple 风格重设计规范(单一真相)
2
-
3
- 所有参与者必须遵守本规范。目标:贴合 Apple 官方视觉语言(macOS/iOS 系统 UI、HIG),在 DSH 侧边栏窄容器(约 380-420px)内呈现。
4
-
5
- ## 硬性约束(所有 teammate)
6
- - **只改自己分配的行区段**,区段外一律不动(edit 基于 old_string 精确匹配)。
7
- - **i18n key 冻结**(client/client.js L18-264):不得新增/删除/改名 key;如确需新文案,在汇报中列出由 Lead 决定。
8
- - **不新增任何 npm 依赖**(单文件 bundle 无构建步骤)。图标只用极简内联 SVG 几何图形(chevron/加号/三角等 primitives)或纯排版。
9
- - 保持 `React.createElement` 形态、保持组件对外 props/签名不变、保持 `dbt-` 类名前缀。
10
- - 保留对宿主变量的 fallback:`var(--dsh-accent, <apple值>)` 形式可保留,但视觉基线以本规范的 Apple token 为准。
11
- - 尊重 `prefers-color-scheme`(light/dark 双模式)与 `prefers-reduced-motion`。
12
- - 完成后 `node --check client/client.js` 必须通过。
13
-
14
- ## 1. Design Tokens(CSS 变量,定义在 .dbt-panel 根)
15
-
16
- 字体:
17
- - UI 字体:`-apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", system-ui, sans-serif`
18
- - 等宽(SQL/数据):`ui-monospace, "SF Mono", Menlo, Consolas, monospace`
19
- - 字号阶梯:caption 11px / 正文 13px / 小标题 13px semibold / 标题 15px semibold。行高 1.45。
20
-
21
- 颜色(dark 模式为基线,light 模式对应反转):
22
- | token | dark | light |
23
- |---|---|---|
24
- | --dbt-accent | #0A84FF(系统蓝) | #007AFF |
25
- | --dbt-bg | 透明(继承宿主) | 同 |
26
- | --dbt-surface | rgba(120,120,128,.12) | rgba(120,120,128,.08) |
27
- | --dbt-surface-strong | rgba(120,120,128,.18) | rgba(120,120,128,.14) |
28
- | --dbt-separator | rgba(120,120,128,.24) | rgba(60,60,67,.18) |
29
- | --dbt-text-secondary | rgba(235,235,245,.6) | rgba(60,60,67,.6) |
30
- | --dbt-danger | #FF453A | #FF3B30 |
31
- | --dbt-success | #30D158 | #34C759 |
32
- | --dbt-warning | #FFD60A | #FF9F0A |
33
-
34
- 材质(Apple 毛玻璃近似,非官方 Liquid Glass):
35
- - 对话框/粘性表头可用 `backdrop-filter: blur(20px) saturate(180%)` + 半透明背景;提供 `@media (prefers-reduced-transparency: reduce)` 下降级为实色。
36
- - 阴影一律带色调(不用纯黑):`0 8px 32px rgba(0,0,0,.28)`(dark)。
37
-
38
- 圆角系统(Shape Consistency Lock):
39
- - 卡片/对话框 12px;按钮/输入/segmented 容器 8px;小 pill 6px。全站只用这三档。
40
-
41
- 间距:8pt 网格。面板 padding 16px;卡片内 12px;元素间 gap 8px(紧凑 4px)。
42
-
43
- 动效:`transition: all .18s cubic-bezier(0.25, 0.1, 0.25, 1)`;仅 transform/opacity;`:active` 时 `transform: scale(.97)`;MOTION 强度低(侧边栏工具,动效仅为反馈)。
44
-
45
- ## 2. 组件契约(类名 → Apple 形态)
46
-
47
- 类名全部保留,视觉重定义:
48
-
49
- - `.dbt-panel`:根容器。font 13px,padding 16px,gap 12px,flex column,overflow-y auto。
50
- - `.dbt-tabs`:**iOS segmented control**。容器 `background: var(--dbt-surface); border-radius: 8px; padding: 2px; gap: 2px; display: flex;` 内 button `flex: 1; border: none; background: transparent; border-radius: 6px; padding: 5px 8px; font-size: 12px;` active:`background: rgba(255,255,255,.14)`(light 模式 rgba(255,255,255,.9)+细阴影)+ `font-weight: 600`,不用蓝色填充。
51
- - `.dbt-card`:**inset grouped 卡片**。`background: var(--dbt-surface); border: none; border-radius: 12px; padding: 12px; gap: 10px;`
52
- - `.dbt-row`:flex row,gap 8px,wrap。
53
- - 输入(`.dbt-row input/select/textarea`、`.dbt-card` 同类):**填充式无边框**。`background: var(--dbt-surface-strong); border: none; border-radius: 8px; padding: 6px 10px; font-size: 13px;` focus:`outline: 2px solid var(--dbt-accent); outline-offset: -1px;`(或 box-shadow ring)。
54
- - `.dbt-btn`:普通= `background: var(--dbt-surface); border: none; border-radius: 8px; padding: 5px 12px; font-size: 13px; font-weight: 500;` hover 提亮(surface-strong);active scale(.97)。
55
- - `.dbt-btn.primary`:`background: var(--dbt-accent); color: #fff; border: none;` 无渐变无发光。
56
- - `.dbt-btn.danger`:`color: var(--dbt-danger); background: transparent; border: 1px solid var(--dbt-danger);`(或 `background: rgba(255,69,58,.12); border: none`,二选一全站统一)。
57
- - `.dbt-btn:disabled`:opacity .4。
58
- - `.dbt-muted`:`color: var(--dbt-text-secondary); font-size: 11px;`
59
- - `.dbt-err`:`color: var(--dbt-danger); font-size: 12px;`(白底黑字对比达标)。
60
- - `.dbt-msg`:`color: var(--dbt-success); font-size: 12px;`
61
- - `.dbt-table`:**去全边框,行 hairline 式**。`th, td: border: none; border-bottom: 1px solid var(--dbt-separator); padding: 6px 8px; font-size: 12px;` th `position: sticky; top: 0; backdrop-filter: blur(20px); background: <半透明宿主底色>; font-weight: 600; font-size: 11px; text-transform: uppercase 可选(不强制)`。td 数字/数据列用等宽字体。
62
- - `.dbt-tablewrap`:`border: none; border-radius: 12px; overflow: auto; max-height: 320px; background: var(--dbt-surface);`
63
- - `.dbt-overlay`:`background: rgba(0,0,0,.4); backdrop-filter: blur(8px);`
64
- - `.dbt-dialog`:**macOS alert 风格**。`background: rgba(40,40,44,.85)(light: rgba(252,252,252,.9)); backdrop-filter: blur(20px) saturate(180%); border: 1px solid var(--dbt-separator); border-radius: 14px; padding: 16px; box-shadow: 0 8px 32px rgba(0,0,0,.28);` 标题 13px semibold 居中可选;按钮区 `.dbt-row` 改为右对齐。
65
- - `.dbt-grant`:列表行式。`padding: 8px 0; border-bottom: 1px solid var(--dbt-separator);`(去 dashed,实 hairline)。
66
- - `.dbt-seg`:与 `.dbt-tabs` 同构的 mini segmented(ro/rw/未授权切换)。
67
-
68
- ## 3. 各面板布局指引(结构重排方向)
69
-
70
- - **ManageView**:连接列表改为列表行卡(每连接一张 inset card:kind 徽标(小 pill 6px、surface 底)、名称 13px semibold、safeUrl 等宽 11px muted、右侧「编辑/删除」为图标化次要按钮);「新建连接」为 accent 主按钮置顶右侧;表单字段分组;审计表保持 resultTable。
71
- - **GrantsView**:每连接一行(列表行式),ro/rw/未授权用 `.dbt-seg` mini segmented;说明文字 muted。
72
- - **BrowseView**:库/表选择器用填充式 select;结构/预览切换可用 mini segmented;表格遵循新 table 契约。
73
- - **ConsoleView**:SQL 输入区用等宽字体 + 填充式输入(min-height 加大、focus ring);执行按钮 accent 主按钮;结果表新契约;challenge 流程与 DangerDialog 样式统一。
74
- - DangerDialog:标题不再用 emoji ⚠️,改为纯文字 + danger 色 accent(左侧 3px 色条或标题色)。
75
-
76
- ## 4. 验收
77
- - `node --check client/client.js` 通过;
78
- - 全站只有一个 accent(系统蓝)、一套圆角、一套材质;
79
- - dark/light 均可读(WCAG AA);
80
- - 无 emoji 图标、无渐变、无发光、无多余装饰 pill。
@@ -1,154 +0,0 @@
1
- # dsh-db-tool 项目架构图
2
-
3
- > 由 cbm(codebase-memory)索引数据绘制:843 节点 / 2702 边,模块分层见各图。
4
- > 索引项目名:`E-Code-dsh-dsh-db-tool`。
5
-
6
- ## 1. 总体分层架构
7
-
8
- ```mermaid
9
- flowchart TB
10
- subgraph UI["客户端层(DSH 侧边栏)"]
11
- CLIENT["client/client.js<br/>连接管理 · 对象树浏览 · SQL 控制台 · 授权面板"]
12
- end
13
-
14
- subgraph API["接入层"]
15
- HTTP["lib/http<br/>HTTP API(13 条路由)"]
16
- TOOL["lib/index.ts<br/>DatabaseManager 工具定义<br/>list_connections / query / execute /<br/>schema / preview / run_script"]
17
- end
18
-
19
- subgraph SVC["服务编排层"]
20
- MGR["lib/manager.ts<br/>ro/rw 项目授权校验 · 适配器生命周期 · 事务编排"]
21
- end
22
-
23
- subgraph SEC["安全层"]
24
- GUARD["lib/guard<br/>SQL/命令分类 · 危险操作识别 ·<br/>一次性 challenge 确认(5 分钟有效)"]
25
- STORE["lib/store<br/>connections · grants · secrets(脱敏)·<br/>audit 审计 · normalize · io"]
26
- SCRIPT["lib/script<br/>runner.ts + worker.cjs<br/>子进程沙箱(vm realm · 禁文件 · 60s 超时)"]
27
- end
28
-
29
- subgraph ADPT["适配器层 lib/adapters(8 库)"]
30
- REG["index.ts<br/>kind → 工厂注册表 + driverMissingError"]
31
- SQLSHARED["sql-shared/<br/>common.ts(标识符/分页/规范化)<br/>pg-like.ts(PostgreSQL + GaussDB 共享)"]
32
- A1["mysql"]
33
- A2["postgresql"]
34
- A3["gaussdb<br/>vendor/gaussdb-pg 驱动加载"]
35
- A4["sqlite"]
36
- A5["redis"]
37
- A6["mongodb"]
38
- A7["oracle"]
39
- A8["dmdb"]
40
- TYPES["types.ts<br/>DatabaseAdapter 契约"]
41
- end
42
-
43
- subgraph DRV["驱动层"]
44
- D1["pg / mysql2 / sqlite3 / redis<br/>mongodb / oracledb / dmdb"]
45
- D2["vendor/gaussdb-pg<br/>openGauss-connector-nodejs 构建产物"]
46
- end
47
-
48
- CLIENT -->|HTTP| HTTP
49
- CLIENT -.->|注入| TOOL
50
- TOOL --> MGR
51
- MGR --> GUARD
52
- MGR --> STORE
53
- MGR --> SCRIPT
54
- MGR --> REG
55
- REG --> SQLSHARED
56
- REG --> A1 & A4 & A5 & A6 & A7 & A8
57
- A2 --> SQLSHARED
58
- A3 --> SQLSHARED
59
- SQLSHARED --> TYPES
60
- A1 & A2 & A3 & A4 & A5 & A6 & A7 & A8 -.-> DRV
61
- SCRIPT -.->|db.query / db.execute 经完整 guard 链路| MGR
62
- ```
63
-
64
- ## 2. 请求时序(query 与危险操作确认流)
65
-
66
- ```mermaid
67
- sequenceDiagram
68
- autonumber
69
- participant U as 用户(聊天)
70
- participant T as DatabaseManager 工具
71
- participant M as lib/manager.ts
72
- participant G as lib/guard
73
- participant S as lib/store
74
- participant A as 适配器(8 库)
75
- participant DB as 数据库
76
-
77
- U->>T: query/execute(conn_id, sql)
78
- T->>S: 授权校验(项目路径 → 连接 → ro/rw)
79
- alt 未授权
80
- S-->>T: UNAUTHORIZED_PROJECT
81
- else ro 授权 + 写操作
82
- S-->>T: READ_ONLY(拒绝)
83
- else 通过
84
- T->>G: 语句分类
85
- alt 危险操作(DDL/DML/维护命令)
86
- G-->>T: NEEDS_CONFIRMATION + challengeId(一次性,5 分钟)
87
- T-->>U: 说明语句与风险,等待明确同意
88
- U->>T: 携同一 challengeId 重试
89
- T->>G: 校验 challenge(绑定语句 hash)
90
- end
91
- T->>M: 执行
92
- M->>A: query/execute(可选 database 跨库路由,pg/gaussdb)
93
- A->>DB: 参数化 SQL(ro 会话级 READ ONLY 双保险)
94
- DB-->>A: 结果
95
- A-->>T: 规范化结果(DECIMAL→string 等)
96
- T->>S: 审计记录
97
- T-->>U: {columns, rows, rowCount} / {affectedRows, message}
98
- end
99
- ```
100
-
101
- ## 3. 适配器体系(8 库 × 共享实现)
102
-
103
- ```mermaid
104
- flowchart LR
105
- subgraph 契约
106
- T["types.ts<br/>DatabaseAdapter / TableInfo / ColumnInfo<br/>DbKind = 8 种"]
107
- end
108
- subgraph 工厂注册
109
- R["adapters/index.ts<br/>kind → lazy import 工厂<br/>缺失驱动 → driverMissingError(附构建提示)"]
110
- end
111
- subgraph SQL系共享
112
- PG["pg-like.ts<br/>PostgreSQL + GaussDB<br/>pool · ro 会话 · 跨库路由 ·<br/>库→schema→表 三层浏览"]
113
- CM["common.ts<br/>assertIdent · quoteIdent<br/>clampLimit/Offset · normalizeCell"]
114
- end
115
- subgraph 独立实现
116
- MY["mysql"]
117
- SQ["sqlite"]
118
- RD["redis(SCAN 禁 KEYS)"]
119
- MG["mongodb(强制显式 filter)"]
120
- OR["oracle"]
121
- DM["dmdb"]
122
- end
123
- PG --> CM
124
- MY & SQ & RD & MG & OR & DM -.-> CM
125
- R --> PG & MY & SQ & RD & MG & OR & DM
126
- PG -.->|"gaussdb 差异仅驱动加载<br/>createRequire vendor/gaussdb-pg"| G["gaussdb/index.ts"]
127
- T --- R
128
- ```
129
-
130
- ## 4. 模块分层职责(cbm layers 数据)
131
-
132
- | 层 | 模块 | 特征 |
133
- |---|---|---|
134
- | entry | lib、script、e2e-mock | 仅出边(调用下游) |
135
- | api | http | 含 13 条 HTTP 路由定义 |
136
- | core | adapters(4 入 0 出)、store(12 入 0 出) | 高扇入核心,不依赖上层 |
137
- | internal | manager(7 入 12 出)、tool、http | 编排枢纽 |
138
- | leaf | guard | 仅入边,纯分类/判定 |
139
-
140
- ## 5. GaussDB 专项链路
141
-
142
- ```mermaid
143
- flowchart TB
144
- T["DatabaseManager(query/execute/schema/preview, database=...)"] --> M["manager"]
145
- M --> A3["gaussdb/index.ts<br/>loadGaussDriver():createRequire<br/>vendor/gaussdb-pg/packages/pg(CJS)"]
146
- A3 --> PL["sql-shared/pg-like.ts<br/>kind='gaussdb'"]
147
- PL -->|poolConfig:host/port 8000/user/password/database/ssl| P["PgLikePool"]
148
- PL -->|database 参数| PF["poolFor(db):按库缓存池(Navicat 式跨库)"]
149
- PL -->|ro 模式| SRO["每个新连接 SET SESSION CHARACTERISTICS<br/>AS TRANSACTION READ ONLY(fail-closed)"]
150
- P --> DB[("GaussDB 服务端<br/>openGauss 协议")]
151
- ```
152
-
153
- - 驱动构建:`scripts/build-gaussdb.sh` / `.ps1` → 克隆 openGauss-connector-nodejs → `vendor/gaussdb-pg/packages/pg`(gitignore)。
154
- - 测试:`tests/adapters-sql/gaussdb.test.ts`(驱动加载/工厂,mock 离线)+ `pg-like.test.ts`(pg/gaussdb 共享行为)。