# 插件数据面 · 管理面流程修订(建库 / 更新) > **工作区**:`E:\ProgramData\AIProject\ai1net-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//-.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 档回滚素材文件名改用 **`<包内版本>-.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` + 三处门禁 + 门户按钮 + 版本回读校验。