From 92cf4f80e4a3afb73df343dce2f65ade446ebbc6 Mon Sep 17 00:00:00 2001 From: osen77 Date: Mon, 16 Mar 2026 13:33:29 +0800 Subject: [PATCH] fix(cli): execute network-capable adapters in Node to bypass CORS Site adapters with `capabilities: ["network"]` make cross-origin API calls (e.g. hackernews/top calls Firebase API from a ycombinator page). These calls fail with "TypeError: Failed to fetch" when executed via CDP Runtime.evaluate in the page context, because the page's CORS/CSP policy blocks cross-origin requests. Fix: detect `capabilities: ["network"]` in adapter metadata and execute the adapter script directly in the Node process using `new Function()`. Node's fetch() has no CORS restrictions, so cross-origin API calls succeed without needing browser context. Adapters without the "network" capability continue to execute via CDP in page context, preserving cookie-based authentication for sites that require login (e.g. zhihu, weibo). Fixes #41 --- packages/cli/src/commands/site.ts | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/cli/src/commands/site.ts b/packages/cli/src/commands/site.ts index 3e4600e3..02745a9d 100644 --- a/packages/cli/src/commands/site.ts +++ b/packages/cli/src/commands/site.ts @@ -615,6 +615,51 @@ async function siteRun( } } + // Adapters with capabilities: ["network"] make cross-origin API calls that + // fail under page-context CORS/CSP. Execute them in Node instead where + // fetch() has no CORS restrictions. + const hasNetworkCap = site.capabilities?.includes("network"); + if (hasNetworkCap) { + try { + const asyncFn = new Function(`return (async () => { return ${script}; })()`); + const result = await asyncFn(); + let parsed: unknown = result; + + if (typeof parsed === "object" && parsed !== null && "error" in parsed) { + const errObj = parsed as { error: string; hint?: string }; + if (options.json) { + console.log(JSON.stringify({ id: generateId(), success: false, error: errObj.error, hint: errObj.hint })); + } else { + console.error(`[error] site ${name}: ${errObj.error}`); + if (errObj.hint) console.error(` Hint: ${errObj.hint}`); + } + process.exit(1); + } + + if (options.jq) { + const { applyJq } = await import("../jq.js"); + const expr = options.jq.replace(/^\.data\./, '.'); + const results = applyJq(parsed, expr); + for (const r of results) { + console.log(typeof r === "string" ? r : JSON.stringify(r)); + } + } else if (options.json) { + console.log(JSON.stringify({ id: generateId(), success: true, data: parsed })); + } else { + console.log(JSON.stringify(parsed, null, 2)); + } + return; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + if (options.json) { + console.log(JSON.stringify({ id: generateId(), success: false, error: errMsg })); + } else { + console.error(`[error] site ${name}: ${errMsg}`); + } + process.exit(1); + } + } + // 执行 const evalReq: Request = { id: generateId(), action: "eval", script, tabId: targetTabId }; const evalResp: Response = await sendCommand(evalReq);