Files
LWLT-AI/archive/handoff/2026-07-12/legacy-erp-handoff/tools/erp_live_session_manager.js
2026-07-13 19:57:46 +08:00

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,
};