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

275 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 范围。