全能创作:换款双形态与素材闸门防跳过

本地上传优先于商品库推荐;点击换款支持只出手/角色日常;角色未锁定禁止进核心卖点,「你来推荐」空库不再假完成。
This commit is contained in:
Azmat@qq.com
2026-09-20 09:28:31 +08:00
parent b5d970a4b5
commit f2e8a5f3c1
5 changed files with 638 additions and 67 deletions
+263 -44
View File
@@ -39,12 +39,15 @@ from .creation_agent import (
_ASSET_CARD_LABELS,
_RESTART_CONTINUATION,
apply_cast_relation_choice,
apply_click_swap_mode,
append_person_source_gate,
apply_pain_point_direction,
apply_confirm_params,
apply_restart_intent,
apply_session_params,
emit_prompt_gate,
emit_final_confirm_gate,
locked_product_references,
set_plot_twist_story_depth,
is_greeting,
is_pain_point_conversation,
@@ -289,6 +292,7 @@ def _store_click_swap_sequence(conversation: CreationConversation, value: str) -
)
def _mark_product_source_resolved(conversation: CreationConversation) -> None:
"""用户选择跳过、自动推荐或直接描述商品后,不重复弹同一个商品闸门。"""
memory = dict(conversation.memory or {})
@@ -297,6 +301,83 @@ def _mark_product_source_resolved(conversation: CreationConversation) -> None:
conversation.save(update_fields=["memory", "updated_at"])
def _is_product_like_ref(ref: dict) -> bool:
"""商品库商品,或本地上传的商品图(排除人物角色/模特)。"""
if not isinstance(ref, dict):
return False
type_ = str(ref.get("type") or "").strip()
ref_id = str(ref.get("id") or "").strip()
if not ref_id:
return False
if type_ == "product":
return True
if type_ != "asset":
return False
category = str(ref.get("category") or "").strip().lower()
return category not in {"character", "person", "model"}
def _dedupe_refs(refs: list[dict]) -> list[dict]:
out: list[dict] = []
seen: set[tuple[str, str]] = set()
for ref in refs or []:
if not _is_product_like_ref(ref):
continue
mark = (str(ref.get("type")), str(ref.get("id")))
if mark in seen:
continue
seen.add(mark)
out.append(ref)
return out
def _session_product_refs(conversation: CreationConversation, request_refs=None) -> list[dict]:
"""优先收集本会话已上传/已锁定的商品图,避免「你来推荐」盲取商品库最新一条。"""
buckets: list[list] = [list(request_refs or []), list(locked_product_references(conversation))]
recent = (
conversation.messages.filter(role=CreationMessage.Role.USER)
.order_by("-seq")[:12]
)
for message in recent:
buckets.append(list(message.refs or []))
merged: list[dict] = []
for bucket in buckets:
merged.extend(bucket)
return _dedupe_refs(merged)
def _ref_display_name(ref: dict) -> str:
name = str(ref.get("name") or "").split(" · ")[0].strip()
if name and not name.lower().endswith((".png", ".jpg", ".jpeg", ".webp", ".gif")):
return name
return "已上传商品图"
def _auto_pick_product_continuation(conversation: CreationConversation, request_refs=None):
"""「你来推荐」:有本地上传/已锁定商品图时用它们;否则才回落到商品库检索。"""
preferred = _session_product_refs(conversation, request_refs)
if preferred:
pin_refs(conversation, preferred)
names = "、".join(_ref_display_name(item) for item in preferred[:6])
return preferred, (
f"用户已提供商品参考图({names})。必须基于这些上传图推进创作,"
"禁止改用商品库里的其他商品(例如库里最新一条)。"
"若品牌或具体品名仍不明确,先确认品牌与品名,再继续方案。"
)
hits = search_mentions(conversation.team, q="", types=["product"], limit=1)
if hits:
pin_refs(conversation, hits)
return hits, (
f"用户希望你帮忙挑选素材,会话里尚无本地上传商品图,已从商品库选定【{hits[0]['name']}】。"
"直接基于该素材推进创作方案,不要复述选项,不要重复追问。"
)
return [], (
"用户希望你帮忙定素材,目前会话无本地上传商品图,商品库也暂无可用商品。"
"请结合所选预设与合理电商默认设想一款匹配的商品继续推进创作方案,不要重复追问。"
)
_STEP_CONTINUE_INSTRUCTIONS = {
"strategy": (
"用户已确认创作策略。现在只调用 write_plan 写方案卡(含完整 video_prompt 存档);"
@@ -1921,6 +2002,24 @@ class CreationConversationViewSet(TeamScopedViewSetMixin, ModelViewSet):
continuation_instruction = _plot_twist_direction_continuation(
conversation, payload, choice
)
elif payload.get("interaction") == "click_swap_mode_gate":
raw = text.strip()
mode = ""
if re.search(r"只出手|手指|不用角色|不要角色|无角色", raw):
mode = "finger"
elif re.search(r"角色|日常|拟人|出镜人物|达人", raw):
mode = "character"
instruction = apply_click_swap_mode(conversation, mode) if mode else ""
if instruction:
payload["answers"] = {"click_swap_mode": mode}
payload["submitted"] = True
payload["answered_via"] = "chat"
pending.payload = payload
pending.save(update_fields=["payload", "updated_at"])
text = ""
record_user_message = False
force_creative_turn = True
continuation_instruction = instruction
elif payload.get("interaction") == "click_swap_sku_gate":
sequence = text.strip()
if sequence:
@@ -1977,40 +2076,83 @@ class CreationConversationViewSet(TeamScopedViewSetMixin, ModelViewSet):
],
}, status=200)
# 2. 用户说「你帮我选/你定/随便」
elif re.search(r"(你来定|你定|你帮我定|你帮我选|帮我挑|没想好|随便|都行|你挑)", text):
hits = search_mentions(conversation.team, q="", types=asset_types, limit=1)
chosen_name = hits[0]["name"] if hits else "推荐商品"
if hits:
mark = (hits[0].get("type"), str(hits[0].get("id")))
existing = {(item.get("type"), str(item.get("id"))) for item in refs if isinstance(item, dict)}
if mark not in existing:
refs.append(hits[0])
payload["answers"] = {"_asset_gate": "auto", str(primary_field.get("key") or "product"): chosen_name}
payload["submitted"] = True
payload["answered_via"] = "chat"
pending.payload = payload
pending.save(update_fields=["payload", "updated_at"])
# 2. 用户说「你帮我选/你定/随便/你来推荐」
elif re.search(r"(你来定|你定|你帮我定|你帮我选|帮我挑|没想好|随便|都行|你挑|你来推荐|推荐一款|推荐一个)", text):
if is_product_gate:
preferred, continuation_instruction = _auto_pick_product_continuation(
conversation, refs
)
chosen_name = (
_ref_display_name(preferred[0]) if preferred else "推荐商品"
)
for item in preferred:
mark = (item.get("type"), str(item.get("id")))
existing = {
(r.get("type"), str(r.get("id")))
for r in refs if isinstance(r, dict)
}
if mark not in existing:
refs.append(item)
payload["answers"] = {
"_asset_gate": "auto",
str(primary_field.get("key") or "product"): chosen_name,
}
payload["submitted"] = True
payload["answered_via"] = "chat"
pending.payload = payload
pending.save(update_fields=["payload", "updated_at"])
_mark_product_source_resolved(conversation)
is_character_gate = any(t in ("character", "model") for t in asset_types)
if is_character_gate:
memory = dict(conversation.memory or {})
memory["person_source"] = "auto"
memory["person_source_ready"] = True
memory["person_source_pending"] = False
conversation.memory = memory
conversation.save(update_fields=["memory", "updated_at"])
force_creative_turn = True
if hits:
continuation_instruction = (
f"用户让你帮忙挑选,系统已为你选定【{hits[0]['name']}】。直接基于该素材推进创作方案,不要复述选项,不要重复追问。"
)
force_creative_turn = True
else:
continuation_instruction = (
"用户让你帮忙定素材,目前素材库暂无已上传素材。请结合所选预设与合理电商默认设想一款匹配的商品继续推进创作方案,不要重复追问。"
)
hits = search_mentions(conversation.team, q="", types=asset_types, limit=1)
chosen_name = hits[0]["name"] if hits else "推荐素材"
if hits:
mark = (hits[0].get("type"), str(hits[0].get("id")))
existing = {(item.get("type"), str(item.get("id"))) for item in refs if isinstance(item, dict)}
if mark not in existing:
refs.append(hits[0])
pin_refs(conversation, [hits[0]])
payload["answers"] = {"_asset_gate": "auto", str(primary_field.get("key") or "product"): chosen_name}
payload["submitted"] = True
payload["answered_via"] = "chat"
pending.payload = payload
pending.save(update_fields=["payload", "updated_at"])
is_character_gate = any(t in ("character", "model") for t in asset_types)
if is_character_gate and hits:
memory = dict(conversation.memory or {})
memory["person_source"] = "auto"
memory["person_source_ready"] = True
memory["person_source_pending"] = False
conversation.memory = memory
conversation.save(update_fields=["memory", "updated_at"])
if hits:
force_creative_turn = True
continuation_instruction = (
f"用户让你帮忙挑选,系统已为你选定【{hits[0]['name']}】。"
"直接基于该素材推进创作方案,不要复述选项,不要再次 ask_user 追问同一类素材。"
)
elif is_character_gate:
memory = dict(conversation.memory or {})
memory.pop("person_source_ready", None)
memory.pop("person_source_pending", None)
memory["person_source"] = ""
conversation.memory = memory
conversation.save(update_fields=["memory", "updated_at"])
gate = append_person_source_gate(conversation)
conversation.agent_status = CreationConversation.AgentStatus.AWAITING_USER
conversation.save(update_fields=["agent_status", "updated_at"])
return JsonResponse({
"conversation_id": str(conversation.id),
"agent_status": conversation.agent_status,
"messages": [CreationMessageSerializer(gate).data],
}, status=200)
else:
force_creative_turn = True
continuation_instruction = (
"用户让你帮忙定素材,目前素材库暂无现成项。"
"按用户刚说的外观要求继续创作;禁止再次弹出同类「发列表 / 你来推荐」追问。"
)
# 3. 用户说「先不用/不选了/跳过」
elif re.search(r"(先不用|不用|不选|先不选|跳过|暂不|没有商品|不需要)", text):
@@ -2274,6 +2416,10 @@ class CreationConversationViewSet(TeamScopedViewSetMixin, ModelViewSet):
)
if not valid:
return JsonResponse({"detail": "请先从模特库选择一位人物"}, status=400)
if payload.get("interaction") == "click_swap_mode_gate":
mode = str(answers.get("click_swap_mode") or "").strip()
if mode not in {"finger", "character"}:
return JsonResponse({"detail": "请选择只出手指出镜,或角色日常换款"}, status=400)
if payload.get("interaction") == "click_swap_sku_gate":
sequence = str(answers.get("sku_sequence") or "").strip()
if not sequence:
@@ -2349,6 +2495,15 @@ class CreationConversationViewSet(TeamScopedViewSetMixin, ModelViewSet):
else "商家选择系统推荐卖点。现在只调用 write_strategy 写创作策略;"
"从商品资料和现有素材中挑一个最容易被画面证明的真实核心卖点,不要虚构功效、价格或规格。"
)
elif payload.get("interaction") == "click_swap_mode_gate":
mode = str(answers.get("click_swap_mode") or "").strip()
instruction = apply_click_swap_mode(conversation, mode)
if not instruction:
return JsonResponse({"detail": "请选择只出手指出镜,或角色日常换款"}, status=400)
text = ""
record_user_message = False
force_creative_turn = True
continuation_instruction = instruction
elif payload.get("interaction") == "click_swap_sku_gate":
sequence = str(answers.get("sku_sequence") or "").strip()
text = ""
@@ -2477,30 +2632,94 @@ class CreationConversationViewSet(TeamScopedViewSetMixin, ModelViewSet):
"messages": [CreationMessageSerializer(pick).data],
}, status=200)
elif choice == "auto":
hits = search_mentions(conversation.team, q="", types=gate_types, limit=1)
if hits:
if is_product_gate:
preferred, continuation_instruction = _auto_pick_product_continuation(
conversation, refs
)
refs = list(refs)
refs.append(hits[0])
existing = {
(item.get("type"), str(item.get("id")))
for item in refs if isinstance(item, dict)
}
for item in preferred:
mark = (item.get("type"), str(item.get("id")))
if mark not in existing:
refs.append(item)
existing.add(mark)
_mark_product_source_resolved(conversation)
else:
hits = search_mentions(conversation.team, q="", types=gate_types, limit=1)
is_character_gate = any(t in ("character", "model") for t in gate_types)
if hits:
refs = list(refs)
refs.append(hits[0])
pin_refs(conversation, [hits[0]])
continuation_instruction = (
f"用户希望你帮忙挑选素材,已为你选定【{hits[0]['name']}】。"
"直接基于该素材推进创作方案,不要复述选项,不要再次 ask_user 追问同一类素材。"
)
else:
if is_character_gate:
# 库里没有角色却点了「你来推荐」:不能假完成,否则会跳到核心卖点。
# 改走人物来源闸门(上传 / 模特库 / 平台生成)。
memory = dict(conversation.memory or {})
memory.pop("person_source_ready", None)
memory.pop("person_source_pending", None)
memory["person_source"] = ""
conversation.memory = memory
conversation.save(update_fields=["memory", "updated_at"])
gate = append_person_source_gate(conversation)
conversation.agent_status = CreationConversation.AgentStatus.AWAITING_USER
conversation.save(update_fields=["agent_status", "updated_at"])
return JsonResponse({
"conversation_id": str(conversation.id),
"agent_status": conversation.agent_status,
"messages": [CreationMessageSerializer(gate).data],
}, status=200)
continuation_instruction = (
"用户希望你帮忙定素材,目前素材库暂无现成项。"
"按用户刚说的外观要求继续创作;不要再弹同一类「发列表 / 你来推荐」。"
)
if is_character_gate and hits:
memory = dict(conversation.memory or {})
memory["person_source"] = "auto"
memory["person_source_ready"] = True
memory["person_source_pending"] = False
conversation.memory = memory
conversation.save(update_fields=["memory", "updated_at"])
text = ""
record_user_message = False
force_creative_turn = True
elif choice == "upload":
# 闸门「上传商品图」:用本轮 refs / 会话已上传图,禁止再去商品库抽奖。
preferred = _session_product_refs(conversation, refs)
if preferred:
pin_refs(conversation, preferred)
refs = list(refs)
existing = {
(item.get("type"), str(item.get("id")))
for item in refs if isinstance(item, dict)
}
for item in preferred:
mark = (item.get("type"), str(item.get("id")))
if mark not in existing:
refs.append(item)
existing.add(mark)
names = "、".join(_ref_display_name(item) for item in preferred[:6])
continuation_instruction = (
f"用户希望你帮忙挑选素材,已为你选定【{hits[0]['name']}】。直接基于该素材推进创作方案,不要复述选项,不要重复追问。"
f"用户刚上传了商品参考图({names})。必须基于这些上传图推进创作,"
"禁止改用商品库里的其他商品。若品牌或具体品名不明确,先确认品牌与品名。"
)
else:
continuation_instruction = (
"用户希望你帮忙定素材,目前素材库暂无已上传素材。请结合所选预设与合理电商默认设想一款匹配的商品继续推进创作方案,不要重复追问。"
"用户选择上传商品图,但本轮尚未收到可用图片。"
"请提醒用户再传一张清晰的商品实物图,不要改从商品库挑选。"
)
text = ""
record_user_message = False
force_creative_turn = True
if is_product_gate:
_mark_product_source_resolved(conversation)
is_character_gate = any(t in ("character", "model") for t in gate_types)
if is_character_gate:
memory = dict(conversation.memory or {})
memory["person_source"] = "auto"
memory["person_source_ready"] = True
memory["person_source_pending"] = False
conversation.memory = memory
conversation.save(update_fields=["memory", "updated_at"])
else:
# 卡片本身已经记录了用户的选择。不要再伪造一条黑色用户气泡;
# 继续原任务,并明确告诉模型不要再次追问同一项素材。