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

274 lines
10 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.
/**
* 店小秘全自动登录 v6
* 每次尝试完整刷新页面 + type 模拟输入 + 响应监听
*/
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 {}
}
}
// 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;
}
async function main() {
console.log('====== 店小秘全自动登录 v6 ======\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();
// 全局响应监听
let lastLoginResponse = null;
page.on('response', async (resp) => {
if (resp.url().includes('userLoginNew2.json')) {
try {
const body = await resp.text();
lastLoginResponse = body;
console.log(` [API] ${resp.status()} ${body.substring(0, 300)}`);
} catch {}
}
});
// 先试 Cookie
if (existsSync(COOKIE_FILE)) {
console.log('>> 尝试复用 Cookie...');
const cookies = JSON.parse(readFileSync(COOKIE_FILE, 'utf-8'));
await context.addCookies(cookies);
await page.goto('https://www.dianxiaomi.com/saleManage/index.htm', {
waitUntil: 'domcontentloaded', timeout: 20000
}).catch(() => {});
const title = await page.title();
if (!title.includes('Error') && !page.url().includes('/home.htm') && !page.url().includes('/index.htm')) {
console.log('>> Cookie 有效!');
return { success: true, page, context, browser };
}
console.log('>> Cookie 无效\n');
await context.clearCookies();
}
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
console.log(`\n>> ===== 第 ${attempt}/${MAX_RETRIES} 次 =====`);
lastLoginResponse = null;
// 每次重新加载登录页(确保干净状态)
await page.goto('https://www.dianxiaomi.com/home.htm', { waitUntil: 'networkidle', timeout: 30000 });
await page.waitForTimeout(1000);
// 确认表单元素存在
const hasForm = await page.$('#exampleInputName');
if (!hasForm) {
console.log(' 登录表单不存在,跳过');
continue;
}
// 截图验证码
const captchaImg = await page.$('#verifyImgCode');
if (!captchaImg) {
console.log(' 验证码图片不存在');
continue;
}
const rawPath = `${SCREENSHOTS_DIR}/captcha_raw.png`;
await captchaImg.screenshot({ path: rawPath });
// OCR
const code = await ocrCaptcha(rawPath);
if (!code) {
console.log(' OCR 失败');
continue;
}
console.log(` 验证码: "${code}"`);
// 清空输入框后逐个输入(模拟真人)
await page.click('#exampleInputName', { clickCount: 3 }); // 全选
await page.keyboard.press('Backspace');
await page.type('#exampleInputName', 'MiLe-kf01', { delay: 50 });
await page.click('#exampleInputPassword', { clickCount: 3 });
await page.keyboard.press('Backspace');
await page.type('#exampleInputPassword', 'Vxdas@302', { delay: 50 });
await page.click('#verifyCode', { clickCount: 3 });
await page.keyboard.press('Backspace');
await page.type('#verifyCode', code, { delay: 50 });
// 确认值
const vals = await page.evaluate(() => ({
a: document.getElementById('exampleInputName')?.value,
p: document.getElementById('exampleInputPassword')?.value,
c: document.getElementById('verifyCode')?.value,
}));
console.log(` 实际值: 账号="${vals.a}" 密码="${vals.p ? '***' : 'empty'}" 验证码="${vals.c}"`);
// 截图
await page.screenshot({ path: `${SCREENSHOTS_DIR}/attempt-${attempt}.png` });
// 点击登录
console.log(' 点击登录...');
await page.click('#loginBtn');
// 等待 API 响应或页面跳转
await page.waitForTimeout(5000);
// 检查 API 响应
if (lastLoginResponse) {
try {
const data = JSON.parse(lastLoginResponse);
if (data.code === 0 || (data.url && data.url !== '' && !data.error)) {
console.log('\n>> ★★★ 登录成功!★★★');
await page.waitForTimeout(2000);
const afterUrl = page.url();
console.log('>> 当前URL:', afterUrl);
// 如果没自动跳转,手动跳
if (afterUrl.includes('/home.htm') || afterUrl.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: 'networkidle', timeout: 30000 }).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('锁定') || err.includes('禁用')) {
console.log('>> 严重错误,停止');
break;
}
} catch {
console.log(` API 响应非 JSON: ${lastLoginResponse.substring(0, 100)}`);
}
} else {
// 没有 API 响应,检查页面是否已跳转
const url = page.url();
const title = await page.title();
console.log(` 无 API 响应URL=${url}, 标题=${title}`);
if (!url.includes('/home.htm') && !url.includes('/index.htm') && !title.includes('Error')) {
console.log('>> 已跳转后台!');
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 });
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}`);
}
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: 10000 });
const t = await page.title();
const ok = !t.includes('Error');
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, [class*="export"], [class*="download"]', 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.cls}: "${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);
}