本指南搭配 Cloudflare 的 Worker 參考實作,採用 由 webhook 管理的佈建方式 。
請參閱 OpenAI Cookbook 中由應用程式管理和由 webhook 管理的範例。
- 您的應用程式建立 Agents API 工作階段並傳送輸入。
- OpenAI 將工作階段 webhooks 傳送至您 Cloudflare 帳戶中的 Worker。
- Worker 會啟動或重新連接專屬於該工作階段、執行
codex exec-server 的 Container。執行器會向外連線至 OpenAI,讓智慧體能夠執行指令並處理檔案。
您的應用程式使用 Agents API;Worker 參考實作則管理沙盒佈建。如需瞭解連線與復原行為,請參閱沙盒生命週期。
您需要具備 Containers 存取權的 Cloudflare 帳戶。應用程式發出請求時,請使用 OPENAI_API_KEY。將 OPENAI_EXECUTOR_API_KEY 設為環境金鑰,並僅將該金鑰以 CODEX_API_KEY 的名稱傳入 Container。
建立智慧體,並將其 ID 儲存為 OPENAI_AGENT_ID。您的應用程式和 Worker 參考實作須使用相同的智慧體 ID。
Cloudflare 的 Worker 參考實作包含 webhook 處理常式、Container 映像檔、部署組態和清理端點。
為清理端點產生密鑰,並將其儲存為 EXECUTOR_CLIENT_SECRET:
openssl rand -hex 32
在您的 Cloudflare 帳戶中部署 Worker:
部署至 Cloudflare
出現提示時,請輸入下列值:
| 變數 | 值 |
|---|
OPENAI_API_KEY | Worker 用來擷取工作階段狀態的金鑰 |
OPENAI_EXECUTOR_API_KEY | 以 CODEX_API_KEY 的名稱傳入執行器的環境金鑰 |
OPENAI_AGENT_ID | 此 Worker 提供服務的智慧體 ID |
OPENAI_WEBHOOK_SECRET | 首次部署時使用 pending-webhook-registration |
EXECUTOR_CLIENT_SECRET | 為清理作業產生的密鑰 |
將部署完成的 Worker URL 儲存為 WORKER_URL。
請依照 webhook 設定的說明,在您的 OpenAI 專案中註冊 $WORKER_URL/webhook。啟用 Cloudflare 參考整合列出的事件:
agent.session.created
agent.session.action_required
agent.session.in_progress
agent.session.idle
agent.session.failed
將 OPENAI_WEBHOOK_SECRET 替換為 OpenAI 傳回的簽署密鑰,然後部署新版 Worker。檢查其組態。下列範例使用標準 HTTP 用戶端呼叫 Worker:
1
2
3
4
5
6
7
8// Replace the illustrative IDs and URLs below with your own resource values.
const response = await fetch(
"https://worker.example.com".replace(/\/+$/, "") + "/health",
{ method: "GET" }
);
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
console.log(await response.text());
1
2
3
4
5
6
7# Replace the illustrative IDs and URLs below with your own resource values.
import urllib.request
url = "https://worker.example.com".rstrip("/") + "/health"
request = urllib.request.Request(url, method="GET")
with urllib.request.urlopen(request) as response:
print(response.read().decode())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// Replace the illustrative IDs and URLs below with your own resource values.
import (
"io"
"net/http"
"os"
"strings"
)
endpoint := strings.TrimRight("https://worker.example.com", "/") + "/health"
request, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
panic(err)
}
response, err := http.DefaultClient.Do(request)
if err != nil {
panic(err)
}
defer response.Body.Close()
if response.StatusCode/100 != 2 {
panic(response.Status)
}
if _, err := io.Copy(os.Stdout, response.Body); err != nil {
panic(err)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// Replace the illustrative IDs and URLs below with your own resource values.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
String endpoint = "https://worker.example.com".replaceAll("/+$", "") + "/health";
var request =
HttpRequest.newBuilder(URI.create(endpoint))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() / 100 != 2)
throw new IllegalStateException("Request failed: " + response.statusCode());
System.out.println(response.body());
1
2
3
4
5
6
7
8
9
10# Replace the illustrative IDs and URLs below with your own resource values.
require "uri"
require "net/http"
uri = URI("https://worker.example.com".sub(%r{/+\z}, "") + "/health")
request = Net::HTTP::Get.new(uri)
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(request) }
raise "Request failed: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
puts response.body
1curl --fail-with-body "$WORKER_URL/health"
回應應同時包含 "configured": true 和 "webhook_configured": true。
environment_connection 必要動作表示需要重新連接離線的執行器。僅憑閒置事件無法判定是否能安全關閉;請參閱生命週期行為。
請使用應用程式的 OPENAI_API_KEY,以及與 Worker 中設定相同的 OPENAI_AGENT_ID,依照工作階段步驟操作。建立自管工作階段,並要求智慧體寫入及讀取 /workspace/hello.txt。
Worker 會接收工作階段 webhooks,並連接沙盒執行器。您的應用程式會透過 Agents API 串流傳輸智慧體的輸出。
將工作階段 ID 儲存為 SESSION_ID。若要繼續對話,請先開啟工作階段事件串流,再傳送後續輸入。如果執行器離線,新的輸入會要求建立環境連線,並等待 Worker 重新連接執行器。重新連線本身不會還原先前 Container 中的檔案。
Cloudflare 的基本 Worker 應用程式使用 @openai/agents-api TypeScript SDK 建立工作階段、傳送初始與後續輸入,以及清理資源。其 POST /demo 端點會執行此工作流程。
此應用程式也採用由 webhook 管理的佈建方式。在 Worker 中執行應用程式,並不表示應用程式必須直接佈建沙盒。
當應用程式不再需要沙盒時,請呼叫 Worker 參考實作中需要身分驗證的清理端點:
1
2
3
4
5
6
7
8// Replace the illustrative IDs and URLs below with your own resource values.
const response = await fetch("https://worker.example.com/executors/sess_123", {
method: "DELETE",
headers: { Authorization: `Bearer ${process.env.EXECUTOR_CLIENT_SECRET}` },
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
console.log(await response.text());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17# Replace the illustrative IDs and URLs below with your own resource values.
import os
from urllib.parse import quote
import urllib.request
url = (
"https://worker.example.com".rstrip("/")
+ "/executors/"
+ quote("sess_123", safe="")
)
request = urllib.request.Request(
url,
method="DELETE",
headers={"Authorization": "Bearer " + os.environ["EXECUTOR_CLIENT_SECRET"]},
)
with urllib.request.urlopen(request) as response:
print(response.read().decode())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// Replace the illustrative IDs and URLs below with your own resource values.
import (
"io"
"net/http"
"net/url"
"os"
"strings"
)
endpoint := strings.TrimRight("https://worker.example.com", "/") + "/executors/" + url.PathEscape("sess_123")
request, err := http.NewRequest("DELETE", endpoint, nil)
if err != nil {
panic(err)
}
request.Header.Set("Authorization", "Bearer "+os.Getenv("EXECUTOR_CLIENT_SECRET"))
response, err := http.DefaultClient.Do(request)
if err != nil {
panic(err)
}
defer response.Body.Close()
if response.StatusCode/100 != 2 {
panic(response.Status)
}
if _, err := io.Copy(os.Stdout, response.Body); err != nil {
panic(err)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// Replace the illustrative IDs and URLs below with your own resource values.
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
String endpoint =
"https://worker.example.com".replaceAll("/+$", "")
+ "/executors/"
+ URLEncoder.encode("sess_123", StandardCharsets.UTF_8).replace("+", "%20");
var request =
HttpRequest.newBuilder(URI.create(endpoint))
.header("Authorization", "Bearer " + System.getenv("EXECUTOR_CLIENT_SECRET"))
.method("DELETE", HttpRequest.BodyPublishers.noBody())
.build();
var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() / 100 != 2)
throw new IllegalStateException("Request failed: " + response.statusCode());
System.out.println(response.body());
1
2
3
4
5
6
7
8
9
10
11# Replace the illustrative IDs and URLs below with your own resource values.
require "uri"
require "net/http"
uri = URI("https://worker.example.com".sub(%r{/+\z}, "") + "/executors/" + URI.encode_www_form_component("sess_123").gsub("+", "%20"))
request = Net::HTTP::Delete.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("EXECUTOR_CLIENT_SECRET")}"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(request) }
raise "Request failed: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
puts response.body
1
2
3
4curl --fail-with-body \
--request DELETE \
--header "Authorization: Bearer $EXECUTOR_CLIENT_SECRET" \
"$WORKER_URL/executors/$SESSION_ID"
請另外刪除 Agents API 工作階段。刪除工作階段不會發出 webhook,因此若要立即完成清理,必須執行這兩項操作。釋放 Container 前,請先擷取所需檔案。
若要直接控制沙盒佈建,請使用 Cloudflare Sandbox SDK,並遵循由應用程式管理的生命週期及執行器連線說明。每個工作階段使用一個佈建控制器。