fixed # 新增本地部署脚本,一键生成对应平台的压缩包内容

This commit is contained in:
2026-10-22 18:33:42 +08:00
parent 37c933db0c
commit d238005561
41 changed files with 1158 additions and 545 deletions
+84
View File
@@ -0,0 +1,84 @@
"""白名单生成AstrBot可安装包,复用已校验的共享模块打包路径。"""
import argparse
import ast
import hashlib
import json
from pathlib import Path
import tempfile
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
def build(root, output):
root, output = Path(root).resolve(), Path(output).resolve()
version = read_version(root)
if output.exists():
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:
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',
)}
source = root / 'integrations/astrbot'
for name in ('main.py', '_conf_schema.json', 'requirements.txt'):
files[name] = (source / name).read_bytes()
files['README.md'] = (source / 'README.md').read_bytes()
files['runtime.py'] = (root / 'integrations/zhenxun/runtime.py').read_bytes()
files['verify_package.py'] = (root / 'scripts/verify_astrbot.py').read_bytes()
# JSON是YAML的子集;标准库即可生成AstrBot可读的metadata.yaml。
files['metadata.yaml'] = (json.dumps({
'name': ROOT_NAME, 'display_name': '农场',
'desc': '种植、收获、仓库、交易与农场图片,复用Farm共享核心。',
'version': version, 'author': 'Shu-Ying',
'repo': 'https://github.com/Shu-Ying/astrbot_plugin_farm',
}, ensure_ascii=False, indent=2) + '\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': 'astrbot', '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)
for name, content in sorted(files.items()):
target = output / 'repository' / name
target.parent.mkdir(parents=True, exist_ok=True)
target.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)
digest = hashlib.sha256(archive_path.read_bytes()).hexdigest()
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__)
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
print(build(Path(__file__).resolve().parents[1], args.output))
if __name__ == '__main__':
main()
+71
View File
@@ -0,0 +1,71 @@
param(
[Parameter(Mandatory = $true)]
[ValidateSet('nonebot', 'zhenxun', 'astrbot')]
[string]$Platform,
[string]$OutputDirectory
)
$ErrorActionPreference = 'Stop'
$farmProjectRoot = Split-Path -Parent $PSScriptRoot
$farmCandidates = @()
if ($env:FARM_PYTHON) {
$farmCandidates += @{ Exe = $env:FARM_PYTHON; Prefix = @() }
} else {
foreach ($farmLocal in @('.venv\Scripts\python.exe',
'venv\Scripts\python.exe')) {
$farmCandidate = Join-Path $farmProjectRoot $farmLocal
if (Test-Path -LiteralPath $farmCandidate -PathType Leaf) {
$farmCandidates += @{ Exe = $farmCandidate; Prefix = @() }
}
}
foreach ($farmName in @('python', 'py')) {
$farmCommand = Get-Command $farmName -ErrorAction SilentlyContinue
if ($farmCommand) {
$farmPrefix = @()
if ($farmName -eq 'py') { $farmPrefix = @('-3') }
$farmCandidates += @{ Exe = $farmCommand.Source;
Prefix = $farmPrefix }
}
}
if ($env:USERPROFILE) {
$farmBundled = Join-Path $env:USERPROFILE ('.cache\codex-runtimes\' +
'codex-primary-runtime\dependencies\python\python.exe')
if (Test-Path -LiteralPath $farmBundled -PathType Leaf) {
$farmCandidates += @{ Exe = $farmBundled; Prefix = @() }
}
}
}
$farmPython = $null
foreach ($farmCandidate in $farmCandidates) {
try {
$farmExe = $farmCandidate.Exe
$farmPrefix = $farmCandidate.Prefix
& $farmExe @farmPrefix -c 'import sys; sys.exit(sys.version_info < (3, 10))'
if ($LASTEXITCODE -eq 0) {
$farmPython = $farmCandidate
break
}
} catch {
Write-Host ('Python candidate unavailable: ' + $farmCandidate.Exe)
}
}
if (-not $farmPython) {
Write-Error 'Python 3.10+ not found. Install Python or set FARM_PYTHON.'
exit 1
}
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
$farmGit = Join-Path $env:USERPROFILE ('.cache\codex-runtimes\' +
'codex-primary-runtime\dependencies\native\git\cmd')
if (Test-Path -LiteralPath (Join-Path $farmGit 'git.exe')) {
$env:PATH = $farmGit + ';' + $env:PATH
}
}
$farmExe = $farmPython.Exe
$farmArguments = @($farmPython.Prefix) + @(
(Join-Path $PSScriptRoot 'build_plugin.py'), $Platform
)
if ($OutputDirectory) { $farmArguments += @('--output', $OutputDirectory) }
& $farmExe @farmArguments
exit $LASTEXITCODE
+36
View File
@@ -0,0 +1,36 @@
"""三平台本地打包入口;每次使用独立新目录,不执行发布或安装。"""
import argparse
from datetime import datetime, timezone
import importlib
from pathlib import Path
import uuid
PLATFORMS = ('nonebot', 'zhenxun', 'astrbot')
def build_plugin(platform, output=None, *, root=None):
if platform not in PLATFORMS:
raise ValueError('Unsupported platform')
source = Path(__file__).resolve().parents[1]
if output is None:
parent = Path(root) if root else source / 'dist'
timestamp = datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')
output = parent / f'{platform}-{timestamp}-{uuid.uuid4().hex[:8]}'
module = importlib.import_module('build_' + platform)
return module.build(source, Path(output).resolve())
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('platform', choices=PLATFORMS)
parser.add_argument('--output', type=Path)
args = parser.parse_args()
archive = build_plugin(args.platform, args.output)
print(f'Package: {archive}')
print(f'Checksum: {archive.with_suffix(".zip.sha256")}')
if __name__ == '__main__':
main()
+3 -1
View File
@@ -17,9 +17,11 @@ def source_identity(root):
return subprocess.check_output(
["git", *args], cwd=root, text=True, encoding="utf-8",
).strip()
commit = git("rev-parse", "HEAD")
return {
"repository": "https://github.com/Shu-Ying/main_plugin_farm.git",
"commit": git("rev-parse", "HEAD"),
"commit": commit,
"message": git("show", "-s", "--format=%B", commit),
"dirty": bool(git("status", "--porcelain", "--untracked-files=normal")),
}
+5 -3
View File
@@ -8,6 +8,7 @@ from urllib.error import HTTPError
from urllib.request import Request, urlopen
from verify_nonebot import read_package, verify_files
from release_metadata import commit_message
REPOSITORY = "Shu-Ying/nonebot_plugin_farm"
BRANCH = "main"
@@ -39,8 +40,10 @@ def github(method, path, payload, token, allow_missing=False):
def publish(package, confirmed=False, request=None):
files = read_package(package)
manifest = verify_files(files)
message = commit_message(manifest)
result = {"repository": REPOSITORY, "branch": BRANCH,
"version": manifest["version"], "published": False}
"version": manifest["version"], "message": message,
"published": False}
if not confirmed:
return result
if manifest["source"]["dirty"]:
@@ -75,8 +78,7 @@ def publish(package, confirmed=False, request=None):
"sha": blob["sha"]})
generated = call("POST", "git/trees", {"tree": tree})
commit = call("POST", "git/commits", {
"message": f"Generate NoneBot farm {manifest['version']} "
f"from {manifest['source']['commit']}",
"message": message,
"tree": generated["sha"], "parents": [parent] if parent else [],
})
if parent:
+5 -3
View File
@@ -8,6 +8,7 @@ from urllib.error import HTTPError
from urllib.request import Request, urlopen
from verify_zhenxun import read_package, verify_files
from release_metadata import commit_message
REPOSITORY = "Shu-Ying/zhenxun_plugin_farm"
BRANCH = "main"
@@ -39,8 +40,10 @@ def github(method, path, payload, token, allow_missing=False):
def publish(package, confirmed=False, request=None):
files = read_package(package)
manifest = verify_files(files)
message = commit_message(manifest)
result = {"repository": REPOSITORY, "branch": BRANCH,
"version": manifest["version"], "published": False}
"version": manifest["version"], "message": message,
"published": False}
if not confirmed:
return result
if manifest["source"]["dirty"]:
@@ -75,8 +78,7 @@ def publish(package, confirmed=False, request=None):
"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']}",
"message": message,
"tree": generated["sha"], "parents": [parent] if parent else [],
})
if parent:
+13
View File
@@ -0,0 +1,13 @@
"""从构建清单生成发布提交说明,不查询发布时的工作目录。"""
def commit_message(manifest):
source = manifest["source"]
message = source.get("message")
if not isinstance(message, str) or not message.strip():
raise ValueError("Missing source commit message; rebuild the package")
return (
f"{message.rstrip()}\n\n"
f"Farm-Source-Commit: {source['commit']}\n"
f"Farm-Version: {manifest['version']}"
)
+85
View File
@@ -0,0 +1,85 @@
"""AstrBot插件ZIP的独立完整性校验;只使用标准库。"""
import argparse
import hashlib
import json
from pathlib import Path, PurePosixPath
import stat
from zipfile import ZipFile
ROOT_NAME = 'astrbot_plugin_farm'
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(p in ('', '.', '..') for p in name.split('/'))):
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') != 'astrbot'
or manifest.get('plugin') != ROOT_NAME):
raise ValueError('Invalid package identity')
expected = manifest.get('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 = {'main.py', 'metadata.yaml', '_conf_schema.json', 'runtime.py',
'requirements.txt', 'LICENSE', 'version.txt', '_version.py',
'_vendor/farm_core/__init__.py',
'_vendor/farm_render/data/assets.zip'}
if not required <= set(files):
raise ValueError('Incomplete plugin')
metadata = json.loads(files['metadata.yaml'])
if (metadata.get('name') != ROOT_NAME
or metadata.get('version') != manifest['version']
or files['version.txt'].decode().strip() != manifest['version']):
raise ValueError('Package version mismatch')
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 main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('package', type=Path)
args = parser.parse_args()
manifest = verify_package(args.package)
print(f"Verified astrbot {manifest['version']}: "
f"{len(manifest['files'])} files")
if __name__ == '__main__':
main()