285 lines
10 KiB
JavaScript
285 lines
10 KiB
JavaScript
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,
|
|
};
|