@cloudflare/computer 给 Durable Object 加了个持久化虚拟文件系统
如何在 Cloudflare Durable Object (DO) 内部为 AI 智能体搭建一个持久化的文件系统,并让模型直接在里面读写和执行命令。@cloudflare/computer 这个库在 DO 的 SQLite 存储之上封装了一层虚拟文件系统 (VFS),同时接入了多种代码与命令执行后端。这套工具目前处于预览阶段,API 随时可能变动,适合用来做原型验证和探索。
安装与最小可用配置,只存文件不跑命令
如何在最短时间内让 DO 拥有跨重启的文件读写能力。装包只需要执行 npm install @cloudflare/computer。你的 Worker 必须开启 nodejs_compat 兼容性标志。如果你只想用文件系统,不需要执行命令,通过 withWorkspace 混入到 DO 类中即可。
在下面的代码里,withWorkspace 接收一个原生的 DO 类,并提供了一个 storage 配置项指向 DO 自身的 SQLite 存储。
import { withWorkspace, getWorkspace } from "@cloudflare/computer";
import { DurableObject } from "cloudflare:workers";
export class Agent extends withWorkspace(
class extends DurableObject<Env> {},
(self) => ({ storage: self.ctx.storage }),
) {}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const id = env.Agent.idFromName("user-123");
using ws = await getWorkspace(env.Agent.get(id));
await ws.fs.writeFile("/notes.md", "- [ ] ship it\n");
const notes = await ws.fs.readFile("/notes.md", "utf8");
return new Response(notes);
},
} satisfies ExportedHandler<Env>;
对应的 wrangler.jsonc 需要配置好 DO 绑定并声明 SQLite 迁移。
{
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [{ "name": "Agent", "class_name": "Agent" }]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["Agent"] }
]
}
这套 VFS 大概能撑住 10 GB 量级的工作区数据。需要注意的是,如果后续你使用容器后端,容器侧的文件系统是存放在内存里的,适合放智能体级别的工作文件,不要往里面塞大型单体仓库。
怎么让 workspace 里的文件跑起命令来
如何在这个文件系统里执行 shell 命令或跑代码。workspace.runtime.exec() 是统一的执行入口。它支持三种后端,你需要根据对真实 Linux 环境的依赖程度来选择。
| 后端 | 引入路径 | 执行内容 | 依赖条件 |
|---|---|---|---|
| Container | @cloudflare/computer/backends/container |
在完整 Linux 环境跑 shell 命令 | 需要 Cloudflare Container 运行 computerd |
| Worker shell | @cloudflare/computer/backends/worker-shell |
在 Dynamic Worker 中跑 just-bash | 需要 Worker Loader 绑定和 experimental 标志 |
| Worker JavaScript | @cloudflare/computer/backends/worker-javascript |
在全新 Dynamic Worker 执行 ECMAScript 模块 | 需要 Worker Loader 绑定和 experimental 标志 |
Worker shell 后端是跑通 exec 最快的方式,它不需要 Docker 容器。代价是你要在 wrangler.jsonc 里加上 experimental 标志和一个 worker_loaders 绑定。
{
"compatibility_flags": ["nodejs_compat", "experimental"],
"worker_loaders": [{ "binding": "LOADER" }]
}
在代码层面,你需要初始化 WorkerShellBackend 并把 loader 和 workspace 绑定传进去。
import { withWorkspace, getWorkspace } from "@cloudflare/computer";
import { WorkerShellBackend } from "@cloudflare/computer/backends/worker-shell";
import curlModules from "@cloudflare/computer/shell/curl";
import { DurableObject } from "cloudflare:workers";
export class Agent extends withWorkspace(
class extends DurableObject<Env> {},
(self) => ({
storage: self.ctx.storage,
backends: [
new WorkerShellBackend({
loader: self.env.LOADER,
workspace: { binding: "Agent", id: self.ctx.id.toString() },
ctx: self.ctx,
commands: [curlModules],
}),
],
}),
) {}
这个 worker shell 把命令拆成了特性分组,比如 curl、sqlite、jq、python 都是按需引入的。你在 commands 里挂载哪个模块,执行环境里才能用哪个命令,没挂载的会被打包器直接剔除。这里所有的文件系统操作都会回传给同一个 Durable Object 处理,不存在第二份存储和同步开销。
容器后端能给你真实的 Linux 二进制文件和完整的网络环境,但冷启动较慢。它会维持自己的 SQLite VFS,并通过 capnweb WebSocket 与主 DO 做状态同步。要注意容器对文件的访问走的是 FUSE,所以涉及重 I/O 的操作(比如装一个庞大的 node_modules 或解压大体积 tar 包)会比原生磁盘慢。
文件系统的进阶用法与 R2 挂载
如何处理二进制流以及给工作区挂载外部只读数据。workspace.fs 接口是异步的,强制使用绝对路径。字符串默认按 UTF-8 编码处理。如果你要写二进制数据,直接传 Uint8Array 或者 ReadableStream 进去。
// 写入字符串、字节或流
await ws.fs.writeFile("/notes/todo.md", "- [ ] ship it\n");
await ws.fs.writeFile("/data/blob.bin", new Uint8Array([1, 2, 3]));
await ws.fs.writeFile("/uploads/big.csv", request.body!);
// 读取为字符串或流
const todo = await ws.fs.readFile("/notes/todo.md", "utf8");
const stream = await ws.fs.readFile("/uploads/big.csv");
return new Response(stream);
// 目录与搜索
await ws.fs.mkdir("/notes/daily", { recursive: true });
await ws.fs.rm("/notes/daily", { recursive: true });
const hits = await ws.fs.grep("TODO", "/", { ignoreCase: true });
如果你想在工作区里预置一些静态文件,可以把 R2 存储桶挂载进来。挂载点下的文件全部是只读的,尝试写入会直接抛出 EROFS 错误。
import { R2Bucket } from "@cloudflare/computer";
new Workspace({
storage: ctx.storage,
mounts: { "/workspace/r2": R2Bucket(env.Bucket) },
});
给大模型配齐 AI 工具和 Git 客户端
如何把这套文件系统直接喂给 Vercel AI SDK。@cloudflare/computer/tools 打包了现成的 AI SDK 工具,默认提供 read、write、edit、ls,配置后还能加上 exec 和 publish。你可以给 read 工具设定读取的字节和行数上限。
在配置 exec 工具时,你可以给每个后端写一段 description。模型在决定把命令发给哪个后端时,会读取这段描述做判断。
import { createAITools } from "@cloudflare/computer/tools";
const tools = createAITools({
workspace,
read: { maxBytes: 32 * 1024, maxLines: 800 },
shell: {
defaultBackend: "shell",
backends: {
shell: { description: "Fast Worker shell with built-in text commands." },
container: { description: "Full Linux userland in a Cloudflare Container." },
},
},
});
Git 功能同样是按需引入的。通过 @cloudflare/computer/git,你能直接在本地 SQLite VFS 上做 clone、add、commit。它底层是 isomorphic-git,把原本的 pako 依赖换成了 Workers 平台的 node:zlib 实现。它按需加载,不开启就不会影响主包体积。
踩坑预警,DO 存根不会自动垃圾回收
长连接场景下为什么会内存泄漏以及怎么防。Worker 访问 DO 上的 Workspace 需要拿到一个存根。这套库的 RPC 层不会自动对存根做垃圾回收。在长会话或高频 exec 调用中,如果你不断获取新存根而不释放,对端的存根就会一直堆积,直到会话结束。
正确的做法是使用 using 关键字来声明返回值。using ws = await getWorkspace(...) 和 using run = await ws.runtime.exec(...) 必须养成肌肉记忆。
挂在父级上的属性(如 ws.fs、ws.runtime、ws.git)不用管。返回纯值的方法(比如 readFile 读成字符串、stat、readdir)本身不带存根,也不用操心释放。
如果你怀疑代码里有泄漏,可以设置环境变量 CAPNWEB_TRACK_STUBS=1,然后从 @cloudflare/computer-rpc/debug 里读取 stubSnapshot() 来检查,或者直接请求 computerd 实例上的 GET /__computerd/stubs 接口。
多后端混用与状态同步重试
一个工作区能不能同时挂多个后端并按需路由。初始化 Workspace 时传入多个 backend 实例,并给每个实例分配一个 id。调用 exec 时不指定 backend 就走第一个,如果传了 { backend: "sandbox" } 就会路由到你指定的那个。
const ws = new Workspace({
storage: ctx.storage,
backends: [
new WorkerShellBackend({ id: "shell", loader: env.LOADER }),
new CloudflareContainerBackend({ id: "sandbox", container: () => this }),
],
});
const grep = await ws.runtime.exec("grep -r TODO /workspace");
const build = await ws.runtime.exec("npm test", { backend: "sandbox" });
后端是懒加载的,第一次调用 exec 或 ready 时才建立连接。每个后端有独立的同步游标,在一个后端上跑任务不会干扰另一个。
有时候命令在后端执行完了,但回拉文件的同步操作失败了,这时返回结果里会带有 sync: { status: "pending" } 标记。你可以在 Workspace 上配置一个 SyncRetryScheduler,然后在你的 DO alarm 里手动调用 workspace.retryPendingSync(backend) 触发重试。重试机制采用指数退避,超过最大次数会返回 "exhausted"。注意库本身不会接管你的 DO alarm,触发权完全交给你。
速览与常见问题
操作清单
-
执行 npm install @cloudflare/computer -
在 wrangler.jsonc中开启nodejs_compat标志 -
若使用 worker-shell 或 worker-javascript 后端,补充开启 experimental标志并配置worker_loaders -
用 withWorkspace包装 DO 类,并在回调中传入self.ctx.storage -
按需引入 shell 命令模块并传给后端构造函数 -
对 getWorkspace和ws.runtime.exec的返回值强制使用using关键字 -
长连接排查泄漏时设置 CAPNWEB_TRACK_STUBS=1
常见问题
问:这套库目前可以上生产环境吗?
答:不行。官方明确标注当前仅为预览阶段,API 不稳定,设计随时可能更改,只适合做实验和原型。
问:最大能放多少文件?
答:每个 workspace 大约 10 GB。因为它和 DO 共享底层存储,同时容器后端的 VFS 是放在内存里的。
问:worker-shell 后端怎么精简打包体积?
答:按特性分组引入。比如只用到了网络请求,就只 import @cloudflare/computer/shell/curl 并传给 commands 配置,未引入的模块会被打包器剔除。
问:R2 挂载进来的文件能修改吗?
答:不能。挂载点下的文件全部是只读的,尝试写入会直接抛出 EROFS 错误。
问:哪些对象必须用 using 关键字释放?
答:getWorkspace(...) 返回的 client 对象,以及 ws.runtime.exec(...) 返回的 run handle。ws.fs 等子对象和纯值返回的函数不用管。
问:同步失败后还会自动重试吗?
答:库本身不会自动重试。需要你配置 SyncRetryScheduler,并在自己的 DO alarm 里调用 workspace.retryPendingSync 手动触发。
问:如何把工作区里的文件分享出去?
答:使用 @cloudflare/computer/assets 提供的 createAssets(...).share 把文件传到 R2 并生成预签名 URL,或者使用 @cloudflare/computer/artifacts 与 Cloudflare Artifacts 绑定交互。

