一本书是怎么被写出来的 书架

第 01 章 / 共 10 章

第一章 · 全景:一单的完整生命周期

这本书讲的是一个约 1770 行的 server.ts,外加两个小模块 book-legs.ts 和 inflight.ts。它们合起来是 lab 的「写书服务端调度」:App 里点一下「写书」,到一本书出现在书架上,中间发生的每一步都在这几个文件里。第一章先把整条链走完一遍,后面九章逐段拆开。

这段代码在哪

它做什么:一单的生命周期

用户在 App 里填一段「种子」(一个词、一句话或一篇文章),点提交。服务端收到后并不等书写完才回话——POST /api/book 是 fire-and-forget(点火即忘:验完资格立刻返回 202,活儿在本进程后台慢慢跑完)。源码注释里写得很直白:「与 /api/chat 的『断线即中止』相反——app 提交完种子就可以关掉」。

完整链条是八步:

步骤函数日志行(journalctl 里 grep 得到)
1. 收单验资格handleBook() → chargeBook()(扣 160 算力)无日志,失败直接回 402/401
2. 开工登记runBookJob() → writeInflight()登记失败只留 [inflight] write failed
3. 装提示词开跑startBookJob()[book] start scope=… job=… seed=…
4. 三腿链跑引擎runBookEngine() → runLeg()[book] leg 1/3 = kimi-compat(${BOOK_CLAUDE_MODEL})
5. 引擎边写边发子进程里的 agent 照 skill 跑 build.mjs done(在子进程日志,不在本服务)
6. 中途早登记30 秒轮询 findBookByJobId()[book] early-registered slug=… scope=…
7. 引擎返回销档removeInflight()[book] done scope=… thread=…
8. 收尾登记patchThreadEntry() / registerBookPost() / notifyBookDone()[book] registered → [book] community-post → [book] push

第 4 步的「三腿链」是这套系统的核心:腿(leg)指一个能跑写书任务的引擎凭据——Kimi 兼容端点、本机 Codex 订阅、lab 自己的 Claude 订阅,三条腿按序降级,一条倒下换一条。装配提示词、跑引擎、销档的骨架代码长这样:

const rec: InflightCreate = {
  kind: "create", jobId: randomUUID(), seed, scope, author, auth, startedAt: Date.now(), attempts: 1,
};
// 先登记再开跑。登记失败只留日志:登记是为了崩溃恢复,不能反过来挡住正常开工。
writeInflight(INFLIGHT_DIR, rec).catch((e) => console.error("[inflight] write failed", e));
startBookJob(rec);

第 5 步发生在子进程里,不在本文件:被拉起的写书 agent 读 skill、逐章写稿、逐章调 build.mjs done 把章节发到 R2。所以即使服务端进程在第 6 步之后被杀,已发布的章节也永远在线上——这是「边写边发」换来的抗断点能力。

为什么这样写

怎么排查

一条订单从头跟到尾,只需要一条命令:

journalctl -u claude-agent | grep '\[book\]'

健康的一单,日志顺序是 start → leg → early-registered → done → registered → community-post → push。缺哪一段,问题就在哪两个环节之间:

一本书是怎么被写出来的 · 王建硕
费曼式写法 · 由多个 AI 代理撰写与互相审校