const fs = require('node:fs/promises'); const http = require('node:http'); const path = require('node:path'); const childProcess = require('node:child_process'); function isLiveSessionEnabled(runtime = {}) { return Boolean(runtime.liveSession && runtime.liveSession.enabled); } function liveSessionPort(runtime = {}) { const live = runtime.liveSession || {}; return Number(live.remoteDebuggingPort || live.port || 9222); } function liveSessionHost(runtime = {}) { const live = runtime.liveSession || {}; return live.host || '127.0.0.1'; } function formatHostForEndpoint(host) { const value = String(host || '').trim(); if (value.includes(':') && !value.startsWith('[')) return `[${value}]`; return value; } function liveSessionHostCandidates(runtime = {}) { const live = runtime.liveSession || {}; if (live.host) return [String(live.host)]; const configured = Array.isArray(live.hostCandidates) ? live.hostCandidates : []; return [...new Set([...configured, '127.0.0.1', '::1'].filter(Boolean).map(String))]; } function devToolsEndpoint(runtime = {}, host = liveSessionHost(runtime)) { return `http://${formatHostForEndpoint(host)}:${liveSessionPort(runtime)}`; } function candidateDevToolsEndpoints(runtime = {}) { return liveSessionHostCandidates(runtime).map((host) => devToolsEndpoint(runtime, host)); } function buildLiveSessionChromeArgs(options = {}) { const args = Array.isArray(options.launchArgs) ? [...options.launchArgs] : []; const port = Number(options.remoteDebuggingPort || options.port || 9222); const profileDir = options.profileDir || ''; const startUrl = options.startUrl || ''; const hasArg = (prefix) => args.some((arg) => String(arg).startsWith(prefix)); if (!hasArg('--remote-debugging-port=')) args.push(`--remote-debugging-port=${port}`); if (profileDir && !hasArg('--user-data-dir=')) args.push(`--user-data-dir=${profileDir}`); if (!args.includes('--no-first-run')) args.push('--no-first-run'); if (!args.includes('--no-default-browser-check')) args.push('--no-default-browser-check'); if (startUrl) args.push(startUrl); return args; } function requestJson(url, timeoutMs = 1500) { return new Promise((resolve, reject) => { const request = http.get(url, { timeout: timeoutMs }, (response) => { let body = ''; response.setEncoding('utf8'); response.on('data', (chunk) => { body += chunk; }); response.on('end', () => { if (response.statusCode < 200 || response.statusCode >= 300) { reject(new Error(`DevTools HTTP ${response.statusCode}`)); return; } try { resolve(JSON.parse(body)); } catch (error) { reject(error); } }); }); request.on('timeout', () => { request.destroy(new Error('DevTools request timed out')); }); request.on('error', reject); }); } function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function isDevToolsAvailable(runtime = {}, deps = {}) { try { return Boolean(await selectDevToolsEndpoint(runtime, deps)); } catch { return false; } } function scoreDevToolsPage(page = {}, runtime = {}) { const url = String(page.url || ''); const title = String(page.title || ''); if (!url || url.startsWith('chrome://')) return 0; if (/loginlt\.html/i.test(url) || /验证码|账\s*号|密\s*码|用户登录/.test(title)) return 1; if (/Mainlt\.asp|orders\.asp|Business\//i.test(url)) return 100; if (/老挝联泰|独立团|散拼团|业务操作|LW5/i.test(title)) return 80; const baseUrls = [runtime.baseUrl]; for (const configuredUrl of [runtime.mainUrl, runtime.ordersUrl]) { try { if (configuredUrl) baseUrls.push(new URL(configuredUrl).origin); } catch { // Ignore malformed optional URLs; they should not block session selection. } } if (baseUrls.some((baseUrl) => url.startsWith(baseUrl))) return 20; return 0; } async function probeDevToolsEndpoint(runtime = {}, endpoint = '', deps = {}) { const requester = deps.requestJson || requestJson; const timeoutMs = runtime.liveSession && runtime.liveSession.probeTimeoutMs; await requester(`${endpoint}/json/version`, timeoutMs); let pages = []; try { const list = await requester(`${endpoint}/json/list`, timeoutMs); if (Array.isArray(list)) pages = list; } catch { pages = []; } const score = pages.reduce((best, page) => Math.max(best, scoreDevToolsPage(page, runtime)), 0); return { endpoint, pages, score }; } async function selectDevToolsEndpoint(runtime = {}, deps = {}) { const endpoints = await selectDevToolsEndpoints(runtime, deps); return endpoints[0] || ''; } async function selectDevToolsEndpoints(runtime = {}, deps = {}) { const endpoints = candidateDevToolsEndpoints(runtime); const probes = await Promise.all(endpoints.map(async (endpoint, index) => { try { return { ...(await probeDevToolsEndpoint(runtime, endpoint, deps)), index }; } catch (error) { return { endpoint, index, error }; } })); const available = probes.filter((probe) => !probe.error); if (!available.length) return []; available.sort((a, b) => (b.score - a.score) || (a.index - b.index)); return available.map((probe) => probe.endpoint); } async function waitForDevTools(runtime = {}, deps = {}) { const timeoutMs = Number((runtime.liveSession && runtime.liveSession.startTimeoutMs) || 30000); const pollMs = Number((runtime.liveSession && runtime.liveSession.pollMs) || 500); const deadline = Date.now() + timeoutMs; let lastError = null; while (Date.now() < deadline) { if (await isDevToolsAvailable(runtime, deps)) return true; await new Promise((resolve) => setTimeout(resolve, pollMs)); } if (lastError) throw lastError; throw new Error(`Timed out waiting for Chrome DevTools at ${devToolsEndpoint(runtime)}`); } function selectChrome(runtime = {}) { const candidates = [runtime.chromeExecutable, ...(runtime.chromeCandidates || [])].filter(Boolean); return candidates[0] || ''; } async function ensureLiveChrome(runtime = {}, deps = {}) { const alreadyAvailable = deps.isDevToolsAvailable || isDevToolsAvailable; const hasCustomAvailability = Boolean(deps.isDevToolsAvailable); if (await alreadyAvailable(runtime, deps)) { const endpoint = hasCustomAvailability && !deps.selectDevToolsEndpoint ? devToolsEndpoint(runtime) : await (deps.selectDevToolsEndpoint || selectDevToolsEndpoint)(runtime, deps); return { started: false, endpoint: endpoint || devToolsEndpoint(runtime) }; } const executablePath = deps.selectChrome ? deps.selectChrome(runtime) : selectChrome(runtime); if (!executablePath) { throw new Error('Chrome executable not found. Set browser.chromeExecutable or browser.chromeCandidates.'); } if (runtime.profileDir) await fs.mkdir(runtime.profileDir, { recursive: true }); const args = buildLiveSessionChromeArgs({ profileDir: runtime.profileDir, remoteDebuggingPort: liveSessionPort(runtime), launchArgs: runtime.launchArgs || [], startUrl: (runtime.liveSession && runtime.liveSession.startUrl) || runtime.mainUrl || runtime.ordersUrl || '', }); const spawn = deps.spawn || childProcess.spawn; const child = spawn(executablePath, args, { detached: true, stdio: 'ignore', }); if (child && typeof child.unref === 'function') child.unref(); const waiter = deps.waitForDevTools || waitForDevTools; await waiter(runtime, deps); const endpoint = deps.selectDevToolsEndpoint ? await deps.selectDevToolsEndpoint(runtime, deps) : await selectDevToolsEndpoint(runtime, deps); return { started: true, endpoint: endpoint || devToolsEndpoint(runtime), executablePath, args }; } function wrapLiveContext(context, browser) { return new Proxy(context, { get(target, prop, receiver) { if (prop === 'close') { return async () => { if (browser && typeof browser.disconnect === 'function') { browser.disconnect(); return; } if (browser && typeof browser.close === 'function') { await browser.close(); } }; } const value = Reflect.get(target, prop, receiver); return typeof value === 'function' ? value.bind(target) : value; }, }); } async function connectLiveContext(runtime = {}, deps = {}) { const liveChrome = await ensureLiveChrome(runtime, deps); const endpoints = []; const addEndpoint = (endpoint) => { if (endpoint && !endpoints.includes(endpoint)) endpoints.push(endpoint); }; addEndpoint(liveChrome.endpoint || devToolsEndpoint(runtime)); const canProbeFallbacks = !deps.isDevToolsAvailable || deps.requestJson || deps.selectDevToolsEndpoint || deps.selectDevToolsEndpoints; if (canProbeFallbacks) { const selectedEndpoints = await (deps.selectDevToolsEndpoints || selectDevToolsEndpoints)(runtime, deps).catch(() => []); for (const selectedEndpoint of selectedEndpoints) addEndpoint(selectedEndpoint); } const chromium = deps.chromium || require('playwright-core').chromium; const live = runtime.liveSession || {}; const attempts = Number(live.connectAttempts || 2); const retryDelayMs = Number(live.connectRetryDelayMs ?? 1000); let browser = null; let lastError = null; for (let endpointIndex = 0; endpointIndex < endpoints.length; endpointIndex += 1) { const endpoint = endpoints[endpointIndex]; for (let attempt = 1; attempt <= attempts; attempt += 1) { try { browser = await chromium.connectOverCDP(endpoint, { timeout: Number(live.connectTimeoutMs || 30000), }); break; } catch (error) { lastError = error; const hasNextEndpoint = endpointIndex < endpoints.length - 1; const shouldTryNextEndpoint = hasNextEndpoint && /Timeout/i.test(String(error && error.message || error || '')); if (shouldTryNextEndpoint || attempt >= attempts) break; await delay(retryDelayMs); } } if (browser) break; } if (!browser) throw lastError || new Error(`Unable to connect to live Chrome at ${devToolsEndpoint(runtime)}`); let context = browser.contexts()[0]; if (!context && typeof browser.newContext === 'function') { context = await browser.newContext(); } if (!context) throw new Error('No browser context available from live ERP Chrome.'); return wrapLiveContext(context, browser); } module.exports = { isLiveSessionEnabled, liveSessionPort, liveSessionHost, devToolsEndpoint, buildLiveSessionChromeArgs, requestJson, isDevToolsAvailable, selectDevToolsEndpoint, selectDevToolsEndpoints, waitForDevTools, selectChrome, ensureLiveChrome, wrapLiveContext, connectLiveContext, };