/** * 店小秘全自动登录 v7 * 纯 HTTP 请求登录(不走浏览器 DOM),更稳定 * 登录成功后把 Cookie 给浏览器用于后续导出操作 */ import { chromium } from 'playwright'; import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs'; import { execSync } from 'child_process'; import sharp from 'sharp'; import https from 'https'; import http from 'http'; const COOKIE_FILE = './cookies.json'; const SCREENSHOTS_DIR = './screenshots'; const DOWNLOAD_DIR = './downloads'; const MAX_RETRIES = 20; mkdirSync(SCREENSHOTS_DIR, { recursive: true }); mkdirSync(DOWNLOAD_DIR, { recursive: true }); // ====== HTTP 请求辅助 ====== function httpGet(url, cookies = '') { return new Promise((resolve, reject) => { const mod = url.startsWith('https') ? https : http; const req = mod.get(url, { headers: { 'Cookie': cookies, 'User-Agent': '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', }, }, (res) => { const setCookies = res.headers['set-cookie'] || []; const chunks = []; res.on('data', c => chunks.push(c)); res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, setCookies, body: Buffer.concat(chunks), })); }); req.on('error', reject); req.setTimeout(15000, () => { req.destroy(); reject(new Error('timeout')); }); }); } function httpPost(url, data, cookies = '') { return new Promise((resolve, reject) => { const mod = url.startsWith('https') ? https : http; const body = new URLSearchParams(data).toString(); const urlObj = new URL(url); const req = mod.request({ hostname: urlObj.hostname, path: urlObj.pathname, method: 'POST', headers: { 'Cookie': cookies, 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(body), 'User-Agent': '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', 'X-Requested-With': 'XMLHttpRequest', 'Referer': 'https://www.dianxiaomi.com/home.htm', }, }, (res) => { const setCookies = res.headers['set-cookie'] || []; const chunks = []; res.on('data', c => chunks.push(c)); res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, setCookies, body: Buffer.concat(chunks).toString('utf-8'), })); }); req.on('error', reject); req.setTimeout(15000, () => { req.destroy(); reject(new Error('timeout')); }); req.write(body); req.end(); }); } function parseCookies(setCookieHeaders) { const map = {}; for (const h of setCookieHeaders) { const parts = h.split(';')[0].split('='); if (parts.length >= 2) { map[parts[0].trim()] = parts.slice(1).join('=').trim(); } } return map; } function cookieString(map) { return Object.entries(map).map(([k, v]) => `${k}=${v}`).join('; '); } // ====== OCR ====== async function ocrCaptcha(imageBuffer) { const rawPath = `${SCREENSHOTS_DIR}/captcha_raw.png`; writeFileSync(rawPath, imageBuffer); 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), async (s, d) => sharp(s).grayscale().resize({ width: 600 }).normalize().sharpen({ sigma: 3 }).threshold(120).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 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; } // ====== 纯 HTTP 登录 ====== async function httpLogin() { console.log('====== 纯 HTTP 登录 ======\n'); for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { console.log(`>> 第 ${attempt}/${MAX_RETRIES} 次尝试...`); try { // 1) 获取初始页面和 Session Cookie const homeResp = await httpGet('https://www.dianxiaomi.com/home.htm'); const cookies = parseCookies(homeResp.setCookies); console.log(` Session: ${Object.keys(cookies).join(', ')}`); // 2) 下载验证码图片(同一 Session) const captchaResp = await httpGet( `https://www.dianxiaomi.com/verify/code.htm?t=${Date.now()}`, cookieString(cookies) ); // 合并新 Cookie Object.assign(cookies, parseCookies(captchaResp.setCookies)); // 3) OCR 验证码 const code = await ocrCaptcha(captchaResp.body); if (!code) { console.log(' OCR 失败,重试'); continue; } console.log(` 验证码: "${code}"`); // 4) POST 登录 const loginResp = await httpPost( 'https://www.dianxiaomi.com/user/userLoginNew2.json', { account: 'MiLe-kf01', password: 'Vxdas@302', verifyCode: code, remeber: 'on', loginReadAndAccept: 'on', url: '', }, cookieString(cookies) ); // 合并登录后的 Cookie Object.assign(cookies, parseCookies(loginResp.setCookies)); console.log(` API 响应: ${loginResp.body.substring(0, 300)}`); const data = JSON.parse(loginResp.body); if (data.code === 0 || (data.url && !data.error)) { console.log('\n>> ★★★ 登录成功!★★★'); console.log(`>> 跳转: ${data.url}`); // 保存 Cookie(转换为 Playwright 格式) const playwrightCookies = Object.entries(cookies).map(([name, value]) => ({ name, value, domain: 'www.dianxiaomi.com', path: '/', httpOnly: false, secure: false, sameSite: 'Lax', })); // 补充 .dianxiaomi.com 域的 Cookie for (const [name, value] of Object.entries(cookies)) { playwrightCookies.push({ name, value, domain: '.dianxiaomi.com', path: '/', httpOnly: false, secure: false, sameSite: 'Lax', }); } writeFileSync(COOKIE_FILE, JSON.stringify(playwrightCookies, null, 2)); console.log(`>> Cookie 已保存`); return { success: true, cookies, redirectUrl: data.url }; } const err = data.error || ''; console.log(` 失败: ${err}`); if (err.includes('密码错误') || err.includes('不存在') || err.includes('锁定') || err.includes('禁用')) { console.log('>> 账号/密码问题,停止'); return { success: false }; } // 验证码错误继续重试 } catch (e) { console.log(` 请求错误: ${e.message}`); } // 稍等再重试 await new Promise(r => setTimeout(r, 1000)); } return { success: false }; } // ====== 浏览器探索后台 ====== async function exploreWithBrowser(httpCookies, redirectUrl) { console.log('\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', }); // 设置 Cookie const playwrightCookies = Object.entries(httpCookies).map(([name, value]) => ({ name, value, domain: '.dianxiaomi.com', path: '/', })); await context.addCookies(playwrightCookies); const page = await context.newPage(); // 跳转到后台 const target = redirectUrl?.startsWith('/') ? `https://www.dianxiaomi.com${redirectUrl}` : 'https://www.dianxiaomi.com/saleManage/index.htm'; console.log(`>> 打开: ${target}`); await page.goto(target, { waitUntil: 'networkidle', timeout: 30000 }).catch(() => {}); 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, 8000)); 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: 15000 }); const t = await page.title(); const ok = !t.includes('Error') && !page.url().includes('/home.htm'); 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', 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.text}"`)); } } } catch { console.log(` ✗ ${p} 超时`); } } // 重新保存完整 Cookie const allCookies = await context.cookies(); writeFileSync(COOKIE_FILE, JSON.stringify(allCookies, null, 2)); await browser.close(); } // ====== 执行 ====== const result = await httpLogin(); if (result.success) { await exploreWithBrowser(result.cookies, result.redirectUrl); } else { console.log('\n>> 登录失败,退出'); process.exit(1); }