diff --git a/.gitignore b/.gitignore index 0abf66e..534dca3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Dependencies node_modules/ +.pnpm-store/ # Build output # lib/ is committed: git-hosted installs (pnpm git deps) and tarball installs diff --git a/README.en.md b/README.en.md index 91b2cb4..cfc5d0c 100644 --- a/README.en.md +++ b/README.en.md @@ -12,17 +12,18 @@ With this preset, you can configure a database connection right in the conversat ## Features -- **Database connection management**: per-session connections to MySQL / PostgreSQL / SQLite / Oracle / Hive / Impala (SQLite uses a file path, Oracle a service name/SID, Hive/Impala a default database). Connection state lives in server memory and survives layout switches; passwords stay in memory and travel to the client via environment variables or stdin connect prefixes — never written to disk on the server. +- **Database connection management**: per-session connections to MySQL / PostgreSQL / SQLite / Oracle / Hive / Impala / ClickHouse / Doris / SQL Server (SQLite uses a file path, Oracle a service name/SID, Hive/Impala a default database, SQL Server browses the schemas of the connected database). Connection state lives in server memory and survives layout switches; passwords stay in memory and travel to the client via environment variables or stdin connect prefixes — never written to disk on the server. ![Database connection](assets/connection.png) - **Database workbench** (embedded above the session's input bar): connection config card (collapses into a summary row after connecting, expandable for review); schema explorer (a "Tables" button opens a Modal — single-click a database to expand its scrollable table list, click a table to inspect its columns); SQL command box (write and run SQL on the non-agent channel, monospace output). The connection config is persisted to browser localStorage — switching pages or restarting restores the form and auto-reconnects. Once the conversation starts, the workbench becomes the left column and the chat records + input bar sit on the right. -- **sqlcmd tool**: runs SQL/commands through the database clients (mysql / psql / sqlite3 / sqlplus / beeline / impala-shell); no shell layer (argv arrays + SQL via stdin), timeouts terminate the process tree, output is bounded and truncated. +- **sqlcmd tool**: runs SQL/commands through the database clients (mysql / psql / sqlite3 / sqlplus / beeline / impala-shell / clickhouse-client / sqlcmd); no shell layer (argv arrays + SQL via stdin), timeouts terminate the process tree, output is bounded and truncated. - **Data Agent preset**: choose "Data Agent" when creating a session — the tool surface is exactly `sqlcmd`/`read`/`write`/`edit`, and every other project tool (bash, grep, skill, todo, goal, web, subagent, …) is simply absent, i.e. disabled; non-Data-Agent sessions render no workbench at all. ![Data Agent preset](assets/settings.png) - **Standard agent loop**: a data-agent session is an ordinary DSH session — standard turn/step, streaming, tool scheduling, and persistence, with zero host changes. +- **Planned, not yet supported**: FlinkSQL / SparkSQL — they are compute engines rather than database connections, and support requires a file-input channel plus engine-semantics rework; they will be evaluated in a later release. ## Quick Install @@ -56,9 +57,9 @@ Start the Web GUI: dsh --profile web ``` -In the Web GUI: create a session → choose the "Data Agent" preset → the database workbench appears above the input bar → fill in the connection info (type/host/port/user/password/database; SQLite uses a file path) → after connecting, browse schemas (single-click a database to expand tables, click a table for its structure) or run SQL directly in the command box → once the conversation starts the workbench moves to the left; in Chat ask the AI to "list all tables and count rows" or "write a SQL query for orders in the last 30 days, save it to orders.sql and run it". +In the Web GUI: create a session → choose the "Data Agent" preset → the database workbench appears above the input bar → fill in the connection info (type/host/port/user/password/database; SQLite uses a file path, SQL Server browses the schemas of the connected database, cross-database work goes through SQL) → after connecting, browse schemas (single-click a database to expand tables, click a table for its structure) or run SQL directly in the command box → once the conversation starts the workbench moves to the left; in Chat ask the AI to "list all tables and count rows" or "write a SQL query for orders in the last 30 days, save it to orders.sql and run it". -> Database client binaries: sqlite3 usually ships with macOS/Linux; mysql / psql / sqlplus / beeline / impala-shell must be provided by the deployment and can be overridden per type via the `clients` config (missing clients are named in the connect error). +> Database client binaries: sqlite3 usually ships with macOS/Linux; mysql / psql / sqlplus / beeline / impala-shell / clickhouse-client / sqlcmd must be provided by the deployment and can be overridden per type via the `clients` config (missing clients are named in the connect error). Doris reuses the mysql client (default port 9030). ## Architecture @@ -66,7 +67,7 @@ In the Web GUI: create a session → choose the "Data Agent" preset → the data Browser (apps/web) Host process (dsh --profile web) ┌─────────────────────────────┐ ┌──────────────────────────────────────┐ │ Database workbench (input.dock) │ fetch │ @yejiming/dsh-data-agent (host row)│ -│ · connection config (6 types) │ ─────▶ │ · /plugins/data-agent/* routes │ +│ · connection config (9 types) │ ─────▶ │ · /plugins/data-agent/* routes │ │ · schema explorer + SQL box │ │ · dataAgentConnections store │ │ · hero stacked / active rail │ │ · preset self-install → $DSH_HOME/ │ └─────────────────────────────┘ └──────────────┬───────────────────────┘ @@ -103,12 +104,12 @@ Every field has a loader default; there are no library-level defaults. Host row | `introspectMaxTables` | Cap on the table list returned by /connect and /status (default 500) | | `queryTimeoutMs` | Deadline for one sqlcmd query (default 30000 ms) | | `maxResultChars` | In-memory cap on captured sqlcmd output, per stream (default 20000 chars) | -| `clients` | Per-type CLI client overrides: `{ command?, args? }` for keys `mysql` / `postgres` / `sqlite` / `oracle` / `hive` / `impala` (built-in defaults mysql/psql/sqlite3/sqlplus/beeline/impala-shell) | +| `clients` | Per-type CLI client overrides: `{ command?, args? }` for keys `mysql` / `postgres` / `sqlite` / `oracle` / `hive` / `impala` / `clickhouse` / `doris` / `sqlserver` (built-in defaults mysql/psql/sqlite3/sqlplus/beeline/impala-shell/clickhouse-client/mysql/sqlcmd; the sqlserver defaults include `-C` to trust the server certificate, overridable via `args`) | | `connections` | Config-seeded connections keyed by session id (`'*'` = wildcard default for any session without its own; headless/keyless runs and deployments pinning one database). **No password field** — passwords only enter memory via the /connect route | The `tool-sqlcmd` row (inside the data-agent preset) additionally has `maxRows` (default 100, injected into the tool description as LIMIT guidance); `queryTimeoutMs` / `maxResultChars` / `clients` share the host row's names and defaults. -The `data-agent-routes` row has its own config: `connectTimeoutMs` / `introspectMaxTables` / `maxResultChars` mirror the main row; plus `queryTimeoutMs` (for /query and metadata queries, default 30000) and `maxQueryChars` (single-SQL length cap for /query, default 65536). +The `data-agent-routes` row has its own config: `connectTimeoutMs` / `introspectMaxTables` / `maxResultChars` / `readonly` / `clients` mirror the main row; plus `queryTimeoutMs` (for /query and metadata queries, default 30000) and `maxQueryChars` (single-SQL length cap for /query, default 65536). ```yaml # Example override in cordis.patch.yml or a profile layer @@ -152,8 +153,9 @@ Schema/table identifiers allow only `[A-Za-z0-9_$#.-]` (server-side whitelist; i ## Security Notes -- **Passwords**: server-side, memory only; transport per type — mysql via `MYSQL_PWD`, postgres via `PGPASSWORD` environment variables; oracle via the sqlplus `connect user/pass@...` stdin prefix, hive via the beeline `!connect` stdin prefix (never argv); impala sends no password by default (LDAP/kerberos configured through `clients`). `/status` and the public connection-store reads strip passwords. -- **Connection-config persistence**: the workbench saves the connection config (**including the password**, in plain text) to browser localStorage (key `dsh-data-agent.connection.v1`, per the confirmed local single-user scenario) after a successful connect, to restore the form and auto-reconnect once on page switches/restarts; disconnecting does not clear it. To clear: run `localStorage.removeItem('dsh-data-agent.connection.v1')` in the browser console. +- **Passwords**: server-side, memory only; transport per type — mysql/doris via `MYSQL_PWD`, postgres via `PGPASSWORD`, clickhouse via `CLICKHOUSE_PASSWORD`, sqlserver via `SQLCMDPASSWORD` environment variables; oracle via the sqlplus `connect user/pass@...` stdin prefix, hive via the beeline `!connect` stdin prefix (never argv); impala sends no password by default (LDAP/kerberos configured through `clients`). `/status` and the public connection-store reads strip passwords. +- **SQL Server certificate trust**: the default client args include `-C` (trust the server certificate) so local/self-signed instances connect out of the box; for strict verification, override `clients.sqlserver.args` with an arg set without `-C`. +- **Connection-config persistence**: the workbench saves the connection config (type/host/port/user/database) to browser localStorage (key `dsh-data-agent.connection.v1`) to restore the form and auto-reconnect once on page switches/restarts. **Passwords are not persisted by default**: they are written to localStorage only when the user checks "Remember password" (explicit opt-in, plain text, local single-user scenario). To clear: run `localStorage.removeItem('dsh-data-agent.connection.v1')` in the browser console. - **No shell layer**: `ctx.subprocess.spawn` uses argv arrays, SQL and connect prefixes travel via stdin — no shell concatenation injection surface; metadata route identifiers pass the whitelist. - **SQL execution authority**: with the approval policy set to `never`, sqlcmd and `/query` execute DDL/DML directly — connections are session-isolated; assess the data-plane risk yourself (a `readonly` mode is a later version). - **Timeouts & caps**: query timeouts, output truncation, table-list caps, and the /query SQL length are all config items — no hard-coded tunables. @@ -176,6 +178,15 @@ pnpm build # cleans and rebuilds lib/ (tsdown: lib/index.js, lib/routes.js, li pnpm test # vitest: connection store / CLI templates / sqlcmd execution (mocked subprocess) ``` +Optional real-environment integration smoke (auto-skipped unless `DSH_SMOKE_HOST` is set; creates and drops a `dsh_smoke` database/table set on each target): + +```sh +DSH_SMOKE_HOST= \ +DSH_SMOKE_CH_PASSWORD='...' DSH_SMOKE_MSSQL_PASSWORD='...' \ +PATH="/path/to/clickhouse-client:/path/to/mysql:/path/to/sqlcmd:$PATH" \ +pnpm vitest run tests/integration-smoke.spec.ts +``` + `lib/` is committed, so installing and debugging (including `dsh plugin add .`) never requires a build. To rebuild the artifacts, just run `pnpm install`: all `@deepseek-ai/*` dependencies are published on npm, so there is no need to copy diff --git a/README.md b/README.md index fe5fb8a..6444d28 100644 --- a/README.md +++ b/README.md @@ -16,17 +16,18 @@ ## 主要功能 -- **数据库连接管理**:按会话连接 MySQL / PostgreSQL / SQLite / Oracle / Hive / Impala(SQLite 走文件路径,Oracle 填服务名/SID,Hive/Impala 填默认库),连接状态驻留服务端内存,布局切换不丢;密码仅内存、经环境变量或 stdin 连接前缀传给客户端,绝不落盘。 +- **数据库连接管理**:按会话连接 MySQL / PostgreSQL / SQLite / Oracle / Hive / Impala / ClickHouse / Doris / SQL Server(SQLite 走文件路径,Oracle 填服务名/SID,Hive/Impala 填默认库,SQL Server 浏览当前库内的 schema),连接状态驻留服务端内存,布局切换不丢;密码仅内存、经环境变量或 stdin 连接前缀传给客户端,绝不落盘。 ![数据库连接](assets/connection.png) - **数据库工作台**(内嵌于会话输入框上方):连接配置卡(连接成功后折叠为摘要行,可展开查看);库表浏览(点击「库表」按钮弹出 Modal:单击库展开表列表(可滚动),点击表查看结构);SQL 命令框(编辑并运行 SQL,非 agent 通道,结果等宽展示)。连接配置持久化到浏览器 localStorage,切换页面/重启自动回填并重连。开始对话后工作台自动变为左侧栏,对话记录与输入框在右侧。 ![数据库工作台](assets/tables.png) -- **sqlcmd 工具**:在数据库客户端(mysql / psql / sqlite3 / sqlplus / beeline / impala-shell)执行 SQL/命令;无 shell 层(argv 数组化 + SQL 走 stdin),超时自动终止进程树,输出有界截断。 +- **sqlcmd 工具**:在数据库客户端(mysql / psql / sqlite3 / sqlplus / beeline / impala-shell / clickhouse-client / sqlcmd)执行 SQL/命令;无 shell 层(argv 数组化 + SQL 走 stdin),超时自动终止进程树,输出有界截断。 - **数据Agent 预设**:新建会话可选「数据Agent」——工具面恰好是 `sqlcmd`/`read`/`write`/`edit` 四个,项目其他工具(bash、grep、skill、todo、goal、web、subagent 等)全部缺席即禁用;非数据Agent 会话不渲染工作台,零影响。 ![数据Agent 预设](assets/settings.png) - **标准 agent loop**:data-agent 会话就是普通 DSH 会话,走标准 turn/step、流式输出、工具调度与持久化,零宿主改动。 +- **规划中(暂不支持)**:FlinkSQL / SparkSQL——它们是计算引擎而非数据库连接,接入需文件输入通道与引擎语义改造,将在后续版本评估。 ## 快速安装 @@ -60,9 +61,9 @@ ls $DSH_HOME/.agent-presets/data-agent/ # 应有 agent.cordis.yml + preset.yml dsh --profile web ``` -在 Web GUI 中:新建会话 → 选择「数据Agent」预设 → 输入框上方出现数据库工作台 → 填写连接信息(类型/主机/端口/用户/密码/库名;SQLite 填文件路径)→ 连接成功后浏览库表(单击库展开表、点击表看结构),或在 SQL 命令框直接运行 SQL → 开始对话后工作台移到左侧,在 Chat 让 AI「列出所有表并统计行数」或「写一条 SQL 查出近 30 天订单,保存到 orders.sql 并执行」。 +在 Web GUI 中:新建会话 → 选择「数据Agent」预设 → 输入框上方出现数据库工作台 → 填写连接信息(类型/主机/端口/用户/密码/库名;SQLite 填文件路径,SQL Server 的「库」列当前库内 schema,跨库走 SQL)→ 连接成功后浏览库表(单击库展开表、点击表看结构),或在 SQL 命令框直接运行 SQL → 开始对话后工作台移到左侧,在 Chat 让 AI「列出所有表并统计行数」或「写一条 SQL 查出近 30 天订单,保存到 orders.sql 并执行」。 -> 数据库客户端二进制要求:sqlite3 一般系统自带(macOS/Linux);mysql / psql / sqlplus / beeline / impala-shell 需部署方安装,且可在插件配置 `clients` 中覆盖命令名或绝对路径(缺失时连接报错会点名缺失的命令)。 +> 数据库客户端二进制要求:sqlite3 一般系统自带(macOS/Linux);mysql / psql / sqlplus / beeline / impala-shell / clickhouse-client / sqlcmd 需部署方安装,且可在插件配置 `clients` 中覆盖命令名或绝对路径(缺失时连接报错会点名缺失的命令)。Doris 复用 mysql 客户端(默认端口 9030)。 ## 架构 @@ -70,7 +71,7 @@ dsh --profile web 浏览器 (apps/web) 宿主进程 (dsh --profile web) ┌─────────────────────────────┐ ┌──────────────────────────────────────┐ │ 数据库工作台 (input.dock) │ fetch │ @yejiming/dsh-data-agent (宿主行) │ -│ · 连接配置 (6 类型) │ ───────▶ │ · /plugins/data-agent/* 路由 │ +│ · 连接配置 (9 类型) │ ───────▶ │ · /plugins/data-agent/* 路由 │ │ · 库表浏览 + SQL 命令框 │ │ · 连接存储服务 dataAgentConnections │ │ · hero 堆叠 / active 左栏 │ │ · 预设自安装 → $DSH_HOME/.agent-presets│ └─────────────────────────────┘ └──────────────┬───────────────────────┘ @@ -108,12 +109,12 @@ dsh --profile web | `queryTimeoutMs` | sqlcmd 单次查询超时(默认 30000 毫秒) | | `maxResultChars` | sqlcmd 捕获输出上限(stdout/stderr 各自,默认 20000 字符) | | `readonly` | 只读护栏(默认 false):true 时 `sqlcmd` 与 `/query` 仅放行读语句(SELECT/SHOW/DESCRIBE/EXPLAIN/PRAGMA 等),写语句直接拒绝 | -| `clients` | 各数据库类型 CLI 客户端覆盖:`{ command?, args? }`,键为 `mysql` / `postgres` / `sqlite` / `oracle` / `hive` / `impala`(内置默认 mysql/psql/sqlite3/sqlplus/beeline/impala-shell) | +| `clients` | 各数据库类型 CLI 客户端覆盖:`{ command?, args? }`,键为 `mysql` / `postgres` / `sqlite` / `oracle` / `hive` / `impala` / `clickhouse` / `doris` / `sqlserver`(内置默认 mysql/psql/sqlite3/sqlplus/beeline/impala-shell/clickhouse-client/mysql/sqlcmd;sqlserver 默认参数含 `-C` 信任服务器证书,可用 `args` 覆盖) | | `connections` | 配置预置连接,键为 sessionId(`'*'` = 通配符默认,任何无自有连接的会话回落它;headless/keyless 运行与部署固定默认库场景)。**不含 password 字段**——密码只允许经 /connect 路由进入内存;可选 `readonly` 字段按连接锁定只读 | 工具行 `tool-sqlcmd`(data-agent 预设内)另有 `maxRows`(默认 100,注入工具描述的 LIMIT 引导)与 `readonly`,`queryTimeoutMs` / `maxResultChars` / `clients` 与宿主行同名可配。 -路由行 `data-agent-routes` 独立配置:`connectTimeoutMs` / `introspectMaxTables` / `maxResultChars` / `readonly` 与主行同名同默认;另有 `queryTimeoutMs`(/query 与元数据查询超时,默认 30000)与 `maxQueryChars`(/query 单条 SQL 长度上限,默认 65536)。 +路由行 `data-agent-routes` 独立配置:`connectTimeoutMs` / `introspectMaxTables` / `maxResultChars` / `readonly` / `clients` 与主行同名同默认;另有 `queryTimeoutMs`(/query 与元数据查询超时,默认 30000)与 `maxQueryChars`(/query 单条 SQL 长度上限,默认 65536)。 ```yaml # cordis.patch.yml 或 profile 层覆盖示例 @@ -156,10 +157,11 @@ schema/table 标识符仅允许 `[A-Za-z0-9_$]`(服务端白名单校验并转 ## 安全说明 -- **密码**:服务端仅存内存,传递通道按类型:mysql 经 `MYSQL_PWD`、postgres 经 `PGPASSWORD` 环境变量;oracle 经 sqlplus `connect user/pass@...` stdin 前缀、hive 经 beeline `!connect` stdin 前缀(均不进 argv);impala 默认不传密码(LDAP/kerberos 由部署侧 `clients` 覆盖)。`/status` 与连接存储的公开读取面均剥离密码。 +- **密码**:服务端仅存内存,传递通道按类型:mysql/doris 经 `MYSQL_PWD`、postgres 经 `PGPASSWORD`、clickhouse 经 `CLICKHOUSE_PASSWORD`、sqlserver 经 `SQLCMDPASSWORD` 环境变量;oracle 经 sqlplus `connect user/pass@...` stdin 前缀、hive 经 beeline `!connect` stdin 前缀(均不进 argv);impala 默认不传密码(LDAP/kerberos 由部署侧 `clients` 覆盖)。`/status` 与连接存储的公开读取面均剥离密码。 - **连接配置持久化**:工作台把连接配置(type/host/port/user/database)保存到浏览器 localStorage(键 `dsh-data-agent.connection.v1`),用于切换页面/重启后回填表单并自动重连一次。**密码默认不落盘**:仅当用户勾选「记住密码」时才持久化密码(明文 localStorage,本机单用户场景的显式 opt-in)。若需清除:浏览器控制台执行 `localStorage.removeItem('dsh-data-agent.connection.v1')`。 - **无 shell 层**:`ctx.subprocess.spawn` 参数数组化,SQL 与连接前缀经 stdin 传入,不存在 shell 拼接注入面;元数据路由的 schema/table 标识符经收紧白名单 `[A-Za-z0-9_$]` 校验并按类型转义引用(反引号/双引号),拒绝 `#`、`--`、`;`、`'` 等注入形字符。 - **SQL 执行权**:审批策略为 never 时,sqlcmd 与 `/query` 的 DDL/DML 会直接执行——连接按 session 隔离,请自行评估数据面风险。可设 `readonly: true`(宿主/工具/路由三行同名,或 `/connect` 传 `readonly: true` 按连接锁定)强制只放行读语句(SELECT/SHOW/DESCRIBE/EXPLAIN/PRAGMA 等),作为误操作防护;对更强对手防护,仍建议配合数据库侧只读账号。 +- **SQL Server 证书信任**:默认客户端参数含 `-C`(信任服务器证书),便于连接本地/自签实例;如需严格校验,在 `clients.sqlserver.args` 中覆盖为不含 `-C` 的参数集。 - **超时与上限**:查询超时、输出截断、表清单上限、/query 单条 SQL 长度均为配置项,无硬编码 tunables。 ## 卸载与回滚 @@ -180,6 +182,15 @@ pnpm build # 清空并重建 lib/(tsdown:lib/index.js、lib/routes.js、li pnpm test # vitest:连接存储 / CLI 模板 / sqlcmd 执行(mock subprocess) ``` +可选的真实环境集成冒烟(未设置 `DSH_SMOKE_HOST` 时自动跳过;会在目标服务上临时创建并清理 `dsh_smoke` 库表): + +```sh +DSH_SMOKE_HOST= \ +DSH_SMOKE_CH_PASSWORD='...' DSH_SMOKE_MSSQL_PASSWORD='...' \ +PATH="/path/to/clickhouse-client:/path/to/mysql:/path/to/sqlcmd:$PATH" \ +pnpm vitest run tests/integration-smoke.spec.ts +``` + `lib/` 已提交进仓库,安装与调试(含 `dsh plugin add .`)都不需要先构建。重新 构建产物时直接 `pnpm install` 即可:`@deepseek-ai/*` 等依赖均已发布到 npm, 无需再从本地 DSH checkout 复制/链接 node_modules。`pnpm-workspace.yaml` 采用 diff --git a/lib/client.js b/lib/client.js index 18cc428..19c13db 100644 --- a/lib/client.js +++ b/lib/client.js @@ -26,7 +26,7 @@ window.__ModuleLoader__.load({ if (value === null || typeof value !== "object" || Array.isArray(value)) return null; const candidate = value; const type = candidate.type; - if (type !== "mysql" && type !== "postgres" && type !== "sqlite" && type !== "oracle" && type !== "hive" && type !== "impala") return null; + if (type !== "mysql" && type !== "postgres" && type !== "sqlite" && type !== "oracle" && type !== "hive" && type !== "impala" && type !== "clickhouse" && type !== "doris" && type !== "sqlserver") return null; const database = candidate.database; if (typeof database !== "string") return null; const saved = { @@ -63,8 +63,8 @@ window.__ModuleLoader__.load({ } } //#endregion - //#region \0dsh-css:/Users/yejiming/Desktop/OpenSource/dsh-data-agent/src/client/DataAgentWorkbench.module.css.mjs - const css = ".Sv3uVW_workbench{box-sizing:border-box;font-family:var(--dsw-font-family,-apple-system, BlinkMacSystemFont, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif);color:var(--dsw-alias-label-primary,inherit);flex-direction:column;gap:10px;font-size:13px;line-height:1.5;display:flex}.Sv3uVW_strip{width:100%}.Sv3uVW_docked{width:100%;max-height:55vh;overflow-y:auto}.Sv3uVW_rail{z-index:8;border-right:1px solid var(--dsw-alias-border-l2,#80808059);background:var(--dsw-specific-sidebar-fill,var(--dsw-alias-bg-layer-1,#ffffff08));padding:12px;overflow-y:auto}html.Sv3uVW_da-split [data-phase=active] [data-conversation-scroll]>:first-child{margin-left:380px}html.Sv3uVW_da-split [data-phase=active] [data-composer-seat]{padding-left:380px}.Sv3uVW_sections{flex-direction:column;gap:12px;display:flex}.Sv3uVW_card{border:1px solid var(--dsw-alias-border-l2,#80808059);border-radius:12px;flex-direction:column;gap:10px;padding:12px 14px;display:flex}.Sv3uVW_cardTitle{text-transform:uppercase;letter-spacing:.06em;color:var(--dsw-alias-label-tertiary,#808080b3);align-items:center;gap:6px;font-size:11px;font-weight:600;display:flex}.Sv3uVW_titleIcon{width:14px;height:14px;color:var(--dsw-alias-label-secondary,#808080cc);flex:none}.Sv3uVW_fieldGrid{flex-direction:column;gap:10px;display:flex}.Sv3uVW_fieldRow2{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:10px;display:grid}.Sv3uVW_field{flex-direction:column;gap:4px;min-width:0;display:flex}.Sv3uVW_label{color:var(--dsw-alias-label-secondary,#808080b3);font-size:11px;font-weight:500}.Sv3uVW_rememberRow{color:var(--dsw-alias-label-primary,inherit);cursor:pointer;user-select:none;align-items:center;gap:6px;font-size:12px;display:flex}.Sv3uVW_rememberRow input[type=checkbox]{width:14px;height:14px;accent-color:var(--dsw-alias-button-info-fill,#3b82f6);cursor:pointer;flex:none;margin:0}.Sv3uVW_rememberHint{color:var(--dsw-alias-label-caption,#80808080);font-size:11px}.Sv3uVW_input{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,#80808059);background:var(--dsw-alias-bg-layer-2,#ffffff0a);width:100%;height:32px;color:var(--dsw-alias-label-primary,inherit);font:inherit;border-radius:8px;padding:0 10px;font-size:13px;transition:border-color .15s ease-out,box-shadow .15s ease-out}.Sv3uVW_input::placeholder{color:var(--dsw-alias-label-caption,#80808080)}.Sv3uVW_input:focus-visible,.Sv3uVW_input:focus{border-color:var(--dsw-alias-button-info-fill,#3b82f6);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-button-info-fill,#3b82f6) 18%, transparent);outline:none}.Sv3uVW_input:disabled{opacity:.55}select.Sv3uVW_input{-webkit-appearance:none;appearance:none;cursor:pointer;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' fill='none' stroke='%23777' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");background-position:right 10px center;background-repeat:no-repeat;padding-right:28px}.Sv3uVW_actions{align-items:center;gap:8px;margin-top:2px;display:flex}.Sv3uVW_actions>.Sv3uVW_primary,.Sv3uVW_actions>.Sv3uVW_ghost{flex:1}.Sv3uVW_primary,.Sv3uVW_ghost{height:32px;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;justify-content:center;align-items:center;gap:6px;padding:0 14px;font-size:13px;font-weight:500;transition:background-color .15s ease-out,border-color .15s ease-out,opacity .15s ease-out;display:inline-flex}.Sv3uVW_primary{background:var(--dsw-alias-button-info-fill,#3b82f6);color:#fff}.Sv3uVW_primary:hover:not(:disabled){background:var(--dsw-alias-button-info-hover,#3573e0)}.Sv3uVW_primary:active:not(:disabled){background:var(--dsw-static-deepseek-600,#2f5fc0)}.Sv3uVW_ghost{border-color:var(--dsw-alias-border-l2,#80808059);color:var(--dsw-alias-label-primary,inherit);background:0 0}.Sv3uVW_ghost:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#80808014)}.Sv3uVW_ghost:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active,#80808024)}.Sv3uVW_primary:focus-visible,.Sv3uVW_ghost:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-button-info-fill,#3b82f6) 25%, transparent);outline:none}.Sv3uVW_primary:disabled,.Sv3uVW_ghost:disabled{opacity:.45;cursor:default}.Sv3uVW_small{border-radius:6px;height:24px;padding:0 10px;font-size:12px}.Sv3uVW_hint{color:var(--dsw-alias-label-tertiary,#80808099);font-size:12px}.Sv3uVW_summaryRow{align-items:center;gap:8px;min-height:28px;display:flex}.Sv3uVW_statusOk{color:var(--dsw-alias-state-success-primary,#22c55e);flex:none;align-items:center;gap:5px;font-size:12px;font-weight:500;display:inline-flex}.Sv3uVW_summaryType{border:1px solid var(--dsw-alias-border-l2,#80808059);background:var(--dsw-alias-bg-layer-2,#ffffff0a);font-family:var(--ds-font-family-code,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);color:var(--dsw-alias-label-secondary,#808080cc);border-radius:999px;flex:none;padding:2px 8px;font-size:11px}.Sv3uVW_summaryDb{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-family:var(--ds-font-family-code,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);flex:1;font-size:12px;overflow:hidden}.Sv3uVW_summaryActions{flex:none;gap:6px;display:flex}.Sv3uVW_heroConnect{border:1px solid var(--dsw-alias-border-l2,#80808059);background:var(--dsw-alias-bg-layer-2,#ffffff0a);width:100%;color:var(--dsw-alias-label-primary,inherit);font:inherit;text-align:left;cursor:pointer;border-radius:10px;align-items:center;gap:12px;padding:14px 16px;transition:background-color .15s ease-out,border-color .15s ease-out;display:flex}.Sv3uVW_heroConnect:hover{background:var(--dsw-alias-interactive-bg-hover,#80808014);border-color:var(--dsw-alias-border-l3,#80808080)}.Sv3uVW_heroConnect:focus-visible{border-color:var(--dsw-alias-button-info-fill,#3b82f6);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-button-info-fill,#3b82f6) 18%, transparent);outline:none}.Sv3uVW_heroIcon{width:22px;height:22px;color:var(--dsw-alias-button-info-fill,#3b82f6);flex:none}.Sv3uVW_heroText{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.Sv3uVW_heroTitle{color:var(--dsw-alias-label-primary,inherit);font-size:13px;font-weight:600}.Sv3uVW_heroHint{color:var(--dsw-alias-label-caption,#80808080);font-size:11.5px;line-height:1.5}.Sv3uVW_heroChevron{width:12px;height:12px;color:var(--dsw-alias-label-tertiary,#80808099);flex:none}.Sv3uVW_browseRow{border:1px solid var(--dsw-alias-border-l2,#80808059);background:var(--dsw-alias-bg-layer-2,#ffffff0a);width:100%;height:36px;color:var(--dsw-alias-label-primary,inherit);font:inherit;text-align:left;cursor:pointer;border-radius:8px;align-items:center;gap:8px;padding:0 12px;font-size:13px;font-weight:500;transition:background-color .15s ease-out,border-color .15s ease-out;display:flex}.Sv3uVW_browseRow:hover{background:var(--dsw-alias-interactive-bg-hover,#80808014);border-color:var(--dsw-alias-border-l3,#80808080)}.Sv3uVW_browseRow:focus-visible{border-color:var(--dsw-alias-button-info-fill,#3b82f6);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-button-info-fill,#3b82f6) 18%, transparent);outline:none}.Sv3uVW_browseIcon{width:14px;height:14px;color:var(--dsw-alias-button-info-fill,#3b82f6);flex:none}.Sv3uVW_browseChevron{width:12px;height:12px;color:var(--dsw-alias-label-tertiary,#80808099);flex:none;margin-left:auto}.Sv3uVW_sqlInput{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,#80808059);background:var(--dsw-alias-bg-layer-2,#ffffff0a);width:100%;min-height:190px;color:var(--dsw-alias-label-primary,inherit);font-family:var(--ds-font-family-code,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);resize:vertical;border-radius:10px;padding:10px 12px;font-size:12.5px;line-height:1.6;transition:border-color .15s ease-out,box-shadow .15s ease-out}.Sv3uVW_sqlInput::placeholder{color:var(--dsw-alias-label-caption,#80808080)}.Sv3uVW_sqlInput:focus-visible,.Sv3uVW_sqlInput:focus{border-color:var(--dsw-alias-button-info-fill,#3b82f6);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-button-info-fill,#3b82f6) 18%, transparent);outline:none}.Sv3uVW_sqlInput:disabled{opacity:.55}.Sv3uVW_sqlActions{justify-content:flex-end;align-items:center;gap:10px;display:flex}.Sv3uVW_sqlActions .Sv3uVW_primary{flex:none;min-width:92px}.Sv3uVW_shortcutHint{color:var(--dsw-alias-label-caption,#80808080);font-size:11.5px}.Sv3uVW_sqlResult{border:1px solid var(--dsw-alias-border-l1,#80808033);background:var(--dsw-alias-markdown-code-block,#ffffff0a);font-family:var(--ds-font-family-code,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);white-space:pre-wrap;word-break:break-word;border-radius:8px;max-height:200px;margin:0;padding:10px 12px;font-size:11.5px;line-height:1.55;overflow-y:auto}.Sv3uVW_errorBar{border:1px solid var(--dsw-alias-state-error-secondary,#ef444480);background:var(--dsw-alias-interactive-bg-hover-danger,#ef444414);border-radius:10px;flex-direction:column;gap:3px;padding:10px 12px;display:flex}.Sv3uVW_errorHead{color:var(--dsw-alias-state-error-primary,#ef4444);align-items:center;gap:6px;font-size:12px;font-weight:600;display:flex}.Sv3uVW_errorIcon{flex:none;width:14px;height:14px}.Sv3uVW_errorText{white-space:pre-wrap;word-break:break-word;font-size:12px}.Sv3uVW_connectModal{width:min(440px,92vw)}.Sv3uVW_connectModalBody{flex-direction:column;gap:14px;display:flex}.Sv3uVW_schemaModal{width:min(900px,94vw)}.Sv3uVW_modalBody{align-items:stretch;gap:20px;height:min(540px,58vh);display:flex}.Sv3uVW_modalCol{flex-direction:column;flex:1;gap:8px;min-width:0;min-height:0;display:flex}.Sv3uVW_modalColTitle{text-transform:uppercase;letter-spacing:.06em;color:var(--dsw-alias-label-tertiary,#808080b3);flex:none;align-items:center;gap:6px;font-size:11px;font-weight:600;display:flex}.Sv3uVW_colCount{border:1px solid var(--dsw-alias-border-l1,#80808033);background:var(--dsw-alias-bg-layer-2,#ffffff0a);color:var(--dsw-alias-label-secondary,#808080cc);border-radius:999px;flex:none;padding:1px 7px;font-size:10px;font-weight:500}.Sv3uVW_treeScroll{flex:1;min-height:0;padding-right:4px;overflow-y:auto}.Sv3uVW_tree{flex-direction:column;gap:1px;margin:0;padding:0;list-style:none;display:flex}.Sv3uVW_treeNode{flex-direction:column;display:flex}.Sv3uVW_treeItem{width:100%;min-height:28px;color:var(--dsw-alias-label-primary,inherit);font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:7px;align-items:center;gap:6px;padding:0 8px;font-size:12.5px;display:flex}.Sv3uVW_treeItem:hover{background:var(--dsw-alias-interactive-bg-hover,#80808014)}.Sv3uVW_treeItem.Sv3uVW_active,.Sv3uVW_treeItem.Sv3uVW_active:hover{background:var(--dsw-alias-interactive-bg-active,#3b82f62e)}.Sv3uVW_treeChevron{width:12px;height:12px;color:var(--dsw-alias-label-tertiary,#80808099);flex:none;transition:transform .15s ease-out}.Sv3uVW_treeChevron.Sv3uVW_open{transform:rotate(90deg)}.Sv3uVW_treeIcon{width:14px;height:14px;color:var(--dsw-alias-label-secondary,#808080cc);flex:none}.Sv3uVW_treeItem.Sv3uVW_active .Sv3uVW_treeIcon{color:var(--dsw-alias-button-info-fill,#3b82f6)}.Sv3uVW_treeName{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-family:var(--ds-font-family-code,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);flex:1;font-size:12px;overflow:hidden}.Sv3uVW_treeCount{border:1px solid var(--dsw-alias-border-l1,#80808033);background:var(--dsw-alias-bg-layer-2,#ffffff0a);color:var(--dsw-alias-label-secondary,#808080cc);border-radius:999px;flex:none;padding:0 6px;font-size:10px}.Sv3uVW_treeChildren{border-left:1px solid var(--dsw-alias-border-l2,#80808059);flex-direction:column;gap:1px;margin-left:12px;padding-left:10px;display:flex}.Sv3uVW_colScroll{border:1px solid var(--dsw-alias-border-l1,#80808033);border-radius:8px;flex:1;min-height:0;overflow:auto}.Sv3uVW_columnsTable{border-collapse:collapse;width:100%;font-family:var(--ds-font-family-code,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);font-size:12px}.Sv3uVW_columnsTable th{z-index:1;text-align:left;text-transform:uppercase;letter-spacing:.05em;color:var(--dsw-alias-label-tertiary,#808080b3);background:var(--dsw-alias-bg-layer-2,#ffffff0a);border-bottom:1px solid var(--dsw-alias-border-l2,#80808059);padding:7px 10px;font-size:10.5px;font-weight:600;position:sticky;top:0}.Sv3uVW_columnsTable td{border-bottom:1px solid var(--dsw-alias-border-l1,#80808033);color:var(--dsw-alias-label-primary,inherit);padding:5px 10px}.Sv3uVW_columnsTable tbody tr:hover td{background:var(--dsw-alias-interactive-bg-hover,#80808014)}.Sv3uVW_typeCell{color:var(--dsw-alias-label-secondary,#808080cc)}.Sv3uVW_nullCell{color:var(--dsw-alias-label-caption,#80808080);font-size:11px}.Sv3uVW_emptyState{border:1px dashed var(--dsw-alias-border-l2,#80808059);min-height:0;color:var(--dsw-alias-label-tertiary,#808080b3);border-radius:8px;flex-direction:column;flex:1;justify-content:center;align-items:center;gap:8px;display:flex}.Sv3uVW_emptyStateIcon{width:20px;height:20px;color:var(--dsw-alias-label-dimmed,#80808066)}.Sv3uVW_emptyStateText{font-size:12px}.Sv3uVW_modalHint{color:var(--dsw-alias-label-caption,#80808080);flex:none;align-items:center;gap:6px;margin-top:2px;font-size:11.5px;display:flex}@media (width<=640px){.Sv3uVW_modalBody{flex-direction:column;height:auto}.Sv3uVW_treeScroll{flex:none;height:300px}.Sv3uVW_colScroll{flex:none;height:240px}.Sv3uVW_emptyState{flex:none;height:120px}}"; + //#region \0dsh-css:/Users/q/code/dsh-data-agent/src/client/DataAgentWorkbench.module.css.mjs + const css = ".IfK3TW_workbench{box-sizing:border-box;font-family:var(--dsw-font-family,-apple-system, BlinkMacSystemFont, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif);color:var(--dsw-alias-label-primary,inherit);flex-direction:column;gap:10px;font-size:13px;line-height:1.5;display:flex}.IfK3TW_strip{width:100%}.IfK3TW_docked{width:100%;max-height:55vh;overflow-y:auto}.IfK3TW_rail{z-index:8;border-right:1px solid var(--dsw-alias-border-l2,#80808059);background:var(--dsw-specific-sidebar-fill,var(--dsw-alias-bg-layer-1,#ffffff08));padding:12px;overflow-y:auto}html.IfK3TW_da-split [data-phase=active] [data-conversation-scroll]>:first-child{margin-left:380px}html.IfK3TW_da-split [data-phase=active] [data-composer-seat]{padding-left:380px}.IfK3TW_sections{flex-direction:column;gap:12px;display:flex}.IfK3TW_card{border:1px solid var(--dsw-alias-border-l2,#80808059);border-radius:12px;flex-direction:column;gap:10px;padding:12px 14px;display:flex}.IfK3TW_cardTitle{text-transform:uppercase;letter-spacing:.06em;color:var(--dsw-alias-label-tertiary,#808080b3);align-items:center;gap:6px;font-size:11px;font-weight:600;display:flex}.IfK3TW_titleIcon{width:14px;height:14px;color:var(--dsw-alias-label-secondary,#808080cc);flex:none}.IfK3TW_fieldGrid{flex-direction:column;gap:10px;display:flex}.IfK3TW_fieldRow2{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:10px;display:grid}.IfK3TW_field{flex-direction:column;gap:4px;min-width:0;display:flex}.IfK3TW_label{color:var(--dsw-alias-label-secondary,#808080b3);font-size:11px;font-weight:500}.IfK3TW_rememberRow{color:var(--dsw-alias-label-primary,inherit);cursor:pointer;user-select:none;align-items:center;gap:6px;font-size:12px;display:flex}.IfK3TW_rememberRow input[type=checkbox]{width:14px;height:14px;accent-color:var(--dsw-alias-button-info-fill,#3b82f6);cursor:pointer;flex:none;margin:0}.IfK3TW_rememberHint{color:var(--dsw-alias-label-caption,#80808080);font-size:11px}.IfK3TW_input{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,#80808059);background:var(--dsw-alias-bg-layer-2,#ffffff0a);width:100%;height:32px;color:var(--dsw-alias-label-primary,inherit);font:inherit;border-radius:8px;padding:0 10px;font-size:13px;transition:border-color .15s ease-out,box-shadow .15s ease-out}.IfK3TW_input::placeholder{color:var(--dsw-alias-label-caption,#80808080)}.IfK3TW_input:focus-visible,.IfK3TW_input:focus{border-color:var(--dsw-alias-button-info-fill,#3b82f6);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-button-info-fill,#3b82f6) 18%, transparent);outline:none}.IfK3TW_input:disabled{opacity:.55}select.IfK3TW_input{-webkit-appearance:none;appearance:none;cursor:pointer;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' fill='none' stroke='%23777' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");background-position:right 10px center;background-repeat:no-repeat;padding-right:28px}.IfK3TW_actions{align-items:center;gap:8px;margin-top:2px;display:flex}.IfK3TW_actions>.IfK3TW_primary,.IfK3TW_actions>.IfK3TW_ghost{flex:1}.IfK3TW_primary,.IfK3TW_ghost{height:32px;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;justify-content:center;align-items:center;gap:6px;padding:0 14px;font-size:13px;font-weight:500;transition:background-color .15s ease-out,border-color .15s ease-out,opacity .15s ease-out;display:inline-flex}.IfK3TW_primary{background:var(--dsw-alias-button-info-fill,#3b82f6);color:#fff}.IfK3TW_primary:hover:not(:disabled){background:var(--dsw-alias-button-info-hover,#3573e0)}.IfK3TW_primary:active:not(:disabled){background:var(--dsw-static-deepseek-600,#2f5fc0)}.IfK3TW_ghost{border-color:var(--dsw-alias-border-l2,#80808059);color:var(--dsw-alias-label-primary,inherit);background:0 0}.IfK3TW_ghost:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#80808014)}.IfK3TW_ghost:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active,#80808024)}.IfK3TW_primary:focus-visible,.IfK3TW_ghost:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-button-info-fill,#3b82f6) 25%, transparent);outline:none}.IfK3TW_primary:disabled,.IfK3TW_ghost:disabled{opacity:.45;cursor:default}.IfK3TW_small{border-radius:6px;height:24px;padding:0 10px;font-size:12px}.IfK3TW_hint{color:var(--dsw-alias-label-tertiary,#80808099);font-size:12px}.IfK3TW_summaryRow{align-items:center;gap:8px;min-height:28px;display:flex}.IfK3TW_statusOk{color:var(--dsw-alias-state-success-primary,#22c55e);flex:none;align-items:center;gap:5px;font-size:12px;font-weight:500;display:inline-flex}.IfK3TW_summaryType{border:1px solid var(--dsw-alias-border-l2,#80808059);background:var(--dsw-alias-bg-layer-2,#ffffff0a);font-family:var(--ds-font-family-code,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);color:var(--dsw-alias-label-secondary,#808080cc);border-radius:999px;flex:none;padding:2px 8px;font-size:11px}.IfK3TW_summaryDb{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-family:var(--ds-font-family-code,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);flex:1;font-size:12px;overflow:hidden}.IfK3TW_summaryActions{flex:none;gap:6px;display:flex}.IfK3TW_heroConnect{border:1px solid var(--dsw-alias-border-l2,#80808059);background:var(--dsw-alias-bg-layer-2,#ffffff0a);width:100%;color:var(--dsw-alias-label-primary,inherit);font:inherit;text-align:left;cursor:pointer;border-radius:10px;align-items:center;gap:12px;padding:14px 16px;transition:background-color .15s ease-out,border-color .15s ease-out;display:flex}.IfK3TW_heroConnect:hover{background:var(--dsw-alias-interactive-bg-hover,#80808014);border-color:var(--dsw-alias-border-l3,#80808080)}.IfK3TW_heroConnect:focus-visible{border-color:var(--dsw-alias-button-info-fill,#3b82f6);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-button-info-fill,#3b82f6) 18%, transparent);outline:none}.IfK3TW_heroIcon{width:22px;height:22px;color:var(--dsw-alias-button-info-fill,#3b82f6);flex:none}.IfK3TW_heroText{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.IfK3TW_heroTitle{color:var(--dsw-alias-label-primary,inherit);font-size:13px;font-weight:600}.IfK3TW_heroHint{color:var(--dsw-alias-label-caption,#80808080);font-size:11.5px;line-height:1.5}.IfK3TW_heroChevron{width:12px;height:12px;color:var(--dsw-alias-label-tertiary,#80808099);flex:none}.IfK3TW_browseRow{border:1px solid var(--dsw-alias-border-l2,#80808059);background:var(--dsw-alias-bg-layer-2,#ffffff0a);width:100%;height:36px;color:var(--dsw-alias-label-primary,inherit);font:inherit;text-align:left;cursor:pointer;border-radius:8px;align-items:center;gap:8px;padding:0 12px;font-size:13px;font-weight:500;transition:background-color .15s ease-out,border-color .15s ease-out;display:flex}.IfK3TW_browseRow:hover{background:var(--dsw-alias-interactive-bg-hover,#80808014);border-color:var(--dsw-alias-border-l3,#80808080)}.IfK3TW_browseRow:focus-visible{border-color:var(--dsw-alias-button-info-fill,#3b82f6);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-button-info-fill,#3b82f6) 18%, transparent);outline:none}.IfK3TW_browseIcon{width:14px;height:14px;color:var(--dsw-alias-button-info-fill,#3b82f6);flex:none}.IfK3TW_browseChevron{width:12px;height:12px;color:var(--dsw-alias-label-tertiary,#80808099);flex:none;margin-left:auto}.IfK3TW_sqlInput{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,#80808059);background:var(--dsw-alias-bg-layer-2,#ffffff0a);width:100%;min-height:190px;color:var(--dsw-alias-label-primary,inherit);font-family:var(--ds-font-family-code,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);resize:vertical;border-radius:10px;padding:10px 12px;font-size:12.5px;line-height:1.6;transition:border-color .15s ease-out,box-shadow .15s ease-out}.IfK3TW_sqlInput::placeholder{color:var(--dsw-alias-label-caption,#80808080)}.IfK3TW_sqlInput:focus-visible,.IfK3TW_sqlInput:focus{border-color:var(--dsw-alias-button-info-fill,#3b82f6);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-button-info-fill,#3b82f6) 18%, transparent);outline:none}.IfK3TW_sqlInput:disabled{opacity:.55}.IfK3TW_sqlActions{justify-content:flex-end;align-items:center;gap:10px;display:flex}.IfK3TW_sqlActions .IfK3TW_primary{flex:none;min-width:92px}.IfK3TW_shortcutHint{color:var(--dsw-alias-label-caption,#80808080);font-size:11.5px}.IfK3TW_sqlResult{border:1px solid var(--dsw-alias-border-l1,#80808033);background:var(--dsw-alias-markdown-code-block,#ffffff0a);font-family:var(--ds-font-family-code,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);white-space:pre-wrap;word-break:break-word;border-radius:8px;max-height:200px;margin:0;padding:10px 12px;font-size:11.5px;line-height:1.55;overflow-y:auto}.IfK3TW_errorBar{border:1px solid var(--dsw-alias-state-error-secondary,#ef444480);background:var(--dsw-alias-interactive-bg-hover-danger,#ef444414);border-radius:10px;flex-direction:column;gap:3px;padding:10px 12px;display:flex}.IfK3TW_errorHead{color:var(--dsw-alias-state-error-primary,#ef4444);align-items:center;gap:6px;font-size:12px;font-weight:600;display:flex}.IfK3TW_errorIcon{flex:none;width:14px;height:14px}.IfK3TW_errorText{white-space:pre-wrap;word-break:break-word;font-size:12px}.IfK3TW_connectModal{width:min(440px,92vw)}.IfK3TW_connectModalBody{flex-direction:column;gap:14px;display:flex}.IfK3TW_schemaModal{width:min(900px,94vw)}.IfK3TW_modalBody{align-items:stretch;gap:20px;height:min(540px,58vh);display:flex}.IfK3TW_modalCol{flex-direction:column;flex:1;gap:8px;min-width:0;min-height:0;display:flex}.IfK3TW_modalColTitle{text-transform:uppercase;letter-spacing:.06em;color:var(--dsw-alias-label-tertiary,#808080b3);flex:none;align-items:center;gap:6px;font-size:11px;font-weight:600;display:flex}.IfK3TW_colCount{border:1px solid var(--dsw-alias-border-l1,#80808033);background:var(--dsw-alias-bg-layer-2,#ffffff0a);color:var(--dsw-alias-label-secondary,#808080cc);border-radius:999px;flex:none;padding:1px 7px;font-size:10px;font-weight:500}.IfK3TW_treeScroll{flex:1;min-height:0;padding-right:4px;overflow-y:auto}.IfK3TW_tree{flex-direction:column;gap:1px;margin:0;padding:0;list-style:none;display:flex}.IfK3TW_treeNode{flex-direction:column;display:flex}.IfK3TW_treeItem{width:100%;min-height:28px;color:var(--dsw-alias-label-primary,inherit);font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:7px;align-items:center;gap:6px;padding:0 8px;font-size:12.5px;display:flex}.IfK3TW_treeItem:hover{background:var(--dsw-alias-interactive-bg-hover,#80808014)}.IfK3TW_treeItem.IfK3TW_active,.IfK3TW_treeItem.IfK3TW_active:hover{background:var(--dsw-alias-interactive-bg-active,#3b82f62e)}.IfK3TW_treeChevron{width:12px;height:12px;color:var(--dsw-alias-label-tertiary,#80808099);flex:none;transition:transform .15s ease-out}.IfK3TW_treeChevron.IfK3TW_open{transform:rotate(90deg)}.IfK3TW_treeIcon{width:14px;height:14px;color:var(--dsw-alias-label-secondary,#808080cc);flex:none}.IfK3TW_treeItem.IfK3TW_active .IfK3TW_treeIcon{color:var(--dsw-alias-button-info-fill,#3b82f6)}.IfK3TW_treeName{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-family:var(--ds-font-family-code,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);flex:1;font-size:12px;overflow:hidden}.IfK3TW_treeCount{border:1px solid var(--dsw-alias-border-l1,#80808033);background:var(--dsw-alias-bg-layer-2,#ffffff0a);color:var(--dsw-alias-label-secondary,#808080cc);border-radius:999px;flex:none;padding:0 6px;font-size:10px}.IfK3TW_treeChildren{border-left:1px solid var(--dsw-alias-border-l2,#80808059);flex-direction:column;gap:1px;margin-left:12px;padding-left:10px;display:flex}.IfK3TW_colScroll{border:1px solid var(--dsw-alias-border-l1,#80808033);border-radius:8px;flex:1;min-height:0;overflow:auto}.IfK3TW_columnsTable{border-collapse:collapse;width:100%;font-family:var(--ds-font-family-code,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);font-size:12px}.IfK3TW_columnsTable th{z-index:1;text-align:left;text-transform:uppercase;letter-spacing:.05em;color:var(--dsw-alias-label-tertiary,#808080b3);background:var(--dsw-alias-bg-layer-2,#ffffff0a);border-bottom:1px solid var(--dsw-alias-border-l2,#80808059);padding:7px 10px;font-size:10.5px;font-weight:600;position:sticky;top:0}.IfK3TW_columnsTable td{border-bottom:1px solid var(--dsw-alias-border-l1,#80808033);color:var(--dsw-alias-label-primary,inherit);padding:5px 10px}.IfK3TW_columnsTable tbody tr:hover td{background:var(--dsw-alias-interactive-bg-hover,#80808014)}.IfK3TW_typeCell{color:var(--dsw-alias-label-secondary,#808080cc)}.IfK3TW_nullCell{color:var(--dsw-alias-label-caption,#80808080);font-size:11px}.IfK3TW_emptyState{border:1px dashed var(--dsw-alias-border-l2,#80808059);min-height:0;color:var(--dsw-alias-label-tertiary,#808080b3);border-radius:8px;flex-direction:column;flex:1;justify-content:center;align-items:center;gap:8px;display:flex}.IfK3TW_emptyStateIcon{width:20px;height:20px;color:var(--dsw-alias-label-dimmed,#80808066)}.IfK3TW_emptyStateText{font-size:12px}.IfK3TW_modalHint{color:var(--dsw-alias-label-caption,#80808080);flex:none;align-items:center;gap:6px;margin-top:2px;font-size:11.5px;display:flex}@media (width<=640px){.IfK3TW_modalBody{flex-direction:column;height:auto}.IfK3TW_treeScroll{flex:none;height:300px}.IfK3TW_colScroll{flex:none;height:240px}.IfK3TW_emptyState{flex:none;height:120px}}"; const tagId = "@yejiming/dsh-data-agent/DataAgentWorkbench.module.css"; if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) { const tag = document.createElement("style"); @@ -74,75 +74,75 @@ window.__ModuleLoader__.load({ document.head.appendChild(tag); } var DataAgentWorkbench_module_css_default = { - "input": "Sv3uVW_input", - "emptyStateIcon": "Sv3uVW_emptyStateIcon", - "field": "Sv3uVW_field", - "browseRow": "Sv3uVW_browseRow", - "treeScroll": "Sv3uVW_treeScroll", - "treeIcon": "Sv3uVW_treeIcon", - "summaryRow": "Sv3uVW_summaryRow", - "heroHint": "Sv3uVW_heroHint", - "fieldGrid": "Sv3uVW_fieldGrid", - "heroChevron": "Sv3uVW_heroChevron", - "browseIcon": "Sv3uVW_browseIcon", - "sqlActions": "Sv3uVW_sqlActions", - "errorHead": "Sv3uVW_errorHead", - "modalCol": "Sv3uVW_modalCol", - "colScroll": "Sv3uVW_colScroll", - "heroConnect": "Sv3uVW_heroConnect", - "connectModalBody": "Sv3uVW_connectModalBody", - "strip": "Sv3uVW_strip", - "sqlInput": "Sv3uVW_sqlInput", - "rememberHint": "Sv3uVW_rememberHint", - "fieldRow2": "Sv3uVW_fieldRow2", - "rememberRow": "Sv3uVW_rememberRow", - "statusOk": "Sv3uVW_statusOk", - "typeCell": "Sv3uVW_typeCell", - "summaryDb": "Sv3uVW_summaryDb", - "rail": "Sv3uVW_rail", - "da-split": "Sv3uVW_da-split", - "colCount": "Sv3uVW_colCount", - "errorBar": "Sv3uVW_errorBar", - "treeItem": "Sv3uVW_treeItem", - "sections": "Sv3uVW_sections", - "cardTitle": "Sv3uVW_cardTitle", - "errorText": "Sv3uVW_errorText", - "docked": "Sv3uVW_docked", - "heroIcon": "Sv3uVW_heroIcon", - "primary": "Sv3uVW_primary", - "card": "Sv3uVW_card", - "ghost": "Sv3uVW_ghost", - "tree": "Sv3uVW_tree", - "titleIcon": "Sv3uVW_titleIcon", - "small": "Sv3uVW_small", - "actions": "Sv3uVW_actions", - "emptyStateText": "Sv3uVW_emptyStateText", - "label": "Sv3uVW_label", - "active": "Sv3uVW_active", - "summaryActions": "Sv3uVW_summaryActions", - "shortcutHint": "Sv3uVW_shortcutHint", - "treeNode": "Sv3uVW_treeNode", - "treeCount": "Sv3uVW_treeCount", - "treeChevron": "Sv3uVW_treeChevron", - "heroTitle": "Sv3uVW_heroTitle", - "open": "Sv3uVW_open", - "modalColTitle": "Sv3uVW_modalColTitle", - "emptyState": "Sv3uVW_emptyState", - "modalHint": "Sv3uVW_modalHint", - "errorIcon": "Sv3uVW_errorIcon", - "hint": "Sv3uVW_hint", - "nullCell": "Sv3uVW_nullCell", - "workbench": "Sv3uVW_workbench", - "schemaModal": "Sv3uVW_schemaModal", - "browseChevron": "Sv3uVW_browseChevron", - "connectModal": "Sv3uVW_connectModal", - "treeName": "Sv3uVW_treeName", - "modalBody": "Sv3uVW_modalBody", - "sqlResult": "Sv3uVW_sqlResult", - "heroText": "Sv3uVW_heroText", - "summaryType": "Sv3uVW_summaryType", - "columnsTable": "Sv3uVW_columnsTable", - "treeChildren": "Sv3uVW_treeChildren" + "sqlResult": "IfK3TW_sqlResult", + "input": "IfK3TW_input", + "treeIcon": "IfK3TW_treeIcon", + "rememberHint": "IfK3TW_rememberHint", + "sqlActions": "IfK3TW_sqlActions", + "da-split": "IfK3TW_da-split", + "browseIcon": "IfK3TW_browseIcon", + "sections": "IfK3TW_sections", + "browseChevron": "IfK3TW_browseChevron", + "treeChevron": "IfK3TW_treeChevron", + "nullCell": "IfK3TW_nullCell", + "card": "IfK3TW_card", + "fieldGrid": "IfK3TW_fieldGrid", + "browseRow": "IfK3TW_browseRow", + "treeChildren": "IfK3TW_treeChildren", + "workbench": "IfK3TW_workbench", + "treeNode": "IfK3TW_treeNode", + "errorText": "IfK3TW_errorText", + "heroChevron": "IfK3TW_heroChevron", + "small": "IfK3TW_small", + "modalCol": "IfK3TW_modalCol", + "cardTitle": "IfK3TW_cardTitle", + "ghost": "IfK3TW_ghost", + "docked": "IfK3TW_docked", + "sqlInput": "IfK3TW_sqlInput", + "errorBar": "IfK3TW_errorBar", + "modalHint": "IfK3TW_modalHint", + "errorHead": "IfK3TW_errorHead", + "fieldRow2": "IfK3TW_fieldRow2", + "heroIcon": "IfK3TW_heroIcon", + "connectModalBody": "IfK3TW_connectModalBody", + "connectModal": "IfK3TW_connectModal", + "schemaModal": "IfK3TW_schemaModal", + "tree": "IfK3TW_tree", + "treeItem": "IfK3TW_treeItem", + "columnsTable": "IfK3TW_columnsTable", + "heroTitle": "IfK3TW_heroTitle", + "treeName": "IfK3TW_treeName", + "hint": "IfK3TW_hint", + "statusOk": "IfK3TW_statusOk", + "rail": "IfK3TW_rail", + "summaryDb": "IfK3TW_summaryDb", + "colScroll": "IfK3TW_colScroll", + "typeCell": "IfK3TW_typeCell", + "heroConnect": "IfK3TW_heroConnect", + "strip": "IfK3TW_strip", + "emptyStateText": "IfK3TW_emptyStateText", + "colCount": "IfK3TW_colCount", + "active": "IfK3TW_active", + "modalColTitle": "IfK3TW_modalColTitle", + "summaryRow": "IfK3TW_summaryRow", + "summaryActions": "IfK3TW_summaryActions", + "label": "IfK3TW_label", + "open": "IfK3TW_open", + "emptyState": "IfK3TW_emptyState", + "field": "IfK3TW_field", + "primary": "IfK3TW_primary", + "modalBody": "IfK3TW_modalBody", + "heroHint": "IfK3TW_heroHint", + "summaryType": "IfK3TW_summaryType", + "errorIcon": "IfK3TW_errorIcon", + "actions": "IfK3TW_actions", + "shortcutHint": "IfK3TW_shortcutHint", + "heroText": "IfK3TW_heroText", + "emptyStateIcon": "IfK3TW_emptyStateIcon", + "treeScroll": "IfK3TW_treeScroll", + "rememberRow": "IfK3TW_rememberRow", + "titleIcon": "IfK3TW_titleIcon", + "treeCount": "IfK3TW_treeCount" }; //#endregion //#region src/client/DataAgentWorkbench.tsx @@ -354,6 +354,9 @@ window.__ModuleLoader__.load({ case "oracle": return "1521"; case "hive": return "10000"; case "impala": return "21050"; + case "clickhouse": return "9000"; + case "doris": return "9030"; + case "sqlserver": return "1433"; case "sqlite": return ""; case "mysql": return "3306"; } @@ -922,7 +925,7 @@ window.__ModuleLoader__.load({ onChange: (event) => { const next = event.target.value; setType(next); - setPort(next === "postgres" ? "5432" : next === "mysql" ? "3306" : next === "oracle" ? "1521" : next === "hive" ? "10000" : next === "impala" ? "21050" : ""); + setPort(defaultPortOf(next)); }, children: [ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", { @@ -948,6 +951,18 @@ window.__ModuleLoader__.load({ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", { value: "impala", children: t("type.impala") + }), + /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", { + value: "clickhouse", + children: t("type.clickhouse") + }), + /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", { + value: "doris", + children: t("type.doris") + }), + /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", { + value: "sqlserver", + children: t("type.sqlserver") }) ] })] @@ -1112,6 +1127,9 @@ window.__ModuleLoader__.load({ "type.oracle": "Oracle", "type.hive": "Hive", "type.impala": "Impala", + "type.clickhouse": "ClickHouse", + "type.doris": "Doris", + "type.sqlserver": "SQL Server", "form.host": "主机", "form.port": "端口", "form.user": "用户名", @@ -1162,6 +1180,9 @@ window.__ModuleLoader__.load({ "type.oracle": "Oracle", "type.hive": "Hive", "type.impala": "Impala", + "type.clickhouse": "ClickHouse", + "type.doris": "Doris", + "type.sqlserver": "SQL Server", "form.host": "Host", "form.port": "Port", "form.user": "User", diff --git a/lib/client.js.map b/lib/client.js.map index ff03d0e..d07ca74 100644 --- a/lib/client.js.map +++ b/lib/client.js.map @@ -1 +1 @@ -{"version":3,"file":"client.js","names":["useRef","useState","css","StateDot","Modal"],"sources":["../src/client/persistence.ts","../src/client/DataAgentWorkbench.tsx","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/**\n * Connection-config persistence for the database workbench. The most recent\n * successful connection (type/host/port/user/database/password) is kept in\n * localStorage under one key so remounts and restarts can restore the form\n * and auto-reconnect (the server-side connection store stays in-memory).\n *\n * Security note: the password is persisted in PLAIN TEXT by explicit user\n * decision (local single-user scenario) — see README 安全说明. The storage\n * key is versioned so a future shape change can migrate or ignore old data.\n * @module @yejiming/dsh-data-agent/persistence\n */\n\nimport type { DatabaseType } from './DataAgentWorkbench.tsx'\n\n/** localStorage key holding the most recent connection configuration. */\nexport const CONNECTION_STORAGE_KEY = 'dsh-data-agent.connection.v1'\n\n/** The persisted connection configuration. */\nexport interface SavedConnection {\n type: DatabaseType\n host?: string\n port?: number\n user?: string\n database: string\n /** Present only when the user explicitly opted in to persist the password. */\n password?: string\n /** Opt-in flag; when true, {@link saveConnection} may write `password`. */\n persistPassword?: boolean\n /** Diagnostic timestamp of the save. */\n savedAt: string\n}\n\n/** Runtime storage face (injectable for tests). */\nexport interface StorageLike {\n getItem(key: string): string | null\n setItem(key: string, value: string): void\n removeItem(key: string): void\n}\n\n/** The default storage face (localStorage; unavailable → degraded no-op). */\nfunction defaultStorage(): StorageLike | undefined {\n try {\n if (typeof localStorage !== 'undefined') {\n const probe = '__dsh_probe__'\n localStorage.setItem(probe, '1')\n localStorage.removeItem(probe)\n return localStorage\n }\n } catch {\n // localStorage disabled (privacy mode, sandboxed frame): degrade silently.\n }\n return undefined\n}\n\n/** Validate one parsed storage value into a SavedConnection (or null). */\nfunction parseSaved(value: unknown): SavedConnection | null {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return null\n const candidate = value as Record\n const type = candidate.type\n if (type !== 'mysql' && type !== 'postgres' && type !== 'sqlite'\n && type !== 'oracle' && type !== 'hive' && type !== 'impala') {\n return null\n }\n const database = candidate.database\n // 草稿语义:database 允许为空串(用户可能只填了部分字段就切换会话)。\n if (typeof database !== 'string') return null\n const saved: SavedConnection = { type, database, savedAt: typeof candidate.savedAt === 'string' ? candidate.savedAt : '' }\n if (typeof candidate.host === 'string') saved.host = candidate.host\n if (typeof candidate.port === 'number' && Number.isInteger(candidate.port)) saved.port = candidate.port\n if (typeof candidate.user === 'string') saved.user = candidate.user\n // Legacy records may carry a password; strip it unless the opt-in flag is set\n // (a persisted password only survives when persistPassword was explicitly true).\n const persistPassword = candidate.persistPassword === true\n if (persistPassword) saved.persistPassword = true\n if (persistPassword && typeof candidate.password === 'string') saved.password = candidate.password\n return saved\n}\n\n/** Save one connection configuration (best-effort; storage failures degrade silently). */\nexport function saveConnection(connection: SavedConnection, storage: StorageLike | undefined = defaultStorage()): void {\n if (storage === undefined) return\n try {\n const toWrite: SavedConnection = { ...connection }\n // Password is opt-in only; strip it unless persistPassword is explicitly set.\n if (toWrite.persistPassword !== true) delete toWrite.password\n storage.setItem(CONNECTION_STORAGE_KEY, JSON.stringify(toWrite))\n } catch {\n // Quota / serialization failure: degrade silently, the UI stays usable.\n }\n}\n\n/** Load the saved connection configuration; null when absent or malformed. */\nexport function loadConnection(storage: StorageLike | undefined = defaultStorage()): SavedConnection | null {\n if (storage === undefined) return null\n try {\n const raw = storage.getItem(CONNECTION_STORAGE_KEY)\n if (raw === null) return null\n return parseSaved(JSON.parse(raw))\n } catch {\n return null\n }\n}\n\n/** Remove the saved connection configuration. */\nexport function clearConnection(storage: StorageLike | undefined = defaultStorage()): void {\n if (storage === undefined) return\n try {\n storage.removeItem(CONNECTION_STORAGE_KEY)\n } catch {\n // Degrade silently.\n }\n}\n","/**\n * The database workbench: connection config (collapsible after connect),\n * a browse button that opens the schema explorer Modal, and the SQL command\n * box, rendered into the composer input dock (the strip ABOVE the input bar)\n * for data-agent sessions.\n *\n * Layout is phase-driven by the conversation root's `data-phase` attribute:\n * - hero (blank session): the workbench is a full-width stacked strip above\n * the input bar (which stays at the bottom);\n * - active (conversation started): the workbench becomes a fixed left rail\n * (measured from the conversation column), the chat records and the input\n * bar shift right via the `da-split` CSS rules; if measurement is\n * unavailable the workbench falls back to a docked bottom panel.\n *\n * Interaction model:\n * - after a successful connect the connection form collapses into a summary\n * row (click 连接配置 to expand the readonly form again);\n * - the schema explorer lives in a Modal (ui-primitives) opened by the\n * 库表 button; a single click on a database toggles its table list in a\n * file-explorer style tree (folder rows with chevrons, indented table rows\n * with guide lines, the whole tree scrolls inside the column);\n * - clicking a table loads its columns into the Modal's structure panel.\n *\n * Non-data-agent sessions render null — zero impact on ordinary sessions.\n * Connection state lives on the server (the dataAgentConnections store), so\n * remounts never lose it — this component mirrors `/status` on mount.\n */\nimport { useEffect, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport { Modal, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'\n// Type-only: pulls the ui-conversation slot declarations (conversation.input.dock)\n// and the framework-standard view props into this program.\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport {\n loadConnection,\n saveConnection,\n type SavedConnection,\n} from './persistence.ts'\nimport css from './DataAgentWorkbench.module.css'\n\n/** The plugin's preset id, matching the installed agent preset directory. */\nconst DATA_AGENT_PRESET = 'data-agent'\n\n/** 16×16 stroke icons drawn inline (the primitives package does not export icon atoms). */\nfunction Icon({ className, children, size = 14 }: { className?: string; children: ReactNode; size?: number }) {\n return (\n \n {children}\n \n )\n}\n\n/** Database cylinder: section header of the connection form. */\nfunction DatabaseIcon({ className }: { className?: string }) {\n return (\n \n \n \n \n \n )\n}\n\n/** Table grid: browse row + table rows in the tree. */\nfunction TableIcon({ className }: { className?: string }) {\n return (\n \n \n \n \n \n )\n}\n\n/** Folder: schema rows in the tree. */\nfunction FolderIcon({ className }: { className?: string }) {\n return (\n \n \n \n )\n}\n\n/** Terminal prompt: section header of the SQL box. */\nfunction TerminalIcon({ className }: { className?: string }) {\n return (\n \n \n \n \n \n )\n}\n\n/** Chevron: tree expand indicator (rotates 90° when open). */\nfunction ChevronIcon({ className }: { className?: string }) {\n return (\n \n \n \n )\n}\n\n/** Alert triangle: error bar heading. */\nfunction AlertIcon({ className }: { className?: string }) {\n return (\n \n \n \n \n \n )\n}\n\n/** Play triangle: SQL run button. */\nfunction PlayIcon({ className }: { className?: string }) {\n return (\n \n \n \n )\n}\n\n/** Database kinds offered by the connection form. */\nexport type DatabaseType = 'mysql' | 'postgres' | 'sqlite' | 'oracle' | 'hive' | 'impala'\n\n/** The sessions-list slice the workbench needs (structural; avoids a runtime import). */\nexport interface SessionListLike {\n byId: Record\n}\n\n/** One described column. */\ninterface ColumnInfo {\n name: string\n type: string\n nullable?: boolean\n}\n\n/** Wire shapes of the plugin routes. */\ninterface ConnectResponse { ok: boolean; tables?: string[]; error?: string }\ninterface StatusResponse {\n connected: boolean\n summary?: {\n type: DatabaseType\n host?: string\n port?: number\n user?: string\n database: string\n readonly?: boolean\n tables?: string[]\n }\n}\ninterface SchemasResponse { ok: boolean; schemas?: string[]; error?: string }\ninterface TablesResponse { ok: boolean; tables?: string[]; error?: string }\ninterface DescribeResponse { ok: boolean; columns?: ColumnInfo[]; error?: string }\ninterface QueryResponse {\n ok: boolean\n result?: { exitCode: number | null; stdout: string; stderr: string; truncated: boolean }\n error?: string\n}\n\n/** Registration-side business face: the sessions-list observable becomes `useSessions`. */\nexport interface DataAgentWorkbenchInjected {\n hooks: {\n sessions: {\n getSnapshot(): SessionListLike\n subscribe(fn: () => void): () => void\n }\n }\n}\n\n/** The workbench's full component props: the dock seat + the locale seat + the injected sessions hook. */\nexport type DataAgentWorkbenchProps =\n PropsRuntime<'conversation.input.dock'>\n & PropsLocale<'data-agent'>\n & InjectFace\n\n/** The conversation column's phase attribute value while the session is blank. */\nfunction isHeroPhase(element: HTMLElement | null): boolean {\n return element?.getAttribute('data-phase') === 'hero'\n}\n\n/**\n * 开始对话后(active 布局)左栏工作台相对会话列顶部的下移偏移(px):\n * 让工作台避开会话头部区域、整体往下沉一些,顶部露出对话记录。\n */\nconst RAIL_TOP_OFFSET = 96\n\n/** Default port per type (used to fill the form from a saved connection). */\nfunction defaultPortOf(type: DatabaseType): string {\n switch (type) {\n case 'postgres': return '5432'\n case 'oracle': return '1521'\n case 'hive': return '10000'\n case 'impala': return '21050'\n case 'sqlite': return ''\n case 'mysql': return '3306'\n }\n}\n\n/** Run one /connect request (shared by the form connect and mount auto-reconnect). */\nasync function performConnect(sessionId: string, body: Record): Promise {\n const response = await fetch('/plugins/data-agent/connect', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ sessionId, ...body }),\n })\n return response.json() as Promise\n}\n\n/** Build the /connect payload from a saved connection. */\nfunction payloadFromSaved(saved: SavedConnection): Record {\n if (saved.type === 'sqlite') return { type: saved.type, database: saved.database }\n const body: Record = {\n type: saved.type,\n host: saved.host ?? '127.0.0.1',\n user: saved.user ?? '',\n database: saved.database,\n }\n if (saved.port !== undefined) body.port = saved.port\n if (saved.password !== undefined && saved.password !== '') body.password = saved.password\n return body\n}\n\n/** The database workbench body. */\nexport function DataAgentWorkbench({ sessionId, useSessions, t }: DataAgentWorkbenchProps) {\n const list = useSessions(snapshot => snapshot)\n const isDataAgent = list.byId[sessionId as never]?.agentPreset === DATA_AGENT_PRESET\n\n const rootRef = useRef(null)\n const [phase, setPhase] = useState<'hero' | 'active'>('hero')\n const [railRect, setRailRect] = useState<{ left: number; top: number; bottom: number } | null>(null)\n\n // 表单从已保存的连接配置(localStorage)惰性初始化:切换会话/刷新/重启后回填。\n const [initialSaved] = useState(loadConnection)\n const [type, setType] = useState(initialSaved?.type ?? 'mysql')\n const [host, setHost] = useState(initialSaved?.host ?? '127.0.0.1')\n const [port, setPort] = useState(\n initialSaved?.port !== undefined ? String(initialSaved.port) : defaultPortOf(initialSaved?.type ?? 'mysql'),\n )\n const [user, setUser] = useState(initialSaved?.user ?? '')\n const [password, setPassword] = useState(initialSaved?.password ?? '')\n const [rememberPassword, setRememberPassword] = useState(initialSaved?.persistPassword === true)\n const [readonly, setReadonly] = useState(false)\n const [database, setDatabase] = useState(initialSaved?.database ?? '')\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState(null)\n const [connected, setConnected] = useState(false)\n\n // Connection config Modal (form lives here now, not inline in the strip/rail).\n const [connectModalOpen, setConnectModalOpen] = useState(false)\n // Mount-time auto-reconnect in flight (from the saved connection).\n const [restoring, setRestoring] = useState(false)\n // Schema explorer Modal.\n const [schemaModalOpen, setSchemaModalOpen] = useState(false)\n\n const [schemas, setSchemas] = useState([])\n const [activeSchema, setActiveSchema] = useState(null)\n const [tables, setTables] = useState([])\n const [activeTable, setActiveTable] = useState(null)\n const [columns, setColumns] = useState(null)\n\n const [sql, setSql] = useState('')\n const [sqlBusy, setSqlBusy] = useState(false)\n const [sqlResult, setSqlResult] = useState(null)\n\n // Track the conversation column phase and its bounds for the left-rail\n // layout; toggle the da-split class that shifts chat + input to the right.\n useEffect(() => {\n const column = rootRef.current?.closest('[data-phase]') ?? null\n if (column === null) return\n const measure = (): void => {\n const rect = column.getBoundingClientRect()\n setRailRect({ left: rect.left, top: rect.top, bottom: window.innerHeight - rect.bottom })\n }\n const refreshPhase = (): void => { setPhase(isHeroPhase(column) ? 'hero' : 'active') }\n refreshPhase()\n measure()\n const observer = new MutationObserver(refreshPhase)\n observer.observe(column, { attributes: true, attributeFilter: ['data-phase'] })\n const resizer = new ResizeObserver(measure)\n resizer.observe(column)\n window.addEventListener('resize', measure)\n return () => {\n observer.disconnect()\n resizer.disconnect()\n window.removeEventListener('resize', measure)\n }\n }, [])\n\n // Split layout is active only for a live conversation (phase active) with\n // a measurable column; everything else stays stacked above the input.\n const split = phase === 'active' && railRect !== null\n useEffect(() => {\n const root = document.documentElement\n const splitClass = css['da-split']\n if (split && splitClass !== undefined) root.classList.add(splitClass)\n else if (splitClass !== undefined) root.classList.remove(splitClass)\n return () => { if (splitClass !== undefined) root.classList.remove(splitClass) }\n }, [split])\n\n // 草稿持久化:任何表单字段变化立即保存(含密码,用户已确认),\n // 使未连接的输入在切换会话/刷新后也能恢复。首轮跳过(初始化回填值\n // 无需重写,也避免从未输入时写入空配置)。\n const firstDraftRun = useRef(true)\n useEffect(() => {\n if (firstDraftRun.current) {\n firstDraftRun.current = false\n return\n }\n const draft: SavedConnection = {\n type,\n database,\n ...type !== 'sqlite' ? { host, user } : {},\n ...type !== 'sqlite' && port !== '' ? { port: Number(port) } : {},\n ...password !== '' ? { password } : {},\n ...rememberPassword ? { persistPassword: true } : {},\n savedAt: new Date().toISOString(),\n }\n saveConnection(draft)\n }, [type, host, port, user, database, password, rememberPassword])\n\n // Mirror the server-side connection on mount; auto-reconnect once when the\n // server store was lost (restart).\n useEffect(() => {\n let cancelled = false\n const saved = initialSaved\n setBusy(true)\n fetch(`/plugins/data-agent/status?sessionId=${encodeURIComponent(sessionId)}`)\n .then(response => response.json() as Promise)\n .then(async (body) => {\n if (cancelled) return\n if (body.connected) {\n setConnected(true)\n if (body.summary !== undefined) {\n setType(body.summary.type)\n setHost(body.summary.host ?? '')\n setPort(body.summary.port !== undefined ? String(body.summary.port) : '')\n setUser(body.summary.user ?? '')\n setDatabase(body.summary.database)\n setReadonly(body.summary.readonly === true)\n }\n const response = await fetch(`/plugins/data-agent/schemas?sessionId=${encodeURIComponent(sessionId)}`)\n const schemasBody = await response.json() as SchemasResponse\n if (!cancelled && schemasBody.ok) setSchemas(schemasBody.schemas ?? [])\n return\n }\n // Server connection lost (restart): restore with the saved config, once.\n // 草稿未完成(database 为空)时不自动重连,仅保留表单回填。\n if (saved !== null && saved.database !== '') {\n setRestoring(true)\n try {\n const result = await performConnect(sessionId, payloadFromSaved(saved))\n if (cancelled) return\n if (result.ok) {\n setConnected(true)\n setConnectModalOpen(false)\n const response = await fetch(`/plugins/data-agent/schemas?sessionId=${encodeURIComponent(sessionId)}`)\n const schemasBody = await response.json() as SchemasResponse\n if (!cancelled && schemasBody.ok) setSchemas(schemasBody.schemas ?? [])\n } else {\n setError(`连接恢复失败:${result.error ?? 'unknown error'}`)\n }\n } catch (cause) {\n if (!cancelled) {\n setError(`连接恢复失败:${cause instanceof Error ? cause.message : String(cause)}`)\n }\n } finally {\n if (!cancelled) setRestoring(false)\n }\n }\n })\n .catch(() => { /* the form stays usable; connect will surface errors */ })\n .finally(() => { if (!cancelled) setBusy(false) })\n return () => { cancelled = true }\n }, [sessionId])\n\n const sqlite = type === 'sqlite'\n\n const connect = async (): Promise => {\n setBusy(true)\n setError(null)\n const body: Record = { sessionId, type, readonly }\n if (sqlite) {\n body.database = database\n } else {\n body.host = host\n if (port !== '') body.port = Number(port)\n body.user = user\n body.database = database\n if (password !== '') body.password = password\n }\n try {\n const result = await performConnect(sessionId, body)\n if (result.ok) {\n setConnected(true)\n // Close the config Modal after a successful connect.\n setConnectModalOpen(false)\n const schemasResponse = await fetch(`/plugins/data-agent/schemas?sessionId=${encodeURIComponent(sessionId)}`)\n const schemasBody = await schemasResponse.json() as SchemasResponse\n if (schemasBody.ok) setSchemas(schemasBody.schemas ?? [])\n } else {\n setError(result.error ?? 'unknown error')\n }\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setBusy(false)\n }\n }\n\n const disconnect = async (): Promise => {\n setBusy(true)\n setError(null)\n try {\n await fetch('/plugins/data-agent/disconnect', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ sessionId }),\n })\n setConnected(false)\n setReadonly(false)\n setSchemaModalOpen(false)\n setSchemas([])\n setActiveSchema(null)\n setTables([])\n setActiveTable(null)\n setColumns(null)\n setSqlResult(null)\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setBusy(false)\n }\n }\n\n /** Toggle one schema's table list (single click; only one open at a time). */\n const toggleSchema = async (schema: string): Promise => {\n if (activeSchema === schema) {\n setActiveSchema(null)\n setTables([])\n setActiveTable(null)\n setColumns(null)\n return\n }\n setActiveSchema(schema)\n setActiveTable(null)\n setColumns(null)\n setTables([])\n try {\n const response = await fetch(\n `/plugins/data-agent/tables?sessionId=${encodeURIComponent(sessionId)}&schema=${encodeURIComponent(schema)}`,\n )\n const result = await response.json() as TablesResponse\n if (result.ok) setTables(result.tables ?? [])\n else setError(result.error ?? 'unknown error')\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n }\n }\n\n const selectTable = async (table: string): Promise => {\n setActiveTable(table)\n setColumns(null)\n const params = new URLSearchParams({ sessionId, table })\n if (activeSchema !== null) params.set('schema', activeSchema)\n try {\n const response = await fetch(`/plugins/data-agent/describe?${params.toString()}`)\n const result = await response.json() as DescribeResponse\n if (result.ok) setColumns(result.columns ?? [])\n else setError(result.error ?? 'unknown error')\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n }\n }\n\n const runSql = async (): Promise => {\n if (sql.trim() === '') return\n setSqlBusy(true)\n setSqlResult(null)\n try {\n const response = await fetch('/plugins/data-agent/query', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ sessionId, sql }),\n })\n const result = await response.json() as QueryResponse\n if (result.ok && result.result !== undefined) {\n const parts: string[] = []\n if (result.result.stdout !== '') parts.push(result.result.stdout)\n if (result.result.stderr !== '') parts.push(`[stderr]\\n${result.result.stderr}`)\n if (result.result.truncated) parts.push('… 输出超过上限,已截断')\n if (result.result.exitCode !== 0) parts.push(`[exit code: ${result.result.exitCode ?? 'signal'}]`)\n setSqlResult(parts.join('\\n'))\n } else {\n setSqlResult(`Error: ${result.error ?? 'unknown error'}`)\n }\n } catch (cause) {\n setSqlResult(`Error: ${cause instanceof Error ? cause.message : String(cause)}`)\n } finally {\n setSqlBusy(false)\n }\n }\n\n // A session not running the data-agent preset: nothing renders at all.\n if (!isDataAgent) return null\n\n const databaseLabel = sqlite\n ? t('form.database.sqlite')\n : type === 'oracle'\n ? t('form.database.oracle')\n : type === 'hive' || type === 'impala'\n ? t('form.database.hive')\n : t('form.database')\n\n const railStyle = split\n ? {\n position: 'fixed' as const,\n left: railRect!.left,\n top: railRect!.top + RAIL_TOP_OFFSET,\n bottom: railRect!.bottom,\n width: 380,\n }\n : undefined\n\n // Form fields are readonly while connected (edit requires disconnecting).\n const formDisabled = busy || connected\n\n return (\n \n
\n {connected ? (\n // ── connected summary row (config lives in a Modal now) ─────────\n
\n
\n \n {t('state.connected')}\n {t(`type.${type}`)}\n {database}\n \n \n \n \n
\n
\n ) : (\n // ── disconnected hero: a single connect entry, no inline form ────\n
\n \n
\n )}\n\n {connected && (\n // ── schema explorer entry ───────────────────────────────────────\n
\n \n
\n )}\n\n {connected && (\n
\n
\n \n {t('wb.sql')}\n
\n setSql(event.target.value)}\n onKeyDown={(event) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void runSql()\n }\n }}\n />\n
\n {t('wb.sql.shortcut')}\n { void runSql() }}\n >\n \n {sqlBusy ? t('wb.sql.running') : t('wb.sql.run')}\n \n
\n
{sqlResult ?? t('wb.sql.empty')}
\n
\n )}\n
\n\n {error !== null && !connectModalOpen && (\n
\n
\n \n {t('error.title')}\n
\n {error}\n
\n )}\n\n {schemaModalOpen && (\n setSchemaModalOpen(false)}\n title={t('wb.modal.title')}\n closeLabel={t('action.close')}\n className={css.schemaModal}\n >\n
\n
\n
\n {t('wb.schemas')}\n {schemas.length > 0 && {schemas.length}}\n
\n
\n {schemas.length === 0 &&
{t('wb.loading')}
}\n
    \n {schemas.map(schema => (\n
  • \n { void toggleSchema(schema) }}\n >\n \n \n {schema}\n {activeSchema === schema && tables.length > 0 && (\n {tables.length}\n )}\n \n {activeSchema === schema && (\n
    \n {tables.length === 0 &&
    {t('wb.empty')}
    }\n {tables.map(table => (\n { void selectTable(table) }}\n >\n \n {table}\n \n ))}\n
    \n )}\n
  • \n ))}\n
\n
\n
{t('wb.hint.click')}
\n
\n\n
\n
\n {t('wb.columns')}{activeTable !== null && ` · ${activeTable}`}\n
\n {columns === null ? (\n
\n \n {t('wb.hint.click')}\n
\n ) : (\n
\n \n \n \n \n \n {columns.map(column => (\n \n \n \n \n \n ))}\n \n
nametypenull
{column.name}{column.type}{column.nullable === undefined ? '' : column.nullable ? 'YES' : 'NO'}
\n
\n )}\n
\n
\n \n )}\n\n {connectModalOpen && (\n setConnectModalOpen(false)}\n title={t('form.title')}\n closeLabel={t('action.close')}\n className={css.connectModal}\n >\n
\n
\n \n\n {!sqlite && (\n
\n \n \n
\n )}\n\n {!sqlite && (\n
\n \n \n
\n )}\n\n {!sqlite && (\n \n )}\n\n \n\n \n
\n\n
\n {!connected ? (\n { void connect() }}\n >\n {restoring ? t('state.reconnecting') : busy ? t('state.checking') : t('action.connect')}\n \n ) : (\n \n )}\n
\n\n {error !== null && (\n
\n
\n \n {t('error.title')}\n
\n {error}\n
\n )}\n
\n \n )}\n \n )\n}\n","/** `data-agent` namespace dictionaries for the database workbench. */\n\n/** Dictionary namespace owned by this plugin. */\nexport const NS = 'data-agent'\n\n/** Simplified Chinese dictionary (the key-set source of truth). */\nexport const zh = {\n 'form.title': '数据库连接',\n 'form.hero.title': '连接数据库',\n 'form.hero.hint': '连接 MySQL / PostgreSQL / SQLite 等,开始与数据 Agent 对话',\n 'form.type': '数据库类型',\n 'type.mysql': 'MySQL',\n 'type.postgres': 'PostgreSQL',\n 'type.sqlite': 'SQLite',\n 'type.oracle': 'Oracle',\n 'type.hive': 'Hive',\n 'type.impala': 'Impala',\n 'form.host': '主机',\n 'form.port': '端口',\n 'form.user': '用户名',\n 'form.password': '密码',\n 'form.rememberPassword': '记住密码',\n 'form.rememberPassword.hint': '仅勾选后才会在本机保存密码',\n 'form.readonly': '只读模式',\n 'form.readonly.hint': '仅放行查询语句,拒绝增删改、建表等写操作',\n 'form.database': '数据库名',\n 'form.database.oracle': '服务名 / SID',\n 'form.database.hive': '默认库',\n 'form.database.impala': '默认库',\n 'form.database.sqlite': '数据库文件路径',\n 'form.database.sqlite.placeholder': '/path/to/orders.db',\n 'action.connect': '连接',\n 'action.disconnect': '断开',\n 'state.connected': '已连接',\n 'state.disconnected': '未连接',\n 'state.checking': '正在检查连接…',\n 'state.reconnecting': '正在恢复连接…',\n 'wb.schemas': '库',\n 'wb.tables': '表',\n 'wb.columns': '表结构',\n 'wb.sql': 'SQL 命令',\n 'wb.sql.placeholder': '在此输入 SQL,例如 SELECT * FROM orders LIMIT 10;',\n 'wb.sql.run': '运行',\n 'wb.sql.running': '运行中…',\n 'wb.sql.shortcut': 'Ctrl / ⌘ + Enter 运行',\n 'wb.sql.empty': '-- 尚无输出 --',\n 'wb.loading': '加载中…',\n 'wb.empty': '(空)',\n 'wb.modal.title': '库表浏览',\n 'wb.hint.click': '单击库展开表 · 点击表查看结构',\n 'action.config': '连接配置',\n 'action.browse': '库表',\n 'action.close': '关闭',\n 'error.title': '操作失败',\n} satisfies Record\n\n/** The data-agent namespace key union. */\nexport type DataAgentKey = keyof typeof zh\n\n/** English dictionary, checked complete against the zh key set. */\nexport const en = {\n 'form.title': 'Database connection',\n 'form.hero.title': 'Connect database',\n 'form.hero.hint': 'Connect MySQL / PostgreSQL / SQLite and start chatting with the data agent',\n 'form.type': 'Database type',\n 'type.mysql': 'MySQL',\n 'type.postgres': 'PostgreSQL',\n 'type.sqlite': 'SQLite',\n 'type.oracle': 'Oracle',\n 'type.hive': 'Hive',\n 'type.impala': 'Impala',\n 'form.host': 'Host',\n 'form.port': 'Port',\n 'form.user': 'User',\n 'form.password': 'Password',\n 'form.rememberPassword': 'Remember password',\n 'form.rememberPassword.hint': 'Saved locally only when checked',\n 'form.readonly': 'Read-only mode',\n 'form.readonly.hint': 'Allow read queries only; reject writes (INSERT/UPDATE/DELETE/DDL)',\n 'form.database': 'Database',\n 'form.database.oracle': 'Service name / SID',\n 'form.database.hive': 'Default database',\n 'form.database.impala': 'Default database',\n 'form.database.sqlite': 'Database file path',\n 'form.database.sqlite.placeholder': '/path/to/orders.db',\n 'action.connect': 'Connect',\n 'action.disconnect': 'Disconnect',\n 'state.connected': 'Connected',\n 'state.disconnected': 'Not connected',\n 'state.checking': 'Checking connection…',\n 'state.reconnecting': 'Restoring connection…',\n 'wb.schemas': 'Databases',\n 'wb.tables': 'Tables',\n 'wb.columns': 'Columns',\n 'wb.sql': 'SQL',\n 'wb.sql.placeholder': 'Write SQL here, e.g. SELECT * FROM orders LIMIT 10;',\n 'wb.sql.run': 'Run',\n 'wb.sql.running': 'Running…',\n 'wb.sql.shortcut': 'Ctrl / ⌘ + Enter to run',\n 'wb.sql.empty': '-- no output --',\n 'wb.loading': 'Loading…',\n 'wb.empty': '(empty)',\n 'wb.modal.title': 'Database explorer',\n 'wb.hint.click': 'Click a database to expand tables · click a table for columns',\n 'action.config': 'Connection settings',\n 'action.browse': 'Tables',\n 'action.close': 'Close',\n 'error.title': 'Operation failed',\n} satisfies Record\n","/**\n * Data Agent browser half, plugin entry: registers the database workbench\n * into the composer input dock (the strip ABOVE the input bar) for\n * data-agent sessions, and the `data-agent` dictionaries. The old\n * conversation-view tab is gone — the workbench lives inside the session.\n * Connection state lives in the server-side connection store, so layout and\n * session switches never lose it — the view only mirrors what\n * `/plugins/data-agent/status` reports.\n * @module @yejiming/dsh-data-agent/client\n */\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\n// Type-only: pulls the locale plugin's Context merge (ctx.locale) into this program.\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls the ui-conversation slot declarations (conversation.input.dock)\n// and the session standard props (sessionId) into this program.\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport { DataAgentWorkbench, type SessionListLike } from './DataAgentWorkbench.tsx'\nimport { NS, en, zh, type DataAgentKey } from './locales.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The database workbench copy. */\n 'data-agent': DataAgentKey\n }\n}\n\n/** Required services: the locale service, the slot registry, and the sessions list. */\nexport const inject = ['slots', 'locale', 'sessions']\n\n/**\n * Client plugin body: register the data-agent dictionaries and the database\n * workbench into the composer input dock. The registration rides the slot\n * service's effect wrapper, so plugin unload removes it.\n * @param ctx - client root context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'data-agent: dictionaries')\n const t = ctx.locale.bind(NS)\n ctx.inject(['slots', 'locale', 'sessions'], (scope: ClientContext) => {\n const list = scope.sessions.list\n // The sessions list is the agent-preset authority on the client: the\n // ui-agent-preset surface records `agentPreset` onto each session summary.\n const sessionsSource = {\n getSnapshot: (): SessionListLike => list.getSnapshot() as unknown as SessionListLike,\n subscribe: (fn: () => void): (() => void) => list.subscribe(fn),\n }\n // The workbench strip sits above the input bar (order 0, first entry);\n // it renders only for data-agent sessions and returns null elsewhere.\n scope.slots.inject('conversation.input.dock', () => scope.slots.register({\n name: 'conversation.input.dock',\n id: 'data-agent',\n order: 0,\n locale: NS,\n inject: () => ({ hooks: { sessions: sessionsSource } }),\n }, DataAgentWorkbench))\n })\n}\n"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"client.js","names":["useRef","useState","css","StateDot","Modal"],"sources":["../src/client/persistence.ts","../src/client/DataAgentWorkbench.tsx","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/**\n * Connection-config persistence for the database workbench. The most recent\n * successful connection (type/host/port/user/database/password) is kept in\n * localStorage under one key so remounts and restarts can restore the form\n * and auto-reconnect (the server-side connection store stays in-memory).\n *\n * Security note: the password is persisted in PLAIN TEXT by explicit user\n * decision (local single-user scenario) — see README 安全说明. The storage\n * key is versioned so a future shape change can migrate or ignore old data.\n * @module @yejiming/dsh-data-agent/persistence\n */\n\nimport type { DatabaseType } from './DataAgentWorkbench.tsx'\n\n/** localStorage key holding the most recent connection configuration. */\nexport const CONNECTION_STORAGE_KEY = 'dsh-data-agent.connection.v1'\n\n/** The persisted connection configuration. */\nexport interface SavedConnection {\n type: DatabaseType\n host?: string\n port?: number\n user?: string\n database: string\n /** Present only when the user explicitly opted in to persist the password. */\n password?: string\n /** Opt-in flag; when true, {@link saveConnection} may write `password`. */\n persistPassword?: boolean\n /** Diagnostic timestamp of the save. */\n savedAt: string\n}\n\n/** Runtime storage face (injectable for tests). */\nexport interface StorageLike {\n getItem(key: string): string | null\n setItem(key: string, value: string): void\n removeItem(key: string): void\n}\n\n/** The default storage face (localStorage; unavailable → degraded no-op). */\nfunction defaultStorage(): StorageLike | undefined {\n try {\n if (typeof localStorage !== 'undefined') {\n const probe = '__dsh_probe__'\n localStorage.setItem(probe, '1')\n localStorage.removeItem(probe)\n return localStorage\n }\n } catch {\n // localStorage disabled (privacy mode, sandboxed frame): degrade silently.\n }\n return undefined\n}\n\n/** Validate one parsed storage value into a SavedConnection (or null). */\nfunction parseSaved(value: unknown): SavedConnection | null {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return null\n const candidate = value as Record\n const type = candidate.type\n if (type !== 'mysql' && type !== 'postgres' && type !== 'sqlite'\n && type !== 'oracle' && type !== 'hive' && type !== 'impala'\n && type !== 'clickhouse' && type !== 'doris' && type !== 'sqlserver') {\n return null\n }\n const database = candidate.database\n // 草稿语义:database 允许为空串(用户可能只填了部分字段就切换会话)。\n if (typeof database !== 'string') return null\n const saved: SavedConnection = { type, database, savedAt: typeof candidate.savedAt === 'string' ? candidate.savedAt : '' }\n if (typeof candidate.host === 'string') saved.host = candidate.host\n if (typeof candidate.port === 'number' && Number.isInteger(candidate.port)) saved.port = candidate.port\n if (typeof candidate.user === 'string') saved.user = candidate.user\n // Legacy records may carry a password; strip it unless the opt-in flag is set\n // (a persisted password only survives when persistPassword was explicitly true).\n const persistPassword = candidate.persistPassword === true\n if (persistPassword) saved.persistPassword = true\n if (persistPassword && typeof candidate.password === 'string') saved.password = candidate.password\n return saved\n}\n\n/** Save one connection configuration (best-effort; storage failures degrade silently). */\nexport function saveConnection(connection: SavedConnection, storage: StorageLike | undefined = defaultStorage()): void {\n if (storage === undefined) return\n try {\n const toWrite: SavedConnection = { ...connection }\n // Password is opt-in only; strip it unless persistPassword is explicitly set.\n if (toWrite.persistPassword !== true) delete toWrite.password\n storage.setItem(CONNECTION_STORAGE_KEY, JSON.stringify(toWrite))\n } catch {\n // Quota / serialization failure: degrade silently, the UI stays usable.\n }\n}\n\n/** Load the saved connection configuration; null when absent or malformed. */\nexport function loadConnection(storage: StorageLike | undefined = defaultStorage()): SavedConnection | null {\n if (storage === undefined) return null\n try {\n const raw = storage.getItem(CONNECTION_STORAGE_KEY)\n if (raw === null) return null\n return parseSaved(JSON.parse(raw))\n } catch {\n return null\n }\n}\n\n/** Remove the saved connection configuration. */\nexport function clearConnection(storage: StorageLike | undefined = defaultStorage()): void {\n if (storage === undefined) return\n try {\n storage.removeItem(CONNECTION_STORAGE_KEY)\n } catch {\n // Degrade silently.\n }\n}\n","/**\n * The database workbench: connection config (collapsible after connect),\n * a browse button that opens the schema explorer Modal, and the SQL command\n * box, rendered into the composer input dock (the strip ABOVE the input bar)\n * for data-agent sessions.\n *\n * Layout is phase-driven by the conversation root's `data-phase` attribute:\n * - hero (blank session): the workbench is a full-width stacked strip above\n * the input bar (which stays at the bottom);\n * - active (conversation started): the workbench becomes a fixed left rail\n * (measured from the conversation column), the chat records and the input\n * bar shift right via the `da-split` CSS rules; if measurement is\n * unavailable the workbench falls back to a docked bottom panel.\n *\n * Interaction model:\n * - after a successful connect the connection form collapses into a summary\n * row (click 连接配置 to expand the readonly form again);\n * - the schema explorer lives in a Modal (ui-primitives) opened by the\n * 库表 button; a single click on a database toggles its table list in a\n * file-explorer style tree (folder rows with chevrons, indented table rows\n * with guide lines, the whole tree scrolls inside the column);\n * - clicking a table loads its columns into the Modal's structure panel.\n *\n * Non-data-agent sessions render null — zero impact on ordinary sessions.\n * Connection state lives on the server (the dataAgentConnections store), so\n * remounts never lose it — this component mirrors `/status` on mount.\n */\nimport { useEffect, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport { Modal, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'\n// Type-only: pulls the ui-conversation slot declarations (conversation.input.dock)\n// and the framework-standard view props into this program.\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport {\n loadConnection,\n saveConnection,\n type SavedConnection,\n} from './persistence.ts'\nimport css from './DataAgentWorkbench.module.css'\n\n/** The plugin's preset id, matching the installed agent preset directory. */\nconst DATA_AGENT_PRESET = 'data-agent'\n\n/** 16×16 stroke icons drawn inline (the primitives package does not export icon atoms). */\nfunction Icon({ className, children, size = 14 }: { className?: string; children: ReactNode; size?: number }) {\n return (\n \n {children}\n \n )\n}\n\n/** Database cylinder: section header of the connection form. */\nfunction DatabaseIcon({ className }: { className?: string }) {\n return (\n \n \n \n \n \n )\n}\n\n/** Table grid: browse row + table rows in the tree. */\nfunction TableIcon({ className }: { className?: string }) {\n return (\n \n \n \n \n \n )\n}\n\n/** Folder: schema rows in the tree. */\nfunction FolderIcon({ className }: { className?: string }) {\n return (\n \n \n \n )\n}\n\n/** Terminal prompt: section header of the SQL box. */\nfunction TerminalIcon({ className }: { className?: string }) {\n return (\n \n \n \n \n \n )\n}\n\n/** Chevron: tree expand indicator (rotates 90° when open). */\nfunction ChevronIcon({ className }: { className?: string }) {\n return (\n \n \n \n )\n}\n\n/** Alert triangle: error bar heading. */\nfunction AlertIcon({ className }: { className?: string }) {\n return (\n \n \n \n \n \n )\n}\n\n/** Play triangle: SQL run button. */\nfunction PlayIcon({ className }: { className?: string }) {\n return (\n \n \n \n )\n}\n\n/** Database kinds offered by the connection form. */\nexport type DatabaseType =\n | 'mysql'\n | 'postgres'\n | 'sqlite'\n | 'oracle'\n | 'hive'\n | 'impala'\n | 'clickhouse'\n | 'doris'\n | 'sqlserver'\n\n/** The sessions-list slice the workbench needs (structural; avoids a runtime import). */\nexport interface SessionListLike {\n byId: Record\n}\n\n/** One described column. */\ninterface ColumnInfo {\n name: string\n type: string\n nullable?: boolean\n}\n\n/** Wire shapes of the plugin routes. */\ninterface ConnectResponse { ok: boolean; tables?: string[]; error?: string }\ninterface StatusResponse {\n connected: boolean\n summary?: {\n type: DatabaseType\n host?: string\n port?: number\n user?: string\n database: string\n readonly?: boolean\n tables?: string[]\n }\n}\ninterface SchemasResponse { ok: boolean; schemas?: string[]; error?: string }\ninterface TablesResponse { ok: boolean; tables?: string[]; error?: string }\ninterface DescribeResponse { ok: boolean; columns?: ColumnInfo[]; error?: string }\ninterface QueryResponse {\n ok: boolean\n result?: { exitCode: number | null; stdout: string; stderr: string; truncated: boolean }\n error?: string\n}\n\n/** Registration-side business face: the sessions-list observable becomes `useSessions`. */\nexport interface DataAgentWorkbenchInjected {\n hooks: {\n sessions: {\n getSnapshot(): SessionListLike\n subscribe(fn: () => void): () => void\n }\n }\n}\n\n/** The workbench's full component props: the dock seat + the locale seat + the injected sessions hook. */\nexport type DataAgentWorkbenchProps =\n PropsRuntime<'conversation.input.dock'>\n & PropsLocale<'data-agent'>\n & InjectFace\n\n/** The conversation column's phase attribute value while the session is blank. */\nfunction isHeroPhase(element: HTMLElement | null): boolean {\n return element?.getAttribute('data-phase') === 'hero'\n}\n\n/**\n * 开始对话后(active 布局)左栏工作台相对会话列顶部的下移偏移(px):\n * 让工作台避开会话头部区域、整体往下沉一些,顶部露出对话记录。\n */\nconst RAIL_TOP_OFFSET = 96\n\n/** Default port per type (used to fill the form from a saved connection). */\nfunction defaultPortOf(type: DatabaseType): string {\n switch (type) {\n case 'postgres': return '5432'\n case 'oracle': return '1521'\n case 'hive': return '10000'\n case 'impala': return '21050'\n case 'clickhouse': return '9000'\n case 'doris': return '9030'\n case 'sqlserver': return '1433'\n case 'sqlite': return ''\n case 'mysql': return '3306'\n }\n}\n\n/** Run one /connect request (shared by the form connect and mount auto-reconnect). */\nasync function performConnect(sessionId: string, body: Record): Promise {\n const response = await fetch('/plugins/data-agent/connect', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ sessionId, ...body }),\n })\n return response.json() as Promise\n}\n\n/** Build the /connect payload from a saved connection. */\nfunction payloadFromSaved(saved: SavedConnection): Record {\n if (saved.type === 'sqlite') return { type: saved.type, database: saved.database }\n const body: Record = {\n type: saved.type,\n host: saved.host ?? '127.0.0.1',\n user: saved.user ?? '',\n database: saved.database,\n }\n if (saved.port !== undefined) body.port = saved.port\n if (saved.password !== undefined && saved.password !== '') body.password = saved.password\n return body\n}\n\n/** The database workbench body. */\nexport function DataAgentWorkbench({ sessionId, useSessions, t }: DataAgentWorkbenchProps) {\n const list = useSessions(snapshot => snapshot)\n const isDataAgent = list.byId[sessionId as never]?.agentPreset === DATA_AGENT_PRESET\n\n const rootRef = useRef(null)\n const [phase, setPhase] = useState<'hero' | 'active'>('hero')\n const [railRect, setRailRect] = useState<{ left: number; top: number; bottom: number } | null>(null)\n\n // 表单从已保存的连接配置(localStorage)惰性初始化:切换会话/刷新/重启后回填。\n const [initialSaved] = useState(loadConnection)\n const [type, setType] = useState(initialSaved?.type ?? 'mysql')\n const [host, setHost] = useState(initialSaved?.host ?? '127.0.0.1')\n const [port, setPort] = useState(\n initialSaved?.port !== undefined ? String(initialSaved.port) : defaultPortOf(initialSaved?.type ?? 'mysql'),\n )\n const [user, setUser] = useState(initialSaved?.user ?? '')\n const [password, setPassword] = useState(initialSaved?.password ?? '')\n const [rememberPassword, setRememberPassword] = useState(initialSaved?.persistPassword === true)\n const [readonly, setReadonly] = useState(false)\n const [database, setDatabase] = useState(initialSaved?.database ?? '')\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState(null)\n const [connected, setConnected] = useState(false)\n\n // Connection config Modal (form lives here now, not inline in the strip/rail).\n const [connectModalOpen, setConnectModalOpen] = useState(false)\n // Mount-time auto-reconnect in flight (from the saved connection).\n const [restoring, setRestoring] = useState(false)\n // Schema explorer Modal.\n const [schemaModalOpen, setSchemaModalOpen] = useState(false)\n\n const [schemas, setSchemas] = useState([])\n const [activeSchema, setActiveSchema] = useState(null)\n const [tables, setTables] = useState([])\n const [activeTable, setActiveTable] = useState(null)\n const [columns, setColumns] = useState(null)\n\n const [sql, setSql] = useState('')\n const [sqlBusy, setSqlBusy] = useState(false)\n const [sqlResult, setSqlResult] = useState(null)\n\n // Track the conversation column phase and its bounds for the left-rail\n // layout; toggle the da-split class that shifts chat + input to the right.\n useEffect(() => {\n const column = rootRef.current?.closest('[data-phase]') ?? null\n if (column === null) return\n const measure = (): void => {\n const rect = column.getBoundingClientRect()\n setRailRect({ left: rect.left, top: rect.top, bottom: window.innerHeight - rect.bottom })\n }\n const refreshPhase = (): void => { setPhase(isHeroPhase(column) ? 'hero' : 'active') }\n refreshPhase()\n measure()\n const observer = new MutationObserver(refreshPhase)\n observer.observe(column, { attributes: true, attributeFilter: ['data-phase'] })\n const resizer = new ResizeObserver(measure)\n resizer.observe(column)\n window.addEventListener('resize', measure)\n return () => {\n observer.disconnect()\n resizer.disconnect()\n window.removeEventListener('resize', measure)\n }\n }, [])\n\n // Split layout is active only for a live conversation (phase active) with\n // a measurable column; everything else stays stacked above the input.\n const split = phase === 'active' && railRect !== null\n useEffect(() => {\n const root = document.documentElement\n const splitClass = css['da-split']\n if (split && splitClass !== undefined) root.classList.add(splitClass)\n else if (splitClass !== undefined) root.classList.remove(splitClass)\n return () => { if (splitClass !== undefined) root.classList.remove(splitClass) }\n }, [split])\n\n // 草稿持久化:任何表单字段变化立即保存(含密码,用户已确认),\n // 使未连接的输入在切换会话/刷新后也能恢复。首轮跳过(初始化回填值\n // 无需重写,也避免从未输入时写入空配置)。\n const firstDraftRun = useRef(true)\n useEffect(() => {\n if (firstDraftRun.current) {\n firstDraftRun.current = false\n return\n }\n const draft: SavedConnection = {\n type,\n database,\n ...type !== 'sqlite' ? { host, user } : {},\n ...type !== 'sqlite' && port !== '' ? { port: Number(port) } : {},\n ...password !== '' ? { password } : {},\n ...rememberPassword ? { persistPassword: true } : {},\n savedAt: new Date().toISOString(),\n }\n saveConnection(draft)\n }, [type, host, port, user, database, password, rememberPassword])\n\n // Mirror the server-side connection on mount; auto-reconnect once when the\n // server store was lost (restart).\n useEffect(() => {\n let cancelled = false\n const saved = initialSaved\n setBusy(true)\n fetch(`/plugins/data-agent/status?sessionId=${encodeURIComponent(sessionId)}`)\n .then(response => response.json() as Promise)\n .then(async (body) => {\n if (cancelled) return\n if (body.connected) {\n setConnected(true)\n if (body.summary !== undefined) {\n setType(body.summary.type)\n setHost(body.summary.host ?? '')\n setPort(body.summary.port !== undefined ? String(body.summary.port) : '')\n setUser(body.summary.user ?? '')\n setDatabase(body.summary.database)\n setReadonly(body.summary.readonly === true)\n }\n const response = await fetch(`/plugins/data-agent/schemas?sessionId=${encodeURIComponent(sessionId)}`)\n const schemasBody = await response.json() as SchemasResponse\n if (!cancelled && schemasBody.ok) setSchemas(schemasBody.schemas ?? [])\n return\n }\n // Server connection lost (restart): restore with the saved config, once.\n // 草稿未完成(database 为空)时不自动重连,仅保留表单回填。\n if (saved !== null && saved.database !== '') {\n setRestoring(true)\n try {\n const result = await performConnect(sessionId, payloadFromSaved(saved))\n if (cancelled) return\n if (result.ok) {\n setConnected(true)\n setConnectModalOpen(false)\n const response = await fetch(`/plugins/data-agent/schemas?sessionId=${encodeURIComponent(sessionId)}`)\n const schemasBody = await response.json() as SchemasResponse\n if (!cancelled && schemasBody.ok) setSchemas(schemasBody.schemas ?? [])\n } else {\n setError(`连接恢复失败:${result.error ?? 'unknown error'}`)\n }\n } catch (cause) {\n if (!cancelled) {\n setError(`连接恢复失败:${cause instanceof Error ? cause.message : String(cause)}`)\n }\n } finally {\n if (!cancelled) setRestoring(false)\n }\n }\n })\n .catch(() => { /* the form stays usable; connect will surface errors */ })\n .finally(() => { if (!cancelled) setBusy(false) })\n return () => { cancelled = true }\n }, [sessionId])\n\n const sqlite = type === 'sqlite'\n\n const connect = async (): Promise => {\n setBusy(true)\n setError(null)\n const body: Record = { sessionId, type, readonly }\n if (sqlite) {\n body.database = database\n } else {\n body.host = host\n if (port !== '') body.port = Number(port)\n body.user = user\n body.database = database\n if (password !== '') body.password = password\n }\n try {\n const result = await performConnect(sessionId, body)\n if (result.ok) {\n setConnected(true)\n // Close the config Modal after a successful connect.\n setConnectModalOpen(false)\n const schemasResponse = await fetch(`/plugins/data-agent/schemas?sessionId=${encodeURIComponent(sessionId)}`)\n const schemasBody = await schemasResponse.json() as SchemasResponse\n if (schemasBody.ok) setSchemas(schemasBody.schemas ?? [])\n } else {\n setError(result.error ?? 'unknown error')\n }\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setBusy(false)\n }\n }\n\n const disconnect = async (): Promise => {\n setBusy(true)\n setError(null)\n try {\n await fetch('/plugins/data-agent/disconnect', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ sessionId }),\n })\n setConnected(false)\n setReadonly(false)\n setSchemaModalOpen(false)\n setSchemas([])\n setActiveSchema(null)\n setTables([])\n setActiveTable(null)\n setColumns(null)\n setSqlResult(null)\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setBusy(false)\n }\n }\n\n /** Toggle one schema's table list (single click; only one open at a time). */\n const toggleSchema = async (schema: string): Promise => {\n if (activeSchema === schema) {\n setActiveSchema(null)\n setTables([])\n setActiveTable(null)\n setColumns(null)\n return\n }\n setActiveSchema(schema)\n setActiveTable(null)\n setColumns(null)\n setTables([])\n try {\n const response = await fetch(\n `/plugins/data-agent/tables?sessionId=${encodeURIComponent(sessionId)}&schema=${encodeURIComponent(schema)}`,\n )\n const result = await response.json() as TablesResponse\n if (result.ok) setTables(result.tables ?? [])\n else setError(result.error ?? 'unknown error')\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n }\n }\n\n const selectTable = async (table: string): Promise => {\n setActiveTable(table)\n setColumns(null)\n const params = new URLSearchParams({ sessionId, table })\n if (activeSchema !== null) params.set('schema', activeSchema)\n try {\n const response = await fetch(`/plugins/data-agent/describe?${params.toString()}`)\n const result = await response.json() as DescribeResponse\n if (result.ok) setColumns(result.columns ?? [])\n else setError(result.error ?? 'unknown error')\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n }\n }\n\n const runSql = async (): Promise => {\n if (sql.trim() === '') return\n setSqlBusy(true)\n setSqlResult(null)\n try {\n const response = await fetch('/plugins/data-agent/query', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ sessionId, sql }),\n })\n const result = await response.json() as QueryResponse\n if (result.ok && result.result !== undefined) {\n const parts: string[] = []\n if (result.result.stdout !== '') parts.push(result.result.stdout)\n if (result.result.stderr !== '') parts.push(`[stderr]\\n${result.result.stderr}`)\n if (result.result.truncated) parts.push('… 输出超过上限,已截断')\n if (result.result.exitCode !== 0) parts.push(`[exit code: ${result.result.exitCode ?? 'signal'}]`)\n setSqlResult(parts.join('\\n'))\n } else {\n setSqlResult(`Error: ${result.error ?? 'unknown error'}`)\n }\n } catch (cause) {\n setSqlResult(`Error: ${cause instanceof Error ? cause.message : String(cause)}`)\n } finally {\n setSqlBusy(false)\n }\n }\n\n // A session not running the data-agent preset: nothing renders at all.\n if (!isDataAgent) return null\n\n const databaseLabel = sqlite\n ? t('form.database.sqlite')\n : type === 'oracle'\n ? t('form.database.oracle')\n : type === 'hive' || type === 'impala'\n ? t('form.database.hive')\n : t('form.database')\n\n const railStyle = split\n ? {\n position: 'fixed' as const,\n left: railRect!.left,\n top: railRect!.top + RAIL_TOP_OFFSET,\n bottom: railRect!.bottom,\n width: 380,\n }\n : undefined\n\n // Form fields are readonly while connected (edit requires disconnecting).\n const formDisabled = busy || connected\n\n return (\n \n
\n {connected ? (\n // ── connected summary row (config lives in a Modal now) ─────────\n
\n
\n \n {t('state.connected')}\n {t(`type.${type}`)}\n {database}\n \n \n \n \n
\n
\n ) : (\n // ── disconnected hero: a single connect entry, no inline form ────\n
\n \n
\n )}\n\n {connected && (\n // ── schema explorer entry ───────────────────────────────────────\n
\n \n
\n )}\n\n {connected && (\n
\n
\n \n {t('wb.sql')}\n
\n setSql(event.target.value)}\n onKeyDown={(event) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void runSql()\n }\n }}\n />\n
\n {t('wb.sql.shortcut')}\n { void runSql() }}\n >\n \n {sqlBusy ? t('wb.sql.running') : t('wb.sql.run')}\n \n
\n
{sqlResult ?? t('wb.sql.empty')}
\n
\n )}\n
\n\n {error !== null && !connectModalOpen && (\n
\n
\n \n {t('error.title')}\n
\n {error}\n
\n )}\n\n {schemaModalOpen && (\n setSchemaModalOpen(false)}\n title={t('wb.modal.title')}\n closeLabel={t('action.close')}\n className={css.schemaModal}\n >\n
\n
\n
\n {t('wb.schemas')}\n {schemas.length > 0 && {schemas.length}}\n
\n
\n {schemas.length === 0 &&
{t('wb.loading')}
}\n
    \n {schemas.map(schema => (\n
  • \n { void toggleSchema(schema) }}\n >\n \n \n {schema}\n {activeSchema === schema && tables.length > 0 && (\n {tables.length}\n )}\n \n {activeSchema === schema && (\n
    \n {tables.length === 0 &&
    {t('wb.empty')}
    }\n {tables.map(table => (\n { void selectTable(table) }}\n >\n \n {table}\n \n ))}\n
    \n )}\n
  • \n ))}\n
\n
\n
{t('wb.hint.click')}
\n
\n\n
\n
\n {t('wb.columns')}{activeTable !== null && ` · ${activeTable}`}\n
\n {columns === null ? (\n
\n \n {t('wb.hint.click')}\n
\n ) : (\n
\n \n \n \n \n \n {columns.map(column => (\n \n \n \n \n \n ))}\n \n
nametypenull
{column.name}{column.type}{column.nullable === undefined ? '' : column.nullable ? 'YES' : 'NO'}
\n
\n )}\n
\n
\n \n )}\n\n {connectModalOpen && (\n setConnectModalOpen(false)}\n title={t('form.title')}\n closeLabel={t('action.close')}\n className={css.connectModal}\n >\n
\n
\n \n\n {!sqlite && (\n
\n \n \n
\n )}\n\n {!sqlite && (\n
\n \n \n
\n )}\n\n {!sqlite && (\n \n )}\n\n \n\n \n
\n\n
\n {!connected ? (\n { void connect() }}\n >\n {restoring ? t('state.reconnecting') : busy ? t('state.checking') : t('action.connect')}\n \n ) : (\n \n )}\n
\n\n {error !== null && (\n
\n
\n \n {t('error.title')}\n
\n {error}\n
\n )}\n
\n \n )}\n \n )\n}\n","/** `data-agent` namespace dictionaries for the database workbench. */\n\n/** Dictionary namespace owned by this plugin. */\nexport const NS = 'data-agent'\n\n/** Simplified Chinese dictionary (the key-set source of truth). */\nexport const zh = {\n 'form.title': '数据库连接',\n 'form.hero.title': '连接数据库',\n 'form.hero.hint': '连接 MySQL / PostgreSQL / SQLite 等,开始与数据 Agent 对话',\n 'form.type': '数据库类型',\n 'type.mysql': 'MySQL',\n 'type.postgres': 'PostgreSQL',\n 'type.sqlite': 'SQLite',\n 'type.oracle': 'Oracle',\n 'type.hive': 'Hive',\n 'type.impala': 'Impala',\n 'type.clickhouse': 'ClickHouse',\n 'type.doris': 'Doris',\n 'type.sqlserver': 'SQL Server',\n 'form.host': '主机',\n 'form.port': '端口',\n 'form.user': '用户名',\n 'form.password': '密码',\n 'form.rememberPassword': '记住密码',\n 'form.rememberPassword.hint': '仅勾选后才会在本机保存密码',\n 'form.readonly': '只读模式',\n 'form.readonly.hint': '仅放行查询语句,拒绝增删改、建表等写操作',\n 'form.database': '数据库名',\n 'form.database.oracle': '服务名 / SID',\n 'form.database.hive': '默认库',\n 'form.database.impala': '默认库',\n 'form.database.sqlite': '数据库文件路径',\n 'form.database.sqlite.placeholder': '/path/to/orders.db',\n 'action.connect': '连接',\n 'action.disconnect': '断开',\n 'state.connected': '已连接',\n 'state.disconnected': '未连接',\n 'state.checking': '正在检查连接…',\n 'state.reconnecting': '正在恢复连接…',\n 'wb.schemas': '库',\n 'wb.tables': '表',\n 'wb.columns': '表结构',\n 'wb.sql': 'SQL 命令',\n 'wb.sql.placeholder': '在此输入 SQL,例如 SELECT * FROM orders LIMIT 10;',\n 'wb.sql.run': '运行',\n 'wb.sql.running': '运行中…',\n 'wb.sql.shortcut': 'Ctrl / ⌘ + Enter 运行',\n 'wb.sql.empty': '-- 尚无输出 --',\n 'wb.loading': '加载中…',\n 'wb.empty': '(空)',\n 'wb.modal.title': '库表浏览',\n 'wb.hint.click': '单击库展开表 · 点击表查看结构',\n 'action.config': '连接配置',\n 'action.browse': '库表',\n 'action.close': '关闭',\n 'error.title': '操作失败',\n} satisfies Record\n\n/** The data-agent namespace key union. */\nexport type DataAgentKey = keyof typeof zh\n\n/** English dictionary, checked complete against the zh key set. */\nexport const en = {\n 'form.title': 'Database connection',\n 'form.hero.title': 'Connect database',\n 'form.hero.hint': 'Connect MySQL / PostgreSQL / SQLite and start chatting with the data agent',\n 'form.type': 'Database type',\n 'type.mysql': 'MySQL',\n 'type.postgres': 'PostgreSQL',\n 'type.sqlite': 'SQLite',\n 'type.oracle': 'Oracle',\n 'type.hive': 'Hive',\n 'type.impala': 'Impala',\n 'type.clickhouse': 'ClickHouse',\n 'type.doris': 'Doris',\n 'type.sqlserver': 'SQL Server',\n 'form.host': 'Host',\n 'form.port': 'Port',\n 'form.user': 'User',\n 'form.password': 'Password',\n 'form.rememberPassword': 'Remember password',\n 'form.rememberPassword.hint': 'Saved locally only when checked',\n 'form.readonly': 'Read-only mode',\n 'form.readonly.hint': 'Allow read queries only; reject writes (INSERT/UPDATE/DELETE/DDL)',\n 'form.database': 'Database',\n 'form.database.oracle': 'Service name / SID',\n 'form.database.hive': 'Default database',\n 'form.database.impala': 'Default database',\n 'form.database.sqlite': 'Database file path',\n 'form.database.sqlite.placeholder': '/path/to/orders.db',\n 'action.connect': 'Connect',\n 'action.disconnect': 'Disconnect',\n 'state.connected': 'Connected',\n 'state.disconnected': 'Not connected',\n 'state.checking': 'Checking connection…',\n 'state.reconnecting': 'Restoring connection…',\n 'wb.schemas': 'Databases',\n 'wb.tables': 'Tables',\n 'wb.columns': 'Columns',\n 'wb.sql': 'SQL',\n 'wb.sql.placeholder': 'Write SQL here, e.g. SELECT * FROM orders LIMIT 10;',\n 'wb.sql.run': 'Run',\n 'wb.sql.running': 'Running…',\n 'wb.sql.shortcut': 'Ctrl / ⌘ + Enter to run',\n 'wb.sql.empty': '-- no output --',\n 'wb.loading': 'Loading…',\n 'wb.empty': '(empty)',\n 'wb.modal.title': 'Database explorer',\n 'wb.hint.click': 'Click a database to expand tables · click a table for columns',\n 'action.config': 'Connection settings',\n 'action.browse': 'Tables',\n 'action.close': 'Close',\n 'error.title': 'Operation failed',\n} satisfies Record\n","/**\n * Data Agent browser half, plugin entry: registers the database workbench\n * into the composer input dock (the strip ABOVE the input bar) for\n * data-agent sessions, and the `data-agent` dictionaries. The old\n * conversation-view tab is gone — the workbench lives inside the session.\n * Connection state lives in the server-side connection store, so layout and\n * session switches never lose it — the view only mirrors what\n * `/plugins/data-agent/status` reports.\n * @module @yejiming/dsh-data-agent/client\n */\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\n// Type-only: pulls the locale plugin's Context merge (ctx.locale) into this program.\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls the ui-conversation slot declarations (conversation.input.dock)\n// and the session standard props (sessionId) into this program.\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport { DataAgentWorkbench, type SessionListLike } from './DataAgentWorkbench.tsx'\nimport { NS, en, zh, type DataAgentKey } from './locales.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The database workbench copy. */\n 'data-agent': DataAgentKey\n }\n}\n\n/** Required services: the locale service, the slot registry, and the sessions list. */\nexport const inject = ['slots', 'locale', 'sessions']\n\n/**\n * Client plugin body: register the data-agent dictionaries and the database\n * workbench into the composer input dock. The registration rides the slot\n * service's effect wrapper, so plugin unload removes it.\n * @param ctx - client root context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'data-agent: dictionaries')\n const t = ctx.locale.bind(NS)\n ctx.inject(['slots', 'locale', 'sessions'], (scope: ClientContext) => {\n const list = scope.sessions.list\n // The sessions list is the agent-preset authority on the client: the\n // ui-agent-preset surface records `agentPreset` onto each session summary.\n const sessionsSource = {\n getSnapshot: (): SessionListLike => list.getSnapshot() as unknown as SessionListLike,\n subscribe: (fn: () => void): (() => void) => list.subscribe(fn),\n }\n // The workbench strip sits above the input bar (order 0, first entry);\n // it renders only for data-agent sessions and returns null elsewhere.\n scope.slots.inject('conversation.input.dock', () => scope.slots.register({\n name: 'conversation.input.dock',\n id: 'data-agent',\n order: 0,\n locale: NS,\n inject: () => ({ hooks: { sessions: sessionsSource } }),\n }, DataAgentWorkbench))\n })\n}\n"],"mappings":";;;;;;;;;;;EAeA,MAAa,yBAAyB;;EAyBtC,SAAS,iBAA0C;GACjD,IAAI;IACF,IAAI,OAAO,iBAAiB,aAAa;KACvC,MAAM,QAAQ;KACd,aAAa,QAAQ,OAAO,GAAG;KAC/B,aAAa,WAAW,KAAK;KAC7B,OAAO;IACT;GACF,QAAQ,CAER;EAEF;;EAGA,SAAS,WAAW,OAAwC;GAC1D,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG,OAAO;GAChF,MAAM,YAAY;GAClB,MAAM,OAAO,UAAU;GACvB,IAAI,SAAS,WAAW,SAAS,cAAc,SAAS,YACnD,SAAS,YAAY,SAAS,UAAU,SAAS,YACjD,SAAS,gBAAgB,SAAS,WAAW,SAAS,aACzD,OAAO;GAET,MAAM,WAAW,UAAU;GAE3B,IAAI,OAAO,aAAa,UAAU,OAAO;GACzC,MAAM,QAAyB;IAAE;IAAM;IAAU,SAAS,OAAO,UAAU,YAAY,WAAW,UAAU,UAAU;GAAG;GACzH,IAAI,OAAO,UAAU,SAAS,UAAU,MAAM,OAAO,UAAU;GAC/D,IAAI,OAAO,UAAU,SAAS,YAAY,OAAO,UAAU,UAAU,IAAI,GAAG,MAAM,OAAO,UAAU;GACnG,IAAI,OAAO,UAAU,SAAS,UAAU,MAAM,OAAO,UAAU;GAG/D,MAAM,kBAAkB,UAAU,oBAAoB;GACtD,IAAI,iBAAiB,MAAM,kBAAkB;GAC7C,IAAI,mBAAmB,OAAO,UAAU,aAAa,UAAU,MAAM,WAAW,UAAU;GAC1F,OAAO;EACT;;EAGA,SAAgB,eAAe,YAA6B,UAAmC,eAAe,GAAS;GACrH,IAAI,YAAY,KAAA,GAAW;GAC3B,IAAI;IACF,MAAM,UAA2B,EAAE,GAAG,WAAW;IAEjD,IAAI,QAAQ,oBAAoB,MAAM,OAAO,QAAQ;IACrD,QAAQ,QAAQ,wBAAwB,KAAK,UAAU,OAAO,CAAC;GACjE,QAAQ,CAER;EACF;;EAGA,SAAgB,eAAe,UAAmC,eAAe,GAA2B;GAC1G,IAAI,YAAY,KAAA,GAAW,OAAO;GAClC,IAAI;IACF,MAAM,MAAM,QAAQ,QAAQ,sBAAsB;IAClD,IAAI,QAAQ,MAAM,OAAO;IACzB,OAAO,WAAW,KAAK,MAAM,GAAG,CAAC;GACnC,QAAQ;IACN,OAAO;GACT;EACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EC5DA,MAAM,oBAAoB;;EAG1B,SAAS,KAAK,EAAE,WAAW,UAAU,OAAO,MAAkE;GAC5G,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;IAAgB;IAAW,OAAO;IAAM,QAAQ;IAAM,SAAQ;IAAY,MAAK;IAAO,eAAY;IAC/F;GACE,CAAA;EAET;;EAGA,SAAS,aAAa,EAAE,aAAqC;GAC3D,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,MAAD;IAAiB;IAAjB,UAAA;KACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;MAAS,IAAG;MAAI,IAAG;MAAI,IAAG;MAAM,IAAG;MAAM,QAAO;MAAe,aAAY;KAAO,CAAA;KAClF,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,GAAE;MAAuE,QAAO;MAAe,aAAY;KAAO,CAAA;KACxH,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,GAAE;MAA+D,QAAO;MAAe,aAAY;KAAO,CAAA;IAC5G;;EAEV;;EAGA,SAAS,UAAU,EAAE,aAAqC;GACxD,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,MAAD;IAAiB;IAAjB,UAAA;KACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,GAAE;MAAI,GAAE;MAAI,OAAM;MAAK,QAAO;MAAK,IAAG;MAAM,QAAO;MAAe,aAAY;KAAO,CAAA;KAC3F,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,GAAE;MAAY,QAAO;MAAe,aAAY;KAAO,CAAA;KAC7D,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,GAAE;MAAc,QAAO;MAAe,aAAY;KAAO,CAAA;IAC3D;;EAEV;;EAGA,SAAS,WAAW,EAAE,aAAqC;GACzD,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD;IAAiB;IACf,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;KACE,GAAE;KACF,QAAO;KACP,aAAY;KACZ,gBAAe;IAChB,CAAA;GACG,CAAA;EAEV;;EAGA,SAAS,aAAa,EAAE,aAAqC;GAC3D,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,MAAD;IAAiB;IAAjB,UAAA;KACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,GAAE;MAAM,GAAE;MAAM,OAAM;MAAK,QAAO;MAAK,IAAG;MAAI,QAAO;MAAe,aAAY;KAAO,CAAA;KAC7F,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,GAAE;MAAsB,QAAO;MAAe,aAAY;MAAM,eAAc;MAAQ,gBAAe;KAAS,CAAA;KACpH,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,GAAE;MAAe,QAAO;MAAe,aAAY;MAAM,eAAc;KAAS,CAAA;IAClF;;EAEV;;EAGA,SAAS,YAAY,EAAE,aAAqC;GAC1D,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;IAAgB;IAAW,OAAM;IAAK,QAAO;IAAK,SAAQ;IAAY,MAAK;IAAO,eAAY;IAC5F,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;KAAM,GAAE;KAAuB,QAAO;KAAe,aAAY;KAAM,eAAc;KAAQ,gBAAe;IAAS,CAAA;GAClH,CAAA;EAET;;EAGA,SAAS,UAAU,EAAE,aAAqC;GACxD,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,MAAD;IAAiB;IAAjB,UAAA;KACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,GAAE;MAA4B,QAAO;MAAe,aAAY;MAAM,gBAAe;KAAS,CAAA;KACpG,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,GAAE;MAAa,QAAO;MAAe,aAAY;MAAM,eAAc;KAAS,CAAA;KACpF,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;MAAQ,IAAG;MAAI,IAAG;MAAK,GAAE;MAAM,MAAK;KAAgB,CAAA;IAChD;;EAEV;;EAGA,SAAS,SAAS,EAAE,aAAqC;GACvD,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;IAAgB;IAAW,OAAM;IAAK,QAAO;IAAK,SAAQ;IAAY,MAAK;IAAe,eAAY;IACpG,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAM,GAAE,wHAAyH,CAAA;GAC9H,CAAA;EAET;;EAkEA,SAAS,YAAY,SAAsC;GACzD,OAAO,SAAS,aAAa,YAAY,MAAM;EACjD;;;;;EAMA,MAAM,kBAAkB;;EAGxB,SAAS,cAAc,MAA4B;GACjD,QAAQ,MAAR;IACE,KAAK,YAAY,OAAO;IACxB,KAAK,UAAU,OAAO;IACtB,KAAK,QAAQ,OAAO;IACpB,KAAK,UAAU,OAAO;IACtB,KAAK,cAAc,OAAO;IAC1B,KAAK,SAAS,OAAO;IACrB,KAAK,aAAa,OAAO;IACzB,KAAK,UAAU,OAAO;IACtB,KAAK,SAAS,OAAO;GACvB;EACF;;EAGA,eAAe,eAAe,WAAmB,MAAyD;GAMxG,QAAO,MALgB,MAAM,+BAA+B;IAC1D,QAAQ;IACR,SAAS,EAAE,gBAAgB,mBAAmB;IAC9C,MAAM,KAAK,UAAU;KAAE;KAAW,GAAG;IAAK,CAAC;GAC7C,CAAC,EAAA,CACe,KAAK;EACvB;;EAGA,SAAS,iBAAiB,OAAiD;GACzE,IAAI,MAAM,SAAS,UAAU,OAAO;IAAE,MAAM,MAAM;IAAM,UAAU,MAAM;GAAS;GACjF,MAAM,OAAgC;IACpC,MAAM,MAAM;IACZ,MAAM,MAAM,QAAQ;IACpB,MAAM,MAAM,QAAQ;IACpB,UAAU,MAAM;GAClB;GACA,IAAI,MAAM,SAAS,KAAA,GAAW,KAAK,OAAO,MAAM;GAChD,IAAI,MAAM,aAAa,KAAA,KAAa,MAAM,aAAa,IAAI,KAAK,WAAW,MAAM;GACjF,OAAO;EACT;;EAGA,SAAgB,mBAAmB,EAAE,WAAW,aAAa,KAA8B;GAEzF,MAAM,cADO,aAAY,aAAY,QACd,CAAC,CAAC,KAAK,UAAmB,EAAE,gBAAgB;GAEnE,MAAM,WAAA,GAAUA,MAAAA,OAAAA,CAA8B,IAAI;GAClD,MAAM,CAAC,OAAO,aAAA,GAAYC,MAAAA,SAAAA,CAA4B,MAAM;GAC5D,MAAM,CAAC,UAAU,gBAAA,GAAeA,MAAAA,SAAAA,CAA+D,IAAI;GAGnG,MAAM,CAAC,iBAAA,GAAgBA,MAAAA,SAAAA,CAAS,cAAc;GAC9C,MAAM,CAAC,MAAM,YAAA,GAAWA,MAAAA,SAAAA,CAAuB,cAAc,QAAQ,OAAO;GAC5E,MAAM,CAAC,MAAM,YAAA,GAAWA,MAAAA,SAAAA,CAAS,cAAc,QAAQ,WAAW;GAClE,MAAM,CAAC,MAAM,YAAA,GAAWA,MAAAA,SAAAA,CACtB,cAAc,SAAS,KAAA,IAAY,OAAO,aAAa,IAAI,IAAI,cAAc,cAAc,QAAQ,OAAO,CAC5G;GACA,MAAM,CAAC,MAAM,YAAA,GAAWA,MAAAA,SAAAA,CAAS,cAAc,QAAQ,EAAE;GACzD,MAAM,CAAC,UAAU,gBAAA,GAAeA,MAAAA,SAAAA,CAAS,cAAc,YAAY,EAAE;GACrE,MAAM,CAAC,kBAAkB,wBAAA,GAAuBA,MAAAA,SAAAA,CAAS,cAAc,oBAAoB,IAAI;GAC/F,MAAM,CAAC,UAAU,gBAAA,GAAeA,MAAAA,SAAAA,CAAS,KAAK;GAC9C,MAAM,CAAC,UAAU,gBAAA,GAAeA,MAAAA,SAAAA,CAAS,cAAc,YAAY,EAAE;GACrE,MAAM,CAAC,MAAM,YAAA,GAAWA,MAAAA,SAAAA,CAAS,KAAK;GACtC,MAAM,CAAC,OAAO,aAAA,GAAYA,MAAAA,SAAAA,CAAwB,IAAI;GACtD,MAAM,CAAC,WAAW,iBAAA,GAAgBA,MAAAA,SAAAA,CAAS,KAAK;GAGhD,MAAM,CAAC,kBAAkB,wBAAA,GAAuBA,MAAAA,SAAAA,CAAS,KAAK;GAE9D,MAAM,CAAC,WAAW,iBAAA,GAAgBA,MAAAA,SAAAA,CAAS,KAAK;GAEhD,MAAM,CAAC,iBAAiB,uBAAA,GAAsBA,MAAAA,SAAAA,CAAS,KAAK;GAE5D,MAAM,CAAC,SAAS,eAAA,GAAcA,MAAAA,SAAAA,CAAmB,CAAC,CAAC;GACnD,MAAM,CAAC,cAAc,oBAAA,GAAmBA,MAAAA,SAAAA,CAAwB,IAAI;GACpE,MAAM,CAAC,QAAQ,cAAA,GAAaA,MAAAA,SAAAA,CAAmB,CAAC,CAAC;GACjD,MAAM,CAAC,aAAa,mBAAA,GAAkBA,MAAAA,SAAAA,CAAwB,IAAI;GAClE,MAAM,CAAC,SAAS,eAAA,GAAcA,MAAAA,SAAAA,CAA8B,IAAI;GAEhE,MAAM,CAAC,KAAK,WAAA,GAAUA,MAAAA,SAAAA,CAAS,EAAE;GACjC,MAAM,CAAC,SAAS,eAAA,GAAcA,MAAAA,SAAAA,CAAS,KAAK;GAC5C,MAAM,CAAC,WAAW,iBAAA,GAAgBA,MAAAA,SAAAA,CAAwB,IAAI;GAI9D,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,MAAM,SAAS,QAAQ,SAAS,QAAqB,cAAc,KAAK;IACxE,IAAI,WAAW,MAAM;IACrB,MAAM,gBAAsB;KAC1B,MAAM,OAAO,OAAO,sBAAsB;KAC1C,YAAY;MAAE,MAAM,KAAK;MAAM,KAAK,KAAK;MAAK,QAAQ,OAAO,cAAc,KAAK;KAAO,CAAC;IAC1F;IACA,MAAM,qBAA2B;KAAE,SAAS,YAAY,MAAM,IAAI,SAAS,QAAQ;IAAE;IACrF,aAAa;IACb,QAAQ;IACR,MAAM,WAAW,IAAI,iBAAiB,YAAY;IAClD,SAAS,QAAQ,QAAQ;KAAE,YAAY;KAAM,iBAAiB,CAAC,YAAY;IAAE,CAAC;IAC9E,MAAM,UAAU,IAAI,eAAe,OAAO;IAC1C,QAAQ,QAAQ,MAAM;IACtB,OAAO,iBAAiB,UAAU,OAAO;IACzC,aAAa;KACX,SAAS,WAAW;KACpB,QAAQ,WAAW;KACnB,OAAO,oBAAoB,UAAU,OAAO;IAC9C;GACF,GAAG,CAAC,CAAC;GAIL,MAAM,QAAQ,UAAU,YAAY,aAAa;GACjD,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,MAAM,OAAO,SAAS;IACtB,MAAM,aAAaC,sCAAI;IACvB,IAAI,SAAS,eAAe,KAAA,GAAW,KAAK,UAAU,IAAI,UAAU;SAC/D,IAAI,eAAe,KAAA,GAAW,KAAK,UAAU,OAAO,UAAU;IACnE,aAAa;KAAE,IAAI,eAAe,KAAA,GAAW,KAAK,UAAU,OAAO,UAAU;IAAE;GACjF,GAAG,CAAC,KAAK,CAAC;GAKV,MAAM,iBAAA,GAAgBF,MAAAA,OAAAA,CAAO,IAAI;GACjC,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,cAAc,SAAS;KACzB,cAAc,UAAU;KACxB;IACF;IAUA,eAAe;KARb;KACA;KACA,GAAG,SAAS,WAAW;MAAE;MAAM;KAAK,IAAI,CAAC;KACzC,GAAG,SAAS,YAAY,SAAS,KAAK,EAAE,MAAM,OAAO,IAAI,EAAE,IAAI,CAAC;KAChE,GAAG,aAAa,KAAK,EAAE,SAAS,IAAI,CAAC;KACrC,GAAG,mBAAmB,EAAE,iBAAiB,KAAK,IAAI,CAAC;KACnD,0BAAS,IAAI,KAAK,EAAA,CAAE,YAAY;IAEnB,CAAK;GACtB,GAAG;IAAC;IAAM;IAAM;IAAM;IAAM;IAAU;IAAU;GAAgB,CAAC;GAIjE,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,YAAY;IAChB,MAAM,QAAQ;IACd,QAAQ,IAAI;IACZ,MAAM,wCAAwC,mBAAmB,SAAS,GAAG,CAAC,CAC3E,MAAK,aAAY,SAAS,KAAK,CAA4B,CAAC,CAC5D,KAAK,OAAO,SAAS;KACpB,IAAI,WAAW;KACf,IAAI,KAAK,WAAW;MAClB,aAAa,IAAI;MACjB,IAAI,KAAK,YAAY,KAAA,GAAW;OAC9B,QAAQ,KAAK,QAAQ,IAAI;OACzB,QAAQ,KAAK,QAAQ,QAAQ,EAAE;OAC/B,QAAQ,KAAK,QAAQ,SAAS,KAAA,IAAY,OAAO,KAAK,QAAQ,IAAI,IAAI,EAAE;OACxE,QAAQ,KAAK,QAAQ,QAAQ,EAAE;OAC/B,YAAY,KAAK,QAAQ,QAAQ;OACjC,YAAY,KAAK,QAAQ,aAAa,IAAI;MAC5C;MAEA,MAAM,cAAc,OAAM,MADH,MAAM,yCAAyC,mBAAmB,SAAS,GAAG,EAAA,CAClE,KAAK;MACxC,IAAI,CAAC,aAAa,YAAY,IAAI,WAAW,YAAY,WAAW,CAAC,CAAC;MACtE;KACF;KAGA,IAAI,UAAU,QAAQ,MAAM,aAAa,IAAI;MAC3C,aAAa,IAAI;MACjB,IAAI;OACF,MAAM,SAAS,MAAM,eAAe,WAAW,iBAAiB,KAAK,CAAC;OACtE,IAAI,WAAW;OACf,IAAI,OAAO,IAAI;QACb,aAAa,IAAI;QACjB,oBAAoB,KAAK;QAEzB,MAAM,cAAc,OAAM,MADH,MAAM,yCAAyC,mBAAmB,SAAS,GAAG,EAAA,CAClE,KAAK;QACxC,IAAI,CAAC,aAAa,YAAY,IAAI,WAAW,YAAY,WAAW,CAAC,CAAC;OACxE,OACE,SAAS,UAAU,OAAO,SAAS,iBAAiB;MAExD,SAAS,OAAO;OACd,IAAI,CAAC,WACH,SAAS,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAAG;MAE/E,UAAU;OACR,IAAI,CAAC,WAAW,aAAa,KAAK;MACpC;KACF;IACF,CAAC,CAAC,CACD,YAAY,CAA2D,CAAC,CAAC,CACzE,cAAc;KAAE,IAAI,CAAC,WAAW,QAAQ,KAAK;IAAE,CAAC;IACnD,aAAa;KAAE,YAAY;IAAK;GAClC,GAAG,CAAC,SAAS,CAAC;GAEd,MAAM,SAAS,SAAS;GAExB,MAAM,UAAU,YAA2B;IACzC,QAAQ,IAAI;IACZ,SAAS,IAAI;IACb,MAAM,OAAgC;KAAE;KAAW;KAAM;IAAS;IAClE,IAAI,QACF,KAAK,WAAW;SACX;KACL,KAAK,OAAO;KACZ,IAAI,SAAS,IAAI,KAAK,OAAO,OAAO,IAAI;KACxC,KAAK,OAAO;KACZ,KAAK,WAAW;KAChB,IAAI,aAAa,IAAI,KAAK,WAAW;IACvC;IACA,IAAI;KACF,MAAM,SAAS,MAAM,eAAe,WAAW,IAAI;KACnD,IAAI,OAAO,IAAI;MACb,aAAa,IAAI;MAEjB,oBAAoB,KAAK;MAEzB,MAAM,cAAc,OAAM,MADI,MAAM,yCAAyC,mBAAmB,SAAS,GAAG,EAAA,CAClE,KAAK;MAC/C,IAAI,YAAY,IAAI,WAAW,YAAY,WAAW,CAAC,CAAC;KAC1D,OACE,SAAS,OAAO,SAAS,eAAe;IAE5C,SAAS,OAAO;KACd,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;IACjE,UAAU;KACR,QAAQ,KAAK;IACf;GACF;GAEA,MAAM,aAAa,YAA2B;IAC5C,QAAQ,IAAI;IACZ,SAAS,IAAI;IACb,IAAI;KACF,MAAM,MAAM,kCAAkC;MAC5C,QAAQ;MACR,SAAS,EAAE,gBAAgB,mBAAmB;MAC9C,MAAM,KAAK,UAAU,EAAE,UAAU,CAAC;KACpC,CAAC;KACD,aAAa,KAAK;KAClB,YAAY,KAAK;KACjB,mBAAmB,KAAK;KACxB,WAAW,CAAC,CAAC;KACb,gBAAgB,IAAI;KACpB,UAAU,CAAC,CAAC;KACZ,eAAe,IAAI;KACnB,WAAW,IAAI;KACf,aAAa,IAAI;IACnB,SAAS,OAAO;KACd,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;IACjE,UAAU;KACR,QAAQ,KAAK;IACf;GACF;;GAGA,MAAM,eAAe,OAAO,WAAkC;IAC5D,IAAI,iBAAiB,QAAQ;KAC3B,gBAAgB,IAAI;KACpB,UAAU,CAAC,CAAC;KACZ,eAAe,IAAI;KACnB,WAAW,IAAI;KACf;IACF;IACA,gBAAgB,MAAM;IACtB,eAAe,IAAI;IACnB,WAAW,IAAI;IACf,UAAU,CAAC,CAAC;IACZ,IAAI;KAIF,MAAM,SAAS,OAAM,MAHE,MACrB,wCAAwC,mBAAmB,SAAS,EAAE,UAAU,mBAAmB,MAAM,GAC3G,EAAA,CAC8B,KAAK;KACnC,IAAI,OAAO,IAAI,UAAU,OAAO,UAAU,CAAC,CAAC;UACvC,SAAS,OAAO,SAAS,eAAe;IAC/C,SAAS,OAAO;KACd,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;IACjE;GACF;GAEA,MAAM,cAAc,OAAO,UAAiC;IAC1D,eAAe,KAAK;IACpB,WAAW,IAAI;IACf,MAAM,SAAS,IAAI,gBAAgB;KAAE;KAAW;IAAM,CAAC;IACvD,IAAI,iBAAiB,MAAM,OAAO,IAAI,UAAU,YAAY;IAC5D,IAAI;KAEF,MAAM,SAAS,OAAM,MADE,MAAM,gCAAgC,OAAO,SAAS,GAAG,EAAA,CAClD,KAAK;KACnC,IAAI,OAAO,IAAI,WAAW,OAAO,WAAW,CAAC,CAAC;UACzC,SAAS,OAAO,SAAS,eAAe;IAC/C,SAAS,OAAO;KACd,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;IACjE;GACF;GAEA,MAAM,SAAS,YAA2B;IACxC,IAAI,IAAI,KAAK,MAAM,IAAI;IACvB,WAAW,IAAI;IACf,aAAa,IAAI;IACjB,IAAI;KAMF,MAAM,SAAS,OAAM,MALE,MAAM,6BAA6B;MACxD,QAAQ;MACR,SAAS,EAAE,gBAAgB,mBAAmB;MAC9C,MAAM,KAAK,UAAU;OAAE;OAAW;MAAI,CAAC;KACzC,CAAC,EAAA,CAC6B,KAAK;KACnC,IAAI,OAAO,MAAM,OAAO,WAAW,KAAA,GAAW;MAC5C,MAAM,QAAkB,CAAC;MACzB,IAAI,OAAO,OAAO,WAAW,IAAI,MAAM,KAAK,OAAO,OAAO,MAAM;MAChE,IAAI,OAAO,OAAO,WAAW,IAAI,MAAM,KAAK,aAAa,OAAO,OAAO,QAAQ;MAC/E,IAAI,OAAO,OAAO,WAAW,MAAM,KAAK,cAAc;MACtD,IAAI,OAAO,OAAO,aAAa,GAAG,MAAM,KAAK,eAAe,OAAO,OAAO,YAAY,SAAS,EAAE;MACjG,aAAa,MAAM,KAAK,IAAI,CAAC;KAC/B,OACE,aAAa,UAAU,OAAO,SAAS,iBAAiB;IAE5D,SAAS,OAAO;KACd,aAAa,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAAG;IACjF,UAAU;KACR,WAAW,KAAK;IAClB;GACF;GAGA,IAAI,CAAC,aAAa,OAAO;GAEzB,MAAM,gBAAgB,SAClB,EAAE,sBAAsB,IACxB,SAAS,WACP,EAAE,sBAAsB,IACxB,SAAS,UAAU,SAAS,WAC1B,EAAE,oBAAoB,IACtB,EAAE,eAAe;GAEzB,MAAM,YAAY,QACd;IACA,UAAU;IACV,MAAM,SAAU;IAChB,KAAK,SAAU,MAAM;IACrB,QAAQ,SAAU;IAClB,OAAO;GACT,IACE,KAAA;GAGJ,MAAM,eAAe,QAAQ;GAE7B,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;IACE,KAAK;IACL,WAAW,GAAGE,sCAAI,UAAU,GAAG,QAAQA,sCAAI,OAAO,UAAU,WAAWA,sCAAI,SAASA,sCAAI;IACxF,OAAO;IAHT,UAAA;KAKE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAWA,sCAAI;MAApB,UAAA;OACG,YAEC,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;QAAS,WAAWA,sCAAI;QACtB,UAAA,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAAK,WAAWA,sCAAI;SAApB,UAAA;UACE,iBAAA,GAAA,kBAAA,IAAA,CAACC,sCAAAA,UAAD;WAAU,OAAM;WAAO,MAAM;WAAG,WAAWD,sCAAI;UAAM,CAAA;UACrD,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;WAAM,WAAWA,sCAAI;WAAW,UAAA,EAAE,iBAAiB;UAAQ,CAAA;UAC3D,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;WAAM,WAAWA,sCAAI;WAAc,UAAA,EAAE,QAAQ,MAAM;UAAQ,CAAA;UAC3D,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;WAAM,WAAWA,sCAAI;WAAW,OAAO;WAAW,UAAA;UAAe,CAAA;UACjE,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD;WAAM,WAAWA,sCAAI;WAArB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;YAAQ,MAAK;YAAS,WAAW,GAAGA,sCAAI,MAAM,GAAGA,sCAAI;YAAS,eAAe,oBAAoB,IAAI;YAClG,UAAA,EAAE,eAAe;WACZ,CAAA,GACR,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;YAAQ,MAAK;YAAS,WAAW,GAAGA,sCAAI,MAAM,GAAGA,sCAAI;YAAS,UAAU;YAAM,eAAe;aAAE,WAAgB;YAAE;YAC9G,UAAA,EAAE,mBAAmB;WAChB,CAAA,CACJ;;SACH;;OACE,CAAA,IAGT,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;QAAS,WAAWA,sCAAI;QACtB,UAAA,iBAAA,GAAA,kBAAA,KAAA,CAAC,UAAD;SAAQ,MAAK;SAAS,WAAWA,sCAAI;SAAa,eAAe,oBAAoB,IAAI;SAAzF,UAAA;UACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,cAAD,EAAc,WAAWA,sCAAI,SAAW,CAAA;UACxC,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD;WAAM,WAAWA,sCAAI;WAArB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;YAAM,WAAWA,sCAAI;YAAY,UAAA,EAAE,iBAAiB;WAAQ,CAAA,GAC5D,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;YAAM,WAAWA,sCAAI;YAAW,UAAA,EAAE,gBAAgB;WAAQ,CAAA,CACtD;;UACN,iBAAA,GAAA,kBAAA,IAAA,CAAC,aAAD,EAAa,WAAWA,sCAAI,YAAc,CAAA;SACpC;;OACD,CAAA;OAGV,aAEC,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;QAAS,WAAWA,sCAAI;QACtB,UAAA,iBAAA,GAAA,kBAAA,KAAA,CAAC,UAAD;SAAQ,MAAK;SAAS,WAAWA,sCAAI;SAAW,eAAe,mBAAmB,IAAI;SAAtF,UAAA;UACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD,EAAW,WAAWA,sCAAI,WAAa,CAAA;UACvC,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAA,UAAO,EAAE,eAAe,EAAQ,CAAA;UAChC,iBAAA,GAAA,kBAAA,IAAA,CAAC,aAAD,EAAa,WAAWA,sCAAI,cAAgB,CAAA;SACtC;;OACD,CAAA;OAGV,aACD,iBAAA,GAAA,kBAAA,KAAA,CAAC,WAAD;QAAS,WAAWA,sCAAI;QAAxB,UAAA;SACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;UAAK,WAAWA,sCAAI;UAApB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,cAAD,EAAc,WAAWA,sCAAI,UAAY,CAAA,GACzC,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAA,UAAO,EAAE,QAAQ,EAAQ,CAAA,CACtB;;SACL,iBAAA,GAAA,kBAAA,IAAA,CAAC,YAAD;UACE,WAAWA,sCAAI;UACf,OAAO;UACP,MAAM;UACN,YAAY;UACZ,aAAa,EAAE,oBAAoB;UACnC,WAAW,UAAU,OAAO,MAAM,OAAO,KAAK;UAC9C,YAAY,UAAU;WACpB,KAAK,MAAM,WAAW,MAAM,YAAY,MAAM,QAAQ,SAAS;YAC7D,MAAM,eAAe;YACrB,OAAY;WACd;UACF;SACD,CAAA;SACD,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;UAAK,WAAWA,sCAAI;UAApB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;WAAM,WAAWA,sCAAI;WAAe,UAAA,EAAE,iBAAiB;UAAQ,CAAA,GAC/D,iBAAA,GAAA,kBAAA,KAAA,CAAC,UAAD;WACE,MAAK;WACL,WAAWA,sCAAI;WACf,UAAU,WAAW,CAAC,aAAa,IAAI,KAAK,MAAM;WAClD,eAAe;YAAE,OAAY;WAAE;WAJjC,UAAA,CAME,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD,CAAW,CAAA,GACV,UAAU,EAAE,gBAAgB,IAAI,EAAE,YAAY,CACzC;UACL,CAAA,CAAA;;SACL,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;UAAK,WAAWA,sCAAI;UAAY,UAAA,aAAa,EAAE,cAAc;SAAO,CAAA;QAC7D;;MAEN;;KAEJ,UAAU,QAAQ,CAAC,oBAClB,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAWA,sCAAI;MAApB,UAAA,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;OAAK,WAAWA,sCAAI;OAApB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD,EAAW,WAAWA,sCAAI,UAAY,CAAA,GACtC,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAA,UAAO,EAAE,aAAa,EAAQ,CAAA,CAC3B;MACL,CAAA,GAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;OAAM,WAAWA,sCAAI;OAAY,UAAA;MAAY,CAAA,CAC1C;;KAGN,mBACC,iBAAA,GAAA,kBAAA,IAAA,CAACE,sCAAAA,OAAD;MACE,MAAM;MACN,eAAe,mBAAmB,KAAK;MACvC,OAAO,EAAE,gBAAgB;MACzB,YAAY,EAAE,cAAc;MAC5B,WAAWF,sCAAI;MAEf,UAAA,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;OAAK,WAAWA,sCAAI;OAApB,UAAA,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;QAAK,WAAWA,sCAAI;QAApB,UAAA;SACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;UAAK,WAAWA,sCAAI;UAApB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAA,UAAO,EAAE,YAAY,EAAQ,CAAA,GAC5B,QAAQ,SAAS,KAAK,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;WAAM,WAAWA,sCAAI;WAAW,UAAA,QAAQ;UAAa,CAAA,CACzE;;SACL,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;UAAK,WAAWA,sCAAI;UAApB,UAAA,CACG,QAAQ,WAAW,KAAK,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;WAAK,WAAWA,sCAAI;WAAO,UAAA,EAAE,YAAY;UAAO,CAAA,GACzE,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD;WAAI,WAAWA,sCAAI;WAChB,UAAA,QAAQ,KAAI,WACX,iBAAA,GAAA,kBAAA,KAAA,CAAC,MAAD;YAAiB,WAAWA,sCAAI;YAAhC,UAAA,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,UAAD;aACE,MAAK;aACL,WAAW,GAAGA,sCAAI,WAAW,iBAAiB,SAAS,IAAIA,sCAAI,WAAW;aAC1E,eAAe;cAAE,aAAkB,MAAM;aAAE;aAH7C,UAAA;cAKE,iBAAA,GAAA,kBAAA,IAAA,CAAC,aAAD,EAAa,WAAW,GAAGA,sCAAI,cAAc,iBAAiB,SAAS,IAAIA,sCAAI,SAAS,KAAO,CAAA;cAC/F,iBAAA,GAAA,kBAAA,IAAA,CAAC,YAAD,EAAY,WAAWA,sCAAI,SAAW,CAAA;cACtC,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;eAAM,WAAWA,sCAAI;eAAW,UAAA;cAAa,CAAA;cAC5C,iBAAiB,UAAU,OAAO,SAAS,KAC1C,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;eAAM,WAAWA,sCAAI;eAAY,UAAA,OAAO;cAAa,CAAA;aAEjD;YACP,CAAA,GAAA,iBAAiB,UAChB,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;aAAK,WAAWA,sCAAI;aAApB,UAAA,CACG,OAAO,WAAW,KAAK,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;cAAK,WAAWA,sCAAI;cAAO,UAAA,EAAE,UAAU;aAAO,CAAA,GACrE,OAAO,KAAI,UACV,iBAAA,GAAA,kBAAA,KAAA,CAAC,UAAD;cAEE,MAAK;cACL,WAAW,GAAGA,sCAAI,WAAW,gBAAgB,QAAQ,IAAIA,sCAAI,WAAW;cACxE,eAAe;eAAE,YAAiB,KAAK;cAAE;cAJ3C,UAAA,CAME,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD,EAAW,WAAWA,sCAAI,SAAW,CAAA,GACrC,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;eAAM,WAAWA,sCAAI;eAAW,UAAA;cAAY,CAAA,CACtC;aAPD,GAAA,KAOC,CACT,CACE;YAEL,CAAA,CAAA;WA7BK,GAAA,MA6BL,CACL;UACC,CAAA,CACD;;SACL,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;UAAK,WAAWA,sCAAI;UAAY,UAAA,EAAE,eAAe;SAAO,CAAA;QACrD;OAEL,CAAA,GAAA,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;QAAK,WAAWA,sCAAI;QAApB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;SAAK,WAAWA,sCAAI;SAClB,UAAA,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD,EAAA,UAAA,CAAO,EAAE,YAAY,GAAG,gBAAgB,QAAQ,MAAM,aAAoB,EAAA,CAAA;QACvE,CAAA,GACJ,YAAY,OACX,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAAK,WAAWA,sCAAI;SAApB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD,EAAW,WAAWA,sCAAI,eAAiB,CAAA,GAC3C,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;UAAM,WAAWA,sCAAI;UAAiB,UAAA,EAAE,eAAe;SAAQ,CAAA,CAC5D;QAEL,CAAA,IAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;SAAK,WAAWA,sCAAI;SAClB,UAAA,iBAAA,GAAA,kBAAA,KAAA,CAAC,SAAD;UAAO,WAAWA,sCAAI;UAAtB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD,EAAA,UACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,MAAD,EAAA,UAAA;WAAI,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAI,OAAQ,CAAA;WAAC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAI,OAAQ,CAAA;WAAC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAI,OAAQ,CAAA;UAAK,EAAA,CAAA,EAC1C,CAAA,GACP,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD,EAAA,UACG,QAAQ,KAAI,WACX,iBAAA,GAAA,kBAAA,KAAA,CAAC,MAAD,EAAA,UAAA;WACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAK,OAAO,KAAS,CAAA;WACrB,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD;YAAI,WAAWA,sCAAI;YAAW,UAAA,OAAO;WAAS,CAAA;WAC9C,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD;YAAI,WAAWA,sCAAI;YAAW,UAAA,OAAO,aAAa,KAAA,IAAY,KAAK,OAAO,WAAW,QAAQ;WAAS,CAAA;UACpG,EAAA,GAJK,OAAO,IAIZ,CACL,EACI,CAAA,CACF;;QACJ,CAAA,CAEJ;OACF,CAAA,CAAA;;KACA,CAAA;KAGR,oBACC,iBAAA,GAAA,kBAAA,IAAA,CAACE,sCAAAA,OAAD;MACE,MAAM;MACN,eAAe,oBAAoB,KAAK;MACxC,OAAO,EAAE,YAAY;MACrB,YAAY,EAAE,cAAc;MAC5B,WAAWF,sCAAI;MAEf,UAAA,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;OAAK,WAAWA,sCAAI;OAApB,UAAA;QACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAAK,WAAWA,sCAAI;SAApB,UAAA;UACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,SAAD;WAAO,WAAWA,sCAAI;WAAtB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;YAAM,WAAWA,sCAAI;YAAQ,UAAA,EAAE,WAAW;WAAQ,CAAA,GAClD,iBAAA,GAAA,kBAAA,KAAA,CAAC,UAAD;YACE,WAAWA,sCAAI;YACf,OAAO;YACP,UAAU;YACV,WAAW,UAAU;aACnB,MAAM,OAAO,MAAM,OAAO;aAC1B,QAAQ,IAAI;aACZ,QAAQ,cAAc,IAAI,CAAC;YAC7B;YARF,UAAA;aAUE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;cAAQ,OAAM;cAAS,UAAA,EAAE,YAAY;aAAU,CAAA;aAC/C,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;cAAQ,OAAM;cAAY,UAAA,EAAE,eAAe;aAAU,CAAA;aACrD,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;cAAQ,OAAM;cAAU,UAAA,EAAE,aAAa;aAAU,CAAA;aACjD,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;cAAQ,OAAM;cAAU,UAAA,EAAE,aAAa;aAAU,CAAA;aACjD,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;cAAQ,OAAM;cAAQ,UAAA,EAAE,WAAW;aAAU,CAAA;aAC7C,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;cAAQ,OAAM;cAAU,UAAA,EAAE,aAAa;aAAU,CAAA;aACjD,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;cAAQ,OAAM;cAAc,UAAA,EAAE,iBAAiB;aAAU,CAAA;aACzD,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;cAAQ,OAAM;cAAS,UAAA,EAAE,YAAY;aAAU,CAAA;aAC/C,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;cAAQ,OAAM;cAAa,UAAA,EAAE,gBAAgB;aAAU,CAAA;YACjD;WACH,CAAA,CAAA;;UAEN,CAAC,UACA,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;WAAK,WAAWA,sCAAI;WAApB,UAAA,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,SAAD;YAAO,WAAWA,sCAAI;YAAtB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;aAAM,WAAWA,sCAAI;aAAQ,UAAA,EAAE,WAAW;YAAQ,CAAA,GAClD,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;aAAO,WAAWA,sCAAI;aAAO,MAAK;aAAO,OAAO;aAAM,UAAU;aAAc,WAAW,UAAU,QAAQ,MAAM,OAAO,KAAK;YAAI,CAAA,CAC5H;WACP,CAAA,GAAA,iBAAA,GAAA,kBAAA,KAAA,CAAC,SAAD;YAAO,WAAWA,sCAAI;YAAtB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;aAAM,WAAWA,sCAAI;aAAQ,UAAA,EAAE,WAAW;YAAQ,CAAA,GAClD,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;aAAO,WAAWA,sCAAI;aAAO,MAAK;aAAS,KAAK;aAAG,KAAK;aAAO,OAAO;aAAM,UAAU;aAAc,WAAW,UAAU,QAAQ,MAAM,OAAO,KAAK;YAAI,CAAA,CAClJ;WACJ,CAAA,CAAA;;UAGN,CAAC,UACA,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;WAAK,WAAWA,sCAAI;WAApB,UAAA,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,SAAD;YAAO,WAAWA,sCAAI;YAAtB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;aAAM,WAAWA,sCAAI;aAAQ,UAAA,EAAE,WAAW;YAAQ,CAAA,GAClD,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;aAAO,WAAWA,sCAAI;aAAO,MAAK;aAAO,OAAO;aAAM,UAAU;aAAc,WAAW,UAAU,QAAQ,MAAM,OAAO,KAAK;YAAI,CAAA,CAC5H;WACP,CAAA,GAAA,iBAAA,GAAA,kBAAA,KAAA,CAAC,SAAD;YAAO,WAAWA,sCAAI;YAAtB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;aAAM,WAAWA,sCAAI;aAAQ,UAAA,EAAE,eAAe;YAAQ,CAAA,GACtD,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;aAAO,WAAWA,sCAAI;aAAO,MAAK;aAAW,OAAO;aAAU,cAAa;aAAe,UAAU;aAAc,WAAW,UAAU,YAAY,MAAM,OAAO,KAAK;YAAI,CAAA,CACpK;WACJ,CAAA,CAAA;;UAGN,CAAC,UACA,iBAAA,GAAA,kBAAA,KAAA,CAAC,SAAD;WAAO,WAAWA,sCAAI;WAAtB,UAAA;YACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;aACE,MAAK;aACL,SAAS;aACT,UAAU;aACV,WAAW,UAAU,oBAAoB,MAAM,OAAO,OAAO;YAC9D,CAAA;YACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAA,UAAO,EAAE,uBAAuB,EAAQ,CAAA;YACxC,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;aAAM,WAAWA,sCAAI;aAAe,UAAA,EAAE,4BAA4B;YAAQ,CAAA;WACrE;;UAGT,iBAAA,GAAA,kBAAA,KAAA,CAAC,SAAD;WAAO,WAAWA,sCAAI;WAAtB,UAAA;YACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;aACE,MAAK;aACL,SAAS;aACT,UAAU;aACV,WAAW,UAAU,YAAY,MAAM,OAAO,OAAO;YACtD,CAAA;YACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAA,UAAO,EAAE,eAAe,EAAQ,CAAA;YAChC,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;aAAM,WAAWA,sCAAI;aAAe,UAAA,EAAE,oBAAoB;YAAQ,CAAA;WAC7D;;UAEP,iBAAA,GAAA,kBAAA,KAAA,CAAC,SAAD;WAAO,WAAWA,sCAAI;WAAtB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;YAAM,WAAWA,sCAAI;YAAQ,UAAA;WAAoB,CAAA,GACjD,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;YACE,WAAWA,sCAAI;YACf,MAAK;YACL,OAAO;YACP,aAAa,SAAS,EAAE,kCAAkC,IAAI,KAAA;YAC9D,UAAU;YACV,WAAW,UAAU,YAAY,MAAM,OAAO,KAAK;WACpD,CAAA,CACI;;SACJ;;QAEL,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;SAAK,WAAWA,sCAAI;SACjB,UAAA,CAAC,YACA,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;UACE,MAAK;UACL,WAAWA,sCAAI;UACf,UAAU,QAAQ,aAAa,MAAO,CAAC,UAAU,SAAS;UAC1D,eAAe;WAAE,QAAa;UAAE;UAE/B,UAAA,YAAY,EAAE,oBAAoB,IAAI,OAAO,EAAE,gBAAgB,IAAI,EAAE,gBAAgB;SAChF,CAAA,IAER,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;UAAQ,MAAK;UAAS,WAAWA,sCAAI;UAAO,UAAU;UAAM,eAAe;WAAE,WAAgB;UAAE;UAC5F,UAAA,EAAE,mBAAmB;SAChB,CAAA;QAEP,CAAA;QAEJ,UAAU,QACT,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAAK,WAAWA,sCAAI;SAApB,UAAA,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;UAAK,WAAWA,sCAAI;UAApB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD,EAAW,WAAWA,sCAAI,UAAY,CAAA,GACtC,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAA,UAAO,EAAE,aAAa,EAAQ,CAAA,CAC3B;SACL,CAAA,GAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;UAAM,WAAWA,sCAAI;UAAY,UAAA;SAAY,CAAA,CAC1C;;OAEJ;;KACA,CAAA;IAEN;;EAET;;;;;ECt1BA,MAAa,KAAK;;EAGlB,MAAa,KAAK;GAChB,cAAc;GACd,mBAAmB;GACnB,kBAAkB;GAClB,aAAa;GACb,cAAc;GACd,iBAAiB;GACjB,eAAe;GACf,eAAe;GACf,aAAa;GACb,eAAe;GACf,mBAAmB;GACnB,cAAc;GACd,kBAAkB;GAClB,aAAa;GACb,aAAa;GACb,aAAa;GACb,iBAAiB;GACjB,yBAAyB;GACzB,8BAA8B;GAC9B,iBAAiB;GACjB,sBAAsB;GACtB,iBAAiB;GACjB,wBAAwB;GACxB,sBAAsB;GACtB,wBAAwB;GACxB,wBAAwB;GACxB,oCAAoC;GACpC,kBAAkB;GAClB,qBAAqB;GACrB,mBAAmB;GACnB,sBAAsB;GACtB,kBAAkB;GAClB,sBAAsB;GACtB,cAAc;GACd,aAAa;GACb,cAAc;GACd,UAAU;GACV,sBAAsB;GACtB,cAAc;GACd,kBAAkB;GAClB,mBAAmB;GACnB,gBAAgB;GAChB,cAAc;GACd,YAAY;GACZ,kBAAkB;GAClB,iBAAiB;GACjB,iBAAiB;GACjB,iBAAiB;GACjB,gBAAgB;GAChB,eAAe;EACjB;;EAMA,MAAa,KAAK;GAChB,cAAc;GACd,mBAAmB;GACnB,kBAAkB;GAClB,aAAa;GACb,cAAc;GACd,iBAAiB;GACjB,eAAe;GACf,eAAe;GACf,aAAa;GACb,eAAe;GACf,mBAAmB;GACnB,cAAc;GACd,kBAAkB;GAClB,aAAa;GACb,aAAa;GACb,aAAa;GACb,iBAAiB;GACjB,yBAAyB;GACzB,8BAA8B;GAC9B,iBAAiB;GACjB,sBAAsB;GACtB,iBAAiB;GACjB,wBAAwB;GACxB,sBAAsB;GACtB,wBAAwB;GACxB,wBAAwB;GACxB,oCAAoC;GACpC,kBAAkB;GAClB,qBAAqB;GACrB,mBAAmB;GACnB,sBAAsB;GACtB,kBAAkB;GAClB,sBAAsB;GACtB,cAAc;GACd,aAAa;GACb,cAAc;GACd,UAAU;GACV,sBAAsB;GACtB,cAAc;GACd,kBAAkB;GAClB,mBAAmB;GACnB,gBAAgB;GAChB,cAAc;GACd,YAAY;GACZ,kBAAkB;GAClB,iBAAiB;GACjB,iBAAiB;GACjB,iBAAiB;GACjB,gBAAgB;GAChB,eAAe;EACjB;;;;ECvFA,MAAa,SAAS;GAAC;GAAS;GAAU;EAAU;;;;;;;EAQpD,SAAgB,MAAM,KAA0B;GAC9C,IAAI,aAAa,IAAI,OAAO,SAAS,IAAI;IAAE;IAAI;GAAG,CAAC,GAAG,0BAA0B;GACtE,IAAI,OAAO,KAAK,EAAE;GAC5B,IAAI,OAAO;IAAC;IAAS;IAAU;GAAU,IAAI,UAAyB;IACpE,MAAM,OAAO,MAAM,SAAS;IAG5B,MAAM,iBAAiB;KACrB,mBAAoC,KAAK,YAAY;KACrD,YAAY,OAAiC,KAAK,UAAU,EAAE;IAChE;IAGA,MAAM,MAAM,OAAO,iCAAiC,MAAM,MAAM,SAAS;KACvE,MAAM;KACN,IAAI;KACJ,OAAO;KACP,QAAQ;KACR,eAAe,EAAE,OAAO,EAAE,UAAU,eAAe,EAAE;IACvD,GAAG,kBAAkB,CAAC;GACxB,CAAC;EACH"} \ No newline at end of file diff --git a/lib/defaults-Dgu2B2Yq.js b/lib/defaults-DSPWeO3b.js similarity index 83% rename from lib/defaults-Dgu2B2Yq.js rename to lib/defaults-DSPWeO3b.js index 7c0caf8..131eaad 100644 --- a/lib/defaults-Dgu2B2Yq.js +++ b/lib/defaults-DSPWeO3b.js @@ -149,10 +149,13 @@ function sanitizeIdentifier(type, identifier) { switch (type) { case "mysql": case "hive": - case "impala": return "`" + identifier.replace(/`/g, "``") + "`"; + case "impala": + case "clickhouse": + case "doris": return "`" + identifier.replace(/`/g, "``") + "`"; case "postgres": case "oracle": case "sqlite": return "\"" + identifier.replace(/"/g, "\"\"") + "\""; + case "sqlserver": return "[" + identifier.replace(/]/g, "]]") + "]"; } } /** @@ -180,7 +183,21 @@ const QUERY_ARGS = { sqlite: ["-header", "-column"], oracle: ["-S", "/nolog"], hive: ["--silent=true", "--outputformat=tsv2"], - impala: ["-B"] + impala: ["-B"], + clickhouse: [ + "--multiquery", + "--format", + "TSV" + ], + doris: ["--batch", "--raw"], + sqlserver: [ + "-W", + "-s", + "|", + "-h", + "-1", + "-C" + ] }; /** Introspection-mode flag arguments per type (machine-readable listing). */ const INTROSPECT_ARGS = { @@ -189,7 +206,21 @@ const INTROSPECT_ARGS = { sqlite: ["-noheader", "-list"], oracle: ["-S", "/nolog"], hive: ["--silent=true", "--outputformat=tsv2"], - impala: ["-B"] + impala: ["-B"], + clickhouse: [ + "--multiquery", + "--format", + "TSV" + ], + doris: ["--batch", "--raw"], + sqlserver: [ + "-W", + "-s", + "|", + "-h", + "-1", + "-C" + ] }; /** Default ports when the connection does not carry one. */ const DEFAULT_PORTS = { @@ -198,7 +229,10 @@ const DEFAULT_PORTS = { sqlite: 0, oracle: 1521, hive: 1e4, - impala: 21050 + impala: 21050, + clickhouse: 9e3, + doris: 9030, + sqlserver: 1433 }; /** Built-in commands per type (also the loader defaults; see `src/defaults.ts`). */ const DEFAULT_CLIENTS_COMMAND = { @@ -207,7 +241,10 @@ const DEFAULT_CLIENTS_COMMAND = { sqlite: "sqlite3", oracle: "sqlplus", hive: "beeline", - impala: "impala-shell" + impala: "impala-shell", + clickhouse: "clickhouse-client", + doris: "mysql", + sqlserver: "sqlcmd" }; /** * Connection flags for one type. Oracle and Hive carry NO connection flags: @@ -244,6 +281,33 @@ function connectionArgs(type, connection) { "-d", connection.database ]; + case "doris": return [ + "-h", + connection.host ?? "127.0.0.1", + "-P", + String(connection.port ?? DEFAULT_PORTS.doris), + "-u", + connection.user ?? "root", + "-D", + connection.database + ]; + case "clickhouse": return [ + "--host", + connection.host ?? "127.0.0.1", + "--port", + String(connection.port ?? DEFAULT_PORTS.clickhouse), + "--user", + connection.user ?? "default", + "--database", + connection.database + ]; + case "sqlserver": return [ + "-S", + `${connection.host ?? "127.0.0.1"},${connection.port ?? DEFAULT_PORTS.sqlserver}`, + ...connection.user !== void 0 && connection.user !== "" ? ["-U", connection.user] : ["-E"], + "-d", + connection.database + ]; case "oracle": case "hive": return []; } @@ -253,8 +317,11 @@ function credentialEnv(type, connection) { const password = connection.password; if (password === void 0) return {}; switch (type) { - case "mysql": return { MYSQL_PWD: password }; + case "mysql": + case "doris": return { MYSQL_PWD: password }; case "postgres": return { PGPASSWORD: password }; + case "clickhouse": return { CLICKHOUSE_PASSWORD: password }; + case "sqlserver": return { SQLCMDPASSWORD: password }; case "sqlite": case "oracle": case "hive": @@ -281,7 +348,10 @@ function stdinPrefix(type, connection) { case "mysql": case "postgres": case "sqlite": - case "impala": return ""; + case "impala": + case "clickhouse": + case "doris": + case "sqlserver": return ""; } } /** Apply one deployment override's extra args in front of the built-in flags. */ @@ -320,9 +390,12 @@ function buildIntrospectTemplate(type, connection, override) { function tableListingSql(type, connection) { switch (type) { case "mysql": return `SHOW TABLES FROM \`${connection?.database ?? ""}\`;`; + case "doris": return `SHOW TABLES FROM \`${connection?.database ?? ""}\`;`; case "postgres": return "SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY 1;"; case "sqlite": return "SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1;"; case "oracle": return "SELECT table_name FROM user_tables ORDER BY 1;"; + case "clickhouse": return "SHOW TABLES;"; + case "sqlserver": return "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES ORDER BY 1;"; case "hive": case "impala": return "SHOW TABLES;"; } @@ -338,6 +411,9 @@ function metadataQuery(kind, type, schema, table) { case "postgres": return "SELECT schema_name FROM information_schema.schemata ORDER BY 1;"; case "sqlite": return "SELECT 'main';"; case "oracle": return "SELECT username FROM all_users ORDER BY 1;"; + case "clickhouse": + case "doris": return "SHOW DATABASES;"; + case "sqlserver": return "SELECT name FROM sys.schemas ORDER BY 1;"; case "hive": case "impala": return "SHOW DATABASES;"; } @@ -346,6 +422,9 @@ function metadataQuery(kind, type, schema, table) { case "postgres": return `SELECT tablename FROM pg_tables WHERE schemaname=${quoteStringLiteral(schema)} ORDER BY 1;`; case "sqlite": return "SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1;"; case "oracle": return `SELECT table_name FROM all_tables WHERE owner=${quoteStringLiteral(schema)} ORDER BY 1;`; + case "clickhouse": + case "doris": return `SHOW TABLES FROM ${sanitizeIdentifier(type, schema)};`; + case "sqlserver": return `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=${quoteStringLiteral(schema)} ORDER BY 1;`; case "hive": case "impala": return `SHOW TABLES IN ${sanitizeIdentifier(type, schema)};`; } @@ -354,6 +433,9 @@ function metadataQuery(kind, type, schema, table) { case "postgres": return `SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema=${quoteStringLiteral(schema)} AND table_name=${quoteStringLiteral(table)} ORDER BY ordinal_position;`; case "sqlite": return `PRAGMA table_info(${sanitizeIdentifier(type, table)});`; case "oracle": return `SELECT column_name, data_type, nullable FROM all_tab_columns WHERE owner=${quoteStringLiteral(schema)} AND table_name=${quoteStringLiteral(table)} ORDER BY column_id;`; + case "clickhouse": return `DESCRIBE TABLE ${sanitizeIdentifier(type, schema)}.${sanitizeIdentifier(type, table)};`; + case "doris": return `DESCRIBE ${sanitizeIdentifier(type, schema)}.${sanitizeIdentifier(type, table)};`; + case "sqlserver": return `SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=${quoteStringLiteral(schema)} AND TABLE_NAME=${quoteStringLiteral(table)} ORDER BY ORDINAL_POSITION;`; case "hive": case "impala": return `DESCRIBE ${sanitizeIdentifier(type, schema)}.${sanitizeIdentifier(type, table)};`; } @@ -367,7 +449,7 @@ function metadataQuery(kind, type, schema, table) { */ function parseListing(type, stdout) { const lines = stdout.split("\n"); - const start = type === "mysql" ? 1 : 0; + const start = type === "mysql" || type === "doris" ? 1 : 0; const items = []; for (let index = start; index < lines.length; index += 1) { const name = lines[index].trim(); @@ -382,14 +464,17 @@ function parseTableListing(type, stdout) { /** * Parse one type's describe output into columns. Formats: * - mysql `--batch`: `Field\tType\tNull\tKey\t...` (skip header); +* - doris: same mysql-style shape (skip header); * - postgres `-t -A`: `name|type|is_nullable`; * - sqlite `-noheader -list`: `cid|name|type|notnull|dflt|pk` (name is part 1); * - oracle (`SET COLSEP '|'`, heading off): `NAME|TYPE|NULLABLE`; -* - hive/impala batch: `name\ttype\tcomment`. +* - hive/impala batch: `name\ttype\tcomment`; +* - clickhouse `--format TSV`: `name\ttype\t...` (no nullability); +* - sqlserver `-s '|' -h -1`: `name|type|IS_NULLABLE`. */ function parseColumns(type, stdout) { const lines = stdout.split("\n"); - const start = type === "mysql" ? 1 : 0; + const start = type === "mysql" || type === "doris" ? 1 : 0; const columns = []; for (let index = start; index < lines.length; index += 1) { const line = lines[index].trim(); @@ -403,6 +488,7 @@ function parseColumns(type, stdout) { let nullable; switch (type) { case "mysql": + case "doris": nullable = rawNullable === "yes"; break; case "postgres": @@ -414,8 +500,12 @@ function parseColumns(type, stdout) { case "oracle": nullable = rawNullable === "y"; break; + case "sqlserver": + nullable = rawNullable === "yes"; + break; case "hive": - case "impala": nullable = void 0; + case "impala": + case "clickhouse": nullable = void 0; } columns.push({ name, diff --git a/lib/index.js b/lib/index.js index 0279236..c80ca8f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,4 +1,4 @@ -import { a as DEFAULT_PRESET_ID, i as DEFAULT_MAX_RESULT_CHARS, o as DEFAULT_QUERY_TIMEOUT_MS, t as DEFAULT_CONNECT_TIMEOUT_MS, u as clientsSchema } from "./defaults-Dgu2B2Yq.js"; +import { a as DEFAULT_PRESET_ID, i as DEFAULT_MAX_RESULT_CHARS, o as DEFAULT_QUERY_TIMEOUT_MS, t as DEFAULT_CONNECT_TIMEOUT_MS, u as clientsSchema } from "./defaults-DSPWeO3b.js"; import { access, cp, mkdir } from "node:fs/promises"; import { homedir } from "node:os"; import { join, resolve } from "node:path"; @@ -79,7 +79,10 @@ const Config = z.object({ z.const("sqlite"), z.const("oracle"), z.const("hive"), - z.const("impala") + z.const("impala"), + z.const("clickhouse"), + z.const("doris"), + z.const("sqlserver") ]), host: z.string(), port: z.natural(), diff --git a/lib/query-vK9dr7Z6.js b/lib/query-WSTw_Cul.js similarity index 99% rename from lib/query-vK9dr7Z6.js rename to lib/query-WSTw_Cul.js index a805c39..b665d6e 100644 --- a/lib/query-vK9dr7Z6.js +++ b/lib/query-WSTw_Cul.js @@ -1,4 +1,4 @@ -import { c as buildIntrospectTemplate, s as buildClientTemplate } from "./defaults-Dgu2B2Yq.js"; +import { c as buildIntrospectTemplate, s as buildClientTemplate } from "./defaults-DSPWeO3b.js"; //#region src/query.ts /** Read one collected stream from offset 0. */ function readCaptured(reader) { diff --git a/lib/routes.js b/lib/routes.js index 86261bb..6fd4a6f 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -1,5 +1,5 @@ -import { d as metadataQuery, f as parseColumns, g as tableListingSql, h as sanitizeIdentifier, i as DEFAULT_MAX_RESULT_CHARS, l as classifyStatement, m as parseTableListing, o as DEFAULT_QUERY_TIMEOUT_MS, p as parseListing, r as DEFAULT_MAX_QUERY_CHARS, t as DEFAULT_CONNECT_TIMEOUT_MS } from "./defaults-Dgu2B2Yq.js"; -import { t as runClientQuery } from "./query-vK9dr7Z6.js"; +import { d as metadataQuery, f as parseColumns, g as tableListingSql, h as sanitizeIdentifier, i as DEFAULT_MAX_RESULT_CHARS, l as classifyStatement, m as parseTableListing, o as DEFAULT_QUERY_TIMEOUT_MS, p as parseListing, r as DEFAULT_MAX_QUERY_CHARS, t as DEFAULT_CONNECT_TIMEOUT_MS, u as clientsSchema } from "./defaults-DSPWeO3b.js"; +import { t as runClientQuery } from "./query-WSTw_Cul.js"; import { resolve } from "node:path"; import z from "schemastery"; //#region src/routes.ts @@ -21,7 +21,8 @@ const Config = z.object({ maxResultChars: z.number().step(1).min(1024).default(DEFAULT_MAX_RESULT_CHARS), queryTimeoutMs: z.number().step(1).min(1e3).default(DEFAULT_QUERY_TIMEOUT_MS), maxQueryChars: z.number().step(1).min(1024).default(DEFAULT_MAX_QUERY_CHARS), - readonly: z.boolean().default(false) + readonly: z.boolean().default(false), + clients: clientsSchema }); /** * Validate an untrusted /connect body; sqlite paths resolve to absolute @@ -36,7 +37,7 @@ function validateConnectBody(value, cwd = process.cwd()) { const sessionId = candidate.sessionId; if (typeof sessionId !== "string" || sessionId.length === 0) throw new Error("sessionId 必须是非空字符串"); const type = candidate.type; - if (type !== "mysql" && type !== "postgres" && type !== "sqlite" && type !== "oracle" && type !== "hive" && type !== "impala") throw new Error("type 必须是 \"mysql\"、\"postgres\"、\"sqlite\"、\"oracle\"、\"hive\" 或 \"impala\""); + if (type !== "mysql" && type !== "postgres" && type !== "sqlite" && type !== "oracle" && type !== "hive" && type !== "impala" && type !== "clickhouse" && type !== "doris" && type !== "sqlserver") throw new Error("type 必须是 \"mysql\"、\"postgres\"、\"sqlite\"、\"oracle\"、\"hive\"、\"impala\"、\"clickhouse\"、\"doris\" 或 \"sqlserver\""); const database = candidate.database; if (typeof database !== "string" || database.length === 0) throw new Error("database 必须是非空字符串" + (type === "sqlite" ? "(SQLite 为数据库文件路径)" : "")); if (type === "sqlite") return { @@ -95,12 +96,12 @@ function apply(ctx, config) { ], (scope) => { const store = scope.dataAgentConnections; const connectOptions = { - clients: {}, + clients: config.clients, timeoutMs: config.connectTimeoutMs, maxResultChars: config.maxResultChars }; const queryOptions = { - clients: {}, + clients: config.clients, timeoutMs: config.queryTimeoutMs, maxResultChars: config.maxResultChars }; diff --git a/lib/tool.js b/lib/tool.js index c625028..c76dca0 100644 --- a/lib/tool.js +++ b/lib/tool.js @@ -1,5 +1,5 @@ -import { i as DEFAULT_MAX_RESULT_CHARS, l as classifyStatement, o as DEFAULT_QUERY_TIMEOUT_MS, u as clientsSchema } from "./defaults-Dgu2B2Yq.js"; -import { t as runClientQuery } from "./query-vK9dr7Z6.js"; +import { i as DEFAULT_MAX_RESULT_CHARS, l as classifyStatement, o as DEFAULT_QUERY_TIMEOUT_MS, u as clientsSchema } from "./defaults-DSPWeO3b.js"; +import { t as runClientQuery } from "./query-WSTw_Cul.js"; import z from "schemastery"; import { defineTool } from "@deepseek-ai/dsh-tools"; //#region src/tool.ts @@ -48,7 +48,7 @@ function apply(ctx, config) { }; ctx.tools.register(defineTool({ name: "sqlcmd", - description: `在已连接的数据库上执行 SQL 或客户端命令(如 SHOW TABLES、DESCRIBE users、SELECT * FROM orders LIMIT ${resolved.maxRows})。需要先在「数据库」标签页连接数据库;SQL 经 stdin 传给客户端(mysql/psql/sqlite3),无 shell 层。结果包含 exitCode 与 stdout/stderr 文本。`, + description: `在已连接的数据库上执行 SQL 或客户端命令(如 SHOW TABLES、DESCRIBE users、SELECT * FROM orders LIMIT ${resolved.maxRows})。需要先在「数据库」标签页连接数据库;SQL 经 stdin 传给客户端(mysql/psql/sqlite3/sqlplus/beeline/impala-shell/clickhouse-client/sqlcmd 等),无 shell 层。结果包含 exitCode 与 stdout/stderr 文本。`, parameters: { sql: { type: "string", required: true, diff --git a/lib/types/client/DataAgentWorkbench.d.ts b/lib/types/client/DataAgentWorkbench.d.ts index f6673e9..41af0dd 100644 --- a/lib/types/client/DataAgentWorkbench.d.ts +++ b/lib/types/client/DataAgentWorkbench.d.ts @@ -1,6 +1,6 @@ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'; /** Database kinds offered by the connection form. */ -export type DatabaseType = 'mysql' | 'postgres' | 'sqlite' | 'oracle' | 'hive' | 'impala'; +export type DatabaseType = 'mysql' | 'postgres' | 'sqlite' | 'oracle' | 'hive' | 'impala' | 'clickhouse' | 'doris' | 'sqlserver'; /** The sessions-list slice the workbench needs (structural; avoids a runtime import). */ export interface SessionListLike { byId: Record; }>, string>>; connections: import("@deepseek-ai/schemastery").default, import("@deepseek-ai/cosmokit").Dict; + type: import("@deepseek-ai/schemastery").default<"mysql" | "postgres" | "sqlite" | "oracle" | "hive" | "impala" | "clickhouse" | "doris" | "sqlserver", "mysql" | "postgres" | "sqlite" | "oracle" | "hive" | "impala" | "clickhouse" | "doris" | "sqlserver">; host: import("@deepseek-ai/schemastery").default; port: import("@deepseek-ai/schemastery").default; user: import("@deepseek-ai/schemastery").default; @@ -115,14 +121,14 @@ export declare const Config: import("@deepseek-ai/schemastery").default; }>, string>>; connections: import("@deepseek-ai/schemastery").default, import("@deepseek-ai/cosmokit").Dict; + type: import("@deepseek-ai/schemastery").default<"mysql" | "postgres" | "sqlite" | "oracle" | "hive" | "impala" | "clickhouse" | "doris" | "sqlserver", "mysql" | "postgres" | "sqlite" | "oracle" | "hive" | "impala" | "clickhouse" | "doris" | "sqlserver">; host: import("@deepseek-ai/schemastery").default; port: import("@deepseek-ai/schemastery").default; user: import("@deepseek-ai/schemastery").default; diff --git a/lib/types/routes.d.ts b/lib/types/routes.d.ts index 9462df0..7aa442d 100644 --- a/lib/types/routes.d.ts +++ b/lib/types/routes.d.ts @@ -40,6 +40,7 @@ declare module '@deepseek-ai/cordis' { } } import type { DatabaseType } from './connections.ts'; +import { type ClientConfig } from './clients.ts'; /** Cordis plugin name (diagnostics only). */ export declare const name = "data-agent-routes"; /** @@ -65,6 +66,8 @@ export interface Config { maxQueryChars: number; /** Read-only guard: true rejects write statements in /query. */ readonly: boolean; + /** CLI client overrides keyed by database type (same shape as the main row). */ + clients: Partial>; } /** Loader schema with deployment defaults (no library defaults). */ export declare const Config: import("@deepseek-ai/schemastery").default; maxQueryChars: import("@deepseek-ai/schemastery").default; readonly: import("@deepseek-ai/schemastery").default; + clients: import("@deepseek-ai/schemastery").default, import("@deepseek-ai/cosmokit").Dict; + args: import("@deepseek-ai/schemastery").default; + }>, string>>; }>, Schemastery.ObjectT<{ connectTimeoutMs: import("@deepseek-ai/schemastery").default; introspectMaxTables: import("@deepseek-ai/schemastery").default; @@ -81,6 +91,13 @@ export declare const Config: import("@deepseek-ai/schemastery").default; maxQueryChars: import("@deepseek-ai/schemastery").default; readonly: import("@deepseek-ai/schemastery").default; + clients: import("@deepseek-ai/schemastery").default, import("@deepseek-ai/cosmokit").Dict; + args: import("@deepseek-ai/schemastery").default; + }>, string>>; }>>; /** The connection request wire body (validated in the /connect handler). */ export interface ConnectRequestBody { diff --git a/lib/types/tool.d.ts b/lib/types/tool.d.ts index 2aab008..57f8b5e 100644 --- a/lib/types/tool.d.ts +++ b/lib/types/tool.d.ts @@ -13,6 +13,7 @@ * @module @yejiming/dsh-data-agent/tool */ import type { Context } from '@deepseek-ai/cordis'; +import type { DatabaseType } from './connections.ts'; import { type ClientConfig } from './clients.ts'; /** Cordis plugin name (diagnostics only). */ export declare const name = "data-agent-tool"; @@ -29,7 +30,7 @@ export interface Config { /** Read-only guard: true rejects write statements. */ readonly: boolean; /** CLI client overrides keyed by database type. */ - clients: Partial>; + clients: Partial>; } /** Loader schema with deployment defaults (no library defaults). */ export declare const Config: import("@deepseek-ai/schemastery").default- 你是数据工程师 Agent。工作目录 {{cwd}}。你只有 4 个工具:sqlcmd(在数据库客户端执行 - SQL 或命令,如 SHOW TABLES、DESCRIBE users、SELECT * FROM orders LIMIT 5)、read、 + SQL 或命令,如 SHOW TABLES、DESCRIBE users、SELECT * FROM orders LIMIT 5;支持 + MySQL/PostgreSQL/SQLite/Oracle/Hive/Impala/ClickHouse/Doris/SQL Server)、read、 write、edit(读写改本地文件)。工作流:先用 sqlcmd 探查表结构与样例数据(禁止臆造 表名与列名),把 SQL 写入 .sql 文件(write/edit),再用 sqlcmd 执行并核对结果; 结果可疑时缩小查询范围重试。 diff --git a/preset/data-agent/preset.yml b/preset/data-agent/preset.yml index 54e656b..7b643cb 100644 --- a/preset/data-agent/preset.yml +++ b/preset/data-agent/preset.yml @@ -1,3 +1,3 @@ name: 数据Agent -description: 数据工程师 Agent:连接 MySQL/PostgreSQL/SQLite 数据库,用 sqlcmd/read/write/edit 四个工具探查 schema、编写并执行 SQL。 +description: 数据工程师 Agent:连接 MySQL/PostgreSQL/SQLite/Oracle/Hive/Impala/ClickHouse/Doris/SQL Server 数据库,用 sqlcmd/read/write/edit 四个工具探查 schema、编写并执行 SQL。 order: 4 diff --git a/seed/README.md b/seed/README.md index e7dfb4f..a7a4e73 100644 --- a/seed/README.md +++ b/seed/README.md @@ -2,7 +2,6 @@ 本目录提供一套**表结构复杂、字段丰富、数据量充足**的 MySQL 测试数据集,用于验证 dsh-data-agent 插件全链路(连接 / 库表浏览 / SQL 命令框 / sqlcmd / AI 对话探查)。 -规划文档见 `docs/mysql-demo-plan.md`。 ## 快速开始 diff --git a/src/client/DataAgentWorkbench.tsx b/src/client/DataAgentWorkbench.tsx index c20ad00..829fabc 100644 --- a/src/client/DataAgentWorkbench.tsx +++ b/src/client/DataAgentWorkbench.tsx @@ -128,7 +128,16 @@ function PlayIcon({ className }: { className?: string }) { } /** Database kinds offered by the connection form. */ -export type DatabaseType = 'mysql' | 'postgres' | 'sqlite' | 'oracle' | 'hive' | 'impala' +export type DatabaseType = + | 'mysql' + | 'postgres' + | 'sqlite' + | 'oracle' + | 'hive' + | 'impala' + | 'clickhouse' + | 'doris' + | 'sqlserver' /** The sessions-list slice the workbench needs (structural; avoids a runtime import). */ export interface SessionListLike { @@ -199,6 +208,9 @@ function defaultPortOf(type: DatabaseType): string { case 'oracle': return '1521' case 'hive': return '10000' case 'impala': return '21050' + case 'clickhouse': return '9000' + case 'doris': return '9030' + case 'sqlserver': return '1433' case 'sqlite': return '' case 'mysql': return '3306' } @@ -734,7 +746,7 @@ export function DataAgentWorkbench({ sessionId, useSessions, t }: DataAgentWorkb onChange={(event) => { const next = event.target.value as DatabaseType setType(next) - setPort(next === 'postgres' ? '5432' : next === 'mysql' ? '3306' : next === 'oracle' ? '1521' : next === 'hive' ? '10000' : next === 'impala' ? '21050' : '') + setPort(defaultPortOf(next)) }} > @@ -743,6 +755,9 @@ export function DataAgentWorkbench({ sessionId, useSessions, t }: DataAgentWorkb + + + diff --git a/src/client/locales.ts b/src/client/locales.ts index 7e6d927..2b2a6e4 100644 --- a/src/client/locales.ts +++ b/src/client/locales.ts @@ -15,6 +15,9 @@ export const zh = { 'type.oracle': 'Oracle', 'type.hive': 'Hive', 'type.impala': 'Impala', + 'type.clickhouse': 'ClickHouse', + 'type.doris': 'Doris', + 'type.sqlserver': 'SQL Server', 'form.host': '主机', 'form.port': '端口', 'form.user': '用户名', @@ -69,6 +72,9 @@ export const en = { 'type.oracle': 'Oracle', 'type.hive': 'Hive', 'type.impala': 'Impala', + 'type.clickhouse': 'ClickHouse', + 'type.doris': 'Doris', + 'type.sqlserver': 'SQL Server', 'form.host': 'Host', 'form.port': 'Port', 'form.user': 'User', diff --git a/src/client/persistence.ts b/src/client/persistence.ts index 94f30ab..afa46b3 100644 --- a/src/client/persistence.ts +++ b/src/client/persistence.ts @@ -58,7 +58,8 @@ function parseSaved(value: unknown): SavedConnection | null { const candidate = value as Record const type = candidate.type if (type !== 'mysql' && type !== 'postgres' && type !== 'sqlite' - && type !== 'oracle' && type !== 'hive' && type !== 'impala') { + && type !== 'oracle' && type !== 'hive' && type !== 'impala' + && type !== 'clickhouse' && type !== 'doris' && type !== 'sqlserver') { return null } const database = candidate.database diff --git a/src/clients.ts b/src/clients.ts index 5e65888..9139875 100644 --- a/src/clients.ts +++ b/src/clients.ts @@ -170,11 +170,15 @@ export function sanitizeIdentifier(type: DatabaseType, identifier: string): stri case 'mysql': case 'hive': case 'impala': + case 'clickhouse': + case 'doris': return '`' + identifier.replace(/`/g, '``') + '`' case 'postgres': case 'oracle': case 'sqlite': return '"' + identifier.replace(/"/g, '""') + '"' + case 'sqlserver': + return '[' + identifier.replace(/]/g, ']]') + ']' } } @@ -231,6 +235,9 @@ const QUERY_ARGS: Readonly> = { oracle: ['-S', '/nolog'], hive: ['--silent=true', '--outputformat=tsv2'], impala: ['-B'], + clickhouse: ['--multiquery', '--format', 'TSV'], + doris: ['--batch', '--raw'], + sqlserver: ['-W', '-s', '|', '-h', '-1', '-C'], } /** Introspection-mode flag arguments per type (machine-readable listing). */ @@ -241,6 +248,9 @@ const INTROSPECT_ARGS: Readonly> = { oracle: ['-S', '/nolog'], hive: ['--silent=true', '--outputformat=tsv2'], impala: ['-B'], + clickhouse: ['--multiquery', '--format', 'TSV'], + doris: ['--batch', '--raw'], + sqlserver: ['-W', '-s', '|', '-h', '-1', '-C'], } /** Default ports when the connection does not carry one. */ @@ -251,6 +261,9 @@ const DEFAULT_PORTS: Readonly> = { oracle: 1521, hive: 10000, impala: 21050, + clickhouse: 9000, + doris: 9030, + sqlserver: 1433, } /** Built-in commands per type (also the loader defaults; see `src/defaults.ts`). */ @@ -261,6 +274,9 @@ const DEFAULT_CLIENTS_COMMAND: Readonly> = { oracle: 'sqlplus', hive: 'beeline', impala: 'impala-shell', + clickhouse: 'clickhouse-client', + doris: 'mysql', + sqlserver: 'sqlcmd', } /** @@ -292,6 +308,28 @@ function connectionArgs(type: DatabaseType, connection: DatabaseConnection): rea '-i', `${connection.host ?? '127.0.0.1'}:${connection.port ?? DEFAULT_PORTS.impala}`, '-d', connection.database, ] + case 'doris': + return [ + '-h', connection.host ?? '127.0.0.1', + '-P', String(connection.port ?? DEFAULT_PORTS.doris), + '-u', connection.user ?? 'root', + '-D', connection.database, + ] + case 'clickhouse': + return [ + '--host', connection.host ?? '127.0.0.1', + '--port', String(connection.port ?? DEFAULT_PORTS.clickhouse), + '--user', connection.user ?? 'default', + '--database', connection.database, + ] + case 'sqlserver': + return [ + '-S', `${connection.host ?? '127.0.0.1'},${connection.port ?? DEFAULT_PORTS.sqlserver}`, + ...connection.user !== undefined && connection.user !== '' + ? ['-U', connection.user] + : ['-E'], + '-d', connection.database, + ] case 'oracle': case 'hive': return [] @@ -304,9 +342,14 @@ function credentialEnv(type: DatabaseType, connection: DatabaseConnection): Read if (password === undefined) return {} switch (type) { case 'mysql': + case 'doris': return { MYSQL_PWD: password } case 'postgres': return { PGPASSWORD: password } + case 'clickhouse': + return { CLICKHOUSE_PASSWORD: password } + case 'sqlserver': + return { SQLCMDPASSWORD: password } case 'sqlite': case 'oracle': case 'hive': @@ -344,6 +387,9 @@ function stdinPrefix(type: DatabaseType, connection: DatabaseConnection): string case 'postgres': case 'sqlite': case 'impala': + case 'clickhouse': + case 'doris': + case 'sqlserver': return '' } } @@ -395,9 +441,12 @@ export function buildIntrospectTemplate( export function tableListingSql(type: DatabaseType, connection?: DatabaseConnection): string { switch (type) { case 'mysql': return `SHOW TABLES FROM \`${connection?.database ?? ''}\`;` + case 'doris': return `SHOW TABLES FROM \`${connection?.database ?? ''}\`;` case 'postgres': return "SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY 1;" case 'sqlite': return "SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1;" case 'oracle': return 'SELECT table_name FROM user_tables ORDER BY 1;' + case 'clickhouse': return 'SHOW TABLES;' + case 'sqlserver': return 'SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES ORDER BY 1;' case 'hive': case 'impala': return 'SHOW TABLES;' } @@ -420,6 +469,9 @@ export function metadataQuery( case 'postgres': return 'SELECT schema_name FROM information_schema.schemata ORDER BY 1;' case 'sqlite': return "SELECT 'main';" case 'oracle': return 'SELECT username FROM all_users ORDER BY 1;' + case 'clickhouse': + case 'doris': return 'SHOW DATABASES;' + case 'sqlserver': return 'SELECT name FROM sys.schemas ORDER BY 1;' case 'hive': case 'impala': return 'SHOW DATABASES;' } @@ -429,6 +481,9 @@ export function metadataQuery( case 'postgres': return `SELECT tablename FROM pg_tables WHERE schemaname=${quoteStringLiteral(schema!)} ORDER BY 1;` case 'sqlite': return "SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1;" case 'oracle': return `SELECT table_name FROM all_tables WHERE owner=${quoteStringLiteral(schema!)} ORDER BY 1;` + case 'clickhouse': + case 'doris': return `SHOW TABLES FROM ${sanitizeIdentifier(type, schema!)};` + case 'sqlserver': return `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=${quoteStringLiteral(schema!)} ORDER BY 1;` case 'hive': case 'impala': return `SHOW TABLES IN ${sanitizeIdentifier(type, schema!)};` } @@ -438,6 +493,9 @@ export function metadataQuery( case 'postgres': return `SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema=${quoteStringLiteral(schema!)} AND table_name=${quoteStringLiteral(table!)} ORDER BY ordinal_position;` case 'sqlite': return `PRAGMA table_info(${sanitizeIdentifier(type, table!)});` case 'oracle': return `SELECT column_name, data_type, nullable FROM all_tab_columns WHERE owner=${quoteStringLiteral(schema!)} AND table_name=${quoteStringLiteral(table!)} ORDER BY column_id;` + case 'clickhouse': return `DESCRIBE TABLE ${sanitizeIdentifier(type, schema!)}.${sanitizeIdentifier(type, table!)};` + case 'doris': return `DESCRIBE ${sanitizeIdentifier(type, schema!)}.${sanitizeIdentifier(type, table!)};` + case 'sqlserver': return `SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=${quoteStringLiteral(schema!)} AND TABLE_NAME=${quoteStringLiteral(table!)} ORDER BY ORDINAL_POSITION;` case 'hive': case 'impala': return `DESCRIBE ${sanitizeIdentifier(type, schema!)}.${sanitizeIdentifier(type, table!)};` } @@ -452,7 +510,7 @@ export function metadataQuery( */ export function parseListing(type: DatabaseType, stdout: string): string[] { const lines = stdout.split('\n') - const start = type === 'mysql' ? 1 : 0 + const start = type === 'mysql' || type === 'doris' ? 1 : 0 const items: string[] = [] for (let index = start; index < lines.length; index += 1) { const name = lines[index]!.trim() @@ -476,14 +534,17 @@ export interface ColumnInfo { /** * Parse one type's describe output into columns. Formats: * - mysql `--batch`: `Field\tType\tNull\tKey\t...` (skip header); + * - doris: same mysql-style shape (skip header); * - postgres `-t -A`: `name|type|is_nullable`; * - sqlite `-noheader -list`: `cid|name|type|notnull|dflt|pk` (name is part 1); * - oracle (`SET COLSEP '|'`, heading off): `NAME|TYPE|NULLABLE`; - * - hive/impala batch: `name\ttype\tcomment`. + * - hive/impala batch: `name\ttype\tcomment`; + * - clickhouse `--format TSV`: `name\ttype\t...` (no nullability); + * - sqlserver `-s '|' -h -1`: `name|type|IS_NULLABLE`. */ export function parseColumns(type: DatabaseType, stdout: string): ColumnInfo[] { const lines = stdout.split('\n') - const start = type === 'mysql' ? 1 : 0 + const start = type === 'mysql' || type === 'doris' ? 1 : 0 const columns: ColumnInfo[] = [] for (let index = start; index < lines.length; index += 1) { const line = lines[index]!.trim() @@ -498,12 +559,15 @@ export function parseColumns(type: DatabaseType, stdout: string): ColumnInfo[] { const rawNullable = parts[nameIndex + 2]?.trim().toLowerCase() let nullable: boolean | undefined switch (type) { - case 'mysql': nullable = rawNullable === 'yes'; break + case 'mysql': + case 'doris': nullable = rawNullable === 'yes'; break case 'postgres': nullable = rawNullable === 'yes'; break case 'sqlite': nullable = rawNullable !== '1'; break case 'oracle': nullable = rawNullable === 'y'; break + case 'sqlserver': nullable = rawNullable === 'yes'; break case 'hive': - case 'impala': nullable = undefined; break + case 'impala': + case 'clickhouse': nullable = undefined; break } columns.push({ name, type: columnType, ...nullable !== undefined ? { nullable } : {} }) } diff --git a/src/connections.ts b/src/connections.ts index 050034e..9856450 100644 --- a/src/connections.ts +++ b/src/connections.ts @@ -24,7 +24,16 @@ export const WILDCARD_SESSION = '*' /** Supported database client kinds. */ -export type DatabaseType = 'mysql' | 'postgres' | 'sqlite' | 'oracle' | 'hive' | 'impala' +export type DatabaseType = + | 'mysql' + | 'postgres' + | 'sqlite' + | 'oracle' + | 'hive' + | 'impala' + | 'clickhouse' + | 'doris' + | 'sqlserver' /** * One session's database connection. `host`/`port`/`user` are empty for diff --git a/src/index.ts b/src/index.ts index 92b8ae3..a6b3c1c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,6 +53,12 @@ export interface ClientsConfig { mysql?: ClientConfig postgres?: ClientConfig sqlite?: ClientConfig + oracle?: ClientConfig + hive?: ClientConfig + impala?: ClientConfig + clickhouse?: ClientConfig + doris?: ClientConfig + sqlserver?: ClientConfig } /** @@ -104,7 +110,7 @@ export const Config = z.object({ readonly: z.boolean().default(false), clients: clientsSchema, connections: z.dict(z.object({ - type: z.union([z.const('mysql'), z.const('postgres'), z.const('sqlite'), z.const('oracle'), z.const('hive'), z.const('impala')]), + type: z.union([z.const('mysql'), z.const('postgres'), z.const('sqlite'), z.const('oracle'), z.const('hive'), z.const('impala'), z.const('clickhouse'), z.const('doris'), z.const('sqlserver')]), host: z.string(), port: z.natural(), user: z.string(), diff --git a/src/routes.ts b/src/routes.ts index be08a9a..bbdb068 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -55,7 +55,7 @@ import type { DatabaseConnection, DatabaseType, } from './connections.ts' -import { metadataQuery, parseColumns, parseListing, parseTableListing, sanitizeIdentifier, classifyStatement, tableListingSql } from './clients.ts' +import { clientsSchema, metadataQuery, parseColumns, parseListing, parseTableListing, sanitizeIdentifier, classifyStatement, tableListingSql, type ClientConfig } from './clients.ts' import { DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_INTROSPECT_MAX_TABLES, @@ -93,6 +93,8 @@ export interface Config { maxQueryChars: number /** Read-only guard: true rejects write statements in /query. */ readonly: boolean + /** CLI client overrides keyed by database type (same shape as the main row). */ + clients: Partial> } /** Loader schema with deployment defaults (no library defaults). */ @@ -103,6 +105,7 @@ export const Config = z.object({ queryTimeoutMs: z.number().step(1).min(1000).default(DEFAULT_QUERY_TIMEOUT_MS), maxQueryChars: z.number().step(1).min(1024).default(DEFAULT_MAX_QUERY_CHARS), readonly: z.boolean().default(false), + clients: clientsSchema, }) /** The connection request wire body (validated in the /connect handler). */ @@ -135,8 +138,9 @@ export function validateConnectBody(value: unknown, cwd = process.cwd()): Connec } const type = candidate.type if (type !== 'mysql' && type !== 'postgres' && type !== 'sqlite' - && type !== 'oracle' && type !== 'hive' && type !== 'impala') { - throw new Error('type 必须是 "mysql"、"postgres"、"sqlite"、"oracle"、"hive" 或 "impala"') + && type !== 'oracle' && type !== 'hive' && type !== 'impala' + && type !== 'clickhouse' && type !== 'doris' && type !== 'sqlserver') { + throw new Error('type 必须是 "mysql"、"postgres"、"sqlite"、"oracle"、"hive"、"impala"、"clickhouse"、"doris" 或 "sqlserver"') } const database = candidate.database if (typeof database !== 'string' || database.length === 0) { @@ -185,12 +189,12 @@ export function apply(ctx: Context, config: Config): void { ctx.inject(['webServer', 'subprocess', 'dataAgentConnections'], (scope) => { const store: DataAgentConnections = scope.dataAgentConnections const connectOptions = { - clients: {}, + clients: config.clients, timeoutMs: config.connectTimeoutMs, maxResultChars: config.maxResultChars, } const queryOptions = { - clients: {}, + clients: config.clients, timeoutMs: config.queryTimeoutMs, maxResultChars: config.maxResultChars, } diff --git a/src/tool.ts b/src/tool.ts index fd6100c..ed0660e 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -20,6 +20,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools' // the ctx.dataAgentConnections merge (the main data-agent row). import type {} from '@deepseek-ai/dsh-subprocess' import type {} from './index.ts' +import type { DatabaseType } from './connections.ts' import { clientsSchema, type ClientConfig } from './clients.ts' import { classifyStatement } from './clients.ts' import { @@ -45,7 +46,7 @@ export interface Config { /** Read-only guard: true rejects write statements. */ readonly: boolean /** CLI client overrides keyed by database type. */ - clients: Partial> + clients: Partial> } /** Loader schema with deployment defaults (no library defaults). */ @@ -91,7 +92,8 @@ export function apply(ctx: Context, config: Config): void { description: '在已连接的数据库上执行 SQL 或客户端命令(如 SHOW TABLES、DESCRIBE users、' + `SELECT * FROM orders LIMIT ${resolved.maxRows})。` - + '需要先在「数据库」标签页连接数据库;SQL 经 stdin 传给客户端(mysql/psql/sqlite3),' + + '需要先在「数据库」标签页连接数据库;SQL 经 stdin 传给客户端' + + '(mysql/psql/sqlite3/sqlplus/beeline/impala-shell/clickhouse-client/sqlcmd 等),' + '无 shell 层。结果包含 exitCode 与 stdout/stderr 文本。', parameters: { sql: { diff --git a/tests/clients.spec.ts b/tests/clients.spec.ts index c1d8ddf..916a054 100644 --- a/tests/clients.spec.ts +++ b/tests/clients.spec.ts @@ -362,3 +362,123 @@ describe('sanitizeIdentifier', () => { } }) }) + +describe('buildClientTemplate — clickhouse/doris/sqlserver', () => { + it('builds the clickhouse argv and keeps the password in CLICKHOUSE_PASSWORD', () => { + const template = buildClientTemplate('clickhouse', { + type: 'clickhouse', host: 'ch01', port: 9000, user: 'default', + database: 'analytics', password: 's3cret', + }) + expect(template.command).toBe('clickhouse-client') + expect(template.args).toEqual([ + '--multiquery', '--format', 'TSV', + '--host', 'ch01', '--port', '9000', '--user', 'default', '--database', 'analytics', + ]) + expect(template.env).toEqual({ CLICKHOUSE_PASSWORD: 's3cret' }) + expect(JSON.stringify(template.args)).not.toContain('s3cret') + }) + + it('builds the doris template on the mysql client with port 9030', () => { + const template = buildClientTemplate('doris', { + type: 'doris', host: 'fe01', database: 'dwd', password: 'pw', + }) + expect(template.command).toBe('mysql') + expect(template.args).toEqual(['--batch', '--raw', '-h', 'fe01', '-P', '9030', '-u', 'root', '-D', 'dwd']) + expect(template.env).toEqual({ MYSQL_PWD: 'pw' }) + }) + + it('builds the sqlserver template with -S/-U/-d and SQLCMDPASSWORD env', () => { + const template = buildClientTemplate('sqlserver', { + type: 'sqlserver', host: 'db01', port: 1433, user: 'sa', database: 'sales', password: 'pw', + }) + expect(template.command).toBe('sqlcmd') + expect(template.args).toEqual(['-W', '-s', '|', '-h', '-1', '-C', '-S', 'db01,1433', '-U', 'sa', '-d', 'sales']) + expect(template.env).toEqual({ SQLCMDPASSWORD: 'pw' }) + expect(JSON.stringify(template.args)).not.toContain('pw') + }) + + it('uses -E integrated auth for sqlserver without a user', () => { + const template = buildClientTemplate('sqlserver', { type: 'sqlserver', database: 'sales' }) + expect(template.args).toEqual(['-W', '-s', '|', '-h', '-1', '-C', '-S', '127.0.0.1,1433', '-E', '-d', 'sales']) + expect(template.env).toEqual({}) + }) + + it('uses machine-readable introspect flags for the three new types', () => { + expect(buildIntrospectTemplate('clickhouse', { type: 'clickhouse', database: 'a' }).args.slice(0, 3)) + .toEqual(['--multiquery', '--format', 'TSV']) + expect(buildIntrospectTemplate('doris', { type: 'doris', database: 'a' }).args.slice(0, 2)) + .toEqual(['--batch', '--raw']) + expect(buildIntrospectTemplate('sqlserver', { type: 'sqlserver', database: 'a' }).args.slice(0, 5)) + .toEqual(['-W', '-s', '|', '-h', '-1']) + }) +}) + +describe('tableListingSql / metadataQuery — clickhouse/doris/sqlserver', () => { + it('builds the connectivity listing per type', () => { + expect(tableListingSql('clickhouse', { type: 'clickhouse', database: 'analytics' })).toBe('SHOW TABLES;') + expect(tableListingSql('doris', { type: 'doris', database: 'dwd' })).toBe('SHOW TABLES FROM `dwd`;') + expect(tableListingSql('sqlserver', { type: 'sqlserver', database: 'sales' })) + .toBe('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES ORDER BY 1;') + }) + + it('builds the schemas query per type', () => { + expect(metadataQuery('schemas', 'clickhouse')).toBe('SHOW DATABASES;') + expect(metadataQuery('schemas', 'doris')).toBe('SHOW DATABASES;') + expect(metadataQuery('schemas', 'sqlserver')).toBe('SELECT name FROM sys.schemas ORDER BY 1;') + }) + + it('builds the tables query per type', () => { + expect(metadataQuery('tables', 'clickhouse', 'analytics')).toBe('SHOW TABLES FROM `analytics`;') + expect(metadataQuery('tables', 'doris', 'dwd')).toBe('SHOW TABLES FROM `dwd`;') + expect(metadataQuery('tables', 'sqlserver', 'dbo')) + .toBe("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='dbo' ORDER BY 1;") + }) + + it('builds the describe query per type', () => { + expect(metadataQuery('describe', 'clickhouse', 'analytics', 'orders')) + .toBe('DESCRIBE TABLE `analytics`.`orders`;') + expect(metadataQuery('describe', 'doris', 'dwd', 'orders')).toBe('DESCRIBE `dwd`.`orders`;') + expect(metadataQuery('describe', 'sqlserver', 'dbo', 'orders')) + .toContain("TABLE_SCHEMA='dbo' AND TABLE_NAME='orders'") + }) +}) + +describe('parseColumns / parseListing — clickhouse/doris/sqlserver', () => { + it('parses clickhouse TSV describe output without nullability', () => { + const out = 'id\tUInt64\t\nname\tString\t\n' + expect(parseColumns('clickhouse', out)).toEqual([ + { name: 'id', type: 'UInt64' }, + { name: 'name', type: 'String' }, + ]) + }) + + it('parses doris describe output skipping the header like mysql', () => { + const out = 'Field\tType\tNull\tKey\nid\tint\tNO\tPRI\n' + expect(parseColumns('doris', out)).toEqual([{ name: 'id', type: 'int', nullable: false }]) + }) + + it('parses sqlserver pipe-separated columns with YES/NO nullability', () => { + const out = 'id|int|NO\nname|nvarchar|YES\n' + expect(parseColumns('sqlserver', out)).toEqual([ + { name: 'id', type: 'int', nullable: false }, + { name: 'name', type: 'nvarchar', nullable: true }, + ]) + }) + + it('parses clickhouse/doris/sqlserver listings without surprises', () => { + expect(parseListing('clickhouse', 'orders\nusers\n')).toEqual(['orders', 'users']) + expect(parseListing('doris', 'Tables_in_dwd\norders\nusers\n')).toEqual(['orders', 'users']) + expect(parseListing('sqlserver', 'orders\nusers\n')).toEqual(['orders', 'users']) + }) +}) + +describe('sanitizeIdentifier — new types', () => { + it('wraps clickhouse and doris identifiers in backticks', () => { + expect(sanitizeIdentifier('clickhouse', 'orders')).toBe('`orders`') + expect(sanitizeIdentifier('doris', 'dwd')).toBe('`dwd`') + }) + + it('wraps sqlserver identifiers in brackets', () => { + expect(sanitizeIdentifier('sqlserver', 'OrderDetails')).toBe('[OrderDetails]') + }) +}) diff --git a/tests/integration-smoke.spec.ts b/tests/integration-smoke.spec.ts new file mode 100644 index 0000000..0801ae2 --- /dev/null +++ b/tests/integration-smoke.spec.ts @@ -0,0 +1,220 @@ +/** + * Opt-in integration smoke tests against real ClickHouse / Doris / SQL Server + * services. They exercise the exact CLI templates and parsers produced by + * src/clients.ts — same argv shape, same credential environment entries, same + * stdin SQL channel as the plugin's runClientQuery path. + * + * Run (skipped unless DSH_SMOKE_HOST is set): + * DSH_SMOKE_HOST= \ + * DSH_SMOKE_CH_PASSWORD=... \ + * DSH_SMOKE_MSSQL_PASSWORD=... \ + * PATH="/path/to/clickhouse-client:/path/to/mysql:/path/to/sqlcmd:$PATH" \ + * pnpm vitest run tests/integration-smoke.spec.ts + * + * The tests create and drop a `dsh_smoke` database/table set on each service. + */ +import { spawn } from 'node:child_process' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { + buildClientTemplate, + buildIntrospectTemplate, + classifyStatement, + metadataQuery, + parseColumns, + parseListing, + parseTableListing, + tableListingSql, + type ClientTemplate, +} from '../src/clients.ts' +import type { DatabaseConnection } from '../src/connections.ts' + +const HOST = process.env.DSH_SMOKE_HOST +const CH_PASSWORD = process.env.DSH_SMOKE_CH_PASSWORD +const MSSQL_PASSWORD = process.env.DSH_SMOKE_MSSQL_PASSWORD + +/** One captured client-process outcome. */ +interface RunOutcome { + exitCode: number | null + stdout: string + stderr: string +} + +/** Run one CLI invocation exactly the way the plugin does: argv + env + stdin SQL. */ +function runTemplate(template: ClientTemplate, sql: string, timeoutMs = 60_000): Promise { + return new Promise((resolve, reject) => { + const child = spawn(template.command, [...template.args], { + env: { ...process.env, ...template.env }, + stdio: ['pipe', 'pipe', 'pipe'], + }) + const stdout: Buffer[] = [] + const stderr: Buffer[] = [] + child.stdout.on('data', chunk => stdout.push(chunk as Buffer)) + child.stderr.on('data', chunk => stderr.push(chunk as Buffer)) + const timer = setTimeout(() => { + child.kill('SIGKILL') + reject(new Error(`smoke client timed out after ${timeoutMs}ms: ${template.command}`)) + }, timeoutMs) + child.on('error', error => { + clearTimeout(timer) + reject(error) + }) + child.on('close', (code) => { + clearTimeout(timer) + resolve({ + exitCode: code, + stdout: Buffer.concat(stdout).toString('utf8'), + stderr: Buffer.concat(stderr).toString('utf8'), + }) + }) + child.stdin.end(`${template.stdinPrefix}${sql}\n`) + }) +} + +/** Run one SQL text through the query or introspect template of a type. */ +async function runFor( + connection: DatabaseConnection, + sql: string, + introspect = true, +): Promise { + const template = introspect + ? buildIntrospectTemplate(connection.type, connection) + : buildClientTemplate(connection.type, connection) + return runTemplate(template, sql) +} + +const clickhouse: DatabaseConnection = { + type: 'clickhouse', + host: HOST, + port: 9000, + user: 'default', + database: 'dsh_smoke', + password: CH_PASSWORD, +} + +const doris: DatabaseConnection = { + type: 'doris', + host: HOST, + port: 9030, + user: 'root', + database: 'dsh_smoke', +} + +const sqlserver: DatabaseConnection = { + type: 'sqlserver', + host: HOST, + port: 1433, + user: 'sa', + database: 'dsh_smoke', + password: MSSQL_PASSWORD, +} + +const targets: Array<{ name: string; connection: DatabaseConnection; browseSchema: string }> = [ + { name: 'clickhouse', connection: clickhouse, browseSchema: 'dsh_smoke' }, + { name: 'doris', connection: doris, browseSchema: 'dsh_smoke' }, + { name: 'sqlserver', connection: sqlserver, browseSchema: 'dbo' }, +] + +/** Fail loud when one setup SQL run does not exit 0. */ +async function requireSuccess(connection: DatabaseConnection, sql: string): Promise { + const result = await runFor(connection, sql) + if (result.exitCode !== 0) { + throw new Error( + `setup failed for ${connection.type}: exit ${result.exitCode}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, + ) + } +} + +describe.skipIf(HOST === undefined || CH_PASSWORD === undefined || MSSQL_PASSWORD === undefined)( + 'integration smoke — clickhouse / doris / sqlserver', + () => { + beforeAll(async () => { + // ClickHouse: fresh database + table + one row. + await requireSuccess( + { ...clickhouse, database: 'default' }, + 'DROP DATABASE IF EXISTS dsh_smoke;\n' + + 'CREATE DATABASE dsh_smoke;\n' + + 'CREATE TABLE dsh_smoke.orders (id UInt64, name String, amount Decimal(10,2)) ENGINE = MergeTree ORDER BY id;\n' + + 'INSERT INTO dsh_smoke.orders VALUES (1, \'smoke\', 9.9);\n', + ) + // Doris: fresh database + table + one row (connect to an existing db first). + await requireSuccess( + { ...doris, database: 'information_schema' }, + 'DROP DATABASE IF EXISTS dsh_smoke;\n' + + 'CREATE DATABASE dsh_smoke;\n' + + 'USE dsh_smoke;\n' + + 'CREATE TABLE orders (id INT, name VARCHAR(32), amount DECIMAL(10,2)) ' + + 'ENGINE=OLAP DUPLICATE KEY(id) DISTRIBUTED BY HASH(id) BUCKETS 1 ' + + "PROPERTIES('replication_num'='1');\n" + + 'INSERT INTO orders VALUES (1, \'smoke\', 9.9);\n', + ) + // SQL Server: fresh database + table + one row (GO batches via stdin). + await requireSuccess( + { ...sqlserver, database: 'master' }, + 'DROP DATABASE IF EXISTS dsh_smoke;\nGO\n' + + 'CREATE DATABASE dsh_smoke;\nGO\n' + + 'USE dsh_smoke;\nGO\n' + + 'CREATE TABLE dbo.orders (id INT NOT NULL, name NVARCHAR(32) NULL, amount DECIMAL(10,2) NULL);\nGO\n' + + "INSERT INTO dbo.orders VALUES (1, N'smoke', 9.9);\nGO\n", + ) + }, 180_000) + + afterAll(async () => { + const cleanup: Array> = [] + cleanup.push(runFor({ ...clickhouse, database: 'default' }, 'DROP DATABASE IF EXISTS dsh_smoke;\n')) + cleanup.push(runFor({ ...doris, database: 'information_schema' }, 'DROP DATABASE IF EXISTS dsh_smoke;\n')) + cleanup.push(runFor( + { ...sqlserver, database: 'master' }, + 'USE master;\nGO\nDROP DATABASE IF EXISTS dsh_smoke;\nGO\n', + )) + await Promise.allSettled(cleanup) + }, 120_000) + + for (const { name, connection, browseSchema } of targets) { + describe(name, () => { + it('passes the /connect table-listing check', async () => { + const result = await runFor(connection, tableListingSql(connection.type, connection)) + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0) + const tables = parseTableListing(connection.type, result.stdout) + expect(tables).toContain('orders') + }, 60_000) + + it('lists schemas/databases through the metadata query', async () => { + const result = await runFor(connection, metadataQuery('schemas', connection.type)) + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0) + const schemas = parseListing(connection.type, result.stdout) + expect(schemas).toContain(browseSchema) + }, 60_000) + + it('lists tables and describes the smoke table', async () => { + const tablesResult = await runFor( + connection, + metadataQuery('tables', connection.type, browseSchema), + ) + expect(tablesResult.exitCode, `stderr: ${tablesResult.stderr}`).toBe(0) + const tables = parseListing(connection.type, tablesResult.stdout) + expect(tables).toContain('orders') + + const describeResult = await runFor( + connection, + metadataQuery('describe', connection.type, browseSchema, 'orders'), + ) + expect(describeResult.exitCode, `stderr: ${describeResult.stderr}`).toBe(0) + const columns = parseColumns(connection.type, describeResult.stdout) + expect(columns.map(column => column.name)).toContain('id') + expect(columns.find(column => column.name === 'amount')?.type).toBeTruthy() + }, 60_000) + + it('runs a query through the non-introspect template', async () => { + const result = await runFor(connection, 'SELECT COUNT(*) FROM orders;\n', false) + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0) + expect(result.stdout).toContain('1') + }, 60_000) + + it('classifies reads/writes for the readonly guard', () => { + expect(classifyStatement('SELECT * FROM orders', connection.type)).toBe('read') + expect(classifyStatement('INSERT INTO orders VALUES (2)', connection.type)).toBe('write') + }) + }) + } + }, +) diff --git a/tests/persistence.spec.ts b/tests/persistence.spec.ts index fd02f37..f31582c 100644 --- a/tests/persistence.spec.ts +++ b/tests/persistence.spec.ts @@ -70,6 +70,15 @@ describe('persistence', () => { expect(loadConnection(storage)).toEqual(sqlite) }) + it('round-trips the new database types', () => { + const storage = memoryStorage() + for (const type of ['clickhouse', 'doris', 'sqlserver'] as const) { + const connection = { type, host: 'h', port: 9000, user: 'u', database: 'db', savedAt: 'x' } + saveConnection(connection, storage) + expect(loadConnection(storage)).toEqual(connection) + } + }) + it('returns null for absent data', () => { expect(loadConnection(memoryStorage())).toBeNull() }) diff --git a/tests/routes.spec.ts b/tests/routes.spec.ts new file mode 100644 index 0000000..0bbd7d2 --- /dev/null +++ b/tests/routes.spec.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest' +import { validateConnectBody } from '../src/routes.ts' + +describe('validateConnectBody — clickhouse/doris/sqlserver', () => { + it('accepts the three new database types', () => { + expect(validateConnectBody({ sessionId: 's1', type: 'clickhouse', database: 'analytics' })) + .toMatchObject({ type: 'clickhouse', database: 'analytics' }) + expect(validateConnectBody({ sessionId: 's1', type: 'doris', host: 'fe01', port: 9030, database: 'dwd' })) + .toMatchObject({ type: 'doris', port: 9030 }) + expect(validateConnectBody({ sessionId: 's1', type: 'sqlserver', user: 'sa', password: 'pw', database: 'sales' })) + .toMatchObject({ type: 'sqlserver', password: 'pw' }) + }) + + it('keeps rejecting unknown types', () => { + expect(() => validateConnectBody({ sessionId: 's1', type: 'flinksql', database: 'x' })) + .toThrow(/type/) + }) + + it('still requires a non-empty database for the new types', () => { + expect(() => validateConnectBody({ sessionId: 's1', type: 'clickhouse', database: '' })) + .toThrow(/database/) + }) + + it('resolves sqlite file paths to absolute while leaving the new types intact', () => { + const sqlite = validateConnectBody({ sessionId: 's1', type: 'sqlite', database: 'orders.db' }, '/work') + expect(sqlite.database.startsWith('/work/')).toBe(true) + const sqlserver = validateConnectBody({ sessionId: 's1', type: 'sqlserver', database: 'sales' }) + expect(sqlserver.database).toBe('sales') + }) +})