init repo
This commit is contained in:
337
auto-login-v7.mjs
Normal file
337
auto-login-v7.mjs
Normal file
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* 店小秘全自动登录 v7
|
||||
* 纯 HTTP 请求登录(不走浏览器 DOM),更稳定
|
||||
* 登录成功后把 Cookie 给浏览器用于后续导出操作
|
||||
*/
|
||||
import { chromium } from 'playwright';
|
||||
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { execSync } from 'child_process';
|
||||
import sharp from 'sharp';
|
||||
import https from 'https';
|
||||
import http from 'http';
|
||||
|
||||
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 });
|
||||
|
||||
// ====== HTTP 请求辅助 ======
|
||||
function httpGet(url, cookies = '') {
|
||||
return new Promise((resolve, reject) => {
|
||||
const mod = url.startsWith('https') ? https : http;
|
||||
const req = mod.get(url, {
|
||||
headers: {
|
||||
'Cookie': cookies,
|
||||
'User-Agent': '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',
|
||||
},
|
||||
}, (res) => {
|
||||
const setCookies = res.headers['set-cookie'] || [];
|
||||
const chunks = [];
|
||||
res.on('data', c => chunks.push(c));
|
||||
res.on('end', () => resolve({
|
||||
status: res.statusCode,
|
||||
headers: res.headers,
|
||||
setCookies,
|
||||
body: Buffer.concat(chunks),
|
||||
}));
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.setTimeout(15000, () => { req.destroy(); reject(new Error('timeout')); });
|
||||
});
|
||||
}
|
||||
|
||||
function httpPost(url, data, cookies = '') {
|
||||
return new Promise((resolve, reject) => {
|
||||
const mod = url.startsWith('https') ? https : http;
|
||||
const body = new URLSearchParams(data).toString();
|
||||
const urlObj = new URL(url);
|
||||
const req = mod.request({
|
||||
hostname: urlObj.hostname,
|
||||
path: urlObj.pathname,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Cookie': cookies,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
'User-Agent': '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',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Referer': 'https://www.dianxiaomi.com/home.htm',
|
||||
},
|
||||
}, (res) => {
|
||||
const setCookies = res.headers['set-cookie'] || [];
|
||||
const chunks = [];
|
||||
res.on('data', c => chunks.push(c));
|
||||
res.on('end', () => resolve({
|
||||
status: res.statusCode,
|
||||
headers: res.headers,
|
||||
setCookies,
|
||||
body: Buffer.concat(chunks).toString('utf-8'),
|
||||
}));
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.setTimeout(15000, () => { req.destroy(); reject(new Error('timeout')); });
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function parseCookies(setCookieHeaders) {
|
||||
const map = {};
|
||||
for (const h of setCookieHeaders) {
|
||||
const parts = h.split(';')[0].split('=');
|
||||
if (parts.length >= 2) {
|
||||
map[parts[0].trim()] = parts.slice(1).join('=').trim();
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function cookieString(map) {
|
||||
return Object.entries(map).map(([k, v]) => `${k}=${v}`).join('; ');
|
||||
}
|
||||
|
||||
// ====== OCR ======
|
||||
async function ocrCaptcha(imageBuffer) {
|
||||
const rawPath = `${SCREENSHOTS_DIR}/captcha_raw.png`;
|
||||
writeFileSync(rawPath, imageBuffer);
|
||||
|
||||
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](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) return r;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
// fallback
|
||||
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;
|
||||
}
|
||||
|
||||
// ====== 纯 HTTP 登录 ======
|
||||
async function httpLogin() {
|
||||
console.log('====== 纯 HTTP 登录 ======\n');
|
||||
|
||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||
console.log(`>> 第 ${attempt}/${MAX_RETRIES} 次尝试...`);
|
||||
|
||||
try {
|
||||
// 1) 获取初始页面和 Session Cookie
|
||||
const homeResp = await httpGet('https://www.dianxiaomi.com/home.htm');
|
||||
const cookies = parseCookies(homeResp.setCookies);
|
||||
console.log(` Session: ${Object.keys(cookies).join(', ')}`);
|
||||
|
||||
// 2) 下载验证码图片(同一 Session)
|
||||
const captchaResp = await httpGet(
|
||||
`https://www.dianxiaomi.com/verify/code.htm?t=${Date.now()}`,
|
||||
cookieString(cookies)
|
||||
);
|
||||
|
||||
// 合并新 Cookie
|
||||
Object.assign(cookies, parseCookies(captchaResp.setCookies));
|
||||
|
||||
// 3) OCR 验证码
|
||||
const code = await ocrCaptcha(captchaResp.body);
|
||||
if (!code) {
|
||||
console.log(' OCR 失败,重试');
|
||||
continue;
|
||||
}
|
||||
console.log(` 验证码: "${code}"`);
|
||||
|
||||
// 4) POST 登录
|
||||
const loginResp = await httpPost(
|
||||
'https://www.dianxiaomi.com/user/userLoginNew2.json',
|
||||
{
|
||||
account: 'MiLe-kf01',
|
||||
password: 'Vxdas@302',
|
||||
verifyCode: code,
|
||||
remeber: 'on',
|
||||
loginReadAndAccept: 'on',
|
||||
url: '',
|
||||
},
|
||||
cookieString(cookies)
|
||||
);
|
||||
|
||||
// 合并登录后的 Cookie
|
||||
Object.assign(cookies, parseCookies(loginResp.setCookies));
|
||||
|
||||
console.log(` API 响应: ${loginResp.body.substring(0, 300)}`);
|
||||
|
||||
const data = JSON.parse(loginResp.body);
|
||||
|
||||
if (data.code === 0 || (data.url && !data.error)) {
|
||||
console.log('\n>> ★★★ 登录成功!★★★');
|
||||
console.log(`>> 跳转: ${data.url}`);
|
||||
|
||||
// 保存 Cookie(转换为 Playwright 格式)
|
||||
const playwrightCookies = Object.entries(cookies).map(([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
domain: 'www.dianxiaomi.com',
|
||||
path: '/',
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
sameSite: 'Lax',
|
||||
}));
|
||||
|
||||
// 补充 .dianxiaomi.com 域的 Cookie
|
||||
for (const [name, value] of Object.entries(cookies)) {
|
||||
playwrightCookies.push({
|
||||
name,
|
||||
value,
|
||||
domain: '.dianxiaomi.com',
|
||||
path: '/',
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
sameSite: 'Lax',
|
||||
});
|
||||
}
|
||||
|
||||
writeFileSync(COOKIE_FILE, JSON.stringify(playwrightCookies, null, 2));
|
||||
console.log(`>> Cookie 已保存`);
|
||||
|
||||
return { success: true, cookies, redirectUrl: data.url };
|
||||
}
|
||||
|
||||
const err = data.error || '';
|
||||
console.log(` 失败: ${err}`);
|
||||
|
||||
if (err.includes('密码错误') || err.includes('不存在') || err.includes('锁定') || err.includes('禁用')) {
|
||||
console.log('>> 账号/密码问题,停止');
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
// 验证码错误继续重试
|
||||
} catch (e) {
|
||||
console.log(` 请求错误: ${e.message}`);
|
||||
}
|
||||
|
||||
// 稍等再重试
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
}
|
||||
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
// ====== 浏览器探索后台 ======
|
||||
async function exploreWithBrowser(httpCookies, redirectUrl) {
|
||||
console.log('\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',
|
||||
});
|
||||
|
||||
// 设置 Cookie
|
||||
const playwrightCookies = Object.entries(httpCookies).map(([name, value]) => ({
|
||||
name, value,
|
||||
domain: '.dianxiaomi.com',
|
||||
path: '/',
|
||||
}));
|
||||
await context.addCookies(playwrightCookies);
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
// 跳转到后台
|
||||
const target = redirectUrl?.startsWith('/') ? `https://www.dianxiaomi.com${redirectUrl}` : 'https://www.dianxiaomi.com/saleManage/index.htm';
|
||||
console.log(`>> 打开: ${target}`);
|
||||
await page.goto(target, { waitUntil: 'networkidle', timeout: 30000 }).catch(() => {});
|
||||
|
||||
console.log(`>> URL: ${page.url()}`);
|
||||
console.log(`>> 标题: ${await page.title()}`);
|
||||
|
||||
await page.screenshot({ path: `${SCREENSHOTS_DIR}/backend-main.png`, fullPage: true });
|
||||
|
||||
// 获取页面文本
|
||||
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}`);
|
||||
}
|
||||
|
||||
// 尝试关键路径
|
||||
const paths = [
|
||||
'/saleManage/index.htm',
|
||||
'/purchaseManage/purchaseSuggestion.htm',
|
||||
'/purchaseManage/purchaseOrder.htm',
|
||||
'/stockManage/stockList.htm',
|
||||
];
|
||||
|
||||
for (const p of paths) {
|
||||
try {
|
||||
await page.goto(`https://www.dianxiaomi.com${p}`, { waitUntil: 'domcontentloaded', 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 btns = await page.$$eval('button, a.btn, .btn', els =>
|
||||
els.map(el => ({
|
||||
tag: el.tagName, text: el.textContent.trim().substring(0, 50),
|
||||
id: el.id, cls: (el.className || '').substring(0, 60),
|
||||
})).filter(e => e.text).slice(0, 30)
|
||||
);
|
||||
if (btns.length) {
|
||||
console.log(' 按钮:');
|
||||
btns.forEach(b => console.log(` ${b.tag}#${b.id}: "${b.text}"`));
|
||||
}
|
||||
}
|
||||
} catch { console.log(` ✗ ${p} 超时`); }
|
||||
}
|
||||
|
||||
// 重新保存完整 Cookie
|
||||
const allCookies = await context.cookies();
|
||||
writeFileSync(COOKIE_FILE, JSON.stringify(allCookies, null, 2));
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
// ====== 执行 ======
|
||||
const result = await httpLogin();
|
||||
if (result.success) {
|
||||
await exploreWithBrowser(result.cookies, result.redirectUrl);
|
||||
} else {
|
||||
console.log('\n>> 登录失败,退出');
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user