#!/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); });