智教助手平台:完整初始化
- 前端:Vue3 + TS + Element Plus,24 个页面路由(课件/组题/教案/动画/思维导图/作文批改/命题/课堂/资源/社区等) - 后端:FastAPI + SQLAlchemy + SQLite,17 个路由模块,AI 服务层含降级模板 - AI:glm-5.x 推理模型已禁用思维链,确保输出真实内容 - 修复:ai_service 两处请求体注入 enable_thinking/thinking disabled - 测试账号:13900999999 / test1234
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
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"),
|
||||
}
|
||||
Reference in New Issue
Block a user