在 Sandbox SDK 0.12 中,runCode() 在每个代码上下文的解释器进程中运行 Python、JavaScript 和 TypeScript,变量在调用之间保持定义。1.0 没有解释器。本页在 Durable Object 启动的 IPython 内核中运行 Python,返回 0.12 返回的结果,并在 Dynamic Worker 中运行 JavaScript。
你的 1.0 类已替换 0.12 Sandbox 类,并具有 替换 Sandbox 类 中的 container getter、ensureRunning() 和 startContainer() 方法,以及 ENV 常量。要在调用之间保留变量,src/index.ts 还需要 在后台运行服务器 第 1 步中的脚本和函数。
没有 context 的 runCode() 调用使用其语言的默认上下文,因此同一个 sandbox 中的普通调用会共享变量:
const sandbox = getSandbox(env.Sandbox, "ada");
await sandbox.runCode("sales = [120, 95]");
const execution = await sandbox.runCode("sum(sales)");如果你的代码依赖此行为,或依赖你创建的上下文,请遵循 在调用之间保留 Python 变量。如果每次调用中的代码彼此独立,则为每次调用启动新的 python3 进程,如 运行 Python 代码 所述。其他 0.12 方法有各自的章节:
| 0.12 | 章节 |
|---|---|
runCode() 且 language: "javascript" 或 "typescript" |
在 Dynamic Worker 中运行 JavaScript |
runCodeStream(), onStdout, onStderr, onResult |
替换流式输出 |
内核是一个 Python 进程,用 IPython 运行每次调用的代码,并在退出前保留其变量。你的 Durable Object 为每个上下文在其自己的端口上启动一个内核。
-
将 IPython 和代码导入的包添加到
Dockerfile,并复制内核脚本:Dockerfiledockerfile FROM node:24-trixie-slim RUN apt-get update \ && apt-get install -y --no-install-recommends \ ca-certificates git python3 \ python3-ipython python3-matplotlib \ python3-numpy python3-pandas \ && rm -rf /var/lib/apt/lists/* COPY --from=docker.io/cloudflare/sandbox:1.0.1 /usr/local/bin/sandbox-shim /usr/local/bin/sandbox-shim WORKDIR /workspace COPY kernel.py /opt/kernel.py CMD ["sleep", "infinity"]0.12
python镜像包含 IPython、matplotlib、NumPy 和 pandas。Debian 包无需pip即可安装它们。 -
在
Dockerfile旁边创建kernel.py:kernel.pypython import base64 import io import json import os import signal import sys import traceback from http.server import BaseHTTPRequestHandler from socketserver import TCPServer os.environ["MPLBACKEND"] = "Agg" from IPython.core.interactiveshell import InteractiveShell from IPython.utils.capture import capture_output PORT = int(sys.argv[1]) FORMATS = { "text/plain": "text", "text/html": "html", "text/markdown": "markdown", "text/latex": "latex", "image/png": "png", "image/jpeg": "jpeg", "image/svg+xml": "svg", "application/json": "json", "application/javascript": "javascript", } shell = InteractiveShell.instance(colors="NoColor") # Return values and errors in the response, not in stdout. shell.displayhook.write_output_prompt = lambda: None shell.displayhook.write_format_data = lambda *a, **k: None shell.showtraceback = lambda *a, **k: None shell.showsyntaxerror = lambda *a, **k: None running = False def interrupt(signum, frame): # Stop a running cell. An idle kernel ignores the signal. if running: raise KeyboardInterrupt def formats(data): return { FORMATS[kind]: value for kind, value in data.items() if kind in FORMATS } def figures(): if "matplotlib.pyplot" not in sys.modules: return [] import matplotlib.pyplot as plt images = [] for number in plt.get_fignums(): buffer = io.BytesIO() plt.figure(number).savefig( buffer, format="png", bbox_inches="tight" ) png = base64.b64encode(buffer.getvalue()).decode() images.append({"png": png}) plt.close("all") return images def describe(error): if error is None: return None if isinstance(error, SyntaxError): lines = traceback.format_exception_only(error) else: # Skip the frame of the kernel itself. lines = traceback.format_exception( type(error), error, error.__traceback__.tb_next ) return { "name": type(error).__name__, "message": str(error), "traceback": lines, } def run(code): global running running = True try: with capture_output(display=True) as captured: cell = shell.run_cell(code, store_history=True) finally: running = False results = [formats(output.data) for output in captured.outputs] results += figures() if isinstance(cell.result, (dict, list)): results.append({"json": cell.result}) elif cell.result is not None: data, _ = shell.display_formatter.format(cell.result) results.append(formats(data)) stdout, stderr = captured.stdout, captured.stderr return { "logs": { "stdout": [stdout] if stdout else [], "stderr": [stderr] if stderr else [], }, "results": results, "error": describe( cell.error_before_exec or cell.error_in_exec ), "executionCount": cell.execution_count, } class Kernel(BaseHTTPRequestHandler): def do_GET(self): self.reply(b"ready") def do_POST(self): length = int(self.headers["Content-Length"]) code = self.rfile.read(length).decode() result = json.dumps(run(code), default=repr) self.reply(result.encode()) def reply(self, body): self.send_response(200) self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, *args): pass signal.signal(signal.SIGINT, interrupt) # HTTPServer looks up the container hostname, which is too # long to resolve. TCPServer.allow_reuse_address = True TCPServer(("0.0.0.0", PORT), Kernel).serve_forever()内核会将每个
POST请求的正文作为 IPython 单元运行,并返回与 0.12 相同的内容:标准输出和标准错误、单元显示的每个值对应的结果、每个打开的 matplotlib 图像生成的 PNG、最后一个表达式的值以及错误。它一次处理一个请求。 -
在类之前添加结果和上下文类型:
src/index.tsts type Execution = { logs: { stdout: string[]; stderr: string[] }; // One object for each value, keyed by format. results: Record<string, string | object>[]; error: { name: string; message: string; traceback: string[]; } | null; executionCount: number; }; type CodeContext = { id: string; port: number; cwd: string; env?: Record<string, string>; }; const DEFAULT_CONTEXT: CodeContext = { id: "default", port: 8800, cwd: "/workspace", }; // Each kernel runs as a server in its own directory. const kernelDir = (context: CodeContext) => `${SERVERS}/kernel-${context.port}`; -
向类添加启动内核并中断它的方法:
src/index.tsts export class MySandbox extends DurableObject<Env> { // ... // Ports of the kernels that answered in this container. private kernels = new Set<number>(); private async startKernel( context: CodeContext, ): Promise<Fetcher> { if (this.kernels.has(context.port)) { return this.container.getTcpPort(context.port); } await startServer( this.container, kernelDir(context), ["python3", "/opt/kernel.py", String(context.port)], { cwd: context.cwd, env: { ...ENV, ...context.env } }, ); const kernel = await waitForServer( this.container, kernelDir(context), context.port, 30_000, ); this.kernels.add(context.port); return kernel; } private async interruptKernel(context: CodeContext): Promise<void> { const kill = await this.container.exec([ "sh", "-c", `${CURRENT}\ncurrent "$1" && kill -INT "$pid"`, "sh", kernelDir(context), ]); await kill.exitCode; } }在
startContainer()中、if (newContainer)内部调用this.kernels.clear(),因为新容器没有内核。如果 Durable Object 在容器运行时重启,startServer()会找到已经在运行的内核,内核会保留其变量。如果内核退出,waitForServer()会抛出并带上日志末尾。 -
添加一个方法,按顺序运行同一上下文的调用,并添加
runCode()方法:src/index.tsts export class MySandbox extends DurableObject<Env> { // ... // The last call queued for each context. private queues = new Map<string, Promise<unknown>>(); private inContext<T>(id: string, operation: () => Promise<T>): Promise<T> { const previous = this.queues.get(id) ?? Promise.resolve(); const result = previous.then(operation); this.queues.set(id, result.catch(() => {})); return result; } async runCode( code: string, options: { context?: string; timeout?: number } = {}, ): Promise<Execution> { const id = options.context ?? DEFAULT_CONTEXT.id; return this.inContext(id, async () => { await this.ensureRunning(); const context = await this.getContext(id); const kernel = await this.startKernel(context); try { const response = await kernel.fetch("http://kernel/", { method: "POST", body: code, signal: AbortSignal.timeout(options.timeout ?? 60_000), }); return await response.json<Execution>(); } catch (error) { if ((error as Error).name === "TimeoutError") { // Stop the cell, and keep the kernel variables. await this.interruptKernel(context); } else { // The kernel exited. The next call starts a new one. this.kernels.delete(context.port); } throw error; } }); } }同一上下文中的调用会相互等待,与 0.12 中相同。不同上下文的调用会并行运行。由于同一时间只有一个调用可以运行在某个上下文中,时间限制会在调用到达内核时开始计时,而不是在等待先前调用时开始;中断也会发送给超时调用对应的单元。队列还会防止两个首次调用同时启动同一个内核。
0.12 没有默认超时,超时的单元格会继续运行。此处,运行时间超过
timeout的调用会抛出TimeoutError,内核用KeyboardInterrupt停止单元格。较早调用定义的变量会保留。如果内核退出(例如因为代码内存不足),调用会抛出,下一次调用会启动没有变量的内核。 -
添加替换
createCodeContext()、listCodeContexts()和deleteCodeContext()的方法:src/index.tsts export class MySandbox extends DurableObject<Env> { // ... private async getContext(id: string): Promise<CodeContext> { if (id === DEFAULT_CONTEXT.id) { return DEFAULT_CONTEXT; } const context = await this.ctx.storage.get<CodeContext>( `context:${id}`, ); if (!context) { throw new Error(`No code context ${id}`); } return context; } async createCodeContext( options: { cwd?: string; env?: Record<string, string> } = {}, ): Promise<CodeContext> { // Take the lowest port that no stored context uses. const stored = await this.ctx.storage.list<CodeContext>({ prefix: "context:", }); const used = new Set([...stored.values()].map((c) => c.port)); let port = DEFAULT_CONTEXT.port + 1; while (used.has(port)) { port++; } const context: CodeContext = { id: crypto.randomUUID(), port, cwd: options.cwd ?? "/workspace", env: options.env, }; await this.ctx.storage.put(`context:${context.id}`, context); return context; } async listCodeContexts(): Promise<CodeContext[]> { const contexts = await this.ctx.storage.list<CodeContext>({ prefix: "context:", }); return [DEFAULT_CONTEXT, ...contexts.values()]; } async deleteCodeContext(id: string): Promise<void> { // Wait for the calls already queued for the context. return this.inContext(id, async () => { const context = await this.getContext(id); if (this.container.running) { await stopServer(this.container, kernelDir(context)); this.kernels.delete(context.port); } await this.ctx.storage.delete(`context:${id}`); }); } }createCodeContext()为每个新上下文分配8800之后未被已存储上下文使用的最低端口,因此新上下文可以使用已删除上下文的端口。这些上下文共享一个容器,因此一个上下文中的代码可以读取其他上下文的文件并到达其端口。为每个用户提供自己的 sandbox 名称。
-
替换每次调用。传递上下文的
id,以及用于envVars的env:src/index.ts (0.12)ts const sandbox = getSandbox(env.Sandbox, "ada"); const context = await sandbox.createCodeContext({ language: "python", envVars: { REGION: "east" }, }); const execution = await sandbox.runCode( "import os; os.environ['REGION']", { context, timeout: 30_000 }, );src/index.ts (1.0)ts const sandbox = env.Sandbox.getByName("ada"); const context = await sandbox.createCodeContext({ env: { REGION: "east" }, }); const execution = await sandbox.runCode( "import os; os.environ['REGION']", { context: context.id, timeout: 30_000 }, );然后更改读取结果的代码:
0.12 1.0 results[i].text,html,png,jpeg,svg,latex,markdown,javascript,json相同的键。0.12 将每种格式作为自己的结果返回,例如 pandas DataFrame 的一个 html结果和一个text结果。内核返回带有这两个键的一个结果。results[i].formats()Object.keys(results[i]).results[i].chart,results[i].data不返回。0.12 Python 解释器未设置它们。 logs.stdout,logs.stderr相同,每次调用一个字符串。0.12 还将最后一行的值写入 stdout,例如Out[0]: 215。内核仅在results中返回该值。error.name,error.message,error.traceback相同。代码成功时 error为null,而不是undefined。executionCount相同。 -
部署,并向一个 sandbox 发送两次调用。在此示例中,Worker 路由
/sandboxes/<name>/code将请求正文传给runCode():WORKER="https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev" curl "$WORKER/sandboxes/ada/code" --data-binary 'sales = [120, 95]' curl "$WORKER/sandboxes/ada/code" --data-binary 'sum(sales)'第二次调用读取第一次调用定义的变量:
{ "logs": { "stdout": [], "stderr": [] }, "results": [{ "text": "215" }], "error": null, "executionCount": 2 }
0.12 上下文持有的变量留在 0.12 容器中。切换后,再次运行设置代码,并再次创建上下文,因为 0.12 上下文 ID 不在你的 Durable Object 的存储中。
在 Worker 加载的 Dynamic Worker 中运行 JavaScript,因此 JavaScript 不需要容器。
-
向 Wrangler 配置添加 Worker Loader 绑定:
{ "worker_loaders": [ { "binding": "LOADER", }, ], }[[worker_loaders]] binding = "LOADER"npx wrangler typesyarn wrangler typespnpm wrangler types -
创建
src/sandbox.ts,其中包含 构建 AI 代码解释器 中的runJavaScript()函数。 -
替换每次用于 JavaScript 的
runCode()调用:src/index.ts (0.12)ts const execution = await sandbox.runCode( "[2, 3, 5, 7].reduce((a, b) => a + b)", { language: "javascript" }, );src/index.ts (1.0)ts import { runJavaScript } from "./sandbox"; const output = await runJavaScript( env, "return [2, 3, 5, 7].reduce((a, b) => a + b);", );output为{ "result": 17, "logs": [] },logs保存console.log()输出。代码作为异步函数的正文运行,因此必须return其结果。0.12 改为返回最后一个表达式的值。代码没有网络访问。runJavaScript()设置的限制请参阅 构建 AI 代码解释器。
每次调用加载新的 Dynamic Worker,因此变量不会延续。将代码需要的值放入代码中。0.12 在运行前编译 TypeScript。要运行 TypeScript,先用 @cloudflare/worker-bundler 编译它。
内核在单元格完成时返回其输出,因此 runCodeStream()、onStdout、onStderr 和 onResult 没有 1.0 版本。要在代码运行时发送输出,请用 python3 - 将代码作为命令运行,通过标准输入传入,并从进程返回 stdout,如 迁移命令 所述。命令不保留变量。