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

第 09 章 / 共 10 章

第九章 · 修书:/api/book/revise

书写完不是终点。主人在 App 里可以持续提修改指令,走 POST /api/book/revise。修书与写书共用引擎链、在飞登记、对话线,但入口逻辑独立:handleBookRevise()。它的设计重点是把「主人校验」放在「扣费」之前——不是主人,一分钱不动。

这段代码在哪

它做什么

入口的分支顺序(每一道都是故意的):

  1. 400 校验:slug 过 SLUG_RE;非 dry 时 instruction 不能为空(截 4000 字)。
  2. dry 探路:chargeBook(auth, {slug, kind:"revise"}, true)——验 token、验余额、拿 scope,不扣费。探路失败直接透传状态码。
  3. 主人校验:owner 真源 = 线上 _src/book.json 的 owner,退回对话线的 scope。owner 存在且 ≠ 请求者 → 403 not-owner;owner 不存在 → 只有发布账号本人(publisherScope())且书在线(bookExistsOnline())才放行,否则 404。
  4. 补建对话线:meta 不存在就当场建一条(老书没有 bookmeta 的场景)。
  5. 同书单飞:meta.thread.some(e => e.status === "running") → 409 busy。
  6. dry 就此返回 200(App 预检走到这里为止)。
  7. 真扣费:同一 URL,dry=false,预扣 40 算力。
  8. 落 thread 条目 + inflight 登记 + 开跑,202 返回。

主人校验那段的原文:

const srcBook = await fetchSrcBook(slug);
let meta = await readBookMeta(slug);
const owner = String(srcBook?.owner ?? "") || String(meta?.scope ?? "");
if (owner) {
  if (owner !== requester) {
    res.writeHead(403, json).end(JSON.stringify({ error: "not-owner" }));
    return;
  }
} else {
  const pub = await publisherScope();
  if (!requester || !pub || requester !== pub || !(await bookExistsOnline(slug))) {
    res.writeHead(404, json).end(JSON.stringify({ error: "no-book" }));
    return;
  }
}

runReviseJob() 与写书的差别在提示词和收尾:

为什么这样写

怎么排查

症状先看
主人修自己的书被 403探真源:curl -s "https://jianshuo.dev/voicedrop/books/<slug>/_src/book.json" | grep owner,与 whoami 的 scope 对比;边缘缓存 5 分钟,代码里已带 ?_= 穿透,手动 curl 记得也带
409 busy 一直不解看对话线里有没有卡在 running 的条目:curl -s "…/_src/bookmeta.json";有 = 上次修书收尾没 patch 成功(查 [revise] done 与 [bookmeta] 日志)
修书改了但没生效journalctl -u claude-agent | grep '\[revise\]' 看 start/done;done 带 ERROR 则已自动退 40
怀疑重复扣费grep '\[book\] refund' 看 kind=revise 的 ref 是否 deduped
一本书是怎么被写出来的 · 王建硕
费曼式写法 · 由多个 AI 代理撰写与互相审校