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

286 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.
/**
* 店小秘全自动登录 v5
* 用 jQuery 设值 + 点击按钮 + 监听 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 = 15;
mkdirSync(SCREENSHOTS_DIR, { recursive: true });
mkdirSync(DOWNLOAD_DIR, { recursive: true });
async function ocrCaptcha(page) {
const captchaImg = await page.$('#verifyImgCode');
if (!captchaImg) return null;
const rawPath = `${SCREENSHOTS_DIR}/captcha_raw.png`;
await captchaImg.screenshot({ path: rawPath });
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),
];
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: 任何 >=3 字符
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('====== 店小秘全自动登录 v5 ======\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();
// 先试 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();
}
// 打开登录页
await page.goto('https://www.dianxiaomi.com/home.htm', { waitUntil: 'networkidle', timeout: 30000 });
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
console.log(`\n>> 第 ${attempt}/${MAX_RETRIES} 次尝试...`);
// 确保在登录页
if (!page.url().includes('home.htm') && !page.url().includes('index.htm')) {
await page.goto('https://www.dianxiaomi.com/home.htm', { waitUntil: 'networkidle', timeout: 30000 });
}
// 刷新验证码
if (attempt > 1) {
await page.evaluate(() => {
const img = document.getElementById('verifyImgCode');
if (img) img.src = '/verify/code.htm?t=' + Date.now();
});
await page.waitForTimeout(1500);
}
// 用 jQuery 设置表单值(确保 .val() 能读到)
await page.evaluate(() => {
if (typeof $ !== 'undefined') {
$('#exampleInputName').val('MiLe-kf01').trigger('change').trigger('input');
$('#exampleInputPassword').val('Vxdas@302').trigger('change').trigger('input');
}
});
// OCR 验证码
const code = await ocrCaptcha(page);
if (!code) { console.log(' OCR 失败'); continue; }
console.log(` 验证码: "${code}"`);
// 用 jQuery 设验证码
await page.evaluate((c) => {
if (typeof $ !== 'undefined') {
$('#verifyCode').val(c).trigger('change').trigger('input');
}
}, code);
// 截图确认
await page.screenshot({ path: `${SCREENSHOTS_DIR}/attempt-${attempt}.png` });
// 验证 jQuery 读到的值
const formValues = await page.evaluate(() => {
if (typeof $ === 'undefined') return { error: 'no jQuery' };
return {
account: $.trim($('#exampleInputName').val()),
password: $.trim($('#exampleInputPassword').val()),
verifyCode: $.trim($('#verifyCode').val()),
};
});
console.log(` 表单值: ${JSON.stringify(formValues)}`);
// 监听 API 响应
const apiResponsePromise = page.waitForResponse(
resp => resp.url().includes('userLoginNew2.json'),
{ timeout: 15000 }
).catch(() => null);
// 点击登录按钮(按钮 onclick="login()"
console.log(' 点击登录按钮...');
await page.click('#loginBtn');
// 等待 API 响应
const apiResp = await apiResponsePromise;
if (apiResp) {
let data;
try {
data = await apiResp.json();
} catch {
const text = await apiResp.text().catch(() => '');
console.log(` API 原始响应: ${text.substring(0, 200)}`);
data = {};
}
console.log(` API 响应: ${JSON.stringify(data)}`);
if (data.code === 0 || (data.url && data.url !== '')) {
console.log('\n>> ★★★ 登录成功!★★★');
// 等待页面跳转
await page.waitForTimeout(3000);
const afterUrl = page.url();
console.log('>> 自动跳转到:', afterUrl);
// 如果没跳转,手动去
if (afterUrl.includes('/home.htm') || afterUrl.includes('/index.htm')) {
const target = data.url ? ('https://www.dianxiaomi.com' + data.url) : 'https://www.dianxiaomi.com/saleManage/index.htm';
await page.goto(target, { waitUntil: 'networkidle', timeout: 30000 }).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());
return { success: true, page, context, browser };
}
const errMsg = data.error || JSON.stringify(data);
console.log(` 失败: ${errMsg}`);
if (errMsg.includes('账号') || errMsg.includes('密码错误') || errMsg.includes('不存在') || errMsg.includes('锁定')) {
console.log('>> 账号/密码问题,停止');
break;
}
} else {
console.log(' 未收到 API 响应');
await page.waitForTimeout(2000);
// 也许已经登录跳转了
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 });
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, 5000));
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}"`));
}
// 特别查找导出
const exportBtns = btns.filter(b => b.text.includes('导出'));
if (exportBtns.length) {
console.log(' ★ 导出按钮:');
exportBtns.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);
}