2026-10-22 18:33:42 +08:00
|
|
|
|
"""白名单生成AstrBot可安装包,复用已校验的共享模块打包路径。"""
|
|
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
|
import ast
|
|
|
|
|
|
import hashlib
|
|
|
|
|
|
import json
|
2026-10-23 19:12:16 +08:00
|
|
|
|
import runpy
|
2026-10-22 18:33:42 +08:00
|
|
|
|
import tempfile
|
2026-10-27 23:02:43 +08:00
|
|
|
|
from pathlib import Path
|
2026-10-22 18:33:42 +08:00
|
|
|
|
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
|
2026-10-22 18:33:42 +08:00
|
|
|
|
from verify_astrbot import MANIFEST, ROOT_NAME, verify_files, verify_package
|
|
|
|
|
|
from verify_zhenxun import read_package as read_shared
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-27 11:40:59 +08:00
|
|
|
|
def host_logging(content):
|
|
|
|
|
|
"""仅替换日志入口,保留共享业务源码和注释;未知用法交给包校验拒绝。"""
|
2026-10-27 23:02:43 +08:00
|
|
|
|
lines = content.decode("utf-8").splitlines(keepends=True)
|
2026-10-27 11:40:59 +08:00
|
|
|
|
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
|
|
|
|
|
|
):
|
2026-10-27 11:40:59 +08:00
|
|
|
|
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"
|
|
|
|
|
|
):
|
2026-10-27 11:40:59 +08:00
|
|
|
|
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"))
|
2026-10-27 11:40:59 +08:00
|
|
|
|
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-27 11:40:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
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):
|
2026-10-22 18:33:42 +08:00
|
|
|
|
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")
|
2026-10-22 18:33:42 +08:00
|
|
|
|
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",
|
|
|
|
|
|
):
|
2026-10-22 18:33:42 +08:00
|
|
|
|
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()
|
2026-10-22 18:33:42 +08:00
|
|
|
|
# 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()
|
2026-10-22 18:33:42 +08:00
|
|
|
|
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"):
|
2026-10-22 18:33:42 +08:00
|
|
|
|
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-22 18:33:42 +08:00
|
|
|
|
}
|
2026-10-27 23:02:43 +08:00
|
|
|
|
files[MANIFEST] = (
|
|
|
|
|
|
json.dumps(
|
|
|
|
|
|
manifest,
|
|
|
|
|
|
ensure_ascii=False,
|
|
|
|
|
|
sort_keys=True,
|
|
|
|
|
|
indent=2,
|
|
|
|
|
|
)
|
|
|
|
|
|
+ "\n"
|
|
|
|
|
|
).encode()
|
2026-10-22 18:33:42 +08:00
|
|
|
|
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
|
2026-10-22 18:33:42 +08:00
|
|
|
|
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:
|
2026-10-22 18:33:42 +08:00
|
|
|
|
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))
|
2026-10-22 18:33:42 +08:00
|
|
|
|
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")
|
2026-10-22 18:33:42 +08:00
|
|
|
|
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",
|
2026-10-22 18:33:42 +08:00
|
|
|
|
)
|
|
|
|
|
|
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)
|
2026-10-22 18:33:42 +08:00
|
|
|
|
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-22 18:33:42 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-10-27 23:02:43 +08:00
|
|
|
|
if __name__ == "__main__":
|
2026-10-22 18:33:42 +08:00
|
|
|
|
main()
|