增加不可变源码检出、两阶段 finalize/activate、Runtime 同源对象读取与 iframe 安全边界,并用 fail-closed CI 门固定契约。Git 仅提交平台实现、契约、迁移和 SoT,不包含具体游戏源码、素材、bundle 或验收证据。
169 lines
7.0 KiB
Python
169 lines
7.0 KiB
Python
"""Agent 游戏内容仓库 CLI:按 committed 身份检出,或把新版本提交到对象存储。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
from pathlib import Path
|
||
|
||
from worker.game_artifact_storage_store import MySqlArtifactStorageStore
|
||
from worker.game_artifact_store import ArtifactError
|
||
from worker.game_content_repository import (
|
||
GameContentControlClient,
|
||
GameContentError,
|
||
GameContentRepository,
|
||
)
|
||
|
||
|
||
def _infra_section(name: str, required: tuple[str, ...]) -> dict:
|
||
"""从项目内网配置读取完整分区;缺任一凭据即拒绝联网。"""
|
||
from service import infra_config
|
||
|
||
values = {key: infra_config.get(name, key, None) for key in required}
|
||
missing = [key for key, value in values.items() if value in (None, "")]
|
||
if missing:
|
||
raise GameContentError(f"内网配置分区 {name} 缺字段:{','.join(missing)}")
|
||
return values
|
||
|
||
|
||
def _source_config() -> dict:
|
||
"""读取独立 source writer,只允许访问 game-sources。"""
|
||
values = _infra_section(
|
||
"game_source_minio", ("endpoint", "access_key", "secret_key", "source_bucket", "secure"),
|
||
)
|
||
values.update({
|
||
"artifact_bucket": "game-artifacts",
|
||
"evidence_bucket": "game-evidence",
|
||
})
|
||
return values
|
||
|
||
|
||
def _artifact_config() -> dict:
|
||
"""读取 artifact writer,只允许访问运行制品与证据桶。"""
|
||
return _infra_section(
|
||
"game_artifact_minio",
|
||
("endpoint", "access_key", "secret_key", "artifact_bucket", "evidence_bucket", "secure"),
|
||
)
|
||
|
||
|
||
def _control_client() -> GameContentControlClient:
|
||
"""读取独立 HMAC 控制面配置;密钥缺失时在任何网络请求前 fail-closed。"""
|
||
config = _infra_section(
|
||
"game_content_control", ("endpoint", "secret", "timeout_s"),
|
||
)
|
||
return GameContentControlClient(
|
||
str(config["endpoint"]), str(config["secret"]), timeout_s=config["timeout_s"],
|
||
)
|
||
|
||
|
||
def _connection_factory(section: str):
|
||
"""构造 game-cloud 业务库连接工厂,游标固定为字典行以保字段名边界。"""
|
||
config = _infra_section(section, ("host", "port", "user", "password", "database"))
|
||
|
||
def connect():
|
||
try:
|
||
import pymysql
|
||
except Exception as exc: # pragma: no cover - 部署依赖缺失时给出稳定错误
|
||
raise GameContentError(f"缺少 pymysql:{exc}") from exc
|
||
return pymysql.connect(
|
||
host=str(config["host"]), port=int(config["port"]),
|
||
user=str(config["user"]), password=str(config["password"]),
|
||
database=str(config["database"]), charset="utf8mb4",
|
||
cursorclass=pymysql.cursors.DictCursor, autocommit=False,
|
||
)
|
||
|
||
return connect
|
||
|
||
|
||
def _repository(mysql_section: str) -> GameContentRepository:
|
||
"""创建统一仓库入口,调用方不能替换桶或对象 key。"""
|
||
storage = MySqlArtifactStorageStore(_connection_factory(mysql_section))
|
||
return GameContentRepository(
|
||
storage, control_client=_control_client(),
|
||
source_config=_source_config(), artifact_config=_artifact_config(),
|
||
)
|
||
|
||
|
||
def _positive(value: str) -> int:
|
||
"""argparse 正整数转换,阻止布尔/负数/零进入业务身份。"""
|
||
parsed = int(value)
|
||
if parsed <= 0:
|
||
raise argparse.ArgumentTypeError("必须是正整数")
|
||
return parsed
|
||
|
||
|
||
def main(argv: list[str] | None = None) -> int:
|
||
"""执行 prepare、checkout、commit 或 activate,成功只向 stdout 输出结构化回执。"""
|
||
parser = argparse.ArgumentParser(
|
||
description="Agent 游戏内容仓库:prepare/committed checkout/commit/activate"
|
||
)
|
||
parser.add_argument("--mysql-section", default="game_content_mysql",
|
||
help="infra.yaml 中的 game-cloud 业务库分区")
|
||
subparsers = parser.add_subparsers(dest="action", required=True)
|
||
|
||
prepare = subparsers.add_parser("prepare", help="向 Project 权威预留或复用内容版本")
|
||
prepare.add_argument("--tenant-id", type=_positive, required=True)
|
||
prepare.add_argument("--game-id", type=_positive, required=True)
|
||
prepare.add_argument("--revision-id", required=True)
|
||
|
||
checkout = subparsers.add_parser("checkout", help="按 committed 版本检出 Agent 工作区")
|
||
checkout.add_argument("--tenant-id", type=_positive, required=True)
|
||
checkout.add_argument("--game-id", type=_positive, required=True)
|
||
checkout.add_argument("--version-id", type=_positive, required=True)
|
||
checkout.add_argument("--target", type=Path, required=True)
|
||
|
||
commit = subparsers.add_parser("commit", help="上传并 CAS 提交新的不可变版本")
|
||
commit.add_argument("--root", type=Path, required=True)
|
||
commit.add_argument("--tenant-id", type=_positive, required=True)
|
||
commit.add_argument("--game-id", type=_positive, required=True)
|
||
commit.add_argument("--version-id", type=_positive, required=True)
|
||
commit.add_argument("--revision-id", required=True)
|
||
commit.add_argument("--engine", required=True)
|
||
commit.add_argument("--package-type", choices=("phaser", "littlejs", "canvas"), required=True)
|
||
commit.add_argument("--runtime-manifest", default="game-package.json")
|
||
commit.add_argument("--creator", default="agent")
|
||
|
||
activate = subparsers.add_parser("activate", help="浏览器验收后按 expected-current 激活版本")
|
||
activate.add_argument("--tenant-id", type=_positive, required=True)
|
||
activate.add_argument("--game-id", type=_positive, required=True)
|
||
activate.add_argument("--version-id", type=_positive, required=True)
|
||
activate.add_argument("--revision-id", required=True)
|
||
activate.add_argument("--expected-current-version-id", type=_positive)
|
||
|
||
args = parser.parse_args(argv)
|
||
try:
|
||
content = _repository(args.mysql_section)
|
||
if args.action == "prepare":
|
||
result = content.prepare(
|
||
tenant_id=args.tenant_id, game_id=args.game_id, revision_id=args.revision_id,
|
||
)
|
||
elif args.action == "checkout":
|
||
result = content.checkout(
|
||
tenant_id=args.tenant_id, game_id=args.game_id,
|
||
version_id=args.version_id, target=args.target,
|
||
)
|
||
elif args.action == "activate":
|
||
result = content.activate(
|
||
tenant_id=args.tenant_id,
|
||
game_id=args.game_id,
|
||
version_id=args.version_id,
|
||
revision_id=args.revision_id,
|
||
expected_current_version_id=args.expected_current_version_id,
|
||
)
|
||
else:
|
||
result = content.commit(
|
||
root=args.root, tenant_id=args.tenant_id, game_id=args.game_id,
|
||
version_id=args.version_id, revision_id=args.revision_id,
|
||
engine=args.engine, package_type=args.package_type,
|
||
runtime_manifest_path=args.runtime_manifest, creator=args.creator,
|
||
)
|
||
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
|
||
return 0
|
||
except (ArtifactError, ValueError) as exc:
|
||
print(f"GAME-CONTENT-ERROR:{exc}")
|
||
return 2
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|