dsh-dbhub-live 4.1.0 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -10,18 +10,22 @@ DSH host 插件(Node ESM)+ Web client 插件(浏览器半):把 dbhub
10
10
 
11
11
  ```
12
12
  lib/
13
- index.mjs 入口:name/inject/apply、状态命名空间(schemastery)接线、后台启动(仅登记+更新检查)、配置变更应用
14
- config.mjs 路径/原子持久化(store,runtime)/DSN 解析/工作区发现/零知识助手(describeConn,connLabel,scrubSecrets)/小工具
13
+ index.mjs 入口:name/inject/apply、Config(volatile 选项)、发布核心(createHostCore)、两条通道接线(0.1.x settings 命名空间 / connection.fetch HTTP 桥)、settings 表单策略与选项写回
14
+ bridge.mjs HTTP 桥(纯函数):/api/dsh-dbhub-live 四条 exact Fetch 路由的请求/响应编解码
15
+ config.mjs 路径/原子持久化(store,runtime,prefs)/DSN 解析/工作区发现/零知识助手(describeConn,connLabel,scrubSecrets)/小工具
15
16
  state.mjs 运行时状态机:enabled/phase/toolCount/lastError/mode + 订阅发布(无服务器生命周期)
16
- options.mjs 可配置参数:updateIntervalDays(设置UI > 环境默认;dbhubPackage 仅内部环境knob)
17
+ options.mjs 可配置参数:updateIntervalDays/showSidebarEntry(Config 采纳 > prefs.json > 0.1.x 设置文档 > 环境变量 > 内置默认;dbhubPackage 仅内部环境knob)
17
18
  runtime.mjs dbhub 可执行文件发现、按需自动安装、定期自动更新(已删除孤儿进程清理)
18
19
  mcp.mjs MCP JSON-RPC 客户端 + 工具注册表 + 工作区×环境来源发现(collectSources/resolveSource)+ 摘要缓存
19
20
  adhoc.mjs 唯一执行引擎:每次调用一条一次性 dbhub 进程(零知识标签 + stderr 清洗)
20
21
  collect.mjs 授权扫描项目配置文件并提取 DSN 候选(含 askUser 桥接)
21
22
  tools.mjs host 自有工具定义与注册:恒定 4 个(configure/list_sources/execute_sql/search_objects);configure 探连优先(见零知识契约 5)
22
- client.js Web 半(手写 lazy-CJS bundle):配置页 = `settings.section` 一级设置页 + 可选侧边栏面板(`showSidebarEntry` 开关)+ `plugins.row.config`(summary/page 两视图)
23
- test/ node:test 单元测试(纯逻辑 + client bundle 格式契约 + 零知识泄漏门)
23
+ client.js Web 半(手写 lazy-CJS bundle):配置页 = `settings.section` 一级设置页 + 可选侧边栏面板(`showSidebarEntry` 开关)+ `plugins.row.config`(summary/page 两视图);host face 按运行时探测(settingsScope 或 HTTP 桥)
24
+ test/ node:test 单元测试(纯逻辑 + bridge 契约 + client bundle 格式契约 + 零知识泄漏门)
25
+ scripts/ 仓库维护脚本:desensitize.config.mjs(**脱敏槽位表 = 单一事实源**)、desensitize-check.mjs(脱敏门禁)、render-check.mjs(客户端布局离屏渲染/交互门禁,无需 React 依赖)
26
+ .github/ CI:push/PR 跑语法检查 + 脱敏门禁 + 单测
24
27
  doc/ 需求文档
28
+ .env 本地脱敏黑名单(DSH_DBHUB_DESENSITIZE_RE),gitignore 排除、不进 npm 包——**唯一允许存放真实标识的地方**
25
29
  cordis.patch.yml bundle patch:`name: dsh-dbhub-live` 挂载本包
26
30
  ```
27
31
 
@@ -36,9 +40,11 @@ cordis.patch.yml bundle patch:`name: dsh-dbhub-live` 挂载本包
36
40
  4. **鉴权/连接失败闭环**:`likelyAuthOrConnError` 命中时在错误文本后附 `result.authHint`(引导 `dbhub_configure` 或配置页修改,密码由用户输入)。
37
41
  5. **configure 探连优先(交互红线)**:`dbhub_configure` 收到非敏感预填(type/host/database 齐备、非 sqlite)时先宿主侧试连候选 DSN(空密码):可连通 → 直接持久化返回,**零弹窗**;试连提示需凭据 → 只弹「账号(未知时)+ 密码」最小输入框(`result.ask-*`),其余信息自动沿用;信息不完整或非鉴权失败 → 才弹完整选项(DSN/分项/扫描)。已配置行先探真实 DSN(含已存密码),通过即 `existing-ok` 零输入返回;**模型参数改变了端点(type/host/port/database 任一不同,`argsChangedEndpoint`)时视为新目标,对预填候选重跑试连循环**。凭据弹窗带诊断明细(`detail` = 试连失败原因)与逃生口(`result.askCredMore` → 回到完整选项,避免凭据弹窗死循环;凭据保存后再探失败附 `result.authHint`)。模式菜单里**不再有「仅填写密码」选项**——最小凭据框已取代它;「仍用现有连接」是字面沿用(不补填缺失字段,`result.useExisting*` 文案已澄清);分项表单会把已知值带入(`result.askHost/askPort/askUserField/askDatabase`,未触碰字段保留预填值)。**试连诊断明确指向“空账号被拒”(`emptyUserDenied`,如 `Access denied for user ''@…`)时,账号为必填**:最小凭据框不提供「留空(使用空账号)」选项,用户仍提交空账号则拒绝保存(`result.askAccountRequired*`),避免把同一个必失败的 DSN 再存回去。决策逻辑收敛在 `config.mjs` 的 `decideConfigureStep`/`argsChangedEndpoint`/`emptyUserDenied`(纯函数,`util.test` 覆盖)。
38
42
  6. **零密码泄漏门**:`test/zero-knowledge.test.mjs` 断言 summarizeRows 无 dsn 字段、无工具声明 `dsn` 参数、描述/i18n 文案不含真实形态 DSN;`util.test` 断言 scrubSecrets/connLabel/buildDsnFromParts/decideConfigureStep 行为。改模型可见文案时这些测试必须保持绿。
39
- - **状态命名空间**:Host 注册 `dsh-dbhub-live` 命名空间(`ctx.inject(['settings'], …)` + schema);Web 端有三个入口指向同一个页面:**`settings.section`(id `dbhub`,主入口,settings 面板是核心 shell)**、**可选侧边栏面板**(`sidebar.panellist` + `main`,受 `showSidebarEntry` 控制)、**`plugins.row.config`**(键 `<包名>#<行 id>` = `dsh-dbhub-live#dbhub-live`,owner props 为 `view: 'summary' | 'page'`,该 slot 只在官方插件页的浏览器半区活跃时存在)。**dsh 0.1.6 已删除旧的 `settings.plugin.item` 槽位**:注册到不存在的 slot 会被静默忽略(不报错、界面什么都不显示)——升级 dsh 后「配置页消失」就是这个原因,不要再改回去。命名空间值 = 状态 `{enabled, phase(running|disabled), toolCount(恒定4), lastError, mode('oneshot')}` + 配置 `{updateIntervalDays, showSidebarEntry}` + `{workspaces(JSON 元数据摘要:title/path/env/conn(主机端口库)/source/persisted/srcId)}` + `{configOp}`(主机消费后自动清空)+ `{testResult}`(连接测试的一次性回执,仅内存态、不落任何持久化)。**注入回调是独立作用域:内部需要的服务(如 `subprocess`)必须用 `ctx.get('subprocess')` 就地获取,绝不能引用 `apply()` 的局部变量——否则 ReferenceError 会静默杀死整条发布/配置链路(配置页只剩静态值、工作区连接不刷新)**。回调整体套 `wrap()` 防护,异常必须打日志。
40
- - **可配置参数**:`options.mjs` 只此一处持有部署开关:`updateIntervalDays`(设置 UI > 环境变量种子 > 内置默认;`dbhubPackage` 仅环境变量/内部,不进设置)、`showSidebarEntry`(布尔,默认 `true`,设置页开关即时生效)。**已删除 `idleMinutes`**(无常驻进程可回收)。不要绕过 `applyPatch` 直接改 `options` 内部值。
41
- - **工作区连接管理(configOp 通道)**:store v2 = `{ [wsPath]: { environments: { [env]: {dsn, source, updatedAt} } } }`(v1 单 dsn 条目自动迁移进 `environments.default`)。环境名一律过 `normalizeEnvName`(trim、去控制字符、限长、空值收敛为 `default`),因此**中文环境名是一等公民**。配置页只读**元数据**摘要(`collectSources` → `latestSummaries`,密码/用户名永不出 Host);增/改/删/改名通过命名空间 `configOp` 单向命令下发,`index.mjs` 用 `setWorkspaceEnv`/`removeWorkspaceEnv`/`renameWorkspaceEnv` 落盘后重扫摘要并发布(configOp 随发布清空,天然防环)。`add`/`set` 可带 `renameFrom` 做「改名 + 改连接」的**单条原子命令**(`configOp` 是单字段,拆两次写会互相覆盖);纯改名走 `{op:'rename', workspace, env, newEnv}`。自动发现(mise/.env)只提供未覆盖的 `default`,不持久化。**连接测试(`{op:'test', workspace, env, nonce}`)**:`handleTestOp` 用摘要背后的真实 DSN 调 `adhoc.probeConnection`(一次性 dbhub + `SELECT 1`,Host 侧 30s 超时兜底),结果经 `testResult` 镜像字段回推 `{nonce, ok, message}`;测试失败只是行内一次性提示,不得写入 store/lastError/phase。配置页只认自己派发过的 nonce,展示 ~10s 自动消退,刷新即失,天然不保留状态。
43
+ - **状态通道(双栈,5.0 迭代)**:Host 只有**一个**发布核心(`index.mjs` 的 `createHostCore`:walk / row-mirror / `runOp` / `view()`),由**两条通道**适配到浏览器:① **0.1.x 的 settings 命名空间**(`ctx.inject(['settings'])` + `settings.register(…, STATUS_SCHEMA, {base})` → `scope.replace/watch`,仅在 `typeof settings.register === 'function'` 时启用);② **鉴权 HTTP 桥**(`ctx.inject(['connection'])` → `connection.fetch.register` 注册 `/api/dsh-dbhub-live/{state,enabled,options,op}` 四条 exact Fetch 路由,**两条 dsh 线都有**)。dsh 0.1.7 用 Config 表单取代了 settings 命名空间(`register` 消失),0.2.x 又删掉了浏览器侧的 `settingsScope` 服务——**这是 5.0 移植的根因**:旧版客户端 `inject: ["slots","settingsScope"]` 永远等不到服务,整个浏览器半区 pending(页面/侧边栏/卡片全部静默消失,连报错都没有)。因此客户端**只 inject `slots`**,host face 用 `ctx.get('settingsScope')` 探测:有就用 legacy 命名空间,没有就走 HTTP 桥(`httpFace()`);写路由继承 connection 的 Host/Origin 围栏与浏览器鉴权(无 cookie 的 `/api/dsh-dbhub-live/*` 返回 401,实测)。路由编解码在纯函数模块 `lib/bridge.mjs`(`test/bridge.test.mjs` 覆盖)。Web 端有三个入口指向同一个页面:**`settings.section`(id `dbhub`,主入口,settings 面板是核心 shell)**、**可选侧边栏面板**(`sidebar.panellist` + `main`,受 `showSidebarEntry` 控制)、**`plugins.row.config`**(键 `<包名>#<行 id>` = `dsh-dbhub-live#dbhub-live`,owner props 为 `view: 'summary' | 'page'`,该 slot 只在官方插件页的浏览器半区活跃时存在)。**dsh 0.1.6 已删除旧的 `settings.plugin.item` 槽位**:注册到不存在的 slot 会被静默忽略(不报错、界面什么都不显示)——升级 dsh 后「配置页消失」就是这个原因,不要再改回去。**两条通道发布的是同一个 view**:状态 `{enabled, phase(running|disabled), toolCount(恒定4), lastError, mode('oneshot')}` + 配置 `{updateIntervalDays, showSidebarEntry}` + `{workspaces(JSON 元数据摘要:title/path/env/conn(主机端口库)/source/persisted/srcId)}` + `{testResult}`(连接测试的一次性回执,仅内存态、不落任何持久化);legacy 命名空间里额外有 `{configOp}`(主机消费后由下一次发布清空,天然防环),HTTP 桥则把它换成 `POST /op` 的请求体——**DSN 依旧只从浏览器流向 Host,永不进模型上下文**。**注入回调是独立作用域:内部需要的服务(如 `subprocess`)必须用 `ctx.get('subprocess')` 就地获取,绝不能引用 `apply()` 的局部变量——否则 ReferenceError 会静默杀死整条发布/配置链路(配置页只剩静态值、工作区连接不刷新)**;发布核心把这个读取收敛在 `subprocessOf()` 里,两条通道都不再自己捕获。legacy 回调整体套 `wrap()` 防护,异常必须打日志。
44
+ - **可配置参数(三条通道,一个有效值)**:`options.mjs` 只此一处持有部署开关:`updateIntervalDays`、`showSidebarEntry`(布尔,默认 `true`,设置页开关即时生效);`dbhubPackage` 仅环境变量/内部,不进设置。三条通道都汇到同一个有效值:① **`Config`(named export,两个 `.volatile()` 字段)**——0.1.7+/0.2.x 官方「插件」表单与 cordis.yml 的写入口,**没有默认值**(未设置 = `undefined`,与"显式选择"可区分);② **`prefs.json`**——有效值的存储(页面写入落点);③ **0.1.x 设置文档**——只在 boot 时补 prefs 未认领的字段。层叠(低→高):内置默认 < 环境变量 < 0.1.x 设置文档 < prefs.json;**显式 Config 值在 boot 与 `loader/volatile-update` 时被"采纳"**(`options.configValuesOf(config)` → `applyPatch(..., {persist:true})`),因此官方表单改一项、本插件页面也立刻显示同一值,反向亦然(页面写入 → `settings.update(entryId, …)` 写回 profile patch,实测会真实改写 patch 文件)。`apply(ctx, config)` 的第二个参数就是这个 Config;`entryIdOf(ctx)` 用 `ctx.fiber.entry.options.id` 运行时解析 entry id(**不要写死行 id**,部署方可以换)。**已删除 `idleMinutes`**(无常驻进程可回收)。不要绕过 `applyPatch` 直接改 `options` 内部值。
45
+ - **工作区连接管理(configOp 通道)**:store v2 = `{ [wsPath]: { environments: { [env]: {dsn, source, updatedAt} } } }`(v1 单 dsn 条目自动迁移进 `environments.default`)。环境名一律过 `normalizeEnvName`(trim、去控制字符、限长、空值收敛为 `default`),因此**中文环境名是一等公民**。配置页只读**元数据**摘要(`collectSources` → `latestSummaries`,密码/用户名永不出 Host);增/改/删/改名通过命名空间 `configOp` 单向命令下发,`index.mjs` 用 `setWorkspaceEnv`/`removeWorkspaceEnv`/`renameWorkspaceEnv` 落盘后重扫摘要并发布(configOp 随发布清空,天然防环)。`add`/`set` 可带 `renameFrom` 做「改名 + 改连接」的**单条原子命令**(`configOp` 是单字段,拆两次写会互相覆盖);纯改名走 `{op:'rename', workspace, env, newEnv}`。自动发现(mise/.env)只提供未覆盖的 `default`,不持久化。**连接测试(`{op:'test', workspace, env, nonce}`)**:`handleTestOp` 用摘要背后的真实 DSN 调 `adhoc.probeConnection`(一次性 dbhub + `SELECT 1`,Host 侧 30s 超时兜底),结果经 `testResult` 镜像字段回推 `{nonce, ok, message}`;测试失败只是行内一次性提示,不得写入 store/lastError/phase。配置页只认自己派发过的 nonce,展示 ~10s 自动消退,刷新即失,天然不保留状态。**覆盖门(`requestWrite`/`envCollision`,4.2 迭代)**:`setWorkspaceEnv` 是**无条件写入**,所以"新增落到已存在环境"和"改名落到已存在环境"在宿主侧都会静默替换原连接。客户端半区因此**先问后写**:`requestWrite(form, check)` 用 `envCollision(workspaces, ws, env)` 判定(环境名比较必须先过 `normalizeEnvForCompare`——它镜像宿主的 `normalizeEnvName`,否则"`prod `"与"`prod`"这种同键异写会漏判),命中就弹 `dbh-warncard`(`role="alert"`,取消/覆盖两个出口),**确认后才走原有的 `dispatchPretest` 预检流程**,取消则一个 op 都不发且保留表单内容。仅"改同一个环境的连接"不设门(编辑的本意)。局限:workspace 留空时页面无法知道宿主会选哪个工作区(`resolveWorkspaceRef` 回退到 `workspaces[0]`),只能用"第一行所属工作区"做近似,不确定时显示 `ovr.bodyAmbiguous` 提示而非断言;要精确判定需要新增宿主侧预检 op(暂不做)。回归门:`npm run check:ui` 第 15/16 组。**即时回推 + 扫描串行化(`createRowMirror`/`publishFast`,4.2 迭代)**:`collectSources` 每次都要重扫全部工作区(没有持久化 `default` 的工作区会各起一次 `mise env`),一次卡片写入过去要等 1–2 秒才在界面上体现;更糟的是**并发扫描互相覆盖**——探针回执触发的那次扫描可能在写入前就开始、在写入后才结束,把用户刚存的行又刷掉(现场症状:行"立刻出现、又立刻消失,之后一直不出来")。现在的规则:① `handleConfigOp` **返回本次生效的 patch 列表**(`add`/`set`/`rename`/`remove`;`test` 与失败为 `[]`)并立刻 `publishFast`(把补丁套进缓存行后同步 `scope.replace`,仅当 `hasRowCache()` 为真——未扫描的空缓存不能发布);② **`reportTest` 只 `publishMirror()` 不再扫描**(探针不改变任何行);③ 扫描**单飞**(`runWalk`:进行中只记 `walkRequested`,结束后补跑一次),并且 `createRowMirror` 会把补丁**持有到"在它之后才开始的那次扫描"落地**为止(`beginWalk`/`endWalk` 对扫描期间到达的补丁重新套用)——所以任何一次扫描都不可能发布出缺少该写入的列表;④ `applyRowPatch` 的 `rename` 必须**幂等**(`from` 行已不存在时返回原列表而不是把 `to` 行删掉),否则重放补丁会丢行。一次点击的开销:1 次扫描 + 2 次纯内存镜像(原来是 3 次扫描)。补丁行里的 `dsn` 只存在于宿主内存,`summarizeRows` 依旧只输出 `conn` 元数据。回归门:`npm run check:ui` 第 15/16/17 组 + `node test/row-cache.test.mjs`(含重放/幂等/扫描期间补丁重放三条)。
46
+ - **自动发现的代价与边界(4.2 迭代,别退回"每次同步阻塞探测")**:`collectSources` 会对**没有持久化 `default`** 的工作区逐个跑 `runMiseEnv`(spawn `mise env`)。三个硬约束:① **每次探测必须有总超时**(`MISE_ENV_TIMEOUT_MS`,`Promise.race` + `terminate()`),`graceMs` 只是 SIGTERM→SIGKILL 的宽限、**不构成超时**——曾经一个不退出的 `mise env` 会让整轮扫描永不返回,而 `dbhub_list_sources` 走的就是这条同步路径,模型侧看到的是调用被中断(`Interrupted`),偏偏卡片因为持有补丁还显示着旧行;② **探测结果(含"什么都没发现")按工作区路径短期缓存**(`AUTO_DSN_TTL_MS`,`resetAutoDsnCache()` 供测试与显式重扫),否则每一次发布、每一次模型列源/执行都要重新 spawn 一遍 `mise env`;③ **工作区之间并发解析**(`Promise.all`),一个慢目录不得串行拖累其他工作区。回归门:`node test/auto-dsn.test.mjs`(挂起→限时放弃并 terminate、拒绝可执行文件、TTL 内复用、有持久化 default 时不探测)。另:`resolveWorkspaceRef(ref, ctx)` 必须用**与扫描同一个** `listWorkspaces(ctx)` 解析工作区,否则卡片写入的 store key 与扫描查的 key 可能不是同一个拼写,出现"设置里有、模型看不到"。
47
+ - **预算计时器必须保持被引用:绝不要 `unref()`**(`runMiseEnv` 的总超时、`withProbeTimeout` 的探针兜底都是)。unref 的计时器在"它是唯一待办"时根本不会触发——事件循环直接排空、进程先退出,超时**形同虚设**;探针兜底还会留下一个活着的计时器,所以要在竞速结束时 `.finally(() => clearTimeout(t))`。这个缺陷**只在 Linux CI 暴露**(`Promise resolution is still pending but the event loop has already resolved`,整文件被判 cancelled),本地 Windows 会因为别的句柄侥幸留住循环而"通过"——别拿本地绿灯当证据,改这类代码后必须看 CI。
42
48
  - **工具声明数量恒定(上下文预算红线)**:恒定 4 个(configure / list_sources / execute_sql / search_objects),**绝不为每个工作区 × 环境注册独有工具**;`dbhub_execute_sql(source,…)` / `dbhub_search_objects(source,…)` 调用时 `resolveSource` 按 source 值解析到真实连接。曾实现过的 per-source 注册与 `dbhub_query` 临时连接工具已移除,勿回恢复。
43
49
  - **工作区语义(模型契约,勿退化)**:**每条 source 只属于一个工作区**,跨工作区的「看起来一样」的连接是不同目标。`sourceIdOf(row)` 是 source 值的**唯一**构造点(`<标题slug>_<工作区路径哈希>[_<环境slug>]`),列表输出、解析器、各工具回执必须都调它,否则模型会拿到解析不回去的句柄。`envSlug`:`default` 保持裸值、纯 ASCII 名保持原 slug(`test`→`test`,既有 source 值不变)、含非 ASCII 的名(`线上`)取 `env-<shortHash>`——**旧实现对每个纯中文名都返回同一个 slug**,`线上` 与 `测试` 曾共用 source 值并被静默查错库;这是回归红线。`resolveSource(ctx, sub, ref, {preferredWsPath})` 精确优先,其次按**当前会话工作区**(`currentWorkspace(ctx, exec)`:会话 cwd 精确匹配 → 最长包含路径)筛选,仍有多条则返回 `{ambiguous:[…]}`,由 `result.ambiguousSource` 让模型用完整 source 值澄清——**绝不按列表顺序猜**。`dbhub_configure` 的工作区解析也**不再回退到 `workspaces[0]`**(那是「把连接配到别人工作区」的隐患):显式 workspace 必须匹配,否则 `result.unknownWorkspace` 列出可用工作区;未给 workspace 时用当前工作区,解析不出来则 `result.needWorkspace`。跨工作区复用连接一律走 `copyFrom`(宿主侧复制 DSN,零知识不破)而不是直接用别人的 source。`dbhub_list_sources` 按工作区分组并标出【当前工作区】(`result.wsCurrentHead`/`wsOtherHead`/`wsGroup`)。
44
50
  - **schema 弹性依赖**:优先用真实 `@deepseek-ai/schemastery` schema(`await import`);解析失败时降级为 `lib/index.mjs` 内建的最小 callable schema(`schema(v)` 合默认值 + `toJSON()`),保证**链路安装(`dsh plugin add <本地目录>`,Node ESM 按源码真实路径解析裸导入)下插件照样启动、卡片照常工作**。tarball/npm 安装(真实目录在 profile node_modules 下)走真实 schemastery 路径。不要把这个 import 改回静态顶层 import——会重新引入链路安装时启动失败。
@@ -47,9 +53,9 @@ cordis.patch.yml bundle patch:`name: dsh-dbhub-live` 挂载本包
47
53
 
48
54
  ## 存储与容错(初始化即处理)
49
55
 
50
- - **实例隔离**:所有持久化都在 `$DSH_HOME/storages/dsh-dbhub-live/`(`credentials.json` 凭据+enabled、`runtime.json`、`dbhub-runtime/` 自动安装前缀)。隔离粒度 = `DSH_HOME`(同一 home 的多个 profile 共享,与 dsh 自身 workspace.json 约定一致);执行进程、状态机、工具注册天然按进程隔离。**进程环境变量不参与连接解析**。不再有 dbhub.toml;同 home 并发多实例的残余共享只余 `credentials.json`(用户驱动低频写),写出已原子化(见下)。
56
+ - **实例隔离**:所有持久化都在 `$DSH_HOME/storages/dsh-dbhub-live/`(`credentials.json` 凭据+enabled、`prefs.json` 卡片选项、`runtime.json`、`dbhub-runtime/` 自动安装前缀)。隔离粒度 = `DSH_HOME`(同一 home 的多个 profile 共享,与 dsh 自身 workspace.json 约定一致);执行进程、状态机、工具注册天然按进程隔离。**进程环境变量不参与连接解析**。不再有 dbhub.toml;同 home 并发多实例的残余共享只余 `credentials.json`/`prefs.json`(用户驱动低频写),写出已原子化(见下)。
51
57
  - **原子写**:`writeJsonFile` 一律 tmp+rename(同目录写临时文件再改名覆盖),并发读者永不见半个文件、并发写者末写覆盖不交错;目录被删时先 `mkdirSync` 重建。写入失败**不抛致命**,`warnOnce` 一次性告警并继续内存态运行(`saveStore`/`saveRuntime` 返回 false)。
52
- - **升级/手改遗留兼容**:`loadStore`/`loadRuntime` 先用纯函数 `normalizeStore`/`normalizeRuntime` 清洗:丢弃非布尔 `enabled`、非对象/空 dsn 条目、非法 `dbhubExe`/`dbhubInstallAt`;`dsn` 统一 trim;v1 单 dsn 条目自动迁移为 `environments.default`;**未知字段保留**(向前兼容)。清洗结果与原文不同时**一次性回写迁移**。
58
+ - **升级/手改遗留兼容**:`loadStore`/`loadRuntime`/`loadPrefs` 先用纯函数 `normalizeStore`/`normalizeRuntime`/`normalizePrefs` 清洗:丢弃非布尔 `enabled`、非对象/空 dsn 条目、非法 `dbhubExe`/`dbhubInstallAt`;`dsn` 统一 trim;v1 单 dsn 条目自动迁移为 `environments.default`;**未知字段保留**(向前兼容)。清洗结果与原文不同时**一次性回写迁移**(prefs 只保留两个已知字段,非法值直接丢弃而不是回写)。
53
59
  - **空值安全**:解析器对缺失/空值全部有兜底(`resolveWorkspaceEnvs` 判空、`describeConn` 对不可解析 DSN 兜底、状态 schema 默认值、settings 镜像的 `enabled` 只认布尔),清洗后不存在半吊子条目。
54
60
 
55
61
  ## 调试方法(不影响正在运行的 Harness)
@@ -58,16 +64,40 @@ DSH 启动是 fail-loud:任一插件激活失败整树拒绝启动、GUI 打
58
64
 
59
65
  1. **静态校验(30 秒,零风险)** — `npm run check`(node --check 全部 lib)→ 逐个跑单测(沙箱下 `node --test` 的 runner 子进程会被 EPERM 挡,按本文件惯例**逐个文件**跑:`node test/x.test.mjs`)。单测会自己建临时 `DSH_HOME`,不会碰真实 store。
60
66
  2. **会话内跑通逻辑(不重启)** — 用动态插件工具连(`cordis_define`/`cordis_run`/`cordis_stop`/`cordis_undefine`):把要验证的纯逻辑(如 configure 流程、scrubSecrets)以无 import 的 Host 代码贴进动态包,在会话里跑,`cordis_stop` 即清场。动态包只活在进程内存 + 当前会话,改动/出错都不影响主进程。动态 Host 代码不能用 `import`,带 `@deepseek-ai/schemastery` import 的 `lib/index.mjs` 不能整文件贴进动态包;只对纯逻辑做动态验证。
61
- 3. **冷启动验证(隔离实例)** — 用独立 `DSH_HOME` 起测试实例。本机备有独立测试 home:`deepseek-harness/mise.toml` 已把 `DSH_HOME` 指向 `D:/my/app/dsh/plugin/.dsh-a`,所以在该目录下用 `mise r dsh …` 启动的实例**与主进程(`%USERPROFILE%\.dsh`,3080)完全隔离**:
67
+ 3. **冷启动验证(隔离实例)** — 用独立 `DSH_HOME` 起测试实例。本机备有独立测试 home:`.dsh-a`(`deepseek-harness/mise.toml` 把 `DSH_HOME` 指向它)。两条可用路径:
68
+ - **源码实例(`mise r dsh …`,走 tsx)**:`cd D:\my\app\dsh\plugin\deepseek-harness` 后 `mise r dsh web --port 3082 --no-open`。注意 `pnpm dsh` 会先跑 pnpm 依赖校验,node_modules 不同步时它会想重装并因无 TTY 中止——直接 `node --import tsx/esm apps/cli/src/bin.ts …` 可跳过;esbuild 的 spawn 在沙箱下会 EPERM(属沙箱边界,别绕)。
69
+ - **已安装实例(推荐,无 esbuild/pnpm)**:直接用 npm 安装版 CLI,`$env:DSH_HOME='D:/my/app/dsh/plugin/.dsh-a'`,并**清掉继承来的 `DSH_PROFILE`/`DSH_PROFILE_DIR`**(它们指向主 profile 会造成污染/参数冲突),然后 `node <dsh 安装目录>/lib/bin.js web --port 3082 --no-open`。`dsh web` 自带 `web` profile,**不要再传 `--profile web`**(会报 "select a profile only once")。
62
70
  ```powershell
63
- cd D:\my\app\dsh\plugin\deepseek-harness # mise.toml 在此,DSH_HOME 已指向 .dsh-a
64
- mise r dsh web --port 3082 --no-open # 首次自动初始化 web 模板
65
- mise r dsh plugin --profile web add D:/path/to/<pkg>.tgz # 用 pnpm pack 产物;同版本会被跳过,必须先升版本号
66
- mise r dsh plugin --profile web ls # 核对 node_modules/<pkg>/package.json 版本确已替换
67
- mise r dsh web --port 3082 --no-open
68
- # 验证:日志出现 [dsh-dbhub-live] 加载 + 「设置页入口已注册: settings.section id=dbhub」「插件页配置入口已注册: plugins.row.config …」;
69
- # GET /plugins/dsh-dbhub-live/client.js 返回 bundle;http://127.0.0.1:3082 侧边栏出现「DBHub 数据库工具」、设置里出现同名一级页面
71
+ # 装包:pnpm 需要可写 store(默认 store 在 workspace 外会被沙箱拒绝)
72
+ $env:npm_config_store_dir='D:/my/app/dsh/plugin/.pnpm-store'; pnpm pack
73
+ node <dsh>/lib/bin.js plugin --profile web add D:/path/to/dsh-dbhub-live-<ver>.tgz
74
+ # 沙箱内 pnpm 可能事先被别处 store 链接过而报 UNEXPECTED_STORE;此时可手工替换
75
+ # .dsh-a/profiles/web/node_modules/dsh-dbhub-live/ 的目录内容(file: 依赖就是普通目录,
76
+ # 不写 store、不动 lockfile),仅用于验证,不要当成发布流程。
77
+ # 验证清单(0.2.x):
78
+ # 日志:`[dsh-dbhub-live] host apply` + `dbhub 插件已加载(4 个工具)` + `浏览器桥接已注册: /api/dsh-dbhub-live/*`
79
+ # 取 token(启动日志里的 ?token=…)→ GET /?token=… 拿会话 cookie →
80
+ # GET /api/dsh-dbhub-live/state # 200 + {ok,value};不带 cookie 必须 401
81
+ # POST /api/dsh-dbhub-live/options {"updateIntervalDays":3} # 200,且 $DSH_HOME/storages/dsh-dbhub-live/prefs.json 落盘
82
+ # POST /api/dsh-dbhub-live/op {"op":{"op":"nope"}} # 200 + patches: []
83
+ # volatile Config 通道(0.1.7+/0.2.x):
84
+ # 在 profile 的 cordis.patch.yml 里钉 `- id: <行 id>` + `config: {updateIntervalDays: 5}` → 重启后
85
+ # 视图与 prefs.json 都应变 5(采纳),日志出现「配置表单策略已注册: auto=false」;
86
+ # 再 POST /options {"updateIntervalDays":9} → **profile patch 文件本身**应被改写成 9(写回),
87
+ # 视图里的 entryId 应等于该行 id(不是包名)。
88
+ # 客户端包:/plugins/??dsh-dbhub-live/client.js&rev=<index HTML 里的 rev> # 200(**必须带 rev,单斜杠路径一律 404**)
89
+ # 浏览器:http://127.0.0.1:3082 侧边栏出现「DBHub 数据库工具」、设置里出现同名一级页面,行内显示连接元数据
70
90
  ```
91
+ **取 bundle URL 的正确方式**:index HTML 里嵌着模块清单 JSON(`{"id":"dsh-dbhub-live","url":"/plugins/??…&rev=<rev>"}`),**rev 带 `-<n>` 后缀**(如 `d4aeeae7c6c2a859-55`,每次渲染会变);用正则从组合列表里截 rev 会截断后缀而 404,必须整段取出该 `url` 再请求。
92
+ **0.1.x 线(双栈的另一半)**:另起一个 scratch home 装 0.1.6 的 CLI 跑一遍即可,`dsh web` 会自己初始化 web 模板:
93
+ ```powershell
94
+ # 用 0.1.6 的 CLI 初始化 DSH_HOME=.dsh-b,然后把包目录拷进 profiles/web/node_modules/
95
+ # 并在 package.json 的 dsh.profile.bundles 里加上 dsh-dbhub-live(bundle 的 patch 会自行插入插件行)
96
+ # 期望日志:`host apply` + `dbhub 插件已加载(4 个工具)` + `状态命名空间已注册: dsh-dbhub-live`
97
+ # + `浏览器桥接已注册: /api/dsh-dbhub-live/*`(0.1.6 没有 settings.configure,故没有"配置表单策略"那行)
98
+ # 期望接口:/api/dsh-dbhub-live/state 带 cookie 200、不带 401;view 里 entryId=行 id;bundle 200
99
+ ```
100
+ `mise r dsh plugin … ls` 只用于核对安装版本;沙箱里 pnpm 的 store 可能不可写,见上面手工路径。
71
101
  4. **在位热更新(有失败保护,谨慎)** — 主实例的 `cordis.patch.yml` 支持 HMR,读取/解析失败时保留最后一个可用树,GUI 不会挂。但 HMR 不监听插件源码,只适合挂载/卸载验证,代码迭代请用 1/2/3。
72
102
  5. **安全网** — `$DSH_SNAPSHOT=replay` 可从 `cordis.snapshot.yml` 启动回放点;改动主 profile 前先备份 `cordis.patch.yml`。
73
103
 
@@ -79,31 +109,105 @@ DSH 启动是 fail-loud:任一插件激活失败整树拒绝启动、GUI 打
79
109
  - `id` 必须等于 Loader entry 名(`dsh-dbhub-live`,见 cordis.patch.yml 的 `name`),不是行 id;
80
110
  - factory 只允许 `require("react")` 等 baseline 模块;不 import 其他插件的值(bundle-purity);
81
111
  - 导出 `name/inject/apply`,结尾 `return module.exports`;
82
- - 注册的 slot 必须是 `plugins.row.config`(key = `dsh-dbhub-live#dbhub-live`;见「状态命名空间」条),组件同时服务 `props.view === 'summary'`(一行摘要)与 `'page'`(完整表单,页面自己画标题/图标/面包屑);**另外必须注册 `settings.section`(id `dbhub`,order 40)作为主入口**——设置面板是核心 shell,任何部署都在,用户也在那里找连接管理;`plugins.row.config` 只在官方插件页的浏览器半区活跃时存在,不能作为唯一入口。侧边栏快捷入口(`sidebar.panellist` id `dbhub` + `main` key `dbhub`,组件 `PanelIcon`/`ConfigPanel`)**由 `showSidebarEntry` 选项控制**:`syncSidebar()` 在 `host.subscribe` 上按镜像值即时 register/dispose,关闭它不得影响设置页与插件页入口(避免自锁);
83
- - 组件不得接触 `ctx`,数据/回调一律通过注册时的 `inject: () => face` 传入 props;用内联样式,不依赖 CSS 文件;
112
+ - 注册的 slot 必须是 `plugins.row.config`(key = `dsh-dbhub-live#dbhub-live`;见「状态通道」条),组件同时服务 `props.view === 'summary'`(一行摘要)与 `'page'`(完整表单,页面自己画标题/图标/面包屑);**另外必须注册 `settings.section`(id `dbhub`,order 40)作为主入口**——设置面板是核心 shell,任何部署都在,用户也在那里找连接管理;`plugins.row.config` 只在官方插件页的浏览器半区活跃时存在,不能作为唯一入口。侧边栏快捷入口(`sidebar.panellist` id `dbhub` + `main` key `dbhub`,组件 `PanelIcon`/`ConfigPanel`)**由 `showSidebarEntry` 选项控制**:`syncSidebar()` 挂在 `face.subscribe` 上,按当前 view 即时 register/dispose,关闭它不得影响设置页与插件页入口(避免自锁)。**HTTP 通道的轮询是「按订阅」的**:只有渲染中的页面用 `props.subscribe(fn, { live: true })` 拉起 1.5s 轮询(`document.hidden` 时跳过),插件作用域那次订阅(`face.subscribe(syncSidebar)`)只做一次性拉取——空闲的 GUI 不得持续轮询桥接(曾经漏掉这个区分,`npm run check:ui` 会因残留 interval 挂住不退出)。
113
+ - **host face 的探测与写回(源代码级契约,别退回 inject)**:`export const inject` 只能有 `["slots"]`;`legacyFace(scope)` 与 `httpFace()` 实现同一接口(`getSnapshot/subscribe/setEnabled/saveConfig/configOp`,快照形状 `{ status, value }`),`ctx.get('settingsScope')` 存在时优先 legacy。HTTP face 的四个入口分别是 `GET /state`、`POST /enabled`、`POST /options`、`POST /op`,写完用响应里的 view 直接刷新快照(不等下一次轮询);`configOp` 的 `{op: …}` 对象直接 POST,不再走「写字符串字段再等主机清空」的绕路。**选项写入优先走官方 settings 表单**:Host 在 view 里发布 `entryId`,客户端有 `ctx.configForms` 且 entryId 非空时用 `configForms.get(entryId).set(field, value)`,失败或解析不到 entryId 才回退 `POST /options`(0.1.x 与没有 settings 客户端的部署走这条)。回归门:`node test/client-format.test.mjs` + `npm run check:ui` 第 18 组(无 settingsScope 时走桥接、四个写入口、`configForms` 优先与 entryId 缺失时的回退、轮询可回收)。
114
+ - 组件不得接触 `ctx`,数据/回调一律通过注册时的 `inject: () => face` 传入 props;**样式改为 `apply()` 里 `ensureStyle()` 注入一份作用域样式表(`#dsh-dbhub-live-style`,类名前缀 `dbh-`)**——悬浮/焦点态是伪类,内联样式表达不了;颜色一律用 `--dsw-alias-*` 主题 token + 中性兜底(深色/skin 切换不串色),只在动态取值时用内联样式。**不要把样式退回成一堆内联对象。**
115
+ - **`apply()` 必须能被重复执行(客户端 HMR 会重跑整个 bundle)**:`localeSvc.register(NS, …)` **不是幂等**——同一 namespace+locale 再注册会抛 `locale namespace … already has locale zh`,客户端半区会整块同步失败(页面提示「插件未能完成同步」)。因此:① 捕获 register 返回的 disposer 并用 `ctx.effect(() => dispose, …)` 挂在插件作用域上(`ctx.effect` 是客户端插件的标准清理口,`ctx.slots.inject` 自身已按作用域清理,沿用即可);② 注册失败时 **降级到 `localTranslator(localeSvc)`**(读本 bundle 自己的字典 + `localeSvc.getLocale().active`),绝不让重复注册打断整个插件半;③ 新增/修改 `apply()` 里的任何服务注册都要问一句「reload 第二次还会成功吗」。回归门:`npm run check:ui` 第 14 组(重复注册 + dispose 后再注册 + 降级后文案仍是本地化文本)。
84
116
  - 配置页渲染的 `workspaces` 行只显示 `w.conn`(元数据标签),**不得显示/回显 DSN**;改名走 `configOp({op:'rename'})`,改名+改连接走**一条** `{op:'add', renameFrom}`(`configOp` 是单字段,连续两次写会互相覆盖)。
117
+ - **布局契约(4.2 迭代沉淀,别再退回「每行三到四行 + 常显表单」)**:
118
+ - **按 `path` 分组**(`groupRows`,标题不是身份:同名工作区是不同目标,同名组头用路径消歧),`layoutModeOf` **无阈值**判定:所有工作区都只有 1 个环境 → **平铺网格**(`dbh-tiles`,`minmax(280px,1fr)` + 内容列 `max-width:960px` 自然封顶 3 列);只要有任一工作区是多环境 → **分组折叠**(`dbh-group` + 可点击组头 `aria-expanded`)。判定必须是纯函数,改判定先改 `scripts/render-check.mjs` 的断言。
119
+ - **每个连接只占一行**:`环境 chip(窄、淡) · 🔒 连接元数据 · source chip · [▶][✏][🗑] 图标按钮`。三条硬规则:
120
+ ① 行内操作**只允许图标**(`primitives` 的 `IconPlayOutline16` / `IconEditOutline16` / `IconCloseOutline16` / `IconTrashOutline16`,无边框 24px,label 走 `title`/`aria-label`)——文字按钮一放上来就吃掉半行,这是被用户点名过的;
121
+ ② `source` 句柄做成**可复制 chip**(`sourceChip`):虚线边框 + 复制图标 + **短句柄**(`shortHandle`:≤14 字全显,否则保留"工作区哈希 + 环境段"两段),让人一眼看出"这是 id、点了复制",而不是"复制整行连接串";全文与用途进 tooltip(`ws.srcHint`),复制走 `primitives.writeClipboard`(自带 execCommand 兜底);
122
+ ③ 连接元数据用 `connLabelView` 拆成 `🔒 + 前缀(type://host:port/) + 库名`,**前缀可截断、库名绝不截断**(`dbh-conn-pre` 省略号 / `dbh-conn-db` 不收缩)——"窄行里看不到库名"是最常见的抱怨。
123
+ 行内只允许出现 `w.conn`,`srcId` 只是句柄、绝不回显 DSN。**「来源」(手填/扫描/复制/自动发现)不得做成行内徽章**——它几乎每行都一样、却是最抢眼的元素;改为环境名 chip 的 tooltip(`originTitle`),只有"自动发现且未持久化"这一例外在 tooltip 里显式点名。
124
+ - **层级**:工作区分组是 level-1(边框卡片 + 极淡阴影),环境行是 level-2(缩进 + 左侧引导竖线,见 `dbh-groupbody{border-left}`);两层绝不能长得像同一层。
125
+ - **主题适配(红线)**:样式表**只允许 `--dsw-alias-*` token**,兜底值必须中性且半透明(`rgba(127,127,127,.3)`、`transparent`),**禁止任何不透明色字面量**(`#ffffff`/`#f6f8fa`/`#1f2328`/品牌色…)——用户会装主题/皮肤插件(本机就有 skin-center,且背景可能是半透明的),写死白底或固定浅灰字会直接看不清;`test/client-format.test.mjs` 有"tokens only"断言门禁。对比度:正文及以上用 `label-primary`/`label-secondary`,`label-dimmed` 只留给极次要说明。
126
+ - **默认展开**:单个工作区自动展开;多工作区时展开「最近活跃工作区」(由 `settings.section` 标准 prop `useWorkspaces` 的 `WorkspaceSnapshot.updatedAt` 推出——设置面板是全局的,拿不到"当前会话"选中态,别去猜);取不到快照就全折叠。另有「全部展开/折叠」总控。展开态、添加面板、设置条存在模块级 `uiMemory`(同一页面会话内跨重挂载保留,**不持久化**——这是视图状态,不是配置)。
127
+ - **顶部只留一行**:状态徽章 + 工具数 + 设置条摘要 + `[⚙ 设置]` + `[启用/禁用]`;自动更新间隔与侧边栏开关收进默认收起的设置条。**添加连接默认收起**:「+ 添加连接」(全局,工作区用 `<datalist id="dbh-ws-list">` 给已有工作区)/ 分组头「+ 环境」(工作区自动带入,少一个字段);保存仍走原有「试连 → 失败才确认」门禁。
128
+ - 布局/交互改动的验收 = `npm run check:ui`(离屏渲染 + 点击交互,涵盖模式判定、默认展开、全部展开/折叠、添加面板、复制按钮、中英 i18n 覆盖率)全绿 + `node test/client-format.test.mjs`。
129
+ - **复制必须给出干净字符串**:连接串被拆成两个 flex item 渲染后,浏览器自己的复制序列化会在块级子元素之间插分隔符(粘进 DSN 输入框就成了 `…3306/ app`,测试直接失败)。因此 `connLabelView` 的元素自己接管 `onCopy`(`e.clipboardData.setData('text/plain', w.conn)` 并 `preventDefault()`)——挂在**连接串元素**而不是整行上,复制环境名不受影响;`w.conn` 本身是元数据,不涉及凭据。回归门:`npm run check:ui` 第 17 组。
130
+ - **等待态不给二次提交**:预检进行中(`pendingPretest`)保存/添加按钮 `disabled`,避免重复派发同一个测试。真实的连接探测本来就要几秒(要 spawn dbhub),但**写入结果必须立刻可见**——这靠宿主侧的即时回推(见「工作区连接管理」条的 `publishFast`),不要改回"等整轮扫描"。
85
131
  - 若本包脱离仓库(发布 npm),`dsh.client` 与 `exports["./client"]` 必须保留,否则 client-modules 扫描会启动报错。
86
132
 
87
133
  ## 质量门
88
134
 
89
135
  ```bash
90
- npm run check # 全部 lib 语法
136
+ npm run check # 全部 lib 语法
137
+ npm run check:secrets # 脱敏门禁(槽位表 + 通用兜底 + 本地 denylist,见下)
138
+ npm run check:ui # 客户端布局:离屏渲染 + 点击交互门禁(自带极简 React shim,无依赖)
91
139
  # 单测:沙箱内逐个文件跑(node --test 的 runner 子进程在沙箱下 EPERM)
92
140
  node test/util.test.mjs && node test/state.test.mjs && node test/options.test.mjs \
93
141
  && node test/init.test.mjs && node test/i18n.test.mjs \
94
142
  && node test/resolve-source.test.mjs && node test/resolve-workspace.test.mjs \
143
+ && node test/row-cache.test.mjs && node test/auto-dsn.test.mjs \
144
+ && node test/bridge.test.mjs \
95
145
  && node test/client-format.test.mjs && node test/zero-knowledge.test.mjs
96
146
  ```
97
147
 
98
- 脱敏自检(**每次提交/发布前必须零匹配**;黑名单按需追加新发现的真实值):
148
+ **CI(`.github/workflows/quality.yml`,GitHub Actions 的 `quality` workflow)**:`push` 到 `main` 与 `dev`、所有 PR、以及手动 `workflow_dispatch` 都会跑同一个 job **`gate`**(`ubuntu-latest` + Node 22,约 12–15 秒),六步 = 语法检查 → 脱敏门禁 → 客户端布局门禁 → 逐个单测(12 个文件、117 个断言)。`main` 的分支保护把 `gate` 设为必需检查。**CI 是真实执行**:它在 5.0.0 上真的跑挂过(`test/auto-dsn.test.mjs` 全部 cancelled),这正是它存在的意义——**本地 Windows 会掩盖只影响 Linux 的缺陷**(见「自动发现的代价与边界」条的 `unref` 教训),所以"本地全绿"不能当作发布依据,必须看 CI。
99
149
 
100
- ```bash
101
- git grep -inE '10\.253\.|192\.168\.77|tx_zdsf|tx_sd_jineng|db_zdsf|hbtx|1hn7yaw|9sqpcz|唐讯|广告宝|110\.120\.66' -- ':!node_modules' ':!AGENTS.md'
102
- ```
150
+ ### 脱敏门禁(`npm run check:secrets`)
151
+
152
+ 真实主机/账号/密码/库名/项目名只允许存在于两处:`$DSH_HOME/storages/dsh-dbhub-live/credentials.json`(仓库外)与本地 `.env`(`.gitignore` 排除、`package.json` 的 `files` 不含,永不进 git 历史/公开仓库/npm 包)。仓库内一切内容都视为会公开分发。
153
+
154
+ 门禁由 `scripts/desensitize-check.mjs` 执行,规则与槽位来自 `scripts/desensitize.config.mjs`:
155
+
156
+ 1. **槽位规则(主力,高精度)**:槽位表登记"真实值可能出现的出口"(宿主文案、浏览器文案、连接构造代码、扫描器、测试夹具、文档示例、包元数据、本提示词、维护脚本),每个槽位声明它需要的规则(`password`/`dsn`/`host`/`name`)。**槽位 glob 必须匹配到文件,否则门禁报 `slot-stale`**——槽位表不会随目录移动悄悄失效。
157
+ 2. **通用兜底(覆盖未登记的新出口)**:`ip`(只允许回环、`0.0.0.0` 与 RFC 5737 文档段 `192.0.2.x`/`198.51.100.x`/`203.0.113.x`)、`secret`(GitHub/npm/OpenAI/Slack/AWS/JWT/PEM 等真凭据格式)、`entropy`(大小写/数字混排的高熵串)、`host`(`.local`/`.internal`/`.corp` 等内网后缀)、`name`(本地 denylist)。
158
+ 3. **本地 denylist(补充)**:pattern 抓不到的**裸名字**(项目/公司/库名)放在 `.env` 的 `DSH_DBHUB_DESENSITIZE_RE`(`|` 分隔、正则转义、多条取并集)。CI 里没有 `.env`,该层自动降级为提示,不影响其余两层。
159
+
160
+ **约定(新增内容一律照此写)**:示例主机用 `127.0.0.1`/`localhost`/RFC 5737 段;示例密码用 `CHANGE_ME`;测试夹具可用短占位(`u:p`、`root:secret`),但主机仍须回环/保留段;误报走行内标记 `desensitize:allow`(仅限确认无害的行)。
103
161
 
104
- 零知识自检(模型可见面不得含密码):`node test/zero-knowledge.test.mjs` 全绿 + 上面 git grep 零匹配。
162
+ **义务(硬规则)**:新增功能若引入新的示例、默认值、文案串或夹具 → **同一次改动里更新 `scripts/desensitize.config.mjs` 的槽位表**;提交前跑 `npm run check:secrets`,命中就改成占位符/规范值再提交 git 与 npm。执行点:本地手动 + `prepublishOnly`(挡住带真实值的包)+ CI(`.github/workflows/quality.yml`,别人的提交也拦得住)。历史事故:这些值曾随 npm 包与公开仓库分发,公开即视为泄露、无法追回。
163
+
164
+ 零知识自检(模型可见面不得含密码):`node test/zero-knowledge.test.mjs` 全绿 + 上面 `npm run check:secrets` 零匹配。
165
+
166
+ 发布前:按「调试方法」第 3 步在隔离实例完整冷启动一遍,确认 Host 无报错、`/api/dsh-dbhub-live/state` 带会话 cookie 返回 200(不带 cookie 返回 401)、`/plugins/??<包名>/client.js&rev=…` 可访问。
167
+
168
+ ## 分支与发布流程(dev 测试版 → main 正式版)
169
+
170
+ **分支职责**
171
+ - `dev`:**集成/测试分支**。所有改动先落这里(直接提交,或功能分支 PR 进 dev);允许 force push;CI(`gate`)在这里跑。
172
+ - `main`:**只用于发布**。已开分支保护:必需检查 `gate`、禁止 force push、禁止删除。改动只能从 `dev` 合入(PR),main 上不再有其它直接提交。
173
+ - 每次发布后 `dev` 与 `main` 收敛:`git checkout dev && git merge --ff-only main`。
174
+
175
+ **触发词与硬约束(最重要,别越界)**
176
+ - 用户说「**可以了,发布**」(或类似)= **发 dev 测试版**。此时**绝不**动 `main`、**绝不**执行 `npm publish` 的默认(`latest`)发布。
177
+ - 只有用户明确说「**合并主分支发布正式版**」(或等价明确指令)时,才走正式版流程;并且**正式版的 npm 发布命令只输出给用户手动执行**——代理不代发(用户自己用浏览器登录拿凭据)。仓库里没有 `NPM_TOKEN` secret,CI 也不会替你发。
178
+ - **已发布的 tag 与 npm 版本绝不移动 / 删除 / unpublish**;发现缺陷就发下一个补丁版本。这条是 5.0.0→5.0.1 那次事故的直接教训(当时为了"对齐"移动过 tag,用户明确否掉了这种做法)。
179
+ - 测试版**必须** `--tag dev`(若发 npm)+ GitHub `--prerelease`;只有正式版才能用 `--latest` 与默认 dist-tag(否则 `latest` 会被指到测试版,普通用户直接中招)。
180
+ - 只有 CI(`gate`)绿的提交才能进 `main`;`pnpm pack` 的产物名必须与 `package.json` 版本一致。
181
+
182
+ **阶段一:测试版(在 dev 上)**
183
+ ```powershell
184
+ git checkout dev && git pull
185
+ # 改代码;package.json 版本 = <下一个正式版>-dev.<N>(N 不复用,如 5.1.0-dev.1)
186
+ npm run check; npm run check:secrets; npm run check:ui # 再逐个跑单测
187
+ $env:npm_config_store_dir='D:/my/app/dsh/plugin/.pnpm-store'; pnpm pack # dsh-dbhub-live-<ver>.tgz
188
+ git add -A; git commit -m "..."; git push origin dev # CI 在 dev 上跑 gate
189
+ git tag -a v<ver> -m "..."; git push origin v<ver>
190
+ gh release create v<ver> --prerelease --title "v<ver>" --notes-file <notes> dsh-dbhub-live-<ver>.tgz
191
+ # 可选(只在需要验证 npm 安装路径时):npm publish --registry=https://registry.npmjs.org/ --tag dev
192
+ # 安装验证(不碰 npm 也行):dsh plugin --profile web add <tarball>(或 dsh-dbhub-live@dev)
193
+ ```
194
+ 预发布不会抢 Release 页的 "Latest" 徽章,普通用户 `dsh plugin add dsh-dbhub-live` 仍拿正式版。
195
+
196
+ **阶段二:正式版(仅在用户明确要求时)**
197
+ ```powershell
198
+ # 1) 在 dev 上把版本号转正(去掉 -dev.N),提交 "vX.Y.Z (release): ...",push dev
199
+ # 2) 开 PR 合入 main,等 CI 绿后合并
200
+ gh pr create --base main --head dev --title "vX.Y.Z (release): ..." --body "..."
201
+ gh pr merge --merge
202
+ # 3) 在 main 的合并提交上打 tag
203
+ git checkout main && git pull; git tag -a vX.Y.Z -m "..."; git push origin vX.Y.Z
204
+ # 4) GitHub 正式发布(--latest,附 tarball)
205
+ gh release create vX.Y.Z --latest --title "vX.Y.Z" --notes-file <notes> dsh-dbhub-live-X.Y.Z.tgz
206
+ # 5) npm:把命令交给用户手动执行(代理不代发)
207
+ # npm publish --registry=https://registry.npmjs.org/ # 不带 --tag → latest 指向它
208
+ ```
105
209
 
106
- 发布前:按「调试方法」第 3 步在隔离实例完整冷启动一遍,确认 Host 无报错、`/plugins/dsh-dbhub-live/client.js` 可访问。
210
+ **给代理的默认行为**:本仓库里代理只往 `dev` 提交/推送;`main` 只通过 PR 合入。需要动 `main`/发 npm 时先停下来向用户确认。
107
211
 
108
212
  ## 版本号策略(测试版本 → 发布版本)
109
213
 
@@ -120,7 +224,7 @@ git grep -inE '10\.253\.|192\.168\.77|tx_zdsf|tx_sd_jineng|db_zdsf|hbtx|1hn7yaw|
120
224
  ## 约定
121
225
 
122
226
  - 产品文案中文、代码注释英文;密码脱敏/零知识不可绕过;扫描必须经 `askUser` 授权。
123
- - **脱敏红线(提交/发布前强制自检)**:仓库任何文件——**包括 `test/`(会打进 npm tarball)与工具描述文案**——不得出现真实内网 IP、真实库名、真实密码、公司/项目标识。示例 DSN 一律用 `127.0.0.1` 或文档保留段(`192.0.2.x` / `198.51.100.x` / `203.0.113.x`),且**密码位只用占位词**(`user:pass@`、`u:p@`、`username:password@`、`账号:密码@`)。历史事故:工具描述里的示例 DSN 与测试用例里的「真实值」曾随 npm 包与公开仓库历史分发——公开即视为泄露、无法追回。每次提交前跑「质量门」中的脱敏自检;发现新的真实值,清洗后将其加入黑名单。
227
+ - **脱敏红线(提交/发布前强制自检)**:仓库任何文件——**包括 `test/`(会打进 npm tarball)与工具描述文案**——不得出现真实内网 IP、真实库名、真实密码、公司/项目标识。写法约定与三层门禁见上面「脱敏门禁」:示例主机只用回环或 RFC 5737 段、示例密码只用 `CHANGE_ME`、新出口同次登记进 `scripts/desensitize.config.mjs`;发现新的真实值**只追加到本地 `.env` 的 `DSH_DBHUB_DESENSITIZE_RE`**,绝不写进任何受版本控制的文件。
124
228
  - `state.*` 之外不要直接改持久化。
125
229
  - 增加行为时同步更新本文件、README(用户侧)与 doc/REQUIREMENTS.md(业务侧)。
126
230
  - README 双语同步:`README.md`(中文)为唯一真源,`README.en.md` 由 AI 从最新中文派生——改动任一侧必须同次更新另一侧,章节结构一一对应。
package/README.en.md CHANGED
@@ -20,7 +20,7 @@
20
20
  - **Workspace × environment connection management** — one workspace can hold multiple environments (default / prod / dev / test…) distinguished by their `source` value.
21
21
  - **Auth-failure loop** — when credentials or connection details are wrong, the tool gives clear guidance; the model steers you to update the password in the UI (it never asks you for it), or you edit it directly in the settings card. **Auto-probe during configuration**: non-sensitive connection facts are tested host-side first — if they connect they take effect with zero input (password-less databases never prompt), and if only the password is missing the dialog asks just for "account (when unknown) + password" instead of re-asking for the whole input method. When the probe explicitly blames an EMPTY account (e.g. `Access denied for user ''`), the account is required — the "leave empty (use empty account)" choice is not offered, so the same doomed DSN is never saved again.
22
22
  - **Enable / disable switch** — turning it off makes every dbhub tool return a friendly "plugin disabled" message immediately; no restart needed.
23
- - **Browser configure page** — three entries to the same page: **Settings → DBHub Database Tools** (a first-level settings page, recommended), the **sidebar entry DBHub Database Tools** (a shortcut you can switch off on the settings page), and the plugin row's Configure control on the Plugins page. Shows live: status badge, mode (one-shot), registered tool count, environment count, recent error, plus the enable/disable switch, connection CRUD, **environment rename** and connection tests.
23
+ - **Browser configure page** — three entries to the same page: **Settings → DBHub Database Tools** (a first-level settings page, recommended), the **sidebar entry DBHub Database Tools** (a shortcut you can switch off on the settings page), and the plugin row's Configure control on the Plugins page. It shows the status badge, tool count and environment count, and offers the enable/disable switch, connection CRUD, **environment rename** and connection tests. **The connection list is organised per workspace**: when every workspace holds exactly one environment it renders as flat tiles (up to 3 columns, everything visible); as soon as one workspace owns several environments the rows fold per workspace with **click-to-expand group heads**, opening the workspace you used most recently by default, plus an expand/collapse-all control. Each connection **takes one line only** (environment · 🔒 type/host/port/**database** · source handle · icon actions): a narrow column truncates the host/port prefix first and **the database name always stays visible**; the actions are three small icons — ⚡ test / ✏ edit / 🗑 delete — instead of text buttons eating half the row; the `source` handle is a dashed chip showing `…` plus its distinguishing tail (e.g. `…1a2b3c`) — click it to copy, hover for the full value (it is the handle the model uses to target this connection, not a copy of the whole row). Where a connection came from (hand-entered / scanned / copied / auto-discovered) never takes row width — hover the environment chip to see it (an auto-discovered connection that is not saved says so explicitly). The auto-update interval and the sidebar switch live in the "⚙ Settings" strip, and the add form is folded away too (a group's "+ Environment" adds straight into that workspace, no workspace field to fill). All colours come from DSH theme tokens, so the page follows the active theme/skin and dark mode — **including translucent-background themes**. Writes are guarded against silent overwrites: **saving into an environment name that already exists asks “overwrite?” first** (cancelling writes nothing at all and leaves the form as typed so you can rename it), and renaming an environment onto an existing one asks too.
24
24
  - **Out of the box** — if `dbhub` is missing, the plugin installs it on first use and keeps it updated at your configured interval.
25
25
 
26
26
  ## Supported Data Sources
@@ -30,6 +30,7 @@ MySQL · PostgreSQL · MariaDB · SQLite · SQL Server
30
30
  ## Requirements
31
31
 
32
32
  - DeepSeek Harness's `dsh` CLI (`dsh web` runs the GUI)
33
+ - Works with **every dsh release line from 0.1.5 on, 0.2.x included**: the configure page rides the settings namespace on 0.1.x and the plugin's own authenticated HTTP bridge (`/api/dsh-dbhub-live/*`) on 0.1.7+/0.2.x, and both carry the same state
33
34
  - Node.js ≥ 18 with `npm` recommended — `dbhub` is auto-installed on first use
34
35
 
35
36
  ## Installation
@@ -42,11 +43,13 @@ dsh plugin --profile web add dsh-dbhub-live
42
43
  npx @deepseek-ai/dsh plugin --profile web add dsh-dbhub-live
43
44
 
44
45
  # Update to a specific version (pin the currently published version so pnpm doesn't skip with "Already up to date")
45
- dsh plugin --profile web update dsh-dbhub-live@4.1.0
46
+ dsh plugin --profile web update dsh-dbhub-live@5.0.0
46
47
  ```
47
48
 
48
49
  After installing, **restart `dsh web`** for it to take effect (you can then see the configure page at **Settings → DBHub Database Tools**).
49
50
 
51
+ > Upgrading from 4.x: dsh 0.2.x removed the settings-namespace interface the old configure page relied on, so dsh refuses to load 4.x there. 5.0.0 serves both release lines; upgrading the plugin keeps your connections and your saved options.
52
+
50
53
  ## Quick Start
51
54
 
52
55
  The tools below are invoked automatically by DSH's AI — you don't run them by hand; just state your request in natural language (e.g., "look up the users table"):
@@ -77,7 +80,7 @@ dbhub_list_sources
77
80
 
78
81
  ### Configuration Methods
79
82
 
80
- 0. **Auto-probe (preferred by default)** — once you state connection facts in chat (e.g. "mysql 10.0.0.1:3307/mydb"), the plugin tests them first: connectable → saved immediately (password-less databases need zero input); password required → the dialog asks only for "account (when unknown) + password" with the failure reason and a "use another way" escape when the connection details themselves are wrong; when the probe blames an EMPTY account (e.g. `Access denied for user ''`), the account is required (no "leave empty" choice); incomplete info or another failure → the three methods below appear. If you name a different database/host/port than the saved connection, the plugin re-runs the probe against the new target.
83
+ 0. **Auto-probe (preferred by default)** — once you state connection facts in chat (e.g. "mysql 198.51.100.1:3307/mydb"), the plugin tests them first: connectable → saved immediately (password-less databases need zero input); password required → the dialog asks only for "account (when unknown) + password" with the failure reason and a "use another way" escape when the connection details themselves are wrong; when the probe blames an EMPTY account (e.g. `Access denied for user ''`), the account is required (no "leave empty" choice); incomplete info or another failure → the three methods below appear. If you name a different database/host/port than the saved connection, the plugin re-runs the probe against the new target.
81
84
  1. **Explicit DSN** — enter a full connection string in the UI, e.g. `mysql://user:pass@host:3306/db`.
82
85
  2. **Fill in fields** — fill type / host / port / user / password / database name in the UI; fields already known from the conversation are pre-filled — you only fill in the gaps.
83
86
  3. **Authorized scan** — after authorization, scan project config files (`.env`, `application*.yml`, `docker-compose`, `jdbc.properties`, etc.) and list candidates (host/port/database only — passwords are read host-side and never shown) for you to confirm.
@@ -105,7 +108,7 @@ All page copy (name, status, config fields, buttons, connection rows) follows th
105
108
  | Auto-update interval (days) | How often dbhub is auto-updated; `0` disables | `7` |
106
109
  | Show the sidebar entry | Whether the DBHub Database Tools shortcut appears in the sidebar; applies immediately | on |
107
110
 
108
- Precedence: **user settings > process environment variables (default seeds) > built-in defaults**. The auto-installed package is not in the UI (controlled separately by the `DSH_DBHUB_PACKAGE` environment variable, default `@bytebase/dbhub`).
111
+ Precedence: **settings saved on the configure page > process environment variables (default seeds) > built-in defaults**; when upgrading from an older 0.1.x install, the options still stored in the settings document are read as a lower layer (they are never silently reset). On dsh 0.1.7 and later these two options are also the plugin's declared configuration (volatile Config): set them in the plugin's row in `cordis.patch.yml`, or in the official Plugins form — **either change is adopted, and this plugin's page shows the same value** (a page save writes back to that configuration too, so the two surfaces never disagree). The auto-installed package is not in the UI (controlled separately by the `DSH_DBHUB_PACKAGE` environment variable, default `@bytebase/dbhub`).
109
112
 
110
113
  **Workspace connections**:
111
114
 
@@ -155,6 +158,7 @@ dsh plugin --profile web remove dsh-dbhub-live
155
158
  | A query to one environment fails with connection refused / auth error | Environments use independent one-shot connections: an unreachable environment only fails that call — other environments and calls keep working. The error carries guidance — for wrong credentials/details, ask the AI to run `dbhub_configure` and enter the password in the UI, or edit it directly in Plugins → dsh-dbhub-live → Configure. |
156
159
  | Tools say "plugin disabled" | Open Plugins → dsh-dbhub-live → Configure and click "Enable". |
157
160
  | Configure Page not visible | Confirm the plugin is installed and restart `dsh web`; the card only shows in the Web settings panel (`dsh web`) — on terminal environments without the panel, tool usage is unaffected. |
161
+ | dsh reports the plugin as incompatible after a dsh upgrade | That is dsh's peer-version check: upgrade the plugin to 5.0.0 or later (it serves both 0.1.x and 0.2.x). Forcing it through with `dsh plugin --profile web allow-version` does not help — the interface 4.x depends on is gone in 0.2.x, so the configure page stays empty. |
158
162
  | No config files found by the scan | `node_modules` / `.git` / `target` / `dist` etc. are skipped by default; use "Enter DSN" or "Fill in fields" instead. |
159
163
  | Need a custom dbhub version | Set the `DSH_DBHUB_PACKAGE` environment variable (e.g. `@bytebase/dbhub@1.2.1`) and restart; or delete `~/.dsh/storages/dsh-dbhub-live` and let it reinstall automatically. |
160
164
  | Don't want automatic dbhub updates | Set "Auto-update interval (days)" to `0` in the Configure Page and save; or set `DSH_DBHUB_UPDATE_DAYS=0`. |
package/README.md CHANGED
@@ -20,7 +20,7 @@
20
20
  - **按工作区 × 环境管理连接** — 一个工作区可配多个环境(default / prod / dev / test…),通过 `source` 值区分,互不混淆。
21
21
  - **鉴权失败闭环** — 凭据或连接信息不对时,工具给出明确指引;模型引导你在界面更新密码(不会向模型索要密码),或你直接在设置卡片修改。**配置时自动试连**:说的是非敏感连接信息会先在宿主侧实测——可连通就直接生效(免密码库零输入),只差密码时弹窗只问「账号(未知时)+ 密码」,不再让你重选输入方式;若试连明确报“空账号被拒”,账号为必填(不再提供「留空(使用空账号)」选项),避免把同一个必失败的配置再存回去。
22
22
  - **启用 / 禁用开关** — 关闭后所有 dbhub 工具立即返回「插件已禁用」友好提示,无需重启。
23
- - **浏览器配置页** — 三个入口同一个页面:**设置 → DBHub 数据库工具**(一级设置页,推荐)、**左侧栏「DBHub 数据库工具」快捷入口**(可在设置页用开关关闭)、插件页 dsh-dbhub-live 那行的「配置」。实时展示:状态徽章、工作模式(一次性连接)、已注册工具数、环境数、最近错误,并提供启用/禁用开关、连接增删改查、**环境改名**与连接测试。
23
+ - **浏览器配置页** — 三个入口同一个页面:**设置 → DBHub 数据库工具**(一级设置页,推荐)、**左侧栏「DBHub 数据库工具」快捷入口**(可在设置页用开关关闭)、插件页 dsh-dbhub-live 那行的「配置」。实时展示状态徽章、工具数与环境数,并提供启用/禁用开关、连接增删改查、**环境改名**与连接测试。**连接列表按工作区组织**:所有工作区都只有一个环境时用平铺卡片(最多 3 列,一眼看全);任一工作区有多个环境时按工作区分组折叠,**点组头展开/收起**,默认展开你最近使用的那个工作区,另有「全部展开/折叠」。每个连接**只占一行**(环境 · 🔒 类型/主机/端口/**库名** · source 句柄 · 图标操作):窄窗口下会先截断主机/端口,**库名始终可见**;行内操作是 ⚡ 测试 / ✏ 修改 / 🗑 删除 三个小图标,不再用文字按钮占半行;`source` 句柄做成一个虚线小片(显示 `…` + 尾段,如 `…1a2b3c`),点它即复制、悬浮看全文(它就是让模型定位这条连接的句柄,不是复制整行连接串)。连接「来源」(手填 / 扫描 / 复制 / 自动发现)不占行宽,悬浮在环境名上即可看到(自动发现且未保存的会明确标出)。自动更新间隔与侧边栏开关收进「⚙ 设置」条,添加连接也默认收起(分组头右上角「+ 环境」可直接在该工作区新增,工作区自动带入)。配色全部走 DSH 主题令牌,**跟随主题/皮肤与深色模式,包括半透明背景的主题**。写入前会拦截覆盖:**同名环境保存前先问「是否覆盖」**(取消则一个字都不写、表单原样留着让你改名),把环境改名落到已存在的环境时同样会问。
24
24
  - **开箱即用** — 未安装 `dbhub` 时首次执行自动安装,之后按设置间隔自动更新。
25
25
 
26
26
  ## 支持的数据源
@@ -30,6 +30,7 @@ MySQL · PostgreSQL · MariaDB · SQLite · SQL Server
30
30
  ## 环境要求
31
31
 
32
32
  - DeepSeek Harness 的 `dsh` CLI(`dsh web` 负责 GUI 运行)
33
+ - 兼容 **dsh 0.1.5 起的所有版本线**(含 0.2.x):配置页在 0.1.x 走设置命名空间、在 0.1.7+/0.2.x 走插件自带的鉴权 HTTP 桥(`/api/dsh-dbhub-live/*`),两条通道展示同一份状态
33
34
  - 推荐本机有 Node.js ≥ 18(含 `npm`)——首次执行会自动安装 `dbhub`
34
35
 
35
36
  ## 安装
@@ -42,11 +43,13 @@ dsh plugin --profile web add dsh-dbhub-live
42
43
  npx @deepseek-ai/dsh plugin --profile web add dsh-dbhub-live
43
44
 
44
45
  # 更新到指定版本(推荐写明当前发布的版本号,避免 pnpm 判「Already up to date」跳过)
45
- dsh plugin --profile web update dsh-dbhub-live@4.1.0
46
+ dsh plugin --profile web update dsh-dbhub-live@5.0.0
46
47
  ```
47
48
 
48
49
  安装后**重启 `dsh web`** 生效(重启后到 **设置 → DBHub 数据库工具** 可看到配置页)。
49
50
 
51
+ > 从 4.x 升级:dsh 0.2.x 删除了旧版配置页依赖的 settings 命名空间接口,4.x 在 0.2.x 上会被 dsh 拒绝加载。5.0.0 起同时支持新旧两条 dsh 版本线,升级插件后配置页与既有连接、既有选项都会保留。
52
+
50
53
  ## 快速开始
51
54
 
52
55
  以下工具由 DSH 的 AI 自动调用,你不用手动执行——直接用自然语言提出需求即可(如「查一下 users 表」):
@@ -77,7 +80,7 @@ dbhub_list_sources
77
80
 
78
81
  ### 配置方式
79
82
 
80
- 0. **自动试连(默认优先)** — 你在对话里说出连接信息(如「mysql 10.0.0.1:3307/mydb」)后,插件先用这些非敏感信息实测:能连上就直接保存(数据库不需要密码时全程零输入);提示需要密码 → 弹窗只问「账号(未知时)+ 密码」,其余信息自动沿用,并附失败原因与该连接信息不对时的「改用其他方式」入口;若试连明确报“空账号被拒”(如 `Access denied for user ''`),账号为必填(不提供「留空」选项);信息不完整或其它原因失败,才进入下面三种方式。若你说的是另一个库/主机/端口(与已存连接不同),插件会按新信息重新试连判断。
83
+ 0. **自动试连(默认优先)** — 你在对话里说出连接信息(如「mysql 198.51.100.1:3307/mydb」)后,插件先用这些非敏感信息实测:能连上就直接保存(数据库不需要密码时全程零输入);提示需要密码 → 弹窗只问「账号(未知时)+ 密码」,其余信息自动沿用,并附失败原因与该连接信息不对时的「改用其他方式」入口;若试连明确报“空账号被拒”(如 `Access denied for user ''`),账号为必填(不提供「留空」选项);信息不完整或其它原因失败,才进入下面三种方式。若你说的是另一个库/主机/端口(与已存连接不同),插件会按新信息重新试连判断。
81
84
  1. **显式 DSN** — 在界面输入完整连接串,如 `mysql://user:pass@host:3306/db`。
82
85
  2. **填写分项** — 在界面按类型 / 主机 / 端口 / 账号 / 密码 / 库名依次填写;已从对话中得知的字段会自动带入,只补缺项即可。
83
86
  3. **授权扫描** — 授权后扫描项目配置文件(`.env`、`application*.yml`、`docker-compose`、`jdbc.properties` 等),列出候选(只显示主机/端口/库,密码不显示,由插件直接读取)供你确认。
@@ -105,7 +108,7 @@ dbhub_list_sources
105
108
  | 自动更新间隔(天) | 自动更新 dbhub 的间隔天数,`0` 关闭 | `7` |
106
109
  | 在侧边栏显示入口 | 是否在左侧栏显示「DBHub 数据库工具」快捷入口;改动即时生效 | 开 |
107
110
 
108
- 优先级:**用户设置 > 进程环境变量(默认值种子)> 内置默认**。自动安装包不在 UI 中(`DSH_DBHUB_PACKAGE` 环境变量单独控制,默认 `@bytebase/dbhub`)。
111
+ 优先级:**配置页保存的设置 > 进程环境变量(默认值种子)> 内置默认**;从 0.1.x 老版本升级时,原先存在设置文档里的选项会被读进来作为更低一层(不会被静默重置)。在 dsh 0.1.7 及以后,这两项同时是本插件的声明式配置(volatile Config):也可以直接写进 `cordis.patch.yml` 的该插件行,或在官方「插件」页的表单里改——**任一处改动都会被采纳,本插件配置页会显示同一个值**(配置页保存也会同步写回该配置,两边不会打架)。自动安装包不在 UI 中(`DSH_DBHUB_PACKAGE` 环境变量单独控制,默认 `@bytebase/dbhub`)。
109
112
 
110
113
  **工作区连接**:
111
114
 
@@ -155,6 +158,7 @@ dsh plugin --profile web remove dsh-dbhub-live
155
158
  | 某个环境的查询报连接被拒 / 认证失败 | 该环境为独立一次性连接:不可达只让该次调用报错,其他环境与调用不受影响。错误末尾会附指引——凭据或连接信息有误时,可让 AI 调用 `dbhub_configure` 引导你在界面更新,或直接在 插件 → dsh-dbhub-live → 配置 中修改。 |
156
159
  | 工具显示「插件已禁用」 | 打开 插件 → dsh-dbhub-live → 配置,点击「启用」。 |
157
160
  | 看不到配置页 | 确认插件已安装并重启 `dsh web`;配置页只在 Web 设置面板(`dsh web`)显示,在无设置面板的终端环境下不影响工具使用。 |
161
+ | 升级 dsh 后提示插件与本版 dsh 不兼容 | 这是 dsh 的 peer 版本检查:升级插件到 5.0.0 及以上(同时支持 0.1.x 与 0.2.x)。用 `dsh plugin --profile web allow-version` 强行豁免没有意义——4.x 依赖的接口在 0.2.x 已被删除,加载后配置页依然是空的。 |
158
162
  | 扫描不到配置文件 | 默认跳过 `node_modules` / `.git` / `target` / `dist` 等目录,可改用「输入 DSN」或「填写分项」。 |
159
163
  | 需要自定义 dbhub 版本 | 设置环境变量 `DSH_DBHUB_PACKAGE`(如 `@bytebase/dbhub@1.2.1`)后重启;或删除 `~/.dsh/storages/dsh-dbhub-live` 重新自动安装。 |
160
164
  | 不希望自动更新 dbhub | 配置页「自动更新间隔(天)」填 `0` 并保存;或设置环境变量 `DSH_DBHUB_UPDATE_DAYS=0`。 |
@@ -1,8 +1,8 @@
1
1
  # dsh-dbhub-live 需求事实记录
2
2
 
3
3
  > 本文只记录「产品必须满足什么」的**需求事实**(概念层面),不含实现细节;开发实现说明见 `AGENTS.md`,用户安装使用见 `README.md`。
4
- > 版本基线:4.1(正式版;4.0 的零知识凭据 + 一次性进程执行架构之上,新增工作区隔离与来源解析、环境改名、设置入口与侧边栏开关)。
5
- > 当前迭代:无(4.1 已发布;下一轮在此基线上挂 `-dev.N` 迭代)。
4
+ > 版本基线:5.0(正式版;4.0 的零知识凭据 + 一次性进程执行架构之上,新增工作区隔离与来源解析、环境改名、设置入口与侧边栏开关,以及 dsh 0.2.x 双栈适配)。
5
+ > 当前迭代:5.0 — dsh 0.2.x 兼容(在 4.1/4.2 能力之上重做浏览器↔宿主通道;见第十节 R56-R58)。
6
6
 
7
7
  ## 一、产品定位
8
8
 
@@ -41,7 +41,7 @@
41
41
 
42
42
  ## 六、设置与状态界面
43
43
 
44
- - R21. 浏览器端提供配置页:展示启用状态、工作模式、工具声明数、环境数、最近错误,并支持启用/禁用开关、连接的增删改查、环境改名与连接测试。在 dsh 0.1.6 及以后,该配置页承载于官方「插件」页的插件行上(`plugins.row.config`),不再是设置面板里的独立卡片。
44
+ - R21. 浏览器端提供配置页:展示启用状态、工具声明数、环境数、最近错误(工作模式作为状态行提示),并支持启用/禁用开关、连接的增删改查、环境改名与连接测试。在 dsh 0.1.6 及以后,该配置页承载于官方「插件」页的插件行上(`plugins.row.config`),不再是设置面板里的独立卡片。
45
45
  - R22. 面板文案与模型侧反馈跟随 DSH 界面语言(中/英)。
46
46
 
47
47
  ## 七、存储与实例隔离
@@ -83,4 +83,23 @@
83
83
 
84
84
  - R42. 配置页必须可通过**设置**进入:插件在设置面板注册一级页面(设置 → DBHub 数据库工具),该承载位属核心界面、任何部署都存在,用户也在此寻找连接管理;同时在官方「插件」页按当前 dsh 版本提供的单插件配置槽位(0.1.6 起为 `<包名>#<行 id>` 的配置页)再挂一份。三处进入的是同一个页面与同一份状态。
85
85
  - R43. 插件另提供**侧边栏快捷入口**,且该入口的显示与否由设置页上的开关(`在侧边栏显示入口`,默认开)控制,改动即时生效、无需重启或刷新。
86
- - R44. 官方槽位变化或缺失(如 dsh 升级移除旧槽位)不得导致配置页不可达;关闭侧边栏入口**不得**造成自锁——设置页与插件页入口必须始终可用。
86
+ - R44. 官方槽位变化或缺失(如 dsh 升级移除旧槽位)不得导致配置页不可达;关闭侧边栏入口**不得**造成自锁——设置页与插件页入口必须始终可用。
87
+ - R56. 插件必须同时支持两条 dsh 版本线(0.1.x 与 0.2.x)的配置页数据通道:dsh 升级删除插件所依赖的宿主接口时(如 0.1.7 起 settings 命名空间被 Config 表单取代、0.2.x 删除浏览器侧 `settingsScope` 服务),插件**不得**因等待一个永远不会出现的服务而静默失活(表现为配置页/侧边栏/插件页入口全部消失且无任何报错)。浏览器半必须主动探测可用通道并降级;页面数据与写入能力在两条通道上必须等价。
88
+ - R57. 新增通道不得削弱既有安全边界:浏览器↔宿主的状态读取只承载元数据(延续 R3/R4),连接串只在写入方向从浏览器直达宿主;写入路由必须与 dsh 自身的接口鉴权一致(未鉴权的请求必须被拒绝),不得因为"插件自有路由"而绕过鉴权。
89
+ - R58. 插件选项(自动更新间隔、侧边栏入口)必须在两条版本线上都能持久化,并且升级插件不得静默重置用户已保存的值(旧版本的设置文档作为更低一层被读入)。在 0.1.7 及以后,这两项同时作为插件的声明式配置暴露:可经 `cordis.patch.yml` 或官方插件表单设置,且**与插件自有配置页共享同一个有效值**——任一处的改动都必须在另一处立刻可见(不得出现"改了表单但页面不变"或反之)。
90
+
91
+ ## 十一、配置页布局与空间占用
92
+
93
+ > 本节由 4.2 迭代沉淀:连接数量增长后,原「每个连接三到四行 + 常显添加表单」的平铺列表把大量高度花在重复的工作区标题与固定块上;下列要求把列表改为按工作区组织、每连接一行。
94
+
95
+ - R45. 连接列表必须**按工作区组织**,并在两种呈现间自动选择:**所有工作区都恰好只有一个环境**时使用**平铺**(一格一个工作区,信息全露、无需展开);**任一工作区有多个环境**时使用**按工作区分组**。判定只取决于"是否存在多环境工作区",不引入数量阈值。
96
+ - R46. 工作区分组必须支持**点击组头展开/折叠**,组头显示该工作区的环境数与已保存数;分组以**工作区路径**为身份(同名工作区是不同目标,不得仅凭标题合并,同名时必须可见地消歧)。
97
+ - R47. 默认展开态:单个工作区自动展开;多工作区时展开**用户最近使用的工作区**,其余折叠;无法判定最近工作区时全部折叠。必须提供「全部展开 / 全部折叠」总控。用户的展开/折叠选择在同一次页面会话内保留(跨重挂载有效),**不跨刷新持久化**。
98
+ - R48. 列表中**每条连接只占一行**:环境名、连接元数据(类型/主机/端口/库名)与操作;`source` 句柄不得再单独占据整行,改为**可复制入口**(保持可复制性,且必须让人看得出复制的是 source 句柄而不是整行连接信息)。**库名必须始终可见**:窄行下可截断主机/端口前缀,不得把库名截掉。行内操作以图标承载(必须带 `title`/`aria-label` 说明),不得用文字按钮占掉行宽。**连接的「来源」(手填/扫描/复制/自动发现)不得作为行内徽章常显**——它对绝大多数行都一样,却会抢走行的视觉焦点;应改为按需查看(悬浮提示),其中「自动发现且未持久化」这一例外需明确提示(该条尚未保存)。列表内不得出现密码或完整 DSN(延续 R3)。
99
+ - R49. 低频内容必须收拢:状态与开关收成页面顶部一行,自动更新间隔与侧边栏开关放进默认收起的设置条;**添加连接表单默认收起**。分组内新增连接必须自动带入该工作区(不要求用户重复填写/选择工作区)。
100
+ - R50. 界面配色必须使用 DSH 主题令牌,随主题/皮肤与**半透明背景**自适应:样式表中不得出现不透明色字面量(含兜底值),正文与操作文案的对比度不得依赖固定浅灰。用户可能安装主题/皮肤插件覆盖或省略令牌,此时界面仍需可读(表面继承页面背景而不是涂上白块)。
101
+ - R51. 工作区分组与其环境行之间必须有**可见的层级区分**(缩进、引导线、边框或阴影等),不得让组头与环境行看起来是同一层。
102
+ - R52. 界面写入连接时,若目标工作区的目标环境名**已经存在**(新增落到已存在环境,或改名落到已存在环境),必须**先提示"将覆盖该连接"并要求用户确认**;取消则不得产生任何写入,并保留用户已填内容以便改名。判定必须使用与宿主一致的环境名规范化规则(去控制字符、trim、空值→`default`、限长),否则会出现"看起来不同、实际是同一个键"的漏判。仅修改**同一个环境**的连接不属此列(那正是编辑的目的,不需二次确认)。
103
+ - R53. 界面操作的**结果必须即时可见**:新增/删除/改名/连接测试提交后,列表不得等到"重新扫描全部工作区"才更新,也不得出现"先整块消失、稍后再出现"的空窗。理想顺序是:宿主应用写入后立刻按已知状态回推,再以权威扫描校正(扫描失败时保留最后一次可用列表)。
104
+ - R54. 从界面复制连接信息(连接串标签等)必须得到**可直接使用**的字符串:不得因为渲染分层(例如把类型/主机段与库名拆成两个盒子)而混入空格、换行等分隔符。
105
+ - R55. **连接发现不得被单个工作区拖死**:自动发现(mise env / .env)的每一次外部探测都必须在有限时间内返回(超时即放弃该工作区的自动发现,不影响其他工作区与已配置连接),且同一目录的结果在短时间内可复用(避免每次列表/执行都重新探测)。**界面显示连接与模型侧看到连接必须一致**:卡片写入所用的工作区标识必须与发现/查询所用的同一份注册表,不得出现"设置里有、模型列不出来"。