# 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 范围。