Files
main_plugin_farm/scripts/build_astrbot.py
T

211 lines
7.8 KiB
Python
Raw Normal View History

"""白名单生成AstrBot可安装包,复用已校验的共享模块打包路径。"""
import argparse
import ast
import hashlib
import json
2026-10-23 19:12:16 +08:00
import runpy
import tempfile
2026-10-27 23:02:43 +08:00
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
2026-10-27 23:02:43 +08:00
from build_zhenxun import build as build_shared
from build_zhenxun import read_version, source_identity
from help_image import build_help_image
from format_generated import format_generated
from verify_astrbot import MANIFEST, ROOT_NAME, verify_files, verify_package
from verify_zhenxun import read_package as read_shared
def host_logging(content):
"""仅替换日志入口,保留共享业务源码和注释;未知用法交给包校验拒绝。"""
2026-10-27 23:02:43 +08:00
lines = content.decode("utf-8").splitlines(keepends=True)
edits = []
for node in ast.parse(content).body:
2026-10-27 23:02:43 +08:00
if isinstance(node, ast.Import) and any(
alias.name == "logging" for alias in node.names
):
if len(node.names) != 1 or node.names[0].asname:
2026-10-27 23:02:43 +08:00
raise ValueError("Unsupported logging import")
edits.append((node.lineno, node.end_lineno, ""))
if (
isinstance(node, ast.Assign)
and len(node.targets) == 1
and isinstance(node.targets[0], ast.Name)
and isinstance(node.value, ast.Call)
and isinstance(node.value.func, ast.Attribute)
and isinstance(node.value.func.value, ast.Name)
and node.value.func.value.id == "logging"
and node.value.func.attr == "getLogger"
):
alias = node.targets[0].id
2026-10-27 23:02:43 +08:00
statement = "from astrbot.api import logger"
if alias != "logger":
statement += " as " + alias
edits.append((node.lineno, node.end_lineno, statement + "\n"))
for start, end, replacement in sorted(edits, reverse=True):
2026-10-27 23:02:43 +08:00
lines[start - 1 : end] = [replacement]
return "".join(lines).encode("utf-8")
2026-10-23 19:12:16 +08:00
def render_readme(root):
2026-10-27 23:02:43 +08:00
template = (root / "readme/astrbot.md").read_text(encoding="utf-8")
marker = "<!-- FARM_COMMANDS -->"
2026-10-26 11:34:00 +08:00
if marker not in template:
2026-10-27 23:02:43 +08:00
if "./images/help-astrbot.png" not in template:
raise ValueError("AstrBot README requires its help menu image")
return template.encode("utf-8")
2026-10-23 19:12:16 +08:00
if template.count(marker) != 1:
2026-10-27 23:02:43 +08:00
raise ValueError("AstrBot README requires one command marker")
definitions = runpy.run_path(str(root / "src/farm_core/commands.py"))
lines = ["| 分类 | 指令 | 说明 |", "| --- | --- | --- |"]
specs = definitions["command_specs"]("astrbot")
for group, rows in definitions["help_groups"](specs):
2026-10-23 19:12:16 +08:00
for usage, description in rows:
2026-10-27 23:02:43 +08:00
values = (group, f"`/{usage}`", description)
lines.append(
"| "
+ " | ".join(
value.replace("|", r"\|").replace("\n", " ")
for value in values
)
+ " |"
)
return template.replace(marker, "\n".join(lines)).encode("utf-8")
2026-10-23 19:12:16 +08:00
2026-10-26 11:34:00 +08:00
def build(root, output, *, help_image=None):
root, output = Path(root).resolve(), Path(output).resolve()
version = read_version(root)
if output.exists():
2026-10-27 23:02:43 +08:00
raise FileExistsError("Use a new output directory")
if output.is_relative_to(root) and not output.is_relative_to(root / "dist"):
raise ValueError("In-project output must be inside dist")
with tempfile.TemporaryDirectory() as temporary:
2026-10-27 23:02:43 +08:00
shared = read_shared(build_shared(root, Path(temporary) / "shared"))
files = {
name: content
for name, content in shared.items()
if name.startswith("_vendor/")
or name
in (
"_version.py",
"version.txt",
"LICENSE",
)
if not name.endswith("/__main__.py")
}
source = root / "integrations/astrbot"
for name in (
"main.py",
"avatars.py",
"_conf_schema.json",
"requirements.txt",
"CHANGELOG.md",
):
files[name] = (source / name).read_bytes()
2026-10-23 19:12:16 +08:00
# AstrBot识别固定文件名logo.png;README单独使用高清原图。
2026-10-27 23:02:43 +08:00
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,
2026-10-26 11:34:00 +08:00
)
2026-10-27 23:02:43 +08:00
files["README.md"] = render_readme(root)
files["runtime.py"] = (root / "integrations/shared/runtime.py").read_bytes()
files["runtime_logging.py"] = (source / "runtime_logging.py").read_bytes()
files["verify_package.py"] = (
root / "scripts/verify_astrbot.py"
2026-10-23 00:41:51 +08:00
).read_bytes()
# JSON是YAML的子集;标准库即可生成AstrBot可读的metadata.yaml。
2026-10-27 23:02:43 +08:00
files["metadata.yaml"] = (
json.dumps(
{
"name": ROOT_NAME,
"display_name": "真寻农场",
"short_desc": "开通农场,体验种植、收获、偷菜与土地开垦。",
"desc": "农场种植小游戏,支持种子商店、分类仓库、作物交易、"
"除虫、偷菜、土地开垦和动态图片帮助;使用本地存档。",
"version": version,
"author": "Shu-Ying",
"repo": "https://github.com/Shu-Ying/astrbot_plugin_farm",
"social_link": "https://github.com/Shu-Ying",
"tags": ["农场", "种植", "小游戏"],
"support_platforms": ["aiocqhttp"],
},
ensure_ascii=False,
indent=2,
)
+ "\n"
).encode()
for name, content in files.items():
2026-10-27 23:02:43 +08:00
if name.endswith(".py"):
files[name] = host_logging(content)
format_generated(root, files)
for name, content in files.items():
if name.endswith(".py"):
ast.parse(content, filename=name, feature_version=(3, 10))
manifest = {
2026-10-27 23:02:43 +08:00
"schema": 1,
"target": "astrbot",
"plugin": ROOT_NAME,
"version": version,
"source": source_identity(root),
"files": {
name: hashlib.sha256(content).hexdigest()
for name, content in sorted(files.items())
},
}
2026-10-27 23:02:43 +08:00
files[MANIFEST] = (
json.dumps(
manifest,
ensure_ascii=False,
sort_keys=True,
indent=2,
)
+ "\n"
).encode()
verify_files(files)
output.mkdir(parents=True)
for name, content in sorted(files.items()):
2026-10-27 23:02:43 +08:00
target = output / "repository" / name
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(content)
2026-10-27 23:02:43 +08:00
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()):
2026-10-27 23:02:43 +08:00
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)
2026-10-26 11:34:00 +08:00
if archive_path.stat().st_size > 16_000_000:
2026-10-27 23:02:43 +08:00
raise ValueError("AstrBot plugin ZIP exceeds 16 MB")
digest = hashlib.sha256(archive_path.read_bytes()).hexdigest()
2026-10-27 23:02:43 +08:00
archive_path.with_suffix(".zip.sha256").write_text(
f"{digest} {archive_path.name}\n",
encoding="ascii",
)
return archive_path
def main():
parser = argparse.ArgumentParser(description=__doc__)
2026-10-27 23:02:43 +08:00
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--help-image", type=Path)
args = parser.parse_args()
2026-10-27 23:02:43 +08:00
print(
build(
Path(__file__).resolve().parents[1],
args.output,
help_image=args.help_image,
)
)
2026-10-27 23:02:43 +08:00
if __name__ == "__main__":
main()