commit ada92373c2363ad3f6eda1efa3ee9efbac1311a1 Author: kang Date: Sat Apr 25 21:50:03 2026 +0800 init repo diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..310df25 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.memory/dxm-auto-procurement.md b/.memory/dxm-auto-procurement.md new file mode 100644 index 0000000..de515b2 --- /dev/null +++ b/.memory/dxm-auto-procurement.md @@ -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 过期提醒) diff --git a/.memory/worklog.json b/.memory/worklog.json new file mode 100644 index 0000000..046955d --- /dev/null +++ b/.memory/worklog.json @@ -0,0 +1,3 @@ +{ + "entries": [] +} diff --git a/.project.json b/.project.json new file mode 100644 index 0000000..0781461 --- /dev/null +++ b/.project.json @@ -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": [] +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2751749 --- /dev/null +++ b/AGENTS.md @@ -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` 的部署事实 +- 如果只更新了代码但没回写部署元数据,这个任务不算完成 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2751749 --- /dev/null +++ b/CLAUDE.md @@ -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` 的部署事实 +- 如果只更新了代码但没回写部署元数据,这个任务不算完成 diff --git a/RULES.md b/RULES.md new file mode 100644 index 0000000..1fc38ad --- /dev/null +++ b/RULES.md @@ -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` 保持空数组 +- 任何部署或域名变化,都要先改元数据,再视为任务完成 + +## 注意事项 +- 待补充 diff --git a/auto-login-v2.mjs b/auto-login-v2.mjs new file mode 100644 index 0000000..b4c94ec --- /dev/null +++ b/auto-login-v2.mjs @@ -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); diff --git a/auto-login-v3.mjs b/auto-login-v3.mjs new file mode 100644 index 0000000..d84f68b --- /dev/null +++ b/auto-login-v3.mjs @@ -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); +} diff --git a/auto-login-v4.mjs b/auto-login-v4.mjs new file mode 100644 index 0000000..462b420 --- /dev/null +++ b/auto-login-v4.mjs @@ -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); +} diff --git a/auto-login-v5.mjs b/auto-login-v5.mjs new file mode 100644 index 0000000..2c1a2f9 --- /dev/null +++ b/auto-login-v5.mjs @@ -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); +} diff --git a/auto-login-v6.mjs b/auto-login-v6.mjs new file mode 100644 index 0000000..154c910 --- /dev/null +++ b/auto-login-v6.mjs @@ -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); +} diff --git a/auto-login-v7.mjs b/auto-login-v7.mjs new file mode 100644 index 0000000..ec437bb --- /dev/null +++ b/auto-login-v7.mjs @@ -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); +} diff --git a/auto-login-v8.mjs b/auto-login-v8.mjs new file mode 100644 index 0000000..704e134 --- /dev/null +++ b/auto-login-v8.mjs @@ -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); +} diff --git a/auto-login-v9.mjs b/auto-login-v9.mjs new file mode 100644 index 0000000..c654d87 --- /dev/null +++ b/auto-login-v9.mjs @@ -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); +} diff --git a/auto-login.mjs b/auto-login.mjs new file mode 100644 index 0000000..4597226 --- /dev/null +++ b/auto-login.mjs @@ -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); diff --git a/captcha-samples/captcha_1.png b/captcha-samples/captcha_1.png new file mode 100644 index 0000000..9ce5be5 Binary files /dev/null and b/captcha-samples/captcha_1.png differ diff --git a/captcha-samples/captcha_10.png b/captcha-samples/captcha_10.png new file mode 100644 index 0000000..6804773 Binary files /dev/null and b/captcha-samples/captcha_10.png differ diff --git a/captcha-samples/captcha_2.png b/captcha-samples/captcha_2.png new file mode 100644 index 0000000..888efad Binary files /dev/null and b/captcha-samples/captcha_2.png differ diff --git a/captcha-samples/captcha_3.png b/captcha-samples/captcha_3.png new file mode 100644 index 0000000..4c5d24f Binary files /dev/null and b/captcha-samples/captcha_3.png differ diff --git a/captcha-samples/captcha_4.png b/captcha-samples/captcha_4.png new file mode 100644 index 0000000..5bebc35 Binary files /dev/null and b/captcha-samples/captcha_4.png differ diff --git a/captcha-samples/captcha_5.png b/captcha-samples/captcha_5.png new file mode 100644 index 0000000..2ccc428 Binary files /dev/null and b/captcha-samples/captcha_5.png differ diff --git a/captcha-samples/captcha_6.png b/captcha-samples/captcha_6.png new file mode 100644 index 0000000..209207a Binary files /dev/null and b/captcha-samples/captcha_6.png differ diff --git a/captcha-samples/captcha_7.png b/captcha-samples/captcha_7.png new file mode 100644 index 0000000..d6470fa Binary files /dev/null and b/captcha-samples/captcha_7.png differ diff --git a/captcha-samples/captcha_8.png b/captcha-samples/captcha_8.png new file mode 100644 index 0000000..cab39ed Binary files /dev/null and b/captcha-samples/captcha_8.png differ diff --git a/captcha-samples/captcha_9.png b/captcha-samples/captcha_9.png new file mode 100644 index 0000000..d2e92b9 Binary files /dev/null and b/captcha-samples/captcha_9.png differ diff --git a/cookies.json b/cookies.json new file mode 100644 index 0000000..1faabdf --- /dev/null +++ b/cookies.json @@ -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" + } +] \ No newline at end of file diff --git a/debug-export.mjs b/debug-export.mjs new file mode 100644 index 0000000..2bb570c --- /dev/null +++ b/debug-export.mjs @@ -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>> 完成'); diff --git a/downloads/20260325_1349_采购_导出全部_备用下载地址_caigoujianyi_20260325134708805_960090.xlsx b/downloads/20260325_1349_采购_导出全部_备用下载地址_caigoujianyi_20260325134708805_960090.xlsx new file mode 100644 index 0000000..691e7b7 Binary files /dev/null and b/downloads/20260325_1349_采购_导出全部_备用下载地址_caigoujianyi_20260325134708805_960090.xlsx differ diff --git a/downloads/20260325_1349_采购_导出全部_高速下载地址_caigoujianyi_20260325134708805_960090.xlsx b/downloads/20260325_1349_采购_导出全部_高速下载地址_caigoujianyi_20260325134708805_960090.xlsx new file mode 100644 index 0000000..691e7b7 Binary files /dev/null and b/downloads/20260325_1349_采购_导出全部_高速下载地址_caigoujianyi_20260325134708805_960090.xlsx differ diff --git a/downloads/20260325_1356_采购_导出全部_备用下载地址_caigoujianyi_20260325135436988_960090.xlsx b/downloads/20260325_1356_采购_导出全部_备用下载地址_caigoujianyi_20260325135436988_960090.xlsx new file mode 100644 index 0000000..c9ac940 Binary files /dev/null and b/downloads/20260325_1356_采购_导出全部_备用下载地址_caigoujianyi_20260325135436988_960090.xlsx differ diff --git a/downloads/20260325_1356_采购_导出全部_高速下载地址_caigoujianyi_20260325135436988_960090.xlsx b/downloads/20260325_1356_采购_导出全部_高速下载地址_caigoujianyi_20260325135436988_960090.xlsx new file mode 100644 index 0000000..c9ac940 Binary files /dev/null and b/downloads/20260325_1356_采购_导出全部_高速下载地址_caigoujianyi_20260325135436988_960090.xlsx differ diff --git a/downloads/20260325_1401_采购_导出全部_备用下载地址_caigoujianyi_20260325135916282_960090.xlsx b/downloads/20260325_1401_采购_导出全部_备用下载地址_caigoujianyi_20260325135916282_960090.xlsx new file mode 100644 index 0000000..16b188e Binary files /dev/null and b/downloads/20260325_1401_采购_导出全部_备用下载地址_caigoujianyi_20260325135916282_960090.xlsx differ diff --git a/downloads/20260325_1401_采购_导出全部_高速下载地址_caigoujianyi_20260325135916282_960090.xlsx b/downloads/20260325_1401_采购_导出全部_高速下载地址_caigoujianyi_20260325135916282_960090.xlsx new file mode 100644 index 0000000..16b188e Binary files /dev/null and b/downloads/20260325_1401_采购_导出全部_高速下载地址_caigoujianyi_20260325135916282_960090.xlsx differ diff --git a/downloads/20260325_1402_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260325140211513_960090.zip b/downloads/20260325_1402_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260325140211513_960090.zip new file mode 100644 index 0000000..ffeabfc Binary files /dev/null and b/downloads/20260325_1402_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260325140211513_960090.zip differ diff --git a/downloads/20260325_1402_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260325140211802_960090.xlsx b/downloads/20260325_1402_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260325140211802_960090.xlsx new file mode 100644 index 0000000..effe23d Binary files /dev/null and b/downloads/20260325_1402_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260325140211802_960090.xlsx differ diff --git a/downloads/20260326_0956_采购_导出全部_备用下载地址_caigoujianyi_20260326095429182_960090.xlsx b/downloads/20260326_0956_采购_导出全部_备用下载地址_caigoujianyi_20260326095429182_960090.xlsx new file mode 100644 index 0000000..d524f6e Binary files /dev/null and b/downloads/20260326_0956_采购_导出全部_备用下载地址_caigoujianyi_20260326095429182_960090.xlsx differ diff --git a/downloads/20260326_0956_采购_导出全部_高速下载地址_caigoujianyi_20260326095429182_960090.xlsx b/downloads/20260326_0956_采购_导出全部_高速下载地址_caigoujianyi_20260326095429182_960090.xlsx new file mode 100644 index 0000000..d524f6e Binary files /dev/null and b/downloads/20260326_0956_采购_导出全部_高速下载地址_caigoujianyi_20260326095429182_960090.xlsx differ diff --git a/downloads/20260326_0957_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260326095721328_960090.zip b/downloads/20260326_0957_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260326095721328_960090.zip new file mode 100644 index 0000000..d99733b Binary files /dev/null and b/downloads/20260326_0957_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260326095721328_960090.zip differ diff --git a/downloads/20260326_0957_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260326095721571_960090.xlsx b/downloads/20260326_0957_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260326095721571_960090.xlsx new file mode 100644 index 0000000..e237c4e Binary files /dev/null and b/downloads/20260326_0957_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260326095721571_960090.xlsx differ diff --git a/downloads/20260326_1202_采购_导出全部_备用下载地址_caigoujianyi_20260326120035302_960090.xlsx b/downloads/20260326_1202_采购_导出全部_备用下载地址_caigoujianyi_20260326120035302_960090.xlsx new file mode 100644 index 0000000..5ba2374 Binary files /dev/null and b/downloads/20260326_1202_采购_导出全部_备用下载地址_caigoujianyi_20260326120035302_960090.xlsx differ diff --git a/downloads/20260326_1202_采购_导出全部_高速下载地址_caigoujianyi_20260326120035302_960090.xlsx b/downloads/20260326_1202_采购_导出全部_高速下载地址_caigoujianyi_20260326120035302_960090.xlsx new file mode 100644 index 0000000..5ba2374 Binary files /dev/null and b/downloads/20260326_1202_采购_导出全部_高速下载地址_caigoujianyi_20260326120035302_960090.xlsx differ diff --git a/downloads/20260326_1203_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260326120339986_960090.zip b/downloads/20260326_1203_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260326120339986_960090.zip new file mode 100644 index 0000000..f4f11ae Binary files /dev/null and b/downloads/20260326_1203_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260326120339986_960090.zip differ diff --git a/downloads/20260326_1203_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260326120340270_960090.xlsx b/downloads/20260326_1203_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260326120340270_960090.xlsx new file mode 100644 index 0000000..e00d4ce Binary files /dev/null and b/downloads/20260326_1203_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260326120340270_960090.xlsx differ diff --git a/downloads/20260326_1602_采购_导出全部_备用下载地址_caigoujianyi_20260326160034743_960090.xlsx b/downloads/20260326_1602_采购_导出全部_备用下载地址_caigoujianyi_20260326160034743_960090.xlsx new file mode 100644 index 0000000..603948c Binary files /dev/null and b/downloads/20260326_1602_采购_导出全部_备用下载地址_caigoujianyi_20260326160034743_960090.xlsx differ diff --git a/downloads/20260326_1602_采购_导出全部_高速下载地址_caigoujianyi_20260326160034743_960090.xlsx b/downloads/20260326_1602_采购_导出全部_高速下载地址_caigoujianyi_20260326160034743_960090.xlsx new file mode 100644 index 0000000..603948c Binary files /dev/null and b/downloads/20260326_1602_采购_导出全部_高速下载地址_caigoujianyi_20260326160034743_960090.xlsx differ diff --git a/downloads/20260326_1603_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260326160342172_960090.zip b/downloads/20260326_1603_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260326160342172_960090.zip new file mode 100644 index 0000000..46ffca0 Binary files /dev/null and b/downloads/20260326_1603_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260326160342172_960090.zip differ diff --git a/downloads/20260326_1603_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260326160342502_960090.xlsx b/downloads/20260326_1603_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260326160342502_960090.xlsx new file mode 100644 index 0000000..c156778 Binary files /dev/null and b/downloads/20260326_1603_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260326160342502_960090.xlsx differ diff --git a/downloads/20260326_2002_采购_导出全部_备用下载地址_caigoujianyi_20260326200022467_960090.xlsx b/downloads/20260326_2002_采购_导出全部_备用下载地址_caigoujianyi_20260326200022467_960090.xlsx new file mode 100644 index 0000000..802ace5 Binary files /dev/null and b/downloads/20260326_2002_采购_导出全部_备用下载地址_caigoujianyi_20260326200022467_960090.xlsx differ diff --git a/downloads/20260326_2002_采购_导出全部_高速下载地址_caigoujianyi_20260326200022467_960090.xlsx b/downloads/20260326_2002_采购_导出全部_高速下载地址_caigoujianyi_20260326200022467_960090.xlsx new file mode 100644 index 0000000..802ace5 Binary files /dev/null and b/downloads/20260326_2002_采购_导出全部_高速下载地址_caigoujianyi_20260326200022467_960090.xlsx differ diff --git a/downloads/20260326_2003_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260326200320234_960090.zip b/downloads/20260326_2003_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260326200320234_960090.zip new file mode 100644 index 0000000..ca6de5e Binary files /dev/null and b/downloads/20260326_2003_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260326200320234_960090.zip differ diff --git a/downloads/20260326_2003_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260326200320470_960090.xlsx b/downloads/20260326_2003_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260326200320470_960090.xlsx new file mode 100644 index 0000000..fe1bb88 Binary files /dev/null and b/downloads/20260326_2003_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260326200320470_960090.xlsx differ diff --git a/downloads/20260327_1202_采购_导出全部_备用下载地址_caigoujianyi_20260327120043927_960090.xlsx b/downloads/20260327_1202_采购_导出全部_备用下载地址_caigoujianyi_20260327120043927_960090.xlsx new file mode 100644 index 0000000..5e15ec2 Binary files /dev/null and b/downloads/20260327_1202_采购_导出全部_备用下载地址_caigoujianyi_20260327120043927_960090.xlsx differ diff --git a/downloads/20260327_1202_采购_导出全部_高速下载地址_caigoujianyi_20260327120043927_960090.xlsx b/downloads/20260327_1202_采购_导出全部_高速下载地址_caigoujianyi_20260327120043927_960090.xlsx new file mode 100644 index 0000000..5e15ec2 Binary files /dev/null and b/downloads/20260327_1202_采购_导出全部_高速下载地址_caigoujianyi_20260327120043927_960090.xlsx differ diff --git a/downloads/20260327_1203_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260327120347221_960090.zip b/downloads/20260327_1203_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260327120347221_960090.zip new file mode 100644 index 0000000..4b28510 Binary files /dev/null and b/downloads/20260327_1203_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260327120347221_960090.zip differ diff --git a/downloads/20260327_1203_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260327120347429_960090.xlsx b/downloads/20260327_1203_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260327120347429_960090.xlsx new file mode 100644 index 0000000..983a3d9 Binary files /dev/null and b/downloads/20260327_1203_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260327120347429_960090.xlsx differ diff --git a/downloads/20260327_1602_采购_导出全部_备用下载地址_caigoujianyi_20260327160018901_960090.xlsx b/downloads/20260327_1602_采购_导出全部_备用下载地址_caigoujianyi_20260327160018901_960090.xlsx new file mode 100644 index 0000000..74846aa Binary files /dev/null and b/downloads/20260327_1602_采购_导出全部_备用下载地址_caigoujianyi_20260327160018901_960090.xlsx differ diff --git a/downloads/20260327_1602_采购_导出全部_高速下载地址_caigoujianyi_20260327160018901_960090.xlsx b/downloads/20260327_1602_采购_导出全部_高速下载地址_caigoujianyi_20260327160018901_960090.xlsx new file mode 100644 index 0000000..74846aa Binary files /dev/null and b/downloads/20260327_1602_采购_导出全部_高速下载地址_caigoujianyi_20260327160018901_960090.xlsx differ diff --git a/downloads/20260327_1603_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260327160321509_960090.zip b/downloads/20260327_1603_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260327160321509_960090.zip new file mode 100644 index 0000000..a04bbf7 Binary files /dev/null and b/downloads/20260327_1603_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260327160321509_960090.zip differ diff --git a/downloads/20260327_1603_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260327160321849_960090.xlsx b/downloads/20260327_1603_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260327160321849_960090.xlsx new file mode 100644 index 0000000..0184c1a Binary files /dev/null and b/downloads/20260327_1603_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260327160321849_960090.xlsx differ diff --git a/downloads/20260328_0002_采购_导出全部_备用下载地址_caigoujianyi_20260328000018630_960090.xlsx b/downloads/20260328_0002_采购_导出全部_备用下载地址_caigoujianyi_20260328000018630_960090.xlsx new file mode 100644 index 0000000..352b237 Binary files /dev/null and b/downloads/20260328_0002_采购_导出全部_备用下载地址_caigoujianyi_20260328000018630_960090.xlsx differ diff --git a/downloads/20260328_0002_采购_导出全部_高速下载地址_caigoujianyi_20260328000018630_960090.xlsx b/downloads/20260328_0002_采购_导出全部_高速下载地址_caigoujianyi_20260328000018630_960090.xlsx new file mode 100644 index 0000000..352b237 Binary files /dev/null and b/downloads/20260328_0002_采购_导出全部_高速下载地址_caigoujianyi_20260328000018630_960090.xlsx differ diff --git a/downloads/20260328_0003_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260328000314464_960090.zip b/downloads/20260328_0003_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260328000314464_960090.zip new file mode 100644 index 0000000..ce8fadc Binary files /dev/null and b/downloads/20260328_0003_仓库_全部导出_下载单品/加工SKU_cangkuqingdan_20260328000314464_960090.zip differ diff --git a/downloads/20260328_0003_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260328000314765_960090.xlsx b/downloads/20260328_0003_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260328000314765_960090.xlsx new file mode 100644 index 0000000..453fff5 Binary files /dev/null and b/downloads/20260328_0003_仓库_全部导出_下载组合SKU文件_cangkuqingdan_20260328000314765_960090.xlsx differ diff --git a/dxm-auto.mjs b/dxm-auto.mjs new file mode 100644 index 0000000..03c6b8f --- /dev/null +++ b/dxm-auto.mjs @@ -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); diff --git a/explore-pages.mjs b/explore-pages.mjs new file mode 100644 index 0000000..d56092e --- /dev/null +++ b/explore-pages.mjs @@ -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(); diff --git a/explore.mjs b/explore.mjs new file mode 100644 index 0000000..a6127d0 --- /dev/null +++ b/explore.mjs @@ -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(); +} diff --git a/export.mjs b/export.mjs new file mode 100644 index 0000000..b28289e --- /dev/null +++ b/export.mjs @@ -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 }); + // 通过创建 标签触发下载,避免 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); + } + + // 如果没找到 链接,尝试点击按钮 + 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); diff --git a/grab-captchas.mjs b/grab-captchas.mjs new file mode 100644 index 0000000..4ab9efe --- /dev/null +++ b/grab-captchas.mjs @@ -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/ 目录'); diff --git a/login-save-cookies.mjs b/login-save-cookies.mjs new file mode 100644 index 0000000..70f1cfe --- /dev/null +++ b/login-save-cookies.mjs @@ -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('>> 完成'); diff --git a/login.mjs b/login.mjs new file mode 100644 index 0000000..0d69549 --- /dev/null +++ b/login.mjs @@ -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); diff --git a/ocr_captcha.py b/ocr_captcha.py new file mode 100644 index 0000000..e18af79 --- /dev/null +++ b/ocr_captcha.py @@ -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() diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..60a41c5 --- /dev/null +++ b/package-lock.json @@ -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 + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7391dfa --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{"name":"dxm-exporter","version":"1.0.0","type":"module","dependencies":{"playwright":"^1.58.2","sharp":"^0.34.5"}} \ No newline at end of file diff --git a/run-export.sh b/run-export.sh new file mode 100755 index 0000000..8c85bef --- /dev/null +++ b/run-export.sh @@ -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 diff --git a/screenshots/01-homepage.png b/screenshots/01-homepage.png new file mode 100644 index 0000000..1a70404 Binary files /dev/null and b/screenshots/01-homepage.png differ diff --git a/screenshots/02-filled-login.png b/screenshots/02-filled-login.png new file mode 100644 index 0000000..b1742e0 Binary files /dev/null and b/screenshots/02-filled-login.png differ diff --git a/screenshots/03-after-login.png b/screenshots/03-after-login.png new file mode 100644 index 0000000..b643832 Binary files /dev/null and b/screenshots/03-after-login.png differ diff --git a/screenshots/05-login-success.png b/screenshots/05-login-success.png new file mode 100644 index 0000000..9d54cac Binary files /dev/null and b/screenshots/05-login-success.png differ diff --git a/screenshots/after-export-仓库_全部导出.png b/screenshots/after-export-仓库_全部导出.png new file mode 100644 index 0000000..7cba598 Binary files /dev/null and b/screenshots/after-export-仓库_全部导出.png differ diff --git a/screenshots/after-export-采购_导出全部.png b/screenshots/after-export-采购_导出全部.png new file mode 100644 index 0000000..3f8505d Binary files /dev/null and b/screenshots/after-export-采购_导出全部.png differ diff --git a/screenshots/after-login.png b/screenshots/after-login.png new file mode 100644 index 0000000..99c6f63 Binary files /dev/null and b/screenshots/after-login.png differ diff --git a/screenshots/attempt-1.png b/screenshots/attempt-1.png new file mode 100644 index 0000000..d9c2dfc Binary files /dev/null and b/screenshots/attempt-1.png differ diff --git a/screenshots/attempt-2.png b/screenshots/attempt-2.png new file mode 100644 index 0000000..8a9fea3 Binary files /dev/null and b/screenshots/attempt-2.png differ diff --git a/screenshots/backend-main.png b/screenshots/backend-main.png new file mode 100644 index 0000000..6e8e86e Binary files /dev/null and b/screenshots/backend-main.png differ diff --git a/screenshots/backend.png b/screenshots/backend.png new file mode 100644 index 0000000..543c5d5 Binary files /dev/null and b/screenshots/backend.png differ diff --git a/screenshots/cap.png b/screenshots/cap.png new file mode 100644 index 0000000..0e4c1fa Binary files /dev/null and b/screenshots/cap.png differ diff --git a/screenshots/cap_p0.png b/screenshots/cap_p0.png new file mode 100644 index 0000000..073ad00 Binary files /dev/null and b/screenshots/cap_p0.png differ diff --git a/screenshots/cap_p1.png b/screenshots/cap_p1.png new file mode 100644 index 0000000..e397bc0 Binary files /dev/null and b/screenshots/cap_p1.png differ diff --git a/screenshots/cap_p2.png b/screenshots/cap_p2.png new file mode 100644 index 0000000..d89dc42 Binary files /dev/null and b/screenshots/cap_p2.png differ diff --git a/screenshots/captcha.png b/screenshots/captcha.png new file mode 100644 index 0000000..a958a3f Binary files /dev/null and b/screenshots/captcha.png differ diff --git a/screenshots/captcha_p0.png b/screenshots/captcha_p0.png new file mode 100644 index 0000000..b7a0cfd Binary files /dev/null and b/screenshots/captcha_p0.png differ diff --git a/screenshots/captcha_processed.png b/screenshots/captcha_processed.png new file mode 100644 index 0000000..2e29261 Binary files /dev/null and b/screenshots/captcha_processed.png differ diff --git a/screenshots/captcha_processed2.png b/screenshots/captcha_processed2.png new file mode 100644 index 0000000..52da58c Binary files /dev/null and b/screenshots/captcha_processed2.png differ diff --git a/screenshots/captcha_raw.png b/screenshots/captcha_raw.png new file mode 100644 index 0000000..80bbd4c Binary files /dev/null and b/screenshots/captcha_raw.png differ diff --git a/screenshots/dashboard.png b/screenshots/dashboard.png new file mode 100644 index 0000000..4e3fbef Binary files /dev/null and b/screenshots/dashboard.png differ diff --git a/screenshots/dialog-仓库_全部导出.png b/screenshots/dialog-仓库_全部导出.png new file mode 100644 index 0000000..48689b3 Binary files /dev/null and b/screenshots/dialog-仓库_全部导出.png differ diff --git a/screenshots/dialog-采购_导出全部.png b/screenshots/dialog-采购_导出全部.png new file mode 100644 index 0000000..0c0fa85 Binary files /dev/null and b/screenshots/dialog-采购_导出全部.png differ diff --git a/screenshots/dialog-采购_导出勾选项.png b/screenshots/dialog-采购_导出勾选项.png new file mode 100644 index 0000000..57a8432 Binary files /dev/null and b/screenshots/dialog-采购_导出勾选项.png differ diff --git a/screenshots/explore-仓库商品.png b/screenshots/explore-仓库商品.png new file mode 100644 index 0000000..d8d4e23 Binary files /dev/null and b/screenshots/explore-仓库商品.png differ diff --git a/screenshots/explore-自定导出.png b/screenshots/explore-自定导出.png new file mode 100644 index 0000000..70619dd Binary files /dev/null and b/screenshots/explore-自定导出.png differ diff --git a/screenshots/explore-采购单.png b/screenshots/explore-采购单.png new file mode 100644 index 0000000..2c7e031 Binary files /dev/null and b/screenshots/explore-采购单.png differ diff --git a/screenshots/explore-采购建议.png b/screenshots/explore-采购建议.png new file mode 100644 index 0000000..62a3d6a Binary files /dev/null and b/screenshots/explore-采购建议.png differ diff --git a/screenshots/export-仓库-dialog.png b/screenshots/export-仓库-dialog.png new file mode 100644 index 0000000..f1650bd Binary files /dev/null and b/screenshots/export-仓库-dialog.png differ diff --git a/screenshots/export-仓库.png b/screenshots/export-仓库.png new file mode 100644 index 0000000..b06067d Binary files /dev/null and b/screenshots/export-仓库.png differ diff --git a/screenshots/export-采购建议.png b/screenshots/export-采购建议.png new file mode 100644 index 0000000..300a871 Binary files /dev/null and b/screenshots/export-采购建议.png differ diff --git a/screenshots/fail-仓库_全部导出.png b/screenshots/fail-仓库_全部导出.png new file mode 100644 index 0000000..ed47ea0 Binary files /dev/null and b/screenshots/fail-仓库_全部导出.png differ diff --git a/screenshots/fail-仓库_全部导出_direct.png b/screenshots/fail-仓库_全部导出_direct.png new file mode 100644 index 0000000..6a7bd8f Binary files /dev/null and b/screenshots/fail-仓库_全部导出_direct.png differ diff --git a/screenshots/fail-仓库_按所有页导出.png b/screenshots/fail-仓库_按所有页导出.png new file mode 100644 index 0000000..7da508f Binary files /dev/null and b/screenshots/fail-仓库_按所有页导出.png differ diff --git a/screenshots/fail-采购_导出全部.png b/screenshots/fail-采购_导出全部.png new file mode 100644 index 0000000..4476ffb Binary files /dev/null and b/screenshots/fail-采购_导出全部.png differ diff --git a/screenshots/fail-采购_导出全部_1.png b/screenshots/fail-采购_导出全部_1.png new file mode 100644 index 0000000..6b65750 Binary files /dev/null and b/screenshots/fail-采购_导出全部_1.png differ diff --git a/screenshots/fail-采购_导出全部_direct.png b/screenshots/fail-采购_导出全部_direct.png new file mode 100644 index 0000000..8577c34 Binary files /dev/null and b/screenshots/fail-采购_导出全部_direct.png differ diff --git a/screenshots/fail-采购_导出勾选项.png b/screenshots/fail-采购_导出勾选项.png new file mode 100644 index 0000000..b913060 Binary files /dev/null and b/screenshots/fail-采购_导出勾选项.png differ diff --git a/screenshots/fail-采购_导出建议.png b/screenshots/fail-采购_导出建议.png new file mode 100644 index 0000000..81be3d5 Binary files /dev/null and b/screenshots/fail-采购_导出建议.png differ diff --git a/screenshots/final-fail.png b/screenshots/final-fail.png new file mode 100644 index 0000000..5a360c5 Binary files /dev/null and b/screenshots/final-fail.png differ diff --git a/screenshots/page-仓库-dialog.png b/screenshots/page-仓库-dialog.png new file mode 100644 index 0000000..354622d Binary files /dev/null and b/screenshots/page-仓库-dialog.png differ diff --git a/screenshots/page-仓库-dropdown.png b/screenshots/page-仓库-dropdown.png new file mode 100644 index 0000000..976bb01 Binary files /dev/null and b/screenshots/page-仓库-dropdown.png differ diff --git a/screenshots/page-仓库.png b/screenshots/page-仓库.png new file mode 100644 index 0000000..400fbe1 Binary files /dev/null and b/screenshots/page-仓库.png differ diff --git a/screenshots/page-采购建议-dropdown.png b/screenshots/page-采购建议-dropdown.png new file mode 100644 index 0000000..6cba3b7 Binary files /dev/null and b/screenshots/page-采购建议-dropdown.png differ diff --git a/screenshots/page-采购建议.png b/screenshots/page-采购建议.png new file mode 100644 index 0000000..dbed348 Binary files /dev/null and b/screenshots/page-采购建议.png differ diff --git a/screenshots/page_user_index.htm.png b/screenshots/page_user_index.htm.png new file mode 100644 index 0000000..2d1fcb0 Binary files /dev/null and b/screenshots/page_user_index.htm.png differ diff --git a/screenshots/page_user_setting.htm.png b/screenshots/page_user_setting.htm.png new file mode 100644 index 0000000..2d1fcb0 Binary files /dev/null and b/screenshots/page_user_setting.htm.png differ diff --git a/screenshots/processed_big_noline.png b/screenshots/processed_big_noline.png new file mode 100644 index 0000000..3abd82f Binary files /dev/null and b/screenshots/processed_big_noline.png differ diff --git a/screenshots/processed_noline.png b/screenshots/processed_noline.png new file mode 100644 index 0000000..17e7c37 Binary files /dev/null and b/screenshots/processed_noline.png differ