168 lines
8.7 KiB
JavaScript
168 lines
8.7 KiB
JavaScript
import fs from 'node:fs';
|
||
import { launchEdge, CDP, openPage, goto, evaluate, sleep } from './cdp.mjs';
|
||
|
||
const PROFILE = 'F:\\Ai Code\\CaiWuXiTong\\runtime\\private\\erp-profile';
|
||
const SECRET_FILE = process.env.ERP_SECRET_FILE
|
||
|| 'F:\\Ai Code\\CaiWuXiTong\\runtime\\private\\敏感信息.md';
|
||
|
||
export function credentials() {
|
||
const text = fs.readFileSync(SECRET_FILE, 'utf8');
|
||
const id = /账号:([^\s(]+)/.exec(text)?.[1];
|
||
const pwd = /密码:([^\s]+)/.exec(text)?.[1];
|
||
if (!id || !pwd) throw new Error('私有文件缺少聚胜算账号或密码');
|
||
return { id, pwd };
|
||
}
|
||
|
||
export const jsClickByText = (text, tag = 'button,a,span,li,div') => `(() => {
|
||
const norm = (s) => (s || '').replace(/\\s+/g, '');
|
||
const els = [...document.querySelectorAll(${JSON.stringify(tag)})]
|
||
.filter(el => norm(el.innerText) === ${JSON.stringify(text)} && (el.offsetWidth || el.offsetHeight));
|
||
if (!els.length) return 'not-found';
|
||
els[els.length - 1].click();
|
||
return 'clicked:' + els.length;
|
||
})()`;
|
||
|
||
export async function dismissDialogs(cdp, sessionId) {
|
||
const res = await evaluate(cdp, sessionId, `(() => {
|
||
const norm = (s) => (s || '').replace(/\\s+/g, '');
|
||
const dialogs = [...document.querySelectorAll('.ant-modal, .ant-modal-confirm, .layui-layer, [class*=modal], [class*=dialog]')]
|
||
.filter(d => d.offsetWidth || d.offsetHeight);
|
||
const clicked = [];
|
||
for (const d of dialogs) {
|
||
const btn = [...d.querySelectorAll('button,span,a')].find(b => /^(确定|确认|知道了|稍后修改)$/.test(norm(b.innerText)));
|
||
if (btn) { btn.click(); clicked.push(norm(btn.innerText)); }
|
||
}
|
||
return clicked;
|
||
})()`);
|
||
return res;
|
||
}
|
||
|
||
export async function realClick(cdp, sessionId, text, tag = 'button,a,span,li,div,p') {
|
||
const box = await evaluate(cdp, sessionId, `(() => {
|
||
const norm = (s) => (s || '').replace(/\\s+/g, '');
|
||
const els = [...document.querySelectorAll(${JSON.stringify(tag)})]
|
||
.filter(el => norm(el.innerText) === ${JSON.stringify(text)} && (el.offsetWidth || el.offsetHeight));
|
||
if (!els.length) return null;
|
||
const el = els[els.length - 1];
|
||
const r = el.getBoundingClientRect();
|
||
return { x: r.left + r.width / 2, y: r.top + r.height / 2, w: r.width, h: r.height };
|
||
})()`);
|
||
if (!box) return 'not-found';
|
||
const base = { x: Math.round(box.x), y: Math.round(box.y), button: 'left', clickCount: 1 };
|
||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', ...base }, sessionId);
|
||
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', ...base }, sessionId);
|
||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', ...base }, sessionId);
|
||
return `clicked@${base.x},${base.y}`;
|
||
}
|
||
|
||
export async function realClickSelector(cdp, sessionId, selector) {
|
||
const box = await evaluate(cdp, sessionId, `(() => {
|
||
const el = document.querySelector(${JSON.stringify(selector)});
|
||
if (!el) return null;
|
||
const r = el.getBoundingClientRect();
|
||
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
||
})()`);
|
||
if (!box) return 'not-found';
|
||
const base = { x: Math.round(box.x), y: Math.round(box.y), button: 'left', clickCount: 1 };
|
||
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', ...base }, sessionId);
|
||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', ...base }, sessionId);
|
||
return true;
|
||
}
|
||
|
||
export async function typeInto(cdp, sessionId, selector, value, { enter = true, clear = true } = {}) {
|
||
await realClickSelector(cdp, sessionId, selector);
|
||
if (clear) {
|
||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'a', code: 'KeyA', modifiers: 2 }, sessionId);
|
||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'a', code: 'KeyA', modifiers: 2 }, sessionId);
|
||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, sessionId);
|
||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, sessionId);
|
||
}
|
||
await cdp.send('Input.insertText', { text: value }, sessionId);
|
||
if (enter) {
|
||
await cdp.send('Input.dispatchKeyEvent', { type: 'rawKeyDown', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13 }, sessionId);
|
||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13 }, sessionId);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
export async function closeOverlays(cdp, sessionId, rounds = 6) {
|
||
for (let i = 0; i < rounds; i++) {
|
||
const left = await evaluate(cdp, sessionId, `(() => {
|
||
const vis = (el) => el.offsetWidth || el.offsetHeight;
|
||
const modals = [...document.querySelectorAll('.ant-modal, .ant-modal-root, [class*=modal]')].filter(vis);
|
||
const norm = (s) => (s || '').replace(/\\s+/g, '');
|
||
const buttons = [...document.querySelectorAll('button,span,a,div')]
|
||
.filter(el => vis(el) && /^(不再提醒|知道了|确定|确认|关闭|我知道了|稍后再说)$/.test(norm(el.innerText)));
|
||
let clicked = null;
|
||
if (buttons.length) { buttons[buttons.length - 1].click(); clicked = norm(buttons[buttons.length - 1].innerText); }
|
||
const closers = [...document.querySelectorAll('.ant-modal-close, .ant-modal-close-x, [aria-label=Close]')].filter(vis);
|
||
if (closers.length) { closers[closers.length - 1].click(); clicked = (clicked || '') + '+close'; }
|
||
return { modals: modals.length, clicked };
|
||
})()`);
|
||
await sleep(900);
|
||
if (!left.modals && !left.clicked) return left;
|
||
if (i === rounds - 1) return left;
|
||
}
|
||
}
|
||
|
||
export async function clickText(cdp, sessionId, text, { tag = 'button,a,span,li,div', log = true } = {}) {
|
||
const box = await evaluate(cdp, sessionId, `(() => {
|
||
const norm = (s) => (s || '').replace(/\\s+/g, '');
|
||
const els = [...document.querySelectorAll(${JSON.stringify(tag)})]
|
||
.filter(el => norm(el.innerText) === ${JSON.stringify(text)} && (el.offsetWidth || el.offsetHeight))
|
||
.sort((a, b) => (a.innerText || '').length - (b.innerText || '').length);
|
||
if (!els.length) return null;
|
||
const el = els[0];
|
||
const r = el.getBoundingClientRect();
|
||
return { x: r.left + r.width / 2, y: r.top + r.height / 2, html: el.outerHTML.slice(0, 160), tag: el.tagName };
|
||
})()`);
|
||
if (!box) return { ok: false, reason: 'not-found' };
|
||
const base = { x: Math.round(box.x), y: Math.round(box.y), button: 'left', clickCount: 1 };
|
||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', ...base }, sessionId);
|
||
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', ...base }, sessionId);
|
||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', ...base }, sessionId);
|
||
if (log) console.log(`click ${text} @${base.x},${base.y} <${box.tag}> ${box.html.slice(0, 120)}`);
|
||
return { ok: true, x: base.x, y: base.y, html: box.html };
|
||
}
|
||
|
||
export async function loginErp({ port = 9333, headless = true, keepBrowser = false } = {}) {
|
||
const { child, version } = await launchEdge({ port, profileDir: PROFILE, headless });
|
||
const cdp = await CDP.connect(version.webSocketDebuggerUrl);
|
||
const { sessionId } = await openPage(cdp, 'about:blank');
|
||
await goto(cdp, sessionId, 'https://www.erp321.com/login.aspx');
|
||
await sleep(2500);
|
||
|
||
const needsLogin = await evaluate(cdp, sessionId, `!!document.querySelector('#login_id')`);
|
||
if (needsLogin) {
|
||
const { id, pwd } = credentials();
|
||
await evaluate(cdp, sessionId, `(() => {
|
||
const setValue = (el, value) => {
|
||
const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), 'value')?.set;
|
||
setter ? setter.call(el, value) : (el.value = value);
|
||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||
};
|
||
setValue(document.querySelector('#login_id'), ${JSON.stringify(id)});
|
||
setValue(document.querySelector('#password'), ${JSON.stringify(pwd)});
|
||
[...document.querySelectorAll('input.ant-checkbox-input')].forEach(b => { if (!b.checked) b.click(); });
|
||
return true;
|
||
})()`);
|
||
await evaluate(cdp, sessionId, jsClickByText('立即登录'));
|
||
await sleep(5000);
|
||
await dismissDialogs(cdp, sessionId);
|
||
await sleep(3000);
|
||
// 弹窗若把跳转挡住,再确认一次并在必要时重复提交
|
||
for (let i = 0; i < 3; i++) {
|
||
const still = await evaluate(cdp, sessionId, `!!document.querySelector('#login_id')`);
|
||
if (!still) break;
|
||
await evaluate(cdp, sessionId, jsClickByText('立即登录'));
|
||
await sleep(4000);
|
||
await dismissDialogs(cdp, sessionId);
|
||
await sleep(2500);
|
||
}
|
||
}
|
||
|
||
const state = await evaluate(cdp, sessionId, `({ url: location.href, title: document.title })`);
|
||
return { child, version, cdp, sessionId, state, close: () => { cdp.close(); if (!keepBrowser) child.kill(); } };
|
||
}
|