优化全能创作部分优化脚本
This commit is contained in:
+179
-17
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user