dsh-dbhub-live 4.0.0 → 5.0.0
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 +104 -35
- package/README.en.md +35 -31
- package/README.md +35 -31
- package/doc/REQUIREMENTS.md +56 -10
- package/lib/bridge.mjs +125 -0
- package/lib/client.js +1188 -427
- package/lib/config.mjs +250 -12
- package/lib/i18n.mjs +96 -28
- package/lib/index.mjs +371 -91
- package/lib/mcp.mjs +175 -27
- package/lib/options.mjs +77 -11
- package/lib/tools.mjs +420 -150
- package/package.json +18 -6
- package/scripts/desensitize-check.mjs +324 -0
- package/scripts/desensitize.config.mjs +237 -0
- package/scripts/render-check.mjs +843 -0
- package/test/auto-dsn.test.mjs +126 -0
- package/test/bridge.test.mjs +185 -0
- package/test/client-format.test.mjs +352 -96
- package/test/i18n.test.mjs +3 -3
- package/test/init.test.mjs +2 -2
- package/test/options.test.mjs +123 -39
- package/test/resolve-workspace.test.mjs +115 -0
- package/test/row-cache.test.mjs +138 -0
- package/test/util.test.mjs +146 -12
- package/test/zero-knowledge.test.mjs +2 -2
package/AGENTS.md
CHANGED
|
@@ -4,24 +4,28 @@
|
|
|
4
4
|
|
|
5
5
|
## 一句话定位
|
|
6
6
|
|
|
7
|
-
DSH host 插件(Node ESM)+ Web client 插件(浏览器半):把 dbhub(Bytebase 的数据库 MCP 服务器)接入 DSH。**零知识凭据管理 + 一次性进程执行**——DSN/密码只存在于宿主侧,模型只见 source 句柄与元数据(类型/主机/端口/库);每次工具调用 spawn 一条独立 `dbhub --dsn`
|
|
7
|
+
DSH host 插件(Node ESM)+ Web client 插件(浏览器半):把 dbhub(Bytebase 的数据库 MCP 服务器)接入 DSH。**零知识凭据管理 + 一次性进程执行**——DSN/密码只存在于宿主侧,模型只见 source 句柄与元数据(类型/主机/端口/库);每次工具调用 spawn 一条独立 `dbhub --dsn` 进程跑完即杀,无常驻服务。附配置页(设置 → DBHub 数据库工具 / 侧边栏快捷入口(可关)/ 插件页该行的「配置」):实时状态 + 连接管理(启用禁用、增删改查、环境改名、连接测试)。
|
|
8
8
|
|
|
9
9
|
## 目录结构
|
|
10
10
|
|
|
11
11
|
```
|
|
12
12
|
lib/
|
|
13
|
-
index.mjs 入口:name/inject/apply
|
|
14
|
-
|
|
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
|
|
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
|
-
tools.mjs host 自有工具定义与注册:恒定 4 个(configure/list_sources/execute_sql/search_objects)
|
|
22
|
-
client.js Web 半(手写 lazy-CJS bundle
|
|
23
|
-
test/ node:test 单元测试(纯逻辑 + client bundle 格式契约 + 零知识泄漏门)
|
|
22
|
+
tools.mjs host 自有工具定义与注册:恒定 4 个(configure/list_sources/execute_sql/search_objects);configure 探连优先(见零知识契约 5)
|
|
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
|
|
|
@@ -32,22 +36,26 @@ cordis.patch.yml bundle patch:`name: dsh-dbhub-live` 挂载本包
|
|
|
32
36
|
- **零知识模型契约(红线)**:
|
|
33
37
|
1. 工具**参数**:`source`/`sql`/`object_type` 等 + configure 的非密预填(type/host/port/database/user)。**任何工具都不得声明 `dsn` 参数**(模型传 DSN 会把密码带进上下文);configure 收到 `args.dsn` 直接拒绝(`result.noDsnViaModel`)。`dbhub_query`/`dbhub_query_objects` 已删除,勿恢复。
|
|
34
38
|
2. 工具**输出**:结果/列表/标签一律 `connLabel(dsn)`(`type://host:port/db`)或 i18n 模板,密码与用户名永不出现;`dbhub` 返回的文本(含 stderr)进模型前必须过 `scrubSecrets(text, dsn)`。
|
|
35
|
-
3. **密码只在界面输入**:configure 的密码/完整 DSN 一律经 `askUser
|
|
36
|
-
4.
|
|
37
|
-
5.
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
39
|
+
3. **密码只在界面输入**:configure 的密码/完整 DSN 一律经 `askUser`(用户敲键盘,不经模型上下文);配置页 add/edit 的 DSN 走 `configOp` 通道(浏览器→Host),同样不进模型上下文。
|
|
40
|
+
4. **鉴权/连接失败闭环**:`likelyAuthOrConnError` 命中时在错误文本后附 `result.authHint`(引导 `dbhub_configure` 或配置页修改,密码由用户输入)。
|
|
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` 覆盖)。
|
|
42
|
+
6. **零密码泄漏门**:`test/zero-knowledge.test.mjs` 断言 summarizeRows 无 dsn 字段、无工具声明 `dsn` 参数、描述/i18n 文案不含真实形态 DSN;`util.test` 断言 scrubSecrets/connLabel/buildDsnFromParts/decideConfigureStep 行为。改模型可见文案时这些测试必须保持绿。
|
|
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。
|
|
48
|
+
- **工具声明数量恒定(上下文预算红线)**:恒定 4 个(configure / list_sources / execute_sql / search_objects),**绝不为每个工作区 × 环境注册独有工具**;`dbhub_execute_sql(source,…)` / `dbhub_search_objects(source,…)` 调用时 `resolveSource` 按 source 值解析到真实连接。曾实现过的 per-source 注册与 `dbhub_query` 临时连接工具已移除,勿回恢复。
|
|
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`)。
|
|
42
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——会重新引入链路安装时启动失败。
|
|
43
51
|
- **执行时机**:`apply()` 只注册核心工具 + 后台 bookkeeping(`startBackgroundBoot`:置 phase running、刷新工具数、非阻塞检查 dbhub 自动更新);真正的 dbhub 解析(persisted → mise → PATH → 自动安装)发生在**每次调用**(`resolveDbhubExe` 有 runtime.json 缓存,首次或缺二进制时才安装)。**无工作区数据源时一切照常**——工具正常注册,list_sources 返回空、execute 返回 noSource,不会拉起任何进程。
|
|
44
52
|
- **启用/禁用**:`state.setEnabled` 持久化到 `credentials.json`(权威值);禁用时所有工具 execute 首行返回「插件已禁用」(无进程可停,无需清理);重新启用即恢复。
|
|
45
53
|
|
|
46
54
|
## 存储与容错(初始化即处理)
|
|
47
55
|
|
|
48
|
-
- **实例隔离**:所有持久化都在 `$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`(用户驱动低频写),写出已原子化(见下)。
|
|
49
57
|
- **原子写**:`writeJsonFile` 一律 tmp+rename(同目录写临时文件再改名覆盖),并发读者永不见半个文件、并发写者末写覆盖不交错;目录被删时先 `mkdirSync` 重建。写入失败**不抛致命**,`warnOnce` 一次性告警并继续内存态运行(`saveStore`/`saveRuntime` 返回 false)。
|
|
50
|
-
- **升级/手改遗留兼容**:`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 只保留两个已知字段,非法值直接丢弃而不是回写)。
|
|
51
59
|
- **空值安全**:解析器对缺失/空值全部有兜底(`resolveWorkspaceEnvs` 判空、`describeConn` 对不可解析 DSN 兜底、状态 schema 默认值、settings 镜像的 `enabled` 只认布尔),清洗后不存在半吊子条目。
|
|
52
60
|
|
|
53
61
|
## 调试方法(不影响正在运行的 Harness)
|
|
@@ -56,14 +64,40 @@ DSH 启动是 fail-loud:任一插件激活失败整树拒绝启动、GUI 打
|
|
|
56
64
|
|
|
57
65
|
1. **静态校验(30 秒,零风险)** — `npm run check`(node --check 全部 lib)→ 逐个跑单测(沙箱下 `node --test` 的 runner 子进程会被 EPERM 挡,按本文件惯例**逐个文件**跑:`node test/x.test.mjs`)。单测会自己建临时 `DSH_HOME`,不会碰真实 store。
|
|
58
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` 不能整文件贴进动态包;只对纯逻辑做动态验证。
|
|
59
|
-
3. **冷启动验证(隔离实例)** — 用独立 `DSH_HOME`
|
|
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")。
|
|
60
70
|
```powershell
|
|
61
|
-
|
|
62
|
-
dsh
|
|
63
|
-
dsh plugin --profile web add D:/path/to/dsh-dbhub-live
|
|
64
|
-
|
|
65
|
-
#
|
|
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 数据库工具」、设置里出现同名一级页面,行内显示连接元数据
|
|
66
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 可能不可写,见上面手工路径。
|
|
67
101
|
4. **在位热更新(有失败保护,谨慎)** — 主实例的 `cordis.patch.yml` 支持 HMR,读取/解析失败时保留最后一个可用树,GUI 不会挂。但 HMR 不监听插件源码,只适合挂载/卸载验证,代码迭代请用 1/2/3。
|
|
68
102
|
5. **安全网** — `$DSH_SNAPSHOT=replay` 可从 `cordis.snapshot.yml` 启动回放点;改动主 profile 前先备份 `cordis.patch.yml`。
|
|
69
103
|
|
|
@@ -75,41 +109,76 @@ DSH 启动是 fail-loud:任一插件激活失败整树拒绝启动、GUI 打
|
|
|
75
109
|
- `id` 必须等于 Loader entry 名(`dsh-dbhub-live`,见 cordis.patch.yml 的 `name`),不是行 id;
|
|
76
110
|
- factory 只允许 `require("react")` 等 baseline 模块;不 import 其他插件的值(bundle-purity);
|
|
77
111
|
- 导出 `name/inject/apply`,结尾 `return module.exports`;
|
|
78
|
-
-
|
|
79
|
-
-
|
|
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 后再注册 + 降级后文案仍是本地化文本)。
|
|
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`),不要改回"等整轮扫描"。
|
|
80
131
|
- 若本包脱离仓库(发布 npm),`dsh.client` 与 `exports["./client"]` 必须保留,否则 client-modules 扫描会启动报错。
|
|
81
132
|
|
|
82
133
|
## 质量门
|
|
83
134
|
|
|
84
135
|
```bash
|
|
85
|
-
npm run check
|
|
136
|
+
npm run check # 全部 lib 语法
|
|
137
|
+
npm run check:secrets # 脱敏门禁(槽位表 + 通用兜底 + 本地 denylist,见下)
|
|
138
|
+
npm run check:ui # 客户端布局:离屏渲染 + 点击交互门禁(自带极简 React shim,无依赖)
|
|
86
139
|
# 单测:沙箱内逐个文件跑(node --test 的 runner 子进程在沙箱下 EPERM)
|
|
87
140
|
node test/util.test.mjs && node test/state.test.mjs && node test/options.test.mjs \
|
|
88
141
|
&& node test/init.test.mjs && node test/i18n.test.mjs \
|
|
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 \
|
|
89
145
|
&& node test/client-format.test.mjs && node test/zero-knowledge.test.mjs
|
|
90
146
|
```
|
|
91
147
|
|
|
92
|
-
|
|
148
|
+
### 脱敏门禁(`npm run check:secrets`)
|
|
93
149
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
150
|
+
真实主机/账号/密码/库名/项目名只允许存在于两处:`$DSH_HOME/storages/dsh-dbhub-live/credentials.json`(仓库外)与本地 `.env`(`.gitignore` 排除、`package.json` 的 `files` 不含,永不进 git 历史/公开仓库/npm 包)。仓库内一切内容都视为会公开分发。
|
|
151
|
+
|
|
152
|
+
门禁由 `scripts/desensitize-check.mjs` 执行,规则与槽位来自 `scripts/desensitize.config.mjs`:
|
|
153
|
+
|
|
154
|
+
1. **槽位规则(主力,高精度)**:槽位表登记"真实值可能出现的出口"(宿主文案、浏览器文案、连接构造代码、扫描器、测试夹具、文档示例、包元数据、本提示词、维护脚本),每个槽位声明它需要的规则(`password`/`dsn`/`host`/`name`)。**槽位 glob 必须匹配到文件,否则门禁报 `slot-stale`**——槽位表不会随目录移动悄悄失效。
|
|
155
|
+
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)。
|
|
156
|
+
3. **本地 denylist(补充)**:pattern 抓不到的**裸名字**(项目/公司/库名)放在 `.env` 的 `DSH_DBHUB_DESENSITIZE_RE`(`|` 分隔、正则转义、多条取并集)。CI 里没有 `.env`,该层自动降级为提示,不影响其余两层。
|
|
157
|
+
|
|
158
|
+
**约定(新增内容一律照此写)**:示例主机用 `127.0.0.1`/`localhost`/RFC 5737 段;示例密码用 `CHANGE_ME`;测试夹具可用短占位(`u:p`、`root:secret`),但主机仍须回环/保留段;误报走行内标记 `desensitize:allow`(仅限确认无害的行)。
|
|
159
|
+
|
|
160
|
+
**义务(硬规则)**:新增功能若引入新的示例、默认值、文案串或夹具 → **同一次改动里更新 `scripts/desensitize.config.mjs` 的槽位表**;提交前跑 `npm run check:secrets`,命中就改成占位符/规范值再提交 git 与 npm。执行点:本地手动 + `prepublishOnly`(挡住带真实值的包)+ CI(`.github/workflows/quality.yml`,别人的提交也拦得住)。历史事故:这些值曾随 npm 包与公开仓库分发,公开即视为泄露、无法追回。
|
|
97
161
|
|
|
98
|
-
零知识自检(模型可见面不得含密码):`node test/zero-knowledge.test.mjs` 全绿 + 上面
|
|
162
|
+
零知识自检(模型可见面不得含密码):`node test/zero-knowledge.test.mjs` 全绿 + 上面 `npm run check:secrets` 零匹配。
|
|
99
163
|
|
|
100
|
-
发布前:按「调试方法」第 3 步在隔离实例完整冷启动一遍,确认 Host 无报错、`/
|
|
164
|
+
发布前:按「调试方法」第 3 步在隔离实例完整冷启动一遍,确认 Host 无报错、`/api/dsh-dbhub-live/state` 带会话 cookie 返回 200(不带 cookie 返回 401)、`/plugins/??<包名>/client.js&rev=…` 可访问。
|
|
101
165
|
|
|
102
|
-
##
|
|
166
|
+
## 版本号策略(测试版本 → 发布版本)
|
|
103
167
|
|
|
104
|
-
-
|
|
168
|
+
- **基线 = 代码库当前版本**(`package.json` 版本,即最近一次发布)。测试/调试迭代一律在基线上挂 `-dev.<序号>`,序号从 1 递增、不复用(如 `4.0.1-dev.1`、`4.0.1-dev.2`…),`pnpm pack` 产物随之换名;
|
|
105
169
|
- 原因:pnpm 对 `dsh plugin add <同名同版本 tarball>` 会判「Already up to date」跳过、**不换包**;升版本号才能保证调试包真正部署。
|
|
106
170
|
- `dsh plugin add` 后务必用 `dsh plugin ls` 或核对 `node_modules/<pkg>/package.json` 的版本号确认已替换。
|
|
107
|
-
-
|
|
171
|
+
- **测试完成要发版时,在基线上按「跨度和力度」决定升级幅度**(不以 dev 序号原号发布):
|
|
172
|
+
- 小 bug 修复 / 小功能改动 → 升**补丁号**:`4.0.0` → `4.0.1`(例:测试到 `4.0.0-dev.8` 仅小修 → 发布 `4.0.1`);
|
|
173
|
+
- 大功能新增 / 影响面较广的功能改动 → 升**次版本号**:`4.0.0` → `4.1.0`;
|
|
174
|
+
- 破坏性升级 / 阶段性架构变更 / 跨度过大 → 升**主版本号**:`4.0.0` → `5.0.0`(tag/npm 一律打正式版本号)。
|
|
175
|
+
- 跨档并存时按最高档计(含破坏性改动 → 主版本);归属拿不准时**先向用户确认再发版**,禁止未确认跨度直接升主版本。
|
|
176
|
+
- **发布版本成为新基线**,下一轮测试从 `新基线-dev.1` 重新起序(如 `5.0.0-dev.1`…)。
|
|
108
177
|
|
|
109
178
|
## 约定
|
|
110
179
|
|
|
111
180
|
- 产品文案中文、代码注释英文;密码脱敏/零知识不可绕过;扫描必须经 `askUser` 授权。
|
|
112
|
-
- **脱敏红线(提交/发布前强制自检)**:仓库任何文件——**包括 `test/`(会打进 npm tarball)与工具描述文案**——不得出现真实内网 IP
|
|
181
|
+
- **脱敏红线(提交/发布前强制自检)**:仓库任何文件——**包括 `test/`(会打进 npm tarball)与工具描述文案**——不得出现真实内网 IP、真实库名、真实密码、公司/项目标识。写法约定与三层门禁见上面「脱敏门禁」:示例主机只用回环或 RFC 5737 段、示例密码只用 `CHANGE_ME`、新出口同次登记进 `scripts/desensitize.config.mjs`;发现新的真实值**只追加到本地 `.env` 的 `DSH_DBHUB_DESENSITIZE_RE`**,绝不写进任何受版本控制的文件。
|
|
113
182
|
- `state.*` 之外不要直接改持久化。
|
|
114
183
|
- 增加行为时同步更新本文件、README(用户侧)与 doc/REQUIREMENTS.md(业务侧)。
|
|
115
184
|
- README 双语同步:`README.md`(中文)为唯一真源,`README.en.md` 由 AI 从最新中文派生——改动任一侧必须同次更新另一侧,章节结构一一对应。
|
package/README.en.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[简体中文](README.md) | English
|
|
4
4
|
|
|
5
|
-
> Let [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/DeepSeek-Harness) operate databases directly and safely: **zero-knowledge credentials** (passwords never reach the model) + **one-shot process execution** (no resident server — naturally concurrent and multi-instance safe) + workspace × environment connection management + a browser
|
|
5
|
+
> Let [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/DeepSeek-Harness) operate databases directly and safely: **zero-knowledge credentials** (passwords never reach the model) + **one-shot process execution** (no resident server — naturally concurrent and multi-instance safe) + workspace × environment connection management + a browser Configure Page.
|
|
6
6
|
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
[](#installation)
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
- **One-shot process execution** — no resident dbhub service: each call spawns an independent process and recycles it when done. A hung/failed query only affects its own call; parallel tasks and multiple DSH instances running at once never interfere (no shared ports, no cross-kills).
|
|
19
19
|
- **Constant 4 tool declarations** — `dbhub_configure` / `dbhub_list_sources` / `dbhub_execute_sql` / `dbhub_search_objects`; more environments never inflate the model context.
|
|
20
20
|
- **Workspace × environment connection management** — one workspace can hold multiple environments (default / prod / dev / test…) distinguished by their `source` value.
|
|
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.
|
|
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
|
|
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,10 +43,12 @@ 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@
|
|
46
|
+
dsh plugin --profile web update dsh-dbhub-live@5.0.0
|
|
46
47
|
```
|
|
47
48
|
|
|
48
|
-
After installing, **restart `dsh web`** for it to take effect (you can then see the
|
|
49
|
+
After installing, **restart `dsh web`** for it to take effect (you can then see the configure page at **Settings → DBHub Database Tools**).
|
|
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.
|
|
49
52
|
|
|
50
53
|
## Quick Start
|
|
51
54
|
|
|
@@ -66,9 +69,9 @@ dbhub_list_sources
|
|
|
66
69
|
|
|
67
70
|
| Tool | Description |
|
|
68
71
|
| --- | --- |
|
|
69
|
-
| `dbhub_configure(workspace?, env?, type?, host?, port?, database?, user?)` | Configure/persist a workspace connection. **Does NOT accept a dsn argument** — the password/DSN is always entered in the UI (never through the model); type/host/port/database/user may be passed as non-sensitive prefills. |
|
|
70
|
-
| `dbhub_list_sources()` | List every connection source
|
|
71
|
-
| `dbhub_execute_sql(source, sql)` | Execute SQL on a source; `source` comes from `dbhub_list_sources
|
|
72
|
+
| `dbhub_configure(workspace?, env?, renameFrom?, copyFrom?, type?, host?, port?, database?, user?)` | Configure/persist a workspace connection. **Does NOT accept a dsn argument** — the password/DSN is always entered in the UI (never through the model); type/host/port/database/user may be passed as non-sensitive prefills. **With enough prefills the plugin auto-probes first**: connectable → saved directly and the source value returned; password required → a minimal "account (when unknown) + password" dialog; incomplete info or another failure → the full method choice appears. **Rename**: `env=new name` + `renameFrom=old name` (the connection and its credentials stay as they are; works for `default` → `prod` or a Chinese/English swap; an existing target is confirmed with the user first). **Cross-workspace copy**: `copyFrom=<another workspace's source value>` + `env=<environment to create here>`; the Host copies the connection, so the password never passes through the model. |
|
|
73
|
+
| `dbhub_list_sources()` | List every connection source, **grouped by workspace** and marking the **[CURRENT WORKSPACE]**: metadata only (type/host/port/database) + origin badge + the corresponding **source** value. |
|
|
74
|
+
| `dbhub_execute_sql(source, sql)` | Execute SQL on a source; `source` comes from `dbhub_list_sources`. **Every source belongs to exactly one workspace** — prefer the current workspace's; to reuse another workspace's connection, first copy it here with `dbhub_configure`'s `copyFrom` instead of querying that workspace's source directly. Each call is an independent one-shot connection; multiple statements separated by `;`. |
|
|
72
75
|
| `dbhub_search_objects(source, object_type, ...)` | Search database objects (tables/views/columns/indexes, etc.) on a given source. |
|
|
73
76
|
|
|
74
77
|
> **Security**: the model can never obtain a password through this plugin — results and lists only show metadata like `mysql://host:3306/db`; dbhub's error text is scrubbed before it is returned. On a failing query, follow the hint and update the password in the UI.
|
|
@@ -77,44 +80,44 @@ dbhub_list_sources
|
|
|
77
80
|
|
|
78
81
|
### Configuration Methods
|
|
79
82
|
|
|
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.
|
|
80
84
|
1. **Explicit DSN** — enter a full connection string in the UI, e.g. `mysql://user:pass@host:3306/db`.
|
|
81
|
-
2. **Fill in fields** — fill type / host / port / user / password / database name in the UI.
|
|
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.
|
|
82
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.
|
|
83
87
|
|
|
84
88
|
If a workspace already has `mise env` or `.env` (`DSN` / `DB_*`), the plugin discovers it automatically — no manual configuration needed.
|
|
85
89
|
|
|
86
|
-
##
|
|
87
|
-
|
|
88
|
-
Settings → Plugins → dsh-dbhub-live: the plugin syncs its state and configuration to the Web settings panel in real time (only visible on the `dsh web` side). The card uses the **single-row collapsible** style (consistent with the other plugin settings cards):
|
|
90
|
+
## Configure Page
|
|
89
91
|
|
|
90
|
-
|
|
92
|
+
Three entries, one page and one state:
|
|
91
93
|
|
|
92
|
-
**
|
|
94
|
+
1. **Settings → DBHub Database Tools** (recommended): the first-level settings page the plugin registers; present in every deployment.
|
|
95
|
+
2. **Sidebar entry DBHub Database Tools**: a shortcut, on by default; turn off "Show the sidebar entry" on the settings page to drop it (applies immediately, no restart or reload).
|
|
96
|
+
3. **Plugins → dsh-dbhub-live → Configure**: the configure control the official Plugins page gives each plugin row since dsh 0.1.6 (that page is provided by the official plugin manager; use the first two entries when it is absent).
|
|
93
97
|
|
|
94
|
-
|
|
98
|
+
> Hiding the sidebar entry never removes access: the settings page and the plugin row's Configure control stay available.
|
|
95
99
|
|
|
96
|
-
|
|
100
|
+
All page copy (name, status, config fields, buttons, connection rows) follows the dsh UI language (Chinese / English — Settings → General → Language); model-facing errors, feedback and the host logs follow it as well.
|
|
97
101
|
|
|
98
|
-
|
|
99
|
-
- Tool declarations: `4 (fixed)`; environments: `N · M saved`.
|
|
100
|
-
- Most recent error (shown in red on error).
|
|
102
|
+
**Status**: status badge (🟢 running / ⚪ disabled), the **enable/disable switch**, mode (one-shot connection), tool declarations (`4 (fixed)`), environments (`N · M saved`), most recent error (shown in red on error).
|
|
101
103
|
|
|
102
104
|
**Configuration** (edit, then click "Save Configuration" to apply immediately and persist):
|
|
103
105
|
|
|
104
106
|
| Parameter | Description | Default |
|
|
105
107
|
| --- | --- | --- |
|
|
106
108
|
| Auto-update interval (days) | How often dbhub is auto-updated; `0` disables | `7` |
|
|
109
|
+
| Show the sidebar entry | Whether the DBHub Database Tools shortcut appears in the sidebar; applies immediately | on |
|
|
107
110
|
|
|
108
|
-
Precedence: **
|
|
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
|
|
|
112
|
-
- Lists every workspace × environment connection: workspace name, environment name, **source value** (what the model passes to `dbhub_execute_sql`, shown in monospace), **connection metadata** (🔒 `mysql://host:3306/db` — no username, no password; passwords never appear on the
|
|
113
|
-
- `saved`: you configured it (`dbhub_configure` or added in the
|
|
115
|
+
- Lists every workspace × environment connection: workspace name, environment name, **source value** (what the model passes to `dbhub_execute_sql`, shown in monospace), **connection metadata** (🔒 `mysql://host:3306/db` — no username, no password; passwords never appear on the page), origin badge (`saved` / `auto`) and origin detail (`saved · user` / `saved · scan` / `saved · copied` / `auto · mise env` / `auto · .env`).
|
|
116
|
+
- `saved`: you configured it (`dbhub_configure` or added in the page).
|
|
114
117
|
- `auto`: not saved, discovered from `mise env` / `.env` — not persisted and follows the source files; if auto-discovery is wrong, use "Edit" to override it with a manual configuration.
|
|
115
|
-
- Each row can be **tested** (connectivity probe, see below), **edited** (
|
|
118
|
+
- Each row can be **tested** (connectivity probe, see below), **edited** (change the connection string **and/or the environment name**; a rename keeps the connection and its credentials untouched — enter only a name to rename) or **deleted** (saved items only).
|
|
116
119
|
- **Connection test**: clicking "Test" makes the Host probe the row's real DSN through a throwaway connection (a one-off dbhub process running `SELECT 1`) and shows success/failure inline. The report is one-shot feedback: never persisted, fades after ~10 s; a failure never marks, restricts or alters the connection, other environments or queries (slow/unreachable databases wait at most ~30 s).
|
|
117
|
-
- **Multiple environments per workspace**: fill "workspace (path or title, empty = default current workspace) + environment name + connection string" in the form and click "Add Connection".
|
|
120
|
+
- **Multiple environments per workspace**: fill "workspace (path or title, empty = default current workspace) + environment name + connection string" in the form and click "Add Connection". Environment names **may be Chinese** (e.g. `线上` / `测试`): the name is shown verbatim while the environment segment of the source value becomes `env-<short hash>`, so two Chinese names can never collide (purely ASCII names such as `test` / `dev` keep their existing source values).
|
|
118
121
|
|
|
119
122
|
## dbhub Environment Variables
|
|
120
123
|
|
|
@@ -126,10 +129,10 @@ Precedence: **user settings > process environment variables (default seeds) > bu
|
|
|
126
129
|
## 🔄 Automatic Installation & Updates
|
|
127
130
|
|
|
128
131
|
- **Auto-install on first use** — when `dbhub` is missing locally, the plugin installs it on the first execution; afterwards it also works offline.
|
|
129
|
-
- **Kept up to date automatically** — silently updates to the latest version in the background (interval under "
|
|
132
|
+
- **Kept up to date automatically** — silently updates to the latest version in the background (interval under "Configure Page → configurable parameters"); on failure the existing version is kept.
|
|
130
133
|
- **Never touches your configuration** — a `dbhub` you installed yourself via PATH / mise is left untouched.
|
|
131
134
|
|
|
132
|
-
> The default update interval can also be seeded by an environment variable, see "
|
|
135
|
+
> The default update interval can also be seeded by an environment variable, see "Configure Page → Configuration"; once saved in the settings card, the saved value wins.
|
|
133
136
|
|
|
134
137
|
## Data Location
|
|
135
138
|
|
|
@@ -152,13 +155,14 @@ dsh plugin --profile web remove dsh-dbhub-live
|
|
|
152
155
|
| Symptom | Fix |
|
|
153
156
|
| --- | --- |
|
|
154
157
|
| "Cannot locate dbhub" on first use | Make sure npm is present and online; offline, install `dbhub` manually and add it to PATH. |
|
|
155
|
-
| 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
|
|
156
|
-
| Tools say "plugin disabled" | Open
|
|
157
|
-
|
|
|
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. |
|
|
159
|
+
| Tools say "plugin disabled" | Open Plugins → dsh-dbhub-live → Configure and click "Enable". |
|
|
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
|
-
| Don't want automatic dbhub updates | Set "Auto-update interval (days)" to `0` in the
|
|
161
|
-
| Can't update the password in chat (the model asks you for it) | That is by design — the model must not handle passwords. Ask the AI to run `dbhub_configure` and fill in the password in the UI prompt, or edit it yourself in
|
|
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`. |
|
|
165
|
+
| Can't update the password in chat (the model asks you for it) | That is by design — the model must not handle passwords. Ask the AI to run `dbhub_configure` and fill in the password in the UI prompt, or edit it yourself in Plugins → dsh-dbhub-live → Configure. |
|
|
162
166
|
|
|
163
167
|
## License
|
|
164
168
|
|