dsh-db-tool 0.1.9 → 1.3.0
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 +8 -6
- package/dist/adapters/index.js +1 -1
- package/dist/adapters/mongodb/index.js +51 -16
- package/dist/index.js +21 -4
- package/docs/dsh-db-tool /351/224/231/350/257/257/350/256/260/345/275/225.txt" +66 -0
- package/docs/install.md +1 -1
- package/package.json +11 -4
- package/patches/dsh-app-boot-route-scoped-hotfix-0.2.0-rc.1.patch +40 -0
- package/scripts/build-mongodb.mjs +62 -0
- package/scripts/hotfix-core.d.mts +2 -0
- package/scripts/hotfix-core.mjs +38 -11
- package/vendor/mongodb-driver.cjs +28132 -0
package/README.md
CHANGED
|
@@ -36,11 +36,11 @@ dsh plugin --profile web add "dsh-db-tool@github:mengqi1436/dsh-db-tool"
|
|
|
36
36
|
dsh plugin --profile web add "link:E:\path\to\dsh-db-tool"
|
|
37
37
|
```
|
|
38
38
|
|
|
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
|
|
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
41
|
## 安全模型
|
|
42
42
|
|
|
43
|
-
- HTTP API 仅同源(`ctx.webServer` prefix `/dsh-db-tool/api`)+ loopback/Origin trust
|
|
43
|
+
- 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
44
|
- ro 双保险:服务层拦截 + 驱动会话级 `SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY`(pg/gaussdb)、readonly 打开(SQLite)
|
|
45
45
|
- SQL 参数绑定 + 标识符白名单(`[A-Za-z0-9_$]+` + 引用包裹);MySQL `multipleStatements:false`;Redis 元数据走 `SCAN`(禁 `KEYS`);Mongo 递归拒 `$where`
|
|
46
46
|
- `run_script`:node:vm 独立 context、60s 超时、无 require/process/网络/文件系统,仅注入受限 `db.{query,execute}` 句柄
|
|
@@ -52,7 +52,7 @@ GaussDB 官方驱动未发布 npm,需先构建 vendor:`npm run build:gaussdb
|
|
|
52
52
|
|
|
53
53
|
症状:DSH 启动后插件加载报 `failed to import`,伴随 `TypeError: Cannot read properties of null (reading 'Symbol(Symbol.iterator)')`,栈指向 `dsh-app-boot` 的 `routeScoped`。
|
|
54
54
|
|
|
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`
|
|
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` 与 `0.2.0-rc.1` 中同型缺陷两处(锚点行逐字节一致,仅整体行号偏移 1 行):`routeScoped()` 与 `nativePackageDir()`,本补丁一并修复(两处分别独立判定,已打过旧版单处补丁的机器只会补上缺的那处)。
|
|
56
56
|
|
|
57
57
|
一键修复(幂等,应用前自动备份为 `index.js.bak-hotfix`;`--revert` 可还原):
|
|
58
58
|
|
|
@@ -61,7 +61,9 @@ npm run patch:dsh # Windows(PowerShell)
|
|
|
61
61
|
npm run patch:dsh:sh # macOS / Linux
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
脚本自动探测 DSH 安装根(`--dsh-root`
|
|
64
|
+
脚本自动探测 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`,与内建替换路径逐字节等价已验证);不支持的版本会警告(`--force` 覆盖)。上游 issue:<https://github.com/mengqi1436/dsh-db-tool/issues>(占位,待上游仓库开放后替换)。
|
|
65
|
+
|
|
66
|
+
**本插件的免补丁路径(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` 仍保留,用于修复**其他**含同类依赖的插件或宿主自身报错。
|
|
65
67
|
|
|
66
68
|
## 测试
|
|
67
69
|
|
|
@@ -79,8 +81,8 @@ npx stryker run # 变异测试(范围 lib/guard + lib/manager + lib/store,
|
|
|
79
81
|
lib/ host 插件(store / adapters×8 / guard / manager / http / index)
|
|
80
82
|
client/ 侧边栏单文件产物(client.js,即源码)
|
|
81
83
|
skills/ db-admin skill
|
|
82
|
-
scripts/ GaussDB vendor 构建、DSH dsh-app-boot 热修复(patch:dsh)
|
|
84
|
+
scripts/ GaussDB vendor 构建、mongodb 驱动 bundle(build:mongodb)、DSH dsh-app-boot 热修复(patch:dsh)
|
|
83
85
|
docs/ 安装、HTTP 契约(api-contract.md)、skill 说明
|
|
84
86
|
tests/ vitest(离线 mock + DBT_TEST_* 门控真机)
|
|
85
|
-
vendor/ gaussdb-
|
|
87
|
+
vendor/ gaussdb 构建源/产物与 mongodb-driver.cjs bundle(gitignore,发布经 files 白名单收录 bundle)
|
|
86
88
|
```
|
package/dist/adapters/index.js
CHANGED
|
@@ -15,7 +15,7 @@ const INSTALL_HINTS = {
|
|
|
15
15
|
gaussdb: 'npm run build:gaussdb(构建 vendor/gaussdb-pg)',
|
|
16
16
|
sqlite: 'npm install better-sqlite3',
|
|
17
17
|
redis: 'npm install redis',
|
|
18
|
-
mongodb: 'npm
|
|
18
|
+
mongodb: 'npm run build:mongodb(构建 vendor/mongodb-driver.cjs;发布包已内置产物,仅在缺失时需要)',
|
|
19
19
|
oracle: 'npm install oracledb',
|
|
20
20
|
dmdb: 'npm install dmdb',
|
|
21
21
|
};
|
|
@@ -10,8 +10,36 @@
|
|
|
10
10
|
* - execute() 写白名单;updateMany/deleteMany 空 filter 直接拒绝(适配器级防线)。
|
|
11
11
|
* - 错误判定用 instanceof MongoServerError(官方建议不解析 message)。
|
|
12
12
|
* - 只读账号由用户配置官方内置角色 read / readWrite(见文档)。
|
|
13
|
+
* - 驱动不进依赖树:mongodb 经 esbuild bundle 成 vendor/mongodb-driver.cjs(见
|
|
14
|
+
* scripts/build-mongodb.mjs),运行时经 createRequire 相对加载(不经宿主
|
|
15
|
+
* dsh-app-boot 的模块解析器,规避其 resolve.paths 未防护的 for..of bug,
|
|
16
|
+
* 该 bug 会因依赖树中的 punycode 等核心模块同名包直接炸掉插件加载)。
|
|
17
|
+
* bson 保持 dependencies(bundle 的 external),保证 BSON 类实例与外部一致。
|
|
13
18
|
*/
|
|
14
|
-
import {
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
let vendorCache;
|
|
21
|
+
/** 容错加载 vendor bundle:缺失时返回 undefined(调用方降级处理),成功后缓存。
|
|
22
|
+
* 失败不缓存 → vendor 构建完成后无需重启即可恢复。 */
|
|
23
|
+
function vendorOrNull() {
|
|
24
|
+
if (vendorCache === undefined) {
|
|
25
|
+
try {
|
|
26
|
+
// 相对路径从本模块出发:dist/adapters/mongodb/ 或 lib/adapters/mongodb/ 均指向包根 vendor/
|
|
27
|
+
vendorCache = createRequire(import.meta.url)('../../../vendor/mongodb-driver.cjs');
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return vendorCache;
|
|
34
|
+
}
|
|
35
|
+
/** 严格加载 vendor bundle:缺失时抛带修复指引的错误 */
|
|
36
|
+
function vendor() {
|
|
37
|
+
const m = vendorOrNull();
|
|
38
|
+
if (m === undefined) {
|
|
39
|
+
throw new Error('mongodb 驱动不可用:缺少 vendor/mongodb-driver.cjs。请在插件目录运行 npm run build:mongodb 构建后重试');
|
|
40
|
+
}
|
|
41
|
+
return m;
|
|
42
|
+
}
|
|
15
43
|
/** 危险操作:服务层拦截器用于确认流程。deleteMany/updateMany 仅空 filter 时危险(适配器已拦截);createIndex 在大集合上可能阻塞。createIndexes/dropIndexes 为单数形式别名,renameCollection 影响集合可见性。 */
|
|
16
44
|
export const DANGEROUS_OPS = new Set([
|
|
17
45
|
'dropDatabase', 'dropCollection', 'renameCollection', 'deleteMany', 'updateMany',
|
|
@@ -33,7 +61,9 @@ const CELL_TRUNC = 1000;
|
|
|
33
61
|
function trunc(s, max = CELL_TRUNC) {
|
|
34
62
|
return s.length > max ? `${s.slice(0, max)}…[截断,共${s.length}字符]` : s;
|
|
35
63
|
}
|
|
36
|
-
/** NormalizedCell 规范化:ObjectId/Decimal128/Long/Timestamp → toString();Binary → hex 截断;Date → ISO;嵌套对象/数组 → JSON 截断 1000
|
|
64
|
+
/** NormalizedCell 规范化:ObjectId/Decimal128/Long/Timestamp → toString();Binary → hex 截断;Date → ISO;嵌套对象/数组 → JSON 截断 1000。
|
|
65
|
+
* BSON 类型判别依赖 vendor bundle 的类(external bson,与外部 require('bson') 同实例);
|
|
66
|
+
* vendor 缺失时降级为普通对象处理,不阻断非 BSON 值的规范化。 */
|
|
37
67
|
export function normalizeMongoCell(v) {
|
|
38
68
|
if (v === null || v === undefined)
|
|
39
69
|
return null;
|
|
@@ -45,10 +75,11 @@ export function normalizeMongoCell(v) {
|
|
|
45
75
|
return v ? 'true' : 'false';
|
|
46
76
|
if (v instanceof Date)
|
|
47
77
|
return v.toISOString();
|
|
48
|
-
|
|
78
|
+
const M = vendorOrNull();
|
|
79
|
+
if (M && (v instanceof M.ObjectId || v instanceof M.Decimal128 || v instanceof M.Long || v instanceof M.Timestamp)) {
|
|
49
80
|
return trunc(v.toString());
|
|
50
81
|
}
|
|
51
|
-
if (v instanceof Binary)
|
|
82
|
+
if (M && v instanceof M.Binary)
|
|
52
83
|
return trunc(Buffer.from(v.buffer ?? []).toString('hex') || String(v));
|
|
53
84
|
if (typeof v === 'object') {
|
|
54
85
|
try {
|
|
@@ -120,7 +151,7 @@ export async function createMongoAdapter(conn, opts) {
|
|
|
120
151
|
}
|
|
121
152
|
else {
|
|
122
153
|
const uri = conn.url ?? buildUri(conn.fields, conn.ssl);
|
|
123
|
-
client = new MongoClient(uri, {
|
|
154
|
+
client = new (vendor().MongoClient)(uri, {
|
|
124
155
|
maxPoolSize: 10,
|
|
125
156
|
readPreference: 'primary',
|
|
126
157
|
w: 'majority',
|
|
@@ -138,7 +169,8 @@ export async function createMongoAdapter(conn, opts) {
|
|
|
138
169
|
: defaultDbName(conn.fields);
|
|
139
170
|
const db = client.db(dbName);
|
|
140
171
|
function wrap(e) {
|
|
141
|
-
|
|
172
|
+
const M = vendorOrNull();
|
|
173
|
+
if (M && e instanceof M.MongoServerError) {
|
|
142
174
|
return new Error(`MongoDB 服务端错误:${e.message}${e.codeName ? `(${e.codeName})` : ''}`);
|
|
143
175
|
}
|
|
144
176
|
return new Error(`MongoDB 错误:${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -471,16 +503,19 @@ function objectToResult(obj) {
|
|
|
471
503
|
function inferBsonType(v) {
|
|
472
504
|
if (v === null)
|
|
473
505
|
return 'null';
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
506
|
+
const M = vendorOrNull();
|
|
507
|
+
if (M) {
|
|
508
|
+
if (v instanceof M.ObjectId)
|
|
509
|
+
return 'ObjectId';
|
|
510
|
+
if (v instanceof M.Decimal128)
|
|
511
|
+
return 'Decimal128';
|
|
512
|
+
if (v instanceof M.Long)
|
|
513
|
+
return 'Long';
|
|
514
|
+
if (v instanceof M.Timestamp)
|
|
515
|
+
return 'Timestamp';
|
|
516
|
+
if (v instanceof M.Binary)
|
|
517
|
+
return 'Binary';
|
|
518
|
+
}
|
|
484
519
|
if (v instanceof Date)
|
|
485
520
|
return 'date';
|
|
486
521
|
if (Array.isArray(v))
|
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/...') 即可达;
|
|
8
|
-
*
|
|
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)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
DSH 插件 dsh-db-tool 加载错误记录
|
|
2
|
+
=====================================
|
|
3
|
+
记录时间:2026-09-28
|
|
4
|
+
当前状态:已从 desktop profile 卸载;卸载后该错误归零,其余 12 个第三方插件全部正常。
|
|
5
|
+
|
|
6
|
+
一、错误原文
|
|
7
|
+
-------------------------------------
|
|
8
|
+
dsh: error: [/197] createRequire.resolve.paths is not a function or its return value is not iterable
|
|
9
|
+
|
|
10
|
+
对应条目:/197 id=db-tool name=dsh-db-tool status=error
|
|
11
|
+
|
|
12
|
+
出现场景:DSH 0.1.7-rc.2 桌面版,desktop profile 的插件组合检查
|
|
13
|
+
(等价 `dsh --profile desktop --dump-config-schema` 的官方加载路径)。
|
|
14
|
+
同一检查中,其余 12 个第三方插件均无错误。
|
|
15
|
+
|
|
16
|
+
二、根因(DSH 0.1.7-rc.2 自带代码的 bug)
|
|
17
|
+
-------------------------------------
|
|
18
|
+
1) DSH 自带 @deepseek-ai/dsh-app-boot 的 lib/index.js 里有 6 处
|
|
19
|
+
`createRequire(parent).resolve.paths(name)` 调用,其中 5 处未做空值防护。
|
|
20
|
+
2) Node 对「内置模块同名包」调用该 API 会返回 null
|
|
21
|
+
(本机实测:resolve.paths('punycode') 返回 null;普通包返回数组)。
|
|
22
|
+
对 null 做 for..of 即抛 TypeError —— 这就是上面的错误消息。
|
|
23
|
+
3) dsh-db-tool 的依赖链包含 npm 包 punycode:
|
|
24
|
+
dsh-db-tool → mongodb → mongodb-connection-string-url → whatwg-url → tr46 → punycode
|
|
25
|
+
因此该插件的加载路径必然触发这个 bug。
|
|
26
|
+
|
|
27
|
+
三、本机复现证据
|
|
28
|
+
-------------------------------------
|
|
29
|
+
运行环境:Electron Node(DeepSeek Harness.exe 加 --expose-internals),可直接访问 asar 内模块。
|
|
30
|
+
|
|
31
|
+
复现结果(最小用例):
|
|
32
|
+
createRequire('.../node_modules/dsh-db-tool/dist/index.js').resolve.paths('punycode') => null
|
|
33
|
+
createRequire('.../node_modules/tr46/index.js').resolve.paths('punycode') => null
|
|
34
|
+
createRequire('.../node_modules/dsh-db-tool/dist/index.js').resolve.paths('ssh2') => array[11]
|
|
35
|
+
|
|
36
|
+
DSH 官方检查的诊断输出:
|
|
37
|
+
卸载前:F:\DSH\_recon\dump-schema-err.txt (含 /197 db-tool 错误)
|
|
38
|
+
卸载后:F:\DSH\_recon\dump-schema-err-2.txt (该错误消失,只剩 DSH 内置 agent-preset 的 4 条既有诊断)
|
|
39
|
+
|
|
40
|
+
四、插件作者已知该问题(包内自带 hotfix)
|
|
41
|
+
-------------------------------------
|
|
42
|
+
dsh-db-tool 包内提供:
|
|
43
|
+
scripts/hotfix-core.mjs
|
|
44
|
+
patches/dsh-app-boot-route-scoped-hotfix.patch
|
|
45
|
+
目标:@deepseek-ai/dsh-app-boot 0.1.7-rc.2 中未防护的那行 for..of。
|
|
46
|
+
修法:resolve.paths 返回 null 时回退到标准 node_modules 搜索链。
|
|
47
|
+
|
|
48
|
+
限制(本机为何没能直接用):
|
|
49
|
+
该脚本按「文件系统安装的 DSH」定位目标(找 DSH_HOME、PATH 上的 dsh、全局 npm 目录),
|
|
50
|
+
而桌面版把 dsh-app-boot 打包在 F:\DSH\resources\app.asar 内,脚本找不到文件,无法应用。
|
|
51
|
+
|
|
52
|
+
五、可选修复路径(供向 DSH/插件作者反馈)
|
|
53
|
+
-------------------------------------
|
|
54
|
+
1) 等 DSH 上游修复(推荐):该 bug 属于 0.1.7-rc.2 自带代码,任意「依赖树含 core-module 同名包」的插件都会触发。
|
|
55
|
+
2) 重打包 app.asar 应用该 hotfix:需先关闭 DSH 应用,修改应用安装文件,版本更新时会被覆盖,有风险。
|
|
56
|
+
3) 插件侧规避:依赖树中避免引入 core-module 同名包(例如替换 punycode 的实现)。
|
|
57
|
+
|
|
58
|
+
六、本次卸载记录
|
|
59
|
+
-------------------------------------
|
|
60
|
+
卸载方式:DSH plugin-manager operations(pnpm remove + 从 dsh.profile.bundles 移除,等价插件页卸载)。
|
|
61
|
+
结果核对:依赖 13 → 12;bundles 17 → 16;传递依赖 mongodb / punycode 等已一并清理。
|
|
62
|
+
其余 12 个插件的运行时依赖健康检查全部通过(ssh2 纯 JS 可用、better-sqlite3/oracledb 可用、rtk 0.50.0 可用)。
|
|
63
|
+
|
|
64
|
+
相关产物:
|
|
65
|
+
报告目录:F:\DSH\_recon\(install-results.json、各 recon 报告、dump 诊断、验证脚本)
|
|
66
|
+
安装前备份:F:\DSH\_backup\profile-20260928-162128\
|
package/docs/install.md
CHANGED
|
@@ -73,6 +73,6 @@ rm -rf ../gaussdb-pg && cp -r packages/pg ../gaussdb-pg
|
|
|
73
73
|
## 常见问题
|
|
74
74
|
|
|
75
75
|
- **侧边栏没有「数据库」tab**:确认 dsh-better-sidebar >= 0.12 已随 DSH 加载;本地开发模式确认 `--patch ./cordis.patch.yml` 生效。
|
|
76
|
-
- **插件导入失败(punycode / resolve.paths)**:宿主 `dsh-app-boot@0.1.7-rc.2`
|
|
76
|
+
- **插件导入失败(punycode / resolve.paths)**:宿主 `dsh-app-boot@0.1.7-rc.2` 已知缺陷,触发条件是插件依赖树含 core-module 同名包(mongodb 链的 `tr46 → punycode` 即元凶)。**0.1.9 起 mongodb 驱动已 bundle 进发布包(`vendor/mongodb-driver.cjs`),本插件依赖树不含 punycode,正常安装不再触发此 bug**——包括无法打补丁的桌面版 app.asar 形态。若错误仍指向其他插件,或需修复宿主自身,在插件目录运行 `npm run patch:dsh`(macOS/Linux 用 `patch:dsh:sh`)一键修复,详见 README「Troubleshooting」。从源码安装(`npm install` 后直接用)时构建链会自动产出 bundle;缺失时可单独跑 `npm run build:mongodb`。
|
|
77
77
|
- **API 全部报错**:host 端 webServer 须注册前缀 `/dsh-db-tool/api`(loopback only);前端与 host 前缀不一致是最常见的接错方式。
|
|
78
78
|
- **连接测试通过但查询报 UNAUTHORIZED_PROJECT**:当前项目路径未被授权,在「授权」面板为该项目路径授 ro 或 rw。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-db-tool",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.3.0",
|
|
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": {
|
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
"test": "vitest run",
|
|
27
27
|
"test:watch": "vitest",
|
|
28
28
|
"typecheck": "tsc --noEmit",
|
|
29
|
-
"build": "tsc -p tsconfig.build.json && node -e \"const fs=require('fs'); fs.mkdirSync('dist/script',{recursive:true}); fs.copyFileSync('lib/script/worker.cjs','dist/script/worker.cjs'); console.log('build OK')\"",
|
|
29
|
+
"build": "tsc -p tsconfig.build.json && node -e \"const fs=require('fs'); fs.mkdirSync('dist/script',{recursive:true}); fs.copyFileSync('lib/script/worker.cjs','dist/script/worker.cjs'); console.log('build OK')\" && node scripts/build-mongodb.mjs",
|
|
30
|
+
"build:mongodb": "node scripts/build-mongodb.mjs",
|
|
31
|
+
"prepack": "npm run build",
|
|
30
32
|
"build:gaussdb": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/build-gaussdb.ps1",
|
|
31
33
|
"build:gaussdb:sh": "bash scripts/build-gaussdb.sh",
|
|
32
34
|
"patch:dsh": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/apply-dsh-hotfix.ps1",
|
|
@@ -45,18 +47,22 @@
|
|
|
45
47
|
},
|
|
46
48
|
"dependencies": {
|
|
47
49
|
"better-sqlite3": "^13.0.3",
|
|
50
|
+
"bson": "^7.2.0",
|
|
48
51
|
"dmdb": "^1.0.52452",
|
|
49
|
-
"mongodb": "^7.6.0",
|
|
50
52
|
"mysql2": "^3.24.4",
|
|
51
53
|
"oracledb": "^7.0.1",
|
|
52
54
|
"pg": "^8.23.0",
|
|
53
55
|
"redis": "^6.2.1"
|
|
54
56
|
},
|
|
55
57
|
"devDependencies": {
|
|
58
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
56
59
|
"@stryker-mutator/core": "^10.0.0",
|
|
57
60
|
"@stryker-mutator/vitest-runner": "^10.0.0",
|
|
58
61
|
"@types/better-sqlite3": "latest",
|
|
59
62
|
"@types/node": "latest",
|
|
63
|
+
"dsh-better-sidebar": "^0.24.1",
|
|
64
|
+
"esbuild": "^0.28.0",
|
|
65
|
+
"mongodb": "^7.6.0",
|
|
60
66
|
"stryker-cli": "^1.1.0",
|
|
61
67
|
"typescript": "^5.9.3",
|
|
62
68
|
"vitest": "^4.1.11"
|
|
@@ -76,6 +82,7 @@
|
|
|
76
82
|
"docs",
|
|
77
83
|
"patches",
|
|
78
84
|
"scripts",
|
|
85
|
+
"vendor/mongodb-driver.cjs",
|
|
79
86
|
"README.md"
|
|
80
87
|
]
|
|
81
88
|
}
|
|
@@ -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,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 构建 vendor/mongodb-driver.cjs:把 mongodb 驱动 bundle 成单文件(esbuild JS API)。
|
|
4
|
+
*
|
|
5
|
+
* 背景:DSH 宿主 @deepseek-ai/dsh-app-boot(<= 0.1.7-rc.2)在插件安装/加载时对依赖树
|
|
6
|
+
* 逐包调 `require.resolve.paths(name)` 且未防护 null 返回值(for..of null → TypeError)。
|
|
7
|
+
* mongodb → mongodb-connection-string-url → whatwg-url → tr46 → punycode 链会让生产依赖树
|
|
8
|
+
* 出现核心模块同名包 punycode,在未修复宿主上直接炸掉整个插件。bundle 后 punycode/
|
|
9
|
+
* whatwg-url/tr46/@mongodb-js/saslprep 全部内联进产物,不再出现在依赖树。
|
|
10
|
+
*
|
|
11
|
+
* --external:bson 是刻意设计:bson 保留在 dependencies,bundle 内 mongodb re-export 的
|
|
12
|
+
* ObjectId/Binary 等来自运行时 require('bson'),与测试/插件其他代码 require 的 bson 是
|
|
13
|
+
* 同一模块实例(instanceof 兼容,避免双份 BSON 类)。其余 externals 为 mongodb 全部
|
|
14
|
+
* optional peer 依赖(驱动对其本就 try/catch 降级,缺失不影响功能)。
|
|
15
|
+
*
|
|
16
|
+
* 每次执行全量重建(esbuild 亚秒级),不做幂等跳过,避免"源升级产物旧"。
|
|
17
|
+
*/
|
|
18
|
+
import { build } from 'esbuild';
|
|
19
|
+
import { mkdirSync, statSync } from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
24
|
+
const outfile = path.join(root, 'vendor', 'mongodb-driver.cjs');
|
|
25
|
+
|
|
26
|
+
/** bson 必须 external(实例一致性,见文件头注释);其余为 mongodb optional peers */
|
|
27
|
+
const externals = [
|
|
28
|
+
'bson',
|
|
29
|
+
'bson-ext',
|
|
30
|
+
'kerberos',
|
|
31
|
+
'snappy',
|
|
32
|
+
'socks',
|
|
33
|
+
'gcp-metadata',
|
|
34
|
+
'@aws-sdk/credential-providers',
|
|
35
|
+
'@mongodb-js/zstd',
|
|
36
|
+
'mongodb-client-encryption',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const entry = path.join(root, 'node_modules', 'mongodb', 'lib', 'index.js');
|
|
40
|
+
if (!statSync(entry, { throwIfNoEntry: false })) {
|
|
41
|
+
console.error('[build-mongodb] 未找到 node_modules/mongodb/lib/index.js —— 请先 npm install(mongodb 在 devDependencies,提供 bundle 源)');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
mkdirSync(path.dirname(outfile), { recursive: true });
|
|
46
|
+
await build({
|
|
47
|
+
entryPoints: [entry],
|
|
48
|
+
outfile,
|
|
49
|
+
bundle: true,
|
|
50
|
+
platform: 'node',
|
|
51
|
+
format: 'cjs',
|
|
52
|
+
target: 'node20',
|
|
53
|
+
external: externals,
|
|
54
|
+
legalComments: 'none',
|
|
55
|
+
sourcemap: false,
|
|
56
|
+
minify: false,
|
|
57
|
+
logLevel: 'warning',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const size = statSync(outfile).size;
|
|
61
|
+
console.log(`[build-mongodb] wrote vendor/mongodb-driver.cjs (${(size / 1024 / 1024).toFixed(2)} MB)`);
|
|
62
|
+
console.log('[build-mongodb] done');
|
|
@@ -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;
|
package/scripts/hotfix-core.mjs
CHANGED
|
@@ -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
|
|
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()(
|
|
8
|
+
* 导致 hoisted profile 下 npm 安装插件导入失败。rc.2 起共两处同型缺陷:
|
|
9
|
+
* 1. routeScoped()(rc.2 L1419 区域 / 0.2.0-rc.1 L1423 区域)—— 症状直接暴露;
|
|
10
|
+
* 2. nativePackageDir()(rc.2 L1244 区域 / 0.2.0-rc.1 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,13 @@ 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 = rc.2 行号 +1)。 */
|
|
27
|
+
export const TARGET_VERSIONS = ['0.1.7-rc.2', '0.2.0-rc.1'];
|
|
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
|
+
};
|
|
23
33
|
/** 判断目标文件是否已打补丁的标识串(hotfix 注释首行片段)。 */
|
|
24
34
|
export const HOTFIX_MARKER = 'local hotfix: resolve.paths returns null';
|
|
25
35
|
/** dsh-app-boot 内 lib/index.js 相对包根的路径。 */
|
|
@@ -287,15 +297,18 @@ function isGitApplyable(indexFile, patchFile) {
|
|
|
287
297
|
/** 命令行主入口。@returns {number} 退出码 */
|
|
288
298
|
export function run(argv = process.argv.slice(2)) {
|
|
289
299
|
let dshRoot;
|
|
290
|
-
let patchFile = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'patches',
|
|
300
|
+
let patchFile = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'patches', PATCH_BY_VERSION[TARGET_VERSION]);
|
|
301
|
+
let patchFileExplicit = false;
|
|
291
302
|
let force = false;
|
|
292
303
|
let revert = false;
|
|
293
304
|
let dryRun = false;
|
|
294
305
|
for (let i = 0; i < argv.length; i++) {
|
|
295
306
|
const arg = argv[i];
|
|
296
307
|
if (arg === '--dsh-root') dshRoot = argv[++i];
|
|
297
|
-
else if (arg === '--patch-file')
|
|
298
|
-
|
|
308
|
+
else if (arg === '--patch-file') {
|
|
309
|
+
patchFile = argv[++i];
|
|
310
|
+
patchFileExplicit = true;
|
|
311
|
+
} else if (arg === '--force') force = true;
|
|
299
312
|
else if (arg === '--revert') revert = true;
|
|
300
313
|
else if (arg === '--dry-run') dryRun = true;
|
|
301
314
|
else if (arg === '--help' || arg === '-h') {
|
|
@@ -324,11 +337,25 @@ export function run(argv = process.argv.slice(2)) {
|
|
|
324
337
|
} catch {
|
|
325
338
|
/* 读不到版本号时跳过比对 */
|
|
326
339
|
}
|
|
340
|
+
// 按安装版本选择行号匹配的补丁(未显式指定 --patch-file 时);
|
|
341
|
+
// 锚点内容各版本一致,选错补丁只会让 git-apply 路径失配回退内建替换,但优先选对。
|
|
342
|
+
if (!patchFileExplicit && actualVersion && PATCH_BY_VERSION[actualVersion]) {
|
|
343
|
+
patchFile = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'patches', PATCH_BY_VERSION[actualVersion]);
|
|
344
|
+
}
|
|
327
345
|
const patchText = fs.existsSync(patchFile) ? fs.readFileSync(patchFile, 'utf8') : '';
|
|
328
|
-
const declared = parsePatchTargetVersion(patchText)
|
|
329
|
-
if (actualVersion &&
|
|
330
|
-
|
|
331
|
-
|
|
346
|
+
const declared = parsePatchTargetVersion(patchText);
|
|
347
|
+
if (actualVersion && !force) {
|
|
348
|
+
if (declared) {
|
|
349
|
+
if (declared !== actualVersion) {
|
|
350
|
+
console.warn(`[dsh-hotfix] version mismatch: installed ${actualVersion} != patch target ${declared}. Use --force to apply anyway.`);
|
|
351
|
+
return 2;
|
|
352
|
+
}
|
|
353
|
+
} else if (!TARGET_VERSIONS.includes(actualVersion)) {
|
|
354
|
+
console.warn(
|
|
355
|
+
`[dsh-hotfix] version mismatch: installed ${actualVersion} is not supported (supported: ${TARGET_VERSIONS.join(', ')}) and patch declares no Target-Version. Use --force to apply anyway.`,
|
|
356
|
+
);
|
|
357
|
+
return 2;
|
|
358
|
+
}
|
|
332
359
|
}
|
|
333
360
|
|
|
334
361
|
let content;
|