Files
jiaoyu/backend/routers/chat.py
T
Zhang Jing Xuan 0841b1a103 智教助手平台:完整初始化
- 前端:Vue3 + TS + Element Plus,24 个页面路由(课件/组题/教案/动画/思维导图/作文批改/命题/课堂/资源/社区等)
- 后端:FastAPI + SQLAlchemy + SQLite,17 个路由模块,AI 服务层含降级模板
- AI:glm-5.x 推理模型已禁用思维链,确保输出真实内容
- 修复:ai_service 两处请求体注入 enable_thinking/thinking disabled
- 测试账号:13900999999 / test1234
2026-07-29 16:29:10 +08:00

185 lines
6.1 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlalchemy.orm import Session
from database import get_db
from models.chat import ChatConversation, ChatMessage
from models.user import User
from schemas.chat import ConversationCreate, ConversationRename, ChatSend, MessageOut, ConversationOut
from services.credits import credits_payload, spend_credits
from services.limiter import limiter
from services.auth import get_current_user
from services.audit import log_action
from services.ai_service import AIService
router = APIRouter(prefix="/api/chat", tags=["AI教学助手"])
ai_service = AIService()
# 历史上下文窗口:最近 N 轮(user+assistant 计为 2 条),避免 token 超限
MAX_HISTORY_TURNS = 12
MAX_TITLE_LEN = 60
def _ensure_owned(db: Session, conversation_id: int, user: User) -> ChatConversation:
item = db.query(ChatConversation).filter(
ChatConversation.id == conversation_id,
ChatConversation.user_id == user.id,
).first()
if not item:
raise HTTPException(status_code=404, detail="对话不存在")
return item
def _derive_title(text: str) -> str:
cleaned = " ".join(text.split())
return (cleaned[:MAX_TITLE_LEN] + "…") if len(cleaned) > MAX_TITLE_LEN else (cleaned or "新对话")
@router.get("/conversations", response_model=list[ConversationOut])
def list_conversations(
keyword: str | None = None,
skip: int = Query(0, ge=0),
limit: int = Query(30, ge=1, le=100),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
query = db.query(ChatConversation).filter(ChatConversation.user_id == current_user.id)
if keyword:
query = query.filter(ChatConversation.title.contains(keyword))
return (
query.order_by(ChatConversation.pinned.desc(), ChatConversation.updated_at.desc())
.offset(skip).limit(limit).all()
)
@router.post("/conversations", response_model=ConversationOut, status_code=201)
def create_conversation(
data: ConversationCreate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
item = ChatConversation(
user_id=current_user.id,
title=(data.title or "新对话").strip() or "新对话",
subject=data.subject,
grade=data.grade,
)
db.add(item)
db.commit()
db.refresh(item)
return item
@router.get("/conversations/{conversation_id}", response_model=ConversationOut)
def get_conversation(
conversation_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
return _ensure_owned(db, conversation_id, current_user)
@router.put("/conversations/{conversation_id}", response_model=ConversationOut)
def rename_conversation(
conversation_id: int,
data: ConversationRename,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
item = _ensure_owned(db, conversation_id, current_user)
item.title = data.title.strip() or item.title
if data.pinned is not None:
item.pinned = data.pinned
db.commit()
db.refresh(item)
return item
@router.delete("/conversations/{conversation_id}", status_code=204)
def delete_conversation(
conversation_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
item = _ensure_owned(db, conversation_id, current_user)
db.delete(item)
db.commit()
return None
@router.post("/conversations/{conversation_id}/messages", response_model=dict)
@limiter.limit("20/minute")
async def send_message(
request: Request,
conversation_id: int,
data: ChatSend,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
conversation = _ensure_owned(db, conversation_id, current_user)
account = spend_credits(db, current_user, "chat_generate", "AI教学助手对话")
db.add(ChatMessage(
conversation_id=conversation.id,
role="user",
content=data.content,
))
db.flush()
history = (
db.query(ChatMessage)
.filter(ChatMessage.conversation_id == conversation.id)
.order_by(ChatMessage.id.desc())
.limit(MAX_HISTORY_TURNS)
.all()
)
history = list(reversed(history))
history_payload = [{"role": m.role, "content": m.content} for m in history[:-1]] if history else []
try:
reply = await ai_service.chat_teaching(
history=history_payload,
user_message=data.content,
subject=data.subject or conversation.subject,
grade=data.grade or conversation.grade,
)
except RuntimeError as exc:
msg = str(exc)
if "CONNECT" in msg or "TIMEOUT" in msg:
reply = (
"无法连接到 AI 服务,请检查网络或稍后重试。\n"
"你的问题我已记录,服务恢复后重新发送即可继续对话。"
)
elif "EMPTY" in msg:
reply = (
"AI 模型本次未返回内容(推理模型可能因思考超时导致空回复),请稍后重试或换一种问法。"
)
else:
reply = (
"当前 AI 服务暂时不可用,请稍后重试。我已经记录了你的问题,"
"服务恢复后再次发送即可继续对话。"
)
assistant_msg = ChatMessage(
conversation_id=conversation.id,
role="assistant",
content=reply,
)
db.add(assistant_msg)
first_message = len(conversation.messages) <= 2
if conversation.title == "新对话" or first_message:
conversation.title = _derive_title(data.content)
db.commit()
db.refresh(assistant_msg)
log_action(
db, action="chat_send", user=current_user, request=request,
target_type="chat_conversation", target_id=conversation.id,
detail=f"AI助手对话({len(data.content)}字)",
)
return {
"success": True,
"data": MessageOut.model_validate(assistant_msg).model_dump(),
"title": conversation.title,
"credits": credits_payload(account, "chat_generate"),
}