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/;交接单不入库(政策)。
279 lines
26 KiB
Markdown
279 lines
26 KiB
Markdown
# 插件数据面规范(插件如何接入数据库)
|
||
|
||
> 状态:✅ **已落文档库,本文件即插件数据面的唯一正式来源** —— `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 无)。
|
||
|
||
示例(跑团插件):
|
||
|
||
```yaml
|
||
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(唯一入口)
|
||
|
||
```ts
|
||
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)。
|