Files
SimpleFarm/.workbuddy/skills/simplefarm-verify-and-ship/SKILL.md
T
snowy-yang 92b1a628b1 网页端新增 Canvas 二维农场地图
为什么:地块和牲畜原本是响应式卡片列表,信息全但没有"农场"的形状感。
改成一张俯视像素风地图作主视觉:农田按 4 列排成方阵(地块多自动改 6 列),
土壤颜色随水分深浅变化、作物随生长进度放大、成熟发金光、缺水画干裂;
牧场围栏里牲畜随机漫步,待收产出挂金色气泡、没喂食标 💢;
已拥有的建筑在底部建筑带列队;五种天气各有天色,雨/暴雨有雨滴动画。
悬停出提示框,点地块即播种/浇水/收获、点牲畜即喂食/收取——全部复用
既有 openCropPick/act() 流程,卡片面板原样保留作精确操作入口。

实现要点:画布用逻辑坐标绘制、按容器宽 × devicePixelRatio 缩放,
高分屏不糊、窄屏等比缩;命中检测在逻辑坐标上自己算。
服务端零改动——state 里的 plots/animals/buildings 现成够用。

修复一个真机才抓得到的初始化 bug:动画循环首帧同步执行,原顺序
"先绑定后布局"让首帧在未初始化的字段上 TypeError,rAF 注册被一起吞掉,
表现为画布全白但点击命中正常(命中不经过画布)。改为先布局再绑定,
并给 mapStep/mapDraw 加空值护栏。

真浏览器端到端验证:登录、canvas 坐标点击浇水(AP/水分/toast 全对)、
悬停提示、动画循环存活(云位移 5.24px/600ms)、截图目检渲染效果,
errors/console 干净。534 项测试全绿,ruff/pyright 无告警。
2026-09-25 22:24:08 +08:00

157 lines
9.2 KiB
Markdown
Raw 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.
---
name: simplefarm-verify-and-ship
description: Use when finishing ANY change to the SimpleFarm project (D:/SimpleFarm) — after adding or refactoring features, before declaring the task done or pushing to gitea.4i.hk. Covers the four-step delivery ritual — run the quality gate (ruff format/check + pyright + unittest), verify the change end-to-end on a REAL server with seeded saves, write memory notes, then commit and push. Triggers include "改完了", "验收", "跑测试", "提交推送", "推送到 gitea", or any turn where farm/*.py was edited. Also load it when a test fails after a behaviour change, when the server seems frozen, or when `git push` says "Failed to authenticate user".
agent_created: true
---
# SimpleFarm:改完之后的验收与交付
任何一次改动,走完这四步才算做完。别跳过第 2 步——单元测试全绿**不代表**功能真的能跑
(本项目已经出现过:测试全绿,但真实服务器上 `--pid-file pid.txt` 直接崩、
非实时客户端轮询永远读到旧状态)。
## 第 1 步:质量门禁(唯一可信口径)
```bash
uv run ruff format . && uv run ruff check . && uv run pyright
uv run python -m unittest discover -s tests
uv run python tools/bench.py --repeat 5 # 只有碰了热路径才需要
```
预期:`All checks passed!` / `0 errors, 0 warnings` / `OK (skipped=35)`。
`skipped=35` 是没装 httpx 时 `TestClient` 那批用例的正常跳过,不是失败。
bench 里带调用计数器,计数变红说明轻量路径被改回重路径了。
**别用管道**:本机 Git Bash 缺 `head`/`tail`/`grep`/`ls`/`mkdir`,任何 `|` 都会整条失败。
查看/删除文件一律写 Python 脚本。
## 第 2 步:真机端到端验收
单元测试覆盖不到的,是"服务器起来了吗、接口真的返回新字段了吗"。必须起真服务器验。
**关键:服务器要用后台任务跑,别用 `--daemon`。** 脱离父进程的守护进程会随工具调用
结束被回收,下一条命令就连不上了(表现为 `ConnectionRefusedError`)。用
`run_in_background: true` 跑前台服务器,验完再停掉。
```bash
# 1) 预置存档(SQLite,v2 起):想验"买得起"的路径,就先塞一行有钱的档。
# python -c "
# from farm.models import GameState
# from farm.storage import save_state
# st = GameState(); st.gold = 5000; st.level = 5
# save_state(st, '.e2e-saves/farm.db', 'boss')"
# 字段缺失一律走默认值;老 JSON 档也可以直接丢进 --save-dir,启动时自动迁移。
# 2) 起服务器(tick 小一点,3 秒一天)
uv run main.py serve --port 8799 --tick 3 --save-dir .e2e-saves
# 3) 用 RemoteFarm 驱动,跑完删掉脚本和 .e2e-saves/
```
**两个必须知道的坑:**
- **`RemoteFarm.snapshot()` 是本地缓存,不发请求。** 脚本里写
`while True: client.snapshot()` 会永远读到同一份旧数据——服务器在推进,
看起来却像卡死。非实时客户端必须自己调 `client.refresh()` 再读 `snapshot()`。
TUI/网页走 SSE 所以没这个问题。
- **端口上可能有别人的服务器。** 8765 是默认端口,用户自己(或另一个 checkout)
起的实例可能正在上面跑、里面还有玩家进度。动手前先看 `/api/farms` 里有没有
不认识的活跃牧场;有就换端口验,**别 stop 它**。判断一个实例是不是本工作区
的代码,看它的存档目录在不在这个项目里(`saves/farm.db` 的 mtime 动不动)。
- **作物会缺水停长。** 天气随机消耗水分(晴 1 / 多云 0 / 干旱 2),
等成熟的循环里要盯水分补浇,否则 120 秒也等不到成熟。这也顺带证明了
天气→浇水这条链真的在起作用。
验收脚本该断言什么:新字段真的出现在 `/api/state` 和 `/api/catalog` 里、
动作回执带新字段、错误码符合预期(如 `nothing_to_sell` / `locked` / `unknown_building`)。
错误码去 `farm/engine.py` 里核对,别猜。
**动了存档/会话层必须加验一条**:只走 HTTP(不连 SSE、0 客户端在线)做一个动作,
等 60 秒自动存档周期过去,直接查 `saves/farm.db` 那一行有没有更新。
单元测试抓不住 autosave 失效——2026-09-23 就漏过一次:`dirty` 被推送循环高频清零,
autosave 60s 才看一眼,等于永不存盘,全量测试照样绿。
**动了 webui 前端必须加验一条**:API 正常 ≠ 页面能用(window.prompt 被
sandbox 屏蔽、中文用户名炸 fetch 头,都是 API 测不出来的)。真浏览器验收:
```bash
# 1) 起 headless Chrome(后台任务保活;--executable-path 直起会 exit 0 连不上)
uv run python -c "
import subprocess, time
subprocess.Popen([r'C:\Program Files\Google\Chrome\Application\chrome.exe',
'--headless=new','--remote-debugging-port=9333',
'--user-data-dir='+__import__('os').environ['TEMP']+r'\abchrome',
'--no-first-run','about:blank'])
while True: time.sleep(3600)"
# 2) agent-browser 走 CDP 连接(每条命令都要带 --cdp)
PATH="/c/Program Files/nodejs:$PATH" agent-browser --cdp 9333 open http://127.0.0.1:8765/
PATH="/c/Program Files/nodejs:$PATH" agent-browser --cdp 9333 eval "..." # 页面内点按钮、读 toast
```
注意:系统 Chromium(Chrome 153+ / Edge)`--executable-path` 模式起不来,
必须自己起进程再 `--cdp` 连;`find text "登录"` 会点到标题而非按钮,
用 `eval` 精确 `document.getElementById('dologin').click()`。
Canvas / rAF 动画类改动的额外坑(2026-09-25 踩过):动画循环的**第一帧是同步
执行**的——必须先跑完布局再绑定循环,否则首帧 TypeError 会把
requestAnimationFrame 的注册一起吞掉,表现为「画布全白但点击命中正常」
(命中是纯数学,不经过画布)。且 headless 截图空白 ≠ 没画:先
`ctx.getImageData` 读像素确认「画了没」,再放 rAF 计数探针 / 记录动画对象
位移确认「循环活着没」,两连才能定位。
## 第 3 步:写记忆
- 当天日志追加到 `.workbuddy/memory/YYYY-MM-DD.md`(append-only)。
- 长期约定改 `.workbuddy/memory/MEMORY.md`(原地编辑)。
- 用 `read_bytes()` / `write_bytes()` 保 LF;同一个文件**一条消息里改多处**时,
写一个幂等补丁脚本(整文件读入 → 精确匹配替换 → 逐条打印 `ALREADY`/`APPLIED`/`MISS`),
跑完即删。**不要**在一条消息里对同一文件连发多个 Edit——会被静默覆盖。
## 第 4 步:提交并推送
```bash
git add -A && git commit -F - <<'MSG'
...多行中文 commit message...
MSG
NO_PROXY=gitea.4i.hk no_proxy=gitea.4i.hk GCM_INTERACTIVE=always git push origin main
```
- **`NO_PROXY=gitea.4i.hk`(大小写都要)是必需的。** 环境代理对它返回 502,
直连秒通。缺了这个会误判成认证问题。
- **push 卡死几分钟不动 = system 级 `credential.helper=helper-selector` 在
等人点选择框**(tasklist 里能看到 git-credential-helper-selector.exe)。
已修:global 里用**空 helper 重置列表**顶掉它,再依次挂 wincred 和 GCM:
```bash
git config --global --get-all credential.helper
# 应该是(顺序重要):空串 → wincred → GCM
# "" 、 !"C:/Users/Administrator/.workbuddy/binaries/PortableGit/versions/1.2.0/mingw64/bin/git-credential-wincred.exe"
# 、 !"C:/Program Files/Git/mingw64/bin/git-credential-manager.exe"
```
GCM 路径**必须带 `!` 前缀和引号**(`!"C:/Program Files/..."`)——
否则空格截断成 `C:/Program`,报 "No such file or directory"。
- **`GCM_INTERACTIVE=always` 是必需的。** 默认(非交互)下 GCM 直接拒绝
`Cannot prompt because user interactivity has been disabled`,
报错却显示成远端的 `Failed to authenticate user`,很容易误判成 token 失效。
加上它之后 GCM 会静默续期。
- 还是失败就加 `GCM_TRACE=1` 重新推一次,日志会明确说
`No existing credentials found` 还是拿到了 token。别急着去翻 tea 的
`credentials.json.enc`(加密的,读不出来)。凭据库里有
`git:https://refresh_token.gitea.4i.hk`(GCM 的刷新令牌)。
- **验证推送成功看 `git ls-remote origin`**,不要看 `git status -sb`——
沙箱会丢掉 `.git/refs/remotes/**` 新建目录的写入,status 会谎报
`## main...origin/main [gone]`。`ls-remote` 的 sha 与 `git rev-parse HEAD`
一致就是推上去了,`[gone]` 是无害的本地缓存问题。
- 被杀掉的 push 会留下 git.exe / git-credential-manager.exe 僵尸进程,
占住 GCM 的单例锁让后续 push 排队挂死。推送反复挂时先
`taskkill /F /IM git.exe` 等清场再试。
## 别踩的坑(本机特有)
- `os.makedirs(os.path.dirname(path))` 在 path 只有文件名时会崩(dirname 是空串)。
用 `farm/server/daemon.py` 的 `ensure_parent_dir()`。
- 写文件必须显式保 LF(`write_bytes` 或 `newline="\n"`),别让 `\n` 被转成 `\r\n`。
- 访问本机 HTTP 走 `farm/net.py` 的 `open_url()`,环境里有 `HTTP_PROXY` 但没有
`no_proxy`,直连会被代理拦成 502。
- 端口自检用"能不能连上"判断,别用 bind;uvicorn 设了 SO_REUSEADDR,
Windows 上第二个实例能绑上已被监听的端口并抢走新连接。