
























@@ -1,8 +1,8 @@
11import { REALTIME_VOICE_AUDIO_FORMAT_PCM16_24KHZ } from "openclaw/plugin-sdk/realtime-voice";
2-import { beforeEach, describe, expect, it, vi } from "vitest";
2+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
33import { buildOpenAIRealtimeVoiceProvider } from "./realtime-voice-provider.js";
445-const { FakeWebSocket } = vi.hoisted(() => {
5+const { FakeWebSocket, fetchWithSsrFGuardMock } = vi.hoisted(() => {
66type Listener = (...args: unknown[]) => void;
7788class MockWebSocket {
@@ -15,8 +15,10 @@ const { FakeWebSocket } = vi.hoisted(() => {
1515sent: string[] = [];
1616closed = false;
1717terminated = false;
18+args: unknown[];
181919-constructor() {
20+constructor(...args: unknown[]) {
21+this.args = args;
2022MockWebSocket.instances.push(this);
2123}
2224@@ -49,13 +51,17 @@ const { FakeWebSocket } = vi.hoisted(() => {
4951}
5052}
515352-return { FakeWebSocket: MockWebSocket };
54+return { FakeWebSocket: MockWebSocket, fetchWithSsrFGuardMock: vi.fn() };
5355});
54565557vi.mock("ws", () => ({
5658default: FakeWebSocket,
5759}));
586061+vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
62+fetchWithSsrFGuard: fetchWithSsrFGuardMock,
63+}));
64+5965type FakeWebSocketInstance = InstanceType<typeof FakeWebSocket>;
6066type SentRealtimeEvent = {
6167type: string;
@@ -70,9 +76,93 @@ function parseSent(socket: FakeWebSocketInstance): SentRealtimeEvent[] {
7076return socket.sent.map((payload: string) => JSON.parse(payload) as SentRealtimeEvent);
7177}
727879+function createJsonResponse(body: unknown, init?: { status?: number }): Response {
80+return new Response(JSON.stringify(body), {
81+status: init?.status ?? 200,
82+headers: {
83+"Content-Type": "application/json",
84+},
85+});
86+}
87+7388describe("buildOpenAIRealtimeVoiceProvider", () => {
7489beforeEach(() => {
7590FakeWebSocket.instances = [];
91+fetchWithSsrFGuardMock.mockReset();
92+});
93+94+afterEach(() => {
95+vi.unstubAllEnvs();
96+});
97+98+it("adds OpenClaw attribution headers to native realtime websocket requests", () => {
99+vi.stubEnv("OPENCLAW_VERSION", "2026.3.22");
100+const provider = buildOpenAIRealtimeVoiceProvider();
101+const bridge = provider.createBridge({
102+providerConfig: { apiKey: "sk-test" }, // pragma: allowlist secret
103+onAudio: vi.fn(),
104+onClearAudio: vi.fn(),
105+});
106+107+void bridge.connect();
108+bridge.close();
109+110+const socket = FakeWebSocket.instances[0];
111+const options = socket?.args[1] as { headers?: Record<string, string> } | undefined;
112+expect(options?.headers).toMatchObject({
113+originator: "openclaw",
114+version: "2026.3.22",
115+"User-Agent": "openclaw/2026.3.22",
116+});
117+});
118+119+it("returns browser-safe OpenClaw attribution headers for native WebRTC offers", async () => {
120+vi.stubEnv("OPENCLAW_VERSION", "2026.3.22");
121+fetchWithSsrFGuardMock.mockResolvedValueOnce({
122+response: createJsonResponse({
123+client_secret: { value: "client-secret-123" },
124+expires_at: 1_765_000_000,
125+}),
126+release: vi.fn(async () => undefined),
127+});
128+const provider = buildOpenAIRealtimeVoiceProvider();
129+if (!provider.createBrowserSession) {
130+throw new Error("expected OpenAI realtime provider to support browser sessions");
131+}
132+133+const session = await provider.createBrowserSession({
134+providerConfig: { apiKey: "sk-test" }, // pragma: allowlist secret
135+instructions: "Be concise.",
136+});
137+138+expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
139+expect.objectContaining({
140+url: "https://api.openai.com/v1/realtime/client_secrets",
141+init: expect.objectContaining({
142+method: "POST",
143+headers: expect.objectContaining({
144+Authorization: "Bearer sk-test", // pragma: allowlist secret
145+"Content-Type": "application/json",
146+originator: "openclaw",
147+version: "2026.3.22",
148+"User-Agent": "openclaw/2026.3.22",
149+}),
150+}),
151+}),
152+);
153+expect(session).toMatchObject({
154+provider: "openai",
155+transport: "webrtc-sdp",
156+clientSecret: "client-secret-123",
157+offerUrl: "https://api.openai.com/v1/realtime/calls",
158+offerHeaders: {
159+originator: "openclaw",
160+version: "2026.3.22",
161+},
162+});
163+expect((session as { offerHeaders?: Record<string, string> }).offerHeaders).not.toHaveProperty(
164+"User-Agent",
165+);
76166});
7716778168it("normalizes provider-owned voice settings from raw provider config", () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。