init repo
This commit is contained in:
348
auto-login-v2.mjs
Normal file
348
auto-login-v2.mjs
Normal file
@@ -0,0 +1,348 @@
|
||||
/**
|
||||
* 全自动登录店小秘 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);
|
||||
Reference in New Issue
Block a user