diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c4514f3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,90 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 项目概览 + +自动登录 5 家物流承运商工作台,下载"应到 / 实到"货物数据,离线比对出**应到未到** +异常运单并汇总成 Excel。4 个网页站点 + 1 个 Electron 应用(安能)。 + +## 常用命令 + +所有 Python 一律在项目虚拟环境 `.venv` 中运行(Windows 下可直接用 +`.venv/Scripts/python.exe`,无需激活)。 + +```bash +# 安装依赖(安能的 CDP 驱动还需 websocket-client,requirements.txt 暂未列入) +pip install -r requirements.txt +pip install websocket-client +playwright install chromium + +# 运行主程序(交互式菜单,详见 main_router 的 run_multi_site_daemon) +.venv/Scripts/python.exe main_router.py + +# 单站点联调:在 config.yaml 设 debug.enabled=true + debug.target_site=顺心|百世|中通|韵达|安能 +# 网页站:只挂载该站;安能:只启动 Electron 应用。 + +# 安能独立运行(需先以 --remote-debugging-port=9222 启动「安能全网门户.exe」并手动登录) +.venv/Scripts/python.exe site_anneng.py expected # 或 actual + +# 格式化(全局规范:改完 Python 必须 Black) +.venv/Scripts/python.exe -m black + +# 语法自检 +.venv/Scripts/python.exe -m py_compile +``` + +**没有 pytest 测试套件。** "测试"指 `main_router` 菜单 **[8] 自动化测试** +(`run_automation_test`,按 `CROSS_TEST_SEQUENCE` 交叉跑通各站点流程)。 + +## 架构(big picture) + +### 两套驱动模态 —— 这是理解全局的关键 +- **网页 4 站**(顺心/百世/中通/韵达):`main_router` 用 Playwright 开 chromium, + 每站一个 `page`,流程函数签名为 `xxx_download_impl(page)`。 +- **安能**:Electron 桌面应用,**不走 Playwright**。`main_router` 以 + `--remote-debugging-port=<动态空闲端口>` 启动 exe(`launch_anneng`), + 通过 `site_anneng.set_cdp_port` 告知模块;`site_anneng.py` 用裸 CDP(websocket) + 驱动,业务 tab 是独立 webContents。这也是 `playwright-cli` 接管不了安能的原因 + (Electron 19 / Chrome 102 不支持 Playwright 要的 setDownloadBehavior)。 + +### 分层:路由纯调度,站点模块自洽 +- `main_router.py` **只调度**:启动浏览器/安能、就绪轮询、登录检测、菜单分发。 + 菜单项直接调 `site_xxx.xxx_download(page)`,**不关心**重试/重置。 +- 每个 `site_xxx.py` 对外只暴露"把任务做了"的入口,内部自洽: + - `xxx_download(...)` —— 公开入口,= `with_retry(站点, 标签, xxx_download_impl, xxx_reset)` + - `xxx_download_impl(...)` —— 单次执行、**无重试**(自动化测试刻意调它以探测原始失败) + - `xxx_reset(...)` —— 重置回初始态(网页 = `page.goto(HOME_URL)`;安能 = 关业务 tab + 收菜单) + - `with_retry(...)` —— 重试逻辑**内联在每个站点模块**(不抽公共组件,现阶段刻意不优化结构); + 失败→重置→重试,最多 3 次(含首次),每次失败都重置(含最终放弃那次清场) + - `HOME_URL` —— 站点首页 URL,`main_router.SITES_CONFIG` 引用它(单一来源) + +### 导出任务队列模式(顺心/中通/韵达/安能-应到 共用) +这些站的下载是异步的:提交导出(记 `export_times` 时间戳)→ 跳"导出任务管理"页轮询 → +匹配本批任务 → 下载 → 合并多个临时 xlsx。 +- **匹配只按时间容差 ≤40s**,**不校验任务标题/模块名**(标题可能被站点改动;单账号无并发, + 时间窗内的任务必为本批所建)。**不要把标题校验加回来。** +- 轮询循环都有 **300s deadline**:超时 `raise` → 流程返回 False → 触发 `with_retry` 重试。 +- 例外:**百世**是同步下载(`expect_download`,无队列);**安能-实到**页面无导出按钮, + 直接抓表格 DOM 翻页。 + +### 比对 +`expected_undelivered.py`(菜单 [9])纯离线:读 `downloads/` 下各站应到/实到 xlsx, +比对生成 `output/应到未到数据.xlsx`(汇总 + 各站明细)。 + +### 路径 +`paths.py` 把 `DOWNLOAD_DIR` / `CONFIG_PATH` / `BASE_DIR` 全部锚定到项目目录, +**不依赖运行时 cwd**——别用相对路径或 `os.getcwd()`。 + +## 重要约定 / 易踩坑 + +- **登录是手动的**:`main_router` 启动后会停在就绪轮询(`READY_SELECTORS` / `anneng_ready`), + 直到检测到所有站点进入工作台才进菜单。仅韵达支持凭 `config.yaml` 凭据自动登录。 +- **安能单实例**:启动前必须先关闭已打开的安能窗口,否则调试端口起不来。 +- **安能重置绝不能用 `Page.reload`**:reload 会让已开业务 webContents 失去 app 引用、 + 变成 `Target.closeTarget` / `window.close` 都杀不掉的僵尸。重置只能走 tab 条 X 关 tab + 收菜单。 +- **`config.yaml` 已 gitignore**,存放凭据 / `query_days` / `debug` / `anneng.app_path`, + 切勿提交,也别把真实凭据写进 `config.example.yaml`。 +- **不要自动提交 / 推送**:本仓库约定改动后等用户明确说"提交"再 commit/push + (覆盖全局 CLAUDE.md 的 auto-push 默认)。 +- 改完 Python 文件**必须跑 Black**(全局规范)。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..99b26ae --- /dev/null +++ b/README.md @@ -0,0 +1,163 @@ +# 物流到货数据自动下载与应到未到核对工具 + +自动登录 5 家物流承运商的工作台,下载"应到 / 实到"货物数据,并离线比对出 +**应到未到**(该到没到)的异常运单,最终汇总成一份 Excel 报表。 + +适用场景:网点每日核对进站货物是否到齐。 + +--- + +## 一、覆盖站点与流程 + +5 个站点中,4 个是网页、1 个是 Electron 桌面应用: + +| 站点 | 形态 | 应到 | 实到 | +|---|---|---|---| +| 顺心捷达 | 网页 | ✅ 运单信息 | ✅ 卸车扫描记录 | +| 百世快运 | 网页 | — | —(直接提取"应到未到/当日未扫",单流程) | +| 中通快运 | 网页 | ✅ 运单信息 | ✅ 到件扫描 | +| 韵达快运 | 网页 | ✅ 进站主单 | ✅ 扫描记录 | +| 安能全网门户 | Electron 应用 | ✅ 运单信息 | ✅ 网点到件扫描 | + +- **应到** = 进站交接单下的运单明细(这批货"应该"到); +- **实到** = 到件 / 卸车扫描记录(实际扫到了哪些); +- 共 **9 个下载流程**(顺心/中通/韵达/安能 各 2 个 + 百世 1 个)。 + +每个站点产出独立的 `{站点}-应到货物数据.xlsx` / `{站点}-实到货物数据.xlsx` +(百世为 `百世-应到未到货物数据.xlsx`),落在 `downloads/`。 + +--- + +## 二、目录结构 + +``` +InboundVerify/ +├── main_router.py # 主入口 / 调度层(菜单、启动浏览器与安能、就绪轮询、登录检测) +├── site_shunxin.py # 顺心站点模块(流程 + 重置 + 重试,自洽) +├── site_baishi.py # 百世站点模块 +├── site_zto.py # 中通站点模块 +├── site_yunda.py # 韵达站点模块(含自动登录) +├── site_anneng.py # 安能站点模块(Electron + CDP 驱动) +├── expected_undelivered.py # 全站点应到未到离线比对,输出 output/应到未到数据.xlsx +├── paths.py # 统一路径锚点(以本目录为基准,不依赖 cwd) +├── config.example.yaml # 配置模板 +├── config.yaml # 真实配置(自行创建,已被 .gitignore 忽略) +├── requirements.txt +├── downloads/ # 各站点下载的原始数据 +├── output/ # 比对报表输出 +└── docs/ # 说明文档 +``` + +--- + +## 三、环境准备 + +需 Python 3.10+。 + +```bash +# 1. 创建并激活虚拟环境 +python -m venv .venv +.venv\Scripts\activate # Windows +# source .venv/bin/activate # macOS / Linux + +# 2. 安装依赖 +pip install -r requirements.txt + +# 3. 安装 Playwright 浏览器内核(网页站点用) +playwright install chromium + +# 4. 安装 websocket-client(安能 CDP 驱动需要,requirements 暂未列入) +pip install websocket-client + +# 5. 由模板创建本地配置并填入真实凭据 +cp config.example.yaml config.yaml +``` + +> 安能是 Electron 应用,还需在 `config.yaml` 里填 `anneng.app_path` +> 指向本机的「安能全网门户.exe」路径。 + +--- + +## 四、配置说明(config.yaml) + +`config.yaml` 存放凭据与各站参数,**已被 .gitignore 忽略,不会提交**。 +所有项都有默认值,留空不会报错(但凭据留空会导致对应站点登录/导出失败)。 + +| 配置项 | 说明 | +|---|---| +| `debug.enabled` / `debug.target_site` | 调试模式:仅挂载启动指定单个站点(顺心/百世/中通/韵达/安能) | +| `shunxin.query_days` | 顺心查询时间范围(向前回溯 N 天至今天) | +| `baishi.password` | 百世导出授权密码(必填,否则导出失败) | +| `zto.query_days` | 中通查询时间范围 | +| `yunda.username` / `yunda.password` | 韵达自动登录凭据(留空则需手动登录) | +| `yunda.query_days` | 韵达查询时间范围 | +| `anneng.query_days` | 安能查询时间范围 | +| `anneng.app_path` | 安能 Electron 可执行文件路径 | + +--- + +## 五、运行 + +```bash +python main_router.py +``` + +程序会: + +1. 用 Playwright 打开 4 个网页站点(韵达若配了凭据会尝试自动登录,其余手动登录); +2. 以调试模式启动安能 Electron 应用(动态空闲端口),**需在应用内手动登录**; +3. **轮询各站点登录就绪状态**——全部登录完成后自动进入主菜单; +4. 弹出主菜单,按编号选择任务。 + +主菜单: + +``` +[1][2] 顺心 应到 / 实到 +[3] 百世 应到未到(当日未扫) +[4][5] 中通 应到 / 实到 +[6][7] 韵达 应到 / 实到 +[10][11] 安能 应到 / 实到 +[8] 全站点自动化测试(交叉跑通校验) +[9] 应到未到比对(全站点汇总 → output/应到未到数据.xlsx) +[0] 退出 +``` + +--- + +## 六、架构 + +**分层原则:路由层只调度,站点模块自洽。** + +- **`main_router.py`(调度层)**:负责启动浏览器 / 安能、就绪轮询、登录检测、 + 菜单分发。**不关心**"任务能否完成、失败怎么办"——只调 + `site_xxx.xxx_download(page)` 然后等结果。 +- **各 `site_xxx.py`(站点模块)**:每个模块对外只暴露一个"把任务做了"的入口 + `xxx_download(...)`,内部自行处理一切: + - `HOME_URL`:站点首页 URL(也供路由层 `SITES_CONFIG` 引用,单一来源); + - `xxx_reset(...)`:异常兜底的重置(网页 = 跳首页 URL;安能 = 关业务 tab + 收菜单); + - `with_retry(...)`:内联在本模块的重试逻辑; + - `xxx_download(...)`:**公开入口** = `with_retry(站点, 标签, xxx_download_impl, xxx_reset)`; + - `xxx_download_impl(...)`:单次执行、无重试(供自动化测试探测原始失败)。 + +**异常兜底(失败 → 重置 → 重试)**:任一流程失败(返回 False 或抛异常)→ 调对应 +站点的 `xxx_reset` 回到初始态 → 重试,**最多 3 次(含首次)**;每次失败都重置 +(含最终放弃那次),确保环境不残留脏状态。 + +> 安能是 Electron,由 CDP(远程调试端口)驱动而非 Playwright page。其重置**不用 +> `Page.reload`**——reload 会让已开的业务 webContents 失去引用、变成无法清理的僵尸, +> 故改用"走 tab 条 X 关 tab + 收起菜单"。 + +--- + +## 七、输出 + +- `downloads/{站点}-应到货物数据.xlsx`、`{站点}-实到货物数据.xlsx`:各站点原始数据; +- `output/应到未到数据.xlsx`:全站点比对报表(汇总 + 各站明细),由菜单 [9] 生成。 + +--- + +## 八、备注 + +- 首次运行需手动登录各站点(程序会停在就绪轮询,直到检测到所有站点进入工作台); +- 安能为单实例 Electron 应用:启动前请先关闭已打开的安能窗口; +- 所有下载/输出路径以项目目录为基准(见 `paths.py`),与从哪个目录启动无关。