From dc17f7f65cde6bb0e03c96a5a9f0e36fd9d719c6 Mon Sep 17 00:00:00 2001 From: Art_Sakura <1754798088@qq.com> Date: Fri, 23 Oct 2026 19:19:01 +0800 Subject: [PATCH] =?UTF-8?q?fixed=20#=20=E4=BF=AE=E5=A4=8D=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/astrbot-preview.yml | 2 + .github/workflows/nonebot-preview.yml | 2 + .github/workflows/zhenxun-preview.yml | 2 + docs/astrbot-store-preparation.md | 5 ++ readme/astrbot.md | 12 ++++ readme/nonebot.md | 9 +++ readme/zhenxun.md | 7 ++ src/farm_render/bundle.py | 2 +- tests/test_asset_bundle.py | 92 +++++++++++++++++++++++++++ tests/test_zhenxun_package.py | 4 +- 10 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 tests/test_asset_bundle.py diff --git a/.github/workflows/astrbot-preview.yml b/.github/workflows/astrbot-preview.yml index 6e71553..d02ec5e 100644 --- a/.github/workflows/astrbot-preview.yml +++ b/.github/workflows/astrbot-preview.yml @@ -47,6 +47,8 @@ jobs: - uses: actions/setup-python@v7 with: python-version: '3.10' + - name: Verify external asset manifest + run: python scripts/check_asset_manifest.py - run: python scripts/build_astrbot.py --output dist/astrbot-preview - name: Verify AstrBot package shell: bash diff --git a/.github/workflows/nonebot-preview.yml b/.github/workflows/nonebot-preview.yml index 2463f20..dc7f1fd 100644 --- a/.github/workflows/nonebot-preview.yml +++ b/.github/workflows/nonebot-preview.yml @@ -47,6 +47,8 @@ jobs: - uses: actions/setup-python@v7 with: python-version: '3.10' + - name: Verify external asset manifest + run: python scripts/check_asset_manifest.py - run: python scripts/build_nonebot.py --output dist/nonebot-preview - run: >- python scripts/verify_nonebot.py diff --git a/.github/workflows/zhenxun-preview.yml b/.github/workflows/zhenxun-preview.yml index 326383d..4d232f2 100644 --- a/.github/workflows/zhenxun-preview.yml +++ b/.github/workflows/zhenxun-preview.yml @@ -57,6 +57,8 @@ jobs: - uses: actions/setup-python@v7 with: python-version: '3.12' + - name: Verify external asset manifest + run: python scripts/check_asset_manifest.py - name: Build selected target only run: >- python scripts/build_zhenxun.py diff --git a/docs/astrbot-store-preparation.md b/docs/astrbot-store-preparation.md index 844f256..7251156 100644 --- a/docs/astrbot-store-preparation.md +++ b/docs/astrbot-store-preparation.md @@ -32,6 +32,11 @@ ## 待确认与已知限制 +2026-10-23后续进展:已将三端发行包的assets.zip移至farm_service仓库 +`assets/`,本地新包`dist/astrbot-external-assets-20261023/`大小4,599,804字节 +(4.39 MiB),通过`--marketplace`大小及完整性检查;以下24.08 MiB为旧产物记录。 +配置及服务说明以`farm_service/docs/assets.md`为准;尚未推送或验证远端素材URL。 + 1. **商店容量限制尚未满足**:官方普通投稿限制16MB,现有素材包 `src/farm_render/data/assets.zip`本身已21,264,561字节;完整ZIP24.08 MiB。 `--marketplace`已实际执行并拒绝。不能把本地构建成功当作市场发布就绪。 diff --git a/readme/astrbot.md b/readme/astrbot.md index 91595b9..244c964 100644 --- a/readme/astrbot.md +++ b/readme/astrbot.md @@ -40,11 +40,23 @@ | `data_dir` | 空 | 留空使用 `data/plugin_data/astrbot_plugin_farm`;更改不会自动搬迁存档 | | `image_format` | `AUTO` | `AUTO` 为 JPEG;也可选 `JPEG` 或 `PNG` | | `image_quality` | `75` | 整数1~95,下一次图片请求生效;PNG不受影响 | +| `resource_source`(资源获取地址) | `Github` | `Github`、`Gitea`、`farm_service`,重载生效 | +| `farm_service_url`(farm_service 地址) | 空 | 独立HTTPS基础地址,供资源及后续活动服务共用;选farm_service下载时必填 | 除图片质量外,配置变更后请重载插件。高清农场的图片质量不低于85。 依赖 Pillow 和 aiohttp,由插件依赖声明安装;插件不捆绑中文字体。 当前重点适配 QQ/OneBot(`aiocqhttp`),其他平台尚未完成真实环境验证。 +插件不携带大型`assets.zip`。启动时校验数据目录下缓存素材的SHA256, +缺失或不匹配时从所选源下载并校验后原子替换;GitHub/Gitea均从 +`farm_service`仓库根目录`assets/`获取。默认Github,不自动跨源切换; +下载失败保留文字功能,调整来源后重载重试。两项配置始终同时存在, +活动功能尚未实现。已有正确缓存时不联网。 + +离线部署可将服务仓库的`assets/assets.zip`放到农场数据目录 +`assets//assets.zip`;SHA见插件的`_vendor/farm_render/data/asset-manifest.json`。 +请先提交/发布服务仓库的素材,再发布依赖它的插件;不要把素材ZIP放回插件仓库。 + ## 玩法与旧账号兼容 - 新账号初始6块土地。`/开垦 条件` 查询下一块地的等级和费用, diff --git a/readme/nonebot.md b/readme/nonebot.md index 5c6a518..e51fac0 100644 --- a/readme/nonebot.md +++ b/readme/nonebot.md @@ -23,6 +23,8 @@ FARM_FONT=C:/Windows/Fonts/msyh.ttc FARM_IMAGE_FORMAT=AUTO FARM_IMAGE_QUALITY=75 FARM_DEPLOYMENT=nonebot +FARM_RESOURCE_SOURCE=Github +FARM_SERVICE_URL= ``` `FARM_DATA_DIR` 可显式指定数据目录,默认沿用旧版插件 localstore 下的 @@ -30,6 +32,13 @@ FARM_DEPLOYMENT=nonebot 机器人和平台迁入 `farm.sqlite3`;遇到账号或平台冲突会拒绝覆盖。 日志写入该目录的 `farm-preview.log`。字体不可用时提供文字响应。 +`FARM_RESOURCE_SOURCE`(资源获取地址)支持`Github`、`Gitea`、`farm_service`, +默认`Github`。`FARM_SERVICE_URL`(farm_service 地址)始终独立存在, +供资源及后续活动功能共用;选择farm_service下载时须填写HTTPS基础地址。 +两项修改后重载。插件不包含素材ZIP:每次启动校验本地SHA256,缺失或不匹配 +才从`farm_service`仓库根目录`assets/assets.zip`下载,校验成功后原子替换。 +失败时保留文字功能,不自动跨源切换,重载后重试。活动功能尚未实施。 + `FARM_IMAGE_QUALITY` 为整数1~95,默认75,统一控制全部农场图片 的编码质量;越高通常越清晰、文件越大,PNG无损格式不受此值影响。 启动时校验配置。只有“我的农场 高清”允许高清,使用1536宽,质量 diff --git a/readme/zhenxun.md b/readme/zhenxun.md index fd1af4f..a7b2637 100644 --- a/readme/zhenxun.md +++ b/readme/zhenxun.md @@ -3,6 +3,13 @@ # 真寻农场(zhenxun_plugin_farm) 真寻农场 Logo + +> 资源配置:「资源获取地址」默认Github,可选Github、Gitea、farm_service; +> 「farm_service 地址」始终独立存在,供资源及后续活动服务共用。 +> 选择farm_service下载时填写HTTPS基础地址,两项修改后重载。 +> 插件不携带assets.zip,启动校验缓存SHA256,缺失或不匹配时从所选源获取; +> Github/Gitea均使用farm_service仓库根目录assets/。失败保留文字功能,重载重试。 +> 活动服务尚未实现,请先发布服务仓库素材再发布插件。

license diff --git a/src/farm_render/bundle.py b/src/farm_render/bundle.py index 947f7a6..b3067f3 100644 --- a/src/farm_render/bundle.py +++ b/src/farm_render/bundle.py @@ -37,7 +37,7 @@ def checked_url(url): parsed = urlsplit(url) if (parsed.scheme != 'https' or not parsed.hostname or parsed.username or parsed.password or parsed.query or parsed.fragment): - raise ValueError('Resource URL requires public HTTPS without credentials') + raise ValueError('Resource URL requires HTTPS without credentials') return url diff --git a/tests/test_asset_bundle.py b/tests/test_asset_bundle.py new file mode 100644 index 0000000..cbe205e --- /dev/null +++ b/tests/test_asset_bundle.py @@ -0,0 +1,92 @@ +from concurrent.futures import ThreadPoolExecutor +from io import BytesIO +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +from farm_render import bundle + +ROOT = Path(__file__).resolve().parents[1] +ZIP = ROOT / 'src/farm_render/data/assets.zip' + + +class BundleTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.expected = bundle.expected_manifest() + + def download(self, url, destination, limit): + content = ZIP.read_bytes() + self.assertEqual(limit, len(content)) + destination.write(content) + + def test_download_deduplicates_and_cache_hash_checked_each_start(self): + with patch.object(bundle, '_copy_remote', + side_effect=self.download) as fetch: + with ThreadPoolExecutor(4) as pool: + results = list(pool.map( + lambda _: bundle.ensure_assets(self.root), range(4), + )) + self.assertEqual(len(set(results)), 1) + self.assertEqual(fetch.call_count, 1) + path = results[0] + content = bytearray(path.read_bytes()) + content[-1] ^= 1 + path.write_bytes(content) + bundle.ensure_assets(self.root) + self.assertEqual(fetch.call_count, 2) + self.assertEqual(path.read_bytes(), ZIP.read_bytes()) + + def test_gitea_and_service_use_service_repository_and_configured_base(self): + with patch.object(bundle, '_copy_remote', + side_effect=self.download) as fetch: + 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') + 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') + + def test_failed_download_leaves_no_partial_and_wrong_hash_is_rejected(self): + def corrupt(url, destination, limit): + data = bytearray(ZIP.read_bytes()) + data[-1] ^= 1 + destination.write(data) + with patch.object(bundle, '_copy_remote', side_effect=corrupt): + with self.assertRaisesRegex(ValueError, 'SHA256'): + bundle.ensure_assets(self.root) + self.assertFalse(list(self.root.rglob('assets.zip'))) + self.assertFalse(list(self.root.rglob('*.download'))) + with patch.object(bundle, '_copy_remote', side_effect=OSError()): + with self.assertRaises(OSError): + bundle.ensure_assets(self.root) + self.assertFalse(list(self.root.rglob('*.download'))) + + def test_invalid_source_and_service_configuration(self): + for source, url in [('unknown', ''), ('farm_service', ''), + ('farm_service', 'http://farm.example'), + ('Github', 'https://user:pass@farm.example'), + ('Gitea', 'https://farm.example/?token=secret')]: + with self.subTest(source=source, url=url): + with self.assertRaises(ValueError): + bundle.ensure_assets(self.root, source, url) + + def test_download_enforces_response_limit_and_https_redirect(self): + class Response(BytesIO): + headers = {'Content-Length': '20'} + def geturl(self): + return 'https://farm.example/assets.zip' + with patch.object(bundle, 'urlopen', return_value=Response(b'x'*20)): + with self.assertRaisesRegex(ValueError, 'limit'): + bundle._copy_remote('https://farm.example/a', BytesIO(), 10) + response = Response(b'') + response.geturl = lambda: 'http://farm.example/a' + with patch.object(bundle, 'urlopen', return_value=response): + with self.assertRaises(ValueError): + bundle._copy_remote('https://farm.example/a', BytesIO(), 10) diff --git a/tests/test_zhenxun_package.py b/tests/test_zhenxun_package.py index 6855dcd..ca6c499 100644 --- a/tests/test_zhenxun_package.py +++ b/tests/test_zhenxun_package.py @@ -72,7 +72,9 @@ class ZhenxunPackageTests(unittest.TestCase): 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(p.endswith("assets.zip") for p in paths)) + self.assertTrue(any(p.endswith('asset-manifest.json') + for p in paths)) self.assertFalse(any("astrbot" in p or "legacy/" in p for p in paths)) source = archive.read(next(