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

第 02 章 / 共 10 章

第二章 · 入口与门禁:/api/book

这一章拆开链条的第一步:HTTP 入口 handleBook() 和它唯一的门禁 chargeBook()。核心设计一句话:没有独立的权限系统,扣费就是准入。

这段代码在哪

它做什么

handleBook() 的分支逻辑,按执行顺序:

条件结果说明
seed 为空400 empty seedseed 先 trim().slice(0, 20000),防巨型 body
bearer 不是 Bearer … 开头401 bad token在 chargeBook() 里判
charge 返回非 200 或 !body.ok原样透传状态码(如 402)402 的 body 带 need_suanli/suanli,App 据此弹充值
payload.dry === true 且 charge 通过200 返回 charge 结果只验余额不扣费,开跑前的预检
全部通过202 {ok, charged_suanli, suanli}同步起 runBookJob(),不等待

扣费调用本身是一次转发——lab 不自己算钱,钱的事全在 agent worker:

async function chargeBook(
  auth: string | undefined,
  extra: { seed?: string; slug?: string; kind?: "revise" },
  dry: boolean,
): Promise<{ status: number; body: any }> {
  if (!auth?.startsWith("Bearer ")) return { status: 401, body: { error: "bad token" } };
  try {
    const r = await fetch(BOOK_CHARGE_URL, {
      method: "POST",
      headers: { Authorization: auth, "Content-Type": "application/json" },
      body: JSON.stringify({ ...extra, seed: extra.seed?.slice(0, 200), dry }),
    });
    const body = await r.json().catch(() => ({}));
    return { status: r.status, body };
  } catch {
    return { status: 502, body: { error: "charge unreachable" } };
  }
}

注意三个细节:

署名在扣费成功后才解析:App 显式传的 author 优先;没传就用同一枚 bearer 去拉提交者的 CLAUDE.json 取 profile.name:

const author =
  String(payload?.author ?? "").trim().slice(0, 20) ||
  (await fetchAuthorName(req.headers.authorization));
runBookJob(seed, String(charge.body.scope ?? ""), author, req.headers.authorization);

fetchAuthorName() 拉不到就返回空串——空的意思是「这本书不署名」。注释里有一条硬规矩:「绝不回落到任何默认人名」。

为什么这样写

怎么排查

症状先看
App 报「算力不足」正常流程——402 的 body 里有 need_suanli,让用户充值即可
点了写书没反应、无 [book] start 日志手动复现:curl -X POST localhost:8787/api/book -H "Authorization: Bearer <token>" -d '{"seed":"测试","dry":true}',看返回哪一档
全部 401bearer 过期,让用户在 App 里重新登录
全部 502 charge unreachablelab 到 jianshuo.dev 的出站网络断了,查 VPS 出口
dry 通、真扣失败worker 端 dry 与真扣路径分叉,去查 agent worker 日志
一本书是怎么被写出来的 · 王建硕
费曼式写法 · 由多个 AI 代理撰写与互相审校