书写完不是终点。主人在 App 里可以持续提修改指令,走 POST /api/book/revise。修书与写书共用引擎链、在飞登记、对话线,但入口逻辑独立:handleBookRevise()。它的设计重点是把「主人校验」放在「扣费」之前——不是主人,一分钱不动。
这段代码在哪
src/server.ts的handleBookRevise():路由处理。src/server.ts的runReviseJob():修书 job 本体。
它做什么
入口的分支顺序(每一道都是故意的):
- 400 校验:slug 过
SLUG_RE;非 dry 时 instruction 不能为空(截 4000 字)。 - dry 探路:
chargeBook(auth, {slug, kind:"revise"}, true)——验 token、验余额、拿 scope,不扣费。探路失败直接透传状态码。 - 主人校验:owner 真源 = 线上
_src/book.json的owner,退回对话线的scope。owner 存在且 ≠ 请求者 → 403 not-owner;owner 不存在 → 只有发布账号本人(publisherScope())且书在线(bookExistsOnline())才放行,否则 404。 - 补建对话线:meta 不存在就当场建一条(老书没有 bookmeta 的场景)。
- 同书单飞:
meta.thread.some(e => e.status === "running")→ 409 busy。 - dry 就此返回 200(App 预检走到这里为止)。
- 真扣费:同一 URL,
dry=false,预扣 40 算力。 - 落 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() 与写书的差别在提示词和收尾:
- fresh session,不 resume:注释——「不 resume 写书旧线程(背着整段历史只多花钱)——每次修改都是全新 codex exec,以工作目录/线上成书这些『文件』为真源」。原书写书时的几十轮对话不背进来;引擎自己去读文件。
- 署名冻结:byline 叮嘱「这本书署名『X』,改动不要动署名」。署名取自
bookAuthor(srcBook, meta)——线上 book.json 优先(修书可能改过署名),登记簿只是创建期快照。 - 成功刷新书帖:
registerBookPost(auth, slug)——标题/hidden/章节数可能变了。 - 失败退 40:
refundBook(auth, {ref: `${slug}#${entryTs}`, kind: "revise"})——ref 用条目时间戳,与 inflight 的修书 id(slug#entryTs)同一套,幂等对得上。
为什么这样写
- 403 先于扣费:先 dry 拿 scope 比对、再真扣——主人校验失败时用户余额一分不动。如果反过来先扣再验,每个乱试别人 slug 的请求都得走一次退款。
- 409 单飞防并发改书:两个修改同时跑,写回 book.json / 章节文件会互相覆盖。对话线里任何一条
status:"running"就拒绝新单。 - 404 而不是 403 给外人:owner 缺失的老书路径上,非发布账号拿到的是 404 no-book——不透露「这本书存在但不是你的」。
- dry 探路复用扣费通道:不另造一个「验余额」接口,dry=true 的 charge 就是它。一个接口两种模式,少一处分叉。
怎么排查
| 症状 | 先看 |
|---|---|
| 主人修自己的书被 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 |