fix: parse nested image generation tasks

This commit is contained in:
Codex
2026-08-02 09:30:23 +08:00
parent a96b1bf9f9
commit d91c137834
4 changed files with 152 additions and 7 deletions
@@ -9,6 +9,97 @@ import httpx
from app.integrations.model_router import ModelRouter from app.integrations.model_router import ModelRouter
DIAGNOSTIC_SCALAR_KEYS = {
"code",
"error",
"error_code",
"error_message",
"id",
"is_final",
"message",
"msg",
"state",
"status",
"status_group",
"task_id",
"type",
}
def safe_response_diagnostic(payload: Any) -> dict[str, Any]:
"""Describe provider payload shape without persisting images, URLs, or secrets."""
signals: dict[str, Any] = {}
def shape(value: Any, path: str = "root", depth: int = 0) -> Any:
if depth > 4:
return type(value).__name__
if isinstance(value, dict):
result: dict[str, Any] = {}
for key, child in list(value.items())[:40]:
child_path = f"{path}.{key}"
if key in DIAGNOSTIC_SCALAR_KEYS and isinstance(child, (str, int, float, bool)):
rendered = str(child)
signals[child_path] = rendered[:500]
if key in {"url", "result_url", "b64_json", "data", "inlineData", "inline_data"}:
if isinstance(child, list):
result[key] = {"type": "list", "length": len(child)}
elif isinstance(child, dict):
result[key] = {"type": "object", "keys": list(child)[:30]}
elif isinstance(child, str):
result[key] = {"type": "string", "length": len(child)}
else:
result[key] = type(child).__name__
if isinstance(child, (dict, list)):
nested = shape(child, child_path, depth + 1)
result[f"{key}_shape"] = nested
continue
result[key] = shape(child, child_path, depth + 1)
return result
if isinstance(value, list):
return {
"type": "list",
"length": len(value),
"items": [shape(item, f"{path}[{index}]", depth + 1) for index, item in enumerate(value[:3])],
}
if value is None:
return "null"
if isinstance(value, str):
return {"type": "string", "length": len(value)}
return type(value).__name__
return {"shape": shape(payload), "signals": signals}
def extract_task_id(payload: Any) -> str | int | None:
if not isinstance(payload, dict):
return None
direct = payload.get("task_id")
if direct not in (None, ""):
return direct
data = payload.get("data")
if isinstance(data, dict):
nested = data.get("task_id")
if nested not in (None, ""):
return nested
for key in ("task_ids", "任务ids"):
candidates = data.get(key)
if isinstance(candidates, list) and candidates:
return candidates[0]
return None
def unwrap_media_status(payload: Any) -> dict[str, Any]:
if not isinstance(payload, dict):
raise RuntimeError("AIGC 状态接口返回的不是 JSON 对象。")
data = payload.get("data")
if isinstance(data, dict) and any(
key in data for key in ("state", "is_final", "result_url", "error", "progress")
):
return {**payload, **data}
return payload
class OpenAICompatibleGateway: class OpenAICompatibleGateway:
"""Adapter for independently configured OpenAI-compatible model-pool items.""" """Adapter for independently configured OpenAI-compatible model-pool items."""
@@ -75,7 +166,11 @@ class OpenAICompatibleGateway:
json=payload, json=payload,
) )
response.raise_for_status() response.raise_for_status()
return response.json() created = response.json()
task_id = extract_task_id(created)
if task_id is not None:
return await self._poll_aigc_media(client, item, task_id, created)
return created
async def edit_image( async def edit_image(
self, self,
@@ -128,8 +223,8 @@ class OpenAICompatibleGateway:
) )
response.raise_for_status() response.raise_for_status()
created = response.json() created = response.json()
task_id = created.get("task_id") if isinstance(created, dict) else None task_id = extract_task_id(created)
if not task_id: if task_id is None:
return created return created
return await self._poll_aigc_media(client, item, task_id, created) return await self._poll_aigc_media(client, item, task_id, created)
@@ -148,7 +243,7 @@ class OpenAICompatibleGateway:
params={"task_id": task_id}, params={"task_id": task_id},
) )
response.raise_for_status() response.raise_for_status()
status = response.json() status = unwrap_media_status(response.json())
if not status.get("is_final"): if not status.get("is_final"):
continue continue
if status.get("state") != "success": if status.get("state") != "success":
+4 -1
View File
@@ -872,10 +872,13 @@ async def test_image_generation(values: dict[str, Any], model_id: str) -> Runtim
) )
output = _find_generated_image(result) output = _find_generated_image(result)
if not output: if not output:
from app.integrations.openai_compatible import safe_response_diagnostic
return RuntimeSettingsTestResult( return RuntimeSettingsTestResult(
target=target, target=target,
ok=False, ok=False,
message="请求已结束,但响应中没有找到图片地址或图片数据。", message="平台请求已结束,但返回体中没有找到可识别的任务 ID、图片地址或图片数据。",
details={"response_diagnostic": safe_response_diagnostic(result)},
) )
verification = await _verify_generated_image(output) verification = await _verify_generated_image(output)
details = { details = {
+3 -2
View File
@@ -16,7 +16,7 @@ from app.domain.models import (
StructureAnalysis, StructureAnalysis,
StyleDirection, StyleDirection,
) )
from app.integrations.openai_compatible import OpenAICompatibleGateway from app.integrations.openai_compatible import OpenAICompatibleGateway, safe_response_diagnostic
def crop_plan_preview(preview: bytes, region: PlanRegion | None) -> bytes: def crop_plan_preview(preview: bytes, region: PlanRegion | None) -> bytes:
@@ -302,7 +302,8 @@ async def generated_image_bytes(payload: dict[str, Any]) -> tuple[bytes, str]:
mime = header.split(";", 1)[0].replace("data:", "") mime = header.split(";", 1)[0].replace("data:", "")
return base64.b64decode(encoded), mime return base64.b64decode(encoded), mime
if not url: if not url:
raise ValueError("生图模型返回了成功响应,但没有图片地址或图片数据。") diagnostic = json.dumps(safe_response_diagnostic(payload), ensure_ascii=False)
raise ValueError(f"平台返回体中没有可识别的图片结果。脱敏诊断:{diagnostic}")
async with httpx.AsyncClient(timeout=90, follow_redirects=True) as client: async with httpx.AsyncClient(timeout=90, follow_redirects=True) as client:
response = await client.get(url) response = await client.get(url)
response.raise_for_status() response.raise_for_status()
+46
View File
@@ -1,6 +1,11 @@
import pytest import pytest
from app.integrations.openai_compatible import OpenAICompatibleGateway from app.integrations.openai_compatible import OpenAICompatibleGateway
from app.integrations.openai_compatible import (
extract_task_id,
safe_response_diagnostic,
unwrap_media_status,
)
from app.runtime_settings import ( from app.runtime_settings import (
RuntimeSettingsTestResult, RuntimeSettingsTestResult,
_verify_generated_image, _verify_generated_image,
@@ -234,3 +239,44 @@ async def test_generated_image_url_rejects_html_placeholder(monkeypatch) -> None
with pytest.raises(ValueError, match="不是可识别的图片"): with pytest.raises(ValueError, match="不是可识别的图片"):
await _verify_generated_image(("url", "https://cdn.example.com/not-image")) await _verify_generated_image(("url", "https://cdn.example.com/not-image"))
def test_response_diagnostic_keeps_errors_but_redacts_payloads() -> None:
diagnostic = safe_response_diagnostic(
{
"code": 402,
"error": {"message": "insufficient balance"},
"data": [{"b64_json": "secret-image-bytes", "url": "https://private.example"}],
}
)
rendered = str(diagnostic)
assert "insufficient balance" in rendered
assert "secret-image-bytes" not in rendered
assert "https://private.example" not in rendered
def test_extract_task_id_accepts_aggregator_nested_response() -> None:
payload = {
"code": 200,
"data": {"task_id": 91584074, "task_ids": [91584074]},
"msg": "Task created successfully",
}
assert extract_task_id(payload) == 91584074
def test_unwrap_media_status_accepts_nested_data() -> None:
payload = {
"code": 200,
"data": {
"state": "success",
"is_final": True,
"result_url": "https://cdn.example.com/result.png",
},
}
status = unwrap_media_status(payload)
assert status["is_final"] is True
assert status["result_url"].endswith("result.png")