docs: update README/CLAUDE.md for package layout; add Tier 1 spec and plan
Rewrite run commands to python -m inbound_verify.* (and console_script aliases); add pip install -e . to env prep; refresh the directory tree. Also commit the design spec and Tier 1 implementation plan under docs/superpowers/. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
65
CLAUDE.md
65
CLAUDE.md
@@ -8,73 +8,82 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
异常运单并汇总成 Excel。4 个网页站点 + 1 个 Electron 应用(安能)。
|
异常运单并汇总成 Excel。4 个网页站点 + 1 个 Electron 应用(安能)。
|
||||||
|
|
||||||
**两种运行模式**(阶段1 起):
|
**两种运行模式**(阶段1 起):
|
||||||
- **交互模式** `main_router.py`:人工调试 / 操作,交互菜单(登录、触发下载、[12] 状态盘)。
|
- **交互模式** `inbound_verify.cli.router`:人工调试 / 操作,交互菜单(登录、触发下载、[12] 状态盘)。
|
||||||
- **服务模式** `server.py`:常驻 + FastAPI,客户端经 HTTP 触发任务、查状态、下载数据(API 文档 `/docs`)。
|
- **服务模式** `inbound_verify.cli.server`:常驻 + FastAPI,客户端经 HTTP 触发任务、查状态、下载数据(API 文档 `/docs`)。
|
||||||
- 两者共享 `runtime.py`(启动 / 就绪 / 任务派发 / 心跳)与 `state_store.py`(SQLite 状态持久化)。
|
- 两者共享 `inbound_verify.runtime`(启动 / 就绪 / 任务派发 / 心跳)与 `inbound_verify.state_store`(SQLite 状态持久化)。
|
||||||
|
|
||||||
## 常用命令
|
## 常用命令
|
||||||
|
|
||||||
所有 Python 一律在项目虚拟环境 `.venv` 中运行(Windows 下可直接用
|
所有 Python 一律在项目虚拟环境 `.venv` 中运行(Windows 下可直接用
|
||||||
`.venv/Scripts/python.exe`,无需激活)。
|
`.venv/Scripts/python.exe`,无需激活)。首次 / 拉取新代码后需
|
||||||
|
`.venv/Scripts/python.exe -m pip install -e .`(以可编辑模式注册
|
||||||
|
`inbound-verify` 等命令)。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 安装依赖(含安能 CDP 驱动所需的 websocket-client)
|
# 安装依赖(含安能 CDP 驱动所需的 websocket-client)+ 以可编辑模式注册命令
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
pip install -e .
|
||||||
playwright install chromium
|
playwright install chromium
|
||||||
|
|
||||||
# 运行主程序(交互式菜单,详见 main_router 的 run_multi_site_daemon)
|
# 运行主程序(交互式菜单,详见 inbound_verify.cli.router 的 run_multi_site_daemon)
|
||||||
.venv/Scripts/python.exe main_router.py
|
.venv/Scripts/python.exe -m inbound_verify.cli.router
|
||||||
|
# 装包后也可直接用命令:inbound-verify
|
||||||
|
|
||||||
# 服务模式(常驻 + FastAPI,客户端经 HTTP 触发;默认 :8000,API 文档见 /docs)
|
# 服务模式(常驻 + FastAPI,客户端经 HTTP 触发;默认 :8000,API 文档见 /docs)
|
||||||
.venv/Scripts/python.exe server.py
|
.venv/Scripts/python.exe -m inbound_verify.cli.server
|
||||||
|
# 或:inbound-verify-server
|
||||||
|
|
||||||
|
# DB CLI:建库 / 初始化 / 灌数据 / 全流程
|
||||||
|
.venv/Scripts/python.exe -m inbound_verify.store createdb # 或 init | ingest | all
|
||||||
|
# 或:inbound-verify-db createdb|init|ingest|all
|
||||||
|
|
||||||
# 单站点联调:在 config.yaml 设 debug.enabled=true + debug.target_site=顺心|百世|中通|韵达|安能
|
# 单站点联调:在 config.yaml 设 debug.enabled=true + debug.target_site=顺心|百世|中通|韵达|安能
|
||||||
# 网页站:只挂载该站;安能:只启动 Electron 应用。
|
# 网页站:只挂载该站;安能:只启动 Electron 应用。
|
||||||
|
|
||||||
# 安能独立运行(需先以 --remote-debugging-port=9222 启动「安能全网门户.exe」并手动登录)
|
# 安能独立运行(需先以 --remote-debugging-port=9222 启动「安能全网门户.exe」并手动登录)
|
||||||
.venv/Scripts/python.exe site_anneng.py expected # 或 actual
|
.venv/Scripts/python.exe -m inbound_verify.sites.anneng expected # 或 actual
|
||||||
|
|
||||||
# 格式化(全局规范:改完 Python 必须 Black)
|
# 格式化(全局规范:改完 Python 必须 Black)
|
||||||
.venv/Scripts/python.exe -m black <file.py>
|
.venv/Scripts/python.exe -m black inbound_verify
|
||||||
|
|
||||||
# 语法自检
|
# 语法自检
|
||||||
.venv/Scripts/python.exe -m py_compile <file.py>
|
.venv/Scripts/python.exe -m py_compile inbound_verify
|
||||||
```
|
```
|
||||||
|
|
||||||
**没有 pytest 测试套件。** "测试"指 `main_router` 菜单 **[8] 自动化测试**
|
**没有 pytest 测试套件。** "测试"指 `inbound_verify.cli.router` 菜单 **[8] 自动化测试**
|
||||||
(`run_automation_test`,按 `CROSS_TEST_SEQUENCE` 交叉跑通各站点流程)。
|
(`run_automation_test`,按 `CROSS_TEST_SEQUENCE` 交叉跑通各站点流程)。
|
||||||
|
|
||||||
## 架构(big picture)
|
## 架构(big picture)
|
||||||
|
|
||||||
### 运行模式与共享核心(阶段0/1 重构)
|
### 运行模式与共享核心(阶段0/1 重构)
|
||||||
- **`runtime.py`**:两种模式共享的核心——`launch_and_prepare`(启动 Playwright + 各站就绪 + 弹窗 + 心跳初值,阻塞至就绪)、`dispatch_task(ctx, {site,kind})`(派发任务,掉登录直接判 failed)、`run_heartbeat`、`probe_site_login/probe_data_file`、`RuntimeContext.stop()`。常量 `SITES_CONFIG/READY_SELECTORS/APP_SITES/HEARTBEAT_INTERVAL/DATA_FILENAMES` 在此。
|
- **`inbound_verify.runtime`**:两种模式共享的核心——`launch_and_prepare`(启动 Playwright + 各站就绪 + 弹窗 + 心跳初值,阻塞至就绪)、`dispatch_task(ctx, {site,kind})`(派发任务,掉登录直接判 failed)、`run_heartbeat`、`probe_site_login/probe_data_file`、`RuntimeContext.stop()`。常量 `SITES_CONFIG/READY_SELECTORS/APP_SITES/HEARTBEAT_INTERVAL/DATA_FILENAMES` 在此。
|
||||||
- **`state_store.py`**:SQLite 状态持久化(`state/state.db`)。`site_status`(登录态 + 数据态 + 时间戳,心跳刷新)、`task_history`(任务记录)。重启不丢。
|
- **`inbound_verify.state_store`**:SQLite 状态持久化(`state/state.db`)。`site_status`(登录态 + 数据态 + 时间戳,心跳刷新)、`task_history`(任务记录)。重启不丢。
|
||||||
- **`main_router.py`**:交互模式,菜单循环(input 后台线程 + `_await_command` + `dispatch_task` + 心跳)。
|
- **`inbound_verify.cli.router`**:交互模式,菜单循环(input 后台线程 + `_await_command` + `dispatch_task` + 心跳)。
|
||||||
- **`server.py`**:服务模式。**FastAPI(主线程)+ Playwright worker(独立线程)**——主线程处理 HTTP(绝不碰 Playwright),worker 独占 page 操作,经 `task_queue` + `state_store` 通信。API:`POST/GET /tasks`、`GET /status`、`GET /data/{file}`。
|
- **`inbound_verify.cli.server`**:服务模式。**FastAPI(主线程)+ Playwright worker(独立线程)**——主线程处理 HTTP(绝不碰 Playwright),worker 独占 page 操作,经 `task_queue` + `state_store` 通信。API:`POST/GET /tasks`、`GET /status`、`GET /data/{file}`。
|
||||||
- **关键线程约束**:Playwright sync 对象绑定创建它的线程;`launch_and_prepare`(含 `sync_playwright().start()`)必须在持有 Playwright 的线程调用(交互=主线程,服务=worker 线程)。FastAPI 路由绝不访问 page。
|
- **关键线程约束**:Playwright sync 对象绑定创建它的线程;`launch_and_prepare`(含 `sync_playwright().start()`)必须在持有 Playwright 的线程调用(交互=主线程,服务=worker 线程)。FastAPI 路由绝不访问 page。
|
||||||
- 站点模块 `site_*.py` 的 `with_retry` 返回 `True/False`(成功 / 放弃),供 `dispatch_task` 判成败。
|
- 站点模块 `inbound_verify.sites.*` 的 `with_retry` 返回 `True/False`(成功 / 放弃),供 `dispatch_task` 判成败。
|
||||||
|
|
||||||
### 两套驱动模态 —— 这是理解全局的关键
|
### 两套驱动模态 —— 这是理解全局的关键
|
||||||
- **网页 4 站**(顺心/百世/中通/韵达):`main_router` 用 Playwright 开 chromium,
|
- **网页 4 站**(顺心/百世/中通/韵达):`inbound_verify.cli.router` 用 Playwright 开 chromium,
|
||||||
每站一个 `page`,流程函数签名为 `xxx_download_impl(page)`。**例外:顺心是双账号**
|
每站一个 `page`,流程函数签名为 `xxx_download_impl(page)`。**例外:顺心是双账号**
|
||||||
——同一窗口开两个标签页(两个归属地账号),`pages_map["顺心"]` 存为 page **列表**,
|
——同一窗口开两个标签页(两个归属地账号),`pages_map["顺心"]` 存为 page **列表**,
|
||||||
`shunxin_download(pages)` 接收列表(详见下文「顺心双账号」)。
|
`shunxin_download(pages)` 接收列表(详见下文「顺心双账号」)。
|
||||||
- **安能**:Electron 桌面应用,**不走 Playwright**。`main_router` 以
|
- **安能**:Electron 桌面应用,**不走 Playwright**。`inbound_verify.cli.router` 以
|
||||||
`--remote-debugging-port=<动态空闲端口>` 启动 exe(`launch_anneng`),
|
`--remote-debugging-port=<动态空闲端口>` 启动 exe(`launch_anneng`),
|
||||||
通过 `site_anneng.set_cdp_port` 告知模块;`site_anneng.py` 用裸 CDP(websocket)
|
通过 `inbound_verify.sites.anneng.set_cdp_port` 告知模块;`inbound_verify.sites.anneng` 用裸 CDP(websocket)
|
||||||
驱动,业务 tab 是独立 webContents。这也是 `playwright-cli` 接管不了安能的原因
|
驱动,业务 tab 是独立 webContents。这也是 `playwright-cli` 接管不了安能的原因
|
||||||
(Electron 19 / Chrome 102 不支持 Playwright 要的 setDownloadBehavior)。
|
(Electron 19 / Chrome 102 不支持 Playwright 要的 setDownloadBehavior)。
|
||||||
|
|
||||||
### 分层:路由纯调度,站点模块自洽
|
### 分层:路由纯调度,站点模块自洽
|
||||||
- `main_router.py` **只调度**:启动浏览器/安能、就绪轮询、登录检测、菜单分发。
|
- `inbound_verify.cli.router` **只调度**:启动浏览器/安能、就绪轮询、登录检测、菜单分发。
|
||||||
菜单项直接调 `site_xxx.xxx_download(page)`(顺心传 page 列表),**不关心**重试/重置。
|
菜单项直接调 `sites.xxx_download(page)`(顺心传 page 列表),**不关心**重试/重置。
|
||||||
- 每个 `site_xxx.py` 对外只暴露"把任务做了"的入口,内部自洽:
|
- 每个 `inbound_verify.sites.*` 模块对外只暴露"把任务做了"的入口,内部自洽:
|
||||||
- `xxx_download(...)` —— 公开入口,= `with_retry(站点, 标签, xxx_download_impl, xxx_reset)`
|
- `xxx_download(...)` —— 公开入口,= `with_retry(站点, 标签, xxx_download_impl, xxx_reset)`
|
||||||
- `xxx_download_impl(...)` —— 单次执行、**无重试**(自动化测试刻意调它以探测原始失败)
|
- `xxx_download_impl(...)` —— 单次执行、**无重试**(自动化测试刻意调它以探测原始失败)
|
||||||
- `xxx_reset(...)` —— 重置回初始态(网页 = `page.goto(HOME_URL)`;安能 = 关业务 tab + 收菜单)
|
- `xxx_reset(...)` —— 重置回初始态(网页 = `page.goto(HOME_URL)`;安能 = 关业务 tab + 收菜单)
|
||||||
- `with_retry(...)` —— 重试逻辑**内联在每个站点模块**(不抽公共组件,现阶段刻意不优化结构);
|
- `with_retry(...)` —— 重试逻辑**内联在每个站点模块**(不抽公共组件,现阶段刻意不优化结构);
|
||||||
失败→重置→重试,最多 3 次(含首次),每次失败都重置(含最终放弃那次清场)
|
失败→重置→重试,最多 3 次(含首次),每次失败都重置(含最终放弃那次清场)
|
||||||
- `HOME_URL` —— 站点首页 URL,`main_router.SITES_CONFIG` 引用它(单一来源)
|
- `HOME_URL` —— 站点首页 URL,`runtime.SITES_CONFIG` 引用它(单一来源)
|
||||||
|
|
||||||
### 导出任务队列模式(顺心/中通/韵达/安能-应到 共用)
|
### 导出任务队列模式(顺心/中通/韵达/安能-应到 共用)
|
||||||
这些站的下载是异步的:提交导出(记 `export_times` 时间戳)→ 跳"导出任务管理"页轮询 →
|
这些站的下载是异步的:提交导出(记 `export_times` 时间戳)→ 跳"导出任务管理"页轮询 →
|
||||||
@@ -88,7 +97,7 @@ playwright install chromium
|
|||||||
### 顺心双账号(双归属地)
|
### 顺心双账号(双归属地)
|
||||||
顺心业务上要同时处理**两个归属地网点**(两个账号)。程序在同一窗口开两个标签页,
|
顺心业务上要同时处理**两个归属地网点**(两个账号)。程序在同一窗口开两个标签页,
|
||||||
人工分别登录两个账号(顺心站点支持同浏览器双账号并存,无需独立 context/窗口)。
|
人工分别登录两个账号(顺心站点支持同浏览器双账号并存,无需独立 context/窗口)。
|
||||||
- `main_router` 启动时为顺心开 2 个 `context.new_page()`,`pages_map["顺心"]` 为列表;
|
- `runtime` 启动时为顺心开 2 个 `context.new_page()`,`pages_map["顺心"]` 为列表;
|
||||||
就绪轮询要求**两个标签页都进主页**才算就绪;初始弹窗对两个标签页各处理一遍。
|
就绪轮询要求**两个标签页都进主页**才算就绪;初始弹窗对两个标签页各处理一遍。
|
||||||
- `shunxin_expected_download(pages)` / `shunxin_actual_download(pages)` 接收 page 列表:
|
- `shunxin_expected_download(pages)` / `shunxin_actual_download(pages)` 接收 page 列表:
|
||||||
先用 `shunxin_belonging(page)` 读各账号归属地(首页「切换网点」控件 `.site___3o7nH`),
|
先用 `shunxin_belonging(page)` 读各账号归属地(首页「切换网点」控件 `.site___3o7nH`),
|
||||||
@@ -100,16 +109,16 @@ playwright install chromium
|
|||||||
配合每账号独立 `export_times` + ≤40s 容差,B 不会误匹配 A 的任务。
|
配合每账号独立 `export_times` + ≤40s 容差,B 不会误匹配 A 的任务。
|
||||||
|
|
||||||
### 比对
|
### 比对
|
||||||
`expected_undelivered.py`(菜单 [9])纯离线:读 `downloads/` 下各站应到/实到 xlsx,
|
`inbound_verify.expected_undelivered`(菜单 [9])纯离线:读 `downloads/` 下各站应到/实到 xlsx,
|
||||||
比对生成 `output/应到未到数据.xlsx`(汇总 + 各站明细)。
|
比对生成 `output/应到未到数据.xlsx`(汇总 + 各站明细)。
|
||||||
|
|
||||||
### 路径
|
### 路径
|
||||||
`paths.py` 把 `DOWNLOAD_DIR` / `CONFIG_PATH` / `BASE_DIR` 全部锚定到项目目录,
|
`inbound_verify.paths` 把 `DOWNLOAD_DIR` / `CONFIG_PATH` / `BASE_DIR` 全部锚定到项目目录,
|
||||||
**不依赖运行时 cwd**——别用相对路径或 `os.getcwd()`。
|
**不依赖运行时 cwd**——别用相对路径或 `os.getcwd()`。
|
||||||
|
|
||||||
## 重要约定 / 易踩坑
|
## 重要约定 / 易踩坑
|
||||||
|
|
||||||
- **登录是手动的**:`main_router` 启动后会停在就绪轮询(`READY_SELECTORS` / `anneng_ready`),
|
- **登录是手动的**:`inbound_verify.cli.router` 启动后会停在就绪轮询(`READY_SELECTORS` / `anneng_ready`),
|
||||||
直到检测到所有站点进入工作台才进菜单。仅韵达支持凭 `config.yaml` 凭据自动登录。
|
直到检测到所有站点进入工作台才进菜单。仅韵达支持凭 `config.yaml` 凭据自动登录。
|
||||||
**顺心需登录两个账号**:同一窗口的两个标签页分别登录两个不同归属地账号,两个标签页
|
**顺心需登录两个账号**:同一窗口的两个标签页分别登录两个不同归属地账号,两个标签页
|
||||||
都进主页后才算就绪(顺心站点支持同浏览器双账号并存,故用同 context 标签页而非独立窗口)。
|
都进主页后才算就绪(顺心站点支持同浏览器双账号并存,故用同 context 标签页而非独立窗口)。
|
||||||
|
|||||||
56
README.md
56
README.md
@@ -32,20 +32,30 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
InboundVerify/
|
InboundVerify/
|
||||||
├── main_router.py # 主入口 / 调度层(菜单、启动浏览器与安能、就绪轮询、登录检测)
|
├── pyproject.toml # 打包 + 依赖 + console_scripts(inbound-verify 等)
|
||||||
├── site_shunxin.py # 顺心站点模块(流程 + 重置 + 重试,自洽)
|
├── inbound_verify/ # 源码包
|
||||||
├── site_baishi.py # 百世站点模块
|
│ ├── paths.py # 统一路径锚点(以项目目录为基准,不依赖 cwd)
|
||||||
├── site_zto.py # 中通站点模块
|
│ ├── runtime.py # 两种模式共享核心(启动 / 就绪 / 任务派发 / 心跳)
|
||||||
├── site_yunda.py # 韵达站点模块(含自动登录)
|
│ ├── state_store.py # SQLite 状态持久化(state/state.db)
|
||||||
├── site_anneng.py # 安能站点模块(Electron + CDP 驱动)
|
│ ├── expected_undelivered.py# 全站点应到未到离线比对,输出 output/应到未到数据.xlsx
|
||||||
├── expected_undelivered.py # 全站点应到未到离线比对,输出 output/应到未到数据.xlsx
|
│ ├── store.py # DB CLI 入口(createdb|init|ingest|all)
|
||||||
├── paths.py # 统一路径锚点(以本目录为基准,不依赖 cwd)
|
│ ├── sites/ # 各站点模块(流程 + 重置 + 重试,自洽)
|
||||||
|
│ │ ├── shunxin.py # 顺心(含双账号)
|
||||||
|
│ │ ├── baishi.py # 百世
|
||||||
|
│ │ ├── zto.py # 中通
|
||||||
|
│ │ ├── yunda.py # 韵达(含自动登录)
|
||||||
|
│ │ └── anneng.py # 安能(Electron + CDP 驱动)
|
||||||
|
│ └── cli/ # 命令行入口
|
||||||
|
│ ├── router.py # 交互菜单(调度层:启动 / 就绪轮询 / 登录检测 / 菜单分发)
|
||||||
|
│ └── server.py # FastAPI 服务模式(常驻 + HTTP 触发)
|
||||||
├── config.example.yaml # 配置模板
|
├── config.example.yaml # 配置模板
|
||||||
├── config.yaml # 真实配置(自行创建,已被 .gitignore 忽略)
|
├── config.yaml # 真实配置(自行创建,已被 .gitignore 忽略)
|
||||||
├── requirements.txt
|
├── schema.sql # 数据库表结构(store.py createdb / init 使用)
|
||||||
├── downloads/ # 各站点下载的原始数据
|
├── requirements.txt # pyproject 依赖的静态镜像
|
||||||
├── output/ # 比对报表输出
|
├── downloads/ # 各站点下载的原始数据
|
||||||
└── docs/ # 说明文档
|
├── output/ # 比对报表输出
|
||||||
|
├── state/ # 运行状态持久化(state.db)
|
||||||
|
└── docs/ # 说明文档
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -63,10 +73,13 @@ python -m venv .venv
|
|||||||
# 2. 安装依赖(含安能 CDP 驱动所需的 websocket-client)
|
# 2. 安装依赖(含安能 CDP 驱动所需的 websocket-client)
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
# 3. 安装 Playwright 浏览器内核(网页站点用)
|
# 3. 以可编辑模式安装本包(注册 inbound-verify 等命令)
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
# 4. 安装 Playwright 浏览器内核(网页站点用)
|
||||||
playwright install chromium
|
playwright install chromium
|
||||||
|
|
||||||
# 4. 由模板创建本地配置并填入真实凭据
|
# 5. 由模板创建本地配置并填入真实凭据
|
||||||
cp config.example.yaml config.yaml
|
cp config.example.yaml config.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -96,7 +109,10 @@ cp config.example.yaml config.yaml
|
|||||||
## 五、运行
|
## 五、运行
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python main_router.py
|
# 交互菜单(任选其一)
|
||||||
|
python -m inbound_verify.cli.router
|
||||||
|
# 或装包后直接用命令:
|
||||||
|
inbound-verify
|
||||||
```
|
```
|
||||||
|
|
||||||
程序会:
|
程序会:
|
||||||
@@ -126,10 +142,10 @@ python main_router.py
|
|||||||
|
|
||||||
**分层原则:路由层只调度,站点模块自洽。**
|
**分层原则:路由层只调度,站点模块自洽。**
|
||||||
|
|
||||||
- **`main_router.py`(调度层)**:负责启动浏览器 / 安能、就绪轮询、登录检测、
|
- **`inbound_verify.cli.router`(调度层)**:负责启动浏览器 / 安能、就绪轮询、登录检测、
|
||||||
菜单分发。**不关心**"任务能否完成、失败怎么办"——只调
|
菜单分发。**不关心**"任务能否完成、失败怎么办"——只调
|
||||||
`site_xxx.xxx_download(page)` 然后等结果。
|
`inbound_verify.sites.xxx_download(page)` 然后等结果。
|
||||||
- **各 `site_xxx.py`(站点模块)**:每个模块对外只暴露一个"把任务做了"的入口
|
- **各 `inbound_verify.sites.*`(站点模块)**:每个模块对外只暴露一个"把任务做了"的入口
|
||||||
`xxx_download(...)`,内部自行处理一切:
|
`xxx_download(...)`,内部自行处理一切:
|
||||||
- `HOME_URL`:站点首页 URL(也供路由层 `SITES_CONFIG` 引用,单一来源);
|
- `HOME_URL`:站点首页 URL(也供路由层 `SITES_CONFIG` 引用,单一来源);
|
||||||
- `xxx_reset(...)`:异常兜底的重置(网页 = 跳首页 URL;安能 = 关业务 tab + 收菜单);
|
- `xxx_reset(...)`:异常兜底的重置(网页 = 跳首页 URL;安能 = 关业务 tab + 收菜单);
|
||||||
@@ -162,4 +178,4 @@ python main_router.py
|
|||||||
|
|
||||||
- 首次运行需手动登录各站点(程序会停在就绪轮询,直到检测到所有站点进入工作台);
|
- 首次运行需手动登录各站点(程序会停在就绪轮询,直到检测到所有站点进入工作台);
|
||||||
- 安能为单实例 Electron 应用:启动前请先关闭已打开的安能窗口;
|
- 安能为单实例 Electron 应用:启动前请先关闭已打开的安能窗口;
|
||||||
- 所有下载/输出路径以项目目录为基准(见 `paths.py`),与从哪个目录启动无关。
|
- 所有下载/输出路径以项目目录为基准(见 `inbound_verify.paths`),与从哪个目录启动无关。
|
||||||
|
|||||||
579
docs/superpowers/plans/2026-07-23-tier1-package-move.md
Normal file
579
docs/superpowers/plans/2026-07-23-tier1-package-move.md
Normal file
@@ -0,0 +1,579 @@
|
|||||||
|
# Tier 1: Package Move Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Move all 12 flat root-level Python modules into an installable `inbound_verify/` package (sites/, cli/ subpackages), rewrite every internal import to package-qualified form, and expose three console_script entry points — with **zero behavior change**.
|
||||||
|
|
||||||
|
**Architecture:** Pure mechanical relocation (git mv preserves history) + import rewrite + paths.py anchor fix + entry-point `main()` wrappers + `pyproject.toml` packaging. No logic changes. Module names kept for heavily-referenced modules (`state_store`, `expected_undelivered`, `runtime`, `paths`) to avoid call-site churn; only leaf entries (`db_store→store`, `main_router→cli/router`, `server→cli/server`) and site files (drop `site_` prefix) are renamed.
|
||||||
|
|
||||||
|
**Tech Stack:** Python ≥3.10, setuptools (PEP 517/621), pip editable install, psycopg3, FastAPI/uvicorn, Playwright.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- **Python ≥ 3.10** (`requires-python = ">=3.10"` in pyproject).
|
||||||
|
- **Package** import name `inbound_verify`; **distribution** name `inbound-verify`.
|
||||||
|
- **Dependency floors** (verbatim from spec): `pandas>=2.0.0`, `playwright>=1.40.0`, `openpyxl>=3.1.0`, `PyYAML>=6.0`, `websocket-client>=1.0.0`, `fastapi>=0.110.0`, `uvicorn>=0.27.0`, `apscheduler>=3.10.0`, `psycopg[binary]>=3.1`.
|
||||||
|
- **No test suite** (user decision). Verification = `compileall` + import smoke + grep-for-stale-refs + DB connectivity. No pytest.
|
||||||
|
- **Behavior must not change** in Tier 1 — pure move.
|
||||||
|
- **No auto-commit/push.** Every commit step below runs ONLY after the user explicitly says "提交/commit". Commit messages in English.
|
||||||
|
- **Black-format** every changed `.py` (global rule).
|
||||||
|
- All changes are **inside the `InboundVerify` git submodule**; the parent repo pointer bump is a separate parent-repo step, out of scope.
|
||||||
|
- All commands run from the `InboundVerify/` directory using the venv interpreter `.venv/Scripts/python.exe` (Windows; no activation needed).
|
||||||
|
|
||||||
|
**Reference spec:** `docs/superpowers/specs/2026-07-23-package-restructure-design.md` (§3 mapping table, §4 paths anchor, §5 import rules, §6 entry/packaging, §7 verification gate).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure (what each file becomes responsible for)
|
||||||
|
|
||||||
|
```
|
||||||
|
inbound_verify/
|
||||||
|
├── __init__.py # empty (package marker)
|
||||||
|
├── paths.py # path anchors → PROJECT ROOT (one dir above package)
|
||||||
|
├── runtime.py # orchestration core (unchanged logic)
|
||||||
|
├── state_store.py # SQLite state (unchanged; name kept)
|
||||||
|
├── expected_undelivered.py# offline compare (unchanged; name kept — Tier 2 renames to compare)
|
||||||
|
├── store.py # PostgreSQL persist + main() (was db_store.py)
|
||||||
|
├── sites/
|
||||||
|
│ ├── __init__.py # empty
|
||||||
|
│ ├── shunxin.py # (was site_shunxin.py)
|
||||||
|
│ ├── baishi.py # (was site_baishi.py)
|
||||||
|
│ ├── zto.py # (was site_zto.py)
|
||||||
|
│ ├── yunda.py # (was site_yunda.py)
|
||||||
|
│ └── anneng.py # (was site_anneng.py)
|
||||||
|
└── cli/
|
||||||
|
├── __init__.py # empty
|
||||||
|
├── router.py # interactive menu + main() (was main_router.py)
|
||||||
|
└── server.py # FastAPI service + main() (was server.py)
|
||||||
|
```
|
||||||
|
|
||||||
|
Root keeps: `pyproject.toml` (new), `config.yaml`, `config.example.yaml`, `schema.sql`, `requirements.txt`, `README.md`, `CLAUDE.md`, `docs/`, `downloads/`, `output/`, `state/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Package scaffold + pyproject + editable install
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `inbound_verify/__init__.py`, `inbound_verify/sites/__init__.py`, `inbound_verify/cli/__init__.py`
|
||||||
|
- Create: `pyproject.toml`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces: an importable (near-empty) `inbound_verify` package + console_script registration. The flat root scripts remain 100% functional after this task (untouched).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create package marker files**
|
||||||
|
|
||||||
|
Create three empty files:
|
||||||
|
- `inbound_verify/__init__.py`
|
||||||
|
- `inbound_verify/sites/__init__.py`
|
||||||
|
- `inbound_verify/cli/__init__.py`
|
||||||
|
|
||||||
|
Each is a single comment line:
|
||||||
|
```python
|
||||||
|
# inbound_verify package
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write pyproject.toml**
|
||||||
|
|
||||||
|
Create `pyproject.toml`:
|
||||||
|
```toml
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=68"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "inbound-verify"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "物流到货数据自动下载与应到未到核对工具"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"pandas>=2.0.0",
|
||||||
|
"playwright>=1.40.0",
|
||||||
|
"openpyxl>=3.1.0",
|
||||||
|
"PyYAML>=6.0",
|
||||||
|
"websocket-client>=1.0.0",
|
||||||
|
"fastapi>=0.110.0",
|
||||||
|
"uvicorn>=0.27.0",
|
||||||
|
"apscheduler>=3.10.0",
|
||||||
|
"psycopg[binary]>=3.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
inbound-verify = "inbound_verify.cli.router:main"
|
||||||
|
inbound-verify-server = "inbound_verify.cli.server:main"
|
||||||
|
inbound-verify-db = "inbound_verify.store:main"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
include = ["inbound_verify*"]
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Editable-install into the existing venv**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
.venv/Scripts/python.exe -m pip install -e .
|
||||||
|
```
|
||||||
|
Expected: `Successfully installed inbound-verify-0.1.0` (deps already satisfied from earlier install — no network needed).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Verify package imports**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
.venv/Scripts/python.exe -c "import inbound_verify, inbound_verify.sites, inbound_verify.cli; print('package OK')"
|
||||||
|
```
|
||||||
|
Expected output: `package OK`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit (only after user confirms)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add inbound_verify/__init__.py inbound_verify/sites/__init__.py inbound_verify/cli/__init__.py pyproject.toml
|
||||||
|
git commit -m "chore: scaffold inbound_verify package and pyproject
|
||||||
|
|
||||||
|
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||||
|
```
|
||||||
|
> Do NOT commit until the user says to.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Atomic move — relocate, rewrite imports, fix anchor, wire mains, verify
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Move (git mv): all 12 root `.py` modules → package locations (see Step 1)
|
||||||
|
- Modify: `inbound_verify/paths.py` (anchor), and import lines + call sites in every moved module
|
||||||
|
- Modify: entry `main()` in `cli/router.py`, `cli/server.py`, `store.py`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: the scaffold from Task 1 (package importable + editable install active).
|
||||||
|
- Produces: a fully functional `inbound_verify` package invokable via `python -m inbound_verify.cli.router`, `python -m inbound_verify.cli.server`, `python -m inbound_verify.store`, or the three console_scripts. Old root `.py` files are gone. The flat root scripts no longer exist — invocation switches to package form.
|
||||||
|
|
||||||
|
> **Why this is one task:** in a flat-import codebase, moving `paths.py` (imported by everyone) immediately breaks every importer until ALL moves + rewrites are complete. There is no intermediate state that imports cleanly, so the whole move is one atomic unit verified by the gate at the end. Each file-edit step below is followed by `py_compile` of that file to catch syntax errors as we go.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Relocate all 12 modules with git mv (history preserved)**
|
||||||
|
|
||||||
|
From the `InboundVerify/` directory:
|
||||||
|
```bash
|
||||||
|
git mv paths.py inbound_verify/paths.py
|
||||||
|
git mv runtime.py inbound_verify/runtime.py
|
||||||
|
git mv state_store.py inbound_verify/state_store.py
|
||||||
|
git mv expected_undelivered.py inbound_verify/expected_undelivered.py
|
||||||
|
git mv db_store.py inbound_verify/store.py
|
||||||
|
git mv main_router.py inbound_verify/cli/router.py
|
||||||
|
git mv server.py inbound_verify/cli/server.py
|
||||||
|
git mv site_shunxin.py inbound_verify/sites/shunxin.py
|
||||||
|
git mv site_baishi.py inbound_verify/sites/baishi.py
|
||||||
|
git mv site_zto.py inbound_verify/sites/zto.py
|
||||||
|
git mv site_yunda.py inbound_verify/sites/yunda.py
|
||||||
|
git mv site_anneng.py inbound_verify/sites/anneng.py
|
||||||
|
```
|
||||||
|
After this the tree is temporarily broken (imports unresolved) — expected. Continue.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Fix paths.py anchor to point at project root**
|
||||||
|
|
||||||
|
In `inbound_verify/paths.py`, replace:
|
||||||
|
```python
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
```
|
||||||
|
with:
|
||||||
|
```python
|
||||||
|
# __file__ = <root>/inbound_verify/paths.py → 上两级 = 项目根
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
```
|
||||||
|
(`DOWNLOAD_DIR`/`OUTPUT_DIR`/`CONFIG_PATH`/`STATE_DB_PATH` lines stay unchanged — they derive from BASE_DIR.)
|
||||||
|
|
||||||
|
Verify syntax:
|
||||||
|
```bash
|
||||||
|
.venv/Scripts/python.exe -m py_compile inbound_verify/paths.py
|
||||||
|
```
|
||||||
|
Expected: no output (success).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Rewrite imports in state_store.py**
|
||||||
|
|
||||||
|
In `inbound_verify/state_store.py`, replace:
|
||||||
|
```python
|
||||||
|
from paths import STATE_DB_PATH
|
||||||
|
```
|
||||||
|
with:
|
||||||
|
```python
|
||||||
|
from inbound_verify.paths import STATE_DB_PATH
|
||||||
|
```
|
||||||
|
Verify: `.venv/Scripts/python.exe -m py_compile inbound_verify/state_store.py` → no output.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Rewrite imports + call sites in runtime.py**
|
||||||
|
|
||||||
|
In `inbound_verify/runtime.py`, replace the import block:
|
||||||
|
```python
|
||||||
|
from paths import DOWNLOAD_DIR, CONFIG_PATH
|
||||||
|
import state_store
|
||||||
|
import site_shunxin
|
||||||
|
import site_baishi
|
||||||
|
import site_zto
|
||||||
|
import site_yunda
|
||||||
|
import site_anneng
|
||||||
|
import expected_undelivered # dispatch 的 compare 任务用
|
||||||
|
```
|
||||||
|
with:
|
||||||
|
```python
|
||||||
|
from inbound_verify.paths import DOWNLOAD_DIR, CONFIG_PATH
|
||||||
|
from inbound_verify import state_store
|
||||||
|
from inbound_verify.sites import shunxin, baishi, zto, yunda, anneng
|
||||||
|
from inbound_verify import expected_undelivered # dispatch 的 compare 任务用
|
||||||
|
```
|
||||||
|
|
||||||
|
Then **drop the `site_` prefix at every call site** (28 references — 18 in this file). Apply these 5 replacements (all occurrences):
|
||||||
|
- `site_shunxin.` → `shunxin.`
|
||||||
|
- `site_baishi.` → `baishi.`
|
||||||
|
- `site_zto.` → `zto.`
|
||||||
|
- `site_yunda.` → `yunda.`
|
||||||
|
- `site_anneng.` → `anneng.`
|
||||||
|
|
||||||
|
Affected lines in runtime.py (for reference, all must change): 36, 37, 38, 39, 118, 139, 336, 408, 417, 463, 464, 467, 469, 470, 472, 473, 475, 476.
|
||||||
|
|
||||||
|
`state_store.` and `expected_undelivered.` call sites stay UNCHANGED (names kept). Confirm the key handler block now reads:
|
||||||
|
```python
|
||||||
|
TASK_HANDLERS = {
|
||||||
|
("顺心", "expected"): _web_handler("顺心", shunxin.shunxin_expected_download),
|
||||||
|
("顺心", "actual"): _web_handler("顺心", shunxin.shunxin_actual_download),
|
||||||
|
("顺心", "undelivered"): _site_undelivered_handler("顺心"),
|
||||||
|
("百世", "undelivered"): _web_handler(
|
||||||
|
"百世", baishi.baishi_download_undelivered_data
|
||||||
|
),
|
||||||
|
("中通", "expected"): _web_handler("中通", zto.zto_expected_download),
|
||||||
|
("中通", "actual"): _web_handler("中通", zto.zto_actual_download),
|
||||||
|
("中通", "undelivered"): _site_undelivered_handler("中通"),
|
||||||
|
("韵达", "expected"): _web_handler("韵达", yunda.yunda_expected_download),
|
||||||
|
("韵达", "actual"): _web_handler("韵达", yunda.yunda_actual_download),
|
||||||
|
("韵达", "undelivered"): _site_undelivered_handler("韵达"),
|
||||||
|
("安能", "expected"): lambda ctx: anneng.anneng_expected_download(),
|
||||||
|
("安能", "actual"): lambda ctx: anneng.anneng_actual_download(),
|
||||||
|
("安能", "undelivered"): _site_undelivered_handler("安能"),
|
||||||
|
("__compare__", "compare"): lambda ctx: (expected_undelivered.main() or True),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Verify: `.venv/Scripts/python.exe -m py_compile inbound_verify/runtime.py` → no output.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Rewrite imports in expected_undelivered.py**
|
||||||
|
|
||||||
|
This module has TWO lazy `import state_store` statements (inside functions). Replace each occurrence of:
|
||||||
|
```python
|
||||||
|
import state_store
|
||||||
|
```
|
||||||
|
with:
|
||||||
|
```python
|
||||||
|
from inbound_verify import state_store
|
||||||
|
```
|
||||||
|
(There is no `from paths import` here — the module has its own `BASE/DOWNLOADS/OUTPUT` constants; that dedup is Tier 2, not now.)
|
||||||
|
Verify: `.venv/Scripts/python.exe -m py_compile inbound_verify/expected_undelivered.py` → no output.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Rewrite imports + rename _cli→main in store.py**
|
||||||
|
|
||||||
|
In `inbound_verify/store.py`, replace:
|
||||||
|
```python
|
||||||
|
from paths import BASE_DIR, CONFIG_PATH, DOWNLOAD_DIR
|
||||||
|
```
|
||||||
|
with:
|
||||||
|
```python
|
||||||
|
from inbound_verify.paths import BASE_DIR, CONFIG_PATH, DOWNLOAD_DIR
|
||||||
|
```
|
||||||
|
Replace:
|
||||||
|
```python
|
||||||
|
import expected_undelivered as eu # 复用站点 / 文件名 / 列映射 / 基号口径(单一来源)
|
||||||
|
```
|
||||||
|
with:
|
||||||
|
```python
|
||||||
|
from inbound_verify import expected_undelivered as eu # 复用站点 / 文件名 / 列映射 / 基号口径(单一来源)
|
||||||
|
```
|
||||||
|
(`eu.` call sites stay unchanged.)
|
||||||
|
|
||||||
|
Rename the CLI entry: replace the function definition:
|
||||||
|
```python
|
||||||
|
def _cli():
|
||||||
|
```
|
||||||
|
with:
|
||||||
|
```python
|
||||||
|
def main():
|
||||||
|
```
|
||||||
|
And at the bottom replace:
|
||||||
|
```python
|
||||||
|
if __name__ == "__main__":
|
||||||
|
_cli()
|
||||||
|
```
|
||||||
|
with:
|
||||||
|
```python
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
Verify: `.venv/Scripts/python.exe -m py_compile inbound_verify/store.py` → no output.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Rewrite imports in all 5 site modules**
|
||||||
|
|
||||||
|
In each of `inbound_verify/sites/{shunxin,baishi,zto,yunda,anneng}.py`, replace:
|
||||||
|
```python
|
||||||
|
from paths import DOWNLOAD_DIR, CONFIG_PATH
|
||||||
|
import state_store
|
||||||
|
```
|
||||||
|
with:
|
||||||
|
```python
|
||||||
|
from inbound_verify.paths import DOWNLOAD_DIR, CONFIG_PATH
|
||||||
|
from inbound_verify import state_store
|
||||||
|
```
|
||||||
|
(Each site module has exactly these two internal imports; `state_store.` call sites unchanged.)
|
||||||
|
Verify all five:
|
||||||
|
```bash
|
||||||
|
.venv/Scripts/python.exe -m py_compile inbound_verify/sites/shunxin.py inbound_verify/sites/baishi.py inbound_verify/sites/zto.py inbound_verify/sites/yunda.py inbound_verify/sites/anneng.py
|
||||||
|
```
|
||||||
|
Expected: no output.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Rewrite imports + call sites + add main() in cli/router.py**
|
||||||
|
|
||||||
|
In `inbound_verify/cli/router.py`, replace the import block (lines ~14–31):
|
||||||
|
```python
|
||||||
|
from paths import CONFIG_PATH
|
||||||
|
from runtime import (
|
||||||
|
APP_SITES,
|
||||||
|
HEARTBEAT_INTERVAL,
|
||||||
|
dispatch_task,
|
||||||
|
launch_and_prepare,
|
||||||
|
run_heartbeat,
|
||||||
|
)
|
||||||
|
|
||||||
|
import state_store
|
||||||
|
|
||||||
|
# 各站点模块(自动化测试 + 比对用;任务派发在 runtime)
|
||||||
|
import site_shunxin
|
||||||
|
import site_baishi
|
||||||
|
import site_zto
|
||||||
|
import site_yunda
|
||||||
|
import site_anneng
|
||||||
|
import expected_undelivered
|
||||||
|
```
|
||||||
|
with:
|
||||||
|
```python
|
||||||
|
from inbound_verify.paths import CONFIG_PATH
|
||||||
|
from inbound_verify.runtime import (
|
||||||
|
APP_SITES,
|
||||||
|
HEARTBEAT_INTERVAL,
|
||||||
|
dispatch_task,
|
||||||
|
launch_and_prepare,
|
||||||
|
run_heartbeat,
|
||||||
|
)
|
||||||
|
|
||||||
|
from inbound_verify import state_store
|
||||||
|
|
||||||
|
# 各站点模块(自动化测试 + 比对用;任务派发在 runtime)
|
||||||
|
from inbound_verify.sites import shunxin, baishi, zto, yunda, anneng
|
||||||
|
from inbound_verify import expected_undelivered
|
||||||
|
```
|
||||||
|
|
||||||
|
Drop the `site_` prefix at the 10 call sites (apply the same 5 replacements as Step 4). Affected lines: 60, 61, 64, 65, 68, 69, 72, 73, 86, 105. (`expected_undelivered.main()` at line 38 stays unchanged.)
|
||||||
|
|
||||||
|
Add an entry function and update the `__main__` guard. Replace:
|
||||||
|
```python
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_multi_site_daemon()
|
||||||
|
```
|
||||||
|
with:
|
||||||
|
```python
|
||||||
|
def main():
|
||||||
|
"""交互菜单模式入口。"""
|
||||||
|
run_multi_site_daemon()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
Verify: `.venv/Scripts/python.exe -m py_compile inbound_verify/cli/router.py` → no output.
|
||||||
|
|
||||||
|
- [ ] **Step 9: Rewrite imports + add main() in cli/server.py**
|
||||||
|
|
||||||
|
In `inbound_verify/cli/server.py`, replace:
|
||||||
|
```python
|
||||||
|
from paths import DOWNLOAD_DIR, OUTPUT_DIR
|
||||||
|
import state_store
|
||||||
|
from runtime import (
|
||||||
|
HEARTBEAT_INTERVAL,
|
||||||
|
TASK_HANDLERS,
|
||||||
|
dispatch_task,
|
||||||
|
launch_and_prepare,
|
||||||
|
run_heartbeat,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
with:
|
||||||
|
```python
|
||||||
|
from inbound_verify.paths import DOWNLOAD_DIR, OUTPUT_DIR
|
||||||
|
from inbound_verify import state_store
|
||||||
|
from inbound_verify.runtime import (
|
||||||
|
HEARTBEAT_INTERVAL,
|
||||||
|
TASK_HANDLERS,
|
||||||
|
dispatch_task,
|
||||||
|
launch_and_prepare,
|
||||||
|
run_heartbeat,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the bottom entry block:
|
||||||
|
```python
|
||||||
|
if __name__ == "__main__":
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
|
```
|
||||||
|
with:
|
||||||
|
```python
|
||||||
|
def main():
|
||||||
|
"""服务模式入口。传字符串导入路径(规范写法;不开 reload/workers 时进程内 import,行为等价)。"""
|
||||||
|
uvicorn.run("inbound_verify.cli.server:app", host="0.0.0.0", port=8000)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
Verify: `.venv/Scripts/python.exe -m py_compile inbound_verify/cli/server.py` → no output.
|
||||||
|
|
||||||
|
- [ ] **Step 10: Clean up stale filename comments**
|
||||||
|
|
||||||
|
Cosmetic but keeps grep clean (Step 12 depends on it). In each `inbound_verify/sites/*.py`, update the line-1 header `# site_xxx.py` → `# sites/xxx.py`. In `inbound_verify/sites/anneng.py`, update the standalone-run comment near the top:
|
||||||
|
```python
|
||||||
|
# .venv/Scripts/python.exe site_anneng.py
|
||||||
|
```
|
||||||
|
→
|
||||||
|
```python
|
||||||
|
# python -m inbound_verify.sites.anneng expected # 或 actual
|
||||||
|
```
|
||||||
|
And the comment at the `CDP_PORT` line referencing "独立运行 site_anneng.py" — update to "独立运行(python -m inbound_verify.sites.anneng)".
|
||||||
|
|
||||||
|
- [ ] **Step 11: Black-format all changed files**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/Scripts/python.exe -m black inbound_verify
|
||||||
|
```
|
||||||
|
Expected: `reformatted ...` / `left unchanged` lines, exit 0.
|
||||||
|
|
||||||
|
- [ ] **Step 12: VERIFICATION GATE — run all five checks**
|
||||||
|
|
||||||
|
**12a. compileall (syntax across whole package):**
|
||||||
|
```bash
|
||||||
|
.venv/Scripts/python.exe -m compileall inbound_verify
|
||||||
|
```
|
||||||
|
Expected: no errors.
|
||||||
|
|
||||||
|
**12b. Import smoke (catches every wrong import path / missed rewrite):**
|
||||||
|
```bash
|
||||||
|
.venv/Scripts/python.exe -c "import inbound_verify.cli.router, inbound_verify.cli.server, inbound_verify.store, inbound_verify.runtime, inbound_verify.state_store; print('import smoke OK')"
|
||||||
|
```
|
||||||
|
Expected: `import smoke OK`. (The three entries transitively import sites + expected_undelivered.)
|
||||||
|
|
||||||
|
**12c. No stale `site_` references:**
|
||||||
|
```bash
|
||||||
|
grep -rn "site_shunxin\|site_baishi\|site_zto\|site_yunda\|site_anneng" inbound_verify || echo "no stale site_ refs OK"
|
||||||
|
```
|
||||||
|
Expected: `no stale site_ refs OK`.
|
||||||
|
|
||||||
|
**12d. No stale bare flat imports:**
|
||||||
|
```bash
|
||||||
|
grep -rnE "^from paths import|^from runtime import|^import site_|^import state_store$|^import expected_undelivered$" inbound_verify || echo "no stale flat imports OK"
|
||||||
|
```
|
||||||
|
Expected: `no stale flat imports OK`.
|
||||||
|
|
||||||
|
**12e. paths anchor points at project root + DB still connects:**
|
||||||
|
```bash
|
||||||
|
.venv/Scripts/python.exe -c "from inbound_verify.paths import BASE_DIR; print('BASE_DIR', BASE_DIR)"
|
||||||
|
.venv/Scripts/python.exe -c "from inbound_verify.store import _connect, _load_pg_config; c=_load_pg_config(); conn=_connect(c['dbname']); print('DB OK', conn.info.server_version); conn.close()"
|
||||||
|
```
|
||||||
|
Expected: `BASE_DIR` prints the `InboundVerify` project root (the dir containing `config.yaml`); `DB OK <pg version>`.
|
||||||
|
|
||||||
|
> If 12b fails with ModuleNotFoundError for a site module, run `.venv/Scripts/python.exe -m pip install -e .` again (editable finder refresh) and retry. If 12d still shows a line, that import was missed — rewrite it per Step 4/8 rules.
|
||||||
|
|
||||||
|
- [ ] **Step 13: Commit (only after user confirms)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A inbound_verify
|
||||||
|
git commit -m "refactor: move flat modules into inbound_verify package (Tier 1, behavior-identical)
|
||||||
|
|
||||||
|
- relocate 12 root .py into inbound_verify/ (sites/, cli/ subpackages)
|
||||||
|
- rewrite all internal imports to package-qualified
|
||||||
|
- fix paths.py BASE_DIR to anchor at project root
|
||||||
|
- add main() entry wrappers; register console_scripts
|
||||||
|
- drop site_ prefix on site modules; keep state_store/expected_undelivered names
|
||||||
|
|
||||||
|
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||||
|
```
|
||||||
|
> Do NOT commit until the user says to. This commit is the **safety baseline**; the manual end-to-end test (spec §7 gate) runs against this state before any Tier 2/Tier 3 work.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Docs sync (README + CLAUDE.md)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `README.md` (§二 directory tree, §三 env prep, §五 run)
|
||||||
|
- Modify: `CLAUDE.md` (常用命令 section)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: the completed package from Task 2 (docs must describe the real new layout/commands).
|
||||||
|
- Produces: documentation matching the new invocation model. No code impact.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update README §二 directory tree**
|
||||||
|
|
||||||
|
Replace the tree block (README lines ~33–49) with the actual new layout:
|
||||||
|
```
|
||||||
|
InboundVerify/
|
||||||
|
├── pyproject.toml # 打包 + 依赖 + console_scripts
|
||||||
|
├── inbound_verify/ # 源码包
|
||||||
|
│ ├── paths.py runtime.py state_store.py expected_undelivered.py store.py
|
||||||
|
│ ├── sites/ shunxin / baishi / zto / yunda / anneng
|
||||||
|
│ └── cli/ router(交互菜单)/ server(FastAPI 服务)
|
||||||
|
├── config.example.yaml / config.yaml
|
||||||
|
├── schema.sql
|
||||||
|
├── requirements.txt # pyproject 的静态镜像
|
||||||
|
├── downloads/ output/ state/
|
||||||
|
└── docs/
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update README §三 env prep — add editable install**
|
||||||
|
|
||||||
|
In the env-prep command block (README lines ~58–71), after `pip install -r requirements.txt`, add:
|
||||||
|
```bash
|
||||||
|
# 4. 以可编辑模式安装本包(注册 inbound-verify 等命令)
|
||||||
|
pip install -e .
|
||||||
|
```
|
||||||
|
(renumber the subsequent `cp config.example.yaml config.yaml` step).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update README §五 run — new commands**
|
||||||
|
|
||||||
|
Replace `python main_router.py` with:
|
||||||
|
```bash
|
||||||
|
# 交互菜单(任选其一)
|
||||||
|
python -m inbound_verify.cli.router
|
||||||
|
# 或装包后:inbound-verify
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Update CLAUDE.md 常用命令**
|
||||||
|
|
||||||
|
In the 常用命令 section, change every `.venv/Scripts/python.exe <module>.py` to the package form:
|
||||||
|
- `main_router.py` → `python -m inbound_verify.cli.router` (or `inbound-verify`)
|
||||||
|
- `server.py` → `python -m inbound_verify.cli.server` (or `inbound-verify-server`)
|
||||||
|
- `db_store.py createdb|init|ingest|all` → `python -m inbound_verify.store createdb|init|ingest|all` (or `inbound-verify-db ...`)
|
||||||
|
- `site_anneng.py expected|actual` → `python -m inbound_verify.sites.anneng expected|actual`
|
||||||
|
- `black`/`py_compile` targets → package paths (e.g. `-m black inbound_verify`)
|
||||||
|
|
||||||
|
Add a one-liner near the top of that section: `首次/拉取新代码后需 .venv/Scripts/python.exe -m pip install -e .`。
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify docs render + commands are real**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -n "main_router.py\|python server.py\|python db_store.py\|site_anneng.py\|site_shunxin" README.md CLAUDE.md || echo "no stale old-path commands OK"
|
||||||
|
```
|
||||||
|
Expected: `no stale old-path commands OK` (every old invocation updated).
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit (only after user confirms)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add README.md CLAUDE.md
|
||||||
|
git commit -m "docs: update README and CLAUDE.md for package layout and console_scripts
|
||||||
|
|
||||||
|
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review (completed)
|
||||||
|
|
||||||
|
- **Spec coverage:** spec §3 (layout) → Task 1+2; §4 (paths anchor) → Task 2 Step 2; §5 (import rules) → Task 2 Steps 3–9; §6 (entry/packaging) → Task 1 Step 2 + Task 2 Steps 6/8/9; §7 (verification gate) → Task 2 Step 12; §10 (docs) → Task 3. All covered.
|
||||||
|
- **Placeholder scan:** none — every step has exact code or an exact command with expected output. The 28 call-site rewrites are given as a deterministic prefix-drop rule + enumerated line numbers + verification grep (complete, not a placeholder).
|
||||||
|
- **Type/name consistency:** kept-module names (`state_store`, `expected_undelivered`, `runtime`, `paths`) used consistently across all import rewrites and call sites; renamed entries (`store`, `cli/router`, `cli/server`) consistent with pyproject `[project.scripts]`. `main()` signature consistent across router/server/store and console_scripts.
|
||||||
274
docs/superpowers/specs/2026-07-23-package-restructure-design.md
Normal file
274
docs/superpowers/specs/2026-07-23-package-restructure-design.md
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
# InboundVerify 包化重构设计
|
||||||
|
|
||||||
|
- **日期**:2026-07-23
|
||||||
|
- **方案**:C(规范)—— `pyproject.toml` + `[project.scripts]` + 根级包 `inbound_verify/`,根目录不留 `.py` 薄壳
|
||||||
|
- **力度**:彻底(建包 + 定点小改进 + 大文件拆分),但**大文件拆分(Tier 3)前置手动测试闸门**
|
||||||
|
- **状态**:已与用户对齐,待 spec 评审
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景与目标
|
||||||
|
|
||||||
|
当前 13 个 Python 模块(共约 6500 行)全部平铺在仓库根目录,随脚本量增长结构混乱。本设计将其重组为一个规范的、可 `pip install -e .` 安装的 Python 包。
|
||||||
|
|
||||||
|
**目标**
|
||||||
|
|
||||||
|
1. 扁平脚本 → `inbound_verify/` 包(`sites/`、`cli/` 两个子包,其余平铺包根,避免一层只放一两个文件的过度嵌套)。
|
||||||
|
2. 全部内部 import 改为包内绝对引用。
|
||||||
|
3. `pyproject.toml` 打包,`[project.scripts]` 暴露命令入口;**根目录不留 `.py` 薄壳**(规范要求)。
|
||||||
|
4. 顺手抽离共享配置、消重(定点小改进,Tier 2)。
|
||||||
|
5. 大文件拆分作为**最后、可选、风险隔离**的阶段(Tier 3),且必须先过手动测试闸门。
|
||||||
|
|
||||||
|
**已确认约束**
|
||||||
|
|
||||||
|
- InboundVerify 是 git **子模块**;改动在子模块内提交,父仓库 `LogisticsHubIPA` 仅跟踪子模块指针。
|
||||||
|
- **不加测试套件**(用户决定);验证靠 `compileall` + 导入冒烟 + grep 查残留引用 + 手动端到端测试。
|
||||||
|
- 入口走规范:无根薄壳;一次性 `pip install -e .` 后用命令或 `python -m` 启动。
|
||||||
|
- **不做 src/ 布局**(内部工具、不发 PyPI、无测试,边际价值有限)。
|
||||||
|
- 本仓库约定:**不自动提交 / 不自动推送**;改动等用户明确说"提交"再 commit/push(本约定覆盖 brainstorming 默认的"写完即提交")。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 现状分析
|
||||||
|
|
||||||
|
### 2.1 依赖分层(自底向上)
|
||||||
|
|
||||||
|
```
|
||||||
|
paths ← 万物之基(被所有模块 import)
|
||||||
|
state_store ← SQLite 状态持久化
|
||||||
|
expected_undelivered ← 离线比对(兼职存站点/文件名/列映射共享配置,被 db_store 复用 —— 耦合点)
|
||||||
|
site_*.py(×5) ← 各依赖 paths + state_store
|
||||||
|
runtime ← 编排核心(启动/派发/心跳,依赖上面全部)
|
||||||
|
main_router / server / db_store ← 三入口(均有 __main__/CLI)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 关键发现
|
||||||
|
|
||||||
|
1. **`paths.py` 是地雷**:`BASE_DIR = dirname(abspath(__file__))`——paths.py 在哪,根就在哪。搬进子目录后必须改为上跳一级,否则 `downloads/`、`output/`、`config.yaml`、`state/state.db`、`schema.sql` 全部跑偏。
|
||||||
|
2. **import 全是扁平顶层**(`import site_shunxin`、`from paths import …`),且**没有任何地方按字符串名引用模块**(`TASK_HANDLERS` 用函数引用、`dispatch_task` 用 site/kind 字典),改写纯机械。
|
||||||
|
3. **重复代码**:`with_retry`、`_remove_if_exists` 在 5 个站点逐字重复(CLAUDE.md 注明"现阶段刻意不优化结构")。
|
||||||
|
4. **路径常量重复**:`expected_undelivered.py` 自带 `BASE/DOWNLOADS/OUTPUT`,与 `paths.py` 重复。
|
||||||
|
5. **耦合**:`db_store` 为读站点配置而 `import expected_undelivered`(为了配置而依赖整个比对引擎)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 目标目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
InboundVerify/
|
||||||
|
├── pyproject.toml # 新增:打包 + 依赖 + console_scripts
|
||||||
|
├── config.example.yaml
|
||||||
|
├── config.yaml # 留根(gitignored)
|
||||||
|
├── schema.sql # 留根(store 按绝对路径读)
|
||||||
|
├── requirements.txt # 保留为静态镜像(pyproject 为准)
|
||||||
|
├── README.md / CLAUDE.md / .gitignore
|
||||||
|
├── downloads/ output/ state/ # 运行时数据,留根
|
||||||
|
├── docs/
|
||||||
|
└── inbound_verify/ # ← 包
|
||||||
|
├── __init__.py
|
||||||
|
├── __main__.py # 可选:python -m inbound_verify → 交互菜单
|
||||||
|
├── paths.py # 锚点改为指向项目根(§4)
|
||||||
|
├── config.py # 新增(Tier2):集中 load_config()
|
||||||
|
├── domain.py # 新增(Tier2):从 expected_undelivered 抽出的共享站点/文件/列映射
|
||||||
|
├── runtime.py # ← runtime.py(编排核心,整体保留不拆)
|
||||||
|
├── state_store.py # ← state_store.py(保留名,仅搬运)
|
||||||
|
├── expected_undelivered.py # ← 搬运;Tier2 改名 compare.py + 抽 domain.py
|
||||||
|
├── store.py # ← db_store.py(叶子,改名无 churn)
|
||||||
|
├── sites/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── shunxin.py baishi.py zto.py yunda.py # ← site_*.py(去 site_ 前缀)
|
||||||
|
│ └── anneng.py # ← site_anneng.py(Tier3 可选再拆成子包)
|
||||||
|
└── cli/
|
||||||
|
├── __init__.py
|
||||||
|
├── router.py # ← main_router.py(叶子,改名无 churn)
|
||||||
|
└── server.py # ← server.py(叶子)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.1 搬运映射表(全部 `git mv` 保历史)
|
||||||
|
|
||||||
|
| 现在 | Tier 1 后 | 调用点改动 |
|
||||||
|
|---|---|---|
|
||||||
|
| `paths.py` | `inbound_verify/paths.py` | 无(仅改 import 行 + 锚点) |
|
||||||
|
| `runtime.py` | `inbound_verify/runtime.py` | 无(保留名) |
|
||||||
|
| `state_store.py` | `inbound_verify/state_store.py` | 无(**保留名**,仅改 import 行) |
|
||||||
|
| `expected_undelivered.py` | `inbound_verify/expected_undelivered.py` | 无(Tier1 保留名;Tier2 改名 compare) |
|
||||||
|
| `db_store.py` | `inbound_verify/store.py` | 无(叶子,无人 import) |
|
||||||
|
| `main_router.py` | `inbound_verify/cli/router.py` | 无(叶子) |
|
||||||
|
| `server.py` | `inbound_verify/cli/server.py` | 无(叶子) |
|
||||||
|
| `site_{shunxin,baishi,zto,yunda,anneng}.py` | `inbound_verify/sites/{…}.py`(去 `site_` 前缀) | 改 `runtime` + `main_router` 调用点;**grep 查残留引用兜底** |
|
||||||
|
|
||||||
|
> **命名策略(降低无测试下的风险)**:Tier 1 只对**被多处裸名引用**的模块(`state_store`、`expected_undelivered`、`runtime`、`paths`)**保留原名**,做到"仅改 import 行、零调用点改动";只对**叶子入口**(`db_store`/`main_router`/`server`)和**站点文件**(去 `site_` 前缀)做改名。`expected_undelivered` 的改名(`→ compare.py`)推迟到 Tier 2——那时本就要为抽 `domain.py` 重做该文件及其调用方,把改名 churn 并入一个已经在改的批次。这样 Tier 1 的纯搬运可被 `compileall` + 导入冒烟 + grep 充分验证,不依赖手测。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. `paths.py` 锚点修正(唯一地雷,必须改对)
|
||||||
|
|
||||||
|
包搬进 `inbound_verify/` 后,`__file__` 多降一级。要把 `BASE_DIR` 继续指回项目根:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# inbound_verify/paths.py
|
||||||
|
import os
|
||||||
|
# __file__ = .../InboundVerify/inbound_verify/paths.py
|
||||||
|
# 上两级 = .../InboundVerify (= 项目根,config.yaml/downloads/schema.sql 所在)
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
DOWNLOAD_DIR = os.path.join(BASE_DIR, "downloads")
|
||||||
|
OUTPUT_DIR = os.path.join(BASE_DIR, "output")
|
||||||
|
CONFIG_PATH = os.path.join(BASE_DIR, "config.yaml")
|
||||||
|
STATE_DB_PATH = os.path.join(BASE_DIR, "state", "state.db")
|
||||||
|
```
|
||||||
|
|
||||||
|
editable 安装不会移动文件,`__file__` 仍指向源码树,两级上跳稳定指向项目根。`store.py` 的 `SCHEMA_PATH = join(BASE_DIR, "schema.sql")` 自动跟着对。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. import 改写规则
|
||||||
|
|
||||||
|
```python
|
||||||
|
from paths import ... → from inbound_verify.paths import ...
|
||||||
|
import state_store → from inbound_verify import state_store # 保留名,调用点 state_store.X 不变
|
||||||
|
from runtime import (...) → from inbound_verify.runtime import (...)
|
||||||
|
import site_shunxin → from inbound_verify.sites import shunxin # 5 站同理,调用点改 shunxin.X
|
||||||
|
import expected_undelivered → from inbound_verify import expected_undelivered # Tier1 保留名
|
||||||
|
import expected_undelivered as eu → from inbound_verify import expected_undelivered as eu # eu.X 不变
|
||||||
|
```
|
||||||
|
|
||||||
|
(Tier 2 后,`expected_undelivered` 改名 `compare`,`db_store` 的共享配置改 `from inbound_verify import domain`。)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 入口与打包
|
||||||
|
|
||||||
|
### 6.1 `main()` 包装(根目录不留薄壳)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# inbound_verify/cli/server.py 末尾
|
||||||
|
def main():
|
||||||
|
uvicorn.run("inbound_verify.cli.server:app", host="0.0.0.0", port=8000)
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
> `uvicorn.run` 改传**字符串** `"inbound_verify.cli.server:app"`(规范写法;不开 `reload`/`workers` 时仍在当前进程 import,行为等价)。`router.py` 套 `main()` 调 `run_multi_site_daemon()`;`store.py` 已有 `_cli`,改名为 `main`。
|
||||||
|
|
||||||
|
### 6.2 `pyproject.toml`(骨架)
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=68"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "inbound-verify"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = ["pandas>=2.0.0", "playwright>=1.40.0", "openpyxl>=3.1.0",
|
||||||
|
"PyYAML>=6.0", "websocket-client>=1.0.0", "fastapi>=0.110.0",
|
||||||
|
"uvicorn>=0.27.0", "apscheduler>=3.10.0", "psycopg[binary]>=3.1"]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
inbound-verify = "inbound_verify.cli.router:main"
|
||||||
|
inbound-verify-server = "inbound_verify.cli.server:main"
|
||||||
|
inbound-verify-db = "inbound_verify.store:main"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
include = ["inbound_verify*"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 装包后三种启动方式(都汇到同一个 `main()`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -e . # 一次性
|
||||||
|
inbound-verify-server # 命令
|
||||||
|
python -m inbound_verify.cli.server # 兜底
|
||||||
|
uvicorn inbound_verify.cli.server:app # 生产最标准(端口/worker 命令行控)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 执行顺序与验证闸门(为"无测试"量身)
|
||||||
|
|
||||||
|
**安全原则**:Tier 1 必须先独立完成并验证为**行为等价**,再做任何动逻辑的改动。每段后必验证。验证职责分清——自动化部分我跑,端到端部分需你跑(真实登录/凭据/安能 Electron 只有你能提供)。
|
||||||
|
|
||||||
|
```
|
||||||
|
Tier 1 纯搬运(行为零改变)
|
||||||
|
├─ 我的自动验证: ① compileall 全过 ② 导入冒烟 ③ grep 查残留 site_ 引用 ④ DB 连通
|
||||||
|
└─ [等用户说"提交"] commit ← 安全基线
|
||||||
|
Tier 2 domain 抽取 / config 集中 / 路径消重 / expected_undelivered 改名 compare
|
||||||
|
├─ 我的自动验证: 同上
|
||||||
|
└─ [等用户说"提交"] commit
|
||||||
|
═══════ 手动测试闸门(你跑)═══════
|
||||||
|
全链路端到端:
|
||||||
|
起 inbound-verify → 登录 5 站(顺心双账号)→ 各站下载 → 比对(菜单 9)
|
||||||
|
→ inbound-verify-db ingest → 核对 output/应到未到数据.xlsx 与 PostgreSQL 三张表
|
||||||
|
通过? 否 → 修到通过
|
||||||
|
是 ↓
|
||||||
|
Tier 3 此时再定:anneng 拆不拆 / with_retry 抽不抽 base.py
|
||||||
|
└─ 每项后重跑我的自动验证,你按需复测
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.1 自动验证命令清单(我每个 Tier 后都跑)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/Scripts/python.exe -m compileall inbound_verify # ① 语法
|
||||||
|
.venv/Scripts/python.exe -c "import inbound_verify.cli.router, \
|
||||||
|
inbound_verify.cli.server, inbound_verify.store, inbound_verify.runtime, \
|
||||||
|
inbound_verify.state_store" # ② 导入冒烟(三个入口会传递导入 sites/expected_undelivered 等;
|
||||||
|
# Tier2 改名后 expected_undelivered → compare,无需单独显式导入)
|
||||||
|
grep -rn "site_shunxin\|site_baishi\|site_zto\|site_yunda\|site_anneng" \
|
||||||
|
inbound_verify || echo "无残留 site_ 引用 ✓" # ③ 改名残留
|
||||||
|
.venv/Scripts/python.exe -c "from inbound_verify.store import _connect, _load_pg_config; \
|
||||||
|
c=_load_pg_config(); conn=_connect(c['dbname']); print('DB OK', conn.info.server_version)" # ④ DB
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 各 Tier 内容
|
||||||
|
|
||||||
|
### Tier 1 — 纯搬运(行为零改变)
|
||||||
|
|
||||||
|
1. 建包骨架 + 空白 `__init__.py`(`sites/`、`cli/`)。
|
||||||
|
2. `git mv` §3.1 表中所有文件到新位置。
|
||||||
|
3. 按 §5 规则机械改写全部 import;站点改名后改 `runtime` + `main_router` 调用点(`shunxin.shunxin_expected_download(...)` 等)。
|
||||||
|
4. 改 `paths.py` 锚点(§4)。
|
||||||
|
5. 三个入口加 `main()`(`store` 的 `_cli` 改名 `main`)。
|
||||||
|
6. 写 `pyproject.toml`,`.venv` 内 `pip install -e .`。
|
||||||
|
7. 跑 §7.1 四项自动验证。
|
||||||
|
8. 等用户说"提交"→ commit(子模块内)。
|
||||||
|
|
||||||
|
### Tier 2 — 定点小改进(每项后跑 §7.1)
|
||||||
|
|
||||||
|
- **抽 `domain.py`**:把 `expected_undelivered.py` 里的 `STATIONS`/`_site_cfg`/`ALL_REPORT_SITES`/`SITE_UNDELIVERED_FILE`/`BAISHI_FILE`/`BAISHI_COLUMNS`/`arrived_pieces_*` 移到 `inbound_verify/domain.py`;`store.py` 从依赖整个比对引擎改为 `from inbound_verify import domain`。**接缝最干净、收益明确,推荐做。**
|
||||||
|
- **`expected_undelivered.py` → `compare.py`** 改名,更新调用方(`store` 的 `as eu`、`runtime`、`cli/router`)。
|
||||||
|
- **加 `config.py`**:集中 `load_config()`(带缓存),各站点把自家的 `yaml.safe_load(open(CONFIG_PATH))` 换掉。
|
||||||
|
- **消重路径常量**:`compare.py` 自带那份 `BASE/DOWNLOADS/OUTPUT` 改成引用 `paths.py`。
|
||||||
|
|
||||||
|
### Tier 3 — 大文件拆分(手动测试闸门之后;具体决策推迟到闸门)
|
||||||
|
|
||||||
|
- **`anneng.py` → `sites/anneng/` 子包**(`cdp.py`/`nav.py`/`expected.py`/`actual.py`):接缝分层清晰,但共享可变状态多(`CDP_PORT` 全局被 `set_cdp_port` 改、各种 URL hint、僵尸 tab 逻辑),CLAUDE.md 标注为脚gun。**倾向不拆**(1375 行虽大但是内聚的 CDP 驱动,强拆无测试网兜底风险高)——最终在闸门后定。
|
||||||
|
- **抽 `sites/base.py`**:`with_retry` / `_remove_if_exists` 在 5 站逐字重复;原作者在 CLAUDE.md 写"现阶段刻意不优化结构"。**抽不抽,闸门后定。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 待决策(推迟到手动测试闸门)
|
||||||
|
|
||||||
|
| 决策 | 默认倾向 | 何时定 |
|
||||||
|
|---|---|---|
|
||||||
|
| `anneng.py` 拆不拆子包 | **不拆** | 手动测试通过后 |
|
||||||
|
| `with_retry`/`_remove_if_exists` 抽 `base.py` | 待定 | 手动测试通过后 |
|
||||||
|
| `requirements.txt` 留还是删 | **留静态镜像**(注明 pyproject 为准) | 随时可改 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 文档同步(Tier 1 必做)
|
||||||
|
|
||||||
|
- **README.md**:第二节目录树、第三节环境准备(加 `pip install -e .`)、第五节运行(改新命令)。
|
||||||
|
- **CLAUDE.md**:常用命令段全部改 `python -m inbound_verify…` / `inbound-verify…`,补 `pip install -e .` 与 `playwright install chromium`、`black`/`py_compile` 的包内路径写法。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 范围外
|
||||||
|
|
||||||
|
- **不做** src/ 布局。
|
||||||
|
- **不做** 给站点流程加 mock 测试。
|
||||||
|
- **不自动** commit/push(等用户明确指示)。
|
||||||
|
- 父仓库 `LogisticsHubIPA` 的子模块指针更新,是父仓库的单独一步,不在本 spec 范围。
|
||||||
Reference in New Issue
Block a user