diff --git a/chat/.env.example b/chat/.env.example new file mode 100644 index 0000000..f8d1e3c --- /dev/null +++ b/chat/.env.example @@ -0,0 +1,2 @@ +OPENAI_API_KEY=你的API密钥 +DEEPSEEK_API_KEY='你的API密钥' \ No newline at end of file diff --git a/chat/api/v1/ai_chat.py b/chat/api/v1/ai_chat.py index 307f85b..3cf5270 100644 --- a/chat/api/v1/ai_chat.py +++ b/chat/api/v1/ai_chat.py @@ -4,27 +4,20 @@ from fastapi import APIRouter, Depends, Request, Query from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session from typing import List -from datetime import datetime from pydantic import BaseModel, SecretStr from langchain.chains import ConversationChain -from langchain_community.chat_models import ChatOpenAI -from api.v1.vo import MessageVO, ConversationsVO +from api.v1.vo import MessageVO from deps.auth import get_current_user from services.chat_service import ChatDBService from db.session import get_db -from models.ai import ChatConversation, ChatMessage, MessageType +from models.ai import ChatConversation, ChatMessage from utils.resp import resp_success, Response from langchain_deepseek import ChatDeepSeek router = APIRouter() -class ChatRequest(BaseModel): - prompt: str - - - def get_deepseek_llm(api_key: SecretStr, model: str): # deepseek 兼容 OpenAI API,需指定 base_url return ChatDeepSeek( @@ -38,26 +31,22 @@ async def chat_stream(request: Request, user=Depends(get_current_user), db: Sess body = await request.json() content = body.get('content') conversation_id = body.get('conversation_id') - print(content, 'content') model = 'deepseek-chat' api_key = os.getenv("DEEPSEEK_API_KEY") - openai_api_base = "https://api.deepseek.com/v1" - llm = get_deepseek_llm(api_key, model) + llm = get_deepseek_llm(SecretStr(api_key), model) if not content or not isinstance(content, str): from fastapi.responses import JSONResponse return JSONResponse({"error": "content不能为空"}, status_code=400) user_id = user["user_id"] - print(conversation_id, 'conversation_id') - # 1. 获取或新建对话 + # 1. 获取对话 try: - conversation = ChatDBService.get_or_create_conversation(db, conversation_id, user_id, model, content) + conversation = ChatDBService.get_conversation(db, conversation_id) + conversation = db.merge(conversation) # ✅ 防止 DetachedInstanceError except ValueError as e: - print(23232) from fastapi.responses import JSONResponse return JSONResponse({"error": str(e)}, status_code=400) - print(conversation, 'dsds') # 2. 插入当前消息 ChatDBService.add_message(db, conversation, user_id, content) context = [ @@ -65,13 +54,16 @@ async def chat_stream(request: Request, user=Depends(get_current_user), db: Sess ] # 3. 查询历史消息,组装上下文 history = ChatDBService.get_history(db, conversation.id) + # === 新增:如果只有一条消息,更新 title === + if len(history) == 1: + ChatDBService.update_conversation_title(db, conversation.id, content[:255]) + for msg in history: # 假设 msg.type 存储的是 'user' 或 'assistant' # role = msg.type if msg.type in ("user", "assistant") else "user" context.append((msg.type, msg.content)) - print('context', context) - ai_reply = "" + ai_reply = "" async def event_generator(): nonlocal ai_reply async for chunk in llm.astream(context): @@ -88,6 +80,13 @@ async def chat_stream(request: Request, user=Depends(get_current_user), db: Sess return StreamingResponse(event_generator(), media_type='text/event-stream') +@router.post("/conversations") +def create_conversation(db: Session = Depends(get_db), user=Depends(get_current_user),): + user_id = user["user_id"] + model = 'deepseek-chat' + conversation = ChatDBService.get_or_create_conversation(db, None, user_id, model, '新对话') + return resp_success(data=conversation.id) + @router.get('/conversations') async def get_conversations( db: Session = Depends(get_db), diff --git a/chat/crud/ai_api_key.py b/chat/crud/ai_api_key.py deleted file mode 100644 index 1525d3d..0000000 --- a/chat/crud/ai_api_key.py +++ /dev/null @@ -1,18 +0,0 @@ -from fastapi import HTTPException -from sqlalchemy.orm import Session - -from crud.base import CRUDBase -from models.ai import AIApiKey # SQLAlchemy模型 -from schemas.ai_api_key import AIApiKeyCreate, AIApiKeyUpdate - -# 继承通用CRUD基类,指定模型和Pydantic类型 -class CRUDApiKey(CRUDBase[AIApiKey, AIApiKeyCreate, AIApiKeyUpdate]): - # 如有特殊逻辑,可重写父类方法(如创建时验证平台唯一性) - def create(self, db: Session, *, obj_in: AIApiKeyCreate): - # 示例:验证平台+名称唯一 - if self.get_by(db, platform=obj_in.platform, name=obj_in.name): - raise HTTPException(status_code=400, detail="该平台下名称已存在") - return super().create(db, obj_in=obj_in) - -# 创建CRUD实例 -ai_api_key_crud = CRUDApiKey(AIApiKey) \ No newline at end of file diff --git a/chat/main.py b/chat/main.py index 425a90d..7fa9a5e 100644 --- a/chat/main.py +++ b/chat/main.py @@ -3,7 +3,6 @@ from fastapi import FastAPI from dotenv import load_dotenv from api.v1 import ai_chat from fastapi.middleware.cors import CORSMiddleware -from routers.ai_api_key import router as ai_api_key_router # 加载.env环境变量,优先项目根目录 load_dotenv() @@ -25,7 +24,6 @@ app.add_middleware( # 注册路由 app.include_router(ai_chat.router, prefix="/chat/api/v1", tags=["chat"]) -app.include_router(ai_api_key_router, tags=["chat"]) # 健康检查 @app.get("/ping") diff --git a/chat/requirements.txt b/chat/requirements.txt index 4050ff9..02e3c2d 100644 --- a/chat/requirements.txt +++ b/chat/requirements.txt @@ -1,3 +1,6 @@ -fastapi -uvicorn[standard] -langchain-openai \ No newline at end of file +fastapi==0.116.1 +uvicorn[standard]==0.35.0 +langchain-openai==0.3.28 +langchain-deepseek==0.1.3 +langchain==0.3.26 +langchain-community==0.3.26 diff --git a/chat/routers/ai_api_key.py b/chat/routers/ai_api_key.py deleted file mode 100644 index 35b33cc..0000000 --- a/chat/routers/ai_api_key.py +++ /dev/null @@ -1,13 +0,0 @@ -from schemas.ai_api_key import AIApiKeyCreate, AIApiKeyUpdate, AIApiKeyRead -from crud.ai_api_key import ai_api_key_crud -from routers.base import GenericRouter - -# 继承通用路由基类,传入参数即可生成所有CRUD接口 -router = GenericRouter( - crud=ai_api_key_crud, - create_schema=AIApiKeyCreate, - update_schema=AIApiKeyUpdate, - read_schema=AIApiKeyRead, - prefix="/chat/api/ai-api-keys", - tags=["AI API密钥"] -) \ No newline at end of file diff --git a/chat/schemas/ai_chat.py b/chat/schemas/ai_chat.py new file mode 100644 index 0000000..c296902 --- /dev/null +++ b/chat/schemas/ai_chat.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel + +class ChatCreate(BaseModel): + pass + +class Chat(ChatCreate): + id: int + + class Config: + orm_mode = True \ No newline at end of file diff --git a/chat/services/chat_service.py b/chat/services/chat_service.py index 02479cd..9a34e2d 100644 --- a/chat/services/chat_service.py +++ b/chat/services/chat_service.py @@ -4,10 +4,13 @@ from datetime import datetime from models.ai import ChatConversation, ChatMessage, MessageType class ChatDBService: + @staticmethod + def get_conversation(db: Session, conversation_id: int): + return db.query(ChatConversation).filter(ChatConversation.id == conversation_id).first() + @staticmethod def get_or_create_conversation(db: Session, conversation_id: int | None, user_id: int, model: str, content: str) -> ChatConversation: if not conversation_id: - print(conversation_id, 'conversation_id') conversation = ChatConversation( title=content, user_id=user_id, @@ -32,6 +35,17 @@ class ChatDBService: raise ValueError("无效的conversation_id") return conversation + @staticmethod + def update_conversation_title(db, conversation_id: int, title: str): + conversation = db.query(ChatConversation).filter(ChatConversation.id == conversation_id).first() + if conversation: + conversation.title = title[:255] # 保证不超过255字符 + db.add(conversation) + db.commit() + return conversation + else: + raise ValueError("Conversation not found") + @staticmethod def add_message(db: Session, conversation: ChatConversation, user_id: int, content: str) -> ChatMessage: message = ChatMessage( diff --git a/chat/utils/resp.py b/chat/utils/resp.py index 14a2592..8af103e 100644 --- a/chat/utils/resp.py +++ b/chat/utils/resp.py @@ -12,5 +12,5 @@ class Response(BaseModel, Generic[T]): def resp_success(data: T, message: str = "success") -> Response[T]: return Response(code=0, message=message, data=data) -def resp_error(message="error", code=1) -> Response[None]: +def resp_error(message="error", code=1) -> Response[T]: return Response(code=code, message=message, data=None) \ No newline at end of file diff --git a/web/apps/web-antd/src/api/ai/chat.ts b/web/apps/web-antd/src/api/ai/chat.ts index 537a610..8009fd5 100644 --- a/web/apps/web-antd/src/api/ai/chat.ts +++ b/web/apps/web-antd/src/api/ai/chat.ts @@ -5,6 +5,16 @@ export async function getConversations() { return await res.json(); } +export async function createConversation() { + const response = await fetchWithAuth('/chat/api/v1/conversations', { + method: 'POST', + }); + if (!response.ok) { + throw new Error('创建对话失败'); + } + return await response.json(); +} + export async function getMessages(conversationId: number) { const res = await fetchWithAuth( `/chat/api/v1/messages?conversation_id=${conversationId}`, diff --git a/web/apps/web-antd/src/locales/langs/en-US/ai.json b/web/apps/web-antd/src/locales/langs/en-US/ai.json index d347132..5f59222 100644 --- a/web/apps/web-antd/src/locales/langs/en-US/ai.json +++ b/web/apps/web-antd/src/locales/langs/en-US/ai.json @@ -1,6 +1,6 @@ { "title": "AI Management", - "ai_api_key": { + "api_key": { "title": "KEY Management", "name": "KEY Management" }, diff --git a/web/apps/web-antd/src/locales/langs/zh-CN/ai.json b/web/apps/web-antd/src/locales/langs/zh-CN/ai.json index 224e451..53f2fe8 100644 --- a/web/apps/web-antd/src/locales/langs/zh-CN/ai.json +++ b/web/apps/web-antd/src/locales/langs/zh-CN/ai.json @@ -1,6 +1,6 @@ { "title": "AI大模型", - "ai_api_key": { + "api_key": { "title": "API 密钥", "name": "API 密钥" }, diff --git a/web/apps/web-antd/src/views/ai/ai_api_key/data.ts b/web/apps/web-antd/src/views/ai/api_key/data.ts similarity index 100% rename from web/apps/web-antd/src/views/ai/ai_api_key/data.ts rename to web/apps/web-antd/src/views/ai/api_key/data.ts diff --git a/web/apps/web-antd/src/views/ai/ai_api_key/list.vue b/web/apps/web-antd/src/views/ai/api_key/list.vue similarity index 97% rename from web/apps/web-antd/src/views/ai/ai_api_key/list.vue rename to web/apps/web-antd/src/views/ai/api_key/list.vue index 2f1bc73..165a5ea 100644 --- a/web/apps/web-antd/src/views/ai/ai_api_key/list.vue +++ b/web/apps/web-antd/src/views/ai/api_key/list.vue @@ -133,7 +133,7 @@ function refreshGrid() { v-permission="'ai:ai_api_key:create'" > - {{ $t('ui.actionTitle.create', [$t('ai.ai_api_key.name')]) }} + {{ $t('ui.actionTitle.create', [$t('ai.api_key.name')]) }} diff --git a/web/apps/web-antd/src/views/ai/ai_api_key/modules/form.vue b/web/apps/web-antd/src/views/ai/api_key/modules/form.vue similarity index 93% rename from web/apps/web-antd/src/views/ai/ai_api_key/modules/form.vue rename to web/apps/web-antd/src/views/ai/api_key/modules/form.vue index a32ec36..359ead3 100644 --- a/web/apps/web-antd/src/views/ai/ai_api_key/modules/form.vue +++ b/web/apps/web-antd/src/views/ai/api_key/modules/form.vue @@ -20,8 +20,8 @@ const formModel = new AiAIApiKeyModel(); const formData = ref(); const getTitle = computed(() => { return formData.value?.id - ? $t('ui.actionTitle.edit', [$t('ai.ai_api_key.name')]) - : $t('ui.actionTitle.create', [$t('ai.ai_api_key.name')]); + ? $t('ui.actionTitle.edit', [$t('ai.api_key.name')]) + : $t('ui.actionTitle.create', [$t('ai.api_key.name')]); }); const [Form, formApi] = useVbenForm({ diff --git a/web/apps/web-antd/src/views/ai/chat/index.vue b/web/apps/web-antd/src/views/ai/chat/index.vue index 3c352e1..a65de00 100644 --- a/web/apps/web-antd/src/views/ai/chat/index.vue +++ b/web/apps/web-antd/src/views/ai/chat/index.vue @@ -14,16 +14,21 @@ import { Select, } from 'ant-design-vue'; -import { fetchAIStream, getConversations, getMessages } from '#/api/ai/chat'; +import { + createConversation, + fetchAIStream, + getConversations, + getMessages, +} from '#/api/ai/chat'; interface Message { - id: null | number; + id: number; type: 'assistant' | 'user'; content: string; } interface ChatItem { - id: null | number; + id: number; title: string; lastMessage: string; } @@ -60,14 +65,13 @@ async function selectChat(id: number) { nextTick(scrollToBottom); } -function handleNewChat() { - const newId = null; - chatList.value.unshift({ - id: newId, - title: `新对话${chatList.value.length + 1}`, - lastMessage: '', - }); - selectedChatId.value = newId; +async function handleNewChat() { + // 调用后端新建对话 + const { data } = await createConversation(); + // 刷新对话列表 + await fetchConversations(); + // 选中新建的对话 + selectedChatId.value = data; messages.value = []; nextTick(scrollToBottom); } @@ -195,7 +199,7 @@ onMounted(() => { /> -
+