- 变更规模:新增 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/ 知识文件,按口径入库)
14 KiB
插件数据面 · 管理面流程修订(建库 / 更新)
工作区:
E:\ProgramData\AIProject\ai1net-dsh-server立稿:2026-09-22 18:0x | 本棒:插件投放与分库线 · 执行棒①续(automation6fb3f4b2) 归口:本件是过程稿,待落两处 ——交接单/插件投放与分库线-①共享只读包库与插件数据面.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)同族,抽成一个前端组件 + 一条后端约定:
dryRun⇒ 只回plan(⛔ 不动任何库、不动任何文件);- 执行时必须带
expect === planHash,服务端重新预演并比对 ⇒ 不一致 ⇒ 409 + 新 plan(防"展示完又换了包"); - 执行前自动落结构备份:
pg_dump --schema-only→/opt/dsh/backups/plugin-db/<pkg>/<schemaVersion>-<ts>.sql;备份失败 ⇒ ⛔ 不执行(B 档同款纪律); - DDL 在同一事务内执行;
CREATE DATABASE不可回滚 ⇒ 失败不 DROP,留空库 +status=error+last_error,等[重试]覆盖; - 全部记
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,且平台没有"文件被外部替换"检测)。
对「更新」设计的直接影响(必须落进实现):
- 判「有没有更新」只认 tgz 内容指纹(sha256)或包内
version,⛔ 不用池记录字段; business_plugins增列tgz_sha256,与共享层.manifest.json对账;不一致 ⇒ 标「已被外部替换」;- 上传钩子
upsertBusinessPlugin后回读校验(记录 version == 包内 version),不一致写审计告警; - 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 + 三处门禁 + 门户按钮 + 版本回读校验。