import type { BrowserContext, Response } from 'playwright'; export async function safeJson(res: Response): Promise { const ctype = res.headers()['content-type'] || ''; if (ctype.includes('application/json')) { return (await res.json()) as T; } const t = await res.text(); try { return JSON.parse(t) as T; } catch { return null; } } /** * 使用 Playwright 的 APIRequestContext 下载二进制内容 * - 使用指定 headers 模拟浏览器请求 * - referrer 使用链接本身 */ export async function downloadBinary( context: BrowserContext, url: string ): Promise<{ buffer: Buffer; contentType: string; ext: string }> { console.log('Download bin:', url); const headers = { referer: url, } as Record; const res = await context.request.get(url, { headers, maxRedirects: 3, timeout: 240_000, failOnStatusCode: true, }); if (!res.ok()) { throw new Error(`下载内容失败: ${res.status()} ${res.statusText()}`); } const buffer = await res.body(); const contentType = res.headers()['content-type'] || 'application/octet-stream'; const ext = (contentType.split('/')[1] || 'bin').split(';')[0] || 'bin'; return { buffer, contentType, ext }; } /** * 在多个候选匹配中“先到先得”地返回首个命中的 Response。 * - 不为每个候选单独设长超时,改用整体兜底超时,避免无意义等待。 */ export function waitForFirstResponse( context: BrowserContext, candidates: { key: string; test: (r: Response) => boolean }[], timeoutMs = 20_000 ): Promise<{ key: string; response: Response } | null> { return new Promise((resolve) => { let resolved = false; let timer: NodeJS.Timeout | undefined; const handler = (res: Response) => { if (resolved) return; for (const c of candidates) { try { if (c.test(res)) { resolved = true; cleanup(); resolve({ key: c.key, response: res }); return; } } catch { // ignore predicate errors } } }; const cleanup = () => { context.off('response', handler); if (timer) clearTimeout(timer); }; context.on('response', handler); if (timeoutMs > 0) { timer = setTimeout(() => { if (!resolved) { resolved = true; cleanup(); resolve(null); } }, timeoutMs); } }); } /** * 等待符合条件的单个 Response,带短超时;用于评论等“可有可无”的数据。 */ export function waitForResponseWithTimeout( context: BrowserContext, predicate: (r: Response) => boolean, timeoutMs = 5_000 ): Promise { return new Promise((resolve, reject) => { let timer: NodeJS.Timeout | undefined; const handler = (res: Response) => { try { if (predicate(res)) { cleanup(); resolve(res); } } catch { // ignore predicate errors } }; const cleanup = () => { context.off('response', handler); if (timer) clearTimeout(timer); }; context.on('response', handler); if (timeoutMs > 0) { timer = setTimeout(() => { cleanup(); reject(new Error('timeout')); }, timeoutMs); } }); }