dsh-db-tool 1.8.5 → 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/LICENSE +201 -0
- package/README.md +206 -71
- package/README_EN.md +207 -72
- package/client/client.js +117 -5
- package/dist/adapters/mysql/index.js +36 -22
- package/dist/adapters/redis/index.js +5 -2
- package/dist/adapters/sql-shared/pg-like.js +53 -4
- package/dist/adapters/sqlserver/index.js +99 -21
- package/dist/http/index.js +4 -0
- package/dist/manager.js +63 -2
- package/dist/store/connections.js +3 -1
- package/dist/store/grants.js +46 -10
- package/dist/store/normalize.js +11 -2
- package/package.json +5 -3
package/README_EN.md
CHANGED
|
@@ -2,65 +2,166 @@
|
|
|
2
2
|
|
|
3
3
|
[简体中文](README.md) | English
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
##
|
|
7
|
+
## Feature flow
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
11
|
+

|
|
18
12
|
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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**:
|
|
25
|
-
- **Per-statement execution**: statements are pre-classified client-side
|
|
26
|
-
- **`GO` batch separator**: `GO` and `GO n` follow sqlcmd semantics
|
|
27
|
-
- **Transactions**: begin/commit/rollback under rw
|
|
28
|
-
- **Result grid**:
|
|
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
|
-
|
|
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
|
-
|
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
174
|
+
dsh plugin --profile web add "link:/path/to/dsh-db-tool"
|
|
74
175
|
```
|
|
75
176
|
|
|
76
|
-
### Desktop
|
|
177
|
+
### Desktop app
|
|
77
178
|
|
|
78
|
-
DSH
|
|
179
|
+
DSH desktop 0.2.0-rc.2+ bundles the `dsh` command:
|
|
79
180
|
|
|
80
|
-
1.
|
|
81
|
-
2.
|
|
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.
|
|
185
|
+
dsh plugin --profile desktop add dsh-db-tool@1.8.8 --registry=https://registry.npmjs.org/
|
|
85
186
|
```
|
|
86
187
|
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
207
|
+
---
|
|
91
208
|
|
|
92
|
-
##
|
|
209
|
+
## Development
|
|
93
210
|
|
|
94
211
|
```bash
|
|
95
|
-
npm
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
+
### Repository layout
|
|
103
228
|
|
|
104
229
|
```
|
|
105
|
-
lib/
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3516
|
-
|
|
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;
|