games-development-ai/tier2/gen-worker/scripts/game_content_repository.py
lili 1981a6be01
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
feat(storage): 闭合 Agent 到 Runtime 的 OSS 游戏链路
增加不可变源码检出、两阶段 finalize/activate、Runtime 同源对象读取与 iframe 安全边界,并用 fail-closed CI 门固定契约。Git 仅提交平台实现、契约、迁移和 SoT,不包含具体游戏源码、素材、bundle 或验收证据。
2026-07-30 05:21:44 -07:00

169 lines
7.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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())