fixed # 修改进度条表现

add # 新增Ruff代码格式工作流
This commit is contained in:
2026-10-27 23:02:43 +08:00
parent c6b504924e
commit b09d51c7c9
20 changed files with 486 additions and 136 deletions
+144 -95
View File
@@ -5,157 +5,206 @@ import ast
import hashlib
import json
import runpy
from pathlib import Path
import tempfile
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
from build_zhenxun import build as build_shared, read_version, source_identity
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
from help_image import build_help_image
def host_logging(content):
"""仅替换日志入口,保留共享业务源码和注释;未知用法交给包校验拒绝。"""
lines = content.decode('utf-8').splitlines(keepends=True)
lines = content.decode("utf-8").splitlines(keepends=True)
edits = []
for node in ast.parse(content).body:
if (isinstance(node, ast.Import)
and any(alias.name == 'logging' for alias in node.names)):
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:
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'):
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
statement = 'from astrbot.api import logger'
if alias != 'logger':
statement += ' as ' + alias
edits.append((node.lineno, node.end_lineno, statement + '\n'))
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):
lines[start-1:end] = [replacement]
return ''.join(lines).encode('utf-8')
lines[start - 1 : end] = [replacement]
return "".join(lines).encode("utf-8")
def render_readme(root):
template = (root / 'readme/astrbot.md').read_text(encoding='utf-8')
marker = '<!-- FARM_COMMANDS -->'
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 "./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'))
lines = ['| 分类 | 指令 | 说明 |', '| --- | --- | --- |']
specs = definitions['command_specs']('astrbot')
for group, rows in definitions['help_groups'](specs):
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):
for usage, description in rows:
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')
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")
def build(root, output, *, help_image=None):
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')
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',
) if not name.endswith('/__main__.py')}
source = root / 'integrations/astrbot'
for name in ('main.py', 'avatars.py', '_conf_schema.json',
'requirements.txt', 'CHANGELOG.md'):
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()
# AstrBot识别固定文件名logo.png;README单独使用高清原图。
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["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'
).read_bytes()
files['runtime_logging.py'] = (
source / 'runtime_logging.py'
).read_bytes()
files['verify_package.py'] = (
root / 'scripts/verify_astrbot.py'
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"
).read_bytes()
# JSON是YAML的子集;标准库即可生成AstrBot可读的metadata.yaml。
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()
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():
if name.endswith('.py'):
content = host_logging(content)
files[name] = content
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 = {
'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())},
"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()
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 = 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:
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 = 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)
if archive_path.stat().st_size > 16_000_000:
raise ValueError('AstrBot plugin ZIP exceeds 16 MB')
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',
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)
parser.add_argument('--help-image', type=Path)
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,
help_image=args.help_image))
print(
build(
Path(__file__).resolve().parents[1],
args.output,
help_image=args.help_image,
)
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+2
View File
@@ -11,6 +11,7 @@ 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
from format_generated import format_generated
def build(root, output, *, help_image=None):
@@ -67,6 +68,7 @@ def build(root, output, *, help_image=None):
}.items():
files[name] = (root / source).read_bytes()
files[prefix + "_version.py"] = f'__version__ = "{version}"\n'.encode()
format_generated(root, files)
for name, content in files.items():
if name.endswith(".py"):
ast.parse(content, filename=name, feature_version=(3, 10))
+5 -3
View File
@@ -43,7 +43,8 @@ foreach ($farmCandidate in $farmCandidates) {
$farmPrefix = $farmCandidate.Prefix
$farmProbe = 'import sys, importlib.util; sys.exit(' +
'sys.version_info < (3, 10) or ' +
'importlib.util.find_spec("PIL") is None)'
'importlib.util.find_spec("PIL") is None or ' +
'importlib.util.find_spec("ruff") 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 -
@@ -56,8 +57,9 @@ foreach ($farmCandidate in $farmCandidates) {
}
}
if (-not $farmPython) {
Write-Error ('Python 3.10+ with Pillow not found. ' +
'Install Python/Pillow or set FARM_PYTHON.')
Write-Error ('Python 3.10+ with Pillow and Ruff not found. ' +
'Install Pillow and requirements-build.txt in the same Python, ' +
'or set FARM_PYTHON.')
exit 1
}
+2
View File
@@ -11,6 +11,7 @@ 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
from format_generated import format_generated
def source_identity(root):
@@ -97,6 +98,7 @@ def build(root, output, *, help_image=None):
}.items():
files[name] = source.read_bytes()
files["_version.py"] = f'__version__ = "{version}"\n'.encode()
format_generated(root, files)
for name, content in files.items():
if name.endswith(".py"):
ast.parse(content, filename=name, feature_version=(3, 10))
+70
View File
@@ -0,0 +1,70 @@
"""在生成清单前统一格式化内存中的发布代码,不修改开发源文件。"""
import importlib.util
import subprocess
import sys
import tempfile
from pathlib import Path, PurePosixPath
def format_generated(root, files):
"""仅整理导入和代码格式;成功后一次性更新文件集合。"""
root = Path(root).resolve()
if importlib.util.find_spec("ruff") is None:
raise RuntimeError(
"Ruff is required to build plugins. Install with: "
"python -m pip install -r requirements-build.txt"
)
selected = {}
for name, content in files.items():
if not name.endswith(".py"):
continue
path = PurePosixPath(name)
if (
path.is_absolute()
or "\\" in name
or ":" in name
or any(part in ("", ".", "..") for part in name.split("/"))
):
raise ValueError("Unsafe generated Python path")
selected[name] = content
if not selected:
return
with tempfile.TemporaryDirectory(prefix="farm-ruff-") as temporary:
staging = Path(temporary)
for name, content in selected.items():
target = staging / name
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(content)
common = ["--config", str(root / "pyproject.toml"), "--no-cache"]
for arguments in (
["check", "--select", "I", "--fix"],
["format"],
["check", "--select", "I"],
["format", "--check"],
):
result = subprocess.run(
[
sys.executable,
"-m",
"ruff",
*arguments,
*common,
str(staging),
],
cwd=root,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=60,
)
if result.returncode:
raise RuntimeError(
"Generated Python failed Ruff; install the pinned "
"requirements-build.txt dependencies.\n"
+ result.stdout
+ result.stderr
)
formatted = {name: (staging / name).read_bytes() for name in selected}
files.update(formatted)