init repo
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
.next/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.nuxt/
|
||||||
|
.output/
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# bootstrap-gitea-no-git
|
||||||
90
.memory/dxm-auto-procurement.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
---
|
||||||
|
name: 店小秘自动采购全流程
|
||||||
|
description: 从数据采集到下单的AI自动化采购流程,人只做关键审批(财务批款、供应商下单确认)
|
||||||
|
type: project
|
||||||
|
---
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
将店小秘的采购全流程AI自动化,人只在关键决策点介入(确认/拒绝)。
|
||||||
|
|
||||||
|
## 完整流程链
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 数据采集(AI自动)
|
||||||
|
→ 登录店小秘,爬取采购建议 + 自营仓库数据
|
||||||
|
→ 每4小时执行一次,24小时6次
|
||||||
|
|
||||||
|
2. 数据分析(AI自动)
|
||||||
|
→ 本地合并表格,交叉比对库存/销量/在途
|
||||||
|
→ 生成采购需求清单(哪些SKU、买多少、找哪个供应商)
|
||||||
|
|
||||||
|
3. 采购审批(人判断)
|
||||||
|
→ AI 生成采购建议摘要,推送给人
|
||||||
|
→ 人确认:是否转发给财务批款
|
||||||
|
→ 人确认:是否转发给供应商微信下单
|
||||||
|
|
||||||
|
4. 财务批款(人判断)
|
||||||
|
→ AI 生成付款申请
|
||||||
|
→ 转发财务审批
|
||||||
|
|
||||||
|
5. 供应商下单(人判断)
|
||||||
|
→ AI 生成下单信息(SKU/数量/价格)
|
||||||
|
→ 人确认后转发供应商微信
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** 采购环节重复性高(每天多次导出、对比、算量),但涉及资金和外部沟通必须人确认。AI 做数据密集型工作,人做判断。
|
||||||
|
|
||||||
|
**How to apply:** 每一步独立模块化,数据流串联。步骤1已完成(`~/Projects/business/20260324-店小秘自动导出/`),下一步是步骤2的表格合并分析。
|
||||||
|
|
||||||
|
## 当前进度
|
||||||
|
|
||||||
|
- ✅ 步骤1:数据采集脚本已完成,cron每4小时自动导出
|
||||||
|
- 采购建议 xlsx
|
||||||
|
- 仓库清单 zip/xlsx(单品SKU + 组合SKU)
|
||||||
|
- 路径:`~/Projects/business/20260324-店小秘自动导出/`
|
||||||
|
- **需要先手动登录一次**:`node login.mjs`(输验证码,保存Cookie)
|
||||||
|
- 之后自动跑:`node export.mjs`(cron 每4小时)
|
||||||
|
- Cookie 过期会弹 macOS 通知,重新 `node login.mjs` 即可
|
||||||
|
- **用户还没实际启用,待用户自己操作**
|
||||||
|
|
||||||
|
- ⬜ 步骤2:表格合并分析 → 生成采购需求
|
||||||
|
- ⬜ 步骤3:推送审批 → 人确认
|
||||||
|
- ⬜ 步骤4:财务批款
|
||||||
|
- ⬜ 步骤5:供应商下单
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/Projects/business/20260324-店小秘自动导出
|
||||||
|
node login.mjs # 首次/Cookie过期:手动登录
|
||||||
|
node export.mjs # 自动导出(cron 已配好每4小时跑)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 账号信息
|
||||||
|
|
||||||
|
- 平台:店小秘 https://www.dianxiaomi.com
|
||||||
|
- 账号:MiLe-kf01(子账号,部分权限受限)
|
||||||
|
- 登录方式:手动登录 + Cookie 持久化复用
|
||||||
|
|
||||||
|
## 技术路线决策(2026-03-28 确认)
|
||||||
|
|
||||||
|
**铁律:不碰 UI,只抓接口。**
|
||||||
|
|
||||||
|
店小秘没有开放 API,但前端所有操作底层都是 REST 请求。技术路线:
|
||||||
|
|
||||||
|
1. **登录拿 Cookie** — Playwright 模拟登录(唯一需要浏览器的环节)
|
||||||
|
2. **抓真实 API** — 拿到 Cookie 后直接 HTTP 请求调店小秘内部接口,不再碰页面
|
||||||
|
3. **数据落表** — JSON → 清洗 → CSV / 数据库
|
||||||
|
4. **本地合并** — 跨店铺、跨平台数据在本地处理
|
||||||
|
|
||||||
|
**Why:** 评估过 AI 操作网页方案(如阿里 PageAgent),结论是对复杂 SaaS 不可靠——Element Plus 组件识别不了、速度慢、每步都要调 LLM 有成本。抓接口方案:批量快(几百条/秒)、稳定(接口不轻易变)、零 LLM 成本。
|
||||||
|
|
||||||
|
**How to apply:** 任何新增的数据采集需求,优先 F12 抓接口,写 HTTP 请求脚本。只在登录/验证码环节用 Playwright。绝不走"AI 点击页面按钮"的路线。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- Playwright(仅用于登录)
|
||||||
|
- Node.js(HTTP 请求 + 数据处理)
|
||||||
|
- cron 定时任务
|
||||||
|
- macOS 通知(Cookie 过期提醒)
|
||||||
3
.memory/worklog.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"entries": []
|
||||||
|
}
|
||||||
24
.project.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "店小秘自动采购",
|
||||||
|
"description": "AI 自动采购全流程(数据采集+cron+自动下单)",
|
||||||
|
"status": "active",
|
||||||
|
"kind": "app",
|
||||||
|
"created": "2026-03-24",
|
||||||
|
"stack": [
|
||||||
|
"Node.js",
|
||||||
|
"Playwright",
|
||||||
|
"Python"
|
||||||
|
],
|
||||||
|
"urls": [
|
||||||
|
{
|
||||||
|
"url": "https://www.dianxiaomi.com",
|
||||||
|
"type": "app",
|
||||||
|
"label": "www.dianxiaomi.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"worklog": {
|
||||||
|
"path": ".memory/worklog.json",
|
||||||
|
"auto": true
|
||||||
|
},
|
||||||
|
"ports": []
|
||||||
|
}
|
||||||
21
AGENTS.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# 店小秘自动采购 Agent Rules
|
||||||
|
|
||||||
|
## Must Read First
|
||||||
|
|
||||||
|
- `.project.json` 是机器真源:公网链接、快捷登录、凭证引用都以它为准
|
||||||
|
- `RULES.md` 是人工规则和部署事实:启动命令、平台、域名、注意事项都写这里
|
||||||
|
- 不允许编造不存在的域名、账号、密码;未知就保持空白并明确标记待补充
|
||||||
|
|
||||||
|
## Deployment Metadata Contract
|
||||||
|
|
||||||
|
- 任何任务只要新增、删除或修改公网地址,必须在同一次任务里更新 `.project.json`
|
||||||
|
- `urls[]` 推荐显式写 `type`:`app`、`backend`、`docs`、`admin`、`repo`
|
||||||
|
- 项目专属的网页登录信息,如果允许放进仓库,就写 `.project.json.quick_login`
|
||||||
|
- 不能直接入库的敏感登录,不要伪造 `quick_login`,改为写 `.project.json.credentials` 引用
|
||||||
|
- 数据库密码、API Key、服务器 root 密码,不属于 `quick_login`
|
||||||
|
|
||||||
|
## Completion Gate
|
||||||
|
|
||||||
|
- 部署完成后,不允许在 `.project.json` 缺少最新公网链接的状态下结束任务
|
||||||
|
- 部署完成后,必须同步更新 `RULES.md` 的部署事实
|
||||||
|
- 如果只更新了代码但没回写部署元数据,这个任务不算完成
|
||||||
21
CLAUDE.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# 店小秘自动采购 Agent Rules
|
||||||
|
|
||||||
|
## Must Read First
|
||||||
|
|
||||||
|
- `.project.json` 是机器真源:公网链接、快捷登录、凭证引用都以它为准
|
||||||
|
- `RULES.md` 是人工规则和部署事实:启动命令、平台、域名、注意事项都写这里
|
||||||
|
- 不允许编造不存在的域名、账号、密码;未知就保持空白并明确标记待补充
|
||||||
|
|
||||||
|
## Deployment Metadata Contract
|
||||||
|
|
||||||
|
- 任何任务只要新增、删除或修改公网地址,必须在同一次任务里更新 `.project.json`
|
||||||
|
- `urls[]` 推荐显式写 `type`:`app`、`backend`、`docs`、`admin`、`repo`
|
||||||
|
- 项目专属的网页登录信息,如果允许放进仓库,就写 `.project.json.quick_login`
|
||||||
|
- 不能直接入库的敏感登录,不要伪造 `quick_login`,改为写 `.project.json.credentials` 引用
|
||||||
|
- 数据库密码、API Key、服务器 root 密码,不属于 `quick_login`
|
||||||
|
|
||||||
|
## Completion Gate
|
||||||
|
|
||||||
|
- 部署完成后,不允许在 `.project.json` 缺少最新公网链接的状态下结束任务
|
||||||
|
- 部署完成后,必须同步更新 `RULES.md` 的部署事实
|
||||||
|
- 如果只更新了代码但没回写部署元数据,这个任务不算完成
|
||||||
37
RULES.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# 店小秘自动采购
|
||||||
|
|
||||||
|
## 启动
|
||||||
|
- `待补充`
|
||||||
|
|
||||||
|
## 部署事实
|
||||||
|
- 平台:待定
|
||||||
|
- 发布状态:已部署
|
||||||
|
- 主站 / 前端:https://www.dianxiaomi.com
|
||||||
|
- API / 后端:待定
|
||||||
|
- 文档 / 解析:待定
|
||||||
|
- 管理后台:待定
|
||||||
|
- 代码仓:待定
|
||||||
|
|
||||||
|
## 快捷登录
|
||||||
|
- 登录地址:待补充
|
||||||
|
- 用户名:待补充
|
||||||
|
- 密码:待补充
|
||||||
|
- 说明:这里只写项目专属网页登录;数据库密码、API Key、服务器 root 密码不要写这里
|
||||||
|
|
||||||
|
## 元数据回写清单
|
||||||
|
- 新增或变更公网地址后,必须同步更新 `.project.json.urls`
|
||||||
|
- 如果有网页后台登录:
|
||||||
|
- 可直接入库:写 `.project.json.quick_login`
|
||||||
|
- 不应入库:写 `.project.json.credentials` 引用
|
||||||
|
- 部署完成后,`RULES.md` 和 `.project.json` 必须同一次任务一起更新
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
- 待补充
|
||||||
|
|
||||||
|
## 规则
|
||||||
|
- 不允许编造不存在的部署域名、账号、密码
|
||||||
|
- 没有公网地址时,`.project.json.urls` 保持空数组
|
||||||
|
- 任何部署或域名变化,都要先改元数据,再视为任务完成
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
- 待补充
|
||||||
348
auto-login-v2.mjs
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
/**
|
||||||
|
* 全自动登录店小秘 v2 - 监听网络请求,处理 AJAX 登录
|
||||||
|
*/
|
||||||
|
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 = 10;
|
||||||
|
|
||||||
|
mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
||||||
|
|
||||||
|
async function ocrCaptcha(page) {
|
||||||
|
// 验证码图片 ID: verifyImgCode
|
||||||
|
const captchaImg = await page.$('#verifyImgCode');
|
||||||
|
if (!captchaImg) {
|
||||||
|
console.log(' 未找到 #verifyImgCode');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const captchaPath = `${SCREENSHOTS_DIR}/captcha.png`;
|
||||||
|
await captchaImg.screenshot({ path: captchaPath });
|
||||||
|
|
||||||
|
// 预处理图片
|
||||||
|
const processedPath = `${SCREENSHOTS_DIR}/captcha_processed.png`;
|
||||||
|
await sharp(captchaPath)
|
||||||
|
.grayscale()
|
||||||
|
.resize({ width: 468, kernel: 'lanczos3' })
|
||||||
|
.normalize()
|
||||||
|
.sharpen({ sigma: 2 })
|
||||||
|
.threshold(140)
|
||||||
|
.negate() // 有时候反色效果更好
|
||||||
|
.toFile(processedPath);
|
||||||
|
|
||||||
|
// 同时生成一个不反色的版本
|
||||||
|
const processedPath2 = `${SCREENSHOTS_DIR}/captcha_processed2.png`;
|
||||||
|
await sharp(captchaPath)
|
||||||
|
.grayscale()
|
||||||
|
.resize({ width: 468, kernel: 'lanczos3' })
|
||||||
|
.normalize()
|
||||||
|
.sharpen({ sigma: 2 })
|
||||||
|
.threshold(140)
|
||||||
|
.toFile(processedPath2);
|
||||||
|
|
||||||
|
// 尝试两种预处理
|
||||||
|
for (const path of [processedPath2, processedPath]) {
|
||||||
|
try {
|
||||||
|
const result = execSync(
|
||||||
|
`tesseract "${path}" stdout --psm 7 -c tessedit_char_whitelist=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`,
|
||||||
|
{ encoding: 'utf-8', timeout: 10000 }
|
||||||
|
).trim().replace(/\s/g, '');
|
||||||
|
|
||||||
|
if (result && result.length >= 4 && result.length <= 6) {
|
||||||
|
console.log(` OCR 结果 (${path.includes('2') ? '正常' : '反色'}): "${result}"`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果上面都不行,直接用原图试
|
||||||
|
try {
|
||||||
|
const result = execSync(
|
||||||
|
`tesseract "${captchaPath}" stdout --psm 7 -c tessedit_char_whitelist=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`,
|
||||||
|
{ encoding: 'utf-8', timeout: 10000 }
|
||||||
|
).trim().replace(/\s/g, '');
|
||||||
|
console.log(` OCR 结果 (原图): "${result}"`);
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(' OCR 失败:', e.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('====== 店小秘全自动登录 v2 ======\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();
|
||||||
|
|
||||||
|
// 监听所有网络响应
|
||||||
|
const responses = [];
|
||||||
|
page.on('response', async (resp) => {
|
||||||
|
const url = resp.url();
|
||||||
|
if (url.includes('login') || url.includes('Login') || url.includes('user') ||
|
||||||
|
url.includes('auth') || url.includes('verify') || url.includes('check')) {
|
||||||
|
const status = resp.status();
|
||||||
|
let body = '';
|
||||||
|
try {
|
||||||
|
body = await resp.text();
|
||||||
|
if (body.length > 500) body = body.substring(0, 500) + '...';
|
||||||
|
} catch (e) {}
|
||||||
|
responses.push({ url: url.substring(0, 100), status, body });
|
||||||
|
console.log(` [NET] ${status} ${url.substring(0, 80)}`);
|
||||||
|
if (body) console.log(` [BODY] ${body.substring(0, 200)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('>> 打开登录页...');
|
||||||
|
await page.goto('https://www.dianxiaomi.com/home.htm', { waitUntil: 'networkidle', timeout: 30000 });
|
||||||
|
|
||||||
|
// 查看登录表单的 action 和 method
|
||||||
|
const formInfo = await page.evaluate(() => {
|
||||||
|
const forms = document.querySelectorAll('form');
|
||||||
|
return Array.from(forms).map(f => ({
|
||||||
|
id: f.id,
|
||||||
|
action: f.action,
|
||||||
|
method: f.method,
|
||||||
|
className: f.className,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
console.log('>> 表单信息:', JSON.stringify(formInfo, null, 2));
|
||||||
|
|
||||||
|
// 查看登录按钮的 onclick 事件
|
||||||
|
const btnInfo = await page.evaluate(() => {
|
||||||
|
const btns = document.querySelectorAll('button');
|
||||||
|
return Array.from(btns).map(b => ({
|
||||||
|
text: b.textContent.trim().substring(0, 20),
|
||||||
|
onclick: b.getAttribute('onclick'),
|
||||||
|
type: b.type,
|
||||||
|
id: b.id,
|
||||||
|
className: b.className,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
console.log('>> 按钮信息:', JSON.stringify(btnInfo, null, 2));
|
||||||
|
|
||||||
|
// 查看是否有 JS 登录函数
|
||||||
|
const loginFunctions = await page.evaluate(() => {
|
||||||
|
// 查找登录相关的脚本
|
||||||
|
const scripts = document.querySelectorAll('script:not([src])');
|
||||||
|
const loginCode = [];
|
||||||
|
for (const script of scripts) {
|
||||||
|
const text = script.textContent;
|
||||||
|
if (text.includes('login') || text.includes('Login') || text.includes('登录')) {
|
||||||
|
// 只取包含登录逻辑的关键部分
|
||||||
|
const lines = text.split('\n').filter(l =>
|
||||||
|
l.includes('login') || l.includes('Login') || l.includes('ajax') ||
|
||||||
|
l.includes('submit') || l.includes('url') || l.includes('post') ||
|
||||||
|
l.includes('$.') || l.includes('fetch')
|
||||||
|
);
|
||||||
|
loginCode.push(lines.join('\n').substring(0, 800));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return loginCode;
|
||||||
|
});
|
||||||
|
console.log('>> 登录相关JS代码:');
|
||||||
|
loginFunctions.forEach(code => console.log(code));
|
||||||
|
|
||||||
|
// 多次尝试
|
||||||
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||||
|
console.log(`\n>> ===== 第 ${attempt} 次尝试 =====`);
|
||||||
|
|
||||||
|
// 刷新验证码(非首次)
|
||||||
|
if (attempt > 1) {
|
||||||
|
// 点击验证码图片刷新
|
||||||
|
const captchaImg = await page.$('#verifyImgCode');
|
||||||
|
if (captchaImg) {
|
||||||
|
await captchaImg.click();
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空并填写
|
||||||
|
await page.fill('input[name="account"]', 'MiLe-kf01');
|
||||||
|
await page.fill('input[name="password"]', '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 || code.length < 4) {
|
||||||
|
console.log(' 验证码识别太短,跳过...');
|
||||||
|
const captchaImg = await page.$('#verifyImgCode');
|
||||||
|
if (captchaImg) await captchaImg.click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填验证码
|
||||||
|
await page.fill('#verifyCode', code);
|
||||||
|
|
||||||
|
// 截图确认
|
||||||
|
await page.screenshot({ path: `${SCREENSHOTS_DIR}/attempt-${attempt}.png` });
|
||||||
|
|
||||||
|
// 清空响应记录
|
||||||
|
responses.length = 0;
|
||||||
|
|
||||||
|
// 点击登录
|
||||||
|
console.log(' 点击登录...');
|
||||||
|
const loginBtn = await page.$('button:has-text("登录")');
|
||||||
|
if (!loginBtn) {
|
||||||
|
console.log(' 找不到登录按钮!');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
loginBtn.click(),
|
||||||
|
// 同时等待可能的页面跳转或 AJAX 响应
|
||||||
|
page.waitForTimeout(5000),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 打印所有捕获的响应
|
||||||
|
console.log(`\n 捕获 ${responses.length} 个网络响应`);
|
||||||
|
|
||||||
|
// 检查页面状态
|
||||||
|
const currentUrl = page.url();
|
||||||
|
console.log(` 当前 URL: ${currentUrl}`);
|
||||||
|
|
||||||
|
// 检查页面上是否有错误消息
|
||||||
|
const pageText = await page.evaluate(() => {
|
||||||
|
// 查找弹窗、错误提示
|
||||||
|
const alerts = document.querySelectorAll('.alert, .error, .msg, .tip, .toast, [class*="error"], [class*="alert"], [class*="msg"], [class*="tip"]');
|
||||||
|
const texts = [];
|
||||||
|
alerts.forEach(el => {
|
||||||
|
const t = el.textContent.trim();
|
||||||
|
if (t) texts.push(t.substring(0, 100));
|
||||||
|
});
|
||||||
|
// 也检查 layer 弹窗(常用于中文网站)
|
||||||
|
const layerContent = document.querySelector('.layui-layer-content, .layer-content');
|
||||||
|
if (layerContent) texts.push('layer: ' + layerContent.textContent.trim().substring(0, 100));
|
||||||
|
return texts;
|
||||||
|
});
|
||||||
|
if (pageText.length > 0) {
|
||||||
|
console.log(' 页面消息:', pageText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否出现了后台元素
|
||||||
|
const hasBackend = await page.evaluate(() => {
|
||||||
|
const body = document.body.textContent;
|
||||||
|
return body.includes('退出') || body.includes('注销') || body.includes('控制台') ||
|
||||||
|
body.includes('我的账号') || body.includes('操作中心') || body.includes('工作台');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasBackend || (currentUrl !== 'https://www.dianxiaomi.com/home.htm' &&
|
||||||
|
currentUrl !== 'https://www.dianxiaomi.com/index.htm' &&
|
||||||
|
!currentUrl.includes('home.htm'))) {
|
||||||
|
console.log('\n>> ★★★ 登录成功!★★★');
|
||||||
|
const cookies = await context.cookies();
|
||||||
|
writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2));
|
||||||
|
console.log(`>> Cookie 已保存(${cookies.length} 条)`);
|
||||||
|
|
||||||
|
await page.screenshot({ path: `${SCREENSHOTS_DIR}/backend.png`, fullPage: true });
|
||||||
|
await exploreDashboard(page, context);
|
||||||
|
await browser.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可能 AJAX 登录后需要手动跳转
|
||||||
|
// 检查 cookie 中是否有登录标识
|
||||||
|
const cookies = await context.cookies();
|
||||||
|
const sessionCookies = cookies.filter(c =>
|
||||||
|
c.name.toLowerCase().includes('session') ||
|
||||||
|
c.name.toLowerCase().includes('token') ||
|
||||||
|
c.name.toLowerCase().includes('user') ||
|
||||||
|
c.name.toLowerCase().includes('login')
|
||||||
|
);
|
||||||
|
console.log(' Session cookies:', sessionCookies.map(c => `${c.name}=${c.value.substring(0, 20)}...`));
|
||||||
|
|
||||||
|
// 尝试手动跳转到后台
|
||||||
|
console.log(' 尝试手动跳转后台...');
|
||||||
|
const backendUrls = [
|
||||||
|
'https://www.dianxiaomi.com/saleManage/index.htm',
|
||||||
|
'https://www.dianxiaomi.com/user/index.htm',
|
||||||
|
'https://www.dianxiaomi.com/setting/index.htm',
|
||||||
|
'https://www.dianxiaomi.com/purchaseManage/purchaseSuggestion.htm',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const url of backendUrls) {
|
||||||
|
await page.goto(url, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||||
|
const newUrl = page.url();
|
||||||
|
console.log(` ${url} -> ${newUrl}`);
|
||||||
|
if (!newUrl.includes('/home.htm') && !newUrl.includes('/index.htm')) {
|
||||||
|
console.log('\n>> ★★★ 已进入后台!★★★');
|
||||||
|
const cookies = await context.cookies();
|
||||||
|
writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2));
|
||||||
|
console.log(`>> Cookie 已保存(${cookies.length} 条)`);
|
||||||
|
await page.screenshot({ path: `${SCREENSHOTS_DIR}/backend.png`, fullPage: true });
|
||||||
|
await exploreDashboard(page, context);
|
||||||
|
await browser.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(' 仍未进入后台,可能验证码错误,继续重试...');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n>> 全部 ' + MAX_RETRIES + ' 次尝试失败');
|
||||||
|
await page.screenshot({ path: `${SCREENSHOTS_DIR}/final-fail.png`, fullPage: true });
|
||||||
|
await browser.close();
|
||||||
|
return false;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('致命错误:', err.message);
|
||||||
|
await page.screenshot({ path: `${SCREENSHOTS_DIR}/error.png` }).catch(() => {});
|
||||||
|
await browser.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exploreDashboard(page, context) {
|
||||||
|
console.log('\n>> ===== 探索后台 =====');
|
||||||
|
console.log('>> URL:', page.url());
|
||||||
|
console.log('>> 标题:', await page.title());
|
||||||
|
|
||||||
|
// 获取整个页面文本的前 3000 字符
|
||||||
|
const bodyText = await page.evaluate(() => document.body?.innerText?.substring(0, 3000));
|
||||||
|
console.log('>> 页面内容(前3000字):\n', bodyText);
|
||||||
|
|
||||||
|
// 所有链接
|
||||||
|
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 && e.href.startsWith('http'))
|
||||||
|
.filter((e, i, arr) => arr.findIndex(a => a.href === e.href) === i)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`\n>> 所有链接(${links.length}个):`);
|
||||||
|
for (const l of links) {
|
||||||
|
const tag = (l.text.includes('采购') || l.text.includes('仓库') ||
|
||||||
|
l.text.includes('导出') || l.text.includes('库存') ||
|
||||||
|
l.text.includes('备货')) ? '★' : ' ';
|
||||||
|
console.log(` ${tag} [${l.text}] ${l.href}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = await main();
|
||||||
|
process.exit(ok ? 0 : 1);
|
||||||
324
auto-login-v3.mjs
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
/**
|
||||||
|
* 店小秘全自动登录 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);
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
285
auto-login-v5.mjs
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
/**
|
||||||
|
* 店小秘全自动登录 v5
|
||||||
|
* 用 jQuery 设值 + 点击按钮 + 监听 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 });
|
||||||
|
|
||||||
|
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) return r;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// fallback: 任何 >=3 字符
|
||||||
|
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 main() {
|
||||||
|
console.log('====== 店小秘全自动登录 v5 ======\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') && !page.url().includes('/index.htm')) {
|
||||||
|
console.log('>> Cookie 有效!');
|
||||||
|
return { success: true, page, context, browser };
|
||||||
|
}
|
||||||
|
console.log('>> Cookie 无效\n');
|
||||||
|
await context.clearCookies();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开登录页
|
||||||
|
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 (!page.url().includes('home.htm') && !page.url().includes('index.htm')) {
|
||||||
|
await page.goto('https://www.dianxiaomi.com/home.htm', { waitUntil: 'networkidle', timeout: 30000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新验证码
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用 jQuery 设置表单值(确保 .val() 能读到)
|
||||||
|
await page.evaluate(() => {
|
||||||
|
if (typeof $ !== 'undefined') {
|
||||||
|
$('#exampleInputName').val('MiLe-kf01').trigger('change').trigger('input');
|
||||||
|
$('#exampleInputPassword').val('Vxdas@302').trigger('change').trigger('input');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// OCR 验证码
|
||||||
|
const code = await ocrCaptcha(page);
|
||||||
|
if (!code) { console.log(' OCR 失败'); continue; }
|
||||||
|
console.log(` 验证码: "${code}"`);
|
||||||
|
|
||||||
|
// 用 jQuery 设验证码
|
||||||
|
await page.evaluate((c) => {
|
||||||
|
if (typeof $ !== 'undefined') {
|
||||||
|
$('#verifyCode').val(c).trigger('change').trigger('input');
|
||||||
|
}
|
||||||
|
}, code);
|
||||||
|
|
||||||
|
// 截图确认
|
||||||
|
await page.screenshot({ path: `${SCREENSHOTS_DIR}/attempt-${attempt}.png` });
|
||||||
|
|
||||||
|
// 验证 jQuery 读到的值
|
||||||
|
const formValues = await page.evaluate(() => {
|
||||||
|
if (typeof $ === 'undefined') return { error: 'no jQuery' };
|
||||||
|
return {
|
||||||
|
account: $.trim($('#exampleInputName').val()),
|
||||||
|
password: $.trim($('#exampleInputPassword').val()),
|
||||||
|
verifyCode: $.trim($('#verifyCode').val()),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
console.log(` 表单值: ${JSON.stringify(formValues)}`);
|
||||||
|
|
||||||
|
// 监听 API 响应
|
||||||
|
const apiResponsePromise = page.waitForResponse(
|
||||||
|
resp => resp.url().includes('userLoginNew2.json'),
|
||||||
|
{ timeout: 15000 }
|
||||||
|
).catch(() => null);
|
||||||
|
|
||||||
|
// 点击登录按钮(按钮 onclick="login()")
|
||||||
|
console.log(' 点击登录按钮...');
|
||||||
|
await page.click('#loginBtn');
|
||||||
|
|
||||||
|
// 等待 API 响应
|
||||||
|
const apiResp = await apiResponsePromise;
|
||||||
|
|
||||||
|
if (apiResp) {
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = await apiResp.json();
|
||||||
|
} catch {
|
||||||
|
const text = await apiResp.text().catch(() => '');
|
||||||
|
console.log(` API 原始响应: ${text.substring(0, 200)}`);
|
||||||
|
data = {};
|
||||||
|
}
|
||||||
|
console.log(` API 响应: ${JSON.stringify(data)}`);
|
||||||
|
|
||||||
|
if (data.code === 0 || (data.url && data.url !== '')) {
|
||||||
|
console.log('\n>> ★★★ 登录成功!★★★');
|
||||||
|
|
||||||
|
// 等待页面跳转
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
const afterUrl = page.url();
|
||||||
|
console.log('>> 自动跳转到:', afterUrl);
|
||||||
|
|
||||||
|
// 如果没跳转,手动去
|
||||||
|
if (afterUrl.includes('/home.htm') || afterUrl.includes('/index.htm')) {
|
||||||
|
const target = data.url ? ('https://www.dianxiaomi.com' + data.url) : 'https://www.dianxiaomi.com/saleManage/index.htm';
|
||||||
|
await page.goto(target, { 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 || JSON.stringify(data);
|
||||||
|
console.log(` 失败: ${errMsg}`);
|
||||||
|
|
||||||
|
if (errMsg.includes('账号') || errMsg.includes('密码错误') || errMsg.includes('不存在') || errMsg.includes('锁定')) {
|
||||||
|
console.log('>> 账号/密码问题,停止');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(' 未收到 API 响应');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// 也许已经登录跳转了
|
||||||
|
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 });
|
||||||
|
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-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',
|
||||||
|
'/stockManage/stockList.htm',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const p of paths) {
|
||||||
|
try {
|
||||||
|
await page.goto(`https://www.dianxiaomi.com${p}`, { waitUntil: 'domcontentloaded', timeout: 10000 });
|
||||||
|
const t = await page.title();
|
||||||
|
const ok = !t.includes('Error');
|
||||||
|
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, [class*="export"], [class*="download"]', 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.cls}: "${b.text}"`));
|
||||||
|
}
|
||||||
|
// 特别查找导出
|
||||||
|
const exportBtns = btns.filter(b => b.text.includes('导出'));
|
||||||
|
if (exportBtns.length) {
|
||||||
|
console.log(' ★ 导出按钮:');
|
||||||
|
exportBtns.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);
|
||||||
|
}
|
||||||
273
auto-login-v6.mjs
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
/**
|
||||||
|
* 店小秘全自动登录 v6
|
||||||
|
* 每次尝试完整刷新页面 + type 模拟输入 + 响应监听
|
||||||
|
*/
|
||||||
|
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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('====== 店小秘全自动登录 v6 ======\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();
|
||||||
|
|
||||||
|
// 全局响应监听
|
||||||
|
let lastLoginResponse = null;
|
||||||
|
page.on('response', async (resp) => {
|
||||||
|
if (resp.url().includes('userLoginNew2.json')) {
|
||||||
|
try {
|
||||||
|
const body = await resp.text();
|
||||||
|
lastLoginResponse = body;
|
||||||
|
console.log(` [API] ${resp.status()} ${body.substring(0, 300)}`);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 先试 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') && !page.url().includes('/index.htm')) {
|
||||||
|
console.log('>> Cookie 有效!');
|
||||||
|
return { success: true, page, context, browser };
|
||||||
|
}
|
||||||
|
console.log('>> Cookie 无效\n');
|
||||||
|
await context.clearCookies();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||||
|
console.log(`\n>> ===== 第 ${attempt}/${MAX_RETRIES} 次 =====`);
|
||||||
|
lastLoginResponse = null;
|
||||||
|
|
||||||
|
// 每次重新加载登录页(确保干净状态)
|
||||||
|
await page.goto('https://www.dianxiaomi.com/home.htm', { waitUntil: 'networkidle', timeout: 30000 });
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// 确认表单元素存在
|
||||||
|
const hasForm = await page.$('#exampleInputName');
|
||||||
|
if (!hasForm) {
|
||||||
|
console.log(' 登录表单不存在,跳过');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 截图验证码
|
||||||
|
const captchaImg = await page.$('#verifyImgCode');
|
||||||
|
if (!captchaImg) {
|
||||||
|
console.log(' 验证码图片不存在');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawPath = `${SCREENSHOTS_DIR}/captcha_raw.png`;
|
||||||
|
await captchaImg.screenshot({ path: rawPath });
|
||||||
|
|
||||||
|
// OCR
|
||||||
|
const code = await ocrCaptcha(rawPath);
|
||||||
|
if (!code) {
|
||||||
|
console.log(' OCR 失败');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
console.log(` 验证码: "${code}"`);
|
||||||
|
|
||||||
|
// 清空输入框后逐个输入(模拟真人)
|
||||||
|
await page.click('#exampleInputName', { clickCount: 3 }); // 全选
|
||||||
|
await page.keyboard.press('Backspace');
|
||||||
|
await page.type('#exampleInputName', 'MiLe-kf01', { delay: 50 });
|
||||||
|
|
||||||
|
await page.click('#exampleInputPassword', { clickCount: 3 });
|
||||||
|
await page.keyboard.press('Backspace');
|
||||||
|
await page.type('#exampleInputPassword', 'Vxdas@302', { delay: 50 });
|
||||||
|
|
||||||
|
await page.click('#verifyCode', { clickCount: 3 });
|
||||||
|
await page.keyboard.press('Backspace');
|
||||||
|
await page.type('#verifyCode', code, { delay: 50 });
|
||||||
|
|
||||||
|
// 确认值
|
||||||
|
const vals = await page.evaluate(() => ({
|
||||||
|
a: document.getElementById('exampleInputName')?.value,
|
||||||
|
p: document.getElementById('exampleInputPassword')?.value,
|
||||||
|
c: document.getElementById('verifyCode')?.value,
|
||||||
|
}));
|
||||||
|
console.log(` 实际值: 账号="${vals.a}" 密码="${vals.p ? '***' : 'empty'}" 验证码="${vals.c}"`);
|
||||||
|
|
||||||
|
// 截图
|
||||||
|
await page.screenshot({ path: `${SCREENSHOTS_DIR}/attempt-${attempt}.png` });
|
||||||
|
|
||||||
|
// 点击登录
|
||||||
|
console.log(' 点击登录...');
|
||||||
|
await page.click('#loginBtn');
|
||||||
|
|
||||||
|
// 等待 API 响应或页面跳转
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
// 检查 API 响应
|
||||||
|
if (lastLoginResponse) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(lastLoginResponse);
|
||||||
|
if (data.code === 0 || (data.url && data.url !== '' && !data.error)) {
|
||||||
|
console.log('\n>> ★★★ 登录成功!★★★');
|
||||||
|
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
const afterUrl = page.url();
|
||||||
|
console.log('>> 当前URL:', afterUrl);
|
||||||
|
|
||||||
|
// 如果没自动跳转,手动跳
|
||||||
|
if (afterUrl.includes('/home.htm') || afterUrl.includes('/index.htm')) {
|
||||||
|
const target = data.url?.startsWith('/') ? 'https://www.dianxiaomi.com' + data.url : 'https://www.dianxiaomi.com/saleManage/index.htm';
|
||||||
|
await page.goto(target, { waitUntil: 'networkidle', timeout: 30000 }).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 err = data.error || '';
|
||||||
|
console.log(` 失败: ${err}`);
|
||||||
|
|
||||||
|
if (err.includes('密码错误') || err.includes('不存在') || err.includes('锁定') || err.includes('禁用')) {
|
||||||
|
console.log('>> 严重错误,停止');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.log(` API 响应非 JSON: ${lastLoginResponse.substring(0, 100)}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 没有 API 响应,检查页面是否已跳转
|
||||||
|
const url = page.url();
|
||||||
|
const title = await page.title();
|
||||||
|
console.log(` 无 API 响应,URL=${url}, 标题=${title}`);
|
||||||
|
|
||||||
|
if (!url.includes('/home.htm') && !url.includes('/index.htm') && !title.includes('Error')) {
|
||||||
|
console.log('>> 已跳转后台!');
|
||||||
|
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 });
|
||||||
|
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-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: 10000 });
|
||||||
|
const t = await page.title();
|
||||||
|
const ok = !t.includes('Error');
|
||||||
|
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, [class*="export"], [class*="download"]', 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.cls}: "${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);
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
260
auto-login-v8.mjs
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
/**
|
||||||
|
* 店小秘全自动登录 v8
|
||||||
|
* 浏览器方案:稳定性修复
|
||||||
|
* - domcontentloaded 替代 networkidle
|
||||||
|
* - 等 jQuery 就绪后再操作
|
||||||
|
* - waitForResponse 确保 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 = 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 main() {
|
||||||
|
console.log('====== 店小秘全自动登录 v8 ======\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();
|
||||||
|
|
||||||
|
// 监听请求体(调试用)
|
||||||
|
page.on('request', req => {
|
||||||
|
if (req.url().includes('userLoginNew2')) {
|
||||||
|
console.log(` [REQ] ${req.method()} ${req.url()}`);
|
||||||
|
console.log(` [REQ body] ${req.postData()?.substring(0, 500)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
page.on('response', async resp => {
|
||||||
|
if (resp.url().includes('userLoginNew2')) {
|
||||||
|
try {
|
||||||
|
const body = await resp.text();
|
||||||
|
console.log(` [RESP] ${resp.status()} ${body.substring(0, 300)}`);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||||
|
console.log(`\n>> ===== 第 ${attempt}/${MAX_RETRIES} 次 =====`);
|
||||||
|
|
||||||
|
// 每次完整刷新
|
||||||
|
try {
|
||||||
|
await page.goto('https://www.dianxiaomi.com/home.htm', {
|
||||||
|
waitUntil: 'domcontentloaded',
|
||||||
|
timeout: 20000,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log(` 页面加载失败: ${e.message}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待 jQuery 和登录表单就绪
|
||||||
|
try {
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
return typeof $ !== 'undefined' &&
|
||||||
|
document.getElementById('exampleInputName') &&
|
||||||
|
document.getElementById('verifyImgCode') &&
|
||||||
|
typeof login === 'function';
|
||||||
|
}, { timeout: 10000 });
|
||||||
|
} catch {
|
||||||
|
console.log(' JS 未就绪');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等验证码图片加载
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// 截图验证码
|
||||||
|
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 设值(login() 用 $.trim($('#xxx').val()) 读取)
|
||||||
|
await page.evaluate((c) => {
|
||||||
|
$('#exampleInputName').val('MiLe-kf01');
|
||||||
|
$('#exampleInputPassword').val('Vxdas@302');
|
||||||
|
$('#verifyCode').val(c);
|
||||||
|
}, code);
|
||||||
|
|
||||||
|
// 确认
|
||||||
|
const vals = await page.evaluate(() => ({
|
||||||
|
a: $.trim($('#exampleInputName').val()),
|
||||||
|
p: $.trim($('#exampleInputPassword').val()) ? '***' : 'empty',
|
||||||
|
c: $.trim($('#verifyCode').val()),
|
||||||
|
}));
|
||||||
|
console.log(` 值确认: ${JSON.stringify(vals)}`);
|
||||||
|
|
||||||
|
// 设置 waitForResponse
|
||||||
|
const respPromise = page.waitForResponse(
|
||||||
|
r => r.url().includes('userLoginNew2.json'),
|
||||||
|
{ timeout: 15000 }
|
||||||
|
).catch(() => null);
|
||||||
|
|
||||||
|
// 直接调用 login() 函数
|
||||||
|
console.log(' 调用 login()...');
|
||||||
|
const loginResult = await page.evaluate(() => {
|
||||||
|
try {
|
||||||
|
login();
|
||||||
|
return 'called';
|
||||||
|
} catch (e) {
|
||||||
|
return 'error: ' + e.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(` login() 返回: ${loginResult}`);
|
||||||
|
|
||||||
|
// 等待 API 响应
|
||||||
|
const resp = await respPromise;
|
||||||
|
|
||||||
|
if (resp) {
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = await resp.json();
|
||||||
|
} catch {
|
||||||
|
console.log(' 响应非JSON');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` 结果: ${JSON.stringify(data)}`);
|
||||||
|
|
||||||
|
if (data.code === 0 || (data.url && data.url.length > 1 && !data.error)) {
|
||||||
|
console.log('\n>> ★★★ 登录成功!★★★');
|
||||||
|
|
||||||
|
// 等页面跳转
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
console.log('>> 跳转后 URL:', page.url());
|
||||||
|
|
||||||
|
if (page.url().includes('/home.htm') || page.url().includes('/index.htm')) {
|
||||||
|
const target = data.url?.startsWith('/') ? `https://www.dianxiaomi.com${data.url}` : 'https://www.dianxiaomi.com/saleManage/index.htm';
|
||||||
|
await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 20000 }).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 err = data.error || '';
|
||||||
|
console.log(` 错误: ${err}`);
|
||||||
|
|
||||||
|
if (err.includes('密码错误') || err.includes('不存在') || err.includes('锁定')) {
|
||||||
|
console.log('>> 严重错误,停止');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(' 无 API 响应(超时)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.screenshot({ path: `${SCREENSHOTS_DIR}/final-fail.png`, fullPage: true }).catch(() => {});
|
||||||
|
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-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}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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: '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 }))
|
||||||
|
.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} 超时`); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await main();
|
||||||
|
if (result.success) {
|
||||||
|
await explore(result.page);
|
||||||
|
await result.browser.close();
|
||||||
|
} else {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
300
auto-login-v9.mjs
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
/**
|
||||||
|
* 店小秘全自动登录 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);
|
||||||
|
}
|
||||||
338
auto-login.mjs
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
/**
|
||||||
|
* 全自动登录店小秘(含验证码 OCR)
|
||||||
|
* 登录成功后保存 Cookie + 截图后台页面结构
|
||||||
|
*/
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
import { writeFileSync, readFileSync, existsSync, mkdirSync, unlinkSync } from 'fs';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
const COOKIE_FILE = './cookies.json';
|
||||||
|
const SCREENSHOTS_DIR = './screenshots';
|
||||||
|
const MAX_CAPTCHA_RETRIES = 10;
|
||||||
|
|
||||||
|
mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
||||||
|
|
||||||
|
// ====== 验证码 OCR ======
|
||||||
|
async function solveCaptcha(page) {
|
||||||
|
// 找到验证码图片
|
||||||
|
const captchaImg = await page.$('#verifyCodeImg, img[id*="verify"], img[id*="captcha"], img[src*="verify"], img[src*="captcha"], .verify-code img, img[onclick*="verify"]');
|
||||||
|
|
||||||
|
if (!captchaImg) {
|
||||||
|
// 如果没有独立 img 标签,可能是 canvas 或背景图,尝试找验证码区域
|
||||||
|
console.log(' 未找到验证码 img 标签,尝试其他方式...');
|
||||||
|
|
||||||
|
// 尝试通过验证码输入框附近的图片
|
||||||
|
const codeInput = await page.$('#verifyCode, input[name="verifyCode"]');
|
||||||
|
if (codeInput) {
|
||||||
|
const parent = await codeInput.evaluateHandle(el => el.parentElement);
|
||||||
|
const nearbyImg = await page.evaluateHandle(
|
||||||
|
el => el.querySelector('img') || el.nextElementSibling?.querySelector('img') || el.parentElement?.querySelector('img'),
|
||||||
|
parent
|
||||||
|
);
|
||||||
|
if (nearbyImg) {
|
||||||
|
const captchaPath = `${SCREENSHOTS_DIR}/captcha.png`;
|
||||||
|
await nearbyImg.asElement()?.screenshot({ path: captchaPath });
|
||||||
|
if (existsSync(captchaPath)) {
|
||||||
|
return await ocrImage(captchaPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后尝试:截图整个验证码区域
|
||||||
|
console.log(' 尝试截图验证码区域...');
|
||||||
|
// 找到 "点击刷新" 按钮附近的图片
|
||||||
|
const refreshBtn = await page.$('button:has-text("点击刷新"), a:has-text("点击刷新"), span:has-text("点击刷新")');
|
||||||
|
if (refreshBtn) {
|
||||||
|
// 验证码可能在刷新按钮的兄弟元素中
|
||||||
|
const captchaArea = await page.evaluateHandle(
|
||||||
|
btn => btn.previousElementSibling || btn.parentElement,
|
||||||
|
refreshBtn
|
||||||
|
);
|
||||||
|
const imgEl = await captchaArea.asElement();
|
||||||
|
if (imgEl) {
|
||||||
|
const captchaPath = `${SCREENSHOTS_DIR}/captcha.png`;
|
||||||
|
await imgEl.screenshot({ path: captchaPath });
|
||||||
|
return await ocrImage(captchaPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 截图验证码图片
|
||||||
|
const captchaPath = `${SCREENSHOTS_DIR}/captcha.png`;
|
||||||
|
await captchaImg.screenshot({ path: captchaPath });
|
||||||
|
console.log(' 验证码图片已截图');
|
||||||
|
|
||||||
|
return await ocrImage(captchaPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ocrImage(imagePath) {
|
||||||
|
try {
|
||||||
|
// 预处理:灰度 + 高对比度 + 放大 + 二值化
|
||||||
|
const processedPath = imagePath.replace('.png', '_processed.png');
|
||||||
|
await sharp(imagePath)
|
||||||
|
.grayscale()
|
||||||
|
.resize({ width: 400, kernel: 'lanczos3' }) // 放大
|
||||||
|
.normalize() // 增强对比
|
||||||
|
.sharpen({ sigma: 2 }) // 锐化
|
||||||
|
.threshold(128) // 二值化
|
||||||
|
.toFile(processedPath);
|
||||||
|
|
||||||
|
// 调用 Tesseract OCR
|
||||||
|
const result = execSync(
|
||||||
|
`tesseract "${processedPath}" stdout --psm 7 -c tessedit_char_whitelist=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`,
|
||||||
|
{ encoding: 'utf-8', timeout: 10000 }
|
||||||
|
).trim().replace(/\s/g, '');
|
||||||
|
|
||||||
|
console.log(` OCR 结果: "${result}"`);
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(' OCR 失败:', e.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 刷新验证码 ======
|
||||||
|
async function refreshCaptcha(page) {
|
||||||
|
// 尝试点击刷新按钮或验证码图片本身
|
||||||
|
const refreshBtn = await page.$('button:has-text("点击刷新")');
|
||||||
|
if (refreshBtn) {
|
||||||
|
await refreshBtn.click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击验证码图片刷新
|
||||||
|
const captchaImg = await page.$('#verifyCodeImg, img[id*="verify"], img[src*="verify"], img[src*="captcha"]');
|
||||||
|
if (captchaImg) {
|
||||||
|
await captchaImg.click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 主流程 ======
|
||||||
|
async function main() {
|
||||||
|
console.log('====== 店小秘全自动登录 ======\n');
|
||||||
|
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
headless: true,
|
||||||
|
args: ['--no-sandbox', '--disable-setuid-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);
|
||||||
|
await page.goto('https://www.dianxiaomi.com/saleManage/index.htm', { waitUntil: 'networkidle', timeout: 30000 });
|
||||||
|
|
||||||
|
if (!page.url().includes('/home.htm') && !page.url().includes('/index.htm')) {
|
||||||
|
console.log('>> Cookie 有效,已直接进入后台!URL:', page.url());
|
||||||
|
await exploreDashboard(page);
|
||||||
|
await browser.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
console.log('>> Cookie 已过期,需要重新登录\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开登录页
|
||||||
|
console.log('>> 打开登录页...');
|
||||||
|
await page.goto('https://www.dianxiaomi.com/home.htm', { waitUntil: 'networkidle', timeout: 30000 });
|
||||||
|
|
||||||
|
// 分析页面上的验证码结构
|
||||||
|
console.log('>> 分析验证码结构...');
|
||||||
|
const pageInfo = await page.evaluate(() => {
|
||||||
|
const imgs = Array.from(document.querySelectorAll('img'));
|
||||||
|
return imgs.map(img => ({
|
||||||
|
id: img.id,
|
||||||
|
src: img.src?.substring(0, 100),
|
||||||
|
className: img.className,
|
||||||
|
width: img.width,
|
||||||
|
height: img.height,
|
||||||
|
alt: img.alt,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
console.log(' 页面图片:', JSON.stringify(pageInfo, null, 2));
|
||||||
|
|
||||||
|
// 多次尝试登录
|
||||||
|
for (let attempt = 1; attempt <= MAX_CAPTCHA_RETRIES; attempt++) {
|
||||||
|
console.log(`\n>> 第 ${attempt}/${MAX_CAPTCHA_RETRIES} 次尝试登录...`);
|
||||||
|
|
||||||
|
// 填写账号密码
|
||||||
|
await page.fill('input[name="account"]', '');
|
||||||
|
await page.fill('input[name="account"]', 'MiLe-kf01');
|
||||||
|
await page.fill('input[type="password"]', '');
|
||||||
|
await page.fill('input[type="password"]', 'Vxdas@302');
|
||||||
|
|
||||||
|
// 勾选记住我
|
||||||
|
const rememberCheckbox = await page.$('input[name="remeber"]');
|
||||||
|
if (rememberCheckbox) {
|
||||||
|
await rememberCheckbox.check().catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新验证码(确保是新的)
|
||||||
|
if (attempt > 1) {
|
||||||
|
await refreshCaptcha(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// OCR 验证码
|
||||||
|
const captchaCode = await solveCaptcha(page);
|
||||||
|
if (!captchaCode || captchaCode.length < 3) {
|
||||||
|
console.log(` 验证码识别结果太短 ("${captchaCode}"),刷新重试...`);
|
||||||
|
await refreshCaptcha(page);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填入验证码
|
||||||
|
await page.fill('#verifyCode, input[name="verifyCode"]', '');
|
||||||
|
await page.fill('#verifyCode, input[name="verifyCode"]', captchaCode);
|
||||||
|
|
||||||
|
// 截图确认
|
||||||
|
await page.screenshot({ path: `${SCREENSHOTS_DIR}/attempt-${attempt}.png` });
|
||||||
|
|
||||||
|
// 点击登录
|
||||||
|
const loginBtn = await page.$('button:has-text("登录")');
|
||||||
|
if (loginBtn) {
|
||||||
|
await loginBtn.click();
|
||||||
|
} else {
|
||||||
|
console.log(' 未找到登录按钮!');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待页面响应
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// 检查是否登录成功
|
||||||
|
const currentUrl = page.url();
|
||||||
|
console.log(` 登录后 URL: ${currentUrl}`);
|
||||||
|
|
||||||
|
// 检查是否有错误提示
|
||||||
|
const errorMsg = await page.$eval('.error-msg, .alert-danger, .login-error, .msg-error, .text-danger', el => el.textContent.trim()).catch(() => null);
|
||||||
|
if (errorMsg) {
|
||||||
|
console.log(` 登录失败: ${errorMsg}`);
|
||||||
|
await refreshCaptcha(page);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 URL 是否变化(离开登录页 = 成功)
|
||||||
|
if (!currentUrl.includes('/home.htm')) {
|
||||||
|
console.log('>> 登录成功!');
|
||||||
|
|
||||||
|
// 保存 Cookie
|
||||||
|
const cookies = await context.cookies();
|
||||||
|
writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2));
|
||||||
|
console.log(`>> Cookie 已保存(${cookies.length} 条)`);
|
||||||
|
|
||||||
|
await page.screenshot({ path: `${SCREENSHOTS_DIR}/05-login-success.png`, fullPage: true });
|
||||||
|
|
||||||
|
// 探索后台
|
||||||
|
await exploreDashboard(page);
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(' 仍在登录页,验证码可能错误,重试...');
|
||||||
|
await refreshCaptcha(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n>> 登录失败:验证码识别 ' + MAX_CAPTCHA_RETRIES + ' 次均未成功');
|
||||||
|
await page.screenshot({ path: `${SCREENSHOTS_DIR}/login-failed.png`, fullPage: true });
|
||||||
|
await browser.close();
|
||||||
|
return false;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('致命错误:', err.message);
|
||||||
|
await page.screenshot({ path: `${SCREENSHOTS_DIR}/error.png`, fullPage: true }).catch(() => {});
|
||||||
|
await browser.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 探索后台页面结构 ======
|
||||||
|
async function exploreDashboard(page) {
|
||||||
|
console.log('\n>> ===== 探索后台页面 =====');
|
||||||
|
console.log('>> 当前 URL:', page.url());
|
||||||
|
|
||||||
|
// 截图
|
||||||
|
await page.screenshot({ path: `${SCREENSHOTS_DIR}/dashboard.png`, fullPage: true });
|
||||||
|
|
||||||
|
// 收集所有链接
|
||||||
|
const allLinks = await page.$$eval('a', els =>
|
||||||
|
els.map(el => ({
|
||||||
|
text: el.textContent.trim().replace(/\s+/g, ' ').substring(0, 50),
|
||||||
|
href: el.href,
|
||||||
|
}))
|
||||||
|
.filter(e => e.text && e.href && e.href.includes('dianxiaomi.com'))
|
||||||
|
.filter((e, i, arr) => arr.findIndex(a => a.href === e.href) === i)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`\n>> 共 ${allLinks.length} 个链接:`);
|
||||||
|
for (const link of allLinks) {
|
||||||
|
const tag = (link.text.includes('采购') || link.text.includes('仓库') ||
|
||||||
|
link.text.includes('导出') || link.text.includes('库存') ||
|
||||||
|
link.text.includes('备货')) ? '★' : ' ';
|
||||||
|
console.log(` ${tag} [${link.text}] -> ${link.href}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试直接访问常见的采购管理页面路径
|
||||||
|
const possiblePaths = [
|
||||||
|
'/saleManage/index.htm',
|
||||||
|
'/purchaseManage/purchaseSuggestion.htm',
|
||||||
|
'/purchaseManage/index.htm',
|
||||||
|
'/warehouse/index.htm',
|
||||||
|
'/stockManage/index.htm',
|
||||||
|
'/inventory/index.htm',
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log('\n>> 尝试常见后台路径:');
|
||||||
|
for (const path of possiblePaths) {
|
||||||
|
const url = `https://www.dianxiaomi.com${path}`;
|
||||||
|
const resp = await page.goto(url, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => null);
|
||||||
|
if (resp) {
|
||||||
|
const finalUrl = page.url();
|
||||||
|
const title = await page.title();
|
||||||
|
const status = resp.status();
|
||||||
|
console.log(` ${status === 200 && !finalUrl.includes('/home.htm') ? '✓' : '✗'} ${path} -> ${finalUrl} [${title}]`);
|
||||||
|
|
||||||
|
if (status === 200 && !finalUrl.includes('/home.htm')) {
|
||||||
|
await page.screenshot({ path: `${SCREENSHOTS_DIR}/page-${path.replace(/\//g, '_')}.png`, fullPage: true });
|
||||||
|
|
||||||
|
// 在这个页面上找导出按钮
|
||||||
|
const exportBtns = await page.$$eval(
|
||||||
|
'button, a, span, div',
|
||||||
|
els => els
|
||||||
|
.filter(el => el.textContent.includes('导出') || el.textContent.includes('下载'))
|
||||||
|
.map(el => ({
|
||||||
|
tag: el.tagName,
|
||||||
|
text: el.textContent.trim().substring(0, 40),
|
||||||
|
id: el.id,
|
||||||
|
className: el.className?.substring(0, 60),
|
||||||
|
}))
|
||||||
|
.slice(0, 20)
|
||||||
|
);
|
||||||
|
if (exportBtns.length > 0) {
|
||||||
|
console.log(` 导出相关按钮:`);
|
||||||
|
for (const btn of exportBtns) {
|
||||||
|
console.log(` ${btn.tag}#${btn.id}.${btn.className}: "${btn.text}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await main();
|
||||||
|
process.exit(success ? 0 : 1);
|
||||||
BIN
captcha-samples/captcha_1.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
captcha-samples/captcha_10.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
captcha-samples/captcha_2.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
captcha-samples/captcha_3.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
captcha-samples/captcha_4.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
captcha-samples/captcha_5.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
captcha-samples/captcha_6.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
captcha-samples/captcha_7.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
captcha-samples/captcha_8.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
captcha-samples/captcha_9.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
122
cookies.json
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "HMACCOUNT_BFESS",
|
||||||
|
"value": "DE81BA945A4718DB",
|
||||||
|
"domain": ".hm.baidu.com",
|
||||||
|
"path": "/",
|
||||||
|
"expires": 1808969756.983332,
|
||||||
|
"httpOnly": false,
|
||||||
|
"secure": true,
|
||||||
|
"sameSite": "None"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hm_lvt_f8001a3f3d9bf5923f780580eb550c0b",
|
||||||
|
"value": "1774409757",
|
||||||
|
"domain": ".dianxiaomi.com",
|
||||||
|
"path": "/",
|
||||||
|
"expires": 1805945762,
|
||||||
|
"httpOnly": false,
|
||||||
|
"secure": false,
|
||||||
|
"sameSite": "Lax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "HMACCOUNT",
|
||||||
|
"value": "DE81BA945A4718DB",
|
||||||
|
"domain": ".dianxiaomi.com",
|
||||||
|
"path": "/",
|
||||||
|
"expires": -1,
|
||||||
|
"httpOnly": false,
|
||||||
|
"secure": false,
|
||||||
|
"sameSite": "Lax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dxm_i",
|
||||||
|
"value": "MjAzOTMyMSFhVDB5TURNNU16SXghMGFkMDczZDY0MzQ5NDk0OWVhODZhODFjYWZlNDM0MzQ",
|
||||||
|
"domain": "www.dianxiaomi.com",
|
||||||
|
"path": "/",
|
||||||
|
"expires": 1805513759.572959,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": false,
|
||||||
|
"sameSite": "Lax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dxm_t",
|
||||||
|
"value": "MTc3NDQwOTc1OSFkRDB4TnpjME5EQTVOelU1ITU2MjIxMWU0NmE2NWEwMTIwODZhNzNhZTRmN2NiNDM4",
|
||||||
|
"domain": "www.dianxiaomi.com",
|
||||||
|
"path": "/",
|
||||||
|
"expires": 1805513759.572972,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": false,
|
||||||
|
"sameSite": "Lax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dxm_c",
|
||||||
|
"value": "Ym9hUjBWOW8hWXoxaWIyRlNNRlk1YnchMWY3ZmU1NjU1MzEzNDQ5MTNjYjViOTI5NDI5OGE1YjY",
|
||||||
|
"domain": "www.dianxiaomi.com",
|
||||||
|
"path": "/",
|
||||||
|
"expires": 1805513759.572984,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": false,
|
||||||
|
"sameSite": "Lax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dxm_w",
|
||||||
|
"value": "YmEwNTU4OWIwOTkxNzc3ZWM2NGQ0ZjE1Y2Q2ZTdlY2YhZHoxaVlUQTFOVGc1WWpBNU9URTNOemRsWXpZMFpEUm1NVFZqWkRabE4yVmpaZyE2MGVhZTdlZjY5ZWFjNDJhM2EyNmEzMjRlNTBhNWVlZA",
|
||||||
|
"domain": "www.dianxiaomi.com",
|
||||||
|
"path": "/",
|
||||||
|
"expires": 1805513759.572995,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": false,
|
||||||
|
"sameSite": "Lax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dxm_s",
|
||||||
|
"value": "aZL8NmoNN9DAmVrE0zMNVpZx2hD5pBNHusp0Lo8v944",
|
||||||
|
"domain": "www.dianxiaomi.com",
|
||||||
|
"path": "/",
|
||||||
|
"expires": 1805513759.573007,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": false,
|
||||||
|
"sameSite": "Lax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_dxm_ad_client_id",
|
||||||
|
"value": "F5F82B4A7CC2A143730D622C2C19E99AD",
|
||||||
|
"domain": "www.dianxiaomi.com",
|
||||||
|
"path": "/",
|
||||||
|
"expires": 1777001761,
|
||||||
|
"httpOnly": false,
|
||||||
|
"secure": false,
|
||||||
|
"sameSite": "Lax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hm_lpvt_f8001a3f3d9bf5923f780580eb550c0b",
|
||||||
|
"value": "1774409762",
|
||||||
|
"domain": ".dianxiaomi.com",
|
||||||
|
"path": "/",
|
||||||
|
"expires": -1,
|
||||||
|
"httpOnly": false,
|
||||||
|
"secure": false,
|
||||||
|
"sameSite": "Lax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MYJ_fapsc5t4tc",
|
||||||
|
"value": "JTdCJTIyZGV2aWNlSWQlMjIlM0ElMjIzODUyMDgxNi02YWE5LTQ1MjktOTlkNy1mZWE3NzUzOGY5ODQlMjIlMkMlMjJ1c2VySWQlMjIlM0ElMjIyMDM5MzIxJTIyJTJDJTIycGFyZW50SWQlMjIlM0ElMjI5NjAwOTAlMjIlMkMlMjJzZXNzaW9uSWQlMjIlM0ExNzc0NDA5NzYyNDQ4JTJDJTIyb3B0T3V0JTIyJTNBZmFsc2UlMkMlMjJsYXN0RXZlbnRJZCUyMiUzQTAlN0Q=",
|
||||||
|
"domain": ".dianxiaomi.com",
|
||||||
|
"path": "/",
|
||||||
|
"expires": 1805945762,
|
||||||
|
"httpOnly": false,
|
||||||
|
"secure": false,
|
||||||
|
"sameSite": "Lax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "JSESSIONID",
|
||||||
|
"value": "37E305A9312D0A025EC60F7A301BAD61",
|
||||||
|
"domain": "www.dianxiaomi.com",
|
||||||
|
"path": "/",
|
||||||
|
"expires": -1,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": false,
|
||||||
|
"sameSite": "Lax"
|
||||||
|
}
|
||||||
|
]
|
||||||
174
debug-export.mjs
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
/**
|
||||||
|
* 调试导出:点击导出按钮后,监控网络请求、页面变化、弹窗等
|
||||||
|
*/
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
import { readFileSync, existsSync, mkdirSync } from 'fs';
|
||||||
|
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 SS = path.join(__dirname, 'screenshots');
|
||||||
|
const BASE = 'https://www.dianxiaomi.com';
|
||||||
|
|
||||||
|
mkdirSync(SS, { recursive: true });
|
||||||
|
|
||||||
|
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] });
|
||||||
|
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',
|
||||||
|
acceptDownloads: true,
|
||||||
|
});
|
||||||
|
const page = await ctx.newPage();
|
||||||
|
|
||||||
|
// Cookie
|
||||||
|
await ctx.addCookies(JSON.parse(readFileSync(COOKIE_FILE, 'utf-8')));
|
||||||
|
|
||||||
|
// 监听所有网络请求
|
||||||
|
page.on('request', req => {
|
||||||
|
const url = req.url();
|
||||||
|
if (url.includes('export') || url.includes('Export') || url.includes('download') ||
|
||||||
|
url.includes('Download') || url.includes('导出') || url.includes('.xls') ||
|
||||||
|
url.includes('.csv') || url.includes('.xlsx')) {
|
||||||
|
console.log(` [REQ] ${req.method()} ${url}`);
|
||||||
|
if (req.postData()) console.log(` [POST] ${req.postData().substring(0, 200)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
page.on('response', async resp => {
|
||||||
|
const url = resp.url();
|
||||||
|
if (url.includes('export') || url.includes('Export') || url.includes('download') ||
|
||||||
|
url.includes('Download') || url.includes('.xls') || url.includes('.csv')) {
|
||||||
|
const ct = resp.headers()['content-type'] || '';
|
||||||
|
const cd = resp.headers()['content-disposition'] || '';
|
||||||
|
console.log(` [RESP] ${resp.status()} ${url.substring(0, 100)}`);
|
||||||
|
console.log(` [CT] ${ct} [CD] ${cd}`);
|
||||||
|
if (ct.includes('json') || ct.includes('text')) {
|
||||||
|
try {
|
||||||
|
const body = await resp.text();
|
||||||
|
console.log(` [BODY] ${body.substring(0, 500)}`);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
page.on('download', d => {
|
||||||
|
console.log(` [DOWNLOAD] ${d.suggestedFilename()} url=${d.url()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
page.on('dialog', async d => {
|
||||||
|
console.log(` [DIALOG] ${d.type()}: ${d.message()}`);
|
||||||
|
await d.accept();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听新页面/标签
|
||||||
|
ctx.on('page', p => {
|
||||||
|
console.log(` [NEW PAGE] ${p.url()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// === 测试采购建议的导出 ===
|
||||||
|
console.log('\n=== 采购建议页面 ===');
|
||||||
|
await page.goto(`${BASE}/purchasingProposal/index.htm?state=3`, { waitUntil: 'load', timeout: 30000 });
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
// 点击"导出建议"下拉
|
||||||
|
const dropdown = await page.locator('text=导出建议').first();
|
||||||
|
await dropdown.click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// 点击"导出全部"
|
||||||
|
console.log('>> 点击"导出全部"...');
|
||||||
|
const exportAll = await page.locator('text=导出全部').first();
|
||||||
|
await exportAll.click();
|
||||||
|
|
||||||
|
// 等待并观察
|
||||||
|
console.log('>> 等待 15 秒观察反应...');
|
||||||
|
await page.waitForTimeout(15000);
|
||||||
|
|
||||||
|
// 截图
|
||||||
|
await page.screenshot({ path: path.join(SS, 'debug-after-export-click.png'), fullPage: true });
|
||||||
|
|
||||||
|
// 检查页面变化
|
||||||
|
const alerts = await page.evaluate(() => {
|
||||||
|
// 检查是否有弹窗/提示
|
||||||
|
const modals = document.querySelectorAll('.el-dialog, .modal, [class*="dialog"], [class*="modal"], [class*="popup"], [class*="toast"], [class*="message"], [class*="notify"]');
|
||||||
|
return Array.from(modals).map(m => ({
|
||||||
|
visible: m.offsetHeight > 0,
|
||||||
|
text: m.textContent?.substring(0, 200),
|
||||||
|
cls: m.className?.substring(0, 80),
|
||||||
|
})).filter(m => m.visible);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (alerts.length) {
|
||||||
|
console.log('\n>> 弹窗/提示:');
|
||||||
|
alerts.forEach(a => console.log(` ${a.cls}: "${a.text}"`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有 iframe
|
||||||
|
const iframes = await page.$$('iframe');
|
||||||
|
console.log(`\n>> iframe 数量: ${iframes.length}`);
|
||||||
|
|
||||||
|
// 查看页面当前状态
|
||||||
|
const pageText = await page.evaluate(() => document.body?.innerText?.substring(0, 3000));
|
||||||
|
console.log('\n>> 页面当前文本(前1500字):\n', pageText?.substring(0, 1500));
|
||||||
|
|
||||||
|
await page.screenshot({ path: path.join(SS, 'debug-final.png') });
|
||||||
|
|
||||||
|
// === 同样测试仓库 ===
|
||||||
|
console.log('\n\n=== 仓库页面 ===');
|
||||||
|
await page.goto(`${BASE}/warehouseProduct/index.htm`, { waitUntil: 'load', timeout: 30000 });
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
// 导入/导出 → 按所有页导出
|
||||||
|
const impExpBtn = await page.locator('text=导入/导出').first();
|
||||||
|
await impExpBtn.click();
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
|
||||||
|
console.log('>> 点击"按所有页导出"...');
|
||||||
|
const allPages = await page.locator('text=按所有页导出').first();
|
||||||
|
await allPages.click();
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// 看看弹窗
|
||||||
|
await page.screenshot({ path: path.join(SS, 'debug-warehouse-dialog.png') });
|
||||||
|
|
||||||
|
// 找到并点击对话框的导出按钮
|
||||||
|
console.log('>> 查找对话框中的导出按钮...');
|
||||||
|
const dialogBtns = await page.$$eval('button', els =>
|
||||||
|
els.filter(el => el.offsetHeight > 0)
|
||||||
|
.map(el => ({
|
||||||
|
text: el.textContent.trim(), id: el.id,
|
||||||
|
cls: (el.className || '').substring(0, 60),
|
||||||
|
rect: el.getBoundingClientRect(),
|
||||||
|
})).filter(e => e.text.includes('导出') || e.text.includes('确认') || e.text.includes('确定'))
|
||||||
|
);
|
||||||
|
console.log(' 可见导出/确认按钮:', JSON.stringify(dialogBtns, null, 2));
|
||||||
|
|
||||||
|
if (dialogBtns.length) {
|
||||||
|
// 点击最后一个"导出"按钮(通常是对话框内的)
|
||||||
|
const targetText = dialogBtns.find(b => b.text === '导出')?.text || dialogBtns[0].text;
|
||||||
|
console.log(`>> 点击按钮: "${targetText}"`);
|
||||||
|
const btn = await page.locator(`button:visible:has-text("${targetText}")`).last();
|
||||||
|
await btn.click();
|
||||||
|
|
||||||
|
console.log('>> 等待 15 秒...');
|
||||||
|
await page.waitForTimeout(15000);
|
||||||
|
|
||||||
|
await page.screenshot({ path: path.join(SS, 'debug-warehouse-after-confirm.png') });
|
||||||
|
|
||||||
|
// 再次检查弹窗
|
||||||
|
const alerts2 = await page.evaluate(() => {
|
||||||
|
const modals = document.querySelectorAll('.el-dialog, .modal, [class*="dialog"], [class*="message"], [class*="notify"], [class*="toast"]');
|
||||||
|
return Array.from(modals).map(m => ({
|
||||||
|
visible: m.offsetHeight > 0,
|
||||||
|
text: m.textContent?.substring(0, 300),
|
||||||
|
})).filter(m => m.visible);
|
||||||
|
});
|
||||||
|
if (alerts2.length) {
|
||||||
|
console.log('\n>> 点击后弹窗:');
|
||||||
|
alerts2.forEach(a => console.log(` "${a.text}"`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
console.log('\n>> 完成');
|
||||||
274
dxm-auto.mjs
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
/**
|
||||||
|
* 店小秘全自动登录 + 后台探索 + 导出
|
||||||
|
* - 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) 判断结果
|
||||||
|
// 情况 A:API 返回 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 重定向 = 成功
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 情况 B:API 响应读取失败(__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);
|
||||||
165
explore-pages.mjs
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
/**
|
||||||
|
* 登录后探索 采购建议 和 仓库 页面,找导出按钮
|
||||||
|
*/
|
||||||
|
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 OCR_SCRIPT = path.join(__dirname, 'ocr_captcha.py');
|
||||||
|
const BASE_URL = 'https://www.dianxiaomi.com';
|
||||||
|
|
||||||
|
mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
||||||
|
|
||||||
|
function ocrCaptcha(imagePath) {
|
||||||
|
try {
|
||||||
|
return execSync(`python3 "${OCR_SCRIPT}" "${imagePath}"`, { encoding: 'utf-8', timeout: 30000 }).trim() || null;
|
||||||
|
} catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(browser) {
|
||||||
|
// 先试 Cookie
|
||||||
|
if (existsSync(COOKIE_FILE)) {
|
||||||
|
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();
|
||||||
|
await ctx.addCookies(JSON.parse(readFileSync(COOKIE_FILE, 'utf-8')));
|
||||||
|
await pg.goto(`${BASE_URL}/home.htm`, { waitUntil: 'load', timeout: 20000 });
|
||||||
|
// 检查是否已登录(看页面有没有用户名)
|
||||||
|
const isLogged = await pg.evaluate(() => {
|
||||||
|
return document.body.innerText.includes('MiLe-kf01') || document.body.innerText.includes('待办事项');
|
||||||
|
});
|
||||||
|
if (isLogged) {
|
||||||
|
console.log('>> Cookie 有效');
|
||||||
|
return { page: pg, context: ctx };
|
||||||
|
}
|
||||||
|
await ctx.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
for (let i = 1; i <= 20; i++) {
|
||||||
|
console.log(`>> 登录尝试 ${i}...`);
|
||||||
|
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();
|
||||||
|
let apiResult = null;
|
||||||
|
pg.on('response', async (r) => {
|
||||||
|
if (r.url().includes('userLoginNew2')) {
|
||||||
|
try { apiResult = await r.text(); } catch { apiResult = '__REDIRECT__'; }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
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, { timeout: 5000 }).catch(() => {});
|
||||||
|
await pg.waitForTimeout(1000);
|
||||||
|
|
||||||
|
const el = await pg.$('#verifyImgCode');
|
||||||
|
const capPath = path.join(SCREENSHOTS_DIR, 'cap.png');
|
||||||
|
await el.screenshot({ path: capPath });
|
||||||
|
const code = ocrCaptcha(capPath);
|
||||||
|
if (!code || code.length < 3) { await ctx.close(); continue; }
|
||||||
|
console.log(` 验证码: "${code}"`);
|
||||||
|
|
||||||
|
await pg.evaluate((c) => {
|
||||||
|
$('#exampleInputName').val('MiLe-kf01');
|
||||||
|
$('#exampleInputPassword').val('Vxdas@302');
|
||||||
|
$('#verifyCode').val(c);
|
||||||
|
}, code);
|
||||||
|
|
||||||
|
apiResult = null;
|
||||||
|
const nav = pg.waitForNavigation({ timeout: 15000, waitUntil: 'load' }).catch(() => null);
|
||||||
|
await pg.evaluate(() => { login(); });
|
||||||
|
await nav;
|
||||||
|
await pg.waitForTimeout(3000);
|
||||||
|
|
||||||
|
if (apiResult === '__REDIRECT__' || !pg.url().includes('/home.htm') ||
|
||||||
|
(apiResult && !apiResult.includes('"code":-1'))) {
|
||||||
|
console.log('>> 登录成功!');
|
||||||
|
const cookies = await ctx.cookies();
|
||||||
|
writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2));
|
||||||
|
return { page: pg, context: ctx };
|
||||||
|
}
|
||||||
|
console.log(` 失败: ${apiResult?.substring(0, 100)}`);
|
||||||
|
await ctx.close();
|
||||||
|
} catch (e) {
|
||||||
|
console.log(` 异常: ${e.message}`);
|
||||||
|
await ctx.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function explorePage(pg, name, url) {
|
||||||
|
console.log(`\n>> ===== ${name} =====`);
|
||||||
|
try {
|
||||||
|
await pg.goto(url, { waitUntil: 'load', timeout: 20000 });
|
||||||
|
} catch {
|
||||||
|
console.log(' 加载超时');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await pg.waitForTimeout(3000);
|
||||||
|
|
||||||
|
console.log(` URL: ${pg.url()}`);
|
||||||
|
console.log(` 标题: ${await pg.title()}`);
|
||||||
|
|
||||||
|
await pg.screenshot({ path: path.join(SCREENSHOTS_DIR, `explore-${name}.png`), fullPage: true });
|
||||||
|
|
||||||
|
// 查找所有按钮
|
||||||
|
const btns = await pg.$$eval('button, a, span, input[type="button"]', els =>
|
||||||
|
els.map(el => ({
|
||||||
|
tag: el.tagName, text: el.textContent.trim().substring(0, 50),
|
||||||
|
id: el.id, cls: (el.className || '').substring(0, 60),
|
||||||
|
onclick: el.getAttribute('onclick')?.substring(0, 80) || '',
|
||||||
|
href: el.href || '',
|
||||||
|
})).filter(e => e.text && e.text.length < 30)
|
||||||
|
.slice(0, 50)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(` 按钮(${btns.length} 个):`);
|
||||||
|
for (const b of btns) {
|
||||||
|
const star = b.text.includes('导出') ? '★' : ' ';
|
||||||
|
console.log(` ${star} ${b.tag}#${b.id}: "${b.text}" onclick="${b.onclick}" ${b.href ? `href=${b.href}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 特别查找导出相关
|
||||||
|
const exportBtns = btns.filter(b => b.text.includes('导出') || b.text.includes('下载'));
|
||||||
|
if (exportBtns.length) {
|
||||||
|
console.log('\n ★★ 导出按钮详情:');
|
||||||
|
for (const b of exportBtns) {
|
||||||
|
console.log(` ${b.tag}#${b.id} class="${b.cls}" text="${b.text}" onclick="${b.onclick}" href="${b.href}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面内容关键部分
|
||||||
|
const content = await pg.evaluate(() => document.body?.innerText?.substring(0, 2000));
|
||||||
|
console.log(`\n 页面内容(前1000字):\n ${content?.substring(0, 1000)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主流程
|
||||||
|
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] });
|
||||||
|
const result = await login(browser);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
const { page } = result;
|
||||||
|
|
||||||
|
// 探索关键页面
|
||||||
|
await explorePage(page, '采购建议', `${BASE_URL}/purchasingProposal/index.htm?state=3`);
|
||||||
|
await explorePage(page, '采购单', `${BASE_URL}/dxmPurchasingNote/waitPayIndex.htm?state=2`);
|
||||||
|
await explorePage(page, '仓库商品', `${BASE_URL}/warehouseProduct/index.htm`);
|
||||||
|
await explorePage(page, '自定导出', `${BASE_URL}/sys/index.htm?go=m409`);
|
||||||
|
|
||||||
|
await result.context.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
147
explore.mjs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* 探索脚本:登录店小秘,截图页面结构
|
||||||
|
*/
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
import { mkdirSync } from 'fs';
|
||||||
|
|
||||||
|
const SCREENSHOTS_DIR = './screenshots';
|
||||||
|
mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
||||||
|
|
||||||
|
const browser = await chromium.launch({ headless: false }); // 有头模式方便观察
|
||||||
|
const context = await browser.newContext({
|
||||||
|
viewport: { width: 1920, height: 1080 },
|
||||||
|
locale: 'zh-CN',
|
||||||
|
});
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 打开登录页
|
||||||
|
console.log('>> 打开店小秘登录页...');
|
||||||
|
await page.goto('https://www.dianxiaomi.com/home.htm', { waitUntil: 'networkidle', timeout: 30000 });
|
||||||
|
await page.screenshot({ path: `${SCREENSHOTS_DIR}/01-homepage.png`, fullPage: true });
|
||||||
|
console.log('>> 截图: 01-homepage.png');
|
||||||
|
|
||||||
|
// 打印页面标题
|
||||||
|
console.log('>> 页面标题:', await page.title());
|
||||||
|
|
||||||
|
// 查找登录表单元素
|
||||||
|
const inputs = await page.$$('input');
|
||||||
|
for (const input of inputs) {
|
||||||
|
const type = await input.getAttribute('type');
|
||||||
|
const name = await input.getAttribute('name');
|
||||||
|
const placeholder = await input.getAttribute('placeholder');
|
||||||
|
const id = await input.getAttribute('id');
|
||||||
|
console.log(` input: type=${type}, name=${name}, id=${id}, placeholder=${placeholder}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找所有按钮
|
||||||
|
const buttons = await page.$$('button, input[type="submit"], .btn, a.btn');
|
||||||
|
for (const btn of buttons) {
|
||||||
|
const text = await btn.textContent();
|
||||||
|
const tag = await btn.evaluate(el => el.tagName);
|
||||||
|
console.log(` button: tag=${tag}, text="${text.trim().substring(0, 50)}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 尝试登录
|
||||||
|
console.log('\n>> 尝试填写登录信息...');
|
||||||
|
|
||||||
|
// 尝试多种可能的选择器
|
||||||
|
const usernameSelectors = [
|
||||||
|
'input[name="username"]', 'input[name="userName"]', 'input[name="account"]',
|
||||||
|
'input[name="loginName"]', 'input[id="username"]', 'input[id="account"]',
|
||||||
|
'input[placeholder*="账号"]', 'input[placeholder*="用户"]', 'input[placeholder*="手机"]',
|
||||||
|
'input[type="text"]:first-of-type'
|
||||||
|
];
|
||||||
|
|
||||||
|
const passwordSelectors = [
|
||||||
|
'input[name="password"]', 'input[name="pwd"]', 'input[type="password"]',
|
||||||
|
'input[id="password"]', 'input[placeholder*="密码"]'
|
||||||
|
];
|
||||||
|
|
||||||
|
let usernameInput = null;
|
||||||
|
let passwordInput = null;
|
||||||
|
|
||||||
|
for (const sel of usernameSelectors) {
|
||||||
|
usernameInput = await page.$(sel);
|
||||||
|
if (usernameInput) {
|
||||||
|
console.log(` 找到用户名输入框: ${sel}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sel of passwordSelectors) {
|
||||||
|
passwordInput = await page.$(sel);
|
||||||
|
if (passwordInput) {
|
||||||
|
console.log(` 找到密码输入框: ${sel}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usernameInput && passwordInput) {
|
||||||
|
await usernameInput.fill('MiLe-kf01');
|
||||||
|
await passwordInput.fill('Vxdas@302');
|
||||||
|
await page.screenshot({ path: `${SCREENSHOTS_DIR}/02-filled-login.png` });
|
||||||
|
console.log('>> 截图: 02-filled-login.png');
|
||||||
|
|
||||||
|
// 查找登录按钮
|
||||||
|
const loginBtnSelectors = [
|
||||||
|
'button:has-text("登录")', 'button:has-text("登 录")', 'input[type="submit"]',
|
||||||
|
'a:has-text("登录")', '.login-btn', '#loginBtn', 'button[type="submit"]'
|
||||||
|
];
|
||||||
|
|
||||||
|
let loginBtn = null;
|
||||||
|
for (const sel of loginBtnSelectors) {
|
||||||
|
loginBtn = await page.$(sel);
|
||||||
|
if (loginBtn) {
|
||||||
|
console.log(` 找到登录按钮: ${sel}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginBtn) {
|
||||||
|
await loginBtn.click();
|
||||||
|
console.log('>> 点击登录...');
|
||||||
|
|
||||||
|
// 等待页面跳转或加载
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
await page.screenshot({ path: `${SCREENSHOTS_DIR}/03-after-login.png`, fullPage: true });
|
||||||
|
console.log('>> 截图: 03-after-login.png');
|
||||||
|
console.log('>> 登录后URL:', page.url());
|
||||||
|
console.log('>> 登录后标题:', await page.title());
|
||||||
|
|
||||||
|
// 打印页面上的所有导航链接
|
||||||
|
console.log('\n>> 页面导航链接:');
|
||||||
|
const links = await page.$$('a, .menu-item, .nav-item, [class*="menu"], [class*="nav"]');
|
||||||
|
for (const link of links.slice(0, 50)) {
|
||||||
|
const text = (await link.textContent()).trim();
|
||||||
|
const href = await link.getAttribute('href');
|
||||||
|
if (text && text.length < 30) {
|
||||||
|
console.log(` link: "${text}" -> ${href || '(no href)'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找"采购"相关菜单
|
||||||
|
console.log('\n>> 查找采购/仓库相关元素:');
|
||||||
|
const purchaseElements = await page.$$('*:has-text("采购"), *:has-text("仓库"), *:has-text("导出")');
|
||||||
|
for (const el of purchaseElements.slice(0, 20)) {
|
||||||
|
const tag = await el.evaluate(el => el.tagName);
|
||||||
|
const text = (await el.textContent()).trim().substring(0, 60);
|
||||||
|
const className = await el.getAttribute('class');
|
||||||
|
if (tag === 'A' || tag === 'LI' || tag === 'SPAN' || tag === 'DIV') {
|
||||||
|
console.log(` ${tag}.${className}: "${text}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('!! 未找到登录表单,请查看截图 01-homepage.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('错误:', err.message);
|
||||||
|
await page.screenshot({ path: `${SCREENSHOTS_DIR}/error.png`, fullPage: true });
|
||||||
|
} finally {
|
||||||
|
// 保持浏览器打开 30 秒方便观察
|
||||||
|
console.log('\n>> 浏览器将在 30 秒后关闭...');
|
||||||
|
await page.waitForTimeout(30000);
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
437
export.mjs
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
/**
|
||||||
|
* 店小秘自动导出脚本 - 生产版
|
||||||
|
*
|
||||||
|
* 流程:
|
||||||
|
* 1. 复用 Cookie 登录(Cookie 由 login.mjs 手动登录获取)
|
||||||
|
* 2. 采购建议 → 导出全部
|
||||||
|
* 3. 自营仓库 → 按所有页导出
|
||||||
|
* 4. 文件保存到 downloads/
|
||||||
|
*
|
||||||
|
* 首次使用:先运行 node login.mjs 手动登录获取 Cookie
|
||||||
|
* 定时运行:crontab 每 4 小时执行 run-export.sh
|
||||||
|
* Cookie 过期:重新运行 node login.mjs
|
||||||
|
*/
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
import { writeFileSync, readFileSync, existsSync, mkdirSync, readdirSync, appendFileSync } from 'fs';
|
||||||
|
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 LOG_FILE = path.join(__dirname, 'export.log');
|
||||||
|
const BASE_URL = 'https://www.dianxiaomi.com';
|
||||||
|
|
||||||
|
mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
||||||
|
mkdirSync(DOWNLOAD_DIR, { recursive: true });
|
||||||
|
|
||||||
|
function log(msg) {
|
||||||
|
const ts = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
|
||||||
|
const line = `[${ts}] ${msg}`;
|
||||||
|
console.log(line);
|
||||||
|
try { appendFileSync(LOG_FILE, line + '\n'); } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dateTag() {
|
||||||
|
const d = new Date();
|
||||||
|
return `${d.getFullYear()}${String(d.getMonth()+1).padStart(2,'0')}${String(d.getDate()).padStart(2,'0')}_${String(d.getHours()).padStart(2,'0')}${String(d.getMinutes()).padStart(2,'0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 登录(仅 Cookie 复用,过期需手动 node login.mjs)======
|
||||||
|
async function doLogin(browser) {
|
||||||
|
log('检查登录状态...');
|
||||||
|
|
||||||
|
if (!existsSync(COOKIE_FILE)) {
|
||||||
|
log('✗ Cookie 不存在!请先运行: node login.mjs');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
await ctx.addCookies(JSON.parse(readFileSync(COOKIE_FILE, 'utf-8')));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pg.goto(`${BASE_URL}/home.htm`, { waitUntil: 'load', timeout: 30000 });
|
||||||
|
const isLogged = await pg.evaluate(() =>
|
||||||
|
document.body.innerText.includes('MiLe-kf01') || document.body.innerText.includes('待办事项')
|
||||||
|
);
|
||||||
|
if (isLogged) {
|
||||||
|
log('Cookie 有效!');
|
||||||
|
return { page: pg, context: ctx };
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
await ctx.close();
|
||||||
|
log('✗ Cookie 已过期!请重新运行: node login.mjs');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 关闭页面上的所有弹窗/公告 ======
|
||||||
|
async function closeModals(page) {
|
||||||
|
// 关闭 ant-modal 弹窗、公告、通知
|
||||||
|
await page.evaluate(() => {
|
||||||
|
// 关闭所有 ant-modal
|
||||||
|
document.querySelectorAll('.ant-modal-wrap, .bullet-layer, .comm-modal').forEach(el => {
|
||||||
|
el.style.display = 'none';
|
||||||
|
el.remove();
|
||||||
|
});
|
||||||
|
// 关闭遮罩层
|
||||||
|
document.querySelectorAll('.ant-modal-mask, .modal-backdrop, .v-modal').forEach(el => {
|
||||||
|
el.style.display = 'none';
|
||||||
|
el.remove();
|
||||||
|
});
|
||||||
|
// 点击所有可见的关闭按钮
|
||||||
|
document.querySelectorAll('.ant-modal-close, .close, [class*="close-btn"]').forEach(btn => {
|
||||||
|
if (btn.offsetHeight > 0) btn.click();
|
||||||
|
});
|
||||||
|
}).catch(() => {});
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 下载辅助 ======
|
||||||
|
async function doExport(page, clickAction, label) {
|
||||||
|
log(`导出: ${label}`);
|
||||||
|
try {
|
||||||
|
const downloadPromise = page.waitForEvent('download', { timeout: 120000 });
|
||||||
|
await clickAction();
|
||||||
|
const download = await downloadPromise;
|
||||||
|
const tag = dateTag();
|
||||||
|
const saveName = `${tag}_${label}_${download.suggestedFilename()}`;
|
||||||
|
const savePath = path.join(DOWNLOAD_DIR, saveName);
|
||||||
|
await download.saveAs(savePath);
|
||||||
|
log(`✓ 下载完成: ${saveName}`);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
log(`✗ 下载失败(${label}): ${e.message}`);
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, `fail-${label}.png`) }).catch(() => {});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 直接 URL 下载 ======
|
||||||
|
async function downloadUrl(page, url, label) {
|
||||||
|
log(`直接下载: ${label} -> ${url.substring(0, 80)}...`);
|
||||||
|
try {
|
||||||
|
const downloadPromise = page.waitForEvent('download', { timeout: 60000 });
|
||||||
|
// 通过创建 <a> 标签触发下载,避免 page.goto 的问题
|
||||||
|
await page.evaluate((href) => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = href;
|
||||||
|
a.download = '';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
}, url);
|
||||||
|
const download = await downloadPromise;
|
||||||
|
const tag = dateTag();
|
||||||
|
const saveName = `${tag}_${label}_${download.suggestedFilename()}`;
|
||||||
|
const savePath = path.join(DOWNLOAD_DIR, saveName);
|
||||||
|
await download.saveAs(savePath);
|
||||||
|
log(`✓ 下载完成: ${saveName}`);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
log(`✗ URL下载失败(${label}): ${e.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 处理采购建议导出对话框(字段选择 → 点导出 → 等下载链接)======
|
||||||
|
async function handlePurchaseExportDialog(page, label) {
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, `dialog-${label}.png`) });
|
||||||
|
|
||||||
|
// 对话框里的"导出"按钮(蓝色按钮,和"关闭"并列)
|
||||||
|
// 用 JavaScript 直接点,避免被其他元素挡住
|
||||||
|
const clicked = await page.evaluate(() => {
|
||||||
|
const btns = Array.from(document.querySelectorAll('button'));
|
||||||
|
const exportBtn = btns.find(b =>
|
||||||
|
b.textContent.trim() === '导出' && b.offsetHeight > 0 &&
|
||||||
|
b.closest('.ant-modal, [class*="dialog"], [class*="modal"]')
|
||||||
|
);
|
||||||
|
if (exportBtn) { exportBtn.click(); return true; }
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!clicked) {
|
||||||
|
log(`✗ 未找到对话框"导出"按钮 (${label})`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(` 点击了对话框"导出"按钮`);
|
||||||
|
// 等待处理(可能生成文件,可能弹出下载链接,可能直接下载)
|
||||||
|
await page.waitForTimeout(10000);
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, `after-export-${label}.png`) });
|
||||||
|
|
||||||
|
// 检查是否有下载链接出现
|
||||||
|
return await clickDownloadLinks(page, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 处理仓库导出对话框(字段选择 → 点导出 → 进度条 → 下载链接)======
|
||||||
|
async function handleWarehouseExportDialog(page, label) {
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, `dialog-${label}.png`) });
|
||||||
|
|
||||||
|
// 1. 先点对话框里的"导出"按钮
|
||||||
|
const clicked = await page.evaluate(() => {
|
||||||
|
const btns = Array.from(document.querySelectorAll('button'));
|
||||||
|
const exportBtn = btns.find(b =>
|
||||||
|
b.textContent.trim() === '导出' && b.offsetHeight > 0 &&
|
||||||
|
b.closest('.ant-modal, [class*="dialog"], [class*="modal"]')
|
||||||
|
);
|
||||||
|
if (exportBtn) { exportBtn.click(); return true; }
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!clicked) {
|
||||||
|
log(`✗ 仓库对话框未找到"导出"按钮`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log(' 已点击仓库对话框"导出"按钮');
|
||||||
|
|
||||||
|
// 2. 等进度条完成(最多等 300 秒,6000+商品需要时间)
|
||||||
|
log(' 等待仓库导出处理(6000+商品)...');
|
||||||
|
for (let i = 0; i < 150; i++) {
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
const state = await page.evaluate(() => {
|
||||||
|
const text = document.body.innerText;
|
||||||
|
// 精确检测:完成状态有"已导出 X 个商品"和"下载"链接
|
||||||
|
const hasCompleted = text.includes('已导出') && text.match(/已导出.*?\d+.*?商品/);
|
||||||
|
// 仍在处理
|
||||||
|
const isProcessing = text.includes('导出中') && text.includes('正在导出');
|
||||||
|
return {
|
||||||
|
done: !!hasCompleted,
|
||||||
|
processing: isProcessing,
|
||||||
|
text: text.substring(text.indexOf('导出'), text.indexOf('导出') + 100),
|
||||||
|
};
|
||||||
|
}).catch(() => ({ done: false }));
|
||||||
|
|
||||||
|
if (state.done) {
|
||||||
|
log(` 仓库导出完成! ${state.text}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (i % 15 === 0 && i > 0) {
|
||||||
|
log(` 仍在处理...(${i * 2}秒)${state.text || ''}`);
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, `progress-${label}-${i}.png`) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, `after-export-${label}.png`) });
|
||||||
|
|
||||||
|
// 3. 查找下载链接(仓库导出完成后有"下载单品/加工SKU文件"等链接)
|
||||||
|
const downloadLinks = await page.evaluate(() => {
|
||||||
|
const links = Array.from(document.querySelectorAll('a'));
|
||||||
|
return links
|
||||||
|
.filter(a => a.textContent.includes('下载') && a.offsetHeight > 0)
|
||||||
|
.map(a => ({ text: a.textContent.trim(), href: a.href }));
|
||||||
|
});
|
||||||
|
|
||||||
|
log(` 找到 ${downloadLinks.length} 个下载链接`);
|
||||||
|
let downloaded = 0;
|
||||||
|
|
||||||
|
for (const link of downloadLinks) {
|
||||||
|
if (!link.href || link.href === 'null' || (!link.href.includes('.xls') && !link.href.includes('temp/'))) continue;
|
||||||
|
log(` 下载: "${link.text}" -> ${link.href.substring(0, 80)}`);
|
||||||
|
const ok = await downloadUrl(page, link.href, `${label}_${link.text.substring(0, 10)}`);
|
||||||
|
if (ok) downloaded++;
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没找到 <a> 链接,尝试点击按钮
|
||||||
|
if (downloaded === 0) {
|
||||||
|
const btns = await page.locator('a:visible:has-text("下载"), button:visible:has-text("下载")').all();
|
||||||
|
for (let i = 0; i < btns.length; i++) {
|
||||||
|
const text = await btns[i].textContent().catch(() => '');
|
||||||
|
if (text.includes('下载') && !text.includes('自定导出')) {
|
||||||
|
log(` 点击下载按钮: "${text.trim()}"`);
|
||||||
|
const ok = await doExport(page, () => btns[i].click(), `${label}_btn${i}`);
|
||||||
|
if (ok) downloaded++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后尝试用 JS 查找隐藏的下载 URL
|
||||||
|
if (downloaded === 0) {
|
||||||
|
const hiddenLinks = await page.evaluate(() => {
|
||||||
|
const all = Array.from(document.querySelectorAll('a[href*=".xls"], a[href*="temp/"], a[href*="download"]'));
|
||||||
|
return all.map(a => ({ text: a.textContent.trim(), href: a.href }));
|
||||||
|
});
|
||||||
|
for (const l of hiddenLinks) {
|
||||||
|
if (l.href.includes('.xls') || l.href.includes('temp/')) {
|
||||||
|
log(` 隐藏链接: "${l.text}" -> ${l.href.substring(0, 80)}`);
|
||||||
|
const ok = await downloadUrl(page, l.href, `${label}_hidden`);
|
||||||
|
if (ok) downloaded++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return downloaded > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 点击所有"下载"链接 ======
|
||||||
|
async function clickDownloadLinks(page, label) {
|
||||||
|
let downloaded = 0;
|
||||||
|
|
||||||
|
// 查找所有包含"下载"文字的链接/按钮
|
||||||
|
const downloadLinks = await page.locator('a:has-text("下载"), button:has-text("下载")').all();
|
||||||
|
log(` 找到 ${downloadLinks.length} 个下载链接`);
|
||||||
|
|
||||||
|
for (let i = 0; i < downloadLinks.length; i++) {
|
||||||
|
const link = downloadLinks[i];
|
||||||
|
if (!(await link.isVisible().catch(() => false))) continue;
|
||||||
|
const text = await link.textContent().catch(() => '');
|
||||||
|
const href = await link.getAttribute('href').catch(() => '');
|
||||||
|
log(` 下载: "${text.trim()}" href="${href}"`);
|
||||||
|
|
||||||
|
if (href && (href.includes('.xls') || href.includes('.csv') || href.includes('temp/'))) {
|
||||||
|
// 直接 URL 下载
|
||||||
|
const fullUrl = href.startsWith('http') ? href : `${BASE_URL}${href}`;
|
||||||
|
const ok = await downloadUrl(page, fullUrl, `${label}_${i + 1}`);
|
||||||
|
if (ok) downloaded++;
|
||||||
|
} else {
|
||||||
|
// 点击按钮下载
|
||||||
|
const ok = await doExport(page, () => link.click(), `${label}_${i + 1}`);
|
||||||
|
if (ok) downloaded++;
|
||||||
|
}
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloaded === 0) {
|
||||||
|
log(` 未成功下载任何文件,尝试用 JS 查找隐藏链接...`);
|
||||||
|
// 有些下载链接可能是通过 window.open 或 location.href
|
||||||
|
const links = await page.evaluate(() => {
|
||||||
|
const els = document.querySelectorAll('a[href*="download"], a[href*="export"], a[href*=".xls"], a[href*=".csv"]');
|
||||||
|
return Array.from(els).map(a => ({ text: a.textContent.trim(), href: a.href }));
|
||||||
|
});
|
||||||
|
if (links.length) {
|
||||||
|
log(' 找到直接下载链接:');
|
||||||
|
links.forEach(l => log(` ${l.text} -> ${l.href}`));
|
||||||
|
for (const l of links) {
|
||||||
|
// 跳过非文件链接
|
||||||
|
if (!l.href.includes('.xls') && !l.href.includes('.csv') && !l.href.includes('.zip') &&
|
||||||
|
!l.href.includes('download') && !l.href.includes('temp/')) continue;
|
||||||
|
const ok = await downloadUrl(page, l.href, `${label}_${l.text.substring(0, 10)}`);
|
||||||
|
if (ok) downloaded++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return downloaded > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 采购建议页面 ======
|
||||||
|
async function exportPurchase(page) {
|
||||||
|
log('\n===== 采购建议 =====');
|
||||||
|
|
||||||
|
// --- 导出全部 ---
|
||||||
|
await page.goto(`${BASE_URL}/purchasingProposal/index.htm?state=3`, { waitUntil: 'load', timeout: 30000 });
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
await closeModals(page);
|
||||||
|
|
||||||
|
const dropdown1 = await page.locator('text=导出建议').first();
|
||||||
|
if (await dropdown1.isVisible().catch(() => false)) {
|
||||||
|
await dropdown1.click();
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
|
||||||
|
const exportAllItem = await page.locator('text=导出全部').first();
|
||||||
|
if (await exportAllItem.isVisible().catch(() => false)) {
|
||||||
|
await exportAllItem.click(); // 弹出字段选择对话框
|
||||||
|
await handlePurchaseExportDialog(page, '采购_导出全部');
|
||||||
|
} else {
|
||||||
|
log('✗ 未找到"导出全部"菜单项');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('✗ 未找到"导出建议"按钮');
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// --- 导出建议 ---
|
||||||
|
await page.goto(`${BASE_URL}/purchasingProposal/index.htm?state=3`, { waitUntil: 'load', timeout: 30000 });
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
await closeModals(page);
|
||||||
|
|
||||||
|
const dropdown2 = await page.locator('text=导出建议').first();
|
||||||
|
if (await dropdown2.isVisible().catch(() => false)) {
|
||||||
|
await dropdown2.click();
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
|
||||||
|
const exportSuggItem = await page.locator('text=导出勾选项').first();
|
||||||
|
if (await exportSuggItem.isVisible().catch(() => false)) {
|
||||||
|
await exportSuggItem.click();
|
||||||
|
await handlePurchaseExportDialog(page, '采购_导出勾选项');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 自营仓库页面 ======
|
||||||
|
async function exportWarehouse(page) {
|
||||||
|
log('\n===== 自营仓库 =====');
|
||||||
|
await page.goto(`${BASE_URL}/warehouseProduct/index.htm`, { waitUntil: 'load', timeout: 30000 });
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
await closeModals(page);
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, 'page-仓库.png'), fullPage: true });
|
||||||
|
|
||||||
|
// 1. 点击"导入/导出"展开下拉
|
||||||
|
const importExportBtn = await page.locator('text=导入/导出').first();
|
||||||
|
if (!(await importExportBtn.isVisible().catch(() => false))) {
|
||||||
|
log('✗ 未找到"导入/导出"按钮');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await importExportBtn.click();
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
|
||||||
|
// 2. 点"按所有页导出"
|
||||||
|
const allPagesBtn = await page.locator('text=按所有页导出').first();
|
||||||
|
if (!(await allPagesBtn.isVisible().catch(() => false))) {
|
||||||
|
log('✗ 未找到"按所有页导出"');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await allPagesBtn.click();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// 3. 等进度条完成,下载文件
|
||||||
|
await handleWarehouseExportDialog(page, '仓库_全部导出');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 主流程 ======
|
||||||
|
async function main() {
|
||||||
|
log('========================================');
|
||||||
|
log('店小秘自动导出 启动');
|
||||||
|
log('========================================\n');
|
||||||
|
|
||||||
|
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = await doLogin(browser);
|
||||||
|
if (!session) { log('登录失败'); await browser.close(); return false; }
|
||||||
|
|
||||||
|
const { page, context } = session;
|
||||||
|
|
||||||
|
await exportPurchase(page);
|
||||||
|
await exportWarehouse(page);
|
||||||
|
|
||||||
|
// 统计
|
||||||
|
const tag = dateTag().substring(0, 8);
|
||||||
|
const files = readdirSync(DOWNLOAD_DIR).filter(f => f.startsWith(tag));
|
||||||
|
log(`\n今日已下载 ${files.length} 个文件:`);
|
||||||
|
files.forEach(f => log(` 📄 ${f}`));
|
||||||
|
|
||||||
|
await context.close();
|
||||||
|
await browser.close();
|
||||||
|
log('\n✓ 导出完成\n');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
log(`致命错误: ${e.message}`);
|
||||||
|
await browser.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = await main();
|
||||||
|
process.exit(ok ? 0 : 1);
|
||||||
54
grab-captchas.mjs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* 抓取 10 张验证码图片,用于分析 OCR 准确度
|
||||||
|
*/
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
import { 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 DIR = path.join(__dirname, 'captcha-samples');
|
||||||
|
mkdirSync(DIR, { recursive: true });
|
||||||
|
|
||||||
|
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] });
|
||||||
|
|
||||||
|
for (let i = 1; i <= 10; i++) {
|
||||||
|
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 page = await ctx.newPage();
|
||||||
|
|
||||||
|
await page.goto('https://www.dianxiaomi.com/home.htm', { waitUntil: 'load', timeout: 30000 });
|
||||||
|
await page.waitForSelector('#verifyImgCode', { timeout: 10000 });
|
||||||
|
await page.waitForFunction(() => document.getElementById('verifyImgCode')?.complete === true, { timeout: 5000 }).catch(() => {});
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
const el = await page.$('#verifyImgCode');
|
||||||
|
const imgPath = path.join(DIR, `captcha_${i}.png`);
|
||||||
|
await el.screenshot({ path: imgPath });
|
||||||
|
|
||||||
|
// ddddocr
|
||||||
|
let ddddResult = '';
|
||||||
|
try {
|
||||||
|
ddddResult = execSync(`python3 ocr_captcha.py "${imgPath}"`, { encoding: 'utf-8', timeout: 30000 }).trim();
|
||||||
|
} catch { ddddResult = 'FAIL'; }
|
||||||
|
|
||||||
|
// tesseract (方案0: 灰度+放大+阈值)
|
||||||
|
let tessResult = '';
|
||||||
|
try {
|
||||||
|
tessResult = execSync(
|
||||||
|
`tesseract "${imgPath}" stdout --psm 7 -c tessedit_char_whitelist=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`,
|
||||||
|
{ encoding: 'utf-8', timeout: 10000 }
|
||||||
|
).trim().replace(/[\s\n\r]/g, '');
|
||||||
|
} catch { tessResult = 'FAIL'; }
|
||||||
|
|
||||||
|
console.log(`#${i}: ddddocr="${ddddResult}" tesseract="${tessResult}" -> ${imgPath}`);
|
||||||
|
|
||||||
|
await ctx.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
console.log('\n>> 完成,请查看 captcha-samples/ 目录');
|
||||||
119
login-save-cookies.mjs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* 第一步:有头浏览器手动登录,保存 Cookie
|
||||||
|
* 用法:node login-save-cookies.mjs
|
||||||
|
*
|
||||||
|
* 运行后会打开浏览器,手动输入验证码并登录,
|
||||||
|
* 登录成功后脚本自动保存 Cookie 到 cookies.json
|
||||||
|
*/
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
import { writeFileSync, existsSync } from 'fs';
|
||||||
|
|
||||||
|
const COOKIE_FILE = './cookies.json';
|
||||||
|
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
headless: false, // 有头模式让你操作
|
||||||
|
channel: 'chrome', // 用系统 Chrome
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = await browser.newContext({
|
||||||
|
viewport: { width: 1920, height: 1080 },
|
||||||
|
locale: 'zh-CN',
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
console.log('>> 打开店小秘登录页...');
|
||||||
|
console.log('>> 请在浏览器中手动输入验证码并登录');
|
||||||
|
console.log('>> 登录成功后脚本会自动保存 Cookie\n');
|
||||||
|
|
||||||
|
await page.goto('https://www.dianxiaomi.com/home.htm', { waitUntil: 'networkidle', timeout: 30000 });
|
||||||
|
|
||||||
|
// 自动填入账号密码
|
||||||
|
const usernameInput = await page.$('input[name="account"]');
|
||||||
|
const passwordInput = await page.$('input[type="password"]');
|
||||||
|
const rememberCheckbox = await page.$('input[name="remeber"]');
|
||||||
|
|
||||||
|
if (usernameInput && passwordInput) {
|
||||||
|
await usernameInput.fill('MiLe-kf01');
|
||||||
|
await passwordInput.fill('Vxdas@302');
|
||||||
|
// 勾选"记住我"延长 Cookie 有效期
|
||||||
|
if (rememberCheckbox) {
|
||||||
|
await rememberCheckbox.check();
|
||||||
|
}
|
||||||
|
console.log('>> 已自动填入账号密码,请手动输入验证码后点击"登录"');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待用户手动登录成功 - 检测 URL 变化(离开登录页)
|
||||||
|
// 店小秘后台通常会跳转到 /saleManage/ 或类似路径
|
||||||
|
try {
|
||||||
|
await page.waitForURL(url => {
|
||||||
|
const u = url.toString();
|
||||||
|
return !u.includes('/home.htm') && !u.includes('/index.htm') && u.includes('dianxiaomi.com');
|
||||||
|
}, { timeout: 300000 }); // 等待 5 分钟
|
||||||
|
|
||||||
|
console.log('\n>> 登录成功!当前URL:', page.url());
|
||||||
|
|
||||||
|
// 多等几秒让页面完全加载
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// 保存 Cookie
|
||||||
|
const cookies = await context.cookies();
|
||||||
|
writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2));
|
||||||
|
console.log(`>> Cookie 已保存到 ${COOKIE_FILE}(共 ${cookies.length} 条)`);
|
||||||
|
|
||||||
|
// 截图后台页面
|
||||||
|
await page.screenshot({ path: './screenshots/04-dashboard.png', fullPage: true });
|
||||||
|
console.log('>> 已截图: screenshots/04-dashboard.png');
|
||||||
|
|
||||||
|
// 打印页面结构 - 查找导航菜单
|
||||||
|
console.log('\n>> ===== 后台菜单结构 =====');
|
||||||
|
|
||||||
|
// 通常的左侧菜单
|
||||||
|
const menuItems = await page.$$eval(
|
||||||
|
'a, .menu-item, [class*="menu"] a, [class*="nav"] a, .sidebar a, li a',
|
||||||
|
els => els
|
||||||
|
.map(el => ({ text: el.textContent.trim(), href: el.href, className: el.className }))
|
||||||
|
.filter(e => e.text && e.text.length < 40 && e.text.length > 0)
|
||||||
|
.filter((e, i, arr) => arr.findIndex(a => a.text === e.text) === i) // 去重
|
||||||
|
.slice(0, 80)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const item of menuItems) {
|
||||||
|
console.log(` [${item.text}] -> ${item.href}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 特别搜索采购、仓库、导出相关
|
||||||
|
console.log('\n>> ===== 采购/仓库/导出 相关菜单 =====');
|
||||||
|
const relatedItems = menuItems.filter(i =>
|
||||||
|
i.text.includes('采购') || i.text.includes('仓库') || i.text.includes('导出') ||
|
||||||
|
i.text.includes('库存') || i.text.includes('备货')
|
||||||
|
);
|
||||||
|
for (const item of relatedItems) {
|
||||||
|
console.log(` ★ [${item.text}] -> ${item.href}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保持浏览器打开让用户可以继续观察
|
||||||
|
console.log('\n>> 浏览器将在 60 秒后关闭(你可以在这段时间内浏览后台页面)');
|
||||||
|
await page.waitForTimeout(60000);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
if (e.name === 'TimeoutError') {
|
||||||
|
console.log('\n>> 等待超时(5分钟),请重新运行脚本');
|
||||||
|
} else {
|
||||||
|
console.error('错误:', e.message);
|
||||||
|
}
|
||||||
|
// 检查是否已经在后台了(可能 URL 检测没覆盖到)
|
||||||
|
const currentUrl = page.url();
|
||||||
|
console.log('>> 当前 URL:', currentUrl);
|
||||||
|
|
||||||
|
if (currentUrl.includes('dianxiaomi.com') && !currentUrl.includes('/home.htm')) {
|
||||||
|
const cookies = await context.cookies();
|
||||||
|
writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2));
|
||||||
|
console.log(`>> 看起来已登录,Cookie 已保存到 ${COOKIE_FILE}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.waitForTimeout(30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
console.log('>> 完成');
|
||||||
75
login.mjs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* 手动登录脚本
|
||||||
|
* 用法:node login.mjs
|
||||||
|
*
|
||||||
|
* 打开浏览器 → 你输入验证码点登录 → 自动保存 Cookie
|
||||||
|
* Cookie 保存后,export.mjs 会自动复用,不需要再登录
|
||||||
|
*/
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
import { writeFileSync, mkdirSync } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const COOKIE_FILE = path.join(__dirname, 'cookies.json');
|
||||||
|
|
||||||
|
console.log('========================================');
|
||||||
|
console.log(' 店小秘手动登录');
|
||||||
|
console.log(' 请在浏览器中输入验证码并登录');
|
||||||
|
console.log(' 登录成功后会自动保存 Cookie');
|
||||||
|
console.log('========================================\n');
|
||||||
|
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
headless: false, // 有头模式,你能看到浏览器
|
||||||
|
channel: 'chrome', // 用系统 Chrome
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = await browser.newContext({
|
||||||
|
viewport: { width: 1400, height: 900 },
|
||||||
|
locale: 'zh-CN',
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
// 打开登录页
|
||||||
|
await page.goto('https://www.dianxiaomi.com/home.htm', { waitUntil: 'load', timeout: 30000 });
|
||||||
|
|
||||||
|
// 自动填入账号密码
|
||||||
|
try {
|
||||||
|
await page.waitForSelector('#exampleInputName', { timeout: 10000 });
|
||||||
|
await page.fill('#exampleInputName', 'MiLe-kf01');
|
||||||
|
await page.fill('#exampleInputPassword', 'Vxdas@302');
|
||||||
|
console.log('>> 已填入账号密码,请输入验证码后点击"登录"\n');
|
||||||
|
} catch {
|
||||||
|
console.log('>> 请手动填写账号密码和验证码\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轮询检测登录状态(最多等 5 分钟)
|
||||||
|
const startTime = Date.now();
|
||||||
|
const TIMEOUT = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
while (Date.now() - startTime < TIMEOUT) {
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const isLogged = await page.evaluate(() => {
|
||||||
|
const text = document.body?.innerText || '';
|
||||||
|
return text.includes('MiLe-kf01') && (text.includes('待办事项') || text.includes('订单'));
|
||||||
|
}).catch(() => false);
|
||||||
|
|
||||||
|
if (isLogged) {
|
||||||
|
// 保存 Cookie
|
||||||
|
const cookies = await context.cookies();
|
||||||
|
writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2));
|
||||||
|
console.log(`\n>> ★ 登录成功!Cookie 已保存(${cookies.length} 条)`);
|
||||||
|
console.log(`>> 文件: ${COOKIE_FILE}`);
|
||||||
|
console.log('>> 现在 export.mjs 会自动使用这个 Cookie 运行\n');
|
||||||
|
console.log('>> 3 秒后关闭浏览器...');
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
await browser.close();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n>> 等待超时(5分钟),未检测到登录成功');
|
||||||
|
await browser.close();
|
||||||
|
process.exit(1);
|
||||||
128
ocr_captcha.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
验证码识别脚本
|
||||||
|
输入:验证码图片路径
|
||||||
|
输出:识别结果(打印到 stdout)
|
||||||
|
|
||||||
|
预处理流程:
|
||||||
|
1. 灰度化
|
||||||
|
2. 去除水平干扰线(形态学开运算)
|
||||||
|
3. 二值化
|
||||||
|
4. 去噪
|
||||||
|
5. ddddocr 识别
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import ddddocr
|
||||||
|
|
||||||
|
def preprocess(img_path):
|
||||||
|
"""多种预处理方案"""
|
||||||
|
img = cv2.imread(img_path)
|
||||||
|
if img is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# 方案1:去水平线 + 二值化
|
||||||
|
# 用水平核做形态学开运算,提取水平线
|
||||||
|
h_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (25, 1))
|
||||||
|
h_lines = cv2.morphologyEx(gray, cv2.MORPH_OPEN, h_kernel)
|
||||||
|
# 从原图减去水平线
|
||||||
|
no_lines = cv2.subtract(gray, h_lines)
|
||||||
|
# 二值化
|
||||||
|
_, binary1 = cv2.threshold(no_lines, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
||||||
|
results.append(binary1)
|
||||||
|
|
||||||
|
# 方案2:自适应阈值
|
||||||
|
adaptive = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
||||||
|
cv2.THRESH_BINARY, 11, 2)
|
||||||
|
results.append(adaptive)
|
||||||
|
|
||||||
|
# 方案3:去水平线 + 自适应阈值
|
||||||
|
adaptive2 = cv2.adaptiveThreshold(no_lines, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
||||||
|
cv2.THRESH_BINARY, 11, 2)
|
||||||
|
results.append(adaptive2)
|
||||||
|
|
||||||
|
# 方案4:反色 + 去线 + 二值化
|
||||||
|
inverted = cv2.bitwise_not(gray)
|
||||||
|
h_lines_inv = cv2.morphologyEx(inverted, cv2.MORPH_OPEN, h_kernel)
|
||||||
|
no_lines_inv = cv2.subtract(inverted, h_lines_inv)
|
||||||
|
_, binary4 = cv2.threshold(no_lines_inv, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
||||||
|
results.append(binary4)
|
||||||
|
|
||||||
|
# 方案5:中值滤波去噪 + OTSU
|
||||||
|
median = cv2.medianBlur(gray, 3)
|
||||||
|
_, binary5 = cv2.threshold(median, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
||||||
|
results.append(binary5)
|
||||||
|
|
||||||
|
# 方案6:放大 + 去线 + 锐化
|
||||||
|
big = cv2.resize(gray, None, fx=3, fy=3, interpolation=cv2.INTER_CUBIC)
|
||||||
|
h_kernel_big = cv2.getStructuringElement(cv2.MORPH_RECT, (75, 1))
|
||||||
|
h_lines_big = cv2.morphologyEx(big, cv2.MORPH_OPEN, h_kernel_big)
|
||||||
|
no_lines_big = cv2.subtract(big, h_lines_big)
|
||||||
|
_, binary6 = cv2.threshold(no_lines_big, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
||||||
|
# 去小噪点
|
||||||
|
kernel_small = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
|
||||||
|
binary6 = cv2.morphologyEx(binary6, cv2.MORPH_OPEN, kernel_small)
|
||||||
|
results.append(binary6)
|
||||||
|
|
||||||
|
# 方案7:原图直接用
|
||||||
|
results.append(gray)
|
||||||
|
|
||||||
|
# 方案8: 原始彩色图(ddddocr 有时对彩色图效果更好)
|
||||||
|
results.append(img)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("用法: python3 ocr_captcha.py <图片路径>", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
img_path = sys.argv[1]
|
||||||
|
|
||||||
|
ocr = ddddocr.DdddOcr(show_ad=False)
|
||||||
|
|
||||||
|
# 先直接用原图试
|
||||||
|
with open(img_path, 'rb') as f:
|
||||||
|
raw_result = ocr.classification(f.read())
|
||||||
|
|
||||||
|
if raw_result and len(raw_result) == 4 and raw_result.isalnum():
|
||||||
|
print(raw_result)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 多种预处理方案
|
||||||
|
preprocessed = preprocess(img_path)
|
||||||
|
|
||||||
|
best = raw_result
|
||||||
|
for i, processed in enumerate(preprocessed):
|
||||||
|
try:
|
||||||
|
if len(processed.shape) == 3:
|
||||||
|
_, buf = cv2.imencode('.png', processed)
|
||||||
|
else:
|
||||||
|
_, buf = cv2.imencode('.png', processed)
|
||||||
|
result = ocr.classification(buf.tobytes())
|
||||||
|
|
||||||
|
if result and len(result) == 4 and result.isalnum():
|
||||||
|
print(result)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 记录最佳结果
|
||||||
|
if result and len(result) >= 3 and (not best or len(result) == 4):
|
||||||
|
best = result
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 返回最佳结果
|
||||||
|
if best:
|
||||||
|
print(best[:4] if len(best) > 4 else best)
|
||||||
|
else:
|
||||||
|
print("FAIL", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
607
package-lock.json
generated
Normal file
@@ -0,0 +1,607 @@
|
|||||||
|
{
|
||||||
|
"name": "dxm-exporter",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "dxm-exporter",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "^1.58.2",
|
||||||
|
"sharp": "^0.34.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/colour": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/colour/-/colour-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-darwin-arm64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-darwin-x64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-arm": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-arm": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-arm64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-ppc64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-ppc64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-riscv64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-riscv64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-s390x": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-x64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-x64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-wasm32": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
||||||
|
"cpu": [
|
||||||
|
"wasm32"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/runtime": "^1.7.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-win32-arm64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-win32-ia32": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-win32-x64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/detect-libc": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.58.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.58.2.tgz",
|
||||||
|
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.58.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.58.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||||
|
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "7.7.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz",
|
||||||
|
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sharp": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sharp/-/sharp-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@img/colour": "^1.0.0",
|
||||||
|
"detect-libc": "^2.1.2",
|
||||||
|
"semver": "^7.7.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-darwin-arm64": "0.34.5",
|
||||||
|
"@img/sharp-darwin-x64": "0.34.5",
|
||||||
|
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-arm": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-x64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
||||||
|
"@img/sharp-linux-arm": "0.34.5",
|
||||||
|
"@img/sharp-linux-arm64": "0.34.5",
|
||||||
|
"@img/sharp-linux-ppc64": "0.34.5",
|
||||||
|
"@img/sharp-linux-riscv64": "0.34.5",
|
||||||
|
"@img/sharp-linux-s390x": "0.34.5",
|
||||||
|
"@img/sharp-linux-x64": "0.34.5",
|
||||||
|
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
||||||
|
"@img/sharp-linuxmusl-x64": "0.34.5",
|
||||||
|
"@img/sharp-wasm32": "0.34.5",
|
||||||
|
"@img/sharp-win32-arm64": "0.34.5",
|
||||||
|
"@img/sharp-win32-ia32": "0.34.5",
|
||||||
|
"@img/sharp-win32-x64": "0.34.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
package.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"name":"dxm-exporter","version":"1.0.0","type":"module","dependencies":{"playwright":"^1.58.2","sharp":"^0.34.5"}}
|
||||||
21
run-export.sh
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 店小秘自动导出 - cron 启动脚本
|
||||||
|
|
||||||
|
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"
|
||||||
|
export HOME="/Users/kangwan"
|
||||||
|
|
||||||
|
cd /Users/kangwan/Projects/business/20260324-店小秘自动导出
|
||||||
|
|
||||||
|
# 加载 nvm
|
||||||
|
export NVM_DIR="$HOME/.nvm"
|
||||||
|
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||||||
|
|
||||||
|
# 运行导出
|
||||||
|
node export.mjs >> export.log 2>&1
|
||||||
|
EXIT_CODE=$?
|
||||||
|
|
||||||
|
# 如果失败(Cookie 过期),弹系统通知
|
||||||
|
if [ $EXIT_CODE -ne 0 ]; then
|
||||||
|
osascript -e 'display notification "Cookie 已过期,请运行 node login.mjs 重新登录" with title "店小秘自动导出" sound name "Basso"'
|
||||||
|
echo "[$(date '+%Y/%m/%d %H:%M:%S')] ✗ 导出失败,Cookie 可能过期" >> export.log
|
||||||
|
fi
|
||||||
BIN
screenshots/01-homepage.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
screenshots/02-filled-login.png
Normal file
|
After Width: | Height: | Size: 635 KiB |
BIN
screenshots/03-after-login.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
screenshots/05-login-success.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
screenshots/after-export-仓库_全部导出.png
Normal file
|
After Width: | Height: | Size: 263 KiB |
BIN
screenshots/after-export-采购_导出全部.png
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
screenshots/after-login.png
Normal file
|
After Width: | Height: | Size: 420 KiB |
BIN
screenshots/attempt-1.png
Normal file
|
After Width: | Height: | Size: 610 KiB |
BIN
screenshots/attempt-2.png
Normal file
|
After Width: | Height: | Size: 533 KiB |
BIN
screenshots/backend-main.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
screenshots/backend.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
screenshots/cap.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
screenshots/cap_p0.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
screenshots/cap_p1.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
screenshots/cap_p2.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
screenshots/captcha.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
screenshots/captcha_p0.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
screenshots/captcha_processed.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
screenshots/captcha_processed2.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
screenshots/captcha_raw.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
screenshots/dashboard.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
screenshots/dialog-仓库_全部导出.png
Normal file
|
After Width: | Height: | Size: 293 KiB |
BIN
screenshots/dialog-采购_导出全部.png
Normal file
|
After Width: | Height: | Size: 260 KiB |
BIN
screenshots/dialog-采购_导出勾选项.png
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
screenshots/explore-仓库商品.png
Normal file
|
After Width: | Height: | Size: 3.2 MiB |