Files
LWLT-AI/tools/cdp_lwlt_probe.mjs
2026-07-13 19:57:46 +08:00

475 lines
17 KiB
JavaScript

#!/usr/bin/env node
const PORT = process.env.LWLT_CDP_PORT || '9223';
const TARGET_ID = process.env.LWLT_TARGET_ID || '';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
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() {
const targets = await getJson(`http://127.0.0.1:${PORT}/json`);
const pages = targets.filter((target) => target.type === 'page');
if (TARGET_ID) {
const found = pages.find((target) => target.id.startsWith(TARGET_ID));
if (!found) throw new Error(`No page target matching ${TARGET_ID}`);
return found;
}
const ltjt = pages.find((target) => target.url.includes('ltjt.yunzhi.run'));
if (!ltjt) throw new Error(`No ltjt.yunzhi.run page target on port ${PORT}`);
return ltjt;
}
class CDP {
constructor(wsUrl) {
this.wsUrl = wsUrl;
this.id = 0;
this.pending = new Map();
this.handlers = 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)) {
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);
return;
}
if (msg.method && this.handlers.has(msg.method)) {
for (const handler of this.handlers.get(msg.method)) handler(msg.params || {}, msg);
}
};
});
}
send(method, params = {}, timeoutMs = 15000) {
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 });
});
}
on(method, handler) {
if (!this.handlers.has(method)) this.handlers.set(method, new Set());
this.handlers.get(method).add(handler);
}
close() {
this.ws.close();
}
}
function expressionForMap() {
return `(() => {
const text = (value, max = 240) => String(value || '').replace(/\\s+/g, ' ').trim().slice(0, max);
const attr = (el, name) => el.getAttribute(name) || '';
const compactEl = (el) => ({
tag: el.tagName.toLowerCase(),
text: text(el.innerText || el.textContent || el.value || el.title || el.alt || '', 160),
id: el.id || '',
name: attr(el, 'name'),
className: attr(el, 'class'),
href: el.href || attr(el, 'href'),
src: el.src || attr(el, 'src'),
action: el.action || attr(el, 'action'),
method: attr(el, 'method'),
target: attr(el, 'target'),
onclick: attr(el, 'onclick'),
title: attr(el, 'title')
});
const endpointMatches = (source) => {
const found = new Set();
const re = /(?:['"])([^'"]+\\.(?:asp|aspx)(?:\\?[^'"]*)?)(?:['"])/ig;
let match;
while ((match = re.exec(source || ''))) found.add(match[1]);
return Array.from(found).slice(0, 120);
};
const collectDoc = (doc, label) => {
const pick = (selector, limit = 300) => Array.from(doc.querySelectorAll(selector)).slice(0, limit).map(compactEl);
const inlineScripts = Array.from(doc.querySelectorAll('script:not([src])')).map((script) => script.textContent || '').join('\\n');
const tables = Array.from(doc.querySelectorAll('table')).slice(0, 80).map((table) => {
const headers = Array.from(table.querySelectorAll('th')).map((th) => text(th.innerText || th.textContent, 80)).filter(Boolean);
const firstRowHeaders = headers.length ? headers : Array.from(table.querySelectorAll('tr')).slice(0, 1).flatMap((tr) => Array.from(tr.children).map((cell) => text(cell.innerText || cell.textContent, 80))).filter(Boolean);
return {
id: table.id || '',
className: attr(table, 'class'),
rows: table.querySelectorAll('tr').length,
headers: headers.length || table.querySelectorAll('tr').length <= 2 ? firstRowHeaders.slice(0, 40) : []
};
});
return {
label,
title: doc.title,
url: doc.location.href,
readyState: doc.readyState,
scripts: pick('script[src]', 120).map((el) => el.src),
inlineEndpointRefs: endpointMatches(inlineScripts),
stylesheets: pick('link[href]', 120).map((el) => el.href),
frames: pick('frame,iframe', 80),
links: pick('a[href],a[onclick],area[href],area[onclick]', 500),
forms: pick('form', 80),
buttons: pick('input[type=button],input[type=submit],button', 200),
tables,
inputs: pick('input,select,textarea', 260).map((el) => ({
tag: el.tag,
type: el.tag === 'input' ? (doc.getElementById(el.id)?.type || '') : el.tag,
id: el.id,
name: el.name,
text: el.text,
title: el.title,
valueRedacted: el.name || el.id ? '[redacted]' : ''
}))
};
};
const docs = [collectDoc(document, 'top')];
for (const frame of Array.from(window.frames)) {
try {
if (frame.document) docs.push(collectDoc(frame.document, frame.name || frame.frameElement?.id || 'frame'));
} catch (err) {
docs.push({ label: frame.name || 'cross-origin-frame', error: err.message });
}
}
return docs;
})()`;
}
async function evaluate(cdp, expression) {
const result = await cdp.send('Runtime.evaluate', {
expression,
returnByValue: true,
awaitPromise: true
}, 20000);
if (result.exceptionDetails) throw new Error(result.exceptionDetails.text || 'Evaluation failed');
return result.result.value;
}
function sanitizePostData(postData = '') {
return String(postData)
.replace(/(UserName|UserPwd|Code|session_id|password|pwd|PassWord)=([^&]*)/ig, '$1=[redacted]');
}
async function commandMap(cdp) {
await cdp.send('Runtime.enable');
return evaluate(cdp, expressionForMap());
}
async function commandCookies(cdp) {
await cdp.send('Network.enable');
const { cookies } = await cdp.send('Network.getCookies', { urls: ['https://ltjt.yunzhi.run/'] });
return cookies.map((cookie) => ({
name: cookie.name,
domain: cookie.domain,
path: cookie.path,
expires: cookie.expires,
size: cookie.size,
httpOnly: cookie.httpOnly,
secure: cookie.secure,
sameSite: cookie.sameSite || ''
}));
}
async function commandCapture(cdp, durationMs = 8000, reload = false) {
await cdp.send('Network.enable');
const requests = new Map();
cdp.on('Network.requestWillBeSent', (params) => {
const req = params.request || {};
requests.set(params.requestId, {
requestId: params.requestId,
type: params.type,
frameId: params.frameId,
documentURL: params.documentURL,
method: req.method,
url: req.url,
postData: req.postData || '',
initiatorType: params.initiator?.type || '',
timestamp: params.timestamp,
status: null,
mimeType: '',
responseUrl: ''
});
});
cdp.on('Network.responseReceived', (params) => {
const item = requests.get(params.requestId);
if (!item) return;
item.status = params.response?.status ?? null;
item.mimeType = params.response?.mimeType || '';
item.responseUrl = params.response?.url || '';
});
if (reload) await cdp.send('Page.reload');
await sleep(durationMs);
return Array.from(requests.values())
.filter((item) => item.url.includes('ltjt.yunzhi.run'))
.map((item) => ({
...item,
postData: item.postData ? sanitizePostData(item.postData) : ''
}));
}
async function waitForFrameUrl(cdp, urlNeedle, timeoutMs = 15000) {
const started = Date.now();
while (Date.now() - started < timeoutMs) {
const state = await evaluate(cdp, `(() => {
const frame = document.getElementById('Iframe_Home') || document.querySelector('iframe[name="MainIframe"], iframe');
try {
const doc = frame && frame.contentWindow && frame.contentWindow.document;
return {
url: doc ? doc.location.href : '',
readyState: doc ? doc.readyState : '',
title: doc ? doc.title : ''
};
} catch (err) {
return { error: err.message };
}
})()`).catch((err) => ({ error: err.message }));
if (state.url && state.url.includes(urlNeedle) && state.readyState === 'complete') return state;
await sleep(500);
}
return evaluate(cdp, `(() => {
const frame = document.getElementById('Iframe_Home') || document.querySelector('iframe[name="MainIframe"], iframe');
try {
const doc = frame && frame.contentWindow && frame.contentWindow.document;
return {
url: doc ? doc.location.href : '',
readyState: doc ? doc.readyState : '',
title: doc ? doc.title : ''
};
} catch (err) {
return { error: err.message };
}
})()`);
}
async function commandNavFrame(cdp, path, durationMs = 5000) {
await cdp.send('Runtime.enable');
await cdp.send('Network.enable');
const requests = [];
cdp.on('Network.requestWillBeSent', (params) => {
const req = params.request || {};
if (!req.url || !req.url.includes('ltjt.yunzhi.run')) return;
requests.push({
requestId: params.requestId,
type: params.type,
method: req.method,
url: req.url,
postData: req.postData || '',
initiatorType: params.initiator?.type || '',
status: null,
mimeType: ''
});
});
cdp.on('Network.responseReceived', (params) => {
const item = requests.find((row) => row.requestId === params.requestId);
if (!item) return;
item.status = params.response?.status ?? null;
item.mimeType = params.response?.mimeType || '';
});
const absolute = new URL(path, 'https://ltjt.yunzhi.run').href;
await evaluate(cdp, `(async () => {
const frame = document.getElementById('Iframe_Home') || document.querySelector('iframe[name="MainIframe"], iframe');
if (!frame) throw new Error('Main iframe not found');
frame.src = ${JSON.stringify(absolute)};
return true;
})()`);
const finalFrame = await waitForFrameUrl(cdp, new URL(absolute).pathname, Math.max(12000, durationMs));
await sleep(durationMs);
const docs = await commandMap(cdp);
return {
navigatedTo: absolute,
finalFrame,
requests: requests.map((item) => ({
...item,
postData: item.postData ? sanitizePostData(item.postData) : ''
})),
docs
};
}
async function commandClickMenu(cdp, label, durationMs = 5000) {
await cdp.send('Runtime.enable');
await cdp.send('Network.enable');
await cdp.send('Page.enable');
const requests = [];
const dialogs = [];
cdp.on('Page.javascriptDialogOpening', (params) => {
dialogs.push({
type: params.type,
message: params.message,
url: params.url
});
cdp.send('Page.handleJavaScriptDialog', { accept: true }).catch(() => {});
});
cdp.on('Network.requestWillBeSent', (params) => {
const req = params.request || {};
if (!req.url || !req.url.includes('ltjt.yunzhi.run')) return;
requests.push({
requestId: params.requestId,
type: params.type,
method: req.method,
url: req.url,
postData: req.postData || '',
initiatorType: params.initiator?.type || '',
status: null,
mimeType: ''
});
});
cdp.on('Network.responseReceived', (params) => {
const item = requests.find((row) => row.requestId === params.requestId);
if (!item) return;
item.status = params.response?.status ?? null;
item.mimeType = params.response?.mimeType || '';
});
const currentPage = await evaluate(cdp, `(() => ({ title: document.title, url: location.href }))()`);
if (/login/i.test(currentPage.url) || /云智办公i-5.1/.test(currentPage.title || '')) {
return {
label,
clickResult: { ok: false, reason: 'currently on login page', currentPage },
dialogs,
requests: [],
docs: await commandMap(cdp).catch((error) => ([{ error: error.message }]))
};
}
const clickResult = await evaluate(cdp, `(() => {
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
const label = ${JSON.stringify(label)};
const links = Array.from(document.querySelectorAll('a'));
const found = links.find((link) => normalize(link.innerText || link.textContent) === label);
if (!found) return { ok: false, reason: 'link not found', available: links.map((link) => normalize(link.innerText || link.textContent)).filter(Boolean).slice(0, 120) };
found.click();
return {
ok: true,
text: normalize(found.innerText || found.textContent),
href: found.href || found.getAttribute('href') || '',
onclick: found.getAttribute('onclick') || ''
};
})()`);
await sleep(durationMs);
const docs = await commandMap(cdp).catch((error) => ([{ error: error.message }]));
return {
label,
clickResult,
dialogs,
requests: requests.map((item) => ({
...item,
postData: item.postData ? sanitizePostData(item.postData) : ''
})),
docs
};
}
function summarizeDocs(docs) {
return docs.map((doc) => ({
label: doc.label,
title: doc.title,
url: doc.url,
scripts: doc.scripts || [],
inlineEndpointRefs: doc.inlineEndpointRefs || [],
forms: (doc.forms || []).map((form) => ({
id: form.id,
name: form.name,
action: form.action,
method: form.method,
text: form.text
})),
inputs: (doc.inputs || []).map((input) => ({
tag: input.tag,
type: input.type,
id: input.id,
name: input.name,
text: input.text,
title: input.title
})),
buttons: (doc.buttons || []).map((button) => ({
text: button.text,
id: button.id,
name: button.name,
onclick: button.onclick,
title: button.title
})),
links: (doc.links || []).map((link) => ({
text: link.text,
href: link.href,
onclick: link.onclick,
title: link.title
})),
tables: doc.tables || []
}));
}
async function commandBatchMenus(cdp, labels, durationMs = 4500) {
const results = [];
for (const label of labels) {
const before = await commandMap(cdp).catch(() => []);
const result = await commandClickMenu(cdp, label, durationMs);
const afterDocs = summarizeDocs(result.docs || []);
const newOrMatchingFrames = afterDocs.filter((doc) => {
if (doc.label === 'top') return false;
const existed = before.some((oldDoc) => oldDoc.url === doc.url && oldDoc.label === doc.label);
return !existed || doc.title === label || doc.url.includes('/System/Business/');
});
results.push({
label,
clickResult: result.clickResult,
dialogs: result.dialogs,
requests: result.requests
.filter((request) => request.type === 'Document' || request.type === 'XHR')
.map((request) => ({
method: request.method,
status: request.status,
type: request.type,
mimeType: request.mimeType,
url: request.url,
postData: request.postData
})),
frames: newOrMatchingFrames
});
const latestTop = (result.docs || []).find((doc) => doc.label === 'top');
if (latestTop && (/login/i.test(latestTop.url || '') || /云智办公i-5.1/.test(latestTop.title || ''))) break;
}
return results;
}
async function main() {
const command = process.argv[2] || 'map';
const target = await getTarget();
const cdp = new CDP(target.webSocketDebuggerUrl);
await cdp.connect();
try {
let output;
if (command === 'target') output = target;
else if (command === 'map') output = await commandMap(cdp);
else if (command === 'cookies') output = await commandCookies(cdp);
else if (command === 'capture') output = await commandCapture(cdp, Number(process.argv[3] || 8000), process.argv.includes('--reload'));
else if (command === 'navframe') output = await commandNavFrame(cdp, process.argv[3], Number(process.argv[4] || 5000));
else if (command === 'clickmenu') output = await commandClickMenu(cdp, process.argv[3], Number(process.argv[4] || 5000));
else if (command === 'batchmenus') output = await commandBatchMenus(cdp, JSON.parse(process.argv[3]), Number(process.argv[4] || 4500));
else throw new Error(`Unknown command: ${command}`);
console.log(JSON.stringify(output, null, 2));
} finally {
cdp.close();
}
}
main().catch((error) => {
console.error(error.stack || error.message);
process.exit(1);
});