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

282 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.
/**
* 店小秘全自动登录 v4
* 填表单 + 调用页面原生 login() 函数 + 监听 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 });
// 多种预处理 + 多种 PSM 模式组合尝试
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) {
console.log(` OCR [p${i},psm${psm}]: "${r}"`);
return r;
}
} catch {}
}
}
// 允许长度不精确为 4
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) {
console.log(` OCR [fallback p${i}]: "${r.substring(0, 4)}"`);
return r.substring(0, 4);
}
} catch {}
}
return null;
}
async function main() {
console.log('====== 店小秘全自动登录 v4 ======\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')) {
console.log('>> Cookie 有效!');
return { success: true, page, context, browser };
}
console.log('>> Cookie 无效,重新登录\n');
// 清空 cookie 重新来
await context.clearCookies();
}
// 打开登录页
await page.goto('https://www.dianxiaomi.com/home.htm', { waitUntil: 'networkidle', timeout: 30000 });
// 先打印登录表单的 HTML 和 JS 逻辑
const loginJsCode = await page.evaluate(() => {
if (typeof login === 'function') return login.toString();
return 'login function not found';
});
console.log('>> login() 函数源码:\n', loginJsCode.substring(0, 2000));
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
console.log(`\n>> 第 ${attempt}/${MAX_RETRIES} 次尝试...`);
// 刷新验证码
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);
}
// 用 DOM 操作直接设置表单值(确保 JS 能读到)
await page.evaluate(() => {
document.getElementById('exampleInputName').value = 'MiLe-kf01';
document.getElementById('exampleInputPassword').value = 'Vxdas@302';
});
// 也用 Playwright 的 fill 确保事件触发
await page.fill('#exampleInputName', 'MiLe-kf01');
await page.fill('#exampleInputPassword', 'Vxdas@302');
// 勾选所有 checkbox
const checkboxes = await page.$$('input[type="checkbox"]');
for (const cb of checkboxes) {
const checked = await cb.isChecked();
if (!checked) await cb.check().catch(() => {});
}
// OCR
const code = await ocrCaptcha(page);
if (!code) { console.log(' OCR 失败,跳过'); continue; }
await page.fill('#verifyCode', code);
// 监听 API 响应
const apiResponsePromise = page.waitForResponse(
resp => resp.url().includes('userLoginNew2.json'),
{ timeout: 10000 }
).catch(() => null);
// 调用页面的 login() 函数
console.log(' 调用 login()...');
await page.evaluate(() => {
if (typeof login === 'function') login();
});
// 等待 API 响应
const apiResp = await apiResponsePromise;
if (apiResp) {
const data = await apiResp.json().catch(() => ({}));
console.log(` API 响应: ${JSON.stringify(data)}`);
if (data.code === 0 || data.code === 1 || data.url) {
console.log('\n>> ★★★ 登录成功!★★★');
// 等待页面可能的跳转
await page.waitForTimeout(3000);
// 如果返回了 URL手动跳转
if (data.url) {
const targetUrl = data.url.startsWith('http') ? data.url : 'https://www.dianxiaomi.com' + data.url;
await page.goto(targetUrl, { 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 || '未知错误';
console.log(` 失败: ${errMsg}`);
if (errMsg.includes('账号') || errMsg.includes('密码') || errMsg.includes('不存在') || errMsg.includes('锁定')) {
console.log('>> 账号/密码错误或被锁定,停止');
break;
}
} else {
console.log(' 未收到 API 响应,等待检查...');
await page.waitForTimeout(3000);
// 检查是否已跳转
if (!page.url().includes('/home.htm') && !page.url().includes('/index.htm')) {
console.log('>> 页面已跳转登录成功URL:', page.url());
const cookies = await context.cookies();
writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2));
return { success: true, page, context, browser };
}
}
}
console.log('\n>> 全部尝试失败');
await page.screenshot({ path: `${SCREENSHOTS_DIR}/final-fail.png`, fullPage: true });
await browser.close();
return { success: false };
}
// ====== 探索后台 ======
async function explore(page) {
console.log('\n>> ===== 探索后台 =====');
const url = page.url();
const title = await page.title();
console.log(`>> URL: ${url}`);
console.log(`>> 标题: ${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',
'/purchaseManage/selfWarehouse.htm',
'/stockManage/stockList.htm',
'/stockManage/selfWarehouse.htm',
'/warehouseManage/index.htm',
];
console.log('\n>> 尝试后台路径:');
for (const p of paths) {
try {
await page.goto(`https://www.dianxiaomi.com${p}`, { waitUntil: 'domcontentloaded', timeout: 10000 });
const t = await page.title();
const u = page.url();
const ok = !t.includes('Error') && !u.includes('/home.htm');
console.log(` ${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, span', els =>
els.filter(el => el.textContent.includes('导出'))
.map(el => ({ tag: el.tagName, text: el.textContent.trim().substring(0, 40), id: el.id, cls: (el.className || '').substring(0, 40) }))
.slice(0, 10)
);
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);
}