Files
20260324-42433647/auto-login-v7.mjs
2026-04-25 21:50:03 +08:00

338 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 店小秘全自动登录 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);
}