文档库:目录改为编号制(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/;交接单不入库(政策)。
26 KiB
插件数据面规范(插件如何接入数据库)
状态:✅ 已落文档库,本文件即插件数据面的唯一正式来源 ——
dsh-server-docs/03-数据库/DB-03-插件数据面规范.md(2026-09-20 落点,专项区03-数据库/)。 ⚠️ 不再"待落点":04-调整方案/143是另一件事(「MCN 工作台入口失效与语言切换显示修复」),与本文无关;⛔ 不要再把本文往 04-143 归位。 立稿:2026-09-20 | 起草会话IM插件数据面-专门文档来源:档案 142 §3.6(三条平台保护)·05-交接单/IM群组-01-房间内核与DB.md §九(内核侧 DB 裁决)·05-交接单/IM群组-04-插件SDK与扩展点契约.md §九(插件侧契约)。 本文是专门文档:把散在两处的条款收拢成一份可直接照着实现的规范;冲突时以本文为准,并回改上述两处。
✅ 执行状态(2026-09-23 · 插件投放与分库线第 4 棒):数据面已开工并落地,本规范除标注处外可照着实现。
- ✅ 已落地:共享只读包库
bundled-plugins(D2,"包库"与本文件的"插件库"是两回事)+ admin 投放面(POST /api/plugins/business/share)。逐条读数见05-交接单/插件投放与分库线-01共享只读包库与插件数据面.md §十二。- ✅ 建库权限已解决(2026-09-23 · 用户拍板 B 档):
ALTER ROLE dshs CREATEDB;(47 已落地 + 11 条验收)⇒ §三"谁建库"不再受阻。- ✅ 建库口已接线:
src/db/plugin-data/datastore.ts(CREATE DATABASE单语句非事务 / 台账 + 审计 / 台账 ×pg_database对账)+ 控制面 v15 迁移(两张台账表)。落地件 ⇒交付物/建库口接线-B档落地-20260923.md。- 🔴 §六"跨节点:worker 经覆盖网络调 Manager 侧数据面"仍不通:所需"实例→控制面"身份通道今天为 0(147 §三 G3 / 序47 §②.1)。
- ⚠️ B 档的真实缺口(已由代码兜住):PG 无"只允许
dshs_pl_*前缀"的原生机制 ⇒ 白名单只能靠平台代码三点(库名点生成 + 正则复校 + 每次建库落台账审计)。详见 §六-1。- ✅ 上表
§十 差异清单的 D1–D5 / D7 回改已落文(本轮)。
〇、定位
回答一个问题:业务插件要存数据,怎么接?
一句话:一个插件一个库(dshs_pl_<pluginId>,建在同一 PostgreSQL 13.23 实例上)—— 插件不建表、不写 SQL、不持有连接,用声明式 schema 由内核翻译建表,数据读写一律走内核数据 API、由内核按插件身份路由到该插件自己的库。
- 适用:所有走「候选池 → 实例功能管理启用」通道的业务插件(自研 / 第三方)。
- 不适用:平台自身内核表(
rooms/room_members/messages/files…);那些归控制面库与 A 单。 - 🟢 范围:只讲技术实现。| 架构依据(冲突时以它为准):
02-架构设计/数据-分库与权威存储架构.md。
一、第一步:先判落点(再谈建表)
第一判据:插件数据的落点只有一处 —— 该插件自己的库 dshs_pl_<pluginId>。库在同一个 PostgreSQL 13.23 实例上,由内核按"调用方插件身份"路由;插件看不到库名、也拿不到别的插件的库。
库内再按归属分两类,两者同库共存,靠归属列区分:
| 归属 | 谁的数据 | 归属列(内核自动补) | 读写权限 |
|---|---|---|---|
scope: user |
某个用户自己的(插件里的个人数据 / 索引 / 产物) | user_id |
只能读写自己的行 |
scope: room |
跨用户共享的房间维度数据(跑团角色卡 / MUD 地图 / 办公任务卡) | room_id |
复用房间成员判定(canSee) |
🔴 归属列强制(分库的前提,不是可选项):每张业务表必须带 user_id 或 room_id,由内核建表时自动补齐;插件不许自建表、不许省略归属列。
理由:库的维度是插件,用户维度的备份/迁移只能靠归属列切分 —— 缺了归属列,按用户导出/迁移直接不可能。
⛔ 同一份数据不双写(双写 = 脑裂)。
🔴 不得以实例本地库作为数据权威(2026-09-22 新增):插件可以在实例本地放缓存(丢了可重建),但权威数据必须在插件库。旧形态「插件在实例里自建 SQLite 存业务数据」此后不允许;存量(如 MCN)按架构定稿的 S2 步骤导入插件库。
⚠️ pluginId = 包名去掉 scope(如 @dsh-local/trpg-kit ⇒ trpg_kit):小写、连字符转下划线;库名 = dshs_pl_<pluginId>。
二、红线(违反即不予启用)
-
⛔ 插件不得直连数据库:不持有连接串 / 不 import 数据库驱动 / 不自建连接池。
-
⛔ 插件不得写原生 SQL:无
CREATE TABLE/ALTER/DROP/SELECT;无触发器、存储过程、厂商专有类型。 -
⛔ 插件不得访问其他插件的库:看不到也查不了别人的
dshs_pl_*、控制面库与共享数据。 -
⛔ 插件不得自组跨库事务(PG 跨 database 无原生事务)。
-
⛔ 插件不得绕过归属判定:
scope: user只能读写自己的行;scope: room由内核强制注入成员校验。 -
⛔ 插件不得以实例本地库 / 本地文件作为数据权威(可作为缓存,权威必须在插件库)。
判据(可机械验证):插件包内
grep -rE "CREATE TABLE|ALTER TABLE|DROP TABLE|better-sqlite3|from 'pg'|require\('pg'\)"零命中。
三、建表:声明式 schema
插件在 im.data 上声明表;字段类型仅限中性集合,由内核翻译成 sqlite + pg 两套 DDL:
| 中性类型 | sqlite | pg | 说明 |
|---|---|---|---|
text |
TEXT | TEXT | 短文本 |
bigtext |
TEXT | TEXT | 长文本(有长度上限) |
integer |
INTEGER | INTEGER | 32 位整数 |
bigint |
INTEGER | BIGINT | 计数 / 时间戳毫秒 |
real |
REAL | DOUBLE PRECISION | 浮点 |
boolean |
INTEGER | BOOLEAN | 布尔 |
json |
TEXT | JSONB | 结构化载荷 |
timestamp |
TEXT(ISO) | TIMESTAMPTZ | 时间 |
uuid |
TEXT | UUID | 标识 |
内核在建表时自动补齐:库名 dshs_pl_<pluginId>、表名前缀 p_<pluginId>_、归属列(user_id 或 room_id,按 scope 定)、审计列(created_at / updated_at)。
三·补 —— 谁建库、什么时候建(2026-09-23 落文,原缺失项)
谁建库:平台侧(⛔ 不是插件、⛔ 不是实例)。插件只声明、不建库。建库由 admin 在门户上触发:
上传 ─检测(安全 + 兼容 + 数据面声明校验)→ 通过 ⇒ 状态 pending
⇒ admin 点 [建库]
⇒ POST /api/plugins/business/:id/datastore
⇒ CREATE DATABASE dshs_pl_<pluginId> + 落台账
─建库成功→ [发布] → 用户才能启用
- 权限前提:
ALTER ROLE dshs CREATEDB;(2026-09-23 拍板 B 档;47 已落地 · 11 条验收)。⛔ 不得给CREATEROLE/SUPERUSER。重建步骤 ⇒DEPLOY-本部署.md §6.4。 - 语句姿势:
CREATE DATABASE dshs_pl_<pluginId> OWNER dshs ENCODING 'UTF8' TEMPLATE template0;—— 单语句、⛔ 不走事务(PG 语句级内核限制:CREATE DATABASE cannot be executed from a function/ 不能进事务块)。 - 库名两重防线(B 档缺口只能靠代码兜):①
pluginDbNameOf从.dsh声明换算点生成,⛔ 不收请求体里的库名;②assertPluginDbName在拼 SQL 之前复校^dshs_pl_[a-z][a-z0-9_]{0,40}$+quoteIdent标准引用。非法 ⇒ 400invalid_plugin_db_name,⛔ 不下发 PG。 - 幂等:已存在 ⇒ 返
exists(捕获 PG42P04 duplicate_database),⛔ 不报错、⛔ 不重建。 - 错误码:
PG_CREATEDB_MISSING(503 · 未授CREATEDB或已回滚)|invalid_plugin_db_name(400)。⚠️ D 档的PLUGIN_DB_HELPER_MISSING已作废。 - 对账:
GET /api/plugins/business/datastores/reconcile⇒ 台账 ×pg_database双向差集(orphan = 绕过平台建的库;missing = 台账有而 PG 无)。
示例(跑团插件):
pluginId: trpg_kit
schemaVersion: 3
tables:
- name: character_sheet # 实际表名 => p_trpg_kit_character_sheet
scope: room # room | user(room = 注入房间 ACL;user = 只能读写自己的行)
columns:
- { name: pc_name, type: text, notNull: true, maxBytes: 128 }
- { name: sheet, type: json, notNull: true, maxBytes: 16384 }
- { name: sheet_rev,type: integer, default: 1 }
indexes:
- { columns: [room_id, pc_name], unique: true }
约束:每表至多 20 列、json 列 maxBytes 必填、表与列名 ^[a-z][a-z0-9_]{0,40}$、每插件至多 20 张表。
四、迁移:只增不减
- 插件声明
schemaVersion(整数,单调递增);内核按版本推进。 - 只允许:加表 · 加列(必须带默认值)· 加索引。
- ⛔ 不允许:删列、改列类型、重命名、加无默认值的非空列(会导致上线即失败)。
- 执行时机 = 🔄 2026-09-23 修订:分两步,且都是插件级一次性动作(原写法"启用插件时"会退化成 per-user 动作 —— 同插件每个用户开通都跑一遍 DDL):
- 建库:admin 点
[建库]时(POST …/:id/datastore)⇒ 建库 + 建表 + 归属列 + 索引一次做完,落台账plugin_datastores。 - 迁移(
schemaVersion推进):插件级一次性,全局只推进一次;用户开通只读台账版本、⛔ 不触发 DDL(POST /api/plugins/mine/apply走门禁但不改结构)。 - 任一用户启用前,平台先校验台账
status = ready且版本满足 ⇒ 不满足 ⇒ 拒绝启用(datastore_not_ready)。
- 建库:admin 点
- 任一步失败 ⇒ 拒绝启用(⛔ 不做"半迁移"),旧结构保持可用、可回滚。
- ⚠️ 表级 DDL 在同一事务内(失败整体回退);但 建库项不可回滚 ⇒ 建库失败不 DROP,留空库 + 台账
status='error'+last_error,等[重试]覆盖。 - 迁移语句由内核生成并记入审计(
plugin_data_audit)。 - TOCTOU 防护:预演(
/datastore/plan)产出planHash⇒ 执行时必须回传同一指纹才对;⛔ 建库 / 迁移路径一律不传planHash(omit ⇒ 保留旧值,COALESCE语义),⚠️ 传null会清掉指纹 = 拆掉防护。
四·补 —— 兼容矩阵:包版本 × schemaVersion 怎么判(2026-09-23 落文,P2「要有定义」的兑现)
两个版本号是两件事,⛔ 不混为一谈:
version= 插件包版本(package.json的version,可能因改文案/修 bug 而不动结构);schemaVersion= 数据面结构版本(整数、单调递增)。判"库该不该动"只看schemaVersion;包版本只用来判"该装哪个包"与 B 档回滚素材的指向。
判据(按优先级从上到下取首个命中项):
| 场景 | 包版本 | schemaVersion vs 库现值 |
允许的动作 | 依据 |
|---|---|---|---|---|
| 首次装 | 任意 | 库不存在 | 建库 → 迁移 → 可启用 | §三·补 |
| 升级 | 新 > 旧 | 新 ≥ 现值 | 升级(只增:加表/加列带默认值/加索引) | §四 |
| 等价换包 | 新 > 旧 | 新 = 现值 | 只换代码,不碰库(drift 不触发) |
§四 |
| 降级 | 新 < 旧 | 新 < 现值 | ⛔ 拒绝启用 + 明确错误码;不回滚数据 | §四 + 本表下注 |
| 同名同版本重传 | 相同 | 相同 | 幂等(tgz sha256 相等 ⇒ 不动文件) |
§六-2 / S2 |
| 同名同版本但内容不同 | 相同 | 相同 | 冲突 ⇒ 两段式确认后才替换(⚠️ 会波及所有已指向的用户) | S2 |
三条硬规则:
- 降级不自动发生:
schemaVersion回退 ⇒ 状态drift,executable=false⇒ 前端「确认执行」置灰、后端POST /datastore/migrate也拒。给用户的错误必须可读(原话:不写"未达预期"了事),且库一个字节都不改 —— 旧版代码若认不出新列,忽略即可(与 §八 一致)。 - ⛔
UPDATE ≠ DROP:更新路径永不DROP DATABASE;卸载才走 §六-4(清单 + admin 确认 + 默认保留 30 天)。 - 矩阵的机器判据 =
diffDecl():上表是给人看的口径,机器侧唯一实现是src/db/plugin-data/diff.ts的diffDecl(decl, current, …)+pluginDbNameOf推导的plan.schemaVersion。判据来源是库的现实(真读information_schema),⛔ 不是台账里记的那个数 —— 台账只是对账参照(§六-2 的reconcile抓的正是二者不等)。
B 档(只留最新一版)给本矩阵加的两条前置(§五 S5-a-3,2026-09-23 既有裁决):
- 每次上传先落一份 tgz 到
/opt/dsh/backups/plugins/<pkg>/<ver>.tgz—— 否则"降级到哪一版"没有素材; - 台账
plugin_datastores必须记当前包版本 —— 否则判不出该回滚到哪个包。
⚠️ 无灰度是 B 档的已知代价(已拍板接受):共享层同名替换后,所有已指向它的用户下一次生效即为新版本 ⇒ 执行回报里必须写清"影响 N 个用户、动作耗时"。
验证(兼容矩阵的可机械复现判据):
① 装 v1(schemaVersion=1) → 写 3 行 → 升 v2(schemaVersion=2,加一列)
期望:3 行仍在、新列有默认值;`plan.summary.columns >= 1`;退出码 0
② 强行装回 v1
期望:状态 `drift`、`executable=false`、错误码可读、`select count(*)` 仍 = 3(库未被改)
③ 同名同版本、内容相同重传
期望:`idempotent:true`,共享层 mtime / sha256 不变
④ 同名同版本、内容不同
期望:`conflict:true` + 双方 sha256;确认前**不动任何文件**
五、访问 API(唯一入口)
im.data.table<T>('character_sheet')
.insert(row) // 单行;返回 id
.update(id, patch) // 按主键
.delete(id) // 软删标记(内核保留 30 天,见 §六-4)
.find({ where, order, limit }) // 游标分页:返回 { rows, cursor }
.count({ where })
语义要点:
- 库路由由内核决定:插件只报表名,内核按调用方插件身份路由到
dshs_pl_<pluginId>;插件看不到库名,也拿不到别人的库。 - 归属维度自动注入判定:
scope: user的表每次读写附加「调用者 ==user_id」校验;scope: room附加「调用者是该房成员」(复用内核canSee)⇒ 越权得到 0 行 / 明确拒绝,不是"查得到但看不到"。 - 同库内要原子 ⇒ 走内核事务口:
im.data.tx([...])仅支持同一个插件库内的多表;跨库(如同时改房间属性 + 自己的数据)没有事务 ⇒ 必须拆成幂等步骤 + 补偿,⛔ 不许靠双写。 - 读写路由:写、以及"刚写就读"一律走主库;⛔ 不从只读副本读自己的新行(副本延迟会破坏读己所写)。
- 限流:插件写操作吃与 agent 同族的预算,并受房间级速率上限约束(142 §3.6 保护约束 3)。
- 分页:一律游标,不用
OFFSET(大偏移在 pg 上会退化)。
六、管理面
- 配额(防单插件拖垮库):每插件 总行数 + 单行字节 + 每房行数 三重上限(库维度天然对齐"一插件一库");超限返回明确错误码(⛔ 不静默丢、⛔ 不静默截断),并在插件卡片上可见。
- 审计:DDL、迁移、超阈值批量写记录进内核表
plugin_data_audit(谁 / 何时 / 哪个插件 / 影响行数);空间占用进平台看板。- 🔴 两张台账表 = 控制面 v15 迁移新增(2026-09-23 落地):
表 键 作用 plugin_datastoresplugin_id→database_name库台账:当前 schema_version+ 插件包version+status(created/ready/error) +plan_hash+last_error+ 时间戳。B 档白名单的第三点兜底 —— 使"平台到底建过哪些库"可对账plugin_data_audit自增 id 审计流水:建库 / 迁移 / DDL 逐条留痕(谁 / 何时 / 哪个插件 / 动作 / 影响) - ⚠️ 两表由平台启动时
runPgMigrations自动落(47 当前停在 v13,部署 v15 代码后随restart dshs自动补齐);⛔ 不需手工建表。 - 对账:
GET /api/plugins/business/datastores/reconcile⇒ 台账 ×pg_database双向差集(抓 orphan / missing)。
- 🔴 两张台账表 = 控制面 v15 迁移新增(2026-09-23 落地):
- 可观测:每插件暴露「行数 / 体积 / 最近写入」三项,进"功能管理"页。
- 卸载:停用插件 ⇒ 数据默认保留 30 天(可配置);窗口内可恢复。
彻底卸载 =
DROP DATABASE dshs_pl_<pluginId>(一次带走该插件全部表,比逐表 DROP 干净)⇒ 属不可逆 ⇒ 必须 admin 显式确认 + 先出受影响清单(库名 / 表 / 行数 / 体积),⛔ 不做"卸载即删"。 - 备份 / 迁移(2026-09-22 新增):单插件备份 =
pg_dump dshs_pl_<pluginId>;按用户导出/迁移 = 统一迁移器按归属列遍历(控制面库 + 各插件库 + 桶清单)⇒ 一次遍历、可重入、可对账。⛔ 用户注销不 DROP 插件库(库属插件、不属用户),只删该user_id的行 + 回收桶内对象。 - 禁用后的行为:插件无关;房间照常(数据面异常不得影响消息流,见 §七)。
六·补 —— 插件文件落点与凭据投递(2026-09-23 落文 · 对 carbon 线两问的裁定)
一句话:插件的非数据库文件一律落
<userRoot>/home/.dsh/plugins/<pluginId>/(D4);该目录由实例侧插件自己建,平台侧不碰。
| 项 | 口径 |
|---|---|
| 落点 | <userRoot>/home/.dsh/plugins/<pluginId>/(= /var/lib/dshs/users/<uid>/home/.dsh/plugins/<pluginId>/,实例内是同一绝对路径,bwrap --bind <userRoot> <userRoot> 不重映射) |
<pluginId> 定义 |
去 scope、小写、非 [a-z0-9_] 折 _(src/db/plugin-data/schema.ts:126 pluginIdOf)⇒ 与库名 dshs_pl_<pluginId> 同一个 token。例:@dsh-local/dsh-plugin-carbon ⇒ dsh_plugin_carbon(⛔ 不是 carbon) |
| 谁建目录 | 实例侧插件首次用时 mkdir -p(幂等);投递类文件由投递方 install -d -m 700 -o <uid> -g <uid> 先建。⛔ 平台侧不碰 —— src/fs/user-fs.ts:126 的 home 白名单只有 3 个裸文件名,不含任何目录 |
| 目录内取数姿势 | join(process.env.DSH_HOME, '.dsh', 'plugins', pluginId)。DSH_HOME 由平台注入(src/supervisor/orchestrator.ts:813),实例内必存在,缺失 ⇒ fail-fast 报错,⛔ 不许回退 |
⛔ 禁 homedir() |
实例内 HOME = <userRoot>/ws(orchestrator.ts:811),而 ws 会被平台按产物清理(:857)⇒ homedir() 回退不是「降级可用」,是把数据写到会被清掉的位置 |
⛔ 不存在 im.paths.pluginData() |
实测三处零命中(dsh_shenxian/src / 宿主仓 deepseek-harness / node_modules)。本规范不再引用该 API;实例内取目录按上行的 DSH_HOME 拼 |
| 凭据类文件 | 与插件数据同目录(同一清理单元):文件 0600、属主 = 该用户 uid(⛔ root 写的 0600 实例读不了,同 user-fs.ts:110-111 的教训);插件侧只读,⛔ 不覆盖投递来的文件名 |
| ⚠️ 跨机 | <userRoot> 是那台机上的路径。用户实例在 worker 上时,在管理机直写 <userRoot>/home/… = 静默空操作(src/fs/user-fs.ts:122-124,档案 138 §五)⇒ 投递必须发生在 hostIdFor(userId) 指到的那台机 |
| 卸载清理单元 | 整个 <pluginId>/ 目录(与库 dshs_pl_<pluginId> 同粒度:库走 §六-4,文件走本行) |
⚠️ 现状缺口(⛔ 不许当已知项推断):平台侧没有「把插件凭据投给用户」的通路 ⇒ 凭据仍是插件线带外投递。要做成「用户点一下开通 ⇒ 凭据自动就位」,须在平台立项;建议并进 worker 侧 /plugins/apply 同一条腿(与装配搬到实例所在机同族,零新增入站口)。
七、故障与降级
| 情形 | 行为 |
|---|---|
| 插件回调超时 | 熔断该插件数据面调用;降级为普通聊天,房间消息流不受影响 |
| 超配额 | 返回明确错误码;插件功能不可用但状态可见(⛔ 不静默吞掉用户数据) |
| 迁移失败 | 拒绝启用,保留旧结构 |
| 某插件库不可用 | 只影响该插件;其他插件与内核照常(一插件一库的天然收益) |
| DB 主库不可用 | 房间读取按内核既有降级;插件写入直接失败并提示(⛔ 不落本地再补 = 会造成脑裂) |
八、与其他能力的衔接
- 房间 ACL:房间维度表的权限复用内核判定,⛔ 不新起一套文件/数据权限模型(与
files面同源)。 - 对象存储:大对象(图片 / 视频 / Excel)不进 DB,走
files面(sha256+bucket_key)⇒ 插件只拿file_id;插件表里只存引用。 - 缓存 / 队列:内核提供
im.cache(get/set/del + TTL)与im.queue(enqueue/lease/ack)端口;本期实现 = 内存 Map / PG 表,⛔ 插件不得自行引入 Redis / MQ 进程(宿主内存紧:实例硬顶 1024 MiB)。 - 版本兼容:
schemaVersion与插件包版本一并记录;降级安装(装旧版插件)⇒ 若旧版不认新结构,拒绝启用并提示,⛔ 不回滚数据(完整矩阵见 §四·补)。
八·补 —— 端侧边界声明(2026-09-23 落文 · S6-3)
一句话:端侧设备访问的是同一个平台 API,不另起一套数据面通路。
| 项 | 口径 |
|---|---|
| 看见 | ✅ 端侧(桌面壳 / 浏览器)调 GET /api/plugins/mine 与 GET /api/plugins/shared/catalog,与网页端同一份数据、同一套鉴权 |
| 开通 | ✅ 端侧调 POST /api/plugins/mine/apply,与网页端同一条路径、同一套后端门禁(数据面未就绪一律拒) |
| 本地装包 | ⛔ 不要求。端侧不下载、不安装插件包实体 —— 包在服务端共享只读库(D2),端侧只发请求 |
| 凭据下发 | ⛔ 不下发平台级凭据到客户端。端侧只持有该用户自己的会话;插件库连接由内核代持(§五-1),客户端拿不到库名、更拿不到连接串 |
| 数据落点 | 插件数据一律落服务端库(dshs_pl_<pluginId>);端侧本地不落插件数据(§六-5 的"跟用户走"指的是 home/.dsh/plugins/<id>/,那在服务端该用户的主目录里,不是端侧磁盘) |
| 离线 | ⚠️ 端侧离线时插件不可用(数据面在服务端)—— 这是"数据入库"口径的必然推论,⛔ 不是缺陷 |
⚠️ 现状读数(⛔ 不许当已知项推断):端侧插件投放通路 = 无(交接单 §9.4 实测),且桌面线客户端载体本身尚未产出
(序46 §⑬ 遗留 1 停在 client-artifact-missing)。本声明只定义"端侧该看到什么、不该拿到什么";
⛔ 造端侧载体、做端侧本地包分发不在本规范范围(后者属"跨节点内容分发",需单独立项)。
九、验收清单(可机械复现)
| # | 断言 | 期望 |
|---|---|---|
| 1 | 零裸 SQL / 零 DB 驱动 | 插件包内 CREATE TABLE、better-sqlite3、pg 零命中 |
| 2 | 双后端可建 | 同一声明在 sqlite 与 pg 上都建库建成 |
| 3 | 库与前缀 | 实际库名 dshs_pl_<pluginId>、表名全部 p_<pluginId>_*;不存在无前缀的插件表 |
| 4 | 越权 | 读别的插件库 / 控制面库 ⇒ 拒绝 |
| 5 | 归属判定 | scope: user 读他人行 ⇒ 0 行;scope: room 非成员读 ⇒ 0 行 |
| 6 | 迁移 | 加列成功;声明删列或改类型 ⇒ 拒绝启用(预期失败) |
| 7 | 配额 | 超限返回明确错误码,且无静默丢行(前后计数可对账) |
| 8 | 卸载 | 停用后数据仍在且可查;DROP 走确认流程 |
| 9 | 降级 | 插件数据面挂起 ⇒ 房间消息流照常、无 5xx |
| 10 | 库归属 | 新启用一个插件 ⇒ 只新增 dshs_pl_<pluginId> 一个库;插件触达不到其他库 |
| 11 | 归属列 | 每张业务表都有 user_id 或 room_id;缺列 ⇒ 拒绝启用 |
| 12 | 按用户可切分 | 用迁移器按某 user_id 导出 ⇒ 行数与"该用户在各库的行数之和"一致 |
十、待定
- 配额具体数值(总行数 / 单行字节 / 每房行数)⇒ 待压测标定,与档案 142 §六-2(房间上限与发言预算)同一批。
im.data是否提供聚合查询(groupBy/sum)还是只给count⇒ 先只给count(够用且不易被滥用),确有需要再评估。- 插件数据的跨房共享表(
scope: plugin)默认关闭,需 admin 审核后生效(与档案 142 §六-4 同一条口径)。 - 每插件库的连接池参数(池大小 / 空闲回收 / 全平台上限)⇒ 随架构定稿 S1 定稿;⚠️ 默认档用独立 database(库数 = 插件数),若插件数量增长到连接吃紧,降为同 database 内 schema 档(同一套代码,只改路由映射)。
- 用户注销时的归属行清理器(跨控制面库 + 各插件库 + 桶)⇒ 与统一迁移器同一批实现(S5)。