TOOLS-BACKEND · 请求路径

一个请求进 tools-backend,到出门为止经过的事

tools-backend 是 mai 用来跑实际工具调用的小服务,跑在 Cloudflare Worker 上。这一页讲:你发的请求进来后,先过哪几道关,怎么找到对的工具,工具是同步出结果还是先记一笔任务再轮询。读完你大概能知道每一段责任在哪、出问题去看谁。

topic · tools-backend scope · request-path 对应代码 · tools-backend/app.ts → Auth 这一关单独看
SECTION 01

从客户端进来,先过的几道关

下图把请求从进入到出门画在三条横线上:你那边、worker 里、外面的服务。前三道关谁都跑(贴标签、计时、拦跨域),第四道是 auth — 看你带没带 JWT。

CLIENT WORKER · tools-backend EXTERNAL HTTP FOCAL DISPATCH RESPONSE UPSTREAM CLIENT mai · curl · ignis M1 request-id 贴单号 M2 timing 起秒表 M3 CORS 看来源 FOCAL · M4 auth 查身份 ROUTE /:toolName 找工具 HANDLER tool.run() 干活 UPSTREAM 外部服务 vertex · GCS · cloudinary RESPONSE JSON 回到客户端 LEGEND 焦点 · 这一关决定后面能不能走 前置中间件 · 每个请求都跑 工具 handler
auth 是分流点 — 前三道关只动 header 和日志,到 auth 才决定这个请求该不该往下走。所以这条路上把它画在焦点位上:它一拦,后面 route + handler 都见不到这个请求。
前置中间件
4 道
request-id → timing → CORS → auth
入口路由
POST /:toolName
17 个工具都走这一个路由
两种结果类型
sync 或 async
异步返回 task_id,自己来 /tasks/:id 轮询
特殊豁口
1 条
tobatsu-gateway 回调走自己的 token,绕开 ignisauth
SECTION 02

前三道关:贴标签、计秒、看来源

前三道都在 app.tsapp.use('*', ...) 里挂着,顺序固定,每个请求都跑。它们不挡请求,只往请求上添附信息或给后续看的人留痕。

这道关 在干嘛 留下了什么
request-id 给请求生成或读取一个单号 放到 c.requestId,所有后续 log、错误响应都带上它,方便你串日志
timing 开始计秒 请求结束打一条 timing log,记录这个请求总耗时
CORS 看 origin 决定要不要发 CORS 头 没配 CORS 就跳过,直接 next();预检请求 (OPTIONS) 也在这里处理
顺序为什么不能乱
request-id 必须最早 — 它是后面所有日志的串号;timing 紧跟着,它要量"包括 CORS / auth 在内的总耗时";CORS 放第三是因为它在某些情况下要返回 204 提前结束,要让 timing 仍能记完。auth 在最后,因为只有上面三件事都完成,才轮到判身份。
SECTION 03 · FOCAL

auth:决定这个请求能不能往下走

auth 看请求带没带 ignisauth header,带就拿那段 JWT 去验;没带就退回找 Authorization: Bearer。两者都不通过,直接 401 退回客户端。完整流程见 → auth-flow 这一面

带了 JWT 的情况

  • 签发方mai-backend 用 RS256 私钥签的,公钥在 worker 的环境变量里
  • 验三件事签名对、issuer = mai-backend、audience 在白名单里
  • 拿四个字段email、session_id、canvas_id、turn_id 存到 c.var,给工具用

退回 bearer token 的情况

  • 谁用本地 curl 调试 / 没 mai-backend 的脚本
  • 怎么配worker 环境变量 TOOLS_BACKEND_TOKEN(或 TOOLS_BACKEND_TOKENS 一组用逗号分隔)
  • 请求带Authorization: Bearer xxxX-Worker-Auth-Key: xxx
两个豁口
/healthz/deploy-check 完全不查 auth — 给探活和部署校验用。tobatsu-gateway 的回调路由 /tobatsu-gateway-callbacks/:kind 也绕过 ignisauth,它自己有 bearer,由 tobatsu-gateway 那边验。
SECTION 04

过完 auth 就开始找工具

tools-backend 一共注册了 17 个工具,全都挂在 POST /:toolName 这一个路由上。Hono 拿到 toolName 后去 registry.ts 找;找不到 404;找到先检查 Content-Type 是不是这个工具收的,不收就 415。

看到的请求 结果
GET /GET /tools 返回所有工具的元信息(名字、描述、accepts)
GET /tools/extract-frame 只返这一个工具的元信息
POST /extract-frame + JSON body 把请求交给这个工具的 run(),等它返回结果
POST /未知工具 404 + code: TOOL_NOT_FOUND
POST /extract-frame + 错的 Content-Type 415 + code: UNSUPPORTED_MEDIA_TYPE,告诉你这个工具收什么
GET /tasks/abc123 异步任务结果,下一段说
SECTION 05

同步出结果 vs 先记任务再轮询

有些工具拍一下立刻有结果(比如读视频帧时长),有些得跑很久(视频剪辑、Vertex 大模型)。tool.run() 返回的对象里有个 kind 字段告诉框架走哪条。

HANDLER tool.run() result.kind SYNC · ASYNC SYNC ASYNC OK · OUTCOME A 直接返结果 客户端拿到 result 就完事 OUTCOME B · 第一步 先写 task 到 KV 返回 task_id + poll URL CLIENT POLLS GET /tasks/:id 直到 status: completed
异步路径多了一层 KV — task 状态写在 Cloudflare KV 上,客户端按 poll_interval_ms 间隔来问。tool 自己实现 poll(),框架负责存读 task 和把结果回填。

同步工具 (kind: 'sync')

  • 例子video-probe、audio-isolation 这种本地 ffmpeg 类
  • 返回200 + 你要的 JSON 结果
  • 时间预算worker 子请求时间内能跑完(一般几秒)

异步工具 (kind: 'async')

  • 例子media-analyse (走 Vertex)、3d-model、add-subtitle
  • 第一次202 + {task_id, poll_url, poll_interval_ms, timeout_ms}
  • 之后客户端按间隔 GET /tasks/:id 直到 completed / failed
SECTION 06

哪里炸了 全都走 onError

Hono 的 app.onError 是统一兜底。工具里抛 ApiError(带 status / code / message / details)就照样返回;抛任何其它的 Error,统一变成 500 + INTERNAL_ERROR,不把内部错误消息泄到响应里。但 console.error 会记完整对象,所以 worker 日志能看到。

情况 HTTP 响应 code 什么时候出
路径不存在404NOT_FOUND路由没匹配到任何 handler
工具不存在404TOOL_NOT_FOUNDPOST 到没注册的 toolName
Content-Type 不收415UNSUPPORTED_MEDIA_TYPE请求 Content-Type 不在工具的 accepts 列表里
auth 失败401UNAUTHORIZEDJWT 验不过 / bearer 不对 / 都没带
配置缺失500CONFIG_ERROR比如 ignisauth 公钥环境变量没配
异步任务找不到404TASK_NOT_FOUNDGET /tasks/:id 的 id 在 KV 里没有
其它没接住的500INTERNAL_ERRORonError 兜底,message 不外泄
想看具体错误?
设环境变量 TOOLS_BACKEND_EXPOSE_ERROR_DETAILS=1,ApiError 的 details 字段就会回到响应里(调试用,线上别开)。其它 Error 永远不会回 details。