/** * 店小秘全自动登录 v5 * 用 jQuery 设值 + 点击按钮 + 监听 API */ 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 DOWNLOAD_DIR = './downloads'; const MAX_RETRIES = 15; mkdirSync(SCREENSHOTS_DIR, { recursive: true }); mkdirSync(DOWNLOAD_DIR, { recursive: true }); 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 = [ async (s, d) => sharp(s).grayscale().resize({ width: 468 }).normalize().sharpen({ sigma: 1.5 }).threshold(130).toFile(d), async (s, d) => sharp(s).grayscale().resize({ width: 468 }).normalize().threshold(100).toFile(d), async (s, d) => sharp(s).grayscale().resize({ width: 468 }).normalize().threshold(160).toFile(d), async (s, d) => sharp(s).resize({ width: 468 }).toFile(d), async (s, d) => sharp(s).grayscale().resize({ width: 468 }).negate().normalize().threshold(128).toFile(d), ]; for (let i = 0; i < presets.length; i++) { const p = `${SCREENSHOTS_DIR}/cap_p${i}.png`; try { await presets[i](rawPath, p); } catch { continue; } for (const psm of ['7', '8']) { try { const r = execSync( `tesseract "${p}" stdout --psm ${psm} -c tessedit_char_whitelist=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`, { encoding: 'utf-8', timeout: 10000 } ).trim().replace(/[\s\n\r]/g, ''); if (r && r.length === 4) return r; } catch {} } } // fallback: 任何 >=3 字符 for (let i = 0; i < presets.length; i++) { const p = `${SCREENSHOTS_DIR}/cap_p${i}.png`; if (!existsSync(p)) continue; try { const r = execSync( `tesseract "${p}" stdout --psm 7 -c tessedit_char_whitelist=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`, { encoding: 'utf-8', timeout: 10000 } ).trim().replace(/[\s\n\r]/g, ''); if (r && r.length >= 3) return r.substring(0, 4); } catch {} } return null; } async function main() { console.log('====== 店小秘全自动登录 v5 ======\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(); // 先试 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: 'domcontentloaded', timeout: 20000 }).catch(() => {}); const title = await page.title(); if (!title.includes('Error') && !page.url().includes('/home.htm') && !page.url().includes('/index.htm')) { console.log('>> Cookie 有效!'); return { success: true, page, context, browser }; } console.log('>> Cookie 无效\n'); await context.clearCookies(); } // 打开登录页 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 (!page.url().includes('home.htm') && !page.url().includes('index.htm')) { await page.goto('https://www.dianxiaomi.com/home.htm', { waitUntil: 'networkidle', timeout: 30000 }); } // 刷新验证码 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); } // 用 jQuery 设置表单值(确保 .val() 能读到) await page.evaluate(() => { if (typeof $ !== 'undefined') { $('#exampleInputName').val('MiLe-kf01').trigger('change').trigger('input'); $('#exampleInputPassword').val('Vxdas@302').trigger('change').trigger('input'); } }); // OCR 验证码 const code = await ocrCaptcha(page); if (!code) { console.log(' OCR 失败'); continue; } console.log(` 验证码: "${code}"`); // 用 jQuery 设验证码 await page.evaluate((c) => { if (typeof $ !== 'undefined') { $('#verifyCode').val(c).trigger('change').trigger('input'); } }, code); // 截图确认 await page.screenshot({ path: `${SCREENSHOTS_DIR}/attempt-${attempt}.png` }); // 验证 jQuery 读到的值 const formValues = await page.evaluate(() => { if (typeof $ === 'undefined') return { error: 'no jQuery' }; return { account: $.trim($('#exampleInputName').val()), password: $.trim($('#exampleInputPassword').val()), verifyCode: $.trim($('#verifyCode').val()), }; }); console.log(` 表单值: ${JSON.stringify(formValues)}`); // 监听 API 响应 const apiResponsePromise = page.waitForResponse( resp => resp.url().includes('userLoginNew2.json'), { timeout: 15000 } ).catch(() => null); // 点击登录按钮(按钮 onclick="login()") console.log(' 点击登录按钮...'); await page.click('#loginBtn'); // 等待 API 响应 const apiResp = await apiResponsePromise; if (apiResp) { let data; try { data = await apiResp.json(); } catch { const text = await apiResp.text().catch(() => ''); console.log(` API 原始响应: ${text.substring(0, 200)}`); data = {}; } console.log(` API 响应: ${JSON.stringify(data)}`); if (data.code === 0 || (data.url && data.url !== '')) { console.log('\n>> ★★★ 登录成功!★★★'); // 等待页面跳转 await page.waitForTimeout(3000); const afterUrl = page.url(); console.log('>> 自动跳转到:', afterUrl); // 如果没跳转,手动去 if (afterUrl.includes('/home.htm') || afterUrl.includes('/index.htm')) { const target = data.url ? ('https://www.dianxiaomi.com' + data.url) : 'https://www.dianxiaomi.com/saleManage/index.htm'; await page.goto(target, { waitUntil: 'networkidle', timeout: 30000 }).catch(() => {}); } // 保存 Cookie const cookies = await context.cookies(); writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2)); console.log(`>> Cookie 已保存(${cookies.length} 条)`); console.log('>> 当前 URL:', page.url()); return { success: true, page, context, browser }; } const errMsg = data.error || JSON.stringify(data); console.log(` 失败: ${errMsg}`); if (errMsg.includes('账号') || errMsg.includes('密码错误') || errMsg.includes('不存在') || errMsg.includes('锁定')) { console.log('>> 账号/密码问题,停止'); break; } } else { console.log(' 未收到 API 响应'); await page.waitForTimeout(2000); // 也许已经登录跳转了 const url = page.url(); if (!url.includes('/home.htm') && !url.includes('/index.htm')) { console.log('>> 已跳转到:', url); const cookies = await context.cookies(); writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2)); return { success: true, page, context, browser }; } } } await page.screenshot({ path: `${SCREENSHOTS_DIR}/final-fail.png`, fullPage: true }); 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-main.png`, fullPage: true }); const text = await page.evaluate(() => document.body?.innerText?.substring(0, 5000)); console.log('>> 页面文本:\n', text); const links = 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?.includes('dianxiaomi')) .filter((e, i, arr) => arr.findIndex(a => a.href === e.href) === i) ); console.log(`\n>> 链接(${links.length} 个):`); for (const l of links) { const star = ['采购', '仓库', '导出', '库存', '备货', '建议'].some(k => l.text.includes(k)) ? '★' : ' '; console.log(` ${star} [${l.text}] ${l.href}`); } // 访问几个关键页面截图 const paths = [ '/saleManage/index.htm', '/purchaseManage/purchaseSuggestion.htm', '/purchaseManage/purchaseOrder.htm', '/stockManage/stockList.htm', ]; for (const p of paths) { try { await page.goto(`https://www.dianxiaomi.com${p}`, { waitUntil: 'domcontentloaded', timeout: 10000 }); const t = await page.title(); const ok = !t.includes('Error'); console.log(`\n ${ok ? '✓' : '✗'} ${p} [${t}]`); if (ok) { await page.screenshot({ path: `${SCREENSHOTS_DIR}/page${p.replace(/\//g, '_')}.png`, fullPage: true }); // 打印页面所有按钮 const btns = await page.$$eval('button, a.btn, .btn, [class*="export"], [class*="download"]', els => els.map(el => ({ tag: el.tagName, text: el.textContent.trim().substring(0, 50), id: el.id, cls: (el.className || '').substring(0, 60) })) .filter(e => e.text) .slice(0, 30) ); if (btns.length) { console.log(' 按钮:'); btns.forEach(b => console.log(` ${b.tag}#${b.id} .${b.cls}: "${b.text}"`)); } // 特别查找导出 const exportBtns = btns.filter(b => b.text.includes('导出')); if (exportBtns.length) { console.log(' ★ 导出按钮:'); exportBtns.forEach(b => console.log(` ${b.tag}#${b.id}: "${b.text}"`)); } } } catch { console.log(` ✗ ${p} 超时`); } } } const result = await main(); if (result.success) { await explore(result.page); await result.browser.close(); } else { process.exit(1); }