Files
dsh_ai1net_server/交付物/S5-S6真机E2E验收-20260923.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

485 lines
20 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.
# S5 / S6 真机 E2E 验收 — 部署落地(第 8 棒)
> **线**:插件投放与分库线 | **棒次**:第 8 棒 · 执行棒 | **日期**:2026-09-23
> **执行依据**:`接续入口_插件投放与分库线_20260922.md` §0 最新行 + §5「本轮动作(第 8 棒)」
> **交接单**:`dsh-server-docs/交接单/插件投放与分库线-①共享只读包库与插件数据面.md` §五 S5 / §六
---
## 〇、总判定(先给结论)
| 项 | 判定 | 关键证据 |
|---|---|---|
| 部署到 47 + 106 | ✅ 完成 | 三机 md5 逐字一致 |
| **S5-E ①** v1→v2 升级、存量数据保留 | ✅ **PASS** | 升级前后均 3 行 |
| **S5-E ②** 强行降级 ⇒ 拒绝且库不变 | ✅ **PASS** | `409 plan_blocked` + 列/行未变 |
| **S5-E ③** 同名同版本重传 ⇒ 幂等 | ✅ **PASS** | `http=200`、tgz md5 与源一致 |
| **S5-E ④** 迁移前结构备份落盘 | ✅ **PASS** | 2 个 `.sql`(643 / 1344 字节)、0 字节文件 = 0 |
| **S5-E ⑤** 备份不可写 ⇒ 500 且库不变 | ✅ **PASS** | `500 schema_backup_failed` + `mkdir_failed: EPERM` |
| **S6-E ①** 非 admin `/mine` 条数 = 候选池全量 | ✅ **PASS** | 5 = 5 |
| **S6-E ②** 开关关 ⇒ 404(只记事实) | ✅ **PASS** | `404`(⛔ 未开开关) |
| **S6-E ③** admin 新增 ⇒ 用户刷新即可见 | ✅ **PASS** | 5 → 6 |
| 门户三段式 UI 逻辑自洽 | ✅ **PASS**(逻辑层 7/7) | 互斥 / 完备 / 语义正确 |
| **真机缺陷修复** | 🔴 **发现 1 个真实缺陷并已修** | `pg_dump` env 传递错误 ⇒ 见 §三 |
| 回归三门 | ✅ 全绿 | build rc=0 · 449/447/0/2 · `added: 0` |
**本轮净产出 = 修掉一个"备份机制在生产上根本跑不通"的真实缺陷**(本机测试测不出、只有真机 E2E 能暴露)。
---
## 一、部署与三机一致性
### 1.1 构建
```
$ cd D:/github/dsh_shenxian && npm run build
(无输出 = tsc 通过)
退出码 = 0
```
### 1.2 三机 md5 逐字一致(部署判据)
```
$ md5sum lib/db/plugin-data/datastore.js lib/web/routes/business-plugins.js
```
| 文件 | 本机 | 47 (`/opt/dshs/lib/...`) | 106 (`/opt/dshs-cluster/lib/...`) |
|---|---|---|---|
| `db/plugin-data/datastore.js` | `9fedd77c80812ec47160e9d413c8d0ab` | 同 | 同 |
| `web/routes/business-plugins.js` | `fe456d60c1f827db53fa1a54ba229591` | 同 | 同 |
| `config.js` | 同 | 同 | 同 |
| `web/portal.html` | `c4f012ce9916825935c0d166daafb2c0`(64894 字节) | 同 | — |
```
$ systemctl restart dshs # 47
退出码 = 0
```
### 1.3 v15 账本表仍在(第 6 棒落地的迁移)
```
$ psql -h 127.0.0.1 -p 15432 -U dshs -d dshs -Atc \
"select tablename from pg_tables where schemaname='public' order by 1;"
audit_log business_plugins credential_vault domains dsh_hosts dsh_instances
email_codes folder_plugins messages overlay_devices plugin_data_audit
plugin_datastores room_members rooms schema_migrations sessions users workspaces
退出码 = 0
```
⇒ `plugin_datastores` / `plugin_data_audit` 均在 ⇒ **v15 迁移未丢**。
### 1.4 部署前备份(可回滚)
```
/opt/dsh/backups/lib-s5s6/47-pre-s5s6-20260923-085319.tgz
/opt/dsh/backups/lib-s5s6/106-pre-s5s6-20260923-085319.tgz
```
---
## 二、S5-E ①–⑤ 真机 E2E
**探针插件**:`@dsh-local/s5probe`,表 `p_s5probe_notes`(scope `user`),内核自动注入 `created_at` / `updated_at`。
### 2.1 S5-E ① v1→v2 升级 + 存量数据保留 ⇒ PASS
```
═══ S5-E 真机 E2E v2(47 · Manager · PG 127.0.0.1:15432)═══
── S5-E ① v1 腿 ──
上传 v1 http=200 ver=1.0.0
建库 http=200 created=None
预演 v1 http=200 planHash=ebd541642cd3e9eb…
迁移 v1(建表) http=200
v1 列 user_id:text, created_at:bigint, updated_at:bigint, title:text, body:text
v1 表清单 p_meta_schema, p_s5probe_notes
写 3 行(直连插件库 · 数据由插件负责 · 补内核审计列)
INSERT 0 3
▶ rows_before_upgrade = 3 (期望 3)
── S5-E ① v2 腿(升级 · 只增不减)──
上传 v2 http=200 ver=2.0.0
预演 v2 http=200 items=['add_column'] forbidden=0
迁移 v2(加列) http=200
v2 列 user_id:text, created_at:bigint, updated_at:bigint, title:text, body:text, pinned:boolean
▶ rows_after_upgrade = 3 (期望 3 —— 存量数据原样保留)
① 升级后行数仍在 : 3 (期望 3) ⇒ PASS
退出码 = 0
```
### 2.2 S5-E ② 强行降级 ⇒ 409 且库未变 ⇒ PASS
```
── ② 强行降级(装回 v1 · schemaVersion 1 < 库现 2)──
降级前:台账=ready|2 | 列=user_id,created_at,updated_at,title,body,pinned
上传 v1(降级包) http=200 ver=1.0.0
预演(降级) http=200
items = []
forbidden = [{"kind": "drop_column", "target": "p_s5probe_notes.pinned",
"detail": "现状有此列、声明里没有 ⇒ 拒绝(DB-03 §四:不允许删列)"}]
executable= False
state = blocked
迁移(降级) http=409
error = plan_blocked
msg = 迁移违反「只增不减」:声明里出现了删列 / 改类型 / 重命名 / 无默认值非空列。请修正声明后重传。
降级后:列=user_id,created_at,updated_at,title,body,pinned | 行数=3
▶ 判据 ②(库未被改)⇒ PASS
退出码 = 0
```
⇒ **防降级护栏在真机生效**:预演即拦(`executable=false` / `state=blocked`),迁移 409,**列与行数一字未动**。
### 2.3 S5-E ③ 同名同版本重传 ⇒ 幂等 ⇒ PASS
```
重传 v2 http=200 tgz md5=0e69625bb1a1e626…(与源一致)
退出码 = 0
```
### 2.4 S5-E ④ 迁移前结构备份落盘(新判据)⇒ PASS
```
── ④ 迁移前结构备份落盘判据(/opt/dsh/backups/plugin-db/<pkg>/)──
total 16
drwx------ 2 root root 4096 Sep 23 09:13 .
drwx------ 3 root root 4096 Sep 23 09:13 ..
-rw------- 1 root root 643 Sep 23 09:13 v0-2026-09-23T01-13-10-233.sql
-rw------- 1 root root 1344 Sep 23 09:13 v1-2026-09-23T01-13-32-231.sql
▶ .sql 个数 = 2(≥2:v0 首建后 + v1 升级前)
▶ 0 字节文件数 = 0(期望 0)
▶ 判据 ④ ⇒ PASS
台账里的备份审计行:
datastore_migrate_backup_ok x2
退出码 = 0
```
⇒ 命名符合 `<v>-<ts>.sql`,**均为非 0 字节**,台账双行审计。
### 2.5 S5-E ⑤ 反证:备份不可写 ⇒ 500 且库未变 ⇒ PASS
> ⚠️ **诚实记录:本腿第一次尝试(`chmod 000`)FAILED**。
> 原因:平台以 **root** 运行,`chmod 000` **拦不住 root**(实测 `touch` 仍成功)。
> 第一次失败的原始输出(保留):
> ```
> 迁移(备份不可写) http=200 ⇒ FAIL
> ▶ 判据 ⑤(库结构未变)⇒ FAIL
> ```
> ⇒ 改用**只读文件系统**作反证载体(`DSH_PLATFORM_DIR=/sys/dshs-nope`)重跑。
```
═══ S5-E ⑤ 反证(备份根 → 只读文件系统)═══
① 对照:正常备份根 ⇒ 迁移应 200
migrate http=200 ⇒ PASS(对照成立的证明)
② 反证:把平台目录指到 /sys/dshs-nope(只读文件系统 ⇒ root 也写不进)
Environment="DSH_PLATFORM_DIR=/sys/dshs-nope"
active
反证前库列 = user_id,created_at,updated_at,title,body
migrate http=500 ⇒ PASS
error = schema_backup_failed
backupError = mkdir_failed: EPERM: operation not permitted,
mkdir '/sys/dshs-nope/backups/plugin-db/_dsh-local_s5probe'
反证后库列 = user_id,created_at,updated_at,title,body
▶ 判据 ⑤a(500 schema_backup_failed)⇒ PASS
▶ 判据 ⑤b(库结构未变) ⇒ PASS
③ 复原:还原 platform-dirs.conf → daemon-reload → restart
Environment="DSH_PLATFORM_DIR=/opt/dsh"
active
退出码 = 0
```
**环境复原已复验**:
```
Environment="DSH_PLATFORM_DIR=/opt/dsh" ← drop-in 唯一载体
(无 .bak-s5e 残留 ✓)
```
> ⚠️ **踩坑记录**:第一次尝试新建第二个 drop-in `s5e-counterproof.conf` 会被**静默覆盖**(同名键后写者胜),故改为 sed 编辑**唯一载体** `platform-dirs.conf` 并原样还原 —— 与记忆中的「drop-in 才是唯一载体」一致。
---
## 三、🔴 本轮发现的真实缺陷(已修 + 已回归钉住 + 已重部署)
### 3.1 现象
S5-E ④ 在真机上**根本跑不通** —— 结构备份永远失败:
```
pg_dump: error: connection to database "dshs_pl_s5probe" failed:
could not connect to server: No such file or directory
Is the server running locally and accepting connections on
Unix domain socket "/var/run/postgresql/.s.PGSQL.5432"?
```
### 3.2 根因(受控复现确认)
`src/db/plugin-data/datastore.ts` 里 `pgEnvFrom(baseUrl)` 的返回值被**展开在 `execFileSync` 的 options 层**(与 `env:` 同级),而不是**合进 `env`**:
```ts
// ❌ 缺陷写法:PGHOST/PGPORT/PGUSER 变成了"选项名"而非环境变量
execFileSync('pg_dump', args, { ...pgEnvFrom(baseUrl), env: {...}, timeout: 120_000 })
```
⇒ `pg_dump` 子进程**收不到** `PGHOST`/`PGPORT`/`PGUSER` ⇒ 退回默认 **Unix socket `:5432`** ⇒ 47 上 PG 实际在 `127.0.0.1:15432` ⇒ 连接失败。
**本机测试测不出**:本机没有 `pg_dump`,该路径根本不会被走到;只有真机 E2E 能暴露。
### 3.3 修复
```ts
env: pgDumpEnv(baseUrl, password),
timeout: 120_000,
```
```ts
export function pgDumpEnv(baseUrl: string, password: string): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {
...process.env,
PGPASSWORD: password,
PGCONNECT_TIMEOUT: '15',
}
try {
const u = new URL(baseUrl)
if (u.hostname !== '') env.PGHOST = u.hostname
if (u.port !== '') env.PGPORT = u.port
if (u.username !== '') env.PGUSER = decodeURIComponent(u.username)
} catch { /* 连接串不可解析 ⇒ 保留 ...process.env */ }
return env
}
```
**顺带修一处不实文案**:`business-plugins.ts` 里第 4 棒遗留的 `backup: 'not_implemented'` 未随第 7 棒接线更新,响应改为真实值:
```ts
backup: backupPathOrNull === null ? 'skipped_first_create' : 'taken',
backupPath: backupPathOrNull,
```
### 3.4 回归钉住(`test/plugin-data.test.mjs`,+2 用例)
```
$ npm test
⬇ 见 §四
```
> ⚠️ 该用例第一版尝试用 PATH 假 `pg_dump` 桩,**Windows 上失败**(`execFileSync` 无法解析 PATH 里的 `.cmd`/`.bat`:`ENOENT` / `EINVAL`)⇒ 改为**直接断言不变量**(env 必须含四个 PG 变量、且⛔ 不得混进 `stdio`/`timeout`/`cwd`/`encoding` 这些选项名 —— 正是原缺陷的特征)。
### 3.5 重部署 + 复验
修后重构建、重部署到 47/106,`datastore.js` md5 `9fedd77c80812ec47160e9d413c8d0ab` **三机一致**,重启后 ④ 腿备份**成功落盘**(643 / 1344 字节)⇒ 缺陷闭环。
---
## 四、回归三门(改完 → 回归 → 才放锁)
| 门 | 命令 | 原始输出 | 退出码 |
|---|---|---|---|
| 编译 | `npm run build` | 无输出(tsc 过) | **0** |
| 全量测试 | `npm test`(Node 22.22.2) | **449 tests / 447 pass / 0 fail / 2 skipped** | **0** |
| 分层 | `node scripts/check-layering.mjs` | `✅ 无新增违规`(`added: 0`) | **0** |
> 基线 **447 / 445 pass / 0 fail / 2 skipped** ⇒ 本轮 **+2**(新增的 `pgDumpEnv` 不变量用例),**失败数仍为 0**。
---
## 五、S6-E 真机 E2E
### 5.1 S6-E ① 非 admin `/mine` 条数 = 候选池全量 ⇒ PASS
```
── ① 非 admin 用户 GET /api/plugins/mine 条数 = 候选池全量 ──
admin /api/plugins/business http=200
guest /api/plugins/mine http=200
▶ 候选池全量 = 5 | guest 可见 = 5
▶ 判据 ①(条数一致)⇒ PASS
退出码 = 0
```
### 5.2 S6-E ② 开关关 ⇒ 404(🔴 只记事实,⛔ 未开开关)⇒ PASS
```
── ② GET /api/plugins/shared/catalog —— 🔴 只记事实,⛔ 不打开开关 ──
admin http=404
guest http=404
▶ 开关 DSHS_SHARED_CATALOG_PUBLIC 当前值: (未设置 = 默认 false)
▶ 判据 ②(关着 ⇒ 404,⛔ 不是空列表)⇒ PASS
⛔ 本棒未打开该开关(属 §1「扩大可见面」红线门禁,须用户拍板)
退出码 = 0
```
> 🔴 **红线遵守声明**:`DSHS_SHARED_CATALOG_PUBLIC` 属「扩大可见面」红线门禁 ⇒ **本棒全程未开**,仅记录"关着 ⇒ 404"这一事实。
### 5.3 S6-E ③ admin 新增 ⇒ 用户刷新即可见 ⇒ PASS
```
── ③ admin 新增插件 ⇒ 非 admin 刷新即可见(无需任何管理动作)──
新增前 guest 可见 = 5
上传新插件 http=200
ok=True name=@dsh-local/s6newprobe
新增后 guest 可见 = 6 | 含新插件 = True
▶ 判据 ③(刷新即可见 · 无需管理动作)⇒ PASS
▶ 判据 ③b(条数 +1)⇒ PASS
退出码 = 0
```
### 5.4 S6-E ② 深挖:enable 落点(跨机形态真实发现)
> **背景**:客用户(`guest` / uid `100002`)实例**在 106(worker)**,不在 47。
> `/api/plugins/mine` 读的是**平台本机** `profiles`(47 上为空)⇒ 字面判据 `enabled:true` **未闭合**。
**实际 enable 确实成功,且正确落到实例所在机 106**:
```
POST /api/plugins/mine/apply http=200
{"ok":true,"taskId":"023a87d692d07654"}
--- 106 上 guest 的 node_modules 出现 storyforge 软链,且指向共享层 ---
lrwxrwxrwx 1 dsh-eeccbc638afc46bdb663 100002 123 Sep 23 09:43
.../home/profiles/web/node_modules/@dsh-local/storyforge
-> ../.pnpm/@dsh-local+storyforge@file+..+..+..+..+..+bundled-plugins+_dsh-local_storyforge/node_modules/@dsh-local/storyforge
--- 该软链可解到 package.json --- ✓ 可解
--- 106 侧 package.json 已写入 ---
"@dsh-local/storyforge": "file:/var/lib/dshs/bundled-plugins/_dsh-local_storyforge"
▶ 判据(装配真落到实例所在机)⇒ PASS
```
**判定**:**不是代码缺陷,是拓扑形态的必然结果** ——
`/mine` 的 `enabled` 字段取自**平台本机 profile 快照**;当用户实例在 worker 上时,本机快照**必然**反映不了远端装配。
**旁证(沙箱命令行原文,证明共享层已 ro 绑进实例)**:
```
dsh-100002-9648cef4.scope loaded active running /usr/bin/bwrap
... --ro-bind-try /var/lib/dshs/bundled-plugins /var/lib/dshs/bundled-plugins
... -- setpriv --reuid 100002 --regid 100002 /usr/bin/dsh --profile web --host 127.0.0.1 --port 21000
```
⇒ **S1 共享只读层挂载成立**(`--ro-bind-try`),且实例确实在跑。
**该发现的价值**:这是 S3「跨机装配」要解决的真实形态问题 —— **平台侧的"用户可见启停状态"需要一个跨机回传通路**(当前只有单向 apply,没有状态回流)。**建议单独立项**,本轮只记录、不改架构。
---
## 六、门户三段式 UI 观察
> ⚠️ **本机 `agent-browser` 每次调用均挂死**(8 分钟无输出;Chromium 进程起了但 `open` 不返回)—— 属环境限制,按技能指引 fail-fast 处理(已 `agent-browser close` + 清理 chrome 进程)。
> ⇒ 改用**等价判据**:`ui-logic-check.mjs` 用**真实 API 数据**复跑 `portal.html` 里 `loadVisibility()` 的三段派生逻辑。
派生逻辑(与 `portal.html` **逐字一致**):
```js
const shared = (catalog && catalog.plugins) || []
const sharedIds = new Set(shared.map((p) => p.id))
const enabled = mine.filter((p) => p.enabled)
const disabled = mine.filter((p) => !p.enabled && sharedIds.has(p.id))
const unavailable= mine.filter((p) => !p.enabled && !sharedIds.has(p.id))
```
结果:
```
═══ S6 三段式 UI 逻辑评估(真实 API 数据)═══
catalogHttp: 404 | mineHttp: 200
① shared (平台共享只读): 0
② enabled (已启用): 2
③ disabled (已停用): 0
⋯ unavailable (暂不可用): 4
mine 总数: 6
switchOff (catalog===null ⇒ 显示开启提示): true
--- 断言 ---
A. 互斥(两两不相交): true
B. 完备(②+③+⋯ = mine 总数): true 「6 vs 6」
C. ② 全为 enabled:true: true
D. ③ 全部在共享层内(能装): true
E. ⋯ 全部不在共享层内(装不到): true
F. 开关关 ⇒ ③=0(⛔ 不能把池里的都算"用户没开"): true
G. 开关关 ⇒ ⋯ = mine 里未启用的全部: true
═══ 总判定:✅ 三段自洽/互斥/完备 ═══
退出码 = 0
```
**⚠️ 未验项(具名)**:CSS 视觉渲染、tab 点击切换 —— 需真浏览器,本机环境不具备。
---
## 七、清理与复原(零残留复验)
| 对象 | 处置 | 复验 |
|---|---|---|
| 探针库 `dshs_pl_s5probe` | `DROP DATABASE` | `pg_database like '%probe%'` = **0** ✅ |
| 候选池 `@dsh-local/s5probe` / `s6newprobe` | 删行 + 删 tgz | 池子恢复 = **原始 4 个** ✅ |
| 账本 `plugin_datastores` | 删探针行 | `like '%probe%'` = **0** ✅ |
| 审计 `plugin_data_audit` | 删探针行 | `like '%probe%'` = **0** ✅ |
| 47 备份目录 `plugin-db/_dsh-local_s5probe` | 删 | 目录不存在 ✅ |
| 共享层 `bundled-plugins/*probe*` | 删(**保留** `_dsh-local_storyforge` / `dsh-plugin-mcn-suite`) | 恢复 2 项 ✅ |
| `platform-dirs.conf` | sed 改 → 原样还原 | `DSH_PLATFORM_DIR=/opt/dsh` + **无 `.bak-s5e`** ✅ |
| **106 客实例 `storyforge` 启用** | **已回退**(见下) | ✅ |
**106 客实例回退明细**(还原到我介入前的 4 依赖状态):
```
dependencies: ['@dsh-local/business-plugins', '@dsh-local/portal-entry',
'@dsh-local/workspace-scoped-picker', '@softspark/dsh-file-preview']
bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app',
'@dsh-local/business-plugins', '@dsh-local/portal-entry',
'@dsh-local/workspace-scoped-picker', '@softspark/dsh-file-preview']
✓ package.json 已无 storyforge
✓ symlink 已除
✓ .pnpm 已除
```
**106 健康复验**:
```
dshs-worker.service = active
dshs-relay.service = active
dsh-100002-9648cef4.scope = active running
```
---
## 八、⛔ 未做的事(具名原因)
1. **装配协议 `file:` vs `link:`** ⇒ `awaiting-user-decision`,**本轮零改动**(⛔ 未碰 `plugin-assembly.ts` 协议语法、⛔ 未改交接单里 `file:` 相关描述)。
2. **可见面开关 `DSHS_SHARED_CATALOG_PUBLIC`** ⇒ 属 **§1「扩大可见面」红线门禁**,**未开**,只记录 404 事实。
3. **跨节点内容分发** ⇒ 无平台侧通路(106 共享层手工同步),**需单独立项**。
4. **S6-E ② 字面判据(`enabled:true`)未闭合** ⇒ 原因是**拓扑形态**(实例在 106)而非代码缺陷;已用 106 侧文件证据闭合。**平台侧用户可见启停状态的跨机回流 = 缺失能力,建议立项。**
5. **门户 UI 的 CSS 渲染 / tab 点击** ⇒ 本机 `agent-browser` 挂死,环境不具备;已用逻辑层等价判据覆盖。
6. ⛔ 未 commit / 未 push / ⛔ 未引新依赖 / ⛔ 无 merge / ⛔ 未跑全库 Glob/Grep。
---
## 九、技术附录
### 9.1 测试资产落点
| 文件 | 用途 |
|---|---|
| `tmp/s5e/pkg-v1/` · `pkg-v2/` | 探针插件声明(v1:`title`/`body`;v2:+`pinned`) |
| `tmp/s5e/v1.tgz` · `v2.tgz` | 476 / 523 字节 |
| `tmp/s5e/driver.sh` · `driver2.sh` · `driver3.sh` · `driver5.sh` · `driver6.sh` | 各腿驱动 |
| `tmp/s5e/ui-logic-check.mjs` | 三段式逻辑评估 |
| `tmp/s5e/*.out` | 全部原始输出(本报告引用的每一段均出自此处) |
### 9.2 关键 API 契约(本轮实测确认)
| 端点 | 方法 | 本轮实测 |
|---|---|---|
| `/api/plugins/business` | GET | 200 · 候选池全量 |
| `/api/plugins/mine` | GET | 200 · 含 `enabled` 字段 |
| `/api/plugins/mine/apply` | POST | 200 · `{ok:true, taskId}`(**异步**) |
| `/api/plugins/shared/catalog` | GET | **404**(开关关) |
| `/datastore/plan` | POST | 200 · 返回 `items` / `forbidden` / `executable` / `state` |
| `/datastore/migrate` | POST | 200 / **409 `plan_blocked`** / **500 `schema_backup_failed`** |
### 9.3 表名/列名实况(订正我的两次误判)
- 候选池表 = **`business_plugins`**(**无** `user_id`,全局池)—— 不是 `user_plugins`。
- 账本列 = `plugin_datastores`(**`state`** / `schema_version` / `plan_hash` / `last_error`) —— **无** `status` 列。
- **不存在** `plugin_tasks` 表 ⇒ 异步任务状态**在内存**,落库不可查。
- 探针表实名 = **`p_s5probe_notes`**(`p_<pluginId>_<tableName>`);内核**强制**注入 `created_at`/`updated_at BIGINT NOT NULL`。
---
**本报告全部数字均出自原始输出文件,未做任何"看起来应该对"的推断。**