用单个 Cloudflare Worker 承载 AI 视频应用
作者分享基于 TanStack Start 的 AI 视频应用架构,将界面、API、认证、支付和任务流程集中到一个 Worker 入口。方案结合 D1、R2、KV、Workers AI 与自动部署,讨论大文件和耗时外部任务的处理。
AI 深度解读
一个 AI 视频应用,从上传、登录、付费到异步任务,能不能只部署一个 Cloudflare Worker?作者的独立项目给出了可运行的答案:用 TanStack Start 串起业务,把文件交给 R2、账目交给 D1、视频生成交给托管模型。
这套方案值得看的,是它如何处理大文件、按时长计费和延迟退款。不过,「轮询推进任务」也留下了一个关键问题:用户关掉页面之后,究竟由谁继续推进任务?
一个 Worker 能装下哪些业务
作者使用 TanStack Start、React 19 做 SSR 和文件路由,通过 Vite 与 Cloudflare Vite plugin 构建。所有请求从 src/server.ts 进入,再交给 TanStack Start 的 handler。
业务接口用 createServerFn 实现,配上 Zod 校验和中间件链。前端像调用普通异步函数一样调用它,实际执行发生在 Worker 内部,可以直接访问资源绑定。
数据和文件各有去处:D1 是 SQLite 数据库,经 Drizzle ORM 管理用户、积分和任务;R2 保存上传素材与成片。Better Auth 处理邮箱密码及 Google 登录,Cloudflare Email 通过一个小型 provider 接口发送事务邮件。GitHub Actions 在每次推送到 main 后执行 wrangler deploy,并提供每个 PR 的预览 URL。
这里的「一个 Worker」指统一的应用入口与部署单元,背后仍然依赖多种托管服务。
作者实际运行的是小型独立项目,使用 Kling AI 的动作控制模型,但与 Kling AI、Kuaishou 没有隶属关系。原文没有给出并发量、成本账单或性能对照,不能据此推导出任意规模都适用。
大文件不进内存,计费时长怎么读
动作视频可能有几十 MB。作者没有先把请求体读成完整文件,而是让上传路由直接把流写进 R2:
// simplified
const key = `uploads/${userId}/${crypto.randomUUID()}.mp4`;
await env.BUCKET.put(key, request.body, {
httpMetadata: { contentType: request.headers.get('content-type') ?? '' },
});
return Response.json({ key });
关键是 env.BUCKET.put(key, request.body, ...) 这一行:传入的是请求体流。客户端拿到 key 后,再调用启动任务的服务端函数。素材被任务使用后就删除,R2 生命周期规则还会清理超过一天的文件。
按视频秒数收费,又要求服务端先知道时长。作者利用 MP4、MOV 的文件结构:时长位于 moov 内的 mvhd box,通过其中的 timescale 和 duration 计算。
服务端用 R2 range reads 读取顶层 box 的头部,每个头部只有 8–16 字节,跳过存放媒体数据的 mdat,再读取 moov。作者称,通常只需读取几 KB。提交模型任务时,视频同样以流传给供应商。
这套做法把「读完整个视频」缩成了「读取必要元数据」。但几 KB 是典型情况,原文没有交代异常文件、超大 moov 和解析失败的处理方式。
异步任务最重要的是状态和账目
视频生成要几分钟,不适合一直占着请求。作者没有使用 Queues 或 Durable Objects,而是把流程分成启动和轮询推进。
启动请求负责校验输入、审核照片、扣积分、提交供应商,并将供应商请求 ID 写入 effect_job 表。客户端随后轮询状态函数,每次轮询都查询供应商并更新任务:成功就把成片复制到 R2,失败就退积分,完成后发邮件。
照片审核发生在扣费前,通过 AI binding 调用 Workers AI 视觉模型。色情内容和可识别公众人物直接拒绝,疑似包含未成年人的照片适用额外规则;审核本身报错,也拒绝继续。
退款不能只靠捕获异常。原有 withCredits 可以在回调抛错时退款,但任务可能在扣费请求结束五分钟后才失败。因此作者把实际花费写进任务行,失败时按该金额退款,并标记已经退款。
跨请求的账务依据应该落在持久化状态里。
作者还表示,状态调用具有幂等性,只会从已存状态向前推进,因此多标签页重复轮询无害。不过,原文没有展示数据库事务或条件更新代码,无法验证并发退款与外部操作如何保证一致。
成片默认私有,下载路由检查所有者会话。Cloudflare Media Transformations 生成首帧封面时需要通过 URL 取视频,因此路由还接受短期签名链接;封面流水线使用签名访问,再通过 Images binding 保存 WebP。
我的判断
这套设计最有借鉴价值的,是把文件流、任务状态和积分流水的边界想清楚了。对独立项目而言,少维护一个队列消费者,确实可能降低运维负担。但作者没有提供与队列方案的对照数据,「更简单」仍是特定项目里的经验判断。
最大疑点是轮询的活性。原文一方面说客户端轮询负责推进任务,另一方面说完成后发邮件,用户可以关掉标签页。如果所有页面都关闭,谁来触发成片转存、退款和通知?原文没有说明后台补偿机制。邮件只能通知已经完成的任务,不能替代任务调度。
本地开发和端到端测试直接连接远程 D1、R2、KV,也是有前提的选择。作者在 wrangler.jsonc 的每个绑定上设置 "remote": true,用独立开发数据库换取更接近真实环境的行为,避免 SQLite 版本、R2 行为差异。多人团队则需要进一步考虑测试数据隔离和共享资源冲突。
最后,Worker 存在不能使用 Node-only APIs、内存要精打细算、请求有时间限制等约束。已有 Node 应用如果高度依赖原生模块,这篇文章不足以支持直接迁移。
能拿来做什么
- 为前端上传大文件设计独立上传接口,先流式写入对象存储,再把文件 key 传给业务函数,并设置任务后清理和生命周期兜底。
- 在 AI 编程项目里,把任务状态、供应商请求 ID、实际扣费额和退款标记一起持久化,并审查重复请求与并发更新的处理方式。
- 采用客户端轮询前,明确无人访问时由谁推进任务。如果要求关页后仍能完成转存和通知,就需要补充可靠的后台触发机制。
- 给私有文件的内部处理链路配置短期签名访问,并在独立远程开发资源上验证存储和数据库行为。