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

325 lines
11 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.
/**
* 店小秘全自动登录 v3
* 直接调 AJAX API 登录,改进 OCR 预处理
*/
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 = 15;
const DOWNLOAD_DIR = './downloads';
mkdirSync(SCREENSHOTS_DIR, { recursive: true });
mkdirSync(DOWNLOAD_DIR, { recursive: true });
// ====== 多种预处理方案尝试 OCR ======
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 = [
// 方案1灰度 + 放大 + 二值化(中阈值)
async (src, dst) => {
await sharp(src).grayscale().resize({ width: 468 }).normalize().sharpen({ sigma: 1.5 }).threshold(130).toFile(dst);
},
// 方案2灰度 + 放大 + 低阈值
async (src, dst) => {
await sharp(src).grayscale().resize({ width: 468 }).normalize().threshold(100).toFile(dst);
},
// 方案3灰度 + 放大 + 高阈值
async (src, dst) => {
await sharp(src).grayscale().resize({ width: 468 }).normalize().threshold(160).toFile(dst);
},
// 方案4原图直接放大
async (src, dst) => {
await sharp(src).resize({ width: 468 }).toFile(dst);
},
// 方案5灰度 + 反色 + 二值化
async (src, dst) => {
await sharp(src).grayscale().resize({ width: 468 }).negate().normalize().threshold(128).toFile(dst);
},
];
const psmModes = ['7', '8', '13']; // 单行、单词、单行原始
for (let i = 0; i < presets.length; i++) {
const processedPath = `${SCREENSHOTS_DIR}/captcha_p${i}.png`;
try {
await presets[i](rawPath, processedPath);
} catch (e) { continue; }
for (const psm of psmModes) {
try {
const result = execSync(
`tesseract "${processedPath}" stdout --psm ${psm} -c tessedit_char_whitelist=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`,
{ encoding: 'utf-8', timeout: 10000 }
).trim().replace(/[\s\n\r]/g, '');
if (result && result.length === 4) {
console.log(` OCR [方案${i + 1}, psm${psm}]: "${result}"`);
return result;
}
} catch (e) {}
}
}
// 实在不行,返回任何 >= 4 字符的结果
for (let i = 0; i < presets.length; i++) {
const processedPath = `${SCREENSHOTS_DIR}/captcha_p${i}.png`;
if (!existsSync(processedPath)) continue;
try {
const result = execSync(
`tesseract "${processedPath}" stdout --psm 7 -c tessedit_char_whitelist=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`,
{ encoding: 'utf-8', timeout: 10000 }
).trim().replace(/[\s\n\r]/g, '');
if (result && result.length >= 4) {
console.log(` OCR [fallback 方案${i + 1}]: "${result}" (取前4字符)`);
return result.substring(0, 4);
}
} catch (e) {}
}
console.log(' OCR 全部失败');
return null;
}
// ====== 主流程 ======
async function main() {
console.log('====== 店小秘全自动登录 v3 ======\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();
try {
// 先试 Cookie
if (existsSync(COOKIE_FILE)) {
console.log('>> 尝试复用 Cookie...');
const cookies = JSON.parse(readFileSync(COOKIE_FILE, 'utf-8'));
await context.addCookies(cookies);
// 访问一个需要登录的页面来验证
const resp = await page.goto('https://www.dianxiaomi.com/saleManage/index.htm', {
waitUntil: 'domcontentloaded', timeout: 20000
}).catch(() => null);
const url = page.url();
const title = await page.title();
if (resp && !url.includes('/home.htm') && !url.includes('/index.htm') && !title.includes('Error')) {
console.log('>> Cookie 有效!直接进入后台');
return { success: true, page, context, browser };
}
console.log('>> Cookie 无效,重新登录\n');
}
// 打开登录页
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 (attempt > 1) {
await page.evaluate(() => {
const img = document.getElementById('verifyImgCode');
if (img) img.src = '/verify/code.htm?t=' + Date.now();
});
await page.waitForTimeout(1500);
}
// 填写表单
await page.fill('input[name="account"]', 'MiLe-kf01');
await page.fill('input[name="password"]', 'Vxdas@302');
// OCR 验证码
const code = await ocrCaptcha(page);
if (!code) {
console.log(' 跳过本次');
continue;
}
await page.fill('#verifyCode', code);
// 用 AJAX API 提交登录
const loginResult = await page.evaluate(async (verifyCode) => {
const formData = new FormData();
formData.append('account', 'MiLe-kf01');
formData.append('password', 'Vxdas@302');
formData.append('verifyCode', verifyCode);
formData.append('remeber', 'on');
try {
const resp = await fetch('/user/userLoginNew2.json', {
method: 'POST',
body: formData,
});
return await resp.json();
} catch (e) {
return { code: -999, error: e.message };
}
}, code);
console.log(` API 响应: ${JSON.stringify(loginResult)}`);
if (loginResult.code === 0 || loginResult.code === 1) {
console.log('\n>> ★★★ 登录成功!★★★');
console.log(`>> 跳转 URL: ${loginResult.url}`);
// 跳转到后台
if (loginResult.url) {
await page.goto('https://www.dianxiaomi.com' + loginResult.url, {
waitUntil: 'networkidle', timeout: 30000
});
} else {
await page.goto('https://www.dianxiaomi.com/saleManage/index.htm', {
waitUntil: 'networkidle', timeout: 30000
});
}
// 保存 Cookie
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 errorMsg = loginResult.error || '未知错误';
console.log(` 失败: ${errorMsg}`);
if (errorMsg.includes('账号') || errorMsg.includes('密码') || errorMsg.includes('不存在')) {
console.log('>> 账号或密码错误,停止重试');
break;
}
// 验证码错误继续重试
}
console.log('\n>> 登录失败');
await browser.close();
return { success: false };
} catch (err) {
console.error('致命错误:', err.message);
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.png`, fullPage: true });
// 获取页面文本
const bodyText = await page.evaluate(() => document.body?.innerText?.substring(0, 5000));
console.log('>> 页面内容:\n', bodyText);
// 左侧菜单
const menuItems = await page.$$eval(
'.sidebar a, .menu a, .nav a, .left-menu a, [class*="sidebar"] a, [class*="menu"] a, a[href*="Manage"], a[href*="manage"]',
els => els.map(el => ({
text: el.textContent.trim().replace(/\s+/g, ' ').substring(0, 50),
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>> 菜单链接:');
for (const m of menuItems) {
console.log(` [${m.text}] -> ${m.href}`);
}
// 所有链接
const allLinks = 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.includes('dianxiaomi'))
.filter((e, i, arr) => arr.findIndex(a => a.href === e.href) === i)
);
console.log(`\n>> 全部链接(${allLinks.length} 个):`);
for (const l of allLinks) {
const tag = (l.text.includes('采购') || l.text.includes('仓库') || l.text.includes('导出') ||
l.text.includes('库存') || l.text.includes('备货') || l.text.includes('建议')) ? '★' : ' ';
console.log(` ${tag} [${l.text}] ${l.href}`);
}
// 尝试常见后台路径
const paths = [
'/saleManage/index.htm',
'/purchaseManage/purchaseSuggestion.htm',
'/purchaseManage/purchaseOrder.htm',
'/purchaseManage/index.htm',
'/stockManage/stockList.htm',
'/stockManage/index.htm',
'/warehouseManage/index.htm',
'/warehouseManage/stockList.htm',
];
console.log('\n>> 尝试后台路径:');
for (const p of paths) {
const url = `https://www.dianxiaomi.com${p}`;
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 10000 });
const finalUrl = page.url();
const title = await page.title();
const isError = title.includes('Error') || finalUrl.includes('/home.htm');
console.log(` ${isError ? '✗' : '✓'} ${p} -> [${title}] ${finalUrl}`);
if (!isError) {
await page.screenshot({ path: `${SCREENSHOTS_DIR}/page${p.replace(/\//g, '_')}.png`, fullPage: true });
// 在该页面查找导出按钮
const exportInfo = await page.$$eval('*', els =>
els.filter(el => {
const t = el.textContent?.trim();
return t && (t === '导出' || t === '导出全部' || t === '导出建议' ||
t.includes('导出') || t.includes('下载'));
}).map(el => ({
tag: el.tagName,
text: el.textContent.trim().substring(0, 40),
id: el.id,
className: (el.className || '').substring(0, 50),
})).filter(e => ['A', 'BUTTON', 'SPAN', 'INPUT', 'DIV'].includes(e.tag))
.slice(0, 15)
);
if (exportInfo.length) {
console.log(` 导出按钮:`);
exportInfo.forEach(e => console.log(` ${e.tag}#${e.id}.${e.className}: "${e.text}"`));
}
}
} catch (e) {
console.log(`${p} -> 超时`);
}
}
}
// ====== 执行 ======
const result = await main();
if (result.success) {
await explore(result.page);
await result.browser.close();
} else {
process.exit(1);
}