尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Cloudflare Workspace 实战指南:用 SQLite + R2 为 AI Agent 构建持久化虚拟文件系统

Cloudflare Workspace 实战指南:用 SQLite + R2 为 AI Agent 构建持久化虚拟文件系统 Cloudflare Workspace 实战指南用 SQLite R2 为 AI Agent 构建持久化虚拟文件系统【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agentscloudflare/shell包中的Workspace为 Cloudflare Workers 上的 AI Agent 提供了一套以 SQLite 为主、可选 R2 大文件存储为辅的持久化虚拟文件系统。它可以在任何具备 SQLite 存储的 Durable Object、D1 数据库或自定义 SQL 后端之上运行并可作为state.*沙箱工具和 Agent 的持久化状态层。读完本文你将掌握 Workspace 的完整 API、存储后端接入方式、R2 冷热分层机制、命名空间隔离、变更事件与可观测性接入以及如何与cloudflare/codemode配合为 Agent 提供可被 LLM 直接调用的文件系统能力。实验性特性Workspace 当前标记为 Experimental后续版本可能出现破坏性变更升级时请关注 CHANGELOG。快速开始给 Agent 一个可持久化的文件系统安装包后即可在 Agent 中直接使用 Workspacenpm install cloudflare/shell最小使用方式是在 Agent 类字段中初始化一个 Workspace并把 SQLite 存储this.ctx.storage.sql作为数据后端import { Agent } from agents; import { Workspace } from cloudflare/shell; class MyAgent extends AgentEnv { workspace new Workspace({ sql: this.ctx.storage.sql, name: () this.name }); async onMessage(conn, msg) { await this.workspace.writeFile(/hello.txt, world); const content await this.workspace.readFile(/hello.txt); conn.send(content); // world } }writeFile/readFile会自动完成表的惰性初始化ensureInit与父目录创建。从源码看首次调用时会执行CREATE TABLE IF NOT EXISTS cf_workspace_namespace并插入根目录/记录之后所有操作都复用这一张表见 filesystem.ts。也就是说文件系统只是普通 SQLite 表中的行path是主键content直接作为列存储inline 模式因此天然获得 Durable Object 的持久化与容错能力。SQL 后端三种接入方式与自动检测Workspace 通过sql选项接收任意 SQL 数据源构造函数会自动检测你传入的类型见 filesystem.ts 的isSqlStorage/isD1Database判断后端类型写法检测依据Durable Object SQLiteSqlStoragenew Workspace({ sql: ctx.storage.sql })对象上存在databaseSize属性D1 数据库D1Databasenew Workspace({ sql: env.MY_DB })对象上存在prepare与batch方法自定义后端SqlBackend实现两个方法手动接入否则视为原始SqlBackend直接使用Durable Object SQLiteSqlStorage任何启用 SQLite 存储的 Durable Object——不限于 Agent——都可以直接使用// Inside any Durable Object const workspace new Workspace({ sql: ctx.storage.sql });D1接入 D1 数据库绑定即可把 D1 当作文件系统元数据与内联内容的存储// Using a D1 database binding const workspace new Workspace({ sql: env.MY_DB });自定义后端实现SqlBackend接口可以接入任何其他 SQL 源如 Postgres 代理、内存 SQL 引擎等import type { SqlBackend } from cloudflare/shell; const backend: SqlBackend { query(sql, ...params) { // Return rows as an array of objects return myDb.execute(sql, params); }, run(sql, ...params) { // Execute without returning rows myDb.execute(sql, params); } }; const workspace new Workspace({ sql: backend });query和run既可以同步返回也可以返回 Promise——Workspace 内部统一await处理。SqlParam的类型为string | number | boolean | null所有语句都使用参数化查询杜绝 SQL 注入。构造选项全解所有选项通过单个对象传入new Workspace(options)完整定义见 filesystem.ts 的WorkspaceOptions接口OptionTypeDefaultDescriptionsqlSqlStorage \| D1Database \| SqlBackendrequiredSQL backend for file metadata and inline contentnamespacestringdefaultTable namespace for isolationr2R2BucketnullR2 bucket for large filesr2PrefixstringnameKey prefix for R2 objectsinlineThresholdnumber1_500_000Byte size above which files spill to R2namestring \| () string \| undefinedundefinedName for R2 prefix fallback and observabilityonChange(event: WorkspaceChangeEvent) voidundefinedCallback fired on create, update, and deleteLazy name resolution惰性名称解析在 Durable Object 中this.name在类字段初始化阶段可能无法解析例如对象通过原始 id 而非名称寻址时读取它会抛错。这时应传入函数以延迟求值class MyAgent extends AgentEnv { workspace new Workspace({ sql: this.ctx.storage.sql, name: () this.name // evaluated when needed, not at construction }); }从源码看name有双重用途未提供r2Prefix时作为 R2 对象键前缀的回退值resolveR2Prefix见 filesystem.ts以及作为agents:workspace可观测事件中的name字段。同源同命名空间的重复构造校验这是一个值得注意的源码级细节Workspace 内部通过WeakMapSqlSource, Mapnamespace, config注册表记录每个{sql, namespace}组合的存储配置。同一数据源上允许构造第二个实例Vite HMR、辅助复用等场景但r2、r2Prefix、inlineThreshold必须与首次构造完全一致否则会抛错——因为大文件会按不同 R2 key 或不同大小阈值路由导致一个实例读不到另一个实例写的数据见 filesystem.ts。对应测试覆盖在 workspace.test.ts。onChange有意不参与校验因为它是按实例注册的监听器。文件操作读写、二进制、流式、追加与删除读与写await workspace.writeFile( /config.json, {debug: true}, application/json ); const content await workspace.readFile(/config.json); // string | nullreadFile对不存在的文件返回null如果路径是目录则抛出EISDIR。文本写入默认 MIME 为text/plainwriteFileBytes默认 MIME 为application/octet-stream。写入时若目标路径已存在会走ON CONFLICT(path) DO UPDATE完成覆盖更新若目标已以 R2 模式存储也会同步删除旧的 R2 对象见 filesystem.ts。二进制文件await workspace.writeFileBytes(/image.png, pngBytes, image/png); const bytes await workspace.readFileBytes(/image.png); // Uint8Array | nullwriteFileBytes接受Uint8Array或ArrayBuffer。内联存储的二进制内容统一以 base64 编码保存在content列中content_encoding base64。流式传输const stream await workspace.readFileStream(/large.bin); await workspace.writeFileStream(/upload.bin, requestBody);writeFileStream会先收集全部 chunk再依据阈值决定走 inline 还是 R2 存储。最大流大小为100 MB常量MAX_STREAM_SIZE 100 * 1024 * 1024见 filesystem.ts超限抛出EFBIG。R2 存储的文件读取流直接透传R2Object.bodyinline 文件则一次性入队一个 ReadableStream。追加写await workspace.appendFile(/log.txt, new line\n);对于内联 UTF-8 文件这是一个高效的 SQLUPDATE content content || ?操作同时用size size ?更新大小见 filesystem.ts。对 R2 存储的文件则退化为读取 → 拼接 → 重写。删除const deleted await workspace.deleteFile(/old.txt); // true | false缺失文件返回false目录抛出EISDIR应改用rm()。R2 存储的文件会先删除对应对象再删 SQLite 行见 filesystem.ts。目录操作创建、列举、glob、删除、复制与移动await workspace.mkdir(/src/components, { recursive: true }); const entries await workspace.readDir(/src); // FileInfo[] // Each entry: { path, name, type, mimeType, size, createdAt, updatedAt } const matches await workspace.glob(/src/**/*.ts); // FileInfo[]readDir默认返回前 1000 条按type ASC, name ASC排序并支持{ limit, offset }分页参数。glob支持*、**、?、[...]、{a,b}通配符实现上先按 path 主键做范围扫描预过滤而非 LIKE规避 D1 对复杂 LIKE/GLOB 模式的拒绝再用正则精确匹配见 filesystem.ts 与注释中的 issue #1539。删除目录await workspace.rm(/src, { recursive: true }); await workspace.rm(/maybe-missing, { force: true }); // no error if absent递归删除目录时会先用范围扫描[dirPath/, dirPath0)找出所有 R2 文件并批量r2.delete(keys)再删除 SQLite 行。非空目录不带recursive会抛ENOTEMPTY根目录/禁止删除EPERM。复制与移动await workspace.cp(/src, /backup, { recursive: true }); await workspace.mv(/old.txt, /new.txt);cp对符号链接按符号链接复制对目录需要recursivemv对目录默认递归先 cp 后 rm对文件会更新 SQLite 行并同步迁移 R2 对象拷贝新 key、删除旧 key。目标已存在时目录抛EISDIR文件则先删除再移动。状态查询与存在性判断const stat await workspace.stat(/file.txt); // FileStat | null // { path, name, type, mimeType, size, createdAt, updatedAt } const exists await workspace.exists(/file.txt); // true for files and dirs const isFile await workspace.fileExists(/file.txt); // true only for filesstat会跟随符号链接先resolveSymlink再查询lstat返回符号链接条目本身。FileStat与FileInfo同构createdAt/updatedAt在 SQLite 中以 Unix 秒存储返回时换算为毫秒。符号链接await workspace.symlink(/real.txt, /link.txt); const target await workspace.readlink(/link.txt); // /real.txt const stat await workspace.lstat(/link.txt); // type: symlink读写经过符号链接会自动沿目标链解析最多 40 层超出抛ELOOP。绝对与相对目标都支持相对目标基于链接所在目录解析。创建符号链接时目标非空且不超过 4096 字符ENAMETOOLONG已存在路径抛EEXIST。相关测试覆盖链式解析与悬空链接场景见 workspace.test.ts。Diff生成 unified diffconst diff await workspace.diff(/a.txt, /b.txt); // unified diff string const diff2 await workspace.diffContent(/file.txt, newContent); // compare against string两份内容完全相同时返回空字符串。文件超过10,000 行会被拒绝EFBIG。实现内置了 Myers diff 算法并输出标准 unified 格式3 行上下文见 filesystem.ts不依赖外部 diff 库。测试见 workspace.test.ts。获取工作区统计信息const info await workspace.getWorkspaceInfo(); // { fileCount, directoryCount, totalBytes, r2FileCount }实现上通过一次聚合 SQL 查询统计文件数、目录数、文件总字节数与 R2 存储文件数见 filesystem.ts。命名空间隔离多个工作区共存于同一数据源同一个 SQL 数据源上可以创建多个 Workspace 实例各实例使用不同的namespace对应独立的表cf_workspace_namespace另有索引cf_workspace_namespace_parentconst code new Workspace({ sql: ctx.storage.sql, namespace: code }); const data new Workspace({ sql: ctx.storage.sql, namespace: data });命名空间名称必须以字母开头且只包含字母、数字或下划线正则^[a-zA-Z][a-zA-Z0-9_]*$。表名由命名空间派生因此该正则校验也是防 SQL 注入的第一道关卡。R2 大文件存储冷热分层设计低于内联阈值默认 1.5 MB的文件直接存进 SQLite更大的文件元数据留在 SQLite、内容放入 R2const workspace new Workspace({ sql: this.ctx.storage.sql, r2: this.env.WORKSPACE_FILES, name: () this.name, inlineThreshold: 2_000_000 // 2 MB });R2 对象键遵循{name}/{namespace}{path}模式如果未提供r2Prefix则以name作为前缀。若某文件超过阈值但没有配置 R2 bucket则降级为 inline 存储并打印 console 警告——这是被允许的兜底行为但超大文件可能触及 SQLite 行大小限制源码中的警告信息明确提示了这一点见 filesystem.ts。阈值判断的依据是size this.threshold字节数。文本与二进制写入走同一套分流逻辑先写 R2、再写 SQLite 元数据若 SQL 步骤失败会回滚删除刚写入的 R2 对象防孤儿对象从 R2 模式更新为 inline 模式时也会先删除旧 R2 对象。变更事件实时感知文件系统变化传入onChange回调即可实时响应文件变更const workspace new Workspace({ sql: this.ctx.storage.sql, onChange: (event) { // event: { type: create | update | delete, path, entryType } this.broadcast(JSON.stringify(event)); } });事件类型为WorkspaceChangeEvent { type: WorkspaceChangeType; path: string; entryType: EntryType }entryType取值file | directory | symlink。writeFile、deleteFile、mkdir、rm、cp、mv、symlink都会触发相应事件mv会依次发出源路径的 delete 与目标路径的 create。可观测性零开销的 diagnostics_channel 事件Workspace 通过node:diagnostics_channel向agents:workspace通道发布结构化事件覆盖读、写、删除、mkdir、rm、cp、mv 操作。每个事件包含 workspace 名称、命名空间与操作相关载荷import { subscribe } from node:diagnostics_channel; subscribe(agents:workspace, (message) { console.log(message); // { type: workspace:write, name: my-agent, payload: { path, size, storage, namespace }, timestamp } });事件载荷示例写入事件包含{ path, size, storage: inline | r2, update, namespace }mkdir 包含{ path, recursive, namespace }cp 包含{ src, dest, recursive, namespace }。实现位于 filesystem.ts 的_observe方法通过channel(agents:workspace)发布。该通道仅在存在订阅者时才活跃无订阅时零开销。与 codemode 集成把文件系统暴露为沙箱state.*工具Workspace 与cloudflare/codemode集成后可以让沙箱内运行的代码通过state对象访问文件系统。使用cloudflare/shell/workers提供的stateTools()import { Workspace } from cloudflare/shell; import { stateTools } from cloudflare/shell/workers; import { DynamicWorkerExecutor, resolveProvider } from cloudflare/codemode; class MyAgent extends AgentEnv { workspace new Workspace({ sql: this.ctx.storage.sql, name: () this.name }); async run(code: string) { const executor new DynamicWorkerExecutor({ loader: this.env.LOADER }); return executor.execute(code, [ resolveProvider(stateTools(this.workspace)) ]); } }在沙箱内部state对象暴露了完整的文件操作、搜索替换、JSON 辅助、归档工具等能力。从实现看stateTools(workspace)会依次经过createWorkspaceStateBackend(workspace)把 Workspace 包装成 codemode 所需的FileSystemStateBackend见 workspace.ts与createStateToolProvider为每个状态方法生成{ description, execute }工具并统一名为state见 workers.ts。工具调用兼容对象参数state.readFile({ path })与位置参数state.readFile(path)两种形式。底层通过WorkspaceFileSystem适配器把 Workspace 语义映射为标准FileSystem语义缺失文件返回null被转换为抛ENOENTNode fs 消费者预期stat/lstat统一为{ type, size, mtime }见 workspace.ts。WorkspaceFsLike接口是cloudflare/shell与cloudflare/codemode、cloudflare/think之间的衔接层——跨 DO 的 RPC 代理只需满足该形状即可复用整套工具链。cloudflare/shell还额外提供git.*工具gitTools(workspace)来自cloudflare/shell/git与纯 JS 的 git 实现createGit两者都基于虚拟文件系统相关用法见 packages/shell/README.md。codemode 沙箱运行时本身的文档位于 packages/codemode。类型导出以下类型均从cloudflare/shell导出见 index.tsimport type { SqlBackend, SqlSource, SqlParam, WorkspaceOptions, EntryType, // file | directory | symlink FileInfo, // { path, name, type, mimeType, size, createdAt, updatedAt, target? } FileStat, // same as FileInfo WorkspaceChangeEvent, // { type, path, entryType } WorkspaceChangeType // create | update | delete } from cloudflare/shell;路径处理规则路径会被规范化缺失的前导/自动补全.与..段被解析重复斜杠被折叠最大路径长度为 4,096 字符超出抛ENAMETOOLONGwriteFile和writeFileBytes自动创建父目录内部走ensureParentDir从最深缺失目录逐层INSERT OR IGNORE创建安全设计要点路径遍历防护..段在规范化阶段即被解析/etc/passwd这类逃逸尝试会被折叠回根目录内的路径杜绝目录逃逸SQL 注入防护表名由命名空间派生命名空间经^[a-zA-Z][a-zA-Z0-9_]*$校验所有查询参数一律使用参数化查询符号链接环解析深度封顶 40 层循环时抛ELOOP流大小限制writeFileStream拒绝超过 100 MB 的流抛EFBIGDiff 大小限制diff与diffContent拒绝超过 10,000 行的文件目录递归深度mkdir递归深度上限 100 层防止恶意深链源码常量MAX_MKDIR_DEPTH 100对应的行为在测试套件中有系统验证包括 EISDIR/ENOENT/EFBIG 等错误码断言见 workspace.test.ts。官方完整文档位于 docs/shell/index.md沙箱运行时的state.*工具说明见 packages/codemode 的文档Agent 使用 Workspace 作为状态层的说明见 docs/agents/state.md 与 docs/agents/durable-execution.md。【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表