mirror of
https://github.com/Shu-Ying/main_plugin_farm.git
synced 2026-10-28 13:10:02 +08:00
add # 新增对zhenxun的工作流
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
*.toml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.txt text eol=lf
|
||||
LICENSE text eol=lf
|
||||
*.html text eol=lf
|
||||
*.sh text eol=lf
|
||||
*.ps1 text eol=lf
|
||||
@@ -14,6 +16,8 @@
|
||||
*.db binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.zip binary
|
||||
*.webp binary
|
||||
|
||||
# Preserve exact legacy bytes so manifest hashes survive checkout
|
||||
legacy/** -text
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
name: Zhenxun preview
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Preview version
|
||||
default: 0.1.0-preview.1
|
||||
required: true
|
||||
type: string
|
||||
publish:
|
||||
description: Sync fixed farm-preview branch in zhenxun repository
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: farm-preview-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
pillow: ['10.4.0', '12.3.0']
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
PYTHONIOENCODING: utf-8
|
||||
PILLOW_VERSION: ${{ matrix.pillow }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Install test dependencies
|
||||
shell: bash
|
||||
run: >-
|
||||
python -m pip install "Pillow==$PILLOW_VERSION"
|
||||
setuptools wheel "PyYAML>=6,<7"
|
||||
- run: python scripts/ci_font.py
|
||||
- run: python scripts/check_workflows.py
|
||||
- run: python scripts/verify_foundation.py
|
||||
|
||||
build:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PREVIEW_VERSION: ${{ inputs.version || '0.1.0-preview.1' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Build selected target only
|
||||
run: >-
|
||||
python scripts/build_zhenxun.py
|
||||
--output dist/zhenxun-preview --version "$PREVIEW_VERSION"
|
||||
- name: Copy standalone helper scripts
|
||||
run: |
|
||||
cp scripts/install_zhenxun.py dist/zhenxun-preview/
|
||||
cp scripts/verify_zhenxun.py dist/zhenxun-preview/
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: zhenxun-preview
|
||||
path: dist/zhenxun-preview/
|
||||
include-hidden-files: true
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
sync-preview:
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' && inputs.publish &&
|
||||
github.ref == 'refs/heads/main' &&
|
||||
github.repository == 'Shu-Ying/main_plugin_farm'
|
||||
needs: [test, build]
|
||||
runs-on: ubuntu-latest
|
||||
environment: zhenxun-preview
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: zhenxun-preview
|
||||
path: incoming
|
||||
- name: Publish validated preview only
|
||||
env:
|
||||
FARM_RELEASE_TOKEN: ${{ secrets.FARM_RELEASE_TOKEN }}
|
||||
run: >-
|
||||
python scripts/publish_zhenxun.py
|
||||
incoming/zhenxun_plugin_farm_preview-*.zip --publish
|
||||
@@ -15,8 +15,9 @@
|
||||
- 可选共享图片层:旧布局与素材、目标尺寸绘制、WebP/JPEG/PNG、
|
||||
有界缓存和受控后台执行,参见 [绘图说明](docs/10-rendering.md)。
|
||||
|
||||
本目录目前不是可直接放入真寻、NoneBot 或 AstrBot 插件目录的成品。
|
||||
integrations 中仍是职责说明,框架接入与发布流水线尚未实现。
|
||||
主仓库不能整体放入插件目录。真寻预览包由脚本白名单生成,参见
|
||||
[真寻测试说明](docs/11-zhenxun-preview.md);AstrBot/通用NoneBot接入
|
||||
仍未实现。工作流和同步脚本已准备,真实机器人和远端运行尚未验证。
|
||||
签到、偷菜、升级、兑换和真实玩家数据迁移不在本轮范围。
|
||||
|
||||
## 安装与本地演示
|
||||
|
||||
@@ -46,7 +46,9 @@ NoneBot wheel 明确包含资源;不能假设 zip 的相对目录在 wheel 内
|
||||
8. 提交安装内容和来源清单后,再创建指向同一提交的发布标签。
|
||||
9. 按目标发布到仓库/Release/PyPI,并记录各目标结果。
|
||||
|
||||
未来可使用 GitHub Actions;本轮未编写可执行发布工作流、未配置凭据。
|
||||
初始设计时没有工作流;后续真寻预览开发已补充主仓库构建附件、
|
||||
固定预览分支的可选手动同步和发布仓库校验工作流,见11-zhenxun-preview。
|
||||
尚未配置凭据或执行真实GitHub工作流。其他目标仍是设计。
|
||||
发布工作区使用隔离的临时目录;清理只允许精确列出的生成路径。
|
||||
保留目标仓库中的治理资料和人工维护文件,禁止无条件清空仓库。
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# 真寻农场预览版
|
||||
|
||||
本文件随生成包分发;业务源码只在主仓库main_plugin_farm维护。
|
||||
生成目标为Shu-Ying/zhenxun_plugin_farm的farm-preview分支,保留main。
|
||||
|
||||
## 安装前
|
||||
|
||||
此版沿用“开通农场”“我的农场”等原指令,没有测试前缀。请先停止
|
||||
机器人,并停用旧农场插件,避免同名指令被旧版抢先处理。仅在菜单
|
||||
禁用旧版不一定阻止其导入;建议把旧插件目录备份到插件扫描目录之外。
|
||||
安装脚本不会自动禁用、删除或覆盖旧版。
|
||||
|
||||
预览数据默认位于真寻DATA_PATH/farm_preview/farm-preview.sqlite3。
|
||||
不读取旧版数据库,不迁移真实玩家资产;同平台、同机器人内共享农场,
|
||||
不同机器人/平台分开。不要将预览数据路径指向旧版数据库。
|
||||
|
||||
## 主仓库本地生成
|
||||
|
||||
```text
|
||||
python scripts/build_zhenxun.py --output dist/zhenxun-preview
|
||||
python scripts/verify_zhenxun.py dist/zhenxun-preview/zhenxun_plugin_farm_preview-0.1.0-preview.1.zip
|
||||
python scripts/install_zhenxun.py dist/zhenxun-preview/zhenxun_plugin_farm_preview-0.1.0-preview.1.zip --plugins-dir /path/to/zhenxun_bot/zhenxun/plugins
|
||||
```
|
||||
|
||||
必须使用真寻的Python环境安装包内requirements.txt;该依赖约束为
|
||||
Pillow>=10.4,<11,不静默升级宿主到12。NoneBot、真寻和Uninfo由
|
||||
现有宿主提供,不把通用NoneBot适配器或AstrBot代码一起分发。
|
||||
|
||||
如果安装目录已存在,脚本会拒绝覆盖。更新前停机器人、备份插件目录,
|
||||
手动移走旧预览目录后再安装;数据目录位于插件外,不随插件删除。
|
||||
|
||||
## 配置与指令
|
||||
|
||||
在真寻使用的.env配置中文字体路径,例如Windows:
|
||||
|
||||
```dotenv
|
||||
FARM_PREVIEW_FONT=C:/Windows/Fonts/msyh.ttc
|
||||
FARM_PREVIEW_DEPLOYMENT=zhenxun-preview
|
||||
FARM_PREVIEW_IMAGE_FORMAT=AUTO
|
||||
```
|
||||
|
||||
Linux请提供合法可用的中文字体。没有字体时文本交易可用,图片命令
|
||||
会提示配置;不捆绑Windows字体。可选FARM_PREVIEW_DATA_DIR显式设置
|
||||
独立预览数据目录。AUTO默认JPEG以保守兼容真实平台,确认WebP支持后
|
||||
可设WEBP,也支持JPEG/PNG。
|
||||
|
||||
测试顺序(是否需要/前缀遵循宿主COMMAND_START配置):
|
||||
|
||||
```text
|
||||
农场帮助
|
||||
开通农场
|
||||
我的农场
|
||||
种子商店
|
||||
购买种子 白萝卜 1
|
||||
我的种子
|
||||
播种 白萝卜 1
|
||||
我的农场 高清
|
||||
农场详述
|
||||
收获
|
||||
```
|
||||
|
||||
作物未成熟时收获会明确失败,不提供管理员快进命令或加币后门。
|
||||
其他入口:铲除、我的作物、我的农场币、我的点券、作物加锁 作物、
|
||||
作物解锁 作物、出售作物 作物 数量;不带参数出售全部未锁定产物。
|
||||
也可发送“我的农场 buy-seed 白萝卜 1”等旧子命令(支持范围见帮助)。
|
||||
不实现签到、偷菜、充值、兑换、升级和旧数据迁移,不能替代旧版全部功能。
|
||||
|
||||
重复事件通过稳定消息标识幂等;无法获取消息标识时写操作拒绝。
|
||||
头像最多等待2秒,失败使用默认图;图片缓存与后台任务均有限额。
|
||||
关键日志写入数据目录farm-preview.log,同时沿用宿主日志传播配置;
|
||||
不记录真实玩家数据。
|
||||
|
||||
## GitHub工作流
|
||||
|
||||
先由你提交主仓库代码后,主仓库的Zhenxun preview工作流会检查并上传
|
||||
Actions artifact。下载并解压附件,取其中插件ZIP安装;不需要令牌。
|
||||
Windows/Linux、Pillow10.4/当前支持版属于CI矩阵,不代表本机已跑Linux。
|
||||
|
||||
若还需将生成内容写入发布仓库,主仓库手动Run workflow时勾选publish,
|
||||
并配置受保护environment zhenxun-preview。该环境Secret名为
|
||||
FARM_RELEASE_TOKEN,仅授权目标仓库Contents写入;生成内容含工作流,
|
||||
相应令牌也须允许Workflows写入(GitHub App使用对应权限)。配置
|
||||
环境审批及仅允许main分支部署。不把Token填进脚本、URL或输入参数。
|
||||
脚本默认dry-run,只有--publish才访问GitHub;固定写farm-preview,
|
||||
不更新main、不force、不创建Release。第一次创建预览分支,之后普通
|
||||
提交更新。专用预览分支只放生成内容,不在那里手改业务或存储治理资料。
|
||||
已有预览分支必须带本项目来源清单;若它已成为默认分支则拒绝发布,
|
||||
并发期间分支发生变化也拒绝更新,避免覆盖其他提交。
|
||||
用ZIP安装最直接,不能假设插件管理器支持任意分支地址。
|
||||
|
||||
产物内FARM-MANIFEST.json记录版本、源提交、dirty和每文件SHA256;
|
||||
源有未提交修改会标dirty,禁止发布这种产物。本地测试允许dirty。
|
||||
哈希用于内容一致性,不是签名或来源身份保证,请使用你信任的附件。
|
||||
|
||||
## 验证边界与回退
|
||||
|
||||
本轮不会在真实机器人中安装/启动,也不会执行真实GitHub同步。真实
|
||||
登录、权限、适配器消息格式和发图需要你部署后验证,不能把离线模拟
|
||||
报告为线上成功。旧素材455个阶段引用缺图时显示问号,不会整图失败。
|
||||
|
||||
源码中ZIP时间、系统类型和文件权限固定;文本统一LF,以便跨平台
|
||||
复现。构建后repository目录是发布仓库根,外层插件ZIP用于本地安装;
|
||||
二者不要再叠套一层主仓库目录。
|
||||
|
||||
回退:停机器人,移走预览插件,恢复旧插件目录,再由你启动;旧版库
|
||||
没有被本版更改。保留预览数据备份,不尝试让旧版打开新SQLite schema。
|
||||
|
||||
官方依据:[Actions附件](https://docs.github.com/en/actions/tutorials/store-and-share-data)、
|
||||
[手动触发](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow)。
|
||||
@@ -7,6 +7,8 @@
|
||||
|---|---|
|
||||
| [09-foundation.md](09-foundation.md) | 已实现的第一批核心、API、测试与限制 |
|
||||
| [10-rendering.md](10-rendering.md) | 共享图片工具、农场绘制、缓存、编码与接入 |
|
||||
| [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) | 长期目标、实施顺序、里程碑与验收 |
|
||||
| [02-architecture.md](02-architecture.md) | 分层、目录、依赖、接入与接口设计 |
|
||||
|
||||
@@ -35,6 +35,9 @@ AGENTS.md 缺失开始标记已修复。用户已明确选择当前目录继续
|
||||
由接入方提供,失败使用默认头像。保留旧农场背景、地块和作物素材。
|
||||
- 按可见状态缓存成图并在生长阶段边界失效;并发相同请求合并。
|
||||
提供离线生成、性能记录、安装包独立运行和中文字体配置说明。
|
||||
- 用户选择补齐真寻测试版:最小命令接入、离线白名单打包、GitHub
|
||||
构建工作流、可选手动同步真寻发布仓库测试分支、安装/卸载说明。
|
||||
独立测试库、沿用原指令;保留旧版和原业务/渲染验收A1-A13。
|
||||
|
||||
## Directory structure
|
||||
|
||||
@@ -43,6 +46,9 @@ AGENTS.md 缺失开始标记已修复。用户已明确选择当前目录继续
|
||||
- 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
|
||||
|
||||
@@ -50,6 +56,7 @@ AGENTS.md 缺失开始标记已修复。用户已明确选择当前目录继续
|
||||
- scripts/verify_foundation.py:增加渲染资源可复现检查。
|
||||
- README.md、docs/README.md、docs/task_plan.md、docs/progress.md。
|
||||
- 当前 brief 与新增 farm-render 目标规格;原业务规格保留。
|
||||
- release/targets.json 与真寻职责说明;新增真寻预览完整目标规格。
|
||||
|
||||
### Deleted
|
||||
|
||||
@@ -57,15 +64,16 @@ AGENTS.md 缺失开始标记已修复。用户已明确选择当前目录继续
|
||||
|
||||
### Not created
|
||||
|
||||
- 不创建真实平台插件、远端工作流、真实玩家库或字体再分发包。
|
||||
- 不创建AstrBot/通用NoneBot接入、真实玩家库或字体再分发包。
|
||||
- 不修改 legacy 基线,不创建多个框架的业务副本。
|
||||
|
||||
# Non-goals
|
||||
|
||||
本轮不实现真实宿主插件、消息命令、签到、偷菜、活动、
|
||||
本轮不实现AstrBot/通用NoneBot宿主插件、签到、偷菜、活动、
|
||||
外部金币兑换、点券兑换、土地升级、星级进阶、账号绑定或在线联机。
|
||||
不读取或迁移真实玩家数据,不重置旧资产,不改旧基线业务。
|
||||
不 clone/fetch/pull/push 任何远程,不创建 PR、Release 或发布工作流。
|
||||
不在本机 clone/fetch/pull/push 任何远程,不创建 PR 或远端Release。
|
||||
生成工作流与发布脚本,但不执行跨仓库发布或安装/重启真实机器人。
|
||||
不将设计占位目录报告为已支持三框架。
|
||||
|
||||
# Acceptance examples
|
||||
@@ -100,6 +108,20 @@ AGENTS.md 缺失开始标记已修复。用户已明确选择当前目录继续
|
||||
不提前释放运行名额、不泄漏后台任务;记录耗时、尺寸和缓存状态。
|
||||
- A13: 离线生成农场样例、对比尺寸/编码与冷暖缓存,保存测量报告和
|
||||
接入文档;验证旧基线及原有核心测试,明确尚未接入真实消息平台。
|
||||
- A14: 真寻接入沿用原指令提供帮助、注册、我的农场、仓库、购买、
|
||||
播种、收获、铲除、锁定/解锁、出售;稳定事件幂等标识、身份隔离,
|
||||
独立测试数据库与启动/关闭生命周期;缺字体时给出配置提示。
|
||||
- A15: 白名单构建完整真寻插件ZIP,包含私有共享模块、静态素材、
|
||||
依赖、许可证及来源/文件哈希;不含其他适配、legacy、测试或玩家数据,
|
||||
源提交/脏工作区明确记录,相同输入可复现,构建不修改开发源码。
|
||||
- A16: 离线校验ZIP内容、导入和命令路由;安装脚本拒绝路径穿越、
|
||||
哈希错误和已有目标,不覆盖旧插件或玩家数据;不自动启动真实机器人。
|
||||
- A17: 主仓库工作流完成检查/构建/上传测试附件;可选手动发布只面向
|
||||
固定真寻仓库的专用测试分支,默认不发布、不改main、不force,凭据
|
||||
由Secrets提供且不进入产物;发布仓库收到生成内容后可独立校验。
|
||||
- A18: 本机离线验证打包、边界与安装行为;CI覆盖Windows/Linux及
|
||||
Pillow10.4和当前支持版本,记录真实宿主、GitHub运行未验证边界;
|
||||
提供下载、字体/数据路径、测试命令、回退/卸载和Secrets配置说明。
|
||||
|
||||
# Constraints and invariants
|
||||
|
||||
@@ -124,3 +146,8 @@ AGENTS.md 缺失开始标记已修复。用户已明确选择当前目录继续
|
||||
兼容输出;尺寸、质量、编码力度独立配置,平台能力由适配器声明。
|
||||
- 素材保留原 GPL 归属,不捆绑未确认再分发许可的字体;部署显式配置
|
||||
中文字体路径。真实宿主头像缓存和上传由后续适配器实现。
|
||||
- 用户已明确选择“补齐真寻测试版:命令接入+打包工作流+安装脚本”。
|
||||
由于当前目录未归档核心与新接入需联合验证,作为同一基础测试交付
|
||||
扩展;不代用户接受上轮结果。用户明确不加测试前缀,沿用原指令;
|
||||
测试库与旧库隔离,发布脚本只操作专用farm-preview分支,实际发布
|
||||
不执行。测试时由用户停用旧插件,安装器不删除或覆盖旧插件。
|
||||
|
||||
@@ -3,10 +3,10 @@ name: farm-core-foundation
|
||||
language: en
|
||||
phase: verify
|
||||
status: await-user
|
||||
state_version: 14
|
||||
state_version: 26
|
||||
brief: brief.md
|
||||
document_constraints_version: 3
|
||||
shape_confirmation_hash: 49a2a3fb4be0159531d63bcdd52c5895f2586e309c33be91fe4e38c7a159461d
|
||||
shape_confirmation_hash: d1bc1489c0aa3d9a28ff7a24c62aa4bcb4e0a953955d265bcf06f802252cec36
|
||||
spec_changes:
|
||||
- capability: farm-core
|
||||
operation: create
|
||||
@@ -14,6 +14,9 @@ spec_changes:
|
||||
- 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
|
||||
@@ -21,8 +24,8 @@ workspace:
|
||||
finish: null
|
||||
loop:
|
||||
stage: await-user
|
||||
goal_cycle: 3
|
||||
iteration: 1
|
||||
goal_cycle: 5
|
||||
iteration: 2
|
||||
attempt: 1
|
||||
retry_epoch: 0
|
||||
failed_iteration_count: 0
|
||||
@@ -36,21 +39,21 @@ acceptance:
|
||||
text: "A1: 旧基线 827 文件校验继续通过;新核心不导入三框架、无导入副作用, 可从安装包或源码独立运行,核心不通过路径引用 legacy。"
|
||||
result: passed
|
||||
reason:
|
||||
text: 827文件基线正式检查通过;检查包导入无线程/文件副作用及wheel独立安装,核心无框架和legacy运行依赖。
|
||||
text: 827文件基线正式检查通过;核心源码仅相对导入,安装及导入副作用测试通过,不依赖legacy或框架。
|
||||
truncated: false
|
||||
- id: A2
|
||||
source: brief.md
|
||||
text: "A2: 可复现导出 202 条作物配置,名称和必要数值对应源静态数据库; 本地加载支持格式校验,坏数据明确失败,不访问网络。"
|
||||
result: passed
|
||||
reason:
|
||||
text: 只读源库导出按id排序并带源hash;202项导出检查通过,加载器校验版本、整数、阶段和重复定义。
|
||||
text: 202条目录源值及导出复现、损坏目录拒绝测试通过;Catalogue本地读取且验证schema与重复对象。
|
||||
truncated: false
|
||||
- id: A3
|
||||
source: brief.md
|
||||
text: "A3: 注册立即可查询 500 农场币、0 点券、0 经验、3 块空地; 重复注册不重复赠送;不同显式作用域不串号。"
|
||||
result: passed
|
||||
reason:
|
||||
text: 账户注册与初始500资产、三空地在事务内完成;重复注册及身份作用域隔离测试正式通过。
|
||||
text: repository.register在事务内发放500币和3地;重复注册与四段身份隔离正式测试通过。
|
||||
truncated: false
|
||||
- id: A4
|
||||
source: brief.md
|
||||
@@ -58,7 +61,7 @@ acceptance:
|
||||
不留下部分写入。"
|
||||
result: passed
|
||||
reason:
|
||||
text: 购买和播种在完整事务内扣款、入库、耗种、存图;数量、等级、余额和空地边界测试正式通过。
|
||||
text: 购买播种均通过同一事务调用,整数/等级/资产/空地检查明确;失败无部分写入测试通过。
|
||||
truncated: false
|
||||
- id: A5
|
||||
source: brief.md
|
||||
@@ -66,7 +69,7 @@ acceptance:
|
||||
多季次数及枯萎/铲除遵守保存的规则与可注入时钟。"
|
||||
result: passed
|
||||
reason:
|
||||
text: 成熟边界、经验增量、多季再生长及枯萎清除源实现与规格一致,逐地块加成及顺序测试通过。
|
||||
text: 收获按各地块单独计算增量、保存多季状态;成熟边界、加成排序、冻结规则与枯萎测试通过。
|
||||
truncated: false
|
||||
- id: A6
|
||||
source: brief.md
|
||||
@@ -74,7 +77,7 @@ acceptance:
|
||||
不发生旧版模板参数缺失。"
|
||||
result: passed
|
||||
reason:
|
||||
text: 锁定状态对指定及全部出售生效,出售只增本次收入,Receipt和文本使用真实余额;正式测试通过。
|
||||
text: 指定和全部出售均保护锁定状态,只加本次收入;文本及结构化回执返回真实账户,回归通过。
|
||||
truncated: false
|
||||
- id: A7
|
||||
source: brief.md
|
||||
@@ -82,14 +85,14 @@ acceptance:
|
||||
不重复领取,不吞数据库故障伪装成功;重启后数据可读。"
|
||||
result: passed
|
||||
reason:
|
||||
text: BEGIN IMMEDIATE、操作指纹与结果同事务保存;异常回滚、并发超卖/重复收获、取消重放、重启与schema测试通过。
|
||||
text: BEGIN IMMEDIATE和操作指纹/持久结果覆盖原子回滚、幂等冲突、并发及取消重放;重启/模式拒绝测试通过。
|
||||
truncated: false
|
||||
- id: A8
|
||||
source: brief.md
|
||||
text: "A8: 提供可执行的离线测试、运行入口和使用说明,保留真实宿主、 真实库迁移及远程发布未验证的说明;文档记录已实现范围和剩余项。"
|
||||
result: passed
|
||||
reason:
|
||||
text: README、核心说明、离线入口与测试可用,准确说明尚无真实宿主、真实库迁移和远程发布验证。
|
||||
text: 离线入口、完整56项回归及实现说明可复核;真实宿主、迁库及远程发布边界有说明。
|
||||
truncated: false
|
||||
- id: A9
|
||||
source: brief.md
|
||||
@@ -97,43 +100,83 @@ acceptance:
|
||||
依赖;安装包含所需素材。"
|
||||
result: passed
|
||||
reason:
|
||||
text: Pillow可选渲染模块支持透明合成、复制、缩放、裁剪、文字测量省略及三格式;核心无Pillow导入,wheel携带673素材并独立渲染通过。
|
||||
text: Canvas支持透明合成/复制/缩放/裁剪/文字/三格式编码;核心无Pillow运行依赖,wheel独立渲染测试通过。
|
||||
truncated: false
|
||||
- id: A10
|
||||
source: brief.md
|
||||
text: "A10: 从一次快照绘制空地、生长、成熟、再生长、枯萎及锁定地块, 使用统一时间、旧布局和正确偏移;输出普通/高清可解码图片。"
|
||||
result: passed
|
||||
reason:
|
||||
text: 纯场景转换消费一次快照和统一now,首季/再季锚点与枯萎优先正确;旧30块布局及真实offset字段已迁出,普通高清解码与阶段测试通过,样例已视觉检查。
|
||||
text: 统一快照和now生成场景,阶段按首熟/再生长锚点映射,枯萎优先并使用实际偏移;普通高清解码及边界测试通过。
|
||||
truncated: false
|
||||
- id: A11
|
||||
source: brief.md
|
||||
text: "A11: 小画布从开始按目标尺寸绘制;素材和结果缓存有内存上限, 共享素材不被污染;阶段/成熟边界、账户、头像及作用域变化不复用旧图。"
|
||||
result: passed
|
||||
reason:
|
||||
text: 目标尺寸直接合成;素材/底图与成图LRU有字节预算且返回副本;键含作用域、账户、阶段、头像、主题及编码配置,边界变化不复用旧图。
|
||||
text: 底图直接为目标宽度;素材返回副本,LRU预算有界;身份、可见账户/地块阶段、头像和配置纳入键,正式缓存测试通过。
|
||||
truncated: false
|
||||
- id: A12
|
||||
source: brief.md
|
||||
text: "A12: 合成和编码在后台执行且限制并发,相同请求合并;取消和异常 不提前释放运行名额、不泄漏后台任务;记录耗时、尺寸和缓存状态。"
|
||||
result: passed
|
||||
reason:
|
||||
text: 整体合成编码由to_thread执行,semaphore限制并发、pending限制容量,shield维持取消后的任务容量并在关闭排空;合并、异常重试、事件循环和并发测试通过,日志不含用户标识。
|
||||
text: RenderService整图to_thread,有限排队及并发、相同请求合并,shield取消和关闭排空;并发/取消/失败重试测试通过。
|
||||
truncated: false
|
||||
- id: A13
|
||||
source: brief.md
|
||||
text: "A13: 离线生成农场样例、对比尺寸/编码与冷暖缓存,保存测量报告和 接入文档;验证旧基线及原有核心测试,明确尚未接入真实消息平台。"
|
||||
result: passed
|
||||
reason:
|
||||
text: 离线脚本生成两尺寸三格式冷暖测量及日志,报告与已查看样例一致;正式检查45项和基线/两类导出通过,文档明确本机单次、未测上传/低配/Linux及真实平台。
|
||||
text: 离线样例报告标注本机单次及未测上传;三格式冷暖数据、布局及文档保留,旧基线和原核心回归继续通过。
|
||||
truncated: false
|
||||
- id: A14
|
||||
source: brief.md
|
||||
text: "A14: 真寻接入沿用原指令提供帮助、注册、我的农场、仓库、购买、 播种、收获、铲除、锁定/解锁、出售;稳定事件幂等标识、身份隔离,
|
||||
独立测试数据库与启动/关闭生命周期;缺字体时给出配置提示。"
|
||||
result: passed
|
||||
reason:
|
||||
text: 沿用原中文指令及旧子命令,独立预览库、稳定事件哈希、完整身份、生命周期/字体/头像降级已核对;余额不足映射修正并通过无扣款新测试。
|
||||
truncated: false
|
||||
- id: A15
|
||||
source: brief.md
|
||||
text: "A15: 白名单构建完整真寻插件ZIP,包含私有共享模块、静态素材、 依赖、许可证及来源/文件哈希;不含其他适配、legacy、测试或玩家数据,
|
||||
源提交/脏工作区明确记录,相同输入可复现,构建不修改开发源码。"
|
||||
result: passed
|
||||
reason:
|
||||
text: 构建白名单包含私有模块、素材/许可证NOTICE及来源哈希,固定ZIP元数据,不修改开发导入;复现测试通过;最终修复包校验30文件且runtime匹配源码。
|
||||
truncated: false
|
||||
- id: A16
|
||||
source: brief.md
|
||||
text: "A16: 离线校验ZIP内容、导入和命令路由;安装脚本拒绝路径穿越、 哈希错误和已有目标,不覆盖旧插件或玩家数据;不自动启动真实机器人。"
|
||||
result: passed
|
||||
reason:
|
||||
text: 校验器拒绝重复/额外/穿越/符号链接/哈希错误,先验再安装且独占创建不覆盖;正式边界测试通过。Windows临时目录实测尾点空格变体未复现逃逸。
|
||||
truncated: false
|
||||
- id: A17
|
||||
source: brief.md
|
||||
text: "A17: 主仓库工作流完成检查/构建/上传测试附件;可选手动发布只面向 固定真寻仓库的专用测试分支,默认不发布、不改main、不force,凭据
|
||||
由Secrets提供且不进入产物;发布仓库收到生成内容后可独立校验。"
|
||||
result: passed
|
||||
reason:
|
||||
text: CI默认只读/附件输出;同步需手动main触发与受保护环境Secret,脚本默认离线且固定仓库farm-preview、拒绝默认/非生成分支、无force并保护竞态;模拟/守卫通过。
|
||||
truncated: false
|
||||
- id: A18
|
||||
source: brief.md
|
||||
text: "A18: 本机离线验证打包、边界与安装行为;CI覆盖Windows/Linux及
|
||||
Pillow10.4和当前支持版本,记录真实宿主、GitHub运行未验证边界;
|
||||
提供下载、字体/数据路径、测试命令、回退/卸载和Secrets配置说明。"
|
||||
result: passed
|
||||
reason:
|
||||
text: Windows本机56项及隔离Pillow10.4下24项均正式通过;CI矩阵明确Windows/Linux和10.4/12.3,文档含安装/字体/数据/原指令冲突/卸载/Secrets及未验证边界。
|
||||
truncated: false
|
||||
builder_handoff:
|
||||
candidate_id: e88969d9-32c0-4321-a5a3-658023a60414
|
||||
candidate_id: 24c9df3f-4c07-4b34-a21a-8f2ccf9f4adb
|
||||
identity_provider: skill-coordinated
|
||||
builder_execution_ref: skill-coordinated:builder:455faebd-84e1-49d0-8041-c6f44318a0ea
|
||||
iteration: 1
|
||||
builder_execution_ref: skill-coordinated:builder:e3b4b544-7ef0-4d3f-bd5f-fb269d4455c2
|
||||
iteration: 2
|
||||
summary:
|
||||
text: 保留原框架无关业务核心,追加共享Pillow图片工具与农场快照渲染。673原素材独立打包,目标尺寸合成、三种编码、阶段变化失效、有界缓存、受控后台任务、取消排空和日志。13项绘图定向测试及3项打包检查通过,样图已目视检查。尚未执行本轮完整核心回归,由Runtime正式执行;真实框架与上传未接入,旧素材455个阶段引用缺图。
|
||||
text: 按独立验收建议修正余额不足的中文提示映射,增加无扣款回归测试;其余已确认A1-A18实现不变。此前55项/最低23项/工作流检查及独立验收通过,本轮受影响检查由Runtime重验,未执行远端或真实宿主。
|
||||
truncated: false
|
||||
addressed_acceptance_ids:
|
||||
- A1
|
||||
@@ -149,6 +192,11 @@ builder_handoff:
|
||||
- A11
|
||||
- A12
|
||||
- A13
|
||||
- A14
|
||||
- A15
|
||||
- A16
|
||||
- A17
|
||||
- A18
|
||||
acceptance_review:
|
||||
- id: A1
|
||||
status: implemented-with-evidence
|
||||
@@ -234,50 +282,71 @@ builder_handoff:
|
||||
- docs/10-rendering.md
|
||||
- docs/render-benchmark.json
|
||||
note: 新实现对应已确认规格,定向测试和离线样例可复核。
|
||||
- id: A14
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- integrations/zhenxun/runtime.py
|
||||
- integrations/zhenxun/commands.py
|
||||
- tests/test_zhenxun_runtime.py
|
||||
- tests/test_zhenxun_runtime.py::test_insufficient_funds_has_chinese_message_and_no_debit
|
||||
note: 按用户最终选择沿用原指令并保留独立测试数据;定向检查通过,完整与最低依赖检查纳入Runtime计划。
|
||||
- id: A15
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- scripts/build_zhenxun.py
|
||||
- tests/test_zhenxun_package.py
|
||||
note: 按用户最终选择沿用原指令并保留独立测试数据;定向检查通过,完整与最低依赖检查纳入Runtime计划。
|
||||
- id: A16
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- scripts/verify_zhenxun.py
|
||||
- scripts/install_zhenxun.py
|
||||
- tests/test_zhenxun_package.py
|
||||
note: 按用户最终选择沿用原指令并保留独立测试数据;定向检查通过,完整与最低依赖检查纳入Runtime计划。
|
||||
- id: A17
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- .github/workflows/zhenxun-preview.yml
|
||||
- release/zhenxun/verify.yml
|
||||
- scripts/publish_zhenxun.py
|
||||
- scripts/check_workflows.py
|
||||
- tests/test_zhenxun_package.py
|
||||
note: 按用户最终选择沿用原指令并保留独立测试数据;定向检查通过,完整与最低依赖检查纳入Runtime计划。
|
||||
- id: A18
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- docs/11-zhenxun-preview.md
|
||||
- scripts/check_pillow_compat.py
|
||||
- tests/test_zhenxun_runtime.py
|
||||
note: 按用户最终选择沿用原指令并保留独立测试数据;定向检查通过,完整与最低依赖检查纳入Runtime计划。
|
||||
checks:
|
||||
- name:
|
||||
text: render-behavior
|
||||
text: insufficient-funds-message
|
||||
truncated: false
|
||||
result: passed
|
||||
note:
|
||||
text: 13 tests passed
|
||||
truncated: false
|
||||
- name:
|
||||
text: wheel-packaging
|
||||
truncated: false
|
||||
result: passed
|
||||
note:
|
||||
text: 3 tests passed; installed renderer generated PNG
|
||||
truncated: false
|
||||
- name:
|
||||
text: visual-demo
|
||||
truncated: false
|
||||
result: passed
|
||||
note:
|
||||
text: 1024 WebP sample reviewed; three formats measured
|
||||
truncated: false
|
||||
- name:
|
||||
text: full-regression
|
||||
truncated: false
|
||||
result: not-run
|
||||
note:
|
||||
text: 本轮完整检查交给Runtime执行
|
||||
text: Observed failing test, fixed mapping, targeted test passed; balance
|
||||
unchanged
|
||||
truncated: false
|
||||
checks_truncated: false
|
||||
known_limits:
|
||||
- text: 真实框架、上传、Linux和低配主机尚未运行验证。
|
||||
- text: 真实真寻/NoneBot依赖注入与平台发图尚未实际运行;模拟宿主不是线上验证。
|
||||
truncated: false
|
||||
- text: 中文字体需部署方合法提供;不捆绑字体。
|
||||
- text: 本机Windows已测,Linux仅配置CI尚未执行。
|
||||
truncated: false
|
||||
- text: 旧基线455个阶段引用缺图,已占位降级。
|
||||
- text: 工作流与跨仓库API只离线检查/模拟,未推送未运行GitHub。
|
||||
truncated: false
|
||||
- text: 测量为本机单次合成数据,不是旧版或等画质基准。
|
||||
- text: 独立预览库不迁移旧数据;同名指令测试需用户手动停用旧版。
|
||||
truncated: false
|
||||
- text: 公开发布前仍需核实历史素材分发权。
|
||||
- text: 默认JPEG;WebP需要用户确认消息平台能力后配置。
|
||||
truncated: false
|
||||
- text: 缺失455个旧阶段素材引用以占位降级;字体及历史素材发布权需部署/发布前核实。
|
||||
truncated: false
|
||||
- text: 本地源码dirty故产物会标记dirty,发布脚本拒绝发布dirty产物。
|
||||
truncated: false
|
||||
known_limits_truncated: false
|
||||
review: null
|
||||
submitted_at: 2026-10-15T08:36:49.457Z
|
||||
submitted_at: 2026-10-15T09:52:39.181Z
|
||||
blockers:
|
||||
- owner: user
|
||||
reason:
|
||||
@@ -287,17 +356,17 @@ blockers:
|
||||
acceptance_ids: []
|
||||
resolution_action: await-user
|
||||
verification:
|
||||
candidate_id: e88969d9-32c0-4321-a5a3-658023a60414
|
||||
candidate_id: 24c9df3f-4c07-4b34-a21a-8f2ccf9f4adb
|
||||
identity_provider: skill-coordinated
|
||||
verifier_execution_ref: skill-coordinated:verifier:853b70de-beda-44dd-b303-cf2b3b300676
|
||||
iteration: 1
|
||||
verifier_execution_ref: skill-coordinated:verifier:d68f2636-d977-4a84-bc40-0dec8d89c753
|
||||
iteration: 2
|
||||
attempt: 1
|
||||
assurance: skill-coordinated
|
||||
verdict: pass
|
||||
checks:
|
||||
- id: offline-foundation-render
|
||||
- id: all-offline
|
||||
name:
|
||||
text: Offline core and renderer
|
||||
text: Core render and zhenxun regression
|
||||
truncated: false
|
||||
argv_display:
|
||||
- text: scripts/verify_foundation.py
|
||||
@@ -306,23 +375,51 @@ verification:
|
||||
cwd_ref: .
|
||||
status: passed
|
||||
exit_code: 0
|
||||
duration_ms: 10608
|
||||
duration_ms: 22011
|
||||
- id: pillow-10-compat
|
||||
name:
|
||||
text: Pillow10.4 render and zhenxun
|
||||
truncated: false
|
||||
argv_display:
|
||||
- text: scripts/check_pillow_compat.py
|
||||
truncated: false
|
||||
- text: --dependency-path
|
||||
truncated: false
|
||||
- text: .release-cache/pillow10
|
||||
truncated: false
|
||||
argv_truncated: false
|
||||
cwd_ref: .
|
||||
status: passed
|
||||
exit_code: 0
|
||||
duration_ms: 12697
|
||||
- id: workflow-guards
|
||||
name:
|
||||
text: Workflow YAML and boundaries
|
||||
truncated: false
|
||||
argv_display:
|
||||
- text: scripts/check_workflows.py
|
||||
truncated: false
|
||||
- text: --dependency-path
|
||||
truncated: false
|
||||
- text: .release-cache/qa
|
||||
truncated: false
|
||||
argv_truncated: false
|
||||
cwd_ref: .
|
||||
status: passed
|
||||
exit_code: 0
|
||||
duration_ms: 95
|
||||
summary:
|
||||
text: 独立只读核对全部13项、完整规格、核心与渲染源码、测试/打包/导出脚本、正式Runtime日志和图片样例;复用匹配候选的45项正式检查,全部验收通过。图索引新增渲染未覆盖且旧文件metadata_changed,已直接源文件补齐,无需重复完整套件。
|
||||
text: 独立只读核对完整brief、三份规格和18项,审阅核心/渲染/真寻接入、白名单构建/校验/安装/固定发布边界、测试工作流与文档。当前candidate绑定Runtime的56项完整回归、Pillow10.4下24项及工作流守卫均退出码0;日志前缀658580e0与当前cedb操作绑定已核实。额外只读确认最终ZIP30文件且runtime匹配修复源码,余额不足中文提示与无扣款回归通过。图索引过期/缺覆盖已直接源码补齐,未重复完整套件、修改实现或正式文件、远端操作、安装启动宿主或代用户接受。
|
||||
truncated: false
|
||||
risks:
|
||||
- text: 真实消息平台、上传链路、Linux及低配服务器尚未验证;性能报告仅本机单次构造样例,不等画质对照。
|
||||
- text: 真实真寻依赖注入、登录及平台发图未运行;Linux与GitHub工作流/跨仓库API仅配置或模拟,未宣称上线。
|
||||
truncated: false
|
||||
- text: 旧素材存在455个阶段引用缺失,已按规格显示问号并记录警告;不能宣称202种作物图片完整。
|
||||
- text: 沿用原指令需用户先停用旧插件;独立新库不迁移旧玩家数据。AUTO保守默认JPEG,WebP实际平台能力待用户测试。
|
||||
truncated: false
|
||||
- text: 部署需自备有权使用且包含所需中文字形的字体;当前只验证字体可加载,任意Unicode/Emoji覆盖不保证。
|
||||
truncated: false
|
||||
- text: 历史素材公开分发权仍需在发布前核实。
|
||||
truncated: false
|
||||
- text: 缓存预算为像素/编码内容核算,含预留,不代表整个进程峰值RSS硬上限。
|
||||
- text: 旧素材455个阶段引用缺图以占位降级;公开发布前需核实素材及字体使用权;当前dirty本地产物只用于测试,发布器拒绝dirty。
|
||||
truncated: false
|
||||
risks_truncated: false
|
||||
completed_at: 2026-10-15T08:40:00.608Z
|
||||
completed_at: 2026-10-15T09:58:23.989Z
|
||||
history:
|
||||
- goal_cycle: 1
|
||||
iteration: 1
|
||||
@@ -361,6 +458,51 @@ history:
|
||||
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
|
||||
history_overflow:
|
||||
dropped_entries: 0
|
||||
first_dropped_at: null
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# 真寻可安装预览包
|
||||
|
||||
## 接入边界
|
||||
|
||||
仅提供真寻接入;业务和图片仍来自主仓库共享源码。接入层加载插件
|
||||
私有_vendor命名空间,渲染层对farm_core的引用在构建时改为私有相对
|
||||
导入,不修改开发源码、不污染全局sys.path或sys.modules别名。
|
||||
命令不增加测试前缀,沿用开通农场、我的农场、农场帮助、我的种子、
|
||||
我的作物、购买种子、播种、收获、铲除、作物加锁、作物解锁、出售作物
|
||||
等原指令,覆盖帮助、注册、我的农场、仓库、购买、
|
||||
播种、收获、铲除、锁定/解锁和出售;复用已有规则,不移植旧损坏模块。
|
||||
部署标识由配置给出,同一平台玩家在本预览部署内共享农场,不跨平台
|
||||
绑定;测试数据位置与旧版不同。事件标识包含机器人、平台、用户及
|
||||
原消息标识;无法获得稳定消息标识时写操作明确拒绝,不能随机重试。
|
||||
在启动时初始化存储、渲染器,在关闭时等待任务排空后关闭存储;字体
|
||||
必须配置为部署方拥有使用权的中文字体。无字体时文本功能仍可用,
|
||||
我的农场返回明确配置提示。头像失败降级,不让头像下载无限等待。
|
||||
|
||||
Scenario: 真寻命令与生命周期
|
||||
Acceptance: A14
|
||||
模拟宿主验证命令路由、身份/幂等标识、异常反馈与启动关闭,使用独立库;
|
||||
不把模拟宿主测试报告为真实平台发图成功。
|
||||
|
||||
## 产物
|
||||
|
||||
scripts/build_zhenxun.py按白名单复制真寻接入、farm_core、farm_render、
|
||||
对应数据/图片、LICENSE及NOTICE。ZIP含单一插件根目录,仓库导出目录
|
||||
可作为真寻发布仓库根。生成来源manifest与独立SHA256附件,清单不
|
||||
自包含哈希;本地工作区脏时标识dirty,不能冒充源HEAD的纯净构建。
|
||||
输入版本严格校验,ZIP排序/时间固定,目标存在则拒绝或显式选择新路径。
|
||||
不打包其他适配器、运行库、凭据、缓存、legacy或全局字体。
|
||||
|
||||
Scenario: 可复现白名单打包
|
||||
Acceptance: A15
|
||||
重复构建内容相同,独立目录导入私有模块成功,无旧目录和其他框架依赖。
|
||||
|
||||
## 安装验证
|
||||
|
||||
独立verify脚本校验manifest中每个文件与ZIP精确一致,拒绝多余/重复项、
|
||||
绝对路径、反斜线穿越、符号链接、不可信插件根名、哈希错误。
|
||||
安装器只安装到用户显式提供的插件父目录,既有目标拒绝覆盖,不删旧
|
||||
插件、不接触旧数据、不安装依赖、不启动/重启机器人;先校验再写入,
|
||||
失败清理自己新建的临时目录。提供明确安装步骤和停止机器人提示。
|
||||
|
||||
Scenario: 安全校验与安装
|
||||
Acceptance: A16
|
||||
临时目录测试有效包可安装,坏包/穿越/已有目标均拒绝且不留下半成品。
|
||||
|
||||
## 工作流和发布
|
||||
|
||||
主仓库CI在PR/push及手动触发时执行检查,构建ZIP/清单/校验附件并上传
|
||||
Actions artifact。工作流权限默认只读;不可信PR不触发跨仓库写入。
|
||||
跨仓库同步是独立的手动选择,使用专用Secret/受保护environment,并
|
||||
固定目标Shu-Ying/zhenxun_plugin_farm的farm-preview分支。
|
||||
脚本默认dry-run;只有显式发布才联网。只创建或普通更新预览分支,
|
||||
不force、不碰main/旧标签;并发分支改变时失败。发布仓库只含生成的
|
||||
可安装内容、来源和验证工具,以及验证工作流,不在那里维护业务源码。
|
||||
下载ZIP测试不要求先同步仓库,不要求用户先给令牌。
|
||||
|
||||
Scenario: 测试附件与可控同步
|
||||
Acceptance: A17
|
||||
静态检查工作流触发/权限和输入边界;模拟服务验证固定目标、冲突保护、
|
||||
令牌不落盘与默认dry-run。此任务不执行真实GitHub发布。
|
||||
|
||||
## 兼容与文档
|
||||
|
||||
真寻本地配置要求Pillow>=10,<11,本预览使用与其相交的>=10.4版本;
|
||||
CI测试最低10.4和本项目支持当前版本,Windows/Linux。不得静默强制
|
||||
升级宿主到12。文档列出本机实际检查、模拟与真实宿主的差别,并提供
|
||||
主仓库生成、发布仓库布局、下载/依赖安装、字体、数据库、测试命令、
|
||||
与旧版避免冲突、备份/卸载和可选同步的凭据最小权限步骤。沿用原
|
||||
指令时必须由用户停用旧插件,不静默更名指令,不自动禁用旧插件。
|
||||
|
||||
Scenario: 可复现的真寻测试交付
|
||||
Acceptance: A18
|
||||
产物可离线验证,文档能按步骤启动人工测试;明确真实机器人与GitHub
|
||||
工作流仍未实际执行,不宣称已经上线或已兼容全部平台。
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
generated_from_state_version: 14
|
||||
generated_from_state_version: 26
|
||||
---
|
||||
|
||||
# Verification
|
||||
@@ -8,49 +8,55 @@ generated_from_state_version: 14
|
||||
|
||||
- Result: **Verification passed; your confirmation is required**
|
||||
- Verification status: **Checks completed, but your confirmation is required**
|
||||
- Goal cycle: 3
|
||||
- Iteration: 1
|
||||
- Goal cycle: 5
|
||||
- Iteration: 2
|
||||
- Verifier attempt: 1
|
||||
- Completed: 2026-10-15T08:40:00.608Z
|
||||
- Summary: 独立只读核对全部13项、完整规格、核心与渲染源码、测试/打包/导出脚本、正式Runtime日志和图片样例;复用匹配候选的45项正式检查,全部验收通过。图索引新增渲染未覆盖且旧文件metadata_changed,已直接源文件补齐,无需重复完整套件。
|
||||
- Completed: 2026-10-15T09:58:23.989Z
|
||||
- Summary: 独立只读核对完整brief、三份规格和18项,审阅核心/渲染/真寻接入、白名单构建/校验/安装/固定发布边界、测试工作流与文档。当前candidate绑定Runtime的56项完整回归、Pillow10.4下24项及工作流守卫均退出码0;日志前缀658580e0与当前cedb操作绑定已核实。额外只读确认最终ZIP30文件且runtime匹配修复源码,余额不足中文提示与无扣款回归通过。图索引过期/缺覆盖已直接源码补齐,未重复完整套件、修改实现或正式文件、远端操作、安装启动宿主或代用户接受。
|
||||
|
||||
## Acceptance
|
||||
|
||||
| ID | Result | Source | Criterion | Reason |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| A1 | passed | brief.md | A1: 旧基线 827 文件校验继续通过;新核心不导入三框架、无导入副作用, 可从安装包或源码独立运行,核心不通过路径引用 legacy。 | 827文件基线正式检查通过;检查包导入无线程/文件副作用及wheel独立安装,核心无框架和legacy运行依赖。 |
|
||||
| A2 | passed | brief.md | A2: 可复现导出 202 条作物配置,名称和必要数值对应源静态数据库; 本地加载支持格式校验,坏数据明确失败,不访问网络。 | 只读源库导出按id排序并带源hash;202项导出检查通过,加载器校验版本、整数、阶段和重复定义。 |
|
||||
| A3 | passed | brief.md | A3: 注册立即可查询 500 农场币、0 点券、0 经验、3 块空地; 重复注册不重复赠送;不同显式作用域不串号。 | 账户注册与初始500资产、三空地在事务内完成;重复注册及身份作用域隔离测试正式通过。 |
|
||||
| A4 | passed | brief.md | A4: 购买到播种全过程扣款、入库、耗种和地块改变正确;0、负数、 布尔值、超大数量、余额/库存不足、等级不足和不存在对象明确失败, 不留下部分写入。 | 购买和播种在完整事务内扣款、入库、耗种、存图;数量、等级、余额和空地边界测试正式通过。 |
|
||||
| A5 | passed | brief.md | A5: 未成熟不能收获;成熟后只增加本次产量/经验;历史经验 100、 本次获得20时为120;多地加成逐块计算且交换顺序不改变总和; 多季次数及枯萎/铲除遵守保存的规则与可注入时钟。 | 成熟边界、经验增量、多季再生长及枯萎清除源实现与规格一致,逐地块加成及顺序测试通过。 |
|
||||
| A6 | passed | brief.md | A6: 出售收益只增加一次,例如余额100、收益20后为120; 锁定同时约束指定与全部出售;返回结构化真实余额及安全文本, 不发生旧版模板参数缺失。 | 锁定状态对指定及全部出售生效,出售只增本次收入,Receipt和文本使用真实余额;正式测试通过。 |
|
||||
| A7 | passed | brief.md | A7: 模拟交易中途异常时余额、库存、地块及流水整体回滚; 同一操作标识重试返回原结果、参数冲突拒绝;并发写入不超卖、 不重复领取,不吞数据库故障伪装成功;重启后数据可读。 | BEGIN IMMEDIATE、操作指纹与结果同事务保存;异常回滚、并发超卖/重复收获、取消重放、重启与schema测试通过。 |
|
||||
| A8 | passed | brief.md | A8: 提供可执行的离线测试、运行入口和使用说明,保留真实宿主、 真实库迁移及远程发布未验证的说明;文档记录已实现范围和剩余项。 | README、核心说明、离线入口与测试可用,准确说明尚无真实宿主、真实库迁移和远程发布验证。 |
|
||||
| A9 | passed | brief.md | A9: 独立图片工具支持透明合成、缩放、裁剪、文本测量/省略和三格式编码; 渲染不导入框架、不联网,核心保持无 Pillow 依赖;安装包含所需素材。 | Pillow可选渲染模块支持透明合成、复制、缩放、裁剪、文字测量省略及三格式;核心无Pillow导入,wheel携带673素材并独立渲染通过。 |
|
||||
| A10 | passed | brief.md | A10: 从一次快照绘制空地、生长、成熟、再生长、枯萎及锁定地块, 使用统一时间、旧布局和正确偏移;输出普通/高清可解码图片。 | 纯场景转换消费一次快照和统一now,首季/再季锚点与枯萎优先正确;旧30块布局及真实offset字段已迁出,普通高清解码与阶段测试通过,样例已视觉检查。 |
|
||||
| A11 | passed | brief.md | A11: 小画布从开始按目标尺寸绘制;素材和结果缓存有内存上限, 共享素材不被污染;阶段/成熟边界、账户、头像及作用域变化不复用旧图。 | 目标尺寸直接合成;素材/底图与成图LRU有字节预算且返回副本;键含作用域、账户、阶段、头像、主题及编码配置,边界变化不复用旧图。 |
|
||||
| A12 | passed | brief.md | A12: 合成和编码在后台执行且限制并发,相同请求合并;取消和异常 不提前释放运行名额、不泄漏后台任务;记录耗时、尺寸和缓存状态。 | 整体合成编码由to_thread执行,semaphore限制并发、pending限制容量,shield维持取消后的任务容量并在关闭排空;合并、异常重试、事件循环和并发测试通过,日志不含用户标识。 |
|
||||
| A13 | passed | brief.md | A13: 离线生成农场样例、对比尺寸/编码与冷暖缓存,保存测量报告和 接入文档;验证旧基线及原有核心测试,明确尚未接入真实消息平台。 | 离线脚本生成两尺寸三格式冷暖测量及日志,报告与已查看样例一致;正式检查45项和基线/两类导出通过,文档明确本机单次、未测上传/低配/Linux及真实平台。 |
|
||||
| A1 | passed | brief.md | A1: 旧基线 827 文件校验继续通过;新核心不导入三框架、无导入副作用, 可从安装包或源码独立运行,核心不通过路径引用 legacy。 | 827文件基线正式检查通过;核心源码仅相对导入,安装及导入副作用测试通过,不依赖legacy或框架。 |
|
||||
| A2 | passed | brief.md | A2: 可复现导出 202 条作物配置,名称和必要数值对应源静态数据库; 本地加载支持格式校验,坏数据明确失败,不访问网络。 | 202条目录源值及导出复现、损坏目录拒绝测试通过;Catalogue本地读取且验证schema与重复对象。 |
|
||||
| A3 | passed | brief.md | A3: 注册立即可查询 500 农场币、0 点券、0 经验、3 块空地; 重复注册不重复赠送;不同显式作用域不串号。 | repository.register在事务内发放500币和3地;重复注册与四段身份隔离正式测试通过。 |
|
||||
| A4 | passed | brief.md | A4: 购买到播种全过程扣款、入库、耗种和地块改变正确;0、负数、 布尔值、超大数量、余额/库存不足、等级不足和不存在对象明确失败, 不留下部分写入。 | 购买播种均通过同一事务调用,整数/等级/资产/空地检查明确;失败无部分写入测试通过。 |
|
||||
| A5 | passed | brief.md | A5: 未成熟不能收获;成熟后只增加本次产量/经验;历史经验 100、 本次获得20时为120;多地加成逐块计算且交换顺序不改变总和; 多季次数及枯萎/铲除遵守保存的规则与可注入时钟。 | 收获按各地块单独计算增量、保存多季状态;成熟边界、加成排序、冻结规则与枯萎测试通过。 |
|
||||
| A6 | passed | brief.md | A6: 出售收益只增加一次,例如余额100、收益20后为120; 锁定同时约束指定与全部出售;返回结构化真实余额及安全文本, 不发生旧版模板参数缺失。 | 指定和全部出售均保护锁定状态,只加本次收入;文本及结构化回执返回真实账户,回归通过。 |
|
||||
| A7 | passed | brief.md | A7: 模拟交易中途异常时余额、库存、地块及流水整体回滚; 同一操作标识重试返回原结果、参数冲突拒绝;并发写入不超卖、 不重复领取,不吞数据库故障伪装成功;重启后数据可读。 | BEGIN IMMEDIATE和操作指纹/持久结果覆盖原子回滚、幂等冲突、并发及取消重放;重启/模式拒绝测试通过。 |
|
||||
| A8 | passed | brief.md | A8: 提供可执行的离线测试、运行入口和使用说明,保留真实宿主、 真实库迁移及远程发布未验证的说明;文档记录已实现范围和剩余项。 | 离线入口、完整56项回归及实现说明可复核;真实宿主、迁库及远程发布边界有说明。 |
|
||||
| A9 | passed | brief.md | A9: 独立图片工具支持透明合成、缩放、裁剪、文本测量/省略和三格式编码; 渲染不导入框架、不联网,核心保持无 Pillow 依赖;安装包含所需素材。 | Canvas支持透明合成/复制/缩放/裁剪/文字/三格式编码;核心无Pillow运行依赖,wheel独立渲染测试通过。 |
|
||||
| A10 | passed | brief.md | A10: 从一次快照绘制空地、生长、成熟、再生长、枯萎及锁定地块, 使用统一时间、旧布局和正确偏移;输出普通/高清可解码图片。 | 统一快照和now生成场景,阶段按首熟/再生长锚点映射,枯萎优先并使用实际偏移;普通高清解码及边界测试通过。 |
|
||||
| A11 | passed | brief.md | A11: 小画布从开始按目标尺寸绘制;素材和结果缓存有内存上限, 共享素材不被污染;阶段/成熟边界、账户、头像及作用域变化不复用旧图。 | 底图直接为目标宽度;素材返回副本,LRU预算有界;身份、可见账户/地块阶段、头像和配置纳入键,正式缓存测试通过。 |
|
||||
| A12 | passed | brief.md | A12: 合成和编码在后台执行且限制并发,相同请求合并;取消和异常 不提前释放运行名额、不泄漏后台任务;记录耗时、尺寸和缓存状态。 | RenderService整图to_thread,有限排队及并发、相同请求合并,shield取消和关闭排空;并发/取消/失败重试测试通过。 |
|
||||
| A13 | passed | brief.md | A13: 离线生成农场样例、对比尺寸/编码与冷暖缓存,保存测量报告和 接入文档;验证旧基线及原有核心测试,明确尚未接入真实消息平台。 | 离线样例报告标注本机单次及未测上传;三格式冷暖数据、布局及文档保留,旧基线和原核心回归继续通过。 |
|
||||
| A14 | passed | brief.md | A14: 真寻接入沿用原指令提供帮助、注册、我的农场、仓库、购买、 播种、收获、铲除、锁定/解锁、出售;稳定事件幂等标识、身份隔离, 独立测试数据库与启动/关闭生命周期;缺字体时给出配置提示。 | 沿用原中文指令及旧子命令,独立预览库、稳定事件哈希、完整身份、生命周期/字体/头像降级已核对;余额不足映射修正并通过无扣款新测试。 |
|
||||
| A15 | passed | brief.md | A15: 白名单构建完整真寻插件ZIP,包含私有共享模块、静态素材、 依赖、许可证及来源/文件哈希;不含其他适配、legacy、测试或玩家数据, 源提交/脏工作区明确记录,相同输入可复现,构建不修改开发源码。 | 构建白名单包含私有模块、素材/许可证NOTICE及来源哈希,固定ZIP元数据,不修改开发导入;复现测试通过;最终修复包校验30文件且runtime匹配源码。 |
|
||||
| A16 | passed | brief.md | A16: 离线校验ZIP内容、导入和命令路由;安装脚本拒绝路径穿越、 哈希错误和已有目标,不覆盖旧插件或玩家数据;不自动启动真实机器人。 | 校验器拒绝重复/额外/穿越/符号链接/哈希错误,先验再安装且独占创建不覆盖;正式边界测试通过。Windows临时目录实测尾点空格变体未复现逃逸。 |
|
||||
| A17 | passed | brief.md | A17: 主仓库工作流完成检查/构建/上传测试附件;可选手动发布只面向 固定真寻仓库的专用测试分支,默认不发布、不改main、不force,凭据 由Secrets提供且不进入产物;发布仓库收到生成内容后可独立校验。 | CI默认只读/附件输出;同步需手动main触发与受保护环境Secret,脚本默认离线且固定仓库farm-preview、拒绝默认/非生成分支、无force并保护竞态;模拟/守卫通过。 |
|
||||
| A18 | passed | brief.md | A18: 本机离线验证打包、边界与安装行为;CI覆盖Windows/Linux及 Pillow10.4和当前支持版本,记录真实宿主、GitHub运行未验证边界; 提供下载、字体/数据路径、测试命令、回退/卸载和Secrets配置说明。 | Windows本机56项及隔离Pillow10.4下24项均正式通过;CI矩阵明确Windows/Linux和10.4/12.3,文档含安装/字体/数据/原指令冲突/卸载/Secrets及未验证边界。 |
|
||||
|
||||
## Checks
|
||||
|
||||
| Check | Command | Working directory | Status | Exit | Duration |
|
||||
| --- | --- | --- | --- | ---: | ---: |
|
||||
| Offline core and renderer | scripts/verify_foundation.py | . | passed | 0 | 10608 ms |
|
||||
| Core render and zhenxun regression | scripts/verify_foundation.py | . | passed | 0 | 22011 ms |
|
||||
| Pillow10.4 render and zhenxun | scripts/check_pillow_compat.py --dependency-path .release-cache/pillow10 | . | passed | 0 | 12697 ms |
|
||||
| Workflow YAML and boundaries | scripts/check_workflows.py --dependency-path .release-cache/qa | . | passed | 0 | 95 ms |
|
||||
|
||||
### Builder-reported evidence
|
||||
|
||||
These are Builder reports, not Runtime check receipts or independent verification results.
|
||||
|
||||
- render-behavior: passed — 13 tests passed
|
||||
- wheel-packaging: passed — 3 tests passed; installed renderer generated PNG
|
||||
- visual-demo: passed — 1024 WebP sample reviewed; three formats measured
|
||||
- full-regression: not-run — 本轮完整检查交给Runtime执行
|
||||
- Known limitation: 真实框架、上传、Linux和低配主机尚未运行验证。
|
||||
- Known limitation: 中文字体需部署方合法提供;不捆绑字体。
|
||||
- Known limitation: 旧基线455个阶段引用缺图,已占位降级。
|
||||
- Known limitation: 测量为本机单次合成数据,不是旧版或等画质基准。
|
||||
- Known limitation: 公开发布前仍需核实历史素材分发权。
|
||||
- insufficient-funds-message: passed — Observed failing test, fixed mapping, targeted test passed; balance unchanged
|
||||
- Known limitation: 真实真寻/NoneBot依赖注入与平台发图尚未实际运行;模拟宿主不是线上验证。
|
||||
- Known limitation: 本机Windows已测,Linux仅配置CI尚未执行。
|
||||
- Known limitation: 工作流与跨仓库API只离线检查/模拟,未推送未运行GitHub。
|
||||
- Known limitation: 独立预览库不迁移旧数据;同名指令测试需用户手动停用旧版。
|
||||
- Known limitation: 默认JPEG;WebP需要用户确认消息平台能力后配置。
|
||||
- Known limitation: 缺失455个旧阶段素材引用以占位降级;字体及历史素材发布权需部署/发布前核实。
|
||||
- Known limitation: 本地源码dirty故产物会标记dirty,发布脚本拒绝发布dirty产物。
|
||||
|
||||
## Blockers
|
||||
|
||||
@@ -58,11 +64,9 @@ These are Builder reports, not Runtime check receipts or independent verificatio
|
||||
|
||||
## Risks and skipped work
|
||||
|
||||
- 真实消息平台、上传链路、Linux及低配服务器尚未验证;性能报告仅本机单次构造样例,不等画质对照。
|
||||
- 旧素材存在455个阶段引用缺失,已按规格显示问号并记录警告;不能宣称202种作物图片完整。
|
||||
- 部署需自备有权使用且包含所需中文字形的字体;当前只验证字体可加载,任意Unicode/Emoji覆盖不保证。
|
||||
- 历史素材公开分发权仍需在发布前核实。
|
||||
- 缓存预算为像素/编码内容核算,含预留,不代表整个进程峰值RSS硬上限。
|
||||
- 真实真寻依赖注入、登录及平台发图未运行;Linux与GitHub工作流/跨仓库API仅配置或模拟,未宣称上线。
|
||||
- 沿用原指令需用户先停用旧插件;独立新库不迁移旧玩家数据。AUTO保守默认JPEG,WebP实际平台能力待用户测试。
|
||||
- 旧素材455个阶段引用缺图以占位降级;公开发布前需核实素材及字体使用权;当前dirty本地产物只用于测试,发布器拒绝dirty。
|
||||
|
||||
## Previous iterations
|
||||
|
||||
@@ -72,9 +76,14 @@ These are Builder reports, not Runtime check receipts or independent verificatio
|
||||
| 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 |
|
||||
|
||||
|
||||
|
||||
## Conclusion
|
||||
|
||||
独立只读核对全部13项、完整规格、核心与渲染源码、测试/打包/导出脚本、正式Runtime日志和图片样例;复用匹配候选的45项正式检查,全部验收通过。图索引新增渲染未覆盖且旧文件metadata_changed,已直接源文件补齐,无需重复完整套件。
|
||||
独立只读核对完整brief、三份规格和18项,审阅核心/渲染/真寻接入、白名单构建/校验/安装/固定发布边界、测试工作流与文档。当前candidate绑定Runtime的56项完整回归、Pillow10.4下24项及工作流守卫均退出码0;日志前缀658580e0与当前cedb操作绑定已核实。额外只读确认最终ZIP30文件且runtime匹配修复源码,余额不足中文提示与无扣款回归通过。图索引过期/缺覆盖已直接源码补齐,未重复完整套件、修改实现或正式文件、远端操作、安装启动宿主或代用户接受。
|
||||
|
||||
@@ -52,3 +52,24 @@
|
||||
可重复检查全部成功。已启动独立只读Verifier检查A1-A13,尚待结论。
|
||||
- 独立Verifier已完成完整规格/源码/测试和实际样图核对,A1-A13全部
|
||||
通过,Runtime接受报告并等待用户接受;未执行归档、提交或发布。
|
||||
|
||||
## 真寻预览交付
|
||||
|
||||
- 用户选择接入+工作流+安装脚本,并明确不增加测试前缀,沿用原指令。
|
||||
- 独立预览数据库和日志、稳定事件幂等、启动/关闭、头像超时及图片接入。
|
||||
- 白名单私有模块包、源清单和哈希、拒绝覆盖的安装器、固定测试分支
|
||||
的默认dry-run同步脚本已实现;未执行远端或真实机器人操作。
|
||||
- 6项打包/安装/模拟发布测试和4项模拟宿主/核心联合测试通过。
|
||||
- Pillow10.4独立测试目录的13项绘图测试通过;不改宿主依赖。
|
||||
- 工作流YAML及发布守卫检查通过;主CI覆盖Windows/Linux和两版Pillow,
|
||||
真实GitHub运行仍未验证。
|
||||
- 固定ZIP创建系统与权限,解决Windows资源到Linux CI复现的问题。
|
||||
- 代码图连接本轮不可用,相关宿主接口已直接阅读本地源码核对。
|
||||
- 当前候选准备进入Runtime全量回归和独立只读验收。
|
||||
- 首轮55项回归、最低依赖23项和工作流守卫通过,独立A1-A18通过。
|
||||
- 按审查建议补正余额不足中文提示,先观察定向回归失败再修复;
|
||||
第二轮56项回归、最低依赖24项和工作流守卫全部通过。
|
||||
- 新的独立只读Verifier确认A1-A18全部通过,最终ZIP30文件校验通过,
|
||||
包内runtime.py与修复源码一致。已生成dist/zhenxun-preview-ready交付。
|
||||
- 最终大小/哈希和验证边界保存zhenxun-preview-checks.json。
|
||||
- 当前等待用户接受验收;没有提交、归档、推送、发布或真实宿主安装。
|
||||
|
||||
@@ -68,3 +68,13 @@
|
||||
- [x] 生成普通/高清样例并检查文字布局,保存三格式测量和使用文档。
|
||||
- [x] 正式Runtime检查45项测试通过;独立Verifier判定全部A1-A13通过。
|
||||
- [ ] 用户接受结果后归档。
|
||||
|
||||
## 第四轮:真寻原指令预览包
|
||||
|
||||
- [x] 用户确认接入、打包工作流和安装脚本;原指令,不加测试前缀。
|
||||
- [x] 真寻命令、独立库、日志、生命周期和图片接入。
|
||||
- [x] 白名单ZIP、校验/安装、固定预览分支dry-run发布脚本。
|
||||
- [x] 主仓库CI附件与发布仓库校验工作流;未执行远端操作。
|
||||
- [x] 定向打包与宿主模拟测试、最低Pillow绘图和YAML检查。
|
||||
- [x] Runtime56项、Pillow10.4下24项及工作流检查通过;独立A1-A18通过。
|
||||
- [ ] 用户接受后归档;真实机器人测试由用户部署后进行。
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# 真寻测试包实施计划
|
||||
|
||||
沿用已确认A1-A13;新增验收A14-A18,正式目标见Comet zhenxun-preview规格。
|
||||
用户明确使用原指令,不增加测试前缀;独立测试库不迁移旧玩家数据。
|
||||
|
||||
- [x] 先写打包/安装/发布边界测试并观察缺功能失败。
|
||||
- [x] integrations/zhenxun:原指令、身份及事件幂等、独立存储、生命周期。
|
||||
- [x] scripts/build_zhenxun.py:白名单完整插件与来源清单、可复现ZIP。
|
||||
- [x] scripts/verify_zhenxun.py、install_zhenxun.py:离线校验与拒绝覆盖。
|
||||
- [x] scripts/publish_zhenxun.py:固定预览分支、默认dry-run、普通更新。
|
||||
- [x] 主仓库工作流构建附件;生成仓库校验工作流;最小权限与手动同步。
|
||||
- [x] 原指令路由与实际私有核心联合测试;Pillow10.4与当前版本检查。
|
||||
- [x] 构建可下载本地ZIP和文档,提交Runtime检查与独立只读验收。
|
||||
|
||||
不执行远端操作,不安装/重启真实机器人,不复制无授权字体。
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"schema": "farm.zhenxun-preview-checks.v1",
|
||||
"date": "2026-10-15",
|
||||
"target": "Shu-Ying/zhenxun_plugin_farm",
|
||||
"preview_branch": "farm-preview",
|
||||
"package": {
|
||||
"path": "dist/zhenxun-preview-ready/zhenxun_plugin_farm_preview-0.1.0-preview.1.zip",
|
||||
"bytes": 12386909,
|
||||
"sha256": "8fe66569a886111ce85187d5ecfe9ec68cc9aee9cd8311bd3fcff43239a61f85",
|
||||
"files": 30,
|
||||
"source_dirty": true
|
||||
},
|
||||
"checks": {
|
||||
"runtime_regression_tests": 56,
|
||||
"pillow_10_4_tests": 24,
|
||||
"workflow_yaml_guards": "passed",
|
||||
"independent_acceptance": "A1-A18 passed",
|
||||
"final_zip_checksum": "passed"
|
||||
},
|
||||
"remote_operations_executed": false,
|
||||
"real_bot_installed_or_started": false,
|
||||
"limitations": [
|
||||
"真实NoneBot依赖注入、权限及平台发图未运行验证",
|
||||
"本机Windows;Linux和GitHub工作流仅配置,未实际运行",
|
||||
"独立测试库不读取旧资产;沿用原指令,需停用旧插件避免冲突",
|
||||
"缺少历史455阶段图片仍占位;字体需自行合法提供",
|
||||
"本地dirty产物仅供测试,正式同步需提交后的干净源码"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
# 真寻接入预留位置
|
||||
# 真寻预览接入
|
||||
|
||||
发布目标:https://github.com/Shu-Ying/zhenxun_plugin_farm.git
|
||||
负责命令、配置、真寻金币、数据目录、权限和生命周期。
|
||||
共享核心通过插件私有路径加载。旧真寻入口仍在 legacy 中,
|
||||
本目录尚不可安装。远程仓库本轮不再拉取或推送。
|
||||
原指令、独立预览数据目录、身份和事件幂等、启动/关闭及发图接入。
|
||||
不接入真寻金币兑换,不迁移旧数据。核心/渲染由主仓库白名单打包,
|
||||
生成时修正为插件私有相对导入,本目录本身不是完整安装包。
|
||||
使用 scripts/build_zhenxun.py 生成ZIP,详见 docs/11-zhenxun-preview.md。
|
||||
本轮没有拉取、推送或操作真实机器人。
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
"""真寻农场独立预览插件入口。"""
|
||||
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from zhenxun.configs.utils import Command, PluginExtraData
|
||||
|
||||
from ._version import __version__
|
||||
from .runtime import COMMANDS, HELP
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="农场预览", description="共享农场核心的真寻预览版",
|
||||
usage=HELP, type="application",
|
||||
homepage="https://github.com/Shu-Ying/zhenxun_plugin_farm",
|
||||
supported_adapters={"~onebot.v11", "~qq"},
|
||||
extra=PluginExtraData(
|
||||
author="Shu-Ying", version=__version__, menu_type="群内小游戏",
|
||||
commands=[Command(command=name) for name in COMMANDS],
|
||||
).to_dict(),
|
||||
)
|
||||
|
||||
from . import commands
|
||||
@@ -0,0 +1,84 @@
|
||||
"""宿主依赖止于接入层;保持旧版中文入口,不新增测试前缀。"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
|
||||
from nonebot import get_driver, on_command, require
|
||||
from nonebot.adapters import Bot, Event, Message
|
||||
from nonebot.params import CommandArg
|
||||
|
||||
require("nonebot_plugin_uninfo")
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
from zhenxun.configs.path_config import DATA_PATH
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
from .runtime import COMMANDS, Runtime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
driver = get_driver()
|
||||
runtime = Runtime(
|
||||
getattr(driver.config, "farm_preview_data_dir", DATA_PATH / "farm_preview"),
|
||||
getattr(driver.config, "farm_preview_font", None),
|
||||
getattr(driver.config, "farm_preview_deployment", "zhenxun-preview"),
|
||||
str(getattr(driver.config, "farm_preview_image_format", "AUTO")).upper(),
|
||||
)
|
||||
driver.on_startup(runtime.start)
|
||||
driver.on_shutdown(runtime.close)
|
||||
_avatars = {}
|
||||
|
||||
|
||||
async def avatar_for(user_id, platform):
|
||||
if platform not in ("OneBot V11", "QQ"):
|
||||
return None
|
||||
key = (platform, user_id)
|
||||
now = time.monotonic()
|
||||
cached = _avatars.get(key)
|
||||
if cached and cached[0] > now:
|
||||
return cached[1]
|
||||
try:
|
||||
content = await asyncio.wait_for(
|
||||
PlatformUtils.get_user_avatar(user_id, "qq"), timeout=2,
|
||||
)
|
||||
if content and len(content) > 2 * 1024 * 1024:
|
||||
content = None
|
||||
except Exception:
|
||||
content = None
|
||||
logger.warning("farm preview avatar unavailable; using default")
|
||||
while len(_avatars) >= 16:
|
||||
_avatars.pop(next(iter(_avatars)))
|
||||
_avatars[key] = (now + (300 if content else 30), content)
|
||||
return content
|
||||
|
||||
|
||||
def handler_for(action):
|
||||
async def handle(
|
||||
bot: Bot, event: Event, session: Uninfo,
|
||||
args: Message = CommandArg(),
|
||||
):
|
||||
platform = bot.adapter.get_name()
|
||||
user_id = str(session.user.id)
|
||||
message_id = getattr(event, "message_id", None)
|
||||
if message_id is None:
|
||||
message_id = getattr(event, "id", None)
|
||||
try:
|
||||
avatar = (await avatar_for(user_id, platform)
|
||||
if action == "farm" else None)
|
||||
response = await runtime.execute(
|
||||
action, args.extract_plain_text(), bot_id=str(bot.self_id),
|
||||
platform=platform, user_id=user_id, message_id=message_id,
|
||||
display_name=session.user.name or "农场玩家", avatar=avatar,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
"farm preview request failed type=%s", type(error).__name__,
|
||||
)
|
||||
response = "农场服务异常,请查看服务器日志。"
|
||||
await MessageUtils.build_message(response).send(reply_to=True)
|
||||
return handle
|
||||
|
||||
|
||||
for command, action in COMMANDS.items():
|
||||
matcher = on_command(command, priority=5, block=True)
|
||||
matcher.handle()(handler_for(action))
|
||||
@@ -0,0 +1,3 @@
|
||||
# 使用宿主已有的NoneBot/真寻/Alconna/Uninfo,不重复安装宿主。
|
||||
# 与真寻当前pillow<11的约束取交集,不升级到12。
|
||||
Pillow>=10.4,<11
|
||||
@@ -0,0 +1,188 @@
|
||||
"""真寻接入编排;核心私有导入,测试数据与旧版隔离。"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
|
||||
from ._vendor.farm_core import Catalogue, FarmService, Identity, SQLiteStore
|
||||
from ._vendor.farm_core.domain import FarmError
|
||||
from ._vendor.farm_core.presentation import receipt_text, snapshot_text
|
||||
from ._vendor.farm_render import FarmRenderer, RenderOptions, RenderService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
COMMANDS = {
|
||||
"开通农场":"register", "我的农场":"farm", "农场详述":"inventory",
|
||||
"我的农场币":"inventory", "我的点券":"inventory",
|
||||
"我的种子":"inventory", "我的作物":"inventory",
|
||||
"种子商店":"shop", "农场帮助":"help", "购买种子":"buy",
|
||||
"播种":"sow", "收获":"harvest", "铲除":"eradicate",
|
||||
"作物加锁":"lock", "作物解锁":"unlock", "出售作物":"sell",
|
||||
}
|
||||
HELP = (
|
||||
"开通农场 [名称]|我的农场 [高清]|农场详述\n"
|
||||
"种子商店 [页码]|购买种子 作物 数量|播种 作物 [数量]\n"
|
||||
"收获|铲除|我的种子|我的作物\n"
|
||||
"作物加锁 作物|作物解锁 作物|出售作物 [作物] [数量]\n"
|
||||
"本版使用独立预览数据库,不会读取旧版资产。"
|
||||
)
|
||||
ERRORS = {
|
||||
"not_registered":"请先发送:开通农场",
|
||||
"unknown_plant":"没有这种作物,请查看种子商店。",
|
||||
"invalid_integer":"数量必须为有效的正整数。",
|
||||
"insufficient_funds":"余额不足。",
|
||||
"insufficient_stock":"库存不足。",
|
||||
"insufficient_empty_plots":"空地不足。",
|
||||
"level_required":"等级不足。", "plant_locked":"该作物已锁定。",
|
||||
"nothing_to_harvest":"当前没有成熟作物。",
|
||||
"nothing_to_eradicate":"当前没有枯萎作物。",
|
||||
"nothing_to_sell":"当前没有可出售作物。",
|
||||
"operation_conflict":"该事件已处理但参数不一致,请发送新消息。",
|
||||
}
|
||||
|
||||
|
||||
def operation_id(bot_id, platform, user_id, message_id, action):
|
||||
if message_id is None or str(message_id) == "":
|
||||
return None
|
||||
return hashlib.sha256(json.dumps(
|
||||
[str(bot_id), platform, user_id, str(message_id), action],
|
||||
ensure_ascii=False,
|
||||
).encode()).hexdigest()
|
||||
|
||||
|
||||
class Runtime:
|
||||
def __init__(self, data_dir, font_path=None, deployment="zhenxun-preview",
|
||||
image_format="AUTO"):
|
||||
self.data_dir = Path(data_dir).resolve()
|
||||
self.font_path = font_path
|
||||
self.deployment = deployment
|
||||
self.image_format = image_format
|
||||
self.store = SQLiteStore(self.data_dir / "farm-preview.sqlite3")
|
||||
self.service = FarmService(self.store, Catalogue.bundled())
|
||||
self.rendering = None
|
||||
self.started = False
|
||||
self.log_handler = None
|
||||
|
||||
async def start(self):
|
||||
if self.started:
|
||||
return
|
||||
self.data_dir.mkdir(parents=True, exist_ok=True)
|
||||
await self.store.initialize()
|
||||
self.log_handler = logging.FileHandler(
|
||||
self.data_dir / "farm-preview.log", encoding="utf-8",
|
||||
)
|
||||
self.log_handler.setFormatter(logging.Formatter(
|
||||
"%(asctime)s %(levelname)s %(name)s %(message)s",
|
||||
))
|
||||
logging.getLogger(__package__).setLevel(logging.INFO)
|
||||
logging.getLogger(__package__).addHandler(self.log_handler)
|
||||
if self.font_path:
|
||||
try:
|
||||
self.rendering = RenderService(FarmRenderer(self.font_path))
|
||||
except (ValueError, OSError):
|
||||
logger.warning("farm preview font unavailable; text mode only")
|
||||
self.started = True
|
||||
logger.info("farm preview initialized schema=1")
|
||||
|
||||
async def close(self):
|
||||
try:
|
||||
if self.rendering:
|
||||
await self.rendering.close()
|
||||
finally:
|
||||
await self.store.close()
|
||||
self.started = False
|
||||
if self.log_handler:
|
||||
logging.getLogger(__package__).removeHandler(self.log_handler)
|
||||
self.log_handler.close()
|
||||
self.log_handler = None
|
||||
|
||||
async def execute(
|
||||
self, action, arguments, *, bot_id, platform, user_id, message_id,
|
||||
display_name="农场玩家", avatar=None,
|
||||
):
|
||||
if not self.started:
|
||||
return "农场尚未初始化,请检查启动日志。"
|
||||
try:
|
||||
who = Identity(self.deployment, str(bot_id), platform, str(user_id))
|
||||
args = shlex.split(arguments)
|
||||
if action == "farm" and args:
|
||||
aliases = {
|
||||
"detail":"inventory", "my-point":"inventory",
|
||||
"my-vipPoint":"inventory", "my-seed":"inventory",
|
||||
"my-plant":"inventory", "seed-shop":"shop",
|
||||
"buy-seed":"buy", "sowing":"sow", "harvest":"harvest",
|
||||
"eradicate":"eradicate", "lock-plant":"lock",
|
||||
"unlock-plant":"unlock", "sell-plant":"sell",
|
||||
"farm-help":"help",
|
||||
}
|
||||
if args[0] in aliases:
|
||||
action = aliases[args[0]]
|
||||
args = args[1:]
|
||||
op = operation_id(
|
||||
bot_id, platform, str(user_id), message_id, action,
|
||||
)
|
||||
if action == "help":
|
||||
return HELP
|
||||
if action == "shop":
|
||||
page = int(args[0]) if args else 1
|
||||
if not 1 <= page <= 100 or len(args) > 1:
|
||||
raise ValueError()
|
||||
plants = sorted(
|
||||
self.service.catalogue, key=lambda p: p.plant_id,
|
||||
)
|
||||
chunk = plants[(page-1)*12:page*12]
|
||||
return "种子商店 " + str(page) + "页\n" + "\n".join(
|
||||
f"{p.name}:{p.cost}{'点券' if p.is_vip else '农场币'}"
|
||||
f" / 等级{p.level}" for p in chunk
|
||||
)
|
||||
if action == "inventory":
|
||||
if args:
|
||||
raise ValueError()
|
||||
return snapshot_text(await self.service.snapshot(who))
|
||||
if action == "farm":
|
||||
if args not in ([], ["高清"]):
|
||||
raise ValueError()
|
||||
if not self.rendering:
|
||||
return "请配置 FARM_PREVIEW_FONT 为可用中文字体路径。"
|
||||
options = (RenderOptions.high(format=self.image_format)
|
||||
if args else RenderOptions(format=self.image_format))
|
||||
picture = await self.rendering.my_farm(
|
||||
self.service, who, options=options, avatar=avatar,
|
||||
supported=("JPEG",),
|
||||
)
|
||||
return picture.content
|
||||
if op is None:
|
||||
return "无法取得稳定消息标识,本次写操作未执行。"
|
||||
if action == "register":
|
||||
name = " ".join(args) if args else display_name
|
||||
name = " ".join(str(name).split())[:64] or "农场玩家"
|
||||
receipt = await self.service.register(who, name, op)
|
||||
elif action in ("buy", "sow", "sell"):
|
||||
if action == "sell" and not args:
|
||||
receipt = await self.service.sell(who, None, None, op)
|
||||
else:
|
||||
if len(args) not in (1, 2) or (action == "buy"
|
||||
and len(args) != 2):
|
||||
raise ValueError()
|
||||
count = int(args[1]) if len(args) == 2 else None
|
||||
receipt = await getattr(self.service, action)(
|
||||
who, args[0], count, op,
|
||||
)
|
||||
elif action in ("lock", "unlock"):
|
||||
if len(args) != 1:
|
||||
raise ValueError()
|
||||
receipt = await self.service.lock(
|
||||
who, args[0], action == "lock", op,
|
||||
)
|
||||
elif action in ("harvest", "eradicate"):
|
||||
if args:
|
||||
raise ValueError()
|
||||
receipt = await getattr(self.service, action)(who, op)
|
||||
else:
|
||||
return HELP
|
||||
return receipt_text(receipt)
|
||||
except FarmError as error:
|
||||
return ERRORS.get(error.code, "操作未完成:" + error.code)
|
||||
except ValueError:
|
||||
return "参数有误,请发送:农场帮助"
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema": "farm.release-targets.v1",
|
||||
"status": "design-only",
|
||||
"status": "zhenxun-preview",
|
||||
"source_repository": "https://github.com/Shu-Ying/main_plugin_farm.git",
|
||||
"network_actions_enabled": false,
|
||||
"targets": [
|
||||
@@ -9,7 +9,9 @@
|
||||
"repository": "https://github.com/Shu-Ying/zhenxun_plugin_farm.git",
|
||||
"observed_default_branch": "main",
|
||||
"entry_directory": "integrations/zhenxun",
|
||||
"enabled": false
|
||||
"enabled": true,
|
||||
"preview_branch": "farm-preview",
|
||||
"publish_by_default": false
|
||||
},
|
||||
{
|
||||
"name": "astrbot",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
name: Verify generated farm preview
|
||||
on:
|
||||
push:
|
||||
branches: [farm-preview]
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- run: python verify_package.py --directory .
|
||||
@@ -0,0 +1,116 @@
|
||||
"""从主仓库白名单构建真寻独立预览包,无远端操作。"""
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
|
||||
|
||||
from verify_zhenxun import MANIFEST, ROOT_NAME, verify_files, verify_package
|
||||
|
||||
|
||||
def source_identity(root):
|
||||
def git(*args):
|
||||
return subprocess.check_output(
|
||||
["git", *args], cwd=root, text=True, encoding="utf-8",
|
||||
).strip()
|
||||
return {
|
||||
"repository": "https://github.com/Shu-Ying/main_plugin_farm.git",
|
||||
"commit": git("rev-parse", "HEAD"),
|
||||
"dirty": bool(git("status", "--porcelain", "--untracked-files=normal")),
|
||||
}
|
||||
|
||||
|
||||
def build(root, output, version):
|
||||
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:-[a-z0-9.-]+)?", version):
|
||||
raise ValueError("Invalid preview version")
|
||||
root, output = Path(root).resolve(), Path(output).resolve()
|
||||
if output.exists():
|
||||
raise FileExistsError("Use a new output directory")
|
||||
if not output.is_relative_to(root / "dist") and output.is_relative_to(root):
|
||||
if any(output.is_relative_to(root / part) for part in (
|
||||
"src", "integrations", "scripts", "legacy", ".git",
|
||||
)):
|
||||
raise ValueError("Output must not be a source directory")
|
||||
files = {}
|
||||
integration = root / "integrations/zhenxun"
|
||||
for name in (
|
||||
"__init__.py", "runtime.py", "commands.py", "requirements.txt",
|
||||
):
|
||||
files[name] = (integration / name).read_bytes()
|
||||
files["_vendor/__init__.py"] = b'"""Private shared farm modules."""\n'
|
||||
for package in ("farm_core", "farm_render"):
|
||||
source = root / "src" / package
|
||||
selected = list(source.glob("*.py")) + [
|
||||
p for p in (source / "data").iterdir() if p.suffix in (
|
||||
".json", ".zip", ".md",
|
||||
)
|
||||
]
|
||||
for path in selected:
|
||||
if path.is_symlink():
|
||||
raise ValueError("Source links are not allowed")
|
||||
content = path.read_bytes()
|
||||
if package == "farm_render" and path.suffix == ".py":
|
||||
content = re.sub(
|
||||
rb"(?m)^from farm_core(?=[. ])", b"from ..farm_core",
|
||||
content,
|
||||
)
|
||||
relative = path.relative_to(source).as_posix()
|
||||
files[f"_vendor/{package}/{relative}"] = content
|
||||
for name, source in {
|
||||
"LICENSE": root / "LICENSE",
|
||||
"README.md": root / "docs/11-zhenxun-preview.md",
|
||||
"verify_package.py": root / "scripts/verify_zhenxun.py",
|
||||
".github/workflows/verify.yml": root / "release/zhenxun/verify.yml",
|
||||
}.items():
|
||||
files[name] = source.read_bytes()
|
||||
files["_version.py"] = f'__version__ = "{version}"\n'.encode()
|
||||
for name, content in files.items():
|
||||
if name.endswith(".py"):
|
||||
ast.parse(content, filename=name, feature_version=(3, 10))
|
||||
manifest = {
|
||||
"schema": 1, "target": "zhenxun", "plugin": ROOT_NAME,
|
||||
"version": version, "source": source_identity(root),
|
||||
"files": {name: hashlib.sha256(content).hexdigest()
|
||||
for name, content in sorted(files.items())},
|
||||
}
|
||||
files[MANIFEST] = (json.dumps(
|
||||
manifest, ensure_ascii=False, sort_keys=True, indent=2,
|
||||
) + "\n").encode()
|
||||
verify_files(files)
|
||||
output.mkdir(parents=True)
|
||||
repository = output / "repository"
|
||||
for name, content in sorted(files.items()):
|
||||
path = repository / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(content)
|
||||
archive_path = output / f"{ROOT_NAME}-{version}.zip"
|
||||
with ZipFile(archive_path, "w", compression=ZIP_DEFLATED) as archive:
|
||||
for name, content in sorted(files.items()):
|
||||
item = ZipInfo(f"{ROOT_NAME}/{name}", (2020, 1, 1, 0, 0, 0))
|
||||
item.create_system = 3
|
||||
item.compress_type = ZIP_DEFLATED
|
||||
item.external_attr = 0o100644 << 16
|
||||
archive.writestr(item, content)
|
||||
verify_package(archive_path)
|
||||
checksum = hashlib.sha256(archive_path.read_bytes()).hexdigest()
|
||||
archive_path.with_suffix(".zip.sha256").write_text(
|
||||
f"{checksum} {archive_path.name}\n", encoding="ascii",
|
||||
)
|
||||
return archive_path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--version", default="0.1.0-preview.1")
|
||||
args = parser.parse_args()
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
print(build(root, args.output, args.version))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,32 @@
|
||||
"""在显式隔离依赖路径下检查绘图及真寻测试,不修改宿主环境。"""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
def main():
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--dependency-path", type=Path, required=True)
|
||||
parser.add_argument("--expected-version", default="10.4.0")
|
||||
parser.add_argument("--pattern", action="append")
|
||||
args = parser.parse_args()
|
||||
sys.path.insert(0, str(root / "src"))
|
||||
sys.path.insert(0, str(args.dependency_path.resolve(strict=True)))
|
||||
import PIL
|
||||
if PIL.__version__ != args.expected_version:
|
||||
raise SystemExit("Unexpected Pillow version")
|
||||
print(f"Pillow compatibility check: {PIL.__version__}", flush=True)
|
||||
suite = unittest.TestSuite()
|
||||
for pattern in args.pattern or ("test_render.py", "test_zhenxun*.py"):
|
||||
suite.addTests(unittest.defaultTestLoader.discover(
|
||||
str(root / "tests"), pattern=pattern,
|
||||
))
|
||||
result = unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
return 0 if result.wasSuccessful() else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,44 @@
|
||||
"""检查两个生成工作流的YAML及发布权限;依赖仅用于开发检查。"""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--dependency-path", type=Path)
|
||||
args = parser.parse_args()
|
||||
if args.dependency_path:
|
||||
sys.path.insert(0, str(args.dependency_path.resolve(strict=True)))
|
||||
import yaml
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
source = yaml.load((
|
||||
root / ".github/workflows/zhenxun-preview.yml"
|
||||
).read_text(encoding="utf-8"), Loader=yaml.BaseLoader)
|
||||
generated = yaml.load((
|
||||
root / "release/zhenxun/verify.yml"
|
||||
).read_text(encoding="utf-8"), Loader=yaml.BaseLoader)
|
||||
if source["permissions"] != {"contents":"read"}:
|
||||
raise ValueError("Source workflow permissions differ")
|
||||
trigger = source["on"]
|
||||
if set(trigger) != {"push", "pull_request", "workflow_dispatch"}:
|
||||
raise ValueError("Unsafe or missing workflow trigger")
|
||||
inputs = trigger["workflow_dispatch"]["inputs"]
|
||||
if inputs["publish"]["default"] != "false":
|
||||
raise ValueError("Publish must be opt-in")
|
||||
sync = source["jobs"]["sync-preview"]
|
||||
if (sync["environment"] != "zhenxun-preview"
|
||||
or "refs/heads/main" not in sync["if"]
|
||||
or "workflow_dispatch" not in sync["if"]
|
||||
or sync["needs"] != ["test", "build"]):
|
||||
raise ValueError("Publish boundary differs")
|
||||
if generated["permissions"] != {"contents":"read"}:
|
||||
raise ValueError("Generated workflow permissions differ")
|
||||
if generated["on"]["push"]["branches"] != ["farm-preview"]:
|
||||
raise ValueError("Generated branch differs")
|
||||
print("Workflow YAML and publication guards checked")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,27 @@
|
||||
"""为CI选择现有字体;部署仍须显式配置中文字体。"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def choose_font():
|
||||
configured = os.environ.get("FARM_TEST_FONT")
|
||||
candidates = ([configured] if configured else []) + [
|
||||
"C:/Windows/Fonts/msyh.ttc", "C:/Windows/Fonts/simsun.ttc",
|
||||
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"C:/Windows/Fonts/arial.ttf",
|
||||
]
|
||||
for name in candidates:
|
||||
if Path(name).is_file():
|
||||
return name
|
||||
raise RuntimeError("Set FARM_TEST_FONT to an existing font")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
path = choose_font()
|
||||
environment_file = os.environ.get("GITHUB_ENV")
|
||||
if environment_file:
|
||||
with open(environment_file, "a", encoding="utf-8") as output:
|
||||
output.write(f"FARM_TEST_FONT={path}\n")
|
||||
print(path)
|
||||
@@ -45,7 +45,10 @@ def export(root):
|
||||
metadata["images"][name] = {
|
||||
"size": size, "sha256": hashlib.sha256(content).hexdigest(),
|
||||
}
|
||||
bundle.writestr(ZipInfo(name, (2020, 1, 1, 0, 0, 0)), content)
|
||||
item = ZipInfo(name, (2020, 1, 1, 0, 0, 0))
|
||||
item.create_system = 3
|
||||
item.external_attr = 0o100644 << 16
|
||||
bundle.writestr(item, content)
|
||||
return output.getvalue(), (
|
||||
json.dumps(metadata, ensure_ascii=False, sort_keys=True, indent=2)
|
||||
+ "\n"
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"""安装到显式指定的插件父目录;拒绝覆盖,不安装依赖或重启宿主。"""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from verify_zhenxun import ROOT_NAME, read_package
|
||||
|
||||
|
||||
def install(package, plugins):
|
||||
files = read_package(package)
|
||||
plugins = Path(plugins).resolve(strict=True)
|
||||
if not plugins.is_dir():
|
||||
raise ValueError("Plugin parent must be an existing directory")
|
||||
target = plugins / ROOT_NAME
|
||||
if target.exists() or target.is_symlink():
|
||||
raise FileExistsError(
|
||||
"Target already exists; backup/uninstall manually",
|
||||
)
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix=".farm-install-", dir=plugins,
|
||||
) as temporary:
|
||||
staging = Path(temporary) / ROOT_NAME
|
||||
staging.mkdir()
|
||||
for name, content in files.items():
|
||||
path = staging / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(content)
|
||||
# 独占创建避免并发覆盖;失败仅清理本次新建目标。
|
||||
target.mkdir()
|
||||
try:
|
||||
for item in staging.iterdir():
|
||||
shutil.move(str(item), str(target / item.name))
|
||||
except BaseException:
|
||||
if target.resolve().parent == plugins:
|
||||
shutil.rmtree(target)
|
||||
raise
|
||||
return target
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("package", type=Path)
|
||||
parser.add_argument("--plugins-dir", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
print(install(args.package, args.plugins_dir))
|
||||
print("Installed files only. Configure font/data, disable old plugin, "
|
||||
"then start the bot manually.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,100 @@
|
||||
"""只向固定真寻预览分支普通更新;默认离线dry-run。"""
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
from urllib.error import HTTPError
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from verify_zhenxun import read_package, verify_files
|
||||
|
||||
REPOSITORY = "Shu-Ying/zhenxun_plugin_farm"
|
||||
BRANCH = "farm-preview"
|
||||
|
||||
|
||||
def github(method, path, payload, token):
|
||||
request = Request(
|
||||
f"https://api.github.com/repos/{REPOSITORY}/{path}",
|
||||
data=json.dumps(payload).encode() if payload is not None else None,
|
||||
headers={"Authorization": f"Bearer {token}",
|
||||
"Accept": "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
"User-Agent": "farm-preview-publisher"}, method=method,
|
||||
)
|
||||
try:
|
||||
with urlopen(request, timeout=30) as response:
|
||||
return json.load(response)
|
||||
except HTTPError as error:
|
||||
if error.code == 404 and method == "GET":
|
||||
return None
|
||||
raise RuntimeError(
|
||||
f"GitHub request failed: HTTP {error.code}",
|
||||
) from None
|
||||
|
||||
|
||||
def publish(package, confirmed=False, request=None):
|
||||
files = read_package(package)
|
||||
manifest = verify_files(files)
|
||||
result = {"repository": REPOSITORY, "branch": BRANCH,
|
||||
"version": manifest["version"], "published": False}
|
||||
if not confirmed:
|
||||
return result
|
||||
if manifest["source"]["dirty"]:
|
||||
raise ValueError("Publish requires a clean source checkout")
|
||||
token = os.environ.get("FARM_RELEASE_TOKEN")
|
||||
if not token:
|
||||
raise ValueError("Configure FARM_RELEASE_TOKEN in a protected job")
|
||||
call = request or (lambda method, path, data: github(
|
||||
method, path, data, token,
|
||||
))
|
||||
repository = call("GET", "", None)
|
||||
if not repository or repository.get("default_branch") == BRANCH:
|
||||
raise ValueError("Preview must not be the repository default branch")
|
||||
ref = call("GET", f"git/ref/heads/{BRANCH}", None)
|
||||
parent = ref["object"]["sha"] if ref else None
|
||||
if parent:
|
||||
owned = call("GET", f"contents/FARM-MANIFEST.json?ref={BRANCH}", None)
|
||||
if not owned or owned.get("encoding") != "base64":
|
||||
raise ValueError("Existing preview branch is not generated by Farm")
|
||||
previous = json.loads(base64.b64decode(owned["content"]))
|
||||
if (previous.get("target") != "zhenxun"
|
||||
or previous.get("plugin") != manifest["plugin"]):
|
||||
raise ValueError("Existing preview branch ownership differs")
|
||||
tree = []
|
||||
for name, content in sorted(files.items()):
|
||||
blob = call("POST", "git/blobs", {
|
||||
"content": base64.b64encode(content).decode(), "encoding":"base64",
|
||||
})
|
||||
tree.append({"path":name, "mode":"100644", "type":"blob",
|
||||
"sha":blob["sha"]})
|
||||
generated = call("POST", "git/trees", {"tree":tree})
|
||||
commit = call("POST", "git/commits", {
|
||||
"message": f"Generate farm preview {manifest['version']} "
|
||||
f"from {manifest['source']['commit']}",
|
||||
"tree": generated["sha"], "parents": [parent] if parent else [],
|
||||
})
|
||||
if parent:
|
||||
current = call("GET", f"git/ref/heads/{BRANCH}", None)
|
||||
if not current or current["object"]["sha"] != parent:
|
||||
raise RuntimeError("Preview branch changed; retry explicitly")
|
||||
call("PATCH", f"git/refs/heads/{BRANCH}", {
|
||||
"sha": commit["sha"], "force": False,
|
||||
})
|
||||
else:
|
||||
call("POST", "git/refs", {
|
||||
"ref": f"refs/heads/{BRANCH}", "sha": commit["sha"],
|
||||
})
|
||||
return {**result, "published": True, "commit": commit["sha"]}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("package")
|
||||
parser.add_argument("--publish", action="store_true")
|
||||
args = parser.parse_args()
|
||||
print(json.dumps(publish(args.package, args.publish), ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,98 @@
|
||||
"""独立验证生成的真寻包;仅标准库,不导入或启动插件。"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path, PurePosixPath
|
||||
import stat
|
||||
from zipfile import ZipFile
|
||||
|
||||
ROOT_NAME = "zhenxun_plugin_farm_preview"
|
||||
MANIFEST = "FARM-MANIFEST.json"
|
||||
MAX_BYTES = 100 * 1024 * 1024
|
||||
|
||||
|
||||
def safe_path(name):
|
||||
path = PurePosixPath(name)
|
||||
if (not name or "\\" in name or ":" in name or path.is_absolute()
|
||||
or any(part in (".", "..", "") for part in name.split("/"))
|
||||
or path.as_posix() != name):
|
||||
raise ValueError("Unsafe package path")
|
||||
return path
|
||||
|
||||
|
||||
def verify_files(files):
|
||||
if MANIFEST not in files or len(files[MANIFEST]) > 1024 * 1024:
|
||||
raise ValueError("Missing or oversized manifest")
|
||||
manifest = json.loads(files[MANIFEST])
|
||||
if (manifest.get("schema") != 1 or manifest.get("target") != "zhenxun"
|
||||
or manifest.get("plugin") != ROOT_NAME):
|
||||
raise ValueError("Invalid package identity")
|
||||
expected = manifest["files"]
|
||||
if (not isinstance(expected, dict)
|
||||
or set(files) != set(expected) | {MANIFEST}):
|
||||
raise ValueError("Package files differ from manifest")
|
||||
for name, digest in expected.items():
|
||||
safe_path(name)
|
||||
if hashlib.sha256(files[name]).hexdigest() != digest:
|
||||
raise ValueError("Package checksum mismatch")
|
||||
required = {"__init__.py", "runtime.py", "commands.py", "requirements.txt",
|
||||
"LICENSE", "_vendor/farm_core/__init__.py",
|
||||
"_vendor/farm_render/data/assets.zip"}
|
||||
if not required <= set(files):
|
||||
raise ValueError("Incomplete plugin")
|
||||
return manifest
|
||||
|
||||
|
||||
def read_package(path):
|
||||
with ZipFile(path) as archive:
|
||||
items = archive.infolist()
|
||||
names = [item.filename for item in items]
|
||||
if (len(names) != len(set(names)) or len(names) > 200
|
||||
or sum(item.file_size for item in items) > MAX_BYTES):
|
||||
raise ValueError("Duplicate paths or package too large")
|
||||
files = {}
|
||||
for item in items:
|
||||
name = safe_path(item.filename)
|
||||
if (len(name.parts) < 2 or name.parts[0] != ROOT_NAME
|
||||
or item.is_dir()
|
||||
or stat.S_ISLNK(item.external_attr >> 16)):
|
||||
raise ValueError("Unexpected package root or link")
|
||||
files[name.relative_to(ROOT_NAME).as_posix()] = archive.read(item)
|
||||
verify_files(files)
|
||||
return files
|
||||
|
||||
|
||||
def verify_package(path):
|
||||
return verify_files(read_package(path))
|
||||
|
||||
|
||||
def verify_directory(directory):
|
||||
directory = Path(directory).resolve()
|
||||
files = {}
|
||||
for path in directory.rglob("*"):
|
||||
relative = path.relative_to(directory)
|
||||
if any(part in (".git", "__pycache__") for part in relative.parts):
|
||||
continue
|
||||
if path.is_symlink():
|
||||
raise ValueError("Unexpected symbolic link")
|
||||
if path.is_file():
|
||||
files[relative.as_posix()] = path.read_bytes()
|
||||
return verify_files(files)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("package", nargs="?", type=Path)
|
||||
parser.add_argument("--directory", type=Path)
|
||||
args = parser.parse_args()
|
||||
if bool(args.package) == bool(args.directory):
|
||||
parser.error("Choose a ZIP or --directory")
|
||||
manifest = (verify_directory(args.directory) if args.directory
|
||||
else verify_package(args.package))
|
||||
print(f"Verified zhenxun {manifest['version']}: "
|
||||
f"{len(manifest['files'])} files")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
@@ -0,0 +1,160 @@
|
||||
import importlib.util
|
||||
import base64
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from zipfile import ZipFile
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
try:
|
||||
from build_zhenxun import build
|
||||
from install_zhenxun import install
|
||||
from verify_zhenxun import verify_package
|
||||
except ImportError:
|
||||
build = None
|
||||
|
||||
|
||||
class ZhenxunPackageTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.assertIsNotNone(build, "真寻打包功能未实现")
|
||||
self.temporary = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temporary.cleanup)
|
||||
self.root = Path(self.temporary.name)
|
||||
self.output = self.root / "bundle"
|
||||
self.bundle = build(ROOT, self.output, "0.1.0-preview.1")
|
||||
|
||||
def test_reproducible_private_complete_package(self):
|
||||
other = build(ROOT, self.root / "second", "0.1.0-preview.1")
|
||||
self.assertEqual(self.bundle.read_bytes(), other.read_bytes())
|
||||
manifest = verify_package(self.bundle)
|
||||
self.assertEqual(manifest["target"], "zhenxun")
|
||||
self.assertEqual(manifest["version"], "0.1.0-preview.1")
|
||||
self.assertIn("dirty", manifest["source"])
|
||||
with ZipFile(self.bundle) as archive:
|
||||
paths = archive.namelist()
|
||||
self.assertTrue(any(p.endswith("assets.zip") for p in paths))
|
||||
self.assertFalse(any("astrbot" in p or "legacy/" in p
|
||||
for p in paths))
|
||||
source = archive.read(next(
|
||||
p for p in paths if p.endswith("farm_render/models.py")
|
||||
)).decode()
|
||||
self.assertIn("from ..farm_core.domain", source)
|
||||
self.assertNotIn("from farm_core.domain", source)
|
||||
with self.assertRaises(FileExistsError):
|
||||
build(ROOT, self.output, "0.1.0-preview.1")
|
||||
|
||||
def test_install_and_existing_target_are_safe(self):
|
||||
plugins = self.root / "plugins"
|
||||
plugins.mkdir()
|
||||
target = install(self.bundle, plugins)
|
||||
sentinel = target / "do-not-overwrite.txt"
|
||||
sentinel.write_text("preserved")
|
||||
with self.assertRaises(FileExistsError):
|
||||
install(self.bundle, plugins)
|
||||
self.assertEqual(sentinel.read_text(), "preserved")
|
||||
self.assertTrue((target / "__init__.py").is_file())
|
||||
|
||||
def test_tampering_extra_path_and_traversal_rejected(self):
|
||||
for name in ("../outside.txt", "C:/escape.txt", "extra.txt",
|
||||
"..\\outside.txt"):
|
||||
with self.subTest(name=name):
|
||||
bad = self.root / (str(len(name)) + ".zip")
|
||||
bad.write_bytes(self.bundle.read_bytes())
|
||||
with ZipFile(bad, "a") as archive:
|
||||
archive.writestr(name, "bad")
|
||||
with self.assertRaises(ValueError):
|
||||
verify_package(bad)
|
||||
bad = self.root / "corrupt.zip"
|
||||
with ZipFile(self.bundle) as original, ZipFile(bad, "w") as changed:
|
||||
for item in original.infolist():
|
||||
content = original.read(item.filename)
|
||||
if item.filename.endswith("requirements.txt"):
|
||||
content += b"bad\n"
|
||||
changed.writestr(item, content)
|
||||
with self.assertRaises(ValueError):
|
||||
install(bad, self.root)
|
||||
self.assertFalse((self.root / "zhenxun_plugin_farm_preview").exists())
|
||||
|
||||
def test_publisher_is_fixed_target_and_dry_by_default(self):
|
||||
from publish_zhenxun import publish
|
||||
report = publish(self.bundle)
|
||||
self.assertFalse(report["published"])
|
||||
self.assertEqual(report["branch"], "farm-preview")
|
||||
self.assertEqual(report["repository"], "Shu-Ying/zhenxun_plugin_farm")
|
||||
self.assertNotIn("token", json.dumps(report))
|
||||
with self.assertRaises(ValueError):
|
||||
build(ROOT, self.root / "bad", "bad; echo secret")
|
||||
|
||||
def clean_package(self):
|
||||
from verify_zhenxun import read_package, MANIFEST, ROOT_NAME
|
||||
files = read_package(self.bundle)
|
||||
manifest = json.loads(files[MANIFEST])
|
||||
manifest["source"]["dirty"] = False
|
||||
files[MANIFEST] = json.dumps(manifest).encode()
|
||||
path = self.root / "clean.zip"
|
||||
with ZipFile(path, "w") as archive:
|
||||
for name, content in files.items():
|
||||
archive.writestr(f"{ROOT_NAME}/{name}", content)
|
||||
return path, manifest
|
||||
|
||||
def test_publish_uses_nonforced_managed_branch_and_rejects_race(self):
|
||||
from publish_zhenxun import publish
|
||||
package, manifest = self.clean_package()
|
||||
for race in (False, True):
|
||||
calls = []
|
||||
reads = []
|
||||
|
||||
def request(method, path, data):
|
||||
calls.append((method, path, data))
|
||||
if path == "":
|
||||
return {"default_branch":"main"}
|
||||
if path.startswith("contents/"):
|
||||
return {"encoding":"base64", "content":base64.b64encode(
|
||||
json.dumps(manifest).encode(),
|
||||
).decode()}
|
||||
if method == "GET":
|
||||
reads.append(1)
|
||||
return {"object":{"sha":(
|
||||
"raced" if race and len(reads) > 1 else "parent"
|
||||
)}}
|
||||
return {"sha":"created"}
|
||||
|
||||
with patch.dict("os.environ", {"FARM_RELEASE_TOKEN":"unit-test"}):
|
||||
if race:
|
||||
with self.assertRaises(RuntimeError):
|
||||
publish(package, True, request)
|
||||
self.assertFalse(any(c[0] == "PATCH" for c in calls))
|
||||
else:
|
||||
self.assertTrue(publish(
|
||||
package, True, request,
|
||||
)["published"])
|
||||
self.assertIn(("PATCH", "git/refs/heads/farm-preview", {
|
||||
"sha":"created", "force":False,
|
||||
}), calls)
|
||||
|
||||
def test_symlink_duplicate_and_default_branch_rejected(self):
|
||||
from publish_zhenxun import publish
|
||||
from zipfile import ZipInfo
|
||||
package, _ = self.clean_package()
|
||||
with patch.dict("os.environ", {"FARM_RELEASE_TOKEN":"unit-test"}):
|
||||
with self.assertRaises(ValueError):
|
||||
publish(package, True, lambda *args: {
|
||||
"default_branch":"farm-preview",
|
||||
})
|
||||
for kind in ("duplicate", "link"):
|
||||
bad = self.root / (kind + ".zip")
|
||||
bad.write_bytes(self.bundle.read_bytes())
|
||||
with ZipFile(bad, "a") as archive:
|
||||
item = ZipInfo("zhenxun_plugin_farm_preview/extra")
|
||||
if kind == "duplicate":
|
||||
item.filename = archive.namelist()[0]
|
||||
else:
|
||||
item.create_system = 3
|
||||
item.external_attr = 0o120777 << 16
|
||||
archive.writestr(item, b"outside")
|
||||
with self.assertRaises(ValueError):
|
||||
verify_package(bad)
|
||||
@@ -0,0 +1,153 @@
|
||||
import asyncio
|
||||
import importlib
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
from types import ModuleType, SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
from build_zhenxun import build
|
||||
from ci_font import choose_font
|
||||
|
||||
|
||||
class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def asyncSetUp(self):
|
||||
self.temporary = tempfile.TemporaryDirectory()
|
||||
self.root = Path(self.temporary.name)
|
||||
build(ROOT, self.root / "built", "0.1.0-preview.1")
|
||||
self.package = "farm_fixture"
|
||||
module = ModuleType(self.package)
|
||||
module.__path__ = [str(self.root / "built/repository")]
|
||||
sys.modules[self.package] = module
|
||||
self.module = importlib.import_module(self.package + ".runtime")
|
||||
self.runtime = self.module.Runtime(self.root / "data", choose_font())
|
||||
await self.runtime.start()
|
||||
self.context = dict(bot_id="bot", platform="OneBot V11", user_id="1",
|
||||
message_id="registration", display_name="离线玩家")
|
||||
|
||||
async def asyncTearDown(self):
|
||||
await self.runtime.close()
|
||||
for name in tuple(sys.modules):
|
||||
if name == self.package or name.startswith(self.package + "."):
|
||||
del sys.modules[name]
|
||||
self.temporary.cleanup()
|
||||
|
||||
async def call(self, action, arguments="", message="event", **changes):
|
||||
return await self.runtime.execute(
|
||||
action, arguments,
|
||||
**{**self.context, "message_id":message, **changes},
|
||||
)
|
||||
|
||||
async def test_original_commands_transactions_and_png(self):
|
||||
self.assertNotIn("测试农场", self.module.COMMANDS)
|
||||
self.assertIn("开通农场", self.module.COMMANDS)
|
||||
self.assertIn("我的农场", self.module.COMMANDS)
|
||||
self.assertIsInstance(await self.call("register"), str)
|
||||
await self.call("buy", "白萝卜 1", "buy")
|
||||
before = await self.call("inventory")
|
||||
await self.call("buy", "白萝卜 1", "buy")
|
||||
self.assertEqual(before, await self.call("inventory"))
|
||||
await self.call("sow", "白萝卜 1", "sow")
|
||||
result = await self.call("farm", "高清")
|
||||
self.assertIsInstance(result, bytes)
|
||||
await self.call("farm", "my-seed")
|
||||
self.assertIn("成熟", await self.call("harvest", message="harvest"))
|
||||
self.assertTrue((self.root / "data/farm-preview.log").exists())
|
||||
|
||||
async def test_missing_message_id_and_other_identity(self):
|
||||
self.assertIn("未执行", await self.call("register", message=None))
|
||||
await self.call("register")
|
||||
response = await self.call("inventory", user_id="other")
|
||||
self.assertIsInstance(response, str)
|
||||
self.assertTrue("注册" in response or "开通" in response)
|
||||
self.assertNotEqual(
|
||||
self.module.operation_id("b", "p", "u", "1", "buy"),
|
||||
self.module.operation_id("b", "p", "u", "2", "buy"),
|
||||
)
|
||||
|
||||
async def test_invalid_arguments_and_missing_font_are_explicit(self):
|
||||
await self.call("register")
|
||||
for action, arguments in (("buy", "白萝卜 nope"),
|
||||
("sow", "白萝卜 -2"),
|
||||
("lock", ""), ("farm", "unknown")):
|
||||
self.assertIsInstance(await self.call(action, arguments), str)
|
||||
other = self.module.Runtime(self.root / "without-font")
|
||||
await other.start()
|
||||
try:
|
||||
response = await other.execute("farm", "", **self.context)
|
||||
self.assertIn("FARM_PREVIEW_FONT", response)
|
||||
finally:
|
||||
await other.close()
|
||||
|
||||
async def test_insufficient_funds_has_chinese_message_and_no_debit(self):
|
||||
await self.call("register")
|
||||
before = await self.call("inventory")
|
||||
self.assertEqual(
|
||||
await self.call("buy", "白萝卜 100000", "too-expensive"),
|
||||
"余额不足。",
|
||||
)
|
||||
self.assertEqual(before, await self.call("inventory"))
|
||||
|
||||
async def test_mock_host_load_registers_hooks_and_original_matchers(self):
|
||||
hooks = {"start":[], "close":[], "commands":{}}
|
||||
driver = SimpleNamespace(
|
||||
config=SimpleNamespace(farm_preview_data_dir=self.root / "host"),
|
||||
on_startup=hooks["start"].append,
|
||||
on_shutdown=hooks["close"].append,
|
||||
)
|
||||
nonebot = ModuleType("nonebot")
|
||||
nonebot.get_driver = lambda: driver
|
||||
nonebot.require = lambda _: None
|
||||
|
||||
def register(name, **kwargs):
|
||||
return SimpleNamespace(handle=lambda: (
|
||||
lambda handler: hooks["commands"].update({name:handler})
|
||||
))
|
||||
|
||||
nonebot.on_command = register
|
||||
adapters = ModuleType("nonebot.adapters")
|
||||
adapters.Bot = adapters.Event = adapters.Message = type(
|
||||
"HostType", (), {},
|
||||
)
|
||||
params = ModuleType("nonebot.params")
|
||||
params.CommandArg = lambda: None
|
||||
uninfo = ModuleType("nonebot_plugin_uninfo")
|
||||
uninfo.Uninfo = type("Uninfo", (), {})
|
||||
paths = ModuleType("zhenxun.configs.path_config")
|
||||
paths.DATA_PATH = self.root
|
||||
messages = ModuleType("zhenxun.utils.message")
|
||||
sent = []
|
||||
|
||||
async def send(**kwargs):
|
||||
return None
|
||||
|
||||
messages.MessageUtils = SimpleNamespace(build_message=lambda content: (
|
||||
sent.append(content) or SimpleNamespace(send=send)
|
||||
))
|
||||
platform = ModuleType("zhenxun.utils.platform")
|
||||
platform.PlatformUtils = SimpleNamespace()
|
||||
modules = {m.__name__:m for m in (
|
||||
nonebot, adapters, params, uninfo, paths, messages, platform,
|
||||
)}
|
||||
with patch.dict(sys.modules, modules):
|
||||
imported = importlib.import_module(self.package + ".commands")
|
||||
self.assertEqual(set(hooks["commands"]), set(self.module.COMMANDS))
|
||||
self.assertEqual(len(hooks["start"]), 1)
|
||||
await hooks["start"][0]()
|
||||
try:
|
||||
bot = SimpleNamespace(self_id="bot", adapter=SimpleNamespace(
|
||||
get_name=lambda:"OneBot V11",
|
||||
))
|
||||
await hooks["commands"]["开通农场"](
|
||||
bot, SimpleNamespace(message_id="m1"),
|
||||
SimpleNamespace(user=SimpleNamespace(id="u", name="用户")),
|
||||
SimpleNamespace(extract_plain_text=lambda:""),
|
||||
)
|
||||
self.assertTrue(sent)
|
||||
finally:
|
||||
await hooks["close"][0]()
|
||||
self.assertFalse(imported.runtime.started)
|
||||
Reference in New Issue
Block a user