SuperAPI AI 工具集

ai-persistence/build-cloudflare-artifact-store

部署与运维

查看项目

更新于 10-16 10:42

技能介绍

用途

当一个 Cloudflare Worker 需要为 TanStack AI 生成的媒体(图片、音频、视频、转录文本)提供持久字节存储时使用本技能。withGenerationPersistence(persistence) 只需要 stores.generationRuns 来追踪一次生成的生命周期。若同时添加 stores.artifacts(元数据)和 stores.blobs(字节)——两者要么都加,要么都不加——中间件还会持久化生成的媒体:图片/音频/TTS/视频/转录的字节存放在 blob 键 artifacts/<runId>/<artifactId>,并用一行 ArtifactRecord 描述每一项。

交付物是 Worker 中的一个文件——例如 src/lib/generation-persistence.ts——导出一个工厂,从请求的 R2 + D1 绑定构建 AIPersistence,再加一个 GET 路由,用 retrieveArtifact / retrieveBlob 提供 artifact 字节。

关于每请求绑定的规则、wrangler 配置形态、D1 迁移工作流以及 chat(generation-run/message)一侧,请阅读同级的 ai-persistence/build-cloudflare-adapter 技能。本技能只涵盖两个字节存储以及如何组合它们。

两个契约

两者都来自 @tanstack/ai-persistence。defineBlobStore / defineArtifactStore 以内联方式对对象字面量做类型标注(提供自动补全和契约检查,无需单独的类型注解)。

BlobStore 是字节层,由 R2 支撑。其接口包含 put、get、head、delete、list。get 返回元数据加字节访问器;options.range 读取一个切片(用于 206 响应)。head 只返回元数据。delete 在不存在时是静默无操作。list 返回分页结果。

ArtifactStore 是元数据层,由 D1 支撑。其接口包含 save(插入或覆盖)、get、list(按 runId)、listForThread(按 threadId)、delete、deleteForRun。list 和 listForThread 返回的记录按 createdAt 排序,再按 artifactId 的序数值字节序排列——从左到右比较 UTF-8 字节,不要使用 locale 排序规则。

BlobBody 类型为 ReadableStream<Uint8Array> | ArrayBuffer | ArrayBufferView | string | Blob。非流式形态可以直接传给 R2Bucket.put,但 ReadableStream 体在一般情况下不行:workerd 的 put 要求流具有已知长度。当中间件不得不限制抓取体的大小时,会交给你一个被 TransformStream 包装的无长度流,直接传给 bucket.put 会抛出 TypeError: Provided readable stream must have a known length。

这种情况只在没有其他东西限制传输大小时发生:供应商响应带 content-length 且无 content-encoding 时,体原样保留、声明长度不变,直接走 bucket.put;chunked(无声明长度)时被包装、无长度,走 multipart;content-encoding: gzip 时被包装、无长度,走 multipart。

另一类无长度流完全不需要 URL 抓取。没有公开 URL 的视频供应商(OpenRouter、Lovable、没有 url 的 Sora 任务)以 ReadableStream 返回下载内容,中间件原样交给 put,没有 expectedLength。OpenRouter 这类 SDK 流不携带声明长度,所以总是走 multipart 路径。

供应商 CDN 通常发送 content-length,所以第一行是常见情况,bucket.put(key, body) 直接可用。下面的配方让另外两种情况也能工作:当中间件能担保长度时从 BlobPutOptions.expectedLength 重新声明长度,否则通过 multipart 上传流式处理(一次一个 8 MiB 分片——任意 artifact 大小下内存占用平稳)。写一次即可覆盖所有响应形态。

withGenerationPersistence(persistence, { maxArtifactBytes: false }) 会去掉上限和包装器,因此即使 chunked 回复也原样到达。它对 R2 没有额外好处(chunked 体没有长度可保留),所以单独根据自身价值选择它:不对源站能流进你的桶的内容设应用限制。R2 自身的限制仍然适用——单次 put 5 GiB,10,000 个 multipart 分片(按下面 8 MiB 分片大小约 80 GiB)。当 allowInputUrl 允许调用者指定 URL 时,保留上限。

BlobPutOptions 为 { contentType?, customMetadata?, expectedLength? };BlobGetOptions 为 { range?: { offset: number, length?: number } },映射到 R2 自身的 range;BlobListOptions 为 { prefix?, cursor?, limit? };BlobListPage 为 { objects: BlobRecord[], cursor?, truncated? }。

1. 由 R2 支撑的 BlobStore

R2Object 携带 size、etag、httpMetadata.contentType、customMetadata 和 uploaded(一个 Date)。BlobRecord 需要 createdAt / updatedAt 为 epoch 毫秒——R2 只追踪单个 uploaded 时刻,所以把它映射到两者。get / head 是字节体与仅元数据的区分;R2ObjectBody 已经暴露 body、arrayBuffer() 和 text(),所以 BlobObject 本质上是 R2 对象加上映射后的元数据。

实现要点:R2 multipart 分片必须 ≥ 5 MiB,且除最后一个外全部大小完全一致,所以分片读取器必须在精确边界上切割并携带剩余。8 MiB × 10,000 分片上限使 multipart 路径的限制约为 80 GiB;更大的对象要调高这个值,并在承诺更多之前检查 R2 当前的对象大小限制。

readPart 函数从流中精确切出 limit 字节(仅 EOF 时更少),把超出部分留到下一分片。carry 是上一次调用留下的剩余。分片按引用收集,每分片复制一次:逐块增长 Uint8Array 会在每次读取时重新复制整个分片——对于由数百个小块构成的分片来说是二次方工作量。

putStream 函数处理无已知长度的流:当 expectedLength 提供时重新声明长度——中间件只在它是精确解码字节数时设置——否则通过 multipart 上传流式处理,一次缓冲一个分片。R2 单次 put 上限 5 GiB,超过则即使长度已知也必须 multipart。

一致性测试工具断言的不变量:get / head 对缺失键返回 null;delete 是静默无操作。put 覆盖已存在的键。put 接受没有声明长度的 ReadableStream 体——中间件把 URL 抓取的 artifact 和供应商的视频下载流都作为这种体传入。这正是朴素“把体直接传给 bucket.put”配方在运行时报错的地方(workerd 要求已知长度),也就是上面 putStream 所处理的。get 遵守 options.range:只返回该切片,作为 range 报告,并把 size 保留为整个对象的大小。这是视频播放器 seek 所依赖的 206,R2 在桶内切片,字节从不穿过 Worker。list 按 prefix 字面过滤(R2 prefix 是字面字节前缀——无 glob),按键升序返回,在截断时通过不透明 cursor 分页。R2 自己的 cursor 是不透明的,直接满足这一点。limit: 0 必须产生空且未截断的页——R2 把 limit: 0 视为“使用默认值”,所以特殊处理它:if (options?.limit === 0) return { objects: [] }。

2. 由 D1 支撑的 ArtifactStore

ArtifactRecord 为 { artifactId, runId, threadId, blobKey?, name, mimeType, size, sourceUrl?, createdAt }(createdAt 为 epoch 毫秒)。一张扁平表以 artifact_id 为主键,带 run 和 thread 有序索引,用于 list 和 listForThread。建表语句包含 generation_artifacts 表及 generation_artifacts_run_order 等索引。