Files
snowy-yang cb4c1e7800 feat(engine): 四座自动化设施——喷灌 / 收割机 / 售货机 / 磨坊
为什么:自动化建筑只有水井和喂食器,中期以后每天的操作只剩浇水喂食
的重复劳动。新设施把"替代行动点动作"的思路推到完整流水线:
浇水(井/喷灌)→ 收割机收熟地 → 磨坊把陈粮变饲料 → 喂食器喂牲口
→ 售货机趁高价清库存,人不在也能持续产出。

- config.BUILDINGS 注册 4 座:喷灌系统 2800 金(全量浇水)、
  自动收割机 900 金 ×3(每天各收 2 块熟地,刻意不给经验——机器收的
  不算劳动,给手动玩法留价值)、自动售货机 1200 金(市价 > 基准价才
  出手、每天上限 5 份、越贵越先卖)、饲料磨坊 500 金(1 份最便宜的
  作物磨 3 份饲料)。
- engine._run_buildings 扩成六段流水线,顺序即依赖:磨坊排在喂食器前,
  早上磨的饲料当天就能喂上;收割机遇满仓停摆、作物留地(同喂食器
  缺料停摆哲学——自动系统绝不硬来,更绝不自动花钱)。
- webui / build 动作 / catalog 全部由 config.BUILDINGS 驱动,零改动。
- 测试 +6(喷灌全量浇水、收割入库且无经验、满仓停摆留作物、磨坊挑
  最便宜、磨坊→喂食器同日链路、售货机阈值与配额)。修复断言时确认
  两个既有行为:引擎自带 START_FEED=5;advance_day 末尾会把动物 fed
  复位("喂到了"的证据是 pending 累计而非 fed 本身)。
- 门禁:534 项 OK(skip 39)、ruff/pyright 全绿。
2026-09-25 21:05:27 +08:00

92 lines
6.8 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-23 修网页版「点播种没反应」+ 顺手挖出自动存档失效的真 bug
- 用户报网页版点播种没反应。服务端 plant 动作完全正常 → 定位到前端。
- **根因 1(前端)**:播种按钮走 `window.prompt` 选作物,重置走 `window.confirm`
——这俩在 sandbox iframe(内置预览面板)里会被**静默屏蔽**:不弹窗、直接返回
null,于是"点了没反应"。普通浏览器里是好的,所以一直没暴露。
修复:改成页面内弹层(`#pickmask` + `openCropPick`,未锁定作物生成按钮 +
取消按钮,点遮罩关闭);重置改两步确认(第一次点变"再点一次确认",3 秒还原)。
- **根因 2(后端,真 bug)**:顺着验证存档时发现 `default` 牧场**根本没写进库**
——内存里每次重启都是新档。排查出 `session.dirty` 一个标志位伺二主:
Publisher 推送循环每 ~250ms 就把 dirty 清掉(无人在线时更是直接清),
而 `autosave()` 每 60s 才看一眼 dirty——窗口太小,**自动存档几乎永远不触发**
(约 0.4% 撞率)。进度只在手动存档/优雅停机时落盘,进程被强杀就全丢。
修复:新增 `session.unsaved` 专管「有变化没落盘」,`_on_change`/`load`/
`touch`/`advance_day` 置位、只有成功的 `save()` 清位;`autosave()` 改看
unsaved;dirty 继续只管推送节流。hub.py 的推送循环一字未动。
- 两只旧测试(`test_autosave_skips_when_clean` / `..._writes_when_dirty` /
`autosave_all...`)直接操纵 dirty 驱动 autosave,固化了被修掉的耦合——
全部改成用 unsaved 表达意图。新增回归测试:
「推送循环清了 dirty 也要照常落盘」「没有新变化不重复写库」。
- **真机验证**:0 客户端在线、纯 HTTP 种一块地,70 秒内看到库行更新
(gold 120→112,种子钱都对得上)。
- 门禁:ruff/pyright 全绿,515 项测试 OK(skip 35)。
### 教训
- 嵌入预览面板里 `window.prompt`/`confirm`/`alert` 都不可信,交互一律用页面内
弹层实现。
- 一个布尔标志被两个子系统共用时,其中一个高频清零,另一个低频读取——
低频的那个等于瞎了。存盘依据必须由"成功的写入"来清。
## 2026-09-23 用户名登录(多用户)
- 用户要求「添加多用户的 需要使用用户名登录」;AskUserQuestion 确认:
**仅用户名**(不设密码)+ **一账号一个牧场**(用户名即牧场名)。
- 新增 `farm/server/auth.py`:`LoginManager` 签发/校验 HMAC-SHA256 签名的
HttpOnly Cookie(`sf_auth`,30 天,SameSite=Lax)。Cookie 值 =
`base64("用户名|过期时间") + "." + hmac`,验签 `hmac.compare_digest`。
密钥 32 字节存 `<save_dir>/secret.key`(0600)——**重启不掉线**,无需建表。
- `app.py`:新增 `POST /api/login`(guard 照常检查全局 token)、
`POST /api/logout`、`GET /api/me`(刻意不 guard,首屏探测用);
`_Services` 加 `auth` 字段与 `user_of()`;**三个认 ?farm= 的地方全部改为
「有 Cookie 强制用自己的牧场」**:`session_of`(?farm=/X-Farm-Id 被忽略)、
SSE `/api/stream`、`POST /api/farms`。无 Cookie 的脚本/AI/CLI 保持旧语义
(互信小圈子,`?farm=` 不受影响),bot/TUI 零改动。
- `webui.py`:移除「牧场名」输入框和「进入牧场」按钮,改为登录遮罩
(`#loginmask` 复用 `.mask/.pick` 样式)+ 用户徽章 `#who` + 退出按钮;
JS 先 `whoami()` 再决定 `connect()` 还是弹登录;令牌输入框保留
(change 时重连)。farmName() = 登录用户。
- 测试 +13:`LoginCookieTestCase` 9 项纯逻辑(伪造/过期/跨密钥/垃圾串/非法名/
密钥复用/session_of 优先级——starlette Request 可直接用 scope dict 构造,
不需要 httpx)+ AuthTestCase 4 项 TestClient 档(照例 skip)。
门禁 528 项 OK(skip 39),ruff/pyright 全绿。
- 真机验收 16/16:登录/Cookie/HttpOnly/?farm= 被忽略/动作落自己牧场/
篡改退回未登录/退出/非法用户名 400/首页元素。跨重启:secret.key 复用,
Cookie 仍认 carol、显式 save 后作物也在。
- 教训:幂等补丁脚本里 `new == ""` 时 `if new in text` 恒真,会误报 ALREADY
跳过删除类替换(这次是删全局 Enter 监听那行,靠核对输出抓回来)。
删除类编辑别走补丁脚本,直接用 Edit 工具。
## 2026-09-23(晚)「种不了东西」:中文用户名 + X-Farm-Id 头
- 用户报种不了东西。API 直发 plant 全正常、页面代码静态看着也正常,
最后用 headless Chrome(本机 Chrome + `--cdp 9333` 连接模式)真机复现:
登录"阳"后点播种→选小麦,toast 不变、地块不变,act() 抛
`TypeError: Failed to execute 'fetch': String contains non ISO-8859-1 code point`。
- **根因**:`headers()` 把用户名塞进 `X-Farm-Id` 头,HTTP 头只允许 Latin-1,
中文用户名让 fetch 在发送前就抛异常。登录时 me=null(头是 ASCII 的
default)所以能登录成功,**登录后所有走 api() 的写操作全部静默失败**
(Promise 拒绝无人接住);页面靠 SSE 照常渲染,看起来一切正常。
- 修复(webui.py,幂等补丁 6 处全中):headers() 非 ASCII 值不塞头
(登录态走 Cookie,头只是匿名脚本备用通道);api() 捕获 fetch 异常并
toast,杜绝静默失败;localStorage 裸调用加 try/catch(sandbox iframe
无 allow-same-origin 时访问即抛,启动段有三处裸调用会炸掉整个脚本)。
- 真机回归:登录阳→点播种→选小麦→toast「1 号地已种下小麦」+ 地块状态
实时更新。门禁 528 项 OK(skip 39)、ruff/pyright 全绿。提交 2676927。
- 顺带发现:上一轮遗留的调试实例用 `--tick 60` 跑了好几个小时(day 冲到
92),已用默认 40 分钟/天重启。
- **推送排障(重要环境坑)**:push 卡死 5 分钟 = **system 级
`credential.helper=helper-selector` 在等人点选择框**(进程
git-credential-helper-selector.exe)。修法:global 里放**空 helper 重置
列表**再依次挂 wincred + GCM(`!"C:/Program Files/..."` **必须带 `!` 和
引号**,否则空格截断)。GCM 路径还要 `NO_PROXY=gitea.4i.hk`——系统代理
对它 502。修完推送秒过。Chrome/Edge 直接 `--executable-path` 起不来
(exit 0 无端口),要自己 headless 起进程再 `--cdp <port>` 连。
### 教训
- HTTP 头只允许 Latin-1:任何把用户可输入内容(用户名、中文名)塞进请求头
的代码,中文输入即炸 fetch。要么不塞(走 Cookie/body),要么白名单校验。
- api() 层必须接住网络异常并可见化——静默的 Promise 拒绝让这个 bug 从
"登录功能上线"一直活到用户报障。