/** * 店小秘全自动登录 v3 * 直接调 AJAX API 登录,改进 OCR 预处理 */ import { chromium } from 'playwright'; import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs'; import { execSync } from 'child_process'; import sharp from 'sharp'; const COOKIE_FILE = './cookies.json'; const SCREENSHOTS_DIR = './screenshots'; const MAX_RETRIES = 15; const DOWNLOAD_DIR = './downloads'; mkdirSync(SCREENSHOTS_DIR, { recursive: true }); mkdirSync(DOWNLOAD_DIR, { recursive: true }); // ====== 多种预处理方案尝试 OCR ====== async function ocrCaptcha(page) { const captchaImg = await page.$('#verifyImgCode'); if (!captchaImg) return null; const rawPath = `${SCREENSHOTS_DIR}/captcha_raw.png`; await captchaImg.screenshot({ path: rawPath }); const presets = [ // 方案1:灰度 + 放大 + 二值化(中阈值) async (src, dst) => { await sharp(src).grayscale().resize({ width: 468 }).normalize().sharpen({ sigma: 1.5 }).threshold(130).toFile(dst); }, // 方案2:灰度 + 放大 + 低阈值 async (src, dst) => { await sharp(src).grayscale().resize({ width: 468 }).normalize().threshold(100).toFile(dst); }, // 方案3:灰度 + 放大 + 高阈值 async (src, dst) => { await sharp(src).grayscale().resize({ width: 468 }).normalize().threshold(160).toFile(dst); }, // 方案4:原图直接放大 async (src, dst) => { await sharp(src).resize({ width: 468 }).toFile(dst); }, // 方案5:灰度 + 反色 + 二值化 async (src, dst) => { await sharp(src).grayscale().resize({ width: 468 }).negate().normalize().threshold(128).toFile(dst); }, ]; const psmModes = ['7', '8', '13']; // 单行、单词、单行原始 for (let i = 0; i < presets.length; i++) { const processedPath = `${SCREENSHOTS_DIR}/captcha_p${i}.png`; try { await presets[i](rawPath, processedPath); } catch (e) { continue; } for (const psm of psmModes) { try { const result = execSync( `tesseract "${processedPath}" stdout --psm ${psm} -c tessedit_char_whitelist=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`, { encoding: 'utf-8', timeout: 10000 } ).trim().replace(/[\s\n\r]/g, ''); if (result && result.length === 4) { console.log(` OCR [方案${i + 1}, psm${psm}]: "${result}"`); return result; } } catch (e) {} } } // 实在不行,返回任何 >= 4 字符的结果 for (let i = 0; i < presets.length; i++) { const processedPath = `${SCREENSHOTS_DIR}/captcha_p${i}.png`; if (!existsSync(processedPath)) continue; try { const result = execSync( `tesseract "${processedPath}" stdout --psm 7 -c tessedit_char_whitelist=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`, { encoding: 'utf-8', timeout: 10000 } ).trim().replace(/[\s\n\r]/g, ''); if (result && result.length >= 4) { console.log(` OCR [fallback 方案${i + 1}]: "${result}" (取前4字符)`); return result.substring(0, 4); } } catch (e) {} } console.log(' OCR 全部失败'); return null; } // ====== 主流程 ====== async function main() { console.log('====== 店小秘全自动登录 v3 ======\n'); const browser = await chromium.launch({ headless: true, args: ['--no-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); // 访问一个需要登录的页面来验证 const resp = await page.goto('https://www.dianxiaomi.com/saleManage/index.htm', { waitUntil: 'domcontentloaded', timeout: 20000 }).catch(() => null); const url = page.url(); const title = await page.title(); if (resp && !url.includes('/home.htm') && !url.includes('/index.htm') && !title.includes('Error')) { console.log('>> Cookie 有效!直接进入后台'); return { success: true, page, context, browser }; } console.log('>> Cookie 无效,重新登录\n'); } // 打开登录页 await page.goto('https://www.dianxiaomi.com/home.htm', { waitUntil: 'networkidle', timeout: 30000 }); for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { console.log(`\n>> 第 ${attempt}/${MAX_RETRIES} 次尝试...`); // 刷新验证码 if (attempt > 1) { await page.evaluate(() => { const img = document.getElementById('verifyImgCode'); if (img) img.src = '/verify/code.htm?t=' + Date.now(); }); await page.waitForTimeout(1500); } // 填写表单 await page.fill('input[name="account"]', 'MiLe-kf01'); await page.fill('input[name="password"]', 'Vxdas@302'); // OCR 验证码 const code = await ocrCaptcha(page); if (!code) { console.log(' 跳过本次'); continue; } await page.fill('#verifyCode', code); // 用 AJAX API 提交登录 const loginResult = await page.evaluate(async (verifyCode) => { const formData = new FormData(); formData.append('account', 'MiLe-kf01'); formData.append('password', 'Vxdas@302'); formData.append('verifyCode', verifyCode); formData.append('remeber', 'on'); try { const resp = await fetch('/user/userLoginNew2.json', { method: 'POST', body: formData, }); return await resp.json(); } catch (e) { return { code: -999, error: e.message }; } }, code); console.log(` API 响应: ${JSON.stringify(loginResult)}`); if (loginResult.code === 0 || loginResult.code === 1) { console.log('\n>> ★★★ 登录成功!★★★'); console.log(`>> 跳转 URL: ${loginResult.url}`); // 跳转到后台 if (loginResult.url) { await page.goto('https://www.dianxiaomi.com' + loginResult.url, { waitUntil: 'networkidle', timeout: 30000 }); } else { await page.goto('https://www.dianxiaomi.com/saleManage/index.htm', { waitUntil: 'networkidle', timeout: 30000 }); } // 保存 Cookie const cookies = await context.cookies(); writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2)); console.log(`>> Cookie 已保存(${cookies.length} 条)`); return { success: true, page, context, browser }; } // 登录失败 const errorMsg = loginResult.error || '未知错误'; console.log(` 失败: ${errorMsg}`); if (errorMsg.includes('账号') || errorMsg.includes('密码') || errorMsg.includes('不存在')) { console.log('>> 账号或密码错误,停止重试'); break; } // 验证码错误继续重试 } console.log('\n>> 登录失败'); await browser.close(); return { success: false }; } catch (err) { console.error('致命错误:', err.message); await browser.close(); return { success: false }; } } // ====== 探索后台 ====== async function explore(page) { console.log('\n>> ===== 探索后台页面 ====='); console.log('>> URL:', page.url()); console.log('>> 标题:', await page.title()); await page.screenshot({ path: `${SCREENSHOTS_DIR}/backend.png`, fullPage: true }); // 获取页面文本 const bodyText = await page.evaluate(() => document.body?.innerText?.substring(0, 5000)); console.log('>> 页面内容:\n', bodyText); // 左侧菜单 const menuItems = await page.$$eval( '.sidebar a, .menu a, .nav a, .left-menu a, [class*="sidebar"] a, [class*="menu"] a, a[href*="Manage"], a[href*="manage"]', els => els.map(el => ({ text: el.textContent.trim().replace(/\s+/g, ' ').substring(0, 50), href: el.href, })).filter(e => e.text && e.href.includes('dianxiaomi')) .filter((e, i, arr) => arr.findIndex(a => a.href === e.href) === i) ); console.log('\n>> 菜单链接:'); for (const m of menuItems) { console.log(` [${m.text}] -> ${m.href}`); } // 所有链接 const allLinks = await page.$$eval('a', els => els.map(el => ({ text: el.textContent.trim().replace(/\s+/g, ' ').substring(0, 60), href: el.href, })) .filter(e => e.text && e.href && e.href.includes('dianxiaomi')) .filter((e, i, arr) => arr.findIndex(a => a.href === e.href) === i) ); console.log(`\n>> 全部链接(${allLinks.length} 个):`); for (const l of allLinks) { const tag = (l.text.includes('采购') || l.text.includes('仓库') || l.text.includes('导出') || l.text.includes('库存') || l.text.includes('备货') || l.text.includes('建议')) ? '★' : ' '; console.log(` ${tag} [${l.text}] ${l.href}`); } // 尝试常见后台路径 const paths = [ '/saleManage/index.htm', '/purchaseManage/purchaseSuggestion.htm', '/purchaseManage/purchaseOrder.htm', '/purchaseManage/index.htm', '/stockManage/stockList.htm', '/stockManage/index.htm', '/warehouseManage/index.htm', '/warehouseManage/stockList.htm', ]; console.log('\n>> 尝试后台路径:'); for (const p of paths) { const url = `https://www.dianxiaomi.com${p}`; try { await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 10000 }); const finalUrl = page.url(); const title = await page.title(); const isError = title.includes('Error') || finalUrl.includes('/home.htm'); console.log(` ${isError ? '✗' : '✓'} ${p} -> [${title}] ${finalUrl}`); if (!isError) { await page.screenshot({ path: `${SCREENSHOTS_DIR}/page${p.replace(/\//g, '_')}.png`, fullPage: true }); // 在该页面查找导出按钮 const exportInfo = await page.$$eval('*', els => els.filter(el => { const t = el.textContent?.trim(); return t && (t === '导出' || t === '导出全部' || t === '导出建议' || t.includes('导出') || t.includes('下载')); }).map(el => ({ tag: el.tagName, text: el.textContent.trim().substring(0, 40), id: el.id, className: (el.className || '').substring(0, 50), })).filter(e => ['A', 'BUTTON', 'SPAN', 'INPUT', 'DIV'].includes(e.tag)) .slice(0, 15) ); if (exportInfo.length) { console.log(` 导出按钮:`); exportInfo.forEach(e => console.log(` ${e.tag}#${e.id}.${e.className}: "${e.text}"`)); } } } catch (e) { console.log(` ✗ ${p} -> 超时`); } } } // ====== 执行 ====== const result = await main(); if (result.success) { await explore(result.page); await result.browser.close(); } else { process.exit(1); }