215 lines
14 KiB
Markdown
215 lines
14 KiB
Markdown
# 插件数据面 · 管理面流程修订(建库 / 更新)
|
||||
|
|
|
|||
|
|
> **工作区**:`E:\ProgramData\AI技能\aliyun-dsh-server`
|
|||
|
|
> **立稿**:2026-09-22 18:0x | **本棒**:插件投放与分库线 · 执行棒①续(automation `6fb3f4b2`)
|
|||
|
|
> **归口**:本件是**过程稿**,待落两处 —— `交接单/插件投放与分库线-①共享只读包库与插件数据面.md §五 S4/S5`(改写 D1/D3)+ `数据库/DB-03-插件数据面规范.md §三/§四/§六`(回改)。
|
|||
|
|
> ⚠️ 本轮**全局执行锁被 IM线-第2棒 占用**(17:53 起)⇒ 按 R9 **未落笔任何共享文件**,本件只在工作区产出。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一 一句话结论
|
|||
|
|
|
|||
|
|
把「建库」与「迁移」从**上传钩子里的隐式动作**改成 **admin 门户上的显式按钮**:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
上传 ─检测(安全 + 兼容 + 数据面声明)→ 通过 ⇒ [建库] ─成功→ [发布] ─→ 用户才能启用
|
|||
|
|
更新 ─检测→ 通过 ⇒ 展示「将执行的 N 条改动」→ [确认执行] ─成功→ 回到就绪
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
⛔ **取消** `§十-D1` 原写法「建库 = 平台侧上传钩子」—— 上传钩子**不再**自动 `CREATE DATABASE`。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二 用户口径 → 落位(逐条)
|
|||
|
|
|
|||
|
|
| 用户原话 | 落位 |
|
|||
|
|
|---|---|
|
|||
|
|
| 「上传后插件检测流程通过后,可以点击按钮建库」 | 「检测」新增第 3 项**数据面声明校验**(静态、零执行);三项全过 ⇒ 状态 `待建库` ⇒ `[建库]` 可点 |
|
|||
|
|
| 「上传后需要**建库成功才能开启**」 | 两道门禁(后端强制,⛔ 不只靠前端置灰):① `POST /api/plugins/business/:id/share`(发布)② `POST /api/plugins/mine/apply`(用户启用) |
|
|||
|
|
| 「插件还要增加**更新按钮**」 | 候选池行内 `[更新]` = 行内选包 → 走后端既有**同名替换**语义(`POST /api/plugins/business`) |
|
|||
|
|
| 「更新后也要检测」 | 替换走同一条检测链(校验对象 = 新包) |
|
|||
|
|
| 「检测通过后**告知数据库改动**」 | 上传响应新增 `dataPlane.diff`;门户把**逐条 DDL** 与影响(N 表 / M 列 / K 索引)列出来 |
|
|||
|
|
| 「也要有**按钮确认执行**」 | `[确认执行]` → `POST /api/plugins/business/:id/datastore/migrate`,带 `expect=planHash`(TOCTOU 防护) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三 状态机(行级 · 决定按钮显隐)
|
|||
|
|
|
|||
|
|
| 状态 | 判据 | 可点的按钮 | 能否发布 / 启用 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| `none` | 包内无 `dsh.data` | `[更新] [删除]` | ✅(无数据面,不受门禁约束) |
|
|||
|
|
| `pending` | 有声明、库不存在 | `[建库] [更新]` | ⛔ 拒绝(`datastore_not_ready`) |
|
|||
|
|
| `created` | 库已建、未发布 | `[发布] [更新]` | ⛔ 发布前置已满足;**用户**启用仍要等发布 |
|
|||
|
|
| `ready` | 库存在且库内 `schema_version` == 包声明 | `[更新] [删除]` | ✅ |
|
|||
|
|
| `drift` | 库存在但声明 ≠ 库现值 | `[查看改动] [确认执行]` | ⛔ 拒绝(避免旧代码撞新结构) |
|
|||
|
|
| `blocked` | 声明违反「只增不减」(删列 / 改类型 / 重命名 / 无默认值非空列) | `[更新]`(⛔ 无确认按钮) | ⛔ 拒绝,给明确错误 |
|
|||
|
|
| `error` | 上次建库/迁移失败 | `[重试]`+显示 `last_error` | ⛔ 拒绝 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四 检测新增第 3 项:数据面声明校验
|
|||
|
|
|
|||
|
|
既有检测两项(`business-plugins.ts:242-275`):**P0 安全扫描** + **兼容性预检**(`checkPluginCompat`)。
|
|||
|
|
新增第三项 = **声明解析 + 约束校验**(`scanDirDetailed` 之后、`return` 之前,同一 fail-closed 口径):
|
|||
|
|
|
|||
|
|
- 名字:表 / 列名 `^[a-z][a-z0-9_]{0,40}$`;表名前缀与 `pluginId` 一致;
|
|||
|
|
- 规模:每插件 ≤ 20 表、每表 ≤ 20 列;
|
|||
|
|
- 类型:只认 `DB-03 §三` 的 **9 种中性类型**;
|
|||
|
|
- 归属列:每表 `scope ∈ {user, room}` 必填;⛔ 声明里不许自带 `user_id`/`room_id`(由内核补);
|
|||
|
|
- 迁移禁令:与库现值比对后判 `blocked`(见 §七)。
|
|||
|
|
|
|||
|
|
⇒ 声明非法 ⇒ 检测不通过(409,同 `scan_blocked` 形态,逐条回显命中)⇒ **建库按钮不出现**。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五 声明格式(新增 · 静态落点)
|
|||
|
|
|
|||
|
|
**为什么必须静态**:门户要在**执行前**把 DDL 展示给 admin ⇒ 必须在不加载、不运行插件的前提下解析。
|
|||
|
|
`DB-03 §三` 写的 `im.data` 是**运行时 SDK 口**,跑不到;两者不一致 ⇒ 检测告警。
|
|||
|
|
|
|||
|
|
`package.json`(与既有 `dsh.bundle` / `dsh.client` 同族):
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
"dsh": {
|
|||
|
|
"bundle": { "patch": "./cordis.patch.yml" },
|
|||
|
|
"client": { "platform": "web", "inject": [] },
|
|||
|
|
"data": { "schema": "./dsh.data.yaml" }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`dsh.data.yaml` 内容照 `DB-03 §三` 原样(`schemaVersion` + `tables[]`),⛔ 不新造第二种格式。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六 API 契约(新增 / 改动)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
GET /api/plugins/business ← 池列表,每行新增 dataPlane:{state, schemaVersion, dbName, lastError}
|
|||
|
|
POST /api/plugins/business ← 上传/替换;响应新增 dataPlane:{declared, schemaVersion, plan, requiresMigration, blocked}
|
|||
|
|
POST /api/plugins/business/:id/datastore body {dryRun?, expect?} ← 建库(dryRun 只回 plan)
|
|||
|
|
POST /api/plugins/business/:id/datastore/migrate body {confirm:true, expect} ← 执行迁移
|
|||
|
|
POST /api/plugins/business/share | /:id/share ← 【既有】发布;新增建库门禁
|
|||
|
|
POST /api/plugins/mine/apply ← 【既有】新增建库+发布门禁
|
|||
|
|
GET /api/plugins/mine ← 【既有】每项新增 dataPlaneReady / dataPlaneState
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`plan` 结构(「告知数据库改动」的载荷):
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
{ planHash: string,
|
|||
|
|
items: [{ kind: 'create_database'|'create_table'|'add_column'|'add_index',
|
|||
|
|
target: string, sql: string, note?: string }],
|
|||
|
|
summary: { tables: n, columns: m, indexes: k },
|
|||
|
|
forbidden: [{ kind: 'drop_column'|'alter_type'|'rename'|'notnull_no_default', target, detail }] }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 七 两段式「预演 → 确认执行」(统一约定 · 三处复用)
|
|||
|
|
|
|||
|
|
与既有 `share` 的两段式(`conflict` + `confirm`)同族,抽成**一个前端组件 + 一条后端约定**:
|
|||
|
|
|
|||
|
|
1. `dryRun` ⇒ 只回 `plan`(⛔ 不动任何库、不动任何文件);
|
|||
|
|
2. 执行时必须带 `expect === planHash`,服务端**重新预演**并比对 ⇒ 不一致 ⇒ **409 + 新 plan**(防"展示完又换了包");
|
|||
|
|
3. 执行前自动落**结构备份**:`pg_dump --schema-only` → `/opt/dsh/backups/plugin-db/<pkg>/<schemaVersion>-<ts>.sql`;备份失败 ⇒ ⛔ 不执行(B 档同款纪律);
|
|||
|
|
4. DDL 在**同一事务**内执行;`CREATE DATABASE` 不可回滚 ⇒ 失败**不 DROP**,留空库 + `status=error` + `last_error`,等 `[重试]` 覆盖;
|
|||
|
|
5. 全部记 `plugin_data_audit`(谁 / 何时 / 哪个插件 / 影响条数)。
|
|||
|
|
|
|||
|
|
**同一个交互三处复用**:建库 · 迁移 · 彻底卸载(`DROP DATABASE`,`DB-03 §六-4` 本来就要求"先出受影响清单 + admin 显式确认")。
|
|||
|
|
⛔ 更新**永不** `DROP DATABASE`(`§五 S5-a-5` 既有裁决,不动)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 八 要改的文件
|
|||
|
|
|
|||
|
|
| 文件 | 改动 |
|
|||
|
|
|---|---|
|
|||
|
|
| `src/db/plugin-data/schema.ts`(新) | 声明解析 + 约束校验 + 中性类型 → PG DDL + `diff` |
|
|||
|
|
| `src/db/plugin-data/datastore.ts`(新) | `CREATE DATABASE`/迁移执行口 + 台账读写 + 结构备份 |
|
|||
|
|
| `src/db/schema.ts` | 新表 `plugin_datastores` + `plugin_data_audit`(追加迁移号) |
|
|||
|
|
| `src/db/{pg,sqlite,adapter}.ts` | 台账 CRUD(双后端) |
|
|||
|
|
| `src/web/routes/business-plugins.ts` | 检测加第 3 项;上传响应带 `diff`;3 个新路由;`/mine` 带状态;两处门禁;**版本回读校验**(§十) |
|
|||
|
|
| `web/portal.html` | 池表加「数据面」列 + 状态驱动按钮 + 预演清单面板(逐条 DDL) |
|
|||
|
|
|
|||
|
|
⚠️ `plugin_datastores` / `plugin_data_audit` **今天都不存在**(已实测:`information_schema` 零命中)⇒ 全新表。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 九 实测读数(本棒取证 · 47)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
pg_roles dshs: rolsuper=f rolcreatedb=f rolcreaterole=f
|
|||
|
|
postgres: rolsuper=t
|
|||
|
|
pg_database dshs -> owner=dshs ← dshs 拥有该库
|
|||
|
|
priv has_database_privilege('dshs','dshs','CREATE') = t ← 建 schema 今天就能做
|
|||
|
|
select count(*) from pg_database where datname like 'dshs_pl_%' => 0
|
|||
|
|
version PostgreSQL 13.23
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
⇒ **`dshs` 角色今天建不了库**(`rolcreatedb=f`),但**建 schema 不需要任何新权限**。见 §十一。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 十 🔴 本棒新发现:候选池记录的版本字段不可信
|
|||
|
|
|
|||
|
|
| 项 | 记录(`business_plugins`) | 磁盘实际(池内 tgz) |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `version` | **0.3.9** | **0.3.13**(包内 `package/package.json`) |
|
|||
|
|
| `file_size` | 1953267 | **1989680** |
|
|||
|
|
| `updated_at` | 2026-09-13 00:02:16 | **2026-09-20 22:13:45** |
|
|||
|
|
|
|||
|
|
- 池内 tgz sha256 = `d14cd5fe…` = 共享层 `.manifest.json` 的 `tgzSha256` = 备份 `/opt/dsh/backups/plugins/dsh-plugin-mcn-suite/0.3.9.tgz`(**同一份文件**,但备份**文件名带错版本号**)。
|
|||
|
|
- 对照:`@dsh-local/storyforge` 记录 `0.1.0 / 1754963` 与磁盘**完全一致** ⇒ 机制本身没问题。
|
|||
|
|
|
|||
|
|
**判据**:该 tgz 在 **09-20 被平台之外的方式替换过**(记录 `updated_at` 停在 09-13,且平台没有"文件被外部替换"检测)。
|
|||
|
|
|
|||
|
|
**对「更新」设计的直接影响(必须落进实现)**:
|
|||
|
|
1. 判「有没有更新」**只认 tgz 内容指纹(sha256)或包内 `version`**,⛔ 不用池记录字段;
|
|||
|
|
2. `business_plugins` 增列 `tgz_sha256`,与共享层 `.manifest.json` 对账;不一致 ⇒ 标「已被外部替换」;
|
|||
|
|
3. 上传钩子 `upsertBusinessPlugin` 后**回读校验**(记录 version == 包内 version),不一致写审计告警;
|
|||
|
|
4. B 档回滚素材文件名改用 **`<包内版本>-<sha256 前 8 位>.tgz`**(现名 `0.3.9.tgz` 会让"取上一版"取错)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 十一 🔴 阻断项(唯一需要拍板的一项)
|
|||
|
|
|
|||
|
|
「点按钮就能建库」今天**必然**要动 DB 权限。三条路,各有优劣:
|
|||
|
|
|
|||
|
|
**候选 A · `ALTER ROLE dshs CREATEDB`**
|
|||
|
|
优点:一条命令、零新增资产;`CREATE DATABASE` 不授予对**既有库**的任何权限;保住定稿的「一插件一库」(库级隔离、单库 `pg_dump` / 单库卸载都最干净)。
|
|||
|
|
缺点:平台 DB 角色从此可建**任意名字**的库(PG **没有**库名前缀级授权);属权限扩大,须记档 + 加「非 `dshs_pl_*` 新库」监控告警。
|
|||
|
|
|
|||
|
|
**候选 C · 改「一插件一 schema」(`dshs` 库内 `CREATE SCHEMA dshs_pl_*`)**
|
|||
|
|
优点:**零扩权、今天就能跑**(实测 `CREATE` 权限 = t);不新增库 ⇒ 连接数天然可控;`DB-03 §十` 已把它列为既定备选档,实现代价小("同一套代码,只改路由映射")。
|
|||
|
|
缺点:库级隔离降为 schema 级 —— 与控制面库同 catalog、同连接预算,插件侧跑飞更容易波及控制面;`pg_dump` 要改 `-n` 按 schema 导出;属**改已定稿口径**(`架构设计/数据-分库与权威存储架构.md` 写的是"一插件一库")⇒ 需回改定稿。
|
|||
|
|
|
|||
|
|
**候选 D · `SECURITY DEFINER` 建库函数 + `GRANT EXECUTE`**(一次性超级用户 DDL)
|
|||
|
|
优点:扩权面最小且**库名被强约束**(函数内白名单 `^dshs_pl_[a-z0-9_]{1,40}$`,违规直接 `RAISE`);可在函数内写审计;仍然「一插件一库」,**不动定稿**。
|
|||
|
|
缺点:多一份 DB 侧 SQL 资产(须进部署脚本 + 回滚清单 + 变更留痕);`CREATE DATABASE` 不能在事务里,`IF NOT EXISTS` 要改写成捕获 `duplicate_database`;函数属 postgres,将来改它要走 DBA 路径。
|
|||
|
|
|
|||
|
|
> 另有候选 B(sudo helper 脚本)—— 与 D 等效但走 OS 层:平台进程**本来就是 root**(能 `chown` / `setpriv` / 写 `/var/lib/dshs`),隔离收益近乎为零,却多一条 sudo 规则与一个脚本 ⇒ **自行删去**。
|
|||
|
|
|
|||
|
|
**✅ 已拍板 = D**(2026-09-22 19:3x 用户原话「D」)。理由:A 的"任意库名"缺口用一条 `SECURITY DEFINER` 函数即可补上,代价只是一份可纳管的 SQL 资产;D 同时保住定稿的**库级隔离**,而 C 用隔离性去换免扩权,降级的是**架构主判据**(比 D 的"多一份资产"代价更重)。
|
|||
|
|
|
|||
|
|
> 🔴 **2026-09-23 更新:D 档已被实测证伪,该拍板失效 ⇒ 用户当日改拍「B」档并已落地。**
|
|||
|
|
> **证据**:PG 显式禁止从函数内执行 `CREATE DATABASE` ⇒ `ERROR: CREATE DATABASE cannot be executed from a function`(`SECURITY DEFINER` 也不例外)。同姿势下 `CREATE SCHEMA` / `CREATE ROLE` 均成功 ⇒ 是**语句级内核限制**,非实现姿势问题。
|
|||
|
|
> **落地件**:`交付物/插件建库权限-B档落地-20260923.md`(`ALTER ROLE dshs CREATEDB` + 11 条验收通过 + 平台侧必须兜住的三点)。判定过程 ⇒ `交付物/建库权限-D档被证伪与修正判定-20260923.md`。
|
|||
|
|
> **对本文的影响**:§十一 的 D 档落地段作废;错误码**恢复为 `PG_CREATEDB_MISSING`**(D 档的 `PLUGIN_DB_HELPER_MISSING` 作废);§八 中建库口以外的部分**不受影响,仍可直接做**。
|
|||
|
|
> ⚠️ **B 档的真实缺口(须代码兜住)**:PG 无"只允许 `dshs_pl_*` 前缀"的原生机制 ⇒ 白名单只能靠平台代码 —— 库名单点生成 + 正则复校 + 每次建库落台账审计,三点缺一即为漏洞面。
|
|||
|
|
|
|||
|
|
📌 **落地脚本与 7 条验收 ⇒ 另见 `交付物/插件建库权限-D档落地Runbook-20260922.md`**(含前置实测读数、幂等 DDL、回滚、必须同步落的三处文档)。
|
|||
|
|
建库口的错误码相应改为 **`PLUGIN_DB_HELPER_MISSING`**(函数缺 / 无 `EXECUTE` 权 ⇒ 503),非法库名 ⇒ **400 `invalid_plugin_db_name`**;⛔ 不再使用 `PG_CREATEDB_MISSING`。
|
|||
|
|
⚠️ **本轮未执行** Runbook:全局锁被 IM线-第5棒 占用(19:33 起),动服务器受 R9 管辖。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 十二 本轮未做(具名原因,⛔ 未混进成功)
|
|||
|
|
|
|||
|
|
| 项 | 原因 |
|
|||
|
|
|---|---|
|
|||
|
|
| 落笔交接单 §五 S4/S5 改写、DB-03 回改 | `EXEC_LOCK_HELD_BY_IM_LINE_BATON2`(17:53 起,R9 不碰共享文件) |
|
|||
|
|
| §八 的代码实现与上线 | 同上(代码仓同锁)|✅ 权限已拍板 = D(19:3x),不再是阻塞项 |
|
|||
|
|
| D 档 Runbook 的**执行** | `EXEC_LOCK_HELD_BY_IM_LINE_BATON5`(19:33 起占用;动服务器受 R9 管辖)⇒ 脚本已备好,本轮未跑 |
|
|||
|
|
| S3 跨机装配 / S5-a 兼容矩阵 / S6 门户三段式 UI | 上一棒已登记未做,本棒未动 |
|
|||
|
|
|
|||
|
|
**下一棒直接可做(不依赖拍板)**:§八 中除建库口以外的全部 —— 声明解析 + 校验 + `diff` + 三处门禁 + 门户按钮 + 版本回读校验。
|