Files
dsh_ai1net_server/交付物/StoryForge端到端验收-未通过-20260925.md
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 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/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

296 lines
19 KiB
Markdown
Raw Permalink 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.
# 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`)。