# 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 root@47.77.182.89 "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":""}' ``` **① 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`)。