AI 参与说明(Agent:Codex;模型:gpt-5.6-terra;reasoning effort:ultra):本文由 Codex 根据作者提供的主题、对话素材与 Cloudflare 官方文档辅助生成,属于 AI 生成/整理内容,非作者原创。请读者自行甄别并交叉验证。 本次由 Codex(模型:
gpt-6-astra;reasoning effort:medium;执行入口:Codex Desktop;Codex CLI 版本:0.153.4;提供方:OpenAI)于 2026-09-07 修订示例与适用条件;代码按文中说明区分演示骨架和实际验证范围。
这是一篇“把 API 放回运行时”的实战页。Dynamic Workers 不是把一段字符串直接执行掉:先部署一个可信的 Loader Worker;Loader 再决定动态代码、它可拿到的 binding、网络出口和资源上限。真正重要的接口只有三层:WorkerLoader、WorkerCode 与 WorkerStub / entrypoint。
Dynamic Workers 仍在快速演进,本文按 2026-08-07 的官方文档整理。它目前面向 Workers Paid;部署前应重新核对限额、价格与 beta 状态。
先记住对象关系
flowchart LR
U["用户请求"] --> H["可信 Loader Worker"]
H --> A["认证、租户解析、代码版本选择"]
A --> L["env.LOADER.load() 或 get()"]
L --> C["WorkerCode:modules + bindings + 网络策略"]
C --> S["WorkerStub"]
S --> E["getEntrypoint()"]
E --> D["动态 Worker 的 fetch / RPC 入口"]
D --> B["仅被显式授予的 capability"]
H -. "ctx.exports Service Binding" .-> B
不要把 get(id) 当作“拿到一个常驻进程”。它只是尽力复用已加载 isolate 的缓存:同一个 ID 的两次请求也可能进入不同 isolate,getCodeCallback 也可能在以后再次调用。动态代码必须把可靠状态放在 Durable Objects、D1、R2 等外部能力中。
1. 配置与最小调用
先在宿主 Worker 配置一个 Loader binding:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "tenant-code-host",
"main": "src/index.ts",
"compatibility_date": "2026-08-07",
"worker_loaders": [
{ "binding": "LOADER" }
]
}
随后,宿主在每次请求中创建或取得一个 WorkerStub,再调用它的默认 entrypoint:
export default {
async fetch(request, env) {
const worker = env.LOADER.get(
"greeting:v3:7f61e2",
async () => ({
compatibilityDate: "2026-08-07",
mainModule: "index.js",
modules: {
"index.js": [
"export default {",
" fetch() {",
" return new Response('hello from dynamic code');",
" }",
"};",
].join("\n"),
},
globalOutbound: null,
limits: { cpuMs: 50, subRequests: 10 },
}),
);
return worker.getEntrypoint().fetch(request);
},
};
get() 本身同步返回 stub,不需要 await;若代码尚未装载,首次向 stub 发出的请求会等待装载完成。装载失败时,实际调用会抛错。
2. API 速查:应该在什么时机调用谁
| 对象 | 接口 | 使用方式与边界 |
|---|---|---|
env.LOADER | load(code) | 每次调用创建一个新的 Dynamic Worker;适合一次性、每次代码都不同的 AI 工具调用。 |
env.LOADER | get(id, getCodeCallback) | 用不可变版本 ID 尽力复用代码/isolate;适合同一租户同一版本的小应用。 |
WorkerStub | getEntrypoint() | 取得默认模块 entrypoint;通常接着调用 fetch(request)。 |
WorkerStub | getEntrypoint(name) | 取得命名的 WorkerEntrypoint,用于明确暴露的 RPC API。 |
WorkerCode | modules、mainModule | 给出入口模块和已打包的依赖图;这不是运行时 TypeScript 编译器。 |
WorkerCode | env | 把结构化可克隆数据或 Service Binding 作为动态代码的 env。 |
WorkerCode | globalOutbound | 决定全局 fetch()/connect() 是继承、拒绝还是被网关接管。 |
WorkerCode | limits、tails | 为不可信代码设置 CPU/subrequest 上限,并投递日志和异常到 Tail Worker。 |
WorkerCode 还可携带 compatibility flags 和实验开关;应把它们也视为版本的一部分。只要代码、依赖、权限、bindings、compatibility 配置任一项变化,就必须换新的 get() ID,例如 tenant-42:editor:v17:content-hash。
3. load() 还是 get()?
| 场景 | 选择 | ID 原则 |
|---|---|---|
| LLM 刚生成、只运行一次的工具调用 | load() | 不需要缓存身份。 |
| 一个租户的固定版本预览应用 | get() | 租户 + 版本 + 内容 hash。 |
| 同一代码但权限不同 | get() | 权限也进入 ID;不能共用旧 ID。 |
| 调试期间每次都改 bundle | get() | 每次构建产生新 hash。 |
错误做法是把租户 ID 单独作为 ID,却在 callback 中根据“当前最新版本”返回不同代码。恢复或重新装载时,缓存语义会变得不确定,也可能把旧会话悄悄切到新权限。
4. 不要把 KV、密钥或数据库直接交给动态代码
最小权限的核心是:动态 Worker 得到的是业务 capability,不是原始 Cloudflare binding。下面的宿主 TenantStore 在可信 Worker 内运行,因此可进行鉴权、租户隔离与审计;动态代码只能调用它允许的 readProfile()。
import { WorkerEntrypoint } from "cloudflare:workers";
export class TenantStore extends WorkerEntrypoint {
async readProfile() {
const tenantId = this.ctx.props.tenantId;
return this.env.PROFILES.get(tenantId + ":profile");
}
}
export default {
async fetch(request, env, ctx) {
const tenantId = await authenticateAndGetTenantId(request);
const version = await getPublishedVersion(tenantId);
const worker = env.LOADER.get(
"tenant:" + tenantId + ":v:" + version,
async () => ({
compatibilityDate: "2026-08-07",
mainModule: "index.js",
modules: {
"index.js": await loadBundledCode(tenantId, version),
},
env: {
STORE: ctx.exports.TenantStore({
props: { tenantId },
}),
},
globalOutbound: null,
limits: { cpuMs: 50, subRequests: 10 },
}),
);
return worker.getEntrypoint().fetch(request);
},
};
动态代码一侧只需了解自己被授予的接口:
export default {
async fetch(_request, env) {
const profile = await env.STORE.readProfile();
return Response.json({ profile });
},
};
这样做有三个直接好处:
- 动态代码看不到
PROFILESbinding,也看不到宿主密钥。 - 宿主可以对每个方法做参数验证、配额、日志和对象级授权。
- 将来改变存储实现时,动态代码不需要知道 KV、D1 或 DO 的细节。
5. 网络是另一个 capability,不是默认便利功能
如果省略 globalOutbound,Dynamic Worker 会继承 Loader 的网络能力,通常意味着可访问公共互联网。对于用户上传或 AI 生成的代码,默认值应是:
globalOutbound: null
只有确实需要请求第三方时,才把全局出站指到宿主的网关 Service Binding。网关负责 allowlist、方法限制、请求大小、速率、审计以及在宿主侧注入密钥;动态代码只使用普通 fetch(),永远不接触 token。
export class OutboundGateway extends WorkerEntrypoint {
async fetch(request) {
const url = new URL(request.url);
// 示例只授予一个只读端点;其他服务使用各自的策略和凭据。
if (
url.origin !== "https://api.example.com" ||
url.pathname !== "/v1/profile" ||
url.search !== "" ||
url.username !== "" ||
url.password !== "" ||
request.method !== "GET"
) {
return new Response("destination not allowed", { status: 403 });
}
// 只构造所需请求头,不转发来路 Cookie 或其他敏感头。
return fetch(url.href, {
method: "GET",
headers: {
accept: "application/json",
authorization: "Bearer " + this.env.PARTNER_TOKEN,
},
redirect: "error",
});
}
}
// WorkerCode 中:
// globalOutbound: ctx.exports.OutboundGateway({ props: { tenantId } })
这个片段只演示目的地和凭据转发策略,未在真实服务部署。固定端点还必须在宿主中按 this.ctx.props.tenantId 及业务对象做授权;共用服务凭据不能替代租户级权限。本地使用虚构 token 和模拟 fetch 检查了允许请求、HTTP、非默认端口、错误路径及方法等分支。
只检查 hostname 不够:HTTP 和其他端口也可能匹配;自动重定向还可能把敏感头发往另一域名。这里使用精确 HTTPS origin 并禁止跟随重定向;需要重定向时,必须逐跳重新检查目标和凭据范围。Cloudflare 官方明确说明 redirect: “follow” 可能跨域转发 Authorization 等请求头。Workers Request
6. 模块、入口、状态与可观测性
modules可承载 JavaScript、CommonJS、Python、文本、数据和 JSON 模块;普通字符串源文件要使用正确的扩展名。- TypeScript 与 npm 依赖必须在交给 Loader 前完成转译和 bundle。不要期待生产请求在运行时安装依赖。
- Python 需要相应的
python_workerscompatibility flag,且启动特性与 JavaScript 不同,应单独压测。 limits可在WorkerCode或 entrypoint 调用处收紧;有效上限取更小值。遇到超过 CPU/subrequest 上限的异常时,按失败处理而不是无限重试。- 使用
tails将日志、异常和请求执行信息送到 Tail Worker;日志至少带 tenant ID、代码版本与请求 ID,但不要写入原始密钥或用户敏感数据。 - 动态代码若真正需要按实体持久化/串行协调,应考虑 Durable Object Facets:概念、使用场景与 Durable Objects 对比 或宿主提供的受控 DO capability,而不是依赖 isolate 内存。
7. 上线前最短检查单
-
get()的 ID 同时包含代码、依赖、权限和 compatibility 配置的版本信息。 - 不可信代码的
globalOutbound显式设为null,或明确指向审计网关。 - 动态代码得到的是小型 RPC capability,而不是原始 KV、R2、D1、DO 或 secret。
- 代码在部署前已 bundle;TypeScript、npm 依赖和 Python flag 都已验证。
- CPU、subrequest、日志、租户配额和异常处理有明确策略。
- 任何需要可靠状态的设计都已移到 DO / D1 / R2 等持久层。