Files
dsh_ai1net_server/交付物/数据面管理面-建库与更新流程-20260922.md
T
admin ce8e6ceed9 chore(工作区): 纳入版本控制基线(回收 411 MB 过程产物)
回收 411 MB(470 M → 58.8 M),全部经回收站,可恢复:
- 待清理/(146.2 M,含 relay 分片 128 M 与 42 项过程目录)
- tmp/(32.4 M,按接续棒命名的过程临时区)
- .workbuddy/tmp/(39.5 M)
- 4 份 workbuddy.db 冗余副本(101 M,09-23 事故的坏副本 / 抢救产物)
- tmp/im16/gw/centrifugo 二进制(63.9 M,可重下)+ 缓存残留

入库范围:常驻规则(CODEBUDDY.md / README.md / state.py)、在途接续入口与
接续包、docs/、交付物/、交接单/、归档/、scripts/、.codebuddy/、
.workbuddy/memory/;共 398 件,其中 >60 KB 的 26 件全为文档。

排除(.gitignore):tmp/、待清理/、运行态日志与缓存、*.db 与 DB 备份整目录、
打包二进制(*.tar.gz / *.tgz)、记忆修复前备份。
2026-09-24 07:51:03 +08:00

216 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 插件数据面 · 管理面流程修订(建库 / 更新)
> **工作区**:`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` + 三处门禁 + 门户按钮 + 版本回读校验。