Add prototype gameplay loop with character/battlefield assets and server game APIs.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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}"},
|
||||
|
||||
Reference in New Issue
Block a user