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

275 lines
10 KiB
JavaScript
Raw Permalink 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.
/**
* 店小秘全自动登录 + 后台探索 + 导出
* - OCR: ddddocr (Python)
* - 每次尝试用新 Context 避免安全机制
* - 通过 API 响应判断登录结果(非 JSON = 成功跳转)
*/
import { chromium } from 'playwright';
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
import { execSync } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const COOKIE_FILE = path.join(__dirname, 'cookies.json');
const SCREENSHOTS_DIR = path.join(__dirname, 'screenshots');
const DOWNLOAD_DIR = path.join(__dirname, 'downloads');
const OCR_SCRIPT = path.join(__dirname, 'ocr_captcha.py');
const MAX_RETRIES = 20;
const BASE_URL = 'https://www.dianxiaomi.com';
mkdirSync(SCREENSHOTS_DIR, { recursive: true });
mkdirSync(DOWNLOAD_DIR, { recursive: true });
function ocrCaptcha(imagePath) {
try {
return execSync(`python3 "${OCR_SCRIPT}" "${imagePath}"`, {
encoding: 'utf-8', timeout: 30000,
}).trim() || null;
} catch { return null; }
}
function timestamp() {
return new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
}
// ====== 登录 ======
async function doLogin(browser) {
console.log(`[${timestamp()}] 开始登录...\n`);
// 先试 Cookie
if (existsSync(COOKIE_FILE)) {
console.log('>> 尝试复用 Cookie...');
const ctx = 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 pg = await ctx.newPage();
const cookies = JSON.parse(readFileSync(COOKIE_FILE, 'utf-8'));
await ctx.addCookies(cookies);
// 用 AJAX 请求一个需要登录的接口检查 Cookie 是否有效
try {
await pg.goto(`${BASE_URL}/home.htm`, { waitUntil: 'load', timeout: 20000 });
const checkResult = await pg.evaluate(async () => {
try {
const resp = await fetch('/saleManage/searchSale.json', { method: 'POST' });
const text = await resp.text();
// 如果返回 JSON 数据(不是登录页 HTML说明 Cookie 有效
return { status: resp.status, isJson: text.startsWith('{') || text.startsWith('['), preview: text.substring(0, 100) };
} catch (e) {
return { error: e.message };
}
});
console.log(' Cookie 检查:', JSON.stringify(checkResult));
if (checkResult.isJson && checkResult.status === 200) {
console.log('>> Cookie 有效!\n');
return { page: pg, context: ctx };
}
} catch {}
console.log('>> Cookie 无效\n');
await ctx.close();
}
// 循环尝试登录
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
console.log(`>> 第 ${attempt}/${MAX_RETRIES} 次...`);
const ctx = 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 pg = await ctx.newPage();
// 监听登录 API 响应
let loginApiResult = null;
pg.on('response', async (resp) => {
if (resp.url().includes('userLoginNew2')) {
try {
loginApiResult = await resp.text();
} catch {
// 读取失败 = 页面已跳转 = 登录成功
loginApiResult = '__REDIRECT__';
}
}
});
try {
// 1) 加载
await pg.goto(`${BASE_URL}/home.htm`, { waitUntil: 'load', timeout: 30000 });
await pg.waitForSelector('#exampleInputName', { timeout: 10000 });
await pg.waitForFunction(() => typeof window.login === 'function', { timeout: 10000 });
await pg.waitForFunction(() => document.getElementById('verifyImgCode')?.complete === true, { timeout: 5000 }).catch(() => {});
await pg.waitForTimeout(1000);
// 2) OCR
const captchaEl = await pg.$('#verifyImgCode');
if (!captchaEl) { await ctx.close(); continue; }
const captchaPath = path.join(SCREENSHOTS_DIR, 'captcha_raw.png');
await captchaEl.screenshot({ path: captchaPath });
const code = ocrCaptcha(captchaPath);
if (!code || code.length < 3) { console.log(` OCR 失败: "${code}"`); await ctx.close(); continue; }
console.log(` 验证码: "${code}"`);
// 3) 填值
await pg.evaluate((c) => {
$('#exampleInputName').val('MiLe-kf01');
$('#exampleInputPassword').val('Vxdas@302');
$('#verifyCode').val(c);
}, code);
// 4) 调用 login()
loginApiResult = null;
const navPromise = pg.waitForNavigation({ timeout: 15000, waitUntil: 'load' }).catch(() => null);
await pg.evaluate(() => { login(); });
await navPromise;
await pg.waitForTimeout(3000);
const url = pg.url();
console.log(` URL: ${url}`);
console.log(` API: ${loginApiResult?.substring(0, 200)}`);
// 5) 判断结果
// 情况 AAPI 返回 JSON 错误
if (loginApiResult && loginApiResult !== '__REDIRECT__') {
try {
const data = JSON.parse(loginApiResult);
if (data.code === -1) {
console.log(`${data.error}`);
if (data.error?.includes('密码') || data.error?.includes('锁定')) {
await ctx.close(); return null;
}
await ctx.close(); continue;
}
if (data.code === 0) {
console.log('\n>> ★★★ 登录成功(code=0)!★★★');
if (data.url) {
await pg.goto(`${BASE_URL}${data.url}`, { waitUntil: 'load', timeout: 20000 }).catch(() => {});
}
const cookies = await ctx.cookies();
writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2));
console.log(`>> Cookie 已保存(${cookies.length} 条)\n`);
return { page: pg, context: ctx };
}
} catch {
// JSON 解析失败 = 可能是 HTML 重定向 = 成功
}
}
// 情况 BAPI 响应读取失败__REDIRECT__= 页面跳转 = 成功
if (loginApiResult === '__REDIRECT__' || !url.includes('/home.htm')) {
console.log('\n>> ★★★ 登录成功(页面跳转)!★★★');
const cookies = await ctx.cookies();
writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2));
console.log(`>> Cookie 已保存(${cookies.length} 条)\n`);
return { page: pg, context: ctx };
}
// 情况 C仍在登录页
console.log(' 仍在登录页,重试\n');
await ctx.close();
} catch (e) {
console.log(` 异常: ${e.message}`);
await ctx.close();
}
}
console.log('>> 全部尝试失败');
return null;
}
// ====== 探索后台 ======
async function explore(pg) {
console.log('>> ===== 探索后台 =====');
console.log(`>> URL: ${pg.url()}`);
await pg.screenshot({ path: path.join(SCREENSHOTS_DIR, 'after-login.png'), fullPage: true });
// 当前页面内容
const text = await pg.evaluate(() => document.body?.innerText?.substring(0, 5000));
console.log('>> 页面文本前2000字:\n', text?.substring(0, 2000));
// 尝试找到真正的后台入口
// 从当前页面的所有链接中查找
const links = await pg.$$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) {
console.log(` [${l.text}] ${l.href}`);
}
// 用 AJAX 探测后台 API
console.log('\n>> 探测后台 API...');
const apis = [
{ name: '订单搜索', url: '/saleManage/searchSale.json', method: 'POST' },
{ name: '采购建议', url: '/purchaseManage/purchaseSuggestion.json', method: 'POST' },
{ name: '采购建议列表', url: '/purchaseManage/getSuggestionList.json', method: 'POST' },
{ name: '采购单', url: '/purchaseManage/purchaseOrderList.json', method: 'POST' },
{ name: '库存列表', url: '/stockManage/stockList.json', method: 'POST' },
{ name: '自营仓库', url: '/stockManage/selfWarehouse.json', method: 'POST' },
{ name: '仓库列表', url: '/warehouseManage/warehouseList.json', method: 'POST' },
{ name: '用户信息', url: '/user/getUserInfo.json', method: 'GET' },
{ name: '菜单', url: '/user/getMenuList.json', method: 'GET' },
{ name: '首页数据', url: '/index/getData.json', method: 'GET' },
];
for (const api of apis) {
const result = await pg.evaluate(async (a) => {
try {
const resp = await fetch(a.url, { method: a.method });
const text = await resp.text();
return { status: resp.status, ok: resp.ok, preview: text.substring(0, 300) };
} catch (e) {
return { error: e.message };
}
}, api);
const isJson = result.preview?.startsWith('{') || result.preview?.startsWith('[');
console.log(` ${isJson ? '✓' : '✗'} ${api.name} (${api.url}) [${result.status}] ${result.preview?.substring(0, 150)}`);
}
// 尝试后台页面
console.log('\n>> 尝试后台页面...');
const pagePaths = [
'/saleManage/index.htm',
'/purchaseManage/purchaseSuggestion.htm',
'/purchaseManage/purchaseOrder.htm',
'/stockManage/stockList.htm',
'/warehouseManage/index.htm',
'/user/setting.htm',
'/user/index.htm',
];
for (const p of pagePaths) {
try {
await pg.goto(`${BASE_URL}${p}`, { waitUntil: 'load', timeout: 15000 });
const t = await pg.title();
const u = pg.url();
const isError = t.includes('Error') || u.includes('/home.htm');
console.log(` ${isError ? '✗' : '✓'} ${p} [${t}] -> ${u}`);
if (!isError) {
await pg.screenshot({ path: path.join(SCREENSHOTS_DIR, `page${p.replace(/\//g, '_')}.png`), fullPage: true });
const pageContent = await pg.evaluate(() => document.body?.innerText?.substring(0, 1000));
console.log(` 内容: ${pageContent?.substring(0, 300)}`);
}
} catch { console.log(`${p} 超时`); }
}
}
// ====== 主流程 ======
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] });
const result = await doLogin(browser);
if (result) {
await explore(result.page);
await result.context.close();
}
await browser.close();
process.exit(result ? 0 : 1);