Add hot-update boot pipeline, UI bundles/atlas assets, and design docs.

Ignore AIClient/Library and AIClient/HybridCLRData permanently; include HybridCLR/YooAsset project setup and UI tooling.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
udx
2026-09-21 16:02:58 +08:00
co-authored by Cursor
parent f3f66ab063
commit b7a0bbcb89
279 changed files with 27332 additions and 14 deletions
+191
View File
@@ -0,0 +1,191 @@
#!/usr/bin/env python3
"""CDN 发布与本地联调工具。
目录约定(与 ProjectDesign / Unity BuildAndStage 一致)::
Tools/cdn_root/wechat/version.json
Tools/cdn_root/wechat/1.0.0/DefaultPackage/<yooasset files>
用法::
# 本地 HTTP(HostPlayMode 联调)
python Tools/cdn_publish.py serve --port 8787
# 校验目录
python Tools/cdn_publish.py status
# 上传到远端(需配置)
python Tools/cdn_publish.py upload --mode copy --dest //server/cdn/www
python Tools/cdn_publish.py upload --mode rsync --dest user@host:/var/cdn/www
# 或自定义命令({src} {dest}):
set CDN_UPLOAD_CMD=scp -r {src}/. {dest}/
python Tools/cdn_publish.py upload --mode cmd --dest user@host:/var/www
"""
from __future__ import annotations
import argparse
import json
import os
import shutil
import subprocess
import sys
from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
CDN_ROOT = REPO / "Tools" / "cdn_root"
CHANNEL = "wechat"
DEFAULT_PORT = 8787
class QuietHandler(SimpleHTTPRequestHandler):
def log_message(self, fmt: str, *args) -> None:
sys.stderr.write("[%s] %s\n" % (self.log_date_time_string(), fmt % args))
def end_headers(self) -> None:
# 方便微信/编辑器跨域拉 version.json;生产 CDN 由 Caddy 配
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Cache-Control", "no-cache")
super().end_headers()
def cmd_status(_: argparse.Namespace) -> int:
version = CDN_ROOT / CHANNEL / "version.json"
meta = CDN_ROOT / CHANNEL / "last_publish.json"
print(f"cdn_root={CDN_ROOT}")
print(f"version_json_exists={version.is_file()} path={version}")
if version.is_file():
print(version.read_text(encoding="utf-8"))
print(f"meta_exists={meta.is_file()} path={meta}")
if meta.is_file():
print(meta.read_text(encoding="utf-8"))
pkg_root = CDN_ROOT / CHANNEL
if pkg_root.is_dir():
for p in sorted(pkg_root.rglob("*")):
if p.is_file() and p.name != "version.json":
rel = p.relative_to(CDN_ROOT)
print(f" file {rel} ({p.stat().st_size} bytes)")
return 0
def cmd_serve(args: argparse.Namespace) -> int:
if not CDN_ROOT.is_dir():
print(f"ERROR: missing {CDN_ROOT}, run Unity menu AIGame/YooAsset/Build And Stage first", file=sys.stderr)
return 1
os.chdir(CDN_ROOT)
handler = partial(QuietHandler, directory=str(CDN_ROOT))
server = ThreadingHTTPServer(("0.0.0.0", args.port), handler)
print(f"serving {CDN_ROOT} at http://127.0.0.1:{args.port}/")
print(f"version url: http://127.0.0.1:{args.port}/{CHANNEL}/version.json")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nstopped")
return 0
def cmd_upload(args: argparse.Namespace) -> int:
src = CDN_ROOT
if not src.is_dir():
print(f"ERROR: missing {src}", file=sys.stderr)
return 1
dest = args.dest
if not dest:
print("ERROR: --dest required", file=sys.stderr)
return 1
# 上传前把 version.prod.json 覆盖为对外 version.json(若存在)
prod = src / CHANNEL / "version.prod.json"
live = src / CHANNEL / "version.json"
if prod.is_file():
shutil.copy2(prod, live)
print(f"promoted {prod.name} -> version.json for upload")
mode = args.mode
if mode == "copy":
dest_path = Path(dest)
dest_path.mkdir(parents=True, exist_ok=True)
# 同步 wechat 通道目录
target = dest_path / CHANNEL
if target.exists():
shutil.rmtree(target)
shutil.copytree(src / CHANNEL, target, ignore=shutil.ignore_patterns("version.prod.json", "last_publish.json"))
print(f"copied {src / CHANNEL} -> {target}")
return 0
if mode == "rsync":
cmd = [
"rsync", "-avz", "--delete",
"--exclude", "version.prod.json",
"--exclude", "last_publish.json",
f"{src / CHANNEL}/",
f"{dest.rstrip('/')}/{CHANNEL}/",
]
print(" ".join(cmd))
return subprocess.call(cmd)
if mode == "cmd":
template = os.environ.get("CDN_UPLOAD_CMD", "").strip()
if not template:
print("ERROR: set CDN_UPLOAD_CMD with {src} and {dest}", file=sys.stderr)
return 1
cmd = template.format(src=str(src / CHANNEL), dest=dest)
print(cmd)
return subprocess.call(cmd, shell=True)
print(f"ERROR: unknown mode {mode}", file=sys.stderr)
return 1
def cmd_write_version(args: argparse.Namespace) -> int:
"""手动改写 version.json(不重新打 AB)。"""
path = CDN_ROOT / CHANNEL / "version.json"
path.parent.mkdir(parents=True, exist_ok=True)
data = {
"appVersionMin": args.app_version,
"appVersionMax": args.app_version,
"resVersion": args.res_version,
"cdnRoot": args.cdn_root,
"packages": ["DefaultPackage"],
"forceUpdate": bool(args.force_update),
"notice": args.notice or "",
}
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(f"wrote {path}")
print(json.dumps(data, ensure_ascii=False, indent=2))
return 0
def main() -> int:
parser = argparse.ArgumentParser(description="AIGame CDN publish helpers")
sub = parser.add_subparsers(dest="cmd", required=True)
p_status = sub.add_parser("status", help="Show staged CDN files")
p_status.set_defaults(func=cmd_status)
p_serve = sub.add_parser("serve", help="Serve Tools/cdn_root over HTTP")
p_serve.add_argument("--port", type=int, default=DEFAULT_PORT)
p_serve.set_defaults(func=cmd_serve)
p_upload = sub.add_parser("upload", help="Upload wechat channel to remote CDN root")
p_upload.add_argument("--mode", choices=("copy", "rsync", "cmd"), default="copy")
p_upload.add_argument("--dest", required=True, help="Remote or local destination root (www root)")
p_upload.set_defaults(func=cmd_upload)
p_ver = sub.add_parser("write-version", help="Rewrite version.json")
p_ver.add_argument("--app-version", default="1.0.0")
p_ver.add_argument("--res-version", required=True)
p_ver.add_argument("--cdn-root", default="https://www.xworld.ren/wechat/1.0.0/DefaultPackage")
p_ver.add_argument("--force-update", action="store_true")
p_ver.add_argument("--notice", default="")
p_ver.set_defaults(func=cmd_write_version)
args = parser.parse_args()
return int(args.func(args) or 0)
if __name__ == "__main__":
raise SystemExit(main())