init repo
This commit is contained in:
338
auto-login.mjs
Normal file
338
auto-login.mjs
Normal file
@@ -0,0 +1,338 @@
|
||||
/**
|
||||
* 全自动登录店小秘(含验证码 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);
|
||||
Reference in New Issue
Block a user