2026-03-13 11:12:10 +08:00
|
|
|
|
"""
|
|
|
|
|
|
MemoirImage 模型与 API 用 dict 的互转(与 schema.normalize_image_asset 字段一致)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
2026-03-18 17:18:23 +08:00
|
|
|
|
from app.features.memoir.models import MemoirImage
|
2026-03-13 11:12:10 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_optional_datetime(s: str | None):
|
|
|
|
|
|
if not s:
|
|
|
|
|
|
return None
|
|
|
|
|
|
try:
|
|
|
|
|
|
return datetime.fromisoformat(s.replace("Z", "+00:00"))
|
|
|
|
|
|
except (ValueError, AttributeError):
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def memoir_image_to_dict(m: MemoirImage | None) -> dict[str, Any] | None:
|
|
|
|
|
|
"""将 MemoirImage 转为前端/API 使用的单条图片 dict。"""
|
|
|
|
|
|
if not m:
|
|
|
|
|
|
return None
|
|
|
|
|
|
d: dict[str, Any] = {
|
|
|
|
|
|
"placeholder": m.placeholder or "",
|
|
|
|
|
|
"description": m.description or "",
|
|
|
|
|
|
"index": m.order_index,
|
|
|
|
|
|
"status": m.status or "pending",
|
|
|
|
|
|
"prompt": m.prompt,
|
|
|
|
|
|
"url": m.url,
|
|
|
|
|
|
"storage_key": m.storage_key,
|
|
|
|
|
|
"provider": m.provider,
|
|
|
|
|
|
"style": m.style,
|
|
|
|
|
|
"size": m.size,
|
|
|
|
|
|
"error": m.error,
|
|
|
|
|
|
"retryable": m.retryable,
|
|
|
|
|
|
"created_at": m.created_at.isoformat() if m.created_at else None,
|
|
|
|
|
|
"updated_at": m.updated_at.isoformat() if m.updated_at else None,
|
|
|
|
|
|
}
|
|
|
|
|
|
return d
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def image_dict_to_row_kwargs(d: dict[str, Any] | None) -> dict[str, Any]:
|
|
|
|
|
|
"""从单条图片 dict 提取可写入 MemoirImage 的字段(不含 id/chapter_id/section_id/order_index)。"""
|
|
|
|
|
|
if not d or not isinstance(d, dict):
|
|
|
|
|
|
return {}
|
|
|
|
|
|
created = d.get("created_at")
|
|
|
|
|
|
updated = d.get("updated_at")
|
|
|
|
|
|
if isinstance(created, str):
|
|
|
|
|
|
created = _parse_optional_datetime(created)
|
|
|
|
|
|
elif not isinstance(created, datetime):
|
|
|
|
|
|
created = None
|
|
|
|
|
|
if isinstance(updated, str):
|
|
|
|
|
|
updated = _parse_optional_datetime(updated)
|
|
|
|
|
|
elif not isinstance(updated, datetime):
|
|
|
|
|
|
updated = None
|
|
|
|
|
|
return {
|
|
|
|
|
|
"placeholder": d.get("placeholder") or None,
|
|
|
|
|
|
"description": d.get("description") or None,
|
|
|
|
|
|
"status": (d.get("status") or "pending").strip() or "pending",
|
|
|
|
|
|
"prompt": d.get("prompt") or None,
|
|
|
|
|
|
"url": d.get("url") or None,
|
|
|
|
|
|
"storage_key": d.get("storage_key") or None,
|
|
|
|
|
|
"provider": d.get("provider") or None,
|
|
|
|
|
|
"style": d.get("style") or None,
|
|
|
|
|
|
"size": d.get("size") or None,
|
|
|
|
|
|
"error": d.get("error") or None,
|
|
|
|
|
|
"retryable": d.get("retryable") if d.get("retryable") is not None else None,
|
|
|
|
|
|
"created_at": created,
|
|
|
|
|
|
"updated_at": updated,
|
|
|
|
|
|
}
|