dsh-db-tool 0.1.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.
Files changed (41) hide show
  1. package/README.md +66 -0
  2. package/client/README.md +53 -0
  3. package/client/client.js +1128 -0
  4. package/cordis.patch.yml +6 -0
  5. package/dist/adapters/dmdb/index.js +326 -0
  6. package/dist/adapters/gaussdb/index.js +32 -0
  7. package/dist/adapters/index.js +41 -0
  8. package/dist/adapters/mongodb/index.js +508 -0
  9. package/dist/adapters/mysql/index.js +222 -0
  10. package/dist/adapters/oracle/index.js +369 -0
  11. package/dist/adapters/postgresql/index.js +15 -0
  12. package/dist/adapters/redis/index.js +492 -0
  13. package/dist/adapters/sql-shared/common.js +69 -0
  14. package/dist/adapters/sql-shared/pg-like.js +170 -0
  15. package/dist/adapters/sqlite/index.js +106 -0
  16. package/dist/adapters/types.js +1 -0
  17. package/dist/guard/index.js +221 -0
  18. package/dist/http/index.js +396 -0
  19. package/dist/index.js +130 -0
  20. package/dist/manager.js +469 -0
  21. package/dist/script/runner.js +147 -0
  22. package/dist/script/worker.cjs +104 -0
  23. package/dist/store/audit.js +41 -0
  24. package/dist/store/connections.js +167 -0
  25. package/dist/store/grants.js +82 -0
  26. package/dist/store/index.js +41 -0
  27. package/dist/store/io.js +66 -0
  28. package/dist/store/normalize.js +22 -0
  29. package/dist/store/secrets.js +42 -0
  30. package/docs/api-contract.md +76 -0
  31. package/docs/apple-redesign-spec.md +80 -0
  32. package/docs/dsh-market-submission/mengqi1436__dsh-db-tool.yml +6 -0
  33. package/docs/install.md +75 -0
  34. package/docs/review-findings.md +95 -0
  35. package/docs/skill.md +48 -0
  36. package/package.json +70 -0
  37. package/scripts/build-gaussdb.ps1 +72 -0
  38. package/scripts/build-gaussdb.sh +53 -0
  39. package/scripts/diag-profile.mjs +21 -0
  40. package/scripts/verify-host.mjs +53 -0
  41. package/skills/db-admin/SKILL.md +138 -0
@@ -0,0 +1,80 @@
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。
@@ -0,0 +1,6 @@
1
+ url: https://github.com/mengqi1436/dsh-db-tool
2
+ name: mengqi1436/dsh-db-tool
3
+ category: tools
4
+ description:
5
+ en: Chat-operated database admin for 8 databases (MySQL, PostgreSQL, GaussDB, SQLite, Oracle, DM, Redis, MongoDB): a DatabaseManager tool with read-only/read-write per-connection modes, per-project grants, one-time challenge confirmation for dangerous operations, sandboxed script execution, plus a sidebar with connection management, project authorization, a Navicat-style object browser and an SQL console.
6
+ zh: 聊天操作数据库的管理插件,支持 8 种数据库(MySQL、PostgreSQL、GaussDB、SQLite、Oracle、达梦、Redis、MongoDB):DatabaseManager 工具带连接级只读/读写分级、项目级授权、危险操作一次性 challenge 对话内确认与沙箱脚本执行;侧边栏提供连接管理、项目授权、Navicat 式对象树浏览和 SQL 控制台。
@@ -0,0 +1,75 @@
1
+ # dsh-db-tool 安装指南
2
+
3
+ dsh-db-tool 是 DSH(DeepSeek Harness)插件:侧边栏管理数据库连接与授权,聊天内经 DatabaseManager 工具操作 8 种数据库(MySQL、PostgreSQL、GaussDB、SQLite、Redis、MongoDB、Oracle、达梦)。
4
+
5
+ ## 一键安装(发布模式)
6
+
7
+ ```powershell
8
+ dsh plugin --profile web add "dsh-db-tool@github:<user>/dsh-db-tool"
9
+ ```
10
+
11
+ 把 `<user>` 换成实际发布仓库的 GitHub 用户名/组织。安装后侧边栏出现「数据库」tab,即装即用。
12
+
13
+ > GaussDB 支持依赖 `vendor/gaussdb-pg` 产物(见下文);不使用 GaussDB 时可不构建,其余 7 库开箱即用。
14
+
15
+ ## 本地开发模式
16
+
17
+ 在仓库根目录执行:
18
+
19
+ ```powershell
20
+ pnpm dsh web --patch ./cordis.patch.yml
21
+ ```
22
+
23
+ `cordis.patch.yml` 会向 DSH 的 cordis 配置注入 `id: db-tool` 的插件条目,指向本仓库入口;改动 `client/client.js` 后重启该命令生效。
24
+
25
+ ## GaussDB vendor 构建
26
+
27
+ 官方 [openGauss-connector-nodejs](https://gitcode.com/opengauss/openGauss-connector-nodejs) 未发布 npm,需从源码构建(详见 `scripts/build-gaussdb.sh` 头注释):
28
+
29
+ ```bash
30
+ bash scripts/build-gaussdb.sh # 幂等:已有产物则跳过
31
+ bash scripts/build-gaussdb.sh --force # 强制重新拉取并构建
32
+ npm run build:gaussdb # 等价入口
33
+ ```
34
+
35
+ 脚本失败(网络等)时可手动执行同样步骤:
36
+
37
+ ```bash
38
+ git clone https://gitcode.com/opengauss/openGauss-connector-nodejs vendor/gaussdb-src
39
+ cd vendor/gaussdb-src && npm install && npm run build
40
+ rm -rf ../gaussdb-pg && cp -r packages/pg ../gaussdb-pg
41
+ # 保留 vendor/gaussdb-pg/package.json 与 dist/
42
+ ```
43
+
44
+ 产物为 `vendor/gaussdb-pg`(CJS,结构同 `pg`:`{ Pool, Client, ... }`)。
45
+
46
+ ## oracledb approve-scripts
47
+
48
+ `oracledb` 安装时需要运行安装脚本编译/下载二进制。仓库 `package.json` 已配置白名单:
49
+
50
+ ```jsonc
51
+ "allowScripts": { "oracledb@7.0.1": true }
52
+ ```
53
+
54
+ 若你的 npm/pnpm 版本提示 approve-scripts 拦截,按提示批准 `oracledb` 即可;已配置白名单的环境无需额外操作。
55
+
56
+ ## 真机测试
57
+
58
+ 单测(vitest)不依赖真实数据库;要跑真机冒烟,设置以下环境变量后执行 `npm test`(或对应集成脚本):
59
+
60
+ | 环境变量 | 示例 |
61
+ |---|---|
62
+ | `DBT_TEST_MYSQL_URL` | `mysql://user:pass@127.0.0.1:3306/test` |
63
+ | `DBT_TEST_PG_URL` | `postgres://user:pass@127.0.0.1:5432/test` |
64
+ | `DBT_TEST_REDIS_URL` | `redis://127.0.0.1:6379/0` |
65
+ | `DBT_TEST_MONGO_URL` | `mongodb://127.0.0.1:27017/test` |
66
+ | `DBT_TEST_ORACLE_CONNECT` | Oracle 连接串/ezconnect 描述 |
67
+ | `DBT_TEST_DM_CONNECT` | 达梦连接串 |
68
+
69
+ 未设置对应变量时该库的真机用例自动跳过。
70
+
71
+ ## 常见问题
72
+
73
+ - **侧边栏没有「数据库」tab**:确认 dsh-better-sidebar >= 0.12 已随 DSH 加载;本地开发模式确认 `--patch ./cordis.patch.yml` 生效。
74
+ - **API 全部报错**:host 端 webServer 须注册前缀 `/dsh-db-tool/api`(loopback only);前端与 host 前缀不一致是最常见的接错方式。
75
+ - **连接测试通过但查询报 UNAUTHORIZED_PROJECT**:当前项目路径未被授权,在「授权」面板为该项目路径授 ro 或 rw。
@@ -0,0 +1,95 @@
1
+ # 代码审查发现与处置记录
2
+
3
+ 三路并行审查(lib 核心 / 8 适配器 / client+tests+scripts),共 38 条 findings,全部处置完毕。
4
+ 处置分类:✅已修复 / 🧪已补测试 / ⏸有意保留(附理由)。
5
+
6
+ ## 一、lib 核心(store / guard / manager / http / index)
7
+
8
+ | 级别 | 位置 | 问题 | 处置 |
9
+ |---|---|---|---|
10
+ | critical | lib/manager.ts(原 vm 沙箱 run_script) | `console.log.constructor('return process')()` 逃逸 vm | ✅ runScript 改子进程隔离:lib/script/worker.cjs(vm.runInNewContext + Node permission model)+ lib/script/runner.ts(fork、SIGKILL 强超时 60s) |
11
+ | high | lib/manager.ts challenge | challenge 未绑定 connId/projectKey,可跨连接重放 | ✅ ChallengeScope{connId,projectKey},create/consume 双向校验 |
12
+ | high | lib/store/connections.ts redactUrl | 正则 `[^/]*@` 密码含 `/` 时明文入库 | ✅ 重写为 authority 段 lastIndexOf('@')/lastIndexOf(':) 解析 |
13
+ | high | lib/manager.ts adapterCache | 缓存键不含 mode,ro→rw 升级后仍用旧连接 | ✅ 缓存键 `connId@mode=...`;grant/revoke/update/remove 均 dropAdapters |
14
+ | high | lib/guard MONGO_WRITE_OPS | 缺 deleteMany/findAndModify/mapReduce 等 | ✅ 补齐 + READ_OPS 改为 import 适配器导出白名单(单一事实来源) |
15
+ | high | lib/guard query 通道 | Redis/Mongo 写命令经 query 仅 warning 放行 | ✅ classifyRedis/classifyMongo 接收通道参数,query 命中写/未识别命令一律 danger(fail-closed) |
16
+ | high | lib/guard 语句分析 | `WITH ... DELETE` / `EXPLAIN DELETE` 免确认 | ✅ SQL_WRITE_BODY_RE 读头命中后补扫语句体;HIDDEN_DANGER_RE 改 `[\s\S]*` |
17
+ | medium | guard FOR UPDATE | 多行语句漏检 | ✅ 同上(全语句扫描) |
18
+ | medium | manager updateConnection | 更新后不清适配器缓存 | ✅ void this.dropAdapters(id) |
19
+ | medium | manager runScript 超时 | 超时后后台写继续执行 | ✅ 子进程 SIGKILL + 关会话适配器 |
20
+ | medium | manager runScript | 单 challenge 可确认多条语句 | ✅ 子进程化后每次 db.execute 独立走 guard(逐条 challenge) |
21
+ | medium | manager 审计时序 | 审计在执行前记 ok:true | ✅ 审计后置,成功/失败如实记录 |
22
+ | medium | store remove 级联 | 非原子,中途失败留脏数据 | ✅ 失败开放顺序 grants→secrets→connections |
23
+ | medium | manager params | 参数未校验类型 | ✅ 非数组抛 INVALID_ARGUMENT |
24
+ | medium | http parseSsl | `ssl="false"` 被 Boolean() 转为 true | ✅ 严格布尔解析,非法值 400 |
25
+ | medium | store normalize | 仅小写盘符不处理其他大小写语义 | ⏸ 保留:NTFS 大小写不敏感语义即此;已补 `..` 穿越用例 🧪 |
26
+ | medium | store audit tail | 全量读文件再截取 | ⏸ 保留:审计文件预期规模小(JSONL 追加),优化属 YAGNI |
27
+ | **security** | guard classifySql(变异测试增补发现) | ① body 正则作用于未剥注释原文 → 注释内写词误报 danger;② SQL_DDL/DML/MAINT 锚定原文 → 前导块注释 `/* x */ DROP` 使 DDL 漏判、execute 通道仅 warning **绕过确认** | ✅ 提取 stripSqlComments,语句体分析与 DDL/DML 锚定一律基于剥注释文本(误报与绕过同修) |
28
+
29
+ ## 二、8 适配器
30
+
31
+ | 级别 | 位置 | 问题 | 处置 |
32
+ |---|---|---|---|
33
+ | high | mysql ro query | 无语句校验、无会话只读 | ✅ SET SESSION TRANSACTION READ ONLY + query 首词白名单 |
34
+ | high | mongodb aggregate | 未拒 $out/$merge(可写副作用) | ✅ 双清单递归:JS_INJECTION_KEYS + WRITE_STAGE_KEYS(含 $unionWith) |
35
+ | high | mongodb assertNoWhere | 缺 $function/$accumulator | ✅ 递归拦截 |
36
+ | medium | mongodb DANGEROUS_OPS | 缺复数/别名操作 | ✅ 8→11(createIndexes/dropIndexes/renameCollection) |
37
+ | medium | redis DANGEROUS_COMMANDS | 缺 ACL dangerous 类 | ✅ 13→19(SLAVEOF/SORT_RO/MODULE/FUNCTION/SCRIPT/RESET) |
38
+ | medium | pg-like ro SET | SET 失败静默 | ✅ fail-closed release(err) |
39
+ | medium | mysql defaultDb | URL pathname 未校验 | ✅ 过 assertIdent |
40
+ | medium | redis SCAN | 不透传 MATCH/COUNT | ✅ 透传(clamp 200) |
41
+ | medium | redis HGETALL 等 | 无截断 | ✅ 500 行 + truncated 标注 |
42
+ | medium | oracle/dmdb query | autoCommit:false 事务悬挂 | ✅ execOpts 默认 autoCommit:true,tx 内显式 false 覆盖 |
43
+
44
+ ## 三、client / tests / scripts
45
+
46
+ | 级别 | 位置 | 问题 | 处置 |
47
+ |---|---|---|---|
48
+ | high | client.js 分页 | 无 offset 翻页无效 | ✅ 后端补 previewRows 第 4 参 offset 全链路(types/manager/http/tool/schema + 8 适配器),client 带 offset |
49
+ | medium | client.js ConsoleView | database 输入框无效 | ✅ 移除 |
50
+ | medium | client.js BrowseView | useEffect 竞态 | ✅ cancelled 标志 |
51
+ | medium | gaussdb.test vendorReady | 探测路径错(旧布局) | ✅ 改 vendor/gaussdb-pg/packages/pg/package.json;顺带发现并修复 lib/adapters/gaussdb/index.ts VENDOR_PATH 三级相对路径真 bug(`../../` 只到 lib/) |
52
+ | medium | build 脚本 skip 检查 | 旧布局路径导致每次重建 | ✅ sh/ps1 均改 packages/pg/package.json |
53
+ | medium | audit.spec | 断言缺字段 | ✅ 补 danger/confirmed/projectPathKey/rowsAffected |
54
+ | medium | normalize.spec | 缺 `..` 穿越用例 | ✅ 已补(win32+posix 双分支) |
55
+ | medium | api.spec | query 通道 challenge 三步无用例 | ✅ 已补(首次确认→成功→篡改重放拒→原句重放拒,符合防探测语义) |
56
+ | medium | mysql/postgresql.test | beforeAll 在 describe.skip 外 | ✅ 移入 suite() 回调 |
57
+
58
+ ## 变异测试
59
+
60
+ - Stryker(vitest runner,范围 lib/guard + lib/manager + lib/store):初版 vitest@5 与 Stryker 兼容性缺陷(phantom survivors,假分 2%)→ 降级 vitest@4 后正常。
61
+ - 基线 57.29%(654 killed / 367 survived / 122 no-cov),高价值存活项已定向补测试(dispose 生命周期、normalize/io 边界、redactUrl 变体、grants 边界、guard 注释剥离、manager 构造默认值)。
62
+
63
+ ## 第二轮:全库审查+优化(5 teammate 按模块并行)
64
+
65
+ 35 项修复 + 13 项报告保留,全量 277 passed / 18 skipped,tsc 0。
66
+
67
+ ### 已修复(按模块)
68
+
69
+ | 模块 | 修复 |
70
+ |---|---|
71
+ | lib 核心(guard-tool) | ① runScript 脚本内 dbQuery 句柄补传顶层 challengeId(原仅 dbExecute 传,行为不一致);② runner.ts IPC `msg:any` → unknown+结构类型,msg.args 加 Array.isArray 守卫;③ lib/index.ts 重复 import 合并;④ TOOL_DESCRIPTION run_script 描述与子进程实现同步;⑤ **[high] Mongo execute 通道未识别 op fail-open(none)→ warning(fail-closed,与 SQL/Redis 对齐)**;⑥ MONGO_WRITE_OPS 补命令文档形式 delete/deleteMany/update/create,`{drop:...}` 归 danger(等同 dropCollection,MONGO_DANGEROUS_EXTRA);⑦ **fork env 白名单 minimalEnv()(11 项),宿主敏感环境变量不再透传脚本子进程**;+5 测试用例(tests/tool/runner-env.spec.ts 新建) |
72
+ | store(store-adapters-core) | ① **[high] writeJsonAtomic 临时文件以目标 fileMode 创建**(原 0644 出生,rename 前窗口期明文机密宽权限暴露);② DSH_HOME 空串产出相对路径 → `?? (…\|\|…)`;③ 删死类型 AdapterRegistry;④ driverMissingError cause 空时文案去多余冒号;⑤ io.spec 补 .bak 现场保留断言 |
73
+ | SQL 适配器(sql-adapters) | ① **mysql-offline 假绿断言实锤**:`.catch()` 吞外层断言失败且 label 拼错(「列出数据库」vs 实际「列出表」),旧白名单回归完全逃逸测试 → 改先捕获再双断言;② mysql URL decodeURIComponent 非法编码裸 URIError → 人类可读提示;③ gaussdb 驱动加载失败附原始错误(区分未构建 vs 产物损坏);④ gaussdb 条件用例 if-return 假 pass → it.skipIf;⑤ MysqlTx/测试 inline 类型统一复用共享 TxHandle |
74
+ | NoSQL/企业适配器(nosql-ent-adapters) | ① oracle resolveOracleConn url/fields 两分支 user 空串校验;② oracle/dmdb testConnect 在 v$version/V$VERSION 不可读(ORA-00942 受限环境)时兜底 `SELECT 1 FROM dual` 判活,不再误报连接失败;③ oracle 声明 TDZ 隐式依赖消除(上移到使用点前);④ dmdb DmPoolLike 删未使用的 execute 成员(与「Pool 无 execute」注释矛盾);⑤ redis buildUrl user 加 encodeURIComponent 且空串跳过(含 @/: 用户名坏 URL);⑥ redis OBJECT/MEMORY 缺 key 参数拦截;⑦ mongodb insertOne 删 cmd.insert 误导性别名;+4 测试用例 |
75
+ | client/skill/docs(client-skill) | ① openTable 竞态:openSeq 序号守卫,旧翻页/切表响应晚到不再覆盖新数据;② auditTable 先截断 80 字符致 title 也是截断版 → 全文进 td+ellipsis,title 显示全文;③ CSS .dbt-tree* 双份定义去重、mono 字体 3 处硬编码统一 var(--dbt-mono);④ i18n ok 文案「确定」→「操作完成」(语义修正,key 冻结);⑤ SKILL.md run_script 段按子进程+permission model 重写、challengeId 示例改真实格式 c_[0-9a-f]{24}、补 {text,isError} 返回契约说明 |
76
+
77
+ ### 报告保留(附理由)
78
+
79
+ | 级别 | 项 | 理由 |
80
+ |---|---|---|
81
+ | medium | pg/gaussdb ro query 无应用层写白名单(仅服务器级只读会话) | CTE(WITH...INSERT)使首词白名单在 PG 语义下不可行;服务层 guard query 通道已拦写语句(SQL_WRITE_BODY_RE 覆盖 CTE 写),双防线已足够 |
82
+ | medium | mysql ro 会话 SET 失败静默 catch | mysql2 无 release(err) 语义;destroy 有重连风暴风险,权衡后保留 |
83
+ | medium | connections update url↔fields 互切残留旧机密 | 与客户端「留空=保留旧密码」语义自洽,url 优先约定由适配器工厂执行;将来可文档固化 |
84
+ | medium | connections create 两阶段写无回滚(孤儿 secrets) | 失败开放方向正确(残留机密好过残留授权);将来可加启动期清扫 |
85
+ | low | guard 字符串字面量内写词误报(SELECT 'delete me') | 方向安全(误报优于漏报),精确化需字符串感知剥离,成本大于收益 |
86
+ | low | ro 连接 query+challenge 确认后由适配器只读兜底拒绝 | 语义无洞(ro 写不可能成功),仅体验 |
87
+ | low | /api/grants PUT 不校验 connId 存在性 | 授权到不存在连接无实害 |
88
+ | low | oracle/dmdb toExecResult 双份同构实现 | 合并需跨模块共享工具层,架构决策,收益低 |
89
+ | low | mongodb aggregate/distinct truncated 用 rows.length>=500 近似 | 恰 500 行误标,精确判定需额外查询,代价大于收益 |
90
+ | low | dm 免密(trust 登录)场景 user 空串未强制校验 | 保留免密可能性,不武断加校验 |
91
+ | low | 事务悬挂时适配器 close 挂起 | 服务层 tx 已 finally rollback 覆盖正常路径;跨适配器行为变更留待需要时 |
92
+ | low | ManageView testConn/loadAudit 无 busy 保护 | 幂等读,testInfo 按 id 分 key 不互踩 |
93
+ | low | 翻页按钮 disabled 在 truncated undefined 时可点 | 可能翻到空页,无数据损害;后端已恒返回 truncated 后可收紧 |
94
+
95
+ > 注:文档早前「DANGEROUS_OPS 8 项」实为 11 项(createIndexes/dropIndexes/renameCollection),以代码为准。
package/docs/skill.md ADDED
@@ -0,0 +1,48 @@
1
+ # dsh-db-tool Skill:db-admin
2
+
3
+ 本插件随附一个 DSH skill:`skills/db-admin/SKILL.md`。安装插件后,模型在涉及数据库操作的任务中会自动按该 skill 的指引调用 **DatabaseManager** 工具;用户也可显式唤起(如"帮我看看 xxx 库里有哪些表")。
4
+
5
+ ## 定位与分工
6
+
7
+ | 入口 | 面向 | 能力 |
8
+ |---|---|---|
9
+ | DatabaseManager 工具(skill 驱动) | 模型 | 6 个 action:查询、执行、浏览、预览等,详见下文 |
10
+ | 侧边栏「数据库」tab(client/) | 用户 | 连接 CRUD、项目授权(ro/rw)、浏览预览、SQL 控制台、审计 |
11
+
12
+ 授权关系只在侧边栏维护:**项目路径 → 连接 → ro/rw**。工具侧只消费授权结果,不能自我提权。
13
+
14
+ ## 六个 action 速览
15
+
16
+ 完整用法、方言差异与示例见 `skills/db-admin/SKILL.md`(模型实际读取的就是它),此处仅列签名:
17
+
18
+ ```
19
+ list_connections # 当前项目已授权的连接
20
+ query { conn_id, sql, params? } # 只读查询
21
+ execute { conn_id, statement, params? } # 单条写语句(rw;DDL/DML 触发确认)
22
+ schema { conn_id, database?, table? } # 库列表 / 表列表 / 列结构(三级递进)
23
+ preview { conn_id, table, limit?, database? } # 预览,limit 上限 50
24
+ run_script { conn_id, code } # Redis 命令数组 JSON / Mongo BSON 文档 JSON
25
+ ```
26
+
27
+ 可选 `challenge_id` 用于危险操作确认后的重试。
28
+
29
+ ## 危险确认通道
30
+
31
+ 模型侧与侧边栏控制台共用同一套确认语义:
32
+
33
+ 1. 首次执行危险语句返回 `{"ok":false,"code":"NEEDS_CONFIRMATION","challengeId","statement","danger","reason"}`。
34
+ 2. 模型向用户说明并征得同意(侧边栏则弹 DangerDialog)。
35
+ 3. 携同一 `challengeId` 重发;challenge 一次性、绑定语句 hash、5 分钟过期,失效返回 `INVALID_CHALLENGE` 需从头再来。
36
+
37
+ ## SKILL.md 规范要点(维护者参考)
38
+
39
+ - 目录形态:`skills/db-admin/SKILL.md`,frontmatter 的 `name` 必须是 kebab-case(`^[a-z0-9]+(-[a-z0-9]+)*$`),`description` 供模型路由、渲染上限 500 字符、单行。
40
+ - 发现根:`<projectRoot>/.dsh/skills` 等(随插件安装的 skill 由插件分发)。
41
+ - 可选 frontmatter:`whenToUse`、`disable-model-invocation`、`user-invocable`——本 skill 均用默认值(允许模型自动调用)。
42
+ - 修改方言速查/确认流程时,保持 SKILL.md 与 `lib/adapters/types.ts`、`lib/guard/index.ts`、`docs/api-contract.md` 三处一致。
43
+
44
+ ## 排错
45
+
46
+ - 工具返回 `UNAUTHORIZED_PROJECT`:项目路径未授权 → 侧边栏「授权」面板授权。
47
+ - 返回 `READ_ONLY`:ro 授权下执行写 → 需用户升级为 rw。
48
+ - `INVALID_ARGUMENT: 未知 action`:只接受上文 6 个 action 名。
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "dsh-db-tool",
3
+ "version": "0.1.0",
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
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": ">=20",
9
+ "dsh": ">=0.1.0-rc.6"
10
+ },
11
+ "main": "dist/index.js",
12
+ "exports": {
13
+ ".": "./dist/index.js",
14
+ "./client": "./client/client.js",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "scripts": {
18
+ "test": "vitest run",
19
+ "test:watch": "vitest",
20
+ "typecheck": "tsc --noEmit",
21
+ "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')\"",
22
+ "build:gaussdb": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/build-gaussdb.ps1",
23
+ "build:gaussdb:sh": "bash scripts/build-gaussdb.sh"
24
+ },
25
+ "dsh": {
26
+ "bundle": {
27
+ "patch": "./cordis.patch.yml"
28
+ },
29
+ "client": {
30
+ "inject": [
31
+ "@deepseek-ai/dsh-client-locale"
32
+ ],
33
+ "platform": "web"
34
+ }
35
+ },
36
+ "dependencies": {
37
+ "better-sqlite3": "^13.0.3",
38
+ "dmdb": "^1.0.52452",
39
+ "mongodb": "^7.6.0",
40
+ "mysql2": "^3.24.4",
41
+ "oracledb": "^7.0.1",
42
+ "pg": "^8.23.0",
43
+ "redis": "^6.2.1"
44
+ },
45
+ "devDependencies": {
46
+ "@stryker-mutator/core": "^10.0.0",
47
+ "@stryker-mutator/vitest-runner": "^10.0.0",
48
+ "@types/better-sqlite3": "latest",
49
+ "@types/node": "latest",
50
+ "stryker-cli": "^1.1.0",
51
+ "typescript": "^5.9.3",
52
+ "vitest": "^4.1.11"
53
+ },
54
+ "peerDependencies": {
55
+ "@deepseek-ai/cordis": "^4.0.1",
56
+ "dsh-better-sidebar": ">=0.12.0"
57
+ },
58
+ "allowScripts": {
59
+ "oracledb@7.0.1": true
60
+ },
61
+ "files": [
62
+ "cordis.patch.yml",
63
+ "dist",
64
+ "client",
65
+ "skills",
66
+ "docs",
67
+ "scripts",
68
+ "README.md"
69
+ ]
70
+ }
@@ -0,0 +1,72 @@
1
+ # Build the official GaussDB (openGauss) Node driver into vendor/.
2
+ # PowerShell version (use when bash/WSL is unavailable on Windows).
3
+ #
4
+ # powershell -ExecutionPolicy Bypass -File scripts/build-gaussdb.ps1 # skip if built
5
+ # powershell -ExecutionPolicy Bypass -File scripts/build-gaussdb.ps1 -Force # rebuild
6
+ #
7
+ # Equivalent to scripts/build-gaussdb.sh. Output: vendor/gaussdb-pg/packages/pg
8
+ # (CJS, pg-like: { Pool, Client, ... }; pg lib requires '../../pg-protocol'/'../../pg-pool',
9
+ # so the monorepo packages/ layout must be preserved)
10
+ # Requires Node >= 16, npm >= 7. Source: gitcode.com/opengauss/openGauss-connector-nodejs (master latest).
11
+ # NOTE: keep this file ASCII-only so Windows PowerShell 5.1 (GBK codepage) can parse it.
12
+ param(
13
+ [switch]$Force
14
+ )
15
+ $ErrorActionPreference = 'Stop'
16
+
17
+ $RepoRoot = Split-Path -Parent $PSScriptRoot
18
+ $Src = Join-Path $RepoRoot 'vendor\gaussdb-src'
19
+ $Dst = Join-Path $RepoRoot 'vendor\gaussdb-pg'
20
+
21
+ if (-not $Force -and (Test-Path (Join-Path $Dst 'packages\pg\package.json'))) {
22
+ Write-Host "[build-gaussdb] $Dst already built, skip (use -Force to rebuild)"
23
+ exit 0
24
+ }
25
+
26
+ if (Test-Path (Join-Path $Src '.git')) {
27
+ Write-Host "[build-gaussdb] source exists, updating..."
28
+ git -C $Src pull --ff-only
29
+ if ($LASTEXITCODE -ne 0) { throw "git pull failed (exit $LASTEXITCODE)" }
30
+ } else {
31
+ Write-Host "[build-gaussdb] cloning openGauss-connector-nodejs..."
32
+ if (Test-Path $Src) { Remove-Item -Recurse -Force $Src }
33
+ New-Item -ItemType Directory -Force -Path (Join-Path $RepoRoot 'vendor') | Out-Null
34
+ git clone https://gitcode.com/opengauss/openGauss-connector-nodejs $Src
35
+ if ($LASTEXITCODE -ne 0) { throw "git clone failed (exit $LASTEXITCODE)" }
36
+ }
37
+
38
+ Write-Host "[build-gaussdb] npm install && npm run build..."
39
+ Push-Location $Src
40
+ try {
41
+ npm install
42
+ if ($LASTEXITCODE -ne 0) { throw "npm install failed (exit $LASTEXITCODE)" }
43
+ npm run build
44
+ if ($LASTEXITCODE -ne 0) { throw "npm run build failed (exit $LASTEXITCODE)" }
45
+ } finally {
46
+ Pop-Location
47
+ }
48
+
49
+ Write-Host "[build-gaussdb] copying packages layout -> vendor/gaussdb-pg/packages ..."
50
+ if (Test-Path $Dst) { Remove-Item -Recurse -Force $Dst }
51
+ New-Item -ItemType Directory -Force (Join-Path $Dst 'packages') | Out-Null
52
+ Copy-Item -Recurse (Join-Path $Src 'packages\pg') (Join-Path $Dst 'packages\pg')
53
+ Copy-Item -Recurse (Join-Path $Src 'packages\pg-protocol') (Join-Path $Dst 'packages\pg-protocol')
54
+ Copy-Item -Recurse (Join-Path $Src 'packages\pg-pool') (Join-Path $Dst 'packages\pg-pool')
55
+
56
+ $PgDir = Join-Path $Dst 'packages\pg'
57
+ Write-Host "[build-gaussdb] installing runtime deps (incl. undeclared p-limit used by fork)..."
58
+ Push-Location $PgDir
59
+ try {
60
+ npm install --omit=dev --no-audit --no-fund
61
+ if ($LASTEXITCODE -ne 0) { throw "npm install failed (exit $LASTEXITCODE)" }
62
+ npm install p-limit --omit=dev --no-audit --no-fund
63
+ if ($LASTEXITCODE -ne 0) { throw "npm install p-limit failed (exit $LASTEXITCODE)" }
64
+ } finally {
65
+ Pop-Location
66
+ }
67
+
68
+ if (-not (Test-Path (Join-Path $PgDir 'package.json'))) {
69
+ throw "build output missing package.json, check $Src\packages\pg layout"
70
+ }
71
+
72
+ Write-Host "[build-gaussdb] done: $PgDir"
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bash
2
+ # 构建 GaussDB(openGauss)官方 Node 驱动 vendor 产物。
3
+ #
4
+ # 官方 openGauss-connector-nodejs 未发布 npm,需从源码构建:
5
+ # bash scripts/build-gaussdb.sh # 已有产物则跳过(幂等)
6
+ # bash scripts/build-gaussdb.sh --force # 强制重新拉取并构建
7
+ # npm run build:gaussdb # 等价入口
8
+ #
9
+ # 手动步骤(脚本因网络等失败时可手动执行):
10
+ # git clone https://gitcode.com/opengauss/openGauss-connector-nodejs vendor/gaussdb-src
11
+ # cd vendor/gaussdb-src && npm install && npm run build
12
+ # rm -rf vendor/gaussdb-pg && cp -r packages/pg vendor/gaussdb-pg
13
+ # # 注意保留 vendor/gaussdb-pg/package.json 与 dist/
14
+ #
15
+ # 产物:vendor/gaussdb-pg/packages/pg(CJS,结构同 pg:{ Pool, Client, ... };
16
+ # pg lib 内 require('../../pg-protocol'/'../../pg-pool') 需 packages/ 布局)
17
+ set -euo pipefail
18
+ cd "$(dirname "$0")/.."
19
+
20
+ SRC=vendor/gaussdb-src
21
+ DST=vendor/gaussdb-pg
22
+ FORCE=0
23
+ if [ "${1:-}" = "--force" ]; then FORCE=1; fi
24
+
25
+ if [ "$FORCE" != "1" ] && [ -f "$DST/packages/pg/package.json" ]; then
26
+ echo "[build-gaussdb] 已存在 $DST/packages/pg/package.json,跳过(--force 可强制重建)"
27
+ exit 0
28
+ fi
29
+
30
+ if [ -d "$SRC/.git" ]; then
31
+ echo "[build-gaussdb] 源码已存在,更新..."
32
+ git -C "$SRC" pull --ff-only
33
+ else
34
+ echo "[build-gaussdb] 克隆 openGauss-connector-nodejs..."
35
+ rm -rf "$SRC"
36
+ mkdir -p vendor
37
+ git clone https://gitcode.com/opengauss/openGauss-connector-nodejs "$SRC"
38
+ fi
39
+
40
+ echo "[build-gaussdb] 安装依赖并构建..."
41
+ (cd "$SRC" && npm install && npm run build)
42
+
43
+ echo "[build-gaussdb] 拷贝 packages 布局 -> $DST"
44
+ rm -rf "$DST"
45
+ mkdir -p "$DST/packages"
46
+ cp -r "$SRC/packages/pg" "$DST/packages/pg"
47
+ # pg lib 内 require('../../pg-protocol'/'../../pg-pool') 按 monorepo packages/ 布局解析
48
+ cp -r "$SRC/packages/pg-protocol" "$DST/packages/pg-protocol"
49
+ cp -r "$SRC/packages/pg-pool" "$DST/packages/pg-pool"
50
+ # 运行依赖(含 fork 源码已用但 package.json 未声明的 p-limit)
51
+ (cd "$DST/packages/pg" && npm install --omit=dev --no-audit --no-fund && npm install p-limit --omit=dev --no-audit --no-fund)
52
+
53
+ echo "[build-gaussdb] 构建完成 -> $DST/packages/pg"
@@ -0,0 +1,21 @@
1
+ /** 诊断:完整复现 DSH 的 profile bundle 组合,输出每层与被跳过的 bundle 及原因。 */
2
+ import { loadProfile } from 'file:///E:/Tool/nvm/v24.19.0/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-app-boot/lib/index.js';
3
+
4
+ const installAnchor = 'E:/Tool/nvm/v24.19.0/node_modules/@deepseek-ai/dsh';
5
+ const profile = loadProfile('dsh', 'web', installAnchor);
6
+ console.log('keys:', Object.keys(profile));
7
+ const composed = profile.profile ?? profile;
8
+
9
+ console.log('=== layers(包名 + patch 条目数)===');
10
+ for (const layer of composed.layers) {
11
+ console.log(`- ${layer.packageName} patches=${layer.patches?.length ?? 0} dir=${layer.packageDir}`);
12
+ }
13
+ console.log('=== skippedBundles ===');
14
+ for (const s of (composed.skippedBundles ?? []) ?? []) {
15
+ console.log(`- ${s.packageName}: ${s.reason}`);
16
+ }
17
+ console.log('=== db-tool patch 条目 ===');
18
+ for (const layer of composed.layers) {
19
+ if (layer.packageName === 'dsh-db-tool') console.log(JSON.stringify(layer.patches, null, 1));
20
+ }
21
+
@@ -0,0 +1,53 @@
1
+ /**
2
+ * 独立验证:模拟 DSH host 的插件加载路径(cordis + 四个注入服务 stub),
3
+ * 验证 dist/index.js 的 apply 全链走通(register 收到 prefix 路由、工具注册、effect 挂载)。
4
+ * DSH_HOME 指向临时目录,不触碰真实 ~/.dsh。
5
+ */
6
+ import * as fs from 'node:fs';
7
+ import * as os from 'node:os';
8
+ import * as path from 'node:path';
9
+
10
+ process.env.DSH_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'dbt-verify-home-'));
11
+
12
+ const { Context } = await import(pathToFileUrl('E:/Tool/nvm/v24.19.0/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/cordis/lib/index.js'));
13
+
14
+ function pathToFileUrl(p) {
15
+ return 'file:///' + p.replace(/\\/g, '/');
16
+ }
17
+
18
+ const registered = { routes: [], tools: [], effects: [] };
19
+
20
+ const app = new Context();
21
+ app.provide('webServer', {
22
+ register(h) {
23
+ registered.routes.push(h);
24
+ return () => registered.routes.splice(registered.routes.indexOf(h), 1);
25
+ },
26
+ });
27
+ app.provide('sessions', { get: () => ({ header: { cwd: 'E:/Code/dsh/dsh-db-tool' } }) });
28
+ app.provide('tools', {
29
+ register(tool) {
30
+ registered.tools.push(tool);
31
+ },
32
+ });
33
+ app.provide('webRuntime', { trustedHosts: [] });
34
+
35
+ const plugin = await import(pathToFileUrl('E:/Code/dsh/dsh-db-tool/dist/index.js'));
36
+ await app.plugin(plugin.default ?? plugin);
37
+ await new Promise((r) => setTimeout(r, 200));
38
+
39
+ console.log('routes:', registered.routes.map((h) => `${h.kind}:${h.path}`));
40
+ console.log('tools:', registered.tools.map((t) => t.name));
41
+
42
+ if (registered.routes.length === 0 || registered.tools.length === 0) {
43
+ console.error('FAIL: apply 未完成注册');
44
+ process.exit(1);
45
+ }
46
+
47
+ // 直呼工具 execute(模拟模型调用,走真实 guard/store 链路)
48
+ const tool = registered.tools[0];
49
+ const list = await tool.execute({ action: 'list_connections' }, { agent: { session: { id: 's1' } } });
50
+ console.log('tool execute list_connections →', list.slice(0, 120));
51
+
52
+ console.log('HOST APPLY OK');
53
+ process.exit(0);