fix: 测试清单(8) 新一轮 — ZWQ/PMC/设置/团队 多项修复

视频/工作台/商品库(ZWQ):
- row19 视频阶段移除已取消的【上传视频】按钮 (pipeline.tsx)
- row20 工作台统计栏(总项目/进行中/成片)点击跳转视频项目对应 tab (dashboard/projects/route-config/App)
- row21 最新项目跳转改用真实 projectId, 不再跳到最近打开项目 (dashboard.tsx)
- row22+PMC28 网格/列表视图模式持久化(localStorage), 跨菜单记住用户选择 (projects/ai-tools + 新 hook use-view-mode.ts)
- row24 已有三视图的演员/模特直接复用, 不再强制点AI生成三视图 (pipeline.tsx)

图片生成/设置(PMC):
- row27 任务中心: 6个进行中状态补全归类, 不再把生成中误判为失败栏 (stage-config.ts)
- row29 头像上传500: avatar_url 存公读直链替代超长预签名URL(URLField max_length=200溢出) (accounts/views.py, 需部署)
- row30 显示名保存后回显: 改读 first_name 回退 username, 修刷新仍旧名 (settings.tsx)
- row31/32 隐藏【创作默认】【显示】两个设置模块 (settings.tsx)

平台套图(YYX):
- row3 图片预览铺满占位 (ai-tools-page.css)

团队:
- PMC25 充值/邀请成员/设置月限额三弹窗禁止点遮罩误关 (team.tsx)

含平台套图线上提示词优化(services.py/views.py/api.ts, 前序会话遗留一并提交)
构建: tsc --noEmit=0, npm run build=0 (1755模块); 后端 py_compile OK
注: row29 头像/后端项需部署生效

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-06-29 16:45:29 +08:00
co-authored by Claude Opus 4.8
parent f3b89f3d49
commit ffada7caab
15 changed files with 292 additions and 82 deletions
+6 -3
View File
@@ -211,7 +211,7 @@ def update_avatar(request):
if upload is None:
return Response({"detail": "no file"}, status=status.HTTP_400_BAD_REQUEST)
user = request.user
suffix = Path(upload.name).suffix.lower() or ".png"
suffix = Path(upload.name or "").suffix.lower() or ".png"
object_key = f"users/{user.id}/avatar/{uuid.uuid4()}{suffix}"
storage = TosStorage()
storage.upload_fileobj(
@@ -219,8 +219,11 @@ def update_avatar(request):
object_key=object_key,
content_type=upload.content_type or "image/png",
)
# 头像直接存可访问的预签名 URL(长有效期);后续如需永久化可改为读时签发
user.avatar_url = storage.presigned_get_url(object_key=object_key, expires_in=7 * 24 * 3600)
# 桶公读,存稳定的虚拟主机式直链(全站资产同款 public_url)——不存预签名 URL:
# 预签名 URL 带一长串 SigV4 查询参数(常 >300 字符),会超出 avatar_url 这个 URLField
# 的默认 max_length=200,MySQL 严格模式下 save 抛 DataError(Data too long)→ 500;
# 且预签名 1h/7d 必过期。直链短、稳定、可缓存,既修 500 又一劳永逸。
user.avatar_url = storage.public_url(object_key=object_key)
user.save(update_fields=["avatar_url"])
return Response(UserSerializer(user).data)
+187 -24
View File
@@ -751,11 +751,16 @@ def _find_entity_group(project, kind: str, label: str, group_id: str | None):
def _ratio_to_image_size(ratio: str) -> str:
"""前端比例 → gpt-image 支持的尺寸(宽高均需被 16 整除)。16:9 用真 16:9 的 1536x864,4:3 用 1536x1024。"""
"""前端比例 → gpt-image 支持的尺寸(宽高均需被 16 整除)。
⚠️ 该网关只接受 1024x1024 / 1024x1536 / 1536x1024 三种固定 size,无法直接产出精确 3:4 / 9:16 / 4:5,
这里只能取最接近的「竖/横/方」近似;要拿到精确像素比例需在出图后做主体保护裁切/补边(见优化文档 §7.1
normalize_output_image,属待部署的后处理项)。修复点:4:5 原会 fallback 成 1024x1024(方图)→ 比例错误,
现归到竖图 1024x1536。"""
return {
"1:1": "1024x1024",
"3:4": "1024x1536",
"9:16": "1024x1536",
"3:4": "1024x1536", # 近似竖图(网关无精确 3:4)
"4:5": "1024x1536", # 近似竖图(原 fallback 成方图是比例 bug)
"9:16": "1024x1536", # 近似竖图(网关无精确 9:16)
"4:3": "1536x1024",
"16:9": "1536x864", # 真 16:9(原来误用 1536x1024 = 3:2)
}.get((ratio or "").strip(), "1024x1024")
@@ -964,6 +969,7 @@ def build_model_tryon_prompt_refs(product, has_model: bool, base_prompt: str = "
# 平台套图同批多张要「同款商品、不同版式」,否则 N 张文案/构图雷同(PMC#24)。
# 锁死商品一致性,只让排版/构图/视角/配色基调按张变化。
# 注:仅纯文生图回落路径(无参考图)仍用这个简表;refs 版改用下面的 slot 体系。
_COVER_VARIATIONS = [
"本张:正面居中主图版式,商品占画面主体,纯净背景,经典电商主图构图",
"本张:换一种排版——商品偏置 + 卖点文案分区,场景化背景,杂志感构图",
@@ -973,27 +979,173 @@ _COVER_VARIATIONS = [
"本张:促销封面版式,留出标题/价签区,高对比配色,强视觉冲击构图",
]
# 平台套图优化版(对照「平台套图线上提示词优化版.md」):平台差异 + slot 版式 + 低信息密度上限。
# 平台 id 用规范化 key(前端 dy/tb… 已在 PLATFORM_ID_MAP 里映射成这些 canonical key)。
_PLATFORM_NAMES = {
"taobao": "淘宝", "tmall": "天猫", "jd": "京东", "pdd": "拼多多", "douyin": "抖音电商",
"xhs": "小红书", "kuaishou": "快手", "wechat": "视频号", "amazon": "亚马逊", "1688": "1688",
}
def build_platform_cover_prompt_refs(product, has_model: bool, base_prompt: str = "", index: int = 0) -> str:
"""平台套图 image_edit 提示词(refs 版):参考图1=商品真实主图(锁外形/品牌/配色/Logo),
有模特时参考图2=出镜模特(锁人脸/身形)。生成电商平台主图 / 封面套图,商品须还原真实包装。
`index` 让同批每张的版式/构图/视角不同,避免 N 张雷同(PMC#24)。"""
# §4 平台块:每块只描述平台调性 / 版式倾向,统一服从「头图低信息密度」,不写比例(比例由 size 控制)。
_PLATFORM_COVER_BLOCKS = {
"taobao": (
"平台:淘宝。画面像淘宝搜索货架和商品主图,移动端缩略图下商品一眼可识别;"
"常见左上小品牌区、中央或偏右商品主体、底部克制活动条 / 轻卖点条;"
"背景干净但不单调,可用浅灰棚拍 / 浅色墙面 / 试衣间 / 窗边挂拍 / 干净台面;"
"禁止详情页 / 长图 / 平台 UI / 二维码 / 直播间界面 / 虚假价格与未提供折扣。"
),
"tmall": (
"平台:天猫。画面像品牌旗舰店商品首图,品牌感 / 质感 / 留白 / 材质光影更重要;"
"促销感弱于淘宝,不要大红大黄低价感,不要拼多多式强利益区。"
),
"jd": (
"平台:京东。画面清晰 / 可信 / 理性 / 标准,像京东商品主图;白底或浅灰商品图、克制品质短标题、包装 / 配件 / 材质细节;"
"信息区极少,只允许 1 个短标题或 1-2 个短标签;不要参数表 / 功能说明长区 / 详情页式模块,不要小红书滤镜,不要强生活方式过度氛围。"
),
"pdd": (
"平台:拼多多。主体大 / 信息直接 / 利益区明显,像商品主图或活动头图;"
"可有高对比活动区和大标题但只讲一个利益点,不能编造价格 / 优惠 / 折扣 / 销量 / 平台补贴;"
"背景明亮,商品边缘清楚,不要复杂品牌大片感。"
),
"douyin": (
"平台:抖音电商。像信息流商品封面 / 短视频电商封面,近景 / 强裁切 / 真实使用瞬间 / 动作感强;"
"常见背景:衣橱 / 梳妆台 / 卧室 / 开箱 / 手部整理 / 拿取 / 穿搭准备;文案更短、商品主体更大;"
"不要抖音 UI / 播放按钮 / 直播间贴片 / 字幕条,也不要套淘宝底部活动条。"
),
"xhs": (
"平台:小红书。像生活方式笔记封面,自然光 / 真实体验 / 种草氛围;"
"常见背景:卧室 / 衣橱 / 梳妆台 / 桌面 / 浴室 / 旅行收纳 / 通勤准备;标题像用户体验表达,少硬广 / 少参数表 / 少促销条。"
),
"kuaishou": (
"平台:快手。像快手小店商品图 / 直播前置封面素材,真实 / 直接 / 可信;"
"可有桌面展示 / 手持展示 / 打包台 / 家中真实环境 / 开箱场景;不要直播 UI / 主播贴片 / 平台水印。"
),
"wechat": (
"平台:视频号 / 微信小店。画面克制可信,适合社交分享和商品卡;"
"背景可用干净家居 / 礼赠 / 办公 / 生活空间 / 柔和自然光;不要微信聊天界面 / 二维码 / 公众号 UI。"
),
"amazon": (
"平台:亚马逊。第 1 张为 MAIN 合规图:纯白背景、只展示售卖商品本体、无文字 / 无道具 / 无边框 / 无水印,商品占画面主要区域;"
"第 2 张以后可为 Lifestyle / Feature / Detail / Package 辅图;不要 Amazon 徽章 / 评分 / 排名 / 优惠 / 平台 UI。"
),
"1688": (
"平台:1688。像批发采购图,商品清楚,偏规格 / 材质 / 工艺 / 包装 / 供货信息;"
"背景可用白底 / 浅灰 / 工厂台面 / 仓储 / 包装台 / 材料工艺背景;头图仍保持低信息密度,只允许 1 个短标题或 1-2 个短标签;"
"不要做成规格表详情页,不要虚构工厂资质 / 库存 / 起订量 / 认证与价格。"
),
}
# §3 / §3.1 slot 版式骨架(低信息密度版):默认 4 张按 hero→scene→selling→detail 取,8/12 张再补 multi/promo。
_COVER_SLOTS = {
"hero": "正面或四分之三角度的商品 / 上身主视觉,主体占画面 60%-80%,纯净干净背景,经典电商主图构图,无文案",
"scene": "场景主视觉:挂拍 / 衣架 / 生活场景 / 手部整理 / 使用情境,环境自然光,氛围感,无文案或仅 1 个极短标题",
"selling": "轻卖点封面:商品主体居中或偏置,最多 1 个短标题加 1-2 个极短标签,文字区克制,不堆参数",
"detail": "质感特写:放大材质 / 做工 / 关键结构(扣位 / 肩带 / 边缘走线),近景视角,突出质感,无文案或 1 个短标签",
"multi": "多角度组合:商品换一个朝向 / 视角,几何分区干净背景,现代简约风,无文案",
"promo": "促销封面:主体大、利益点单一,底部或角落留 1 条活动短语,高对比配色,不铺满文字、不编造价格",
}
_COVER_SLOT_ORDER = ["hero", "scene", "selling", "detail", "multi", "promo"]
# §3.1 / §8 头图低信息密度上限:所有平台 / 所有模型都必须遵守,防止漂成详情页。
_COVER_LOW_DENSITY = (
"这是一张商品头图,不是详情页:画面第一优先级是商品主体 / 上身效果 / 使用场景,"
"商品或模特主体占画面 60%-80%,文字与装饰不得抢主体;如需文案最多 1 个短标题(≤8 字)加 1-2 个极短标签;"
"严禁参数表 / 规格表 / 长句卖点 / 三段式说明 / 密集图标 / 大量箭头 / 2x3 或 3x3 信息宫格 / 左右对比详情页 / 多屏排版。"
)
# §5 背景反差:避免商品与背景同色相融(无法逐图取色时给通用避让规则)。
_COVER_BG_CONTRAST = (
"背景必须与商品主色形成清楚反差,不能让商品与背景同色相融;"
"浅色商品避免奶白 / 浅米 / 浅粉等近似大面积底,优先冷灰 / 蓝灰 / 浅绿 / 自然木色或明确投影、边缘光;"
"深色商品避免黑 / 深灰 / 深棕大底,优先暖白 / 浅灰 / 浅木色 / 日光窗边;"
"除亚马逊 MAIN 或明确白底图外,不要整组都做成纯白 / 浅灰底。"
)
# §6.1 内衣真人上身强约束:防止被第二件衣服遮挡、防性感化漂移。
_UNDERWEAR_ON_MODEL = (
"这是真人试穿商品头图:成人模特直接穿着参考商品中的内衣,内衣是唯一服饰重点,"
"罩杯 / 肩带 / 下围 / 杯型 / 面料 / 颜色与关键版型须完整可见且与参考商品一致;"
"不要出现 T 恤 / 衬衫 / 吊带背心 / 运动上衣 / 连衣裙 / 制服 / 外套等遮挡内衣的第二件衣服,"
"不要把内衣穿在其他衣物外面或在其下面加衣;最多允许轻薄开衫 / 薄纱松搭在肩臂外侧但不得盖住罩杯 / 肩带 / 下围;"
"正规电商服饰目录拍摄,成人、克制、商品展示向,非性感化、非挑逗姿势,关键结构不被裁切。"
)
# §9 通用负面提示词(平台套图专用,压缩版)。
_COVER_NEGATIVE = (
"请规避:详情页 / 详情长图 / 多屏排版 / 店铺页 / 平台 UI / 直播间界面 / 二维码 / 联系方式 / 水印 / 平台 Logo / 虚假角标;"
"不要重新设计商品或改其包装 / Logo / 配色 / 材质 / 文字 / 图案 / 比例,不要生成相似款或改品类,不要擅自加套装 / 配件 / 赠品;"
"不要出现商品数据外的品牌名 / 英文 Logo / 系列名 / 角落签名 / 模型名 / 伪水印;"
"不要编造价格 / 优惠券 / 满减 / 折扣 / 限时活动 / 销量 / 排名 / 认证 / 功效承诺 / 绝对化用语;"
"不要错别字 / 乱码 / 小字堆叠 / 大段文案;不要让文字遮挡主体;不要让商品与背景同色相融。"
)
# 内衣 / 贴身衣物品类提示(比通用穿戴更窄,用于触发内衣强约束分支)。
_UNDERWEAR_HINTS = ("内衣", "文胸", "胸罩", "bra", "内裤", "", "比基尼", "bikini", "睡衣", "塑身", "束身")
def _is_underwear_product(product) -> bool:
blob = f"{getattr(product, 'category', '') or ''} {getattr(product, 'title', '') or ''}".lower()
return any(h in blob for h in _UNDERWEAR_HINTS)
def build_platform_cover_prompt_refs(
product,
*,
has_model: bool = False,
base_prompt: str = "",
platform_id: str = "",
index: int = 0,
count: int = 4, # noqa: ARG001 — 透传保留,后续可据张数扩展 slot 选择
product_ref_count: int = 1,
) -> str:
"""平台套图 image_edit 提示词(refs 优化版,对照「平台套图线上提示词优化版.md」):
参考图1~N=同一件真实商品(锁外形/品牌/配色/Logo/比例),有模特时参考图N+1=出镜模特。
按 `platform_id` 注入平台块、按 `index` 选 slot 版式,统一服从「头图低信息密度」与「背景反差」,
内衣 + 有模特时叠加强约束。只出头图 / 主图 / 封面候选,不再出详情。"""
name = (getattr(product, "title", "") or "商品").strip()
lines = [f"参考图1是「{name}」的真实商品主图。"]
if has_model:
lines += [
"参考图2是出镜模特。请生成参考图2中的这位模特展示参考图1中商品的电商平台套图(主图 / 封面 / 详情);",
"模特的五官、发型、肤色、身形必须与参考图2高度一致,不要换人;",
]
n = max(1, int(product_ref_count or 1))
if n <= 1:
ref_word = "参考图1"
intro = f"参考图1是「{name}」的真实商品参考图。"
else:
lines.append("请基于该商品生成电商平台套图(主图 / 封面 / 详情排版),统一视觉风格;")
rng = f"1-{n}" if n > 2 else "1、2"
ref_word = f"参考图{rng}"
intro = f"参考图{rng}是「{name}」同一件真实商品的不同角度参考图。"
lines = [intro]
lines.append(
"商品的外形、品牌文字、配色、Logo、材质必须与参考图1高度一致,严禁改动或重新设计包装;"
"干净背景、电商主图构图、真实质感"
f"请严格锁定{ref_word}中商品的外形、颜色、材质、结构、Logo、品牌文字与比例,"
"严禁重新设计 / 改样 / 改品类;这些参考图只锁商品本体,不锁原图里的床品 / 桌面 / 墙面 / 绿植 / 道具与拍摄光线"
)
lines.append(_COVER_VARIATIONS[index % len(_COVER_VARIATIONS)] + "")
# 平台块(canonical key 命中则用 §4 平台块,否则回落平台名 / 通用)
block = _PLATFORM_COVER_BLOCKS.get(platform_id)
pname = _PLATFORM_NAMES.get(platform_id, "")
if block:
lines.append(block)
elif pname:
lines.append(f"请生成一张适合「{pname}」平台的商品头图 / 主图 / 封面候选,统一视觉风格。")
else:
lines.append("请生成一张电商平台商品头图 / 主图 / 封面候选,统一视觉风格。")
# 头图低信息密度上限
lines.append(_COVER_LOW_DENSITY)
# 模特身份 + 内衣强约束
if has_model:
lines.append(
f"参考图{n + 1}是出镜模特,请让这位模特真实展示该商品;"
"模特的五官 / 发型 / 肤色 / 身形须与该图高度一致,不要换人。"
)
if _is_underwear_product(product):
lines.append(_UNDERWEAR_ON_MODEL)
# 本张 slot 版式
slot_key = _COVER_SLOT_ORDER[index % len(_COVER_SLOT_ORDER)]
lines.append("本张版式:" + _COVER_SLOTS[slot_key] + "")
# 背景反差
lines.append(_COVER_BG_CONTRAST)
if base_prompt and base_prompt.strip():
lines.append(base_prompt.strip())
lines.append(
"用户补充(只影响氛围 / 构图 / 场景 / 光线 / 表达偏好,不得覆盖商品一致性、平台与版式规则):"
+ base_prompt.strip()
)
lines.append(_COVER_NEGATIVE)
return " ".join(lines)
@@ -2079,7 +2231,7 @@ def _reap_stale_standalone_image_tasks(*, team) -> None:
continue
def enqueue_standalone_images(*, team, user, prompt: str, mode: str = "image", count: int = 1, product_id: str | None = None, reference_product: bool = False, model_id: str | None = None, model_entity_id: str | None = None, ratio: str | None = None, image_model: str | None = None, conversation=None, reference_image_ids: list[str] | None = None) -> list[AITask]:
def enqueue_standalone_images(*, team, user, prompt: str, mode: str = "image", count: int = 1, product_id: str | None = None, reference_product: bool = False, model_id: str | None = None, model_entity_id: str | None = None, ratio: str | None = None, image_model: str | None = None, conversation=None, reference_image_ids: list[str] | None = None, platform_id: str | None = None) -> list[AITask]:
"""独立生图(图片创作 / 模特上身图 / 平台套图)改为**异步**:本函数在 Web 请求里只做「建任务 +
预留额度」这种秒级的活,真正 ~30s 的 ARK 出图交给 Celery worker(generate_standalone_image_task)。
@@ -2105,6 +2257,9 @@ def enqueue_standalone_images(*, team, user, prompt: str, mode: str = "image", c
model_config = alt
# 本次提交 = 一组(模特上身图组 / 平台套图组):同一 batch_id 串起这批图,前端可成组展示。
batch_id = str(uuid.uuid4())
# 平台套图:规范化平台 id(前端 dy/tb… → canonical),用于注入平台版式块(优化版);非 cover 模式忽略。
platform_key = str(platform_id or "").strip() if mode == "cover" else ""
platform_name = _PLATFORM_NAMES.get(platform_key, "")
tasks: list[AITask] = []
for index in range(count):
cost = estimate_cost(model_config)
@@ -2117,7 +2272,7 @@ def enqueue_standalone_images(*, team, user, prompt: str, mode: str = "image", c
status=AITask.Status.CREATED,
model_config=model_config,
idempotency_key=f"standalone-image:{team.id}:{uuid.uuid4()}",
request_payload={"model": model_config.name, "endpoint": model_config.endpoint, "prompt": prompt, "mode": mode, "index": index, "product_id": str(product_id) if product_id else None, "reference_product": bool(reference_product), "model_id": str(model_id) if model_id else None, "model_entity_id": str(model_entity_id) if model_entity_id else None, "batch_id": batch_id, "ratio": str(ratio) if ratio else None, "reference_image_ids": ref_ids},
request_payload={"model": model_config.name, "endpoint": model_config.endpoint, "prompt": prompt, "mode": mode, "index": index, "product_id": str(product_id) if product_id else None, "reference_product": bool(reference_product), "model_id": str(model_id) if model_id else None, "model_entity_id": str(model_entity_id) if model_entity_id else None, "batch_id": batch_id, "ratio": str(ratio) if ratio else None, "reference_image_ids": ref_ids, "platform_id": platform_key or None, "platform_name": platform_name or None},
estimated_cost=cost,
)
# 预留额度若余额不足会抛 ValueError,在同步的 Web 请求里立刻反馈给前端(不会先建半套任务)
@@ -2191,10 +2346,18 @@ def run_standalone_image_task(*, task_id: str) -> None:
product, has_model=bool(model_url), base_prompt=prompt,
index=index, n_product=len(product_urls),
)
elif mode == "cover" and product_url:
# 平台套图:参考图1=商品真实图(锁包装一致性),有模特则参考图2=模特(锁人脸/身形)
edit_images = [product_url] + ([model_url] if model_url else [])
edit_prompt = build_platform_cover_prompt_refs(product, has_model=bool(model_url), base_prompt=prompt, index=index)
elif mode == "cover" and product_urls:
# 平台套图:参考图1~N=商品真实图(多角度,_product_reference_urls 已优先真实上传图/排除 AI 图),
# 有模特则参考图N+1=模特(锁人脸/身形)。platform_id 注入平台版式块(优化版)。
edit_images = product_urls + ([model_url] if model_url else [])
edit_prompt = build_platform_cover_prompt_refs(
product,
has_model=bool(model_url),
base_prompt=prompt,
platform_id=str(payload.get("platform_id") or ""),
index=index,
product_ref_count=len(product_urls),
)
elif bool(payload.get("reference_product")) and product_url:
edit_images = [product_url]
edit_prompt = build_product_triview_prompt_refs(product, "")
+3 -1
View File
@@ -38,6 +38,8 @@ class GenerateImageView(APIView):
model_entity_id = str(request.data.get("model_entity_id") or "").strip() or None
ratio = str(request.data.get("ratio") or "").strip() or None
image_model = str(request.data.get("image_model") or "").strip() or None
# 平台套图:前端传规范化平台 id(taobao/douyin/…),用于后端注入平台版式块(优化版)
platform_id = str(request.data.get("platform_id") or "").strip() or None
conversation_id = str(request.data.get("conversation_id") or "").strip() or None
# 用户在图片创作里上传的参考图(已先传成 Asset),按 id 列表带入 → 生成时作多图参考(image_edit)
raw_refs = request.data.get("reference_image_ids") or []
@@ -58,7 +60,7 @@ class GenerateImageView(APIView):
title=(prompt[:24] or "默认创作"),
)
try:
tasks = enqueue_standalone_images(team=team, user=request.user, prompt=prompt, mode=mode, count=count, product_id=product_id, reference_product=reference_product, model_id=model_id, model_entity_id=model_entity_id, ratio=ratio, image_model=image_model, conversation=conversation, reference_image_ids=reference_image_ids)
tasks = enqueue_standalone_images(team=team, user=request.user, prompt=prompt, mode=mode, count=count, product_id=product_id, reference_product=reference_product, model_id=model_id, model_entity_id=model_entity_id, ratio=ratio, image_model=image_model, conversation=conversation, reference_image_ids=reference_image_ids, platform_id=platform_id)
except ValueError as exc: # 无可用模型 / 余额不足等,立即反馈
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
# 本次提交即刷新对话活跃时间,左栏「最近」据此置顶