Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Dynamic Workers 中的 Durable Objects:给每个 AI 生成的应用一个自己的数据库

原文:Durable Objects in Dynamic Workers: Give each AI-generated app its own database / 2026-04-13 Source: https://blog.cloudflare.com/durable-object-facets-dynamic-workers/

几周前,我们宣布了 Dynamic Workers,Workers 平台的一个新特性,让你可以即时把 Worker 代码加载进一个安全沙箱。Dynamic Worker Loader API 本质上提供对 Workers 一直以来所基于的基础计算隔离原语——isolates 而非容器——的直接访问。Isolates 比容器轻得多,因此可以快 100 倍、用 1/10 的内存加载。它们如此高效,可以被当作“一次性“使用:启动一个跑几行代码,然后扔掉。像一个安全版的 eval()。

Dynamic Workers 有许多用途。在最初的发布中,我们关注于如何用它们运行 AI agent 生成的代码,作为工具调用的替代。这种用例下,AI agent 通过写几行代码并执行来代表用户行动。代码是单次使用、意在执行一项任务一次的,执行后立即扔掉。

但如果你想让 AI 生成更持久的代码呢?如果你想让 AI 构建一个带定制 UI 让用户交互的小应用呢?如果你想让那个应用拥有长寿状态呢?当然,你仍然希望它在安全沙箱中运行。

实现这点的一种方式是使用 Dynamic Workers,简单地为 Worker 提供一个让它访问存储的 RPC API。利用 bindings,你可以给 Dynamic Worker 一个指向你的远程 SQL 数据库的 API(也许后端是 Cloudflare D1,或者通过 Hyperdrive 访问的 Postgres 数据库——由你决定)。

但 Workers 还有一种独特且极快的存储,可能完美契合这个用例:Durable Objects。Durable Object 是一种特殊的 Worker,有一个独特的名字,在全球每个名字对应一个实例。该实例附带一个 SQLite 数据库,这个数据库存放在 Durable Object 运行的机器的 本地磁盘 上。这让存储访问快到离谱:实际上 零延迟

也许你真正想要的是让 AI 为一个 Durable Object 写代码,然后你想在 Dynamic Worker 中运行那段代码。

但要怎么做?

这带来一个奇怪的问题。通常,要使用 Durable Objects 你必须:

  1. 写一个继承 DurableObject 的类。

  2. 从你的 Worker 主模块中导出它。

  3. 在你的 Wrangler config 中指定 这个类应该被分配存储。这创建一个指向你的类来处理传入请求的 Durable Object namespace。

  4. 声明一个 Durable Object namespace binding 指向你的 namespace(或使用 ctx.exports),用它向你的 Durable Object 发起请求。

这并不自然延伸到 Dynamic Workers。首先,有一个明显的问题:代码是动态的。你完全不调用 Cloudflare API 就运行它。但 Durable Object 存储必须通过 API 配置,namespace 必须指向一个实现类。它不能指向你的 Dynamic Worker。

但还有一个更深的问题:即使你能以某种方式把 Durable Object namespace 配置成直接指向一个 Dynamic Worker,你想这样吗?你想让你的 agent(或用户)能创建一整个充满 Durable Object 的 namespace?在世界各地用无限存储?

你大概不想。你大概想要一些控制。你可能想限制,或至少跟踪,他们创建多少对象。也许你想限制他们只能创建一个对象(对 vibe-coded 个人 app 大概够了)。你可能想加日志和其他可观测性。指标。计费。等等。

要做到这一切,你真正想要的是让对这些 Durable Object 的请求 进入 你的 代码,在那里你可以做所有“后勤“,然后 把请求转发进 agent 的代码。你想写一个作为每个 Durable Object 一部分运行的 supervisor

解决方案:Durable Object Facets

今天我们以公测形式发布解决这个问题的特性。

Durable Object Facets 让你可以动态加载并实例化一个 Durable Object 类,同时为它提供一个 SQLite 数据库用于存储。借助 Facets:

  • 首先你创建一个普通的 Durable Object namespace,指向 写的类。

  • 在那个类中,你以 Dynamic Worker 形式加载 agent 的代码,并调用它。

  • Dynamic Worker 的代码可以直接实现一个 Durable Object 类。也就是说,它真的导出一个声明为 extends DurableObject 的类。

  • 你将该类实例化为你自己 Durable Object 的一个 “facet”。

  • 这个 facet 获得自己的 SQLite 数据库,可通过普通的 Durable Object 存储 API 使用。这个数据库与 supervisor 的数据库分离,但二者作为同一个整体 Durable Object 的一部分一起存储。

它如何工作

下面是一个动态加载并运行 Durable Object 类的应用平台的简单、完整实现:

import { DurableObject } from "cloudflare:workers";

// For the purpose of this example, we'll use this static
// application code, but in the real world this might be generated
// by AI (or even, perhaps, a human user).
const AGENT_CODE = `
  import { DurableObject } from "cloudflare:workers";

  // Simple app that remembers how many times it has been invoked
  // and returns it.
  export class App extends DurableObject {
    fetch(request) {
      // We use storage.kv here for simplicity, but storage.sql is
      // also available. Both are backed by SQLite.
      let counter = this.ctx.storage.kv.get("counter") || 0;
      ++counter;
      this.ctx.storage.kv.put("counter", counter);

      return new Response("You've made " + counter + " requests.\\n");
    }
  }
`;

// AppRunner is a Durable Object you write that is responsible for
// dynamically loading applications and delivering requests to them.
// Each instance of AppRunner contains a different app.
export class AppRunner extends DurableObject {
  async fetch(request) {
    // We've received an HTTP request, which we want to forward into
    // the app.

    // The app itself runs as a child facet named "app". One Durable
    // Object can have any number of facets (subject to storage limits)
    // with different names, but in this case we have only one. Call
    // this.ctx.facets.get() to get a stub pointing to it.
    let facet = this.ctx.facets.get("app", async () => {
      // If this callback is called, it means the facet hasn't
      // started yet (or has hibernated). In this callback, we can
      // tell the system what code we want it to load.

      // Load the Dynamic Worker.
      let worker = this.#loadDynamicWorker();

      // Get the exported class we're interested in.
      let appClass = worker.getDurableObjectClass("App");

      return { class: appClass };
    });

    // Forward request to the facet.
    // (Alternatively, you could call RPC methods here.)
    return await facet.fetch(request);
  }

  // RPC method that a client can call to set the dynamic code
  // for this app.
  setCode(code) {
    // Store the code in the AppRunner's SQLite storage.
    // Each unique code must have a unique ID to pass to the
    // Dynamic Worker Loader API, so we generate one randomly.
    this.ctx.storage.kv.put("codeId", crypto.randomUUID());
    this.ctx.storage.kv.put("code", code);
  }

  #loadDynamicWorker() {
    // Use the Dynamic Worker Loader API like normal. Use get()
    // rather than load() since we may load the same Worker many
    // times.
    let codeId = this.ctx.storage.kv.get("codeId");
    return this.env.LOADER.get(codeId, async () => {
      // This Worker hasn't been loaded yet. Load its code from
      // our own storage.
      let code = this.ctx.storage.kv.get("code");

      return {
        compatibilityDate: "2026-04-01",
        mainModule: "worker.js",
        modules: { "worker.js": code },
        globalOutbound: null,  // block network access
      }
    });
  }
}

// This is a simple Workers HTTP handler that uses AppRunner.
export default {
  async fetch(req, env, ctx) {
    // Get the instance of AppRunner named "my-app".
    // (Each name has exactly one Durable Object instance in the
    // world.)
    let obj = ctx.exports.AppRunner.getByName("my-app");

    // Initialize it with code. (In a real use case, you'd only
    // want to call this once, not on every request.)
    await obj.setCode(AGENT_CODE);

    // Forward the request to it.
    return await obj.fetch(req);
  }
}

在这个例子中:

  • AppRunner 是一个由平台开发者(你)写的“普通“ Durable Object。

  • 每个 AppRunner 实例管理一个应用。它存储应用代码并按需加载。

  • 应用本身实现并导出一个 Durable Object 类,平台预期它叫 App

  • AppRunner 用 Dynamic Workers 加载应用代码,然后把代码作为 Durable Object Facet 执行。

  • 每个 AppRunner 实例就是一个由 两个 SQLite 数据库组成的 Durable Object:一个属于父级(AppRunner 自己),一个属于 facet(App)。这两个数据库是隔离的:应用无法读取 AppRunner 的数据库,只能读自己的。

要运行这个例子,把上面代码复制到一个文件 worker.js,与下面的 wrangler.jsonc 配对,用 npx wrangler dev 在本地运行。

// wrangler.jsonc for the above sample worker.
{
  "compatibility_date": "2026-04-01",
  "main": "worker.js",
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": [
        "AppRunner"
      ]
    }
  ],
  "worker_loaders": [
    {
      "binding": "LOADER",
    },
  ],
}

开始构建

Facets 是 Dynamic Workers 的一项特性,即刻起以公测形式向 Workers Paid 计划用户开放。

查看文档了解更多关于 Dynamic WorkersFacets 的信息。

– 原文译于 2026-04-30