优化全能创作部分优化脚本

This commit is contained in:
Azmat@qq.com
2026-09-10 18:59:08 +08:00
parent 0d56fdb299
commit 43f80a4d90
12 changed files with 1223 additions and 95 deletions
+179 -17
View File
@@ -1,4 +1,5 @@
import logging
import re
import uuid
from django.db import transaction
@@ -21,7 +22,7 @@ from apps.products.models import Product
from .generation_errors import classify_generation_error, public_error_for_task
from .creation import append_message, sync_generating_messages
from .creation_agent import apply_confirm_params, apply_session_params, stream_creation_agent, submit_confirmed_image, submit_confirmed_video
from .creation_agent import apply_confirm_params, apply_session_params, stream_creation_agent, submit_confirmed_image, submit_confirmed_video, _sse, _message_payload
from .mentions import TYPE_LABELS, VALID_TYPES, refs_from_elicit_answers, search_mentions
from .models import AITask, CreationConversation, CreationMessage, ImageConversation, ModelConfig
from .serializers import (
@@ -38,6 +39,84 @@ from .services import enqueue_standalone_images
logger = logging.getLogger(__name__)
def _normalise_chat_choice(value: str) -> str:
return re.sub(r"[^0-9a-zA-Z一-鿿]+", "", str(value or "")).lower()
def _chat_answer_for_field(field: dict, text: str, team=None):
"""把自然输入映射回字段值;认不出时保留原话,让 Agent 自己理解。"""
raw = str(text or "").strip()
if field.get("type") == "asset" and team is not None:
answer_norm = _normalise_chat_choice(raw)
types = [item for item in (field.get("asset_types") or []) if item in TYPE_LABELS] or None
hits = search_mentions(team, q="", types=types, limit=50)
if re.search(r"(你来定|你定|你帮我定|你帮我选|帮我挑|没想好|随便|都行)", raw):
# 用户把选择权交给 Agent 时静默选一个可用素材,不把整份库甩回聊天区。
if hits:
return str(hits[0].get("name") or raw)
contained = [
hit for hit in hits
if _normalise_chat_choice(hit.get("name") or "")
and _normalise_chat_choice(hit.get("name") or "") in answer_norm
]
if contained:
# 「就用净颜精华吧」也能绑定净颜精华;同名片段优先取名字更完整的。
contained.sort(key=lambda hit: len(_normalise_chat_choice(hit.get("name") or "")), reverse=True)
return str(contained[0].get("name") or raw)
if field.get("type") != "single":
if field.get("type") == "multi":
answer_norm = _normalise_chat_choice(raw)
matched = []
for option in field.get("options") or []:
value = str(option.get("value") or "")
label = str(option.get("label") or value)
token = _normalise_chat_choice(label)
if token and token in answer_norm:
matched.append(value)
return matched or [raw]
return raw
options = [item for item in (field.get("options") or []) if isinstance(item, dict)]
answer_norm = _normalise_chat_choice(raw)
for option in sorted(
options,
key=lambda item: len(_normalise_chat_choice(item.get("label") or item.get("value") or "")),
reverse=True,
):
value = str(option.get("value") or "")
label_norm = _normalise_chat_choice(option.get("label") or value)
value_norm = _normalise_chat_choice(value)
if answer_norm in {label_norm, value_norm} or (
label_norm and label_norm in answer_norm
) or (
value_norm and value_norm in answer_norm
):
return value
# 「10」也能命中「10 秒」。同一个数字对应多个模型名时不擅自猜。
answer_digits = re.findall(r"\d+(?:\.\d+)?", raw)
numeric_matches = []
if answer_digits:
for option in options:
label = str(option.get("label") or option.get("value") or "")
if re.findall(r"\d+(?:\.\d+)?", label) == answer_digits:
numeric_matches.append(str(option.get("value") or label))
if len(numeric_matches) == 1:
return numeric_matches[0]
return raw
def _pending_chat_question(conversation: CreationConversation) -> CreationMessage | None:
"""找最近一条未回答的新式追问;卡片既可点选,也兼容直接输入。"""
candidates = conversation.messages.filter(
kind=CreationMessage.Kind.ELICIT
).order_by("-seq")[:20]
for message in candidates:
payload = message.payload or {}
if payload.get("interaction") in {"chat", "asset_picker"} and not payload.get("submitted"):
return message
return None
class GenerateImageView(APIView):
"""独立生图(不绑项目)· 图片创作/模特图/平台套图共用 —— **异步**。
@@ -1380,9 +1459,48 @@ class CreationConversationViewSet(TeamScopedViewSetMixin, ModelViewSet):
kind = str(request.data.get("kind") or "text")
text = str(request.data.get("text") or "").strip()
refs = request.data.get("refs") or []
record_user_message = True
force_creative_turn = False
continuation_instruction = ""
if not isinstance(refs, list):
return JsonResponse({"detail": "refs 必须是数组"}, status=400)
if kind == "text" and text:
pending = _pending_chat_question(conversation)
if pending is not None:
payload = dict(pending.payload or {})
fields = [item for item in (payload.get("fields") or []) if isinstance(item, dict)]
if fields:
field = fields[0]
answers = {
str(field.get("key") or "answer"):
_chat_answer_for_field(field, text, conversation.team)
}
payload["answers"] = answers
payload["submitted"] = True
payload["answered_via"] = "chat"
pending.payload = payload
pending.save(update_fields=["payload", "updated_at"])
params_changed = apply_session_params(conversation, fields, answers)
existing = {
(item.get("type"), str(item.get("id")))
for item in refs if isinstance(item, dict)
}
for extra in refs_from_elicit_answers(conversation.team, fields, answers):
mark = (extra.get("type"), str(extra.get("id")))
if mark not in existing:
refs.append(extra)
existing.add(mark)
force_creative_turn = True
continuation_instruction = (
"用户刚用输入框回答了你上一句问题。直接基于这条真实回答继续原任务;"
"不要复述答案,不要回复收到,也不要问要不要继续或要不要生成。"
)
if params_changed:
continuation_instruction += " 会话参数已更新,旧方案作废,按新参数重新产出。"
if kind == "confirm":
reply_to = str(request.data.get("reply_to") or "").strip()
card = conversation.messages.filter(
@@ -1451,22 +1569,63 @@ class CreationConversationViewSet(TeamScopedViewSetMixin, ModelViewSet):
payload["submitted"] = True
card.payload = payload
card.save(update_fields=["payload", "updated_at"])
labels = {f["key"]: f["label"] for f in payload.get("fields", [])}
text = "".join(
f"{labels.get(k, k)}{''.join(v) if isinstance(v, list) else v}"
for k, v in answers.items()
)
if apply_session_params(conversation, payload.get("fields") or [], answers):
text = f"{text}。请按新的会话参数重新写方案,旧方案作废"
# 点选商品/角色必须钉成 Ref:模型常把选项做成单选文字,前端只回 answers。
refs = list(refs)
existing = {(item.get("type"), str(item.get("id"))) for item in refs if isinstance(item, dict)}
for extra in refs_from_elicit_answers(conversation.team, payload.get("fields") or [], answers):
mark = (extra.get("type"), str(extra.get("id")))
if mark in existing:
continue
refs.append(extra)
existing.add(mark)
# 素材选择闸门:用户愿意时再展开列表;跳过则直接沿原任务继续。
if payload.get("phase") == "gate":
choice = str(answers.get("_asset_gate") or "").strip()
if choice == "send":
pending = payload.get("pending_fields") or []
pick = append_message(
conversation,
role="assistant",
kind=CreationMessage.Kind.ELICIT,
payload={
"phase": "pick",
"fields": pending,
"submitted": False,
"answers": {},
},
)
def _gate_send_stream():
yield _sse({"type": "message", "message": _message_payload(pick)})
yield _sse({"type": "done"})
response = StreamingHttpResponse(
_gate_send_stream(), content_type="text/event-stream"
)
response["Cache-Control"] = "no-cache"
response["X-Accel-Buffering"] = "no"
return response
# 卡片本身已经记录了用户的选择。不要再伪造一条黑色用户气泡;
# 继续原任务,并明确告诉模型不要再次追问同一项素材。
text = ""
record_user_message = False
force_creative_turn = True
continuation_instruction = (
"用户刚选择暂不添加这项素材。接受这个选择,按会话里已有的需求和合理默认继续原任务。"
"先用一句自然的话承接,随后直接推进创作;不要再次追问同一素材,不要只说收到或有需要再说。"
)
else:
params_changed = apply_session_params(conversation, payload.get("fields") or [], answers)
# 点选商品/角色必须钉成 Ref:模型常把选项做成单选文字,前端只回 answers。
refs = list(refs)
existing = {(item.get("type"), str(item.get("id"))) for item in refs if isinstance(item, dict)}
for extra in refs_from_elicit_answers(conversation.team, payload.get("fields") or [], answers):
mark = (extra.get("type"), str(extra.get("id")))
if mark in existing:
continue
refs.append(extra)
existing.add(mark)
# 答案已显示在追问卡里,不再复制成一条用户消息。让 Agent 直接往下做。
text = ""
record_user_message = False
force_creative_turn = True
continuation_instruction = (
"用户已经完成刚才的选择。直接基于卡片答案继续原任务,不要复述选项,不要只回复收到。"
)
if params_changed:
continuation_instruction += " 会话参数已经更新,旧方案作废,按新参数重新产出方案。"
elif not text and not refs:
return JsonResponse({"detail": "消息不能为空"}, status=400)
@@ -1485,6 +1644,9 @@ class CreationConversationViewSet(TeamScopedViewSetMixin, ModelViewSet):
text=text,
refs=refs,
model_config=model_config,
record_user_message=record_user_message,
force_creative_turn=force_creative_turn,
continuation_instruction=continuation_instruction,
)
response = StreamingHttpResponse(stream, content_type="text/event-stream")
response["Cache-Control"] = "no-cache"