fix(ai): 图片创作重跑三连修——丢参考图/裂新批次/提示词矛盾

① 重跑丢参考图:「重跑这张/重跑整批」原来不带 refs → 后端收不到
reference_image_ids 落到纯文生图,出图与上传素材无关。现 GenBatch.refs
存 assetId(上传后回写;恢复批次由 tasks 接口新带的 id 还原),重跑原样带回。

② 重跑裂新聊天记录:enqueue 每次新铸 batch_id,后端没有「补进原批次」
概念,刷新/切对话后重跑图裂成新卡、原卡失败格还挂着。现生成接口收/回
batch_id(UUID 校验,非法兜底新铸),重跑沿用原批次并打 batch_append 标记;
前端批次卡存 backendBatchId(提交回调即落,整批全失败也不丢),重跑带回。
恢复分组时 rerun 任务不计入「应出张数」→ 补图成功后失败格自然收掉;
批次状态改按「有任务在跑」判定,已有好图+补图在途也能接续轮询。

③ 同批换装一半不换:带参考图模板「严格保留款式/不要换款」与用户要求
(如"生成现代服装的穿着")直接矛盾,模型每张随机听一边。改为只锁主体身份
(人物五官/发型/体型,商品品类/外形/Logo),用户明确要求改变的以用户要求
为最高优先级,未提及的才默认与参考图一致。

测试:apps 全量 214 过(需 --settings=airshelf.settings.test);
新增 test_rerun_with_batch_id_reuses_batch_and_marks_append。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-07-02 13:40:52 +08:00
co-authored by Claude Fable 5
parent dcc5d7fe50
commit a048c32527
7 changed files with 148 additions and 43 deletions
+42 -5
View File
@@ -263,11 +263,12 @@ class StandaloneImageReferenceTests(TestCase):
)
prov.image_edit.assert_called_once()
self.assertEqual(prov.image_edit.call_args.kwargs["images"], ["http://x/ref.png"])
# 提示词必须显式钉住「保留参考图主体」+ 带上用户原话(否则图生图不真的还原上传素材)
# 提示词必须显式钉住「参考图=主体唯一依据」+ 带上用户原话(否则图生图不真的还原上传素材);
# 但不能再无差别「严格保留款式」——用户要求(如换现代服装)必须是最高优先级,否则同批一半不换装
used_prompt = prov.image_edit.call_args.kwargs["prompt"]
self.assertIn("按这件背心生成不同场景的穿搭", used_prompt)
self.assertIn("参考图", used_prompt)
self.assertIn("严格保留", used_prompt)
self.assertIn("唯一依据", used_prompt)
self.assertIn("用户要求是最高优先级", used_prompt)
prov.image_generation.assert_not_called()
def test_refs_force_image_edit_model_when_volcano_selected(self):
@@ -374,9 +375,45 @@ class ImageConversationTests(TestCase):
# 任务 payload 落了参考图 id
task = AITask.objects.filter(conversation_id=conv_id).first()
self.assertEqual(task.request_payload.get("reference_image_ids"), [str(ref.id)])
# tasks 接口把参考图解析成 {name,url} 回显
# tasks 接口把参考图解析成 {id,name,url} 回显 —— id 供前端重跑时原样复用(否则重跑丢参考图)
data = self.client.get(f"/api/ai/image-conversations/{conv_id}/tasks/").json()["tasks"]
self.assertEqual(data[0]["reference_images"], [{"name": "背心参考", "url": "http://x/ref.png"}])
self.assertEqual(data[0]["reference_images"], [{"id": str(ref.id), "name": "背心参考", "url": "http://x/ref.png"}])
def test_rerun_with_batch_id_reuses_batch_and_marks_append(self):
"""重跑/补图带原批次 batch_id → 新任务沿用同一 batch_id 并打 batch_append 标记(tasks 接口回
rerun=True)—— 守护「重跑不裂新批次卡 + 失败格计数不膨胀」;非法 batch_id 则铸新批次兜底。"""
CreditAccount.objects.create(team=self.team, balance="100.0000")
patch("apps.ai.tasks.generate_standalone_image_task.delay").start()
self.addCleanup(patch.stopall)
first = self.client.post(
"/api/ai/generate-image/", {"prompt": "一只猫", "mode": "image", "count": 2}, format="json"
)
self.assertEqual(first.status_code, 202, first.content)
conv_id = first.json()["conversation_id"]
batch_id = first.json()["batch_id"]
self.assertTrue(batch_id)
# 重跑一张:带原对话 + 原 batch_id → 归回原批次
second = self.client.post(
"/api/ai/generate-image/",
{"prompt": "一只猫", "mode": "image", "count": 1, "conversation_id": conv_id, "batch_id": batch_id},
format="json",
)
self.assertEqual(second.status_code, 202, second.content)
self.assertEqual(second.json()["batch_id"], batch_id)
tasks = self.client.get(f"/api/ai/image-conversations/{conv_id}/tasks/").json()["tasks"]
self.assertEqual(len(tasks), 3)
self.assertTrue(all(t["batch_id"] == batch_id for t in tasks))
self.assertEqual([t["rerun"] for t in tasks], [False, False, True])
# 非法 batch_id(不是 UUID):不沿用,铸新批次,不打 append 标记
third = self.client.post(
"/api/ai/generate-image/",
{"prompt": "一只猫", "mode": "image", "count": 1, "conversation_id": conv_id, "batch_id": "not-a-uuid"},
format="json",
)
self.assertEqual(third.status_code, 202, third.content)
self.assertNotEqual(third.json()["batch_id"], batch_id)
newest = self.client.get(f"/api/ai/image-conversations/{conv_id}/tasks/").json()["tasks"][-1]
self.assertFalse(newest["rerun"])
class StandaloneCategoryTests(TestCase):