Files
InboundVerify/docs/superpowers/specs/2026-07-23-package-restructure-design.md
Misaka_Company a07b9b435b 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>
2026-07-23 12:21:45 +08:00

14 KiB
Raw Blame History

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.yamlstate/state.dbschema.sql 全部跑偏。
  2. import 全是扁平顶层(import site_shunxinfrom 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_storeexpected_undeliveredruntimepaths)保留原名,做到"仅改 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.pySCHEMA_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.pymain()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 — 纯搬运(行为零改变)

  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,.venvpip 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.pycompare.py 改名,更新调用方(storeas euruntimecli/router)。
  • config.py:集中 load_config()(带缓存),各站点把自家的 yaml.safe_load(open(CONFIG_PATH)) 换掉。
  • 消重路径常量:compare.py 自带那份 BASE/DOWNLOADS/OUTPUT 改成引用 paths.py

Tier 3 — 大文件拆分(手动测试闸门之后;具体决策推迟到闸门)

  • anneng.pysites/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_existsbase.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 chromiumblack/py_compile 的包内路径写法。

11. 范围外

  • 不做 src/ 布局。
  • 不做 给站点流程加 mock 测试。
  • 不自动 commit/push(等用户明确指示)。
  • 父仓库 LogisticsHubIPA 的子模块指针更新,是父仓库的单独一步,不在本 spec 范围。