dsh-dbhub-live 5.2.0-dev.3 → 5.2.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 +116 -116
- package/README.en.md +22 -22
- package/README.md +23 -23
- package/doc/ARCHITECTURE.md +108 -108
- package/doc/CLIENT.md +124 -124
- package/doc/DESENSITIZE.md +56 -56
- package/doc/DEVELOPMENT.md +139 -139
- package/doc/IMPACT-MATRIX.md +36 -36
- package/doc/RELEASE.md +93 -93
- package/lib/meta.mjs +267 -267
- package/lib/policy.mjs +92 -92
- package/lib/update.mjs +244 -244
- package/package.json +1 -1
- package/scripts/desensitize-check.mjs +331 -331
- package/scripts/docs-check.mjs +214 -214
- package/scripts/release-preflight.mjs +180 -180
- package/test/meta.test.mjs +234 -234
- package/test/policy.test.mjs +132 -132
- package/test/update.test.mjs +270 -270
package/doc/ARCHITECTURE.md
CHANGED
|
@@ -1,108 +1,108 @@
|
|
|
1
|
-
# 架构理由与踩坑记录(按需加载)
|
|
2
|
-
|
|
3
|
-
> AGENTS.md 只记**结论**;这里记**原因**——每条红线的由来、当时踩到的坑、以及"为什么不能那样改"的判断依据。
|
|
4
|
-
> 改红线或质疑红线前读对应章节。操作步骤见 [DEVELOPMENT.md](DEVELOPMENT.md) / [CLIENT.md](CLIENT.md) / [RELEASE.md](RELEASE.md)。
|
|
5
|
-
|
|
6
|
-
## 零知识边界
|
|
7
|
-
|
|
8
|
-
- 模型上下文就是泄露面:任何进 context 的字符串都会被摘要、日志和后续轮次复制。所以规则不是"少给"而是"永不给"——工具参数不接受 DSN(模型传 DSN 等于把密码写进对话),输出一律经 `connLabel` / `scrubSecrets`。
|
|
9
|
-
- dbhub 的启动横幅**只把密码掩成 `****`,用户名原样打印**;而连接测试的诊断文本正是这段输出。所以清洗必须折叠任意 URL 形态连接串的整个 userinfo(`://user:pass@`、`://user@` → `://****:****@`),只掩密码不够。
|
|
10
|
-
- 仓库被视为公开分发面:这些真实值曾随 npm 包与公开仓库分发过一次,公开即视为泄露、无法追回(详见 [DESENSITIZE.md](DESENSITIZE.md))。
|
|
11
|
-
|
|
12
|
-
## 一次性进程的理由
|
|
13
|
-
|
|
14
|
-
- 常驻 dbhub 服务器会带来一整套生命周期问题:端口/stdio 归属、配置变更重启、指纹与 mtime 监听、空闲回收、孤儿进程、多实例互杀——5.0 之前这些都真实出现过。最终选择"每次调用一条进程":天然并发隔离、一条进程挂只坏它自己、多实例无共享状态、没有常驻资源可泄漏。
|
|
15
|
-
- 代价是每次的启动延迟(spawn + initialize),换来的是可预测性。**不要以"优化启动"为名把常驻服务装回来。**
|
|
16
|
-
|
|
17
|
-
## 一次性TOML的由来
|
|
18
|
-
|
|
19
|
-
- `--dsn` 与 `--config` 互斥,而 dbhub 只有 TOML 方言能表达环境级只读(`[[tools]] readonly`)与 SSH 隧道(`ssh_*`)。所以只有"需要这两个特性"的环境走临时 TOML,其余**逐字节保持** `--dsn`(历史行为,密码在 argv)。
|
|
20
|
-
- 三个踩过的坑:
|
|
21
|
-
1. 秘密不能落文件 → 文件只写 `${DSH_DBHUB_SEC_*}` 占位符,真值走子进程 env。**dbhub 取不到变量时不报错**,会把 `${VAR}` 原样留在 DSN 里(于是连了一个字面量叫 `${VAR}` 的目标,或出现"神秘失败")——所以"文本引用的变量集合 == env 键集合"是硬断言。
|
|
22
|
-
2. `[[tools]]` 是**白名单不是补丁**:只要出现任意一条,dbhub 就不再自动挂 `execute_sql` + `search_objects`。只写一条 read_only 会让该环境的 `search_objects` 直接消失(`Tool search_objects not found`)。
|
|
23
|
-
3. source 级 `read_only` 写法会被**静默忽略**,比报错危险得多 → 绝不生成未实测的旧写法。1.4.0 是实测版本,`< 1.0.0` 明确拒绝。
|
|
24
|
-
- Windows 上 dbhub 会 watch 这个临时文件,删除可能 EBUSY → `finally` 尽力删 + boot 时 `sweepTempToml()` 清残留。
|
|
25
|
-
|
|
26
|
-
## 环境选项与执行预算
|
|
27
|
-
|
|
28
|
-
- 选项**没有版本号也没有迁移函数**是刻意的:`normalizeStore` 保留未知字段,给 env 条目加可选字段不需要迁移;旧文件读回来就是"无只读、无隧道"。别为了"规范"加迁移。
|
|
29
|
-
- `setWorkspaceEnv` 的 opts **缺省必须保留**既有选项:`dbhub_configure` 只更新 DSN 的场景下,若默认清空就会把用户的安全开关静默关掉——这是"只读/隧道莫名丢失"的根因。
|
|
30
|
-
- 安全不对称必须在**工具路径**拦截而不是 store:模型只能收窄不能放宽,但浏览器必须能自由关,否则用户永远关不掉自己开的只读。
|
|
31
|
-
- 预算与看门狗的三角关系:客户端看门狗必须**严格大于**宿主探测预算,否则客户端先放弃、界面报"测试超时",而宿主那条 dbhub 还在跑;宿主再加 +500ms 兜底,处理 spawn 本身永不 settle 的情况。三个数字必须一起改,`test/adhoc-toml.test.mjs` 会断言。
|
|
32
|
-
|
|
33
|
-
## envSlug事故
|
|
34
|
-
|
|
35
|
-
- 早期实现把非 ASCII 环境名统一 slug 化成一个值,`线上` 与 `测试` 拿到同一个 source 句柄 → 解析时**静默查错库**(模型以为在测环境,实际打线上)。现在:`default` 裸值、纯 ASCII 保留原 slug(既有 source 不变)、非 ASCII 取 `env-<shortHash>`。这是回归红线,不是风格问题。
|
|
36
|
-
|
|
37
|
-
## 状态通道与inject根因
|
|
38
|
-
|
|
39
|
-
> **为什么不能改回 inject(5.0 移植的根因)**:dsh 0.1.7 用 Config 表单取代了 settings 命名空间,0.2.x 又删掉浏览器侧 `settingsScope`。旧客户端 `inject: ['slots', 'settingsScope']` 在 0.2.x 上永远等不到服务 → 整个浏览器半区 pending、**无任何报错**,表现就是「配置页 / 侧边栏 / 插件页卡片静默消失」。所以这一层必须做成运行时探测 + 双实现(`legacyFace` / `httpFace`),而不是声明式依赖。
|
|
40
|
-
|
|
41
|
-
- 第二个静默失败面:**注入回调是独立作用域**。在回调里引用 `apply()` 的局部变量会 ReferenceError,而错误发生在发布链路上——页面只剩静态值、无任何可见报错。需要的服务必须 `ctx.get(...)` 就地获取。
|
|
42
|
-
- 两条通道都发布同一个 view 是刻意的:客户端只写一份渲染逻辑,通道差异收敛在 `legacyFace` / `httpFace` 两个适配器里。
|
|
43
|
-
|
|
44
|
-
## 诊断为什么取尾部
|
|
45
|
-
|
|
46
|
-
- dbhub 失败时致命行在输出末尾(前面是启动横幅与重试日志),取开头只能得到 `connecting…`。所以 `diagnosticTail` 取尾部 600 字符、丢 `at …` 栈帧与折行残片、按行边界截断(否则会截出半行 JSON/半个错误)。
|
|
47
|
-
- 分层前缀的意义:SSH 层与数据库层的可诊断性不同——只有**独立的 SSH 探测**能证明"隧道起来了";反向不成立,所以 db 层失败的措辞必须谨慎,不能写成"隧道断了"。
|
|
48
|
-
|
|
49
|
-
## 即时回推与扫描单飞
|
|
50
|
-
|
|
51
|
-
- `collectSources` 每次都要重扫全部工作区(无持久化 `default` 的工作区各起一次 `mise env`),一次写入原本要等 1–2s。更糟的是**并发扫描互相覆盖**:探针回执触发的扫描可能在写入前开始、写入后结束,把新行刷掉——表现就是"立刻出现又消失"。
|
|
52
|
-
- 结论:写入路径返回本次补丁并 `publishFast` 同步镜像;探针只镜像不扫描;扫描单飞并重放扫描期间到达的补丁;`rename` 补丁必须幂等(否则重放会丢行)。一次点击 = 1 次扫描 + 2 次纯内存镜像。
|
|
53
|
-
|
|
54
|
-
## 自动发现的代价与unref教训
|
|
55
|
-
|
|
56
|
-
- `mise env` 探测没有总超时会出事:曾有一个不退出的 `mise env` 让整轮扫描永不返回,而 `dbhub_list_sources` 正走这条同步路径,模型侧看到 `Interrupted`。所以每次探测必须有总超时(`graceMs` 只是 SIGTERM→SIGKILL 宽限,**不是超时**),结果按 TTL 缓存,工作区之间并发。
|
|
57
|
-
- **预算计时器绝不 `unref()`**:unref 计时器在"它是唯一待办"时不触发——事件循环排空、进程先退,超时形同虚设。该缺陷**只在 Linux CI 暴露**(整文件 cancelled),本地 Windows 会侥幸通过:本地全绿不能当发布依据。
|
|
58
|
-
|
|
59
|
-
## 布局取舍
|
|
60
|
-
|
|
61
|
-
- 行内只放图标、来源不做徽章、库名不截断,三条来自同一约束:每个连接只占一行,宽度是稀缺资源。文字按钮吃掉半行;"来源"徽章几乎每行相同却最抢眼;库名被截断是最高频的抱怨。
|
|
62
|
-
- 三面布局的两个阈值(700 / 720):实测出现过 710px 视口既不算手机(>= 700)也不够宽(< 720)的**空档**,布局判定落空。两阈值之间的窄桌面列必须走紧凑设置条,不能退回竖排。
|
|
63
|
-
- 现代面与 legacy 面必须能同时存在:`render-check` 的旧断言写死了 0.1.x 的 DOM,新界面只能挂在 `dshModern` 下;否则一次"统一界面"就会让旧线渲染异常。
|
|
64
|
-
|
|
65
|
-
## 覆盖门为什么先问
|
|
66
|
-
|
|
67
|
-
- `setWorkspaceEnv` 是**无条件写入**:新增或改名落到已存在的环境会静默替换原连接,而客户端无法回滚。所以写入前必须比较,命中就弹确认卡;比较前必须过 `normalizeEnvForCompare`(镜像宿主 `normalizeEnvName`),否则「prod 」与「prod」这种同键异写会漏判。取消 → 零 op 且保留表单内容。
|
|
68
|
-
|
|
69
|
-
## 脱敏的不可逆性
|
|
70
|
-
|
|
71
|
-
- 详见 [DESENSITIZE.md](DESENSITIZE.md)。一句话:真实值曾随 npm 包与公开仓库分发过一次,公开即无法追回;所以门禁是硬门,示例主机只用回环/RFC 5737,示例密码只用 `CHANGE_ME`,任何人不得把本机绝对路径写进受版本控制的文件。
|
|
72
|
-
|
|
73
|
-
## 版本门与更新检查
|
|
74
|
-
|
|
75
|
-
- `dshModern` 门(>= 0.2.1,含预发布)不是"只有新版本测过",而是"新界面依赖的 slot/DOM 能力在这个版本才稳定"。运行版本解析不到时回退为 desktop=true / web=false——桌面端总是新版、旧 web 线保持 legacy,比猜一个值安全。
|
|
76
|
-
- 更新检查**绝不抛错**:它只是页头一行提示,失败必须只体现为 `error` 字段,不得影响插件任何功能。
|
|
77
|
-
|
|
78
|
-
## 模块地图
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
lib/
|
|
82
|
-
index.mjs 入口:name/inject/apply、Config(volatile)、发布核心 createHostCore(walk/row-mirror/runOp/view)、
|
|
83
|
-
双通道接线(0.1.x settings 命名空间 / connection.fetch HTTP 桥)、settings 表单策略与选项写回、configOp 命令处理
|
|
84
|
-
meta.mjs 插件版本(读 package.json)、DSH 运行时版本探测、semver 比较、dshModern 门
|
|
85
|
-
update.mjs 最新版本检查:npm registry 为主 → GitHub Releases 兜底;内存节流、force、绝不抛错
|
|
86
|
-
bridge.mjs /api/dsh-dbhub-live 四条 exact Fetch 路由的请求/响应编解码(纯函数)
|
|
87
|
-
config.mjs 路径与原子持久化(store/runtime/prefs)、DSN 解析、工作区发现、环境选项(readOnly/ssh 归一化、默认密钥)、
|
|
88
|
-
零知识助手(describeConn/connLabel/scrubSecrets)、configure 决策纯函数(decideConfigureStep/argsChangedEndpoint/emptyUserDenied)
|
|
89
|
-
i18n.mjs 宿主侧文案表与插值
|
|
90
|
-
state.mjs 运行时状态机 enabled/phase/toolCount/lastError/mode + 订阅发布
|
|
91
|
-
options.mjs 部署开关:updateIntervalDays / showSidebarEntry / announceAccessPolicy;层叠 Config > prefs.json > 0.1.x 设置文档 > 环境变量 > 内置默认
|
|
92
|
-
runtime.mjs dbhub 可执行文件发现、按需自动安装、定期自动更新
|
|
93
|
-
capability.mjs 上游能力探测(文件级,不 spawn):[[tools]] readonly 与 ssh_* 需 1.4.0+
|
|
94
|
-
toml.mjs 一次性 TOML 生成器 buildDbhubToml/writeTempToml/removeTempToml/sweepTempToml(文本绝不含秘密)
|
|
95
|
-
mcp.mjs MCP JSON-RPC 客户端、工具注册表、来源发现 collectSources/resolveSource、摘要缓存、测试快路径 resolveTestDsn
|
|
96
|
-
adhoc.mjs 唯一执行引擎:一次性 dbhub 进程(描述符 {dsn,ro,ssh})、--dsn 或临时 TOML、stderr 清洗、
|
|
97
|
-
probeConnection、probeSshTunnel、diagnosticTail/DIAG_TAIL_MAX=600/failureLayerOf
|
|
98
|
-
collect.mjs 授权扫描项目配置文件(mise/.env)提取 DSN 候选,含 askUser 桥接
|
|
99
|
-
tools.mjs 4 个工具的定义与注册;configure 探连优先
|
|
100
|
-
policy.mjs 模型侧访问策略:唯一的 system prompt 段(order 8900)
|
|
101
|
-
client.js Web 半(手写 lazy-CJS bundle,无构建):settings.section + 侧边栏面板 + plugins.row.config(summary/page)
|
|
102
|
-
test/ node:test 单测(纯逻辑 + bridge/客户端契约 + 零知识泄漏门)
|
|
103
|
-
scripts/ desensitize.config.mjs(槽位表=单一事实源) / desensitize-check.mjs / docs-check.mjs / render-check.mjs / release-preflight.mjs
|
|
104
|
-
release-test/ 版本矩阵实测:matrix.json(唯一声明) / run.mjs / support-matrix.mjs / mise.toml
|
|
105
|
-
.github/ workflows/quality.yml(CI gate) + release-matrix.yml(定时 support:check)
|
|
106
|
-
doc/ REQUIREMENTS.md / ARCHITECTURE.md / DEVELOPMENT.md / CLIENT.md / RELEASE.md / DESENSITIZE.md / SUPPORT-MATRIX.md / images/
|
|
107
|
-
+ 本地文档(PLAN-*、implementation-notes、RELEASE-NOTES-*,均 gitignore 排除)
|
|
108
|
-
```
|
|
1
|
+
# 架构理由与踩坑记录(按需加载)
|
|
2
|
+
|
|
3
|
+
> AGENTS.md 只记**结论**;这里记**原因**——每条红线的由来、当时踩到的坑、以及"为什么不能那样改"的判断依据。
|
|
4
|
+
> 改红线或质疑红线前读对应章节。操作步骤见 [DEVELOPMENT.md](DEVELOPMENT.md) / [CLIENT.md](CLIENT.md) / [RELEASE.md](RELEASE.md)。
|
|
5
|
+
|
|
6
|
+
## 零知识边界
|
|
7
|
+
|
|
8
|
+
- 模型上下文就是泄露面:任何进 context 的字符串都会被摘要、日志和后续轮次复制。所以规则不是"少给"而是"永不给"——工具参数不接受 DSN(模型传 DSN 等于把密码写进对话),输出一律经 `connLabel` / `scrubSecrets`。
|
|
9
|
+
- dbhub 的启动横幅**只把密码掩成 `****`,用户名原样打印**;而连接测试的诊断文本正是这段输出。所以清洗必须折叠任意 URL 形态连接串的整个 userinfo(`://user:pass@`、`://user@` → `://****:****@`),只掩密码不够。
|
|
10
|
+
- 仓库被视为公开分发面:这些真实值曾随 npm 包与公开仓库分发过一次,公开即视为泄露、无法追回(详见 [DESENSITIZE.md](DESENSITIZE.md))。
|
|
11
|
+
|
|
12
|
+
## 一次性进程的理由
|
|
13
|
+
|
|
14
|
+
- 常驻 dbhub 服务器会带来一整套生命周期问题:端口/stdio 归属、配置变更重启、指纹与 mtime 监听、空闲回收、孤儿进程、多实例互杀——5.0 之前这些都真实出现过。最终选择"每次调用一条进程":天然并发隔离、一条进程挂只坏它自己、多实例无共享状态、没有常驻资源可泄漏。
|
|
15
|
+
- 代价是每次的启动延迟(spawn + initialize),换来的是可预测性。**不要以"优化启动"为名把常驻服务装回来。**
|
|
16
|
+
|
|
17
|
+
## 一次性TOML的由来
|
|
18
|
+
|
|
19
|
+
- `--dsn` 与 `--config` 互斥,而 dbhub 只有 TOML 方言能表达环境级只读(`[[tools]] readonly`)与 SSH 隧道(`ssh_*`)。所以只有"需要这两个特性"的环境走临时 TOML,其余**逐字节保持** `--dsn`(历史行为,密码在 argv)。
|
|
20
|
+
- 三个踩过的坑:
|
|
21
|
+
1. 秘密不能落文件 → 文件只写 `${DSH_DBHUB_SEC_*}` 占位符,真值走子进程 env。**dbhub 取不到变量时不报错**,会把 `${VAR}` 原样留在 DSN 里(于是连了一个字面量叫 `${VAR}` 的目标,或出现"神秘失败")——所以"文本引用的变量集合 == env 键集合"是硬断言。
|
|
22
|
+
2. `[[tools]]` 是**白名单不是补丁**:只要出现任意一条,dbhub 就不再自动挂 `execute_sql` + `search_objects`。只写一条 read_only 会让该环境的 `search_objects` 直接消失(`Tool search_objects not found`)。
|
|
23
|
+
3. source 级 `read_only` 写法会被**静默忽略**,比报错危险得多 → 绝不生成未实测的旧写法。1.4.0 是实测版本,`< 1.0.0` 明确拒绝。
|
|
24
|
+
- Windows 上 dbhub 会 watch 这个临时文件,删除可能 EBUSY → `finally` 尽力删 + boot 时 `sweepTempToml()` 清残留。
|
|
25
|
+
|
|
26
|
+
## 环境选项与执行预算
|
|
27
|
+
|
|
28
|
+
- 选项**没有版本号也没有迁移函数**是刻意的:`normalizeStore` 保留未知字段,给 env 条目加可选字段不需要迁移;旧文件读回来就是"无只读、无隧道"。别为了"规范"加迁移。
|
|
29
|
+
- `setWorkspaceEnv` 的 opts **缺省必须保留**既有选项:`dbhub_configure` 只更新 DSN 的场景下,若默认清空就会把用户的安全开关静默关掉——这是"只读/隧道莫名丢失"的根因。
|
|
30
|
+
- 安全不对称必须在**工具路径**拦截而不是 store:模型只能收窄不能放宽,但浏览器必须能自由关,否则用户永远关不掉自己开的只读。
|
|
31
|
+
- 预算与看门狗的三角关系:客户端看门狗必须**严格大于**宿主探测预算,否则客户端先放弃、界面报"测试超时",而宿主那条 dbhub 还在跑;宿主再加 +500ms 兜底,处理 spawn 本身永不 settle 的情况。三个数字必须一起改,`test/adhoc-toml.test.mjs` 会断言。
|
|
32
|
+
|
|
33
|
+
## envSlug事故
|
|
34
|
+
|
|
35
|
+
- 早期实现把非 ASCII 环境名统一 slug 化成一个值,`线上` 与 `测试` 拿到同一个 source 句柄 → 解析时**静默查错库**(模型以为在测环境,实际打线上)。现在:`default` 裸值、纯 ASCII 保留原 slug(既有 source 不变)、非 ASCII 取 `env-<shortHash>`。这是回归红线,不是风格问题。
|
|
36
|
+
|
|
37
|
+
## 状态通道与inject根因
|
|
38
|
+
|
|
39
|
+
> **为什么不能改回 inject(5.0 移植的根因)**:dsh 0.1.7 用 Config 表单取代了 settings 命名空间,0.2.x 又删掉浏览器侧 `settingsScope`。旧客户端 `inject: ['slots', 'settingsScope']` 在 0.2.x 上永远等不到服务 → 整个浏览器半区 pending、**无任何报错**,表现就是「配置页 / 侧边栏 / 插件页卡片静默消失」。所以这一层必须做成运行时探测 + 双实现(`legacyFace` / `httpFace`),而不是声明式依赖。
|
|
40
|
+
|
|
41
|
+
- 第二个静默失败面:**注入回调是独立作用域**。在回调里引用 `apply()` 的局部变量会 ReferenceError,而错误发生在发布链路上——页面只剩静态值、无任何可见报错。需要的服务必须 `ctx.get(...)` 就地获取。
|
|
42
|
+
- 两条通道都发布同一个 view 是刻意的:客户端只写一份渲染逻辑,通道差异收敛在 `legacyFace` / `httpFace` 两个适配器里。
|
|
43
|
+
|
|
44
|
+
## 诊断为什么取尾部
|
|
45
|
+
|
|
46
|
+
- dbhub 失败时致命行在输出末尾(前面是启动横幅与重试日志),取开头只能得到 `connecting…`。所以 `diagnosticTail` 取尾部 600 字符、丢 `at …` 栈帧与折行残片、按行边界截断(否则会截出半行 JSON/半个错误)。
|
|
47
|
+
- 分层前缀的意义:SSH 层与数据库层的可诊断性不同——只有**独立的 SSH 探测**能证明"隧道起来了";反向不成立,所以 db 层失败的措辞必须谨慎,不能写成"隧道断了"。
|
|
48
|
+
|
|
49
|
+
## 即时回推与扫描单飞
|
|
50
|
+
|
|
51
|
+
- `collectSources` 每次都要重扫全部工作区(无持久化 `default` 的工作区各起一次 `mise env`),一次写入原本要等 1–2s。更糟的是**并发扫描互相覆盖**:探针回执触发的扫描可能在写入前开始、写入后结束,把新行刷掉——表现就是"立刻出现又消失"。
|
|
52
|
+
- 结论:写入路径返回本次补丁并 `publishFast` 同步镜像;探针只镜像不扫描;扫描单飞并重放扫描期间到达的补丁;`rename` 补丁必须幂等(否则重放会丢行)。一次点击 = 1 次扫描 + 2 次纯内存镜像。
|
|
53
|
+
|
|
54
|
+
## 自动发现的代价与unref教训
|
|
55
|
+
|
|
56
|
+
- `mise env` 探测没有总超时会出事:曾有一个不退出的 `mise env` 让整轮扫描永不返回,而 `dbhub_list_sources` 正走这条同步路径,模型侧看到 `Interrupted`。所以每次探测必须有总超时(`graceMs` 只是 SIGTERM→SIGKILL 宽限,**不是超时**),结果按 TTL 缓存,工作区之间并发。
|
|
57
|
+
- **预算计时器绝不 `unref()`**:unref 计时器在"它是唯一待办"时不触发——事件循环排空、进程先退,超时形同虚设。该缺陷**只在 Linux CI 暴露**(整文件 cancelled),本地 Windows 会侥幸通过:本地全绿不能当发布依据。
|
|
58
|
+
|
|
59
|
+
## 布局取舍
|
|
60
|
+
|
|
61
|
+
- 行内只放图标、来源不做徽章、库名不截断,三条来自同一约束:每个连接只占一行,宽度是稀缺资源。文字按钮吃掉半行;"来源"徽章几乎每行相同却最抢眼;库名被截断是最高频的抱怨。
|
|
62
|
+
- 三面布局的两个阈值(700 / 720):实测出现过 710px 视口既不算手机(>= 700)也不够宽(< 720)的**空档**,布局判定落空。两阈值之间的窄桌面列必须走紧凑设置条,不能退回竖排。
|
|
63
|
+
- 现代面与 legacy 面必须能同时存在:`render-check` 的旧断言写死了 0.1.x 的 DOM,新界面只能挂在 `dshModern` 下;否则一次"统一界面"就会让旧线渲染异常。
|
|
64
|
+
|
|
65
|
+
## 覆盖门为什么先问
|
|
66
|
+
|
|
67
|
+
- `setWorkspaceEnv` 是**无条件写入**:新增或改名落到已存在的环境会静默替换原连接,而客户端无法回滚。所以写入前必须比较,命中就弹确认卡;比较前必须过 `normalizeEnvForCompare`(镜像宿主 `normalizeEnvName`),否则「prod 」与「prod」这种同键异写会漏判。取消 → 零 op 且保留表单内容。
|
|
68
|
+
|
|
69
|
+
## 脱敏的不可逆性
|
|
70
|
+
|
|
71
|
+
- 详见 [DESENSITIZE.md](DESENSITIZE.md)。一句话:真实值曾随 npm 包与公开仓库分发过一次,公开即无法追回;所以门禁是硬门,示例主机只用回环/RFC 5737,示例密码只用 `CHANGE_ME`,任何人不得把本机绝对路径写进受版本控制的文件。
|
|
72
|
+
|
|
73
|
+
## 版本门与更新检查
|
|
74
|
+
|
|
75
|
+
- `dshModern` 门(>= 0.2.1,含预发布)不是"只有新版本测过",而是"新界面依赖的 slot/DOM 能力在这个版本才稳定"。运行版本解析不到时回退为 desktop=true / web=false——桌面端总是新版、旧 web 线保持 legacy,比猜一个值安全。
|
|
76
|
+
- 更新检查**绝不抛错**:它只是页头一行提示,失败必须只体现为 `error` 字段,不得影响插件任何功能。
|
|
77
|
+
|
|
78
|
+
## 模块地图
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
lib/
|
|
82
|
+
index.mjs 入口:name/inject/apply、Config(volatile)、发布核心 createHostCore(walk/row-mirror/runOp/view)、
|
|
83
|
+
双通道接线(0.1.x settings 命名空间 / connection.fetch HTTP 桥)、settings 表单策略与选项写回、configOp 命令处理
|
|
84
|
+
meta.mjs 插件版本(读 package.json)、DSH 运行时版本探测、semver 比较、dshModern 门
|
|
85
|
+
update.mjs 最新版本检查:npm registry 为主 → GitHub Releases 兜底;内存节流、force、绝不抛错
|
|
86
|
+
bridge.mjs /api/dsh-dbhub-live 四条 exact Fetch 路由的请求/响应编解码(纯函数)
|
|
87
|
+
config.mjs 路径与原子持久化(store/runtime/prefs)、DSN 解析、工作区发现、环境选项(readOnly/ssh 归一化、默认密钥)、
|
|
88
|
+
零知识助手(describeConn/connLabel/scrubSecrets)、configure 决策纯函数(decideConfigureStep/argsChangedEndpoint/emptyUserDenied)
|
|
89
|
+
i18n.mjs 宿主侧文案表与插值
|
|
90
|
+
state.mjs 运行时状态机 enabled/phase/toolCount/lastError/mode + 订阅发布
|
|
91
|
+
options.mjs 部署开关:updateIntervalDays / showSidebarEntry / announceAccessPolicy;层叠 Config > prefs.json > 0.1.x 设置文档 > 环境变量 > 内置默认
|
|
92
|
+
runtime.mjs dbhub 可执行文件发现、按需自动安装、定期自动更新
|
|
93
|
+
capability.mjs 上游能力探测(文件级,不 spawn):[[tools]] readonly 与 ssh_* 需 1.4.0+
|
|
94
|
+
toml.mjs 一次性 TOML 生成器 buildDbhubToml/writeTempToml/removeTempToml/sweepTempToml(文本绝不含秘密)
|
|
95
|
+
mcp.mjs MCP JSON-RPC 客户端、工具注册表、来源发现 collectSources/resolveSource、摘要缓存、测试快路径 resolveTestDsn
|
|
96
|
+
adhoc.mjs 唯一执行引擎:一次性 dbhub 进程(描述符 {dsn,ro,ssh})、--dsn 或临时 TOML、stderr 清洗、
|
|
97
|
+
probeConnection、probeSshTunnel、diagnosticTail/DIAG_TAIL_MAX=600/failureLayerOf
|
|
98
|
+
collect.mjs 授权扫描项目配置文件(mise/.env)提取 DSN 候选,含 askUser 桥接
|
|
99
|
+
tools.mjs 4 个工具的定义与注册;configure 探连优先
|
|
100
|
+
policy.mjs 模型侧访问策略:唯一的 system prompt 段(order 8900)
|
|
101
|
+
client.js Web 半(手写 lazy-CJS bundle,无构建):settings.section + 侧边栏面板 + plugins.row.config(summary/page)
|
|
102
|
+
test/ node:test 单测(纯逻辑 + bridge/客户端契约 + 零知识泄漏门)
|
|
103
|
+
scripts/ desensitize.config.mjs(槽位表=单一事实源) / desensitize-check.mjs / docs-check.mjs / render-check.mjs / release-preflight.mjs
|
|
104
|
+
release-test/ 版本矩阵实测:matrix.json(唯一声明) / run.mjs / support-matrix.mjs / mise.toml
|
|
105
|
+
.github/ workflows/quality.yml(CI gate) + release-matrix.yml(定时 support:check)
|
|
106
|
+
doc/ REQUIREMENTS.md / ARCHITECTURE.md / DEVELOPMENT.md / CLIENT.md / RELEASE.md / DESENSITIZE.md / SUPPORT-MATRIX.md / images/
|
|
107
|
+
+ 本地文档(PLAN-*、implementation-notes、RELEASE-NOTES-*,均 gitignore 排除)
|
|
108
|
+
```
|
package/doc/CLIENT.md
CHANGED
|
@@ -1,124 +1,124 @@
|
|
|
1
|
-
# 客户端半(lib/client.js)与布局契约
|
|
2
|
-
|
|
3
|
-
> AGENTS.md 的配套文档:改 `lib/client.js`、改配置页布局/交互/i18n 时读。
|
|
4
|
-
> 架构红线(注入、零知识、状态通道)见 [AGENTS.md](../AGENTS.md);**这些布局取舍的依据**(为什么只用图标、为什么“来源”不做徽章、为什么库名不截断、为什么是 700/720 双阈值)见 [ARCHITECTURE.md](ARCHITECTURE.md#布局取舍)。
|
|
5
|
-
|
|
6
|
-
## bundle 契约(手写 lazy-CJS,无构建步骤)
|
|
7
|
-
|
|
8
|
-
`lib/client.js` 由 DSH client 模块系统按 `dsh.client` 声明 + `exports["./client"]` 直接服务,**没有构建步骤**。改完跑 `node test/client-format.test.mjs` 守护:
|
|
9
|
-
|
|
10
|
-
- 必须以 `window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => { … } })` 注册;
|
|
11
|
-
- `id` 必须等于 Loader entry 名(`dsh-dbhub-live`,见 `cordis.patch.yml` 的 `name`),**不是行 id**;
|
|
12
|
-
- factory 只允许 `require("react")` 等 baseline 模块;不 import 其他插件的值(bundle-purity);
|
|
13
|
-
- 导出 `name` / `inject` / `apply`,结尾 `return module.exports`;
|
|
14
|
-
- 若本包脱离仓库发布 npm,`dsh.client` 与 `exports["./client"]` 必须保留,否则 client-modules 扫描会启动报错。
|
|
15
|
-
|
|
16
|
-
## 三个入口(都要在,且不得自锁)
|
|
17
|
-
|
|
18
|
-
| slot | 说明 |
|
|
19
|
-
| --- | --- |
|
|
20
|
-
| `settings.section`(id `dbhub`,order 40) | **主入口,始终注册**。设置面板是核心 shell,任何部署都在,用户也在那里找连接管理 |
|
|
21
|
-
| `sidebar.panellist`(id `dbhub`)+ `main`(key `dbhub`) | 侧边栏快捷入口,**仅 `enabled && showSidebarEntry` 同时成立时显示** |
|
|
22
|
-
| `plugins.row.config`(key `dsh-dbhub-live#dbhub-live`) | 组件同时服务 `props.view === 'summary'`(一行摘要)与 `'page'`(完整表单,自画标题/图标/面包屑);仅在官方插件页浏览器半区活跃时存在,**不能当唯一入口** |
|
|
23
|
-
|
|
24
|
-
- **「隐藏入口」≠「禁用插件」**:关开关只是不显示侧边栏入口,工具照常可用;禁用则一律不贡献入口。
|
|
25
|
-
- 两种状态下 `settings.section` 与 `plugins.row.config` 都**始终注册**,否则用户再也回不来。
|
|
26
|
-
- 侧边栏由 `syncSidebar()` 挂 `face.subscribe` 按 view 即时 register/dispose;判定 `sidebarWanted()` = `view.enabled !== false && view.showSidebarEntry !== false`。
|
|
27
|
-
- 注册不存在的 slot 会被静默忽略(「配置页消失」就是这么来的),别改回已删除的 slot。
|
|
28
|
-
|
|
29
|
-
## host face:探测与写回(别退回 inject)
|
|
30
|
-
|
|
31
|
-
`export const inject` **只能有 `["slots"]`**。`legacyFace(scope)` 与 `httpFace()` 实现同一接口(`getSnapshot` / `subscribe` / `setEnabled` / `saveConfig` / `configOp`,快照 `{ status, value }`),`ctx.get('settingsScope')` 有则优先 legacy。
|
|
32
|
-
|
|
33
|
-
- HTTP face 四个入口:`GET /state`、`POST /enabled`、`POST /options`、`POST /op`;写完用响应里的 view 直接刷新快照(不等轮询);`configOp` 的 `{op: …}` 直接 POST。
|
|
34
|
-
- **选项写入优先走官方 settings 表单**:Host 发布 `entryId`,客户端有 `ctx.configForms` 且 entryId 非空时 `configForms.get(entryId).set(field, value)`,失败/解析不到才回退 `POST /options`(0.1.x 与无 settings 客户端走这条)。
|
|
35
|
-
- **HTTP 通道轮询是「按订阅」的**:只有渲染中页面用 `props.subscribe(fn, { live: true })` 起 1.5s 轮询(`document.hidden` 跳过);插件作用域订阅(`face.subscribe(syncSidebar)`)只做一次性拉取。**空闲 GUI 不得持续轮询**,残留 interval 会让 `check:ui` 挂住不退出。
|
|
36
|
-
- 组件不得接触 `ctx`:数据/回调一律通过注册时的 `inject: () => face` 传入 props。
|
|
37
|
-
|
|
38
|
-
## apply() 必须能被重复执行
|
|
39
|
-
|
|
40
|
-
客户端 HMR 会重跑整个 bundle。`localeSvc.register(NS, …)` **不幂等**(同 namespace+locale 再注册抛 `locale namespace … already has locale zh`,整个浏览器半区同步失败)。所以:
|
|
41
|
-
|
|
42
|
-
1. 捕获 disposer 用 `ctx.effect(() => dispose, …)` 挂在插件作用域;
|
|
43
|
-
2. 注册失败**降级到 `localTranslator(localeSvc)`**(读本 bundle 字典 + `localeSvc.getLocale().active`),绝不打断整个插件半;
|
|
44
|
-
3. 新增/改动 `apply()` 注册都要问一句:**reload 第二次还会成功吗?**
|
|
45
|
-
|
|
46
|
-
样式在 `apply()` 里由 `ensureStyle()` 注入一份作用域样式表(`#dsh-dbhub-live-style`,类名前缀 `dbh-`)——悬浮/焦点是伪类,内联样式表达不了;**不要把样式退回成一堆内联对象**。
|
|
47
|
-
|
|
48
|
-
## 主题适配(红线)
|
|
49
|
-
|
|
50
|
-
- 样式表**只允许 `--dsw-alias-*` token**,兜底值必须中性且半透明(`rgba(127,127,127,.3)`、`transparent`);
|
|
51
|
-
- **禁止任何不透明色字面量**(`#ffffff` / `#f6f8fa` / `#1f2328` / 品牌色…)。用户会装主题/皮肤插件(本机就有 skin-center,背景可能是半透明的),写死白底或固定浅灰字会直接看不清;`test/client-format.test.mjs` 有 "tokens only" 断言门禁;
|
|
52
|
-
- 只在动态取值时用内联样式;对比度:正文及以上用 `label-primary` / `label-secondary`,`label-dimmed` 只留给极次要说明。
|
|
53
|
-
|
|
54
|
-
## 布局契约
|
|
55
|
-
|
|
56
|
-
### 整体结构
|
|
57
|
-
|
|
58
|
-
- **按 `path` 分组**(`groupRows`;标题不是身份——同名工作区是不同目标,同名组头用路径消歧)。
|
|
59
|
-
- `layoutModeOf` **无阈值**判定:所有工作区都只有 1 个环境 → **平铺网格**(`dbh-tiles`,`minmax(280px,1fr)` + 内容列 `max-width:960px`,自然封顶 3 列);只要有任一工作区是多环境 → **分组折叠**(`dbh-group` + 可点击组头 `aria-expanded`)。判定必须是纯函数,改判定先改 `scripts/render-check.mjs` 的断言。
|
|
60
|
-
- **层级**:工作区分组是 level-1(边框卡片 + 极淡阴影),环境行是 level-2(缩进 + 左侧引导竖线 `dbh-groupbody{border-left}`);两层绝不能长得像同一层。
|
|
61
|
-
- **默认展开**:单个工作区自动展开;多工作区时展开「最近活跃工作区」(由 `useWorkspaces` 的 `WorkspaceSnapshot.updatedAt` 推出;设置面板是全局的,拿不到当前会话选中态,**别猜**);取不到快照全折叠。另有「全部展开/折叠」总控。展开态 / 添加面板 / 设置条存模块级 `uiMemory`(同页面会话内跨重挂载保留,**不持久化**——视图状态不是配置)。
|
|
62
|
-
|
|
63
|
-
### 每个连接只占一行
|
|
64
|
-
|
|
65
|
-
```
|
|
66
|
-
环境 chip(窄、淡) · 连接元数据 · source chip · [▶][✏][🗑]
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
三条硬规则:
|
|
70
|
-
|
|
71
|
-
1. 行内操作**只允许图标**(`primitives` 的 `IconPlayOutline16` / `IconEditOutline16` / `IconCloseOutline16` / `IconTrashOutline16`,无边框 24px,label 走 `title`/`aria-label`)——文字按钮一放上来就吃掉半行。
|
|
72
|
-
2. `source` 做成**可复制 chip**(`sourceChip`):虚线边框 + 复制图标 + **短句柄**(`shortHandle`:≤14 字全显,否则保留"工作区哈希 + 环境段"两段),让人一眼看出"这是 id、点了复制",而不是"复制整行连接串";全文与用途进 tooltip(`ws.srcHint`),复制走 `primitives.writeClipboard`(自带 execCommand 兜底)。
|
|
73
|
-
3. 连接元数据用 `connLabelView` 拆成 `前缀(type://host:port/) + 库名`,**前缀可截断、库名绝不截断**(`dbh-conn-pre` 省略号 / `dbh-conn-db` 不收缩)——"窄行里看不到库名"是最常见的抱怨。
|
|
74
|
-
|
|
75
|
-
- 行内只允许出现 `w.conn`(元数据);`srcId` 只是句柄,**绝不回显 DSN**。
|
|
76
|
-
- 🔒 前缀:**现代面不加**(会被误读成"需要密码",且与只读 chip 的锁冲突);legacy 面保留原标记。
|
|
77
|
-
- 「来源」(手填 / 扫描 / 复制 / 自动发现)**不得做成行内徽章**——它几乎每行都一样却最抢眼;改为环境名 chip 的 tooltip(`originTitle`),只有"自动发现且未持久化"这一例外在 tooltip 里显式点名。
|
|
78
|
-
- 改名走 `configOp({op:'rename'})`;改名 + 改连接走**一条** `{op:'add', renameFrom}`(`configOp` 在 legacy 通道是单字段,连续两次写会互相覆盖)。
|
|
79
|
-
|
|
80
|
-
### 顶部与添加
|
|
81
|
-
|
|
82
|
-
- **legacy 面(0.1.x)顶部只留一行**:状态徽章 + 工具数 + 设置条摘要 + `[⚙ 设置]` + `[启用/禁用]`;自动更新间隔与「在侧边栏显示入口」复选框一起收进默认收起的设置条。
|
|
83
|
-
- **现代面(≥0.2.1)按「三面布局」**:标题 + 版本 + 升级链接;状态行只留状态与连接数;设置用 DSH 行样式;长说明进折叠区。
|
|
84
|
-
- **添加连接默认收起**:「+ 添加连接」(全局;工作区用 `<datalist id="dbh-ws-list">` 给已有工作区)/ 分组头「+ 环境」(工作区自动带入,少一个字段)。保存仍走原有「试连 → 失败才确认」门禁。
|
|
85
|
-
|
|
86
|
-
## 环境选项的落点(只读 / SSH)
|
|
87
|
-
|
|
88
|
-
同一组选项在**两个面**上呈现不同,按 `view.dshModern` 分叉——**这是 legacy 与现代面的分工,不是新旧两套规则**:
|
|
89
|
-
|
|
90
|
-
| 元素 | 现代面(`dshModern === true`) | legacy 面(`dshModern === false`) |
|
|
91
|
-
| --- | --- | --- |
|
|
92
|
-
| 只读标记 | 行内 `.dbh-rochip`,弹窗/宽屏/手机三种布局都出现;点击发 `{op:'options', readOnly}`(无 DSN);必须 **开锁+「可写」/ 闭锁+「只读」**(同一 16px 线性图标风格),否则两个灰色「只读」会被误读成都是只读 | 环境 chip 内 `span.dbh-ro`,说明并入 chip `title` |
|
|
93
|
-
| 设置控件 | 开关必须用 `primitives.Switch`(缺失时才回退 `.dbh-fbswitch`) | `.dbh-checkbox` 复选框 |
|
|
94
|
-
| 连接串 🔒 | 不加 | 保留 |
|
|
95
|
-
|
|
96
|
-
- SSH 只影响路由,**不做徽章**,只进环境 chip 的 tooltip(`ro.sshTitle`)。
|
|
97
|
-
- 编辑表单里的只读勾选保留(两面都有);`.dbh-checkbox` **不用 `.dbh-input`、不包进 `.dbh-field`**,否则 `render-check` 的 `dbh-field === 3` 与 `typeWithin('dbh-addpanel',0..2)` 索引会变。
|
|
98
|
-
- SSH 高级区 `.dbh-adv` 折叠面板**必须排在 DSN 输入之后**,折叠态放 `uiMemory.advOpen`;区内「测试该配置」旁是**「测试 SSH 隧道」**(独立按钮/回执,只验 SSH 层)。
|
|
99
|
-
- 秘密输入 `type="password"`、占位符写明「留空 = 保持原值」,**编辑态绝不回显已存密码/口令/密钥路径**(镜像行只给 `authKind` / `hasPassword` / `hasPassphrase` / `keyReady` 布尔)。
|
|
100
|
-
- 写入载荷统一走 `connDraftOf(form)` / `withConnDraft(op, form)`:只读开关与隧道随 `add` / `test` / `options` 三种 op 一起发出,**绝不单独再发一条**。表单对象在产生 op 时会带 `_draft` 指回原始草稿,别绕过这两个函数手拼 `readOnly` / `ssh`。
|
|
101
|
-
|
|
102
|
-
## 三面布局契约(5.2,`dshModern` 门)
|
|
103
|
-
|
|
104
|
-
同一个组件的**三个面由 slot + 实测宽度决定**:`settings.section`(弹窗,窄列)、主面板与 `plugins.row.config` 的 `page`(宽屏整页)、`< 700px` 手机小屏(堆叠紧凑、触摸尺寸;`isWide` 阈值为 720)。
|
|
105
|
-
|
|
106
|
-
- **新界面必须挂在 `view.dshModern` 下**;`dshModern === false`(DSH 0.1.x)必须渲染**与旧版逐字兼容的 DOM**(含旧页头 `4 个工具(固定)`、⚙ 设置条、`dbh-checkbox`),否则 `scripts/render-check.mjs` 既有断言会红。旧断言不动,新断言由 harness 另行扩展。
|
|
107
|
-
- 现代面硬约定:
|
|
108
|
-
1. 页头 = 标题 + 灰色版本号 + 一行「↑ 可升级到 x」文字链接(点击才展开中性说明面板;**禁止红底/红框/error 色**)+ 图标式 ↻;
|
|
109
|
-
2. 状态行只显示「运行中/已停用 · N 个连接」;工具数 / 一次性模式 / dbhub 版本进页面底部「ⓘ 说明与技术详情」折叠区(默认收起);
|
|
110
|
-
3. 设置 = **所有非弹窗、非手机页面(宽屏页 / 插件页 / 窄桌面列)一律一行紧凑设置条**(窄列自动加 `.dbh-barnarrow` 隐藏标题与状态词、收窄间距,**不得退回竖排设置行**;弹窗与手机才用竖排)。宽屏条 `.dbh-settingsbar`:标签 + 状态词 + `primitives.Switch` + 更新输入/保存同排,开关 `scale(.85)`;弹窗/手机为紧凑设置行(13px 标题、8px padding、开关 `scale(.8–.9)`)——**字号与控件刻意弱化,连接列表才是主体**。状态词统一为 `启用/停用`、`显示/隐藏`、`开启/关闭`;更新间隔输入 + 天 + 保存必须在同一行不换行;
|
|
111
|
-
4. 行内只读 chip 在三种布局都出现(见上表);
|
|
112
|
-
5. 弹窗面的设置收进状态行「设置」折叠,宽屏面直接展开。
|
|
113
|
-
|
|
114
|
-
## 交互细节(每条都对应一段回归门)
|
|
115
|
-
|
|
116
|
-
- **复制必须给出干净字符串**:连接串拆成两个 flex item 后,浏览器复制序列化会在块级子元素间插分隔符(粘进 DSN 就成 `…3306/ app`)。因此 `connLabelView` 的元素自己接管 `onCopy`(`e.clipboardData.setData('text/plain', w.conn)` + `preventDefault()`)——挂在**连接串元素**而非整行,复制环境名不受影响。`w.conn` 是元数据,不涉凭据。
|
|
117
|
-
- **测试中显示已耗时**:`startTest` 记 `startedAt`,`testing` 态渲染 `test.elapsed`(`测试中… Xs`);1s 计时器**只在有行处于 testing 时存在**(effect 依赖 `anyTesting`,cleanup 清掉)。看门狗按是否隧道分档(30s / 60s),必须高于宿主探测预算(25s / 45s)。
|
|
118
|
-
- **等待态不给二次提交**:`pendingPretest` 期间保存/添加按钮 `disabled`,避免重复派发同一测试。
|
|
119
|
-
- 探测要几秒(spawn dbhub),但**写入结果必须立刻可见**——靠宿主 `publishFast` 即时回推,不要改回「等整轮扫描」。
|
|
120
|
-
|
|
121
|
-
## 验收
|
|
122
|
-
|
|
123
|
-
- 布局/交互改动的验收 = `npm run check:ui`(离屏渲染 + 点击交互;分段 `[1]`…`[30]`,涵盖模式判定、默认展开、全部展开/折叠、添加面板、复制、i18n 覆盖率、热重载幂等、覆盖门、HTTP 桥、只读/SSH 载荷、现代三面、版本门)全绿 + `node test/client-format.test.mjs`。
|
|
124
|
-
- 已知易错门:`[14]` 重复 locale 注册、`[17]` 复制字符串、`[18]` 无 settingsScope 走桥接 + `configForms` 优先与回退 + 轮询可回收、`[19]` 启用∧开关 / 禁用一律隐藏 / 设置页始终在、`[20]`/`[21]`/`[23]` 只读与 SSH 载荷、`[22]` 残留 interval、`[28]` 手机布局、`[29]` 0.1.x 渲染旧页、`[30]` 窄桌面窗口保持横向设置条。
|
|
1
|
+
# 客户端半(lib/client.js)与布局契约
|
|
2
|
+
|
|
3
|
+
> AGENTS.md 的配套文档:改 `lib/client.js`、改配置页布局/交互/i18n 时读。
|
|
4
|
+
> 架构红线(注入、零知识、状态通道)见 [AGENTS.md](../AGENTS.md);**这些布局取舍的依据**(为什么只用图标、为什么“来源”不做徽章、为什么库名不截断、为什么是 700/720 双阈值)见 [ARCHITECTURE.md](ARCHITECTURE.md#布局取舍)。
|
|
5
|
+
|
|
6
|
+
## bundle 契约(手写 lazy-CJS,无构建步骤)
|
|
7
|
+
|
|
8
|
+
`lib/client.js` 由 DSH client 模块系统按 `dsh.client` 声明 + `exports["./client"]` 直接服务,**没有构建步骤**。改完跑 `node test/client-format.test.mjs` 守护:
|
|
9
|
+
|
|
10
|
+
- 必须以 `window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => { … } })` 注册;
|
|
11
|
+
- `id` 必须等于 Loader entry 名(`dsh-dbhub-live`,见 `cordis.patch.yml` 的 `name`),**不是行 id**;
|
|
12
|
+
- factory 只允许 `require("react")` 等 baseline 模块;不 import 其他插件的值(bundle-purity);
|
|
13
|
+
- 导出 `name` / `inject` / `apply`,结尾 `return module.exports`;
|
|
14
|
+
- 若本包脱离仓库发布 npm,`dsh.client` 与 `exports["./client"]` 必须保留,否则 client-modules 扫描会启动报错。
|
|
15
|
+
|
|
16
|
+
## 三个入口(都要在,且不得自锁)
|
|
17
|
+
|
|
18
|
+
| slot | 说明 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `settings.section`(id `dbhub`,order 40) | **主入口,始终注册**。设置面板是核心 shell,任何部署都在,用户也在那里找连接管理 |
|
|
21
|
+
| `sidebar.panellist`(id `dbhub`)+ `main`(key `dbhub`) | 侧边栏快捷入口,**仅 `enabled && showSidebarEntry` 同时成立时显示** |
|
|
22
|
+
| `plugins.row.config`(key `dsh-dbhub-live#dbhub-live`) | 组件同时服务 `props.view === 'summary'`(一行摘要)与 `'page'`(完整表单,自画标题/图标/面包屑);仅在官方插件页浏览器半区活跃时存在,**不能当唯一入口** |
|
|
23
|
+
|
|
24
|
+
- **「隐藏入口」≠「禁用插件」**:关开关只是不显示侧边栏入口,工具照常可用;禁用则一律不贡献入口。
|
|
25
|
+
- 两种状态下 `settings.section` 与 `plugins.row.config` 都**始终注册**,否则用户再也回不来。
|
|
26
|
+
- 侧边栏由 `syncSidebar()` 挂 `face.subscribe` 按 view 即时 register/dispose;判定 `sidebarWanted()` = `view.enabled !== false && view.showSidebarEntry !== false`。
|
|
27
|
+
- 注册不存在的 slot 会被静默忽略(「配置页消失」就是这么来的),别改回已删除的 slot。
|
|
28
|
+
|
|
29
|
+
## host face:探测与写回(别退回 inject)
|
|
30
|
+
|
|
31
|
+
`export const inject` **只能有 `["slots"]`**。`legacyFace(scope)` 与 `httpFace()` 实现同一接口(`getSnapshot` / `subscribe` / `setEnabled` / `saveConfig` / `configOp`,快照 `{ status, value }`),`ctx.get('settingsScope')` 有则优先 legacy。
|
|
32
|
+
|
|
33
|
+
- HTTP face 四个入口:`GET /state`、`POST /enabled`、`POST /options`、`POST /op`;写完用响应里的 view 直接刷新快照(不等轮询);`configOp` 的 `{op: …}` 直接 POST。
|
|
34
|
+
- **选项写入优先走官方 settings 表单**:Host 发布 `entryId`,客户端有 `ctx.configForms` 且 entryId 非空时 `configForms.get(entryId).set(field, value)`,失败/解析不到才回退 `POST /options`(0.1.x 与无 settings 客户端走这条)。
|
|
35
|
+
- **HTTP 通道轮询是「按订阅」的**:只有渲染中页面用 `props.subscribe(fn, { live: true })` 起 1.5s 轮询(`document.hidden` 跳过);插件作用域订阅(`face.subscribe(syncSidebar)`)只做一次性拉取。**空闲 GUI 不得持续轮询**,残留 interval 会让 `check:ui` 挂住不退出。
|
|
36
|
+
- 组件不得接触 `ctx`:数据/回调一律通过注册时的 `inject: () => face` 传入 props。
|
|
37
|
+
|
|
38
|
+
## apply() 必须能被重复执行
|
|
39
|
+
|
|
40
|
+
客户端 HMR 会重跑整个 bundle。`localeSvc.register(NS, …)` **不幂等**(同 namespace+locale 再注册抛 `locale namespace … already has locale zh`,整个浏览器半区同步失败)。所以:
|
|
41
|
+
|
|
42
|
+
1. 捕获 disposer 用 `ctx.effect(() => dispose, …)` 挂在插件作用域;
|
|
43
|
+
2. 注册失败**降级到 `localTranslator(localeSvc)`**(读本 bundle 字典 + `localeSvc.getLocale().active`),绝不打断整个插件半;
|
|
44
|
+
3. 新增/改动 `apply()` 注册都要问一句:**reload 第二次还会成功吗?**
|
|
45
|
+
|
|
46
|
+
样式在 `apply()` 里由 `ensureStyle()` 注入一份作用域样式表(`#dsh-dbhub-live-style`,类名前缀 `dbh-`)——悬浮/焦点是伪类,内联样式表达不了;**不要把样式退回成一堆内联对象**。
|
|
47
|
+
|
|
48
|
+
## 主题适配(红线)
|
|
49
|
+
|
|
50
|
+
- 样式表**只允许 `--dsw-alias-*` token**,兜底值必须中性且半透明(`rgba(127,127,127,.3)`、`transparent`);
|
|
51
|
+
- **禁止任何不透明色字面量**(`#ffffff` / `#f6f8fa` / `#1f2328` / 品牌色…)。用户会装主题/皮肤插件(本机就有 skin-center,背景可能是半透明的),写死白底或固定浅灰字会直接看不清;`test/client-format.test.mjs` 有 "tokens only" 断言门禁;
|
|
52
|
+
- 只在动态取值时用内联样式;对比度:正文及以上用 `label-primary` / `label-secondary`,`label-dimmed` 只留给极次要说明。
|
|
53
|
+
|
|
54
|
+
## 布局契约
|
|
55
|
+
|
|
56
|
+
### 整体结构
|
|
57
|
+
|
|
58
|
+
- **按 `path` 分组**(`groupRows`;标题不是身份——同名工作区是不同目标,同名组头用路径消歧)。
|
|
59
|
+
- `layoutModeOf` **无阈值**判定:所有工作区都只有 1 个环境 → **平铺网格**(`dbh-tiles`,`minmax(280px,1fr)` + 内容列 `max-width:960px`,自然封顶 3 列);只要有任一工作区是多环境 → **分组折叠**(`dbh-group` + 可点击组头 `aria-expanded`)。判定必须是纯函数,改判定先改 `scripts/render-check.mjs` 的断言。
|
|
60
|
+
- **层级**:工作区分组是 level-1(边框卡片 + 极淡阴影),环境行是 level-2(缩进 + 左侧引导竖线 `dbh-groupbody{border-left}`);两层绝不能长得像同一层。
|
|
61
|
+
- **默认展开**:单个工作区自动展开;多工作区时展开「最近活跃工作区」(由 `useWorkspaces` 的 `WorkspaceSnapshot.updatedAt` 推出;设置面板是全局的,拿不到当前会话选中态,**别猜**);取不到快照全折叠。另有「全部展开/折叠」总控。展开态 / 添加面板 / 设置条存模块级 `uiMemory`(同页面会话内跨重挂载保留,**不持久化**——视图状态不是配置)。
|
|
62
|
+
|
|
63
|
+
### 每个连接只占一行
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
环境 chip(窄、淡) · 连接元数据 · source chip · [▶][✏][🗑]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
三条硬规则:
|
|
70
|
+
|
|
71
|
+
1. 行内操作**只允许图标**(`primitives` 的 `IconPlayOutline16` / `IconEditOutline16` / `IconCloseOutline16` / `IconTrashOutline16`,无边框 24px,label 走 `title`/`aria-label`)——文字按钮一放上来就吃掉半行。
|
|
72
|
+
2. `source` 做成**可复制 chip**(`sourceChip`):虚线边框 + 复制图标 + **短句柄**(`shortHandle`:≤14 字全显,否则保留"工作区哈希 + 环境段"两段),让人一眼看出"这是 id、点了复制",而不是"复制整行连接串";全文与用途进 tooltip(`ws.srcHint`),复制走 `primitives.writeClipboard`(自带 execCommand 兜底)。
|
|
73
|
+
3. 连接元数据用 `connLabelView` 拆成 `前缀(type://host:port/) + 库名`,**前缀可截断、库名绝不截断**(`dbh-conn-pre` 省略号 / `dbh-conn-db` 不收缩)——"窄行里看不到库名"是最常见的抱怨。
|
|
74
|
+
|
|
75
|
+
- 行内只允许出现 `w.conn`(元数据);`srcId` 只是句柄,**绝不回显 DSN**。
|
|
76
|
+
- 🔒 前缀:**现代面不加**(会被误读成"需要密码",且与只读 chip 的锁冲突);legacy 面保留原标记。
|
|
77
|
+
- 「来源」(手填 / 扫描 / 复制 / 自动发现)**不得做成行内徽章**——它几乎每行都一样却最抢眼;改为环境名 chip 的 tooltip(`originTitle`),只有"自动发现且未持久化"这一例外在 tooltip 里显式点名。
|
|
78
|
+
- 改名走 `configOp({op:'rename'})`;改名 + 改连接走**一条** `{op:'add', renameFrom}`(`configOp` 在 legacy 通道是单字段,连续两次写会互相覆盖)。
|
|
79
|
+
|
|
80
|
+
### 顶部与添加
|
|
81
|
+
|
|
82
|
+
- **legacy 面(0.1.x)顶部只留一行**:状态徽章 + 工具数 + 设置条摘要 + `[⚙ 设置]` + `[启用/禁用]`;自动更新间隔与「在侧边栏显示入口」复选框一起收进默认收起的设置条。
|
|
83
|
+
- **现代面(≥0.2.1)按「三面布局」**:标题 + 版本 + 升级链接;状态行只留状态与连接数;设置用 DSH 行样式;长说明进折叠区。
|
|
84
|
+
- **添加连接默认收起**:「+ 添加连接」(全局;工作区用 `<datalist id="dbh-ws-list">` 给已有工作区)/ 分组头「+ 环境」(工作区自动带入,少一个字段)。保存仍走原有「试连 → 失败才确认」门禁。
|
|
85
|
+
|
|
86
|
+
## 环境选项的落点(只读 / SSH)
|
|
87
|
+
|
|
88
|
+
同一组选项在**两个面**上呈现不同,按 `view.dshModern` 分叉——**这是 legacy 与现代面的分工,不是新旧两套规则**:
|
|
89
|
+
|
|
90
|
+
| 元素 | 现代面(`dshModern === true`) | legacy 面(`dshModern === false`) |
|
|
91
|
+
| --- | --- | --- |
|
|
92
|
+
| 只读标记 | 行内 `.dbh-rochip`,弹窗/宽屏/手机三种布局都出现;点击发 `{op:'options', readOnly}`(无 DSN);必须 **开锁+「可写」/ 闭锁+「只读」**(同一 16px 线性图标风格),否则两个灰色「只读」会被误读成都是只读 | 环境 chip 内 `span.dbh-ro`,说明并入 chip `title` |
|
|
93
|
+
| 设置控件 | 开关必须用 `primitives.Switch`(缺失时才回退 `.dbh-fbswitch`) | `.dbh-checkbox` 复选框 |
|
|
94
|
+
| 连接串 🔒 | 不加 | 保留 |
|
|
95
|
+
|
|
96
|
+
- SSH 只影响路由,**不做徽章**,只进环境 chip 的 tooltip(`ro.sshTitle`)。
|
|
97
|
+
- 编辑表单里的只读勾选保留(两面都有);`.dbh-checkbox` **不用 `.dbh-input`、不包进 `.dbh-field`**,否则 `render-check` 的 `dbh-field === 3` 与 `typeWithin('dbh-addpanel',0..2)` 索引会变。
|
|
98
|
+
- SSH 高级区 `.dbh-adv` 折叠面板**必须排在 DSN 输入之后**,折叠态放 `uiMemory.advOpen`;区内「测试该配置」旁是**「测试 SSH 隧道」**(独立按钮/回执,只验 SSH 层)。
|
|
99
|
+
- 秘密输入 `type="password"`、占位符写明「留空 = 保持原值」,**编辑态绝不回显已存密码/口令/密钥路径**(镜像行只给 `authKind` / `hasPassword` / `hasPassphrase` / `keyReady` 布尔)。
|
|
100
|
+
- 写入载荷统一走 `connDraftOf(form)` / `withConnDraft(op, form)`:只读开关与隧道随 `add` / `test` / `options` 三种 op 一起发出,**绝不单独再发一条**。表单对象在产生 op 时会带 `_draft` 指回原始草稿,别绕过这两个函数手拼 `readOnly` / `ssh`。
|
|
101
|
+
|
|
102
|
+
## 三面布局契约(5.2,`dshModern` 门)
|
|
103
|
+
|
|
104
|
+
同一个组件的**三个面由 slot + 实测宽度决定**:`settings.section`(弹窗,窄列)、主面板与 `plugins.row.config` 的 `page`(宽屏整页)、`< 700px` 手机小屏(堆叠紧凑、触摸尺寸;`isWide` 阈值为 720)。
|
|
105
|
+
|
|
106
|
+
- **新界面必须挂在 `view.dshModern` 下**;`dshModern === false`(DSH 0.1.x)必须渲染**与旧版逐字兼容的 DOM**(含旧页头 `4 个工具(固定)`、⚙ 设置条、`dbh-checkbox`),否则 `scripts/render-check.mjs` 既有断言会红。旧断言不动,新断言由 harness 另行扩展。
|
|
107
|
+
- 现代面硬约定:
|
|
108
|
+
1. 页头 = 标题 + 灰色版本号 + 一行「↑ 可升级到 x」文字链接(点击才展开中性说明面板;**禁止红底/红框/error 色**)+ 图标式 ↻;
|
|
109
|
+
2. 状态行只显示「运行中/已停用 · N 个连接」;工具数 / 一次性模式 / dbhub 版本进页面底部「ⓘ 说明与技术详情」折叠区(默认收起);
|
|
110
|
+
3. 设置 = **所有非弹窗、非手机页面(宽屏页 / 插件页 / 窄桌面列)一律一行紧凑设置条**(窄列自动加 `.dbh-barnarrow` 隐藏标题与状态词、收窄间距,**不得退回竖排设置行**;弹窗与手机才用竖排)。宽屏条 `.dbh-settingsbar`:标签 + 状态词 + `primitives.Switch` + 更新输入/保存同排,开关 `scale(.85)`;弹窗/手机为紧凑设置行(13px 标题、8px padding、开关 `scale(.8–.9)`)——**字号与控件刻意弱化,连接列表才是主体**。状态词统一为 `启用/停用`、`显示/隐藏`、`开启/关闭`;更新间隔输入 + 天 + 保存必须在同一行不换行;
|
|
111
|
+
4. 行内只读 chip 在三种布局都出现(见上表);
|
|
112
|
+
5. 弹窗面的设置收进状态行「设置」折叠,宽屏面直接展开。
|
|
113
|
+
|
|
114
|
+
## 交互细节(每条都对应一段回归门)
|
|
115
|
+
|
|
116
|
+
- **复制必须给出干净字符串**:连接串拆成两个 flex item 后,浏览器复制序列化会在块级子元素间插分隔符(粘进 DSN 就成 `…3306/ app`)。因此 `connLabelView` 的元素自己接管 `onCopy`(`e.clipboardData.setData('text/plain', w.conn)` + `preventDefault()`)——挂在**连接串元素**而非整行,复制环境名不受影响。`w.conn` 是元数据,不涉凭据。
|
|
117
|
+
- **测试中显示已耗时**:`startTest` 记 `startedAt`,`testing` 态渲染 `test.elapsed`(`测试中… Xs`);1s 计时器**只在有行处于 testing 时存在**(effect 依赖 `anyTesting`,cleanup 清掉)。看门狗按是否隧道分档(30s / 60s),必须高于宿主探测预算(25s / 45s)。
|
|
118
|
+
- **等待态不给二次提交**:`pendingPretest` 期间保存/添加按钮 `disabled`,避免重复派发同一测试。
|
|
119
|
+
- 探测要几秒(spawn dbhub),但**写入结果必须立刻可见**——靠宿主 `publishFast` 即时回推,不要改回「等整轮扫描」。
|
|
120
|
+
|
|
121
|
+
## 验收
|
|
122
|
+
|
|
123
|
+
- 布局/交互改动的验收 = `npm run check:ui`(离屏渲染 + 点击交互;分段 `[1]`…`[30]`,涵盖模式判定、默认展开、全部展开/折叠、添加面板、复制、i18n 覆盖率、热重载幂等、覆盖门、HTTP 桥、只读/SSH 载荷、现代三面、版本门)全绿 + `node test/client-format.test.mjs`。
|
|
124
|
+
- 已知易错门:`[14]` 重复 locale 注册、`[17]` 复制字符串、`[18]` 无 settingsScope 走桥接 + `configForms` 优先与回退 + 轮询可回收、`[19]` 启用∧开关 / 禁用一律隐藏 / 设置页始终在、`[20]`/`[21]`/`[23]` 只读与 SSH 载荷、`[22]` 残留 interval、`[28]` 手机布局、`[29]` 0.1.x 渲染旧页、`[30]` 窄桌面窗口保持横向设置条。
|