add # 新增使用开源字体

This commit is contained in:
2026-10-26 11:34:00 +08:00
parent dc17f7f65c
commit 22cfddcf06
68 changed files with 1772 additions and 461 deletions
+14 -2
View File
@@ -12,11 +12,16 @@ from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
from build_zhenxun import build as build_shared, read_version, source_identity
from verify_astrbot import MANIFEST, ROOT_NAME, verify_files, verify_package
from verify_zhenxun import read_package as read_shared
from help_image import build_help_image
def render_readme(root):
template = (root / 'readme/astrbot.md').read_text(encoding='utf-8')
marker = '<!-- FARM_COMMANDS -->'
if marker not in template:
if './images/help-astrbot.png' not in template:
raise ValueError('AstrBot README requires its help menu image')
return template.encode('utf-8')
if template.count(marker) != 1:
raise ValueError('AstrBot README requires one command marker')
definitions = runpy.run_path(str(root / 'src/farm_core/commands.py'))
@@ -32,7 +37,7 @@ def render_readme(root):
return template.replace(marker, '\n'.join(lines)).encode('utf-8')
def build(root, output):
def build(root, output, *, help_image=None):
root, output = Path(root).resolve(), Path(output).resolve()
version = read_version(root)
if output.exists():
@@ -53,6 +58,9 @@ def build(root, output):
files['logo.png'] = (root / 'logo-astrbot.png').read_bytes()
files['logo-readme.png'] = (root / 'logo.png').read_bytes()
files['images/farm-preview.png'] = shared['images/farm-preview.png']
files['images/help-astrbot.png'] = build_help_image(
root, 'astrbot', help_image,
)
files['README.md'] = render_readme(root)
files['runtime.py'] = (
root / 'integrations/shared/runtime.py'
@@ -99,6 +107,8 @@ def build(root, output):
item.external_attr = 0o100644 << 16
archive.writestr(item, content)
verify_package(archive_path)
if archive_path.stat().st_size > 16_000_000:
raise ValueError('AstrBot plugin ZIP exceeds 16 MB')
digest = hashlib.sha256(archive_path.read_bytes()).hexdigest()
archive_path.with_suffix('.zip.sha256').write_text(
f'{digest} {archive_path.name}\n', encoding='ascii',
@@ -109,8 +119,10 @@ def build(root, output):
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--help-image', type=Path)
args = parser.parse_args()
print(build(Path(__file__).resolve().parents[1], args.output))
print(build(Path(__file__).resolve().parents[1], args.output,
help_image=args.help_image))
if __name__ == '__main__':
+10 -3
View File
@@ -10,9 +10,10 @@ from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
from build_zhenxun import read_version, source_identity
from verify_nonebot import MANIFEST, ROOT_NAME, verify_files, verify_package
from help_image import build_help_image
def build(root, output):
def build(root, output, *, help_image=None):
root, output = Path(root).resolve(), Path(output).resolve()
version = read_version(root)
if output.exists():
@@ -20,6 +21,9 @@ def build(root, output):
if output.is_relative_to(root) and not output.is_relative_to(root / "dist"):
raise ValueError("In-project output must be inside dist")
files = {}
files['images/help-nonebot.png'] = build_help_image(
root, 'nonebot', help_image,
)
prefix = ROOT_NAME + "/"
integration = root / "integrations/nonebot"
for name in ("__init__.py", "config.py", "commands.py", "data_paths.py"):
@@ -33,7 +37,8 @@ def build(root, output):
source = root / "src" / package
selected = list(source.glob("*.py")) + [
path for path in (source / "data").iterdir()
if path.suffix in (".json", ".md")
if path.suffix in (".json", ".md", ".zip", ".txt")
and path.name != 'assets.zip'
]
for path in selected:
if path.is_symlink():
@@ -96,8 +101,10 @@ def build(root, output):
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument('--help-image', type=Path)
args = parser.parse_args()
print(build(Path(__file__).resolve().parents[1], args.output))
print(build(Path(__file__).resolve().parents[1], args.output,
help_image=args.help_image))
if __name__ == "__main__":
+8 -2
View File
@@ -41,7 +41,12 @@ foreach ($farmCandidate in $farmCandidates) {
try {
$farmExe = $farmCandidate.Exe
$farmPrefix = $farmCandidate.Prefix
& $farmExe @farmPrefix -c 'import sys; sys.exit(sys.version_info < (3, 10))'
$farmProbe = 'import sys, importlib.util; sys.exit(' +
'sys.version_info < (3, 10) or ' +
'importlib.util.find_spec("PIL") is None)'
# Windows PowerShell 5.1 rewrites quotes in native arguments.
# Use stdin instead of -c; keep this script ASCII for PS 5.1.
$farmProbe | & $farmExe @farmPrefix -
if ($LASTEXITCODE -eq 0) {
$farmPython = $farmCandidate
break
@@ -51,7 +56,8 @@ foreach ($farmCandidate in $farmCandidates) {
}
}
if (-not $farmPython) {
Write-Error 'Python 3.10+ not found. Install Python or set FARM_PYTHON.'
Write-Error ('Python 3.10+ with Pillow not found. ' +
'Install Python/Pillow or set FARM_PYTHON.')
exit 1
}
+8 -3
View File
@@ -10,6 +10,7 @@ import subprocess
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
from verify_zhenxun import MANIFEST, ROOT_NAME, verify_files, verify_package
from help_image import build_help_image
def source_identity(root):
@@ -33,7 +34,7 @@ def read_version(root):
return version
def build(root, output):
def build(root, output, *, help_image=None):
root, output = Path(root).resolve(), Path(output).resolve()
version = read_version(root)
if output.exists():
@@ -44,6 +45,9 @@ def build(root, output):
)):
raise ValueError("Output must not be a source directory")
files = {}
files['images/help-zhenxun.png'] = build_help_image(
root, 'zhenxun', help_image,
)
integration = root / "integrations/zhenxun"
for name in (
"__init__.py", "commands.py", "data_paths.py",
@@ -58,7 +62,7 @@ def build(root, output):
source = root / "src" / package
selected = list(source.glob("*.py")) + [
p for p in (source / "data").iterdir() if p.suffix in (
".json", ".zip", ".md",
".json", ".zip", ".md", ".txt",
) and p.name != 'assets.zip'
]
for path in selected:
@@ -121,9 +125,10 @@ def build(root, output):
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument('--help-image', type=Path)
args = parser.parse_args()
root = Path(__file__).resolve().parents[1]
print(build(root, args.output))
print(build(root, args.output, help_image=args.help_image))
if __name__ == "__main__":
+15
View File
@@ -64,6 +64,21 @@ def main():
for tokens in install_commands
):
raise ValueError("Full test suite requires AstrBot dependencies")
for platform, workflow in (
('zhenxun', source), ('nonebot', nonebot), ('astrbot', astrbot),
):
steps = workflow['jobs']['build']['steps']
generation = next((i for i, step in enumerate(steps)
if 'scripts/preview_help.py' in step.get('run', '')),
None)
build = next((i for i, step in enumerate(steps)
if f'scripts/build_{platform}.py'
in step.get('run', '')), None)
if generation is None or build is None or generation >= build:
raise ValueError('Help menu must be generated before packaging')
if (f'--platform {platform}' not in steps[generation]['run']
or f'help-{platform}.png' not in steps[build]['run']):
raise ValueError('Workflow help image platform mismatch')
if nonebot["permissions"] != {"contents": "read"}:
raise ValueError("NoneBot source permissions differ")
trigger = nonebot["on"]
+15 -11
View File
@@ -1,21 +1,25 @@
"""为CI选择现有字体;部署仍须显式配置中文字体。"""
"""预览与CI复用 service 字体缓存,不将大字体打入插件。"""
import os
from pathlib import Path
import sys
import tempfile
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 configured:
if not Path(configured).is_file():
raise RuntimeError("FARM_TEST_FONT does not exist")
return configured
root = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(root / "src"))
from farm_render.font_bundle import FILES, _valid, ensure_font
local = root.parent / "farm_service/fonts/Xiaolai-Regular.ttf"
if _valid(local, *FILES[local.name]):
return str(local)
cache = Path(tempfile.gettempdir()) / "farm-preview-resources"
return str(ensure_font(cache))
if __name__ == "__main__":
+16 -1
View File
@@ -170,12 +170,27 @@ def export(root):
).encode("utf-8")
def export_soils(root):
"""小型升级土地图随插件内置,不改变既有外部素材包摘要。"""
output = BytesIO()
source = root / 'legacy/zhenxun_plugin_farm/resource'
with ZipFile(output, 'w', compression=ZIP_STORED) as bundle:
for name in ('红土地', '黑土地', '金土地'):
path = f'soil/{name}.png'
item = ZipInfo(path, (2020, 1, 1, 0, 0, 0))
item.create_system = 3
item.external_attr = 0o100644 << 16
bundle.writestr(item, (source / path).read_bytes())
return output.getvalue()
def main():
root = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true")
args = parser.parse_args()
files = zip(("assets.zip", "metadata.json"), export(root))
files = (*zip(("assets.zip", "metadata.json"), export(root)),
('land-soils.zip', export_soils(root)))
destination = root / "src/farm_render/data"
for name, content in files:
path = destination / name
+30
View File
@@ -0,0 +1,30 @@
"""构建期间调用独立帮助菜单脚本;不改变开发源或源码身份。"""
from io import BytesIO
from pathlib import Path
import subprocess
import sys
import tempfile
def build_help_image(root, platform, prepared=None):
if platform not in ('zhenxun', 'nonebot', 'astrbot'):
raise ValueError('Unknown help platform')
if prepared is None:
with tempfile.TemporaryDirectory() as temporary:
subprocess.run([
sys.executable, str(Path(root) / 'scripts/preview_help.py'),
'--platform', platform, '--output', temporary,
], check=True, stdout=subprocess.PIPE, timeout=60)
content = (Path(temporary) / f'help-{platform}.png').read_bytes()
else:
path = Path(prepared)
if path.name != f'help-{platform}.png':
raise ValueError('Help image belongs to a different platform')
content = path.read_bytes()
from PIL import Image
with Image.open(BytesIO(content)) as image:
if image.format != 'PNG' or image.width != 1200:
raise ValueError('Expected generated PNG help menu at width 1200')
image.verify()
return content
+58
View File
@@ -0,0 +1,58 @@
"""从当前命令注册表离线生成帮助菜单图片,不访问玩家数据库。"""
import argparse
from pathlib import Path
import sys
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'src'))
from ci_font import choose_font
from farm_core.commands import command_specs
from farm_render import FarmRenderer
from farm_render.help import render_help
PLATFORMS = ('zhenxun', 'nonebot', 'astrbot')
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
'--platform', choices=(*PLATFORMS, 'all'), default='all',
help='目标平台;默认一次生成三个平台的菜单。',
)
parser.add_argument(
'--output', type=Path, default=ROOT / 'images',
help='图片输出目录;默认主仓库 images。',
)
parser.add_argument('--font', type=Path, help='中文字体文件路径。')
parser.add_argument(
'--format', choices=('PNG', 'JPEG', 'WEBP'), default='PNG',
type=str.upper, help='默认PNG无损图片。',
)
parser.add_argument('--quality', type=int, default=75,
help='JPEG/WebP质量,1~95;PNG不受影响。')
args = parser.parse_args(argv)
if not 1 <= args.quality <= 95:
parser.error('--quality 必须为1~95的整数')
try:
renderer = FarmRenderer(str(args.font) if args.font else choose_font())
except (OSError, ValueError, RuntimeError) as error:
parser.error(f'无法加载字体或素材,请用 --font 指定中文字体:{error}')
platforms = PLATFORMS if args.platform == 'all' else (args.platform,)
extension = {'PNG': 'png', 'JPEG': 'jpg', 'WEBP': 'webp'}[args.format]
args.output.mkdir(parents=True, exist_ok=True)
for platform in platforms:
picture = render_help(
renderer, command_specs(platform), platform,
image_format=args.format, quality=args.quality,
)
target = args.output / f'help-{platform}.{extension}'
target.write_bytes(picture)
print(f'{target.resolve()} ({len(picture)} bytes)')
if __name__ == '__main__':
main()
+3 -1
View File
@@ -43,7 +43,9 @@ def verify_files(files):
'images/farm-preview.png',
'requirements.txt', 'LICENSE', 'version.txt', '_version.py',
'_vendor/farm_core/__init__.py',
'_vendor/farm_render/data/asset-manifest.json'}
'_vendor/farm_render/data/asset-manifest.json',
'_vendor/farm_render/data/font-manifest.json',
'_vendor/farm_render/data/OFL-Xiaolai.txt'}
if any(name.endswith('/assets.zip') for name in files):
raise ValueError('Resource ZIP must not be in plugin repository')
if not required <= set(files):
+2
View File
@@ -42,6 +42,8 @@ def verify_files(files):
"nonebot_plugin_farm/commands.py", "nonebot_plugin_farm/runtime.py",
"nonebot_plugin_farm/_vendor/farm_core/__init__.py",
"nonebot_plugin_farm/_vendor/farm_render/data/asset-manifest.json",
"nonebot_plugin_farm/_vendor/farm_render/data/font-manifest.json",
"nonebot_plugin_farm/_vendor/farm_render/data/OFL-Xiaolai.txt",
".github/workflows/publish.yaml", ".github/workflows/verify.yml",
}
if not required <= set(files):
+3 -1
View File
@@ -38,7 +38,9 @@ def verify_files(files):
raise ValueError("Package checksum mismatch")
required = {"__init__.py", "runtime.py", "commands.py", "requirements.txt",
"LICENSE", "_vendor/farm_core/__init__.py",
"_vendor/farm_render/data/asset-manifest.json"}
"_vendor/farm_render/data/asset-manifest.json",
"_vendor/farm_render/data/font-manifest.json",
"_vendor/farm_render/data/OFL-Xiaolai.txt"}
if any(name.endswith('/assets.zip') for name in files):
raise ValueError('Resource ZIP must not be in plugin repository')
if not required <= set(files):