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

261 lines
9.2 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.
/**
* 店小秘全自动登录 v8
* 浏览器方案:稳定性修复
* - domcontentloaded 替代 networkidle
* - 等 jQuery 就绪后再操作
* - waitForResponse 确保 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 = 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 main() {
console.log('====== 店小秘全自动登录 v8 ======\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();
// 监听请求体(调试用)
page.on('request', req => {
if (req.url().includes('userLoginNew2')) {
console.log(` [REQ] ${req.method()} ${req.url()}`);
console.log(` [REQ body] ${req.postData()?.substring(0, 500)}`);
}
});
page.on('response', async resp => {
if (resp.url().includes('userLoginNew2')) {
try {
const body = await resp.text();
console.log(` [RESP] ${resp.status()} ${body.substring(0, 300)}`);
} catch {}
}
});
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
console.log(`\n>> ===== 第 ${attempt}/${MAX_RETRIES} 次 =====`);
// 每次完整刷新
try {
await page.goto('https://www.dianxiaomi.com/home.htm', {
waitUntil: 'domcontentloaded',
timeout: 20000,
});
} catch (e) {
console.log(` 页面加载失败: ${e.message}`);
continue;
}
// 等待 jQuery 和登录表单就绪
try {
await page.waitForFunction(() => {
return typeof $ !== 'undefined' &&
document.getElementById('exampleInputName') &&
document.getElementById('verifyImgCode') &&
typeof login === 'function';
}, { timeout: 10000 });
} catch {
console.log(' JS 未就绪');
continue;
}
// 等验证码图片加载
await page.waitForTimeout(2000);
// 截图验证码
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 设值login() 用 $.trim($('#xxx').val()) 读取)
await page.evaluate((c) => {
$('#exampleInputName').val('MiLe-kf01');
$('#exampleInputPassword').val('Vxdas@302');
$('#verifyCode').val(c);
}, code);
// 确认
const vals = await page.evaluate(() => ({
a: $.trim($('#exampleInputName').val()),
p: $.trim($('#exampleInputPassword').val()) ? '***' : 'empty',
c: $.trim($('#verifyCode').val()),
}));
console.log(` 值确认: ${JSON.stringify(vals)}`);
// 设置 waitForResponse
const respPromise = page.waitForResponse(
r => r.url().includes('userLoginNew2.json'),
{ timeout: 15000 }
).catch(() => null);
// 直接调用 login() 函数
console.log(' 调用 login()...');
const loginResult = await page.evaluate(() => {
try {
login();
return 'called';
} catch (e) {
return 'error: ' + e.message;
}
});
console.log(` login() 返回: ${loginResult}`);
// 等待 API 响应
const resp = await respPromise;
if (resp) {
let data;
try {
data = await resp.json();
} catch {
console.log(' 响应非JSON');
continue;
}
console.log(` 结果: ${JSON.stringify(data)}`);
if (data.code === 0 || (data.url && data.url.length > 1 && !data.error)) {
console.log('\n>> ★★★ 登录成功!★★★');
// 等页面跳转
await page.waitForTimeout(3000);
console.log('>> 跳转后 URL:', page.url());
if (page.url().includes('/home.htm') || page.url().includes('/index.htm')) {
const target = data.url?.startsWith('/') ? `https://www.dianxiaomi.com${data.url}` : 'https://www.dianxiaomi.com/saleManage/index.htm';
await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 20000 }).catch(() => {});
}
const cookies = await context.cookies();
writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2));
console.log(`>> Cookie 已保存(${cookies.length} 条)`);
return { success: true, page, context, browser };
}
const err = data.error || '';
console.log(` 错误: ${err}`);
if (err.includes('密码错误') || err.includes('不存在') || err.includes('锁定')) {
console.log('>> 严重错误,停止');
break;
}
} else {
console.log(' 无 API 响应(超时)');
}
}
await page.screenshot({ path: `${SCREENSHOTS_DIR}/final-fail.png`, fullPage: true }).catch(() => {});
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, 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: '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 }))
.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} 超时`); }
}
}
const result = await main();
if (result.success) {
await explore(result.page);
await result.browser.close();
} else {
process.exit(1);
}