import dotenv from 'dotenv'; import path from 'node:path'; import puppeteer from 'puppeteer'; dotenv.config(); let browser_singleton = null; function get_action_timeout_ms() { return Number(process.env.ACTION_TIMEOUT_MS || 300000); } function get_crx_src_path() { const crx_src_path = process.env.CRX_SRC_PATH; if (!crx_src_path) { throw new Error('缺少环境变量 CRX_SRC_PATH'); } return crx_src_path; } function get_extension_id_from_targets(targets) { for (const target of targets) { const url = target.url(); if (!url) continue; if (url.startsWith('chrome-extension://')) { const match = url.match(/^chrome-extension:\/\/([^/]+)\//); if (match && match[1]) return match[1]; } } return null; } export async function get_or_create_browser() { if (browser_singleton) { return browser_singleton; } const extension_path = path.resolve(get_crx_src_path()); const headless = String(process.env.PUPPETEER_HEADLESS || 'false') === 'true'; browser_singleton = await puppeteer.launch({ headless, args: [ `--disable-extensions-except=${extension_path}`, `--load-extension=${extension_path}`, '--no-default-browser-check', '--disable-popup-blocking', '--disable-dev-shm-usage' ] }); return browser_singleton; } export async function invoke_extension_action(action_name, action_payload) { const browser = await get_or_create_browser(); const page = await browser.newPage(); await page.goto('about:blank'); const targets = await browser.targets(); const extension_id = get_extension_id_from_targets(targets); if (!extension_id) { await page.close(); throw new Error('未找到扩展 extension_id(请确认 CRX_SRC_PATH 指向 src 且成功加载)'); } const bridge_url = `chrome-extension://${extension_id}/bridge/bridge.html`; await page.goto(bridge_url, { waitUntil: 'domcontentloaded' }); const timeout_ms = get_action_timeout_ms(); const action_res = await page.evaluate( async (action, payload, timeout) => { function with_timeout(promise, timeout_ms_inner) { return new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error('action_timeout')), timeout_ms_inner); promise .then((v) => { clearTimeout(timer); resolve(v); }) .catch((e) => { clearTimeout(timer); reject(e); }); }); } if (!window.server_bridge_invoke) { throw new Error('bridge 未注入 window.server_bridge_invoke'); } return await with_timeout(window.server_bridge_invoke(action, payload), timeout); }, action_name, action_payload || {}, timeout_ms ); await page.close(); return action_res; }