欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 教育 > 高考 > python django orm websocket html 实现deepseek持续聊天对话页面

python django orm websocket html 实现deepseek持续聊天对话页面

2025/9/15 5:16:13 来源:https://blog.csdn.net/qq_39208536/article/details/146112062  浏览:    关键词:python django orm websocket html 实现deepseek持续聊天对话页面

最终效果:

技术栈:

 python django orm websocket html

项目结构:

这里只展示关键代码:

@File: consumers.py
# -*- coding:utf-8 -*-
# @Author: 喵酱
# @time: 2025 - 03 -02
# @File: consumers.py
# desc:
import json
from asgiref.sync import sync_to_async
from .models import Message, Conversation
from channels.generic.websocket import AsyncWebsocketConsumer
import requestsclass ChatConsumer(AsyncWebsocketConsumer):def __init__(self, *args, **kwargs):super().__init__(*args, **kwargs)self.message_history = {}  # 用于缓存历史消息async def connect(self):print("WebSocket scope:", self.scope)self.conversation_id = self.scope['url_route']['kwargs']['conversation_id']self.room_group_name = f'chat_{self.conversation_id}'# 初始化历史消息缓存self.message_history[self.conversation_id] = await self.load_message_history()# 加入房间组await self.channel_layer.group_add(self.room_group_name,self.channel_name)await self.accept()async def disconnect(self, close_code):# 离开房间组print(f"WebSocket disconnected with code: {close_code}")if hasattr(self, 'channel_layer') and self.channel_layer:await self.channel_layer.group_discard(self.room_group_name,self.channel_name)async def receive(self, text_data):text_data_json = json.loads(text_data)message = text_data_json['message']print(f"服务端接收到消息: {message}")# 保存用户消息到数据库await self.save_message_to_db(message, role='user')# 将用户消息添加到历史消息缓存self.message_history[self.conversation_id].append({"role": "user", "content": message})# 调用对话接口处理消息processed_message = await self.call_dialogue_service(message)# 将 AI 回复添加到历史消息缓存self.message_history[self.conversation_id].append({"role": "assistant", "content": processed_message})# 保存 AI 回复到数据库await self.save_message_to_db(processed_message, role='assistant')# 发送处理后的消息到房间组await self.channel_layer.group_send(self.room_group_name,{'type': 'chat_message','message': processed_message,'role': 'assistant'  # 角色为 assistant})@sync_to_asyncdef save_message_to_db(self, message, role):# 保存消息到数据库conversation = Conversation.objects.get(id=self.conversation_id)Message.objects.create(conversation=conversation, role=role, content=message)@sync_to_asyncdef load_message_history(self):# 从数据库加载历史消息conversation = Conversation.objects.get(id=self.conversation_id)messages = conversation.messages.all().order_by('created_at')return [{"role": msg.role, "content": msg.content} for msg in messages]async def call_dialogue_service(self, message):# DeepSeek API 的 URL 和 API KeyDEEPSEEK_API_URL = "https://api.deepseek.com/v1/chat/completions"DEEPSEEK_API_KEY = "sk-8b7c9285aac14885a60d610e7fdcedda"# 获取当前对话的历史消息message_history = self.message_history[self.conversation_id]# 构造请求数据,包含历史消息和当前消息data = {"model": "deepseek-chat","messages": message_history,  # 包含历史消息和当前消息"max_tokens": 300  # 根据需要调整}print("发送请求到 DeepSeek API")# 发送请求到 DeepSeek APItry:response = requests.post(DEEPSEEK_API_URL,headers={"Authorization": f"Bearer {DEEPSEEK_API_KEY}","Content-Type": "application/json"},json=data)response.raise_for_status()  # 检查请求是否成功response_data = response.json()ai_response = response_data['choices'][0]['message']['content']print("发送请求到 DeepSeek API,发送成功")except requests.exceptions.RequestException as e:# 如果 API 调用失败,返回错误信息ai_response = f"Error: {str(e)}"print("发送请求到 DeepSeek API,发送失败")return ai_responseasync def chat_message(self, event):print(f"服务端发送消息: {event['message']}")message = event['message']role = event.get('role', 'assistant')  # 默认角色为 assistant# 发送消息到 WebSocketawait self.send(text_data=json.dumps({'message': message,'role': role}))
# views.py
# views.py
from django.http import JsonResponse
from django.shortcuts import render# Create your views here.
import requestsfrom .forms import UploadFileFormfrom django.shortcuts import render, redirect, get_object_or_404
from .models import Conversation, Message
import markdown
import yaml
import json
import os
from django.conf import settings
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exemptDEEPSEEK_API_URL = "https://api.deepseek.com/v1/chat/completions"
DEEPSEEK_API_KEY = "sk-8b"# MAX_FILE_SIZE = 50 * 1024 * 1024  # 50MB
MAX_FILE_SIZE = 100 * 1024 * 1024  # 100MBdef index(request):return render(request, 'index.html')def business_test(request):return render(request, 'business_test.html')def api_test(request):return render(request, 'api_test.html')
def data_generator(request):return render(request, 'data_generator.html')def test_report(request):return render(request, 'test_report.html')def chat(request, conversation_id=None):# 确保会话已初始化if not request.session.session_key:request.session.create()session_id = request.session.session_key# 获取历史对话列表history_conversations = Conversation.objects.filter(session_id=session_id).order_by('-created_at')# 获取当前对话if conversation_id:current_conversation = get_object_or_404(Conversation, id=conversation_id, session_id=session_id)else:# 如果没有传递 conversation_id,则使用最近的对话或创建一个新对话current_conversation = history_conversations.first()if not current_conversation:current_conversation = Conversation.objects.create(session_id=session_id, title="New Conversation")if request.method == 'POST':user_input = request.POST.get('user_input')files = request.FILES.getlist('file')  # 获取上传的文件列表# 检查文件大小for file in files:if file.size > MAX_FILE_SIZE:return JsonResponse({'error': f'文件 {file.name} 大小超过 50MB 限制'}, status=400)# 保存用户输入if user_input:print(f"用户输入:{user_input}")Message.objects.create(conversation=current_conversation, role='user', content=user_input)# 处理上传的文件file_contents = []for file in files:# 保存文件到本地file_path = os.path.join(settings.MEDIA_ROOT, file.name)with open(file_path, 'wb+') as destination:for chunk in file.chunks():destination.write(chunk)# 读取文件内容file_content = file.read().decode('utf-8')file_contents.append(f"文件: {file.name}\n内容:\n{file_content}")# 将文件内容与用户输入合并full_input = user_input if user_input else ""if file_contents:full_input += "\n\n上传的文件内容:\n" + "\n".join(file_contents)# 调用 DeepSeek APItry:response = requests.post(DEEPSEEK_API_URL,headers={"Authorization": f"Bearer {DEEPSEEK_API_KEY}","Content-Type": "application/json"},json={"model": "deepseek-chat","messages": [{"role": "user", "content": full_input}],"max_tokens": 150  # 根据需要调整})response.raise_for_status()  # 检查请求是否成功response_data = response.json()ai_response = response_data['choices'][0]['message']['content']except requests.exceptions.RequestException as e:# 如果 API 调用失败,返回错误信息ai_response = f"Error: {str(e)}"# 保存 AI 回答Message.objects.create(conversation=current_conversation, role='assistant', content=ai_response)# 通过 WebSocket 发送消息到前端channel_layer = get_channel_layer()async_to_sync(channel_layer.group_send)(f'chat_{current_conversation.id}',{'type': 'chat_message','message': ai_response,'role': 'assistant'})# 获取当前对话的所有消息messages = current_conversation.messages.all().order_by('created_at')return render(request, 'chat.html', {'current_conversation': current_conversation,'history_conversations': history_conversations,'messages': messages})
def new_conversation(request):# 确保会话已初始化if not request.session.session_key:request.session.create()session_id = request.session.session_key# 创建新对话new_conversation = Conversation.objects.create(session_id=session_id, title="New Conversation")return redirect('chat_with_id', conversation_id=new_conversation.id)  # 使用 chat_with_id
models.py

from django.db import modelsclass Conversation(models.Model):session_id = models.CharField(max_length=255)title = models.CharField(max_length=255, default="New Conversation")created_at = models.DateTimeField(auto_now_add=True)updated_at = models.DateTimeField(auto_now=True)def __str__(self):return self.titleclass Message(models.Model):conversation = models.ForeignKey(Conversation, related_name='messages', on_delete=models.CASCADE)role = models.CharField(max_length=10, choices=[('user', 'User'), ('assistant', 'Assistant')])content = models.TextField()created_at = models.DateTimeField(auto_now_add=True)def __str__(self):return f"{self.role}: {self.content}"
routing.py
# @File: routing.py
# desc:
from django.urls import re_path
from . import consumers# 定义 WebSocket 路由
websocket_urlpatterns = [re_path(r'ws_chat/(?P<conversation_id>\w+)/$', consumers.ChatConsumer.as_asgi()),
]

 

@File: urls.py
# @File: urls.py
# desc:
from django.urls import path, include
from . import views
from django.conf import settings
from django.conf.urls.static import static
from .routing import websocket_urlpatterns  # 导入 WebSocket 路由urlpatterns = [path('chat/', views.chat, name='chat'),  # 用于没有 conversation_id 的情况path('chat/<int:conversation_id>/', views.chat, name='chat_with_id'),  # 用于有 conversation_id 的情况path('ws/chat/<int:conversation_id>/', views.chat, name='chat_with_id'),  # 用于有 conversation_id 的情况path('ws_chat/', include(websocket_urlpatterns)),  # 这个是关键配置path('new-conversation/', views.new_conversation, name='new_conversation'),path('upload/', views.upload_file, name='upload'),path('autocomplete/', views.autocomplete, name='autocomplete'),path('generate-business-test/', views.generate_business_test, name='generate_business_test'),path('generate-api-test/', views.generate_api_test, name='generate_api_test'),path('', views.index, name='index'),  # 首页] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

# ASGI配置文件
# asgi.py
# ASGI配置文件
# asgi.pyimport os
import django
from django.core.asgi import get_asgi_application# 设置 DJANGO_SETTINGS_MODULE
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'deepseek_project.settings')# 初始化 Django
django.setup()# 导入其他模块
from channels.routing import ProtocolTypeRouter, URLRouter
from deepseek_app.routing import websocket_urlpatterns# 获取 Django 的 ASGI 应用
django_asgi_app = get_asgi_application()# 配置 ProtocolTypeRouter
application = ProtocolTypeRouter({"http": django_asgi_app,  # 处理 HTTP 请求# "http": get_asgi_application(),  # 处理 HTTP 请求"websocket": URLRouter(websocket_urlpatterns  # 处理 WebSocket 请求),
})

settings.py

from pathlib import Path
import os
import pymysql
pymysql.install_as_MySQLdb()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-ge1ljrz+2^1-f4(y)+!a0n#!(4e)+^z4fx$2$2v7=uf+e$^a&5'# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = TrueALLOWED_HOSTS = ['*']# Application definitionINSTALLED_APPS = ['django.contrib.admin','django.contrib.auth','django.contrib.contenttypes','django.contrib.sessions','django.contrib.messages','django.contrib.staticfiles','channels',  # 添加 channels'deepseek_app',]ASGI_APPLICATION = 'deepseek_project.asgi.application'MIDDLEWARE = ['whitenoise.middleware.WhiteNoiseMiddleware','django.middleware.security.SecurityMiddleware','django.contrib.sessions.middleware.SessionMiddleware','django.middleware.common.CommonMiddleware','django.middleware.csrf.CsrfViewMiddleware','django.contrib.auth.middleware.AuthenticationMiddleware','django.contrib.messages.middleware.MessageMiddleware','django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# 配置静态文件存储
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
ROOT_URLCONF = 'deepseek_project.urls'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')TEMPLATES = [{'BACKEND': 'django.template.backends.django.DjangoTemplates','DIRS': [],'APP_DIRS': True,'OPTIONS': {'context_processors': ['django.template.context_processors.debug','django.template.context_processors.request','django.contrib.auth.context_processors.auth','django.contrib.messages.context_processors.messages',],},},
]WSGI_APPLICATION = 'deepseek_project.wsgi.application'# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases# DATABASES = {
#     'default': {
#         'ENGINE': 'django.db.backends.sqlite3',
#         'NAME': BASE_DIR / 'db.sqlite3',
#     }
# }DATABASES = {'default': {'ENGINE': 'django.db.backends.mysql',  # 使用 MySQL'NAME': 'ds11111','USER': 'root','PASSWORD': '121111','HOST': '11.11.21.11',  # 或者是你 MySQL 的主机地址'PORT': '3306',}
}# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validatorsAUTH_PASSWORD_VALIDATORS = [{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',},{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',},{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',},{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',},
]# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/LANGUAGE_CODE = 'en-us'TIME_ZONE = 'UTC'USE_I18N = TrueUSE_TZ = True# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-fieldDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')CORS_ALLOWED_ORIGINS = ["http://127.0.0.1:8000","http://localhost:8000",
]#
# CHANNEL_LAYERS = {
#     "default": {
#         "BACKEND": "channels_redis.core.RedisChannelLayer",
#         "CONFIG": {
#             "hosts": [("11.11.21.11", 6379)],
#             "channel_capacity": {
#                 "http.request": 200,
#                 "http.response!*": 200,
#                 "websocket.send!*": 200,
#             },
#         },
#     },
# }CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer",},
}

wsgi.py
import osfrom django.core.wsgi import get_wsgi_applicationos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'deepseek_project.settings')application = get_wsgi_application()

前端代码:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>AI对话</title><!-- 加载 static 标签库 -->{% load static %}<!-- Bootstrap CSS (本地文件) --><link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet"><!-- Font Awesome (本地文件) --><link href="{% static 'css/all.min.css' %}" rel="stylesheet"><style>/* 自定义样式 */body {display: flex;height: 100vh;background-color: #f8f9fa;margin: 0;}.sidebar {width: 250px;background-color: #2c3e50;color: white;padding: 20px;overflow-y: auto;}.main-content {flex: 1;display: flex;flex-direction: column;background-color: #fff;}.chat-history {flex: 1;overflow-y: auto;padding: 20px;scroll-behavior: smooth; /* 平滑滚动 */}.chat-input {padding: 20px;background-color: #fff;border-top: 1px solid #ddd;}.message {margin-bottom: 15px;}.message.user {text-align: right;}.message.assistant {text-align: left;}.message .card {max-width: 70%;display: inline-block;}.message.user .card {background-color: #007bff;color: white;}.message.assistant .card {background-color: #f1f1f1;color: black;}.loading {display: none;text-align: center;margin: 10px 0;}.loading .spinner-border {width: 1.5rem;height: 1.5rem;}.file-preview {margin-bottom: 10px;}.file-info {padding: 5px;border: 1px solid #ddd;border-radius: 5px;background-color: #f9f9f9;}</style>
</head>
<body><!-- 侧边栏 --><div class="sidebar d-none d-md-block"><h2>Conversations</h2><ul class="list-unstyled">{% for conversation in history_conversations %}<li class="mb-2"><a href="{% url 'chat_with_id' conversation.id %}" class="text-white text-decoration-none"><i class="fas fa-comment-dots me-2"></i>{{ conversation.title }}</a></li>{% endfor %}</ul><button class="btn btn-primary w-100" onclick="location.href='{% url 'new_conversation' %}'"><i class="fas fa-plus me-2"></i>新建对话</button></div><!-- 主内容区 --><div class="main-content"><!-- 对话历史 --><div class="chat-history" id="chat-history"><h2>{{ current_conversation.title }}</h2>{% for message in messages %}<div class="message {% if message.role == 'user' %}user{% else %}assistant{% endif %}"><div class="card"><div class="card-body"><p class="card-text">{{ message.content }}</p><small class="text-muted">{{ message.created_at|date:"Y-m-d H:i:s" }}</small></div></div></div>{% endfor %}</div><!-- 加载动画 --><div class="loading" id="loading"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div></div><!-- 输入框 --><div class="chat-input"><form method="post" class="d-flex flex-column" id="chat-form" enctype="multipart/form-data">{% csrf_token %}<!-- 文件上传 --><div class="file-upload mb-2"><input type="file" class="form-control" id="file-input" name="file" multipleaccept=".txt,.pdf,.docx,.xmind,.jpg,.jpeg,.png,.gif"></div><!-- 文件预览 --><div class="file-preview" id="file-preview"></div><!-- 文本输入 --><div class="d-flex"><textarea name="user_input" class="form-control me-2" placeholder="输入你的内容..." rows="1" id="user-input"></textarea><button type="submit" class="btn btn-primary"><i class="fas fa-paper-plane me-2"></i>提交</button></div></form></div></div><!-- Bootstrap JS (本地文件) --><script src="{% static 'js/bootstrap.bundle.min.js' %}"></script><!-- jQuery (本地文件) --><script src="{% static 'js/jquery-3.6.0.min.js' %}"></script><!-- WebSocket 支持 --><script>// 获取当前对话 IDconst conversationId = '{{ current_conversation.id }}';let chatSocket; // 将 chatSocket 定义在全局作用域if (!conversationId) {console.error('Conversation ID is missing');} else {// 创建 WebSocket 连接const websocketUrl = 'ws://' + window.location.host + '/ws_chat/' + conversationId + '/';console.log('Attempting to establish WebSocket connection to:', websocketUrl);chatSocket = new WebSocket(websocketUrl);// WebSocket 连接成功chatSocket.onopen = function (e) {console.log('WebSocket connection opened successfully');};// 处理接收到的消息chatSocket.onmessage = function (e) {console.log('Received message from server:', e.data);const data = JSON.parse(e.data);const chatHistory = document.getElementById('chat-history');// 添加新消息到对话历史const messageHtml = `<div class="message ${data.role}"><div class="card"><div class="card-body"><p class="card-text">${data.message}</p><small class="text-muted">${new Date().toLocaleString()}</small></div></div></div>`;chatHistory.innerHTML += messageHtml;// 滚动到底部chatHistory.scrollTop = chatHistory.scrollHeight;// 隐藏加载动画$('#loading').hide();};// 处理 WebSocket 关闭事件chatSocket.onclose = function (e) {console.error('WebSocket connection closed:', e);alert('WebSocket 连接已关闭,请刷新页面重试。');};// 处理 WebSocket 错误事件chatSocket.onerror = function (e) {console.error('WebSocket error:', e);alert('WebSocket 连接出错,请检查网络或刷新页面。');};}// 文件选择事件$('#file-input').on('change', function () {console.log('File input changed');const filePreview = $('#file-preview');filePreview.empty(); // 清空之前的预览const files = this.files;for (let i = 0; i < files.length; i++) {const file = files[i];const fileInfo = `<div class="file-info mb-2"><span>${file.name}</span><small class="text-muted">(${(file.size / 1024).toFixed(2)} KB)</small></div>`;filePreview.append(fileInfo);}});// 发送消息时显示加载动画$(document).ready(function () {$('#chat-form').on('submit', function (e) {e.preventDefault();console.log('Form submitted');const userInput = $('#user-input').val().trim();const fileInput = $('#file-input')[0].files;if (!userInput && fileInput.length === 0) {console.log('No input or file selected');alert('请输入内容或选择文件');return;}$('#loading').show(); // 显示加载动画// 手动将用户的消息添加到聊天记录中const chatHistory = document.getElementById('chat-history');const messageHtml = `<div class="message user"><div class="card"><div class="card-body"><p class="card-text">${userInput}</p><small class="text-muted">${new Date().toLocaleString()}</small></div></div></div>`;chatHistory.innerHTML += messageHtml;// 滚动到底部chatHistory.scrollTop = chatHistory.scrollHeight;// 如果有文件上传,使用 FormData 发送if (fileInput.length > 0) {console.log('File upload detected, using AJAX');const formData = new FormData();formData.append('user_input', userInput);for (let i = 0; i < fileInput.length; i++) {formData.append('file', fileInput[i]);}// 使用 AJAX 发送文件$.ajax({url: '{% url "chat_with_id" current_conversation.id %}',method: 'POST',data: formData,processData: false,contentType: false,headers: {'X-CSRFToken': '{{ csrf_token }}'},success: function (response) {console.log('File upload successful:', response);$('#loading').hide();location.reload(); // 刷新页面以显示新消息},error: function (xhr, status, error) {console.error('File upload failed:', error);$('#loading').hide();alert('文件上传失败,请重试。');}});} else {console.log('Sending message via WebSocket:', userInput);if (chatSocket.readyState !== WebSocket.OPEN) {console.error('WebSocket is not open, readyState:', chatSocket.readyState);alert('WebSocket 连接未准备好,请稍后重试。');return;}// 发送消息chatSocket.send(JSON.stringify({'message': userInput}));console.log('Message sent via WebSocket:', userInput);$('#user-input').val(''); // 清空输入框}});
});</script>
</body>
</html>

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词