301 lines
11 KiB
JavaScript
301 lines
11 KiB
JavaScript
/**
|
||
* 店小秘全自动登录 v9
|
||
* - load 事件 + jQuery 等待
|
||
* - 捕获原始响应(可能是 HTML 重定向)
|
||
* - 登录后检测页面状态
|
||
*/
|
||
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 = 20;
|
||
|
||
mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
||
mkdirSync(DOWNLOAD_DIR, { recursive: true });
|
||
|
||
async function ocrCaptcha(imagePath) {
|
||
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),
|
||
async (s, d) => sharp(s).grayscale().resize({ width: 600 }).normalize().sharpen({ sigma: 3 }).threshold(120).toFile(d),
|
||
];
|
||
|
||
for (let i = 0; i < presets.length; i++) {
|
||
const p = `${SCREENSHOTS_DIR}/cap_p${i}.png`;
|
||
try { await presets[i](imagePath, 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 {}
|
||
}
|
||
}
|
||
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 waitForLoginPage(page) {
|
||
try {
|
||
await page.goto('https://www.dianxiaomi.com/home.htm', {
|
||
waitUntil: 'load',
|
||
timeout: 30000,
|
||
});
|
||
} catch (e) {
|
||
console.log(` 页面加载超时,尝试继续...`);
|
||
}
|
||
|
||
// 等 jQuery + 登录表单 + login 函数
|
||
try {
|
||
await page.waitForFunction(() => {
|
||
return typeof window.$ !== 'undefined' &&
|
||
typeof window.jQuery !== 'undefined' &&
|
||
document.getElementById('exampleInputName') &&
|
||
document.getElementById('verifyImgCode') &&
|
||
document.getElementById('verifyImgCode').complete &&
|
||
typeof window.login === 'function';
|
||
}, { timeout: 15000 });
|
||
return true;
|
||
} catch {
|
||
// 试试等更久
|
||
await page.waitForTimeout(3000);
|
||
const hasJQ = await page.evaluate(() => typeof window.$ !== 'undefined').catch(() => false);
|
||
const hasForm = await page.$('#exampleInputName');
|
||
const hasLogin = await page.evaluate(() => typeof window.login === 'function').catch(() => false);
|
||
console.log(` 状态: jQuery=${hasJQ}, form=${!!hasForm}, login=${hasLogin}`);
|
||
return hasJQ && hasForm && hasLogin;
|
||
}
|
||
}
|
||
|
||
async function main() {
|
||
console.log('====== 店小秘全自动登录 v9 ======\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();
|
||
|
||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||
console.log(`\n>> ===== 第 ${attempt}/${MAX_RETRIES} 次 =====`);
|
||
|
||
// 加载登录页
|
||
const ready = await waitForLoginPage(page);
|
||
if (!ready) {
|
||
console.log(' 页面未就绪,重试');
|
||
continue;
|
||
}
|
||
|
||
// 等验证码图片完全加载
|
||
await page.waitForTimeout(1500);
|
||
|
||
// 截图验证码
|
||
const captchaEl = await page.$('#verifyImgCode');
|
||
if (!captchaEl) { console.log(' 验证码元素不存在'); continue; }
|
||
|
||
const rawPath = `${SCREENSHOTS_DIR}/captcha_raw.png`;
|
||
await captchaEl.screenshot({ path: rawPath });
|
||
|
||
// OCR
|
||
const code = await ocrCaptcha(rawPath);
|
||
if (!code) { console.log(' OCR 失败'); continue; }
|
||
console.log(` 验证码: "${code}"`);
|
||
|
||
// 用 jQuery 设值
|
||
await page.evaluate((c) => {
|
||
$('#exampleInputName').val('MiLe-kf01');
|
||
$('#exampleInputPassword').val('Vxdas@302');
|
||
$('#verifyCode').val(c);
|
||
}, code);
|
||
|
||
// 设置响应监听
|
||
let apiResponseText = null;
|
||
const respPromise = new Promise((resolve) => {
|
||
const handler = async (resp) => {
|
||
if (resp.url().includes('userLoginNew2')) {
|
||
try {
|
||
apiResponseText = await resp.text();
|
||
console.log(` [API ${resp.status()}] ${apiResponseText.substring(0, 500)}`);
|
||
} catch (e) {
|
||
console.log(` [API] 读取响应失败: ${e.message}`);
|
||
}
|
||
page.off('response', handler);
|
||
resolve(apiResponseText);
|
||
}
|
||
};
|
||
page.on('response', handler);
|
||
// 超时兜底
|
||
setTimeout(() => { page.off('response', handler); resolve(null); }, 15000);
|
||
});
|
||
|
||
// 调用 login()
|
||
console.log(' 调用 login()...');
|
||
await page.evaluate(() => { login(); });
|
||
|
||
// 等待 API 响应
|
||
const respText = await respPromise;
|
||
|
||
if (respText) {
|
||
// 尝试解析 JSON
|
||
try {
|
||
const data = JSON.parse(respText);
|
||
console.log(` JSON: ${JSON.stringify(data)}`);
|
||
|
||
if (data.code === 0 || (data.url && data.url.length > 1)) {
|
||
console.log('\n>> ★★★ 登录成功!★★★');
|
||
await handleLoginSuccess(page, context, data.url);
|
||
return { success: true, page, context, browser };
|
||
}
|
||
|
||
const err = data.error || '';
|
||
if (err.includes('密码错误') || err.includes('不存在') || err.includes('锁定')) {
|
||
console.log('>> 严重错误,停止');
|
||
break;
|
||
}
|
||
} catch {
|
||
// 不是 JSON,可能是 HTML 重定向(登录成功)
|
||
console.log(` 响应非 JSON(${respText.length} 字节),可能已登录`);
|
||
|
||
// 等待可能的跳转
|
||
await page.waitForTimeout(3000);
|
||
const url = page.url();
|
||
const title = await page.title();
|
||
console.log(` URL=${url} 标题=${title}`);
|
||
|
||
// 检查是否离开登录页
|
||
if (!url.includes('/home.htm')) {
|
||
// 检查是否真的进了后台
|
||
const hasBackend = await page.evaluate(() => {
|
||
const text = document.body?.innerText || '';
|
||
return text.includes('退出') || text.includes('注销') || text.includes('工作台') ||
|
||
text.includes('订单') || text.includes('商品');
|
||
});
|
||
|
||
if (hasBackend || (!title.includes('Error') && !url.includes('/index.htm'))) {
|
||
console.log('\n>> ★★★ 登录成功(重定向)!★★★');
|
||
await handleLoginSuccess(page, context, null);
|
||
return { success: true, page, context, browser };
|
||
}
|
||
}
|
||
|
||
// 检查响应中是否有登录成功标志
|
||
if (respText.includes('redirect') || respText.includes('success') || respText.includes('window.location')) {
|
||
console.log(' 响应含跳转标志,尝试后台...');
|
||
await page.goto('https://www.dianxiaomi.com/saleManage/index.htm', {
|
||
waitUntil: 'load', timeout: 20000
|
||
}).catch(() => {});
|
||
|
||
const t = await page.title();
|
||
if (!t.includes('Error') && !page.url().includes('/home.htm')) {
|
||
console.log('\n>> ★★★ 登录成功!★★★');
|
||
await handleLoginSuccess(page, context, null);
|
||
return { success: true, page, context, browser };
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
console.log(' 无 API 响应');
|
||
|
||
// 检查是否已跳转
|
||
await page.waitForTimeout(3000);
|
||
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 }).catch(() => {});
|
||
await browser.close();
|
||
return { success: false };
|
||
}
|
||
|
||
async function handleLoginSuccess(page, context, redirectUrl) {
|
||
// 等页面稳定
|
||
await page.waitForTimeout(2000);
|
||
|
||
const url = page.url();
|
||
console.log('>> 当前 URL:', url);
|
||
|
||
// 如果还在首页,跳转到后台
|
||
if (url.includes('/home.htm') || url.includes('/index.htm')) {
|
||
const target = redirectUrl?.startsWith('/') ? `https://www.dianxiaomi.com${redirectUrl}` : 'https://www.dianxiaomi.com/saleManage/index.htm';
|
||
await page.goto(target, { waitUntil: 'load', timeout: 20000 }).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());
|
||
|
||
await page.screenshot({ path: `${SCREENSHOTS_DIR}/backend-main.png`, fullPage: true });
|
||
}
|
||
|
||
async function explore(page) {
|
||
console.log('\n>> ===== 探索后台 =====');
|
||
console.log(`>> URL: ${page.url()}`);
|
||
console.log(`>> 标题: ${await page.title()}`);
|
||
|
||
const text = await page.evaluate(() => document.body?.innerText?.substring(0, 8000));
|
||
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}`);
|
||
}
|
||
|
||
for (const p of ['/saleManage/index.htm', '/purchaseManage/purchaseSuggestion.htm', '/purchaseManage/purchaseOrder.htm', '/stockManage/stockList.htm']) {
|
||
try {
|
||
await page.goto(`https://www.dianxiaomi.com${p}`, { waitUntil: 'load', timeout: 15000 });
|
||
const t = await page.title();
|
||
const ok = !t.includes('Error') && !page.url().includes('/home.htm');
|
||
console.log(`\n ${ok ? '✓' : '✗'} ${p} [${t}]`);
|
||
if (ok) {
|
||
await page.screenshot({ path: `${SCREENSHOTS_DIR}/page${p.replace(/\//g, '_')}.png`, fullPage: true });
|
||
const pageText = await page.evaluate(() => document.body?.innerText?.substring(0, 3000));
|
||
console.log(' 内容:', pageText?.substring(0, 500));
|
||
}
|
||
} catch { console.log(` ✗ ${p} 超时`); }
|
||
}
|
||
}
|
||
|
||
const result = await main();
|
||
if (result.success) {
|
||
await explore(result.page);
|
||
await result.browser.close();
|
||
} else {
|
||
process.exit(1);
|
||
}
|