diff --git a/extensions/qqbot/src/bridge/bootstrap.test.ts b/extensions/qqbot/src/bridge/bootstrap.test.ts index 9354a439af66..acc6d528f038 100644 --- a/extensions/qqbot/src/bridge/bootstrap.test.ts +++ b/extensions/qqbot/src/bridge/bootstrap.test.ts @@ -6,9 +6,14 @@ import { ensurePlatformAdapter } from "./bootstrap.js"; const mocks = vi.hoisted(() => ({ getRuntimeConfig: vi.fn(), + readRemoteMediaBuffer: vi.fn(), resolveApprovalOverGateway: vi.fn(), })); +vi.mock("openclaw/plugin-sdk/media-runtime", () => ({ + readRemoteMediaBuffer: (...args: unknown[]) => mocks.readRemoteMediaBuffer(...args), +})); + vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", () => ({ getRuntimeConfig: mocks.getRuntimeConfig, })); @@ -46,6 +51,36 @@ describe("QQBot built-in platform adapter", () => { ensurePlatformAdapter(); }); + it("forwards response header deadlines to the media runtime", async () => { + mocks.readRemoteMediaBuffer.mockResolvedValueOnce({ + buffer: Buffer.from("image"), + fileName: "remote.png", + }); + + const result = await getPlatformAdapter().fetchMedia({ + url: "https://media.qq.com/assets/photo.png", + filePathHint: "photo.png", + maxBytes: 1024, + maxRedirects: 2, + timeoutMs: 5_000, + responseHeaderTimeoutMs: 120_000, + ssrfPolicy: { hostnameAllowlist: ["*.qq.com"] }, + requestInit: { headers: { accept: "image/png" } }, + }); + + expect(result).toEqual({ buffer: Buffer.from("image"), fileName: "remote.png" }); + expect(mocks.readRemoteMediaBuffer).toHaveBeenCalledWith({ + url: "https://media.qq.com/assets/photo.png", + filePathHint: "photo.png", + maxBytes: 1024, + maxRedirects: 2, + timeoutMs: 5_000, + responseHeaderTimeoutMs: 120_000, + ssrfPolicy: { hostnameAllowlist: ["*.qq.com"] }, + requestInit: { headers: { accept: "image/png" } }, + }); + }); + it("preserves plugin ownership and the canonical first-answer result", async () => { const adapter = getPlatformAdapter(); diff --git a/extensions/qqbot/src/bridge/bootstrap.ts b/extensions/qqbot/src/bridge/bootstrap.ts index b89e9e740dbd..8917180a210a 100644 --- a/extensions/qqbot/src/bridge/bootstrap.ts +++ b/extensions/qqbot/src/bridge/bootstrap.ts @@ -77,6 +77,8 @@ function createBuiltinAdapter(): PlatformAdapter { filePathHint: options.filePathHint, maxBytes: options.maxBytes, maxRedirects: options.maxRedirects, + timeoutMs: options.timeoutMs, + responseHeaderTimeoutMs: options.responseHeaderTimeoutMs, ssrfPolicy: options.ssrfPolicy, requestInit: options.requestInit, }); diff --git a/extensions/qqbot/src/engine/adapter/types.ts b/extensions/qqbot/src/engine/adapter/types.ts index 61e62f008bef..f945aebf61f2 100644 --- a/extensions/qqbot/src/engine/adapter/types.ts +++ b/extensions/qqbot/src/engine/adapter/types.ts @@ -17,6 +17,10 @@ export interface FetchMediaOptions { maxBytes?: number; /** Maximum redirects to follow. */ maxRedirects?: number; + /** Abort the complete remote media request after this many milliseconds. */ + timeoutMs?: number; + /** Abort if final response headers have not arrived after this many milliseconds. */ + responseHeaderTimeoutMs?: number; /** SSRF policy configuration. */ ssrfPolicy?: SsrfPolicyConfig; /** Extra fetch() RequestInit options. */ diff --git a/extensions/qqbot/src/engine/utils/file-utils.test.ts b/extensions/qqbot/src/engine/utils/file-utils.test.ts index 7637d67df597..fd8121fe6ace 100644 --- a/extensions/qqbot/src/engine/utils/file-utils.test.ts +++ b/extensions/qqbot/src/engine/utils/file-utils.test.ts @@ -28,6 +28,7 @@ vi.mock("../adapter/index.js", () => ({ import { QQBOT_MEDIA_SSRF_POLICY, + QQBOT_REMOTE_MEDIA_RESPONSE_HEADER_TIMEOUT_MS, checkFileSize, downloadFile, fileExistsAsync, @@ -95,6 +96,7 @@ describe("qqbot file-utils downloadFile", () => { url: "https://media.qq.com/assets/photo.png", filePathHint: "photo.png", ssrfPolicy: QQBOT_MEDIA_SSRF_POLICY, + responseHeaderTimeoutMs: QQBOT_REMOTE_MEDIA_RESPONSE_HEADER_TIMEOUT_MS, }); expect(QQBOT_MEDIA_SSRF_POLICY).toEqual({ hostnameAllowlist: [ diff --git a/extensions/qqbot/src/engine/utils/file-utils.ts b/extensions/qqbot/src/engine/utils/file-utils.ts index eb980526bc25..74e26538abf7 100644 --- a/extensions/qqbot/src/engine/utils/file-utils.ts +++ b/extensions/qqbot/src/engine/utils/file-utils.ts @@ -70,6 +70,8 @@ export const QQBOT_MEDIA_SSRF_POLICY: SsrfPolicyConfig = { allowRfc2544BenchmarkRange: true, }; +export const QQBOT_REMOTE_MEDIA_RESPONSE_HEADER_TIMEOUT_MS = 120_000; + /** Result of local file-size validation. */ interface FileSizeCheckResult { ok: boolean; @@ -183,6 +185,7 @@ export async function downloadFile( url: parsedUrl.toString(), filePathHint: originalFilename, ssrfPolicy: QQBOT_MEDIA_SSRF_POLICY, + responseHeaderTimeoutMs: QQBOT_REMOTE_MEDIA_RESPONSE_HEADER_TIMEOUT_MS, }); let filename = normalizeOptionalString(originalFilename) ?? "";