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>
14 KiB
InboundVerify 包化重构设计
- 日期:2026-07-23
- 方案:C(规范)——
pyproject.toml+[project.scripts]+ 根级包inbound_verify/,根目录不留.py薄壳 - 力度:彻底(建包 + 定点小改进 + 大文件拆分),但大文件拆分(Tier 3)前置手动测试闸门
- 状态:已与用户对齐,待 spec 评审
1. 背景与目标
当前 13 个 Python 模块(共约 6500 行)全部平铺在仓库根目录,随脚本量增长结构混乱。本设计将其重组为一个规范的、可 pip install -e . 安装的 Python 包。
目标
- 扁平脚本 →
inbound_verify/包(sites/、cli/两个子包,其余平铺包根,避免一层只放一两个文件的过度嵌套)。 - 全部内部 import 改为包内绝对引用。
pyproject.toml打包,[project.scripts]暴露命令入口;根目录不留.py薄壳(规范要求)。- 顺手抽离共享配置、消重(定点小改进,Tier 2)。
- 大文件拆分作为最后、可选、风险隔离的阶段(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 关键发现
paths.py是地雷:BASE_DIR = dirname(abspath(__file__))——paths.py 在哪,根就在哪。搬进子目录后必须改为上跳一级,否则downloads/、output/、config.yaml、state/state.db、schema.sql全部跑偏。- import 全是扁平顶层(
import site_shunxin、from paths import …),且没有任何地方按字符串名引用模块(TASK_HANDLERS用函数引用、dispatch_task用 site/kind 字典),改写纯机械。 - 重复代码:
with_retry、_remove_if_exists在 5 个站点逐字重复(CLAUDE.md 注明"现阶段刻意不优化结构")。 - 路径常量重复:
expected_undelivered.py自带BASE/DOWNLOADS/OUTPUT,与paths.py重复。 - 耦合:
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 继续指回项目根:
# 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 改写规则
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() 包装(根目录不留薄壳)
# 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(骨架)
[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())
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 后都跑)
.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 — 纯搬运(行为零改变)
- 建包骨架 + 空白
__init__.py(sites/、cli/)。 git mv§3.1 表中所有文件到新位置。- 按 §5 规则机械改写全部 import;站点改名后改
runtime+main_router调用点(shunxin.shunxin_expected_download(...)等)。 - 改
paths.py锚点(§4)。 - 三个入口加
main()(store的_cli改名main)。 - 写
pyproject.toml,.venv内pip install -e .。 - 跑 §7.1 四项自动验证。
- 等用户说"提交"→ 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 范围。