一个稳定的 AI 短篇小说生成引擎:剧情规划、硬性校验、基础审阅、确定性自动重试、定点修订, 以及一份把以上结论汇到一起的统一质量快照。 A stable pipeline-based AI short-story generator: beat planning, hard validation, basic review, deterministic automatic retry, targeted repair, and a unified quality snapshot.
v2.0.0 起它是一个平台化架构。 StoryLoop 2.0 turns the 1.x generation pipeline into a modular story-generation platform. Generation, quality, experimentation, observability, failure analysis, infrastructure, and interfaces now live behind explicit architectural boundaries while preserving existing user-facing capabilities.
StoryLoop 2.0 不再只是不断扩张的生成 Pipeline,而是正式进入平台化架构:生成、质量、实验、 观测、失败分析、基础设施与接口层拥有明确边界,为后续更复杂能力提供稳定承载层。
这一版不改任何用户可见行为:HTTP API、请求响应字段、CLI、错误码、
runs/下的产物布局与 字段、界面全部与 1.9.1 逐字一致;1.0.0 以来写的产物原样可读,不需要迁移。改的是仓库内部 的依赖方向——六层职责、一个组合根、端口与适配器,见 docs/architecture.md。v2.1.0 在 StoryLoop 2.0 平台架构上升级质量系统:Beat Validator、故事质量 Reviewer 与 Commercial Reviewer 统一使用结构化 Diagnostic 语言,并在不改变既有 Co/N/C/Ca 与 H/P/E/Pf 指标语义的前提下,提高诊断的一致性与可解释性。 三套结论各自多产出一份统一的
QualityDiagnostic清单,一次 Run 再多落一份quality-stack.json把这三套汇到一页; 既有的review.score/commercial_review.score/beat_validation.issues一个字段都没动, 2.0.0 及更早写的 Run 原样可读。见下面 Advanced Quality Stack。v2.3.0 为 StoryLoop 增加正式 Benchmark Platform。 固定且版本化的 Benchmark Suite 可以通过 同一套 Production Pipeline 批量执行,并统一汇总质量、商业可读性、可靠性、失败分布与效率指标, 从而让 StoryLoop 能够持续、可复现地测量自身变化。它只测量,不控制: Benchmark measures the system. Benchmark does not control the system. 同样配置跑两次,数字不会逐字节一致——同样配置并不保证模型输出逐字节一致。 契约见 docs/benchmark.md,题库来源与许可见 docs/benchmark-data.md。
v2.3.1 是 2.3.0 的审计响应补丁:对 2.3.0 重跑了一次完整的 Mimosa 深度扫描,报告的一条 中危经核实是误报(本仓没有任何数据库,被点名的路由也不接受排序入参),本版不修复任何安全 漏洞;人工复核另查出两处「拼路径前先验 id」的防护不对称并补齐,行为逐字未变。 升级说明见 docs/upgrade.md。
node --version # 需要 >= 20.9.0
npm install
cp .env.example .env # 填入 LLM_BASE_URL / LLM_API_KEY / LLM_MODEL
npm run dev # http://localhost:3000命令行生成、校验与审阅(与 UI / API 共用同一条 GenerationPipeline):
# 一次完整 Run:StoryConfig → Plan → Generate → Save Story → Validate → Review(自动重试最多再生成一次)
npx tsx scripts/generate-cli.ts run --config configs/example_story.json
# 手动模式:先单独规划,人工编辑 beats.json 后再生成
npx tsx scripts/generate-cli.ts plan --config configs/example_story.json --out beats.json
npx tsx scripts/generate-cli.ts run --config configs/example_story.json --beats beats.json
# 自定义重试策略:最多 3 次尝试、总分阈值 80、校验失败不重试
npx tsx scripts/generate-cli.ts run --config configs/example_story.json --max-attempts 3 --min-score 80 --no-retry-on-validation-failure
# 开启定点修订:Attempt 不过时先按具体问题修订,再重新校验 / 审阅,修订失败才整篇重生
npx tsx scripts/generate-cli.ts run --config configs/example_story.json --enable-repair --max-repairs 2
# 只校验一段已有正文(不生成、不审阅)
npx tsx scripts/generate-cli.ts validate --config configs/example_story.json --story story.md退出码固定为三个值:0 正常结束(含校验不通过与 exhausted)、1 运行时失败、
2 参数或配置不合法。每个子命令都有自己的 --help。
想看清产物长什么样,看仓库里的 examples/example_run/——那是用假模型跑出来的一次完整 Run。
一次完整生成就是一个 Run,固定顺序:
StoryConfig → Planning →〔Validate BeatPlan〕→〔Attempt 1..max_attempts: Generate → Save Story → Validate → Review → Commercial Review → Repair? → Decide〕→ Finalize
- AI Beat Planning:先生成剧情骨架(BeatPlan),再据此写正文;骨架可手动编辑后进入生成
- Beat Plan Validator(剧情骨架结构校验,v1.4.0):写正文之前先检查这份骨架撑不撑得起一个 完整短篇;结论只报告,不改写骨架,用户改完可以再校验一次
- Story Validator(硬性有效性检查):正文落盘后立即跑一遍确定性规则,回答「这篇正文基本可用吗」
- Basic AI Reviewer:每次生成的正文自动获得一次基础审阅,产出 0–100 整体分, 并给出四个基础维度的分数与短评(连贯性 / 叙事 / 人物 / 因果)
- Commercial Reviewer(商业可读性审阅,v1.5.0):与上面那道结构审阅完全并列的第二个 审阅者,从「读者会不会继续读下去」的角度再出一次结论:Hook(开篇抓力)/ Pacing(节奏)/ Engagement(全篇持续阅读动力)/ Payoff(回报)四个维度各 0–100 分加一句短评, 外加优点 / 问题 / 建议三组清单。两份结论互不覆盖、互不影响
- Automatic Retry(自动重试):生成失败、校验不通过或总分低于阈值时按确定性策略重新生成
- Targeted Story Repair(定点修订):Attempt 不通过时先按具体问题类别改写这篇正文, 修订后再校验、再审阅;修订彻底失败才退回整篇重新生成
- Unified Quality Snapshot(统一质量快照):把校验结论、审阅结论与采纳结论汇成一份
QualityResult(quality.json+ API 的quality字段 + 前端 Quality Summary 面板), 不调用模型、不新增分数 - Run Artifacts:产物统一落在
runs/<run_id>/,每次 Attempt、每次修订单独归档 - Run Provenance(Run 出身清单,v1.6.0):每次 Run 在
metadata.json旁多落一份run-manifest.json,写清跑在哪个代码版本与 commit 上、用的哪个模型、六份提示词各自的版本与 内容摘要、各阶段 temperature 与重试策略、每次 Attempt 的结局,以及这批产物的 SHA-256。 API 的manifest字段与前端 Run Provenance 面板读的是同一份。它只为一次 Run 自证出身: 不做跨 Run 对比、不跑基准、不统计成功率 - Experiment Framework(受控实验框架,v1.7.0):同一份 StoryConfig、同一份骨架、同一批
环境,批量跑多个 Variant(最多 4 个 × 最多 5 次,总计不超过 12 条样本)。可改的变量只有
四个:
model/temperature/retry.maxAttempts/retry.minReviewScore。 执行的是ExperimentRunner(复用 v1.6.0 的 GenerationPipeline,每个样本都是一次完完整整的 普通 Run),落盘的是ExperimentStore里experiments/下的definition.json/runs.json/results.json。它只摆数字:每个 Variant 的计数与均值,顺序永远是你声明时的顺序 - Run Telemetry(Run 可观测性,v1.8.0):每次 Run 在
metadata.json旁多落一份telemetry.json,记下这次怎么跑的——各阶段起止与耗时、每次模型调用的阶段 / 模型 / 耗时 / 结局、Attempt / retries / repairs 计数、失败阶段与稳定错误码,以及 Provider 真实返回的 usage(拿不到就是没有,不补 0)。API 是GET /api/runs/<run_id>/telemetry,界面是 Run 详情页 的「可观测性」面板。它只观察,不控制:没有代码读它来决定重试、修订或采纳 - Failure Analysis(失败分析,v1.9.0):每次 Run 在
metadata.json旁多落一份failure-analysis.json,按同一套固定 12 类清单(优先顺序写死:越靠前越优先,SECURITY>CONFIGURATION>STORAGE> … >UNKNOWN)把「这次失败是哪一类」 整理成结构化分类:主要类别、次要类别、信号表、每条信号指向的直接证据(哪份文件的 哪个字段、哪个 Attempt、哪一轮修订)、首个失败阶段与终态;Retry / Repair 是否用尽 按真实计数与上限判定。它只分类,不归因:没有证据的类别绝不下,证据不足就是UNKNOWN(不猜),也没有任何代码读它来改生成行为。API 是GET /api/runs/<run_id>/failure-analysis,界面是 Run 详情页的「失败分析」面板, 实验详情页另有一组失败类别分布 - Advanced Quality Stack(高级质量栈,v2.1.0):Beat Validator、故事质量 Reviewer 与
Commercial Reviewer 统一使用结构化
QualityDiagnostic语言(source/category/severity/target/message),一次 Run 再多落一份quality-stack.json把三套结论 汇成一页,界面是 Run 详情页的 Quality Center 面板(可按来源 / severity / target / category 筛选)。三套结论的分数语义一个都没改:Co/N/C/Ca 与 H/P/E/Pf 的维度名、0–100 口径与均分 算法逐字保留,review.json/commercial-review.json/beat-validation.json的既有字段 一个不少。诊断只说明、不驱动:不触发重试、不触发修订、不换模型、不换 Prompt - Creator Workspace(创作者工作区,v2.2.0):把散落各处的 Run 收进项目,把一次 Run
复制成一篇可编辑、可导出的稿件,再给每个项目一份确定性的健康结论。产物落在与
runs/同级的projects/树里(project.json/documents//revisions//exports/), HTTP 出口是十二条 additive 路由/api/projects/**,界面是Workspace(项目列表)与/workspace/<id>(Overview / Editor / Runs / Quality / Exports 五个签)。从 Run 建稿是 复制:之后怎么改都不回写 Run 的story.md。Creator Health 只摆九个登记在册的信号码, 同一份磁盘事实两次请求逐字相同——不给建议、不打分、不排序、不驱动任何行为。 契约见 docs/workspace.md - Benchmark Platform(基准测量平台,v2.3.0):把「这条流水线现在表现如何」变成可重复的
测量。一份 Suite = 若干 Case + 一份 Protocol(重复次数、固定骨架还是自己规划、
采纳哪些指标、失败样本算不算分母、PASS 切线);Benchmark Runner 在发出任何一次 LLM 请求
之前先跑五步预检(服务端模型与密钥、Suite 本身、固定骨架的文件、提示词角色、样本数安全线),
之后按「Suite 声明顺序 × 重复次数升序」把每道题跑成一次次完完整整的普通 Run——产物仍落在
runs/<run_id>/,Benchmark 只留runId这一根指针。汇总的是 22 个指标(质量 / 商业可读性 / 可靠性 / 失败 / 效率)、失败类别分布与执行耗时;执行三件套(execution.json/samples.json/aggregate.json)落在与runs/同级的benchmarks/树里,题库原文 (suite.json+ 各题的cases/<case_id>/beat-plan.json)与执行产物分开放,落到终态后 任何再写入都被拒绝。HTTP 出口是八条 additive 路由/api/benchmarks/**,界面是/benchmarks、/benchmarks/<id>与/benchmarks/history。契约见 docs/benchmark.md - 现代 Web UI:六阶段进度、Attempt 计数、修订明细、Validation / Review / Commercial Review / Quality Center / Run Provenance / 可观测性 / 失败分析面板、实验列表与实验详情、 Workspace(项目 / 稿件 / 导出 / 健康)、Benchmark(套件 / 执行详情 / 历史)、 Run ID 与产物清单
- Modular Platform Architecture(平台化架构,v2.0.0):上面每一种能力都落在明确的层里——
Stable Domain Contracts(领域契约与纯规则)、Application Use Cases(用例编排与公共门面)、
Engine(生成 / 校验 / 审阅 / 修订 / 管道)、Analysis(实验汇总与失败分类)、
Ports / Adapters(端口与适配器:
LLMClient/RunRepository/ArtifactStore/ExperimentRepository/Logger)、Composition Root(组合根,全仓唯一装配点)。 依赖方向由 Architecture Boundary Tests 静态扫 import 把守,另有 Cycle Detection 保证没有循环依赖;v1.x 的 Run 产物通过 Legacy Run Compatibility 原样可读,不需要迁移。 架构说明见 docs/architecture.md - OpenAI-compatible LLM:OpenAI / DeepSeek / 硅基流动 / 任意兼容端点
- 可编辑 Prompt 模板:
prompts/*.txt直接改,重启生效 - 稳定 CLI:
run/plan/review/validate/repair五个命令,与 API 共用同一套逻辑
Validator 与 Reviewer 是两件事:
Story Validator Basic Reviewer 回答的问题 「这篇正文基本可用吗」 「这篇故事写得好吗」 实现 确定性规则,不调用 LLM LLM 审阅,主观评价 产出 ValidationResult(passed+issues)ReviewResult(score+ 可选dimensions+summary+strengths+problems)判定性质 硬性: error即不通过软性:分数只做反馈 两者互不影响:Review 分数不参与 Validation 判定——哪怕 Review 打 0 分,校验该过还是过。
维度是评价输出,不是行动依据(v1.3.0):审阅者在整体分之外还给四个基础维度各打一个 0–100 分并附一句短评。有维度时整体分就是这四个数的均分(四舍五入到 1 位小数, 纯算术、无权重),
RetryPolicy里仍然只有min_review_score一个总分门槛—— 维度不新增阈值、不触发重试或修订、不改变采纳判定。维度也可有可无: 1.3.0 之前的 Run 没有这个字段,读取时整个键不出现,行为与当年逐字一致。两个审阅者是两件事(v1.5.0):Basic Reviewer 看结构(连贯性 / 叙事 / 人物 / 因果), Commercial Reviewer 看读者体验(Hook / Pacing / Engagement / Payoff)。两者各自的提示词、 各自的解析器、各自的产物文件(
review.json与commercial-review.json), 绝不合成一个「八维大 Prompt」。商业可读性分数同样不驱动任何决策:不触发重试、不触发 修订、不进QualityResult(质量快照仍然只有 Co/N/C/Ca 四个维度)。它也不是市场预测—— 没有「爆款概率」「必火」「市场成功率」一类字段或文案,分数描述的是文本本身可观察的事实。定点修订在整篇重试之前:修订策略是纯函数(问题类别只由校验 issue code 或审阅问题文本推出, 没有模型参与、没有打分、没有择优),重试同样由确定性策略驱动,没有学习、没有自适应: 同样的输入得到同样的重试次数。修订只回答「这篇正文哪里不对、按类别改一次」,不回答「为什么会失败」。
Beat 校验只报告,不修复(v1.4.0):骨架结构不达标时 Storyloop 不改写任何一拍、不重排顺序、 不自动补拍,也不据此重新规划——它只告诉你哪儿站不住,骨架怎么改由你决定。
受控实验只摆数字,不评比(v1.7.0):一次实验回答「只改这四个变量分别发生了什么」。 它不排名、不评选赢家(结果表的行顺序永远是你声明 Variant 的顺序,绝不按分数重排); 缺分数的那一条显示
—,不被当成 0(补 0 等于凭空制造一个差评); 不是模型跑分平台(没有 leaderboard、没有胜率、没有「推荐 Variant」); 不做显著性检验(没有 p 值 / 置信区间 / 效应量,两个均值差多少由读数字的人自己判断); 不自动调参(同一份定义重跑会被 409 挡住:定义与结果都不可变,要改条件就复制成一个新实验)。 同一次实验里 A 组三条都有分、B 组只有一条有分,两组的均值本来就不可直接比大小—— 这也是界面必须把「有几条真的跑出了分」摆出来的原因。统一诊断只说明,不驱动(v2.1.0):三套质量组件现在说同一种
QualityDiagnostic, 但它不携带 rootCause / confidenceProbability / repairPolicy / adaptiveWeight / causalNodeId / evidenceLedgerId / characterDecisionId,也没有任何代码读它来决定重试、 修订、采纳、换模型或换 Prompt——RetryPolicy里仍然只有min_review_score一个总分门槛。quality-stack.json同样不是第四套评价:不调用模型、不重新评分、不补维度,只是把已有的 三套结论汇到一页。某一套没跑出来时对应的键整个不出现(status相应变成partial或failed),没有这个文件的旧 Run 面板整个隐藏——不补 0、不编一份「0 条诊断」冒充跑过。Creator Workspace 只收拢,不代劳(v2.2.0):项目、稿件、导出三条都是读写已有的事实。 它不删(归档不删数据,本版本没有「永久删除项目 / 删除稿件 / 删除导出」)、 不自动(没有一键成稿、自动优化、推荐下一步;编辑器自动保存只做存盘)、 不打分(除 Run 自带的
review.score/commercial_review.score之外没有别的数字, 缺分显示「—」,不补 0)、不外联(工作区这一层一次 HTTP 请求都不发)、 不碰 Run(建稿是复制,不回写 Story,不给 Run 追加字段)。 Creator Health 那份结论也没有「换个模型试试」这种话——那需要一次新的判断, 而这个版本不打算在背后替用户做任何主张。Benchmark measures the system. Benchmark does not control the system.(v2.3.0) Benchmark 回答「这条流水线现在表现如何」:Benchmark measures the system. 它 不推荐 Variant 或模型、不适应输入、不优化 Prompt 或参数、不学策略、 不改生成行为:Benchmark does not control the system. 要改行为,请去改配置再跑一次测量。 更多边界:PASS 阈值只来自 Suite 自己(没有代码默认值,71 只是发布那份的选择);一次执行 落到终态后不可改写,Suite 要改就开新版本号;比较只摆两侧各自的事实与差值,不解释差异为什么 发生;历史图只画盘上真实读出来的执行,不编时间序列;Baseline 只是会话期标签,不参与 排序与计算;图表只有柱状、折线与分布三类,没有饼图、没有雷达图、没有加权总分。
v2.3.0 可宣传的能力(英文原词):Versioned Benchmark Suites · Benchmark Runner · Fixed Protocols · Benchmark History · Quality Metrics · Commercial Metrics · Reliability Metrics · Failure Distribution · Efficiency Metrics · CSV / JSON Export · Benchmark Dashboard。
后端没有任何为未实现能力预留的隐藏接口——没有的功能就没有入口。
v2.0.0 起是六层平台架构,依赖方向单向,由 tests/test_architecture.test.ts 静态扫
import 把守(含循环依赖检测)。完整说明见 docs/architecture.md:
Interface (src/interface · src/app · src/components · scripts)
│ 解析 · 渲染 · 路由适配,不写业务规则
▼
Composition Root (src/composition)
│ 全仓唯一 new 出具体实现的地方
▼
Application (src/application)
│ 用例编排 · 公共门面 StoryLoopService · DTO ↔ Domain
├──────────────► Engine (src/engine) ── 规划 / 生成 / 校验 / 审阅 / 修订 / 管道
└──────────────► Analysis (src/analysis) ── 实验汇总 / 失败分类(对已有证据做推断)
│
▼
Domain (src/domain) ── 契约与纯函数:模型、规则、RetryPolicy、序列化
▲
Ports (src/ports) ◄───────┴─────── Infrastructure (src/infrastructure)
LLMClient · RunRepository · LLM 客户端 · 文件存储 · 配置 · 日志 · 遥测 ·
ArtifactStore · ExperimentRepository · 哈希 · Prompt 注册表 · URL 关卡
Logger · ProviderProbe · FailureAnalyzer
一个 Run 在层间的走法:路由 / CLI 只负责解析与渲染 → 组合根把装好的用例交出来 →
用例层转 DTO → Engine 的 GenerationPipeline 推进 Run(判定全部来自 Domain 的
确定性规则)→ 沿途结论通过 Ports 交给 Infrastructure 落盘 → Analysis 事后读盘做汇总与分类。
import { createDependencies, createStoryLoopApplication } from "@/composition";
const deps = createDependencies({ runsDir: "runs" }); // 基础设施实例在这里落地
const app = createStoryLoopApplication(deps); // 用例在这里绑成应用
const { status, json } = await app.service.generate(body);createStoryLoop(options) 是这两步的合并写法,路由与 CLI 用的就是它。
下图的每条边都是「调别人」而不是「被别人调」——Infrastructure 实现 Ports 声明的接口,
Domain 谁都能用,Domain 不知道任何一层的存在。
┌──────────────────────────────────────────┐
│ 横切:app-config · logger · api-error │ 配置单一来源 · 带 run_id 的结构化日志 · 统一错误形状
└──────────────────────────────────────────┘
│
▼
┌──────────────────────┐
│ UI / CLI │ StoryConfig 表单 + Save / Load / New · generate-cli run
└──────────┬───────────┘
▼
┌──────────────────────┐
│ Run API │ POST /api/runs · POST /api/runs/from-plan · POST /api/validate · POST /api/review · POST /api/review/commercial · POST /api/validate-beats
└──────────┬───────────┘
▼
┌──────────────────────────────────────────┐
│ generate-service │ CLI 与 API 共用同一批函数,不重复实现业务逻辑
└──────────┬───────────────────────────────┘
▼
┌──────────────────────┐
│ GenerationPipeline │ 固定顺序:Config → Planning →〔Validate BeatPlan〕→〔Attempt 1..max_attempts: Generate → Save Story → Validate → Review → Commercial Review → Repair? → Decide〕→ Finalize
│ · run() │ StoryConfig → BeatPlan → Story → ValidationResult → ReviewResult → CommercialReviewResult
│ · runWithPlan() │ 用户编辑后的 BeatPlan 直接进入生成
│ · RetryPolicy │ max_attempts / min_review_score / retry_on_validation_failure / enable_repair / max_repairs_per_attempt
│ · RepairLoop │ 定点修订在整篇重试之前:Repair → Re-validate → Re-review → 仍不达标才重试
└──────────┬───────────┘
▼
┌──────────────────────┐
│ RunContext │ run_id + status + current_stage + error
└──────────┬───────────┘
▼
┌──────────────────────────────────────────────────────────────┐
│ QualityAssembler(v1.2.0) │
│ 纯函数:ValidationResult + ReviewResult + 采纳结论 │
│ → QualityResult(overall_score / validation_passed / │
│ accepted / issues / suggestions / summary / │
│ dimensions?) │
│ 不调用模型 · 无随机 · 同一输入永远得到同一份 JSON │
└──────────┬───────────────────────────────────────────────────┘
▼
┌──────────────────────┐
│ BeatPlanner │ prompts/beat_planner.txt → LLM → BeatPlan(可手动编辑)
│ BeatValidator │ prompts/beat_validator.txt → LLM → BeatValidationResult(只看结构,不改写骨架)
│ StoryGenerator │ prompts/story.txt 渲染(含 Beat Plan)
│ StoryValidator │ 确定性硬性规则 → ValidationResult(不调用 LLM,只检查,不改写)
│ BasicReviewer │ prompts/reviewer.txt → LLM → ReviewResult(只评价,不改写)
│ CommercialReviewer │ prompts/commercial_reviewer.txt → LLM → CommercialReviewResult(只看读者体验,不改写)
│ StoryRepairer │ prompts/repair.txt → LLM → 修订后正文(可选,不注入则无修订能力)
└──────────┬───────────┘
▼
┌──────────────────────┐
│ LLM Client │ OpenAI-compatible(system + user)· 单次超时 LLM_TIMEOUT · transport retry 硬上限 2 次
└──────────┬───────────┘
▼
┌──────────────────────────────────────────┐
│ ArtifactStore │ 原子写入 runs/<run_id>/ 下的产物,写失败抛 ArtifactWriteError
│ │ v1.2.0 起多一份 quality.json(Run 根与 attempts/NN/ 各一份)
│ │ v1.4.0 起多一份 beat-validation.json(Run 根,对 BeatPlan 唯一)
│ │ v1.5.0 起多一份 commercial-review.json(Run 根与 attempts/NN/ 各一份)
│ │ v1.6.0 起多一份 run-manifest.json(Run 根,一次 Run 一份)
│ │ v1.8.0 起多一份 telemetry.json(Run 根,写盘前过一遍结构校验)
│ │ v1.9.0 起多一份 failure-analysis.json(Run 根,失败分类:类别 / 信号 / 证据)
└──────────────────────────────────────────┘
质量结论的流向:Validator / Reviewer / RetryPolicy 各自产出原始结论(落盘为
validation.json / review.json、metadata 记录分数),QualityAssembler 只做汇总——
把同一份结论整理成 QualityResult 写进 quality.json。它排在原始结论的下游,
不会反过来影响校验、审阅或重试的任何判定。
正文先落盘再校验、再审阅:即使 Validator 自身抛异常或 Reviewer 调用失败 / 输出非法,
story.md 与整个 Run 都保持成功,只有对应的 validation_status / review_status 变为 failed 并记录原因。
一次 Attempt 之后的 RetryDecision 只由确定性规则给出:(1) 生成失败且还有次数 → 重试;
(2) 校验不通过且策略允许 → 重试;(3) 审阅成功但总分低于 min_review_score → 重试;否则接受该次尝试。
第 (1) 种情况若已到达最后一允许的 Attempt,整个 Run 以 generating 阶段失败结束;
第 (2)(3) 种情况到达上限则 Run 以 exhausted 正常收尾,selected_attempt 指向最后一次尝试的正文。
enable_repair 打开时,(2)(3) 两种情况会先走定点修订:Repair → Revalidate → Rereview,
修订成功且达标即接受该次尝试,不再整篇重生。修订阶段的进度记在 current_stage
(repairing / revalidating / rereviewing),修订本身失败不会让 Run 失败——
正文保留修订前那一版,Run 按重试策略继续走。
BeatPlan 在第一个 Attempt 之前单独过一道结构校验(v1.4.0,进度记在 validating_beat_plan):
规则层先查空骨架、拍数不足、编号重复、编号不连续这几件不用问模型的事;这些没问题才把
StoryConfig + BeatPlan 交给模型判结构。结论里带 error 级问题时整个 Run 就此结束——
一个 Attempt 都不跑,story.md 与 attempts/ 都不会出现(骨架都站不住时不花生成额度);
只有 warning 或完全没有问题时照常生成。BeatValidator 自身崩溃则只把
beat_validation_status 置为 failed 并记录原因,骨架与生成流程都不受影响。
结构审阅之后还有一道商业可读性审阅(v1.5.0,进度记在 reviewing_commercial):
同一个故事再拿给 CommercialReviewer,从「读者会不会继续读下去」的角度给 Hook /
Pacing / Engagement / Payoff 四个维度打分。这一步排在结构审阅之后、可能的修订之前,
因此同一篇正文的两份结论描述的是同一版文字。它自身失败(模型超时、输出非法)只把
commercial_review_status 置为 failed 并记录原因——正文、validation.json /
review.json / quality.json 与采纳结论一个都不受影响,Run 照常收尾。
配置格式版本 config_version 与项目 VERSION 是两回事:前者描述 JSON 结构,后者描述软件版本。
| Field | Type | Required | Description |
|---|---|---|---|
config_version |
string | no(缺省 "1") |
配置格式版本 |
title |
string | yes | 故事标题(trim 后非空,≤ 120 字) |
genre |
string | yes | 题材(自由文本,不设枚举) |
premise |
string | yes | 核心设定 |
setting |
string | no | 故事背景 |
protagonist |
object | no | 主角(name 必填,identity/goal/motivation 可选) |
conflict |
string | no | 主要冲突 |
stakes |
string | no | 失败代价 |
ending |
string | no | 结局方向 |
target_words |
integer | yes | 目标字数(500 ~ 30000) |
style |
string | no | 写作风格 |
extra_requirements |
string | no | 附加要求 |
完整示例见 configs/example_story.json,字段级契约见 docs/story-config.md。校验通过的返回值是归一化后的新对象: 字段顺序固定、缺失的可选字段不出现、未知字段一律丢弃。
阶段一产出剧情骨架,阶段二据此写正文。格式版本 beat_plan_version 与 config_version、项目 VERSION 各自独立。
| Field | Type | Required | Description |
|---|---|---|---|
beat_plan_version |
string | no(缺省 "1") |
BeatPlan 格式版本 |
summary |
string | no | 整体规划摘要 |
beats |
array | yes | 非空;id 从 1 连续且唯一 |
beats[].id |
integer | yes | 序号(正整数) |
beats[].purpose |
string | yes | 这一拍的结构作用 |
beats[].event |
string | yes | 这一拍实际发生的事件 |
beats[].characters |
array | yes | 出场人物(可为空数组) |
beats[].conflict |
string | no | 本拍局部冲突 |
beats[].expected_outcome |
string | no | 本拍结束后故事状态的变化 |
字段级契约、编号规则与 JSON 示例见 docs/beat-plan.md。 生成失败或计划非法时,Storyloop 只报告错误并保留你已编辑的 BeatPlan:自动重试只会带着同一份 BeatPlan 整篇重新生成,不会改写它。
v1.4.0 起这份骨架在写正文之前还要过一道结构校验(见下面的 BeatValidationResult): 撑得起一个完整短篇才往下走,撑不起就停在这里。校验只读不写——你在 UI 里编辑 Beat、 保存、再点一次 Validate Beats,拿到的永远是新结论,旧的那份不会被就地改掉。
一次完整生成就是一个 Run。Run ID 形如 20260920_101530_k3f9aq(本地时间戳 + 6 位随机字符),
由服务端生成,不依赖用户输入,可安全用作目录名。
runs/
└── <run_id>/
├── config.json # 本次运行使用的 StoryConfig
├── beats.json # 实际采用的 BeatPlan(多次 Attempt 复用同一份)
├── beat-validation.json # 这份 BeatPlan 的结构校验结论(v1.4.0 新增,跑过这一步才有)
├── story.md # 被选中那一次 Attempt 的正文(发生过修订时为修订后的版本)
├── validation.json # 被选中那一次 Attempt 的首次硬性校验结果
├── review.json # 被选中那一次 Attempt 的首次审阅结果
├── commercial-review.json # 被选中那一次 Attempt 的商业可读性结论(v1.5.0 新增,跑过这一步才有)
├── quality.json # 被选中那一次 Attempt 的统一质量快照(v1.2.0 新增)
├── metadata.json # 运行级 metadata
├── run-manifest.json # 这次 Run 的出身清单(v1.6.0 新增,只在运行级一份)
├── telemetry.json # 这次 Run 的执行过程:阶段耗时 / 调用次数 / 重试修订 / 失败阶段(v1.8.0 新增)
├── failure-analysis.json # 这次 Run 的失败分类:类别 / 信号 / 证据 / 首个失败阶段(v1.9.0 新增)
├── quality-stack.json # 三套质量结论的统一视图 + 统一诊断清单(v2.1.0 新增,只在运行级一份)
└── attempts/
├── 01/
│ ├── story.md # 该次尝试的正文(发生过修订时为修订后的版本)
│ ├── initial_story.md # 修订前的正文(只有发生过修订时才存在)
│ ├── validation.json # 该次尝试的首次校验结论
│ ├── review.json # 该次尝试的首次审阅结论
│ ├── commercial-review.json # 该次尝试最终那一版正文的商业可读性结论(v1.5.0 新增)
│ ├── quality.json # 该次尝试的统一质量快照(v1.2.0 新增)
│ ├── metadata.json # attempt 级 metadata
│ └── repairs/
│ ├── 01/
│ │ ├── story.md # 这次修订产出的正文
│ │ ├── request.json # issue_type / issue_message / 修订请求三要素
│ │ ├── validation.json # 修订后重新校验的结论
│ │ ├── review.json # 修订后重新审阅的结论
│ │ └── metadata.json # repair 级 metadata
│ └── 02/
└── 02/
runs/ 与 outputs/ 已加入 .gitignore,产物只落在本地;仓库里只有
examples/example_run/ 这个合成样例。
运行级 metadata.json 的字段:run_id、project_version、status、current_stage、
started_at、finished_at、model、error、artifacts、validation_status、
validation_passed、validation_issue_count、validation_error、review_status、
review_score、review_error、quality_assembly_status、overall_score、
quality_issue_count、max_attempts、min_review_score、
attempt_count、selected_attempt、quality_status、enable_repair、
max_repairs_per_attempt、repair_count、beat_validation_status、
beat_validation_passed、beat_validation_issue_count、beat_validation_error、
commercial_review_status、commercial_score、commercial_review_error、
duration_ms、llm_call_count、failure_analysis_status、primary_failure_category。
model 始终是「本次真正生效的模型」(请求覆盖 → 环境变量 → 缺省值),attempt 级的 error
没有错误时是 null——这两条是 v1.0.0 固定下来的字段语义。
v2.1.0 起的 quality-stack.json 不在 metadata 里占任何字段:它是纯 additive 的新产物,
读它走 GET /api/runs/<run_id>/quality-stack 或 Run 详情的 qualityStack,
字段口径见 docs/quality-stack.md。
v1.8.0 起的 duration_ms / llm_call_count 是 telemetry.json 的转述:想看阶段耗时、
每次模型调用的起止与 usage,去读 docs/telemetry.md,metadata 里的
这两个数只是不用再翻一个文件时的快捷方式。
v1.9.0 起的两个字段是 failure-analysis.json 的转述:failure_analysis_status 说这次分析
跑出来没有(none / detected / partial / unknown,分析器自己出错时是 unavailable),
primary_failure_category 只在确实分出了主要类别时才出现。分类口径与字段表见
docs/failure-analysis.md。
发生过修订时有一处刻意的不对称:运行级 metadata.json 里的 validation_* / review_score
取自入选 Attempt 修订后的结论,而同目录的 validation.json / review.json(以及
attempts/NN/ 下的同名文件)是首次结论,修订后的那份在 attempts/NN/repairs/MM/ 里。
quality.json 与它派生的 overall_score / quality_issue_count 一律取修订后的结论,
与 metadata、与最终采用的 story.md 同口径;因此同一层里 quality.json 的分数可能高于
review.json(examples/example_run/ 就是这个样子)。1.x 内保持这个语义不变。
校验不通过不是 Run 失败:status 仍为 completed,validation_passed 为 false,
此时会按策略定点修订或自动重试。校验器自身崩溃也不是 Run 失败:validation_status 为 failed,
validation_error 记录原因,此时不写 validation.json,也不会触发修订或重试。
自动重试达到上限同样不是 Run 失败:quality_status 为 exhausted,所有产物都保留。
只有连正文都拿不到时,Run 才以 generating 阶段失败结束。
完整的字段表与「什么情况下缺文件」见 docs/run-artifacts.md。
运行级除了 metadata.json 还多了 run-manifest.json,回答「这份故事是拿什么跑出来的」:
用的是哪个代码版本与 commit、哪个模型、各阶段 temperature、六个提示词各自的版本与内容摘要、
每次 Attempt 的结局(是否入选 / 为什么重试 / 修了几轮),以及这批产物的 SHA-256 清单。
它自带 schemaVersion、用 camelCase,与 snake_case 的 metadata 契约物理隔离;不顶替
metadata.json,也不改它的任何一个字段。清单只记元数据,不搬运正文与结论文字;模型条目只留
模型名与 provider 分类,baseUrl 原文不落盘;客户端没有下发的 topP / maxTokens 也不写。
API 侧 POST /api/runs 与 GET /api/runs/<run_id> 各多一个 manifest 字段,就是这份清单。
v1.6.0 之前生成的 Run 没有它,读接口返回 null,界面面板整个隐藏。清单只为一次 Run 自证
出身:不做跨 Run 对比、不跑基准、不统计成功率(那些能力保留给后续版本)。
v1.7.0 起,实验里跑出来的每个样本在这份清单上多一个可选的 experiment 块
(experimentId / variantId / repetition)。普通 Run 没有这个键,读取时整个键不出现;
它只记录出身,不参与任何流程判断,也不会因为写不进清单而让一次成功的 Run 变成失败。
出身面板从 v1.7.1 起把这行摆出来(Experiment experiment-id · variant x · repetition n)。
实验不是一次 Run,是一组 Run 按同一份定义跑出来的一份结果。一次实验固定回答一个问题: 「只改这四个变量,分别发生了什么?」
ExperimentDefinition(base + variants[] + repetitions)
→ 展开成 variants × repetitions 个样本(顺序执行,不并发)
→ 每个样本走 GenerationPipeline(一次完完整整的普通 Run)
→ experiments/<experimentId>/{definition.json, runs.json, results.json}
→ 按 Variant 汇总:计数 + 均值
四个可改变量:model、generation.temperature、retry.maxAttempts、retry.minReviewScore。
定义里不出现 baseUrl——一条样本的地址永远来自服务端 LLM_BASE_URL,v1.1.0 的地址关卡
因此对每个 Variant 都生效,实验没有第二条入口可以绕。Prompt 也不是变量:每个阶段只读一份
固定提示词文件,注册表里每个角色只有一个版本,「换 Prompt 版本」在这个版本不产生任何变化,
所以定义里直接拒绝(topP / maxTokens 同理)。凭据 / 地址 / 原始提示词形状的键一律 400。
上限写在契约里:variants 14 个、5、总样本数不超过 12。
「什么都没改」也是合法 Variant——基准自己就是一个 Variant。repetitions 1
单个样本失败不中止实验:剩下的接着跑,失败的那一格记下失败阶段(generating /
planning / …)与一句话摘要,整体状态是 partial。每跑完一格就重写一次 runs.json,
进程中途被杀,GET 也能如实显示已经跑出来的部分。
结果(ExperimentResult)以计数与均值两类数字为主:runCount / successCount /
failureCount 与九个均值(整体分、商业分、四个质量维度、四个商业维度)。缺分数的样本
不参与均值,界面显示 —。results.json 的 runs 数组还逐条记下每个样本自己的
overallScore / commercialScore(读自它自己的产物,v1.7.1 补上),所以样本行也有数字可看。
v1.9.0 起每个变体再多一组失败类别分布(Experiment Failure Distribution):按主要失败
类别数样本、顺序固定,外加「有分析 / 分出类别」两个分母;没有 failure-analysis.json 的
样本不进分母(不当作「没有失败」),1.9.0 之前跑的实验整段隐藏。
没有排序、没有赢家、没有显著性检验、没有自动调参。
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/experiments |
{schemaVersion:"1", name, experimentId, base, variants, repetitions} → 201,只建不跑 |
| GET | /api/experiments |
列表:每个实验一行(变体数 × 次数、状态、成败计数) |
| GET | /api/experiments/<experiment_id> |
{definition, runs, result};没跑过时 runs 与 result 是 null |
| POST | /api/experiments/<experiment_id>/run |
跑完整个实验,回传 ExperimentResult;已在运行时 409 |
界面:/experiments(列表 + 创建)、/experiments/<experiment_id>(变体卡片、结果表、
样本行,每条样本可点进既有的 Run 详情)。完整契约见 docs/experiments.md。
重试策略是一个扁平结构,只有五个字段,全部有默认值:
| Field | Type | Default | Description |
|---|---|---|---|
max_attempts |
integer 1 ~ 5 | 2 |
尝试次数上限,含第一次生成;2 = 初次生成 + 最多 1 次自动重试 |
min_review_score |
number 0 ~ 100 | 70 |
单一总分阈值,达到即满足 |
retry_on_validation_failure |
boolean | true |
校验不通过是否值得重试 |
enable_repair |
boolean | true |
是否开启定点修订(Repair-before-Retry) |
max_repairs_per_attempt |
integer 0 ~ 3 | 1 |
同一次 Attempt 内最多修订几次;0 表示这次尝试不做任何修订 |
max_repairs_per_attempt 是「每次尝试」的上限,不是整次 Run:第 2 次 Attempt 仍然有同样的修订预算。
每次尝试记作一个 GenerationAttempt,也只有固定字段:attempt_number、story、validation、
review、accepted、retry_reason(取值 generation_error / validation_failed /
review_score_below_threshold)、repair_count、repairs[]、error。
RetryDecision 按固定顺序判断,没有权重、没有随机、没有模型参与:
(1) 生成失败且还有剩余次数 → generation_error;(2) 校验不通过且
retry_on_validation_failure 为 true → validation_failed;(3) 审阅成功但
review.score < min_review_score → review_score_below_threshold;(4) 以上都不命中 → 接受当前尝试。
两个组件自身异常的处理:Reviewer 调用失败 / 输出非法只让该次尝试拿不到分数,不会触发重试;
Validator 自身异常只作废「校验不通过」这一格判据,不会把正文判为失败。
到达 max_attempts 后硬停止——不存在「不过就无限重生」的循环。
quality_status 是 Run 级结论,只有两种取值:accepted 与 exhausted。
系统不会在多次尝试里挑一个「最好的」——没有 Best-of-N 择优,也没有重试统计与归因。
| Transport Retry | GenerationAttempt Retry | |
|---|---|---|
| 重试什么 | 同一次 HTTP 请求原样重发 | 带着同一份 StoryConfig / BeatPlan 整篇重新生成 |
| 发生在哪 | src/infrastructure/llm/openai-compatible-llm-client.ts 内部 |
GenerationPipeline + RetryPolicy |
| 触发条件 | timeout / 429 / 临时 5xx(500 / 502 / 503 / 504) | 生成失败且还有次数、校验不通过且策略允许、审阅总分低于阈值 |
| 上限 | 硬上限 2 次,即一次 LLM 调用最多 3 个请求 | max_attempts(默认 2,含第一次生成) |
Transport Retry 绝不无限重试:上限是常量 MAX_TRANSPORT_RETRIES = 2。
单次请求超时由 LLM_TIMEOUT 控制,缺省 180000 毫秒;超时耗尽后抛出 LLMTimeoutError,
映射为 LLM_TIMEOUT + HTTP 504。
| Field | Type | Description |
|---|---|---|
passed |
boolean | true 当且仅当没有 severity: "error" 的 issue |
issues |
ValidationIssue[] | 问题清单(可为空数组) |
ValidationIssue 也只有三个字段:code(稳定问题码)、severity(warning / error,
只有两级)、message(人类可读说明)。
| Code | Severity | 触发条件 |
|---|---|---|
EMPTY_CONTENT |
error | 正文为空或全是空白字符 |
INVALID_OUTPUT |
error | 拿到的是错误 JSON / API 错误串 / 明显错误对象,而不是小说正文 |
TOO_SHORT |
error | 实际字数 < max(300, target_words × 0.15) |
POSSIBLE_TRUNCATION |
error | 启发式判定疑似截断:引号未闭合,或结尾停在句子中间 |
MISSING_ENDING |
warning | 结尾缺少终止标点,看起来没有完整收束 |
MISSING_PROTAGONIST |
error | 配置了 protagonist.name 但正文中找不到该名字 |
字数统计对中文与英文一视同仁:CJK 字符逐个计数,连续的拉丁字母 / 数字算一个词, 标点与空白不计入。规则刻意保持宽松,宁可漏报也不误报。
没有角色一致性分析、动机分析、故事弧检查、Beat 校验、读者体验评价—— Validator 只回答「基本可用吗」,不回答「写得好不好」,也不回答「读者读不读得下去」 (后面两个分别是 Basic Reviewer 与 Commercial Reviewer 的事)。
| Field | Type | Description |
|---|---|---|
score |
number | 总分 0 ~ 100(整数或小数,越界即拒绝) |
summary |
string | 一段总体评价 |
strengths |
string[] | 优点列表(可为空数组,条目会 trim) |
problems |
string[] | 问题列表(可为空数组,条目会 trim) |
没有严重度、证据定位、置信度,也没有 PASS / FAIL 判定——审阅只提供反馈。
审阅用的温度固定为 0.3,与生成温度相互独立。
ReviewResult 有一个可选字段 dimensions:四个键,各自一个 0–100 分 + 一句短评。
| 维度键 | 中文名 | 评的是什么 |
|---|---|---|
coherence |
连贯性 | 设定、称呼、时间线前后是否一致 |
narrative |
叙事 | 结构是否完整、起承转合与节奏是否得当 |
character |
人物 | 言行是否符合其目标与动机 |
causality |
因果 | 事件推进是否有清楚的前因后果 |
连贯性与因果是两件事:前者管「前后对得上」,后者管「推得动」。一个故事可以因果清楚 却前后矛盾,也可以前后一致却推不动。
整体分 score 在有维度时等于四维均分(四舍五入到 1 位小数),对齐方式是纯算术,
不引入权重、不让模型自己解释。维度一旦出现就必须四个齐全、各自 0–100 且带非空短评:
缺维度、多维度(模型自己扩到 35 维也一样)、分数越界都按审阅输出非法处理——
不补 0、不挑一个先凑着,因为补出来的数会被当成真实评价参与展示。
维度只影响「看得见多少」,不影响「怎么决策」:没有按维度设的阈值,没有维度驱动的重试或修订。
没有维度的旧结论(1.3.0 之前的 review.json)一路照常可用。
与 ReviewResult 完全并列的第二个结论:同一个故事,一份看结构(Co/N/C/Ca),一份看
「读者会不会继续读下去」。两者是两个独立的审阅者——各自的提示词、各自的解析器、
各自的产物文件(review.json / commercial-review.json),不合成一个八维大 Prompt,
也不互相改写对方的结论。
| Field | Type | Description |
|---|---|---|
score |
number | 商业整体分 0 ~ 100;永远由四个维度重算(等权均分,四舍五入到 1 位小数),模型自报值只作形状校验 |
summary |
string | 一段话说清这篇正文的商业可读性整体表现 |
strengths |
string[] | 商业可读性上的优点(可为空数组) |
problems |
string[] | 商业可读性上的问题(可为空数组) |
suggestions |
string[] | 可以怎么改的建议(可为空数组) |
dimensions |
object | 四个固定维度,见下 |
四个维度一个不能少、也不许多出别的(模型自己扩到 6 个也算非法),各自 0~100 分加一句 非空短评,整体分就是这四个数的均分:
| 维度键 | 中文名 | 评的是什么 |
|---|---|---|
hook |
Hook(开篇抓力) | 只看开头:冲突 / 悬念是否尽早出现,前几段有没有抓力 |
pacing |
Pacing(节奏) | 只看阅读速度与拖沓感:推进快不快、信息密度、有无重复 |
engagement |
Engagement(全篇持续阅读动力) | 只看全篇:悬念是否持续、冲突是否维持、读到哪里容易弃读 |
payoff |
Payoff(回报) | 只看承诺与兑现:高潮够不够值、结尾是否回应主要冲突 |
两条边界必须分清:Hook ≠ Engagement(开头抓人但中段崩塌,是 Hook 高、Engagement 低); Pacing ≠ Narrative(结构成立但读起来拖沓,是 Pacing 低)。 评分描述的是文本本身可观察的事实,不预测市场、销量、读者规模或商业结果。
商业可读性分数不驱动任何决策:RetryPolicy 里仍然只有 min_review_score 一个总分门槛
(比的还是结构审阅的整体分),修订策略仍然只看问题类别,QualityResult 里也没有
商业分。审阅用的温度固定为 0.3,与结构审阅同级、与生成温度相互独立。这一步自身失败
只让 commercial_review_status 变成 failed,Run 与其它结论照常成立。
POST /api/review/commercial 可以单独跑一次:同样的模型、同样的规则,
带 run_id 时覆盖该 Run 根目录的 commercial-review.json。
把上面两样结论与采纳结论汇到一起的统一快照。字段就是「一处校验结论 + 一处审阅结论 + 一个采纳结论」,没有等级、没有趋势:
| Field | Type | Description |
|---|---|---|
overall_score |
number | null | 单一整体分;有维度时取四维均分,否则取 review.score。没有审阅结论时是 null(不是 0) |
validation_passed |
boolean | null | true / false;校验没跑(组件自身异常)时是 null,与「校验没过」是两件事 |
accepted |
boolean | RetryPolicy 的采纳结论,不参与判定 |
issues |
QualityIssue[] | 问题清单(可为空数组) |
suggestions |
QualitySuggestion[] | 建议清单(可为空数组) |
summary |
string | null | 审阅的总体评价;审阅失败或没给时是 null |
dimensions |
object? | v1.3.0 可选:四个基础维度的分数与短评,由 ReviewResult.dimensions 原样搬运;没有维度时整个键不出现 |
QualityIssue:id(validation-N / review-N)、source(validation / review)、
category(校验侧是 issue code,审阅侧统一 review_problem)、message、可选 severity。
QualitySuggestion:id(review-suggestion-N)、source、message。
QualityAssembler 是纯函数:同一输入永远得到同一份 JSON,不调用模型、无随机,
读的三样东西(ValidationResult / ReviewResult / 采纳结论)在 v1.2.0 之前就都在产物里。
它只汇总,不参与任何校验、审阅或重试判定;维度也只是从审阅结论搬到快照里,
不在这里重新打分。
快照出现在三个地方,内容一致:Run 根与 attempts/NN/ 下的 quality.json、
Run 类入口与两个读回接口响应里的 quality、前端 Quality Summary 面板。
修订目录 attempts/NN/repairs/MM/ 里没有 quality.json。v1.2.0 之前的 Run 没有这个文件,
读取时按同一套规则临时装配,不会因此失败。装配取的是这次尝试最终留下的那一版结论
(发生过修订时是修订后那一轮,与 metadata.json 同口径,v1.2.1 起如此)。
写正文之前对 BeatPlan 的结构校验结论。StoryValidator 看正文,这里看骨架(§2)—— 两套结论各自独立,谁也不算谁的输入。
| Field | Type | Description |
|---|---|---|
passed |
boolean | true 当且仅当没有 severity: "error" 的 issue;由 issues 重新推导,不采信模型自报值 |
issues |
BeatValidationIssue[] | 命中项清单(可为空数组) |
summary |
string | 一句话说清这份骨架结构上成不成;面板直接展示,不再二次拼接 |
BeatValidationIssue 四个字段:code(稳定问题码)、severity(warning / error,
只有两级)、message(人类可读说明)、可选 beat_ids(涉及哪几拍,指认不到就不填)。
没有 rewritten_plan / fixed_beats / suggested_plan——本版本只报告,不修复。
| Code | Severity | 谁判的 | 触发条件 |
|---|---|---|---|
EMPTY_PLAN |
error | 规则层 | BeatPlan 里没有任何一拍 |
TOO_FEW_BEATS |
warning | 规则层 | 拍数少于 3,正面建立 / 冲突升级 / 高潮收束必然挤在一起 |
BROKEN_SEQUENCE |
error | 规则层 | 编号不是从 1 连续递增 |
DUPLICATE_BEAT |
error | 规则层 | 两拍同一个 id |
MISSING_OPENING |
warning | 模型 | 没有哪一拍承担建立人物 / 处境 / 世界观的正面任务 |
MISSING_ESCALATION |
warning | 模型 | 没有冲突升级或转折,拍与拍之间原地踏步 |
MISSING_CLIMAX |
error | 模型 | 没有高潮或决定性对抗,故事缺一个顶点 |
MISSING_RESOLUTION |
error | 模型 | 没有收束,结局处于悬空状态 |
CHARACTER_STATE_CONFLICT |
error | 模型 | 同一人物在不同拍之间状态互相矛盾 |
UNSUPPORTED_TURN |
error | 模型 | 某一拍的转折没有任何前文铺垫 |
ENDING_NOT_PREPARED |
warning | 模型 | 结局所需的条件(道具 / 信息 / 关系)从未在前文出现 |
前四条由规则层(不调用 LLM 的纯函数)判定,出现 error 时直接下结论、不再请求模型——
不花冤枉钱。后七条交给模型判结构,温度固定为 0.2,与生成温度相互独立。
规则层与模型报到同一处问题时只保留一份。
校验不通过不是 Run 失败,而是 Run 提前结束:status 为 failed、current_stage 为
validating_beat_plan,beat_validation_passed 为 false,产物里留下 beats.json 与
beat-validation.json——够你看出是哪儿站不住,但一个 Attempt 都没跑。
只有 warning 时照常生成,结论同样落盘。BeatValidator 自身崩溃时
beat_validation_status 为 failed、beat_validation_error 记录原因,生成流程不受影响,
此时没有 beat-validation.json。没跑过这一步的 Run(v1.4.0 之前)
beat_validation_status 是 not_started、beat_validation 是 null。
POST /api/validate-beats 可以单独校验一份骨架:同样的模型、同样的规则,
但不写任何产物——Run 里那一份 beat-validation.json 由 Pipeline 自己负责。
v2.1.0 在 StoryLoop 2.0 平台架构上升级质量系统:Beat Validator、故事质量 Reviewer 与 Commercial Reviewer 统一使用结构化 Diagnostic 语言,并在不改变既有 Co/N/C/Ca 与 H/P/E/Pf 指标语义的前提下,提高诊断的一致性与可解释性。
v2.0.0 及以前,这三套组件各说各话:Beat 校验是 BeatValidationIssue(code + severity +
beat_ids),商业审阅只有一句 problems 字符串。v2.1.0 让它们统一到同一个
QualityDiagnostic 模型上,于是同一个诊断既能进 quality-stack.json,也能进界面的
Quality Center,还能被 FailureAnalyzer 当结构化证据读。
| Field | Type | Description |
|---|---|---|
id |
string | 稳定标识,由校验方按输出顺序指派(<source>-<序号>),不让模型自己编 |
source |
string | beat-validator / quality-reviewer / commercial-reviewer,由解析方给定 |
category |
string | 稳定问题码,按来源各认各的白名单(见下) |
severity |
string | info(可优化)/ warning(明显影响质量,通常不阻止 Pipeline)/ error(结构性问题,可阻止当前阶段) |
target |
string | beat-plan / story / opening / middle / ending / character / global |
message |
string | 人类可读说明,不能为空 |
suggestion |
string? | 可选的非空建议,没给就不出现 |
relatedBeatIds |
number[]? | 可选:涉及哪几拍 |
三个来源各认自己那一份 category 白名单:Beat 校验直接沿用 v1.4.0 的十一个稳定 Code
(于是旧的 issues 与新的 diagnostics 是同一批事实的两种写法,映射时不需要翻译表);
故事质量审阅用十二个类别(causal_gap / weak_climax / character_inconsistency …);
商业可读性审阅用另外十个(weak_opening / slow_pacing / weak_payoff …)。
故事质量诊断写进商业类别、或反过来,一律按非法输出处理——两套评价体系不做换算。
完整清单见 docs/quality-stack.md。
| 组件 | 既有结论(逐字未动) | v2.1.0 新增 |
|---|---|---|
| Quality Reviewer v2 | score(四维均分)+ Co/N/C/Ca 四维 + summary |
diagnostics(结构化问题清单) |
| Beat Validator v2 | passed + issues + summary |
diagnostics(与 issues 同一批事实) |
| Commercial Reviewer v2 | score(四维均分)+ H/P/E/Pf 四维 + summary |
diagnostics(结构化问题清单) |
没有一个分数被改写过:四维均分的算法、0–100 口径、review.score /
commercial_review.score / beat_validation.issues 全部与 2.0.0 逐字一致;
review.json / commercial-review.json / beat-validation.json 的既有字段一个不少,
v1.x 客户端的读法完全不变。
每次 Run 在 runs/<run_id>/ 下多落一份 quality-stack.json(只在 Run 根目录一份,
attempts/ 与 repairs/ 下都没有它),运行级固定文件数从十二个变十三个:
| Field | Type | Description |
|---|---|---|
schemaVersion |
string | 本文件自身的 schema 版本,当前恒为 "1" |
status |
string | complete(三套结论都在)/ partial(至少一套在)/ failed(一套都没有),按在位模块确定性换算 |
beatValidation / qualityReview / commercialReview |
object? | 三套结论原样收录,一分不改、一个维度不补;没跑出来时整键不出现 |
diagnostics |
QualityDiagnostic[] | 三套诊断按 骨架 → 故事质量 → 商业可读性 合并后轻量去重(只按 source + category + target + message 判重) |
summary |
object | totalDiagnostics / errors / warnings / info,由 diagnostics 现算 |
它不是第四套评价:不调用模型、不重新评分、不新增分数。读不到(2.0.0 及更早的 Run、
文件损坏、形状不对)时统一读作 null,磁盘上不会被补写。
| 出口 | 说明 |
|---|---|
GET /api/runs/<run_id>/quality-stack |
v2.1.0 新增只读路由,回 {qualityStack: {status, diagnostics, summary}};旧 Run 是 {qualityStack: null}(仍是 200) |
| Run 详情响应 | 多一个可选字段 qualityStack,形状同上;三套结论的本体仍在 review / beat_validation / commercial_review 各就各位 |
| Quality Center 面板 | Run 详情页多一块:Overview 三行(Planning / Story Quality / Commercial)+ 按 severity 分组的诊断清单 + 四类筛选(来源 / severity / target / category,同时生效)。没有 quality-stack.json 的 Run 整个区域隐藏 |
- 诊断只说明,不驱动。 不触发重试、不触发修订、不进采纳判定、不换模型、不换 Prompt。
诊断从 1 条变 12 条,
RetryPolicy与RepairStrategy的判定一字不变。 - 失败分析可以读它,但不会因此变成归因。
failure-analysis.json可以把诊断当证据 引用,仍然只分类、不推断原因。 - 不落敏感数据。 诊断只走既有的 secret-safe 平台边界序列化:
quality-stack.json里 没有 API Key、没有Authorization/Cookie头、没有原始环境变量、没有带密钥的 URL。
逐字段契约见 docs/quality-stack.md。
请求五个字段:config、beat_plan、story、issue_type、issue_message。
结果:repaired_story、issue_type、success、notes——没有 diff、没有 patch、没有评分。
问题类别只有六个,由 RepairStrategy 按固定规则推出(纯函数,无 LLM、无学习、无分类模型):
先看校验结论(按固定优先级取一个 error 级问题),没有可修的再看审阅问题(关键词命中第一个算数)。
| issue_type | 典型触发 |
|---|---|
length |
校验码 TOO_SHORT;审阅问题命中「字数 / 太短 / too short / brief」 |
ending |
校验码 MISSING_ENDING、POSSIBLE_TRUNCATION;审阅问题命中「结尾 / 结局 / 收束 / abrupt」 |
character_presence |
校验码 MISSING_PROTAGONIST;审阅问题命中「主角 / 人物缺席」 |
continuity |
审阅问题命中「矛盾 / 前后 / 连贯 / 脱节 / contradict」 |
structure |
审阅问题命中「结构 / 中段 / 断层 / pacing」 |
general |
审阅问题一个关键词都没命中时的兜底类别 |
一次只修一个主要问题;拿不准就不猜:校验码 EMPTY_CONTENT / INVALID_OUTPUT
被认为「整篇不可用」,不尝试修订,直接整篇重生。修订拿不到非空正文时返回 200 但
success 为 false——「没修好」是一次诚实的业务结果,不当成 502 报错。
v1.8.0 为 StoryLoop 增加 Run 级可观测性,记录各阶段耗时、LLM 调用、Retry / Repair 次数、 失败阶段以及真实可得的 token / cost 信息,但这些数据只用于观察,不会自动改变生成策略。
每次 Run 多落一份 runs/<run_id>/telemetry.json,与 metadata.json(状态摘要)、
run-manifest.json(出身清单)并列——三份文件回答三个不同的问题,互不替代。
字段级契约见 docs/telemetry.md。
| 能力 | 记的是什么 |
|---|---|
| Run Telemetry | 这次 Run 的总时长、起止时间、进程级结局 |
| Stage Timing | 每个阶段的起止与耗时;同一个阶段跑多次就记多次,不预先求平均 |
| LLM Call Tracking | 每次模型调用的阶段 / 模型 / 耗时 / 结局;transport 重试算在同一次调用里,不拆开 |
| Retry / Repair Counts | Attempt 数换算出的 retries;定点修订次数挂在各 Attempt 名下 |
| Failure Stage | 失败发生在哪个阶段,配一个稳定错误码——是位置,不是原因 |
| Token Usage | Provider 真实返回的 usage;一次都没给就是没有,不补 0 |
| Optional Cost Tracking | Provider 同时给了金额与币种才记,否则成本这一行整个不出现 |
| Experiment Efficiency Metrics | 实验里每个 Variant 的耗时 / 调用 / token / 重试 / 修订均值 |
三条边界是这一版的底线:
- 只观察,不控制。 遥测里没有一个字段是给重试、修订或接受判定用的。没有任何代码读遥测 来决定下一步怎么跑——同样的输入仍然得到同样的重试次数与修订类别。
- 没有就是没有。 Provider 没给 usage 时 token 三项是
null、成本整行不出现,界面显示—而不是 0。「没有数字」和「数字是零」在这里永远是两件事。 - 不落正文、不落密钥、不落原始异常。 完整 Prompt、正文与用户输入在别的产物里;
API Key、
Authorization头、Cookie、原始请求 / 响应头、环境变量原文一个字都不进telemetry.json;异常只降级成一个稳定错误码。
界面在 Run 详情页多一块「可观测性」面板:总览(总时长 / 调用次数 / Attempt / 重试 /
修订 / token)+ 阶段时间线 + 模型调用表 + Attempt 列表。1.8.0 之前的旧 Run 没有这个文件,
面板整个隐藏,GET /api/runs/<run_id>/telemetry 返回 {telemetry: null}(200),
磁盘上不会被补写。失败 Run 也保存遥测:status: failed 配上已经发生过的阶段,
于是一次失败的 Run「走到哪一步、调了几次模型」是读得出来的。
v1.4.0 起 Storyloop 只回答「哪一步失败了」,v1.9.0 把这件事往前推了一步: 把「这次失败是哪一类」按同一份固定的 12 类清单整理成结构化分类,并为每个判断 挂上直接证据。它回答的是分类问题,不是因果问题——没有任何字段宣称自己找到了原因。
每次 Run 在 metadata.json 与 telemetry.json 旁多落一份 failure-analysis.json
(camelCase,自带 schemaVersion,与那份确定性产物放同一层目录)。
字段级契约见 docs/failure-analysis.md。
| 字段 | 是什么 |
|---|---|
status |
none(没检测到运行级失败)/ detected / partial / unknown |
primaryCategory |
主要失败类别;不确定时是 null,不猜 |
secondaryCategories |
次要类别,固定优先级排序后的完整列表 |
summary |
一句话结论:哪一类、在哪个阶段、依据是什么 |
signals |
信号表:code、来源、严重度、说明 |
evidence |
证据表:每条信号指向哪份文件的哪个字段、哪个 Attempt、哪一轮修订 |
firstFailureStage |
首个失败阶段(planning / generating / …) |
terminalState |
终态(completed / failed / …) |
12 个类别按固定优先级排列,越靠前越优先——一次失败同时踩到多类时只归最靠前的那一类,
其余进 secondaryCategories。这个顺序是写死的,不按次数、不按分数重排:
SECURITY(安全策略阻止) > CONFIGURATION(配置问题) > STORAGE(产物存储问题) >
PLANNING(剧情骨架问题) > GENERATION(正文生成问题) > VALIDATION(正文校验问题) >
REVIEWER(审阅环节问题) > RETRY_EXHAUSTION(重试次数用尽) >
REPAIR_EXHAUSTION(修订次数用尽) > QUALITY(质量偏低) > COMMERCIAL(商业可读性偏低) >
UNKNOWN(未能归类)。
六项能力(Failure Signals 与 Evidence Linking 是同一枚硬币的两面:信号是判断,证据是依据):
- Structured Failure Categories:类别是固定清单,不是自由文本;清单见
src/domain/failure-analysis.ts的FAILURE_CATEGORIES。 - Failure Signals:每条信号都有稳定 code、来源与严重度(
RETRY_LIMIT_REACHED、UNRECOGNIZED_FAILURE_CODE这类),不写自由发挥的措辞。 - Evidence Linking:没有证据就没有类别。每条信号都带
evidence,指向具体产物与字段 (validation.json · issues[0].code/metadata.json · attempt_count/run-manifest.json · repairs[0].succeeded这类);找不到证据的信号不出现。 不确定就说不知道:证据不足时status是unknown、primaryCategory是null, 绝不用一个猜出来的类别把空位填满。 - Retry / Repair Exhaustion Detection:
RETRY_EXHAUSTION/REPAIR_EXHAUSTION只由真实 计数与RetryPolicy的上限判定(attempt_count对max_attempts、repair_count对max_repairs_per_attempt),不靠推测。 - Primary / Secondary Failure Classification(主要 / 次要类别归谁):主要类别取优先级
最高的那一个,其余按同一优先级排进次要类别;
unknown只在「有失败迹象但归不进任何一类」时出现, 遇到不属于清单的错误码时保留原文、降级归到该阶段最接近的已知类别或UNKNOWN。
界面在 Run 详情页多一块「失败分类」面板(src/components/failure-panel.tsx):
结论 + 四个字段(主要 / 次要 / 首个失败阶段 / 终态)+ 信号表 + 证据表。
面板是只读的:没有重试、没有重跑、没有「按建议修复」——它不自动重试、不自动修订、
不改任何生成策略,也没有任何一行文字在回答「为什么会失败」。成功的 Run 只有一句话:status: "none" +「没有检测到运行级失败」,
不摆空表格。
实验级聚合(Experiment Failure Distribution):实验详情页的每个变体多一行
「有分析 / 分出类别 / 分布」。只按主要类别数样本,顺序同样是固定优先级;
没有 failure-analysis.json 的样本不进分母——不当作「没有失败」;
1.9.0 之前跑的实验整段隐藏,不显示空表、不补 0。
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/runs |
{config, retry_policy?} → 自动模式完整 Run |
| POST | /api/runs/from-plan |
{config, beat_plan, retry_policy?} → 手动模式 Run(跳过规划) |
| POST | /api/plan |
StoryConfig → BeatPlan(只规划,不生成正文) |
| POST | /api/generate |
兼容入口,等价于 /api/runs/from-plan |
| POST | /api/review |
{config, story}(+可选 run_id)→ 单独审阅正文 |
| POST | /api/review/commercial |
{config, story}(+可选 run_id)→ 单独做商业可读性审阅(v1.5.0) |
| POST | /api/validate |
{config, story}(+可选 run_id)→ 单独校验正文 |
| POST | /api/validate-beats |
{config, beat_plan} → 单独校验剧情骨架的结构,不写任何产物 |
| POST | /api/repair |
{config, beat_plan, story, issue_type, issue_message} → 单独定点修订 |
| POST | /api/prompt/preview |
config(+可选 beat_plan)→ 渲染后的最终 Prompt,不调模型 |
| GET | /api/runs/<run_id> |
读回一次 Run 与它的 Attempt 摘要 |
| GET | /api/runs/<run_id>/telemetry |
v1.8.0 新增:读回这次 Run 的遥测;旧 Run 没有 telemetry.json 时是 {telemetry: null} |
| GET | /api/runs/<run_id>/failure-analysis |
v1.9.0 新增:读回这次 Run 的失败分析;失败或旧 Run 没有 failure-analysis.json 时是 {failureAnalysis: null} |
| GET | /api/runs/<run_id>/quality-stack |
v2.1.0 新增:读回这次 Run 的统一质量视图;旧 Run 没有 quality-stack.json 时是 {qualityStack: null} |
| GET | /api/runs/<run_id>/attempts/<n> |
读回某一次 Attempt 的详情 |
| POST | /api/experiments |
建一份实验定义(201)。只建,不跑 |
| GET | /api/experiments |
实验列表:定义摘要 + 结果状态,不带样本详情 |
| GET | /api/experiments/<experiment_id> |
一份实验的定义 + 格子 + 结果(没跑过时后两者是 null) |
| POST | /api/experiments/<experiment_id>/run |
顺序跑完整个实验,回传 ExperimentResult |
| GET | /api/health |
{status: "ok"} |
| GET | /api/version |
{version: "<VERSION 文件内容>"} |
| GET | /api/projects |
v2.2.0 新增:项目列表(未归档在前,再按更新时间倒序) |
| POST | /api/projects |
v2.2.0 新增:建项目;只认 name / storyConfigRef / isFavorite |
| GET | /api/projects/<id> |
v2.2.0 新增:项目详情(项目 + 归属的 Run + 稿件 id) |
| PATCH | /api/projects/<id> |
v2.2.0 新增:改名 / 收藏 / 归档 |
| GET | /api/projects/<id>/documents |
v2.2.0 新增:稿件列表(不含正文) |
| POST | /api/projects/<id>/documents |
v2.2.0 新增:从一次 Run 建稿(复制,不回写 Run);只认 runId |
| GET | /api/projects/<id>/documents/<docId> |
v2.2.0 新增:读一篇稿(含正文) |
| PATCH | /api/projects/<id>/documents/<docId> |
v2.2.0 新增:保存;只认 title / content / status / isFavorite |
| GET | /api/projects/<id>/exports |
v2.2.0 新增:导出历史 |
| POST | /api/projects/<id>/exports |
v2.2.0 新增:导出一篇稿件为 DOCX / EPUB |
| GET | /api/projects/<id>/exports/<exportId> |
v2.2.0 新增:下载已导出的文件(二进制) |
| GET | /api/projects/<id>/health |
v2.2.0 新增:项目健康结论(确定性,不调模型) |
| GET | /api/benchmarks/suites |
v2.3.0 新增:全部已存 Benchmark Suite 的摘要(含来源与许可) |
| GET | /api/benchmarks/suites/<id> |
v2.3.0 新增:某一版 Suite 全文;?version= 指定版本,缺省读最新版 |
| GET | /api/benchmarks/executions |
v2.3.0 新增:执行列表;?suiteId= 只看一份 Suite 的历史 |
| POST | /api/benchmarks/executions |
v2.3.0 新增:跑完一次执行(请求体只有 suiteId / suiteVersion / label / allowLargeBenchmark) |
| GET | /api/benchmarks/executions/<id> |
v2.3.0 新增:执行详情(头 + 样本 + 汇总);?compare=<另一个执行 id> 带逐指标比较 |
| GET | /api/benchmarks/executions/<id>/export |
v2.3.0 新增:导出(?format=json|csv);同一份执行导出两次字节一致 |
| POST | /api/benchmarks/executions/<id>/baseline |
v2.3.0 新增:标记 / 取消标记基线(会话期标签,不参与排序与计算) |
| GET | /api/benchmarks/history |
v2.3.0 新增:历史图取数;?suiteId= 只看一份 Suite |
Run 类路由的字段与错误码与 v2.1.0 逐字一致;工作区十二条路由是纯新增, 字段级契约见 docs/workspace.md。
retry_policy 可省略,省略时用默认值。它不属于 StoryConfig,不会写进 config.json,
只会记录在 Run 的 metadata.json 里。非法值返回 400,Run 不会开始。
artifacts 是产物文件名映射(config / beat_plan / story / metadata,
校验、审阅或质量装配各自成功时追加 validation / review / quality,
跑过 Beat 结构校验时追加 beat_validation,商业可读性审阅成功时追加 commercial_review),
响应中不会返回服务器绝对路径。
校验不通过不会让 Run 失败——story 与 status: "completed" 照常返回,validation.passed 为 false,
HTTP 状态码仍然是 200(这是一次成功的业务结果,不是错误)。
失败响应统一为一个形状,code 是稳定枚举,message 是给人看的一句话:
{
"error": {
"code": "CONFIG_INVALID",
"message": "StoryConfig 校验失败:target_words 必须是整数。",
"run_id": "20260922_101500_ab12cd",
"stage": "config"
}
}| code | HTTP | 什么时候出现 |
|---|---|---|
CONFIG_INVALID |
400 | 请求体 / StoryConfig / BeatPlan / RetryPolicy / 修订类别非法 |
RUN_NOT_FOUND |
404 | run_id 不存在或格式非法 |
LLM_TIMEOUT |
504 | 单次请求超时,且 transport retry 已用尽 |
LLM_REQUEST_FAILED |
502 | 模型接口返回错误(401 / 429 / 5xx 等)或传输层失败 |
PLANNER_INVALID_OUTPUT |
502 | 规划阶段拿不到合法 BeatPlan |
GENERATION_FAILED |
502 | 生成阶段失败(含连正文都没拿到的最后一次 Attempt) |
REVIEW_FAILED |
502 | 单独审阅入口的模型输出非法 |
COMMERCIAL_REVIEW_FAILED |
502 | 商业可读性审阅拿不到合法 CommercialReviewResult(v1.5.0,与 REVIEW_FAILED 并列) |
REPAIR_FAILED |
502 | 单独修订入口的模型输出非法 |
BEAT_VALIDATION_FAILED |
502 | 骨架结构校验拿不到合法 BeatValidationResult(v1.4.0) |
VALIDATION_FAILED_INTERNAL |
500 | Validator 自身崩溃(不是「校验不通过」) |
EXPERIMENT_INVALID |
400 | v1.7.0 新增:实验定义不合法(含凭据 / 地址 / 原始提示词形状的键),或服务端没配 LLM_API_KEY 却请求跑实验 |
EXPERIMENT_NOT_FOUND |
404 | v1.7.0 新增:experiment_id 不存在 |
EXPERIMENT_CONFLICT |
409 | v1.7.0 新增:同名实验已存在、这份实验已经跑过(定义与结果都不可变),或它正在运行中 |
ARTIFACT_WRITE_FAILED |
500 | 产物写入失败(磁盘 / 权限 / 目录被占用) |
BENCHMARK_INVALID |
400 | v2.3.0 新增:Benchmark 请求体不合法、Suite 不合法、固定骨架缺文件、服务端没配模型或密钥、导出格式不认 |
BENCHMARK_NOT_FOUND |
404 | v2.3.0 新增:没有这个 Suite / 这次执行 |
BENCHMARK_CONFLICT |
409 | v2.3.0 新增:同一版本 Suite 正在跑;样本数超过安全线且没显式 allowLargeBenchmark |
BENCHMARK_WRITE_FAILED |
500 | v2.3.0 新增:Benchmark 数据写不进去(终态再写、目录不可写、磁盘满) |
INTERNAL_ERROR |
500 | 未预期异常;message 固定为「服务器内部错误」 |
用户错误一律 4xx,运行时错误 5xx。响应里永远不出现堆栈与服务器绝对路径:异常文本会先过
src/domain/safe-text.ts(绝对路径替换为 <path>、凭据打码),堆栈只进服务端日志。
curl 示例与逐字段说明见 docs/api.md。
storygen run StoryConfig → Plan → Attempt(Generate → Validate → Review → Repair? → Decide)→ Retry?
storygen plan 只产出 BeatPlan
storygen review 对已有正文单独审阅
storygen validate 对已有正文单独跑硬性规则(不调模型,不需要 API Key)
storygen repair 对已有正文定点修订一次
| 参数 | 命令 | 说明 |
|---|---|---|
--config |
全部 | 必填:StoryConfig JSON 文件 |
--beats |
run / repair | 手动模式用编辑过的 BeatPlan 直接生成 |
--story |
review / validate / repair | 要处理的正文文件 |
--model / --base-url / --temperature |
全部(validate 除外) | 覆盖服务端 LLM 设置;温度只作用于规划与生成——审阅固定 0.3、商业可读性审阅固定 0.3、修订固定 0.5,三者与生成温度相互独立(v1.4.1 起 CLI 会明说这一点) |
--max-attempts / --min-score |
run | 1 |
--enable-repair / --no-repair / --max-repairs |
run | 定点修订开关与上限 0~3 |
--issue-type / --issue-message / --out |
repair | 修订类别 / 问题原文 / 输出路径 |
--help / -h 在任何位置都认:用法打到标准输出,退出码 0。
参数解析不出数字(如 --temperature abc)按参数错误处理,退出码 2,不会静默用缺省值。
完整契约见 docs/cli.md。
四类配置互不越界,各自只有一个来源,优先级固定为「请求覆盖 > 环境变量 > 默认值」:
| 配置 | 来源 | 说明 |
|---|---|---|
| Application Settings | 环境变量 + 默认值 | RUNS_DIR / LOG_LEVEL / LLM_TIMEOUT;与模型无关,不是每请求可调的 |
| LLM Settings | 请求覆盖 > 环境变量 > 默认值 | LLM_BASE_URL / LLM_MODEL / temperature / timeoutMs,全部非敏感;温度只驱动规划与生成,审阅 / 商业可读性审阅 / 修订 / Beat 校验各有固定温度 |
| StoryConfig | 请求体 / configs/*.json |
故事内容与创作目标 |
| RetryPolicy | 请求体可选 retry_policy |
五项策略,见上 |
| 变量 | 缺省 | 说明 |
|---|---|---|
LLM_BASE_URL |
https://api.openai.com/v1 |
兼容 OpenAI 的接口地址(含 /v1) |
LLM_API_KEY |
空 | API Key;留空时调用真实 LLM 的接口返回可读错误,而不是静默失败 |
LLM_MODEL |
gpt-4o-mini |
模型名 |
LLM_TIMEOUT |
180000 |
单次 LLM 请求超时(毫秒);transport retry 共用这一个上限 |
LOG_LEVEL |
INFO |
DEBUG / INFO / WARNING / ERROR;无法识别的值回落到 INFO |
RUNS_DIR |
runs |
Run 产物根目录(相对仓库根或绝对路径) |
API Key 只存在于服务端:LLM_API_KEY 只在 src/infrastructure/llm/openai-compatible-llm-client.ts 里从服务端环境读取一次,
不进请求覆盖、不进任何返回值、不进任何产物、不进浏览器。.env 已被 .gitignore 忽略,
仓库里只有空模板 .env.example。
请求体里的 baseUrl 覆盖会带着这把密钥去请求对应地址,所以它不能指向任意主机:
只允许 http / https 的公网地址,本机、环回、私网、链路本地(含云元数据地址)与保留段一律在
发出请求前按 400 CONFIG_INVALID 拒掉,域名还要解析出地址后再判一遍;IPv6 里内嵌 IPv4 的
mapped / NAT64 地址按内嵌的那个地址判
(详见 docs/api.md 的「baseUrl 覆盖的地址限制」)。
服务端自己的 LLM_BASE_URL 是运维的受信配置,不受这套规则约束;CLI 的 --base-url 与它同级,
见 docs/cli.md。
这些是刻意不做的,不是待修的缺陷。每一条都有对应说明:
- 维度只有四个,且只是评价输出:审阅除了 0–100 整体分只给连贯性 / 叙事 / 人物 / 因果
四个基础维度,没有更细的拆解、没有子维度、没有维度阈值、没有维度驱动的重试或修订
(整体分即四维均分,重试仍只看
min_review_score一个门槛) - 没有 Best-of-N 择优:取第一个满足策略的 Attempt,不会在多次尝试里挑「最好的」
- 没有质量门禁:没有 PASS / FAIL 判定,
quality_status只有accepted/exhausted两种 - 没有商业可行性预测:商业可读性审阅(v1.5.0)只评价文本本身可观察的读者体验 (Hook / Pacing / Engagement / Payoff),不评估市场适配、读者规模、销量或商业结果, 也没有「爆款概率」一类的数字
- 实验不是基准平台:v1.7.0 的受控实验能批量跑多个 Variant 并按 Variant 汇总计数与均值,
但它不排名、不评选赢家、不做显著性检验、没有跨实验统计口径、没有评分回归集;
一个定义里的样本总数上限 12(
variants14 ×5),变量只有repetitions1model/temperature/retry.maxAttempts/retry.minReviewScore四个 - Benchmark 不是排行榜(v2.3.0):它给的是「这份 Suite 在这次执行里量到了什么」。 没有 leaderboard、没有加权总分、没有「哪次跑得最好」的结论;执行列表与历史图里的顺序是 时间与 id,不是成绩,点也不按分数重排
- Benchmark 不做显著性检验(v2.3.0):比较只给两侧各自算出的事实与差值,没有 p 值 / 置信区间 / 效应量,也不解释差异为什么发生——协议固定的是输入条件,不是因果设计。 要因果结论请走受控实验(docs/experiments.md)
- Benchmark 不改任何行为(v2.3.0):没有任何代码读一次执行的结果来决定重试、修订、 采纳、换模型或换 Prompt;Runner 与 Pipeline 之间只隔着「按顺序跑完这些样本」
- Benchmark 的样本数有安全线(v2.3.0):默认 30 条,超过要显式
allowLargeBenchmark: true, 硬上限 200 条(每一条样本都是一次完完整整的 Run,花的都是真实调用)。跑到一半被杀留下的 是如实记的partial,不谎报completed;再次开跑是从第一题重新开始 - Benchmark 不迁移、不搬动既有产物(v2.3.0):它只认自己的 Suite 与执行三件套,
已有的 Run 与实验原样可读,Benchmark 样例的正文、提示词与遥测仍然只在
runs/<run_id>/里 - 没有自动调参与自动搜索:实验只执行你写下来的条件,不尝试新组合、不根据结果反推更好的 参数、不优化 Prompt;同一份定义重跑会被 409 挡住(定义与结果都不可变)
- 实验没有断点续跑:跑到一半被杀,磁盘上留着前几格的进度(
GET看得见),但再次开跑是 从第一格重新开始,已经花过钱的样本会再跑一遍;跑的时候不要中途杀进程 - 没有高级可观测性:v1.8.0 有了 Run 级的
telemetry.json(阶段耗时 / 调用次数 / 重试修订 / 失败阶段 / 真实 usage),但仅此而已——仍然没有跨 Run 的聚合视图,没有 Metrics / Trace / Prometheus / OpenTelemetry / Dashboard,也没有阈值、基线或告警 - 失败分析只分类,不归因:v1.9.0 的
failure-analysis.json把失败归进固定的 12 类, 并给每条判断挂上直接证据,但它不回答「为什么会失败」——没有因果图、没有原因字段、 没有「哪个组件最可能出问题」,也不推断 Prompt 长短、模型能力或温度是否合理 - 没有自动补救与自适应重试:失败分析同样是只读的——没有任何代码读它来决定重试、 修订或采纳,也没有「按建议修复」这类动作;同样的输入得到同样的重试次数与同样的修订类别
- Beat 校验只管结构,且只报告:它不评价规划质量(这一拍写得好不好、该不该这么排), 不做跨拍因果推演,也没有自动改写、重排、补拍或重新规划——发现问题后由你决定怎么改
- 统一诊断只说明,不驱动(v2.1.0):三套质量组件说同一种
QualityDiagnostic, 但诊断条数不改变任何判定——重试仍只看min_review_score一个总分门槛,修订仍只看 校验与结构审阅结论;quality-stack.json也不重新评分、不补维度、不设质量门禁, 只是把已有的三套结论汇到一页供人看 - 没有工作流引擎 / DAG / Stage Registry:阶段顺序固定,不能任意跳段
- 没有鉴权、限流、批量与流式:API 没有用户体系,也没有 SSE / WebSocket
target_words是目标不是保证:实际输出长度受模型能力与上下文窗口影响- 只支持 OpenAI-compatible 端点:没有 Provider Registry / Model Router / Fallback
- v2.0.0 没有换框架、没有上新能力:分层是为了让后续能力有地方长,不是新增智能—— 这一版没有多模态、没有外部知识库、没有检索增强,也没有把任何未实现能力预埋成接口
v2.3.0 加上 Benchmark Platform(基准测量平台),不需要改任何代码、也不需要迁移任何产物:
Run 类路由、请求响应字段、CLI、错误码、runs/ 下原有产物的布局与字段、界面全部与 2.2.0
逐字一致,2.2.0 及更早写的 Run 原样可读。要紧的有四条:多一棵与 runs/ 同级的产物树
benchmarks/(Suite 原文入库,执行三件套不入库,与 runs/ / experiments/ 一样有 .gitignore
规则);多八条 /api/benchmarks/** 路由与四个 BENCHMARK_* 错误码;Benchmark 样例的
run-manifest.json 上多一个可选的 benchmark 块(benchmarkId / suiteId / suiteVersion /
suiteDigest / caseId / repetition),普通 Run 里这个键不出现;GET /api/runs/<run_id> 与
Run 详情页的既有字段一个都没动。回滚到 2.2.0 的代价为零:多出来的树、字段与路由被旧版本整个忽略。
契约见 docs/benchmark.md,题库来源与许可见
docs/benchmark-data.md,逐版说明见 docs/upgrade.md。
v2.2.0 加上 Creator Workspace,不需要改任何代码、也不需要迁移任何产物:Run 类路由、
请求响应字段、CLI、错误码、runs/ 下原有产物的布局与字段、界面全部与 2.1.0 逐字一致。
要紧的有三条:多一棵与 runs/ 同级的产物树 projects/(不入库,与 runs/ 一样在
.gitignore 里);多十二条 /api/projects/** 路由与四个 WORKSPACE_* 错误码;
run-manifest.json 上多一个可选的 workspace.projectId 字段,把一次 Run 指到它所属的项目
——2.1.0 及更早写的 Run 没有这个字段,那些 Run 不属于任何项目(不是错误,只是没有归属)。
回滚到 2.1.0 的代价为零:多出来的树、字段与路由被旧版本整个忽略。逐字段契约见
docs/workspace.md,逐版说明见 docs/upgrade.md。
v2.1.0 在 2.0 平台架构上升级质量系统,不需要改任何代码、也不需要迁移任何产物:
HTTP API、请求响应字段、CLI、错误码、runs/ 下原有产物的布局与字段、界面全部与 2.0.0
逐字一致,2.0.0 及更早写的 Run 原样可读。要紧的有四条:运行级多一个文件
quality-stack.json(固定文件数从十二个变十三个,原有文件一个字段都没少);
Run 详情响应多一个可选字段 qualityStack,另多一条只读路由
GET /api/runs/<run_id>/quality-stack(旧 Run 读作 null、面板整个隐藏、磁盘上不补写);
Co/N/C/Ca 与 H/P/E/Pf 的分数语义一个都没变(review.score /
commercial_review.score / beat_validation.issues 逐字保留);
诊断不驱动任何决策(RetryPolicy 与 Repair 的判定只认整体分与校验结论,诊断条数变了
重试次数与修订类别一字不变)。回滚到 2.0.0 的代价为零:多出来的文件、字段与路由被旧版本
忽略。逐版说明见 docs/upgrade.md,逐字段契约见
docs/quality-stack.md。
v2.0.0 是第一个架构大版本,不需要改任何代码、也不需要迁移任何产物:HTTP API、
请求响应字段、CLI、错误码、runs/ 下的产物布局与字段、界面全部与 1.9.1 逐字一致,
1.0.0 以来写的产物原样可读。改的是仓库内部的依赖方向——六层职责(Domain / Application /
Engine / Analysis / Infrastructure / Interface)、一个组合根、端口与适配器、稳定的领域契约。
要紧的只有一条:把本项目当库 import 的用法要改路径——配置序列化搬到
src/domain/story-config.ts、提供方清单搬到 src/domain/provider.ts、脱敏规则搬到
src/domain/safe-text.ts(旧路径都留了 re-export);想自己装配应用就用组合根的
createDependencies + createStoryLoopApplication。架构说明见
docs/architecture.md,逐条差异见 docs/upgrade.md。
回滚到 1.9.1 的代价为零:产物逐字节同构,回滚后只是少了组合根与架构测试。
回归测试是 tests/test_architecture.test.ts(六层边界 + 无循环依赖)、
tests/test_compat_v1_runs.test.ts(v1.x 产物落盘形状与读回、URL 关卡、密钥隔离)与
tests/test_composition_integration.test.ts(组合根装配 + 门面方法面 + 健康探测)。
v1.9.1 是 1.9.0 / 1.8.0 的补丁,不需要改任何代码:没有新文件、新字段、新路由、新错误码,
产物字段集与 1.9.0 逐字一致。它修十处实现问题——崩过的步骤在遥测里不再冒充 completed、
产物晋升失败时失败阶段指得对了、Run 级 durationMs 取整到毫秒、读不动产物时不再整套 500、
遥测拿不到时 metadata 不再写两个 null、跑成了的 Run 不再被判成失败分类、修订耗尽多一道采纳
闸门且文案说清「Run 级合计」与「每次 Attempt 上限」两个口径、证据不再指向盘上不存在的文件、
修订证据带上 attemptId / repairId——另外把一批文档与实现不一致的地方改回来。
要紧的有一条:按「键一定存在」解析 metadata.duration_ms / llm_call_count 的下游要改成按
「键在不在」判断(与三个 token 键同一条规则)。回归测试在 tests/test_patch_1_9_1.test.ts。
回滚到 1.9.0 的代价为零,没有删任何字段。逐版说明见 docs/upgrade.md。
v1.9.0 新增 失败分析(Failure Analysis):每次 Run 多落一份
runs/<run_id>/failure-analysis.json(固定 12 类清单下的主要 / 次要失败类别、信号、
每条信号指向的证据、首个失败阶段与终态),多两条只读出口——Run 详情响应的
failureAnalysis 字段与 GET /api/runs/<run_id>/failure-analysis;Run 详情页多一块
「失败分类」面板,实验详情页多一组失败类别分布;metadata.json 多两个可选字段
failure_analysis_status 与 primary_failure_category。纯 additive:从 1.8.x 升到 1.9.0
不需要改任何代码,1.8.x 与更早写的产物可以直接读(没有 failure-analysis.json 时
failureAnalysis 读作 null、面板整个隐藏、实验的失败分布整段隐藏,磁盘上不会被补写)。
要紧的有三条:运行级固定文件数从十一变十二;没有任何代码读这份分析来决定重试、修订或
采纳(它只分类,不控制,也不解释原因);分析跑通但写盘失败时 failure_analysis_status
记 unavailable、不写 primary_failure_category(成功的 Run 不会因此变失败)。
回滚到 1.8.0 的代价为零,多出来的文件、字段与路由被旧版本忽略。
逐版说明见 docs/upgrade.md。
v1.8.0 新增 Run 级可观测性(Run Telemetry):每次 Run 多落一份 runs/<run_id>/telemetry.json
(阶段耗时 / LLM 调用 / Retry 与 Repair 次数 / 失败阶段 / 真实可得的 token 与 cost),
多一条只读路由 GET /api/runs/<run_id>/telemetry,Run 详情页多一块「可观测性」面板,
实验汇总的每个变体多一组效率指标。纯 additive:从 1.7.x 升到 1.8.0 不需要改任何代码,
1.7.x 写的产物可以直接读(telemetry 读作 null、面板整个隐藏,磁盘上不会被补写)。
要紧的有三条:运行级固定文件数从十个变十一个;按字段穷举解析 GET /api/runs/<id> 与
实验汇总的下游会多出几个可能是 null 的可选键;遥测只观察不控制——没有任何代码读它来
决定重试、修订或采纳。回滚到 1.7.1 的代价为零,多出来的文件、路由与字段被旧版本忽略。
逐版说明见 docs/upgrade.md。
v1.7.0 新增 受控实验框架(Experiment Framework):POST /api/experiments 建定义、
POST /api/experiments/<id>/run 跑完、GET /api/experiments[/<id>] 读结果,界面是
/experiments 与 /experiments/<id>。数据落在 runs/ 旁的 experiments/ 下三个 JSON。
纯 additive:从 1.6.x 升到 1.7.0 不需要改任何代码,1.6.x 写的产物可以直接读,一次普通 Run
一个字节都没变(run-manifest.json 只在实验样本上多一个可选的 experiment 块,普通 Run 里
这个键不出现)。要紧的有三条:新增三个错误码 EXPERIMENT_INVALID / EXPERIMENT_NOT_FOUND /
EXPERIMENT_CONFLICT;实验定义里不接受凭据、地址与原始提示词形状的键(400);
同一份定义只能跑一次(409,要改条件就复制成新实验)。回滚到 1.6.0 的代价为零,
多出来的目录、路由与字段被旧版本忽略。逐版说明见 docs/upgrade.md。
v1.6.0 新增 Run 出身清单 run-manifest.json:每次 Run 除了 metadata.json 还多写一份
记录「这份故事是拿什么跑出来的」——代码版本与 commit、模型、各阶段 temperature、六份提示词的
版本与内容摘要、每次 Attempt 的结局、以及这批产物的 SHA-256。它只在 Run 根目录一份,
自带 schemaVersion、用 camelCase,与 snake_case 的 metadata 契约物理隔离。纯 additive:
从 1.5.x 升到 1.6.0 不需要改任何代码,1.5.x 写的产物可以直接读(manifest 读作 null
、Run Provenance 面板整个隐藏)。要紧的有三条:运行级固定文件数从九个变十个;两个 Run 类
响应各多一个可选字段 manifest;清单里不写凭据——baseUrl 原文、API Key 都不落盘,
topP / maxTokens 这类客户端没有下发的参数也不写。回滚到 1.5.2 的代价为零,
多出来的文件与字段被旧版本忽略。逐版说明见 docs/upgrade.md。
v1.5.2 是一次界面修订:没有新能力、没有新文件、新字段或新路由,只改 src/**/*.tsx 里的
样式类名。从 1.5.1 升到 1.5.2 不需要改任何代码,1.5.1 写的产物可以直接读。要紧的有三条:
Story Config 外框不再让内容溢出到边框外(两列布局补 min-h-0,两列各自 min-h-0 +
overflow-y-auto,内容超高时在自己的圆角框内滚动);界面颜色改用主题 token,
浅色模式不再是一层看不见的边框配读不清的浅灰正文(border-border / bg-muted* /
text-foreground / bg-input / bg-card);右列结果区原来一像素都滚不动——
scrollHeight 等于 clientHeight,Attempts / Quality / Validation / Review /
Commercial Review 全渲染在框外且不可达,修好后右列和左列一样在自己的圆角框内滚到底。
回滚到 1.5.1 的代价为零。逐版说明见
docs/upgrade.md。
v1.5.1 是一次修订:没有新能力、没有新文件、新字段或新路由,只修 1.5.0 里两处「文档承诺与
实现对不上」的地方。从 1.5.0 升到 1.5.1 不需要改任何代码,1.5.0 写的产物可以直接读。
要紧的有两条:失败的 Run 不再把 commercial_review_status 谎写成 not_started(这一步
跑成了就是 completed、它自己失败了就是 failed,结论本体仍不写,因为失败路径从不 promote);
看非入选 Attempt 时产物清单里的 commercial_review 现在指到 attempt 目录那份,不再把入选
Attempt 的结论当成当前这份。回滚到 1.5.0 的代价为零。逐版说明见
docs/upgrade.md。
v1.5.0 加了商业可读性审阅:与结构审阅完全并列的第二个审阅者,产出
CommercialReviewResult(commercial-review.json + API 的 commercial_review 字段 +
前端 Commercial Review 面板 + POST /api/review/commercial)。纯 additive:
从 1.4.x 升到 1.5.0 不需要改任何代码,1.4.x 写的产物可以直接读
(commercial_review_status 读作 not_started、commercial_review 读作 null),
1.5.0 写的 Run 回落到 1.4.x 只是多一份被忽略的文件与几个被忽略的 metadata 字段。
要紧的有三条:RetryPolicy 仍然只有 min_review_score 一个门槛、仍然只看结构审阅分,
商业分不触发重试也不触发修订;QualityResult 里没有商业分,仍然只有 Co/N/C/Ca 四个维度;
商业可读性审阅自身失败只让 commercial_review_status 变成 failed,正文、校验结论、
结构审阅结论与质量快照一个都不受影响。回滚到 1.4.1 没有任何代价,也不需要迁移。
v1.4.0 在 Planning 之后加了一道 BeatPlan 结构校验(BeatValidationResult /
beat-validation.json / POST /api/validate-beats / 前端 Beat Validation 面板),
纯 additive:从 1.3.x 升到 1.4.0 不需要改任何代码,1.3.x 写的产物可以直接读
(beat_validation_status 读作 not_started、beat_validation 读作 null),
1.4.0 写的 Run 回落到 1.3.x 只是多一份被忽略的文件与几个被忽略的 metadata 字段。
唯一的行为变化在生成链路上:骨架结构带 error 级问题时 Run 会在写正文之前结束,
这类 Run 以前会一路生成到 Attempt 阶段。
v1.4.1 是一次修订:没有新能力、没有新文件、新字段或新路由,只把 1.0.0 ~ 1.4.0 里
「文档写了实现没做到」和「口径没走到底」的地方改回文档承诺的样子。从 1.4.0 升到 1.4.1
不需要改任何代码,1.4.0 写的产物可以直接读。要紧的有三条:attempt 摘要的审阅分
改走 reviewOverallScore(有维度时与 RetryPolicy 比的门槛分、quality.overall_score
是同一个数,没有维度时逐字不变);review.json / validation.json / quality.json /
beat-validation.json 被改坏时读接口仍然 200、对应字段是 null,不再 500;CLI 的
--temperature 现在会透传给规划,而审阅 / 修订收到它会明确打一行「已忽略」
(它们用固定温度 0.3 / 0.5,这一点从 1.0.0 起就没变)。回滚到 1.4.0 的代价也只有一处:
有审阅维度时 attempt 摘要的分会退回 review.score 原值。
v1.3.0 给审阅结论加了四个可选的基础维度(连贯性 / 叙事 / 人物 / 因果),整体分改为四维均分,
纯 additive:从 1.2.x 升到 1.3.0 不需要改任何代码,1.2.x 写的产物可以直接读,
1.3.0 写的 Run 回落到 1.2.x 只是多一个被忽略的 dimensions 字段。
v1.2.0 建立了统一质量工程层(QualityResult / quality.json / API 的 quality 字段 /
前端 Quality Summary 面板),纯 additive:从 1.1.x 升到 1.2.0 不需要改任何代码,
1.1.1 写的产物可以直接读,1.2.0 写的 Run 回落到 1.1.x 也只是多一个被忽略的文件。
v1.2.1 修的是旧 Run 质量兜底的口径(读取时临时装配改取修订后的那一版结论,
与落盘的 quality.json、与 metadata.json 对齐),从 1.2.0 升到 1.2.1 不需要改任何代码。
v1.1.1 修的是 v1.1.0 那道地址关卡自身的问题(CLI 的 --base-url 不再被自己挡、
IPv4-mapped / NAT64 地址改判),约束方向不变;从 1.1.0 升到 1.1.1 不需要改任何代码。
v1.1.0 收紧了请求体 baseUrl 覆盖的取值,只允许 http/https 的公网地址;其余与 1.0.1 一致。
从 1.0.0 升到 1.0.1 不需要改任何代码;v1.0.0 相对于 0.9.x 也几乎没有破坏性变更,
需要留意的只有两条:运行级 metadata 的 model 现在始终存在(以前按条件写),
attempt 级 metadata 的 error 没有错误时是 null(以前按条件写)。
两者都是「字段从可能没有变成一定有」,不会让旧读取方崩掉。
git fetch && git checkout 1.5.0 # tag 不带 v 前缀
npm install
cp .env.example .env
npx tsx scripts/generate-cli.ts run --config configs/example_story.json完整迁移步骤与回滚说明见 docs/upgrade.md。
版本策略见 docs/compatibility.md:主版本内不改字段名与语义、
不删路由与命令,扩展只能追加可选字段;Git tag 不带 v 前缀,GitHub Release 标题带。
npm test # 全部测试
npm run lint # eslint
node node_modules/typescript/bin/tsc --noEmit # 类型检查
node node_modules/next/dist/bin/next build # 生产构建测试分两类。行为测试用注入的假 Generator / Validator / Reviewer 驱动 Pipeline,
覆盖重试路径、修订路径、exhausted 路径与各条边界路径。
合同测试(v1.0.0 新增,v1.0.1 补了产物对照)把公开契约钉死,改一个字段名就红:
| 合同测试 | 钉住什么 |
|---|---|
tests/test_contract_story_config.test.ts |
StoryConfig v1 字段集、缺省值、未知字段不穿透 |
tests/test_contract_beat_plan.test.ts |
BeatPlan v1 字段集、编号规则、文档内 JSON 示例合法 |
tests/test_contract_artifacts.test.ts |
Run 产物布局与三层 metadata 字段集 |
tests/test_contract_api.test.ts |
路由清单、请求/响应字段、错误码与状态码映射 |
tests/test_contract_cli.test.ts |
CLI 命令、参数、退出码、帮助输出位置 |
tests/test_contract_docs.test.ts |
README 必备章节、docs/ 完整性、版本号唯一来源、防泄漏守卫 |
tests/test_contract_docs_sync.test.ts |
文档字段表 ↔ 真实产物逐字段一致(多写、漏写、改名都红) |
tests/test_url_guard.test.ts |
请求体 baseUrl 只允许 http/https 公网地址,私网/环回/保留段在发请求前就被拒 |
v1.2.0 的质量快照另有六个测试文件(同样只用假组件,不调真实接口):
test_quality_assembler(装配规则与确定性)、test_quality_models(快照解析容错)、
test_quality_compat(旧 review.json 解析、无快照的旧 Run、修订后口径兜底)、
test_quality_pipeline(落盘与内存一致)、test_quality_api(三处口径一致、旧 Run 兼容、
quality.json 损坏后的降级、地址关卡回归)、test_quality_ui(面板状态推导)。
v1.3.0 的四个基础维度另有两个测试文件:
test_quality_dimensions(维度模型、确定性聚合、可选字段校验、解析、重试门槛只认整体分)、
test_quality_dimensions_pipeline(维度在真实管道里一路带到 quality.json 与 metadata,
修订后取新的维度,没有维度时与 v1.2.x 逐字一致)。
v1.4.0 的 BeatPlan 结构校验另有四个测试文件:
test_beat_validation(模型、schema 白名单、规则层、解析、BeatValidator 行为)、
test_beat_validation_pipeline(硬失败阻断、warning 放行、校验器自身异常、不注入时与
v1.3.0 逐字一致)、test_beat_validation_api(新路由、错误码、旧 Run 读回)、
test_beat_validation_ui(面板状态推导,含「骨架不过 ≠ 校验器失败」)。
v1.4.1 的修补回归散在原有文件里:分数口径(test_generation_attempt)、
读回容错与原子写(test_artifact_store、test_retry_api)、CLI 温度透传与帮助
(test_cli)、beat_validation_error(test_beat_validation_pipeline)、
产物清单前缀(test_ui_artifacts)。
v1.5.0 的商业可读性审阅另有四个测试文件:
test_commercial_review(维度模型、schema 白名单、确定性聚合、解析、
CommercialReviewer 行为)、test_commercial_review_pipeline(两份产物逐字相同、
修订后重跑、失败隔离、低商业分不触发重试或修订、不注入时与 1.4.1 逐字一致)、
test_commercial_review_ui(面板五种状态、四个维度顺序、没有 Fix / Retry 入口、
没有市场化预言措辞)、test_commercial_api(新路由四种返回、模型自报分被重算值顶掉、
覆盖 commercial-review.json 但不碰 review.json、旧 Run 读回)。
v1.5.1 的修订回归散在原有文件里(+4 条,未新增文件):test_commercial_review_pipeline
补齐四条失败路径的 commercial_review_status 口径(跨 Attempt 记住真实结果、自己失败即
failed、一个 Attempt 都没跑到才是 not_started)、test_ui_artifacts 把非入选 Attempt 的
产物清单从四个文件改成五个(commercial_review 并入前缀规则)。
v1.5.2 的界面修订新增 tests/test_ui_theme_tokens.test.ts(13 条):源码级断言钉住
「不许再写死白 / 锌色边框与底色」「bg-black/* 只允许出现在整屏遮罩上」「300/400 档强调色
必须带 dark: 前缀」「两列主工作区必须同时有 min-h-0 与 overflow-y-auto」「三个内嵌面板
必须用 rounded-lg」「原生 select 必须用 border-input 配 bg-transparent、浅色下不许
出现 bg-input」「生成结果面板必须是 grow shrink-0 而不是 flex-1、正文滚动区不许写死
高度」。拿 1.5.1 的源码跑这批断言会红 37 处。
v1.8.0 的 Run 可观测性另有 tests/test_telemetry.test.ts(34 条):阶段计时非负且
completed / failed 各有该有的字段、3 次 Attempt 记 retries = 2、两次修订记 repairs = 2、
六类组件的调用次数按逻辑调用计数、usage 只在 Provider 真给时落盘(没给就是 null,
一次都没给时三个 token 键整个不出现)、没有可信价格信息时成本为 null 且界面不显示 0、
API Key 与 Authorization 头不进 telemetry.json、含 token 的异常原文不被逐字落盘、
旧 Run 没有遥测仍照常可读、实验子 Run 各自落遥测且 partial / failed 状态不受影响。
全部用假模型跑,不打任何真实付费 API。
v1.9.0 的失败分析另有八个测试文件(同样只用假组件):
test_failure_models(schema 解析、版本不符与坏文件降级、长度上限)、
test_failure_rules(规则表与优先级、未知错误码保留原文、每条规则的用例)、
test_failure_analyzer(确定性、主要 / 次要类别判定、Retry 与 Repair 耗尽按真实计数、
类别优先级固定、不得出现 Prompt / 模型能力 / 温度一类的归因措辞)、
test_failure_artifacts(真实管道落盘、每条证据都指到盘上真实存在的文件与字段、
假密钥不进 failure-analysis.json、分析器自身失败时 Run 仍然成功且记 unavailable、
与 examples/example_run/ 逐字节一致)、
test_failure_api(Run 详情附加字段与独立路由、旧 Run 读作 null、失败 Run 可读)、
test_failure_ui(面板五种状态、只读没有按钮、没有归因措辞)、
test_experiment_failure_distribution(按主要类别计数、没有分析的样本不进分母、
1.9.0 之前的实验整段隐藏)、
test_client_bundle_boundary(源码级守卫:客户端可达的任何文件不许 import node 内置模块,
failure-rules.ts 不得再拖进浏览器包)。
v2.0.0 的平台架构另有三个测试文件:
test_architecture(静态扫 import:六层允许 / 禁止依赖表、Domain 不许碰 Node 与
next/react、路由必须从组合根取用例、assertPublicBaseUrl 只许一份定义、
三个仓储端口存在且没有 GenericRepository<T>、Engine → Infrastructure 白名单、
无循环依赖)、test_compat_v1_runs(走组合根真跑一次 Run:落盘文件名就是 v1.x 冻结的
那 12 个加 attempts/,schemaVersion 仍是 "1",换一个全新的 ArtifactStore /
ExperimentStore 读得回来、/api/runs/<id> 与界面面板照常渲染、磁盘上多一个不认识的键
不致命、请求体 baseUrl 关卡在发请求前就拒掉且密钥不进响应不进产物)、
test_composition_integration(createDependencies + createStoryLoopApplication
两步装配、门面每个用例方法都能取到、/api/health 与 /api/version 只回真假布尔与版本号)。
全部用 FakeLLM,不打任何真实付费 API。
v2.1.0 的统一质量视图新增八个测试文件(同样只用假组件,不调真实接口):
test_quality_diagnostic(诊断 schema:三档 severity × 七个 target、三份类别白名单、
空 message、非法 relatedBeatIds、id 由校验方指派、轻量去重只认字符串相等)、
test_quality_stack(协调器:模块在位数 → status 三值、诊断合并顺序与去重、
summary 由 diagnostics 现算、写盘前严格校验)、
test_quality_stack_storage(落盘位置只在 Run 根目录、不删旧产物、读盘三层降级)、
test_quality_stack_api(新路由与 Run 详情附加字段、旧 Run 读作 null、不补写磁盘)、
test_quality_center(面板状态推导、四类筛选同时生效、筛选选项固定顺序)、
test_quality_stack_compat(旧客户端读 review.score / review.dimensions /
commercial_review.score / beat_validation.issues / quality.overall_score、
没有 quality-stack.json 的 v2.0.0 Run 照常可读、Retry / Repair 回归不受诊断条数影响)、
test_failure_quality_stack(失败分析把诊断当证据读,仍不归因、不自动动作)、
test_quality_stack_security(假密钥 / Cookie / 环境变量原文 / 带密钥的 URL 一律不进
quality-stack.json、Run 详情响应与 /quality-stack 响应、界面输出)。
三套组件的 v2 rubric 与诊断映射散在 test_beat_validation / test_commercial_review /
test_quality_dimensions / test_review_parser / test_failure_analyzer 等原有文件里
(+200 余条,未新增文件)。
v2.2.0 的 Creator Workspace 另有十一个测试文件(同样只用假组件,不调真实接口):
test_workspace_project(项目档案的字段守卫、id 规则、归档与解除)、
test_workspace_document(稿件正文、revision 历史与「另一个 id 就是一坨文本」的拒绝)、
test_workspace_export(导出用例:DOCX/EPUB 走真渲染器、纯文件读写、失败不写半份文件)、
test_workspace_export_store(导出文件落 exports/<项目>/、账本追加不覆盖)、
test_workspace_export_case(每条文档导出只能选那一次指定格式,旧版本文档照样可导)、
test_workspace_run_draft(run 草稿可整体整出可用的 BeatPlan 与 StoryConfig,
且只从盘上真实存在的产物取数)、
test_workspace_health(九类信号的判定规则、retry_pressure 按真实计数、
「当前稿件在审阅后改过」要跟磁盘时间比,不与阈值猜)、
test_workspace_view(给健康信号摆事实不给建议)、
test_workspace_ui(十一个界面面板的状态推导、刷新后回到列表、码与版本对应在界面上原样出现)、
test_workspace_api(十二条路由的字段、错误码与「路由不进第二个栈」)、
test_workspace_network_boundary(§45 网络边界守卫:工作区落地层的 import 闭包与
七条 route 的闭包里都不许出现任何 HTTP 客户端,另有真实跑全流程并用假 fetch
计数确认一次外联都没发的行为测试)。
v2.3.0 的 Benchmark Platform 另有八个测试文件(同样只用假组件与临时目录,不调真实接口):
test_benchmark_suite(Suite / Case / Protocol 的字段守卫、来源与许可必填、beatPlanRef 必须
真的躺在 Suite 目录里、digest 由内容现算)、test_benchmark_runner(五步预检逐条挡住、
样本顺序恒定、partial 如实落盘、超过安全线要显式确认)、
test_benchmark_store(原子写、终态不可写、越界引用一律当不存在、版本比大小、
盘上那份被手改坏时读不回来)、test_benchmark_export(CSV 转义与 BOM、JSON 四块、
两种格式里都没有 baseUrl / 凭据 / 提示词原文 / 正文)、
test_benchmark_comparator(只给两侧事实与差值、缺指标整行不出现、协议不同也允许比)、
test_benchmark_view(界面文本口径:82.5 · 3/4、阈值 71 来自 Suite、没有趋势与排名字样)、
test_benchmark_api(八条路由的字段、错误码与状态码)、
test_benchmark_dataset(随仓库发布那份题库的来源与许可、什么内容永远不会进 Benchmark)。
v2.3.1 是对 2.3.0 的一次审计响应补丁,没有新增测试文件:两个既有文件各补了一条关卡断言
(test_benchmark_store:直接往 putSuite 塞不合法 id / 版本的 Suite 必须抛
BenchmarkWriteError,且盘上一个目录都不多出来;test_workspace_document:删稿件要连
revisions/ 一起删且不越出项目目录)。
全部测试合计 118 个文件 / 1917 条,全部只调用真实 LLM 之外的桩:
LLM 由注入的桩对象或 FakeLLM 替代(tests/helpers/fixtures.ts),
fetch 也被桩掉。重试相关断言同样只用桩,从不触发真实模型调用。
URL 校验的用例用注入的假解析器跑,不真的查 DNS,也不碰任何真实主机。
- Next.js 16 (App Router) + React 19 + TypeScript
- Tailwind CSS 4 + Base UI
- OpenAI-compatible LLM API
- Vitest 单元测试
| 文档 | 内容 |
|---|---|
| docs/story-config.md | StoryConfig v1 字段级契约 |
| docs/beat-plan.md | BeatPlan v1 字段级契约 |
| docs/run-artifacts.md | Run 产物布局与 metadata 字段集 |
| docs/api.md | HTTP API 路由、字段、错误码 |
| docs/telemetry.md | Run Telemetry 契约(v1.8.0):telemetry.json 字段、计数语义与边界 |
| docs/failure-analysis.md | 失败分析契约(v1.9.0):类别、优先级、证据指向、状态记法与边界 |
| docs/quality-stack.md | 统一质量视图契约(v2.1.0):QualityDiagnostic 字段、三套 category 白名单、quality-stack.json 与 Quality Center |
| docs/workspace.md | Creator Workspace 契约(v2.2.0):projects/ 产物布局、StoryDocument 字段、十二条工作区路由与健康信号 |
| docs/benchmark.md | Benchmark Platform 契约(v2.3.0):Suite / Case / Protocol、五步预检、22 个指标口径、不可变性、比较与历史、导出格式与内容边界、八条路由与四个错误码 |
| docs/benchmark-data.md | Benchmark 题库的来源与许可(v2.3.0):随仓库发布的 storyloop-core 1.0.0 三例、什么内容永远不会进 Benchmark、怎么加自己的 Suite |
| docs/cli.md | CLI 命令、参数、退出码 |
| docs/upgrade.md | 从 0.9.x / 1.0.0 升级到当前版本 |
| docs/compatibility.md | 兼容性策略与扩展方式 |
| docs/experiments.md | 受控实验契约(v1.7.0) |
| docs/architecture.md | 平台架构契约(v2.0.0):六层职责、依赖方向、组合根、端口与适配器、Run 生命周期、产物归属与安全边界 |
| examples/example_run/ | 一次完整 Run 的合成样例产物 |
| configs/example_story.json | 覆盖全部可选字段的示例配置 |
| CHANGELOG.md | 版本历史 |
可直接阅读的实现(无外部依赖):
src/domain/retry-policy.ts:重试策略与 RetryDecision 的规则(纯函数)src/engine/quality-assembler.ts:统一质量快照的装配规则(纯函数,不调模型)src/domain/quality-dimensions.ts:四个基础维度与四维均分的实现(纯算术)src/engine/validation-rules.ts:六条硬性校验规则的实现src/application/error-model.ts:稳定错误码与状态码映射src/domain/safe-text.ts:错误文本的净化规则(绝对路径替换、凭据打码),只此一份src/infrastructure/config/version.ts:版本号单一真源tests/helpers/fixtures.ts:共享样例数据与FakeLLM