






















@@ -0,0 +1,259 @@
1+import { createHash, randomBytes, randomUUID } from "node:crypto";
2+import fs from "node:fs";
3+import { setTimeout as delay } from "node:timers/promises";
4+import { WebSocket } from "ws";
5+import { PROTOCOL_VERSION } from "../../../../dist/gateway/protocol/index.js";
6+import { renderBitmapTextPngBase64 } from "../../../../test/helpers/live-image-probe.ts";
7+8+const port = process.env.PORT;
9+const token = process.env.OPENCLAW_GATEWAY_TOKEN;
10+const appServerLog =
11+process.env.OPENCLAW_CODEX_MEDIA_PATH_APP_SERVER_LOG ??
12+"/tmp/openclaw-codex-media-path-app-server.jsonl";
13+const timeoutSeconds = Number.parseInt(
14+process.env.OPENCLAW_CODEX_MEDIA_PATH_TIMEOUT_SECONDS ?? "180",
15+10,
16+);
17+18+if (!port || !token) {
19+throw new Error("missing PORT/OPENCLAW_GATEWAY_TOKEN");
20+}
21+22+function assert(condition, message) {
23+if (!condition) {
24+throw new Error(message);
25+}
26+}
27+28+function sha256Base64(data) {
29+return createHash("sha256").update(Buffer.from(data, "base64")).digest("hex");
30+}
31+32+function readLoggedRequests() {
33+if (!fs.existsSync(appServerLog)) {
34+return [];
35+}
36+return fs
37+.readFileSync(appServerLog, "utf8")
38+.split("\n")
39+.filter(Boolean)
40+.map((line) => JSON.parse(line));
41+}
42+43+async function waitFor(label, predicate, timeoutMs) {
44+const started = Date.now();
45+while (Date.now() - started < timeoutMs) {
46+const value = await predicate();
47+if (value !== undefined) {
48+return value;
49+}
50+await delay(50);
51+}
52+throw new Error(`timeout waiting for ${label}`);
53+}
54+55+function wsDataToString(data) {
56+if (typeof data === "string") {
57+return data;
58+}
59+if (Buffer.isBuffer(data)) {
60+return data.toString("utf8");
61+}
62+if (Array.isArray(data)) {
63+return Buffer.concat(data).toString("utf8");
64+}
65+return Buffer.from(data).toString("utf8");
66+}
67+68+async function connectGateway() {
69+const ws = new WebSocket(`ws://127.0.0.1:${port}`);
70+await new Promise((resolve, reject) => {
71+const timer = setTimeout(() => reject(new Error("gateway ws open timeout")), 45_000);
72+timer.unref?.();
73+ws.once("open", () => {
74+clearTimeout(timer);
75+resolve();
76+});
77+ws.once("error", (error) => {
78+clearTimeout(timer);
79+reject(error);
80+});
81+});
82+83+const events = [];
84+const pending = new Map();
85+ws.on("message", (data) => {
86+let frame;
87+try {
88+frame = JSON.parse(wsDataToString(data));
89+} catch {
90+return;
91+}
92+if (frame?.type === "event" && typeof frame.event === "string") {
93+events.push({
94+event: frame.event,
95+payload: frame.payload && typeof frame.payload === "object" ? frame.payload : {},
96+});
97+return;
98+}
99+if (frame?.type !== "res" || typeof frame.id !== "string") {
100+return;
101+}
102+const match = pending.get(frame.id);
103+if (!match) {
104+return;
105+}
106+pending.delete(frame.id);
107+if (frame.ok === true) {
108+match.resolve(frame.payload ?? frame.result);
109+return;
110+}
111+match.reject(new Error(frame.error?.message ?? "gateway request failed"));
112+});
113+ws.once("close", (code, reason) => {
114+const error = new Error(`gateway closed (${code}): ${wsDataToString(reason)}`);
115+for (const entry of pending.values()) {
116+entry.reject(error);
117+}
118+pending.clear();
119+});
120+121+function request(method, params, opts = {}) {
122+const id = randomUUID();
123+const timeoutMs = opts.timeoutMs ?? 60_000;
124+return new Promise((resolve, reject) => {
125+const timer = setTimeout(() => {
126+pending.delete(id);
127+reject(new Error(`gateway request timeout: ${method}`));
128+}, timeoutMs);
129+timer.unref?.();
130+pending.set(id, {
131+resolve: (value) => {
132+clearTimeout(timer);
133+resolve(value);
134+},
135+reject: (error) => {
136+clearTimeout(timer);
137+reject(error);
138+},
139+});
140+ws.send(JSON.stringify({ type: "req", id, method, params: params ?? {} }));
141+});
142+}
143+144+await request(
145+"connect",
146+{
147+minProtocol: PROTOCOL_VERSION,
148+maxProtocol: PROTOCOL_VERSION,
149+client: {
150+id: "gateway-client",
151+displayName: "docker-codex-media-path",
152+version: "1.0.0",
153+platform: process.platform,
154+mode: "backend",
155+},
156+role: "operator",
157+scopes: ["operator.read", "operator.write", "operator.admin"],
158+caps: [],
159+auth: { token },
160+},
161+{ timeoutMs: 60_000 },
162+);
163+await request("sessions.subscribe", {}, { timeoutMs: 60_000 });
164+165+return {
166+ events,
167+ request,
168+async close() {
169+if (ws.readyState === WebSocket.CLOSED) {
170+return;
171+}
172+await new Promise((resolve) => {
173+const timer = setTimeout(resolve, 2_000);
174+timer.unref?.();
175+ws.once("close", () => {
176+clearTimeout(timer);
177+resolve();
178+});
179+ws.close();
180+});
181+},
182+};
183+}
184+185+const gateway = await connectGateway();
186+187+function randomBitmapTextToken(length = 6) {
188+const alphabet = "24567ACEF";
189+return [...randomBytes(length)].map((byte) => alphabet[byte % alphabet.length]).join("");
190+}
191+192+try {
193+const expectedToken = randomBitmapTextToken();
194+const imageBase64 = renderBitmapTextPngBase64(expectedToken);
195+const expectedHash = sha256Base64(imageBase64);
196+const runId = `codex-media-path-${randomUUID()}`;
197+const started = Date.now();
198+199+const response = await gateway.request(
200+"chat.send",
201+{
202+sessionKey: "agent:main:codex-media-path-e2e",
203+idempotencyKey: runId,
204+message: "Read the code printed in the attached image. Reply only the code.",
205+attachments: [
206+{
207+mimeType: "image/png",
208+fileName: "codex-media-path-probe.png",
209+content: imageBase64,
210+},
211+],
212+originatingChannel: "codex-media-path-e2e",
213+originatingTo: "codex-media-path-e2e",
214+originatingAccountId: "codex-media-path-e2e",
215+},
216+{ timeoutMs: timeoutSeconds * 1000 },
217+);
218+assert(response?.status === "started", `chat.send did not start: ${JSON.stringify(response)}`);
219+220+const turnRequest = await waitFor(
221+"Codex turn/start image input",
222+() =>
223+readLoggedRequests().find((request) => {
224+if (request.method !== "turn/start") {
225+return undefined;
226+}
227+const imageInput = request.params?.input?.find?.(
228+(entry) => entry?.type === "image" && typeof entry.url === "string",
229+);
230+return imageInput ? request : undefined;
231+}),
232+timeoutSeconds * 1000,
233+);
234+235+const imageInput = turnRequest.params.input.find((entry) => entry?.type === "image");
236+const imageUrl = imageInput.url;
237+assert(
238+imageUrl.startsWith("data:image/png;base64,"),
239+`turn/start image input is not an inline PNG: ${JSON.stringify(imageInput)}`,
240+);
241+const actualBase64 = imageUrl.slice("data:image/png;base64,".length);
242+const actualHash = sha256Base64(actualBase64);
243+assert(
244+actualHash === expectedHash,
245+`forwarded PNG hash mismatch: expected ${expectedHash}, got ${actualHash}`,
246+);
247+248+await delay(50);
249+console.log(
250+JSON.stringify({
251+ok: true,
252+elapsedMs: Date.now() - started,
253+ expectedToken,
254+imageSha256: actualHash,
255+}),
256+);
257+} finally {
258+await gateway.close();
259+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。