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

301 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.
/**
* 店小秘全自动登录 v9
* - load 事件 + jQuery 等待
* - 捕获原始响应(可能是 HTML 重定向)
* - 登录后检测页面状态
*/
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 = 20;
mkdirSync(SCREENSHOTS_DIR, { recursive: true });
mkdirSync(DOWNLOAD_DIR, { recursive: true });
async function ocrCaptcha(imagePath) {
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](imagePath, 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 {}
}
}
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 waitForLoginPage(page) {
try {
await page.goto('https://www.dianxiaomi.com/home.htm', {
waitUntil: 'load',
timeout: 30000,
});
} catch (e) {
console.log(` 页面加载超时,尝试继续...`);
}
// 等 jQuery + 登录表单 + login 函数
try {
await page.waitForFunction(() => {
return typeof window.$ !== 'undefined' &&
typeof window.jQuery !== 'undefined' &&
document.getElementById('exampleInputName') &&
document.getElementById('verifyImgCode') &&
document.getElementById('verifyImgCode').complete &&
typeof window.login === 'function';
}, { timeout: 15000 });
return true;
} catch {
// 试试等更久
await page.waitForTimeout(3000);
const hasJQ = await page.evaluate(() => typeof window.$ !== 'undefined').catch(() => false);
const hasForm = await page.$('#exampleInputName');
const hasLogin = await page.evaluate(() => typeof window.login === 'function').catch(() => false);
console.log(` 状态: jQuery=${hasJQ}, form=${!!hasForm}, login=${hasLogin}`);
return hasJQ && hasForm && hasLogin;
}
}
async function main() {
console.log('====== 店小秘全自动登录 v9 ======\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();
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
console.log(`\n>> ===== 第 ${attempt}/${MAX_RETRIES} 次 =====`);
// 加载登录页
const ready = await waitForLoginPage(page);
if (!ready) {
console.log(' 页面未就绪,重试');
continue;
}
// 等验证码图片完全加载
await page.waitForTimeout(1500);
// 截图验证码
const captchaEl = await page.$('#verifyImgCode');
if (!captchaEl) { console.log(' 验证码元素不存在'); continue; }
const rawPath = `${SCREENSHOTS_DIR}/captcha_raw.png`;
await captchaEl.screenshot({ path: rawPath });
// OCR
const code = await ocrCaptcha(rawPath);
if (!code) { console.log(' OCR 失败'); continue; }
console.log(` 验证码: "${code}"`);
// 用 jQuery 设值
await page.evaluate((c) => {
$('#exampleInputName').val('MiLe-kf01');
$('#exampleInputPassword').val('Vxdas@302');
$('#verifyCode').val(c);
}, code);
// 设置响应监听
let apiResponseText = null;
const respPromise = new Promise((resolve) => {
const handler = async (resp) => {
if (resp.url().includes('userLoginNew2')) {
try {
apiResponseText = await resp.text();
console.log(` [API ${resp.status()}] ${apiResponseText.substring(0, 500)}`);
} catch (e) {
console.log(` [API] 读取响应失败: ${e.message}`);
}
page.off('response', handler);
resolve(apiResponseText);
}
};
page.on('response', handler);
// 超时兜底
setTimeout(() => { page.off('response', handler); resolve(null); }, 15000);
});
// 调用 login()
console.log(' 调用 login()...');
await page.evaluate(() => { login(); });
// 等待 API 响应
const respText = await respPromise;
if (respText) {
// 尝试解析 JSON
try {
const data = JSON.parse(respText);
console.log(` JSON: ${JSON.stringify(data)}`);
if (data.code === 0 || (data.url && data.url.length > 1)) {
console.log('\n>> ★★★ 登录成功!★★★');
await handleLoginSuccess(page, context, data.url);
return { success: true, page, context, browser };
}
const err = data.error || '';
if (err.includes('密码错误') || err.includes('不存在') || err.includes('锁定')) {
console.log('>> 严重错误,停止');
break;
}
} catch {
// 不是 JSON可能是 HTML 重定向(登录成功)
console.log(` 响应非 JSON${respText.length} 字节),可能已登录`);
// 等待可能的跳转
await page.waitForTimeout(3000);
const url = page.url();
const title = await page.title();
console.log(` URL=${url} 标题=${title}`);
// 检查是否离开登录页
if (!url.includes('/home.htm')) {
// 检查是否真的进了后台
const hasBackend = await page.evaluate(() => {
const text = document.body?.innerText || '';
return text.includes('退出') || text.includes('注销') || text.includes('工作台') ||
text.includes('订单') || text.includes('商品');
});
if (hasBackend || (!title.includes('Error') && !url.includes('/index.htm'))) {
console.log('\n>> ★★★ 登录成功(重定向)!★★★');
await handleLoginSuccess(page, context, null);
return { success: true, page, context, browser };
}
}
// 检查响应中是否有登录成功标志
if (respText.includes('redirect') || respText.includes('success') || respText.includes('window.location')) {
console.log(' 响应含跳转标志,尝试后台...');
await page.goto('https://www.dianxiaomi.com/saleManage/index.htm', {
waitUntil: 'load', timeout: 20000
}).catch(() => {});
const t = await page.title();
if (!t.includes('Error') && !page.url().includes('/home.htm')) {
console.log('\n>> ★★★ 登录成功!★★★');
await handleLoginSuccess(page, context, null);
return { success: true, page, context, browser };
}
}
}
} else {
console.log(' 无 API 响应');
// 检查是否已跳转
await page.waitForTimeout(3000);
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 }).catch(() => {});
await browser.close();
return { success: false };
}
async function handleLoginSuccess(page, context, redirectUrl) {
// 等页面稳定
await page.waitForTimeout(2000);
const url = page.url();
console.log('>> 当前 URL:', url);
// 如果还在首页,跳转到后台
if (url.includes('/home.htm') || url.includes('/index.htm')) {
const target = redirectUrl?.startsWith('/') ? `https://www.dianxiaomi.com${redirectUrl}` : 'https://www.dianxiaomi.com/saleManage/index.htm';
await page.goto(target, { waitUntil: 'load', timeout: 20000 }).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());
await page.screenshot({ path: `${SCREENSHOTS_DIR}/backend-main.png`, fullPage: true });
}
async function explore(page) {
console.log('\n>> ===== 探索后台 =====');
console.log(`>> URL: ${page.url()}`);
console.log(`>> 标题: ${await page.title()}`);
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}`);
}
for (const p of ['/saleManage/index.htm', '/purchaseManage/purchaseSuggestion.htm', '/purchaseManage/purchaseOrder.htm', '/stockManage/stockList.htm']) {
try {
await page.goto(`https://www.dianxiaomi.com${p}`, { waitUntil: 'load', 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 pageText = await page.evaluate(() => document.body?.innerText?.substring(0, 3000));
console.log(' 内容:', pageText?.substring(0, 500));
}
} catch { console.log(`${p} 超时`); }
}
}
const result = await main();
if (result.success) {
await explore(result.page);
await result.browser.close();
} else {
process.exit(1);
}