From 9b301154440c4cf9d913c5415c8671a78a6b82b9 Mon Sep 17 00:00:00 2001 From: XIE7654 <765462425@qq.com> Date: Thu, 17 Jul 2025 14:42:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0deepseek=20=E5=AF=B9=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/api/v1/ai_chat.py | 47 +++++++++++-- web/apps/web-antd/src/api/ai/chat.ts | 47 +++++++++++++ web/apps/web-antd/src/api/request.ts | 5 +- web/apps/web-antd/src/utils/auth.ts | 3 + web/apps/web-antd/src/views/ai/chat/index.vue | 67 +++++++++++++++---- web/apps/web-antd/vite.config.mts | 9 ++- 6 files changed, 149 insertions(+), 29 deletions(-) create mode 100644 web/apps/web-antd/src/api/ai/chat.ts create mode 100644 web/apps/web-antd/src/utils/auth.ts diff --git a/chat/api/v1/ai_chat.py b/chat/api/v1/ai_chat.py index 106810c..8113dda 100644 --- a/chat/api/v1/ai_chat.py +++ b/chat/api/v1/ai_chat.py @@ -1,5 +1,14 @@ +import os +import asyncio + from fastapi import APIRouter, Depends, Request +from fastapi.responses import StreamingResponse + from pydantic import BaseModel +from langchain.memory import ConversationBufferMemory +from langchain.chains import ConversationChain +# from langchain.chat_models import ChatOpenAI +from langchain_community.chat_models import ChatOpenAI from deps.auth import get_current_user from services.chat_service import chat_service @@ -11,12 +20,36 @@ class ChatRequest(BaseModel): prompt: str -@router.post("/") -def chat_api(data: ChatRequest, user=Depends(get_current_user)): - # return {"msg": "pong"} - return resp_success(data="dasds") +def get_deepseek_llm(api_key: str, model: str, openai_api_base: str): + # deepseek 兼容 OpenAI API,需指定 base_url + return ChatOpenAI( + openai_api_key=api_key, + model_name=model, + streaming=True, + openai_api_base=openai_api_base, # deepseek的API地址 + ) - # reply = chat_service.chat(data.prompt) - # return {"msg": "pong"} +@router.post('/stream') +async def chat_stream(request: Request): + body = await request.json() + content = body.get('content') + 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, openai_api_base) - # return ChatResponse(response=reply) \ No newline at end of file + if not content or not isinstance(content, str): + from fastapi.responses import JSONResponse + return JSONResponse({"error": "content不能为空"}, status_code=400) + + async def event_generator(): + async for chunk in llm.astream(content): + # 只返回 chunk.content 内容 + if hasattr(chunk, 'content'): + yield f"data: {chunk.content}\n\n" + else: + yield f"data: {chunk}\n\n" + await asyncio.sleep(0.01) + + return StreamingResponse(event_generator(), media_type='text/event-stream') \ 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 new file mode 100644 index 0000000..2c79471 --- /dev/null +++ b/web/apps/web-antd/src/api/ai/chat.ts @@ -0,0 +1,47 @@ +import { useAccessStore } from '@vben/stores'; + +import { formatToken } from '#/utils/auth'; + +export interface FetchAIStreamParams { + content: string; +} + +export async function fetchAIStream({ content }: FetchAIStreamParams) { + const accessStore = useAccessStore(); + const token = accessStore.accessToken; + const headers = new Headers(); + + headers.append('Content-Type', 'application/json'); + headers.append('Authorization', formatToken(token)); + + const response = await fetch('/chat/api/v1/stream', { + method: 'POST', + headers, + body: JSON.stringify({ + content, + }), + }); + + if (!response.body) throw new Error('No stream body'); + + const reader = response.body.getReader(); + const decoder = new TextDecoder('utf8'); + let buffer = ''; + + return { + async *[Symbol.asyncIterator]() { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const parts = buffer.split('\n\n'); + buffer = parts.pop() || ''; + for (const part of parts) { + if (part.startsWith('data: ')) { + yield part.replace('data: ', ''); + } + } + } + }, + }; +} diff --git a/web/apps/web-antd/src/api/request.ts b/web/apps/web-antd/src/api/request.ts index 288dddd..2bb489e 100644 --- a/web/apps/web-antd/src/api/request.ts +++ b/web/apps/web-antd/src/api/request.ts @@ -16,6 +16,7 @@ import { useAccessStore } from '@vben/stores'; import { message } from 'ant-design-vue'; import { useAuthStore } from '#/store'; +import { formatToken } from '#/utils/auth'; import { refreshTokenApi } from './core'; @@ -56,10 +57,6 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) { return newToken; } - function formatToken(token: null | string) { - return token ? `Bearer ${token}` : null; - } - // 请求头处理 client.addRequestInterceptor({ fulfilled: async (config) => { diff --git a/web/apps/web-antd/src/utils/auth.ts b/web/apps/web-antd/src/utils/auth.ts new file mode 100644 index 0000000..3342d48 --- /dev/null +++ b/web/apps/web-antd/src/utils/auth.ts @@ -0,0 +1,3 @@ +export function formatToken(token: null | string) { + return token ? `Bearer ${token}` : null; +} 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 30fbddb..4cb5cf9 100644 --- a/web/apps/web-antd/src/views/ai/chat/index.vue +++ b/web/apps/web-antd/src/views/ai/chat/index.vue @@ -14,6 +14,9 @@ import { Select, } from 'ant-design-vue'; +import { fetchAIStream } from '#/api/ai/chat'; +// 移除 import typingSound from '@/assets/typing.mp3'; + interface Message { id: number; role: 'ai' | 'user'; @@ -24,7 +27,7 @@ interface Message { const chatList = ref([ { id: 1, - title: '和GPT-3.5的对话', + title: '和deepseek的对话', lastMessage: 'AI: 你好,有什么可以帮您?', }, { id: 2, title: '工作助理', lastMessage: 'AI: 今天的日程已为您安排。' }, @@ -44,7 +47,7 @@ const messages = ref>({ // mock 模型列表 const modelOptions = [ - { label: 'GPT-3.5', value: 'gpt-3.5' }, + { label: 'deepseek', value: 'deepseek' }, { label: 'GPT-4', value: 'gpt-4' }, ]; @@ -53,6 +56,8 @@ const selectedModel = ref(modelOptions[0].value); const search = ref(''); const input = ref(''); const messagesRef = ref(null); +const currentAiMessage = ref(null); +const isAiTyping = ref(false); const filteredChats = computed(() => { if (!search.value) return chatList.value; @@ -80,22 +85,36 @@ function handleNewChat() { nextTick(scrollToBottom); } -function handleSend() { +async function handleSend() { if (!input.value.trim()) return; const msg: Message = { id: Date.now(), role: 'user', content: input.value }; if (!messages.value[selectedChatId.value]) { messages.value[selectedChatId.value] = []; } messages.value[selectedChatId.value].push(msg); - // mock AI 回复 - setTimeout(() => { - messages.value[selectedChatId.value]?.push({ - id: Date.now() + 1, - role: 'ai', - content: 'AI回复内容(mock)', - }); - nextTick(scrollToBottom); - }, 600); + + // 预留AI消息 + const aiMsgObj: Message = { id: Date.now() + 1, role: 'ai', content: '' }; + messages.value[selectedChatId.value].push(aiMsgObj); + currentAiMessage.value = aiMsgObj; + isAiTyping.value = true; + + const stream = await fetchAIStream({ + content: input.value, + }); + + // 移除打字音效播放 + + for await (const chunk of stream) { + for (const char of chunk) { + aiMsgObj.content += char; + currentAiMessage.value = { ...aiMsgObj }; + // 移除打字音效播放 + await new Promise(resolve => setTimeout(resolve, 15)); + nextTick(scrollToBottom); + } + } + isAiTyping.value = false; input.value = ''; nextTick(scrollToBottom); } @@ -165,7 +184,15 @@ function scrollToBottom() { >
{{ msg.role === 'user' ? '我' : 'AI' }} - {{ msg.content }} + + {{ msg.content }} + +
@@ -327,4 +354,18 @@ function scrollToBottom() { padding: 8px 4px 8px 4px; } } +.typing-cursor { + display: inline-block; + width: 8px; + height: 1.2em; + background: #1677ff; + margin-left: 2px; + animation: blink-cursor 1s steps(1) infinite; + vertical-align: bottom; + border-radius: 2px; +} +@keyframes blink-cursor { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0; } +} diff --git a/web/apps/web-antd/vite.config.mts b/web/apps/web-antd/vite.config.mts index b62f598..d2a8fa8 100644 --- a/web/apps/web-antd/vite.config.mts +++ b/web/apps/web-antd/vite.config.mts @@ -22,15 +22,14 @@ export default defineConfig(async ({ mode }) => { host: '0.0.0.0', // 保证 docker 内外都能访问 port: 5678, proxy: { + '/chat': { + target: 'http://localhost:8010', + changeOrigin: true, + }, '/api': { target: backendUrl, changeOrigin: true, }, - '/ws': { - target: backendUrl, - changeOrigin: true, - ws: true, // 启用WebSocket代理 - }, }, }, plugins: [