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

349 lines
12 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.
/**
* 全自动登录店小秘 v2 - 监听网络请求,处理 AJAX 登录
*/
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 MAX_RETRIES = 10;
mkdirSync(SCREENSHOTS_DIR, { recursive: true });
async function ocrCaptcha(page) {
// 验证码图片 ID: verifyImgCode
const captchaImg = await page.$('#verifyImgCode');
if (!captchaImg) {
console.log(' 未找到 #verifyImgCode');
return null;
}
const captchaPath = `${SCREENSHOTS_DIR}/captcha.png`;
await captchaImg.screenshot({ path: captchaPath });
// 预处理图片
const processedPath = `${SCREENSHOTS_DIR}/captcha_processed.png`;
await sharp(captchaPath)
.grayscale()
.resize({ width: 468, kernel: 'lanczos3' })
.normalize()
.sharpen({ sigma: 2 })
.threshold(140)
.negate() // 有时候反色效果更好
.toFile(processedPath);
// 同时生成一个不反色的版本
const processedPath2 = `${SCREENSHOTS_DIR}/captcha_processed2.png`;
await sharp(captchaPath)
.grayscale()
.resize({ width: 468, kernel: 'lanczos3' })
.normalize()
.sharpen({ sigma: 2 })
.threshold(140)
.toFile(processedPath2);
// 尝试两种预处理
for (const path of [processedPath2, processedPath]) {
try {
const result = execSync(
`tesseract "${path}" stdout --psm 7 -c tessedit_char_whitelist=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`,
{ encoding: 'utf-8', timeout: 10000 }
).trim().replace(/\s/g, '');
if (result && result.length >= 4 && result.length <= 6) {
console.log(` OCR 结果 (${path.includes('2') ? '正常' : '反色'}): "${result}"`);
return result;
}
} catch (e) {}
}
// 如果上面都不行,直接用原图试
try {
const result = execSync(
`tesseract "${captchaPath}" stdout --psm 7 -c tessedit_char_whitelist=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`,
{ encoding: 'utf-8', timeout: 10000 }
).trim().replace(/\s/g, '');
console.log(` OCR 结果 (原图): "${result}"`);
return result;
} catch (e) {
console.error(' OCR 失败:', e.message);
return null;
}
}
async function main() {
console.log('====== 店小秘全自动登录 v2 ======\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();
// 监听所有网络响应
const responses = [];
page.on('response', async (resp) => {
const url = resp.url();
if (url.includes('login') || url.includes('Login') || url.includes('user') ||
url.includes('auth') || url.includes('verify') || url.includes('check')) {
const status = resp.status();
let body = '';
try {
body = await resp.text();
if (body.length > 500) body = body.substring(0, 500) + '...';
} catch (e) {}
responses.push({ url: url.substring(0, 100), status, body });
console.log(` [NET] ${status} ${url.substring(0, 80)}`);
if (body) console.log(` [BODY] ${body.substring(0, 200)}`);
}
});
try {
console.log('>> 打开登录页...');
await page.goto('https://www.dianxiaomi.com/home.htm', { waitUntil: 'networkidle', timeout: 30000 });
// 查看登录表单的 action 和 method
const formInfo = await page.evaluate(() => {
const forms = document.querySelectorAll('form');
return Array.from(forms).map(f => ({
id: f.id,
action: f.action,
method: f.method,
className: f.className,
}));
});
console.log('>> 表单信息:', JSON.stringify(formInfo, null, 2));
// 查看登录按钮的 onclick 事件
const btnInfo = await page.evaluate(() => {
const btns = document.querySelectorAll('button');
return Array.from(btns).map(b => ({
text: b.textContent.trim().substring(0, 20),
onclick: b.getAttribute('onclick'),
type: b.type,
id: b.id,
className: b.className,
}));
});
console.log('>> 按钮信息:', JSON.stringify(btnInfo, null, 2));
// 查看是否有 JS 登录函数
const loginFunctions = await page.evaluate(() => {
// 查找登录相关的脚本
const scripts = document.querySelectorAll('script:not([src])');
const loginCode = [];
for (const script of scripts) {
const text = script.textContent;
if (text.includes('login') || text.includes('Login') || text.includes('登录')) {
// 只取包含登录逻辑的关键部分
const lines = text.split('\n').filter(l =>
l.includes('login') || l.includes('Login') || l.includes('ajax') ||
l.includes('submit') || l.includes('url') || l.includes('post') ||
l.includes('$.') || l.includes('fetch')
);
loginCode.push(lines.join('\n').substring(0, 800));
}
}
return loginCode;
});
console.log('>> 登录相关JS代码:');
loginFunctions.forEach(code => console.log(code));
// 多次尝试
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
console.log(`\n>> ===== 第 ${attempt} 次尝试 =====`);
// 刷新验证码(非首次)
if (attempt > 1) {
// 点击验证码图片刷新
const captchaImg = await page.$('#verifyImgCode');
if (captchaImg) {
await captchaImg.click();
await page.waitForTimeout(1500);
}
}
// 清空并填写
await page.fill('input[name="account"]', 'MiLe-kf01');
await page.fill('input[name="password"]', '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 || code.length < 4) {
console.log(' 验证码识别太短,跳过...');
const captchaImg = await page.$('#verifyImgCode');
if (captchaImg) await captchaImg.click();
await page.waitForTimeout(1000);
continue;
}
// 填验证码
await page.fill('#verifyCode', code);
// 截图确认
await page.screenshot({ path: `${SCREENSHOTS_DIR}/attempt-${attempt}.png` });
// 清空响应记录
responses.length = 0;
// 点击登录
console.log(' 点击登录...');
const loginBtn = await page.$('button:has-text("登录")');
if (!loginBtn) {
console.log(' 找不到登录按钮!');
continue;
}
await Promise.all([
loginBtn.click(),
// 同时等待可能的页面跳转或 AJAX 响应
page.waitForTimeout(5000),
]);
// 打印所有捕获的响应
console.log(`\n 捕获 ${responses.length} 个网络响应`);
// 检查页面状态
const currentUrl = page.url();
console.log(` 当前 URL: ${currentUrl}`);
// 检查页面上是否有错误消息
const pageText = await page.evaluate(() => {
// 查找弹窗、错误提示
const alerts = document.querySelectorAll('.alert, .error, .msg, .tip, .toast, [class*="error"], [class*="alert"], [class*="msg"], [class*="tip"]');
const texts = [];
alerts.forEach(el => {
const t = el.textContent.trim();
if (t) texts.push(t.substring(0, 100));
});
// 也检查 layer 弹窗(常用于中文网站)
const layerContent = document.querySelector('.layui-layer-content, .layer-content');
if (layerContent) texts.push('layer: ' + layerContent.textContent.trim().substring(0, 100));
return texts;
});
if (pageText.length > 0) {
console.log(' 页面消息:', pageText);
}
// 检查是否出现了后台元素
const hasBackend = await page.evaluate(() => {
const body = document.body.textContent;
return body.includes('退出') || body.includes('注销') || body.includes('控制台') ||
body.includes('我的账号') || body.includes('操作中心') || body.includes('工作台');
});
if (hasBackend || (currentUrl !== 'https://www.dianxiaomi.com/home.htm' &&
currentUrl !== 'https://www.dianxiaomi.com/index.htm' &&
!currentUrl.includes('home.htm'))) {
console.log('\n>> ★★★ 登录成功!★★★');
const cookies = await context.cookies();
writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2));
console.log(`>> Cookie 已保存(${cookies.length} 条)`);
await page.screenshot({ path: `${SCREENSHOTS_DIR}/backend.png`, fullPage: true });
await exploreDashboard(page, context);
await browser.close();
return true;
}
// 可能 AJAX 登录后需要手动跳转
// 检查 cookie 中是否有登录标识
const cookies = await context.cookies();
const sessionCookies = cookies.filter(c =>
c.name.toLowerCase().includes('session') ||
c.name.toLowerCase().includes('token') ||
c.name.toLowerCase().includes('user') ||
c.name.toLowerCase().includes('login')
);
console.log(' Session cookies:', sessionCookies.map(c => `${c.name}=${c.value.substring(0, 20)}...`));
// 尝试手动跳转到后台
console.log(' 尝试手动跳转后台...');
const backendUrls = [
'https://www.dianxiaomi.com/saleManage/index.htm',
'https://www.dianxiaomi.com/user/index.htm',
'https://www.dianxiaomi.com/setting/index.htm',
'https://www.dianxiaomi.com/purchaseManage/purchaseSuggestion.htm',
];
for (const url of backendUrls) {
await page.goto(url, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
const newUrl = page.url();
console.log(` ${url} -> ${newUrl}`);
if (!newUrl.includes('/home.htm') && !newUrl.includes('/index.htm')) {
console.log('\n>> ★★★ 已进入后台!★★★');
const cookies = await context.cookies();
writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2));
console.log(`>> Cookie 已保存(${cookies.length} 条)`);
await page.screenshot({ path: `${SCREENSHOTS_DIR}/backend.png`, fullPage: true });
await exploreDashboard(page, context);
await browser.close();
return true;
}
}
console.log(' 仍未进入后台,可能验证码错误,继续重试...');
}
console.log('\n>> 全部 ' + MAX_RETRIES + ' 次尝试失败');
await page.screenshot({ path: `${SCREENSHOTS_DIR}/final-fail.png`, fullPage: true });
await browser.close();
return false;
} catch (err) {
console.error('致命错误:', err.message);
await page.screenshot({ path: `${SCREENSHOTS_DIR}/error.png` }).catch(() => {});
await browser.close();
return false;
}
}
async function exploreDashboard(page, context) {
console.log('\n>> ===== 探索后台 =====');
console.log('>> URL:', page.url());
console.log('>> 标题:', await page.title());
// 获取整个页面文本的前 3000 字符
const bodyText = await page.evaluate(() => document.body?.innerText?.substring(0, 3000));
console.log('>> 页面内容前3000字:\n', bodyText);
// 所有链接
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 && e.href.startsWith('http'))
.filter((e, i, arr) => arr.findIndex(a => a.href === e.href) === i)
);
console.log(`\n>> 所有链接(${links.length}个):`);
for (const l of links) {
const tag = (l.text.includes('采购') || l.text.includes('仓库') ||
l.text.includes('导出') || l.text.includes('库存') ||
l.text.includes('备货')) ? '★' : ' ';
console.log(` ${tag} [${l.text}] ${l.href}`);
}
}
const ok = await main();
process.exit(ok ? 0 : 1);