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