Files
dsh_ai1net_server/交付物/数据面管理面-建库与更新流程-20260922.md
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

14 KiB
Raw Permalink Blame History

插件数据面 · 管理面流程修订(建库 / 更新)

工作区: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 同族):

"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 结构(「告知数据库改动」的载荷):

{ 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 + 三处门禁 + 门户按钮 + 版本回读校验。