DeepSeek本地部署+自主开发对话Web应用
文章目录
- 引言
- 前端部分
- 核心页面DeepSeek.vue
- MyModal.vue
- 后端部分
- WebSocketConfig 配置类
- AbstractDeepSeekTool
- DeepSeekWebSocketHandler
- 数据库设计
- 总结
引言
最近DeepSeep横空出世,在全球内掀起一股热潮,到处都是满血大模型接入的应用,但这些有的是要钱的,有的虽然免费但是也会卡顿,作为一名软件开发人员,当然要想办法了,这不笔者就自主开发了一个web对话页面,不是简单的对话响应哦,还能支持保留历史记录的,可以新建对话框,请往下看(如果对您有帮助记得关注我,点个赞!)。
前端Gitee代码地址:https://gitee.com/buxingzhe/deepseek-chat.git
后端Gitee代码地址:https://gitee.com/buxingzhe/deep-seek-project.git
DeepSeek本地部署教程
前端部分
先来个效果图
安装依赖
npm install
运行命令
npm run serve
打包构建
npm run build
功能特点
-
AI流式响应,无卡顿。
-
支持对话消息文本的持久化,除了临时会话,其他会话框的标题均可修改,会话框支持删除。
-
可以随意新建对话框,支持保存历史对话消息,有一个默认临时会话框,不保存会话历史消息,刷新页面后消息丢失。
-
消息区分思考推理部分和正式回答部分,思考推理部分为浅灰色,正式回答为黑色文本。
-
使用了MarkDown渲染html,格式更加美观。
-
消息生成过程中可随时停止。
-
支持心跳检测,后端服务离线后重启时,前端自动重新建立连接webSocket会话。
核心页面DeepSeek.vue
代码稍微有点复杂了,毕竟要实现的东西也不少,什么保存消息上下文记录啊,定时心跳检测啊,文本渲染啊等等,样式部分我这省略了,具体可从gitee拉取源码。
<template><div id="app"><div class="chat-container"><!-- 会话列表 --><div class="session-list"><divv-for="(session, index) in sessions":key="index"class="session-item":class="{ active: activeSessionIndex === index }"@click="selectSession(index)"><span class="session-status" :class="{ online: session.isConnected, offline:!session.isConnected }"></span><span class="session-status-text">{{ session.isConnected ? '在线' : '离线' }}</span><span class="session-name">{{ session.title }}</span><!-- 三个小点按钮 --><div class="session-actions-trigger" @click.stop="toggleActionsMenu(index)"><span>...</span></div><!-- 编辑和删除操作菜单 --><divv-if="session.showActionsMenu"class="session-actions-menu"@click.stop><button @click="openEditModal(index)">编辑</button><button @click="openDeleteModal(index)">删除</button></div></div><button @click="createNewSession" class="new-session-button" :disabled="hasUnconnectedSession">新建会话</button></div><!-- 聊天内容 --><div class="chat-content"><div class="chatBox"><!-- 错误提示 --><div v-if="errorMessage" class="error-message">{{ errorMessage }}</div><!-- 聊天消息显示区域 --><div class="chat-messages"><div v-for="(message, index) in currentMessages" :key="index" class="message"><!-- 用户消息 --><div v-if="message.sender === 'user'" class="user-message-container"><article class="message-content user-message">{{ message.text }}</article></div><!-- 机器人消息 --><div v-else class="bot-message-container"><article class="message-content bot-message" v-html="renderMarkdown(message.text)"></article></div></div></div><!-- 输入框和发送按钮 --><div class="chat-input"><textareav-model="inputMessage"placeholder="请输入你的问题..."@keyup="handleKeyup"rows="6":disabled="!isConnected"/><button @click="handleButtonClick" :disabled="!isConnected">{{ currentSession.isGenerating ? '停止生成' : '发送' }}</button></div></div></div></div><!-- 编辑模态框 --><MyModal :visible="isEditModalVisible" title="编辑会话标题" @close="closeEditModal"><input v-model="editTitle" placeholder="请输入新的会话标题" /><template #footer><button @click="closeEditModal">取消</button><button @click="confirmEdit">确定</button></template></MyModal><!-- 删除模态框 --><MyModal :visible="isDeleteModalVisible" title="确认删除" @close="closeDeleteModal"><p>确定要删除该会话吗?</p><template #footer><button @click="closeDeleteModal">取消</button><button @click="confirmDelete">确定</button></template></MyModal></div>
</template><script setup>
import {computed, onMounted, onUnmounted, reactive, ref} from 'vue';
import MyModal from './MyModal.vue';
import {marked} from 'marked';
import DOMPurify from 'dompurify';
import axios from 'axios';// 配置 marked(保持不变)
marked.setOptions({gfm: true,breaks: true,highlight: function (code) {return code;}
});// 渲染 Markdown 内容(保持不变)
const renderMarkdown = (content) => {console.log("渲染前内容",content);content = content.replace(/<think>/g, '<div><span class="deepThink">');if (!content.includes('</think>')) {content = content.concat('</span></div>');}if (content.includes('</think>')) {content = content.replace(/<\/span><\/div>/g, '');content = content.replace(/<\/think>/g, '</span></div>');}const html = marked(content);const sanitizedHtml = DOMPurify.sanitize(html);const tempDiv = document.createElement('div');tempDiv.innerHTML = sanitizedHtml.toString();const deepThinkElements = tempDiv.querySelectorAll('.deepThink');deepThinkElements.forEach((element) => {if (element.textContent.trim() === '') {element.textContent = '暂无推理过程';}});console.log("渲染后内容",tempDiv.innerHTML);return tempDiv.innerHTML;
};// 存储所有会话(改为空数组)
const sessions = ref([]);// 当前激活的会话索引
const activeSessionIndex = ref(0);// 新增:统一错误处理函数
const handleNetworkError = (error, session) => {let errorMsg = '服务暂时不可用,请稍后重试';if (!navigator.onLine) {errorMsg = '网络连接已断开,请检查网络设置';} else if (error.message === 'Network Error') {errorMsg = '无法连接到服务器,请确认后端服务已启动';} else if (error.response?.status >= 500) {errorMsg = '服务器内部错误,请联系管理员';}session.errorMessage = errorMsg;setTimeout(() => {session.errorMessage = ''; // 5秒后自动清除错误}, 5000);
};// 保存用户消息到数据库
const saveUserMessageToDatabase = async (talkInfoId, message,messageType) => {try {await axios.post('/api/deepSeek/saveTalkInfoMessage', {talkInfoId,message,messageType});} catch (error) {console.error('保存用户消息到数据库失败:', error);const errorSession = sessions.value[activeSessionIndex.value] || {};handleNetworkError(error, errorSession);}
};// 保存机器人消息到数据库
const saveBotMessageToDatabase = async (talkInfoId, message,messageType) => {try {await axios.post('/api/deepSeek/saveTalkInfoMessage', {talkInfoId,message,messageType});} catch (error) {console.error('保存机器人消息到数据库失败:', error);const errorSession = sessions.value[activeSessionIndex.value] || {};handleNetworkError(error, errorSession);}
};// 获取单个会话框的历史消息列表
const getMessageListByTalkInfoId = async (talkInfoId) => {try {const response = await axios.get('/api/deepSeek/getMessageListByTalkInfoId', {params: { talkInfoId }});// 检查 response.data.data 是否存在且为数组if (Array.isArray(response.data?.data)) {//构建数据const result = response.data.data.map(item => {return {sender: item.messageType,text: item.message};});console.log('构建数据成功+++:', result)return result;}return [];} catch (error) {console.error('获取会话消息列表失败:', error);const errorSession = sessions.value[activeSessionIndex.value] || {};handleNetworkError(error, errorSession);return [];}
};//页面刷新时初始化会话列表
const fetchSessions = async () => {try {const response = await axios.get('/api/deepSeek/talkInfoList');const data = response.data?.data || [];// 创建临时会话const tempSession = reactive({talkInfoId: 'temp-' + Date.now(),title: '临时会话',messages: [{ sender: 'bot', text: '欢迎使用临时对话框!有什么可以帮助你的?' }],inputMessage: '',errorMessage: '',isConnected: false,isGenerating: false,socket: null,reconnectInterval: null,retryCount: 0, // 新增重试次数currentBotMessage: '' // 用于存储当前机器人流式响应消息});// 初始化会话列表,将临时会话添加到列表开头sessions.value = [tempSession, ...data.map(item =>reactive({talkInfoId: item.id,title: item.title || `会话 ${sessions.value.length + 1}`,messages: [{ sender: 'bot', text: '欢迎使用!有什么可以帮助你的?' }],inputMessage: '',errorMessage: '',isConnected: false,isGenerating: false,socket: null,reconnectInterval: null,retryCount: 0, // 新增重试次数currentBotMessage: '' // 用于存储当前机器人流式响应消息}))];// 为每个会话获取对话消息for (const session of sessions.value) {const talkInfoId = session?.talkInfoId;const result = talkInfoId.toString().startsWith('temp-')if (!result){const messages = await getMessageListByTalkInfoId(session?.talkInfoId);console.log("获取的消息为:",messages);session.messages = messages && messages.length > 0 ? messages :[{ sender: 'bot', text: '欢迎使用!有什么可以帮助你的?' }];}}// 若查询到会话框列表,开始逐一连接if (data.length > 0) {sessions.value.forEach(session => {reconnectWebSocket(session);});} else {// 查不到则直接连接临时会话框reconnectWebSocket(tempSession);}} catch (error) {console.error('获取会话列表失败:', error);// 创建临时会话const tempSession = reactive({talkInfoId: 'temp-' + Date.now(),title: '临时会话',messages: [{ sender: 'bot', text: '欢迎使用!' }],inputMessage: '',errorMessage: '',isConnected: false,isGenerating: false,socket: null,reconnectInterval: null,retryCount: 0, // 新增重试次数currentBotMessage: '' // 用于存储当前机器人流式响应消息});sessions.value = [tempSession];activeSessionIndex.value = 0;handleNetworkError(error, tempSession);// 直接连接临时会话框reconnectWebSocket(tempSession);}
};// 计算属性(保持不变)
const currentMessages = computed(() => sessions.value[activeSessionIndex.value]?.messages || []);
const inputMessage = computed({get: () => sessions.value[activeSessionIndex.value]?.inputMessage || '',set: (value) => (sessions.value[activeSessionIndex.value].inputMessage = value)
});
const errorMessage = computed({get: () => sessions.value[activeSessionIndex.value]?.errorMessage || '',set: (value) => (sessions.value[activeSessionIndex.value].errorMessage = value)
});
const isConnected = computed({get: () => sessions.value[activeSessionIndex.value]?.isConnected || false,set: (value) => (sessions.value[activeSessionIndex.value].isConnected = value)
});
const currentSession = computed(() => sessions.value[activeSessionIndex.value] || {});// 发送消息函数
const sendMessage = () => {if (!inputMessage.value) return;const messageData = JSON.stringify({talkInfoId: currentSession.value?.talkInfoId,content: inputMessage.value});currentMessages.value.push({ sender: 'user', text: inputMessage.value });currentSession.value.socket.send(messageData);// 保存用户消息到数据库if (!currentSession.value.talkInfoId.toString().startsWith('temp-')){saveUserMessageToDatabase(currentSession.value.talkInfoId, inputMessage.value,'user');}inputMessage.value = '';currentSession.value.isGenerating = true;
};// 停止生成函数
const stopGenerating = () => {const messageData = JSON.stringify({talkInfoId: currentSession.value?.talkInfoId,content: 'stopSending'});currentSession.value.socket.send(messageData);currentSession.value.isGenerating = false;
};// 发送按钮点击处理
const handleButtonClick = () => {currentSession.value.isGenerating ? stopGenerating() : sendMessage();
};// 键盘事件处理
const handleKeyup = (event) => {if (event.key === 'Enter' &&!event.shiftKey) sendMessage();
};// WebSocket 连接管理(修改)
let fetchSessionsDebounceTimer = null;
const reconnectWebSocket = (session) => {if (session.reconnectInterval) clearInterval(session.reconnectInterval);const initialDelay = 3000; // 初始延迟时间const maxDelay = 60000; // 最大延迟时间const backoffFactor = 2; // 退避因子const attemptReconnect = () => {if (!session.socket || session.socket.readyState === WebSocket.CLOSED) {session.socket = new WebSocket(`ws://localhost:8085/websocket?talkInfoId=${session.talkInfoId}`);session.socket.onopen = () => {session.errorMessage = '';session.isConnected = true;session.retryCount = 0; // 连接成功,重置重试次数clearInterval(session.reconnectInterval);};session.socket.onmessage = (event) => {const targetSession = sessions.value.find(s => s?.socket === session.socket);if (!targetSession) return;if (event.data.includes('[END_OF_MESSAGE_GENERATE]')) {targetSession.isGenerating = false;// 保存机器人消息到数据库if (!targetSession?.talkInfoId.toString().startsWith('temp-')){saveBotMessageToDatabase(targetSession?.talkInfoId, targetSession.currentBotMessage,'bot');}targetSession.currentBotMessage = '';return;}targetSession.currentBotMessage += event.data;const lastMessage = targetSession?.messages[targetSession?.messages.length - 1];lastMessage?.sender === 'bot'? lastMessage.text += event.data: targetSession?.messages.push({ sender: 'bot', text: event.data });};session.socket.onerror = (error) => {console.error('连接错误:', error);handleNetworkError(error, session);session.isConnected = false;session.retryCount++;const nextDelay = Math.min(initialDelay * Math.pow(backoffFactor, session.retryCount), maxDelay);session.reconnectInterval = setTimeout(attemptReconnect, nextDelay);};session.socket.onclose = () => {handleNetworkError(new Error('连接意外关闭'), session);session.isConnected = false;session.retryCount++;const nextDelay = Math.min(initialDelay * Math.pow(backoffFactor, session.retryCount), maxDelay);session.reconnectInterval = setTimeout(attemptReconnect, nextDelay);// 尝试刷新会话列表,添加防抖机制if (fetchSessionsDebounceTimer) {clearTimeout(fetchSessionsDebounceTimer);}fetchSessionsDebounceTimer = setTimeout(() => {fetchSessions();}, 3000); // 3 秒防抖};}};attemptReconnect();
};// 创建新会话
const createNewSession = async () => {if (hasUnconnectedSession.value) return;try {// 新建新会话的同时保存到后台const response = await axios.post('/api/deepSeek/saveTalkInfo', {title: `历史会话 ${sessions.value.length}`});const newSession = reactive({talkInfoId: response.data.data,title: `历史会话 ${sessions.value.length}`,messages: [{ sender: 'bot', text: '欢迎使用!有什么可以帮助你的?' }],inputMessage: '',errorMessage: '',isConnected: false,isGenerating: false,socket: null,reconnectInterval: null,retryCount: 0, // 新增重试次数currentBotMessage: '' // 用于存储当前机器人流式响应消息});sessions.value.push(newSession);activeSessionIndex.value = sessions.value.length - 1;reconnectWebSocket(newSession);} catch (error) {console.error('创建会话失败:', error);const errorSession = sessions.value[activeSessionIndex.value] || {};handleNetworkError(error, errorSession);// 确保至少存在一个会话if (sessions.value.length === 0) {const tempSession = reactive({talkInfoId: 'temp-' + Date.now(),title: '临时会话',messages: [{ sender: 'bot', text: '欢迎使用!' }],inputMessage: '',errorMessage: '',isConnected: false,isGenerating: false,socket: null,reconnectInterval: null,retryCount: 0, // 新增重试次数currentBotMessage: '' // 用于存储当前机器人流式响应消息});sessions.value = [tempSession];activeSessionIndex.value = 0;}}
};// 切换操作菜单显示隐藏
const toggleActionsMenu = (index) => {sessions.value.forEach((session, i) => {session.showActionsMenu = i === index && !session.showActionsMenu;});
};
//============================编辑和删除会话框相关操作逻辑=========================================================================
const isEditModalVisible = ref(false);
const isDeleteModalVisible = ref(false);
const editTitle = ref('');
const deleteTitle = ref('');
let currentEditIndex = -1;
let currentDeleteIndex = -1;const openEditModal = (index) => {currentEditIndex = index;editTitle.value = sessions.value[index].title;isEditModalVisible.value = true;
};const closeEditModal = () => {isEditModalVisible.value = false;
};const confirmEdit = async () => {const session = sessions.value[currentEditIndex];if (editTitle.value && editTitle.value!== session.title) {try {await axios.post('/api/deepSeek/updateTalkInfo', {id: session.talkInfoId,title: editTitle.value});session.title = editTitle.value;} catch (error) {console.error('编辑会话失败:', error);handleNetworkError(error, session);}}closeEditModal();
};const openDeleteModal = (index) => {currentDeleteIndex = index;deleteTitle.value = sessions.value[index].title;isDeleteModalVisible.value = true;
};const closeDeleteModal = () => {isDeleteModalVisible.value = false;
};const confirmDelete = async () => {const session = sessions.value[currentDeleteIndex];if (session.talkInfoId.toString().startsWith('temp-')) {console.log('临时会话不能删除');closeDeleteModal();return;}try {await axios.post('/api/deepSeek/deleteTalkInfo', {id: session.talkInfoId});sessions.value.splice(currentDeleteIndex, 1);if (activeSessionIndex.value === currentDeleteIndex) {activeSessionIndex.value = Math.max(0, activeSessionIndex.value - 1);}} catch (error) {console.error('删除会话失败:', error);handleNetworkError(error, session);}closeDeleteModal();
};
//======================================================================================================================
// 选择会话
const selectSession = (index) => {activeSessionIndex.value = index;isConnected.value = sessions.value[index].isConnected;
};// 计算属性:判断是否有未连接的会话
const hasUnconnectedSession = computed(() => {return sessions.value.some(session =>!session?.isConnected);
});// 生命周期
let serviceCheckInterval;
onMounted(() => {fetchSessions(); // 初始化时获取会话列表// 定期检查后端服务可用性serviceCheckInterval = setInterval(() => {axios.get('/api/deepSeek/heartBeatCheck').then(() => {// 服务可用,对未连接的会话进行重试sessions.value.forEach(session => {if (!session?.isConnected) {reconnectWebSocket(session);}});}).catch(error => {console.error('后端服务不可用:', error);sessions.value.forEach(session => {session.isConnected = false;if (session?.socket) {session.socket.close();}});});}, 10000); // 每10秒检查一次
});onUnmounted(() => {sessions.value.forEach((session) => {if (session?.socket) session.socket.close();if (session?.reconnectInterval) clearInterval(session.reconnectInterval);});clearInterval(serviceCheckInterval);
});
</script><style scoped>
...此处省略
</style>
MyModal.vue
<template><div v-if="visible" class="modal-overlay" @click.self="close"><transition name="modal-fade"><div class="modal"><div class="modal-header"><h3>{{ title }}</h3><button @click="close">×</button></div><div class="modal-body"><slot></slot></div><div class="modal-footer"><slot name="footer"></slot></div></div></transition></div>
</template><script setup>
import { watch } from 'vue';// eslint-disable-next-line no-undef
const props = defineProps({visible: {type: Boolean,required: true},title: {type: String,required: true}
});// eslint-disable-next-line no-undef
const emit = defineEmits(['close']);const close = () => {emit('close');
};watch(() => props.visible, (newVal) => {if (!newVal) {close();}
});
</script><style scoped>
.modal-overlay {position: fixed;top: 0;left: 0;width: 100%;height: 100%;background-color: rgba(0, 0, 0, 0.5);display: flex;justify-content: center;align-items: center;z-index: 1000;animation: fadeIn 0.3s ease-in-out;
}.modal {background: white;border-radius: 12px;box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);width: 500px;max-width: 90%;transform-origin: center;animation: scaleUp 0.3s ease-in-out;
}.modal-header {display: flex;justify-content: space-between;align-items: center;padding: 16px 24px;border-bottom: 1px solid #e9ecef;
}.modal-header h3 {margin: 0;font-size: 1.25rem;font-weight: 600;color: #212529;
}.modal-header button {background: none;border: none;font-size: 1.5rem;line-height: 1;color: #6c757d;cursor: pointer;transition: color 0.2s;padding: 4px;
}.modal-header button:hover {color: #dc3545;
}.modal-body {padding: 20px 24px;color: #495057;line-height: 1.6;max-height: 70vh;overflow-y: auto;
}.modal-footer {display: flex;justify-content: flex-end;gap: 12px;padding: 16px 24px;border-top: 1px solid #e9ecef;background: #f8f9fa;border-radius: 0 0 12px 12px;
}@keyframes fadeIn {from { opacity: 0; }to { opacity: 1; }
}@keyframes scaleUp {from { transform: scale(0.95); }to { transform: scale(1); }
}@media (max-width: 480px) {.modal {width: 95%;margin: 10px;}.modal-header,.modal-body,.modal-footer {padding: 12px 16px;}
}
</style>
后端部分
项目结构图如下
WebSocketConfig 配置类
package com.deepseek.project.websocket;import jakarta.annotation.Resource;
import org.apache.catalina.connector.Connector;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;/*** @author hulei* websocket配置类*/
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {@Resourceprivate ApplicationContext applicationContext;@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}@Beanpublic ServletWebServerFactory servletContainer() {TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();tomcat.addAdditionalTomcatConnectors(createWebSocketConnector());return tomcat;}/*** 设置最大消息大小*/@Beanpublic ServletServerContainerFactoryBean createWebSocketContainer() {ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();// 在此处设置bufferSizecontainer.setMaxTextMessageBufferSize(512000);container.setMaxBinaryMessageBufferSize(512000);container.setMaxSessionIdleTimeout(15 * 60000L);return container;}@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(deepSeekWebSocketHandler(), "/websocket").setAllowedOrigins("*");}public DeepSeekWebSocketHandler deepSeekWebSocketHandler() {// 从spring容器中获取bean,如过直接new的话就不是spring管理的bean了,DeepSeekWebSocketHandler 内部使用依赖注入其他的类则会为nullreturn applicationContext.getBean(DeepSeekWebSocketHandler.class);}private Connector createWebSocketConnector() {Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");connector.setScheme("ws");// WebSocket 服务的端口,多通道公用connector.setPort(8085);return connector;}
}
这段代码是WebSocket配置类,主要用于配置和初始化WebSocket服务。功能包括:
- 启用WebSocket并注册处理器。
- 配置Tomcat服务器以支持WebSocket连接。
- 设置WebSocket的最大消息大小和会话超时时间。
- 创建WebSocket连接器并指定端口。
控制流程图如下:
AbstractDeepSeekTool
package com.deepseek.project.tool;import com.deepseek.project.constant.Constants;
import com.deepseek.project.service.ITalkInfoService;
import com.google.gson.Gson;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;import java.io.IOException;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** author: hulei* 抽象的DeepSeek工具类,为什么要抽象呢,因为笔者既想使用本地部署的deepSeek,又想使用线上官方提供的API,所以抽象了DeepSeek工具类,*/
@Slf4j
@Data
public abstract class AbstractDeepSeekTool {private static final int MAX_CONCURRENT = 20;private static final int BATCH_SIZE = 10;public static OkHttpClient client;private static ExecutorService httpExecutor;public static final Gson gson = new Gson();/*** deepSeek会话历史记录*/public final List<Map<String, String>> conversationHistory = Collections.synchronizedList(new ArrayList<>());/*** 是否停止发送标志,由前端传值控制*/public volatile boolean stopSending = false;/*** DeepSeek流式响应消息缓存列表,攒到一定数量时,批量保存到数据库*/private final List<Map<String, String>> deepSeekMessageCache = new ArrayList<>();/*** ITalkInfoService*/private final ITalkInfoService italkInfoService;public AbstractDeepSeekTool(ITalkInfoService italkInfoService, List<Map<String, String>> conversationHistory) {this.italkInfoService = italkInfoService;this.conversationHistory.addAll(conversationHistory);init();}public void init() {httpExecutor = Executors.newCachedThreadPool(r -> {Thread t = new Thread(r);t.setDaemon(true);return t;});client = new OkHttpClient.Builder().dispatcher(new Dispatcher(httpExecutor)).connectionPool(new ConnectionPool(50, 5, java.util.concurrent.TimeUnit.MINUTES)).connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS).readTimeout(60, java.util.concurrent.TimeUnit.SECONDS).writeTimeout(60, java.util.concurrent.TimeUnit.SECONDS).build();}public void processMessage(String userMessage, WebSocketSession session, String talkInfoId) {synchronized (conversationHistory) {Map<String, String> userMessageMap = Map.of("role", "user", "content", userMessage);conversationHistory.add(userMessageMap);if (!talkInfoId.startsWith(Constants.TEMP_TALK_INFO_ID_PREFIX)) {addMessageToCache(userMessageMap, talkInfoId);}}requestDeepSeekMessage(session, talkInfoId);}/*** 构建请求头抽象方法,交给子类实现*/protected abstract Request buildRequest();/*** 请求DeepSeek深度搜索消息, 交给子类实现*/public abstract void requestDeepSeekMessage(WebSocketSession session, String talkInfoId);/*** 添加消息到DeepSeek流式响应消息缓存列表中*/public void addMessageToCache(Map<String, String> deepSeekMessageMap, String talkInfoId) {deepSeekMessageCache.add(deepSeekMessageMap);if (deepSeekMessageCache.size() >= BATCH_SIZE) {saveCachedDeepSeekMessages(talkInfoId);}}/*** 保存DeepSeek流式响应消息缓存列表中的消息到数据库*/public void saveCachedDeepSeekMessages(String talkInfoId) {if (!deepSeekMessageCache.isEmpty()) {try {italkInfoService.saveTalkInfoDeepSeekHistory(Integer.parseInt(talkInfoId), deepSeekMessageCache);log.info("批量消息保存到数据库成功,数量: {}", deepSeekMessageCache.size());deepSeekMessageCache.clear();} catch (Exception e) {log.error("批量消息保存到数据库失败,数量: {}", deepSeekMessageCache.size(), e);}}}/*** 发送响应结束标记信息给前端, 前端会根据这个标记来判断是否继续接收消息,改变按钮状态从停止生成改为待发送状态*/public void sendEndMarker(WebSocketSession session) {try {session.sendMessage(new TextMessage("[END_OF_MESSAGE_GENERATE]"));} catch (IOException e) {log.error("发送结束标记失败", e);}}/*** 处理异常*/public void handleError(WebSocketSession session, Exception e) {log.error("请求处理异常", e);try {session.sendMessage(new TextMessage("系统错误: " + e.getMessage()));sendEndMarker(session);} catch (IOException ex) {log.error("发送错误信息失败", ex);}}
}
这段代码定义了一个抽象类 AbstractDeepSeekTool,用于处理与 DeepSeek 的交互。主要功能包括初始化 HTTP 客户端和线程池、处理用户消息、请求 DeepSeek 消息、缓存和批量保存消息到数据库、发送结束标记给前端以及处理异常。
- 初始化:设置 HTTP 客户端和线程池。
- 处理用户消息:将用户消息添加到会话历史记录并缓存。
- 请求 DeepSeek 消息:由子类实现具体逻辑。
- 缓存和批量保存:将消息缓存并在达到批量大小时保存到数据库。
- 发送结束标记:通知前端消息生成结束。
- 处理异常:捕获并处理异常,发送错误信息给前端。
控制流图
DeepSeekWebSocketHandler
package com.deepseek.project.websocket;import com.deepseek.project.constant.Constants;
import com.deepseek.project.model.TalkInfoDeepSeekHistory;
import com.deepseek.project.service.ITalkInfoService;
import com.deepseek.project.tool.AbstractDeepSeekTool;
import com.deepseek.project.tool.DeepSeekLocalTool;
import com.deepseek.project.tool.DeepSeekOnlineTool;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;import java.io.IOException;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/*** author: 胡磊* WebSocket处理器,用于处理与客户端的WebSocket通信*/
@Slf4j
@Component
public class DeepSeekWebSocketHandler extends TextWebSocketHandler {// 使用talkInfoId作为键的会话映射private final Map<String, AbstractDeepSeekTool> talkSessionMap = new ConcurrentHashMap<>();private static final Gson gson = new Gson();private static final ObjectMapper objectMapper = new ObjectMapper();@Resourceprivate ITalkInfoService italkInfoService;@Value("${deepseek.tool.type:local}")private String deepSeekToolType;@Overridepublic void afterConnectionEstablished(@NotNull WebSocketSession session) {String talkInfoId = extractTalkInfoId(session);if (talkInfoId == null) {closeWithError(session);return;}// 创建或获取已有会话工具talkSessionMap.computeIfAbsent(talkInfoId, id -> {log.info("构建新会话工具,talkInfoId: {}", talkInfoId);List<Map<String, String>> conversationHistory = new ArrayList<>();if (!talkInfoId.startsWith(Constants.TEMP_TALK_INFO_ID_PREFIX)) {//根据会话框id查询会话框历史记录List<TalkInfoDeepSeekHistory> historyList = italkInfoService.getDeepSeekHistoryListByTalkInfoId(Integer.parseInt(talkInfoId));for (TalkInfoDeepSeekHistory history : historyList) {Type type = new TypeToken<Map<String, String>>() {}.getType();Map<String, String> messageMap = gson.fromJson(history.getContent(), type);conversationHistory.add(messageMap);}}return createDeepSeekTool(conversationHistory);});}/*** 创建AbstractDeepSeekTool具体类的实例,根据配置的toolType参数来选择创建哪种类型的工具*/private AbstractDeepSeekTool createDeepSeekTool(List<Map<String, String>> conversationHistory) {if ("online".equalsIgnoreCase(deepSeekToolType)) {return new DeepSeekOnlineTool(italkInfoService, conversationHistory);} else {return new DeepSeekLocalTool(italkInfoService, conversationHistory);}}/*** 从WebSocketSession中提取talkInfoId参数值* 此方法主要用于从WebSocket连接的URI中提取出talkInfoId参数值,该参数值用于标识对话信息* 如果URI为空,或者没有找到talkInfoId参数,则返回null** @param session WebSocketSession对象,包含客户端与服务器之间的WebSocket连接信息* @return String 返回提取出的talkInfoId参数值,如果没有找到则返回null*/private String extractTalkInfoId(WebSocketSession session) {try {URI uri = session.getUri();if (uri == null) return null;return Arrays.stream(uri.getQuery().split("&")).filter(param -> param.startsWith("talkInfoId=")).findFirst().map(param -> param.split("=")[1]).orElse(null);} catch (Exception e) {log.error("解析talkInfoId失败", e);return null;}}/*** 处理WebSocket消息*/@Overrideprotected void handleTextMessage(@NotNull WebSocketSession session, @NotNull TextMessage message) {try {log.info("收到消息:{}", message.getPayload());JsonNode json = objectMapper.readTree(message.getPayload());String talkInfoId = json.get("talkInfoId").asText();String content = json.get("content").asText();AbstractDeepSeekTool tool = talkSessionMap.get(talkInfoId);if (tool == null) {log.warn("找不到对应的会话工具,talkInfoId: {}", talkInfoId);sendErrorMessage(session, "无效 session");return;}if ("stopSending".equalsIgnoreCase(content)) {handleStopCommand(tool, session);} else {handleNormalMessage(tool, session, content, talkInfoId);}} catch (IOException e) {log.error("消息解析失败", e);sendErrorMessage(session, "Invalid message format");}}/*** 处理前端点击发送的停止生成命令*/private void handleStopCommand(AbstractDeepSeekTool tool, WebSocketSession session) {tool.setStopSending(true);try {session.sendMessage(new TextMessage("[END_OF_MESSAGE_GENERATE]"));} catch (IOException e) {log.error("停止命令响应失败", e);}}/*** 处理普通消息*/private void handleNormalMessage(AbstractDeepSeekTool tool, WebSocketSession session, String content, String talkInfoId) {tool.setStopSending(false);try {tool.processMessage(content, session, talkInfoId);} catch (Exception e) {log.error("消息处理异常", e);sendErrorMessage(session, "消息处理发生异常");}}@Overridepublic void afterConnectionClosed(@NotNull WebSocketSession session, @NotNull CloseStatus status) {String talkInfoId = extractTalkInfoId(session);if (talkInfoId != null) {// 根据业务需求决定是否立即清理资源// 如果是持久化会话可以保留,临时会话则移除,因为每次刷新页面临时会话都会新建,原来的就没用了,需要移除if (talkInfoId.startsWith(Constants.TEMP_TALK_INFO_ID_PREFIX)) {talkSessionMap.remove(talkInfoId);}log.info("会话关闭,talkInfoId: {}", talkInfoId);}}/*** 发送错误消息*/private void sendErrorMessage(WebSocketSession session, String error) {try {session.sendMessage(new TextMessage(objectMapper.createObjectNode().put("type", "error").put("message", error).toString()));} catch (IOException e) {log.error("错误信息发送失败", e);}}private void closeWithError(WebSocketSession session) {try {session.close(new CloseStatus(CloseStatus.BAD_DATA.getCode(), "缺失talkInfoId会话框ID参数"));} catch (IOException e) {log.error("关闭连接失败", e);}}
}
这段代码实现了一个WebSocket处理器,用于处理与客户端的WebSocket通信。主要功能包括:
- 建立连接时提取talkInfoId并创建或获取会话工具。
- 处理接收到的消息,区分普通消息和停止命令。
- 关闭连接时清理资源。
控制流图
数据库设计
之所以要设计几张表,是因为要实现保存上下文会话历史消息记录的需要,这里笔者使用的是mysql,其实每次的问答都会生成大量的文本消息和大量的DeepSeek的历史消息JSON数据,使用MongoDB或者Elasticsearch作为持久化工具对于查询性能更好,但笔者这里没有折腾了,读者朋友可以自己决定使用什么存储工具。
一共三张表:talk_info 、talk_info_deepseek_history、talk_info_messages
- talk_info
这个是对话框列表,就是新建会话的会话框会在这里新增数据
create table talk_info
(id int auto_incrementprimary key,title varchar(32) not null comment '对话标题',create_time datetime not null
)comment '对话框表';
- talk_info_deepseek_history
这个是deepSeek记录上下文请求和响应历史消息的表
create table talk_info_deepseek_history
(id int auto_increment comment '主键'primary key,talk_info_id int not null comment '对话框id',content text collate utf8mb4_unicode_ci null,create_time datetime null
)comment '对话框的deepseek历史对话记录';
- talk_info_messages
这个是干嘛的呢,也是记录上下文对话文本消息的表,只不过是记录单纯的文本消息,用于前端对话框展示的,而deepSeek则要求按照一定的json格式组装,这样才能被其加载读取历史会话记录,所以分开存储了。
create table talk_info_messages
(id int auto_incrementprimary key,talk_info_id int not null comment '对话框id',message_type varchar(10) null comment '消息类型:user,bot',message text collate utf8mb4_unicode_ci null,create_time datetime not null
)comment '对话历史消息列表';
总结
以上就是笔者开发deepSeek对话web应用的整个过程了,笔者的技术能力有限,尤其是前端部分,很多样式的调整,其实是借助了大模型帮我调整的,不足之处请大家多多指教!
相关文章:
DeepSeek本地部署+自主开发对话Web应用
文章目录 引言前端部分核心页面DeepSeek.vueMyModal.vue 后端部分WebSocketConfig 配置类AbstractDeepSeekToolDeepSeekWebSocketHandler 数据库设计总结 引言 最近DeepSeep横空出世,在全球内掀起一股热潮,到处都是满血大模型接入的应用,但这…...
【Springboot】解决问题 o.s.web.servlet.PageNotFound : No mapping for *
使用 cursor 进行老项目更新为 springboot 的 web 项目,发生了奇怪的问题,就是 html 文件访问正常,但是静态文件就是 404 检查了各种配置,各种比较,各种调试,最后放弃时候,清理没用的配置文件&…...
微信小程序点击按钮,将图片下载到本地
前言: 最近在公司完成一个小程序的时候需要实现一个功能:点击按钮获取用户相册权限,将图片下载到用户本地相册,经过了好几次的尝试最终算是实现了。将总结的经验在这里分享给小伙伴们。 实现方式: //.wxml文件 <…...
解锁网络防御新思维:D3FEND 五大策略如何对抗 ATTCK
D3FEND 简介 背景介绍 2021年6月22日(美国时间),美国MITRE公司正式发布了D3FEND——一个网络安全对策知识图谱。该项目由美国国家安全局(NSA)资助,并由MITRE的国家安全工程中心(NSECÿ…...
架构案例:从初创互联网公司到分布式存储与反应式编程框架的架构设计
文章目录 引言一、初创互联网公司架构演化案例1. 万级日订单级别架构2. 十万级日订单级别架构3. 百万级日订单级别架构 二、分布式存储系统 Doris 架构案例三、反应式编程框架架构案例总结 引言 分布式架构 今天我们将探讨三种不同类型的架构案例,分别探讨 一个初…...
Redis数据结构-List列表
1.List列表 列表类型适用于存储多个有序的字符串(这里的有序指的是强调数据排列顺序的重要,不是升序降序的意思),列表中的每个字符串称为元素(element),一个列表最多可以存储2^32-1个元素。在R…...
开启AI短剧新纪元!SkyReels-V1/A1双剑合璧!昆仑万维开源首个面向AI短剧的视频生成模型
论文链接:https://arxiv.org/abs/2502.10841 项目链接:https://skyworkai.github.io/skyreels-a1.github.io/ Demo链接:https://www.skyreels.ai/ 开源地址:https://github.com/SkyworkAI/SkyReels-A1 https://github.com/Skywork…...
mac安装环境
minconda https://docs.anaconda.net.cn/miniconda/install/ 注意在下载下来应该有100多兆,太大了应该是完整版,我们不需要 jdk 镜像网站下载设置环境变量: 终端:sudo vim ~/.zshrc # JDK Config JAVA_HOME/Library/Java/Java…...
js加密之延伸requestAnimationFrame
简言 上篇文章有提到requestAnimationFrame,只是随笔带过。这篇文章就着重研究一下requestAnimationFrame的运用,以及实际作用。还有关于在js加密技术中的落地实现可行性。 功能说明 小声说一下,做开发的同学一定要学会翻官方文档,我这里直接引用一段官方介绍。 …...
SpringBoot @Value 注解使用
Value 注解用于将配置文件中的属性值注入到Spring管理的Bean中。 1. 基本用法 Value 可以直接注入配置文件中的属性值。 配置文件 (application.properties 或 application.yml) 配置文件定义需要注入的数据。 consumer:username: lisiage: 23hobby: sing,read,sleepsubje…...
JavaFunction的使用
一、基础概念与核心方法 定义与作用 Function<T, R> 是一个函数式接口,接收类型为 T 的输入参数,返回类型为 R 的结果。其核心方法为 apply(T t)。例如,将字符串转换为整数长度: java Function<String, Integer>…...
TVbox蜂蜜影视:智能电视观影新选择,简洁界面与强大功能兼具
蜂蜜影视是一款基于猫影视开源项目 CatVodTVJarLoader 开发的智能电视软件,专为追求简洁与高效观影体验的用户设计。该软件从零开始编写,界面清爽,操作流畅,特别适合在智能电视上使用。其最大的亮点在于能够自动跳过失效的播放地址…...
Python基于交互注意力的深度时空网络融合多源信息的剩余寿命预测方法
基于交互注意力的深度时空网络融合多源信息的剩余寿命预测方法 一、方法框架设计 本方法的核心思想是通过交互注意力机制动态捕捉多源数据间的跨模态关联,并结合深度时空网络建模序列的时空退化特征。 1. 多源特征编码器 输入:传感器数据、工况参数、…...
阿里云 | 快速在网站上增加一个AI助手
创建智能体应用 如上所示,登录阿里云百炼人工智能业务控制台,创建智能体应用,智能体应用是一个agent,即提供个人或者企业的代理或中间件组件应用,对接阿里云大模型公共平台,为个人或者企业用户提供大模型应…...
基于Electron的应用程序安全测试基础 — 提取和分析.asar文件的案例研究
目录: 4.4. 案例研究 4.4.2. 情况描述 4.4.3. 信息收集 4.4.3.2. 检查隐藏目录(点目录)的可能性 4.4.3.3. 使用 DB Browser for SQLite 打开 .db 文件 4.4.3.4. 寻找加密算法 4.4.3.5. 找到加密算法 4.4.3.6. 理解加密流程 4.4.3.7. 找到“Ke…...
Vue3生命周期以及与Vue2的区别
文章目录 一、Vue3生命周期核心阶段与钩子函数二、Vue3生命周期示例:选项式 vs 组合式 API选项式 API 示例(Vue2)组合式 API 示例(Vue3) 三、Vue3与Vue2生命周期的核心差异1. 钩子函数更名2. 组合式 API 的影响3. 新增…...
windows下安装CUDA-本地微调大模型
1、查看NVIDIA的控制面板的版本号 2 下载CUDA Toolkit https://developer.nvidia.com/cuda-toolkit-archive 这里要下载和自己电脑NVIDIA适配CUDA的大版本要保持一致 选择对应的版本进行下载 文件比较大,直接右键复制链接,放到迅雷中两分钟就下好了 3 …...
LeetCode:132. 分割回文串 II(DP Java)
目录 132. 分割回文串 II 题目描述: 实现代码与解析: DP 原理思路: 132. 分割回文串 II 题目描述: 给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串。 返回符合要求的 最少分割次数…...
C# 13与.NET 9革新及工业开发应用
摘要 微软推出的C# 13与.NET 9以“高效且智能”为导向,具备扩展类型、半自动属性、锁对象优化等十大革新。本文深入剖析新特性于工业级开发的应用场景,包含性能优化策略、AI集成方案以及EF Core实战技巧,为开发者提供从理论到实践的完整指引…...
IPoIB源码深度解析:如何基于TCP/IP协议栈实现高性能InfiniBand通信
一、IPoIB的核心设计理念 IPoIB(IP over InfiniBand)是一种在InfiniBand网络上承载IP流量的技术,其核心目标是在不修改上层应用的前提下,利用InfiniBand的高带宽和低延迟特性。与自定义协议栈不同,IPoIB通过深度集成到Linux内核TCP/IP协议栈中,将InfiniBand设备抽象为标…...
《白帽子讲 Web 安全:点击劫持》
目录 摘要: 一、点击劫持概述 二、点击劫持的实现示例:诱导用户收藏指定淘宝商品 案例 构建恶意页面: 设置绝对定位和z - index: 控制透明度: 三、其他相关攻击技术 3.1图片覆盖攻击与 XSIO 3.2拖拽劫持与数据…...
PostgreSQL10 逻辑复制实战:构建高可用数据同步架构!
PostgreSQL10 逻辑复制实战:打造高可用数据同步架构! 概述 PostgreSQL 10 引入了逻辑复制(Logical Replication),为数据库高可用和数据同步提供了更灵活的选择。PostgreSQL 复制机制主要分为物理复制和逻辑复制两种&…...
Spring Boot 异步编程深入剖析
Spring Boot 异步编程深入剖析 1. 异步方法的使用 原理深度解析 Spring Boot 的异步方法基于 Spring 的 AOP(面向切面编程)实现。当在方法上添加 Async 注解时,Spring 会为该方法所在的类创建一个代理对象。当调用该异步方法时,…...
《动手学习深度学习》的笔记,将会持续更新。
1.什么是机器学习? 机器学习是:换句话说,我们用数据训练(train)模型。 数据不断的训练出比较好的模型。 1.2 机器学习的关键零件 1.学习的数据。 2. 如何转换数据的模型。 3.一个目标函数。 4.调整模型参数以优化目标函数的算法。 1,数据有什么组成? 数据=样本+…...
[STM32]从零开始的STM32 BSRR、BRR、ODR寄存器讲解
一、前言 学习STM32一阵子以后,相信大家对STM32 GPIO的控制也有一定的了解了。之前在STM32 LED的教程中也教了大家如何使用寄存器以及库函数控制STM32的引脚从而点亮一个LED,之前的寄存器只是作为一个引入,并没有深层次的讲解,在教…...
VUE3+Vite使用TailwindCSS【若依前后端分离框架】
参考:https://tailwind.nodejs.cn/docs/guides/vite#vue 和 https://blog.csdn.net/hjl_and_djj/article/details/144694485依次运行命令: cnpm install -D tailwindcss3.4.17 postcss autoprefixernpx tailwindcss init -p修改配置文件tailwind.config.…...
【Linux文件IO】系统IO详情
目录 一、前言 二、相关API介绍 2.1 open 2.2 read 2.3 write 2.4 lseek 2.5 close 三、简单示例 3.1 示例1 3.2 示例2 一、前言 在 Linux 系统编程中,系统 I/O(又称低级 I/O)是直接通过操作系统提供的系统调用实现的文件操作接口…...
【弹性计算】弹性裸金属服务器和神龙虚拟化(三):弹性裸金属技术
弹性裸金属服务器和神龙虚拟化(三):弹性裸金属技术 1.弹性裸金属技术背景1.1 传统 KVM 虚拟化系统导致 CPU 计算特性损失1.2 传统 KVM 虚拟化系统导致资源争抢不可避免1.3 传统 KVM 虚拟化系统导致 I/O 性能瓶颈 2.弹性裸金属技术实现2.1 VPC…...
(贪心 合并区间)leetcode 56
思路来源:代码随想录--代码随想录_合并区间题解 首先用lambda 按照左界值升序排序 建立答案的二维数组,将第一个行区间放入,判断从第二行开始 第i行的左区间一定大于第i-1行的左区间(排序过了),所以只判断…...
如何理解语言模型
统计语言模型 先看语言模型,语言即自然语言,模型及我们要解决的某个任务。 任务一:判断哪句话出现的概率大 任务二:预判空缺的位置最有可能是哪个词 再看统计,统计即解决上述两个任务的解决方法。先对语句进行分词…...
动态规划/贪心算法
一、动态规划 动态规划 是一种用于解决优化问题的算法设计技术,尤其适用于具有重叠子问题和最优子结构性质的问题。它通过将复杂问题分解为更简单的子问题,并保存这些子问题的解以避免重复计算,从而提高效率。 动态规划的核心思想 最优子结…...
Hadoop简介
1. Hadoop简介 官网:http://hadoop.apache.org 1.1 Hadoop架构 Hadoop由三个模块组成:分布式存储HDFS、分布式计算MapReduce、资源调度引擎YARN 1.2 Hadoop历史 Hadoop作者Doug Cutting Apache Lucene是一个文本搜索系统库 Apache Nutch作为前者的一部…...
Vscode 便用快捷键设置教程
文章目录 简介:1. go to define (跳转到函数定义的位置)2. go to declaration (跳转到函数声明的位置)3. move line (上下移动本行代码)3.1上下复制本行代码 4. 前进和后退(就是前进到光标上一次停留的位置,和后退到那…...
数据库(MySQL):使用命令从零开始在Navicat创建一个数据库及其数据表(一).创建基础表
一. 使用工具和命令 1.1 使用的工具 Navicat Premium 17 :“Navicat”是一套可创建多个连接的数据库管理工具。 MySQL版本8.0.39 。 1.2 使用的命令 Navicat中使用的命令 命令 命令解释 SHOW DATABASES; 展示所有的数据库 CREATE DATABASE 数据…...
水滴tabbar canvas实现思路
废话不多说之间看效果图,只要解决了这个效果水滴tabbar就能做出来了 源码地址 一、核心实现步骤分解 布局结构搭建 使用 作为绘制容器 设置 width=600, height=200 基础尺寸 通过 JS 动态计算实际尺寸(适配高清屏) function initCanvas() {// 获取设备像素比(解决 Re…...
windows安装vue
1、下载nodejs安装包 https://nodejs.cn/download/ 2、安装node 中途记得可以自己改安装路径,其他都是下一步 3、安装完成后检查 node -v :查看nodejs的版本 npm -v :查看npm的版本 4、修改npm默认安装目录与缓存日志目录的位置 在nodejs目…...
使用3090显卡部署Wan2.1生成视频
layout: post title: 使用3090显卡部署Wan2.1生成视频 catalog: true tag: [Kubernetes, GPU, AI] 使用3090显卡部署Wan2.1生成视频 1. 环境说明2. 模型下载3. 克隆仓库4. 安装依赖5. 生成视频 5.1. 使用generate脚本生成5.2. 使用gradio启动UI界面生成 5.2.1. 启动gradio服务5…...
DCN讲解
DCN是DeepFM的升级版,后者是只能做二阶交叉特征,随着阶数上升,模型复杂度大幅提高,且FM网络层较浅,表达能力有限。google团队通过构建深度交叉网络来自动进行特征的高阶交叉,且时空复杂度均为线性增长&…...
ARM 架构下 cache 一致性问题整理
本篇文章主要整理 ARM 架构下,和 Cache 一致性相关的一些知识。 本文假设读者具备一定的计算机体系结构和 Cache 相关基础知识,适合有相关背景的读者阅读 1、引言 简单介绍一下 Cache 和内存之间的关系 在使能 Cache 的情况下,CPU 每次获取数…...
算法-二分查找
二分查找 其实二分查找是一个很简单理解的东西,从他的名字就可以看出,就是要分为两段去查找一个元素 我们确定一个中间元素,然后将这一个元素和左边的部分和右边的部分做对比 然后根据实际情况来选择一个部分来继续做这么一个步骤 直到找…...
Python Cookbook-2.24 在 Mac OSX平台上统计PDF文档的页数
任务 你的计算机运行着比较新的MacOSX系统(10.3的“Panther”或更新的版本),现在需要知道一个 PDF 文档的页数。 解决方案 PDF格式和 Python都已经集成到了Mac OsX系统中(10.3或更高版本),因而这个问题解决起来也相对比较容易: #!/usr/bin python im…...
【MySQL】索引(页目录、B+树)
文章目录 1. 引入索引2. MySQL与磁盘交互的基本单位3. 索引的理解3.1 页目录3.2 B树 4. 聚簇索引、非聚簇索引5. 索引的操作5.1 索引的创建5.1.1 创建主键索引5.1.2 创建唯一索引5.1.3 普通索引的创建5.1.4 全文索引的创建 5.2 索引的查询5.3 删除索引 1. 引入索引 索引&#…...
工业AR眼镜的‘芯’动力:FPC让制造更智能【新立电子】
随着增强现实(AR)技术的快速发展,工业AR智能眼镜也正逐步成为制造业领域的重要工具。它不仅为现场工作人员提供了视觉辅助,还极大地提升了远程协助的效率、优化了仓储管理。FPC在AI眼镜中的应用,为工业AR智能眼镜提供了…...
开启mysql的binlog日志
mysql版本5.7 1.查看是否开启bin_log show global variables like’log_bin’; off的话需要先开启 在mysql的文件夹目录中找到my.ini 加一行log-bin“C:/ProgramData/MySQL/MySQL Server 5.7/logs/log-bin” 并提前创建好目录 2.数据库会把日志放进logs目录中 3.查看log日…...
SpringSecurity基于JWT实现Token的处理
前面介绍了手写单点登录和JWT的应用,本文结合SpringSecurity来介绍下在SpringBoot项目中基于SpringSecurity作为认证授权框架的情况下如何整合JWT来实现Token的处理。 一、认证思路分析 SpringSecurity主要是通过过滤器来实现功能的!我们要找到SpringSecurity实现认证和校验…...
数据结构与算法-图论-最短路-floyd扩展
floyd和它的拓展: 在计算机科学领域,Floyd通常指Floyd Warshall算法,由罗伯特弗洛伊德(Robert W. Floyd)提出,这是一种用于在加权有向图中查找所有顶点对之间最短路径的算法。 算法原理 Floyd Warsha…...
c++中所有构造函数的介绍与使用
C 中,构造函数是一种特殊的成员函数,用于在创建对象时对对象进行初始化。C 中有多种类型的构造函数,下面详细介绍这些构造函数及其特点和使用场景。 1. 默认构造函数 定义:默认构造函数是指在没有提供任何参数的情况下可以被调用…...
力扣1584. 连接所有点的最小费用
力扣1584. 连接所有点的最小费用 题目 题目解析及思路 题目要求返回最小生成树 最小生成树模版题 法一:prim 主要思想是每次找离树最近的顶点,将其加入树种,并更新其他所有点到该点的距离 代码 class Solution { public:int minCostCo…...
FPGA开发,使用Deepseek V3还是R1(8):FPGA的全流程(简略版)
以下都是Deepseek生成的答案 FPGA开发,使用Deepseek V3还是R1(1):应用场景 FPGA开发,使用Deepseek V3还是R1(2):V3和R1的区别 FPGA开发,使用Deepseek V3还是R1&#x…...
处理大数据的架构模式:Lambda 架构 和 Kappa 架构
Lambda 架构 和 Kappa 架构 是两种用于处理大数据的架构模式,尤其在实时数据处理场景中广泛应用。 1. Lambda 架构 核心思想 Lambda 架构将数据处理分为两条独立的流水线: 批处理层(Batch Layer): 处理全量数据&…...