绑定允许您控制 Dynamic Worker 可以访问的内容。在创建 Dynamic Worker 时,您可以准确决定它能使用哪些资源和操作。
这使您能够:
- 为每个 Dynamic Worker 提供其自己的资源 — 对 KV 命名空间、R2 存储桶或数据库进行分区,以便每个 Worker 只能看到其自己的数据。
- 公开自定义功能 — 定义您可以让 Dynamic Workers 调用的方法 — 例如发布到聊天室、发送电子邮件或查询内部服务。您设计接口,而 Dynamic Worker 只需调用它。
- 限制和控制访问 — 在调用到达底层资源之前对其进行检查、转换或拒绝。
借助自定义绑定,您可以:
- 在您的 loader Worker 中定义绑定实现:您创建一个带有方法的类。因为这在您的 loader Worker 中运行,所以您可以在那里添加身份验证、日志记录以及按客户划分的范围访问权限。
- 将其作为绑定传递给 Dynamic Worker:它只需调用诸如
this.env.CHAT_ROOM.post("Hello!")的方法,而无需了解其背后的任何实现细节。
要创建自定义绑定,您的 loader Worker 需要实现一个 WorkerEntrypoint 类并将其导出。您在此类上定义的方法就是 Dynamic Worker 将能够调用的方法。
import { WorkerEntrypoint } from "cloudflare:workers";
export class ChatRoom extends WorkerEntrypoint {
async post(text: string): Promise<void> {
// Your implementation here
}
}然后,您的 loader Worker 将创建导出类的一个实例(称为存根),并将其传递到 Dynamic Worker 的 env 中。
let chatRoom = ctx.exports.ChatRoom({ props: { roomName: "#bot-chat" } });
let worker = env.LOADER.load({
env: { CHAT_ROOM: chatRoom },
// ...
});从 Dynamic Worker 的角度来看,CHAT_ROOM 看起来就像一个具有它可以调用的方法的常规绑定:
// Inside the Dynamic Worker
await this.env.CHAT_ROOM.post("Hello!");一个类可以服务于许多不同的 Dynamic Workers。在创建存根时,您无需为每个用户定义一个单独的类,而是传入包含特定于该用户信息 props。
// Same class, different props per user
let aliceRoom = ctx.exports.ChatRoom({ props: { roomName: "#alice", apiKey: ALICE_KEY } });
let bobRoom = ctx.exports.ChatRoom({ props: { roomName: "#bob", apiKey: BOB_KEY } });当 Dynamic Worker 调用绑定上的方法时,它实际上是在回调您的 loader Worker,这正是该方法运行的地方。在该方法内部,您可以通过 this.ctx.props 读取 props。只有 loader Worker 能够访问这些 props,Dynamic Worker 永远看不到它们。
export class ChatRoom extends WorkerEntrypoint<Cloudflare.Env, ChatRoomProps> {
async post(text: string): Promise<void> {
// Props are set when the stub is created — the Dynamic Worker never sees them
let roomName = this.ctx.props.roomName;
await postToChat(roomName, text);
}
}这是一个将所有内容结合在一起的完整示例。假设您正在构建一个平台,AI 代理可以在其中发布到聊天室。每个代理应该只能发布到其分配的房间,并且永远不应该看到用于身份验证的 API 密钥。
您在父 Worker 中定义了一个 ChatRoom 类。该类有一个 post 方法,这是 Dynamic Worker 在该绑定上唯一可以调用的方法。在该类内部,您控制消息发送到哪个房间、使用哪个 API 密钥以及消息附加什么名称。
import { WorkerEntrypoint } from "cloudflare:workers";
export class ChatRoom extends WorkerEntrypoint<Cloudflare.Env, ChatRoomProps> {
async post(text: string): Promise<void> {
let { apiKey, botName, roomName } = this.ctx.props;
// Prefix the message with the bot's name.
text = `[${botName}]: ${text}`;
// Send it to the chat service.
await postToChat(apiKey, roomName, text);
}
}
type ChatRoomProps = {
apiKey: string;
roomName: string;
botName: string;
};您导出一个 ChatRoom 类,但您创建的每个存根可以具有不同的 props — 不同的房间名称、不同的 API 密钥、不同的机器人名称。props 在创建存根时设置,Dynamic Worker 永远看不到它们。
现在将其传递给 Dynamic Worker:
// Create a stub scoped to a specific room.
let chatRoom = ctx.exports.ChatRoom({
props: {
apiKey,
roomName: "#bot-chat",
botName: "Robo",
},
});
let worker = env.LOADER.load({
env: {
CHAT_ROOM: chatRoom,
},
compatibilityDate: "$today",
mainModule: "index.js",
modules: {
"index.js": `
export class Agent extends WorkerEntrypoint {
async run() {
// This is all the Dynamic Worker sees.
await this.env.CHAT_ROOM.post("Hello!");
}
}
`,
},
globalOutbound: null,
});
return worker.getEntrypoint("Agent").run();代理只需调用 this.env.CHAT_ROOM.post("Hello!")。它无法发布到其他房间、查看或使用 API 密钥,也无法更改附加到其消息上的机器人名称。
为了让 AI 代理针对您的绑定编写代码,它需要了解接口。请向您的代理提供 TypeScript 类型声明以及描述每个方法的文档注释。现代 LLM 非常了解 TypeScript,这使其成为描述 JavaScript API 最简洁的方式。即使代理编写的是纯 JavaScript,这也同样适用。
确保您的 WorkerEntrypoint 类扩展了 TypeScript 类型,以便声明与实现保持同步。
要将 KV 命名空间或 R2 存储桶等资源传递给 Dynamic Worker,您需要将该资源绑定到 loader Worker 并创建一个封装它的自定义绑定。您可以通过为键添加前缀并仅定义您希望公开的方法来限制每个客户的访问范围。
首先,将 KV 命名空间绑定到您的 loader Worker。然后在您的 loader Worker 中,导出一个使用 KV 绑定并定义 Dynamic Workers 可以调用的方法的类:
import { WorkerEntrypoint } from "cloudflare:workers";
export class MyStorage extends WorkerEntrypoint<Cloudflare.Env, MyStorageProps> {
// Export this class from your loader Worker
// The Dynamic Worker will be able to call get() and put()
async get(key: string): Promise<string | null> {
// Prefix the key so this customer can only access their own data
return this.env.MY_KV.get(`${this.ctx.props.prefix}:${key}`);
}
async put(key: string, value: string): Promise<void> {
await this.env.MY_KV.put(`${this.ctx.props.prefix}:${key}`, value);
}
}
type MyStorageProps = {
prefix: string;
};然后使用特定于客户的前缀将其传递给 Dynamic Worker:
// Create a stub scoped to this customer's prefix
let storage = ctx.exports.MyStorage({
props: { prefix: `customer-${customerId}` },
});
let worker = env.LOADER.load({
env: { STORAGE: storage },
// ...
});Dynamic Worker 就像使用任何其他绑定一样使用它:
// Inside the Dynamic Worker, it just sees STORAGE with get and put
let value = await this.env.STORAGE.get("settings");
await this.env.STORAGE.put("settings", "dark-mode");这种相同的模式适用于您的 loader Worker 可以访问的任何资源 — R2 存储桶或 D1 数据库。将资源绑定到您的 loader Worker,导出一个使用该资源的类,并将存根传递给 Dynamic Worker。
有关与每个 Dynamic Worker 一起存在的持久存储,请参阅 Durable Object Facets。
自定义绑定遵循基于能力的安全模型 — Dynamic Worker 只能访问您明确赋予其访问权限的内容。如果它没有收到某个东西的存根,它就无法访问它。
这是由 Workers RPC 提供的,也称为 Cap'n Web ↗,这是一个设计用于跨安全边界传递对象引用的 RPC 系统。当 Dynamic Worker 收到存根时,它可以调用该对象的方法,并且每次调用都是回调到您的 loader Worker 中的原始对象的 RPC。存根没有全局标识符并且不能被伪造,获取存根的唯一方法是接收它。
基于能力的安全性对于大多数成功的沙盒的设计至关重要,尽管它通常作为实现细节被隐藏起来 — Android 有 Binder,Chrome 有 Mojo,而 Cloudflare Workers 有 Cap'n Web。Dynamic Workers 直接将这种力量暴露给您(开发者),以便您可以构建自己的强大沙盒。