168 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(); } };
}