"""套图业务逻辑(从 api/suite.py 下沉):文本素材转换 / 模型校验 / 生成图回写商品素材。 api 层只留参数校验与调用;本模块可独立测试。 """ from __future__ import annotations import uuid from schemas.suite import TextMaterial from services.tasks import TaskImage def texts_to_raw(texts: list[TextMaterial]) -> dict: """前端组装的文本素材 → prompt 上下文用的 raw dict(后写的覆盖先写的)。""" raw: dict = {} for t in texts: if t.kind == "title" and t.content: raw["title"] = t.content elif t.kind == "price" and t.content: raw["price"] = t.content elif t.kind == "brand" and t.content: raw["brand"] = t.content elif t.kind == "params" and t.pairs: merged = {p["key"]: p["value"] for p in (raw.get("params") or [])} for p in t.pairs: merged.setdefault(p["key"], p["value"]) raw["params"] = [{"key": k, "value": v} for k, v in merged.items()] elif t.kind == "selling_point" and t.content: raw["sellingPoints"] = t.content elif t.kind == "desc" and t.content: raw["desc"] = t.content elif t.kind == "sales" and t.content: raw["sales"] = t.content elif t.kind == "shop" and t.content: raw["shop"] = t.content return raw def validate_model(provider_name: str, model: str | None) -> None: """按 provider 校验模型名白名单,不合法抛 ValueError。""" from schemas.suite import RIGHTAPI_MODELS, TONGYI_MODELS if provider_name == "tongyi" and model and model not in TONGYI_MODELS: raise ValueError(f"不支持的模型: {model}(tongyi 支持: {TONGYI_MODELS})") if provider_name == "rightapi" and model and model not in RIGHTAPI_MODELS: raise ValueError(f"不支持的模型: {model}(rightapi 支持: {RIGHTAPI_MODELS})") async def append_generated_asset( product_id: str, image: TaskImage, after_asset_id: str | None = None, ) -> str | None: """把一张生成完成的图追加为 product_assets(generated),并累加 asset_counts。返回 asset_id。 after_asset_id:插入锚点——新素材排在锚点之后(其余素材顺次后移),方便与原图对比; 缺省/锚点无效时追加到组尾。作为 run_suite 的逐张回调使用时签名须为 (image), 调用方用 partial 绑定 product_id。 """ from sqlalchemy import func, select from db import get_session_factory from models import Product, ProductAsset pid = uuid.UUID(product_id) async with get_session_factory()() as db: sort_order: int | None = None if after_asset_id: try: anchor = await db.get(ProductAsset, UUID(after_asset_id)) except ValueError: anchor = None if anchor is not None and anchor.product_id == pid and anchor.group_key == "generated": # 锚点之后的素材顺次后移,腾出插入位 followers = ( await db.scalars( select(ProductAsset).where( ProductAsset.product_id == pid, ProductAsset.group_key == "generated", ProductAsset.sort_order > anchor.sort_order, ) ) ).scalars().all() for follower in followers: follower.sort_order += 1 sort_order = anchor.sort_order + 1 if sort_order is None: sort_order = await db.scalar( select(func.coalesce(func.max(ProductAsset.sort_order), -1)).where( ProductAsset.product_id == pid, ProductAsset.group_key == "generated", ) ) sort_order = (sort_order or 0) + 1 asset = ProductAsset( product_id=pid, group_key="generated", variant_name=None, sort_order=sort_order, type="img", source_url="", stored_url=image.url, status="uploaded", ) db.add(asset) await db.flush() product = await db.get(Product, pid) if product is not None: counts = dict(product.asset_counts or {}) counts["generated"] = int(counts.get("generated") or 0) + 1 product.asset_counts = counts await db.commit() return str(asset.id)