跳转到内容
本站为社区维护的非官方中文翻译,与 Cloudflare, Inc. 无关联。本站不提供账户登录,请勿在任何非官方站点输入您的 Cloudflare 账户信息。官方文档:developers.cloudflare.com

替换代码解释器

最后更新 查看 MarkdownAgent 设置

在 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 中的普通调用会共享变量:

src/index.ts (0.12)ts
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 变量

内核是一个 Python 进程,用 IPython 运行每次调用的代码,并在退出前保留其变量。你的 Durable Object 为每个上下文在其自己的端口上启动一个内核。

  1. 将 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 即可安装它们。

  2. 在 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、最后一个表达式的值以及错误。它一次处理一个请求。

  3. 在类之前添加结果和上下文类型:

    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}`;
  4. 向类添加启动内核并中断它的方法:

    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() 会抛出并带上日志末尾。

  5. 添加一个方法,按顺序运行同一上下文的调用,并添加 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 停止单元格。较早调用定义的变量会保留。如果内核退出(例如因为代码内存不足),调用会抛出,下一次调用会启动没有变量的内核。

  6. 添加替换 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 名称。

  7. 替换每次调用。传递上下文的 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 相同。
  8. 部署,并向一个 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 的存储中。

在 Dynamic Worker 中运行 JavaScript

在 Worker 加载的 Dynamic Worker 中运行 JavaScript,因此 JavaScript 不需要容器。

  1. 向 Wrangler 配置添加 Worker Loader 绑定:

    {
        "worker_loaders": [
            {
                "binding": "LOADER",
            },
        ],
    }
    [[worker_loaders]]
    binding = "LOADER"
    npx wrangler types
  2. 创建 src/sandbox.ts,其中包含 构建 AI 代码解释器 中的 runJavaScript() 函数。

  3. 替换每次用于 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,如 迁移命令 所述。命令不保留变量。

相关资源

这篇文档对您有帮助吗?