/** * 全自动登录店小秘(含验证码 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);