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:
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