Files
snowy-yang 47dd818360 docs: 记录规则生效的探针反证过程
复核 ruff/pyright 规则是否真的被加载:排除所有可能覆盖 pyproject 的配置文件,并用故意违规的探针文件确认精确命中。

新知识:X | None 的规则码在新版 ruff 是 UP045 而非 UP007;SIM105 对 try 体含 return 的情况有意不报,所以'没报出来'不等于'规则没启用'。
2026-09-18 21:32:46 +08:00

123 lines
9.5 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.
# 2026-09-18 · SimpleFarm 从单机 TUI 改造为小型多人游戏服务器
## 需求
用户先要求"CLI/TUI 农场游戏 + HTTP/gRPC 接口让 AI 参与";中途改方向:
**放弃 gRPC,改用 FastAPI/aiohttp**,要求:守护进程可后台运行、SSE 或 WS 二选一、
TUI 只在用户玩时作为客户端、用户也能用 HTTP 调用、支持多用户(小型游戏服务器)。
## 最终架构(v2.0)
- 服务端 `farm/server/`:`app.py`(FastAPI REST+WS) / `actions.py`(动作表) /
`session.py`(多牧场会话 + 单一 FarmClock 统一时钟) / `hub.py`(WS 连接池 + 250ms 节流推送) /
`daemon.py`(守护进程) / `webui.py`(内嵌网页客户端)。
- 客户端 `farm/client.py` 的 `RemoteFarm`:与 `FarmEngine` **接口同构**,
所以 `tui.py` 不用改渲染逻辑就能从"本地引擎"切到"远程服务器"。
写操作优先走 WS(带 id 回执),通道类错误自动退回 HTTP;读数靠 WS 推送。
- `farm/bot.py` 规则机器人 + `ai_player.py` 外挂机器人:只调接口,不 import 游戏规则。
- 统一入口 `main.py`:serve/stop/status/logs/play/solo/do/batch/actions/state/log/watch/bot/farms/help。
## 关键决策
- **选 WebSocket 而非 SSE**:这个游戏动作上行和状态下行是同一件业务的两半,
SSE 只能下行,等于要维护两套鉴权/重连。
- **删掉 gRPC**(`farm/api.py` 旧 stdlib HTTP 层、`farm/grpc_server.py`、`proto/`、`tools/gen_proto.py`):
接口主要喂 LLM/脚本,它们只会捏 HTTP+JSON。
- 多牧场模型:一个牧场 = 一个玩家 = 一份存档 + 一个引擎;空闲 30 分钟先落盘再移出内存。
- 动作表 `server/actions.py` 是所有入口(HTTP / WS / CLI / 机器人)的唯一执行通道,参数校验只写一次。
## 踩过的坑(重要,可复用)
1. **本地请求被系统代理截走**:机器上设了 `HTTP_PROXY` 但没有 `no_proxy`,
`urllib` 把发给 127.0.0.1 的请求也交给代理,本机服务器报 `502 upstream connect failed`。
解决:`farm/net.py` 的 `open_url()`,回环地址走 `ProxyHandler({})` 直连。
2. **pid 归属**:起初父进程把 `Popen().pid` 写进 pid 文件,但 Windows 上 venv 的
`python.exe` 可能是转发器,`os.getpid()`(40564)≠ 父进程看到的 pid(20244),`stop` 会杀错进程。
解决:`--pid-file` 传给子进程,子进程在 lifespan 启动(端口已绑定)后写自己的 pid,退出时删除;
`stop` 优先走 HTTP `/api/admin/shutdown` 优雅停机(能落盘),拿不到 pid 也能停。
3. **精简状态会清空视图**:动作回执里的 `state` 是精简版(无 `plots`/`animals`),
客户端若整体替换就会把农田格子清空。解决:`StateView.apply()` 按"含 plots 即整体替换,
否则合并字段",并且 HTTP 动作带 `full=1` / WS 回执带 `snapshot` 让客户端立刻刷新。
4. **`save` 动作必须由会话处理**:`engine.save()` 会写默认路径,多牧场下会互相覆盖;
在 `app.execute_action()` 里把 save/reset 拦截为会话级操作。
5. 本机 Bash 的 coreutils 坏了(`ls`/`head`/`tail`/`grep` 都不存在),
所有查看/清理动作改用 Python 脚本完成;管道会直接让命令失败,**不要用管道**。
## 验证
- 97 项测试全绿(引擎 45 + 服务器 52),含真 uvicorn + 真客户端 + 真 WebSocket 端到端。
- 实测:daemon 启动 → status 正确 pid → actions/do/batch → 两牧场隔离与排行榜 →
WS 收到 welcome/state/result/day → 机器人 120→197 金币 → stop 优雅停机落盘。
- TUI 远程渲染验证:`tools/preview_tui.py --url http://127.0.0.1:8793 --farm alice`
(112 列 / 76 列都正常,头部显示 `牧场名 @ 服务器`)。
- 依赖装在隔离 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)。
---
# 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 与工作区逐字节相同。
## 复核:ruff / pyright 规则是否真的生效(用探针反证)
用户追问"规则你加了吗",做了**正向反证**而不是只回答"写了":
- 先排除覆盖源:不存在 `ruff.toml` / `.ruff.toml` / `pyrightconfig.json` / `setup.cfg` /
`tox.ini` / `.flake8` / `mypy.ini` —— **唯一配置来源就是 pyproject.toml**。
- 写故意违规的探针文件 `tools/_rule_probe.py`(用完即删),实测命中:
ruff → F401 / RUF012 / UP045 / E741 / UP031 / E501(145>100) / SIM105 / SIM102 / B018;
pyright → reportUnusedImport / reportReturnType ×2 / reportArgumentType。
说明 `select`、`line-length=100`、`reportUnusedImport=true` 等**全部真在生效**。
- `ruff check --show-settings` 复核:`linter.line_length = 100`、
`pycodestyle.max_line_length = 100`、`formatter.line_ending = auto`、
`analyze.target_version = 3.10`;启用规则 **263 条**,前缀
[B,C,E,F,I,PERF,RET,RUF,SIM,UP,W] 与 select 的 11 项一一对应。
- 新知识:`X | None` 的规则码在新版 ruff 里是 **UP045**(不是 UP007);
**SIM105 对 try 体里含 `return` 的情况有意不报**(换个朴素用例才命中),
所以"某条规则没报出来"≠"规则没启用",别急着改配置。
- 该手法已补进技能 `python-uv-ruff-pyright-hardening` 的「反证:确认配置真的被加载了」一节。