新增前后端AI聊天模块,包括流式对话、配置管理和UI组件优化。
This commit is contained in:
132
backend/app/routers/chat.py
Normal file
132
backend/app/routers/chat.py
Normal file
@@ -0,0 +1,132 @@
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlmodel import Session, select, func
|
||||
|
||||
from ..database import get_session, engine
|
||||
from ..models import AIConfig, ChatMessage
|
||||
from ..schemas import ChatRequest, ChatMessageRead, ChatMessageList
|
||||
from ..services.ai_provider import stream_chat
|
||||
|
||||
router = APIRouter(prefix="/api/chat", tags=["chat"])
|
||||
|
||||
|
||||
@router.post("/stream")
|
||||
async def chat_stream(
|
||||
data: ChatRequest,
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
"""SSE 流式对话端点"""
|
||||
config = session.exec(select(AIConfig)).first()
|
||||
if not config or not config.api_key:
|
||||
return StreamingResponse(
|
||||
_error_stream("请先配置AI服务"),
|
||||
media_type="text/event-stream",
|
||||
)
|
||||
|
||||
# 保存用户消息
|
||||
user_msg = data.messages[-1] if data.messages else None
|
||||
if user_msg and user_msg.role == "user":
|
||||
db_msg = ChatMessage(
|
||||
novel_id=data.novel_id,
|
||||
role="user",
|
||||
content=user_msg.content,
|
||||
)
|
||||
session.add(db_msg)
|
||||
session.commit()
|
||||
|
||||
# 构建消息列表
|
||||
messages = [{"role": m.role, "content": m.content} for m in data.messages]
|
||||
|
||||
# 构建上下文摘要(前端展示用)
|
||||
context_preview = [{"role": m["role"], "content": m["content"][:200]} for m in messages]
|
||||
|
||||
async def generate():
|
||||
# 先发送上下文预览
|
||||
yield f"data: {json.dumps({'context': context_preview})}\n\n"
|
||||
|
||||
full_response = ""
|
||||
usage_info = {}
|
||||
try:
|
||||
async for event in stream_chat(config, messages):
|
||||
if event.text:
|
||||
full_response += event.text
|
||||
yield f"data: {json.dumps({'content': event.text})}\n\n"
|
||||
if event.usage:
|
||||
# 累加usage
|
||||
for k, v in event.usage.items():
|
||||
if v:
|
||||
usage_info[k] = usage_info.get(k, 0) + v
|
||||
except Exception as e:
|
||||
yield f"data: {json.dumps({'error': str(e)})}\n\n"
|
||||
finally:
|
||||
# 保存AI回复
|
||||
if full_response:
|
||||
with Session(engine) as s:
|
||||
ai_msg = ChatMessage(
|
||||
novel_id=data.novel_id,
|
||||
role="assistant",
|
||||
content=full_response,
|
||||
)
|
||||
s.add(ai_msg)
|
||||
s.commit()
|
||||
yield f"data: {json.dumps({'done': True, 'usage': usage_info or None})}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
generate(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def _error_stream(message: str):
|
||||
yield f"data: {json.dumps({'error': message})}\n\n"
|
||||
yield f"data: {json.dumps({'done': True})}\n\n"
|
||||
|
||||
|
||||
@router.get("/messages", response_model=ChatMessageList)
|
||||
def list_messages(
|
||||
novel_id: int | None = Query(default=None),
|
||||
limit: int = Query(default=50, ge=1, le=200),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
query = select(ChatMessage)
|
||||
count_query = select(func.count(ChatMessage.id))
|
||||
if novel_id is not None:
|
||||
query = query.where(ChatMessage.novel_id == novel_id)
|
||||
count_query = count_query.where(ChatMessage.novel_id == novel_id)
|
||||
else:
|
||||
query = query.where(ChatMessage.novel_id.is_(None)) # type: ignore
|
||||
count_query = count_query.where(ChatMessage.novel_id.is_(None)) # type: ignore
|
||||
|
||||
total = session.exec(count_query).one()
|
||||
items = session.exec(
|
||||
query.order_by(ChatMessage.created_at.desc()).limit(limit)
|
||||
).all()
|
||||
# 返回时按时间正序
|
||||
items = list(reversed(items))
|
||||
return ChatMessageList(
|
||||
items=[ChatMessageRead.model_validate(m, from_attributes=True) for m in items],
|
||||
total=total,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/messages", status_code=204)
|
||||
def clear_messages(
|
||||
novel_id: int | None = Query(default=None),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
query = select(ChatMessage)
|
||||
if novel_id is not None:
|
||||
query = query.where(ChatMessage.novel_id == novel_id)
|
||||
else:
|
||||
query = query.where(ChatMessage.novel_id.is_(None)) # type: ignore
|
||||
messages = session.exec(query).all()
|
||||
for msg in messages:
|
||||
session.delete(msg)
|
||||
session.commit()
|
||||
Reference in New Issue
Block a user