mirror of
https://github.com/Shu-Ying/main_plugin_farm.git
synced 2026-11-14 00:40:05 +08:00
Compare commits
3
Commits
22cfddcf06
...
e7c6eac07f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7c6eac07f | ||
|
|
e5ec38e9d9 | ||
|
|
ac0209f010 |
+15
-12
@@ -1,6 +1,6 @@
|
||||
# 第一批基础核心实现
|
||||
|
||||
日期:2026-10-15。对应 Comet change:farm-core-foundation。
|
||||
更新日期:2026-10-26。对应Comet change:farm-core-foundation。
|
||||
已确认规格位于 comet/changes/farm-core-foundation。
|
||||
本文件记录实现与开发期测试,不替代 Runtime 的独立验收报告。
|
||||
|
||||
@@ -22,12 +22,15 @@
|
||||
按职责先采用小模块;随着用例增加再拆分目录,
|
||||
不为了贴合远期目录树生成大量空包。
|
||||
核心只用标准库,不导入三框架,包内部相对导入。
|
||||
wheel 随包携带 JSON,不包含 legacy、图片、玩家数据库或其他框架入口。
|
||||
wheel随包携带JSON及小型升级土地素材,不包含legacy、玩家数据库或
|
||||
其他框架入口;大型图片ZIP通过匹配的外部资源提供。
|
||||
|
||||
## 已迁移与修复
|
||||
|
||||
- 沿用500初始币、3地块、等级门槛及源作物价格/产量/经验/成熟时间。
|
||||
- 旧作物库202条记录完整导出且保留源 SHA-256。
|
||||
- 注册500初始币、6地块;可靠识别为初始3块的旧号一次性补3块,
|
||||
保留已开垦成果。详见land-rules.md;价格/产量/成熟时间沿用目录。
|
||||
- 初始作物库202条记录完整迁入Service;当前数据发布和启动更新见
|
||||
[作物数据维护](catalogue-updates.md),旧库基线独立保留。
|
||||
- 多季再生长间隔明确保留去重 phase[1:] 求和,未重平衡数值。
|
||||
- F01/F02:余额和经验只增加本次增量。
|
||||
- F03:每块地单独加成、向下取整,再求和。
|
||||
@@ -66,8 +69,9 @@ SchemaError 表示外来或不兼容库。接入层不能把后两者当作未
|
||||
|
||||
## 持久化与取消语义
|
||||
|
||||
新库使用独立 application_id、schema_version=1:
|
||||
accounts、inventory、plots、operations、ledger。
|
||||
新库使用独立application_id,当前schema_version=5,包含accounts、
|
||||
inventory、plots、operations、ledger及thefts。受支持旧schema按顺序
|
||||
事务升级;迁移失败整体回滚,不手工改版本号。
|
||||
未知表集、外来库或更高版本被拒绝,不把旧 farm.db 自动当新库打开。
|
||||
|
||||
每次写用例使用新连接与 BEGIN IMMEDIATE;提交前统一写幂等结果。
|
||||
@@ -93,12 +97,11 @@ close 拒绝新请求并等待在途任务,无导入时线程或持久数据
|
||||
- 首次事务测试有2项因测试代码未关闭连接而在Windows清理失败;
|
||||
修复测试资源释放后通过,不隐藏该失败。
|
||||
- 最终结果以 Comet 管理的 verification.md 为准。
|
||||
- 真实框架、旧玩家库、联机、图片和远程发布没有测试或启用。
|
||||
- 上述计数为初期历史检查,不是当前完整套件计数;图片、接入与迁移
|
||||
已有后续源码及离线测试,不因此声称真实宿主或生产玩家库已验证。
|
||||
|
||||
## 正式验收结果
|
||||
|
||||
Runtime已执行完整离线检查并通过。
|
||||
独立只读Verifier核对A1至A8后全部判定通过,结果已由Runtime接收;
|
||||
正式报告为 comet/changes/farm-core-foundation/verification.md。
|
||||
通用Skill桥接不能机器认证独立执行身份,当前等待用户接受结果,
|
||||
尚未归档、提交或推送。此状态不等于三框架插件已发布。
|
||||
正式结果以Comet生成报告为准,不在本说明预先宣称本轮验收通过。
|
||||
当前真寻安装与维护见[zhenxun-preview-current.md](zhenxun-preview-current.md)。
|
||||
离线测试、模拟宿主与生产安装/远端发布分别记录,不互相替代。
|
||||
|
||||
+33
-26
@@ -1,7 +1,7 @@
|
||||
# 共享图片工具与“我的农场”
|
||||
|
||||
状态:共享渲染已实现并通过离线检查和独立只读验收;等待用户接受。
|
||||
真实宿主命令与上传仍未接入。
|
||||
更新日期:2026-10-26。共享渲染和宿主接入已有实现;实际验收结果
|
||||
由Comet报告记录,模拟测试不能证明真实宿主消息发送或客户端显示。
|
||||
Python 语法基线3.10;本机实测 Windows/Python3.12.14/Pillow12.3.0。
|
||||
|
||||
## 安装与职责
|
||||
@@ -25,7 +25,8 @@ NoneBot、AstrBot、真寻均可调用,不导入任何宿主对象。
|
||||
默认使用由 farm_service 分发并缓存的小赖字体(lxgw/kose-font v3.126),原版字体与
|
||||
完整 SIL OFL 1.1 许可一同分发,版权声明见
|
||||
`src/farm_render/data/OFL-Xiaolai.txt`。插件固定使用小赖字体,不提供
|
||||
字体路径配置;开发测试可通过 FARM_TEST_FONT 指定路径。当前
|
||||
字体路径网页配置;共享Runtime/绘图API支持显式路径,开发测试可通过
|
||||
FARM_TEST_FONT指定路径。当前
|
||||
实现校验字体可加载,不保证任意字体具备所有中文、Emoji 字形。
|
||||
|
||||
## 调用
|
||||
@@ -33,7 +34,10 @@ NoneBot、AstrBot、真寻均可调用,不导入任何宿主对象。
|
||||
```python
|
||||
from farm_render import FarmRenderer, RenderOptions, RenderService
|
||||
|
||||
renderer = FarmRenderer("/path/to/chinese-font.ttf")
|
||||
# 素材准备在接入层完成;此处传入与包内清单匹配的本地缓存路径。
|
||||
renderer = FarmRenderer(
|
||||
"/path/to/chinese-font.ttf", bundle_path=verified_bundle_path,
|
||||
)
|
||||
rendering = RenderService(renderer, concurrency=1, max_pending=8)
|
||||
try:
|
||||
picture = await rendering.my_farm(
|
||||
@@ -60,7 +64,8 @@ now, options),统一 now 使用UTC秒整数。正常未注册/存储故障仍
|
||||
|
||||
## 尺寸、格式和质量
|
||||
|
||||
- 普通:1024×624、quality75;RenderOptions.high() 为1536×936、85。
|
||||
- 普通默认1024×624、质量75;质量配置整数1~95、默认75。
|
||||
- 仅我的农场支持高清1536×936,质量max(配置值,85),其他查询拒绝高清。
|
||||
- 编码格式独立:AUTO、WEBP、JPEG、PNG;WebP method默认2。
|
||||
- AUTO仅根据接入层声明的 supported 和本机编码能力选择,默认
|
||||
supported只有JPEG。确认平台支持后传入WEBP,才会优先使用它。
|
||||
@@ -81,8 +86,9 @@ now, options),统一 now 使用UTC秒整数。正常未注册/存储故障仍
|
||||
|
||||
布局使用旧30块地坐标。新播种及新一季使用虫害工作量计算阶段,
|
||||
未启用虫害的迁移前当前季仍从成熟时间和基础时长推导。
|
||||
枯萎优先于成熟。作物采用完整画布底边中点统一公式,
|
||||
不再使用人工阶段锚点和旧 offset 字段,详见 `15-crop-position-formula.md`。
|
||||
枯萎优先于成熟。作物采用schema=4的visible_contact_center统一落点,
|
||||
直立等比缩放、保留完整纹理,不再使用人工阶段锚点和旧offset字段。
|
||||
当前规则见 `22-crop-scene-mapping.md`;15号文档记录公式演变历史。
|
||||
虫子使用场景包中的固定坐标与稳定选帧,规则见 `16-pest-mechanics.md`。
|
||||
|
||||
素材LRU默认32MiB,成图LRU默认8MiB;零预算禁用对应缓存,超限
|
||||
@@ -103,9 +109,10 @@ RenderService默认一个绘图任务,最多8个活跃/排队的不同请求
|
||||
|
||||
## 素材完整性与来源
|
||||
|
||||
scripts/export_render_assets.py将已有素材原样打包,附202种作物
|
||||
展示配置、旧布局和统一定位规则;额外打包新提取的 40 张网页素材。
|
||||
--check比对确定性输出与原图哈希,不修改旧基线。
|
||||
现行素材及展示配置由Service的catalogue/source维护,作为完整版本发布,
|
||||
插件启动时统一选择;见[作物数据维护](catalogue-updates.md)。
|
||||
scripts/export_render_assets.py仅保留显式旧基线导出;默认--check核对
|
||||
当前bootstrap,不再把日常配置绑定到旧库。
|
||||
素材包约束为安装资源,不按用户输入路径访问任意文件、不解压到磁盘。
|
||||
保留GPL及原始归属;历史素材分发权仍需在公开发布前核实。
|
||||
|
||||
@@ -113,21 +120,20 @@ scripts/export_render_assets.py将已有素材原样打包,附202种作物
|
||||
缺失时显示问号占位并记录静态警告,避免整个命令失败;尚未补齐
|
||||
缺失图片,也不推测相近作物素材替代。不会声称202种作物均有完整图片。
|
||||
|
||||
## 原版用户面板恢复(2026-10-16)
|
||||
## 当前用户面板(2026-10-26核对)
|
||||
|
||||
使用旧版 `background/userInfo.png` 与 `background/frame/frame.png`,
|
||||
按原有顺序叠放底板、140×150头像、头像框和昵称/经验/等级/货币。
|
||||
不再用通用圆角信息卡,不额外显示地块数量。无效头像继续安全降级。
|
||||
用户确认按原底图校正旧坐标:经验条位于原素材经验槽内,等级与两种
|
||||
货币不越出底板。普通、高清图统一按3072基准坐标缩放。
|
||||
面板使用metadata.json的panel配置及网页场景切片,按reference_canvas
|
||||
映射位置、头像、字段与经验条,不再按旧底板固定坐标绘制。
|
||||
头像无效时安全降级,普通与高清按同一面板配置缩放。
|
||||
|
||||
保留旧版经验计算口径:本级已获经验为累计经验减本级累计门槛,
|
||||
分母仍为下一级累计经验门槛;等级门槛没有变化。点券取真实账户余额,
|
||||
本级已获经验为累计经验减200*level*(level+1)//2,
|
||||
分母为本级升级所需经验200*(level+1),不使用下一级累计门槛。
|
||||
点券取真实账户余额,
|
||||
不继承旧版硬编码0。素材按目标尺寸缓存,查询仍只读一次一致快照;
|
||||
头像字节限制、缓存失效、后台限流和格式选项保留。
|
||||
|
||||
本次离线测量见 `render-panel-restored-benchmark.json`,
|
||||
样图在 `runtime/panel-restored`;不代表真实平台发送耗时。
|
||||
`render-panel-restored-benchmark.json`及runtime/panel-restored是历史
|
||||
面板恢复测量,不代表当前面板或真实平台发送耗时。
|
||||
|
||||
## 测量与验证
|
||||
|
||||
@@ -137,7 +143,10 @@ python scripts/export_render_assets.py --check
|
||||
python scripts/verify_foundation.py
|
||||
```
|
||||
|
||||
本轮测量原始结果见 [render-benchmark.json](render-benchmark.json)。
|
||||
下表为早期历史测量,原始结果见[render-benchmark.json](render-benchmark.json)。
|
||||
2026-10-26新测位于runtime/archive-foundation-20261026/benchmark.json,
|
||||
使用当前源码生成六幅1024/1536的WebP、JPEG和PNG图并验证解码与暖缓存;
|
||||
本机环境为Windows/Python3.12.14/Pillow12.3。输出目录不进入Git。
|
||||
样例为构造的18块解锁地,包含空地、生长、成熟、枯萎与锁定地。
|
||||
输出和日志位于runtime/render-demo,不进入Git。
|
||||
|
||||
@@ -160,8 +169,6 @@ python scripts/verify_foundation.py
|
||||
字节与成图命中,不记录真实姓名、作用域、头像或完整快照。
|
||||
上传阶段由适配器单独记录,不能将发送接口返回当作客户端已显示。
|
||||
|
||||
正式检查结果:45项测试全部通过,827文件基线与两类可复现导出
|
||||
检查通过。独立只读Verifier核对完整核心/渲染规格和源码后,A1-A13
|
||||
全部通过。验收报告见 [Comet报告](
|
||||
comet/changes/farm-core-foundation/verification.md)。通用Skill桥接不提供
|
||||
机器独立身份认证,流程仍要求用户明确接受后才归档。
|
||||
45项测试和早期A1-A13结论属于历史记录;本轮正式结果以Comet生成
|
||||
报告为准,不由旧报告推定。真实平台、其他CI矩阵与远端资源可用性
|
||||
不属于本机绘图样例的证明范围。
|
||||
|
||||
+7
-4
@@ -1,14 +1,17 @@
|
||||
# 文档索引
|
||||
|
||||
记录日期:2026-10-15。状态:已形成设计草案,待关键产品决策确认;
|
||||
第一批基础核心已实施;多框架接入、发布和真实数据迁移尚未实施。
|
||||
更新日期:2026-10-26。共享核心、绘图和三框架接入/打包已有实现;
|
||||
本轮仅做本地归档验收,不代表真实宿主、生产数据迁移或远端发布已验证。
|
||||
长期设计草案及历史页面须与现行使用说明区分。
|
||||
|
||||
| 文档 | 用途 |
|
||||
|---|---|
|
||||
| [catalogue-updates.md](catalogue-updates.md) | Service 作物维护、发布、启动更新、缓存与兼容回滚 |
|
||||
| [23-seasonal-events-and-rust-service.md](23-seasonal-events-and-rust-service.md) | 节日活动交互备选、Rust 发布中心、同步、资产事务、过期清理与上线规划(草案) |
|
||||
| [09-foundation.md](09-foundation.md) | 已实现的第一批核心、API、测试与限制 |
|
||||
| [10-rendering.md](10-rendering.md) | 共享图片工具、农场绘制、缓存、编码与接入 |
|
||||
| [11-zhenxun-preview.md](11-zhenxun-preview.md) | 真寻原指令预览、构建/校验/安装和可选同步 |
|
||||
| [zhenxun-preview-current.md](zhenxun-preview-current.md) | 当前真寻构建、依赖、安装、配置、数据备份/回退/卸载及发布Secrets |
|
||||
| [11-zhenxun-preview.md](11-zhenxun-preview.md) | 旧版页面留档,不作为当前功能和安装依据 |
|
||||
| [zhenxun-preview-checks.json](zhenxun-preview-checks.json) | 最终测试包大小/哈希及实际验证边界 |
|
||||
| [render-benchmark.json](render-benchmark.json) | 本机合成数据的冷暖缓存与编码测量 |
|
||||
| [01-roadmap.md](01-roadmap.md) | 长期目标、实施顺序、里程碑与验收 |
|
||||
@@ -34,4 +37,4 @@
|
||||
- 发布仓库是分发目标,不是 Git 子模块;各框架使用相互独立的插件包。
|
||||
- 主仓库远程为 Shu-Ying/main_plugin_farm;本轮未执行远程操作。
|
||||
- 用户已撤回拉取要求,且要求删除本地克隆;三个本地检出已删除,不再访问远程。
|
||||
- 本轮仅修正迁移副本中的硬编码凭据;业务缺陷记录在审查报告,没有隐式修复。
|
||||
- 历史迁移预检与后续实现分开记录;当前行为以源码、现行说明和正式验收为准。
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# 作物数据维护与启动更新
|
||||
|
||||
作物的可编辑源已迁入独立 Service 仓库的 `catalogue/source/`。
|
||||
插件内只保留首次离线启动所需的轻量基线;日常改价、调整生长参数和新增作物
|
||||
不需要重新发布插件。图片、展示配置、作物定义及升级土地图作为同一版本发布。
|
||||
|
||||
## 日常更新
|
||||
|
||||
1. 在 Service 修改 `catalogue/source/plants.json`。新增作物使用未占用的正整数
|
||||
ID,在 `catalogue/source/images/` 添加对应图片;布局在 `metadata.json`。
|
||||
2. 在 Service 根目录生成一个新的版本标签:
|
||||
|
||||
```text
|
||||
python scripts/prepare_catalogue.py --source catalogue/source --output catalogue --version crops-20261026-01
|
||||
python scripts/prepare_catalogue.py --source catalogue/source --output catalogue --version crops-20261026-01 --check
|
||||
```
|
||||
|
||||
3. 审查并发布 Service 中的源文件、`catalogue/manifest.json` 和新生成的
|
||||
`catalogue/releases/` 历史版本。Github/Gitea 源需将文件发布到相应仓库;
|
||||
HTTP 源需配置 `FARM_CATALOGUE_DIR` 指向完整 `catalogue`,并重启 Service
|
||||
以加载新快照。详细操作见 Service 的 `docs/catalogue.md`。
|
||||
4. 重启或重载农场插件。三框架共用启动流程,按配置的资源源检查一次更新;
|
||||
运行期间不轮询,不改变当前进程已加载的版本。改用 HTTP 源时使用有效的
|
||||
HTTPS Service 基础地址。
|
||||
|
||||
不可删除或改名已发布的作物 ID,也不可删除既有图片路径。停售或停止播种应
|
||||
调整对应开关。同一版本标签不可复用于不同内容。回滚应从旧配置生成新的兼容
|
||||
版本,保留已经发布的 ID、名称和图片;不要直接退回缺少新增 ID 的旧版本。
|
||||
|
||||
## 缓存与玩家数据
|
||||
|
||||
启动会核对清单、大小、SHA-256、JSON 契约和 ZIP 图片内容,成功后才原子切换
|
||||
数据目录下的 `catalogue/current.json`。断网、404 或版本无效时优先使用已经
|
||||
验证的缓存,再使用插件基线。日志位于同一数据目录的 `farm-preview.log`。
|
||||
|
||||
备份应包含完整数据目录及其 `catalogue` 缓存。玩家库存或地块若引用基线没有的
|
||||
新作物 ID,而可用缓存和远端均不能提供这些 ID,插件会明确拒绝启动,避免
|
||||
丢失库存。不要通过删除玩家数据解决版本问题,应恢复缓存或提供兼容发布版本。
|
||||
已播种作物继续按播种时的快照结算;出售使用当前加载版本的价格。
|
||||
|
||||
## 插件维护者刷新离线基线
|
||||
|
||||
只有准备发布新版插件时才需更新内置基线。在 Farm 根目录执行:
|
||||
|
||||
```text
|
||||
python scripts/sync_catalogue_bootstrap.py --source D:/Work/farm_service/catalogue
|
||||
python scripts/check_catalogue_bootstrap.py
|
||||
```
|
||||
|
||||
同步前请确认 Service 发布版本已审查。CI 独立核对 Farm 内置产物,不依赖相邻
|
||||
Service 工作区或在线下载。旧库导出工具只保留为显式迁移参考,不再是日常数据源。
|
||||
@@ -0,0 +1,122 @@
|
||||
# Outcome
|
||||
|
||||
在D:/Work/farm当前main工作区归档现有框架无关基础核心、共享绘图和
|
||||
真寻预览交付。按用户2026-10-26确认,将旧规格同步为已经实现并记录
|
||||
的规则,补齐离线验收;不借归档修改业务代码或实施作物Service迁移。
|
||||
|
||||
# Scope
|
||||
|
||||
- farm_core不依赖三种框架或Pillow;离线迁出旧基线202条作物,以稳定
|
||||
本地ID为身份,运行时允许同名并拒绝名称歧义,不在运行时引用legacy。
|
||||
- SQLite保存权威账户、库存、地块、交易流水和幂等结果;显式身份由
|
||||
deployment/realm/platform/user构成,不推断跨群或跨平台绑定。
|
||||
- 注册赠送500农场币、0点券、0经验及6块普通空地;可靠识别为初始
|
||||
3块的旧用户仅补3块普通空地,保留已付费开垦成果,重试不重复补偿。
|
||||
- 购买、播种、收获、铲除、加锁/解锁、出售和查询保持整数校验、
|
||||
事务原子性、失败传播及可注入时钟。已播种作物保存不可变结算定义。
|
||||
- 经验等级门槛200*k*(k+1)//2;播种每地+2、铲除每地+2经验,收获
|
||||
逐地计算本次增量。首熟为time*3600,再生长沿用去重phase[1:]之和。
|
||||
- farm_render提供Pillow同步工具、有界缓存、目标尺寸绘制、后台受控
|
||||
执行、三格式编码及安全日志;普通宽1024,农场高清宽1536。
|
||||
- 统一图片编码质量为整数1~95、默认75;仅我的农场接受高清,质量
|
||||
为max(配置值,85),其他查询拒绝高清,保留分页与文字。
|
||||
- 作物采用visible_contact_center统一定位及当前稳定地块编号映射,
|
||||
不使用旧offset或逐阶段手调锚点;缺图明确占位并记录静态警告。
|
||||
- 开发素材独立导出,插件包含轻量元数据、清单和小型升级土地图;
|
||||
大型assets.zip及字体ZIP不进发布包,按Github/Gitea/farm_service
|
||||
选择源校验下载缓存。服务地址独立保留,显式中文字体优先;默认使用
|
||||
原版小赖字体和完整OFL许可,资源不可用则接入层保留文字功能。
|
||||
- 真寻原指令、私有_vendor命名空间、稳定幂等与启动关闭生命周期;
|
||||
白名单构建、独立验证及安全安装仅在临时目录测试。
|
||||
- 工作流执行检查并上传附件;push main或显式publish输入可触发固定
|
||||
真寻发布仓库main分支的普通同步,PR不发布,脚本默认dry-run且不force。
|
||||
- 后续已存在的NoneBot/AstrBot、土地扩展/升级、偷菜、虫害及旧库迁移
|
||||
保留现状,不加入本change的新需求;完整离线测试仍检查其回归。
|
||||
|
||||
## Directory structure
|
||||
|
||||
### Created
|
||||
|
||||
本轮不新建业务模块;Comet生成正式验收报告与归档卷宗。
|
||||
|
||||
### Modified
|
||||
|
||||
- 当前brief与specs/farm-core、farm-render、zhenxun-preview/spec.md。
|
||||
- release/targets.json中的真寻目标分支记录与实际main规则对齐。
|
||||
- 如需记录本轮测量,使用现有runtime下的独立样例目录。
|
||||
|
||||
### Deleted
|
||||
|
||||
仅由Comet归档事务迁移当前change文件,不删除业务代码或玩家数据。
|
||||
|
||||
### Not created
|
||||
|
||||
不创建作物远程目录、Service接口、新业务模块、真实玩家库或新框架接入。
|
||||
|
||||
# Non-goals
|
||||
|
||||
- 不实施作物Service迁移或启动在线更新;另一个仓库中的迁移设计不
|
||||
纳入本次归档,不因此视为已获实施确认或已实现。
|
||||
- 不改业务行为、重新平衡数值、修改legacy、重置或迁移真实玩家资产。
|
||||
- 不clone/fetch/pull/push,不创建PR、Release或执行实际跨仓库发布。
|
||||
- 不安装、启动或重启真实机器人,不部署Service,不访问真实玩家库。
|
||||
- 不把本地检查、模拟宿主或静态工作流证据称为真实平台及远端CI通过。
|
||||
|
||||
# Acceptance examples
|
||||
|
||||
- A1: 旧基线827文件校验继续通过;核心不导入框架、无导入副作用,
|
||||
从源码及安装包可独立运行,不在运行时引用legacy。
|
||||
- A2: 可复现导出202条基线作物;ID及必要数值对应源静态数据库;
|
||||
加载校验格式/schema,坏数据明确拒绝。运行时ID唯一,同名拒绝歧义。
|
||||
- A3: 新注册得到500农场币、0点券、0经验、6空地;重复注册不增发,
|
||||
不同作用域隔离;旧初始3块账号仅补3块,并发/重试/重启不重复补偿。
|
||||
- A4: 购买、播种扣款/入库/耗种正确;0、负数、bool、超大数量、余额、
|
||||
库存或等级不足及未知作物明确失败,没有部分写入。
|
||||
- A5: 未成熟不能收获,成熟只增加本次产量/经验;收获前100、本次20
|
||||
后为120,播种经验另算。逐地加成顺序无关,多季/枯萎/铲除及成熟
|
||||
边界遵守保存定义及可注入时钟。
|
||||
- A6: 余额100、出售收益20后为120;锁定限制指定和全部出售;
|
||||
返回真实余额及安全文本,不出现模板参数缺失。
|
||||
- A7: 故障注入使资产/地块/流水/结果整体回滚;重试返回原结果,
|
||||
参数冲突拒绝;并发不透支/重复领奖,故障不伪装成功,重启后可读。
|
||||
- A8: 离线测试、运行入口与说明可执行;文档区分源码、模拟、离线
|
||||
产物与真实宿主/远程验证,准确记录当前实现与待办。
|
||||
- A9: 图片工具支持透明合成、缩放、裁剪、测量/省略及三格式编码;
|
||||
渲染不联网或导入框架,核心无Pillow依赖;安装后提供匹配外部资源可绘图。
|
||||
- A10: 一次快照与统一时间绘制空地、生长、成熟、再生长、枯萎及
|
||||
锁定状态,采用当前地块映射及统一定位,普通/高清输出可解码。
|
||||
- A11: 小图从目标尺寸绘制,素材与成图缓存有界且不污染共享图片;
|
||||
阶段/成熟边界、余额、头像及身份变化不会复用过期图。
|
||||
- A12: 后台合成编码限制并发、相同请求合并;取消不提前释放名额,
|
||||
关闭等待任务排空;日志仅记录耗时、尺寸、缓存等安全信息。
|
||||
- A13: 离线样例和真实测量覆盖尺寸/编码、冷暖缓存及调用方式;
|
||||
图片可解码,明确本机测量不等同真实平台上传性能。
|
||||
- A14: 真寻原指令、路由、身份/幂等、异常反馈与启动关闭通过模拟验证;
|
||||
字体显式路径优先、默认校验下载,资源失败文字可用,不操作真实宿主。
|
||||
- A15: 白名单真寻插件包包含私有共享模块、轻量数据、许可和来源哈希,
|
||||
不含其他适配、legacy、测试、玩家数据或大型资源ZIP;准确标识脏源码,
|
||||
相同输入可复现,构建不改源码。
|
||||
- A16: 离线验证内容、导入与路由;安装拒绝穿越、坏哈希和已有目标,
|
||||
失败无半成品,不操作旧插件/玩家数据或启动真实机器人。
|
||||
- A17: 工作流检查、构建、上传;push main或显式publish输入可触发固定
|
||||
真寻仓库main普通同步,PR不发布;脚本默认dry-run、不force,冲突失败,
|
||||
凭据不落盘。本次只检查与模拟,不执行真实发布。
|
||||
- A18: 本机离线打包与安全边界检查通过;静态核对Windows/Linux与
|
||||
Pillow10.4/12.3矩阵,明确本机环境及未运行矩阵;提供安装、依赖、
|
||||
字体、数据库、回退/卸载与Secrets说明,不宣称真实平台已验证。
|
||||
- A19: 图片质量默认75,整数1和95有效,0、96、bool及非整数拒绝;
|
||||
配置覆盖图片编码并区分缓存。仅我的农场允许高清,质量不低于85与
|
||||
配置值,其他查询拒绝高清,分页/文字可用,帮助和配置说明一致。
|
||||
|
||||
# Constraints and invariants
|
||||
|
||||
沿用当前main目录,不创建或合并分支,不改业务代码。保留许可与来源,
|
||||
不记录令牌、凭据或真实玩家信息。正式状态和报告由Comet维护,不伪造
|
||||
验收证据。本轮检查绑定当前候选与文档,旧报告不能代替本轮验收。
|
||||
|
||||
# Decisions
|
||||
|
||||
- 用户已确认按当前已实现并记录的规则同步旧规格、补齐验收后归档,
|
||||
并再次要求“我已确认,请归档”;授权本地文档、验收与归档收尾。
|
||||
- 原记录中的3块地、仅手动farm-preview分支发布、包内大素材及旧
|
||||
offset定位已被后续实现替代。本规格记录当前行为,不恢复旧规则。
|
||||
@@ -0,0 +1,617 @@
|
||||
schema: comet.native.v4
|
||||
name: farm-core-foundation
|
||||
language: en
|
||||
phase: archive
|
||||
status: done
|
||||
state_version: 50
|
||||
brief: brief.md
|
||||
document_constraints_version: 3
|
||||
shape_confirmation_hash: 0c329cbbb25fef680c0862751441fb78bb7bd4d7a8a2f698edf5a20d17ab828e
|
||||
spec_changes:
|
||||
- capability: farm-core
|
||||
operation: create
|
||||
source: specs/farm-core/spec.md
|
||||
- capability: farm-render
|
||||
operation: create
|
||||
source: specs/farm-render/spec.md
|
||||
- capability: zhenxun-preview
|
||||
operation: create
|
||||
source: specs/zhenxun-preview/spec.md
|
||||
workspace:
|
||||
isolation: current
|
||||
change_branch: main
|
||||
target_branch: main
|
||||
finish: null
|
||||
loop:
|
||||
stage: done
|
||||
goal_cycle: 7
|
||||
iteration: 2
|
||||
attempt: 1
|
||||
retry_epoch: 0
|
||||
failed_iteration_count: 1
|
||||
no_progress_count: 0
|
||||
execution_failure_count: 0
|
||||
previous_unresolved_ids: []
|
||||
next_action: null
|
||||
acceptance:
|
||||
- id: A1
|
||||
source: brief.md
|
||||
text: "A1: 旧基线827文件校验继续通过;核心不导入框架、无导入副作用, 从源码及安装包可独立运行,不在运行时引用legacy。"
|
||||
result: passed
|
||||
reason:
|
||||
text: 当前Runtime
|
||||
foundation-offline核实827旧基线与源码/安装包隔离测试通过;test_packaging.py校验无宿主导入、无导入副作用及临时wheel独立运行。
|
||||
truncated: false
|
||||
- id: A2
|
||||
source: brief.md
|
||||
text: "A2: 可复现导出202条基线作物;ID及必要数值对应源静态数据库; 加载校验格式/schema,坏数据明确拒绝。运行时ID唯一,同名拒绝歧义。"
|
||||
result: passed
|
||||
reason:
|
||||
text: export_catalogue.py只读静态库并排序确定性导出;Runtime导出比对、202条目录及坏schema/重复ID/同名歧义测试通过,catalogue.py显式拒绝无效输入。
|
||||
truncated: false
|
||||
- id: A3
|
||||
source: brief.md
|
||||
text: "A3: 新注册得到500农场币、0点券、0经验、6空地;重复注册不增发, 不同作用域隔离;旧初始3块账号仅补3块,并发/重试/重启不重复补偿。"
|
||||
result: passed
|
||||
reason:
|
||||
text: repository.py:33注册500币和6空地;application.py:61从注册来源识别旧初始3块并以固定幂等事务补3块;正式注册/隔离/并发/故障/重启补地测试通过。
|
||||
truncated: false
|
||||
- id: A4
|
||||
source: brief.md
|
||||
text: "A4: 购买、播种扣款/入库/耗种正确;0、负数、bool、超大数量、余额、 库存或等级不足及未知作物明确失败,没有部分写入。"
|
||||
result: passed
|
||||
reason:
|
||||
text: application.py:177购买与208播种校验目录、数量、余额、库存并在事务内修改;正式非法数量、未知作物、等级/余额不足、耗种与回滚测试通过。
|
||||
truncated: false
|
||||
- id: A5
|
||||
source: brief.md
|
||||
text: "A5: 未成熟不能收获,成熟只增加本次产量/经验;收获前100、本次20 后为120,播种经验另算。逐地加成顺序无关,多季/枯萎/铲除及成熟
|
||||
边界遵守保存定义及可注入时钟。"
|
||||
result: passed
|
||||
reason:
|
||||
text: application.py:251按保存的plot.plant逐地结算本次经验和产量、成熟边界与再生长;test_transactions中冻结定义、逐地加成、历史经验增量、多季和铲除测试通过。
|
||||
truncated: false
|
||||
- id: A6
|
||||
source: brief.md
|
||||
text: "A6: 余额100、出售收益20后为120;锁定限制指定和全部出售; 返回真实余额及安全文本,不出现模板参数缺失。"
|
||||
result: passed
|
||||
reason:
|
||||
text: application.py:437统一保护锁定库存且只加本次收益;正式100余额出售20得120、两种出售入口锁定及安全文本测试通过。
|
||||
truncated: false
|
||||
- id: A7
|
||||
source: brief.md
|
||||
text: "A7: 故障注入使资产/地块/流水/结果整体回滚;重试返回原结果, 参数冲突拒绝;并发不透支/重复领奖,故障不伪装成功,重启后可读。"
|
||||
result: passed
|
||||
reason:
|
||||
text: storage.py:353 BEGIN
|
||||
IMMEDIATE将状态、流水与幂等结果一起提交,异常整体回滚;正式故障注入、参数冲突、取消重放、并发扣款/收获与重启测试通过。
|
||||
truncated: false
|
||||
- id: A8
|
||||
source: brief.md
|
||||
text: "A8: 离线测试、运行入口与说明可执行;文档区分源码、模拟、离线 产物与真实宿主/远程验证,准确记录当前实现与待办。"
|
||||
result: passed
|
||||
reason:
|
||||
text: 新增docs/zhenxun-preview-current.md与实际构建、verify_foundation及安装入口一致;docs索引明确11为历史,09/10和发布README已说明6地、当前字体、内置作物与待办,并区分本机/模拟/真实验证。
|
||||
truncated: false
|
||||
- id: A9
|
||||
source: brief.md
|
||||
text: "A9: 图片工具支持透明合成、缩放、裁剪、测量/省略及三格式编码; 渲染不联网或导入框架,核心无Pillow依赖;安装后提供匹配外部资源可绘图。"
|
||||
result: passed
|
||||
reason:
|
||||
text: canvas.py支持透明、缩裁、测量省略及三编码;assets/renderer不导入框架或联网,准备资源在接入层;正式Canvas及临时wheel绘图测试通过。
|
||||
truncated: false
|
||||
- id: A10
|
||||
source: brief.md
|
||||
text: "A10: 一次快照与统一时间绘制空地、生长、成熟、再生长、枯萎及 锁定状态,采用当前地块映射及统一定位,普通/高清输出可解码。"
|
||||
result: passed
|
||||
reason:
|
||||
text: layout.py纯消费快照和now构造空/生长/成熟/再生长/枯萎状态,renderer使用当前映射;正式阶段、目标尺寸、作物统一落点回归通过,render-decode验证6幅普通/高清图片。
|
||||
truncated: false
|
||||
- id: A11
|
||||
source: brief.md
|
||||
text: "A11: 小图从目标尺寸绘制,素材与成图缓存有界且不污染共享图片; 阶段/成熟边界、余额、头像及身份变化不会复用过期图。"
|
||||
result: passed
|
||||
reason:
|
||||
text: assets.py有界ByteLRU并返回图片副本;renderer请求键含场景、身份摘要、配置、格式和头像摘要;正式预算/共享图片安全/阶段成熟边界及余额头像身份失效测试通过。
|
||||
truncated: false
|
||||
- id: A12
|
||||
source: brief.md
|
||||
text: "A12: 后台合成编码限制并发、相同请求合并;取消不提前释放名额, 关闭等待任务排空;日志仅记录耗时、尺寸、缓存等安全信息。"
|
||||
result: passed
|
||||
reason:
|
||||
text: service.py一次snapshot、to_thread、Semaphore及shield共享任务,关闭排空;正式事件循环不阻塞、并发上限、取消保留容量和失败重试测试通过;源码日志未输出身份或快照。
|
||||
truncated: false
|
||||
- id: A13
|
||||
source: brief.md
|
||||
text: "A13: 离线样例和真实测量覆盖尺寸/编码、冷暖缓存及调用方式; 图片可解码,明确本机测量不等同真实平台上传性能。"
|
||||
result: passed
|
||||
reason:
|
||||
text: render_demo.py提供合成样例和冷暖缓存测量;本轮benchmark明确Windows/Python3.12.14/Pillow12.3及非上传范围,绑定Runtime验证1024/1536三格式六文件可解码且暖缓存全命中。
|
||||
truncated: false
|
||||
- id: A14
|
||||
source: brief.md
|
||||
text: "A14: 真寻原指令、路由、身份/幂等、异常反馈与启动关闭通过模拟验证; 字体显式路径优先、默认校验下载,资源失败文字可用,不操作真实宿主。"
|
||||
result: passed
|
||||
reason:
|
||||
text: commands.py注册原指令与启动关闭、身份和稳定消息标识;runtime.py默认校验资源/字体、显式字体优先、失败文字保留;本候选模拟路由/生命周期/异常/幂等正式测试通过。
|
||||
truncated: false
|
||||
- id: A15
|
||||
source: brief.md
|
||||
text: "A15: 白名单真寻插件包包含私有共享模块、轻量数据、许可和来源哈希,
|
||||
不含其他适配、legacy、测试、玩家数据或大型资源ZIP;准确标识脏源码, 相同输入可复现,构建不改源码。"
|
||||
result: passed
|
||||
reason:
|
||||
text: build_zhenxun.py白名单生成私有_vendor、轻量数据许可与来源哈希,排除assets.zip并固定ZIP时间排序、记录dirty;正式可复现完整包及私有导入测试通过。
|
||||
truncated: false
|
||||
- id: A16
|
||||
source: brief.md
|
||||
text: "A16: 离线验证内容、导入与路由;安装拒绝穿越、坏哈希和已有目标, 失败无半成品,不操作旧插件/玩家数据或启动真实机器人。"
|
||||
result: passed
|
||||
reason:
|
||||
text: verify_zhenxun.py拒绝穿越、重复、链接和哈希差异;install_zhenxun.py先校验、临时写入且拒绝已有目标;正式坏包/安装/目标保留及离线导入路由测试通过。
|
||||
truncated: false
|
||||
- id: A17
|
||||
source: brief.md
|
||||
text: "A17: 工作流检查、构建、上传;push main或显式publish输入可触发固定
|
||||
真寻仓库main普通同步,PR不发布;脚本默认dry-run、不force,冲突失败, 凭据不落盘。本次只检查与模拟,不执行真实发布。"
|
||||
result: passed
|
||||
reason:
|
||||
text: 工作流默认只读且PR无发布路径,main
|
||||
push或明确publish输入经test/build触发固定目标;publish_zhenxun.py默认dry-run、不force、并发冲突失败,正式模拟和workflow-guards通过。
|
||||
truncated: false
|
||||
- id: A18
|
||||
source: brief.md
|
||||
text: "A18: 本机离线打包与安全边界检查通过;静态核对Windows/Linux与
|
||||
Pillow10.4/12.3矩阵,明确本机环境及未运行矩阵;提供安装、依赖、
|
||||
字体、数据库、回退/卸载与Secrets说明,不宣称真实平台已验证。"
|
||||
result: passed
|
||||
reason:
|
||||
text: 当前说明覆盖生成包/requirements、停止旧插件、字体、数据目录、日志、备份回退卸载和Secret;与源码及Windows/Linux/Pillow10.4/12.3矩阵一致,明确1项跳过和未运行矩阵、真实宿主/远端边界;离线包安全测试通过。
|
||||
truncated: false
|
||||
- id: A19
|
||||
source: brief.md
|
||||
text: "A19: 图片质量默认75,整数1和95有效,0、96、bool及非整数拒绝; 配置覆盖图片编码并区分缓存。仅我的农场允许高清,质量不低于85与
|
||||
配置值,其他查询拒绝高清,分页/文字可用,帮助和配置说明一致。"
|
||||
result: passed
|
||||
reason:
|
||||
text: RenderOptions严格整数1至95、默认75;Runtime请求读取质量,仅farm高清1536且max(质量,85),其他查询拒绝高清;正式边界、逐编码器传递、质量缓存与帮助测试通过。
|
||||
truncated: false
|
||||
builder_handoff:
|
||||
candidate_id: 2e7d9479-6e65-480f-b4df-85f937519b86
|
||||
identity_provider: skill-coordinated
|
||||
builder_execution_ref: skill-coordinated:builder:0eff5a20-cba2-445f-95de-405dd2ee7c01
|
||||
iteration: 2
|
||||
summary:
|
||||
text: 修复独立验收A8/A18:新增docs/zhenxun-preview-current.md,补齐构建、部署依赖、安装配置、数据日志、备份回退卸载、Secrets及真实验证边界;docs索引明确旧docs/11为历史,更新09/10现行规则与发布README,取消签到/在线作物更新误报。无业务代码变更;重新核对全部19项,保留原17项证据。
|
||||
truncated: false
|
||||
addressed_acceptance_ids:
|
||||
- A1
|
||||
- A2
|
||||
- A3
|
||||
- A4
|
||||
- A5
|
||||
- A6
|
||||
- A7
|
||||
- A8
|
||||
- A9
|
||||
- A10
|
||||
- A11
|
||||
- A12
|
||||
- A13
|
||||
- A14
|
||||
- A15
|
||||
- A16
|
||||
- A17
|
||||
- A18
|
||||
- A19
|
||||
acceptance_review:
|
||||
- id: A1
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- scripts/check_baseline.py
|
||||
- tests/test_packaging.py
|
||||
note: 已逐项核对当前源码与对应测试覆盖;此声明不是测试通过,正式结果由Runtime与独立Verifier给出。
|
||||
- id: A2
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- scripts/export_catalogue.py
|
||||
- tests/test_domain.py
|
||||
- tests/test_crop_identity.py
|
||||
note: 已逐项核对当前源码与对应测试覆盖;此声明不是测试通过,正式结果由Runtime与独立Verifier给出。
|
||||
- id: A3
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- src/farm_core/repository.py:33
|
||||
- tests/test_transactions.py
|
||||
- tests/test_land.py
|
||||
note: 已逐项核对当前源码与对应测试覆盖;此声明不是测试通过,正式结果由Runtime与独立Verifier给出。
|
||||
- id: A4
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- src/farm_core/application.py:177
|
||||
- tests/test_transactions.py
|
||||
note: 已逐项核对当前源码与对应测试覆盖;此声明不是测试通过,正式结果由Runtime与独立Verifier给出。
|
||||
- id: A5
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- src/farm_core/application.py:251
|
||||
- tests/test_transactions.py
|
||||
note: 已逐项核对当前源码与对应测试覆盖;此声明不是测试通过,正式结果由Runtime与独立Verifier给出。
|
||||
- id: A6
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- src/farm_core/application.py:437
|
||||
- tests/test_transactions.py
|
||||
note: 已逐项核对当前源码与对应测试覆盖;此声明不是测试通过,正式结果由Runtime与独立Verifier给出。
|
||||
- id: A7
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- src/farm_core/storage.py:353
|
||||
- tests/test_transactions.py
|
||||
note: 已逐项核对当前源码与对应测试覆盖;此声明不是测试通过,正式结果由Runtime与独立Verifier给出。
|
||||
- id: A8
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- docs/zhenxun-preview-current.md
|
||||
- docs/README.md
|
||||
- docs/09-foundation.md
|
||||
- docs/10-rendering.md
|
||||
- readme/zhenxun.md
|
||||
- scripts/verify_foundation.py
|
||||
note: 已对照源码修正历史描述,提供完整当前使用说明并清晰区分历史记录和本轮验证;原业务行为未变,供新的独立Verifier复核。
|
||||
- id: A9
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- src/farm_render/canvas.py
|
||||
- src/farm_render/assets.py
|
||||
- tests/test_packaging.py
|
||||
- tests/test_render.py
|
||||
note: 已逐项核对当前源码与对应测试覆盖;此声明不是测试通过,正式结果由Runtime与独立Verifier给出。
|
||||
- id: A10
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- src/farm_render/renderer.py
|
||||
- tests/test_crop_positions.py
|
||||
- tests/test_render.py
|
||||
note: 已逐项核对当前源码与对应测试覆盖;此声明不是测试通过,正式结果由Runtime与独立Verifier给出。
|
||||
- id: A11
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- src/farm_render/assets.py
|
||||
- tests/test_render.py
|
||||
note: 已逐项核对当前源码与对应测试覆盖;此声明不是测试通过,正式结果由Runtime与独立Verifier给出。
|
||||
- id: A12
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- src/farm_render/service.py
|
||||
- tests/test_render.py
|
||||
note: 已逐项核对当前源码与对应测试覆盖;此声明不是测试通过,正式结果由Runtime与独立Verifier给出。
|
||||
- id: A13
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- runtime/archive-foundation-20261026/benchmark.json
|
||||
- scripts/render_demo.py
|
||||
- docs/10-rendering.md
|
||||
note: 本轮Windows/Python3.12.14/Pillow12.3实测六种输出,1024/1536三格式冷暖缓存均生成;正式Verifier复核解码与报告。
|
||||
- id: A14
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- integrations/shared/runtime.py
|
||||
- tests/test_zhenxun_runtime.py
|
||||
note: 已逐项核对当前源码与对应测试覆盖;此声明不是测试通过,正式结果由Runtime与独立Verifier给出。
|
||||
- id: A15
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- scripts/build_zhenxun.py
|
||||
- tests/test_zhenxun_package.py
|
||||
note: 已逐项核对当前源码与对应测试覆盖;此声明不是测试通过,正式结果由Runtime与独立Verifier给出。
|
||||
- id: A16
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- scripts/verify_zhenxun.py
|
||||
- scripts/install_zhenxun.py
|
||||
- tests/test_zhenxun_package.py
|
||||
note: 已逐项核对当前源码与对应测试覆盖;此声明不是测试通过,正式结果由Runtime与独立Verifier给出。
|
||||
- id: A17
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- .github/workflows/zhenxun-preview.yml
|
||||
- scripts/publish_zhenxun.py
|
||||
- tests/test_zhenxun_package.py
|
||||
note: 已逐项核对当前源码与对应测试覆盖;此声明不是测试通过,正式结果由Runtime与独立Verifier给出。
|
||||
- id: A18
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- docs/zhenxun-preview-current.md
|
||||
- docs/README.md
|
||||
- docs/09-foundation.md
|
||||
- docs/10-rendering.md
|
||||
- readme/zhenxun.md
|
||||
- .github/workflows/zhenxun-preview.yml
|
||||
- tests/test_zhenxun_package.py
|
||||
note: 已对照源码修正历史描述,提供完整当前使用说明并清晰区分历史记录和本轮验证;原业务行为未变,供新的独立Verifier复核。
|
||||
- id: A19
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- src/farm_render/models.py
|
||||
- integrations/shared/runtime.py
|
||||
- tests/test_zhenxun_runtime.py
|
||||
note: 已逐项核对当前源码与对应测试覆盖;此声明不是测试通过,正式结果由Runtime与独立Verifier给出。
|
||||
checks:
|
||||
- name:
|
||||
text: documentation-diff
|
||||
truncated: false
|
||||
result: passed
|
||||
note:
|
||||
text: git diff --check退出0;更新内容仅文档/正式规格/发布目标记录。
|
||||
truncated: false
|
||||
checks_truncated: false
|
||||
known_limits:
|
||||
- text: 仅本地Windows/Python3.12.14/Pillow12.3;其他CI矩阵未执行。
|
||||
truncated: false
|
||||
- text: 未访问真实玩家库、未启动宿主、未推送或部署。
|
||||
truncated: false
|
||||
- text: 历史455个作物阶段缺图引用保留占位行为。
|
||||
truncated: false
|
||||
- text: 作物Service迁移尚未实施,另仓库设计不纳入归档。
|
||||
truncated: false
|
||||
known_limits_truncated: false
|
||||
review: null
|
||||
submitted_at: 2026-10-26T04:35:00.062Z
|
||||
blockers: []
|
||||
verification:
|
||||
candidate_id: 2e7d9479-6e65-480f-b4df-85f937519b86
|
||||
identity_provider: skill-coordinated
|
||||
verifier_execution_ref: skill-coordinated:verifier:dbb1408c-96f4-4483-8af0-3b2fbe54af09
|
||||
iteration: 2
|
||||
attempt: 1
|
||||
assurance: skill-coordinated
|
||||
verdict: pass
|
||||
checks:
|
||||
- id: foundation-offline
|
||||
name:
|
||||
text: 完整离线核心/绘图/三端打包回归
|
||||
truncated: false
|
||||
argv_display:
|
||||
- text: -NoProfile
|
||||
truncated: false
|
||||
- text: -Command
|
||||
truncated: false
|
||||
- text: $env:FARM_TEST_FONT='D:\Work\farm_service\fonts\Xiaolai-Regular.ttf';
|
||||
$env:PYTHONIOENCODING='utf-8'; &
|
||||
'D:\Work\farm\.release-cache\ci-aiohttp-20261023\Scripts\python.exe'
|
||||
scripts/verify_foundation.py; exit $LASTEXITCODE
|
||||
truncated: false
|
||||
argv_truncated: false
|
||||
cwd_ref: .
|
||||
status: passed
|
||||
exit_code: 0
|
||||
duration_ms: 107377
|
||||
- id: workflow-guards
|
||||
name:
|
||||
text: 工作流权限与发布边界静态检查
|
||||
truncated: false
|
||||
argv_display:
|
||||
- text: scripts/check_workflows.py
|
||||
truncated: false
|
||||
argv_truncated: false
|
||||
cwd_ref: .
|
||||
status: passed
|
||||
exit_code: 0
|
||||
duration_ms: 110
|
||||
- id: render-decode
|
||||
name:
|
||||
text: 本轮六种绘图样例与暖缓存验证
|
||||
truncated: false
|
||||
argv_display:
|
||||
- text: -c
|
||||
truncated: false
|
||||
- text: import json; from pathlib import Path; from PIL import Image;
|
||||
p=Path('runtime/archive-foundation-20261026');
|
||||
d=json.loads((p/'benchmark.json').read_text(encoding='utf-8'));
|
||||
assert len(d['measurements'])==6; assert all(r['warm_cache_hit'] for
|
||||
r in d['measurements']); files=list(p.glob('farm-*')); assert
|
||||
len(files)==6; [Image.open(f).verify() for f in files]; print('6
|
||||
rendered files verified; all warm cache hits')
|
||||
truncated: false
|
||||
argv_truncated: false
|
||||
cwd_ref: .
|
||||
status: passed
|
||||
exit_code: 0
|
||||
duration_ms: 122
|
||||
summary:
|
||||
text: 新的独立只读Verifier完整核对brief四核心结构、三Spec与A1-A19,直接源码补齐过期图覆盖,并核查本candidate绑定Runtime三检查均exit0;复用219项离线记录(218通过、1项明确跳过)、工作流守卫和六图解码/暖缓存证据。A8/A18现行文档已与实现对齐,其余17项逐项核查通过;最后参考Builder交接,未重跑完整套件、修改业务/测试/文档/报告或操作玩家数据,未代用户接受或归档。
|
||||
truncated: false
|
||||
risks:
|
||||
- text: 正式完整测试共219项,218通过;1项NoneBot宿主配置测试因本地缺Pydantic跳过,不计作通过。该项不属于本change真寻宿主配置验收。
|
||||
truncated: false
|
||||
- text: 仅本机Windows/Python3.12.14/Pillow12.3;工作流矩阵仅静态核对,Linux及Pillow10.4未在本轮运行。
|
||||
truncated: false
|
||||
- text: 未启动真实机器人、访问真实玩家库、验证公开下载、执行远端CI、发布或部署;历史455个缺图引用仍使用明确占位。
|
||||
truncated: false
|
||||
- text: 图索引为2026-10-15,相关路径过期或无覆盖;本轮以直接源码和正式日志补齐,未据旧图推断缺失。
|
||||
truncated: false
|
||||
- text: 作物Service迁移不属于本次归档范围且尚未实施。
|
||||
truncated: false
|
||||
risks_truncated: false
|
||||
completed_at: 2026-10-26T04:40:15.168Z
|
||||
history:
|
||||
- goal_cycle: 1
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: pass
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 独立只读验收完成:完整brief/Spec、实现、测试、配置和文档均已核对,最后参考Builder交接;绑定候选的Runtime
|
||||
foundation-offline以退出码0完成,827基线文件、可复现目录导出与32项测试通过。A1-A8全部通过;未追加或自行重跑完整检查,未修改项目源文件。
|
||||
truncated: false
|
||||
completed_at: 2026-10-15T07:00:29.324Z
|
||||
- goal_cycle: 1
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 用户明确要求在已确认的图片工具及绘图优化方案上执行开发;将共享农场渲染作为当前基础开发的范围补充,保留原业务验收项并追加图片验收。
|
||||
truncated: false
|
||||
completed_at: 2026-10-15T07:55:29.688Z
|
||||
- goal_cycle: 2
|
||||
iteration: 0
|
||||
attempt: 0
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: Native confirmed acceptance criteria changed
|
||||
truncated: false
|
||||
completed_at: 2026-10-15T08:23:15.134Z
|
||||
- goal_cycle: 3
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: pass
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 独立只读核对全部13项、完整规格、核心与渲染源码、测试/打包/导出脚本、正式Runtime日志和图片样例;复用匹配候选的45项正式检查,全部验收通过。图索引新增渲染未覆盖且旧文件metadata_changed,已直接源文件补齐,无需重复完整套件。
|
||||
truncated: false
|
||||
completed_at: 2026-10-15T08:40:00.608Z
|
||||
- goal_cycle: 3
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 用户选择补齐真寻测试版:命令接入、打包工作流和安装脚本;将现有核心与渲染扩展为可安装真寻预览包,保留独立测试库和旧基线,不执行远端操作。
|
||||
truncated: false
|
||||
completed_at: 2026-10-15T09:24:24.236Z
|
||||
- goal_cycle: 4
|
||||
iteration: 0
|
||||
attempt: 0
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: Native confirmed acceptance criteria changed
|
||||
truncated: false
|
||||
completed_at: 2026-10-15T09:28:23.268Z
|
||||
- goal_cycle: 5
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: pass
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 独立只读核对完整brief、三份规格及18项验收,审阅核心/渲染、真寻接入、白名单构建/校验/安装/固定发布边界、工作流和测试文档。复用本候选正式Runtime的55项全回归、Pillow10.4下23项兼容检查及工作流守卫,全部通过;未重复完整套件。图索引旧/新增缺覆盖,已直接源码补齐。未修改实现或正式规格,未访问远端、安装/启动机器人或代用户接受。
|
||||
truncated: false
|
||||
completed_at: 2026-10-15T09:51:26.860Z
|
||||
- goal_cycle: 5
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 独立验收指出余额不足错误码映射缺少insufficient_funds;保持已确认范围,补中文提示及回归测试后重新交付。
|
||||
truncated: false
|
||||
completed_at: 2026-10-15T09:52:02.989Z
|
||||
- goal_cycle: 5
|
||||
iteration: 2
|
||||
attempt: 1
|
||||
outcome: pass
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 独立只读核对完整brief、三份规格和18项,审阅核心/渲染/真寻接入、白名单构建/校验/安装/固定发布边界、测试工作流与文档。当前candidate绑定Runtime的56项完整回归、Pillow10.4下24项及工作流守卫均退出码0;日志前缀658580e0与当前cedb操作绑定已核实。额外只读确认最终ZIP30文件且runtime匹配修复源码,余额不足中文提示与无扣款回归通过。图索引过期/缺覆盖已直接源码补齐,未重复完整套件、修改实现或正式文件、远端操作、安装启动宿主或代用户接受。
|
||||
truncated: false
|
||||
completed_at: 2026-10-15T09:58:23.989Z
|
||||
- goal_cycle: 5
|
||||
iteration: 2
|
||||
attempt: 1
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 用户实际测试要求默认字体:在真寻适配层默认复用宿主字体目录,保留显式字体覆盖和缺失时清楚提示;修复预览版默认不可绘图的接入遗漏。
|
||||
truncated: false
|
||||
completed_at: 2026-10-16T02:06:48.036Z
|
||||
- goal_cycle: 5
|
||||
iteration: 3
|
||||
attempt: 1
|
||||
outcome: pass
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 独立只读验收全部A1-A18:完整brief/三spec与当前字体实现、测试、文档已核对;复用此前未改变核心/渲染/发布的正式独立验收,并核实本候选绑定Runtime三检查exit0,实际58项完整与Pillow10.4下26项通过。真寻FONT_PATH为resources/font,接入层默认选宿主字体,坏默认跳过、显式覆盖优先,共享层未引入真寻路径;安装目录runtime/commands与主源SHA256一致。图索引不覆盖新文件或metadata_changed,已读源码补齐;未重跑完整测试、未修改项目文件、未启动宿主或访问远端。
|
||||
truncated: false
|
||||
completed_at: 2026-10-16T02:13:06.574Z
|
||||
- goal_cycle: 5
|
||||
iteration: 3
|
||||
attempt: 1
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 用户要求修复迁移遗漏:恢复旧版用户面板素材、布局与经验进度语义并保留性能优化。
|
||||
truncated: false
|
||||
completed_at: 2026-10-16T02:17:50.690Z
|
||||
- goal_cycle: 5
|
||||
iteration: 4
|
||||
attempt: 0
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: "Builder handoff Runtime checks failed: all-offline"
|
||||
truncated: false
|
||||
completed_at: 2026-10-16T02:26:17.238Z
|
||||
- goal_cycle: 5
|
||||
iteration: 5
|
||||
attempt: 1
|
||||
outcome: pass
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 独立只读核对全部A1-A18、完整brief/三份Spec、相关实现与测试及本候选Runtime绑定。复用60项完整检查、Pillow10.4下28项及工作流守卫,均退出码0;未重跑完整套件。原面板底图/头像框/头像尺寸和信息已恢复,用户授权的槽位坐标校正经源码与1536样图核验;preview.3最终ZIP30文件与当前安装目录逐文件相同,renderer匹配源码。未修改实现/正式规格、操作远端、真实玩家库或启动宿主。
|
||||
truncated: false
|
||||
completed_at: 2026-10-16T02:31:02.032Z
|
||||
- goal_cycle: 5
|
||||
iteration: 5
|
||||
attempt: 1
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 用户明确要求执行统一图片质量配置,默认75、范围1至95,仅我的农场允许高清覆盖;其他图片禁止高清,保留仓库文字和分页。修订用户可见要求。
|
||||
truncated: false
|
||||
completed_at: 2026-10-22T09:37:42.325Z
|
||||
- goal_cycle: 6
|
||||
iteration: 1
|
||||
attempt: 0
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: "Native formal documents are invalid (brief-structure-missing: brief.md)"
|
||||
truncated: false
|
||||
completed_at: 2026-10-26T04:22:42.893Z
|
||||
- goal_cycle: 7
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: fail
|
||||
unresolved_ids:
|
||||
- A8
|
||||
- A18
|
||||
summary:
|
||||
text: 新的独立只读Verifier核对完整brief、三Spec及全部19项;brief四核心结构有效。复用当前candidate绑定Runtime三项退出0检查,未重跑完整套件。17通过,A8和A18因现行文档陈旧/缺失失败,修复说明后再验收;无需据此修改业务。独立核验完成后最后参考Builder交接,其文档证据不足。未修改业务、测试、规格、报告或玩家数据,未接受结果或归档。
|
||||
truncated: false
|
||||
completed_at: 2026-10-26T04:31:34.844Z
|
||||
- goal_cycle: 7
|
||||
iteration: 2
|
||||
attempt: 1
|
||||
outcome: pass
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 新的独立只读Verifier完整核对brief四核心结构、三Spec与A1-A19,直接源码补齐过期图覆盖,并核查本candidate绑定Runtime三检查均exit0;复用219项离线记录(218通过、1项明确跳过)、工作流守卫和六图解码/暖缓存证据。A8/A18现行文档已与实现对齐,其余17项逐项核查通过;最后参考Builder交接,未重跑完整套件、修改业务/测试/文档/报告或操作玩家数据,未代用户接受或归档。
|
||||
truncated: false
|
||||
completed_at: 2026-10-26T04:40:15.168Z
|
||||
history_overflow:
|
||||
dropped_entries: 0
|
||||
first_dropped_at: null
|
||||
last_dropped_at: null
|
||||
outcome_counts:
|
||||
pass: 0
|
||||
fail: 0
|
||||
blocked: 0
|
||||
execution-error: 0
|
||||
recovery: 0
|
||||
verification_result: pass
|
||||
verification_report: verification.md
|
||||
archived: true
|
||||
created_at: 2026-10-15T06:34:33.214Z
|
||||
+8
-3
@@ -18,7 +18,8 @@ Acceptance: A1
|
||||
|
||||
## Catalogue
|
||||
|
||||
源静态目录 202 条作物名称唯一;映射 id/name/level/buy/isVip/vipBuy/
|
||||
迁移基线含202条作物且当时名称唯一;运行时以稳定本地ID为身份,
|
||||
允许同名,名称查询拒绝歧义,支持#编号。映射id/name/level/buy/isVip/vipBuy/
|
||||
experience/harvest/price/time/crop/phase 等必要数据,规范整数数值,
|
||||
异常的字段或版本错误明确拒绝。购买使用对应币种,按等级校验。
|
||||
不下载远程目录,不将素材路径或绘图偏移当作业务依赖。
|
||||
@@ -31,8 +32,10 @@ Acceptance: A2
|
||||
## Registration and account model
|
||||
|
||||
调用方提供 deployment/realm/platform/user 字符串身份,核心不推断账号
|
||||
等价性或跨群共享。首次注册赠送500 point、0 vipPoint、0 exp、3空地。
|
||||
等价性或跨群共享。首次注册赠送500 point、0 vipPoint、0 exp、6空地。
|
||||
唯一身份只能领取一次初始资产。查询总是基于已提交数据库状态。
|
||||
可靠识别为初始3块的旧用户一次性追加3块普通空地,保留已开垦成果;
|
||||
并发、重试和重启不得重复补偿,来源不明和数量异常明确拒绝。
|
||||
经验等级门槛为 200*k*(k+1)//2,使用整数边界计算。
|
||||
货币、数量和时间范围必须适合 SQLite 整数及应用限制。
|
||||
bool 不作为合法整数参数,不允许负数归零后当作交易成功。
|
||||
@@ -57,7 +60,9 @@ Acceptance: A4
|
||||
|
||||
首熟为播种时间加 time*3600;使用可注入时钟,当前时间等于成熟时可收。
|
||||
每块地独立计算 floor(base*(100+bonus)/100) 后相加,不对累计值加成。
|
||||
经验只增加本次增量。产物入库、经验变动、收获次数和下一季状态
|
||||
经验只增加本次增量。播种每地另加2经验、铲除每地另加2经验;
|
||||
“历史100”指收获操作前已提交的经验。
|
||||
产物入库、经验变动、收获次数和下一季状态
|
||||
在同一事务提交。最后一季后枯萎;铲除只清理枯萎地块。
|
||||
再次重试不重复收获。展示结果不再次修改数据。
|
||||
|
||||
+12
-5
@@ -5,15 +5,19 @@
|
||||
farm_render 使用 Pillow 实现同步画布操作;farm_core 不依赖 Pillow。
|
||||
工具支持透明合成、复制、缩放、裁剪、文本测量与超长省略,以及
|
||||
WebP/JPEG/PNG 编码。JPEG 合并透明背景,输出携带 MIME、尺寸与字节。
|
||||
WebP 默认采用有损质量75、method=2;高清质量85。格式与尺寸独立,
|
||||
WebP 默认采用有损质量75、method=2;高清质量为max(配置质量,85)。格式与尺寸独立,
|
||||
auto 根据调用方声明的平台能力选择 WebP 或 JPEG,本机无 WebP 编码
|
||||
支持时 auto 回退 JPEG,显式 WebP 则清楚报错。PNG 保留无损选项。
|
||||
素材独立打入安装包,不在运行时引用 legacy 或宿主;字体显式配置,
|
||||
不复制 Windows 商业字体。无字体时明确报错,不输出缺字占位图。
|
||||
开发素材独立生成,不在运行时引用 legacy 或宿主。插件携带轻量元数据
|
||||
及绑定摘要的素材清单,大型 assets.zip 从所选源校验下载并缓存,
|
||||
不打入插件发布包;小型升级土地素材仍随包提供。中文字体支持显式
|
||||
配置,或独立校验下载原版小赖字体及完整 OFL 许可,不复制 Windows
|
||||
商业字体。无可用素材或字体时接入层降级文字,不输出缺字占位图。
|
||||
|
||||
Scenario: Framework-independent packaged renderer
|
||||
Acceptance: A9
|
||||
从 wheel 安装到临时目录,无 legacy 和框架仍能加载素材并生成图片。
|
||||
从 wheel 安装到临时目录,无 legacy 和框架,显式提供已校验的外部
|
||||
素材及字体后仍可生成图片;下载与校验止于接入/资源准备层。
|
||||
|
||||
## Snapshot and layout
|
||||
|
||||
@@ -22,7 +26,10 @@ Acceptance: A9
|
||||
首季阶段锚点由保存的 mature_at-growth_seconds 推导,再季使用
|
||||
mature_at-regrowth_seconds,按本季时长映射展示阶段,成熟优先、
|
||||
枯萎优先于成熟。阶段仅影响外观,不修改交易规则和数据库。
|
||||
读取实际 offsetX/Y/W/H 字段;缺失作物资源使用显式占位并记录警告。
|
||||
作物定位采用 schema=4 的 visible_contact_center 统一公式,按本地
|
||||
土地中心落点,保留完整纹理和直立等比缩放,不使用旧 offset 字段
|
||||
或逐阶段手调锚点。地块位置按当前稳定编号映射。缺失作物资源使用
|
||||
显式占位并记录警告。
|
||||
用户信息显示真实快照余额、点券和经验,头像仅接收字节,无头像时
|
||||
绘制默认图;拒绝超大或无效头像并降级。主题由调用方明确提供。
|
||||
|
||||
+13
-8
@@ -16,7 +16,8 @@ Acceptance: A19
|
||||
不共享成图。农场高清为1536宽,质量至少85且不低于配置;其他命令高清
|
||||
返回明确参数错误,仓库分页和文字仍可用,帮助不再宣称其他命令支持高清。
|
||||
|
||||
仅提供真寻接入;业务和图片仍来自主仓库共享源码。接入层加载插件
|
||||
本 capability 验收真寻接入;NoneBot/AstrBot 已有独立接入与打包,
|
||||
不将其当前存在误报为未实现。业务和图片仍来自主仓库共享源码。接入层加载插件
|
||||
私有_vendor命名空间,渲染层对farm_core的引用在构建时改为私有相对
|
||||
导入,不修改开发源码、不污染全局sys.path或sys.modules别名。
|
||||
命令不增加测试前缀,沿用开通农场、我的农场、农场帮助、我的种子、
|
||||
@@ -27,8 +28,10 @@ Acceptance: A19
|
||||
绑定;测试数据位置与旧版不同。事件标识包含机器人、平台、用户及
|
||||
原消息标识;无法获得稳定消息标识时写操作明确拒绝,不能随机重试。
|
||||
在启动时初始化存储、渲染器,在关闭时等待任务排空后关闭存储;字体
|
||||
必须配置为部署方拥有使用权的中文字体。无字体时文本功能仍可用,
|
||||
我的农场返回明确配置提示。头像失败降级,不让头像下载无限等待。
|
||||
支持显式配置合法中文字体,或校验下载原版小赖字体及完整许可。
|
||||
资源源可选 Github、Gitea、farm_service,默认 Github;服务地址独立
|
||||
保留。缺少资源或字体时保留文字功能,日志明确原因。头像失败降级,
|
||||
不让头像下载无限等待。本次不包含作物目录远程更新。
|
||||
|
||||
Scenario: 真寻命令与生命周期
|
||||
Acceptance: A14
|
||||
@@ -38,7 +41,8 @@ Acceptance: A14
|
||||
## 产物
|
||||
|
||||
scripts/build_zhenxun.py按白名单复制真寻接入、farm_core、farm_render、
|
||||
对应数据/图片、LICENSE及NOTICE。ZIP含单一插件根目录,仓库导出目录
|
||||
对应轻量数据、升级土地素材、LICENSE及NOTICE;大型图片及字体 ZIP
|
||||
不进入插件包。ZIP含单一插件根目录,仓库导出目录
|
||||
可作为真寻发布仓库根。生成来源manifest与独立SHA256附件,清单不
|
||||
自包含哈希;本地工作区脏时标识dirty,不能冒充源HEAD的纯净构建。
|
||||
输入版本严格校验,ZIP排序/时间固定,目标存在则拒绝或显式选择新路径。
|
||||
@@ -64,10 +68,11 @@ Acceptance: A16
|
||||
|
||||
主仓库CI在PR/push及手动触发时执行检查,构建ZIP/清单/校验附件并上传
|
||||
Actions artifact。工作流权限默认只读;不可信PR不触发跨仓库写入。
|
||||
跨仓库同步是独立的手动选择,使用专用Secret/受保护environment,并
|
||||
固定目标Shu-Ying/zhenxun_plugin_farm的farm-preview分支。
|
||||
脚本默认dry-run;只有显式发布才联网。只创建或普通更新预览分支,
|
||||
不force、不碰main/旧标签;并发分支改变时失败。发布仓库只含生成的
|
||||
跨仓库同步依当前工作流,在 main 的 push 或显式 publish 手动输入时
|
||||
执行,PR 不触发;使用专用 Secret/受保护 environment,固定目标为
|
||||
Shu-Ying/zhenxun_plugin_farm 的 main 分支。脚本默认 dry-run,只有显式
|
||||
发布才联网;只进行普通更新,不 force、不修改旧标签;并发分支改变
|
||||
时失败。此次本地归档不执行发布。发布仓库只含生成的
|
||||
可安装内容、来源和验证工具,以及验证工作流,不在那里维护业务源码。
|
||||
下载ZIP测试不要求先同步仓库,不要求用户先给令牌。
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
generated_from_state_version: 50
|
||||
---
|
||||
|
||||
# Verification
|
||||
|
||||
## Current result
|
||||
|
||||
- Result: **Archived**
|
||||
- Verification status: **Checks completed; result confirmed**
|
||||
- Goal cycle: 7
|
||||
- Iteration: 2
|
||||
- Verifier attempt: 1
|
||||
- Completed: 2026-10-26T04:40:15.168Z
|
||||
- Summary: 新的独立只读Verifier完整核对brief四核心结构、三Spec与A1-A19,直接源码补齐过期图覆盖,并核查本candidate绑定Runtime三检查均exit0;复用219项离线记录(218通过、1项明确跳过)、工作流守卫和六图解码/暖缓存证据。A8/A18现行文档已与实现对齐,其余17项逐项核查通过;最后参考Builder交接,未重跑完整套件、修改业务/测试/文档/报告或操作玩家数据,未代用户接受或归档。
|
||||
|
||||
## Acceptance
|
||||
|
||||
| ID | Result | Source | Criterion | Reason |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| A1 | passed | brief.md | A1: 旧基线827文件校验继续通过;核心不导入框架、无导入副作用, 从源码及安装包可独立运行,不在运行时引用legacy。 | 当前Runtime foundation-offline核实827旧基线与源码/安装包隔离测试通过;test_packaging.py校验无宿主导入、无导入副作用及临时wheel独立运行。 |
|
||||
| A2 | passed | brief.md | A2: 可复现导出202条基线作物;ID及必要数值对应源静态数据库; 加载校验格式/schema,坏数据明确拒绝。运行时ID唯一,同名拒绝歧义。 | export_catalogue.py只读静态库并排序确定性导出;Runtime导出比对、202条目录及坏schema/重复ID/同名歧义测试通过,catalogue.py显式拒绝无效输入。 |
|
||||
| A3 | passed | brief.md | A3: 新注册得到500农场币、0点券、0经验、6空地;重复注册不增发, 不同作用域隔离;旧初始3块账号仅补3块,并发/重试/重启不重复补偿。 | repository.py:33注册500币和6空地;application.py:61从注册来源识别旧初始3块并以固定幂等事务补3块;正式注册/隔离/并发/故障/重启补地测试通过。 |
|
||||
| A4 | passed | brief.md | A4: 购买、播种扣款/入库/耗种正确;0、负数、bool、超大数量、余额、 库存或等级不足及未知作物明确失败,没有部分写入。 | application.py:177购买与208播种校验目录、数量、余额、库存并在事务内修改;正式非法数量、未知作物、等级/余额不足、耗种与回滚测试通过。 |
|
||||
| A5 | passed | brief.md | A5: 未成熟不能收获,成熟只增加本次产量/经验;收获前100、本次20 后为120,播种经验另算。逐地加成顺序无关,多季/枯萎/铲除及成熟 边界遵守保存定义及可注入时钟。 | application.py:251按保存的plot.plant逐地结算本次经验和产量、成熟边界与再生长;test_transactions中冻结定义、逐地加成、历史经验增量、多季和铲除测试通过。 |
|
||||
| A6 | passed | brief.md | A6: 余额100、出售收益20后为120;锁定限制指定和全部出售; 返回真实余额及安全文本,不出现模板参数缺失。 | application.py:437统一保护锁定库存且只加本次收益;正式100余额出售20得120、两种出售入口锁定及安全文本测试通过。 |
|
||||
| A7 | passed | brief.md | A7: 故障注入使资产/地块/流水/结果整体回滚;重试返回原结果, 参数冲突拒绝;并发不透支/重复领奖,故障不伪装成功,重启后可读。 | storage.py:353 BEGIN IMMEDIATE将状态、流水与幂等结果一起提交,异常整体回滚;正式故障注入、参数冲突、取消重放、并发扣款/收获与重启测试通过。 |
|
||||
| A8 | passed | brief.md | A8: 离线测试、运行入口与说明可执行;文档区分源码、模拟、离线 产物与真实宿主/远程验证,准确记录当前实现与待办。 | 新增docs/zhenxun-preview-current.md与实际构建、verify_foundation及安装入口一致;docs索引明确11为历史,09/10和发布README已说明6地、当前字体、内置作物与待办,并区分本机/模拟/真实验证。 |
|
||||
| A9 | passed | brief.md | A9: 图片工具支持透明合成、缩放、裁剪、测量/省略及三格式编码; 渲染不联网或导入框架,核心无Pillow依赖;安装后提供匹配外部资源可绘图。 | canvas.py支持透明、缩裁、测量省略及三编码;assets/renderer不导入框架或联网,准备资源在接入层;正式Canvas及临时wheel绘图测试通过。 |
|
||||
| A10 | passed | brief.md | A10: 一次快照与统一时间绘制空地、生长、成熟、再生长、枯萎及 锁定状态,采用当前地块映射及统一定位,普通/高清输出可解码。 | layout.py纯消费快照和now构造空/生长/成熟/再生长/枯萎状态,renderer使用当前映射;正式阶段、目标尺寸、作物统一落点回归通过,render-decode验证6幅普通/高清图片。 |
|
||||
| A11 | passed | brief.md | A11: 小图从目标尺寸绘制,素材与成图缓存有界且不污染共享图片; 阶段/成熟边界、余额、头像及身份变化不会复用过期图。 | assets.py有界ByteLRU并返回图片副本;renderer请求键含场景、身份摘要、配置、格式和头像摘要;正式预算/共享图片安全/阶段成熟边界及余额头像身份失效测试通过。 |
|
||||
| A12 | passed | brief.md | A12: 后台合成编码限制并发、相同请求合并;取消不提前释放名额, 关闭等待任务排空;日志仅记录耗时、尺寸、缓存等安全信息。 | service.py一次snapshot、to_thread、Semaphore及shield共享任务,关闭排空;正式事件循环不阻塞、并发上限、取消保留容量和失败重试测试通过;源码日志未输出身份或快照。 |
|
||||
| A13 | passed | brief.md | A13: 离线样例和真实测量覆盖尺寸/编码、冷暖缓存及调用方式; 图片可解码,明确本机测量不等同真实平台上传性能。 | render_demo.py提供合成样例和冷暖缓存测量;本轮benchmark明确Windows/Python3.12.14/Pillow12.3及非上传范围,绑定Runtime验证1024/1536三格式六文件可解码且暖缓存全命中。 |
|
||||
| A14 | passed | brief.md | A14: 真寻原指令、路由、身份/幂等、异常反馈与启动关闭通过模拟验证; 字体显式路径优先、默认校验下载,资源失败文字可用,不操作真实宿主。 | commands.py注册原指令与启动关闭、身份和稳定消息标识;runtime.py默认校验资源/字体、显式字体优先、失败文字保留;本候选模拟路由/生命周期/异常/幂等正式测试通过。 |
|
||||
| A15 | passed | brief.md | A15: 白名单真寻插件包包含私有共享模块、轻量数据、许可和来源哈希, 不含其他适配、legacy、测试、玩家数据或大型资源ZIP;准确标识脏源码, 相同输入可复现,构建不改源码。 | build_zhenxun.py白名单生成私有_vendor、轻量数据许可与来源哈希,排除assets.zip并固定ZIP时间排序、记录dirty;正式可复现完整包及私有导入测试通过。 |
|
||||
| A16 | passed | brief.md | A16: 离线验证内容、导入与路由;安装拒绝穿越、坏哈希和已有目标, 失败无半成品,不操作旧插件/玩家数据或启动真实机器人。 | verify_zhenxun.py拒绝穿越、重复、链接和哈希差异;install_zhenxun.py先校验、临时写入且拒绝已有目标;正式坏包/安装/目标保留及离线导入路由测试通过。 |
|
||||
| A17 | passed | brief.md | A17: 工作流检查、构建、上传;push main或显式publish输入可触发固定 真寻仓库main普通同步,PR不发布;脚本默认dry-run、不force,冲突失败, 凭据不落盘。本次只检查与模拟,不执行真实发布。 | 工作流默认只读且PR无发布路径,main push或明确publish输入经test/build触发固定目标;publish_zhenxun.py默认dry-run、不force、并发冲突失败,正式模拟和workflow-guards通过。 |
|
||||
| A18 | passed | brief.md | A18: 本机离线打包与安全边界检查通过;静态核对Windows/Linux与 Pillow10.4/12.3矩阵,明确本机环境及未运行矩阵;提供安装、依赖、 字体、数据库、回退/卸载与Secrets说明,不宣称真实平台已验证。 | 当前说明覆盖生成包/requirements、停止旧插件、字体、数据目录、日志、备份回退卸载和Secret;与源码及Windows/Linux/Pillow10.4/12.3矩阵一致,明确1项跳过和未运行矩阵、真实宿主/远端边界;离线包安全测试通过。 |
|
||||
| A19 | passed | brief.md | A19: 图片质量默认75,整数1和95有效,0、96、bool及非整数拒绝; 配置覆盖图片编码并区分缓存。仅我的农场允许高清,质量不低于85与 配置值,其他查询拒绝高清,分页/文字可用,帮助和配置说明一致。 | RenderOptions严格整数1至95、默认75;Runtime请求读取质量,仅farm高清1536且max(质量,85),其他查询拒绝高清;正式边界、逐编码器传递、质量缓存与帮助测试通过。 |
|
||||
|
||||
## Checks
|
||||
|
||||
| Check | Command | Working directory | Status | Exit | Duration |
|
||||
| --- | --- | --- | --- | ---: | ---: |
|
||||
| 完整离线核心/绘图/三端打包回归 | -NoProfile -Command $env:FARM_TEST_FONT='D:\Work\farm_service\fonts\Xiaolai-Regular.ttf'; $env:PYTHONIOENCODING='utf-8'; & 'D:\Work\farm\.release-cache\ci-aiohttp-20261023\Scripts\python.exe' scripts/verify_foundation.py; exit $LASTEXITCODE | . | passed | 0 | 107377 ms |
|
||||
| 工作流权限与发布边界静态检查 | scripts/check_workflows.py | . | passed | 0 | 110 ms |
|
||||
| 本轮六种绘图样例与暖缓存验证 | -c import json; from pathlib import Path; from PIL import Image; p=Path('runtime/archive-foundation-20261026'); d=json.loads((p/'benchmark.json').read_text(encoding='utf-8')); assert len(d['measurements'])==6; assert all(r['warm_cache_hit'] for r in d['measurements']); files=list(p.glob('farm-*')); assert len(files)==6; [Image.open(f).verify() for f in files]; print('6 rendered files verified; all warm cache hits') | . | passed | 0 | 122 ms |
|
||||
|
||||
### Builder-reported evidence
|
||||
|
||||
These are Builder reports, not Runtime check receipts or independent verification results.
|
||||
|
||||
- documentation-diff: passed — git diff --check退出0;更新内容仅文档/正式规格/发布目标记录。
|
||||
- Known limitation: 仅本地Windows/Python3.12.14/Pillow12.3;其他CI矩阵未执行。
|
||||
- Known limitation: 未访问真实玩家库、未启动宿主、未推送或部署。
|
||||
- Known limitation: 历史455个作物阶段缺图引用保留占位行为。
|
||||
- Known limitation: 作物Service迁移尚未实施,另仓库设计不纳入归档。
|
||||
|
||||
## Blockers
|
||||
|
||||
_None._
|
||||
|
||||
## Risks and skipped work
|
||||
|
||||
- 正式完整测试共219项,218通过;1项NoneBot宿主配置测试因本地缺Pydantic跳过,不计作通过。该项不属于本change真寻宿主配置验收。
|
||||
- 仅本机Windows/Python3.12.14/Pillow12.3;工作流矩阵仅静态核对,Linux及Pillow10.4未在本轮运行。
|
||||
- 未启动真实机器人、访问真实玩家库、验证公开下载、执行远端CI、发布或部署;历史455个缺图引用仍使用明确占位。
|
||||
- 图索引为2026-10-15,相关路径过期或无覆盖;本轮以直接源码和正式日志补齐,未据旧图推断缺失。
|
||||
- 作物Service迁移不属于本次归档范围且尚未实施。
|
||||
|
||||
## Previous iterations
|
||||
|
||||
| Goal cycle | Iteration | Attempt | Outcome | Unresolved | Summary | Completed |
|
||||
| ---: | ---: | ---: | --- | --- | --- | --- |
|
||||
| 1 | 1 | 1 | pass | — | 独立只读验收完成:完整brief/Spec、实现、测试、配置和文档均已核对,最后参考Builder交接;绑定候选的Runtime foundation-offline以退出码0完成,827基线文件、可复现目录导出与32项测试通过。A1-A8全部通过;未追加或自行重跑完整检查,未修改项目源文件。 | 2026-10-15T07:00:29.324Z |
|
||||
| 1 | 1 | 1 | recovery | — | 用户明确要求在已确认的图片工具及绘图优化方案上执行开发;将共享农场渲染作为当前基础开发的范围补充,保留原业务验收项并追加图片验收。 | 2026-10-15T07:55:29.688Z |
|
||||
| 2 | 0 | 0 | recovery | — | Native confirmed acceptance criteria changed | 2026-10-15T08:23:15.134Z |
|
||||
| 3 | 1 | 1 | pass | — | 独立只读核对全部13项、完整规格、核心与渲染源码、测试/打包/导出脚本、正式Runtime日志和图片样例;复用匹配候选的45项正式检查,全部验收通过。图索引新增渲染未覆盖且旧文件metadata_changed,已直接源文件补齐,无需重复完整套件。 | 2026-10-15T08:40:00.608Z |
|
||||
| 3 | 1 | 1 | recovery | — | 用户选择补齐真寻测试版:命令接入、打包工作流和安装脚本;将现有核心与渲染扩展为可安装真寻预览包,保留独立测试库和旧基线,不执行远端操作。 | 2026-10-15T09:24:24.236Z |
|
||||
| 4 | 0 | 0 | recovery | — | Native confirmed acceptance criteria changed | 2026-10-15T09:28:23.268Z |
|
||||
| 5 | 1 | 1 | pass | — | 独立只读核对完整brief、三份规格及18项验收,审阅核心/渲染、真寻接入、白名单构建/校验/安装/固定发布边界、工作流和测试文档。复用本候选正式Runtime的55项全回归、Pillow10.4下23项兼容检查及工作流守卫,全部通过;未重复完整套件。图索引旧/新增缺覆盖,已直接源码补齐。未修改实现或正式规格,未访问远端、安装/启动机器人或代用户接受。 | 2026-10-15T09:51:26.860Z |
|
||||
| 5 | 1 | 1 | recovery | — | 独立验收指出余额不足错误码映射缺少insufficient_funds;保持已确认范围,补中文提示及回归测试后重新交付。 | 2026-10-15T09:52:02.989Z |
|
||||
| 5 | 2 | 1 | pass | — | 独立只读核对完整brief、三份规格和18项,审阅核心/渲染/真寻接入、白名单构建/校验/安装/固定发布边界、测试工作流与文档。当前candidate绑定Runtime的56项完整回归、Pillow10.4下24项及工作流守卫均退出码0;日志前缀658580e0与当前cedb操作绑定已核实。额外只读确认最终ZIP30文件且runtime匹配修复源码,余额不足中文提示与无扣款回归通过。图索引过期/缺覆盖已直接源码补齐,未重复完整套件、修改实现或正式文件、远端操作、安装启动宿主或代用户接受。 | 2026-10-15T09:58:23.989Z |
|
||||
| 5 | 2 | 1 | recovery | — | 用户实际测试要求默认字体:在真寻适配层默认复用宿主字体目录,保留显式字体覆盖和缺失时清楚提示;修复预览版默认不可绘图的接入遗漏。 | 2026-10-16T02:06:48.036Z |
|
||||
| 5 | 3 | 1 | pass | — | 独立只读验收全部A1-A18:完整brief/三spec与当前字体实现、测试、文档已核对;复用此前未改变核心/渲染/发布的正式独立验收,并核实本候选绑定Runtime三检查exit0,实际58项完整与Pillow10.4下26项通过。真寻FONT_PATH为resources/font,接入层默认选宿主字体,坏默认跳过、显式覆盖优先,共享层未引入真寻路径;安装目录runtime/commands与主源SHA256一致。图索引不覆盖新文件或metadata_changed,已读源码补齐;未重跑完整测试、未修改项目文件、未启动宿主或访问远端。 | 2026-10-16T02:13:06.574Z |
|
||||
| 5 | 3 | 1 | recovery | — | 用户要求修复迁移遗漏:恢复旧版用户面板素材、布局与经验进度语义并保留性能优化。 | 2026-10-16T02:17:50.690Z |
|
||||
| 5 | 4 | 0 | recovery | — | Builder handoff Runtime checks failed: all-offline | 2026-10-16T02:26:17.238Z |
|
||||
| 5 | 5 | 1 | pass | — | 独立只读核对全部A1-A18、完整brief/三份Spec、相关实现与测试及本候选Runtime绑定。复用60项完整检查、Pillow10.4下28项及工作流守卫,均退出码0;未重跑完整套件。原面板底图/头像框/头像尺寸和信息已恢复,用户授权的槽位坐标校正经源码与1536样图核验;preview.3最终ZIP30文件与当前安装目录逐文件相同,renderer匹配源码。未修改实现/正式规格、操作远端、真实玩家库或启动宿主。 | 2026-10-16T02:31:02.032Z |
|
||||
| 5 | 5 | 1 | recovery | — | 用户明确要求执行统一图片质量配置,默认75、范围1至95,仅我的农场允许高清覆盖;其他图片禁止高清,保留仓库文字和分页。修订用户可见要求。 | 2026-10-22T09:37:42.325Z |
|
||||
| 6 | 1 | 0 | recovery | — | Native formal documents are invalid (brief-structure-missing: brief.md) | 2026-10-26T04:22:42.893Z |
|
||||
| 7 | 1 | 1 | fail | A8, A18 | 新的独立只读Verifier核对完整brief、三Spec及全部19项;brief四核心结构有效。复用当前candidate绑定Runtime三项退出0检查,未重跑完整套件。17通过,A8和A18因现行文档陈旧/缺失失败,修复说明后再验收;无需据此修改业务。独立核验完成后最后参考Builder交接,其文档证据不足。未修改业务、测试、规格、报告或玩家数据,未接受结果或归档。 | 2026-10-26T04:31:34.844Z |
|
||||
| 7 | 2 | 1 | pass | — | 新的独立只读Verifier完整核对brief四核心结构、三Spec与A1-A19,直接源码补齐过期图覆盖,并核查本candidate绑定Runtime三检查均exit0;复用219项离线记录(218通过、1项明确跳过)、工作流守卫和六图解码/暖缓存证据。A8/A18现行文档已与实现对齐,其余17项逐项核查通过;最后参考Builder交接,未重跑完整套件、修改业务/测试/文档/报告或操作玩家数据,未代用户接受或归档。 | 2026-10-26T04:40:15.168Z |
|
||||
|
||||
|
||||
|
||||
## Conclusion
|
||||
|
||||
新的独立只读Verifier完整核对brief四核心结构、三Spec与A1-A19,直接源码补齐过期图覆盖,并核查本candidate绑定Runtime三检查均exit0;复用219项离线记录(218通过、1项明确跳过)、工作流守卫和六图解码/暖缓存证据。A8/A18现行文档已与实现对齐,其余17项逐项核查通过;最后参考Builder交接,未重跑完整套件、修改业务/测试/文档/报告或操作玩家数据,未代用户接受或归档。
|
||||
@@ -1,164 +0,0 @@
|
||||
# Outcome
|
||||
|
||||
在 D:/Work/farm 当前目录完成旧版可用规则的第一批迁移,建立不依赖
|
||||
真寻、NoneBot 或 AstrBot 的可运行基础核心;使用 SQLite 验证完整交易
|
||||
闭环,为后续框架接入提供稳定调用入口。Comet 初始化已完成,
|
||||
AGENTS.md 缺失开始标记已修复。用户已明确选择当前目录继续。
|
||||
|
||||
# Scope
|
||||
|
||||
- 本次追加统一图片编码质量配置,整数1~95,默认75,覆盖农场、
|
||||
地块编号、详情、商店及种子/作物仓库。仅“我的农场”接受高清,
|
||||
使用1536宽及max(配置质量,85);其他查询拒绝高清,保留分页与文字。
|
||||
真寻网页配置提供“农场图片质量”,普通NoneBot提供FARM_IMAGE_QUALITY。
|
||||
不发布、不安装、不重启机器人,不改变其他功能。
|
||||
- 迁移基线只读。迁出旧作物目录 202 条唯一名称配置,保留必要数值、
|
||||
购买币种、等级、成熟时间、产量、经验和多季次数;新核心运行不依赖
|
||||
legacy 目录,不复制旧框架依赖。共享渲染素材按下述新增范围迁出。
|
||||
- 建立明确的玩家作用域标识,由调用方提供部署、领域、消息平台与
|
||||
用户 ID,不自行选择群共享或跨框架账号绑定政策。
|
||||
- SQLite 新 schema、显式事务、操作流水和幂等操作记录;只处理新建
|
||||
测试数据库,不导入真实玩家库。余额与库存以数据库为权威。
|
||||
- 注册初始 500 农场币、0 点券、0 经验、3 块普通空地,沿用旧值。
|
||||
- 提供账户/库存/地块查询、购买种子、播种、收获、铲除枯萎作物、
|
||||
锁定/解锁作物、指定出售/全部出售及基础文本结果。
|
||||
- 每级累计经验门槛沿用 200*k*(k+1)/2,以整数计算避免浮点边界。
|
||||
普通币与点券库存独立;本轮不实现两者兑换或外部金币充值。
|
||||
- 多季作物首熟时间沿用 time*3600;再生长间隔沿用旧实现去重后
|
||||
phase[1:] 之和。该规则明确保留兼容,数值平衡后续另议。
|
||||
- 纯规则支持每块地独立计算产量/经验加成再求和;首轮创建普通地,
|
||||
不提供土地升级、星级自动升级或偷菜加成。
|
||||
- 核心应用 API 为异步可调用,SQLite 阻塞操作不在事件循环线程执行;
|
||||
不引入可变玩家余额缓存或导入时后台线程。日志记录事务操作标识、
|
||||
操作类别与提交结果,不记录密钥或真实用户消息。
|
||||
- 使用本地可重复测试覆盖规则、原子性、失败传播、重试幂等和并发;
|
||||
更新 README、实现说明与现有路线状态,保留待确认决策。
|
||||
- 根据用户对已讨论方案的明确执行指令,追加 farm_render:Pillow 同步
|
||||
图片工具、离线素材包、有界缓存、按目标尺寸绘制、后台受控执行、
|
||||
普通/高清预览、WebP/JPEG/PNG 编码和分阶段日志。
|
||||
- 渲染接收一次核心快照和统一时间,不逐地块查库、不联网;头像字节
|
||||
由接入方提供,失败使用默认头像。保留旧农场背景、地块和作物素材。
|
||||
- 按可见状态缓存成图并在生长阶段边界失效;并发相同请求合并。
|
||||
提供离线生成、性能记录、安装包独立运行和中文字体配置说明。
|
||||
- 用户选择补齐真寻测试版:最小命令接入、离线白名单打包、GitHub
|
||||
构建工作流、可选手动同步真寻发布仓库测试分支、安装/卸载说明。
|
||||
独立测试库、沿用原指令;保留旧版和原业务/渲染验收A1-A13。
|
||||
|
||||
## Directory structure
|
||||
|
||||
### Created
|
||||
|
||||
- src/farm_render/:画布、素材、展示模型、布局、渲染器及异步入口。
|
||||
- scripts/export_render_assets.py、scripts/render_demo.py。
|
||||
- tests/test_render*.py、docs/10-rendering.md。
|
||||
- integrations/zhenxun/ 的入口、命令和生命周期;scripts/ 下真寻构建、
|
||||
产物验证、本地安装及手动发布脚本;.github/workflows/ 下构建工作流;
|
||||
tests/test_zhenxun*.py、docs/11-zhenxun-preview.md。
|
||||
|
||||
### Modified
|
||||
|
||||
- pyproject.toml:render 可选依赖及渲染素材打包。
|
||||
- scripts/verify_foundation.py:增加渲染资源可复现检查。
|
||||
- README.md、docs/README.md、docs/task_plan.md、docs/progress.md。
|
||||
- 当前 brief 与新增 farm-render 目标规格;原业务规格保留。
|
||||
- release/targets.json 与真寻职责说明;新增真寻预览完整目标规格。
|
||||
|
||||
### Deleted
|
||||
|
||||
无。
|
||||
|
||||
### Not created
|
||||
|
||||
- 不创建AstrBot/通用NoneBot接入、真实玩家库或字体再分发包。
|
||||
- 不修改 legacy 基线,不创建多个框架的业务副本。
|
||||
|
||||
# Non-goals
|
||||
|
||||
本轮不实现AstrBot/通用NoneBot宿主插件、签到、偷菜、活动、
|
||||
外部金币兑换、点券兑换、土地升级、星级进阶、账号绑定或在线联机。
|
||||
不读取或迁移真实玩家数据,不重置旧资产,不改旧基线业务。
|
||||
不在本机 clone/fetch/pull/push 任何远程,不创建 PR 或远端Release。
|
||||
生成工作流与发布脚本,但不执行跨仓库发布或安装/重启真实机器人。
|
||||
不将设计占位目录报告为已支持三框架。
|
||||
|
||||
# Acceptance examples
|
||||
|
||||
- A19: 默认质量75,整数1和95有效,0、96、布尔值及非整数拒绝;
|
||||
配置应用到所有图片编码,缓存区分质量。仅我的农场接受高清,
|
||||
质量不低于85及配置值;其他查询拒绝高清,仓库分页、文字继续可用。
|
||||
真寻注册网页配置并在请求时读取,NoneBot读取启动配置;帮助与说明一致。
|
||||
- A1: 旧基线 827 文件校验继续通过;新核心不导入三框架、无导入副作用,
|
||||
可从安装包或源码独立运行,核心不通过路径引用 legacy。
|
||||
- A2: 可复现导出 202 条作物配置,名称和必要数值对应源静态数据库;
|
||||
本地加载支持格式校验,坏数据明确失败,不访问网络。
|
||||
- A3: 注册立即可查询 500 农场币、0 点券、0 经验、3 块空地;
|
||||
重复注册不重复赠送;不同显式作用域不串号。
|
||||
- A4: 购买到播种全过程扣款、入库、耗种和地块改变正确;0、负数、
|
||||
布尔值、超大数量、余额/库存不足、等级不足和不存在对象明确失败,
|
||||
不留下部分写入。
|
||||
- A5: 未成熟不能收获;成熟后只增加本次产量/经验;历史经验 100、
|
||||
本次获得20时为120;多地加成逐块计算且交换顺序不改变总和;
|
||||
多季次数及枯萎/铲除遵守保存的规则与可注入时钟。
|
||||
- A6: 出售收益只增加一次,例如余额100、收益20后为120;
|
||||
锁定同时约束指定与全部出售;返回结构化真实余额及安全文本,
|
||||
不发生旧版模板参数缺失。
|
||||
- A7: 模拟交易中途异常时余额、库存、地块及流水整体回滚;
|
||||
同一操作标识重试返回原结果、参数冲突拒绝;并发写入不超卖、
|
||||
不重复领取,不吞数据库故障伪装成功;重启后数据可读。
|
||||
- A8: 提供可执行的离线测试、运行入口和使用说明,保留真实宿主、
|
||||
真实库迁移及远程发布未验证的说明;文档记录已实现范围和剩余项。
|
||||
- A9: 独立图片工具支持透明合成、缩放、裁剪、文本测量/省略和三格式编码;
|
||||
渲染不导入框架、不联网,核心保持无 Pillow 依赖;安装包含所需素材。
|
||||
- A10: 从一次快照绘制空地、生长、成熟、再生长、枯萎及锁定地块,
|
||||
使用统一时间、旧布局和正确偏移;输出普通/高清可解码图片。
|
||||
- A11: 小画布从开始按目标尺寸绘制;素材和结果缓存有内存上限,
|
||||
共享素材不被污染;阶段/成熟边界、账户、头像及作用域变化不复用旧图。
|
||||
- A12: 合成和编码在后台执行且限制并发,相同请求合并;取消和异常
|
||||
不提前释放运行名额、不泄漏后台任务;记录耗时、尺寸和缓存状态。
|
||||
- A13: 离线生成农场样例、对比尺寸/编码与冷暖缓存,保存测量报告和
|
||||
接入文档;验证旧基线及原有核心测试,明确尚未接入真实消息平台。
|
||||
- A14: 真寻接入沿用原指令提供帮助、注册、我的农场、仓库、购买、
|
||||
播种、收获、铲除、锁定/解锁、出售;稳定事件幂等标识、身份隔离,
|
||||
独立测试数据库与启动/关闭生命周期;缺字体时给出配置提示。
|
||||
- A15: 白名单构建完整真寻插件ZIP,包含私有共享模块、静态素材、
|
||||
依赖、许可证及来源/文件哈希;不含其他适配、legacy、测试或玩家数据,
|
||||
源提交/脏工作区明确记录,相同输入可复现,构建不修改开发源码。
|
||||
- A16: 离线校验ZIP内容、导入和命令路由;安装脚本拒绝路径穿越、
|
||||
哈希错误和已有目标,不覆盖旧插件或玩家数据;不自动启动真实机器人。
|
||||
- A17: 主仓库工作流完成检查/构建/上传测试附件;可选手动发布只面向
|
||||
固定真寻仓库的专用测试分支,默认不发布、不改main、不force,凭据
|
||||
由Secrets提供且不进入产物;发布仓库收到生成内容后可独立校验。
|
||||
- A18: 本机离线验证打包、边界与安装行为;CI覆盖Windows/Linux及
|
||||
Pillow10.4和当前支持版本,记录真实宿主、GitHub运行未验证边界;
|
||||
提供下载、字体/数据路径、测试命令、回退/卸载和Secrets配置说明。
|
||||
|
||||
# Constraints and invariants
|
||||
|
||||
源码使用 Python,4 空格、80 字符行长,明确说明数据模型字段。
|
||||
保留 GPLv3 许可与源归属。共享核心内部相对导入。
|
||||
运行数据和测试临时数据库不入 Git。新实现只使用必要依赖,
|
||||
优先标准库 SQLite、异步线程执行和 unittest,不为框架引入依赖。
|
||||
新数据库 schema 与旧库区分,未知或更高 schema 明确拒绝。
|
||||
同一基础交易的存储和应用紧密耦合,使用单个 change,不拆分子任务。
|
||||
|
||||
# Decisions
|
||||
|
||||
- 2026-10-22用户确认统一质量方案,并修正为“除了我的农场允许带高清,
|
||||
其他都不允许。请执行吧”。本次不接受或归档旧验收结果。
|
||||
- 用户已授权初始化 Comet 并执行基础开发;工作区为当前目录 main。
|
||||
- 主仓库地址已明确:https://github.com/Shu-Ying/main_plugin_farm.git。
|
||||
- 现有文档是设计参考,旧代码是实现参考;本轮仅覆盖上述第一批核心,
|
||||
不把整份长期计划视为一次必须全部实施的产品需求。
|
||||
- 未决的农场共享、联机、存量资产、兑换与素材发布不由本轮擅自决定。
|
||||
- 用户随后明确要求“根据上述方案和之前优化的绘制我的农场,请执行”,
|
||||
授权图片层及绘图优化。沿用已选当前目录;该目录已有未提交核心,
|
||||
Runtime 不允许第二个 current change,因此作为基础开发范围补充。
|
||||
- 用户讨论后明确“确认并进入开发”,包含 WebP 优先候选及 JPEG/PNG
|
||||
兼容输出;尺寸、质量、编码力度独立配置,平台能力由适配器声明。
|
||||
- 素材保留原 GPL 归属,不捆绑未确认再分发许可的字体;部署显式配置
|
||||
中文字体路径。真实宿主头像缓存和上传由后续适配器实现。
|
||||
- 用户已明确选择“补齐真寻测试版:命令接入+打包工作流+安装脚本”。
|
||||
由于当前目录未归档核心与新接入需联合验证,作为同一基础测试交付
|
||||
扩展;不代用户接受上轮结果。用户明确不加测试前缀,沿用原指令;
|
||||
测试库与旧库隔离,发布脚本只操作专用farm-preview分支,实际发布
|
||||
不执行。测试时由用户停用旧插件,安装器不删除或覆盖旧插件。
|
||||
@@ -1,296 +0,0 @@
|
||||
schema: comet.native.v4
|
||||
name: farm-core-foundation
|
||||
language: en
|
||||
phase: build
|
||||
status: active
|
||||
state_version: 39
|
||||
brief: brief.md
|
||||
document_constraints_version: 3
|
||||
shape_confirmation_hash: 066f3825ae83e04c24cb7018a75d217a2aaae9a8a4cba1c435eecc776de4a90d
|
||||
spec_changes:
|
||||
- capability: farm-core
|
||||
operation: create
|
||||
source: specs/farm-core/spec.md
|
||||
- capability: farm-render
|
||||
operation: create
|
||||
source: specs/farm-render/spec.md
|
||||
- capability: zhenxun-preview
|
||||
operation: create
|
||||
source: specs/zhenxun-preview/spec.md
|
||||
workspace:
|
||||
isolation: current
|
||||
change_branch: main
|
||||
target_branch: main
|
||||
finish: null
|
||||
loop:
|
||||
stage: building
|
||||
goal_cycle: 6
|
||||
iteration: 1
|
||||
attempt: 0
|
||||
retry_epoch: 0
|
||||
failed_iteration_count: 0
|
||||
no_progress_count: 0
|
||||
execution_failure_count: 0
|
||||
previous_unresolved_ids: []
|
||||
next_action: submit-builder-candidate
|
||||
acceptance:
|
||||
- id: A1
|
||||
source: brief.md
|
||||
text: "A19: 默认质量75,整数1和95有效,0、96、布尔值及非整数拒绝; 配置应用到所有图片编码,缓存区分质量。仅我的农场接受高清,
|
||||
质量不低于85及配置值;其他查询拒绝高清,仓库分页、文字继续可用。 真寻注册网页配置并在请求时读取,NoneBot读取启动配置;帮助与说明一致。"
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A2
|
||||
source: brief.md
|
||||
text: "A1: 旧基线 827 文件校验继续通过;新核心不导入三框架、无导入副作用, 可从安装包或源码独立运行,核心不通过路径引用 legacy。"
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A3
|
||||
source: brief.md
|
||||
text: "A2: 可复现导出 202 条作物配置,名称和必要数值对应源静态数据库; 本地加载支持格式校验,坏数据明确失败,不访问网络。"
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A4
|
||||
source: brief.md
|
||||
text: "A3: 注册立即可查询 500 农场币、0 点券、0 经验、3 块空地; 重复注册不重复赠送;不同显式作用域不串号。"
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A5
|
||||
source: brief.md
|
||||
text: "A4: 购买到播种全过程扣款、入库、耗种和地块改变正确;0、负数、 布尔值、超大数量、余额/库存不足、等级不足和不存在对象明确失败,
|
||||
不留下部分写入。"
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A6
|
||||
source: brief.md
|
||||
text: "A5: 未成熟不能收获;成熟后只增加本次产量/经验;历史经验 100、 本次获得20时为120;多地加成逐块计算且交换顺序不改变总和;
|
||||
多季次数及枯萎/铲除遵守保存的规则与可注入时钟。"
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A7
|
||||
source: brief.md
|
||||
text: "A6: 出售收益只增加一次,例如余额100、收益20后为120; 锁定同时约束指定与全部出售;返回结构化真实余额及安全文本,
|
||||
不发生旧版模板参数缺失。"
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A8
|
||||
source: brief.md
|
||||
text: "A7: 模拟交易中途异常时余额、库存、地块及流水整体回滚; 同一操作标识重试返回原结果、参数冲突拒绝;并发写入不超卖、
|
||||
不重复领取,不吞数据库故障伪装成功;重启后数据可读。"
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A9
|
||||
source: brief.md
|
||||
text: "A8: 提供可执行的离线测试、运行入口和使用说明,保留真实宿主、 真实库迁移及远程发布未验证的说明;文档记录已实现范围和剩余项。"
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A10
|
||||
source: brief.md
|
||||
text: "A9: 独立图片工具支持透明合成、缩放、裁剪、文本测量/省略和三格式编码; 渲染不导入框架、不联网,核心保持无 Pillow
|
||||
依赖;安装包含所需素材。"
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A11
|
||||
source: brief.md
|
||||
text: "A10: 从一次快照绘制空地、生长、成熟、再生长、枯萎及锁定地块, 使用统一时间、旧布局和正确偏移;输出普通/高清可解码图片。"
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A12
|
||||
source: brief.md
|
||||
text: "A11: 小画布从开始按目标尺寸绘制;素材和结果缓存有内存上限, 共享素材不被污染;阶段/成熟边界、账户、头像及作用域变化不复用旧图。"
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A13
|
||||
source: brief.md
|
||||
text: "A12: 合成和编码在后台执行且限制并发,相同请求合并;取消和异常 不提前释放运行名额、不泄漏后台任务;记录耗时、尺寸和缓存状态。"
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A14
|
||||
source: brief.md
|
||||
text: "A13: 离线生成农场样例、对比尺寸/编码与冷暖缓存,保存测量报告和 接入文档;验证旧基线及原有核心测试,明确尚未接入真实消息平台。"
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A15
|
||||
source: brief.md
|
||||
text: "A14: 真寻接入沿用原指令提供帮助、注册、我的农场、仓库、购买、 播种、收获、铲除、锁定/解锁、出售;稳定事件幂等标识、身份隔离,
|
||||
独立测试数据库与启动/关闭生命周期;缺字体时给出配置提示。"
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A16
|
||||
source: brief.md
|
||||
text: "A15: 白名单构建完整真寻插件ZIP,包含私有共享模块、静态素材、 依赖、许可证及来源/文件哈希;不含其他适配、legacy、测试或玩家数据,
|
||||
源提交/脏工作区明确记录,相同输入可复现,构建不修改开发源码。"
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A17
|
||||
source: brief.md
|
||||
text: "A16: 离线校验ZIP内容、导入和命令路由;安装脚本拒绝路径穿越、 哈希错误和已有目标,不覆盖旧插件或玩家数据;不自动启动真实机器人。"
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A18
|
||||
source: brief.md
|
||||
text: "A17: 主仓库工作流完成检查/构建/上传测试附件;可选手动发布只面向 固定真寻仓库的专用测试分支,默认不发布、不改main、不force,凭据
|
||||
由Secrets提供且不进入产物;发布仓库收到生成内容后可独立校验。"
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A19
|
||||
source: brief.md
|
||||
text: "A18: 本机离线验证打包、边界与安装行为;CI覆盖Windows/Linux及
|
||||
Pillow10.4和当前支持版本,记录真实宿主、GitHub运行未验证边界;
|
||||
提供下载、字体/数据路径、测试命令、回退/卸载和Secrets配置说明。"
|
||||
result: pending
|
||||
reason: null
|
||||
builder_handoff: null
|
||||
blockers: []
|
||||
verification: null
|
||||
history:
|
||||
- goal_cycle: 1
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: pass
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 独立只读验收完成:完整brief/Spec、实现、测试、配置和文档均已核对,最后参考Builder交接;绑定候选的Runtime
|
||||
foundation-offline以退出码0完成,827基线文件、可复现目录导出与32项测试通过。A1-A8全部通过;未追加或自行重跑完整检查,未修改项目源文件。
|
||||
truncated: false
|
||||
completed_at: 2026-10-15T07:00:29.324Z
|
||||
- goal_cycle: 1
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 用户明确要求在已确认的图片工具及绘图优化方案上执行开发;将共享农场渲染作为当前基础开发的范围补充,保留原业务验收项并追加图片验收。
|
||||
truncated: false
|
||||
completed_at: 2026-10-15T07:55:29.688Z
|
||||
- goal_cycle: 2
|
||||
iteration: 0
|
||||
attempt: 0
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: Native confirmed acceptance criteria changed
|
||||
truncated: false
|
||||
completed_at: 2026-10-15T08:23:15.134Z
|
||||
- goal_cycle: 3
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: pass
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 独立只读核对全部13项、完整规格、核心与渲染源码、测试/打包/导出脚本、正式Runtime日志和图片样例;复用匹配候选的45项正式检查,全部验收通过。图索引新增渲染未覆盖且旧文件metadata_changed,已直接源文件补齐,无需重复完整套件。
|
||||
truncated: false
|
||||
completed_at: 2026-10-15T08:40:00.608Z
|
||||
- goal_cycle: 3
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 用户选择补齐真寻测试版:命令接入、打包工作流和安装脚本;将现有核心与渲染扩展为可安装真寻预览包,保留独立测试库和旧基线,不执行远端操作。
|
||||
truncated: false
|
||||
completed_at: 2026-10-15T09:24:24.236Z
|
||||
- goal_cycle: 4
|
||||
iteration: 0
|
||||
attempt: 0
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: Native confirmed acceptance criteria changed
|
||||
truncated: false
|
||||
completed_at: 2026-10-15T09:28:23.268Z
|
||||
- goal_cycle: 5
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: pass
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 独立只读核对完整brief、三份规格及18项验收,审阅核心/渲染、真寻接入、白名单构建/校验/安装/固定发布边界、工作流和测试文档。复用本候选正式Runtime的55项全回归、Pillow10.4下23项兼容检查及工作流守卫,全部通过;未重复完整套件。图索引旧/新增缺覆盖,已直接源码补齐。未修改实现或正式规格,未访问远端、安装/启动机器人或代用户接受。
|
||||
truncated: false
|
||||
completed_at: 2026-10-15T09:51:26.860Z
|
||||
- goal_cycle: 5
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 独立验收指出余额不足错误码映射缺少insufficient_funds;保持已确认范围,补中文提示及回归测试后重新交付。
|
||||
truncated: false
|
||||
completed_at: 2026-10-15T09:52:02.989Z
|
||||
- goal_cycle: 5
|
||||
iteration: 2
|
||||
attempt: 1
|
||||
outcome: pass
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 独立只读核对完整brief、三份规格和18项,审阅核心/渲染/真寻接入、白名单构建/校验/安装/固定发布边界、测试工作流与文档。当前candidate绑定Runtime的56项完整回归、Pillow10.4下24项及工作流守卫均退出码0;日志前缀658580e0与当前cedb操作绑定已核实。额外只读确认最终ZIP30文件且runtime匹配修复源码,余额不足中文提示与无扣款回归通过。图索引过期/缺覆盖已直接源码补齐,未重复完整套件、修改实现或正式文件、远端操作、安装启动宿主或代用户接受。
|
||||
truncated: false
|
||||
completed_at: 2026-10-15T09:58:23.989Z
|
||||
- goal_cycle: 5
|
||||
iteration: 2
|
||||
attempt: 1
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 用户实际测试要求默认字体:在真寻适配层默认复用宿主字体目录,保留显式字体覆盖和缺失时清楚提示;修复预览版默认不可绘图的接入遗漏。
|
||||
truncated: false
|
||||
completed_at: 2026-10-16T02:06:48.036Z
|
||||
- goal_cycle: 5
|
||||
iteration: 3
|
||||
attempt: 1
|
||||
outcome: pass
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 独立只读验收全部A1-A18:完整brief/三spec与当前字体实现、测试、文档已核对;复用此前未改变核心/渲染/发布的正式独立验收,并核实本候选绑定Runtime三检查exit0,实际58项完整与Pillow10.4下26项通过。真寻FONT_PATH为resources/font,接入层默认选宿主字体,坏默认跳过、显式覆盖优先,共享层未引入真寻路径;安装目录runtime/commands与主源SHA256一致。图索引不覆盖新文件或metadata_changed,已读源码补齐;未重跑完整测试、未修改项目文件、未启动宿主或访问远端。
|
||||
truncated: false
|
||||
completed_at: 2026-10-16T02:13:06.574Z
|
||||
- goal_cycle: 5
|
||||
iteration: 3
|
||||
attempt: 1
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 用户要求修复迁移遗漏:恢复旧版用户面板素材、布局与经验进度语义并保留性能优化。
|
||||
truncated: false
|
||||
completed_at: 2026-10-16T02:17:50.690Z
|
||||
- goal_cycle: 5
|
||||
iteration: 4
|
||||
attempt: 0
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: "Builder handoff Runtime checks failed: all-offline"
|
||||
truncated: false
|
||||
completed_at: 2026-10-16T02:26:17.238Z
|
||||
- goal_cycle: 5
|
||||
iteration: 5
|
||||
attempt: 1
|
||||
outcome: pass
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 独立只读核对全部A1-A18、完整brief/三份Spec、相关实现与测试及本候选Runtime绑定。复用60项完整检查、Pillow10.4下28项及工作流守卫,均退出码0;未重跑完整套件。原面板底图/头像框/头像尺寸和信息已恢复,用户授权的槽位坐标校正经源码与1536样图核验;preview.3最终ZIP30文件与当前安装目录逐文件相同,renderer匹配源码。未修改实现/正式规格、操作远端、真实玩家库或启动宿主。
|
||||
truncated: false
|
||||
completed_at: 2026-10-16T02:31:02.032Z
|
||||
- goal_cycle: 5
|
||||
iteration: 5
|
||||
attempt: 1
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 用户明确要求执行统一图片质量配置,默认75、范围1至95,仅我的农场允许高清覆盖;其他图片禁止高清,保留仓库文字和分页。修订用户可见要求。
|
||||
truncated: false
|
||||
completed_at: 2026-10-22T09:37:42.325Z
|
||||
history_overflow:
|
||||
dropped_entries: 0
|
||||
first_dropped_at: null
|
||||
last_dropped_at: null
|
||||
outcome_counts:
|
||||
pass: 0
|
||||
fail: 0
|
||||
blocked: 0
|
||||
execution-error: 0
|
||||
recovery: 0
|
||||
verification_result: pending
|
||||
verification_report: null
|
||||
archived: false
|
||||
created_at: 2026-10-15T06:34:33.214Z
|
||||
@@ -0,0 +1,107 @@
|
||||
# Farm Core Foundation
|
||||
|
||||
本规格定义第一批框架无关基础核心的完整行为,覆盖注册、配置、
|
||||
资产库存、种植交易、文本展示和本地持久化。
|
||||
真实机器人入口、联机和旧玩家数据迁移不在本 capability 范围内。
|
||||
|
||||
## Runtime and migration boundary
|
||||
|
||||
共享代码位于 src/farm_core,包内部相对导入;加载模块不执行网络、
|
||||
创建数据库或启动线程。可注入显式配置、时钟与存储生命周期。
|
||||
静态作物目录从保留的旧作物库离线导出,发布或安装后不依赖 legacy。
|
||||
完整静态目录与玩家数据库分开保存,旧基线保持只读。
|
||||
SQLite 工作在线程中执行且每个用例拥有完整事务,异步调用可等待结果。
|
||||
|
||||
Scenario: Independent core and preserved baseline
|
||||
Acceptance: A1
|
||||
基线离线校验通过,包独立导入、安装、调用不需要三种宿主或 legacy。
|
||||
|
||||
## Catalogue
|
||||
|
||||
迁移基线含202条作物且当时名称唯一;运行时以稳定本地ID为身份,
|
||||
允许同名,名称查询拒绝歧义,支持#编号。映射id/name/level/buy/isVip/vipBuy/
|
||||
experience/harvest/price/time/crop/phase 等必要数据,规范整数数值,
|
||||
异常的字段或版本错误明确拒绝。购买使用对应币种,按等级校验。
|
||||
不下载远程目录,不将素材路径或绘图偏移当作业务依赖。
|
||||
多季再生长间隔保持旧去重 phase[1:] 求和规则,不在首轮重新平衡。
|
||||
|
||||
Scenario: Reproducible catalogue migration
|
||||
Acceptance: A2
|
||||
离线导出再生成内容一致;202 条与源值匹配;损坏输入被拒绝。
|
||||
|
||||
## Registration and account model
|
||||
|
||||
调用方提供 deployment/realm/platform/user 字符串身份,核心不推断账号
|
||||
等价性或跨群共享。首次注册赠送500 point、0 vipPoint、0 exp、6空地。
|
||||
唯一身份只能领取一次初始资产。查询总是基于已提交数据库状态。
|
||||
可靠识别为初始3块的旧用户一次性追加3块普通空地,保留已开垦成果;
|
||||
并发、重试和重启不得重复补偿,来源不明和数量异常明确拒绝。
|
||||
经验等级门槛为 200*k*(k+1)//2,使用整数边界计算。
|
||||
货币、数量和时间范围必须适合 SQLite 整数及应用限制。
|
||||
bool 不作为合法整数参数,不允许负数归零后当作交易成功。
|
||||
|
||||
Scenario: Immediate registration and scope isolation
|
||||
Acceptance: A3
|
||||
首次注册立即查询得到默认值;重复注册无增发;不同作用域相互独立。
|
||||
|
||||
## Purchase and sowing
|
||||
|
||||
购买种子时在同一用例事务内检查注册、作物、数量、等级和余额,
|
||||
扣款并增加库存,失败整体回滚。播种检查空地和库存,在同一事务内
|
||||
消耗实际种子、创建作物实例,保存结算所需目录数据避免后续版本改变
|
||||
已播种作物。显式数量超过可用资源返回明确结果,全部操作使用明确
|
||||
参数而不是允许任意负数。返回真实操作数、余额及剩余库存。
|
||||
|
||||
Scenario: Purchase and sow atomically with validated inputs
|
||||
Acceptance: A4
|
||||
非法数量、余额不足、等级不足和库存不足不产生任何部分状态。
|
||||
|
||||
## Harvest and crop lifecycle
|
||||
|
||||
首熟为播种时间加 time*3600;使用可注入时钟,当前时间等于成熟时可收。
|
||||
每块地独立计算 floor(base*(100+bonus)/100) 后相加,不对累计值加成。
|
||||
经验只增加本次增量。播种每地另加2经验、铲除每地另加2经验;
|
||||
“历史100”指收获操作前已提交的经验。
|
||||
产物入库、经验变动、收获次数和下一季状态
|
||||
在同一事务提交。最后一季后枯萎;铲除只清理枯萎地块。
|
||||
再次重试不重复收获。展示结果不再次修改数据。
|
||||
|
||||
Scenario: Correct experience and multi-crop settlement
|
||||
Acceptance: A5
|
||||
历史100加本次20等于120;地块排序不影响总收益;成熟边界及多季状态正确。
|
||||
|
||||
## Inventory locking and selling
|
||||
|
||||
指定出售和全部出售均保护锁定物品。出售收入在事务内增加一次,
|
||||
库存减少为实际成交数量。没有可出售物品时有明确结果。
|
||||
查询及返回结构化账户/库存/地块数据,基础文本仅渲染结果,避免模板
|
||||
占位不匹配;文本不依赖任何消息框架。
|
||||
|
||||
Scenario: Consistent locking and exact sale proceeds
|
||||
Acceptance: A6
|
||||
100余额出售收入20变为120;锁定物品无法通过任一出售入口消耗。
|
||||
|
||||
## Persistence, idempotency and failures
|
||||
|
||||
每个写用例有稳定 operation_id、动作和规范化参数指纹。
|
||||
同作用域同标识相同请求返回已提交结果,不再次扣款或发奖;
|
||||
不同参数冲突拒绝。状态改变、资产流水和结果持久化在同一事务。
|
||||
数据库异常应回滚并传播为明确错误,不能转成不存在/无农场。
|
||||
支持并发调用,通过事务、条件更新及唯一约束避免超卖和重复结算。
|
||||
关闭后重开可读取已提交数据;未知/高版本 schema 拒绝。
|
||||
调用取消不能无记录重复交易,已提交结果可通过相同操作标识恢复。
|
||||
|
||||
Scenario: Rollback, replay and concurrent safety
|
||||
Acceptance: A7
|
||||
故障注入无部分数据;重试幂等;冲突被拒绝;并发不透支;重启后状态一致。
|
||||
|
||||
## Verification and user documentation
|
||||
|
||||
离线测试采用构造数据,覆盖上述全部验收示例以及源目录一致性。
|
||||
提供本地运行/演示入口,不宣称可放入三种框架插件目录加载。
|
||||
记录本轮新实现与旧缺陷对应关系,不把历史设计文档误标为全面完成。
|
||||
不安装或操作真实机器人,不访问远程仓库,不导入真实玩家数据库。
|
||||
|
||||
Scenario: Repeatable local verification and accurate status
|
||||
Acceptance: A8
|
||||
安装及离线测试命令可复现,使用说明清楚标明未实现的接入与发布部分。
|
||||
@@ -0,0 +1,77 @@
|
||||
# Farm Render
|
||||
|
||||
## Independent image layer
|
||||
|
||||
farm_render 使用 Pillow 实现同步画布操作;farm_core 不依赖 Pillow。
|
||||
工具支持透明合成、复制、缩放、裁剪、文本测量与超长省略,以及
|
||||
WebP/JPEG/PNG 编码。JPEG 合并透明背景,输出携带 MIME、尺寸与字节。
|
||||
WebP 默认采用有损质量75、method=2;高清质量为max(配置质量,85)。格式与尺寸独立,
|
||||
auto 根据调用方声明的平台能力选择 WebP 或 JPEG,本机无 WebP 编码
|
||||
支持时 auto 回退 JPEG,显式 WebP 则清楚报错。PNG 保留无损选项。
|
||||
开发素材独立生成,不在运行时引用 legacy 或宿主。插件携带轻量元数据
|
||||
及绑定摘要的素材清单,大型 assets.zip 从所选源校验下载并缓存,
|
||||
不打入插件发布包;小型升级土地素材仍随包提供。中文字体支持显式
|
||||
配置,或独立校验下载原版小赖字体及完整 OFL 许可,不复制 Windows
|
||||
商业字体。无可用素材或字体时接入层降级文字,不输出缺字占位图。
|
||||
|
||||
Scenario: Framework-independent packaged renderer
|
||||
Acceptance: A9
|
||||
从 wheel 安装到临时目录,无 legacy 和框架,显式提供已校验的外部
|
||||
素材及字体后仍可生成图片;下载与校验止于接入/资源准备层。
|
||||
|
||||
## Snapshot and layout
|
||||
|
||||
入口从业务服务取得一次一致快照;绘图纯消费快照与统一 now,不查库。
|
||||
复用旧30块地布局、季节静态背景、草地/普通地和作物阶段素材。
|
||||
首季阶段锚点由保存的 mature_at-growth_seconds 推导,再季使用
|
||||
mature_at-regrowth_seconds,按本季时长映射展示阶段,成熟优先、
|
||||
枯萎优先于成熟。阶段仅影响外观,不修改交易规则和数据库。
|
||||
作物定位采用 schema=4 的 visible_contact_center 统一公式,按本地
|
||||
土地中心落点,保留完整纹理和直立等比缩放,不使用旧 offset 字段
|
||||
或逐阶段手调锚点。地块位置按当前稳定编号映射。缺失作物资源使用
|
||||
显式占位并记录警告。
|
||||
用户信息显示真实快照余额、点券和经验,头像仅接收字节,无头像时
|
||||
绘制默认图;拒绝超大或无效头像并降级。主题由调用方明确提供。
|
||||
|
||||
Scenario: Farm states and target profiles
|
||||
Acceptance: A10
|
||||
空地、生长、成熟、再生长、枯萎和未解锁地可生成图片,普通与高清
|
||||
使用同一布局;统一时间下同一阶段稳定,边界切换正确。
|
||||
|
||||
## Resource and result cache
|
||||
|
||||
普通图默认宽1024,高清宽1536;画布与坐标从开始按比例处理。
|
||||
解码后素材、底图和成图使用有界 LRU,超限条目不入缓存。
|
||||
素材缓存键含路径及目标尺寸,缓存图片只读或复制后修改。
|
||||
成图键含完整身份、可见地块阶段/账户/头像摘要、主题、质量与格式;
|
||||
同阶段可复用,下一阶段或成熟时间到达必须改变键,静态素材版本
|
||||
由渲染器实例绑定;资源更新后重新创建实例。默认缓存不落盘。
|
||||
|
||||
Scenario: Bounded and correct caching
|
||||
Acceptance: A11
|
||||
缓存总字节不超上限;跨账号不串图,阶段、余额、头像变化不命中旧图;
|
||||
普通图绘制画布即为1024宽,无3072宽中间合成画布。
|
||||
|
||||
## Controlled asynchronous entry
|
||||
|
||||
提供异步入口,将整张图的合成与编码交给后台线程;默认最多一个
|
||||
任务运行,等待队列有上限。相同身份/状态请求共享任务。调用取消
|
||||
不取消正在运行的共享绘制,也不提前释放容量。关闭等待任务排空。
|
||||
日志只记录查询/排队/合成/编码耗时、字节、尺寸和缓存结果,不记录
|
||||
真实用户标识、姓名、头像、凭据或完整快照。
|
||||
|
||||
Scenario: Concurrency, cancellation and diagnostics
|
||||
Acceptance: A12
|
||||
慢绘图时事件循环仍可工作;同时运行数有界,重复请求共享任务,
|
||||
失败可重试;取消后后台完成并正确释放容量,关闭无残留任务。
|
||||
|
||||
## Verification and usage
|
||||
|
||||
提供明确中文字体路径的离线示例,使用构造数据和临时数据库,
|
||||
报告冷暖耗时、尺寸、三种编码字节及缓存占用。测量仅代表本机样例,
|
||||
不宣称低配服务器耗时或真实平台上传性能。保留核心离线验证与
|
||||
旧基线校验,不接入/重启真实机器人,不访问远端。
|
||||
|
||||
Scenario: Repeatable offline rendering
|
||||
Acceptance: A13
|
||||
离线样例可解码,性能报告真实可复现,文档提供接入调用和未验证边界。
|
||||
@@ -0,0 +1,96 @@
|
||||
# 真寻可安装预览包
|
||||
|
||||
## 接入边界
|
||||
|
||||
全部图片采用统一整数编码质量(1~95,默认75)。真寻网页配置名称为
|
||||
“农场图片质量”,普通NoneBot接入使用FARM_IMAGE_QUALITY。真寻请求时
|
||||
读取配置;无效值明确拒绝并保留已有有效配置,不静默夹取范围。
|
||||
只有“我的农场 高清”可覆盖默认尺寸至1536宽,质量max(配置值,85)。
|
||||
地块编号、详情、种子商店、我的种子和我的作物拒绝高清参数,保留既有
|
||||
分页和仓库文字查询。编码质量随各类渲染输入传递并进入图片缓存键。
|
||||
PNG继续无损输出,质量参数不改变PNG清晰度。默认其他图片尺寸不变。
|
||||
|
||||
Scenario: 全局图片质量及仅农场高清
|
||||
Acceptance: A19
|
||||
配置1和95可用;0、96、布尔及小数拒绝。所有图片按配置编码且不同质量
|
||||
不共享成图。农场高清为1536宽,质量至少85且不低于配置;其他命令高清
|
||||
返回明确参数错误,仓库分页和文字仍可用,帮助不再宣称其他命令支持高清。
|
||||
|
||||
本 capability 验收真寻接入;NoneBot/AstrBot 已有独立接入与打包,
|
||||
不将其当前存在误报为未实现。业务和图片仍来自主仓库共享源码。接入层加载插件
|
||||
私有_vendor命名空间,渲染层对farm_core的引用在构建时改为私有相对
|
||||
导入,不修改开发源码、不污染全局sys.path或sys.modules别名。
|
||||
命令不增加测试前缀,沿用开通农场、我的农场、农场帮助、我的种子、
|
||||
我的作物、购买种子、播种、收获、铲除、作物加锁、作物解锁、出售作物
|
||||
等原指令,覆盖帮助、注册、我的农场、仓库、购买、
|
||||
播种、收获、铲除、锁定/解锁和出售;复用已有规则,不移植旧损坏模块。
|
||||
部署标识由配置给出,同一平台玩家在本预览部署内共享农场,不跨平台
|
||||
绑定;测试数据位置与旧版不同。事件标识包含机器人、平台、用户及
|
||||
原消息标识;无法获得稳定消息标识时写操作明确拒绝,不能随机重试。
|
||||
在启动时初始化存储、渲染器,在关闭时等待任务排空后关闭存储;字体
|
||||
支持显式配置合法中文字体,或校验下载原版小赖字体及完整许可。
|
||||
资源源可选 Github、Gitea、farm_service,默认 Github;服务地址独立
|
||||
保留。缺少资源或字体时保留文字功能,日志明确原因。头像失败降级,
|
||||
不让头像下载无限等待。本次不包含作物目录远程更新。
|
||||
|
||||
Scenario: 真寻命令与生命周期
|
||||
Acceptance: A14
|
||||
模拟宿主验证命令路由、身份/幂等标识、异常反馈与启动关闭,使用独立库;
|
||||
不把模拟宿主测试报告为真实平台发图成功。
|
||||
|
||||
## 产物
|
||||
|
||||
scripts/build_zhenxun.py按白名单复制真寻接入、farm_core、farm_render、
|
||||
对应轻量数据、升级土地素材、LICENSE及NOTICE;大型图片及字体 ZIP
|
||||
不进入插件包。ZIP含单一插件根目录,仓库导出目录
|
||||
可作为真寻发布仓库根。生成来源manifest与独立SHA256附件,清单不
|
||||
自包含哈希;本地工作区脏时标识dirty,不能冒充源HEAD的纯净构建。
|
||||
输入版本严格校验,ZIP排序/时间固定,目标存在则拒绝或显式选择新路径。
|
||||
不打包其他适配器、运行库、凭据、缓存、legacy或全局字体。
|
||||
|
||||
Scenario: 可复现白名单打包
|
||||
Acceptance: A15
|
||||
重复构建内容相同,独立目录导入私有模块成功,无旧目录和其他框架依赖。
|
||||
|
||||
## 安装验证
|
||||
|
||||
独立verify脚本校验manifest中每个文件与ZIP精确一致,拒绝多余/重复项、
|
||||
绝对路径、反斜线穿越、符号链接、不可信插件根名、哈希错误。
|
||||
安装器只安装到用户显式提供的插件父目录,既有目标拒绝覆盖,不删旧
|
||||
插件、不接触旧数据、不安装依赖、不启动/重启机器人;先校验再写入,
|
||||
失败清理自己新建的临时目录。提供明确安装步骤和停止机器人提示。
|
||||
|
||||
Scenario: 安全校验与安装
|
||||
Acceptance: A16
|
||||
临时目录测试有效包可安装,坏包/穿越/已有目标均拒绝且不留下半成品。
|
||||
|
||||
## 工作流和发布
|
||||
|
||||
主仓库CI在PR/push及手动触发时执行检查,构建ZIP/清单/校验附件并上传
|
||||
Actions artifact。工作流权限默认只读;不可信PR不触发跨仓库写入。
|
||||
跨仓库同步依当前工作流,在 main 的 push 或显式 publish 手动输入时
|
||||
执行,PR 不触发;使用专用 Secret/受保护 environment,固定目标为
|
||||
Shu-Ying/zhenxun_plugin_farm 的 main 分支。脚本默认 dry-run,只有显式
|
||||
发布才联网;只进行普通更新,不 force、不修改旧标签;并发分支改变
|
||||
时失败。此次本地归档不执行发布。发布仓库只含生成的
|
||||
可安装内容、来源和验证工具,以及验证工作流,不在那里维护业务源码。
|
||||
下载ZIP测试不要求先同步仓库,不要求用户先给令牌。
|
||||
|
||||
Scenario: 测试附件与可控同步
|
||||
Acceptance: A17
|
||||
静态检查工作流触发/权限和输入边界;模拟服务验证固定目标、冲突保护、
|
||||
令牌不落盘与默认dry-run。此任务不执行真实GitHub发布。
|
||||
|
||||
## 兼容与文档
|
||||
|
||||
真寻本地配置要求Pillow>=10,<11,本预览使用与其相交的>=10.4版本;
|
||||
CI测试最低10.4和本项目支持当前版本,Windows/Linux。不得静默强制
|
||||
升级宿主到12。文档列出本机实际检查、模拟与真实宿主的差别,并提供
|
||||
主仓库生成、发布仓库布局、下载/依赖安装、字体、数据库、测试命令、
|
||||
与旧版避免冲突、备份/卸载和可选同步的凭据最小权限步骤。沿用原
|
||||
指令时必须由用户停用旧插件,不静默更名指令,不自动禁用旧插件。
|
||||
|
||||
Scenario: 可复现的真寻测试交付
|
||||
Acceptance: A18
|
||||
产物可离线验证,文档能按步骤启动人工测试;明确真实机器人与GitHub
|
||||
工作流仍未实际执行,不宣称已经上线或已兼容全部平台。
|
||||
@@ -0,0 +1,113 @@
|
||||
# 作物目录Service与启动更新 Implementation Plan
|
||||
|
||||
> **For agentic workers:** 使用subagent-driven-development负责Service边界,
|
||||
> 主代理负责紧密耦合的Farm加载、生命周期和发布产物;最后统一只读审查。
|
||||
|
||||
**Goal:** Service独立维护完整作物资源版本,三框架仅启动检查更新并安全回退。
|
||||
**Architecture:** Service的catalogue/source为唯一可编辑源,生成不可变发布版本;
|
||||
Farm离线核心只解析目录,共享接入层下载、校验和选择版本后初始化数据库及绘图。
|
||||
**Tech Stack:** Python3.10+标准库/Pillow,Rust/Axum,SQLite,unittest/cargo test。
|
||||
**Spec:** D:/Work/farm_service/docs/crop-catalogue-design.md;用户明确要求实施。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
不修改legacy、不操作真实玩家数据、不推送部署、不创建定时更新;现有已播种
|
||||
结算快照和当前价格出售规则保留。当前目录为用户此前选择的工作区;本轮不提交。
|
||||
图索引过期,已知scripts excluded/src metadata_changed,所有结论回到FastCtx源文件。
|
||||
|
||||
## Protocol / Interfaces
|
||||
|
||||
发布根catalogue/,latest位于manifest.json;历史位于releases/<release>/。
|
||||
manifest严格schema=1、version(1..64字母数字._-)、release(64位小写SHA256)、
|
||||
files;files精确四项plants.json、metadata.json、assets.zip、land-soils.zip,
|
||||
各为size_bytes正整数和sha256。release=SHA256(files对象的排序紧凑UTF8 JSON)。
|
||||
manifest<=16KiB,plants<=2MiB,metadata<=4MiB,各ZIP<=64MiB,
|
||||
解压总量<=256MiB。不得任意URL/路径,不自动跨源。
|
||||
Github/Gitea根由现有固定Service仓库raw路径拼接catalogue/。
|
||||
HTTP /v1/catalogue/manifest.json 和 /v1/catalogue/releases/{release}/{file}。
|
||||
每历史版本含相同manifest.json。Service启动读完整目录快照,运行中不重读。
|
||||
发布工具调用:python scripts/prepare_catalogue.py --source catalogue/source
|
||||
--output catalogue --version <label>;支持--check只读核对当前源与latest。
|
||||
源目录plants.json/metadata.json/images/<原ZIP成员路径>/land-soils.zip。
|
||||
plants根schema1+plants,不再依赖旧库摘要。metadata.plants从目录重建,
|
||||
images尺寸与摘要从实际图片计算,保留布局/定位/panel等现有配置。
|
||||
原子生成、不可变历史,不允许删除/更名旧ID或删除既有图片路径。
|
||||
|
||||
Farm接口:farm_core.catalogue_release提供纯离线manifest与目录一致性校验;
|
||||
farm_render.catalogue_bundle.load_catalogue_release(data_dir, source, service_url,
|
||||
required_ids=()) -> ReleaseSnapshot(catalogue, metadata, bundle_path, soils_path,
|
||||
manifest, origin),manifest/metadata可为dict,路径可在首次离线回退为空。
|
||||
完整release校验后再更新current.json指针,失败用已验证current再用bootstrap;
|
||||
所有候选必须覆盖required_ids,否则明确失败,不修改玩家库。
|
||||
缓存路径catalogue/releases/<release>/;current.json只含完整manifest;
|
||||
并发同进程锁与独立临时目录、原子替换,最终有效目录可复用不破坏。
|
||||
网络仅接入/资源层,下载限额/超时/HTTPS复用现有模式,异常只记录分类。
|
||||
bootstrap manifest位于farm_core/data/catalogue-manifest.json,其他文件沿用
|
||||
现有core/data/plants与render/data/metadata、assets、land-soils;
|
||||
三端不打包assets.zip,bootstrap无图时仍可文字启动。
|
||||
|
||||
## Review Focus
|
||||
|
||||
- 下载中断/错hash/错schema/文件混用:不切换不破坏current。
|
||||
- 旧缓存丢失但玩家持有新ID:启动拒绝,不覆盖数据库或清空库存。
|
||||
- 同时启动/取消:只采用完整版本,日志关闭后不遗留写缓存/任务。
|
||||
- 新版本禁售/禁播而非删ID:旧库存可出售、旧种植继续快照结算。
|
||||
- 公共HTTP只允许白名单文件/64位版本,保留资料鉴权与字体路由。
|
||||
|
||||
## Tasks
|
||||
|
||||
### 1. Service维护源、确定性发布与HTTP分发
|
||||
Files: service scripts/prepare_catalogue.py、catalogue/source和releases;
|
||||
src/catalogue.rs、config/lib/main与tests/catalogue.rs;docs/catalogue.md。
|
||||
- [x] 先加发布工具/HTTP失败测试,记录RED。
|
||||
- [x] 移出202条与768张现有图片及小型土地图,初始值/图不变;
|
||||
本地生成初始正式版本,不访问远端。
|
||||
- [x] Rust启动校验manifest/各文件与历史集合,公开只读快照路由,
|
||||
新环境FARM_CATALOGUE_DIR,未配置503、未知404、支持ETag。
|
||||
- [x] 运行Python工具测试、cargo fmt/test/clippy;不改Farm文件。
|
||||
|
||||
### 2. Farm纯校验、下载与bootstrap刷新
|
||||
Files: core/catalogue_release.py、render/catalogue_bundle.py、
|
||||
scripts/sync_catalogue_bootstrap.py、check_catalogue_bootstrap.py及新测试。
|
||||
- [x] 先测成功版本、非法输入、缓存回退、原子性、ID兼容和三源路径RED。
|
||||
- [x] 实现完整版本校验与缓存选择;Pillow解码只在render层。
|
||||
- [x] 显式从Service本地发布目录刷新bootstrap,CI只自检独立产物,
|
||||
不要求相邻Service或在线访问;旧导出仅作为显式迁移参考入口。
|
||||
- [x] 定向测试GREEN,不将源码检查称为HTTP发布通过。
|
||||
|
||||
### 3. 接入生命周期、数据库兼容与动态元数据
|
||||
Files: integrations/shared/runtime.py、core/storage.py、render/assets.py、
|
||||
renderer.py和既有运行时测试fixture。
|
||||
- [x] 先测启动采用新版/失败回退/旧库新ID拒绝/关闭与重复start RED。
|
||||
- [x] 启动日志后读取只读required IDs,加载版本后初始化Store/Service;
|
||||
resource_options_provider只在本次启动选择,运行中不再次检查目录。
|
||||
- [x] Assets/FarmRenderer接受metadata与soils_path,渲染绑定同版。
|
||||
- [x] 测试fixture显式mock网络边界,不能让默认单元测试访问公网。
|
||||
|
||||
### 4. 发布守卫、文档、整体审查
|
||||
Files: verify_foundation.py、export脚本CLI、package tests/README/docs。
|
||||
- [x] 替换旧库字节锁定检查为bootstrap一致性,旧基线检查独立保留。
|
||||
- [x] 三端包包含新模块/轻量bootstrap清单,拒绝大型ZIP。
|
||||
- [x] 新增新增/改价/重启/兼容回滚维护步骤;修正三端旧在线更新说明。
|
||||
- [x] 完整219+Python回归、Rust静态/集成检查及新只读审查;仅交付本地。
|
||||
|
||||
## Ledger
|
||||
|
||||
2026-10-26:开始;Farm基线e5ec38e,Service基线72e54c3。
|
||||
Comet resume-probe返回none/no-active-native-changes,按仓库约定不强建工作流。
|
||||
Ruling: 采用当前目录,保留已有未提交设计;不提交合并推送。
|
||||
Ruling: 发布四件套包含小型land-soils.zip,避免更新升级土地图后仍混用旧包。
|
||||
|
||||
2026-10-26完成:初始release为
|
||||
4c88736c2e9b1b5eec4568e953152d59a6918242802da72a6eec1a615a17f18b;
|
||||
202条作物、768张原始图片已迁入Service,初始业务值和图片字节保留。
|
||||
Farm完整verify_foundation退出0:238项,237通过、1项NoneBot/Pydantic
|
||||
宿主配置依赖跳过;工作流守卫和两仓库git diff --check通过。
|
||||
Service发布器18项、Rust集成16项通过,fmt/clippy通过,初始--check通过。
|
||||
两侧独立定向审查完成;修复了完整展示契约、并发发布、名称契约、超大整数
|
||||
溢出和启动关闭交错问题,新增回归先复现失败后通过。文档自检命令已补version。
|
||||
Farm测试日志:runtime/catalogue-verification.log。
|
||||
验证边界:Windows/Python3.12.14/Pillow12.3;未执行Linux系统锁分支、
|
||||
其他依赖矩阵、真实宿主、生产数据库、远端下载、提交、推送或部署。
|
||||
字体及旧素材下载线程的取消等待属于既有实现边界,本次新作物版本加载线程
|
||||
已独立跟踪并在关闭时等待排空,不宣称旧资源线程也具备同等保证。
|
||||
@@ -0,0 +1,121 @@
|
||||
# 当前真寻农场:构建、安装与维护
|
||||
|
||||
更新日期:2026-10-26。本文按当前源码编写;`11-zhenxun-preview.md`
|
||||
保留为旧版资料,不能作为当前版本的功能或安装依据。
|
||||
|
||||
## 支持范围
|
||||
|
||||
主仓库维护共享核心,真寻发布仓库接收生成内容。不要把主仓库的
|
||||
`integrations/zhenxun` 直接复制到插件目录:其中没有构建生成的私有
|
||||
共享模块和版本文件。使用生成的预览包或对应发布仓库完整内容。
|
||||
|
||||
当前支持注册、农场/地块/详情、种子商店、仓库、购买、播种、收获、
|
||||
铲除、加锁/解锁、出售,以及已接入的土地操作、虫害和偷菜命令。
|
||||
实际参数以“农场帮助”为准。新账号初始500农场币、6块普通空地。
|
||||
签到和活动尚未实现。作物目录已迁入 Service,插件启动时检查更新;
|
||||
维护与回滚见 [作物数据维护](catalogue-updates.md)。
|
||||
|
||||
## 构建和验证
|
||||
|
||||
开发或CI环境需要Python3.10及以上、Pillow、PyYAML、setuptools、wheel,
|
||||
完整共享测试还需要 `integrations/astrbot/requirements.txt` 中的依赖。
|
||||
真寻部署环境沿用宿主提供的NoneBot、真寻和Uninfo;仅安装插件的
|
||||
`requirements.txt`,其Pillow约束为 `>=10.4,<11`,不要强制升级宿主到12。
|
||||
|
||||
主仓库根目录执行,输出目录必须尚不存在:
|
||||
|
||||
```text
|
||||
python scripts/ci_font.py
|
||||
python scripts/check_workflows.py
|
||||
python scripts/verify_foundation.py
|
||||
python scripts/build_zhenxun.py --output dist/zhenxun-manual
|
||||
python scripts/verify_zhenxun.py dist/zhenxun-manual/zhenxun_plugin_farm_preview-<版本>.zip
|
||||
```
|
||||
|
||||
将 `<版本>` 替换为构建实际输出的版本。字体脚本优先复用本地合法字体,
|
||||
否则下载默认字体;全量测试可显式设置 `FARM_TEST_FONT` 为已准备字体。
|
||||
源码方式调用渲染工具时设置 `PYTHONPATH=src`。构建帮助图和图片测试
|
||||
需要可用中文字体。完全离线时应提前准备依赖、字体和所需图片缓存。
|
||||
|
||||
CI构建任务会上传 `zhenxun-preview` 附件,解包后包含插件ZIP、哈希文件、
|
||||
生成仓库及辅助脚本。ZIP和 `FARM-MANIFEST.json` 用于内容一致性验证,
|
||||
不能把它们当作发布者身份的密码学签名。
|
||||
|
||||
## 安装与升级
|
||||
|
||||
1. 停止机器人;备份插件代码和完整数据目录,停用会注册相同原指令的
|
||||
旧农场插件。命令不加测试前缀,不能同时启用旧版和预览版。
|
||||
2. 在宿主自己的Python环境安装预览包 `requirements.txt`,保留宿主
|
||||
原有依赖约束。安装脚本不会代为安装依赖或启动机器人。
|
||||
3. 使用主仓库辅助脚本,或附件中配套的同名脚本安装:
|
||||
|
||||
```text
|
||||
python scripts/install_zhenxun.py <插件ZIP完整路径> --plugins-dir <现有插件父目录>
|
||||
```
|
||||
|
||||
目标目录名为 `zhenxun_plugin_farm_preview`;父目录必须已存在。
|
||||
安装器拒绝已有目标、坏哈希、路径穿越及符号链接,不覆盖旧插件或玩家数据。
|
||||
升级时停止机器人,手工备份并移走上一版插件目录,再安装已验证的新包。
|
||||
|
||||
4. 配置宿主加载该插件,检查下述数据目录及资源配置,再手工启动。
|
||||
5. 使用构造的测试账号检查“农场帮助”“开通农场”“我的农场”等命令,
|
||||
查看日志;本仓库的模拟测试不代替该真实宿主验证。
|
||||
|
||||
## 配置、数据及日志
|
||||
|
||||
| 项目 | 当前行为 |
|
||||
| --- | --- |
|
||||
| 农场图片质量 | 真寻网页配置,整数1~95、默认75,请求时读取;PNG无损不受影响 |
|
||||
| 高清 | 仅“我的农场 高清”,宽1536,质量max(配置值,85);其他查询拒绝高清 |
|
||||
| 资源获取地址 | Github、Gitea、farm_service,默认Github,修改后重载 |
|
||||
| farm_service 地址 | 独立HTTPS基础地址,选择服务源时必须有效,不使用示例占位域名 |
|
||||
| FARM_PREVIEW_DATA_DIR | 可选宿主启动配置;未设置时使用真寻DATA_PATH下的farm_db目录 |
|
||||
| FARM_PREVIEW_DEPLOYMENT | 默认zhenxun-preview;改变它会改变玩家身份作用域,不用于改显示名 |
|
||||
| FARM_PREVIEW_IMAGE_FORMAT | 默认AUTO;编码与分辨率分别设置,按宿主能力选择 |
|
||||
|
||||
共享核心数据库为数据目录内的 `farm.sqlite3`,运行日志为
|
||||
`farm-preview.log`;素材和字体缓存在同一数据目录的独立子目录。
|
||||
旧 `farm.db` 与新库不是同一种格式,不得直接覆盖或改名冒充新库。
|
||||
生产旧库迁移应使用独立迁移流程和副本预检,不是安装器的行为。
|
||||
|
||||
公开素材和小赖字体由Service仓库分发,默认下载后按大小及SHA256校验,
|
||||
缓存完整OFL许可。网页不提供自定义字体路径配置;共享Runtime/绘图API
|
||||
及开发测试支持显式字体路径。下载失败时保留文字业务并记录原因,
|
||||
修改源后重载插件重试。插件包不包含大型素材及字体ZIP。
|
||||
|
||||
作物目录仍随插件内置,当前没有启动检查作物目录更新或定时更新。
|
||||
已播种规则按快照保存;出售使用当前目录价格,旧库存也受调价影响。
|
||||
|
||||
## 备份、回退与卸载
|
||||
|
||||
- 备份:先停止机器人,复制完整插件目录与完整数据目录;若存在SQLite
|
||||
的WAL/SHM文件,随数据一起保存,不能在运行中仅复制主数据库文件。
|
||||
- 回退:先停止机器人,恢复匹配的代码与停机备份。旧程序可能拒绝新
|
||||
schema,不把新数据库强行交给旧代码,也不删除或重置库来使它启动。
|
||||
恢复旧数据会丢失备份之后的进度,先保留当前副本。
|
||||
- 卸载:停止机器人并取消加载,备份后移走本插件代码即可。保留数据
|
||||
和缓存供恢复,不要求删除玩家数据库或旧插件目录。
|
||||
|
||||
## 工作流发布与Secrets
|
||||
|
||||
主仓库 `main` 的push,或手动运行工作流且选择 `publish=true`,在test与
|
||||
build通过后可触发同步;PR只测试构建,不执行跨仓库发布。
|
||||
发布固定为 `Shu-Ying/zhenxun_plugin_farm` 的 `main`,并非旧farm-preview。
|
||||
它会把目标分支内容更新为生成仓库,发布前应核对目标确为分发仓库。
|
||||
|
||||
发布任务使用受保护的 `zhenxun-preview` environment,从Secrets读取
|
||||
`FARM_RELEASE_TOKEN`;按组织规则授予固定目标仓库所需的最小写权限。
|
||||
不把令牌写进源码、包、命令示例或日志。缺令牌时发布失败,离线
|
||||
构建和验证仍不需要发布凭据。
|
||||
|
||||
`scripts/publish_zhenxun.py <插件ZIP>` 默认离线dry-run;只有显式
|
||||
`--publish` 才联网,并拒绝脏来源包。普通更新不force,检测分支并发
|
||||
变化后失败。本轮归档不运行真实发布,不新增或读取任何令牌。
|
||||
|
||||
## 本轮证据边界
|
||||
|
||||
2026-10-26在Windows/Python3.12.14/Pillow12.3进行了本地离线检查,
|
||||
实际命令和验收结论以Comet生成报告为准。NoneBot宿主配置的一项测试
|
||||
因本地缺Pydantic而跳过;不能将其说成通过。CI声明Windows/Linux及
|
||||
Pillow10.4/12.3矩阵,本机未运行全部矩阵。真实真寻启动、消息发送、
|
||||
公开资源下载和GitHub发布均不由本地测试证明。
|
||||
@@ -23,6 +23,7 @@ from ._vendor.farm_render import FarmRenderer, RenderOptions, RenderService
|
||||
from ._vendor.farm_render.seasons import season_for
|
||||
from ._vendor.farm_render.bundle import ensure_assets, validate_options
|
||||
from ._vendor.farm_render.font_bundle import ensure_font
|
||||
from ._vendor.farm_render.catalogue_bundle import load_catalogue_release
|
||||
from ._vendor.farm_render.shop import shop_page
|
||||
from ._vendor.farm_render.details import detail_rows, details_text
|
||||
from ._vendor.farm_render.warehouse import (
|
||||
@@ -136,12 +137,21 @@ class Runtime:
|
||||
self._resource_lock = asyncio.Lock()
|
||||
self._closing = False
|
||||
self._close_task = None
|
||||
self._start_lock = asyncio.Lock()
|
||||
self._catalogue_task = None
|
||||
self.catalogue_release = None
|
||||
self._start_epoch = 0
|
||||
|
||||
async def start(self):
|
||||
async with self._start_lock:
|
||||
await self._start()
|
||||
|
||||
async def _start(self):
|
||||
if self._closing:
|
||||
return
|
||||
if self.started:
|
||||
return
|
||||
epoch = self._start_epoch
|
||||
self.data_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.log_handler = logging.FileHandler(
|
||||
self.data_dir / "farm-preview.log", encoding="utf-8",
|
||||
@@ -152,12 +162,34 @@ class Runtime:
|
||||
logging.getLogger(__package__).setLevel(logging.INFO)
|
||||
logging.getLogger(__package__).addHandler(self.log_handler)
|
||||
try:
|
||||
if self.resource_options_provider is not None:
|
||||
self.resource_source, self.service_url = (
|
||||
self.resource_options_provider()
|
||||
)
|
||||
validate_options(self.resource_source, self.service_url)
|
||||
required_ids = await self.store.catalogue_ids()
|
||||
if self._closing or epoch != self._start_epoch:
|
||||
return
|
||||
self._catalogue_task = asyncio.create_task(asyncio.to_thread(
|
||||
load_catalogue_release, self.data_dir, self.resource_source,
|
||||
self.service_url, required_ids,
|
||||
))
|
||||
self.catalogue_release = await asyncio.shield(self._catalogue_task)
|
||||
if self._closing or epoch != self._start_epoch:
|
||||
return
|
||||
self.service = FarmService(
|
||||
self.store, self.catalogue_release.catalogue,
|
||||
)
|
||||
await self.store.initialize(catalogue=self.service.catalogue)
|
||||
except Exception:
|
||||
if self._closing or epoch != self._start_epoch:
|
||||
return
|
||||
except BaseException:
|
||||
logger.error("farm preview database initialization failed")
|
||||
await self.close()
|
||||
raise
|
||||
await self._prepare_resources()
|
||||
if self._closing or epoch != self._start_epoch:
|
||||
return
|
||||
self.started = True
|
||||
logger.info("farm preview initialized schema=%s", SCHEMA_VERSION)
|
||||
|
||||
@@ -167,16 +199,20 @@ class Runtime:
|
||||
return
|
||||
resource = 'configuration'
|
||||
try:
|
||||
if self.resource_options_provider is not None:
|
||||
if (self.catalogue_release is None
|
||||
and self.resource_options_provider is not None):
|
||||
self.resource_source, self.service_url = (
|
||||
self.resource_options_provider()
|
||||
)
|
||||
validate_options(self.resource_source, self.service_url)
|
||||
resource = 'assets'
|
||||
bundle_path = await asyncio.to_thread(
|
||||
ensure_assets, self.data_dir, self.resource_source,
|
||||
self.service_url,
|
||||
)
|
||||
selected = self.catalogue_release
|
||||
bundle_path = selected.bundle_path if selected else None
|
||||
if bundle_path is None:
|
||||
bundle_path = await asyncio.to_thread(
|
||||
ensure_assets, self.data_dir, self.resource_source,
|
||||
self.service_url,
|
||||
)
|
||||
resource = 'font'
|
||||
candidate = (Path(self.font_path) if self.font_path else
|
||||
await asyncio.to_thread(
|
||||
@@ -186,6 +222,8 @@ class Runtime:
|
||||
resource = 'renderer'
|
||||
self.rendering = RenderService(FarmRenderer(
|
||||
candidate, bundle_path=bundle_path,
|
||||
metadata=selected.metadata if selected else None,
|
||||
soils_path=selected.soils_path if selected else None,
|
||||
))
|
||||
logger.info('farm resources ready font=%s', candidate.name)
|
||||
except Exception as error:
|
||||
@@ -202,6 +240,7 @@ class Runtime:
|
||||
async def close(self):
|
||||
if self._close_task is None or self._close_task.done():
|
||||
self._closing = True
|
||||
self._start_epoch += 1
|
||||
self._close_task = asyncio.create_task(self._finish_close())
|
||||
task = self._close_task
|
||||
try:
|
||||
@@ -213,6 +252,11 @@ class Runtime:
|
||||
|
||||
async def _finish_close(self):
|
||||
try:
|
||||
if self._catalogue_task is not None:
|
||||
try:
|
||||
await asyncio.shield(self._catalogue_task)
|
||||
except Exception:
|
||||
pass
|
||||
async with self._resource_lock:
|
||||
if self.rendering:
|
||||
await self.rendering.close()
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@
|
||||
- [x] 完善我的农场图片,例如左上角显示用户数据
|
||||
- [ ] 完善升级数据、作物数据、作物图片
|
||||
- [x] 签到功能
|
||||
- [x] 在线更新作物信息
|
||||
- [x] 作物信息启动时检查更新(Service维护,失败回退已验证缓存)
|
||||
- [ ] 添加渔场功能
|
||||
- [ ] 增加活动、交易行功能
|
||||
- [ ] 增加交易行总行功能
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@
|
||||
- [x] 完善我的农场图片,例如左上角显示用户数据
|
||||
- [ ] 完善升级数据、作物数据、作物图片
|
||||
- [x] 签到功能
|
||||
- [x] 在线更新作物信息
|
||||
- [x] 作物信息启动时检查更新(Service维护,失败回退已验证缓存)
|
||||
- [ ] 添加渔场功能
|
||||
- [ ] 增加活动、交易行功能
|
||||
- [ ] 增加交易行总行功能
|
||||
|
||||
+20
-4
@@ -50,8 +50,24 @@
|
||||
|
||||
## 如何安装
|
||||
|
||||
方法一(推荐):在小真寻后台的插件商店下载即可<br>
|
||||
方法二:下载源码放在小真寻`plugin`目录下
|
||||
使用真寻分发仓库的完整生成内容或Actions中的真寻预览包,不能直接
|
||||
复制主仓库的integrations目录。先停止机器人,备份代码与数据,停用
|
||||
会注册同名命令的旧插件,再在宿主环境安装本插件requirements.txt。
|
||||
Pillow要求>=10.4,<11,不强制升级宿主到12。安装后手工启动并检查日志。
|
||||
|
||||
默认数据目录为真寻DATA_PATH下的farm_db,新库为farm.sqlite3,日志为
|
||||
farm-preview.log;旧farm.db不得直接改名覆盖。资源源默认Github,可在
|
||||
网页配置选择Gitea/farm_service;服务地址独立保留,修改资源源后重载。
|
||||
图片质量默认75、整数1~95,仅“我的农场 高清”支持高清且质量至少85。
|
||||
资源或字体下载失败时文字业务仍可用,详细原因见日志。
|
||||
|
||||
升级前停机备份完整代码与数据目录;安装器拒绝覆盖,先移走旧目录。
|
||||
回退需匹配代码和数据库备份,不让旧代码强行打开新schema;恢复旧数据
|
||||
会丢失备份后进度。卸载时停止加载并移走插件代码,保留数据供恢复。
|
||||
|
||||
开发者构建、包验证、安全安装、配置、备份卸载与Secrets详见
|
||||
[当前维护说明](https://github.com/Shu-Ying/main_plugin_farm/blob/main/docs/zhenxun-preview-current.md)。
|
||||
本地离线验证不代表真实真寻宿主、公开资源或远端发布已验证。
|
||||
|
||||
---
|
||||
|
||||
@@ -64,8 +80,8 @@
|
||||
|
||||
- [x] 完善我的农场图片,例如左上角显示用户数据
|
||||
- [ ] 完善升级数据、作物数据、作物图片
|
||||
- [x] 签到功能
|
||||
- [x] 在线更新作物信息
|
||||
- [ ] 签到功能
|
||||
- [x] 作物信息启动时检查更新(Service维护,失败回退已验证缓存)
|
||||
- [ ] 添加渔场功能
|
||||
- [ ] 增加活动、交易行功能
|
||||
- [ ] 增加交易行总行功能
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"observed_default_branch": "main",
|
||||
"entry_directory": "integrations/zhenxun",
|
||||
"enabled": true,
|
||||
"preview_branch": "farm-preview",
|
||||
"preview_branch": "main",
|
||||
"publish_by_default": false
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,8 +3,14 @@
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from zipfile import ZipFile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'src'))
|
||||
from farm_core.catalogue_release import parse_manifest
|
||||
from farm_render.catalogue_bundle import ROOTS
|
||||
|
||||
|
||||
def check(root):
|
||||
data = Path(root) / 'src/farm_render/data'
|
||||
@@ -19,12 +25,13 @@ def check(root):
|
||||
with ZipFile(bundle) as archive:
|
||||
if set(archive.namelist()) != set(metadata['images']):
|
||||
raise ValueError('Asset manifest and layout differ')
|
||||
for source, prefix in (
|
||||
('github', 'https://raw.githubusercontent.com/Shu-Ying/'
|
||||
'farm_service/main/assets/assets.zip'),
|
||||
('gitea', 'https://gitea.diuse.work/Diuse/farm_service/'
|
||||
'raw/branch/main/assets/assets.zip'),
|
||||
):
|
||||
bootstrap = parse_manifest((Path(root) / 'src/farm_core/data/'
|
||||
'catalogue-manifest.json').read_bytes())
|
||||
if manifest.get('catalogue_release') != bootstrap['release']:
|
||||
raise ValueError('Bootstrap and asset release differ')
|
||||
for name, base in ROOTS.items():
|
||||
source = name.lower()
|
||||
prefix = base + '/releases/' + bootstrap['release'] + '/assets.zip'
|
||||
if manifest['sources'][source] != prefix:
|
||||
raise ValueError('Asset source must use service repository')
|
||||
print('Verified external asset manifest and development ZIP')
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"""CI只核对插件初始版本自身,不访问远端或相邻Service开发仓库。"""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'src'))
|
||||
|
||||
from farm_core.catalogue_release import parse_manifest
|
||||
from farm_render.catalogue_bundle import _validate
|
||||
|
||||
|
||||
def check(root=ROOT):
|
||||
root = Path(root)
|
||||
core = root / 'src/farm_core/data'
|
||||
render = root / 'src/farm_render/data'
|
||||
manifest = parse_manifest((core / 'catalogue-manifest.json').read_bytes())
|
||||
content = {'plants.json': (core / 'plants.json').read_bytes(),
|
||||
**{name: (render / name).read_bytes() for name in (
|
||||
'metadata.json', 'assets.zip', 'land-soils.zip',
|
||||
)}}
|
||||
_validate(content, manifest, None, ())
|
||||
print('Verified independent catalogue bootstrap ' + manifest['release'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
check()
|
||||
@@ -37,14 +37,24 @@ def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--source", type=Path,
|
||||
default=root / "legacy/zhenxun_plugin_farm/resource/db/plant.db",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output", type=Path,
|
||||
default=root / "src/farm_core/data/plants.json",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument("--check", action="store_true")
|
||||
parser.add_argument('--legacy-reference', action='store_true')
|
||||
args = parser.parse_args()
|
||||
if args.check and args.source is None:
|
||||
from check_catalogue_bootstrap import check
|
||||
check(root)
|
||||
return
|
||||
if not args.legacy_reference or args.output is None:
|
||||
parser.error('Daily updates use sync_catalogue_bootstrap.py; '
|
||||
'legacy export requires --legacy-reference --output')
|
||||
if args.source is None:
|
||||
args.source = root / 'legacy/zhenxun_plugin_farm/resource/db/plant.db'
|
||||
content = export(args.source)
|
||||
if args.check:
|
||||
if not args.output.is_file() or args.output.read_bytes() != content:
|
||||
|
||||
@@ -119,9 +119,10 @@ def export(root):
|
||||
) for theme in ("default", "spring", "summer", "autumn", "winter", "bee")]
|
||||
paths += list((source / "background/decorateIcon").glob("decorate_*.png"))
|
||||
layout = json.loads((legacy / "config/soil.json").read_bytes())
|
||||
catalogue = json.loads((
|
||||
root / "src/farm_core/data/plants.json"
|
||||
).read_bytes())
|
||||
from export_catalogue import export as export_legacy_catalogue
|
||||
catalogue = json.loads(export_legacy_catalogue(
|
||||
legacy / 'resource/db/plant.db',
|
||||
))
|
||||
metadata = {
|
||||
"schema": 1, "canvas": [3072, 1872], "soil_size": layout["size"],
|
||||
"positions": [layout["soil"][str(i)] for i in range(1, 31)],
|
||||
@@ -188,10 +189,19 @@ def main():
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--check", action="store_true")
|
||||
parser.add_argument('--legacy-reference', action='store_true')
|
||||
parser.add_argument('--output', type=Path)
|
||||
args = parser.parse_args()
|
||||
if args.check and not args.legacy_reference:
|
||||
from check_catalogue_bootstrap import check
|
||||
check(root)
|
||||
return
|
||||
if not args.legacy_reference or args.output is None:
|
||||
parser.error('Daily updates use Service prepare_catalogue.py; '
|
||||
'legacy export requires --legacy-reference --output')
|
||||
files = (*zip(("assets.zip", "metadata.json"), export(root)),
|
||||
('land-soils.zip', export_soils(root)))
|
||||
destination = root / "src/farm_render/data"
|
||||
destination = args.output
|
||||
for name, content in files:
|
||||
path = destination / name
|
||||
if args.check:
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""显式从已验证Service发布目录刷新插件初始版本,不联网。"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'src'))
|
||||
|
||||
from farm_core import Catalogue
|
||||
from farm_core.catalogue_release import canonical, parse_manifest
|
||||
from farm_render.catalogue_bundle import ROOTS, _snapshot
|
||||
|
||||
|
||||
def sync(source, root=ROOT):
|
||||
source, root = Path(source), Path(root)
|
||||
manifest = parse_manifest((source / 'manifest.json').read_bytes())
|
||||
release = source / 'releases' / manifest['release']
|
||||
snapshot = _snapshot(release, manifest, Catalogue.bundled())
|
||||
destinations = {
|
||||
'plants.json': root / 'src/farm_core/data/plants.json',
|
||||
**{name: root / 'src/farm_render/data' / name for name in (
|
||||
'metadata.json', 'assets.zip', 'land-soils.zip',
|
||||
)},
|
||||
}
|
||||
for name, path in destinations.items():
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(release / name, path)
|
||||
data = root / 'src/farm_core/data'
|
||||
(data / 'catalogue-manifest.json').write_bytes(canonical(manifest) + b'\n')
|
||||
asset = {
|
||||
'schema': 1, 'version': manifest['version'],
|
||||
'sha256': manifest['files']['assets.zip']['sha256'],
|
||||
'size_bytes': manifest['files']['assets.zip']['size_bytes'],
|
||||
'catalogue_release': manifest['release'],
|
||||
'sources': {
|
||||
name.lower(): base + '/releases/' + manifest['release']
|
||||
+ '/assets.zip' for name, base in ROOTS.items()
|
||||
},
|
||||
}
|
||||
asset['sources']['farm_service'] = (
|
||||
'https://farm.example/v1/catalogue/releases/'
|
||||
+ manifest['release'] + '/assets.zip'
|
||||
)
|
||||
(root / 'src/farm_render/data/asset-manifest.json').write_text(
|
||||
json.dumps(asset, ensure_ascii=False, indent=2) + '\n',
|
||||
encoding='utf-8',
|
||||
)
|
||||
return snapshot.manifest
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--source', type=Path, required=True,
|
||||
help='Service catalogue发布目录,不是可编辑source目录')
|
||||
args = parser.parse_args()
|
||||
print('Bootstrap synced release=' + sync(args.source)['release'])
|
||||
@@ -14,8 +14,7 @@ def main():
|
||||
}
|
||||
commands = (
|
||||
[sys.executable, "scripts/check_baseline.py"],
|
||||
[sys.executable, "scripts/export_catalogue.py", "--check"],
|
||||
[sys.executable, "scripts/export_render_assets.py", "--check"],
|
||||
[sys.executable, 'scripts/check_catalogue_bootstrap.py'],
|
||||
[sys.executable, 'scripts/check_asset_manifest.py'],
|
||||
[
|
||||
sys.executable, "-m", "unittest", "discover",
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
"""作物发布协议的纯离线校验;不联网、不依赖绘图库。"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
|
||||
from .catalogue import Catalogue
|
||||
|
||||
FILES = {
|
||||
'plants.json': 2 * 1024 * 1024,
|
||||
'metadata.json': 4 * 1024 * 1024,
|
||||
'assets.zip': 64 * 1024 * 1024,
|
||||
'land-soils.zip': 64 * 1024 * 1024,
|
||||
}
|
||||
MANIFEST_LIMIT = 16 * 1024
|
||||
DIGEST = re.compile(r'[0-9a-f]{64}')
|
||||
VERSION = re.compile(r'[A-Za-z0-9._-]{1,64}')
|
||||
|
||||
|
||||
def _pairs(items):
|
||||
result = {}
|
||||
for key, value in items:
|
||||
if key in result:
|
||||
raise ValueError('Duplicate JSON key')
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def decode(content):
|
||||
def invalid(_value):
|
||||
raise ValueError('Non-finite JSON value')
|
||||
try:
|
||||
return json.loads(content, object_pairs_hook=_pairs,
|
||||
parse_constant=invalid)
|
||||
except (UnicodeError, TypeError, RecursionError) as error:
|
||||
raise ValueError('Invalid release JSON') from error
|
||||
|
||||
|
||||
def canonical(value):
|
||||
return json.dumps(value, sort_keys=True, ensure_ascii=False,
|
||||
separators=(',', ':'), allow_nan=False).encode()
|
||||
|
||||
|
||||
def digest(content):
|
||||
return hashlib.sha256(content).hexdigest()
|
||||
|
||||
|
||||
def make_manifest(files, version):
|
||||
descriptors = {
|
||||
name: {'size_bytes': len(content), 'sha256': digest(content)}
|
||||
for name, content in files.items()
|
||||
}
|
||||
result = {'schema': 1, 'version': version,
|
||||
'release': digest(canonical(descriptors)), 'files': descriptors}
|
||||
return parse_manifest(canonical(result))
|
||||
|
||||
|
||||
def parse_manifest(content):
|
||||
if len(content) > MANIFEST_LIMIT:
|
||||
raise ValueError('Catalogue manifest exceeds limit')
|
||||
value = decode(content)
|
||||
if (type(value) is not dict or set(value) != {
|
||||
'schema', 'version', 'release', 'files'}
|
||||
or type(value['schema']) is not int or value['schema'] != 1
|
||||
or not isinstance(value['version'], str)
|
||||
or VERSION.fullmatch(value['version']) is None
|
||||
or not isinstance(value['release'], str)
|
||||
or DIGEST.fullmatch(value['release']) is None
|
||||
or type(value['files']) is not dict
|
||||
or set(value['files']) != set(FILES)):
|
||||
raise ValueError('Invalid catalogue manifest')
|
||||
for name, limit in FILES.items():
|
||||
item = value['files'][name]
|
||||
if (type(item) is not dict or set(item) != {'size_bytes', 'sha256'}
|
||||
or type(item['size_bytes']) is not int
|
||||
or not 0 < item['size_bytes'] <= limit
|
||||
or not isinstance(item['sha256'], str)
|
||||
or DIGEST.fullmatch(item['sha256']) is None):
|
||||
raise ValueError('Invalid catalogue file descriptor')
|
||||
if digest(canonical(value['files'])) != value['release']:
|
||||
raise ValueError('Catalogue release fingerprint differs')
|
||||
return value
|
||||
|
||||
|
||||
def check_file(content, descriptor):
|
||||
if (len(content) != descriptor['size_bytes']
|
||||
or digest(content) != descriptor['sha256']):
|
||||
raise ValueError('Catalogue file size or SHA256 differs')
|
||||
|
||||
|
||||
def load_catalogue(content, previous=None, required_ids=()):
|
||||
value = decode(content)
|
||||
if (type(value) is not dict or type(value.get('plants')) is not list
|
||||
or not 0 < len(value['plants']) <= 10000):
|
||||
raise ValueError('Invalid catalogue document')
|
||||
catalogue = Catalogue.loads(content)
|
||||
names = [plant.name for plant in catalogue]
|
||||
if (len(set(names)) != len(names) or any(
|
||||
name in ('.', '..') or '/' in name or '\\' in name
|
||||
for name in names)):
|
||||
raise ValueError('Invalid or duplicate catalogue name')
|
||||
indexed = {plant.plant_id: plant for plant in catalogue}
|
||||
if not set(required_ids).issubset(indexed):
|
||||
raise ValueError('catalogue missing player crop ID')
|
||||
if previous is not None:
|
||||
for plant in previous:
|
||||
current = indexed.get(plant.plant_id)
|
||||
if current is None or current.name != plant.name:
|
||||
raise ValueError('Catalogue removed or renamed crop ID')
|
||||
return catalogue
|
||||
@@ -0,0 +1 @@
|
||||
{"files":{"assets.zip":{"sha256":"78d6e8e448fd5296b21d471b98ea7d58c81ef78a82b0e192a1ed041bb5fddad9","size_bytes":21264561},"land-soils.zip":{"sha256":"e27151f0b92c36842c23d9bf3d4855e786ef5a9db37c736e751b53be8d3ebda9","size_bytes":57889},"metadata.json":{"sha256":"6ba1c2134d1a853cd4c2bfd6489e68bd59a0c6068cd79289a1f4bcaa62f2444d","size_bytes":197449},"plants.json":{"sha256":"e05e4430643710ccbeeeb1c7c8640ec5962ee097de6b3d5fad0fb7860c596a2b","size_bytes":87259}},"release":"4c88736c2e9b1b5eec4568e953152d59a6918242802da72a6eec1a615a17f18b","schema":1,"version":"catalogue-20261026-1"}
|
||||
@@ -4445,6 +4445,5 @@
|
||||
"vipBuy": 0
|
||||
}
|
||||
],
|
||||
"schema": 1,
|
||||
"source_sha256": "21f11e818909dda4d60864374062c46e0c86e9c2f01d2759225aba466d9de9eb"
|
||||
"schema": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""跨进程发布/缓存锁;进程退出由操作系统释放,不使用永久锁标记。"""
|
||||
|
||||
from contextlib import contextmanager
|
||||
import os
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
|
||||
@contextmanager
|
||||
def release_lock(path, timeout=180):
|
||||
path = Path(path)
|
||||
with path.open('a+b') as stream:
|
||||
if stream.seek(0, os.SEEK_END) == 0:
|
||||
stream.write(b'0')
|
||||
stream.flush()
|
||||
deadline = time.monotonic() + timeout
|
||||
if os.name == 'nt':
|
||||
import msvcrt
|
||||
def acquire():
|
||||
stream.seek(0)
|
||||
msvcrt.locking(stream.fileno(), msvcrt.LK_NBLCK, 1)
|
||||
def unlock():
|
||||
stream.seek(0)
|
||||
msvcrt.locking(stream.fileno(), msvcrt.LK_UNLCK, 1)
|
||||
else:
|
||||
import fcntl
|
||||
def acquire():
|
||||
fcntl.flock(stream.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
def unlock():
|
||||
fcntl.flock(stream.fileno(), fcntl.LOCK_UN)
|
||||
while True:
|
||||
try:
|
||||
acquire()
|
||||
break
|
||||
except OSError:
|
||||
if time.monotonic() >= deadline:
|
||||
raise TimeoutError('Catalogue cache is busy') from None
|
||||
time.sleep(0.05)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
unlock()
|
||||
@@ -103,6 +103,50 @@ SCHEMA = (
|
||||
|
||||
|
||||
class SQLiteStore:
|
||||
async def catalogue_ids(self):
|
||||
"""初始化前只读检查库存与地块ID;不创建文件或迁移数据库。"""
|
||||
task = asyncio.create_task(asyncio.to_thread(self._catalogue_ids))
|
||||
try:
|
||||
return await asyncio.shield(task)
|
||||
except asyncio.CancelledError:
|
||||
try:
|
||||
await task
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
|
||||
def _catalogue_ids(self):
|
||||
if not self.path.is_file():
|
||||
return set()
|
||||
with closing(sqlite3.connect(
|
||||
self.path.as_uri() + '?mode=ro', uri=True)) as connection:
|
||||
connection.execute('BEGIN')
|
||||
app = connection.execute('PRAGMA application_id').fetchone()[0]
|
||||
version = connection.execute('PRAGMA user_version').fetchone()[0]
|
||||
tables = {row[0] for row in connection.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table'",
|
||||
)}
|
||||
if app == 0 and version == 0 and not tables:
|
||||
return set()
|
||||
if app != APPLICATION_ID or not 1 <= version <= SCHEMA_VERSION:
|
||||
raise SchemaError('Unsupported farm database')
|
||||
result = set()
|
||||
if version >= 2:
|
||||
result.update(row[0] for row in connection.execute(
|
||||
'SELECT DISTINCT plant_id FROM inventory '
|
||||
'WHERE seeds>0 OR crops>0',
|
||||
))
|
||||
for row in connection.execute(
|
||||
'SELECT plant FROM plots WHERE plant IS NOT NULL'):
|
||||
try:
|
||||
result.add(json.loads(row[0])['plant_id'])
|
||||
except (KeyError, TypeError, ValueError) as error:
|
||||
raise SchemaError('Invalid saved crop definition') \
|
||||
from error
|
||||
if any(type(value) is not int or value <= 0 for value in result):
|
||||
raise SchemaError('Invalid saved crop ID')
|
||||
return result
|
||||
|
||||
def __init__(self, path: str | Path, timeout: float = 10):
|
||||
if str(path) == ":memory:":
|
||||
raise ValueError("Use a dedicated file for transaction connections")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""包内素材与有界 LRU;不下载、不解压到磁盘、不暴露共享图片。"""
|
||||
|
||||
from collections import OrderedDict
|
||||
from copy import deepcopy
|
||||
from importlib import resources
|
||||
from io import BytesIO
|
||||
import json
|
||||
@@ -42,10 +43,14 @@ class ByteLRU:
|
||||
|
||||
|
||||
class Assets:
|
||||
def __init__(self, cache_bytes=32 * 1024 * 1024, bundle_path=None):
|
||||
def __init__(self, cache_bytes=32 * 1024 * 1024, bundle_path=None,
|
||||
metadata=None, soils_path=None):
|
||||
data = resources.files(__package__).joinpath("data")
|
||||
self.metadata = json.loads(data.joinpath("metadata.json").read_bytes())
|
||||
self.metadata = (deepcopy(metadata) if metadata is not None else
|
||||
json.loads(data.joinpath('metadata.json')
|
||||
.read_bytes()))
|
||||
self.cache = ByteLRU(cache_bytes)
|
||||
self._soils = soils_path or data.joinpath('land-soils.zip')
|
||||
self._bundle = (bundle_path if bundle_path is not None
|
||||
else data.joinpath('assets.zip'))
|
||||
|
||||
@@ -58,9 +63,7 @@ class Assets:
|
||||
key = (name, size)
|
||||
cached = self.cache.get(key)
|
||||
if cached is None:
|
||||
source = (resources.files(__package__).joinpath(
|
||||
'data', 'land-soils.zip',
|
||||
) if upgrade else self._bundle)
|
||||
source = self._soils if upgrade else self._bundle
|
||||
with source.open("rb") as stream, ZipFile(stream) as bundle:
|
||||
content = bundle.read(name)
|
||||
with Image.open(BytesIO(content)) as original:
|
||||
|
||||
@@ -99,8 +99,12 @@ def ensure_assets(data_dir, source='Github', service_url=''):
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
# 清单绑定在插件内;远端不能通过更换清单放行不兼容的素材。
|
||||
if source == 'farm_service':
|
||||
url = (service_url.rstrip('/') + '/v1/assets/'
|
||||
+ expected['sha256'] + '/assets.zip')
|
||||
if 'catalogue_release' in expected:
|
||||
url = (service_url.rstrip('/') + '/v1/catalogue/releases/'
|
||||
+ expected['catalogue_release'] + '/assets.zip')
|
||||
else:
|
||||
url = (service_url.rstrip('/') + '/v1/assets/'
|
||||
+ expected['sha256'] + '/assets.zip')
|
||||
else:
|
||||
url = expected['sources'][source.lower()]
|
||||
temporary = None
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
"""启动时选择完整作物版本;网络失败保留已验证缓存或初始目录。"""
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
from importlib import resources
|
||||
from io import BytesIO
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import threading
|
||||
import uuid
|
||||
from zipfile import BadZipFile
|
||||
|
||||
from farm_core.catalogue_release import (
|
||||
FILES, MANIFEST_LIMIT, canonical, check_file, decode, load_catalogue,
|
||||
parse_manifest,
|
||||
)
|
||||
from farm_core.domain import FarmError
|
||||
from farm_core.release_lock import release_lock
|
||||
|
||||
from .bundle import _copy_remote, validate_options
|
||||
from .release_validation import validate_archive, validate_metadata
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_LOCK = threading.RLock()
|
||||
_FAILURES = (OSError, ValueError, FarmError, EOFError, BadZipFile)
|
||||
ROOTS = {
|
||||
'Github': 'https://raw.githubusercontent.com/Shu-Ying/'
|
||||
'farm_service/main/catalogue',
|
||||
'Gitea': 'https://gitea.diuse.work/Diuse/farm_service/'
|
||||
'raw/branch/main/catalogue',
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReleaseSnapshot:
|
||||
catalogue: object # 完整且已校验的离线业务目录。
|
||||
metadata: dict # 与目录和图片绑定的展示配置。
|
||||
bundle_path: Path | None # 无图片缓存的离线初始启动可为空。
|
||||
soils_path: Path # 小型升级土地资源所在路径。
|
||||
manifest: dict # 本次采用的发布版本和四文件摘要。
|
||||
origin: str # remote、cache或bootstrap,仅用于安全日志。
|
||||
|
||||
|
||||
def _fetch(url, limit):
|
||||
output = BytesIO()
|
||||
_copy_remote(url, output, limit)
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
def _read(path, limit):
|
||||
with path.open('rb') as stream:
|
||||
content = stream.read(limit + 1)
|
||||
if len(content) > limit:
|
||||
raise ValueError('Catalogue file exceeds limit')
|
||||
return content
|
||||
|
||||
|
||||
def _atomic(path, content):
|
||||
temporary = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
dir=path.parent, suffix='.download', delete=False) as stream:
|
||||
temporary = Path(stream.name)
|
||||
stream.write(content)
|
||||
temporary.replace(path)
|
||||
finally:
|
||||
if temporary is not None:
|
||||
temporary.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def _validate(content, manifest, previous, required_ids):
|
||||
for name, value in content.items():
|
||||
check_file(value, manifest['files'][name])
|
||||
catalogue = load_catalogue(content['plants.json'], previous, required_ids)
|
||||
metadata = validate_metadata(content['metadata.json'], catalogue)
|
||||
for row in decode(content['plants.json'])['plants']:
|
||||
info = metadata['plants'][row['name']]
|
||||
if (info['phase'] != row['phase']
|
||||
or info.get('general') != row.get('general')):
|
||||
raise ValueError('Catalogue and render growth definitions differ')
|
||||
if 'assets.zip' in content:
|
||||
validate_archive(content['assets.zip'], metadata['images'])
|
||||
validate_archive(content['land-soils.zip'])
|
||||
return catalogue, metadata
|
||||
|
||||
|
||||
def _snapshot(root, manifest, previous=None, required_ids=(), origin='cache'):
|
||||
content = {name: _read(root / name, checks['size_bytes'])
|
||||
for name, checks in manifest['files'].items()}
|
||||
catalogue, metadata = _validate(
|
||||
content, manifest, previous, required_ids,
|
||||
)
|
||||
return ReleaseSnapshot(catalogue, metadata, root / 'assets.zip',
|
||||
root / 'land-soils.zip', manifest, origin)
|
||||
|
||||
|
||||
def _bootstrap(required_ids=()):
|
||||
core_name = ('farm_core' if __package__ == 'farm_render' else
|
||||
__package__.rsplit('.', 1)[0] + '.farm_core')
|
||||
core = resources.files(core_name).joinpath('data')
|
||||
render = resources.files(__package__).joinpath('data')
|
||||
manifest = parse_manifest(core.joinpath(
|
||||
'catalogue-manifest.json',
|
||||
).read_bytes())
|
||||
content = {
|
||||
'plants.json': core.joinpath('plants.json').read_bytes(),
|
||||
'metadata.json': render.joinpath('metadata.json').read_bytes(),
|
||||
'land-soils.zip': render.joinpath('land-soils.zip').read_bytes(),
|
||||
}
|
||||
catalogue, metadata = _validate(content, manifest, None, required_ids)
|
||||
return ReleaseSnapshot(catalogue, metadata, None,
|
||||
Path(str(render.joinpath('land-soils.zip'))),
|
||||
manifest, 'bootstrap')
|
||||
|
||||
|
||||
def load_catalogue_release(data_dir, source='Github', service_url='',
|
||||
required_ids=()):
|
||||
"""每次调用只检查一次最新版本;调用方仅在启动时调用。"""
|
||||
validate_options(source, service_url)
|
||||
base = (service_url.rstrip('/') + '/v1/catalogue'
|
||||
if source == 'farm_service' else ROOTS[source])
|
||||
root = Path(data_dir) / 'catalogue'
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
with _LOCK, release_lock(root / '.cache.lock'):
|
||||
releases = root / 'releases'
|
||||
releases.mkdir(exist_ok=True)
|
||||
bootstrap = _bootstrap()
|
||||
current = None
|
||||
try:
|
||||
manifest = parse_manifest(_read(root / 'current.json',
|
||||
MANIFEST_LIMIT))
|
||||
current = _snapshot(
|
||||
releases / manifest['release'], manifest,
|
||||
bootstrap.catalogue,
|
||||
)
|
||||
except _FAILURES:
|
||||
pass
|
||||
previous = (current or bootstrap).catalogue
|
||||
try:
|
||||
manifest = parse_manifest(_fetch(
|
||||
base + '/manifest.json', MANIFEST_LIMIT,
|
||||
))
|
||||
target = releases / manifest['release']
|
||||
if (current is not None
|
||||
and current.manifest['release'] == manifest['release']):
|
||||
if not set(required_ids).issubset(
|
||||
plant.plant_id for plant in current.catalogue):
|
||||
raise ValueError('catalogue missing player crop ID')
|
||||
return replace(current, origin='remote')
|
||||
try:
|
||||
candidate = _snapshot(target, manifest, previous, required_ids,
|
||||
'remote')
|
||||
except _FAILURES:
|
||||
with tempfile.TemporaryDirectory(
|
||||
dir=releases, prefix='.download-') as temporary:
|
||||
staging = Path(temporary)
|
||||
for name, item in manifest['files'].items():
|
||||
content = _fetch(
|
||||
base + '/releases/' + manifest['release']
|
||||
+ '/' + name, item['size_bytes'],
|
||||
)
|
||||
check_file(content, item)
|
||||
(staging / name).write_bytes(content)
|
||||
_snapshot(staging, manifest, previous, required_ids)
|
||||
if target.exists():
|
||||
try:
|
||||
_snapshot(target, manifest, previous, required_ids)
|
||||
except _FAILURES:
|
||||
target.rename(releases / (
|
||||
'.invalid-' + uuid.uuid4().hex
|
||||
))
|
||||
if not target.exists():
|
||||
try:
|
||||
staging.rename(target)
|
||||
except OSError:
|
||||
if not target.exists():
|
||||
raise
|
||||
candidate = _snapshot(target, manifest, previous,
|
||||
required_ids, 'remote')
|
||||
previous_images = (current or bootstrap).metadata['images']
|
||||
if not set(previous_images).issubset(candidate.metadata['images']):
|
||||
raise ValueError('Catalogue removed existing image paths')
|
||||
_atomic(root / 'current.json', canonical(manifest))
|
||||
logger.info('farm catalogue ready release=%s source=%s',
|
||||
manifest['release'], source)
|
||||
return candidate
|
||||
except _FAILURES as error:
|
||||
logger.warning('farm catalogue update unavailable source=%s '
|
||||
'reason=%s; keeping verified fallback',
|
||||
source, type(error).__name__)
|
||||
if current is not None:
|
||||
if not set(required_ids).issubset(
|
||||
plant.plant_id for plant in current.catalogue):
|
||||
raise ValueError('catalogue missing player crop ID')
|
||||
return current
|
||||
bootstrap = _bootstrap(required_ids)
|
||||
# 若旧方式图片缓存可用,离线初始版本也能继续绘图。
|
||||
cached = (Path(data_dir) / 'assets'
|
||||
/ bootstrap.manifest['files']['assets.zip']['sha256']
|
||||
/ 'assets.zip')
|
||||
try:
|
||||
content = _read(cached, FILES['assets.zip'])
|
||||
check_file(content, bootstrap.manifest['files']['assets.zip'])
|
||||
validate_archive(content, bootstrap.metadata['images'])
|
||||
bootstrap = replace(bootstrap, bundle_path=cached)
|
||||
except _FAILURES:
|
||||
pass
|
||||
return bootstrap
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"version": "farm-assets-v1",
|
||||
"version": "catalogue-20261026-1",
|
||||
"sha256": "78d6e8e448fd5296b21d471b98ea7d58c81ef78a82b0e192a1ed041bb5fddad9",
|
||||
"size_bytes": 21264561,
|
||||
"catalogue_release": "4c88736c2e9b1b5eec4568e953152d59a6918242802da72a6eec1a615a17f18b",
|
||||
"sources": {
|
||||
"github": "https://raw.githubusercontent.com/Shu-Ying/farm_service/main/assets/assets.zip",
|
||||
"gitea": "https://gitea.diuse.work/Diuse/farm_service/raw/branch/main/assets/assets.zip",
|
||||
"farm_service": "https://farm.example/v1/assets/78d6e8e448fd5296b21d471b98ea7d58c81ef78a82b0e192a1ed041bb5fddad9/assets.zip"
|
||||
"github": "https://raw.githubusercontent.com/Shu-Ying/farm_service/main/catalogue/releases/4c88736c2e9b1b5eec4568e953152d59a6918242802da72a6eec1a615a17f18b/assets.zip",
|
||||
"gitea": "https://gitea.diuse.work/Diuse/farm_service/raw/branch/main/catalogue/releases/4c88736c2e9b1b5eec4568e953152d59a6918242802da72a6eec1a615a17f18b/assets.zip",
|
||||
"farm_service": "https://farm.example/v1/catalogue/releases/4c88736c2e9b1b5eec4568e953152d59a6918242802da72a6eec1a615a17f18b/assets.zip"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
"""校验同版本展示配置及ZIP图片,不将不可信路径解压到磁盘。"""
|
||||
|
||||
from io import BytesIO
|
||||
import math
|
||||
from pathlib import PurePosixPath
|
||||
from zipfile import ZipFile
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from farm_core.catalogue_release import decode, digest
|
||||
|
||||
|
||||
def _numbers(values, length):
|
||||
if (type(values) is not list or len(values) != length
|
||||
or any(type(v) not in (int, float) or abs(v) > 10000000
|
||||
or not math.isfinite(v) for v in values)):
|
||||
raise ValueError('Invalid render coordinates')
|
||||
return values
|
||||
|
||||
|
||||
def _size(values):
|
||||
_numbers(values, 2)
|
||||
if (any(type(v) is not int or not 0 < v <= 8192 for v in values)
|
||||
or values[0] * values[1] > 32 * 1024 * 1024):
|
||||
raise ValueError('Invalid image dimensions')
|
||||
|
||||
|
||||
def _rect(values):
|
||||
_numbers(values, 4)
|
||||
if values[2] <= 0 or values[3] <= 0:
|
||||
raise ValueError('Invalid render rectangle')
|
||||
|
||||
|
||||
def safe_member(name):
|
||||
if (not isinstance(name, str) or not name or len(name) > 512
|
||||
or '\\' in name or ':' in name):
|
||||
raise ValueError('Invalid image path')
|
||||
path = PurePosixPath(name)
|
||||
if path.is_absolute() or any(p in ('', '.', '..') for p in name.split('/')):
|
||||
raise ValueError('Invalid image path')
|
||||
|
||||
|
||||
def validate_metadata(content, catalogue):
|
||||
value = decode(content)
|
||||
try:
|
||||
if type(value['schema']) is not int or value['schema'] != 1:
|
||||
raise ValueError('Unknown render metadata schema')
|
||||
_size(value['canvas'])
|
||||
_size(value['soil_size'])
|
||||
if len(value['positions']) != 30:
|
||||
raise ValueError('Expected 30 plot positions')
|
||||
for pos in value['positions']:
|
||||
_numbers([pos['x'], pos['y']], 2)
|
||||
images = value['images']
|
||||
if not isinstance(images, dict) or not 0 < len(images) <= 10000:
|
||||
raise ValueError('Invalid image manifest')
|
||||
for name, item in images.items():
|
||||
safe_member(name)
|
||||
_size(item['size'])
|
||||
if (not isinstance(item['sha256'], str)
|
||||
or len(item['sha256']) != 64
|
||||
or any(c not in '0123456789abcdef'
|
||||
for c in item['sha256'])):
|
||||
raise ValueError('Invalid image checksum')
|
||||
plants = value['plants']
|
||||
if set(plants) != {plant.name for plant in catalogue}:
|
||||
raise ValueError('Catalogue and render plants differ')
|
||||
for plant in catalogue:
|
||||
phases = plants[plant.name]['phase'].split(',')
|
||||
numbers = list(dict.fromkeys(int(v) for v in phases))
|
||||
if (len(numbers) < 2 or min(numbers) <= 0
|
||||
or numbers != sorted(numbers)):
|
||||
raise ValueError('Invalid render growth phases')
|
||||
placement = value['placement']
|
||||
if (type(placement['schema']) is not int
|
||||
or placement['schema'] != 4
|
||||
or placement['mode'] != 'visible_contact_center'):
|
||||
raise ValueError('Unknown crop placement')
|
||||
for key in ('common_scale', 'growth_scale', 'reference_soil_height',
|
||||
'reference_soil_width'):
|
||||
_numbers([placement[key]], 1)
|
||||
if placement[key] <= 0:
|
||||
raise ValueError('Invalid crop scale')
|
||||
_numbers([placement['contact_alpha_threshold']], 1)
|
||||
_numbers([placement['contact_band_fraction']], 1)
|
||||
if (not 0 <= placement['contact_alpha_threshold'] <= 255
|
||||
or not 0 < placement['contact_band_fraction'] <= 1):
|
||||
raise ValueError('Invalid ground contact rule')
|
||||
matrix = placement['scene_transform']
|
||||
if len(matrix) != 2:
|
||||
raise ValueError('Invalid scene transform')
|
||||
a, b = _numbers(matrix[0], 2)
|
||||
c, d = _numbers(matrix[1], 2)
|
||||
if abs(a*d-b*c) < 1e-8:
|
||||
raise ValueError('Degenerate scene transform')
|
||||
pest = value['pest']
|
||||
_size(pest['display_size'])
|
||||
if (len(pest['frames']) < 3
|
||||
or len(set(pest['frames'])) != len(pest['frames'])):
|
||||
raise ValueError('Invalid pest frames')
|
||||
for name in pest['frames']:
|
||||
if name not in images:
|
||||
raise ValueError('Missing pest image')
|
||||
if len(pest['positions']) < 3:
|
||||
raise ValueError('Missing pest positions')
|
||||
for pos in pest['positions']:
|
||||
_numbers(pos, 2)
|
||||
panel = value['panel']
|
||||
_size(panel['reference_canvas'])
|
||||
_rect(panel['avatar'])
|
||||
fields = {'name', 'experience', 'level', 'point', 'vip_point'}
|
||||
if {entry['field'] for entry in panel['fields']} != fields:
|
||||
raise ValueError('Invalid panel fields')
|
||||
for entry in panel['fields']:
|
||||
_rect(entry['rect'])
|
||||
_numbers(entry['color'], 3)
|
||||
if (any(not 0 <= v <= 255 for v in entry['color'])
|
||||
or entry['alignment'] not in (3, 4)):
|
||||
raise ValueError('Invalid panel text')
|
||||
for entry in panel['sprites']:
|
||||
_rect(entry['rect'])
|
||||
if entry['sprite'] not in images:
|
||||
raise ValueError('Missing panel sprite')
|
||||
if set(value['decorations']) != {
|
||||
'default', 'spring', 'summer', 'autumn', 'winter', 'bee'}:
|
||||
raise ValueError('Invalid decoration themes')
|
||||
for entries in value['decorations'].values():
|
||||
for entry in entries:
|
||||
_numbers(entry['position'], 2)
|
||||
_numbers(entry['size'], 2)
|
||||
if (min(entry['size']) <= 0
|
||||
or entry['layer'] not in ('background', 'foreground')
|
||||
or entry['sprite'] not in images):
|
||||
raise ValueError('Invalid decoration')
|
||||
for name in ('webgl/seed.png', 'webgl/wither.png',
|
||||
'background/ripe.png', 'soil/草土地.png',
|
||||
'soil/普通土地.png'):
|
||||
if name not in images:
|
||||
raise ValueError('Missing required farm sprite')
|
||||
return value
|
||||
except (KeyError, TypeError, IndexError, AttributeError) as error:
|
||||
raise ValueError('Invalid render metadata') from error
|
||||
|
||||
|
||||
def validate_archive(content, images=None):
|
||||
with ZipFile(BytesIO(content)) as archive:
|
||||
entries = archive.infolist()
|
||||
names = [entry.filename for entry in entries]
|
||||
expected = (set(images) if images is not None else {
|
||||
'soil/红土地.png', 'soil/黑土地.png', 'soil/金土地.png',
|
||||
})
|
||||
if (len(names) != len(set(names)) or set(names) != expected
|
||||
or sum(e.file_size for e in entries) > 256 * 1024 * 1024):
|
||||
raise ValueError('Unexpected release ZIP contents')
|
||||
for entry in entries:
|
||||
safe_member(entry.filename)
|
||||
if (entry.is_dir() or entry.flag_bits & 1
|
||||
or entry.file_size > 32 * 1024 * 1024
|
||||
or (entry.external_attr >> 16) & 0o170000 == 0o120000):
|
||||
raise ValueError('Invalid release ZIP member')
|
||||
data = archive.read(entry)
|
||||
expected_image = images[entry.filename] if images else None
|
||||
if (expected_image is not None
|
||||
and digest(data) != expected_image['sha256']):
|
||||
raise ValueError('Release image checksum differs')
|
||||
with Image.open(BytesIO(data)) as image:
|
||||
_size(list(image.size))
|
||||
if (expected_image is not None
|
||||
and list(image.size) != expected_image['size']):
|
||||
raise ValueError('Release image dimensions differ')
|
||||
image.verify()
|
||||
@@ -26,13 +26,14 @@ class FarmRenderer:
|
||||
def __init__(
|
||||
self, font_path, asset_cache_bytes=32 * 1024 * 1024,
|
||||
result_cache_bytes=8 * 1024 * 1024,
|
||||
bundle_path=None,
|
||||
bundle_path=None, metadata=None, soils_path=None,
|
||||
):
|
||||
self.font_path = Path(font_path).resolve()
|
||||
if not self.font_path.is_file():
|
||||
raise ValueError("Configure an existing Chinese font path")
|
||||
font(self.font_path, 16)
|
||||
self.assets = Assets(asset_cache_bytes, bundle_path=bundle_path)
|
||||
self.assets = Assets(asset_cache_bytes, bundle_path=bundle_path,
|
||||
metadata=metadata, soils_path=soils_path)
|
||||
self.results = ByteLRU(result_cache_bytes)
|
||||
|
||||
def scene(self, identity, snapshot, now):
|
||||
|
||||
@@ -46,12 +46,13 @@ class BundleTests(unittest.TestCase):
|
||||
bundle.ensure_assets(self.root / 'gitea', 'Gitea')
|
||||
self.assertEqual(fetch.call_args.args[0],
|
||||
'https://gitea.diuse.work/Diuse/farm_service/'
|
||||
'raw/branch/main/assets/assets.zip')
|
||||
'raw/branch/main/catalogue/releases/'
|
||||
+ self.expected['catalogue_release'] + '/assets.zip')
|
||||
bundle.ensure_assets(self.root / 'service', 'farm_service',
|
||||
'https://farm.example/prefix/')
|
||||
self.assertEqual(fetch.call_args.args[0],
|
||||
'https://farm.example/prefix/v1/assets/'
|
||||
+ self.expected['sha256'] + '/assets.zip')
|
||||
'https://farm.example/prefix/v1/catalogue/releases/'
|
||||
+ self.expected['catalogue_release'] + '/assets.zip')
|
||||
|
||||
def test_failed_download_leaves_no_partial_and_wrong_hash_is_rejected(self):
|
||||
def corrupt(url, destination, limit):
|
||||
|
||||
@@ -146,7 +146,12 @@ class AstrBotPackageTests(unittest.IsolatedAsyncioTestCase):
|
||||
Path(choose_font()).read_bytes())
|
||||
(fonts / 'OFL-Xiaolai.txt').write_bytes((ROOT /
|
||||
'src/farm_render/data/OFL-Xiaolai.txt').read_bytes())
|
||||
await plugin.initialize()
|
||||
with patch(
|
||||
package.__name__ +
|
||||
'._vendor.farm_render.catalogue_bundle._fetch',
|
||||
side_effect=OSError('offline fixture'),
|
||||
):
|
||||
await plugin.initialize()
|
||||
self.assertEqual(set(commands), set(main.COMMANDS))
|
||||
|
||||
class Event:
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
from io import BytesIO
|
||||
from zipfile import ZipFile
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from farm_core.catalogue_release import make_manifest, parse_manifest
|
||||
from farm_render.catalogue_bundle import load_catalogue_release
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def release(price=21, extra=False):
|
||||
plants = json.loads((ROOT / 'src/farm_core/data/plants.json').read_bytes())
|
||||
plants.pop('source_sha256', None)
|
||||
plants['plants'][0]['price'] = price
|
||||
metadata = json.loads(
|
||||
(ROOT / 'src/farm_render/data/metadata.json').read_bytes()
|
||||
)
|
||||
if extra:
|
||||
row = copy.deepcopy(plants['plants'][0])
|
||||
row.update(id=9001, name='更新测试作物')
|
||||
plants['plants'].append(row)
|
||||
metadata['plants'][row['name']] = copy.deepcopy(
|
||||
metadata['plants'][plants['plants'][0]['name']]
|
||||
)
|
||||
files = {
|
||||
'plants.json': json.dumps(plants, ensure_ascii=False).encode(),
|
||||
'metadata.json': json.dumps(metadata, ensure_ascii=False).encode(),
|
||||
**{name: (ROOT / 'src/farm_render/data' / name).read_bytes()
|
||||
for name in ('assets.zip', 'land-soils.zip')},
|
||||
}
|
||||
return make_manifest(files, 'test-v1'), files
|
||||
|
||||
|
||||
class CatalogueReleaseTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.root = Path(self.temp.name)
|
||||
self.manifest, self.files = release(price=99)
|
||||
|
||||
def tearDown(self):
|
||||
self.temp.cleanup()
|
||||
|
||||
def fetch(self, url, limit):
|
||||
if url.endswith('/manifest.json'):
|
||||
data = json.dumps(self.manifest).encode()
|
||||
else:
|
||||
self.assertIn('/releases/' + self.manifest['release'] + '/', url)
|
||||
data = self.files[url.rsplit('/', 1)[-1]]
|
||||
self.assertLessEqual(len(data), limit)
|
||||
return data
|
||||
|
||||
def load(self, **kwargs):
|
||||
return load_catalogue_release(self.root, **kwargs)
|
||||
|
||||
def test_new_catalogue_and_metadata_activate_as_one_version(self):
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=self.fetch):
|
||||
result = self.load()
|
||||
self.assertEqual(result.catalogue.get_by_id(1).price, 99)
|
||||
self.assertEqual(result.origin, 'remote')
|
||||
self.assertTrue(result.bundle_path.is_file())
|
||||
self.assertTrue(result.soils_path.is_file())
|
||||
current = json.loads((self.root / 'catalogue/current.json').read_bytes())
|
||||
self.assertEqual(current['release'], self.manifest['release'])
|
||||
|
||||
def test_404_reuses_verified_current(self):
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=self.fetch):
|
||||
self.load()
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=OSError('404')):
|
||||
result = self.load()
|
||||
self.assertEqual(result.catalogue.get_by_id(1).price, 99)
|
||||
self.assertEqual(result.origin, 'cache')
|
||||
|
||||
def test_wrong_bytes_do_not_replace_current(self):
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=self.fetch):
|
||||
self.load()
|
||||
before = (self.root / 'catalogue/current.json').read_bytes()
|
||||
self.manifest, self.files = release(price=100)
|
||||
self.files['plants.json'] = b'broken'
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=self.fetch):
|
||||
result = self.load()
|
||||
self.assertEqual(result.catalogue.get_by_id(1).price, 99)
|
||||
self.assertEqual((self.root / 'catalogue/current.json').read_bytes(),
|
||||
before)
|
||||
|
||||
def test_first_offline_start_uses_bootstrap_without_images(self):
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=OSError('offline')):
|
||||
result = self.load()
|
||||
self.assertEqual(result.origin, 'bootstrap')
|
||||
self.assertEqual(len(result.catalogue), 202)
|
||||
self.assertFalse((self.root / 'catalogue/current.json').exists())
|
||||
|
||||
def test_cache_loss_with_unknown_player_id_refuses_start(self):
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=OSError('offline')):
|
||||
with self.assertRaisesRegex(ValueError, 'catalogue.*ID'):
|
||||
self.load(required_ids={9001})
|
||||
|
||||
def test_incompatible_downgrade_keeps_added_crop(self):
|
||||
self.manifest, self.files = release(extra=True)
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=self.fetch):
|
||||
self.load()
|
||||
self.manifest, self.files = release()
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=self.fetch):
|
||||
result = self.load()
|
||||
self.assertEqual(result.catalogue.get_by_id(9001).name, '更新测试作物')
|
||||
self.assertEqual(result.origin, 'cache')
|
||||
|
||||
def test_manifest_rejects_bool_unknown_schema_paths_and_fingerprint(self):
|
||||
for key, value in [('schema', True), ('schema', 2),
|
||||
('release', '0' * 64)]:
|
||||
bad = copy.deepcopy(self.manifest)
|
||||
bad[key] = value
|
||||
with self.assertRaises(ValueError):
|
||||
parse_manifest(json.dumps(bad).encode())
|
||||
bad = copy.deepcopy(self.manifest)
|
||||
bad['files']['../file'] = bad['files'].pop('assets.zip')
|
||||
with self.assertRaises(ValueError):
|
||||
parse_manifest(json.dumps(bad).encode())
|
||||
|
||||
def test_selected_service_prefix_is_respected(self):
|
||||
seen = []
|
||||
def fetch(url, limit):
|
||||
seen.append(url)
|
||||
return self.fetch(url, limit)
|
||||
with patch('farm_render.catalogue_bundle._fetch', side_effect=fetch):
|
||||
self.load(source='farm_service',
|
||||
service_url='https://farm.test/prefix')
|
||||
self.assertTrue(all(
|
||||
url.startswith('https://farm.test/prefix/v1/catalogue/')
|
||||
for url in seen
|
||||
))
|
||||
|
||||
def test_current_matching_remote_still_checks_player_ids(self):
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=self.fetch):
|
||||
self.load()
|
||||
with self.assertRaisesRegex(ValueError, 'catalogue.*ID'):
|
||||
self.load(required_ids={9001})
|
||||
|
||||
def test_consistent_but_invalid_metadata_keeps_current(self):
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=self.fetch):
|
||||
self.load()
|
||||
self.files['metadata.json'] = b'{"schema":1,"canvas":[0,0]}'
|
||||
self.manifest = make_manifest(self.files, 'bad-layout')
|
||||
result = self.load()
|
||||
self.assertEqual(result.origin, 'cache')
|
||||
self.assertEqual(result.metadata['canvas'], [3072, 1872])
|
||||
|
||||
def test_huge_coordinate_keeps_verified_current(self):
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=self.fetch):
|
||||
self.load()
|
||||
metadata = json.loads(self.files['metadata.json'])
|
||||
metadata['positions'][0]['x'] = 10 ** 500
|
||||
self.files['metadata.json'] = json.dumps(metadata).encode()
|
||||
self.manifest = make_manifest(self.files, 'huge-coordinate')
|
||||
result = self.load()
|
||||
self.assertEqual(result.origin, 'cache')
|
||||
self.assertLess(result.metadata['positions'][0]['x'], 10000000)
|
||||
|
||||
def test_removing_old_crop_image_is_rejected_even_with_valid_hashes(self):
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=self.fetch):
|
||||
self.load()
|
||||
metadata = json.loads(self.files['metadata.json'])
|
||||
name = next(key for key in metadata['images']
|
||||
if key.startswith('plant/'))
|
||||
metadata['images'].pop(name)
|
||||
output = BytesIO()
|
||||
with ZipFile(BytesIO(self.files['assets.zip'])) as old, \
|
||||
ZipFile(output, 'w') as new:
|
||||
for item in old.infolist():
|
||||
if item.filename != name:
|
||||
new.writestr(item, old.read(item))
|
||||
self.files['assets.zip'] = output.getvalue()
|
||||
self.files['metadata.json'] = json.dumps(metadata).encode()
|
||||
self.manifest = make_manifest(self.files, 'missing-image')
|
||||
result = self.load()
|
||||
self.assertEqual(result.origin, 'cache')
|
||||
self.assertIn(name, result.metadata['images'])
|
||||
@@ -0,0 +1,34 @@
|
||||
import json
|
||||
from contextlib import closing
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from farm_core import SQLiteStore
|
||||
|
||||
|
||||
class CatalogueStoreTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_missing_store_is_not_created_by_catalogue_check(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
path = Path(temporary) / 'not-created.sqlite3'
|
||||
store = SQLiteStore(path)
|
||||
self.assertEqual(await store.catalogue_ids(), set())
|
||||
self.assertFalse(path.exists())
|
||||
|
||||
async def test_existing_ids_read_without_changing_database(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
path = Path(temporary) / 'farm.sqlite3'
|
||||
store = SQLiteStore(path)
|
||||
await store.initialize()
|
||||
await store.close()
|
||||
with closing(sqlite3.connect(path)) as db, db:
|
||||
db.execute('INSERT INTO accounts(identity,name) VALUES (?,?)',
|
||||
('fixture', 'synthetic'))
|
||||
db.execute('INSERT INTO inventory(identity,plant_id,name,seeds)'
|
||||
' VALUES (?,?,?,?)', ('fixture', 9001, 'test', 1))
|
||||
db.execute('INSERT INTO plots(identity,slot,plant) VALUES (?,?,?)',
|
||||
('fixture', 1, json.dumps({'plant_id': 9002})))
|
||||
before = path.read_bytes()
|
||||
self.assertEqual(await store.catalogue_ids(), {9001, 9002})
|
||||
self.assertEqual(path.read_bytes(), before)
|
||||
@@ -11,6 +11,8 @@ from export_render_assets import export
|
||||
class RenderExportTests(unittest.TestCase):
|
||||
def test_export_preserves_published_land_numbering_and_calibration(self):
|
||||
_, metadata = export(ROOT)
|
||||
stored = (ROOT / "src/farm_render/data/metadata.json").read_bytes()
|
||||
self.assertEqual(json.loads(metadata), json.loads(stored))
|
||||
self.assertEqual(metadata, stored)
|
||||
data = json.loads(metadata)
|
||||
self.assertEqual(len(data['plants']), 202)
|
||||
self.assertEqual(len(data['positions']), 30)
|
||||
self.assertEqual(data['placement']['mode'], 'visible_contact_center')
|
||||
self.assertEqual(metadata, export(ROOT)[1])
|
||||
|
||||
@@ -27,6 +27,12 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
module.__path__ = [str(self.root / "built/repository")]
|
||||
sys.modules[self.package] = module
|
||||
self.module = importlib.import_module(self.package + ".runtime")
|
||||
offline = patch(
|
||||
self.package + '._vendor.farm_render.catalogue_bundle._fetch',
|
||||
side_effect=OSError('offline fixture'),
|
||||
)
|
||||
offline.start()
|
||||
self.addCleanup(offline.stop)
|
||||
self.runtime = self.module.Runtime(self.root / "data", choose_font())
|
||||
self.seed_assets(self.runtime)
|
||||
await self.runtime.start()
|
||||
@@ -78,6 +84,133 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
for action in ('seeds', 'crops'):
|
||||
self.assertIsInstance(await self.call(action, '1'), bytes)
|
||||
|
||||
async def test_catalogue_is_loaded_once_before_store_initialization(self):
|
||||
import json
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
await self.call('register')
|
||||
await self.call('buy', '胡萝卜 1', message='seed')
|
||||
await self.call('sow', '胡萝卜 1', message='sow')
|
||||
before = await self.runtime.service.snapshot(self.runtime_identity())
|
||||
await self.runtime.close()
|
||||
bundle = importlib.import_module(
|
||||
self.package + '._vendor.farm_render.catalogue_bundle',
|
||||
)
|
||||
original = bundle._bootstrap()
|
||||
document = json.loads((ROOT / 'src/farm_core/data/plants.json')
|
||||
.read_bytes())
|
||||
document['plants'][0]['price'] = 99
|
||||
document['plants'][0]['harvest'] = 999
|
||||
updated = replace(original, catalogue=self.module.Catalogue.loads(
|
||||
json.dumps(document),
|
||||
))
|
||||
self.runtime = self.module.Runtime(self.root / 'data', choose_font())
|
||||
self.seed_assets(self.runtime)
|
||||
with patch.object(self.module, 'load_catalogue_release',
|
||||
return_value=updated) as load:
|
||||
await asyncio.gather(self.runtime.start(), self.runtime.start())
|
||||
await self.runtime.start()
|
||||
self.assertEqual(load.call_count, 1)
|
||||
self.assertEqual(self.runtime.service.catalogue.get_by_id(1).price, 99)
|
||||
after = await self.runtime.service.snapshot(self.runtime_identity())
|
||||
self.assertEqual(after.plots[0].plant, before.plots[0].plant)
|
||||
self.assertNotEqual(after.plots[0].plant.harvest, 999)
|
||||
|
||||
async def test_close_during_catalogue_load_does_not_start_service(self):
|
||||
import threading
|
||||
|
||||
await self.runtime.close()
|
||||
bundle = importlib.import_module(
|
||||
self.package + '._vendor.farm_render.catalogue_bundle',
|
||||
)
|
||||
snapshot = bundle._bootstrap()
|
||||
entered, release = threading.Event(), threading.Event()
|
||||
def slow(*args):
|
||||
entered.set()
|
||||
if not release.wait(5):
|
||||
raise TimeoutError('fixture did not release')
|
||||
return snapshot
|
||||
self.runtime = self.module.Runtime(self.root / 'other', choose_font())
|
||||
with patch.object(self.module, 'load_catalogue_release',
|
||||
side_effect=slow):
|
||||
starting = asyncio.create_task(self.runtime.start())
|
||||
await asyncio.wait_for(asyncio.to_thread(entered.wait), 2)
|
||||
closing = asyncio.create_task(self.runtime.close())
|
||||
await asyncio.sleep(0)
|
||||
release.set()
|
||||
await asyncio.gather(starting, closing)
|
||||
self.assertFalse(self.runtime.started)
|
||||
self.assertFalse((self.root / 'other/farm.sqlite3').exists())
|
||||
|
||||
async def test_close_during_id_read_prevents_later_start(self):
|
||||
await self.runtime.close()
|
||||
self.runtime = self.module.Runtime(self.root / 'other', choose_font())
|
||||
self.seed_assets(self.runtime)
|
||||
entered, release = asyncio.Event(), asyncio.Event()
|
||||
async def slow_ids():
|
||||
entered.set()
|
||||
await release.wait()
|
||||
return set()
|
||||
with patch.object(self.runtime.store, 'catalogue_ids',
|
||||
side_effect=slow_ids):
|
||||
starting = asyncio.create_task(self.runtime.start())
|
||||
await entered.wait()
|
||||
await self.runtime.close()
|
||||
release.set()
|
||||
await starting
|
||||
self.assertFalse(self.runtime.started)
|
||||
self.assertFalse((self.root / 'other/farm.sqlite3').exists())
|
||||
|
||||
async def test_cancelled_start_drains_catalogue_download(self):
|
||||
import threading
|
||||
|
||||
await self.runtime.close()
|
||||
bundle = importlib.import_module(
|
||||
self.package + '._vendor.farm_render.catalogue_bundle',
|
||||
)
|
||||
snapshot = bundle._bootstrap()
|
||||
entered, release, finished = (threading.Event() for _ in range(3))
|
||||
def slow(*args):
|
||||
entered.set()
|
||||
if not release.wait(5):
|
||||
raise TimeoutError('fixture did not release')
|
||||
finished.set()
|
||||
return snapshot
|
||||
self.runtime = self.module.Runtime(self.root / 'other', choose_font())
|
||||
with patch.object(self.module, 'load_catalogue_release',
|
||||
side_effect=slow):
|
||||
starting = asyncio.create_task(self.runtime.start())
|
||||
await asyncio.wait_for(asyncio.to_thread(entered.wait), 2)
|
||||
starting.cancel()
|
||||
await asyncio.sleep(0.01)
|
||||
self.assertFalse(starting.done())
|
||||
release.set()
|
||||
with self.assertRaises(asyncio.CancelledError):
|
||||
await starting
|
||||
self.assertTrue(finished.is_set())
|
||||
self.assertIsNone(self.runtime.log_handler)
|
||||
self.assertFalse(self.runtime.started)
|
||||
|
||||
async def test_close_during_initialization_prevents_later_start(self):
|
||||
await self.runtime.close()
|
||||
self.runtime = self.module.Runtime(self.root / 'other', choose_font())
|
||||
self.seed_assets(self.runtime)
|
||||
entered, release = asyncio.Event(), asyncio.Event()
|
||||
initialize = self.runtime.store.initialize
|
||||
async def slow_initialize(**kwargs):
|
||||
await initialize(**kwargs)
|
||||
entered.set()
|
||||
await release.wait()
|
||||
with patch.object(self.runtime.store, 'initialize',
|
||||
side_effect=slow_initialize):
|
||||
starting = asyncio.create_task(self.runtime.start())
|
||||
await entered.wait()
|
||||
await self.runtime.close()
|
||||
release.set()
|
||||
await starting
|
||||
self.assertFalse(self.runtime.started)
|
||||
self.assertIsNone(self.runtime.rendering)
|
||||
|
||||
async def test_land_upgrade_command_conditions_charge_and_replay(self):
|
||||
self.assertEqual(self.module.COMMANDS['土地升级'], 'upgrade')
|
||||
await self.call('register')
|
||||
@@ -577,6 +710,8 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
for resource in ('assets', 'font'):
|
||||
other = self.module.Runtime(self.root / ('stop-' + resource))
|
||||
self.seed_assets(other)
|
||||
if resource == 'assets':
|
||||
next((other.data_dir / 'assets').glob('*/assets.zip')).unlink()
|
||||
try:
|
||||
with patch.object(self.module, 'ensure_' + resource,
|
||||
side_effect=TimeoutError()) as fetch:
|
||||
@@ -598,6 +733,7 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
await asyncio.sleep(0)
|
||||
self.assertEqual(fetch.call_count, 1)
|
||||
await other.close()
|
||||
self.seed_assets(other)
|
||||
await other.start()
|
||||
self.assertIsNotNone(other.rendering)
|
||||
finally:
|
||||
@@ -614,6 +750,8 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
):
|
||||
other = self.module.Runtime(self.root / ('failed-' + resource))
|
||||
self.seed_assets(other)
|
||||
if resource == 'assets':
|
||||
next((other.data_dir / 'assets').glob('*/assets.zip')).unlink()
|
||||
try:
|
||||
with patch.object(self.module, 'ensure_' + resource,
|
||||
side_effect=failure), \
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2.0.2
|
||||
2.0.3
|
||||
Reference in New Issue
Block a user