基于 Flue 框架在 Cloudflare 上构建沙箱 Agent:R2 与 Git 工作区水合实战
2026/9/17 3:44:32 网站建设 项目流程

基于 Flue 框架在 Cloudflare 上构建沙箱 Agent:R2 与 Git 工作区水合实战

【免费下载链接】flueThe sandbox agent framework.项目地址: https://gitcode.com/GitHub_Trending/flue1/flue

本指南以仓库中 examples/cloudflare 示例为核心,完整讲解如何用 Flue 在 Cloudflare Workers 上运行沙箱型 Agent:通过 Workers AI Binding 免密钥调用模型、使用 Worker Loader 驱动的 cloudflare-computer 沙箱,以及如何从 R2 存储桶或 Git 仓库"水合"(hydrate)出可写的工作区并让模型调用技能(skill)。读完你将掌握'use agent'模块、useModel/useSandbox/defineTool三个核心 Hook 的实战组合,以及从本地vite dev到远端wrangler dev --remote、再到pnpm run deploy的完整运行链路。

示例概览:三个最小化 Agent 各自演示一种能力

examples/cloudflare目录下的三个 Agent 被刻意保持最小化——每个只端到端演示一项 Cloudflare 专属能力,方便直接复制模式到真实应用:

Agent 模块演示的能力
with-cloudflare-binding.ts通过 Workers AI Binding 路由模型流量,无需任何 Provider API Key
skills-from-r2.ts从 R2 存储桶水合 cloudflare-computerWorkspace,并使用发现到的技能(通过模型可调用的check_spamaction)
skills-from-git.ts通过内置的workspace.git客户端从 Git 仓库水合 cloudflare-computerWorkspace

三个 Agent 共享同一个项目自有沙箱适配器 src/sandboxes/cloudflare-computer.ts,该文件顶部标注了flue-blueprint: sandbox/cloudflare-computer@1,概念上由flue add sandbox @cloudflare/computer命令生成。

应用使用 Vite 构建:vite.config.ts 中同时装配了@flue/viteflue()和官方@cloudflare/vite-plugincloudflare(),且flue()必须放在前面——Flue 先扫描项目、准备生成的 Worker 入口和合并后的 wrangler 配置,Cloudflare 插件随后消费这些产物。Agent 模块携带'use agent'指令,路由由 src/app.ts 显式挂载。

'use agent'指令与模块注册

每个 Agent 文件的第一行都是'use agent';(见 with-cloudflare-binding.ts)。如 app.ts 顶部注释所述,注册 Agent 的是"对带'use agent'指令模块的扫描",而非路由挂载本身——因此一个仅用于分发的 Agent 甚至不需要挂载路由。

useModel:一行指定 Workers AI 模型

useModel来自@flue/runtime(实现在 packages/runtime/src/hooks/use-model.ts)。三个示例统一使用:

useModel('cloudflare/@cf/moonshotai/kimi-k2.6');

模型 ID 带cloudflare/前缀,Flue 会自动路由到 Workers AI Binding。注意 app.ts 中关于 AI Gateway 的说明:默认情况下每次cloudflare/...模型调用都会经由 Cloudflare 默认 AI Gateway(按需为你的账户创建);要自定义(指定网关、覆盖缓存、附加元数据)或完全退出,可自行注册setProvider(cloudflareBindingProvider({ binding: env.AI, gateway: ... })),用户app.ts的导入先于自动注册(ESM 提升),所以你的注册优先。

环境准备与构建

安装依赖并预热构建产物

pnpm install

全新检出时仓库的dist/目录是陈旧的,需要先构建工作区包:

pnpm run build -F @flue/runtime -F @flue/vite

由于本示例的 Agent 全部使用 Workers AI Binding,不需要任何 Provider API Key。如果切换到非 Cloudflare 模型,请在项目根目录的.env中放入对应的 Provider Key。

Worker Loader 前提(skills-from-r2 与 skills-from-git)

两个水合示例都依赖worker_loadersbinding。Worker Loader目前处于 beta 阶段,需要你的 Cloudflare 账户开通访问权限;binding 已声明在 wrangler.jsonc 中:

"worker_loaders": [{ "binding": "LOADER" }], "compatibility_flags": ["nodejs_compat", "experimental"],

experimental兼容性标志是 cloudflare-computer 沙箱的 worker-shell 后端所必需的(其 Dynamic Worker 依赖它,见 wrangler.jsonc)。

如果账户没有 Worker Loader 访问权限,可以注释掉这段配置——只有使用它的两个 Agent(skills-from-r2、skills-from-git)会在沙箱构建时抛出清晰错误,其余示例(with-cloudflare-binding)不受影响。

wrangler.jsonc 中的绑定与迁移

wrangler.jsonc 是用户自有的 wrangler 配置,Cloudflare Vite 集成读取它,并应用 Flue 的贡献(生成的 Worker 入口与每个 Agent 的 Durable Object binding,经由flueWorkerConfig()定制器),最终产出可部署的 Worker:

{ "name": "cloudflare", "compatibility_date": "2026-06-01", "ai": { "binding": "AI" }, "r2_buckets": [ { "binding": "KNOWLEDGE_BASE", "bucket_name": "flue-example-knowledge-base", "preview_bucket_name": "flue-example-knowledge-base-dev" } ] }

迁移历史由用户维护:新增一个 Agent = 新增导出的函数 + 挂载 + 为其生成的类(Flue<PascalName>Agent,由 Agent 函数名派生)添加一条迁移 tag;重命名 Agent 函数属于存储身份变更,要用 wrangler 原生的renamed_classes表达。preview_bucket_namewrangler dev --remote使用独立的 dev 桶,避免污染 prod 数据。

沙箱适配器:cloudflare-computer 的接线方式

cloudflare-computer.ts 是理解整个示例的关键文件,它展示了三个层次的接线。

workspaceHost:把 Durable Object 变成工作区宿主

Flue 的extendAPI(来自@flue/runtime/cloudflare)被用来包装 Agent 的 Durable Object 基类:

export const workspaceHost = extend({ base: (Base) => class extends Base { // 构造函数中记录 ctx/env 到 per-DO-id 的模块级 Map async __getWorkspaceStub() { // 返回 workspace.stub(),供 shell 的 env.HOST 拨回 } }, });

设计动机值得注意:每个 id 只有一个存活的 Durable Object 实例,Agent 在它内部渲染,所以按 DO id 字符串键控的模块级状态把"扩展捕获的宿主"与"沙箱工厂"连接起来。Workspace 骑在宿主条目上:它绑定到实例的存储缓存,同一 Durable Object 的新构造(驱逐、dev 重载)不能看到前一代的 Workspace。

每个使用该沙箱的 Agent 模块都需要重新导出它,让 worker-shell 后端能拨回工作区(见 skills-from-r2.ts 与 skills-from-git.ts):

export { workspaceHost as cloudflare } from '../sandboxes/cloudflare-computer';

getComputerWorkspace:一次性构造共享的持久文件系统

每个 Durable Object 一个持久文件系统,首次调用时创建并与沙箱共享。它要求传入loader: env.LOADER,并构造WorkspaceOptions

  • storage:来自host.ctx.storage(SQLite),作为WorkspaceOptions的持久层;
  • waitUntil:绑定host.ctx.waitUntil,让分离的工作区工作(模块执行、延迟同步)能活过发起它的请求;
  • git: createGitClient():启用workspace.git与 shell 内置git命令;不需要时可删除此行(连同@platformatic/vfs依赖)以把 git 排除出构建;
  • backends:默认注册WorkerShellBackend(just-bash)。这是该适配器的接线,不是包的能力上限——Workspace 可以针对同一份持久文件注册更多后端,尤其是来自@cloudflare/computer/backends/container的完整 LinuxCloudflareContainerBackend,通过workspace钩子追加,并用runtime.exec(cmd, { backend: '<id>' })按调用选择。

options.workspace钩子允许在构造前重塑默认选项:追加 R2 mount、observer、defaultGitIdentity等。

getComputerSandbox:把 Workspace 包装成通用 Sandbox

返回SandboxFactorycreateSandbox()内部确保/workspace目录存在,并用createWorkspaceSandbox(workspace, DEFAULT_CWD)把 Workspace 的文件系统与运行时暴露成 Flue 的通用Sandbox动词(exec/readFile/writeFile/stat/readdir/exists/mkdir/rm)。因为exec()可用,框架的标准工具集(bash/grep/glob/read/write/edit)直接适用,无需tools覆盖。exec还处理了 AbortSignal 的及时拒绝与SIGKILL尽力终止。

场景一:从 R2 存储桶水合工作区(skills-from-r2)

skills-from-r2.ts 演示从 R2 水合 Workspace,并让模型通过check_spam工具使用水合发现的技能。

一次性水合,放在 SandboxFactory 里

水合是"环境的一次性设置"而非每次渲染的工作,因此它住在传给useSandbox的自写SandboxFactory中——符合SandboxFactory契约的惰性要求:构造工厂对象是廉价的,昂贵的 R2 读取只发生一次,在createSandbox()里、初始化时,绝不在重渲染时发生:

useSandbox({ async createSandbox(options) { const hydrated = await workspace.fs.stat(HYDRATION_SENTINEL).then( () => true, () => false, ); if (!hydrated) { await hydrateFromBucket(workspace, KNOWLEDGE_BASE, HYDRATION_ROOT); await workspace.fs.writeFile(HYDRATION_SENTINEL, new Date().toISOString()); } return computer.createSandbox(options); }, });

sentinel机制:/.hydrated哨兵写入 Durable Object 的 SQLite,首次运行后水合即空操作(no-op)。需要强制重新水合时,可以改源码中的哨兵 key 或清空 DO 的存储。

hydrateFromBucket:流式拷贝 R2 对象

async function hydrateFromBucket(workspace, bucket, root) { let cursor; while (true) { const listing = await bucket.list({ cursor }); for (const obj of listing.objects) { if (obj.key === '' || obj.key.endsWith('/')) continue; const body = await bucket.get(obj.key); if (!body) continue; await workspace.fs.writeFile(`${root}/${obj.key}`, body.body); } if (!listing.truncated) break; if (!listing.cursor) throw new Error('...'); cursor = listing.cursor; } }

R2 的 body 直接流入持久文件系统,无整文件缓冲;用游标分页处理截断的 listing。注释还点出替代方案:只读且不变的数据可用 R2 mount(WorkspaceOptions.mounts),而拷贝方式保持整棵树可写。

check_spam:带子 harness 的模型可调用工具

技能调用位于模型可调用的check_spam工具中。defineTool声明结构化输入输出(valibot schema),harness: true让它的 run 拥有一个子 harness,其 scratch 会话发现同一批水合技能:

const checkSpam = defineTool({ name: 'check_spam', description: 'Classify a message as spam or not using the spam-filter skill...', input: v.object({ message: v.string() }), output: v.object({ spam: v.boolean(), confidence: v.picklist(['low', 'medium', 'high']), reasoning: v.string(), }), harness: true, async run({ harness, data }) { const result = await harness.prompt( `Use the spam-filter skill to classify the following message:\n\n${data.message}`, { result: v.object({ spam: v.boolean(), confidence: v.picklist([...]), reasoning: v.string() }) }, ); return { output: result.data }; }, });

Agent 主体用useTool(checkSpam)注册,系统提示词指示模型:被问及消息是否垃圾邮件时调用该工具并报告判定。

场景二:从 Git 仓库水合工作区(skills-from-git)

skills-from-git.ts 演示通过 Workspace 内置 git 客户端克隆仓库,再让模型用标准 shell 与文件工具探索:

const TARGET_REPO = 'https://github.com/FredKSchott/vinext-starter'; const CLONE_DIR = '/workspace/repo'; useSandbox( { async createSandbox(options) { const hydrated = await workspace.fs.stat(HYDRATION_SENTINEL).then(() => true, () => false); if (!hydrated) { await workspace.git.clone({ url: TARGET_REPO, dir: CLONE_DIR }); await workspace.fs.writeFile(HYDRATION_SENTINEL, new Date().toISOString()); } return computer.createSandbox(options); }, }, { cwd: CLONE_DIR }, );

与 R2 版本同样的惰性水合模式与哨兵检查,区别在于useSandbox的第二个参数{ cwd: CLONE_DIR }把沙箱默认工作目录指向克隆目录。系统提示词要求模型"被问及仓库时务必实际用 shell 与文件工具检查文件,绝不凭假设作答"。

路由挂载与 HTTP 接口

app.ts 用 Hono 构造应用,默认导出拥有整个请求管线:

const app = new Hono(); app.get('/api/ping', (c) => c.json({ pong: true, at: new Date().toISOString() })); app.route('/agents/with-cloudflare-binding', createAgentRouter(WithCloudflareBinding)); app.route('/agents/skills-from-git', createAgentRouter(SkillsFromGit)); app.route('/agents/skills-from-r2', createAgentRouter(SkillsFromR2)); export default app;

createAgentRouter(Fn)是纯路由工厂(实现于 packages/runtime/src/routing.ts)。/api/ping这类自定义路由运行在 worker isolate 中、不在 Agent 的 Durable Object 内,适合存活探针、状态页等不需要 Agent 状态/流式的端点。相对挂载点,每个 Agent 的 HTTP 面为:

  • POST /:id——发送 prompt(202 受理)
  • GET | HEAD /:id——读取会话流
  • POST /:id/abort——中止进行中的工作

挂载路径由你决定;Agent 函数名(其持久身份)才是会话与 Durable Object 类的键。同构的app.ts在 Node 与 Cloudflare 目标上都能工作,flue()内部适配;Cloudflare 上每个挂载的 Agent 路由解析生成的 binding,经 Agents SDK 转发到对应 Durable Object,其余部分就是普通 Hono 应用。

此外 cloudflare.ts 导出WorkspaceServiceProxy——它必须是入口模块的导出:cloudflare-computer 沙箱的 shell 后端为它铸造 loopback binding(ctx.exports.WorkspaceServiceProxy),而 Cloudflare 只为 Worker 入口导出的类铸造 loopback。Flue 会把该文件的所有导出从生成的 Worker 入口重新导出。

本地开发的注意事项

vite dev在本地 workerd 中运行 worker,可以暴露本地的worker_loadersbinding;但Wrangler 的本地 R2 CLI 存储可能对运行中的 dev server 的 R2 binding 不可见。要做端到端的 R2 水合冒烟测试,请使用远端资源,两种选择:

  • wrangler dev --remote——让 worker 使用你的 dev 桶运行在 Cloudflare 边缘,需要账户开通 Worker Loader 访问权限。先执行pnpm run buildwrangler dev/deploy通过 Cloudflare Vite 插件写入的 deploy redirect 读取构建产物)。
  • 部署到 preview 环境——pnpm run deploy,之后通过 HTTP 练习 Agent。

还要注意沙箱的执行能力边界:cloudflare-computer 沙箱在 just-bash(一个 JavaScript shell)中、在持久 Workspace 上运行命令——coreutils 与grep/find可用;原生二进制与npm不可用。如果你的账户没有 Worker Loader 访问权限,或需要 Linux 工具链或可写桶挂载,改用@cloudflare/sandbox(Containers +mountBucket)。

给 R2 播种技能(仅 skills-from-r2)

运行skills-from-r2之前,需要把 SKILL.md 放进 dev R2 桶,让水合步骤有东西可拷贝:

# 在本目录下执行;需要已安装并认证 wrangler ./seed-r2.sh

seed-r2.sh 会把.agents/skills/spam-filter/SKILL.md写入flue-example-knowledge-base-dev。环境变量控制目标:

BUCKET=prod ./seed-r2.sh # 播种 prod 桶 REMOTE=0 ./seed-r2.sh # 播种 wrangler 本地 R2 存储

脚本内容即技能本体:一个带 frontmatter(name: spam-filterdescription: Classify a message as spam or not spam...)的 SKILL.md,描述了结构化判定协议(spam/confidence/reasoning)与强垃圾信号启发式(紧迫感、免费奖品、可疑短链接、敏感信息索求)。若想换桶名,需要同步修改 wrangler.jsonc 与seed-r2.sh中的BUCKET_NAME表。

运行与触发 Agent

# Dev server(经 Cloudflare Vite 插件的本地 workerd) pnpm run dev # 生产构建(dist/ 下的可部署 Worker 输出) pnpm run build # 触发 Agent(挂载点位于 src/app.ts;端口由 vite dev 打印)。 # prompt 是 fire-and-forget(202 受理);用同一 URL 的 GET 读取会话流中的回复。 curl -X POST 'http://localhost:5173/agents/with-cloudflare-binding/test-1' \ -H 'Content-Type: application/json' -d '{"kind": "user", "body": "Say hello."}' curl 'http://localhost:5173/agents/with-cloudflare-binding/test-1' curl -X POST 'http://localhost:5173/agents/skills-from-r2/test-1' \ -H 'Content-Type: application/json' \ -d '{"kind": "user", "body": "Check this message for spam: CONGRATS! You won a free iPhone: http://bit.ly/xyz"}' curl -X POST 'http://localhost:5173/agents/skills-from-git/test-1' \ -H 'Content-Type: application/json' \ -d '{"kind": "user", "body": "List every top-level file and directory in the repo, then describe the project."}'

脚本命令对应 package.json 中的dev: vite devbuild: vite builddeploy: vite build && wrangler deploy

skills-from-r2skills-from-git首次运行时会在 Durable Object 的 SQLite 中写入/.hydrated哨兵;第二次运行的水合在哨兵检查处即为空操作。

演进说明:从 Workflow 到 Agent

示例末尾的注释记录了一次重要演进:以前的skills-from-*workflow 现在是 Agent。Workflow 的run体变成了你发送的消息(skills-from-git),或一个模型可调用 action(skills-from-r2 的check_spam);会话(conversation)是唯一的持久单元。这意味着本示例展示的"一次水合 + 会话内多轮复用"模式,正是 Flue 当前推荐的、以会话为唯一持久单位的 Agent 形态。

【免费下载链接】flueThe sandbox agent framework.项目地址: https://gitcode.com/GitHub_Trending/flue1/flue

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询