muse-agent-example/工具/构建发布.py
zizi 9e6f1c4481 R2 改造交付:新版模块化单体全量成果
- src/muse 新版全模块(装配/共享/上下文/任务运行/作品规划/故事世界/正文写作/审校修订/知识方法/作者经验/效果评测/交付连载/资料研究/正式变更/元数据/接入/基础设施/编排)+ 测试树(单元/契约/集成/架构/迁移/端到端/夹具)
- 129 项功能全部实现与自动验证(功能覆盖.json/矩阵),含 W31 补齐的规则与代价/节奏安排/伏笔与承诺
- 旧实现按处置清单退出(702 条中 324 删,保护合同与未迁移条目留存有据);web/app.py 旧工作台退役,新工作台为唯一写入口
- 数据库/旧库迁移:真实旧库内容批次迁移链(端点守卫/PG作品正文映射/质量资产缺省投影)
- 运行手册 docs/运行手册.md;W30 本机服务阶段一已运行(infra PG 为正式内容权威)
- R2 执行证据与私有运行材料在 .agents.local/改造/R2-20260909/(不入库)
2026-09-15 12:47:42 +08:00

135 lines
5.0 KiB
Python

"""生成可审发布目录:固定轮子、带哈希依赖、工作台和部署清单;不发布或迁移。"""
import argparse
import hashlib
import json
import shutil
import subprocess
import tempfile
from pathlib import Path
from zipfile import ZipFile
根 = Path(__file__).resolve().parents[1]
def 运行(args):
subprocess.run(args, cwd=根, check=True)
def 构建发布(output: Path, *, 生成=True):
output = output.resolve()
if output.exists():
raise ValueError("发布目录已存在;使用新目录保留已有版本")
output.parent.mkdir(parents=True, exist_ok=True)
if 生成:
运行(["uv", "run", "python", "工具/生成接口.py"])
运行(
[
"pnpm",
"--dir",
"web",
"exec",
"openapi-typescript",
"../docs/接口契约/生成/openapi.generated.json",
"-o",
"src/接口/生成/类型.ts",
]
)
运行(["pnpm", "--dir", "web", "run", "build"])
运行(["uv", "run", "python", "工具/构建资源包.py"])
运行(["uv", "run", "python", "工具/维护索引.py", "--检查"])
运行(
[
"uv",
"run",
"python",
"-c",
"from muse.资源加载 import 核对资源; "
"from muse.基础设施.系统运行 import 运行代码哈希; "
"assert 核对资源()['代码哈希'] == 运行代码哈希(), '资源与当前代码不匹配,先重新构建'",
]
)
with tempfile.TemporaryDirectory(prefix=".muse-release-", dir=output.parent) as temporary:
stage = Path(temporary)
运行(["uv", "build", "--wheel", "--out-dir", str(stage / "wheels")])
运行(
[
"uv",
"export",
"--frozen",
"--no-dev",
"--no-emit-project",
"--no-header",
"--output-file",
str(stage / "requirements.txt"),
]
)
wheels = list((stage / "wheels").glob("*.whl"))
if len(wheels) != 1:
raise ValueError("发布目录必须对应一个明确轮子")
with ZipFile(wheels[0]) as archive:
names = archive.namelist()
if len(names) != len(set(names)) or any(
part in {".git", "tests", ".agents.local", ".env"}
for name in names
for part in Path(name).parts
):
raise ValueError("轮子含重复文件或发布范围之外的内容")
manifest = json.loads(archive.read("muse/资源/清单.json"))
for name in [
"Dockerfile",
"compose.yaml",
"compose.test.yaml",
"启动.sh",
"停止.sh",
"应用.example.toml",
"部署.example.env",
"Pi/package.json",
"Pi/package-lock.json",
]:
target = stage / name
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(根 / "部署" / name, target)
config = stage / "应用.example.toml"
config.write_text(config.read_text().replace("<固定资源构建身份>", manifest["构建身份"]))
for name in ["启动.sh", "停止.sh"]:
(stage / name).chmod(0o755)
shutil.copyfile(根 / "数据库/初始化/用途角色.sql", stage / "用途角色.sql")
payloads = [wheels[0], stage / "requirements.txt"]
(stage / "SHA256SUMS").write_text(
"".join(
f"{hashlib.sha256(p.read_bytes()).hexdigest()} {p.relative_to(stage).as_posix()}\n"
for p in payloads
)
)
files = {
p.relative_to(stage).as_posix(): hashlib.sha256(p.read_bytes()).hexdigest()
for p in sorted(stage.rglob("*"))
if p.is_file()
}
release = {
"schema_version": 1,
"version": manifest["代码版本"],
"build_id": manifest["构建身份"],
"code_hash": manifest["代码哈希"],
"resource_release": manifest["发布身份"],
"files": files,
}
release["release_id"] = hashlib.sha256(
json.dumps(release, ensure_ascii=False, sort_keys=True).encode()
).hexdigest()
(stage / "发布清单.json").write_text(
json.dumps(release, ensure_ascii=False, indent=2) + "\n",
)
# 原子替换仅作用于本次新目录;此前已检查不可覆盖历史发布。
stage.rename(output)
return release
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--输出", required=True, type=Path)
parser.add_argument("--已生成", action="store_true", help="复用刚通过构建的资源,仍核对索引")
args = parser.parse_args()
print(json.dumps(构建发布(args.输出, 生成=not args.已生成), ensure_ascii=False))