Files
dsh_ai1net_server/交付物/StoryForge端到端验收-未通过-20260925.md
T

295 lines
19 KiB
Markdown
Raw Normal View History

# StoryForge 端到端验收 · 真机读数(**未通过**)
> **验收线**:`接续入口_StoryForge验收线_20260925.md` 第 1 棒
> **验收容器**:平台侧(模拟 admin 操作)|**对象**:`@dsh-local/storyforge 0.4.9`
> **日期**:2026-09-25 14:29–14:40 | **服务器**:`47.77.182.89`(开发环境,R8)
> **结论口径**:六步里 **2 步达成、1 步部分达成、2 步未达成、1 步未达成(同因)**。卡点**单一且有确定根因**,平台侧一行可修。
---
## 〇、一句话结论
**上传 ✅ → 建库 ✅ → 发布 ❌ → 启用 ❌ → 回读 ⚠️。**
卡在同一个闸门:平台数据面比对**口径不一致**(`W7`)⇒ **任何声明 `bigtext` 类型的插件,只要建库成功就永久 `blocked`,之后发布与启用都拿 409**。
**用户面零影响**:共享层里仍是 `0.3.0`(0.4.9 未发布成功),已启用该插件的用户照旧工作。
| 步 | 结果 | 原因码 |
|---|---|---|
| 2 上传 | ✅ 达成 | —(回执缺 `tgzSha256` ⇒ `W1` 复现) |
| 3 建库 | ✅ 达成(`state=ready`,6/6 执行成功) | — |
| 4 发布 | ❌ 未达成 | `W7`(下游闸门拒绝) |
| 5 启用 | ❌ 未达成(同因,未创建任务) | `W7` |
| 6 回读 | ⚠️ 部分达成(对账 clean,但三面状态打架) | `W8` |
---
## 一、形态与读锁(回报 1/6)
| 项 | 读数 |
|---|---|
| 服务器侧 `op-lock` | `claim storyforge-e2e` @ 2026-09-25 **14:29**(占用者 `StoryForge验收线-第1棒`);`release` @ **14:3x**,归属校验通过;`status` 复核 = **无人占用 ✓** |
| 是否撞第 25 棒 | **否**。开工前 `handoff-guard.sh --locks` = 「全局锁 空闲」;`op-lock.sh status` = 「无人占用 ✓」 |
| 本机域锁 | `StoryForge验收线-第1棒`,域 3 个:`aliyun-dsh-server/交付物` · `aliyun-dsh-server/tmp` · `aliyun-dsh-server/接续入口_storyforge验收线_20260925.md` |
| 影响面声明(claim 时写入) | 47 候选池上传/建库/发布 + admin 自身实例启用重装配;仅 admin 实例可能短暂重启,其他用户实例不受影响;预计 15–25 分钟 |
---
## 二、六步逐条:命令 + 实际输出(回报 2/6)
> 统一前置:`SID` = 47 上 `node /opt/dshs/mksess.cjs admin` 生成的 sid cookie(临时会话,`user_agent='poc-curl2'`)。
> 全部经公网 `https://ai1net.com`(与门户同一入口,非回环),读数为**真机**。
### 步 1 · 取 admin 会话
```bash
ssh [email protected] "node /opt/dshs/mksess.cjs admin" # ⇒ token(64 hex)
curl -s -b "sid=$SID" https://ai1net.com/api/plugins/business
```
- `token_len=64`;探活 **HTTP 200**。
- 收尾:`DELETE FROM sessions WHERE user_agent='poc-curl2'` ⇒ **`DELETE 1`**,复核残留 **`0`**。
- ⛔ 未使用用户日常凭据;未插库以外的任何写操作。
### 步 2 · 上传交付件 `0.4.9` ✅
```bash
# 体材:JSON,字段 file=base64、filename=<原名>(端点 src/web/routes/business-plugins.ts:1194-1207;限 180 MiB)
curl -X POST https://ai1net.com/api/plugins/business \
-H 'content-type: application/json' -b "sid=$SID" --data-binary @02-upload-body.json
```
**HTTP 200,耗时 4.78 s。** 回执关键读数:
| 字段 | 实测 |
|---|---|
| `plugin.version` / `fileSize` | `0.4.9` / **1797711** |
| `replaced` | `true`(同池内旧 `0.3.0` 被替换) |
| `compat.level` / `findings` | `ok` / `[]`(扫描了 240 个平台包,`sawPlatformRef=true`) |
| `blocked` | `[]`(P0 零命中,未用 `trust` 覆盖) |
| `warnings` | **20 条**;规则集合 = `network-egress` / `vm-eval` / `long-base64` / `sensitive-env` / `hidden-or-git` |
| `dataPlane.state` / `declared` | `pending` / `true`(`requiresMigration=true`) |
| `dataPlane.dbName` | `dshs_pl_storyforge` |
| `dataPlane.plan.planHash` | `73d1dc286b0dcf776591d32d57c93717b7c609a42a09de0c78b6dc1ef471e7c3` |
| `versionMismatch` | `false` |
| `backupTgz` | `/opt/dsh/backups/plugins/_dsh-local_storyforge/0.4.9.tgz` |
| **`tgzSha256`** | **❌ 字段不存在 ⇒ `W1` 复现** |
**替代对账(补 W1 缺的证据)** —— 三处副本指纹**逐字相同**:
| 位置 | sha256 |
|---|---|
| 本机原包 `dsh-local-storyforge-0.4.9.tgz` | `71d6d10220c252205b0a768bc1ec1b434240fabcbde38da339ef67c1d0927ae1` |
| 47 池内 `/var/lib/dshs/business-plugins/_dsh-local_storyforge.tgz` | 同上 |
| 47 备份 `/opt/dsh/backups/plugins/_dsh-local_storyforge/0.4.9.tgz` | 同上 |
⇒ **落盘无损**;`W1` 缺的只是**回包字段**,值本身没问题(`V2` 的三元组现在可人工对账)。
### 步 3 · 建库三段 ✅
```bash
# ① 预演
curl -X POST .../api/plugins/business/datastore/plan -d '{"id":"@dsh-local/storyforge"}'
# ② 建库
curl -X POST .../api/plugins/business/datastore -d '{"id":"@dsh-local/storyforge"}'
# ③ 迁移
curl -X POST .../api/plugins/business/datastore/migrate -d '{"id":"...","confirm":true,"expect":"<planHash>"}'
```
**① plan —— HTTP 200,`executable=true`,`executabilityNote=null`,7 项、条条可读:**
| # | kind | target |
|---|---|---|
| 1 | `create_database` | `dshs_pl_storyforge` |
| 2 | `create_table` | `p_storyforge_sf_projects` |
| 3 | `create_table` | `p_storyforge_sf_docs` |
| 4 | `add_index` | `p_storyforge_sf_docs~store_project_ref` |
| 5 | `create_table` | `p_storyforge_sf_chunks` |
| 6 | `add_index` | `p_storyforge_sf_chunks~doc_ref` |
| 7 | `create_table` | `p_storyforge_sf_prefs` |
**② datastore —— HTTP 200,`outcome="created"`、`state="created"`、`elapsedMs=73`。**(`CREATE DATABASE dshs_pl_storyforge` 成功)
**③ migrate —— 第一次 HTTP 409 `plan_stale`,第二次 HTTP 200。**
- 第一次带的是建库**前**的 `planHash`(`73d1dc28…`)⇒ 409,回包给出新 hash `ceac1608a1da027645badb3a44a73313cc033433b5039f3132a1a708de4b71cf`。
- **判定:这不是缺陷。** 建库消耗掉 `create_database` 那一项 ⇒ 计划必然变 ⇒ hash 变是**设计使然**(`planHash` 就是干这个的)。
门户 UI 在建库成功后会**自动二次预演**(`web/portal.html:711 await showPlan(id)`)从而规避;本棒脚本第一版未复刻该次序。
⚠️ 但该 409 回包是 `{error, message, plan}`,**无 `step` 字段 ⇒ `W2` 复现**。
- 第二次带新 hash ⇒ **HTTP 200**:
| 字段 | 实测 |
|---|---|
| `state` | **`ready`** |
| `schemaVersion` | `1` |
| `createdDatabase` | `false`(建库已在上一步完成) |
| `executed` | **6/6 全 `ok`**(4 表 + 2 索引;`create_database` 不在其中,符合预期) |
| `elapsedMs` | 48 |
| `backup` / `backupPath` | `taken` / `/opt/dsh/backups/plugin-db/_dsh-local_storyforge/v0-2026-09-25T06-32-02-823.sql` |
### 步 4 · 发布 ❌ **未达成**
```bash
curl -X POST .../api/plugins/business/share -d '{"id":"@dsh-local/storyforge"}' # 先探冲突
```
**HTTP 409**
```json
{"error":"datastore_not_ready",
"message":"该插件声明了数据面但尚未就绪(当前状态:blocked)⇒ 暂不能发布到用户面。请先完成建库 / 迁移。",
"state":"blocked","dbName":"dshs_pl_storyforge"}
```
**原因码 = `W7`**(见 §三)。⇒ 发布腿**不可达**,`fileRef` / `tgzSha256` / `treeSha256` 三项无从取得。
### 步 5 · 启用 ❌ **未达成(同因)**
```bash
curl -X POST .../api/plugins/mine/apply -H 'content-type: application/json' \
-d '{"plugins":[{"id":"@dsh-local/storyforge","enabled":true}]}'
```
**HTTP 409**(耗时 1.48 s)
```json
{"error":"datastore_not_ready",
"message":"插件「@dsh-local/storyforge」的数据面尚未就绪(当前状态:blocked)⇒ 暂不能启用。请联系管理员完成建库。",
"state":"blocked"}
```
- ⚠️ **未返回 `taskId`** ⇒ `/api/plugins/mine/task/:id` **无对象可轮询** ⇒ 启用腿的**阶段链与 `detail` 都拿不到**。
- 闸门在**创建任务之前**(前置校验),**未走到 pnpm 那一步** ⇒ **`V1`(`ENABLE_LIST_NOT_APPLIED` / `ENABLE_TASK_FAILED`)本轮既未复现、也未证伪**(详见 §五)。
- 收尾核对 `/api/plugins/mine`:storyforge `enabled=false`、`dataPlaneState="blocked"`、`dataPlaneReady=false` ⇒ **admin 实例未被改动**。
### 步 6 · 回读一致性 ⚠️ **部分达成**
| 读面 | 端点 | storyforge 读数 |
|---|---|---|
| 候选池(实时预演) | `GET /api/plugins/business` | `0.4.9`,`dataPlane.state=**blocked**`,`summary={tables:0,columns:0,indexes:0}`,`declOrigin=package-json-inline`,`versionDrift=false`,`diskVersion=0.4.9` |
| 共享层 | `GET /api/plugins/shared` | **`0.3.0`**(`tgzSha256=98ff5e82…`、`treeSha256=1c45c841…`、`fileCount=117`)⇒ **未被 0.4.9 替换**(发布未达成,符合预期) |
| 数据面台账 | `GET /api/plugins/business/datastores` | `state=**ready**`、`schemaVersion=1`、`planHash=73d1dc28…` |
| 数据面**对账** | `GET /api/plugins/business/datastores/reconcile` | **`ok=true`、`clean=true`**、`databases == ledgered`(`dshs_pl_dsh_plugin_mcn_suite`、`dshs_pl_storyforge`)、`orphans=[]`、`missing=[]` |
- ✅ **建库腿是干净落地的**:PG 实体库与台账**完全一致**,无孤儿、无缺失。
- ❌ **但同一插件的「数据面状态」在 admin 面有两个互相打架的值**:台账说 `ready`,实时预演说 `blocked` ⇒ **`W8`**。
- ❌ 三面对「这个包」的结论**不一致**(池 `0.4.9` / 共享层 `0.3.0`),这是发布未达成的**直接后果**,非独立缺陷。
### 步 7 · 收尾 ✅
- 临时会话:`DELETE 1`,复核 **残留 0**。
- `op-lock`:已 `release`,`status` = 无人占用。
- 异常项归位 ⇒ §四。
---
## 三、根因(`W7` · 可 1:1 重放)
**病灶:平台比对「声明类型」与「库内类型」时用了两套口径。**
| 环节 | 位置 | 事实 |
|---|---|---|
| ① 插件声明 | 包内 `package.json` | `p_storyforge_sf_chunks.body` 声明类型 = **`bigtext`** |
| ② 类型表 | `src/db/plugin-data/schema.ts:42-52` | 9 种中性类型**含 `bigtext`** |
| ③ PG 映射 | `src/db/plugin-data/schema.ts:59-69` | `bigtext: 'TEXT'` |
| ④ 实际建表 DDL | 本棒取证(`04-plan.json` item 5) | 生成的是 `"body" TEXT` ✅ |
| ⑤ 库内回读 | PG `data_type` | `text` ⇒ `normalizeCurrentType('text') = 'text'` |
| ⑥ **比对那一行** | `src/db/plugin-data/diff.ts:325` | `if (norm !== d.type)` —— 左边**归一后**,右边**原始声明字面** |
| ⑦ 结果 | `diff.ts:327-329` | `'text' !== 'bigtext'` ⇒ `forbidden{kind:'alter_type', detail:'类型变化 bigtext → text ⇒ 拒绝(DB-03 §四:不允许改列类型)'}` |
**为什么只有 storyforge 中招、`dsh-plugin-mcn-suite` 却是 `ready`**:其余 8 种中性类型的**声明字面恰好等于中性名**(`text`/`integer`/…),左右两边天然相等;只有 `bigtext` 是**声明别名**(PG 里并没有这个类型名),两边永远不等。
**为什么建库前不报、建库后才报**:比对分支要求「声明列与库内列**两侧都有**」(`diff.ts:317-319`:`if (c === undefined) continue`)。库不存在时无从比对 ⇒ 全部放行;**建库一旦成功,下一次预演立刻命中**。
**为什么不可自愈**:`blocked` 状态下 `/datastore/plan` 直接拒绝执行(本棒实测:`executable=false`、`executabilityNote="当前状态 blocked 不允许建库(已就绪或声明非法)"`、`items` 为空)⇒ admin 在门户里**没有任何可点的按钮能把状态救回来**。
**影响面(超出本案)**:**任何**声明 `bigtext` 的插件,建库成功后**永久**不可发布、不可启用。
**修法(平台侧,一行)**:比对前把声明侧也归一到中性名,例如 `const wantNeutral = d.type === 'bigtext' ? 'text' : d.type`,然后比 `norm !== wantNeutral`;`diff.ts:329` 的 detail 文案同步用中性名,避免把内部别名泄漏给 admin。
**复现最小步骤**:任意声明了 `bigtext` 列的包 → 上传 → 建库 → 迁移(成功、`ready`)→ 再 `GET /api/plugins/business` ⇒ 该行 `dataPlane.state` 变 `blocked`,`forbidden` 出现 `alter_type`。
---
## 四、归位清单(回报 3/6)
### 平台侧
| # | 级别 | 问题 | 判据 / 建议落点 |
|---|---|---|---|
| **`W7`** 🔴 | **P0** | 数据面类型比对口径不一致:`bigtext` 声明 ⇒ 建库后**永久** `blocked`(发布 + 启用双双 409) | 修 `src/db/plugin-data/diff.ts:325`(声明侧归一);复现判据见 §三末 |
| **`W8`** 🟡 | P1 | **同一插件两个读面状态相反**:`/datastores` 台账 = `ready`,`/business` 实时预演 = `blocked`。admin 无从判断哪个是真的 | 二者取一为主并互相标注(如台账存「写路径结论」+ 门户明示「可能已被后续声明变更推翻」);`W7` 修好后此分歧自然消失,但**建议仍加口径说明**,否则同类分歧会复发 |
| `W1` | 已登记 | 上传回执**缺 `tgzSha256`** —— 本轮**复现**;但三处副本指纹实测一致 ⇒ 只是字段缺失 | 原判据不变(回包字段与插件线 `sha256.txt` 逐字一致) |
| `W2` | 已登记 | 失败回执**无 `step`** —— 本轮在 `migrate` 的 409 上**复现**(`{error,message,plan}`,无步骤名)。⚠️ 另:本轮触发的两处 409 都带**可读 `message`**,比原判据里的"只有 `{error}`"要好 | 补 `step` |
| `W3`/`W4`/`W5`/`W6` | 已登记 | 本轮**未触及**(撤下按钮 / 规范文档 / 释放锁连坐 / 判据替换) | — |
### 插件侧
| # | 级别 | 问题 | 判据 |
|---|---|---|---|
| **`V4`** 🆕 | P1 | `package.json` 里 `p_storyforge_sf_chunks.body` 声明为 `bigtext`。**把声明改成 `text` 即可绕开 `W7`**(语义等价:平台本来就把 `bigtext` 译成 `TEXT`),本包即可发布/启用 | 改后重打 0.4.10 → 上传 → `/business` 该行应为 `ready`,`forbidden` 为空 |
| **`V1`** ⚠️ | — | 第 2 单(启用腿)**本轮无法判定** —— 被前置闸门拦在任务创建之前,未见 `taskId`。**不是"仍失败",也不是"已修好"** | 解 `W7`(或 `V4`)后重跑;若届时仍失败,带 `/api/plugins/mine/task/:id` 的 **`detail` 原文**回来 |
| `V2` | — | 三元组对账(版本 / 落盘路径 / 指纹):本轮**人工对账通过**(`0.4.9` / `/var/lib/dshs/business-plugins/_dsh-local_storyforge.tgz` / `71d6d102…`)。`W1` 落地后改为回包自动对账 | — |
| `V3` | — | 复现信息点名账号与操作:本件已按此写(见 §二每步的 47 实例 + 端点 + 参数) | — |
> 🔴 **关于 `V4` 的边界**:本轮**只报告、不代改**(`dsh-plugin-forge` 只读)。⚠️ 同时请**不要**把 `V4` 当成 `W7` 的替代品 —— 平台侧 `W7` 必须修,否则下一个用 `bigtext` 的插件会掉进同一个坑。
---
## 五、第 2 单(启用腿)结论(回报 4/6)
**不可判定 —— 未创建任务,`ENABLE_LIST_NOT_APPLIED` / `ENABLE_TASK_FAILED` 未被触发。**
- 拦点 = `datastore_not_ready` 前置校验(HTTP 409,1.48 s,**无 `taskId`**),**未进入 pnpm 装配阶段**。
- 第 20 棒修的 `EPERM chmod` 根因、第 16/17 棒补的 `task.detail`,本轮**都无从验证**。
- ⛔ 我没有拿另一个 `dataPlane ready` 的包(如 admin 实例上在用的 `dsh-plugin-mcn-suite`)去"替跑" —— 那会扰动一个正在被用的插件,且**答不了本单的问题**(问的是 storyforge 的启用腿)。
---
## 六、门禁与回滚(回报 5/6)
| 项 | 内容 |
|---|---|
| **动了生产哪些面** | ① 47 候选池:`@dsh-local/storyforge` `0.3.0` → **`0.4.9`**(上传即替换,旧包无残留);② 新建实体库 **`dshs_pl_storyforge`**(4 表 + 2 索引);③ 其余全部只读 |
| **未动** | ⛔ 未重启任何服务(`restart dshs` **未执行**);⛔ 未改共享层(仍 `0.3.0`);⛔ 未改任何用户 `dependencies`;⛔ 未碰其他用户实例;⛔ **未 DROP 任何库** |
| **影响谁 / 断多久** | 仅本棒的临时 admin 会话(已删)。**无服务中断**,已启用 storyforge 的用户**零影响**(其环境仍指向共享层 `0.3.0`)。预期受影响用户数 **0** |
| **留痕/素材** | 池备份 `/opt/dsh/backups/plugins/_dsh-local_storyforge/{0.2.0,0.3.0,0.4.9}.tgz`(`0.3.0` = 1758443 B,在);库备份 `/opt/dsh/backups/plugin-db/_dsh-local_storyforge/v0-2026-09-25T06-32-02-823.sql` |
| **回滚** | ① 池回 `0.3.0`:重传 `/opt/dsh/backups/plugins/_dsh-local_storyforge/0.3.0.tgz`(同上传端点)② 库:`DROP DATABASE dshs_pl_storyforge` **不可逆** ⇒ ⛔ **不擅自删,只报告**(按入口 §回滚) |
| **留还是撤(自决,可推翻)** | **保留池内 `0.4.9`**。理由:它是本轮验收对象,`W7`(或 `V4`)一落地就能**直接续跑**发布/启用,无需重传;撤掉会把复现态一起撤没。代价如实说:`W7` 修好前,门户/用户会把 `0.4.9` 看成"最新可启用",点下去得 409(**安全但提示不佳**)。要撤 ⇒ 上表 ① 一条命令 |
---
## 七、边界自证(回报 6/6)
- ⛔ **未改插件包**:`E:/ProgramData/AIProject/dsh-plugin-forge/**` 全程只读(仅 `tar -xzf` 抽 `package/package.json` 到本棒 `tmp/` 供读取,**包本体与技能目录零写入**)。
- ⛔ **未改平台 `src/**`**(含 `web/portal.html`):只做了 `grep` / `Read` 只读取证。
- ⛔ **未碰其他用户实例**:全部操作在 admin 会话与候选池面内;`/mine/apply` 也只对 admin 自己(且被闸门拦下,未生效)。
- ⛔ 未使用用户日常凭据;临时会话用后即删(残留 0)。
- ⛔ 未 `DROP` 任何库、未改共享层清单、未推任何仓库。
---
## 八、证据文件(留 `tmp/sf-e2e-20260925/`,7 天内可复核)
| 文件 | 内容 |
|---|---|
| `03-upload-resp.json` | 上传回执(含 `compat` / `warnings` 20 条 / `dataPlane` / `backupTgz`;**无 `tgzSha256`**) |
| `04-plan.json` | 建库**前**预演(7 项、`executable=true`、hash `73d1dc28…`、`sf_chunks` 的 `body TEXT`) |
| `05-datastore.json` | 建库回执(`outcome=created`) |
| `06-migrate.json` | 第一次迁移 = 409 `plan_stale`(+ 新 hash `ceac1608…`) |
| `07-migrate2.json` | 第二次迁移 = 200 `state=ready`、`executed` 6/6、库备份路径 |
| `08-share-probe.json` | 发布 409 `datastore_not_ready`(`state=blocked`) |
| `09-business2.json` | `/business` 实测(`blocked` + `forbidden{alter_type: p_storyforge_sf_chunks.body, 'bigtext → text'}`) |
| `10-datastores.json` | 台账实测(**`ready`**,与 `09` 打架 ⇒ `W8`) |
| `11-plan2.json` | 建库后实时预演(`items=0`、`executable=false`) |
| `12-shared.json` | 共享层实测(仍 `0.3.0`) |
| `13-reconcile.json` | 对账(`clean=true`、无孤儿/缺失) |
| `14-mine.json` | `/mine` 实测(`enabled=false`、`dataPlaneReady=false`) |
| `15-apply.json` | 启用 409 `datastore_not_ready`(无 `taskId`) |
---
## 九、下一棒建议(不代做,供拍板)
1. **平台先修 `W7`**(一行);顺带把 `W8` 的口径说明补上 —— 修完即可**原样续跑**:`发布 → 启用 → 回读`。
2. 若插件线希望**不等平台**:按 `V4` 把 `body` 声明改 `text`、重打 **0.4.10** 上传,本包立即通。
3. 两条都通后,本验收线第 2 棒 = **补跑步 4/5/6** + 顺手验 `W1`(回包 `tgzSha256`)与 `W2`(失败回执 `step`)。