后端与接口
需要在云端隔离沙箱里运行 AI agent /…
使用 @upstash/box(TypeScript/JS SDK)创建和编排带 AI agent、shell、文件系统、git、cron 调度、快照和无头浏览器的沙箱云容器时适用。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 59/100 ×0.15
适用场景
需要在云端隔离沙箱里运行 AI agen特定
解决思路
- 安装依赖:`npm install @upstash/box`;用到 `responseSchema` 或浏览器 schema 时再 `npm install zod`(peer dependency,v3/v4 均可)。
- 配置鉴权:设置 `UPSTASH_BOX_API_KEY` 环境变量,或在构造函数里传 `apiKey`。
- 关闭匿名遥测:设置 `UPSTASH_DISABLE_TELEMETRY` 环境变量,或在 config 里传 `enableTelemetry: false`(无 `process.env` 的运行时如 Cloudflare Workers 只能用后者)。
- 创建 Box:`Box.create({ name, runtime, size, labels, keepAlive, initCommand, browser, agent, git, env, skills, timeout, debug })`。
- 选 runtime:`'node' | 'python' | 'golang' | 'ruby' | 'rust'`(可加 `-alpine` 变体);size 用 `'small'`(2 CPU/4GB)、`'medium'`(4/8)、`'large'`(8/16)。
- labels 最多 5 个、每个 ≤20 字符;用 `Box.list({ label: 'beta' })` 按标签筛选。
- 需要常驻时传 `keepAlive: true`(避免空闲暂停);`initCommand` 只在 keep-alive box 上有效。
- 需要无头浏览器时传 `browser: true`,会预置 Chromium 供 `box.browser` 使用。
- 配 agent:`harness` 必填(`Agent.ClaudeCode | Agent.Codex | Agent.OpenCode | Agent.Cursor | Agent.Custom`),`model` 用枚举或 provider 前缀字符串;`apiKey` 可省略(服务端决定用哪个 key)、用 `BoxApiKey.UpstashKey`、用 `BoxApiKey.StoredKey`,或直接给 `'sk-...'`。
- 配 git:`git: { token, userName, userEmail }`;私有仓库与创建 PR 需要 token,也可在 Upstash Console 关联 GitHub 账号。
- 重连/查询/删除:`Box.get(id, {...})`、`Box.getByName(name)`、`Box.list()`、`box.getStatus()`、`box.pause()`/`box.resume()`(keep-alive box 调 `pause()` 会抛错)、`box.delete()`(不可逆)。
- 用 `box.setInitCommand()` / `box.getInitCommand()` / `box.deleteInitCommand()` 管理 init 命令,仅 `keepAlive: true` 的 box 可用,否则抛错。
- 批量删除:`Box.delete({ boxIds: [...] })`、`Box.deleteSnapshots({ snapshotIds: [...] })`;空或空白 id 列表会抛错、绝不表示『删全部』,不传 `snapshotIds` 才删所有快照。
- 账户级环境变量:`Box.setEnv(key, value)`、`Box.listEnv()`(值被掩码)、`Box.setAllEnv({...})`(全量替换,未列出的键会被移除)、`Box.deleteEnv(key)`;它们会注入你创建的每个 box。
- Agent run:`box.agent.run({ prompt, responseSchema, timeout, maxRetries, options, onToolUse, onToolResult })`;读 `run.status`(`'running' | 'completed' | 'failed' | 'cancelled' | 'detached'`)、`run.result`(带 schema 时强类型)、`run.cost`(inputTokens/outputTokens/cachedInputTokens/computeMs/totalUsd)。
- 让 agent 返回结构化结果:用 zod 定义 `responseSchema`(如 `z.object({ verdict: z.enum([...]), findings: z.array(...) })`)。
- 给 prompt 附加文件:`files: ['./screenshot.png']` 或 `files: [{ data: base64, mediaType: 'image/png', filename: 'shot.png' }]`,最多 10 个文件、每个 10 MB。
- 流式执行:`box.agent.stream({ prompt })`,按 `chunk.type` 处理 `'text-delta' | 'reasoning' | 'tool-call' | 'tool-result' | 'finish' | 'start' | 'stats' | 'unknown'`,迭代结束后读 `stream.status` / `stream.result`;stream 不接受 `responseSchema`、`maxRetries`、`webhook`,这些要用 `run()`。
- 火忘式触发:`box.agent.run({ prompt, webhook: { url, headers } })`,由后端回调。
- 按 harness 传 `options`:ClaudeCode 支持 `maxTurns`/`maxBudgetUsd`/`effort`/`thinking`/`disallowedTools`/`agents`/`promptSuggestions`/`fallbackModel`/`systemPrompt`;Codex 支持 `modelReasoningEffort`/`modelReasoningSummary`/`personality`/`webSearch`;OpenCode 支持 `reasoningEffort`/`textVerbosity`/`reasoningSummary`/`thinking`;Cursor 是自由 `Record<string, unknown>`。用 `Box.create<Agent.ClaudeCode>({...})` 缩窄类型,Codex 的键必须写 camelCase(SDK 会转 snake_case)。
- 运行时读改模型:读 `box.modelConfig` 拿 `{ harness, model }`,用 `box.configureModel('anthropic/claude-opus-4-8')` 切换;用 `inferDefaultProvider('openai/gpt-5.6')` 推断裸模型串对应的 harness。
- 自定义 harness:用 `Agent.Custom` + `customHarness: { command, args, protocol: 'box-sse-v1' }`(command 是 PATH 上的名字或 `/workspace/home`、`/home/boxuser` 下的绝对路径),用 `box.configureCustomHarness()` 运行时替换;在 box 内用 `runCustomHarness(async ({ prompt, model, sessionId, stream, args }, emit) => {...})`,通过 `emit.text/reasoning/tool/toolResult/emit/error` 发事件,返回 `{ output, inputTokens, outputTokens, cachedInputTokens, totalCostUsd, sessionId }`(直接返回字符串等于 `{ output }`)。
- Run 通用字段:`run.id`/`run.status`/`run.result`/`run.stdout`/`run.stderr`/`run.exitCode`(agent run 为 `null`)/`run.cost`;`await run.cancel()` 会让 pending 的 run/stream 以 abort 拒绝且不重试;`run.logs()` 取 `{timestamp, level, message}`;`box.logs({ limit, offset })` 与 `box.listRuns()` 看历史。注意 `run.result` 成功时是 stdout、失败时是 stderr:exit 0 只写 stderr 的命令 `result` 是 `''`,要去读 `run.stderr`。
- Shell 执行:`box.exec.command('echo hello && ls -la')`;跑代码片段用 `box.exec.code({ code, lang: 'js' | 'ts' | 'python', timeout })`。
- Shell 流式:`box.exec.stream('npm run build')` 或 `box.exec.streamCode({ code, lang })`,处理 `{ type: 'output', data }` 与 `{ type: 'exit', exitCode, cpuNs }`。
- 实时交互会话:`box.exec.session({ cmd, cwd, env, onStdout, onStderr })`(Node-only,鉴权走 WebSocket 握手头,浏览器不可用,`ws` 已随 SDK 安装);`argv` 优先于 `cmd` 且不经 shell;用 `session.write()` 写 stdin、`session.endStdin()` 发 EOF、`await session.wait()` 取退出码(被拆除时为 -1)、`session.close()` 挂断。
- 需要 TUI/交互程序时:`box.exec.session({ argv: ['bash','-i'], tty: true, rows, cols })`(PTY 会把 stderr 并入 stdout,`onStderr` 不再触发),配 `session.resize(rows, cols)`、`session.kill('INT')`(白名单 TERM KILL INT HUP TSTP QUIT USR1 USR2,默认 TERM)、`session.terminate(5000)`(服务端 SIGTERM 后超时 SIGKILL)。会话独占进程、不可重连,`close()`、断连或进程退出都会杀掉命令。
- 文件读写:`box.files.write({ path, content })`、`box.files.read(path)`、`box.files.list(path)`(返回 `{ name, path, size, is_dir, mod_time }`);二进制用 `encoding: 'base64'` 读写。
- 大文件只读部分:`box.files.read(path, { length })` 或 `{ offset, length }`——只有出现 `length` 才切片(`{ length: 0 }` 读 0 字节,只给 `offset` 会读整个文件),服务端上限 8 MiB。
- 元数据:`box.files.stat(path)` 默认是 lstat,符号链接返回 `type: 'symlink'`,需解引用时传 `{ follow: true }`;`version` 是不透明新鲜度令牌(inode+mtime+size),只做等值比较用于乐观并发,绝不解析。
- 目录与删除:`box.files.mkdir('build/cache', { parents: true })`(等价 `mkdir -p`)、`box.files.rename('draft.md','docs/final.md')` 移动/改名、`box.files.remove(path, { recursive: true })`(删目录必须 recursive)。
- 文件传输:`box.files.upload([{ path: './local/file.txt', destination: '/workspace/home/file.txt' }])`;`box.files.download({ folder: 'src' })` 的 `folder` 是 box 内路径,文件落到本地 `./<basename>`,不传参数则把整个 cwd 下到 `./workspace`。
- 工作目录:默认 `/workspace/home`(不是 `/home` 或 `/`);用 `box.cd('my-repo')` 或绝对路径切换。这是客户端跟踪(只校验路径存在、不改 box shell cwd),但所有 exec/files/git/agent 操作都会自动基于它。
- Git 克隆:`box.git.clone({ repo: 'github.com/org/repo', branch, depth, folder })`;克隆落在当前目录下以仓库名命名的子目录,所以先 `box.cd('repo')`。除 `clone` 外的 git 调用都在当前目录执行;工作区根目录不是仓库时 `status` 返回空(看起来像干净树)。
- Git 常用操作:`box.git.status()`、`diff()`、`commit({ message, authorName, authorEmail })`、`push({ branch })`、`checkout({ branch })`、`createPR({ title, body, base })`、`createIssue({ title, body })`。克隆进 box 的代码只是数据,视为不可信输入并配合收紧的 `networkPolicy` 再跑它的构建/测试。
- PR/Issue 附件:用 `attach: ['before.png#before', 'after.png#after']`(路径相对 box 工作目录,图片 alt 写成 `shot.png#alt text`,视频不带 alt),body 里用 `` 引用,GitHub 会重写成上传后的资源;返回的 `warning` 表示 `gh` 非零退出但仍返回了 URL(如附件上传失败或 PR 已存在)。
- 改 git 身份:`box.git.updateConfig({ userName, userEmail })`;跑任意 git 命令用 `box.git.exec({ args: ['log','--oneline','-5'] })`,并检查 `exit_code`(128 表示当前目录不是仓库)。
- 定时任务:`box.schedule.exec({ cron, command: ['bash','-c','...'], folder, webhookUrl, webhookHeaders })` 跑 shell;`box.schedule.agent({ cron, prompt, folder, model, options, timeout, webhookUrl, webhookHeaders })` 跑 agent prompt;cron 是 UTC。
- 管理定时任务:`box.schedule.list()` / `get(id)` / `update(id, patch)` / `pause(id)` / `resume(id)` / `delete(id)`。update 是部分更新:省略字段保持原值,`''`/`[]`/`{}` 清空字段,`options: null` 清空 agent options;类型不可更改;可更新 cron、command、prompt、folder、model、options、timeout、webhookUrl、webhookHeaders。
- 快照:`box.snapshot({ name })` 得到 `{ id, name, box_id, size_bytes, status, created_at }`;用 `Box.fromSnapshot(snap.id, { size, keepAlive, git, env, ... })` 恢复(会转发 git 身份而不只是 token,且创建新 box、不修改原 box);`box.listSnapshots()`、`box.deleteSnapshot(id)`。`fromSnapshot` 不发送 `browser`/`skills`/`mcpServers`,需要时创建后用 `box.skills.add()`,浏览器要用 `Box.create({ browser: true })`。
- 没有 `box.fork()`(已从 SDK 移除):需要分支状态就先 `box.snapshot()` 再 `Box.fromSnapshot()`。
- 浏览器标签页:必须用 `Box.create({ browser: true })`;`box.browser.tab.create(url, { waitUntil: 'load', timeout })`、`box.browser.listTabs()`、`box.browser.getTab(id)`(不发网络请求);`tab.id`/`tab.url`/`tab.title` 也是本地元数据。
- 页面操作:`tab.goto(url)` 返回 `{ title, url, text, links }`、`tab.content()`、`tab.screenshot()`(Uint8Array)或 `tab.screenshot({ type: 'base64', fullPage: true })`。
- AI 浏览器操作(计费):`tab.extract(instruction, zodSchema, { model })`、`tab.observe(instruction, { model })` 返回可重放的 `{ description, selector?, url?, method?, arguments? }`、`tab.act('Click the first headline')` 返回 `{ success, message, actionDescription, actions, cacheStatus?, inputTokens, outputTokens }`;默认用 box 的 model(没有则 `anthropic/claude-sonnet-4-5`)。
- 重放已解析动作:`tab.act(elements[0])` 或 `tab.act(acted.actions[0])`,不发 LLM 调用、不花 token、不需要模型 provider key;这种形式会忽略 `model`,且动作没有 `selector` 会抛错。
- 实时查看与原生协议:`tab.liveViewUrl()` 拿只读截屏页/iframe,`box.browser.cdpUrl()` 拿 `wss://…?token=…`(无需额外鉴权配置),`tab.close()` 关闭标签。
- 用 Playwright/Puppeteer/Stagehand 接管同一浏览器:`chromium.connectOverCDP(await box.browser.cdpUrl())`,再取/建 context 与 page 正常操作;Stagehand 用 `new Stagehand({ env: 'LOCAL', localBrowserLaunchOptions: { cdpUrl } })`。
- 会话录制:`box.browser.recordings.start({ maxDurationSeconds: 600 })`(默认与上限都是 600),用 `handle.stop()` 或 `box.browser.recordings.stop()` 停止;`list()` / `get(id)` / `download(id, { path })`(默认写 `./box-recording-<id>.mp4`,MP4 支持前的录制是 `.ts`)。一个 box 同时只有一个活动录制、捕获所有标签页并跟随前台,超时或约 3 分钟无屏幕活动会自动停止,视频保留 14 天(`expiresAt` 是 epoch ms);`playlistUrl` 要用 `X-Box-Api-Key: <apiKey>` 头拉取(hls.js / Safari / ffplay)。
- 多步浏览器目标(`tab.run()` 在 0.7.0 已移除,相关 `BrowserRunOptions`/`BrowserRunResult`/`BrowserRunStep` 类型也没了)的三种替代:① 自己写循环——用 `observe()` 解析步骤,用 `tab.act(action)` 重放使模型不留在热路径,用 `extract()` 做停止判断,必要时每次先 `tab.goto(START)` 做确定性重置;② 把目标交给 box 内 agent——`browser: true` 会自动把 chrome-devtools MCP(Chromium 已在 `127.0.0.1:9222` 预热)接入 agent,直接 `box.agent.run({ prompt })`,无需先 `tab.create()`,计的是 coding agent token 且需要 harness + key;③ 流程完全确定时用 `box.browser.cdpUrl()` 连 Playwright/Puppeteer。
- EphemeralBox:`EphemeralBox.create({ name, runtime, size, ttl, env, labels, networkPolicy, attachHeaders })`,`ttl` 单位秒、最大 259200(3 天)且默认也是 259200;支持 `exec`(含 session)、`files`(含 stat/mkdir/rename/remove)、`schedule`、`cd`、网络策略、快照、`getStatus`、`delete`、`expiresAt`;用 `EphemeralBox.fromSnapshot(id, { ttl })` 恢复;`Box.delete({ boxIds })` / `Box.deleteSnapshots()` 是同一个静态方法,`EphemeralBox.getByName()` 实际返回完整 `Box`。
- 公开 URL:`box.getPublicURL(port)` 返回 `{ url, port }`;要鉴权就传 `{ bearerToken: true }`(额外返回 token)或 `{ basicAuth: true }`(返回 username/password);`box.listPublicURLs()`、`box.deletePublicURL(port)`。
- Skills:创建时传 `skills: ['upstash/qstash-js/qstash-js']`,之后用 `box.skills.add(owner/repo/skill-name)` / `list()` / `remove(...)`。skill 从 Context7 registry 解析、只在 box 容器内运行,但会成为 agent 的指令,所以要像依赖一样只 pin 你信任的 owner。
- Labels:`box.labels.add('prod')`(返回更新后的集合)/ `remove('beta')` / `list()`,配合 `Box.list({ label: 'prod' })` 筛选。
- 网络策略:`networkPolicy: { mode: 'allow-all'(默认)| 'deny-all' | 'custom', allowedDomains, allowedCidrs, deniedCidrs }`;运行时用 `box.updateNetworkPolicy({ mode: 'deny-all' })`;处理不可信代码或仓库前先收紧。
- 出站密钥头:`attachHeaders: { 'api.stripe.com': { Authorization: 'Bearer sk_live_...' }, '*.example.com': { 'X-Custom-Token': '...' } }`,匹配到的出站 HTTPS 请求自动注入头,且只写不可读回。
- MCP 服务器:`mcpServers: [{ name, package, args }]` 或 `[{ name, url, headers }]`;只接你控制或信任的服务器,并在 agent 同时处理不可信输入时保持 networkPolicy 收紧。
- 错误处理:用 `try/catch` 捕获 `BoxError`,读 `e.message` 与 `e.statusCode`;超时抛 `BoxError('Run timed out')` / `'Stream timed out'`,abort 在 `cause` 上。`maxRetries` 只覆盖瞬时失败——被 `run.cancel()` 或自身 `timeout` 中止的 run 会立刻失败,不会另起一个仍在计费的 run。
- SSH 直连:`ssh <box-id>@us-east-1.box.upstash.com`,密码就是 Box API key。
- 所有 `timeout`、`initCommand` 超时等时间值单位都是毫秒;agent 相关的 timeout/maxRetries 尤其要注意这个单位。
适用边界 · 注意事项
- 没有 Upstash 账号/API key,或不能把代码、密钥与出站流量交给第三方云沙箱时不适用——这是依赖 Upstash Box 服务与额度的特定方案。
- 需要 agent、git、skills、headless browser 或公开 URL 时不要用 `EphemeralBox`(它只支持 exec、files、schedule、cd、网络策略与快照),要改用完整 `Box`。
- 需要浏览器『自主多步 agent』时不要找 `tab.run()`(0.7.0 已移除),也不要指望 `files.remove()` 能删目录、`files.mkdir()` 能建嵌套路径、`agent.run` 有 `exitCode`——这些都要按上面的替代做法处理。