Files
dsh_shenxian/dsh-server-docs/03-数据库/DB-03-插件数据面规范.md
T
admin e6207aa691
build / build-and-scan (push) Waiting to run
chore(仓库对齐): 文档库结构治理 + IM/插件线落地
文档库:目录改为编号制(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/;交接单不入库(政策)。
2026-09-24 07:25:16 +08:00

26 KiB
Raw Blame History

插件数据面规范(插件如何接入数据库)

状态:✅ 已落文档库,本文件即插件数据面的唯一正式来源 —— 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>。

二、红线(违反即不予启用)

  1. ⛔ 插件不得直连数据库:不持有连接串 / 不 import 数据库驱动 / 不自建连接池。

  2. ⛔ 插件不得写原生 SQL:无 CREATE TABLE / ALTER / DROP / SELECT;无触发器、存储过程、厂商专有类型。

  3. ⛔ 插件不得访问其他插件的库:看不到也查不了别人的 dshs_pl_*、控制面库与共享数据。

  4. ⛔ 插件不得自组跨库事务(PG 跨 database 无原生事务)。

  5. ⛔ 插件不得绕过归属判定:scope: user 只能读写自己的行;scope: room 由内核强制注入成员校验。

  6. ⛔ 插件不得以实例本地库 / 本地文件作为数据权威(可作为缓存,权威必须在插件库)。

判据(可机械验证):插件包内 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 标准引用。非法 ⇒ 400 invalid_plugin_db_name,⛔ 不下发 PG。
  • 幂等:已存在 ⇒ 返 exists(捕获 PG 42P04 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)。
  • 任一步失败 ⇒ 拒绝启用(⛔ 不做"半迁移"),旧结构保持可用、可回滚。
  • ⚠️ 表级 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

三条硬规则:

  1. 降级不自动发生:schemaVersion 回退 ⇒ 状态 drift,executable=false ⇒ 前端「确认执行」置灰、后端 POST /datastore/migrate 也拒。给用户的错误必须可读(原话:不写"未达预期"了事),且库一个字节都不改 —— 旧版代码若认不出新列,忽略即可(与 §八 一致)。
  2. ⛔ UPDATE ≠ DROP:更新路径永不 DROP DATABASE;卸载才走 §六-4(清单 + admin 确认 + 默认保留 30 天)。
  3. 矩阵的机器判据 = 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 })

语义要点:

  1. 库路由由内核决定:插件只报表名,内核按调用方插件身份路由到 dshs_pl_<pluginId>;插件看不到库名,也拿不到别人的库。
  2. 归属维度自动注入判定:scope: user 的表每次读写附加「调用者 == user_id」校验;scope: room 附加「调用者是该房成员」(复用内核 canSee)⇒ 越权得到 0 行 / 明确拒绝,不是"查得到但看不到"。
  3. 同库内要原子 ⇒ 走内核事务口:im.data.tx([...]) 仅支持同一个插件库内的多表;跨库(如同时改房间属性 + 自己的数据)没有事务 ⇒ 必须拆成幂等步骤 + 补偿,⛔ 不许靠双写。
  4. 读写路由:写、以及"刚写就读"一律走主库;⛔ 不从只读副本读自己的新行(副本延迟会破坏读己所写)。
  5. 限流:插件写操作吃与 agent 同族的预算,并受房间级速率上限约束(142 §3.6 保护约束 3)。
  6. 分页:一律游标,不用 OFFSET(大偏移在 pg 上会退化)。

六、管理面

  1. 配额(防单插件拖垮库):每插件 总行数 + 单行字节 + 每房行数 三重上限(库维度天然对齐"一插件一库");超限返回明确错误码(⛔ 不静默丢、⛔ 不静默截断),并在插件卡片上可见。
  2. 审计:DDL、迁移、超阈值批量写记录进内核表 plugin_data_audit(谁 / 何时 / 哪个插件 / 影响行数);空间占用进平台看板。
    • 🔴 两张台账表 = 控制面 v15 迁移新增(2026-09-23 落地):
      表 键 作用
      plugin_datastores plugin_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)。
  3. 可观测:每插件暴露「行数 / 体积 / 最近写入」三项,进"功能管理"页。
  4. 卸载:停用插件 ⇒ 数据默认保留 30 天(可配置);窗口内可恢复。 彻底卸载 = DROP DATABASE dshs_pl_<pluginId>(一次带走该插件全部表,比逐表 DROP 干净)⇒ 属不可逆 ⇒ 必须 admin 显式确认 + 先出受影响清单(库名 / 表 / 行数 / 体积),⛔ 不做"卸载即删"。
  5. 备份 / 迁移(2026-09-22 新增):单插件备份 = pg_dump dshs_pl_<pluginId>;按用户导出/迁移 = 统一迁移器按归属列遍历(控制面库 + 各插件库 + 桶清单)⇒ 一次遍历、可重入、可对账。⛔ 用户注销不 DROP 插件库(库属插件、不属用户),只删该 user_id 的行 + 回收桶内对象。
  6. 禁用后的行为:插件无关;房间照常(数据面异常不得影响消息流,见 §七)。

六·补 —— 插件文件落点与凭据投递(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)。