Files
20260324-42433647/auto-login.mjs
2026-04-25 21:50:03 +08:00

339 lines
12 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.
/**
* 全自动登录店小秘(含验证码 OCR
* 登录成功后保存 Cookie + 截图后台页面结构
*/
import { chromium } from 'playwright';
import { writeFileSync, readFileSync, existsSync, mkdirSync, unlinkSync } from 'fs';
import { execSync } from 'child_process';
import sharp from 'sharp';
const COOKIE_FILE = './cookies.json';
const SCREENSHOTS_DIR = './screenshots';
const MAX_CAPTCHA_RETRIES = 10;
mkdirSync(SCREENSHOTS_DIR, { recursive: true });
// ====== 验证码 OCR ======
async function solveCaptcha(page) {
// 找到验证码图片
const captchaImg = await page.$('#verifyCodeImg, img[id*="verify"], img[id*="captcha"], img[src*="verify"], img[src*="captcha"], .verify-code img, img[onclick*="verify"]');
if (!captchaImg) {
// 如果没有独立 img 标签,可能是 canvas 或背景图,尝试找验证码区域
console.log(' 未找到验证码 img 标签,尝试其他方式...');
// 尝试通过验证码输入框附近的图片
const codeInput = await page.$('#verifyCode, input[name="verifyCode"]');
if (codeInput) {
const parent = await codeInput.evaluateHandle(el => el.parentElement);
const nearbyImg = await page.evaluateHandle(
el => el.querySelector('img') || el.nextElementSibling?.querySelector('img') || el.parentElement?.querySelector('img'),
parent
);
if (nearbyImg) {
const captchaPath = `${SCREENSHOTS_DIR}/captcha.png`;
await nearbyImg.asElement()?.screenshot({ path: captchaPath });
if (existsSync(captchaPath)) {
return await ocrImage(captchaPath);
}
}
}
// 最后尝试:截图整个验证码区域
console.log(' 尝试截图验证码区域...');
// 找到 "点击刷新" 按钮附近的图片
const refreshBtn = await page.$('button:has-text("点击刷新"), a:has-text("点击刷新"), span:has-text("点击刷新")');
if (refreshBtn) {
// 验证码可能在刷新按钮的兄弟元素中
const captchaArea = await page.evaluateHandle(
btn => btn.previousElementSibling || btn.parentElement,
refreshBtn
);
const imgEl = await captchaArea.asElement();
if (imgEl) {
const captchaPath = `${SCREENSHOTS_DIR}/captcha.png`;
await imgEl.screenshot({ path: captchaPath });
return await ocrImage(captchaPath);
}
}
return null;
}
// 截图验证码图片
const captchaPath = `${SCREENSHOTS_DIR}/captcha.png`;
await captchaImg.screenshot({ path: captchaPath });
console.log(' 验证码图片已截图');
return await ocrImage(captchaPath);
}
async function ocrImage(imagePath) {
try {
// 预处理:灰度 + 高对比度 + 放大 + 二值化
const processedPath = imagePath.replace('.png', '_processed.png');
await sharp(imagePath)
.grayscale()
.resize({ width: 400, kernel: 'lanczos3' }) // 放大
.normalize() // 增强对比
.sharpen({ sigma: 2 }) // 锐化
.threshold(128) // 二值化
.toFile(processedPath);
// 调用 Tesseract OCR
const result = execSync(
`tesseract "${processedPath}" stdout --psm 7 -c tessedit_char_whitelist=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`,
{ encoding: 'utf-8', timeout: 10000 }
).trim().replace(/\s/g, '');
console.log(` OCR 结果: "${result}"`);
return result;
} catch (e) {
console.error(' OCR 失败:', e.message);
return null;
}
}
// ====== 刷新验证码 ======
async function refreshCaptcha(page) {
// 尝试点击刷新按钮或验证码图片本身
const refreshBtn = await page.$('button:has-text("点击刷新")');
if (refreshBtn) {
await refreshBtn.click();
await page.waitForTimeout(1000);
return;
}
// 点击验证码图片刷新
const captchaImg = await page.$('#verifyCodeImg, img[id*="verify"], img[src*="verify"], img[src*="captcha"]');
if (captchaImg) {
await captchaImg.click();
await page.waitForTimeout(1000);
}
}
// ====== 主流程 ======
async function main() {
console.log('====== 店小秘全自动登录 ======\n');
const browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 },
locale: 'zh-CN',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
});
const page = await context.newPage();
try {
// 尝试用已有 Cookie 直接访问后台
if (existsSync(COOKIE_FILE)) {
console.log('>> 发现已有 Cookie尝试复用...');
const cookies = JSON.parse(readFileSync(COOKIE_FILE, 'utf-8'));
await context.addCookies(cookies);
await page.goto('https://www.dianxiaomi.com/saleManage/index.htm', { waitUntil: 'networkidle', timeout: 30000 });
if (!page.url().includes('/home.htm') && !page.url().includes('/index.htm')) {
console.log('>> Cookie 有效已直接进入后台URL:', page.url());
await exploreDashboard(page);
await browser.close();
return true;
}
console.log('>> Cookie 已过期,需要重新登录\n');
}
// 打开登录页
console.log('>> 打开登录页...');
await page.goto('https://www.dianxiaomi.com/home.htm', { waitUntil: 'networkidle', timeout: 30000 });
// 分析页面上的验证码结构
console.log('>> 分析验证码结构...');
const pageInfo = await page.evaluate(() => {
const imgs = Array.from(document.querySelectorAll('img'));
return imgs.map(img => ({
id: img.id,
src: img.src?.substring(0, 100),
className: img.className,
width: img.width,
height: img.height,
alt: img.alt,
}));
});
console.log(' 页面图片:', JSON.stringify(pageInfo, null, 2));
// 多次尝试登录
for (let attempt = 1; attempt <= MAX_CAPTCHA_RETRIES; attempt++) {
console.log(`\n>> 第 ${attempt}/${MAX_CAPTCHA_RETRIES} 次尝试登录...`);
// 填写账号密码
await page.fill('input[name="account"]', '');
await page.fill('input[name="account"]', 'MiLe-kf01');
await page.fill('input[type="password"]', '');
await page.fill('input[type="password"]', 'Vxdas@302');
// 勾选记住我
const rememberCheckbox = await page.$('input[name="remeber"]');
if (rememberCheckbox) {
await rememberCheckbox.check().catch(() => {});
}
// 刷新验证码(确保是新的)
if (attempt > 1) {
await refreshCaptcha(page);
}
// OCR 验证码
const captchaCode = await solveCaptcha(page);
if (!captchaCode || captchaCode.length < 3) {
console.log(` 验证码识别结果太短 ("${captchaCode}"),刷新重试...`);
await refreshCaptcha(page);
continue;
}
// 填入验证码
await page.fill('#verifyCode, input[name="verifyCode"]', '');
await page.fill('#verifyCode, input[name="verifyCode"]', captchaCode);
// 截图确认
await page.screenshot({ path: `${SCREENSHOTS_DIR}/attempt-${attempt}.png` });
// 点击登录
const loginBtn = await page.$('button:has-text("登录")');
if (loginBtn) {
await loginBtn.click();
} else {
console.log(' 未找到登录按钮!');
continue;
}
// 等待页面响应
await page.waitForTimeout(3000);
// 检查是否登录成功
const currentUrl = page.url();
console.log(` 登录后 URL: ${currentUrl}`);
// 检查是否有错误提示
const errorMsg = await page.$eval('.error-msg, .alert-danger, .login-error, .msg-error, .text-danger', el => el.textContent.trim()).catch(() => null);
if (errorMsg) {
console.log(` 登录失败: ${errorMsg}`);
await refreshCaptcha(page);
continue;
}
// 检查 URL 是否变化(离开登录页 = 成功)
if (!currentUrl.includes('/home.htm')) {
console.log('>> 登录成功!');
// 保存 Cookie
const cookies = await context.cookies();
writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2));
console.log(`>> Cookie 已保存(${cookies.length} 条)`);
await page.screenshot({ path: `${SCREENSHOTS_DIR}/05-login-success.png`, fullPage: true });
// 探索后台
await exploreDashboard(page);
await browser.close();
return true;
}
console.log(' 仍在登录页,验证码可能错误,重试...');
await refreshCaptcha(page);
}
console.log('\n>> 登录失败:验证码识别 ' + MAX_CAPTCHA_RETRIES + ' 次均未成功');
await page.screenshot({ path: `${SCREENSHOTS_DIR}/login-failed.png`, fullPage: true });
await browser.close();
return false;
} catch (err) {
console.error('致命错误:', err.message);
await page.screenshot({ path: `${SCREENSHOTS_DIR}/error.png`, fullPage: true }).catch(() => {});
await browser.close();
return false;
}
}
// ====== 探索后台页面结构 ======
async function exploreDashboard(page) {
console.log('\n>> ===== 探索后台页面 =====');
console.log('>> 当前 URL:', page.url());
// 截图
await page.screenshot({ path: `${SCREENSHOTS_DIR}/dashboard.png`, fullPage: true });
// 收集所有链接
const allLinks = await page.$$eval('a', els =>
els.map(el => ({
text: el.textContent.trim().replace(/\s+/g, ' ').substring(0, 50),
href: el.href,
}))
.filter(e => e.text && e.href && e.href.includes('dianxiaomi.com'))
.filter((e, i, arr) => arr.findIndex(a => a.href === e.href) === i)
);
console.log(`\n>> 共 ${allLinks.length} 个链接:`);
for (const link of allLinks) {
const tag = (link.text.includes('采购') || link.text.includes('仓库') ||
link.text.includes('导出') || link.text.includes('库存') ||
link.text.includes('备货')) ? '★' : ' ';
console.log(` ${tag} [${link.text}] -> ${link.href}`);
}
// 尝试直接访问常见的采购管理页面路径
const possiblePaths = [
'/saleManage/index.htm',
'/purchaseManage/purchaseSuggestion.htm',
'/purchaseManage/index.htm',
'/warehouse/index.htm',
'/stockManage/index.htm',
'/inventory/index.htm',
];
console.log('\n>> 尝试常见后台路径:');
for (const path of possiblePaths) {
const url = `https://www.dianxiaomi.com${path}`;
const resp = await page.goto(url, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => null);
if (resp) {
const finalUrl = page.url();
const title = await page.title();
const status = resp.status();
console.log(` ${status === 200 && !finalUrl.includes('/home.htm') ? '✓' : '✗'} ${path} -> ${finalUrl} [${title}]`);
if (status === 200 && !finalUrl.includes('/home.htm')) {
await page.screenshot({ path: `${SCREENSHOTS_DIR}/page-${path.replace(/\//g, '_')}.png`, fullPage: true });
// 在这个页面上找导出按钮
const exportBtns = await page.$$eval(
'button, a, span, div',
els => els
.filter(el => el.textContent.includes('导出') || el.textContent.includes('下载'))
.map(el => ({
tag: el.tagName,
text: el.textContent.trim().substring(0, 40),
id: el.id,
className: el.className?.substring(0, 60),
}))
.slice(0, 20)
);
if (exportBtns.length > 0) {
console.log(` 导出相关按钮:`);
for (const btn of exportBtns) {
console.log(` ${btn.tag}#${btn.id}.${btn.className}: "${btn.text}"`);
}
}
}
}
}
}
const success = await main();
process.exit(success ? 0 : 1);