285 lines
9.6 KiB
JavaScript
285 lines
9.6 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
import { dirname, resolve } from 'node:path';
|
|
|
|
const PORT = process.env.LWLT_CDP_PORT || '9223';
|
|
|
|
function parseArgs(argv) {
|
|
const args = {};
|
|
for (let i = 0; i < argv.length; i += 1) {
|
|
const arg = argv[i];
|
|
if (!arg.startsWith('--')) continue;
|
|
const key = arg.slice(2);
|
|
const next = argv[i + 1];
|
|
if (!next || next.startsWith('--')) args[key] = true;
|
|
else {
|
|
args[key] = next;
|
|
i += 1;
|
|
}
|
|
}
|
|
return args;
|
|
}
|
|
|
|
function writeJson(path, data) {
|
|
mkdirSync(dirname(resolve(path)), { recursive: true });
|
|
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
|
|
}
|
|
|
|
async function getJson(url) {
|
|
const res = await fetch(url);
|
|
if (!res.ok) throw new Error(`${res.status} ${res.statusText}: ${url}`);
|
|
return res.json();
|
|
}
|
|
|
|
async function getTarget(port) {
|
|
const targets = await getJson(`http://127.0.0.1:${port}/json`);
|
|
const target = targets.find((item) => item.type === 'page' && item.url.includes('ltjt.yunzhi.run'));
|
|
if (!target) throw new Error(`No ltjt.yunzhi.run page target on port ${port}`);
|
|
return target;
|
|
}
|
|
|
|
class CDP {
|
|
constructor(wsUrl) {
|
|
this.wsUrl = wsUrl;
|
|
this.id = 0;
|
|
this.pending = new Map();
|
|
}
|
|
|
|
connect() {
|
|
return new Promise((resolve, reject) => {
|
|
this.ws = new WebSocket(this.wsUrl);
|
|
this.ws.onopen = resolve;
|
|
this.ws.onerror = (event) => reject(new Error(event.message || event.type || 'WebSocket error'));
|
|
this.ws.onmessage = (event) => {
|
|
const msg = JSON.parse(event.data);
|
|
if (!msg.id || !this.pending.has(msg.id)) return;
|
|
const { resolve: ok, reject: fail, timer } = this.pending.get(msg.id);
|
|
clearTimeout(timer);
|
|
this.pending.delete(msg.id);
|
|
msg.error ? fail(new Error(msg.error.message)) : ok(msg.result);
|
|
};
|
|
});
|
|
}
|
|
|
|
send(method, params = {}, timeoutMs = 30000) {
|
|
const id = ++this.id;
|
|
this.ws.send(JSON.stringify({ id, method, params }));
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
this.pending.delete(id);
|
|
reject(new Error(`Timeout: ${method}`));
|
|
}, timeoutMs);
|
|
this.pending.set(id, { resolve, reject, timer });
|
|
});
|
|
}
|
|
|
|
close() {
|
|
this.ws?.close();
|
|
}
|
|
}
|
|
|
|
async function evaluate(cdp, expression, timeoutMs = 30000) {
|
|
const result = await cdp.send('Runtime.evaluate', {
|
|
expression,
|
|
returnByValue: true,
|
|
awaitPromise: true
|
|
}, timeoutMs);
|
|
if (result.exceptionDetails) throw new Error(result.exceptionDetails.text || 'Runtime.evaluate failed');
|
|
return result.result.value;
|
|
}
|
|
|
|
function inspectionExpression(openFresh) {
|
|
return `(async () => {
|
|
const openFresh = ${JSON.stringify(Boolean(openFresh))};
|
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
const clean = (value, max = 500) => String(value || '').replace(/\\s+/g, ' ').trim().slice(0, max);
|
|
const entryUrl = 'https://ltjt.yunzhi.run/System/Business/orders_add.asp?fabudanwei=' + encodeURIComponent('老挝联泰') + '©=0&ddid=0&_=' + Date.now();
|
|
const entryStamp = new URL(entryUrl).searchParams.get('_');
|
|
|
|
function findOrderWindow() {
|
|
const queue = [window];
|
|
while (queue.length) {
|
|
const win = queue.shift();
|
|
let doc;
|
|
try { doc = win.document; } catch (err) { continue; }
|
|
if (doc.querySelector('#ListForm') && doc.location.href.includes('/orders_add.asp')) return win;
|
|
Array.from(win.frames).forEach((frame) => queue.push(frame));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
if (openFresh) {
|
|
const frame = document.getElementById('Iframe_Home') || document.querySelector('iframe[name="MainIframe"], iframe');
|
|
if (frame) frame.src = entryUrl;
|
|
}
|
|
|
|
const started = Date.now();
|
|
let orderWin = findOrderWindow();
|
|
while ((!orderWin || !orderWin.document.querySelector('#ListForm') || orderWin.document.querySelector('#ListForm').elements.length < 800 || (openFresh && !orderWin.document.location.href.includes('_=' + entryStamp))) && Date.now() - started < 25000) {
|
|
await sleep(500);
|
|
orderWin = findOrderWindow();
|
|
}
|
|
if (!orderWin) return { ok: false, status: 'orders_add_form_not_found' };
|
|
|
|
const doc = orderWin.document;
|
|
const form = doc.querySelector('#ListForm');
|
|
const inlineScript = Array.from(doc.querySelectorAll('script:not([src])')).map((script) => script.textContent || '').join('\\n');
|
|
|
|
function splitTopLevelArgs(text) {
|
|
const out = [];
|
|
let start = 0;
|
|
let depth = 0;
|
|
let quote = '';
|
|
for (let i = 0; i < text.length; i += 1) {
|
|
const ch = text[i];
|
|
const prev = text[i - 1];
|
|
if (quote) {
|
|
if (ch === quote && prev !== '\\\\') quote = '';
|
|
continue;
|
|
}
|
|
if (ch === '"' || ch === "'" || ch === String.fromCharCode(96)) {
|
|
quote = ch;
|
|
continue;
|
|
}
|
|
if (ch === '(' || ch === '{' || ch === '[') depth += 1;
|
|
else if (ch === ')' || ch === '}' || ch === ']') depth -= 1;
|
|
else if (ch === ',' && depth === 0) {
|
|
out.push(text.slice(start, i).trim());
|
|
start = i + 1;
|
|
}
|
|
}
|
|
out.push(text.slice(start).trim());
|
|
return out.filter(Boolean);
|
|
}
|
|
|
|
function parseObjectLiteralArgs(body) {
|
|
const item = {};
|
|
for (const part of splitTopLevelArgs(body)) {
|
|
const idx = part.indexOf(':');
|
|
if (idx < 0) continue;
|
|
const key = part.slice(0, idx).trim().replace(/^['"]|['"]$/g, '');
|
|
const raw = part.slice(idx + 1).trim();
|
|
item[key] = clean(raw, 300);
|
|
}
|
|
return item;
|
|
}
|
|
|
|
const snippets = [];
|
|
const re = /SelectBox\\.set\\s*\\(\\s*\\{([\\s\\S]*?)\\}\\s*\\)\\s*;/g;
|
|
let match;
|
|
while ((match = re.exec(inlineScript))) {
|
|
snippets.push({
|
|
snippet: clean(match[0], 600),
|
|
config: parseObjectLiteralArgs(match[1])
|
|
});
|
|
}
|
|
|
|
const endpointRefs = Array.from(new Set((inlineScript.match(/(?:\\.\\.\\/|\\/)?(?:DAT|dat)\\/AjaxPublicFun\\.asp\\?[^"'\\s+)]+/gi) || [])
|
|
.map((value) => value.replace(/&/g, '&'))));
|
|
|
|
const variableNames = [
|
|
'Shouxiangmu_Data',
|
|
'Jipao_Data',
|
|
'canting_Datas',
|
|
'jiudian_Datas',
|
|
'jingdian_Datas',
|
|
'zhu_Datas',
|
|
'zao_Datas',
|
|
'zhong_Datas',
|
|
'wan_Datas'
|
|
];
|
|
|
|
function summarizeDataString(value) {
|
|
const str = String(value || '');
|
|
const separators = ['◇', '◆', '^^', '\\n', '\\r\\n', '|', ',', String.fromCharCode(9)];
|
|
const sepCounts = Object.fromEntries(separators.map((sep) => [sep === String.fromCharCode(9) ? 'TAB' : sep, str.split(sep).length - 1]));
|
|
const rowSep = sepCounts['◇'] > 0 ? '◇' : (sepCounts['\\n'] > 0 ? '\\n' : '');
|
|
const rows = rowSep ? str.split(rowSep).filter(Boolean) : (str ? [str] : []);
|
|
const columnCountHistogram = {};
|
|
for (const row of rows.slice(0, 200)) {
|
|
const counts = ['◆', '^', '|', ',', String.fromCharCode(9)].map((sep) => row.split(sep).length);
|
|
const max = Math.max(...counts);
|
|
columnCountHistogram[max] = (columnCountHistogram[max] || 0) + 1;
|
|
}
|
|
return {
|
|
type: typeof value,
|
|
length: str.length,
|
|
separator_counts: sepCounts,
|
|
inferred_row_separator: rowSep,
|
|
inferred_row_count: rows.length,
|
|
column_count_histogram_first_200_rows: columnCountHistogram,
|
|
value_redacted: true
|
|
};
|
|
}
|
|
|
|
const dataVariables = {};
|
|
for (const name of variableNames) {
|
|
dataVariables[name] = summarizeDataString(orderWin[name]);
|
|
}
|
|
dataVariables['window.top.QJ_Yuangong_Data'] = summarizeDataString(orderWin.top.QJ_Yuangong_Data);
|
|
|
|
const selectLinkedFields = Array.from(form.elements)
|
|
.filter((el) => {
|
|
const name = el.name || el.id || '';
|
|
return /^(TianShu|zhuanxianming|chanpinming|zutuanshe|gendanren|xiaoshouren|ys_|banhao|zhusu|zao|zaos|zhong|zhongs|wan|wans|jingdian|Text)/.test(name);
|
|
})
|
|
.map((el) => ({
|
|
name: el.name || '',
|
|
id: el.id || '',
|
|
tag: el.tagName.toLowerCase(),
|
|
type: el.type || '',
|
|
readonly: Boolean(el.readOnly),
|
|
hidden: el.type === 'hidden' || el.offsetParent == null,
|
|
required: el.getAttribute('MastInput') === '1' || el.required
|
|
}));
|
|
|
|
return {
|
|
ok: true,
|
|
status: 'selectbox_inspected',
|
|
page: {
|
|
url: doc.location.href,
|
|
title: doc.title,
|
|
form_element_count: form.elements.length
|
|
},
|
|
selectbox_count: snippets.length,
|
|
selectboxes: snippets,
|
|
endpoint_refs: endpointRefs,
|
|
data_variables: dataVariables,
|
|
select_linked_fields: selectLinkedFields
|
|
};
|
|
})()`;
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
const port = args.port || PORT;
|
|
const target = await getTarget(port);
|
|
const cdp = new CDP(target.webSocketDebuggerUrl);
|
|
await cdp.connect();
|
|
try {
|
|
await cdp.send('Runtime.enable');
|
|
const output = await evaluate(cdp, inspectionExpression(args['open-form']), 45000);
|
|
if (args.out) writeJson(args.out, output);
|
|
console.log(JSON.stringify({
|
|
status: output.status,
|
|
page: output.page,
|
|
selectbox_count: output.selectbox_count,
|
|
endpoint_refs: output.endpoint_refs?.length || 0,
|
|
data_variables: output.data_variables ? Object.fromEntries(Object.entries(output.data_variables).map(([key, value]) => [key, {
|
|
length: value.length,
|
|
inferred_row_count: value.inferred_row_count,
|
|
inferred_row_separator: value.inferred_row_separator
|
|
}])) : {},
|
|
out: args.out || ''
|
|
}, null, 2));
|
|
} finally {
|
|
cdp.close();
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error.stack || error.message);
|
|
process.exit(1);
|
|
});
|