dsh-db-tool 1.8.4 → 1.8.8

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_EN.md CHANGED
@@ -2,65 +2,166 @@
2
2
 
3
3
  [简体中文](README.md) | English
4
4
 
5
- A DSH community plugin for operating databases safely from chat, with a sidebar management console and the `db-admin` skill. Architecture and interaction patterns follow [dsh-ssh-tunnel](https://github.com/thirsty5034/dsh-ssh-tunnel).
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.
6
6
 
7
- ## Features
7
+ ## Feature flow
8
8
 
9
- - **9 databases**: MySQL, PostgreSQL, GaussDB, SQLite, Redis, MongoDB, Oracle, DM (Dameng), and SQL Server
10
- - **One tool, six actions**: `DatabaseManager` exposes `list_connections / query / execute / schema / preview / run_script` (`run_script` executes in a `node:vm` sandbox with a 60s timeout and only restricted `db.{query,execute}` handles)
11
- - **Tiered permissions**: per-connection read-only (ro) / read-write (rw) plus project-scoped grants (`grants.json`: projectPathKey → connection → mode); unauthorized projects are always rejected
12
- - **Dangerous-operation confirmation**: DDL / FLUSHALL / dropDatabase and the like first return `NEEDS_CONFIRMATION`; after confirmation in chat (model via ask) or in the SQL console (dialog), the retry carries a one-time `challengeId` (bound to the statement SHA256, 5-minute TTL)
13
- - **Auditing**: every execution is appended to `audit.jsonl` (statement, danger level, confirmed or not, outcome)
14
- - **Sidebar with 4 panels** (dsh-better-sidebar, zh/en): connections, project grants, data browser, SQL console
15
- - **db-admin skill**: shipped with the plugin, covering dialect cheat sheets for all 9 databases, safety rules, and the confirmation flow
9
+ The complete path of one database operation, from entry point to audit trail — both entry points share the same semantics, and every call must pass through two gates: project-level grants and statement-level risk classification.
16
10
 
17
- ### SQL Console
11
+ ![dsh-db-tool feature flow](assets/feature-flow.png)
18
12
 
19
- The sidebar SQL console panel has been upgraded to a Navicat-style multi-tab editor (CodeMirror 6):
13
+ [**Open the interactive version**](assets/feature-flow.html) — zoom, pan, path highlighting, and click any node to see its source evidence.
20
14
 
21
- - **Multi-tab**: create/close/switch query tabs; each tab keeps its own SQL, result, and history (capped at 50 entries, expandable and click-to-refill)
22
- - **Syntax highlighting & completion**: switches with the dialect — mysql→MySQL, postgresql/gaussdb→PostgreSQL, sqlite→SQLite, oracle/dmdb→PL/SQL, sqlserver→T-SQL (the MSSQL dialect from `@codemirror/lang-sql`); Redis/Mongo get keyword/method completion over plain text; table/column metadata is fetched to feed completion (up to 50 tables). If the editor bundle (`vendor/codemirror-sql.cjs`, built by `npm run build:codemirror`, chained after `npm run build`) fails to load, it silently falls back to a textarea
15
+ > Questions or suggestions? [Open an issue](https://github.com/mengqi1436/dsh-db-tool/issues/new/choose) (bug report and feature request templates provided).
16
+
17
+ ---
18
+
19
+ ## Supported databases
20
+
21
+ | Category | Databases |
22
+ |---|---|
23
+ | Relational | MySQL, PostgreSQL, GaussDB, SQLite, Oracle, Dameng DM, SQL Server |
24
+ | NoSQL | Redis, MongoDB |
25
+
26
+ Every engine supports connection, metadata browsing, query, preview and data transfer. Dialect differences are handled inside the adapters — callers only ever see `?` placeholders and a uniform result set.
27
+
28
+ ---
29
+
30
+ ## Core capabilities
31
+
32
+ ### Model tool `DatabaseManager`
33
+
34
+ One tool, multiple actions — fewer ambiguities for the model to resolve:
35
+
36
+ | action | Purpose |
37
+ |---|---|
38
+ | `list_connections` | List available connections (credentials never included) |
39
+ | `query` | Run a read-only query, return columns and rows |
40
+ | `execute` | Run a write statement; dangerous operations require confirmation |
41
+ | `schema` | Inspect database/table/column structure |
42
+ | `preview` | Preview table data (paged, ≤50 rows per page) |
43
+ | `run_script` | Run a script in a sandbox that can call `db.query` / `db.execute` repeatedly |
44
+
45
+ `run_script` isolation: separate child process plus a `node:vm` sandbox, hard-killed 60 seconds after start, with only a restricted `db.{query,execute}` handle injected — no filesystem, network, or `require`.
46
+
47
+ ### Project-scoped grants (ro / rw)
48
+
49
+ Grants are stored as project path → connection → mode (`grants.json`):
50
+
51
+ - **ro (read-only)**: query and browse allowed, every write rejected
52
+ - **rw (read-write)**: statements, scripts and transactions allowed
53
+
54
+ Connections are always rejected for projects without a grant. Changing the mode (including an rw→ro downgrade) invalidates existing sessions immediately — no reconnect needed.
55
+
56
+ On Windows, paths are normalized with NTFS case semantics, so `E:\GitHub\MyApp` and `e:\github\myapp` resolve to the same project.
57
+
58
+ ### Dangerous-operation confirmation
59
+
60
+ DDL, `FLUSHALL`, `dropDatabase` and similar statements return `NEEDS_CONFIRMATION` instead of executing:
61
+
62
+ 1. The first call returns a pending state and a one-time `challengeId`
63
+ 2. The `challengeId` is bound to the statement's SHA256, valid for 5 minutes and single-use
64
+ 3. Execution happens only when retried with the same `challengeId` after user confirmation
65
+
66
+ Confirmation comes through chat for model calls and through a dialog in the SQL console — both share the same semantics, so neither can bypass the gate.
67
+
68
+ ### Full audit logging
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.
71
+
72
+ ### Data browsing
73
+
74
+ - Four-level tree: connection → database → schema → table
75
+ - Database filter plus cross-database fuzzy table search (MySQL / PostgreSQL / GaussDB use native `information_schema` queries; other engines fall back to in-memory filtering)
76
+ - Table structure and data preview, with sortable/paged grids and cell copy
77
+
78
+ ### SQL console
79
+
80
+ A Navicat-style multi-tab editor in the sidebar (CodeMirror 6):
81
+
82
+ - **Multiple tabs**: each keeps its own SQL, results and history (50 entries, click to refill)
83
+ - **Highlighting and completion**: dialect switches automatically (MySQL / PostgreSQL / SQLite / PL/SQL / T-SQL), with table/column metadata injected into completion
23
84
  - **Shortcuts**: `Ctrl/Cmd+Enter` runs everything, `Ctrl/Cmd+Shift+Enter` runs the selection
24
- - **Formatting**: dialect-aware formatting via sql-formatter (sqlserver→`transactsql`), falling back to the original text on parse failure
25
- - **Per-statement execution**: statements are pre-classified client-side as read/write and routed to `/api/query` vs `/api/execute`; write statements are skipped and flagged under read-only grants; errors can jump to the offending statement
26
- - **`GO` batch separator**: `GO` and `GO n` follow sqlcmd semantics — the preceding batch is repeated n times (`GO 0` / `GO 1` both mean once) and the `GO` line itself produces no statement; the repeat count is capped at **1000** (values above it are truncated) so one run cannot be amplified into an unbounded number of requests
27
- - **Transactions**: begin/commit/rollback under rw grants (server-side sticky-connection session; idle sessions roll back after a 5-minute TTL and are audited; grant revocation takes effect immediately)
28
- - **Result grid**: tri-state column sorting, pagination at 50 rows/page, click-to-copy cells, CSV/JSON export
29
-
30
- ## SQL Server Support Scope and Boundaries
31
-
32
- Coverage: SQL Server 2008/2008R2 → 2022 and Azure SQL Database; the dialect and metadata queries only use SQL Server 2005+ constructs.
33
-
34
- - **Connection form**: exclusively `sqlserver://user:password@host:port/db`, with query parameters `encrypt` / `trustServerCertificate` / `tdsVersion` / `instanceName` / `useUTC` (names are case-insensitive). `tdsVersion` defaults to `7_4` and accepts `7_3_A` / `7_3_B` / `7_2` / `7_1`; the port defaults to 1433, and when `instanceName` is set the port is ignored per tedious semantics (SQL Browser over UDP 1434); the minimum TLS version is set via `cryptoCredentialsDetails.minVersion`, and `ssl=true` is equivalent to `encrypt=true`. ADO.NET key/value connection strings (`Server=...;Database=...`) are **not** accepted.
35
- The defaults are `encrypt=true` + `trustServerCertificate=false` (the same as mssql's own defaults). **Connecting to SQL Server 2008/2008R2 usually requires appending `?trustServerCertificate=true`**: those instances typically use self-signed certificates and may only speak TLS 1.0, while Node's default minimum is TLSv1.2, so the defaults fail the handshake; use `?encrypt=false` when the server does not force encryption, and set `?tdsVersion=7_3_A` (2008) / `7_3_B` (2008R2) explicitly.
36
- - **Authentication**: SQL authentication only (`user` / `password`). **Windows integrated authentication is not supported**, and neither is **Azure AD authentication**.
37
- - **Dialect**: write `?` as the parameter placeholder — the adapter rewrites them in order to `@p1..@pN`; identifiers use `[x]` with `]` doubled as `]]`; database/table/column metadata goes through `sys.*` catalog views only (`sys.databases` / `sys.schemas` / `sys.objects` / `sys.columns` / `sys.types` / `sys.indexes` / `sys.extended_properties`), using 2005+ constructs only.
38
- - **Paging is version-split**: after connecting it reads `SERVERPROPERTY('ProductMajorVersion')` (falling back to parsing the `ProductVersion` prefix) and caches the result; major ≥ 11 (2012+, including Azure SQL's 12) uses `ORDER BY (SELECT NULL) OFFSET n ROWS FETCH NEXT m ROWS ONLY`, while < 11 (2008/2008R2) degrades to a `ROW_NUMBER() OVER (ORDER BY (SELECT NULL))` subquery; a failed probe also takes the degraded branch.
39
- - **decimal precision is side-dependent**: on the browsing side (`query` / `preview` / data-browser grid) **decimal/numeric is not guaranteed digit-exact** — tedious has already turned decimal into a JS number during parsing (>15 significant digits lose precision), and converting it to a string afterwards only prevents later numeric coercion (>1e21 also renders in scientific notation); ask for `CONVERT(nvarchar(...), col)` in SQL when you need digit-exact values. The transfer reader already does that conversion (`CONVERT(nvarchar(64), col)`) and binds strings on write, so precision never passes through JS floats.
40
- - **Type mapping on the transfer side**: `hierarchyid` / `geometry` / `geography` move as binary (bytes) into `VARBINARY(MAX)` on the target — the values are preserved but **UDT type semantics are not** (no `ST_*` / `hierarchyid` methods on the target); `sql_variant` is read as `CONVERT(nvarchar(max), col)` text, so **the original type information is lost** (the driver returns scalars of the value's actual type, which cannot be moved uniformly); `text` / `ntext` / `xml` → `NVARCHAR(MAX)`, source `varchar` / `char` of length 4001–8000 also maps to `NVARCHAR(MAX)` class (capacity preserved, never silently narrowed), `varbinary` / `image` / `rowversion` → `VARBINARY(MAX)`, `decimal` / `numeric` / `money` → `DECIMAL`; `datetime2` is read via `CONVERT(nvarchar(33), col, 126)` and `time` via `CONVERT(nvarchar(16), col, 126)` for exactness.
41
- - **ro read-only is weaker than MySQL/PG**: SQL Server has no equivalent session-level read-only statement. Under an ro grant the adapter's query gate has five checks: ① a first-word allowlist (`select / with / show / describe / desc / explain / use / set / help / table`); ② rejection of multiple statements (lexical: the moment anything follows a top-level semicolon); ③ rejection of `SELECT ... INTO` (token-level, so `into` inside strings, bracket identifiers, or comments is not a false positive); ④ rejection of `WITH ... (INSERT|UPDATE|DELETE|MERGE)`; ⑤ rejection of `OPENQUERY` / `OPENROWSET` / `OPENDATASOURCE` (their string arguments execute arbitrary statements remotely/unattended and cannot be audited, so even legitimate read-only remote queries are refused). The service layer also blocks `execute`, and the guard layer still requires confirmation for the same two "read head, write body" forms. **This is still application-level interception, not the same as a database-side read-only session**; use a database-side read-only account when you need strong isolation.
42
- - **Data transfer**: writes use parameterized multi-row `VALUES` INSERTs (TDS caps a statement at 2100 parameters and a `VALUES` table constructor at 1000 rows, so rows per statement is the smaller of the two; replace/ignore go through `MERGE`). Transfer paging is version-split as well: 2012+ uses `OFFSET/FETCH`, while 2008/2008R2 degrades — ordered cursors (keyset/range) use `SELECT TOP (n) ... ORDER BY key`, and the offset full ordering plus `keyAt` chunking use `ROW_NUMBER() OVER (...)` subqueries; a failed version probe falls back to 0 and takes the conservative branch, so **transfer works on SQL Server 2008/2008R2 too**. Known limits: when the target table already exists and a column is IDENTITY, writing explicit values fails with error 544 (the plugin does not issue `SET IDENTITY_INSERT ON`); tables created by transfer do not generate IDENTITY columns. CREATE TABLE DDL can be rolled back inside an explicit transaction, so no Oracle/DM-style implicit-commit handling is needed.
43
- - **Verification status**: this round ships offline unit tests only (Fake driver); the SQL Server path is **not verified against a live database**. The live smoke switch is `DBT_TEST_SQLSERVER_URL` (e.g. `sqlserver://sa:pw@host:1433/db`).
44
- - **Driver packaging**: `mssql@12` lives in devDependencies only; at runtime the plugin loads `vendor/sqlserver-driver.cjs` built by `npm run build:sqlserver`. Reason: mssql → tedious → bl → readable-stream drags buffer/events/process/string_decoder into the production dependency tree, tripping the host's `dsh-app-boot` `require.resolve.paths` bug and failing the whole plugin load.
45
- - **Error-message redaction**: when a connection URL fails to parse, the error text has the userinfo redacted and never echoes the plaintext password (errors reach `audit.jsonl` and the model context, so redaction is mandatory).
46
- - **Not supported**: SQL Server 2005 (9.x) and earlier — neither supported nor verified; `tdsVersion=8_0` (TDS 8.0) and `encrypt='strict'` are also unsupported — SQL Server 2022 connects fine with the default `7_4`, and TDS 8.0 strict encryption is a later enhancement.
47
-
48
- ## Data Layout
49
-
50
- `$DSH_HOME/db-tool/` (0700):
85
+ - **Formatting**: sql-formatter per dialect, falling back to the original text when parsing fails
86
+ - **Per-statement execution**: statements are pre-classified client-side and routed to the query or execute channel separately; writes are skipped and marked under an ro grant
87
+ - **`GO` batch separator**: `GO` and `GO n` follow sqlcmd semantics (`GO 0` / `GO 1` both mean one pass), with a repeat cap of 1000 to prevent amplification
88
+ - **Transactions**: begin/commit/rollback under an rw grant on a sticky server-side connection; idle sessions roll back automatically after 5 minutes and are audited
89
+ - **Result grid**: three-state sorting, 50 rows per page, CSV/JSON export
51
90
 
52
- | File | Contents |
91
+ ### Data transfer
92
+
93
+ Move table data between databases of the same or different engines:
94
+
95
+ - **Write modes**: `insert` (append), `ignore` (skip conflicts), `replace` (overwrite), `truncate` (clear first)
96
+ - **Automatic DDL**: the target table is created from the source structure when missing
97
+ - **Paged reads**: keyset, range and offset strategies, selected automatically per target version
98
+ - **Progress and history**: live progress plus per-project transfer history
99
+
100
+ ### Connection export / import
101
+
102
+ Export all connections (including passwords) to a passphrase-encrypted file for cross-machine migration:
103
+
104
+ - scrypt-derived key with AES-256-GCM over the whole payload — the file contains no plaintext
105
+ - Minimum passphrase length of 8 characters, enforced server-side
106
+ - Imports skip any connection id that already exists and report an "imported / skipped / failed" summary
107
+
108
+ ---
109
+
110
+ ## Sidebar
111
+
112
+ One tab registered through `dsh-better-sidebar`, containing four panels (bilingual):
113
+
114
+ | Panel | Purpose |
53
115
  |---|---|
54
- | `connections.json` | connection definitions (passwords masked as `***` in `urlSafe`) |
55
- | `secrets.json` | 0600; password/URL credentials, never returned to the model |
56
- | `grants.json` | project grants (normalized path → connId → ro/rw) |
57
- | `audit.jsonl` | append-only audit log |
116
+ | Connections | Add, edit, delete connections; test connectivity; export/import |
117
+ | Grants | Tick ro/rw per connection for the current project |
118
+ | Browse | Tree navigation, search and data preview |
119
+ | SQL Console | Multi-tab editor, transactions, history, result grid |
58
120
 
59
- ## Installation
121
+ ---
122
+
123
+ ## Security model
124
+
125
+ | Layer | Mechanism |
126
+ |---|---|
127
+ | Storage | Passwords and full URLs live in `secrets.json` (0600) and are never returned to the model; credentials in data returned to the UI are redacted to `***` |
128
+ | Authorization | Project-scoped ro/rw allowlist — no grant means rejection |
129
+ | Statement level | Risk classification (danger / warning / none) plus one-time confirmation |
130
+ | Read-only enforcement | MySQL/PG use session-level `SET TRANSACTION READ ONLY`; SQL Server has no equivalent, so it degrades to five application-level gates (below) |
131
+ | Error redaction | When URL parsing fails, userinfo is redacted from the error text so plaintext passwords never appear |
132
+ | Script isolation | Separate child process, vm sandbox, 60s hard kill, only the `db` handle injected |
133
+
134
+ **SQL Server's ro enforcement is weaker than MySQL/PG**: SQL Server has no equivalent session-level read-only statement, so under an ro grant five application-level gates apply — first-word allowlist, multi-statement rejection, `SELECT ... INTO` rejection, `WITH ... (INSERT|UPDATE|DELETE|MERGE)` rejection, and `OPENQUERY`/`OPENROWSET`/`OPENDATASOURCE` rejection. This remains application-level interception; for hard isolation, configure a database-side read-only account for the connection.
135
+
136
+ ---
137
+
138
+ ## SQL Server connection notes
139
+
140
+ Covers SQL Server 2008 / 2008 R2 through 2022 and Azure SQL Database.
141
+
142
+ **Connection URL parameters** (also supported by the field-by-field form):
143
+
144
+ | Parameter | Values | Default |
145
+ |---|---|---|
146
+ | `encrypt` | `true` / `false` | `true` |
147
+ | `trustServerCertificate` | `true` / `false` | `false` |
148
+ | `tdsVersion` | `7_4` / `7_3_A` / `7_3_B` / `7_2` / `7_1` | `7_4` |
149
+ | `tlsMinVersion` | `TLSv1` / `TLSv1.1` / `TLSv1.2` | unset (Node default TLSv1.2) |
150
+ | `instanceName` | Named instance (mutually exclusive with port) | — |
151
+ | `useUTC` | `true` / `false` | `true` |
152
+
153
+ **When connecting to older instances, tell the two TLS failure families apart — their remedies are not interchangeable**:
154
+
155
+ - **Certificate family** (`self signed certificate` / `unable to verify` / `CERT_`) → append `?trustServerCertificate=true`; use `?encrypt=false` when the server does not force encryption
156
+ - **Protocol-version family** (`UNSUPPORTED_PROTOCOL` / `no protocols available` / `alert protocol version`) → client and server share no common TLS version, so the handshake fails during negotiation and **certificate validation never runs**; `trustServerCertificate` is useless here. The real fix is enabling TLS 1.2 on the server (KB4057113 for 2008 R2 SP3, KB4057114 for 2008 SP4); when the server cannot be touched, lower the client floor with `?tlsMinVersion=TLSv1`
157
+
158
+ `tlsMinVersion` does not include `TLSv1.3`: the underlying tedious driver caps the TLS ceiling at 1.2, so setting 1.3 makes the minimum exceed the maximum and the handshake always fails.
60
159
 
61
- Drivers for all 9 databases ship with the npm package (SQLite prefers the built-in `node:sqlite`, optionally `better-sqlite3`; the GaussDB driver is the official Huawei Cloud npm package `gaussdb-node`) — no build steps of any kind. The SQL Server driver is the exception in form: `mssql@12` lives in devDependencies only, and the runtime loads the bundled `vendor/sqlserver-driver.cjs` built by `npm run build:sqlserver` — mssql's production dependency tree pulls in buffer/events/process/string_decoder, which trips the host's resolve.paths bug. GaussDB authentication supports sha256 and md5; md5-sha256 hybrid and SM3 are not supported yet.
160
+ **Other boundaries**: SQL authentication only (Windows integrated authentication and Azure AD are not supported); SQL Server 2005 and earlier are not supported; `tdsVersion=8_0` (TDS 8.0) and `encrypt='strict'` are not yet supported.
62
161
 
63
- The only one-time interaction comes from oracledb: pnpm blocks its install script by default, which makes the first install report a failure — click **"Allow these scripts and retry"** in the DSH plugin installation UI to finish (the script only checks the Node version and prints a banner, so approving is risk-free; the approval is persisted to the profile and upgrades won't prompt again).
162
+ ---
163
+
164
+ ## Installation
64
165
 
65
166
  ```bash
66
167
  # npm (recommended)
@@ -70,48 +171,82 @@ dsh plugin --profile web add dsh-db-tool
70
171
  dsh plugin --profile web add "dsh-db-tool@github:mengqi1436/dsh-db-tool"
71
172
 
72
173
  # Local development
73
- dsh plugin --profile web add "link:E:\path\to\dsh-db-tool"
174
+ dsh plugin --profile web add "link:/path/to/dsh-db-tool"
74
175
  ```
75
176
 
76
- ### Desktop installation
177
+ ### Desktop app
77
178
 
78
- DSH Desktop 0.2.0-rc.2+ bundles the `dsh` command — no separate Node or pnpm required:
179
+ DSH desktop 0.2.0-rc.2+ bundles the `dsh` command:
79
180
 
80
- 1. **First use**: open the desktop menu bar, click **"Manage dsh command"**, and install the bundled CLI, which registers `dsh` on the system PATH.
81
- 2. **Install the plugin** (always pin an exact version: `@latest` falls back to an older release due to release-age checks):
181
+ 1. Open the desktop menu bar and click **"Manage dsh command"** to register `dsh` on your PATH
182
+ 2. Install the plugin (npm installs must pin an exact version — `@latest` falls back to an older release due to release-age checks):
82
183
 
83
184
  ```bash
84
- dsh plugin --profile desktop add dsh-db-tool@1.4.0 --registry=https://registry.npmjs.org/
185
+ dsh plugin --profile desktop add dsh-db-tool@1.8.8 --registry=https://registry.npmjs.org/
85
186
  ```
86
187
 
87
- If you prefer an npm mirror, replace `--registry` with your mirror URL.
88
- 3. **Restart** the desktop app.
188
+ 3. **Restart the desktop app** to load it
189
+
190
+ **About oracledb**: its install script is blocked by pnpm by default, so the first install reports a failure. Click **"Allow these scripts and retry"** in the DSH plugin installer — the script only performs a Node version check and prints a banner, approving it is safe, and the approval persists to your profile so later upgrades do not prompt again.
191
+
192
+ **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
+
194
+ ---
195
+
196
+ ## Data layout
197
+
198
+ `$DSH_HOME/db-tool/` (directory mode 0700):
199
+
200
+ | File | Contents |
201
+ |---|---|
202
+ | `connections.json` | Connection definitions (passwords redacted to `***` in `urlSafe`) |
203
+ | `secrets.json` | Mode 0600; passwords and full URLs, never returned to the model |
204
+ | `grants.json` | Project grants (normalized path → connId → ro/rw) |
205
+ | `audit.jsonl` | Append-only audit log, one JSON object per line |
89
206
 
90
- The MongoDB driver is bundled into the published package (`vendor/mongodb-driver.cjs`), so the dependency tree contains no mongodb/punycode — the desktop host is packed inside `app.asar` and cannot be host-patched, yet this plugin installs and loads there without any patch.
207
+ ---
91
208
 
92
- ## Testing
209
+ ## Development
93
210
 
94
211
  ```bash
95
- npm test # full offline mock suite (incl. e2e-mock end-to-end and adversarial cases)
96
- npx tsc --noEmit
97
- npx stryker run # mutation testing (lib/guard + lib/manager + lib/store, reports in reports/mutation/)
212
+ npm install
213
+ npm run build # compile + build vendor bundles (mongodb / sqlserver / codemirror)
214
+ npm test # full vitest suite
215
+ npm run typecheck # tsc --noEmit
216
+ ```
217
+
218
+ Integration smoke tests are gated by environment variables and only run when set:
219
+
220
+ ```
221
+ DBT_TEST_MYSQL_URL / DBT_TEST_PG_URL / DBT_TEST_REDIS_URL / DBT_TEST_DM_CONNECT
222
+ DBT_TEST_MONGO_URL / DBT_TEST_ORACLE_CONNECT / DBT_TEST_SQLSERVER_URL
98
223
  ```
99
224
 
100
- Live-database smoke tests (run only when set): `DBT_TEST_MYSQL_URL / DBT_TEST_PG_URL / DBT_TEST_REDIS_URL / DBT_TEST_DM_CONNECT / DBT_TEST_MONGO_URL / DBT_TEST_ORACLE_CONNECT / DBT_TEST_SQLSERVER_URL` (SQL Server in the form `sqlserver://sa:pw@host:1433/db`). The GaussDB, Oracle/Mongo, and SQL Server requirements are implemented per their official docs; anything not verified against live databases is annotated in code. **This round ships offline unit tests only (Fake driver) for SQL Server — no live-database verification.**
225
+ CI is defined in [`.github/workflows/ci.yml`](.github/workflows/ci.yml): Node 22 and 24, running `npm ci → build → typecheck → test`.
101
226
 
102
- ## Project Structure
227
+ ### Repository layout
103
228
 
104
229
  ```
105
- lib/ host plugin (store / adapters×9 / guard / manager / http / index)
106
- client/ sidebar single-file artifact (client.js — this is the source)
107
- skills/ db-admin skill
108
- scripts/ build & tooling scripts (MongoDB / SQL Server driver and CodeMirror editor bundles, DSH host hotfix patch:dsh, etc.)
109
- patches/ DSH host dsh-app-boot hotfix patches (selected by host version for patch:dsh)
110
- docs/ reserved (currently empty)
111
- tests/ vitest (offline mocks + DBT_TEST_*-gated live runs)
112
- vendor/ mongodb-driver.cjs, sqlserver-driver.cjs and codemirror-sql.cjs bundles (gitignored; shipped via the files whitelist)
230
+ lib/ plugin core
231
+ adapters/ nine database adapters plus transfer implementations
232
+ store/ connection / grant / audit / secret storage
233
+ guard/ statement risk classification
234
+ manager.ts business orchestration (auth, dispatch, confirm, audit)
235
+ http/ sidebar HTTP routes
236
+ script/ run_script sandbox and child process
237
+ client/ sidebar single-file bundle
238
+ skills/ db-admin skill
239
+ scripts/ build and tooling scripts
240
+ patches/ DSH host hotfix patches
241
+ tests/ vitest (offline mocks plus gated integration tests)
242
+ assets/ feature flow diagram
243
+ vendor/ driver and editor bundles (build artifacts, included on publish)
113
244
  ```
114
245
 
246
+ ---
247
+
115
248
  ## License
116
249
 
117
- MIT
250
+ [Apache License 2.0](LICENSE) © 2026 mengqi1436
251
+
252
+ Free to use commercially, modify, distribute and sublicense, provided copyright and license notices are retained and modified files are marked. The included patent grant is irrevocable, but terminates if you bring a patent suit over this work.
package/client/client.js CHANGED
@@ -97,6 +97,7 @@ window.__ModuleLoader__.load({
97
97
  newConn: "新建连接",
98
98
  editConn: "编辑连接",
99
99
  noConns: "还没有连接,点击「新建连接」添加。",
100
+ needProjectPath: "项目路径为空,数据浏览不可用:请在顶部输入框填写当前项目的绝对路径(如 E:\\Code\\my-app)后重试。",
100
101
  connName: "名称 / 别名",
101
102
  connId: "连接 ID",
102
103
  urlMode: "URL 方式",
@@ -111,6 +112,8 @@ window.__ModuleLoader__.load({
111
112
  fieldsPassword: "密码",
112
113
  passwordSaved: "已保存(留空保持不变)",
113
114
  fieldsDatabase: "数据库名",
115
+ fieldsTlsMinVersion: "最低 TLS 版本(连 SQL Server 2008/2008R2 报 UNSUPPORTED_PROTOCOL 时选 TLSv1)",
116
+ tlsMinDefault: "默认(TLSv1.2)",
114
117
  ssl: "启用 SSL",
115
118
  test: "测试",
116
119
  testing: "测试中…",
@@ -161,6 +164,13 @@ window.__ModuleLoader__.load({
161
164
  modeRw: "读写",
162
165
  // 浏览
163
166
  noDatabases: "无可用数据库",
167
+ dbFilterAll: "全部库",
168
+ dbFilterLabel: "按库筛选",
169
+ searchPlaceholder: "模糊搜索表名,回车执行",
170
+ searchGo: "搜索",
171
+ searchBack: "返回目录树",
172
+ searchResults: "搜索结果",
173
+ searchEmpty: "没有匹配的表",
164
174
  noSchemas: "无模式",
165
175
  noTables: "无表",
166
176
  defaultDb: "默认库",
@@ -316,6 +326,7 @@ window.__ModuleLoader__.load({
316
326
  newConn: "New Connection",
317
327
  editConn: "Edit Connection",
318
328
  noConns: "No connections yet. Click \"New Connection\" to add one.",
329
+ needProjectPath: "Project path is empty and browsing is unavailable: enter the current project's absolute path in the input above (e.g. /home/me/my-app), then retry.",
319
330
  connName: "Name / alias",
320
331
  connId: "Connection ID",
321
332
  urlMode: "URL",
@@ -330,6 +341,8 @@ window.__ModuleLoader__.load({
330
341
  fieldsPassword: "Password",
331
342
  passwordSaved: "Saved (leave blank to keep)",
332
343
  fieldsDatabase: "Database",
344
+ fieldsTlsMinVersion: "Minimum TLS version (pick TLSv1 when SQL Server 2008/2008R2 reports UNSUPPORTED_PROTOCOL)",
345
+ tlsMinDefault: "Default (TLSv1.2)",
333
346
  ssl: "Enable SSL",
334
347
  test: "Test",
335
348
  testing: "Testing…",
@@ -377,6 +390,13 @@ window.__ModuleLoader__.load({
377
390
  modeRo: "read-only ro",
378
391
  modeRw: "read-write rw",
379
392
  noDatabases: "No databases available",
393
+ dbFilterAll: "All databases",
394
+ dbFilterLabel: "Filter by database",
395
+ searchPlaceholder: "Fuzzy-search table names, Enter to run",
396
+ searchGo: "Search",
397
+ searchBack: "Back to tree",
398
+ searchResults: "Search results",
399
+ searchEmpty: "No matching tables",
380
400
  noSchemas: "No schemas",
381
401
  noTables: "No tables",
382
402
  defaultDb: "Default DB",
@@ -559,6 +579,13 @@ window.__ModuleLoader__.load({
559
579
  "/* ===== 填充式无边框输入 ===== */",
560
580
  ".dbt-row input,.dbt-row select,.dbt-card input,.dbt-card select,.dbt-card textarea,.dbt-group input,.dbt-group select{flex:1;min-width:60px;background:var(--dbt-surface-strong);border:none;color:inherit;border-radius:var(--dbt-radius-ctrl);padding:5px 12px;font-size:13px;font-family:inherit;transition:outline-color .18s var(--dbt-ease),outline-width .18s var(--dbt-ease);}",
561
581
  ".dbt-row input:focus,.dbt-row select:focus,.dbt-card input:focus,.dbt-card select:focus,.dbt-card textarea:focus,.dbt-group input:focus,.dbt-group select:focus{outline:2px solid var(--dbt-accent);outline-offset:-1px;}",
582
+ /* select 弹出层配色自洽:option 弹层是原生控件,不继承 .dbt 面板背景——Windows Chromium
583
+ * 未声明 color-scheme 时固定白底,而 option 文字 color:inherit 在第三方主题(深色面板)下
584
+ * 是白字 → 白底白字。option 显式配一套自带对比度的深/浅对(不透明,弹层是独立原生窗口,
585
+ * 半透明会混入白底),并声明 color-scheme 让原生层按系统深浅渲染(选中项高亮/滚动条) */
586
+ ".dbt-row select,.dbt-card select,.dbt-group select{color-scheme:dark light;}",
587
+ ".dbt-row select option,.dbt-card select option,.dbt-group select option{color:var(--dbt-text);background:#28282c;}",
588
+ "@media (prefers-color-scheme: light){.dbt-row select option,.dbt-card select option,.dbt-group select option{background:#fcfcfc;}}",
562
589
  ".dbt-card textarea{font-family:var(--dbt-mono);min-height:96px;resize:vertical;}",
563
590
  "/* ===== 按钮 ===== */",
564
591
  ".dbt-btn{border:none;background:var(--dbt-surface);color:inherit;border-radius:var(--dbt-radius-ctrl);padding:5px 12px;font-size:13px;font-weight:500;cursor:pointer;white-space:nowrap;transition:background .18s var(--dbt-ease),filter .18s var(--dbt-ease),transform .18s var(--dbt-ease),opacity .18s var(--dbt-ease);}",
@@ -775,7 +802,7 @@ window.__ModuleLoader__.load({
775
802
  }
776
803
 
777
804
  /* ---------------- 连接管理 ---------------- */
778
- const EMPTY_FORM = { id: "", kind: "mysql", name: "", mode: "url", url: "", urlUser: "", urlPassword: "", host: "", port: "", user: "", password: "", database: "", ssl: false };
805
+ const EMPTY_FORM = { id: "", kind: "mysql", name: "", mode: "url", url: "", urlUser: "", urlPassword: "", host: "", port: "", user: "", password: "", database: "", tlsMinVersion: "", ssl: false };
779
806
 
780
807
  // 口令弹窗(ManageView 局部复用,导出/导入共用):键盘契约对齐 DangerDialog
781
808
  // (Escape=取消、Tab 在对话框内循环锁定);差异:初始焦点在口令输入框,确认按钮主色。
@@ -1292,7 +1319,7 @@ window.__ModuleLoader__.load({
1292
1319
  const freshForm = () => withKindDefaults({ ...EMPTY_FORM, mode: "fields" });
1293
1320
 
1294
1321
  /** 编辑回填:从 meta 还原已存配置(密码不出库,留空=保留原密码)。
1295
- * url 方式 → 回填脱敏 url;分字段方式 → 回填 host/port/user/database。 */
1322
+ * url 方式 → 回填脱敏 url;分字段方式 → 回填 host/port/user/database/tlsMinVersion。 */
1296
1323
  function buildEditForm(c) {
1297
1324
  const mode = c.mode || (c.safeUrl ? "url" : "fields");
1298
1325
  return {
@@ -1302,6 +1329,7 @@ window.__ModuleLoader__.load({
1302
1329
  urlUser: "", urlPassword: "",
1303
1330
  host: c.host || "", port: c.port != null ? String(c.port) : "",
1304
1331
  user: c.user || "", database: c.database || "",
1332
+ tlsMinVersion: c.tlsMinVersion || "",
1305
1333
  ssl: !!c.ssl,
1306
1334
  };
1307
1335
  }
@@ -1379,6 +1407,9 @@ window.__ModuleLoader__.load({
1379
1407
  // 端口原样发字符串:Number("abc")=NaN 会被 JSON 序列化成 null,服务端按「未提供端口」静默回退默认值,配置错误被掩盖
1380
1408
  if (form.port) body.fields.port = form.port;
1381
1409
  if (form.password) body.fields.password = form.password;
1410
+ // 最低 TLS 版本:空=不注入(沿用 Node 默认 TLSv1.2)。仅 sqlserver 有此字段,
1411
+ // 其它库即使表单残留旧值也不发,避免无关库收到未知键。
1412
+ if (form.kind === "sqlserver" && form.tlsMinVersion) body.fields.tlsMinVersion = form.tlsMinVersion;
1382
1413
  // 从 url 方式切到分字段保存 → 服务端清除已存 url(否则连接仍走旧 url)
1383
1414
  if (props.initial && props.initial.mode === "url") body.clearUrl = true;
1384
1415
  }
@@ -1442,6 +1473,14 @@ window.__ModuleLoader__.load({
1442
1473
  React.createElement("div", { className: "dbt-row" },
1443
1474
  React.createElement("input", { placeholder: t("fieldsUser"), "aria-label": t("fieldsUser") + t("requiredMark"), "aria-required": "true", value: form.user, onChange: (e) => patch({ user: e.target.value }) }),
1444
1475
  React.createElement("input", { type: "password", placeholder: pwLabel, "aria-label": pwLabel, value: form.password, onChange: (e) => patch({ password: e.target.value }) })),
1476
+ // 最低 TLS 版本(仅 sqlserver):老实例只支持 TLS 1.0 时,客户端默认下限 TLSv1.2 会
1477
+ // 报 UNSUPPORTED_PROTOCOL(协议版本协商失败,与证书无关,信任证书无效)。留空=不改默认。
1478
+ form.kind === "sqlserver"
1479
+ ? React.createElement("div", { className: "dbt-row" },
1480
+ React.createElement("select", { "aria-label": t("fieldsTlsMinVersion"), value: form.tlsMinVersion, onChange: (e) => patch({ tlsMinVersion: e.target.value }) },
1481
+ React.createElement("option", { value: "" }, t("tlsMinDefault")),
1482
+ ["TLSv1", "TLSv1.1", "TLSv1.2"].map((v) => React.createElement("option", { key: v, value: v }, v))))
1483
+ : null,
1445
1484
  ),
1446
1485
  // 分组 3:操作
1447
1486
  React.createElement(
@@ -1763,10 +1802,17 @@ window.__ModuleLoader__.load({
1763
1802
  const [dbs, setDbs] = React.useState({}); // connId -> string[]
1764
1803
  const [schemasMap, setSchemasMap] = React.useState({}); // "<connId>/<db>" -> string[](PG/GaussDB 库内 schema 层)
1765
1804
  const [tablesMap, setTablesMap] = React.useState({}); // "<connId>/<db|schema>" -> TableInfo[]
1805
+ // 库筛选 + 跨库表名模糊搜索(连接展开后的内嵌筛选行;搜索走 /api/search-tables)
1806
+ const [dbFilter, setDbFilter] = React.useState({}); // connId -> 选中的库名("" = 全部库)
1807
+ const [searchKw, setSearchKw] = React.useState({}); // connId -> 输入框关键词
1808
+ const [searchRes, setSearchRes] = React.useState({}); // connId -> TableSearchHit[] | null(非空 = 结果视图替代目录树)
1809
+ const [searchBusy, setSearchBusy] = React.useState({});
1810
+ const [searchErr, setSearchErr] = React.useState({});
1766
1811
 
1767
1812
  // 会话项目切换时清空树缓存,避免陈旧授权下的旧数据
1768
1813
  React.useEffect(() => {
1769
1814
  setOpen({}); setLoading({}); setError({}); setDbs({}); setSchemasMap({}); setTablesMap({});
1815
+ setDbFilter({}); setSearchKw({}); setSearchRes({}); setSearchBusy({}); setSearchErr({});
1770
1816
  }, [projectPath]);
1771
1817
 
1772
1818
  // 三层加载统一入口(toggle 展开与手动/自动刷新共用;key: c:<id> | d:<id>/<db> | s:<id>/<db>/<schema>)
@@ -1829,6 +1875,48 @@ window.__ModuleLoader__.load({
1829
1875
  setOpen((o) => Object.assign({}, o, { [key]: false }));
1830
1876
  Promise.resolve().then(() => fn());
1831
1877
  }
1878
+
1879
+ /* -- 库筛选 + 跨库表名搜索 -- */
1880
+ function doSearch(c) {
1881
+ const kw = (searchKw[c.id] || "").trim();
1882
+ if (!kw || searchBusy[c.id]) return;
1883
+ setSearchBusy((s) => Object.assign({}, s, { [c.id]: true }));
1884
+ setSearchErr((s) => Object.assign({}, s, { [c.id]: undefined }));
1885
+ api("search-tables" + qs({ project: projectPath, connId: c.id, keyword: kw, database: dbFilter[c.id] || undefined, limit: 100 }))
1886
+ .then((hits) => setSearchRes((s) => Object.assign({}, s, { [c.id]: hits || [] })))
1887
+ .catch((e) => setSearchErr((s) => Object.assign({}, s, { [c.id]: e && e.message ? e.message : String(e) })))
1888
+ .finally(() => setSearchBusy((s) => Object.assign({}, s, { [c.id]: false })));
1889
+ }
1890
+ function clearSearch(c) {
1891
+ setSearchRes((s) => Object.assign({}, s, { [c.id]: null }));
1892
+ setSearchErr((s) => Object.assign({}, s, { [c.id]: undefined }));
1893
+ }
1894
+ // 连接展开后的内嵌筛选行(库下拉 + 表名搜索框):普通 div 不参与 treeitem 键盘导航
1895
+ function filterRow(c) {
1896
+ const cid = c.id;
1897
+ const busy = !!searchBusy[cid];
1898
+ const hasRes = !!searchRes[cid];
1899
+ return React.createElement("div", { key: "f:" + cid, className: "dbt-row", style: { margin: "4px 12px 4px 28px", gap: 6, alignItems: "center" } },
1900
+ React.createElement("select", {
1901
+ value: dbFilter[cid] || "",
1902
+ onChange: (e) => setDbFilter((m) => Object.assign({}, m, { [cid]: e.target.value })),
1903
+ style: { maxWidth: 160, flex: "none" },
1904
+ "aria-label": t("dbFilterLabel"),
1905
+ },
1906
+ React.createElement("option", { value: "" }, t("dbFilterAll")),
1907
+ (dbs[cid] || []).map((dn) => React.createElement("option", { key: dn, value: dn }, dn))),
1908
+ React.createElement("input", {
1909
+ value: searchKw[cid] || "",
1910
+ placeholder: t("searchPlaceholder"),
1911
+ onChange: (e) => setSearchKw((m) => Object.assign({}, m, { [cid]: e.target.value })),
1912
+ onKeyDown: (e) => { if (e.key === "Enter") doSearch(c); },
1913
+ style: { flex: "1 1 auto", minWidth: 80 },
1914
+ "aria-label": t("searchPlaceholder"),
1915
+ }),
1916
+ React.createElement("button", { className: "dbt-btn", disabled: busy, onClick: () => doSearch(c), style: { flex: "none" } }, busy ? "…" : t("searchGo")),
1917
+ hasRes ? React.createElement("button", { className: "dbt-btn", onClick: () => clearSearch(c), "aria-label": t("searchBack"), title: t("searchBack"), style: { flex: "none" } }, "×") : null,
1918
+ );
1919
+ }
1832
1920
  function toggleConn(c) {
1833
1921
  if (!projectPath) return;
1834
1922
  toggle("c:" + c.id, () => loadByKey("c:" + c.id));
@@ -1929,8 +2017,27 @@ window.__ModuleLoader__.load({
1929
2017
  if (loading[ck]) { treeRows.push(treerow(ck + ":l", 1, false, true, "…")); continue; }
1930
2018
  if (error[ck]) { treeRows.push(treerow(ck + ":x", 1, false, true, t("loadFailed") + ":" + error[ck], () => retry(ck, () => toggleConn(c)), false)); continue; }
1931
2019
  if (!list) continue;
2020
+ // 内嵌筛选行:库下拉 + 表名模糊搜索(仅 mysql 原生;其余走服务端逐库回退)
2021
+ treeRows.push(filterRow(c));
2022
+ // 搜索错误行优先于结果视图渲染(与 hits 解耦:首次失败也要有反馈,点击重试)
2023
+ if (searchErr[c.id]) { treeRows.push(treerow(ck + ":sx", 1, false, true, t("loadFailed") + ":" + searchErr[c.id], () => doSearch(c), false)); continue; }
2024
+ // 搜索结果视图:替代目录树,点结果行打开对应表;「×」返回目录树
2025
+ const hits = searchRes[c.id];
2026
+ if (hits) {
2027
+ if (hits.length === 0) { treeRows.push(treerow(ck + ":s0", 1, false, true, t("searchEmpty"))); }
2028
+ else {
2029
+ for (const h of hits) {
2030
+ const active = !!sel && sel.connId === c.id && sel.db === h.database && (!h.schema || sel.schemaName === h.schema) && !!sel.table && sel.table.name === h.name;
2031
+ treeRows.push(treerow("sr:" + c.id + "/" + h.database + "/" + (h.schema ? h.schema + "." : "") + h.name, 1, false, true,
2032
+ h.database + "." + h.name + (h.comment ? " · " + h.comment : ""),
2033
+ (e) => onSelect({ connId: c.id, db: h.database, schemaName: h.schema, table: { name: h.name, type: h.type } }, e.currentTarget), active));
2034
+ }
2035
+ }
2036
+ continue;
2037
+ }
1932
2038
  if (list.length === 0) { treeRows.push(treerow(ck + ":e", 1, false, true, t("noDatabases"))); continue; }
1933
2039
  for (const d of list) {
2040
+ if (dbFilter[c.id] && d !== dbFilter[c.id]) continue; // 库筛选:前端过滤,未选中库不渲染
1934
2041
  const useSchemas = !!HAS_SCHEMAS[c.kind];
1935
2042
  const dk = "d:" + c.id + "/" + d;
1936
2043
  const dOpen = !!open[dk];
@@ -1979,6 +2086,9 @@ window.__ModuleLoader__.load({
1979
2086
  return React.createElement(
1980
2087
  "div",
1981
2088
  { style: { display: "flex", flexDirection: "column", gap: 8 } },
2089
+ // projectPath 为空时所有展开动作静默 no-op(toggleConn 等防御),必须就地说明原因,
2090
+ // 否则用户点击连接无任何反馈(role=alert 使读屏播报,样式复用 dbt-err 醒目红字)
2091
+ !projectPath ? React.createElement("div", { className: "dbt-err", role: "alert" }, t("needProjectPath")) : null,
1982
2092
  // Navicat 式对象树:点击展开连接/库,点击表上报选中(由父组件就地预览)
1983
2093
  conns.length === 0 ? React.createElement("div", { className: "dbt-muted" }, t("noConns")) :
1984
2094
  React.createElement("div", { className: "dbt-group" },
@@ -3512,8 +3622,10 @@ window.__ModuleLoader__.load({
3512
3622
  const visible = props.visible;
3513
3623
  const scope = props.scope || {};
3514
3624
  const [view, setView] = React.useState("manage");
3515
- const [projectPath, setProjectPath] = React.useState(scope.cwd || scope.workspacePath || "");
3516
- const [projectEdited, setProjectEdited] = React.useState(!(scope.cwd || scope.workspacePath));
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));
3517
3629
  const [conns, setConns] = React.useState([]);
3518
3630
  const [grants, setGrants] = React.useState([]);
3519
3631
  const [busy, setBusy] = React.useState("");
@@ -3534,7 +3646,7 @@ window.__ModuleLoader__.load({
3534
3646
  // host 端以会话 header.cwd 为准归一化出 projectPathKey,前端不自拼 key。
3535
3647
  // scope.cwd/sessionId 变化(切换会话)时自动重新解析;用户手填(projectEdited)时不覆盖。
3536
3648
  const sessionKey = scope.sessionId || "";
3537
- const scopeCwd = scope.cwd || scope.workspacePath || "";
3649
+ const scopeCwd = scope.cwd || scope.repoRoot || scope.workspacePath || "";
3538
3650
  const resolvedRef = React.useRef("");
3539
3651
  React.useEffect(() => {
3540
3652
  if (!visible || projectEdited) return;