/** * 店小秘全自动登录 + 后台探索 + 导出 * - OCR: ddddocr (Python) * - 每次尝试用新 Context 避免安全机制 * - 通过 API 响应判断登录结果(非 JSON = 成功跳转) */ import { chromium } from 'playwright'; import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs'; import { execSync } from 'child_process'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const COOKIE_FILE = path.join(__dirname, 'cookies.json'); const SCREENSHOTS_DIR = path.join(__dirname, 'screenshots'); const DOWNLOAD_DIR = path.join(__dirname, 'downloads'); const OCR_SCRIPT = path.join(__dirname, 'ocr_captcha.py'); const MAX_RETRIES = 20; const BASE_URL = 'https://www.dianxiaomi.com'; mkdirSync(SCREENSHOTS_DIR, { recursive: true }); mkdirSync(DOWNLOAD_DIR, { recursive: true }); function ocrCaptcha(imagePath) { try { return execSync(`python3 "${OCR_SCRIPT}" "${imagePath}"`, { encoding: 'utf-8', timeout: 30000, }).trim() || null; } catch { return null; } } function timestamp() { return new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }); } // ====== 登录 ====== async function doLogin(browser) { console.log(`[${timestamp()}] 开始登录...\n`); // 先试 Cookie if (existsSync(COOKIE_FILE)) { console.log('>> 尝试复用 Cookie...'); const ctx = 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 pg = await ctx.newPage(); const cookies = JSON.parse(readFileSync(COOKIE_FILE, 'utf-8')); await ctx.addCookies(cookies); // 用 AJAX 请求一个需要登录的接口检查 Cookie 是否有效 try { await pg.goto(`${BASE_URL}/home.htm`, { waitUntil: 'load', timeout: 20000 }); const checkResult = await pg.evaluate(async () => { try { const resp = await fetch('/saleManage/searchSale.json', { method: 'POST' }); const text = await resp.text(); // 如果返回 JSON 数据(不是登录页 HTML),说明 Cookie 有效 return { status: resp.status, isJson: text.startsWith('{') || text.startsWith('['), preview: text.substring(0, 100) }; } catch (e) { return { error: e.message }; } }); console.log(' Cookie 检查:', JSON.stringify(checkResult)); if (checkResult.isJson && checkResult.status === 200) { console.log('>> Cookie 有效!\n'); return { page: pg, context: ctx }; } } catch {} console.log('>> Cookie 无效\n'); await ctx.close(); } // 循环尝试登录 for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { console.log(`>> 第 ${attempt}/${MAX_RETRIES} 次...`); const ctx = 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 pg = await ctx.newPage(); // 监听登录 API 响应 let loginApiResult = null; pg.on('response', async (resp) => { if (resp.url().includes('userLoginNew2')) { try { loginApiResult = await resp.text(); } catch { // 读取失败 = 页面已跳转 = 登录成功 loginApiResult = '__REDIRECT__'; } } }); try { // 1) 加载 await pg.goto(`${BASE_URL}/home.htm`, { waitUntil: 'load', timeout: 30000 }); await pg.waitForSelector('#exampleInputName', { timeout: 10000 }); await pg.waitForFunction(() => typeof window.login === 'function', { timeout: 10000 }); await pg.waitForFunction(() => document.getElementById('verifyImgCode')?.complete === true, { timeout: 5000 }).catch(() => {}); await pg.waitForTimeout(1000); // 2) OCR const captchaEl = await pg.$('#verifyImgCode'); if (!captchaEl) { await ctx.close(); continue; } const captchaPath = path.join(SCREENSHOTS_DIR, 'captcha_raw.png'); await captchaEl.screenshot({ path: captchaPath }); const code = ocrCaptcha(captchaPath); if (!code || code.length < 3) { console.log(` OCR 失败: "${code}"`); await ctx.close(); continue; } console.log(` 验证码: "${code}"`); // 3) 填值 await pg.evaluate((c) => { $('#exampleInputName').val('MiLe-kf01'); $('#exampleInputPassword').val('Vxdas@302'); $('#verifyCode').val(c); }, code); // 4) 调用 login() loginApiResult = null; const navPromise = pg.waitForNavigation({ timeout: 15000, waitUntil: 'load' }).catch(() => null); await pg.evaluate(() => { login(); }); await navPromise; await pg.waitForTimeout(3000); const url = pg.url(); console.log(` URL: ${url}`); console.log(` API: ${loginApiResult?.substring(0, 200)}`); // 5) 判断结果 // 情况 A:API 返回 JSON 错误 if (loginApiResult && loginApiResult !== '__REDIRECT__') { try { const data = JSON.parse(loginApiResult); if (data.code === -1) { console.log(` ✗ ${data.error}`); if (data.error?.includes('密码') || data.error?.includes('锁定')) { await ctx.close(); return null; } await ctx.close(); continue; } if (data.code === 0) { console.log('\n>> ★★★ 登录成功(code=0)!★★★'); if (data.url) { await pg.goto(`${BASE_URL}${data.url}`, { waitUntil: 'load', timeout: 20000 }).catch(() => {}); } const cookies = await ctx.cookies(); writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2)); console.log(`>> Cookie 已保存(${cookies.length} 条)\n`); return { page: pg, context: ctx }; } } catch { // JSON 解析失败 = 可能是 HTML 重定向 = 成功 } } // 情况 B:API 响应读取失败(__REDIRECT__)= 页面跳转 = 成功 if (loginApiResult === '__REDIRECT__' || !url.includes('/home.htm')) { console.log('\n>> ★★★ 登录成功(页面跳转)!★★★'); const cookies = await ctx.cookies(); writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2)); console.log(`>> Cookie 已保存(${cookies.length} 条)\n`); return { page: pg, context: ctx }; } // 情况 C:仍在登录页 console.log(' 仍在登录页,重试\n'); await ctx.close(); } catch (e) { console.log(` 异常: ${e.message}`); await ctx.close(); } } console.log('>> 全部尝试失败'); return null; } // ====== 探索后台 ====== async function explore(pg) { console.log('>> ===== 探索后台 ====='); console.log(`>> URL: ${pg.url()}`); await pg.screenshot({ path: path.join(SCREENSHOTS_DIR, 'after-login.png'), fullPage: true }); // 当前页面内容 const text = await pg.evaluate(() => document.body?.innerText?.substring(0, 5000)); console.log('>> 页面文本(前2000字):\n', text?.substring(0, 2000)); // 尝试找到真正的后台入口 // 从当前页面的所有链接中查找 const links = await pg.$$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) { console.log(` [${l.text}] ${l.href}`); } // 用 AJAX 探测后台 API console.log('\n>> 探测后台 API...'); const apis = [ { name: '订单搜索', url: '/saleManage/searchSale.json', method: 'POST' }, { name: '采购建议', url: '/purchaseManage/purchaseSuggestion.json', method: 'POST' }, { name: '采购建议列表', url: '/purchaseManage/getSuggestionList.json', method: 'POST' }, { name: '采购单', url: '/purchaseManage/purchaseOrderList.json', method: 'POST' }, { name: '库存列表', url: '/stockManage/stockList.json', method: 'POST' }, { name: '自营仓库', url: '/stockManage/selfWarehouse.json', method: 'POST' }, { name: '仓库列表', url: '/warehouseManage/warehouseList.json', method: 'POST' }, { name: '用户信息', url: '/user/getUserInfo.json', method: 'GET' }, { name: '菜单', url: '/user/getMenuList.json', method: 'GET' }, { name: '首页数据', url: '/index/getData.json', method: 'GET' }, ]; for (const api of apis) { const result = await pg.evaluate(async (a) => { try { const resp = await fetch(a.url, { method: a.method }); const text = await resp.text(); return { status: resp.status, ok: resp.ok, preview: text.substring(0, 300) }; } catch (e) { return { error: e.message }; } }, api); const isJson = result.preview?.startsWith('{') || result.preview?.startsWith('['); console.log(` ${isJson ? '✓' : '✗'} ${api.name} (${api.url}) [${result.status}] ${result.preview?.substring(0, 150)}`); } // 尝试后台页面 console.log('\n>> 尝试后台页面...'); const pagePaths = [ '/saleManage/index.htm', '/purchaseManage/purchaseSuggestion.htm', '/purchaseManage/purchaseOrder.htm', '/stockManage/stockList.htm', '/warehouseManage/index.htm', '/user/setting.htm', '/user/index.htm', ]; for (const p of pagePaths) { try { await pg.goto(`${BASE_URL}${p}`, { waitUntil: 'load', timeout: 15000 }); const t = await pg.title(); const u = pg.url(); const isError = t.includes('Error') || u.includes('/home.htm'); console.log(` ${isError ? '✗' : '✓'} ${p} [${t}] -> ${u}`); if (!isError) { await pg.screenshot({ path: path.join(SCREENSHOTS_DIR, `page${p.replace(/\//g, '_')}.png`), fullPage: true }); const pageContent = await pg.evaluate(() => document.body?.innerText?.substring(0, 1000)); console.log(` 内容: ${pageContent?.substring(0, 300)}`); } } catch { console.log(` ✗ ${p} 超时`); } } } // ====== 主流程 ====== const browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] }); const result = await doLogin(browser); if (result) { await explore(result.page); await result.context.close(); } await browser.close(); process.exit(result ? 0 : 1);