build / build-and-scan (push) Waiting to run
文档库:目录改为编号制(01-规范/02-架构设计/03-数据库/04-调整方案/
05-交接单/06-ops/07-scripts/08-skills/09-archive),顶层散文件归入 01-规范/;
INDEX.md 与 docs-manifest.json 重刷(档案 146 篇);旧目录名引用全量对齐。
IM 线:src/im/**(SDK / hub / store / presence / ws / gateway-token)、
src/web/routes/im.ts、src/db/plugin-data/**、src/supervisor/plugin-assembly.ts
及对应 test/**。
插件线:poc/{im-agent-bridge,im-connection-gateway,im-conversation-tabs,
business-plugins-im,carbon-mcp-probe}、src/web/routes/{sessions,overlay-device}.ts、
src/net/relay/{device-grant,instance-credential}.ts。
仓库卫生:清出 40 个历史误入库 / 已改名文件(34 个交接单归档 + 6 个旧结构,
本地均有副本);dsh-server-docs/.gitignore 补 tmp/;交接单不入库(政策)。
9.2 KiB
9.2 KiB
数据库专区 · 01 接入指南(各种情况怎么接)
状态:✅ 已落文档库(2026-09-20)| 专区:
03-数据库/
立稿:2026-09-20 | 起草会话
IM插件数据面-专门文档> 单一来源:代码src/db/{adapter,connection,schema,repo,sqlite,pg}.ts;档案 142 §3.6;04-调整方案/140(配置外置)。
〇、本文怎么用
先看你要存的是哪一类数据,再照着对应那节做。顺序不可颠倒:先判落点,再谈建表。
| # | 情形 | 落点 | 能不能建表 | 访问入口 |
|---|---|---|---|---|
| 1 | 平台内核数据(账号 / 实例 / 主机 / 租约) | 控制面库 dshs |
✅ 内核自己建 | src/db/** |
| 2 | 插件业务数据(跟房间走 / 跟用户走) | 该插件自己的库 dshs_pl_<pluginId> |
✅ 声明式声明,内核翻译建表(含归属列) | 内核数据 API |
| 3 | 用户的业务数据(索引 / 导出产物 / 设置) | 库(插件维度进插件库、平台维度进控制面库) | ✅ 由内核建 | 内核数据 API |
| 4 | 本机运维数据(健康 / 临时态) | Worker 本地库 | ✅ Worker 本地 | 平台内部接口 |
| 5 | 大对象(图片 / 视频 / Excel / 代码包) | 对象存储桶 | ❌ 不进 DB | files 面(sha256 + bucket_key) |
| 6 | 实例本地(只剩三类:文件内容 / 缓存 / 临时) | 实例 home | ✅ 只放可重建的东西 | 实例本地 |
| 7 | 缓存 / 队列 / 后处理任务 | 预留端口 | 端口先立、实现后置 | im.cache / im.queue |
判据一句话:跟平台全局走 ⇒ 控制面库 dshs;跟某个插件走 ⇒ 该插件的库 dshs_pl_<pluginId>(库内按归属列切分用户 / 房间);是文件内容 ⇒ 桶;本机运维 ⇒ Worker 本地库。
⛔ 同一份数据不双写(双写 = 脑裂)。
情形 1 · 平台内核数据
- 谁做:平台自己(不是插件)。改
src/db/schema.ts的MIGRATIONS。 - 硬纪律:每条迁移必须同时给
sqlite与pg两份 SQL(双后端可切是本平台的既有承诺);只增不减(加表 / 加列带默认值 / 加索引)。 - 命名:内核表用保留名、不带前缀(
users/sessions/workspaces/folder_plugins/dsh_instances/audit_log/domains/credential_vault/business_plugins/dsh_hosts/email_codes/overlay_devices/schema_migrations)。 - 验收:新库跑到最新 version 且重复执行版本不变(幂等);
npm test的test/db.test.mjs绿。
情形 2 · 插件业务数据(问得最多的那种)
- 判据:一个插件一个库 ——
dshs_pl_<pluginId>,建在同一个 PostgreSQL 13.23 实例上;库内按归属分两类(scope: user跟用户走 /scope: room跟房间走),靠归属列区分。 - 建表:插件不写 SQL —— 用声明式 schema(中性类型
text/integer/bigint/real/boolean/json/timestamp/uuid),由内核翻译成 sqlite + pg 两套 DDL;内核自动补库名、表前缀p_<pluginId>_、归属列(user_id/room_id)、审计列。 - 迁移:只许加表 / 加列(带默认值)/ 加索引;在启用插件时执行;任一步失败 ⇒ 拒绝启用(不做半迁移)。
- 访问:
im.data.table(name).insert/update/delete/find/count+ 游标分页;库路由由内核按插件身份决定(插件看不到库名);归属判定与房间 ACL 由内核强制注入;同库内要原子 ⇒ 走内核事务口;写与"刚写就读"走主库。 - 禁止:直连 DB / 原生 SQL / 访问别的插件库 / 自组跨库事务 / 绕过归属判定 / 以实例本地库当权威。
- 验收:插件包内
CREATE TABLE、better-sqlite3、pg零命中;scope: user读他人行 = 0 行;非成员读 room-scoped 行 = 0 行。 - 📄 完整契约 = 本专区
DB-03-插件数据面规范.md。
情形 3 · 用户自己的数据(2026-09-22 改:入库;实例本地只留缓存 / 临时)
- 🔴 用户数据一律入库(这是"能备份、能迁移"的前提):
- 插件里的用户数据(
scope: user)⇒ 该插件的库dshs_pl_<pluginId>,带user_id; - 平台维度的用户数据(会话 / 设置 / 工作区文件索引)⇒ 控制面库
dshs,带归属列。
- 插件里的用户数据(
- 实例本地(
<userRoot>/)此后只剩三类:① 文件内容(当前形态;目标形态改为落对象存储、本地只留按需缓存)② 缓存(可重建)③ 临时文件。 判据:本地丢了不影响正确性,只影响延迟 —— 凡是"本地丢了就丢数据"的东西,一律不属于实例本地。 - ⛔ 不得以实例本地库 / 本地文件作为数据权威(旧形态「插件在实例里自建 SQLite 装业务数据」已作废;存量按架构定稿 S2 步骤导入插件库)。
- 仍生效的写入门禁:平台侧写实例 home 必须走
UserFs—— 直接fs写 = 静默空操作(用户卷在 worker 上时尤其如此)。 - 🔴 平台侧不得以
cp/mv/ 直接fs写用户 home 下的任意文件(2026-09-21 立,仍有效):这三条路径都不是合规写入通路,⛔ 一律不认。- 理由(实测证据):
UserFs的 home 面只有 3 个白名单裸文件名 ——settings.yaml/.credentials.yaml/.overlay-device.json(src/fs/user-fs.ts:126,⛔ 不收路径,所以home/.dsh/<plugin>.db这类根本进不了白名单);且writeHomeFile签名是text: string= 文本语义(src/fs/user-fs.ts:113),upload(src/fs/user-fs.ts:83)是 workspace 相对路径 ⇒ 只能写ws/。 - ⇒ 结论(2026-09-22 收窄):平台侧对实例 home 下的二进制 / 任意文件没有合规写入通路。⚠️ 但这不再是"插件自有 DB 该落哪"的答案 —— 插件数据的正解是入库(§情形 2 的插件库);home 只放缓存 / 临时,对 home 内的文件平台侧只能只读核对,⛔ 不能代写。
- 理由(实测证据):
- 实例内插件(2026-09-22 改):数据入库 —— 插件数据进
dshs_pl_<pluginId>(scope: user的行带user_id),平台维度的用户数据进控制面库;⛔ 不得再把实例本地 SQLite / 本地文件当数据权威(可以做缓存,丢了可重建)。存量(如 MCN)按架构定稿 S2 步骤导入。 - 坑:⛔ 用
homedir()拼路径必错(实例内HOME=<userRoot>/ws);正解 =process.env.DSH_HOME ? join(DSH_HOME,'.dsh') : join(homedir(),'.dsh')。
情形 4 · Worker 本地运维数据
- 落点:Worker 本机库(该机器的健康 / 临时态 / 运维缓存)。
- 判据:跟机器走、可以重建 ⇒ 放这里,别放平台库(否则每台机器都要往 Manager 写,形成热点)。
- ⛔ 不放归属 / 租约类数据(那类只有 Manager 能写)。
情形 5 · 大对象
- 落点:S3 兼容桶(自建 MinIO 或云 OSS/COS 兼容端点)⇒ 不锁厂商;端点与密钥走
config/platform.env(⛔ 不入库、默认值中性)。 - 分工:桶存内容、DB 存元数据(
files表:sha256/bucket_key/size/mime/visibility)。⛔ 大对象不进 DB(bytea会把库与备份一起拖垮)。 - 上传:预签名直传桶(平台不经中转);下载:短 TTL 预签名(≤5 min),需审计时走平台代理。
- 生命周期:同
sha256只存一份 + 桶 lifecycle;删除 = 先软删元数据,桶内回收交 lifecycle(⛔ 不做同步删,不可逆)。 - 权限:复用房间 ACL,⛔ 不新起一套。
情形 6 · 缓存 / 队列 / 后处理
- 队列先用 PG:
SELECT … FOR UPDATE SKIP LOCKED+LISTEN/NOTIFY⇒ 零新增进程、零新增内存。 - Redis 后置:仅在「跨机扇出 / presence 广播」实测成热点后引入;硬前提 = 单实例 ≤128–256 MiB(实例硬顶 1024 MiB / 宿主 1870 MiB)。
- "预留"的正解:先把端口立起来(
im.cache/im.queue),本期实现 = 内存 Map / PG 表 ⇒ 将来换实现不改调用点。 - ⛔ 插件不得自行引入 Redis / MQ 进程。
主从与扩展(四个情形共用)
| 项 | 结论 |
|---|---|
| 选型 | 保留 PostgreSQL 13.23(现有实例 + db/ 双后端可切)> MySQL(无收益、要重迁移)> SQLite(⛔ 不支持主从,只留单机/测试) |
| 主从 | 流复制 + 只读副本 |
| 读写分离 | ⚠️ 按数据分类:元信息读可走副本;消息/"刚写就读"必须走主(否则破坏读己所写) |
| 扩展顺序 | 先索引与游标分页 → 再只读副本 → 再分区/分片。⚠️ 原「不要提前分库」已作废(2026-09-22 拍板按插件分库) |
验收速查(任何接入都跑这三条)
- 归属对:表名能一眼判断属于谁(无前缀 = 内核;
p_<pluginId>_= 该插件)。 - 双后端:新结构在 sqlite 与 pg 上都建得起来。
- 不越权:非成员 / 跨前缀 / 跨插件 一律拿不到数据(不是"拿得到但看不见")。