feat(chat): 支持流式回复与用户节点导航

This commit is contained in:
2026-06-05 00:11:53 +08:00
parent 84e045f5ab
commit 7ab5aad938
8 changed files with 676 additions and 15 deletions

View File

@@ -1,9 +1,11 @@
from __future__ import annotations
import json
from django.db.models import Q, QuerySet
from django.utils import timezone
from .llm import LLMConfigurationError, LLMRequestError, generate_reply
from .llm import LLMConfigurationError, LLMRequestError, generate_reply, stream_reply
from .models import Conversation, Message
@@ -81,6 +83,47 @@ def send_message(conversation: Conversation, content: str) -> tuple[Message, Mes
return user_message, assistant_message
def stream_message(conversation: Conversation, content: str):
"""Yields SSE events while collecting a streamed assistant reply."""
user_message = append_user_message(conversation, content)
assistant_parts: list[str] = []
yield sse_event(
"meta",
{
"conversation_id": conversation.pk,
"title": conversation.title or build_conversation_title(content),
"user_message_id": user_message.pk,
"user_message": user_message.content,
},
)
try:
for chunk in stream_reply(conversation, content):
assistant_parts.append(chunk)
yield sse_event("chunk", {"delta": chunk})
except (LLMConfigurationError, LLMRequestError) as exc:
fallback = f"模型调用失败:{exc}"
assistant_parts = [fallback]
yield sse_event("error", {"message": fallback})
assistant_message = append_assistant_message(conversation, "".join(assistant_parts).strip())
if conversation.title.startswith("新对话"):
conversation.title = build_conversation_title(content)
conversation.save(update_fields=["title", "updated_at"])
yield sse_event(
"done",
{
"assistant_message_id": assistant_message.pk,
"conversation_id": conversation.pk,
"title": conversation.title,
},
)
def build_conversation_title(content: str) -> str:
"""Creates a concise title from the first user message."""
@@ -88,3 +131,9 @@ def build_conversation_title(content: str) -> str:
if not normalized:
return "新对话"
return normalized[:24]
def sse_event(event_name: str, payload: dict[str, object]) -> str:
"""Formats one server-sent event frame."""
return f"event: {event_name}\ndata: {json.dumps(payload, ensure_ascii=False)}\n\n"