- 变更规模:新增 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/ 知识文件,按口径入库)
19 KiB
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 会话
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 ✅
# 体材: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 · 建库三段 ✅
# ① 预演
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,回包给出新 hashceac1608a1da027645badb3a44a73313cc033433b5039f3132a1a708de4b71cf。 - 判定:这不是缺陷。 建库消耗掉
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 · 发布 ❌ 未达成
curl -X POST .../api/plugins/business/share -d '{"id":"@dsh-local/storyforge"}' # 先探冲突
HTTP 409
{"error":"datastore_not_ready",
"message":"该插件声明了数据面但尚未就绪(当前状态:blocked)⇒ 暂不能发布到用户面。请先完成建库 / 迁移。",
"state":"blocked","dbName":"dshs_pl_storyforge"}
原因码 = W7(见 §三)。⇒ 发布腿不可达,fileRef / tgzSha256 / treeSha256 三项无从取得。
步 5 · 启用 ❌ 未达成(同因)
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)
{"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:storyforgeenabled=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) |
九、下一棒建议(不代做,供拍板)
- 平台先修
W7(一行);顺带把W8的口径说明补上 —— 修完即可原样续跑:发布 → 启用 → 回读。 - 若插件线希望不等平台:按
V4把body声明改text、重打 0.4.10 上传,本包立即通。 - 两条都通后,本验收线第 2 棒 = 补跑步 4/5/6 + 顺手验
W1(回包tgzSha256)与W2(失败回执step)。