回收 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)、记忆修复前备份。
111 lines
6.7 KiB
Markdown
111 lines
6.7 KiB
Markdown
# 建库口接线(B 档)· 落地件 — 2026-09-23
|
||
|
||
> **归口**:插件投放与分库线 · **第 4 棒 · 执行棒**(automation `6e8cedcd`)。
|
||
> **前置件**:`交付物/数据面管理面-建库与更新流程-20260922.md §八`(设计件)+
|
||
> `交付物/插件建库权限-B档落地-20260923.md`(B 档权限与契约)。
|
||
> **状态**:✅ 代码面已落地 + 47 真机取证通过 + 服务器零残留。
|
||
|
||
---
|
||
|
||
## 一 一句话结论
|
||
|
||
设计件 §八 里**最后一个没做的口**(建库执行口 `datastore.ts`)已接线:`CREATE DATABASE` 真调用、
|
||
`currentSchemaOf` 去占位真读 `information_schema`、台账+审计落点、`migrate` 去 503、
|
||
台账 × `pg_database` 对账告警。**B 档"三点兜底"全部由代码兑现**。
|
||
|
||
---
|
||
|
||
## 二 落地清单(逐条对 §5 本轮动作)
|
||
|
||
| # | 要求 | 落点 | 状态 |
|
||
|---|---|---|---|
|
||
| 1 | `CREATE DATABASE` 真调用(单语句、⛔ 不走事务) | `datastore.ts:createPluginDatabase`(`pool.query` 隐式自动提交,⛔ 不包 `BEGIN`) | ✅ |
|
||
| 2 | `currentSchemaOf` 去占位 | `datastore.ts:readCurrentSchema`(真读 `information_schema.columns` + `pg_indexes` + `p_meta_schema`);路由层 `currentSchemaOf` 改调它 | ✅ |
|
||
| 3 | 台账 + 审计落点 | 建库成功 `state='created'`;失败 `state='error'` + `last_error`;两步都 `recordPluginDataAudit` | ✅ |
|
||
| 4 | `migrate` 去 503 | `datastore.ts:executePlan`(建库项非事务/表级 DDL **同一事务**)+ 路由真执行 | ✅ |
|
||
| 5 | 台账 × `pg_database` 对账 | `datastore.ts:reconcilePluginDatabases` + `GET /api/plugins/business/datastores/reconcile` | ✅ |
|
||
|
||
### 关键实现点(三条最容易写错的)
|
||
|
||
1. **建库的语句姿势** —— `CREATE DATABASE` 走 `pool.query()`(implicit autocommit),
|
||
**⛔ 绝不 `withTx`**。这是 D 档被证伪的同一条内核限制(`cannot be executed from a function`
|
||
/ 不能进事务块),B 档只是不再需要函数封装。
|
||
2. **库名的两重防线** —— ① `pluginDbNameOf` 生成(⛔ 不收请求体里的库名);
|
||
② `assertPluginDbName` 在**拼 SQL 之前**复校,再加 `quoteIdent`(`"` 翻倍)标准引用。
|
||
单测有一条注入形态(`x'; DROP DATABASE dshs; --`)专门钉这条。
|
||
3. **`plan_hash` 的 `COALESCE` 语义** —— 建库 / 迁移路径一律**不传** `planHash`(omit ⇒ 保留旧值),
|
||
⛔ 传 `null` 会把预演写下的指纹清掉 ⇒ 等于拆掉 TOCTOU 防护。真机取证已断言保留成立。
|
||
|
||
### 已知缺口(⛔ 不假装做了)
|
||
|
||
| 缺口 | 原因 | 何时补 |
|
||
|---|---|---|
|
||
| **迁移前结构备份**(设计件 §七-3 `pg_dump --schema-only`) | 属运维面(`pg_dump` + `/opt/dsh/backups/plugin-db/`),与 S5-a 同批 | 归入「S5-a 兼容矩阵 / 运维件」棒 |
|
||
| **47 控制面停在迁移 v13** | v14/v15(含两张台账表)要平台启动时由 `runPgMigrations` 落;本棒未部署 47 | **部署本棒代码时随 `restart dshs` 自动补** |
|
||
|
||
风险等价性说明:缺口下的执行失败靠**事务回滚**兜(表级 DDL 全在同一事务,失败即整体回退),
|
||
且「只增不减」已由 `forbidden` 把死 ⇒ 破坏面 = 只增结构(可人工 `DROP`)。
|
||
|
||
---
|
||
|
||
## 三 47 真机取证(用**真实编译产物**,非手写 SQL)
|
||
|
||
机位:47(`/opt/dshs` · PG 13.23 `127.0.0.1:15432`)。探针 `tmp/evid47/probe2.mjs`。
|
||
|
||
```
|
||
created : "exists" ← 第二次跑(首次已建有库);首跑读数 = "created",71 ms
|
||
idempotent : "exists" ← 重复建库幂等,⛔ 不报错
|
||
badNameRejected : "invalid_plugin_db_name" ← evil_probe4 被平台侧拦下,⛔ 未下发 PG
|
||
currentRead : { exists: true, schemaVersion: 7,
|
||
tables: ["p_bselftest4_probe:1", "p_meta_schema:2"] } ← 去占位:真表真列真版本
|
||
ledgerRow : { db_name: "dshs_pl_bselftest4", state: "ready", schema_version: 7 }
|
||
planHashPreserved : true ← COALESCE 语义成立(第二次 upsert 未传 planHash 仍保留)
|
||
auditRows : 1 ← 审计流水落了
|
||
reconcile.orphans : ["dshs_pl_borphan4"] ← 🔴 绕过平台建的库**被抓出**
|
||
reconcile.missing : []
|
||
cleaned : { pgPluginDbsLeft: 0, controlTablesLeft: 13, expectedControlTables: 13 }
|
||
```
|
||
|
||
**残留核查(独立于探针,清理后另跑一次)**:
|
||
`pluginDbs: []` | 控制面表 = 原始 13 张(`plugin_datastores` / `plugin_data_audit` 均已 DROP)
|
||
| `systemctl is-active dshs` = `active` | `/tmp/evid4` 已删。
|
||
|
||
> ⚠️ **取证姿势的一条实测**:探针第一次跑失败了(在台账步骤,因 47 无 v15 表),
|
||
> 而那次失败**跳过了清理** ⇒ 测试库 `dshs_pl_bselftest4` 留在了 PG 上。
|
||
> 第二次跑把清理放进 `catch` 兜底后重跑,才回到零残留。
|
||
> **教训**:真机取证的清理必须写在 `catch` 里(失败路径也会留东西),⛔ 只在成功路径清。
|
||
|
||
---
|
||
|
||
## 四 验证(本机三闸)
|
||
|
||
| 闸 | 判据 | 实测 |
|
||
|---|---|---|
|
||
| 构建 | `npm run build` rc=0 | **rc=0** |
|
||
| 回归 | `npm test`(Node 22)0 败 | **426 tests / 424 过 / 0 败 / 2 跳过**,rc=0(基线 391 过 ⇒ 本棒 **+35**) |
|
||
| 分层 | `node scripts/check-layering.mjs` 无新增违规 | **✅ 无新增违规**(`added: 0`) |
|
||
|
||
`npm run verify` 未跑(按本线惯例它必红且与本线无关)。
|
||
|
||
---
|
||
|
||
## 五 改了哪些文件(本棒)
|
||
|
||
| 文件 | 改动 |
|
||
|---|---|
|
||
| `src/db/plugin-data/datastore.ts`(**新 · 479 行**) | 建库口 / 现状真读 / 计划执行 / 对账 / 错误码映射 / 连接管理 |
|
||
| `src/web/routes/business-plugins.ts` | `currentSchemaOf` 去占位;新增 `POST /datastore`(建库)+ `GET /datastores/reconcile`;`/datastore/plan` 的 `executable` 改为真判据;`/datastore/migrate` 去 503 真执行;控制面连接池(懒创建 + `onClose`) |
|
||
| `web/portal.html` | 「建库」按钮改调新路由(原为"尚未接线"禁态);`createDatastore()`;「确认执行」禁用文案改由服务端原因驱动 |
|
||
| `src/db/plugin-data/diff.ts` | 仅注释:去掉"建库口尚未接线"的过时说明 |
|
||
| `test/plugin-data.test.mjs` | **+35 条**:引用转义 / `pluginDbUrlOf` / 错误码分类 / 台账表 DDL / 对账集合逻辑 |
|
||
|
||
---
|
||
|
||
## 六 下一棒(仍未做,⛔ 本棒不预登记队列)
|
||
|
||
1. **三份文档回写** —— 交接单 §五 S4-2 / §十-D1、`DB-03 §三/§四/§六`、`DEPLOY-本部署.md`
|
||
(追加 `ALTER ROLE dshs CREATEDB;`)。共享文件,须持锁成批做。
|
||
2. **S3 跨机装配 · S5-a 兼容矩阵 · S6 门户三段式 UI**(执行棒①遗留)。
|
||
3. **跨节点内容分发**。
|
||
4. **迁移前结构备份**(本棒记的已知缺口,见 §二)。
|