Add prototype gameplay loop with character/battlefield assets and server game APIs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
udx
2026-10-20 18:50:00 +08:00
co-authored by Cursor
parent 79369e4473
commit 6c64210331
833 changed files with 998079 additions and 1042 deletions
+84 -4
View File
@@ -26,7 +26,9 @@ from typing import Any, Callable, Dict, List, Optional, Sequence, Union
DEFAULT_MODEL = "gpt-image-2.5"
VIP_MODEL = "gpt-image-2-vip"
DEFAULT_API_KEY = "sk-b69877bd82c6412ca4872351f428d580"
# API keys are deliberately read from the environment/CLI only. Keeping a key
# in this checked-in tool would expose it to every clone of the repository.
DEFAULT_API_KEY: Optional[str] = None
DEFAULT_BASE_URL = "https://grsai.dakka.com.cn" # 国内节点;全球: https://grsaiapi.com
DEFAULT_ASPECT_RATIO = "1024x1024"
DEFAULT_QUALITY = "auto"
@@ -42,6 +44,8 @@ VIP_MODELS = frozenset(
"gpt-image-2.5-sunburst",
}
)
SUPPORTED_MODELS = frozenset({"gpt-image-2", "gpt-image-2.5", *VIP_MODELS})
SUPPORTED_QUALITIES = frozenset({"auto", "low", "medium", "high", "xhigh", "max"})
class GenerateError(RuntimeError):
@@ -53,12 +57,17 @@ def _now_iso() -> str:
def _resolve_api_key(explicit: Optional[str] = None) -> str:
return (
key = (
explicit
or os.environ.get("GRSAI_API_KEY")
or os.environ.get("GPT_IMAGE_API_KEY")
or DEFAULT_API_KEY
)
if not key or not key.strip():
raise GenerateError(
"未配置 API key。请设置 GRSAI_API_KEY(或 GPT_IMAGE_API_KEY),"
"或通过 --api-key / JSON.api_key 传入。"
)
return key.strip()
def _normalize_base_url(base_url: str) -> str:
@@ -157,6 +166,63 @@ def http_json(
raise GenerateError(f"网络错误: {exc}") from exc
def http_stream_json(
method: str,
url: str,
*,
headers: Optional[Dict[str, str]] = None,
body: Optional[Dict[str, Any]] = None,
timeout: float = 300.0,
) -> Dict[str, Any]:
"""Read a JSON/SSE response and return its last useful JSON object.
The provider documents ``replyType=stream`` as an event stream, but in
practice deployments return either NDJSON or SSE (``data: {...}``). The
parser accepts both forms and ignores keep-alive/comment lines.
"""
data = json.dumps(body, ensure_ascii=False).encode("utf-8") if body is not None else None
req_headers = {
"Accept": "text/event-stream, application/x-ndjson, application/json",
**(headers or {}),
}
if body is not None:
req_headers["Content-Type"] = "application/json"
request = urllib.request.Request(url, data=data, headers=req_headers, method=method)
latest: Dict[str, Any] = {}
try:
with urllib.request.urlopen(request, timeout=timeout) as resp:
for raw_line in resp:
line = raw_line.decode("utf-8", errors="replace").strip()
if not line or line.startswith(":") or line.startswith("event:"):
continue
if line.startswith("data:"):
line = line[5:].strip()
if line == "[DONE]":
break
try:
payload = json.loads(line)
except json.JSONDecodeError:
# Some gateways split a JSON object across lines. Keep
# consuming the stream; the final object is still useful.
continue
if isinstance(payload, dict):
latest.update(payload)
if not latest:
raise GenerateError("流式响应未包含有效 JSON 数据")
return latest
except urllib.error.HTTPError as exc:
err_body = exc.read().decode("utf-8", errors="replace")
try:
payload = json.loads(err_body) if err_body else {}
except json.JSONDecodeError:
payload = {"error": err_body}
message = payload.get("error") or err_body or str(exc)
raise GenerateError(f"HTTP {exc.code}: {message}") from exc
except urllib.error.URLError as exc:
raise GenerateError(f"网络错误: {exc}") from exc
def download_file(url: str, dest: Path, timeout: float = 120.0) -> Path:
dest.parent.mkdir(parents=True, exist_ok=True)
request = urllib.request.Request(url, headers={"User-Agent": "AIGame-gpt-image/1.0"})
@@ -247,6 +313,19 @@ def submit_generate(
background: Optional[str] = None,
mask: Optional[str] = None,
) -> Dict[str, Any]:
prompt = prompt.strip()
if not prompt:
raise GenerateError("prompt 不能为空")
if model not in SUPPORTED_MODELS:
raise GenerateError(
f"不支持的模型: {model}。可选模型: {', '.join(sorted(SUPPORTED_MODELS))}"
)
if quality not in SUPPORTED_QUALITIES:
raise GenerateError(
f"不支持的 quality: {quality}。可选值: {', '.join(sorted(SUPPORTED_QUALITIES))}"
)
if reply_type not in {"json", "stream", "async"}:
raise GenerateError("reply_type 必须是 json、stream 或 async")
image_refs = [normalize_image_ref(x) for x in (images or [])]
body: Dict[str, Any] = {
"model": model,
@@ -262,7 +341,8 @@ def submit_generate(
body["mask"] = normalize_image_ref(mask)
url = f"{_normalize_base_url(base_url)}/v1/api/generate"
return http_json(
request = http_stream_json if reply_type == "stream" else http_json
return request(
"POST",
url,
headers={"Authorization": f"Bearer {api_key}"},