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