docs: 记录工程化改造(uv + ruff + pyright)的决策与踩过的坑

日志补记下半场:三件套的落地细节、行宽为何定 100、PTH 为何不开、pyright 两个关键开关;新增三条可复用环境坑(write_text 会把 LF 变 CRLF、同文件多次 Edit 互相覆盖、git status 与 git diff 不一致的判别)。

项目长期约定同步更新:运行方式改为 uv 单一可信来源,新增代码风格约定与质量门禁命令。
This commit is contained in:
snowy-yang committed 2026-09-18 21:28:20 +08:00
1 parent c414db4217
commit 52004571c8
2 files changed
+83 -6

No files matched your search

+56
View File
@@ -47,3 +47,59 @@ TUI 只在用户玩时作为客户端、用户也能用 HTTP 调用、支持多
(112 列 / 76 列都正常,头部显示 `牧场名 @ 服务器`)。 (112 列 / 76 列都正常,头部显示 `牧场名 @ 服务器`)。
- 依赖装在隔离 venv:`C:/Users/Administrator/.workbuddy/binaries/python/envs/default/Scripts/python.exe` - 依赖装在隔离 venv:`C:/Users/Administrator/.workbuddy/binaries/python/envs/default/Scripts/python.exe`
(fastapi 0.141.1 / uvicorn 0.53 / websockets 17.1 / httpx 0.28)。 (fastapi 0.141.1 / uvicorn 0.53 / websockets 17.1 / httpx 0.28)。
---
# 2026-09-18(下半场)· 工程化改造:uv + ruff + pyright
## 需求
"完成 uv 来管理,ruff pyright 来完善"。目标是让项目达到可维护的工程标准:
依赖有单一可信来源、锁文件可复现、lint 零告警、类型检查零错误,且测试仍全绿。
## 落地内容
- **uv 作为单一口径**:新增 `pyproject.toml`(唯一真实来源)+ `uv.lock`(提交,保证可复现)
+ `.python-version`(3.13)。`[tool.uv] package = false`(这是应用不是库)。
`requirements.txt` 改为 `uv export --no-dev --no-hashes` 的产物,仅供不用 uv 的人参考。
- **ruff**:474 项告警 → 0。规则集 E/W/F/I/UP/B/C4/SIM/RET/PERF/RUF。
line-length 定为 **100**(实测 >88 有 122 行、>100 只有 18 行,88 会造成大面积无意义重排)。
**刻意不开 PTH**:路径在本项目是"字符串身份"(要喂给 subprocess / 塞进 JSON / 打印),
不是文件系统操作对象,转成 Path 反而要在边界反复 str()。
**忽略 PERF203**:剩下 2 处 try/except 在循环里是刻意的"单项容错",不是性能疏忽。
- **pyright**:15 errors / 10 warnings → 0。basic 模式。
`pythonPlatform = "All"`(代码里有 termios/msvcrt 双平台分支,只看单平台会把另一侧误报成
"模块没有该属性");`reportUnsupportedDunderAll = false`(`__init__.py` 惰性导出属正常写法)。
真实修掉的类型问题:`keys.py` 的 `sys.stdin.fileno()` None 收窄、`app.py` lifespan 需
`@asynccontextmanager` + `AsyncIterator[None]`、`actions.py` 的 `isinstance` 冗余告警。
## 关键决策
- **先建 git 仓库再动手**:400+ 处自动改动前先做基线快照,分阶段提交以便逐段回滚。
- 提交节奏:`a7725f8` 基线 → `c7c8a63` 统一 LF → `fcd4cb4` ruff 清零 →
`d7a7107` pyright 清零 + README 工程化章节 → `c414db4` 提示语统一。
- **`uv.lock` 必须提交**,它是可复现构建的依据,不是缓存(已在 `.gitignore` 里显式说明)。
- 用户可见提示统一为 `uv run ...`。这不只是措辞:依赖装在 `.venv`,
用户照抄 `python main.py serve` 会因缺 fastapi 直接失败。
但 `main.py:26` 与 `README:49` 的两处"没装 uv?"说明**刻意保留** `python main.py` 写法。
## 踩过的坑(重要,可复用)
6. **同一文件多次 Edit 会被静默覆盖**:一条消息里对同一文件发多个 Edit,只有部分落地,
表现为随后 ruff 报出 `F821 Undefined name contextlib`(import 那处没写上)等"幽灵错误"。
解决:改用**幂等 Python 补丁脚本**——整文件读入、精确匹配替换、逐条报告 `ALREADY/APPLIED/MISS`,
跑完即删。本轮 34 + 6 + 15 + 23 + 4 处替换全部这样完成。
注意脚本里判"已应用"要用 `if new in text` 而不是比对两行文本,否则会出现
"new 是 old 子串"导致的**假 ALREADY**(F841 那次就中过招)。
7. **`pathlib.write_text()` 在 Windows 上会把 `\n` 转成 `\r\n`**,等于偷偷破坏"统一 LF"的约定。
症状:`git commit` 冒出一串 `CRLF will be replaced by LF` 警告。
解决:改行尾类操作一律用 `read_bytes()/write_bytes()`(二进制,不经转换),
或 `write_text(..., newline="\n")`。**这是本项目写文件的默认姿势。**
8. **`git status` 报 M 但 `git diff` 为空 = 索引 stat 缓存过期**(文件被重写导致 mtime 变了),
不是真差异。用 `git cat-file -p HEAD:<file>` 与工作区做字节比对可证伪;
`git update-index --refresh` 或 `git add -A`(内容相同时等于无操作)即恢复干净。
## 验证
- `uv run ruff check .` → All checks passed;`uv run ruff format --check .` → 26 files already formatted。
- `uv run pyright` → 0 errors, 0 warnings, 0 informations。
- `uv run python -m unittest discover -s tests` → Ran 97 tests, OK。
- `uv lock --check` → 一致(28 包);`uv run main.py --help` 正常。
- 端到端复验:`uv run main.py serve --daemon --port 8796` 起服 →
status / actions / do(plant, buy_animal, feed) / farms / stop 全通,优雅停机并落盘。
- 行尾复核:全部 `.py` 与 `README.md` 均 0 个 CRLF,blob 与工作区逐字节相同。
+27 -6
View File
@@ -4,13 +4,17 @@
终端农场经营游戏 + 小型多人游戏服务器(v2.0)。游戏跑在服务端进程里, 终端农场经营游戏 + 小型多人游戏服务器(v2.0)。游戏跑在服务端进程里,
TUI 是纯客户端,同时开放 HTTP + WebSocket 接口给 AI/脚本调用(服务端不内置 AI)。 TUI 是纯客户端,同时开放 HTTP + WebSocket 接口给 AI/脚本调用(服务端不内置 AI)。
## 运行方式 ## 运行方式(v2.0 起由 uv 统一管理)
- 依赖放隔离 venv:`C:/Users/Administrator/.workbuddy/binaries/python/envs/default/Scripts/python.exe` - **单一可信来源是 `pyproject.toml`**,`.venv/` 由 uv 创建;`uv.lock` **要提交**(可复现构建依据)。
(服务端需要 fastapi + uvicorn[standard] + websockets;客户端与 TUI 只用标准库) 加/删依赖走 `uv add` / `uv remove`,不要手工编辑依赖表,也不要再列 `pip install`。
- 服务端:`python main.py serve --daemon`,网页 <http://127.0.0.1:8765/>,文档 /docs - 所有命令带 `uv run ` 前缀:`uv run main.py serve --daemon`,网页 <http://127.0.0.1:8765/>,文档 /docs
- 玩:`python main.py play --farm 名字`;单机:`python main.py solo` - 玩:`uv run main.py play --farm 名字`;单机:`uv run main.py solo`
- AI/脚本入口:`main.py actions | do <动作> '<JSON>' | batch | state --json | watch | bot` - AI/脚本入口:`main.py actions | do <动作> '<JSON>' | batch | state --json | watch | bot`
- 测试:`python -m unittest discover -s tests`(97 项;不需要 FastAPI 时只跑 test_engine.py) - 测试:`uv run python -m unittest discover -s tests`(97 项)
- 质量门禁(改动后都应跑一遍):
`uv run ruff check .` / `uv run ruff format .` / `uv run pyright`
- 不用 uv 的人:`requirements.txt` 是 `uv export` 的产物,只读、不要手工改。
README 与 main.py 顶部保留了一段"没装 uv?把 `uv run ` 去掉即可"的兜底说明,**别删**。
## 架构约定(改动时必须遵守) ## 架构约定(改动时必须遵守)
- **分层**:`farm.engine` 只管规则,不认识网络/终端;`farm.tui` 不认识 socket; - **分层**:`farm.engine` 只管规则,不认识网络/终端;`farm.tui` 不认识 socket;
@@ -32,6 +36,23 @@ TUI 是纯客户端,同时开放 HTTP + WebSocket 接口给 AI/脚本调用(
`open_url()`(回环直连),否则会拿到 502 并误判为"服务器没起来"。 `open_url()`(回环直连),否则会拿到 502 并误判为"服务器没起来"。
- Windows 下守护进程的 pid 必须由**服务进程自己**写(见 `farm/server/__main__.py`), - Windows 下守护进程的 pid 必须由**服务进程自己**写(见 `farm/server/__main__.py`),
父进程的 `Popen().pid` 不可信。 父进程的 `Popen().pid` 不可信。
- **写文件必须指定行尾**:Windows 上 `pathlib.write_text()` 会把 `\n` 转成 `\r\n`,
破坏本仓库"统一 LF"的约定(`.gitattributes` 已声明 `eol=lf`)。
用脚本改文件时一律 `read_bytes()/write_bytes()`,或 `write_text(..., newline="\n")`。
- **同一文件在一条消息里多次 Edit 会被静默覆盖**(只落地一部分,随后报出"幽灵"错误)。
批量改同一文件时改用**幂等 Python 补丁脚本**:整文件读入 → 精确匹配替换 →
逐条打印 `ALREADY/APPLIED/MISS`,跑完即删。判断"已应用"要 `if new in text`,
不要比对两行文本(会出现 new 是 old 子串的假阳性)。
- `git status` 报 M 但 `git diff` 为空,是索引 stat 缓存过期,不是真差异;
`git add -A`(内容相同时等于无操作)即恢复干净。
## 代码风格约定(ruff / pyright 已固化在 pyproject.toml)
- 行宽 **100**;字符串统一双引号;`isort` 把 `farm` 视为第一方。
- 类型标注用 PEP 585/604 写法(`dict[str, int]` / `X | None`),不要 `Dict` / `Optional`。
- 吞异常要显式:`contextlib.suppress(...)` 优先于 `try/except/pass`。
- 异常链:转换后重抛用 `raise ... from exc`;有意隐藏原始上下文用 `from None`(**不要省略**,会触发 B904)。
- 路径保持 `str`(不要为了 lint 而转 `Path`);`farm/__init__.py` 的惰性 `__all__` 导出是刻意设计。
- 改完代码的标准动作:`uv run ruff format . && uv run ruff check . && uv run pyright && uv run python -m unittest discover -s tests`
## 相关文档 ## 相关文档
- 项目入口文档:`README.md`(接口表、WS 协议、边界处理、设计取舍全在里面) - 项目入口文档:`README.md`(接口表、WS 协议、边界处理、设计取舍全在里面)