dsh-db-tool 1.8.9 → 1.9.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/CHANGELOG.md CHANGED
@@ -1,5 +1,54 @@
1
1
  # Changelog — dsh-db-tool
2
2
 
3
+ ## 1.9.1
4
+
5
+ 依赖安全与维护性升级(无代码改动,仅 `package-lock.json`)。
6
+
7
+ ### 安全
8
+
9
+ - **`fast-uri` 3.1.8 → 3.1.9**:修复 4 个 URI 解析/规范化漏洞——URI scheme 伪造(百分号编码 scheme 不再被解码为活跃 scheme,CVE-2026-110306)、域名规范化时的 host 混淆(CVE-2026-110180)、相对引用规范化语义错误(CVE-2026-110095)、后缀引用中的 scheme 注入(CVE-2026-110725)。该包经 `ajv` ← `@stryker-mutator/core` 引入,仅用于变异测试,不在运行时代码路径中;项目自身的连接串解析使用原生 `new URL()`,不经过 fast-uri。实测确认修复生效:`%68ttp%3A%2F%2F...` 不再被解析出 scheme。
10
+
11
+ ### 杂项
12
+
13
+ - **`vitest` 4.1.11 → 4.1.12**:mocker 加固手动与重定向 mock。
14
+ - **`vite` 8.3.1 → 8.3.5**:修复 build 阶段的 chunk import map 与 preload 依赖处理。
15
+ - **`@azure/msal-node` 6.0.1 → 7.1.2 / `@azure/identity` 4.13.3 → 4.14.1**:主版本跳跃,新增依赖 `is-in-ssh`、`powershell-utils`(均由 sindresorhus 维护)。该链路经 `tedious` → `@azure/identity` 引入;本项目仅支持 SQL 认证、不支持 Azure AD 认证,故运行时不会走到,升级属跟随上游维护。
16
+
17
+ ## 1.9.0
18
+
19
+ 前端挂载点迁移到 DSH 官方右侧栏,摆脱第三方 `dsh-better-sidebar` 依赖。
20
+
21
+ ### 变更
22
+
23
+ - **改用 DSH 官方右侧栏**:前端挂载点从第三方 `dsh-better-sidebar` 换成 DSH 官方右侧栏 `@deepseek-ai/dsh-client-ui-sidebar-right`,不再调用 `ctx.betterSidebar.registerTab(...)`,改走两段式公开注册路径——
24
+ 1. `ctx.sidebarRightTabs.register({ id, kind, title, guide })` 声明页签类型 `dsh-db-tool`(`guide` 条目 order 45,右侧栏空态页显示「数据库」入口卡片);
25
+ 2. `ctx.slots.inject("sidebar.right.pane.tab", …)` 按类型 `id` 注册面板正文;
26
+ 3. `ctx.slots.inject("sidebar.right.pane.tab.title", …)` 注册页签胶囊标题。
27
+ 正文的会话 `cwd` 改经宿主标准 props `useSessions(s => s.byId[sessionId]?.cwd)` 读取,`visible` 取自 `useTabInfo().tab.visible`(已同时表达「页签激活」与「右侧栏展开 / 非后台会话」,折叠时照旧停轮询)。
28
+ - **获得官方页签能力**:与官方页签同处一个页签系统,可并排、上下拆分、拖出为浮动面板、全屏,布局按会话持久化;这些都由宿主提供,无需本插件维护。
29
+ - **面板数修正为五个**:入口与文档此前按四个面板描述,补上「数据传输」后与实现一致。
30
+
31
+ ### 修复
32
+
33
+ - **补 `keepMounted: true` 声明**:官方右侧栏默认按需卸载未激活页签的正文,而本插件面板状态(连接列表、项目路径、控制台多标签与结果、传输日志)全部存在 React 本地 state 中,不声明就会在切标签 / 切会话 / 收起右栏时全部归零。第三方 `dsh-better-sidebar` 是硬编码 `keepMounted: true` 全量保活的,迁移后该行为不再由宿主默认提供,必须在页签类型声明里显式补上。
34
+ - **删除绑定语言的 React `key`**:原先 `key` 随语言变化,切语言会重建整棵面板组件树,导致未提交的输入丢失。官方右侧栏的做法是文案走 thunk 在渲染时重新取词,不需要靠重挂载来换文案;`key` 因此只保留会话维度。
35
+ - **修 `t()` 读取链**:原先语言快照只在 `apply` 时读一次并闭包捕获,切语言后 `t()` 仍返回旧快照,文案实际不更新(页面看着语言变了、插件面板没变)。改为每次取词现读 locale 快照,语言切换即时生效。
36
+ - **显式声明 `priority: "extension"`**:本插件是页类型、不带 `patterns`,不参与宿主资源路由排序;声明只为让注册意图更可读,不改变行为。
37
+ - **右侧栏服务缺失时打印 warn**:原先服务取不到时静默 `return`,挂载失败无任何痕迹、排查只能靠猜;改为打印 `warn` 指明缺失的服务名。
38
+
39
+ ### 清理
40
+
41
+ - 移除 `devDependencies` 与 `peerDependencies` 中的 `dsh-better-sidebar`,`package-lock.json` 一并移除该包及其独占的传递依赖(重新生成,未升级任何已有依赖)。
42
+ - 删除 Panel 中 `scope.repoRoot` / `scope.workspacePath` 的死兜底——`scope` 实际只有 `{ sessionId, cwd }` 两个字段,这些兜底分支永远不会命中。
43
+ - 清理全部沿用旧名词(side-bar / betterSidebar)的注释。
44
+ - `files` 白名单收录 `docs/sidebar-right-migration.md`(精确路径而非 `docs` 目录:npm 打包不看 `.gitignore`,目录级条目会把本地被忽略的草稿一并发布)。
45
+
46
+ ### 兼容性
47
+
48
+ - `engines.dsh` 下限抬到 `0.1.7-rc.2`:`keepMounted` 字段自官方右侧栏 `0.1.7-rc.1` 起才存在,更早版本(含 `0.1.0-rc.6`、`0.1.5`/`0.1.6` 系)会静默忽略它,表现为切页签/切会话后面板状态归零且无报错,故不再宣称兼容。
49
+ - `apply` 增加能力探测:注册后读回该页签类型,若宿主未接受 `keepMounted` 则打印 warn 提示升级 DSH——收紧 `engines` 挡不住「旧 profile 已装好继续用」的情形。
50
+ - issue 模板的入口名与版本占位符同步为「右侧栏」与 `1.9.0`。
51
+
3
52
  ## 1.8.9
4
53
 
5
54
  发布配置修正:`files` 白名单清理。
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README_EN.md) | 简体中文
4
4
 
5
- 在 DSH 聊天中安全操作数据库:9 种数据库、项目级细粒度授权、危险操作确认、全量审计,配套侧边栏管理台与 SQL 控制台。
5
+ 在 DSH 聊天中安全操作数据库:9 种数据库、项目级细粒度授权、危险操作确认、全量审计,配套右侧栏管理台与 SQL 控制台。
6
6
 
7
7
  ## 功能流程
8
8
 
@@ -67,7 +67,7 @@ DDL、`FLUSHALL`、`dropDatabase` 等语句先返回 `NEEDS_CONFIRMATION`,不
67
67
 
68
68
  ### 全量审计
69
69
 
70
- 每次执行(包括被拒绝的请求)都追加一行到 `audit.jsonl`:语句、危险级别、是否经过确认、结果与耗时。侧边栏可实时查看。
70
+ 每次执行(包括被拒绝的请求)都追加一行到 `audit.jsonl`:语句、危险级别、是否经过确认、结果与耗时。右侧栏可实时查看。
71
71
 
72
72
  ### 数据浏览
73
73
 
@@ -77,7 +77,7 @@ DDL、`FLUSHALL`、`dropDatabase` 等语句先返回 `NEEDS_CONFIRMATION`,不
77
77
 
78
78
  ### SQL 控制台
79
79
 
80
- Navicat 风格的侧边栏多标签编辑器(CodeMirror 6):
80
+ Navicat 风格的右侧栏多标签编辑器(CodeMirror 6):
81
81
 
82
82
  - **多标签**:每标签独立保存 SQL、结果与历史(50 条上限,点击回填)
83
83
  - **语法高亮与补全**:按方言自动切换(MySQL / PostgreSQL / SQLite / PL/SQL / T-SQL),表列元数据注入补全
@@ -107,9 +107,9 @@ Navicat 风格的侧边栏多标签编辑器(CodeMirror 6):
107
107
 
108
108
  ---
109
109
 
110
- ## 侧边栏
110
+ ## 右侧栏
111
111
 
112
- 通过 `dsh-better-sidebar` 注册一个 tab,内含四个面板(中英双语):
112
+ 通过 DSH **官方右侧栏**(`@deepseek-ai/dsh-client-ui-sidebar-right`)注册一个页签,不依赖任何第三方侧边栏。右侧栏展开时,「数据库」随侧边栏空态页(入口页)的卡片出现,可与其他官方页签并排、拆分或浮出为浮窗;页签内含五个面板(中英双语):
113
113
 
114
114
  | 面板 | 用途 |
115
115
  |---|---|
@@ -117,6 +117,14 @@ Navicat 风格的侧边栏多标签编辑器(CodeMirror 6):
117
117
  | 项目授权 | 为当前项目勾选各连接的 ro/rw |
118
118
  | 数据浏览 | 树形浏览库表、搜索、预览数据 |
119
119
  | SQL 控制台 | 多标签编辑器、事务、历史、结果网格 |
120
+ | 数据传输 | 库表级数据迁移,批量/并行与写入模式可选,附传输日志 |
121
+
122
+ ### 使用须知
123
+
124
+ 两条都是官方右侧栏的契约行为,容易误判为插件缺陷:
125
+
126
+ - **未选择会话时,右侧栏不显示数据库面板。** 官方右侧栏的布局状态按会话 id 键控,没有会话就没有停靠面,也就没有入口卡片与页签——先选一个会话再展开右侧栏。
127
+ - **若当前 DSH 就绪的入口卡片只有一个,「+」新建格会直接打开数据库页签而不是引导页。** 这是官方默认页规则(入口恰为一个时直接打开该页;没有入口或多个入口时打开引导页),不是跳过引导页的异常。
120
128
 
121
129
  ---
122
130
 
@@ -182,7 +190,7 @@ DSH 桌面端 0.2.0-rc.2+ 已捆绑 `dsh` 命令:
182
190
  2. 安装插件(npm 安装须钉精确版本,`@latest` 会因 release-age 校验回落):
183
191
 
184
192
  ```bash
185
- dsh plugin --profile desktop add dsh-db-tool@1.8.9 --registry=https://registry.npmjs.org/
193
+ dsh plugin --profile desktop add dsh-db-tool@1.9.1 --registry=https://registry.npmjs.org/
186
194
  ```
187
195
 
188
196
  3. **重启桌面端**生效
@@ -191,6 +199,8 @@ DSH 桌面端 0.2.0-rc.2+ 已捆绑 `dsh` 命令:
191
199
 
192
200
  **运行环境要求**:Node.js ≥ 22。部分数据库驱动(tedious 及其 Azure 依赖)要求 Node 22 起,Node 20 会在建连时报 `Promise.withResolvers is not a function`。
193
201
 
202
+ **前端挂载位置**:数据库面板挂在 DSH 自带的右侧栏上,不需要额外安装任何侧边栏插件;`dsh.client.inject` 已声明 `@deepseek-ai/dsh-client-ui-sidebar-right` 作为加载前置。
203
+
194
204
  ---
195
205
 
196
206
  ## 数据存放
@@ -232,9 +242,9 @@ lib/ 插件主体
232
242
  store/ 连接/授权/审计/密钥存储
233
243
  guard/ 语句风险分级
234
244
  manager.ts 业务编排(授权、分派、确认、审计)
235
- http/ 侧边栏 HTTP 路由
245
+ http/ 右侧栏 HTTP 路由
236
246
  script/ run_script 沙箱与子进程
237
- client/ 侧边栏单文件产物
247
+ client/ 右侧栏单文件产物
238
248
  skills/ db-admin skill
239
249
  scripts/ 构建与工具脚本
240
250
  patches/ DSH 宿主热修复补丁
package/README_EN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [简体中文](README.md) | English
4
4
 
5
- Operate databases safely from DSH chat: 9 database engines, project-scoped grants, dangerous-operation confirmation, full audit logging, plus a sidebar console and SQL editor.
5
+ Operate databases safely from DSH chat: 9 database engines, project-scoped grants, dangerous-operation confirmation, full audit logging, plus a right-sidebar console and SQL editor.
6
6
 
7
7
  ## Feature flow
8
8
 
@@ -67,7 +67,7 @@ Confirmation comes through chat for model calls and through a dialog in the SQL
67
67
 
68
68
  ### Full audit logging
69
69
 
70
- Every execution — including rejected requests — appends one line to `audit.jsonl`: statement, danger level, whether it was confirmed, result and duration. The sidebar tails it live.
70
+ Every execution — including rejected requests — appends one line to `audit.jsonl`: statement, danger level, whether it was confirmed, result and duration. The right sidebar tails it live.
71
71
 
72
72
  ### Data browsing
73
73
 
@@ -77,7 +77,7 @@ Every execution — including rejected requests — appends one line to `audit.j
77
77
 
78
78
  ### SQL console
79
79
 
80
- A Navicat-style multi-tab editor in the sidebar (CodeMirror 6):
80
+ A Navicat-style multi-tab editor in the right sidebar (CodeMirror 6):
81
81
 
82
82
  - **Multiple tabs**: each keeps its own SQL, results and history (50 entries, click to refill)
83
83
  - **Highlighting and completion**: dialect switches automatically (MySQL / PostgreSQL / SQLite / PL/SQL / T-SQL), with table/column metadata injected into completion
@@ -107,9 +107,9 @@ Export all connections (including passwords) to a passphrase-encrypted file for
107
107
 
108
108
  ---
109
109
 
110
- ## Sidebar
110
+ ## Right sidebar
111
111
 
112
- One tab registered through `dsh-better-sidebar`, containing four panels (bilingual):
112
+ One tab registered through the official DSH right sidebar (`@deepseek-ai/dsh-client-ui-sidebar-right`) — no third-party sidebar required. When the panel is expanded, **Databases** appears as a card on the sidebar's guide (empty-state) page and can sit next to, split with, or float out as a floating window alongside any other official tab. The tab holds five panels (bilingual):
113
113
 
114
114
  | Panel | Purpose |
115
115
  |---|---|
@@ -117,6 +117,14 @@ One tab registered through `dsh-better-sidebar`, containing four panels (bilingu
117
117
  | Grants | Tick ro/rw per connection for the current project |
118
118
  | Browse | Tree navigation, search and data preview |
119
119
  | SQL Console | Multi-tab editor, transactions, history, result grid |
120
+ | Transfer | Table-level data migration with batch/parallelism and write modes, plus a transfer log |
121
+
122
+ ### Notes
123
+
124
+ Both are contract behaviours of the official right sidebar and are easy to mistake for plugin defects:
125
+
126
+ - **Without a selected session the right sidebar shows no database panel.** The official right sidebar keys its layout state by session id, so with no session there is no docking surface — hence no guide card and no tab. Select a session first, then expand the right sidebar.
127
+ - **If the only ready entry card is the database one, the "+" new-pane button opens the database tab directly instead of the guide page.** This is the official default-page rule (exactly one entry opens that page; zero or several entries open the guide), not a failure to show the guide.
120
128
 
121
129
  ---
122
130
 
@@ -182,7 +190,7 @@ DSH desktop 0.2.0-rc.2+ bundles the `dsh` command:
182
190
  2. Install the plugin (npm installs must pin an exact version — `@latest` falls back to an older release due to release-age checks):
183
191
 
184
192
  ```bash
185
- dsh plugin --profile desktop add dsh-db-tool@1.8.9 --registry=https://registry.npmjs.org/
193
+ dsh plugin --profile desktop add dsh-db-tool@1.9.1 --registry=https://registry.npmjs.org/
186
194
  ```
187
195
 
188
196
  3. **Restart the desktop app** to load it
@@ -191,6 +199,8 @@ DSH desktop 0.2.0-rc.2+ bundles the `dsh` command:
191
199
 
192
200
  **Runtime requirement**: Node.js ≥ 22. Some database drivers (tedious and its Azure dependencies) require Node 22+; on Node 20 connections fail with `Promise.withResolvers is not a function`.
193
201
 
202
+ **Where the UI is mounted**: the database panel docks into DSH's built-in right sidebar — no extra sidebar plugin needs installing. `dsh.client.inject` already declares `@deepseek-ai/dsh-client-ui-sidebar-right` as a load prerequisite.
203
+
194
204
  ---
195
205
 
196
206
  ## Data layout
@@ -232,9 +242,9 @@ lib/ plugin core
232
242
  store/ connection / grant / audit / secret storage
233
243
  guard/ statement risk classification
234
244
  manager.ts business orchestration (auth, dispatch, confirm, audit)
235
- http/ sidebar HTTP routes
245
+ http/ right-sidebar HTTP routes
236
246
  script/ run_script sandbox and child process
237
- client/ sidebar single-file bundle
247
+ client/ right-sidebar single-file bundle
238
248
  skills/ db-admin skill
239
249
  scripts/ build and tooling scripts
240
250
  patches/ DSH host hotfix patches
package/client/README.md CHANGED
@@ -1,47 +1,83 @@
1
- # dsh-db-tool client(侧边栏前端)
1
+ # dsh-db-tool client(右侧栏前端)
2
2
 
3
3
  ## 形态:无构建、单文件产物
4
4
 
5
- 本目录**不需要任何构建步骤**。`client.js` 即最终发布产物,与 dsh-ssh-tunnel 的发布形态一致:一个 `window.__ModuleLoader__.load({ id, factory })` 包裹的单文件 IIFE 模块,`factory(require)` 内 `require("react")` 取 React,运行于 DSH 侧边栏底座 dsh-better-sidebar(>=0.12)。
5
+ 本目录**不需要任何构建步骤**。`client.js` 即最终发布产物,与 dsh-ssh-tunnel 的发布形态一致:一个 `window.__ModuleLoader__.load({ id, factory })` 包裹的单文件 IIFE 模块,`factory(require)` 内 `require("react")` 取 React,运行于 DSH **官方右侧栏**(`@deepseek-ai/dsh-client-ui-sidebar-right`)。
6
6
 
7
- 这样做的好处:无打包器、无依赖漂移、产物可直接被插件安装流程原样打包。
7
+ 不存在第三方侧边栏依赖:页签走官方的两段式公开注册路径,与官方自带页签类型完全同一套接口。
8
+
9
+ 这样做的好处:无打包器、无依赖漂移、产物可直接被插件安装流程原样打包,且随官方右侧栏升级自动适配——拆分窗格、浮动面板、全屏、布局持久化都由宿主提供,本插件不必自维护。
10
+
11
+ ## 接入官方右侧栏
12
+
13
+ `exports.inject = ["slots", "sidebarRightTabs", "locale"]`,`apply(ctx)` 内三段注册:
14
+
15
+ | 阶段 | 调用 | 作用 |
16
+ |---|---|---|
17
+ | 1. 页签类型 | `ctx.sidebarRightTabs.register({ id, kind, title, guide, priority, keepMounted })` | 声明 `id = kind = "dsh-db-tool"` 的页签类型;`guide` 条目(order 45)让右侧栏空态页显示「数据库」入口卡片,点击即打开本页签;`priority: "extension"` 显式写明第三方档位;`keepMounted: true` 见下节 |
18
+ | 2. 页签正文 | `ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register({ name, key: id }, DbToolBody))` | 按类型 `id` 挂载面板正文 |
19
+ | 3. 页签标题 | `ctx.slots.inject("sidebar.right.pane.tab.title", () => ctx.slots.register({ name, key: id }, DbToolTitle))` | 页签胶囊上的图标 + 标题,随语言切换重渲染 |
20
+
21
+ 两个必须记住的易错点:
22
+
23
+ - **席位 `key` 必须是类型的 `id`,不是 `kind`。** 宿主按 `definition.id` 派发席位;官方允许 extension 接管 builtin 的 `kind`,若按 `kind` 挂席位,同 kind 的两份实现会互相串位。本插件 `id` 与 `kind` 取值恰好相同,但这是两条独立的身份,改 `kind` 时不能顺手改席位 `key`。
24
+ - **每一处注册都要包在 `ctx.effect(...)` 里。** 类型注册与两个席位注册各自一个 effect,插件卸载时宿主据此自动注销;漏掉 effect 的注册会残留到下次加载,表现为同 id 二次注册报错。席位侧还必须用 `ctx.slots.inject` 包一层,让注册表按声明时序等待。
25
+
26
+ ## 正文拿到的宿主标准 props
27
+
28
+ 正文组件由宿主渲染,携带框架标准 props:
29
+
30
+ - `sessionId` —— 当前会话 id;
31
+ - `useTabInfo()` —— 返回 `{ sidebar, panel, tab }`,其中 `tab.visible` 已同时表达「页签激活」「右侧栏展开」「非后台会话」三重语义,直接当可见性用即可,业务侧不必再自判前后台;折叠或切到后台会话时面板应停止轮询与请求;
32
+ - `useSessions(selector)` —— 会话 store 的订阅钩子,会话工作目录取自 `useSessions(s => s.byId[sessionId]?.cwd)`,与官方各页签类型同一来源。
33
+
34
+ ## 为什么声明 keepMounted
35
+
36
+ 官方右侧栏**默认按需卸载正文**(`keepMounted` 缺省 `false`):切页签、切会话、收起右栏都会把正文卸载。而本插件的面板状态几乎全在 React 本地 state ——面板分段、项目路径、连接与授权列表、树刷新信号、SQL 控制台的多标签与结果、传输视图的选择项 —— 不声明 `keepMounted` 就会在上面的任一动作后全部归零。
37
+
38
+ 因此类型声明里写 `keepMounted: true`:已访问过的正文在切页签、切会话、收起与停靠切换期间保留。注意它**不会提前挂载**正文(首次可见才挂载),也不保「插件卸载 / 页面刷新 / 关闭页签」之后的实例——那属于持久化范畴,本插件不做。
8
39
 
9
40
  ## 文件
10
41
 
11
- - `client.js` — 全部前端逻辑(单文件,约 1400 行),内含:
12
- - 4 个面板:管理(连接 CRUD + 测试 + 审计查看)、授权(项目路径 → 连接 ro/rw/撤销)、浏览(树选库/表 → 点表打开 Navicat 式弹窗:左树 + 结构/数据 Tabs + 分页;portal 挂 body,Esc/遮罩/✕ 关闭)、SQL 控制台(连接/库选择、query/execute/script、危险确认对话框 → challengeId 重发)
13
- - zh/en 双语词典(zh 默认),经 `ctx.locale.register(NS, ...)` 注册,`useSyncExternalStore` 订阅语言切换
42
+ - `client.js` — 全部前端逻辑(单文件,约 3900 行),内含:
43
+ - 5 个面板视图:管理(连接 CRUD + 测试 + 审计查看)、授权(项目路径 → 连接 ro/rw/撤销)、浏览(树选库/表 → 点表打开 Navicat 式弹窗:左树 + 结构/数据 Tabs + 分页;portal 挂 body,Esc/遮罩/✕ 关闭)、SQL 控制台(连接/库选择、query/execute/script、危险确认对话框 → challengeId 重发)、传输(库表级数据迁移、写入模式、进度与传输日志)
44
+ - zh/en 双语词典(zh 默认),经 `ctx.locale.register(NS, ...)` 注册,`useSyncExternalStore` 订阅语言切换;文案一律走 thunk(`title()` / `description()` 每次调用现读),语言变化不需要重注册
14
45
  - 危险确认:`runGuarded()` 先发请求,收到 `code:"NEEDS_CONFIRMATION"` 且有 `challengeId` 时弹 `DangerDialog`,用户确认后携 challengeId 重发(challenge 一次性、绑语句、5 分钟有效)
15
46
  - REST API 调用走同源前缀 **`/dsh-db-tool/api`**(须与 host 端 `webServer.register` 的 prefix 一致)
16
47
 
17
48
  ## 验证(本地跑通的方式)
18
49
 
19
- 无构建器,验证即两项检查:
50
+ 无构建器,验证分三项:
20
51
 
21
52
  ```powershell
22
53
  node --check client/client.js # 语法检查
54
+ npx vitest run tests/client # 前端单测
23
55
  ```
24
56
 
25
- 再加一个最小冒烟:stub `window.__ModuleLoader__` / `require("react")`,eval 加载文件,调 `factory` 得到 `exports`,断言 `exports.inject` 含 `"betterSidebar"`、`exports.apply(ctx)` 能注册 `id:"dsh-db-tool"` 的 tab、根组件可创建(见本文件历史提交中的 smoke 脚本写法;依赖仅 node 内置 fs)。
57
+ 再加一条注册契约冒烟(`tests/client/sidebar-registration.spec.ts`):stub `window.__ModuleLoader__` / `require("react")` 与最小 `document`,eval 加载文件,调 `factory` 得到 `exports`,断言 `exports.inject` 含 `"sidebarRightTabs"` 与 `"slots"`、`exports.apply(ctx)` 能注册 `kind:"dsh-db-tool"` 的页签类型并在两个席位各注册一个 `key:"dsh-db-tool"` 的组件(`key` 与类型 `id` 必须相等),并断言卸载后注册项全部撤销。桩 ctx 只需记录 `locale` / `effect` / `sidebarRightTabs` / `slots` 四项服务,依赖仅 node 内置模块。
26
58
 
27
59
  ## 打包接入(发布形态,需根 package.json 配合)
28
60
 
29
- npm 包需暴露 client 供 DSH 注入(此字段属根 package.json,超出 client scope,需 lib/lead 侧补上):
61
+ npm 包需暴露 client 供 DSH 注入,并声明对官方右侧栏客户端的依赖(`dsh.client.inject` 里的包名会先于本插件加载):
30
62
 
31
63
  ```jsonc
32
64
  {
33
65
  "exports": { "./client": "./client/client.js" },
34
66
  "dsh": {
35
67
  "bundle": { "patch": "./cordis.patch.yml" },
36
- "client": { "inject": ["@deepseek-ai/dsh-client-locale"], "platform": "web" }
68
+ "client": {
69
+ "inject": ["@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-ui-sidebar-right"],
70
+ "platform": "web"
71
+ }
37
72
  },
38
73
  "peerDependencies": {
39
- "@deepseek-ai/cordis": "^4.0.1",
40
- "dsh-better-sidebar": ">=0.12.0"
74
+ "@deepseek-ai/cordis": "^4.0.1"
41
75
  }
42
76
  }
43
77
  ```
44
78
 
79
+ `dsh.client.inject` 负责「谁先加载」,`peerDependencies` 只声明框架本体——右侧栏包属于宿主提供的运行时,不放 peerDependencies。
80
+
45
81
  ## 本地开发
46
82
 
47
83
  仓库根目录执行(Windows):
@@ -50,4 +86,4 @@ npm 包需暴露 client 供 DSH 注入(此字段属根 package.json,超出 c
50
86
  pnpm dsh web --patch ./cordis.patch.yml
51
87
  ```
52
88
 
53
- 侧边栏出现「数据库」tab(order 45)即加载成功。projectPath 取自 `scope.cwd || scope.workspacePath`,也可在面板顶部手动输入后点「加载」。
89
+ 右侧栏空态页出现「数据库」入口卡片(order 45),点开后即为面板,即加载成功;若右侧栏已有页签,用页签条上的「+」打开入口页再点「数据库」。projectPath 取自当前会话的 `cwd`,也可在面板顶部手动输入后点「加载」。
package/client/client.js CHANGED
@@ -7,18 +7,21 @@ window.__ModuleLoader__.load({
7
7
  const React = require("react");
8
8
 
9
9
  /* ============================================================
10
- * dsh-db-tool 侧边栏客户端
10
+ * dsh-db-tool 右侧栏客户端
11
11
  * 形态与 dsh-ssh-tunnel 一致:单文件 __ModuleLoader__ 模块,
12
12
  * exports.inject / exports.apply(ctx) 由 DSH web 宿主加载,
13
- * 经 dsh-better-sidebar registerTab 注册侧边栏 tab。
13
+ * 经 DSH 官方右侧栏(@deepseek-ai/dsh-client-ui-sidebar-right)
14
+ * 的 sidebarRightTabs + slots 两段式注册页签,不依赖第三方侧边栏。
14
15
  * ============================================================ */
15
16
 
16
- // --- i18n(dsh-better-sidebar 同款模式)---
17
+ // --- i18n(ctx.locale 注册 + useSyncExternalStore 订阅语言切换)---
17
18
  // i18n 字典/HTTP 封装/CSS/对话框均无单元断言,仅变异下方纯函数层(Stryker 区段指令需精确匹配,不得带后缀文本)
18
19
  // Stryker disable all
19
20
  const LOCALE_NS = "dbTool";
20
21
  const zh = {
21
22
  tabTitle: "数据库",
23
+ tabDesc: "多库连接管理、项目授权、数据浏览与 SQL 控制台",
24
+ tabUnavailable: "页签尚未就绪,请切换页签后重试",
22
25
  projectLabel: "项目: {path}",
23
26
  projectUnbound: "未识别当前项目路径,请在下方填写工作区绝对路径",
24
27
  projectPathPlaceholder: "工作区绝对路径(如 E:\\Code\\my-app)",
@@ -249,6 +252,8 @@ window.__ModuleLoader__.load({
249
252
  };
250
253
  const en = {
251
254
  tabTitle: "Databases",
255
+ tabDesc: "Connection management, project grants, data browse and a SQL console",
256
+ tabUnavailable: "This tab is not ready yet; switch tabs and try again",
252
257
  projectLabel: "Project: {path}",
253
258
  projectUnbound: "Cannot detect current project path; enter the workspace absolute path below",
254
259
  projectPathPlaceholder: "workspace absolute path (e.g. /home/me/my-app)",
@@ -473,13 +478,29 @@ window.__ModuleLoader__.load({
473
478
  };
474
479
 
475
480
  let activeLocale = "zh";
481
+ // 持有 locale 服务引用:切语言时不会重新 apply,故 t() 必须每次现读快照,
482
+ // activeLocale 仅作取不到服务时的兜底(apply 期写入一次即过期)。
483
+ let localeService = null;
476
484
  function attachLocale(locale) {
477
485
  try {
478
- activeLocale = (locale && locale.getSnapshot && locale.getSnapshot().active) || "zh";
486
+ if (locale && typeof locale.getSnapshot === "function") localeService = locale;
487
+ // 取不到有效语言时保留原值:否则重新 apply 会把已切好的语言打回默认
488
+ const active = locale && locale.getSnapshot && locale.getSnapshot().active;
489
+ if (active) activeLocale = active;
479
490
  } catch (e) { /* 保持默认 */ }
480
491
  }
492
+ // 当前语言:优先读 locale 服务实时快照,读不到才回落 apply 期缓存值
493
+ function currentLocale() {
494
+ try {
495
+ if (localeService) {
496
+ const snapshot = localeService.getSnapshot();
497
+ if (snapshot && snapshot.active) return snapshot.active;
498
+ }
499
+ } catch (e) { /* 回落缓存值 */ }
500
+ return activeLocale;
501
+ }
481
502
  function t(key, params) {
482
- let text = (activeLocale === "en" ? en[key] : undefined) || zh[key] || en[key] || key;
503
+ let text = (currentLocale() === "en" ? en[key] : undefined) || zh[key] || en[key] || key;
483
504
  if (params !== undefined && params !== null) {
484
505
  for (const [name, value] of Object.entries(params)) {
485
506
  text = String(text).split("{" + name + "}").join(String(value));
@@ -553,6 +574,12 @@ window.__ModuleLoader__.load({
553
574
  );
554
575
  }
555
576
 
577
+ // 官方右侧栏 guide 入口的 icon 是 React 组件(宿主按 <Icon size className /> 渲染),
578
+ // 这里把既有 icon(size) 适配成组件形态,保持同一枚数据库图标。
579
+ function DbIcon(props) {
580
+ return icon(props && props.size);
581
+ }
582
+
556
583
  let styleEl = null;
557
584
  function ensureStyles() {
558
585
  if (styleEl) return;
@@ -2671,6 +2698,13 @@ window.__ModuleLoader__.load({
2671
2698
  loadDbs();
2672
2699
  }, [connId, projectPath]);
2673
2700
 
2701
+ // 换项目时作废事务:事务按创建时的 projectKey 授权与审计,
2702
+ // 残留的 sessionToken 会让后续语句写进旧项目的库、审计记旧项目,
2703
+ // 而顶栏显示的是新项目路径。不主动回滚,交服务端 TTL 清扫。
2704
+ React.useEffect(() => {
2705
+ setTxToken(null);
2706
+ }, [projectPath]);
2707
+
2674
2708
  // vendor bundle 异步加载(失败降级 textarea,静默)
2675
2709
  React.useEffect(() => {
2676
2710
  let dead = false;
@@ -3622,10 +3656,11 @@ window.__ModuleLoader__.load({
3622
3656
  const visible = props.visible;
3623
3657
  const scope = props.scope || {};
3624
3658
  const [view, setView] = React.useState("manage");
3625
- // 项目路径初始值:scope.cwd 优先;cwd 是工作区容器时用选中的 git 仓库 repoRoot 兜底
3626
- //(SessionScope 契约字段:{ sessionId, cwd?, repoRoot? };workspacePath 为历史遗留兜底)
3627
- const [projectPath, setProjectPath] = React.useState(scope.cwd || scope.repoRoot || scope.workspacePath || "");
3628
- const [projectEdited, setProjectEdited] = React.useState(!(scope.cwd || scope.repoRoot || scope.workspacePath));
3659
+ // 项目路径初始值取 scope.cwd;scope 由本文件构造为 { sessionId, cwd },cwd 缺失时留空等下方异步解析或手填
3660
+ const [projectPath, setProjectPath] = React.useState(scope.cwd || "");
3661
+ // 是否「用户手填过」:只由输入框 onChange 置位。首帧 cwd 取不到时不能预设为已编辑,
3662
+ // 否则下方 project-context 解析被永久跳过,用户卡在空路径输入框且连接全显示未授权。
3663
+ const [projectEdited, setProjectEdited] = React.useState(false);
3629
3664
  const [conns, setConns] = React.useState([]);
3630
3665
  const [grants, setGrants] = React.useState([]);
3631
3666
  const [busy, setBusy] = React.useState("");
@@ -3646,7 +3681,7 @@ window.__ModuleLoader__.load({
3646
3681
  // host 端以会话 header.cwd 为准归一化出 projectPathKey,前端不自拼 key。
3647
3682
  // scope.cwd/sessionId 变化(切换会话)时自动重新解析;用户手填(projectEdited)时不覆盖。
3648
3683
  const sessionKey = scope.sessionId || "";
3649
- const scopeCwd = scope.cwd || scope.repoRoot || scope.workspacePath || "";
3684
+ const scopeCwd = scope.cwd || "";
3650
3685
  const resolvedRef = React.useRef("");
3651
3686
  React.useEffect(() => {
3652
3687
  if (!visible || projectEdited) return;
@@ -3689,7 +3724,9 @@ window.__ModuleLoader__.load({
3689
3724
  manage: React.createElement(ManageView, shared),
3690
3725
  grants: React.createElement(GrantsView, shared),
3691
3726
  console: React.createElement(ConsoleView, shared),
3692
- transfer: React.createElement(TransferView, Object.assign({}, shared, { visible })),
3727
+ // visible 含本视图是否被选中:keepMounted 让正文在后台存活,
3728
+ // 只看顶层 visible 会让 transfer/live 在其他视图下继续 2s 轮询。
3729
+ transfer: React.createElement(TransferView, Object.assign({}, shared, { visible: visible && view === "transfer" })),
3693
3730
  };
3694
3731
  return React.createElement(
3695
3732
  "div",
@@ -3711,7 +3748,7 @@ window.__ModuleLoader__.load({
3711
3748
  // 填充式输入:裸 input 命中样式层 .dbt-row input 契约(去掉旧 dbt-card + 内联 padding hack)
3712
3749
  React.createElement("input", {
3713
3750
  placeholder: t("projectPathPlaceholder"), value: projectPath,
3714
- onChange: (e) => setProjectPath(e.target.value),
3751
+ onChange: (e) => { setProjectPath(e.target.value); setProjectEdited(true); },
3715
3752
  // 失焦时:输入与权威解析一致才收回自动跟随;用户填了自己的路径则保持手填态(不被权威值打回)
3716
3753
  onBlur: (e) => {
3717
3754
  const v = (e.target.value || "").trim();
@@ -3764,28 +3801,32 @@ window.__ModuleLoader__.load({
3764
3801
 
3765
3802
  function DbToolRoot(props) {
3766
3803
  const ctx = props.ctx;
3767
- // DSH locale 切换时重渲染(better-sidebar 同款模式)
3768
- const localeKey = React.useSyncExternalStore(
3804
+ // 订阅宿主 locale 服务只为在语言切换时触发重渲染;文案由 t() 每次现读快照,
3805
+ // 因此不再把语言拼进 key —— 那会重建整棵面板,丢掉所有本地状态。
3806
+ React.useSyncExternalStore(
3769
3807
  React.useCallback(function (cb) {
3770
3808
  if (!ctx.locale || typeof ctx.locale.subscribe !== "function") return function () {};
3771
3809
  return ctx.locale.subscribe(cb);
3772
3810
  }, [ctx]),
3773
3811
  React.useCallback(function () {
3774
3812
  try {
3775
- return ctx.locale && ctx.locale.getSnapshot ? ctx.locale.getSnapshot().active : activeLocale;
3776
- } catch (e) { return activeLocale; }
3813
+ return ctx.locale && ctx.locale.getSnapshot ? ctx.locale.getSnapshot().active : currentLocale();
3814
+ } catch (e) { return currentLocale(); }
3777
3815
  }, [ctx]),
3778
3816
  function () { return "zh"; },
3779
3817
  );
3780
3818
  return React.createElement(Panel, {
3781
- key: "db-tool-" + String(localeKey || "zh"),
3782
3819
  visible: props.visible,
3783
3820
  scope: props.scope,
3784
3821
  ctx: ctx, // 宿主 ctx 下传,Panel 透传给 BrowsePane
3785
3822
  });
3786
3823
  }
3787
3824
 
3788
- const inject = ["betterSidebar", "locale"];
3825
+ // 依赖官方右侧栏(@deepseek-ai/dsh-client-ui-sidebar-right)的两个服务:
3826
+ // sidebarRightTabs = 页签类型注册表,slots = 席位居中器(sidebar.right.pane.tab / .title)。
3827
+ // locale 为 DSH 客户端通用服务(dsh.client.inject 里已声明 @deepseek-ai/dsh-client-locale)。
3828
+ const inject = ["slots", "sidebarRightTabs", "locale"];
3829
+
3789
3830
  function apply(ctx) {
3790
3831
  ensureStyles();
3791
3832
  if (ctx.locale) {
@@ -3799,23 +3840,103 @@ window.__ModuleLoader__.load({
3799
3840
  };
3800
3841
  }, "dsh-db-tool: dictionaries");
3801
3842
  }
3802
- if (!ctx.betterSidebar) return;
3843
+ // 官方右侧栏两段式注册:先声明页签类型,再在席位里按类型 id 挂正文与标题。
3844
+ // 类型 id 同时是席位 key(宿主用 definition.id 作 entryKey),故正文/标题与 id 一致。
3845
+ if (!ctx.sidebarRightTabs || !ctx.slots) {
3846
+ console.warn("[dsh-db-tool] 宿主未提供 sidebarRightTabs / slots 服务,右侧栏面板不会挂载");
3847
+ return;
3848
+ }
3849
+
3850
+ // 页签正文:宿主注入标准 props { sessionId, useTabInfo, useSessions? }。
3851
+ // useTabInfo().tab.visible 同时表达「页签激活」与「右侧栏展开/非后台会话」,可直接当 visible 用。
3852
+ function DbToolBody(props) {
3853
+ // useTabInfo 在页签记录未提交时会 throw。异常一旦冒到宿主 SlotErrorBoundary,
3854
+ // 该席位条目会被 abdicate 永久除名(宿主 entriesOfSlot 过滤后无 delete 路径),
3855
+ // 即数据库页签在本进程内彻底消失且无法自愈 —— 故就地降级,不让它冒出去。
3856
+ let info;
3857
+ try {
3858
+ info = props.useTabInfo();
3859
+ } catch (e) {
3860
+ return React.createElement("div", { className: "dbt-panel" },
3861
+ React.createElement("div", { className: "dbt-muted" }, t("tabUnavailable")));
3862
+ }
3863
+ const cwd = typeof props.useSessions === "function"
3864
+ ? props.useSessions(function (snapshot) {
3865
+ const row = snapshot && snapshot.byId ? snapshot.byId[props.sessionId] : undefined;
3866
+ return row ? row.cwd : undefined;
3867
+ })
3868
+ : undefined;
3869
+ return React.createElement(DbToolRoot, {
3870
+ ctx: ctx,
3871
+ visible: info.tab.visible,
3872
+ scope: { sessionId: props.sessionId, cwd: cwd },
3873
+ });
3874
+ }
3875
+
3876
+ // 页签标题(chip):数据库图标 + 当前语言标题,随语言切换重渲染。
3877
+ // 文案走 thunk(每次渲染现读 t()),不把语言拼进 key,避免切语言重建节点。
3878
+ function DbToolTitle() {
3879
+ React.useSyncExternalStore(
3880
+ React.useCallback(function (cb) {
3881
+ if (!ctx.locale || typeof ctx.locale.subscribe !== "function") return function () {};
3882
+ return ctx.locale.subscribe(cb);
3883
+ }, []),
3884
+ React.useCallback(function () {
3885
+ try {
3886
+ return ctx.locale && ctx.locale.getSnapshot ? ctx.locale.getSnapshot().active : currentLocale();
3887
+ } catch (e) { return currentLocale(); }
3888
+ }, []),
3889
+ function () { return "zh"; },
3890
+ );
3891
+ return React.createElement("span", {
3892
+ style: { display: "inline-flex", alignItems: "center", gap: 6 },
3893
+ }, icon(16), t("tabTitle"));
3894
+ }
3895
+
3803
3896
  ctx.effect(function () {
3804
- return ctx.betterSidebar.registerTab({
3897
+ const disposer = ctx.sidebarRightTabs.register({
3805
3898
  id: TAB_ID,
3899
+ kind: TAB_ID,
3900
+ // 页签正文在切换页签 / 切会话 / 收起右侧栏时保持挂载:
3901
+ // Panel 的 view/projectPath/conns/grants 与 ConsoleView、TransferView 的状态全在 React 本地,
3902
+ // 默认的 keepMounted=false 会卸载正文,切回来就全丢。
3903
+ keepMounted: true,
3904
+ // 页签类型优先级:本插件无 patterns(不认领资源地址、是页类型),
3905
+ // priority 不参与路由排序,省略即 extension;显式声明只为可读性。
3906
+ priority: "extension",
3806
3907
  title: function () { return t("tabTitle"); },
3807
- icon,
3808
- order: 45,
3809
- single: true,
3810
- component: function (p) {
3811
- return React.createElement(DbToolRoot, {
3812
- ctx: ctx,
3813
- visible: p.visible,
3814
- scope: p.scope,
3815
- });
3816
- },
3908
+ // guide 条目:右侧栏空态(无页签)时显示成入口卡片,点一下即打开本页签;
3909
+ // order 45 沿用旧工具栏的相对位置。
3910
+ guide: [{
3911
+ id: TAB_ID,
3912
+ order: 45,
3913
+ title: function () { return t("tabTitle"); },
3914
+ description: function () { return t("tabDesc"); },
3915
+ icon: DbIcon,
3916
+ }],
3917
+ });
3918
+ // 能力探测:keepMounted 自官方右侧栏 0.1.7-rc.1 起才有,旧版本会静默忽略它,
3919
+ // 表现为切页签/切会话后面板状态全部归零且无任何报错。读回注册结果比收紧 engines 更能覆盖实情。
3920
+ try {
3921
+ const registered = ctx.sidebarRightTabs.get && ctx.sidebarRightTabs.get(TAB_ID);
3922
+ if (registered && registered.keepMounted !== true) {
3923
+ console.warn("[dsh-db-tool] 当前 DSH 版本不支持 keepMounted,切换页签/会话后面板状态会丢失,请升级 DSH");
3924
+ }
3925
+ } catch (e) { /* 探测失败不影响主流程 */ }
3926
+ return disposer;
3927
+ }, "dsh-db-tool: register tab type");
3928
+
3929
+ ctx.effect(function () {
3930
+ return ctx.slots.inject("sidebar.right.pane.tab", function () {
3931
+ return ctx.slots.register({ name: "sidebar.right.pane.tab", key: TAB_ID }, DbToolBody);
3932
+ });
3933
+ }, "dsh-db-tool: register tab body");
3934
+
3935
+ ctx.effect(function () {
3936
+ return ctx.slots.inject("sidebar.right.pane.tab.title", function () {
3937
+ return ctx.slots.register({ name: "sidebar.right.pane.tab.title", key: TAB_ID }, DbToolTitle);
3817
3938
  });
3818
- }, "dsh-db-tool: register tab");
3939
+ }, "dsh-db-tool: register tab title");
3819
3940
  }
3820
3941
  exports.apply = apply;
3821
3942
  exports.inject = inject;