[dev]resources & chat

This commit is contained in:
susie-laptop 2025-04-27 19:52:26 -04:00
parent f2a6029076
commit 89d42b9e10
7 changed files with 498 additions and 103 deletions

8
package-lock.json generated
View File

@ -11,7 +11,7 @@
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@reduxjs/toolkit": "^2.6.0", "@reduxjs/toolkit": "^2.6.0",
"axios": "^1.8.1", "axios": "^1.8.1",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.5",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react": "^19.0.0", "react": "^19.0.0",
@ -1656,9 +1656,9 @@
} }
}, },
"node_modules/bootstrap": { "node_modules/bootstrap": {
"version": "5.3.3", "version": "5.3.5",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.5.tgz",
"integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", "integrity": "sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",

View File

@ -13,7 +13,7 @@
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@reduxjs/toolkit": "^2.6.0", "@reduxjs/toolkit": "^2.6.0",
"axios": "^1.8.1", "axios": "^1.8.1",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.5",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react": "^19.0.0", "react": "^19.0.0",

View File

@ -3,8 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import AppRouter from './router/router'; import AppRouter from './router/router';
import { checkAuthThunk } from './store/auth/auth.thunk'; import { checkAuthThunk } from './store/auth/auth.thunk';
import { login } from './store/auth/auth.slice'; import { initWebSocket, closeWebSocket, initChatWebSocket } from './services/websocket';
import { initWebSocket, closeWebSocket } from './services/websocket';
import { setWebSocketConnected } from './store/notificationCenter/notificationCenter.slice'; import { setWebSocketConnected } from './store/notificationCenter/notificationCenter.slice';
function App() { function App() {
@ -24,6 +23,7 @@ function App() {
if (!isConnected) { if (!isConnected) {
// WebSocket // WebSocket
initWebSocket(dispatch); initWebSocket(dispatch);
initChatWebSocket()
} }
} else { } else {
if (isConnected) { if (isConnected) {

View File

@ -1,11 +1,12 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { resetSendMessageStatus, addMessage } from '../../store/chat/chat.slice'; import { resetSendMessageStatus, addMessage, updateMessage } from '../../store/chat/chat.slice';
import { showNotification } from '../../store/notification.slice'; import { showNotification } from '../../store/notification.slice';
import { createChatRecord, fetchAvailableDatasets, fetchConversationDetail } from '../../store/chat/chat.thunks'; import { createChatRecord, fetchAvailableDatasets, fetchConversationDetail, getChatResources } from '../../store/chat/chat.thunks';
import SvgIcon from '../../components/SvgIcon'; import SvgIcon from '../../components/SvgIcon';
import SafeMarkdown from '../../components/SafeMarkdown'; import SafeMarkdown from '../../components/SafeMarkdown';
import ResourceList from '../../components/ResourceList'; import ResourceList from '../../components/ResourceList';
import { sendChatMessageViaWebSocket } from '../../services/websocket';
export default function ChatWindow({ chatId, knowledgeBaseId }) { export default function ChatWindow({ chatId, knowledgeBaseId }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -20,7 +21,6 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
const currentChat = chatList.find((chat) => chat.conversation_id === currentChatId); const currentChat = chatList.find((chat) => chat.conversation_id === currentChatId);
const messages = currentChat?.messages || []; const messages = currentChat?.messages || [];
const messageStatus = useSelector((state) => state.chat.list.messageStatus); const messageStatus = useSelector((state) => state.chat.list.messageStatus);
const messageError = useSelector((state) => state.chat.list.messageError);
const { status: sendStatus, error: sendError } = useSelector((state) => state.chat.sendMessage); const { status: sendStatus, error: sendError } = useSelector((state) => state.chat.sendMessage);
// //
@ -29,7 +29,6 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
// 使Redux // 使Redux
const knowledgeBases = useSelector((state) => state.knowledgeBase.knowledgeBases || []); const knowledgeBases = useSelector((state) => state.knowledgeBase.knowledgeBases || []);
const knowledgeBase = knowledgeBases.find((kb) => kb.id === knowledgeBaseId); const knowledgeBase = knowledgeBases.find((kb) => kb.id === knowledgeBaseId);
const isLoadingKnowledgeBases = useSelector((state) => state.knowledgeBase.loading);
// //
const availableDatasets = useSelector((state) => state.chat.availableDatasets.items || []); const availableDatasets = useSelector((state) => state.chat.availableDatasets.items || []);
@ -63,14 +62,12 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
// 使conversation // 使conversation
if (conversation && conversation.datasets && conversation.datasets.length > 0) { if (conversation && conversation.datasets && conversation.datasets.length > 0) {
const datasetIds = conversation.datasets.map((ds) => ds.id); const datasetIds = conversation.datasets.map((ds) => ds.id);
console.log('从会话中获取知识库列表:', datasetIds);
setSelectedKnowledgeBaseIds(datasetIds); setSelectedKnowledgeBaseIds(datasetIds);
} }
// 使URLID // 使URLID
else if (knowledgeBaseId) { else if (knowledgeBaseId) {
// IDID // IDID
const ids = knowledgeBaseId.split(',').map((id) => id.trim()); const ids = knowledgeBaseId.split(',').map((id) => id.trim());
console.log('从URL参数中获取知识库列表:', ids);
setSelectedKnowledgeBaseIds(ids); setSelectedKnowledgeBaseIds(ids);
} }
}, [conversation, knowledgeBaseId]); }, [conversation, knowledgeBaseId]);
@ -81,7 +78,6 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
// chatId // chatId
if (hasLoadedDetailRef.current[chatId]) { if (hasLoadedDetailRef.current[chatId]) {
console.log('跳过已加载过的会话详情:', chatId);
return; return;
} }
@ -90,18 +86,15 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
// //
if (isNewlyCreatedChat && conversation && conversation.conversation_id === chatId) { if (isNewlyCreatedChat && conversation && conversation.conversation_id === chatId) {
console.log('跳过新创建会话的详情获取:', chatId);
hasLoadedDetailRef.current[chatId] = true; hasLoadedDetailRef.current[chatId] = true;
return; return;
} }
console.log('获取会话详情:', chatId);
setLoading(true); setLoading(true);
dispatch(fetchConversationDetail(chatId)) dispatch(fetchConversationDetail(chatId))
.unwrap() .unwrap()
.then((response) => { .then((response) => {
console.log('获取会话详情成功:', response);
// //
hasLoadedDetailRef.current[chatId] = true; hasLoadedDetailRef.current[chatId] = true;
}) })
@ -198,21 +191,17 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
if (selectedKnowledgeBaseIds.length > 0) { if (selectedKnowledgeBaseIds.length > 0) {
// 使 // 使
dataset_id_list = selectedKnowledgeBaseIds.map((id) => id.replace(/-/g, '')); dataset_id_list = selectedKnowledgeBaseIds.map((id) => id.replace(/-/g, ''));
console.log('使用组件状态中的知识库列表:', dataset_id_list);
} else if (conversation && conversation.datasets && conversation.datasets.length > 0) { } else if (conversation && conversation.datasets && conversation.datasets.length > 0) {
// 使 // 使
dataset_id_list = conversation.datasets.map((ds) => ds.id.replace(/-/g, '')); dataset_id_list = conversation.datasets.map((ds) => ds.id.replace(/-/g, ''));
console.log('使用会话中的知识库列表:', dataset_id_list);
} else if (knowledgeBaseId) { } else if (knowledgeBaseId) {
// 使 // 使
// IDID // IDID
const ids = knowledgeBaseId.split(',').map((id) => id.trim().replace(/-/g, '')); const ids = knowledgeBaseId.split(',').map((id) => id.trim().replace(/-/g, ''));
dataset_id_list = ids; dataset_id_list = ids;
console.log('使用URL参数中的知识库:', dataset_id_list);
} else if (availableDatasets.length > 0) { } else if (availableDatasets.length > 0) {
// 使 // 使
dataset_id_list = [availableDatasets[0].id.replace(/-/g, '')]; dataset_id_list = [availableDatasets[0].id.replace(/-/g, '')];
console.log('使用可用知识库列表中的第一个:', dataset_id_list);
} }
if (dataset_id_list.length === 0) { if (dataset_id_list.length === 0) {
@ -225,35 +214,125 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
return; return;
} }
console.log('发送消息参数:', { const requestBody = {
dataset_id_list, dataset_id_list: dataset_id_list,
question: inputMessage, question: inputMessage,
conversation_id: chatId, conversation_id: chatId,
}); };
// console.log('发送消息参数:', requestBody);
// WebSocketAIID -
const wsMessageId = `ws-${Date.now()}`;
let wsMessageContent = '';
//
const userMessageId = `user-${Date.now()}`;
dispatch( dispatch(
createChatRecord({ addMessage({
dataset_id_list: dataset_id_list, id: userMessageId,
role: 'user',
content: inputMessage,
created_at: new Date().toISOString(),
})
);
// WebSocket
dispatch(
addMessage({
id: wsMessageId,
role: 'assistant',
content: '',
created_at: new Date().toISOString(),
is_streaming: true,
is_websocket: true,
})
);
// -
dispatch(
getChatResources({
dataset_id_list,
question: inputMessage, question: inputMessage,
conversation_id: chatId, messageId: wsMessageId, //
}) })
) );
.unwrap()
.then((response) => { sendChatMessageViaWebSocket(requestBody, (data) => {
// try {
console.log('消息发送成功:', response); //
}) //
.catch((error) => { if (data.message === 'partial') {
// const newText = data.data.content || '';
console.error('消息发送失败:', error); wsMessageContent += newText;
// - 使React
dispatch(
updateMessage({
id: wsMessageId,
content: wsMessageContent,
is_streaming: true,
updated_at: new Date().toISOString(), //
})
);
//
setTimeout(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, 50);
}
//
if (data.message === '完成') {
console.log('WebSocket流式传输结束最终内容:', data.data.content);
// 使
dispatch(
updateMessage({
id: wsMessageId,
content: data.data.content || wsMessageContent,
is_streaming: false,
updated_at: new Date().toISOString(), //
})
);
//
setTimeout(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, 50);
}
//
if (data.code !== 200 && data.code !== 201) {
console.error('WebSocket错误:', data.message);
dispatch(
showNotification({
message: `WebSocket错误: ${data.message}`,
type: 'warning',
})
);
//
dispatch(
updateMessage({
id: wsMessageId,
content: `Error: ${data.message}`,
is_streaming: false,
})
);
}
} catch (error) {
console.error('处理WebSocket响应失败:', error);
//
dispatch( dispatch(
showNotification({ updateMessage({
message: `发送失败: ${error}`, id: wsMessageId,
type: 'danger', content: `处理WebSocket响应失败: ${error.message}`,
is_streaming: false,
}) })
); );
}); }
});
// //
setInputMessage(''); setInputMessage('');
@ -336,10 +415,11 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
</span> </span>
)} )}
{/* 只在AI回复消息下方显示资源列表 */} {/* 只在AI回复消息下方显示资源列表,仅当有内容时显示 */}
{message.role === 'assistant' && {message.role === 'assistant' &&
!message.is_streaming && !message.is_streaming &&
resources.messageId === message.id && resources.messageId === message.id &&
resources.status === 'succeeded' &&
resources.items.length > 0 && ( resources.items.length > 0 && (
<ResourceList resources={resources.items} /> <ResourceList resources={resources.items} />
)} )}

View File

@ -12,14 +12,21 @@ const secretKey = import.meta.env.VITE_SECRETKEY;
const API_URL = import.meta.env.VITE_API_URL || 'http://81.69.223.133:8008'; const API_URL = import.meta.env.VITE_API_URL || 'http://81.69.223.133:8008';
// 将 HTTP URL 转换为 WebSocket URL // 将 HTTP URL 转换为 WebSocket URL
const WS_BASE_URL = API_URL.replace(/^http/, 'ws').replace(/\/api\/?$/, ''); const WS_BASE_URL = API_URL.replace(/^http/, 'ws').replace(/\/api\/?$/, '');
const WS_CHAT_URL = 'ws://81.69.223.133:8008/ws/chat/stream/';
let socket = null; let socket = null;
let chatSocket = null;
let reconnectTimer = null; let reconnectTimer = null;
let pingInterval = null; let pingInterval = null;
let reconnectAttempts = 0; // 添加重连尝试计数器 let reconnectAttempts = 0; // 添加重连尝试计数器
let globalReconnectAttempts = 0; // 添加全局重连计数器
const RECONNECT_DELAY = 5000; // 5秒后尝试重连 const RECONNECT_DELAY = 5000; // 5秒后尝试重连
const PING_INTERVAL = 30000; // 30秒发送一次ping const PING_INTERVAL = 30000; // 30秒发送一次ping
const MAX_RECONNECT_ATTEMPTS = 3; // 最大重连尝试次数 const MAX_RECONNECT_ATTEMPTS = 3; // 最大重连尝试次数
const MAX_GLOBAL_RECONNECT_ATTEMPTS = 3; // 单个会话中允许的总重连次数
// 添加的聊天消息回调处理
let chatMessageCallback = null;
/** /**
* 初始化WebSocket连接 * 初始化WebSocket连接
@ -27,36 +34,63 @@ const MAX_RECONNECT_ATTEMPTS = 3; // 最大重连尝试次数
*/ */
export const initWebSocket = () => { export const initWebSocket = () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 检查全局重连次数
if (globalReconnectAttempts >= MAX_GLOBAL_RECONNECT_ATTEMPTS) {
console.warn(`已达到全局最大重连次数(${MAX_GLOBAL_RECONNECT_ATTEMPTS}),不再尝试重连`);
reject(new Error('Maximum global reconnection attempts reached'));
return;
}
// 如果已经有一个连接,先关闭它 // 如果已经有一个连接,先关闭它
if (socket && socket.readyState !== WebSocket.CLOSED) { if (socket && socket.readyState !== WebSocket.CLOSED) {
socket.close(); console.log('关闭已有WebSocket连接');
socket.close(1000, 'Normal closure, reconnecting');
} }
// 清除之前的定时器 // 清除之前的定时器
if (reconnectTimer) clearTimeout(reconnectTimer); if (reconnectTimer) {
if (pingInterval) clearInterval(pingInterval); clearTimeout(reconnectTimer);
reconnectTimer = null;
}
if (pingInterval) {
clearInterval(pingInterval);
pingInterval = null;
}
try { try {
// 从sessionStorage获取token // 从sessionStorage获取token
const encryptedToken = sessionStorage.getItem('token'); const encryptedToken = sessionStorage.getItem('token');
let token = '';
if (!encryptedToken) { if (!encryptedToken) {
console.error('No token found, cannot connect to notification service'); console.error('No token found, cannot connect to notification service');
store.dispatch(setWebSocketConnected(false)); store.dispatch(setWebSocketConnected(false));
reject(new Error('No token found')); reject(new Error('No token found'));
return; return;
} }
if (encryptedToken) {
let token = '';
try {
token = CryptoJS.AES.decrypt(encryptedToken, secretKey).toString(CryptoJS.enc.Utf8); token = CryptoJS.AES.decrypt(encryptedToken, secretKey).toString(CryptoJS.enc.Utf8);
if (!token) {
throw new Error('Token decryption resulted in empty string');
}
} catch (e) {
console.error('Failed to decrypt token:', e);
store.dispatch(setWebSocketConnected(false));
reject(new Error('Invalid token'));
return;
} }
const wsUrl = `${WS_BASE_URL}/ws/notifications/?token=${token}`;
console.log('WebSocket URL:', wsUrl); const wsUrl = `${WS_BASE_URL}/ws/notifications?token=${token}`;
console.log('正在连接WebSocket...', wsUrl.substring(0, wsUrl.indexOf('?')));
socket = new WebSocket(wsUrl); socket = new WebSocket(wsUrl);
// 连接建立时的处理 // 连接建立时的处理
socket.onopen = () => { socket.onopen = () => {
console.log('WebSocket connection established'); console.log('WebSocket 连接成功!');
reconnectAttempts = 0; // 连接成功后重置重连计数器 reconnectAttempts = 0; // 连接成功后重置当前重连计数器
// 不重置全局重连计数器,确保总重连次数限制
// 更新Redux中的连接状态 // 更新Redux中的连接状态
store.dispatch(setWebSocketConnected(true)); store.dispatch(setWebSocketConnected(true));
@ -65,8 +99,9 @@ export const initWebSocket = () => {
subscribeToNotifications(); subscribeToNotifications();
// 设置定时发送ping消息 // 设置定时发送ping消息
if (pingInterval) clearInterval(pingInterval);
pingInterval = setInterval(() => { pingInterval = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) { if (socket && socket.readyState === WebSocket.OPEN) {
sendPing(); sendPing();
} }
}, PING_INTERVAL); }, PING_INTERVAL);
@ -80,47 +115,68 @@ export const initWebSocket = () => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
handleWebSocketMessage(data); handleWebSocketMessage(data);
} catch (error) { } catch (error) {
console.error('Error parsing WebSocket message:', error); console.error('解析WebSocket消息失败:', error, 'Raw message:', event.data);
} }
}; };
// 错误处理 // 错误处理
socket.onerror = (error) => { socket.onerror = (error) => {
console.error('WebSocket error:', error); console.error('WebSocket连接错误:', error);
// 更新Redux中的连接状态 // 不立即更新Redux状态让onclose处理
store.dispatch(setWebSocketConnected(false));
reject(error);
}; };
// 连接关闭时的处理 // 连接关闭时的处理
socket.onclose = (event) => { socket.onclose = (event) => {
console.log(`WebSocket connection closed: ${event.code} ${event.reason}`); console.log(
`WebSocket连接关闭: 代码=${event.code} 原因="${event.reason || '未知'}" 是否干净=${event.wasClean}`
);
// 清除ping定时器
if (pingInterval) {
clearInterval(pingInterval);
pingInterval = null;
}
// 更新Redux中的连接状态 // 更新Redux中的连接状态
store.dispatch(setWebSocketConnected(false)); store.dispatch(setWebSocketConnected(false));
// 清除ping定时器
if (pingInterval) clearInterval(pingInterval);
// 如果不是正常关闭,尝试重连 // 如果不是正常关闭,尝试重连
if (event.code !== 1000) { if (!event.wasClean && event.code !== 1000 && event.code !== 1001) {
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { // 检查是否已达到最大重连次数
if (
reconnectAttempts < MAX_RECONNECT_ATTEMPTS &&
globalReconnectAttempts < MAX_GLOBAL_RECONNECT_ATTEMPTS
) {
reconnectAttempts++; reconnectAttempts++;
console.log(`WebSocket reconnection attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}`); globalReconnectAttempts++;
const delay = Math.min(RECONNECT_DELAY * reconnectAttempts, 15000); // 指数退避但最大15秒
console.log(
`WebSocket将在${
delay / 1000
}秒后尝试重连 (第${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}, 总计${globalReconnectAttempts}/${MAX_GLOBAL_RECONNECT_ATTEMPTS})`
);
reconnectTimer = setTimeout(() => { reconnectTimer = setTimeout(() => {
console.log('Attempting to reconnect WebSocket...'); console.log('正在尝试重新连接WebSocket...');
initWebSocket().catch((err) => { initWebSocket()
console.error('Failed to reconnect WebSocket:', err); .then(() => {
// 重连失败时更新Redux中的连接状态 console.log('WebSocket重连成功');
store.dispatch(setWebSocketConnected(false)); })
}); .catch((err) => {
}, RECONNECT_DELAY); console.error('WebSocket重连失败:', err);
// 重连失败时更新Redux中的连接状态
store.dispatch(setWebSocketConnected(false));
});
}, delay);
} else { } else {
console.log('Maximum reconnection attempts reached. Giving up.'); const msg = `已达到最大重连次数 (当前${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}, 总计${globalReconnectAttempts}/${MAX_GLOBAL_RECONNECT_ATTEMPTS})`;
console.warn(msg);
// 达到最大重连次数时更新Redux中的连接状态 // 达到最大重连次数时更新Redux中的连接状态
store.dispatch(setWebSocketConnected(false)); store.dispatch(setWebSocketConnected(false));
} }
} else {
console.log('正常关闭,不会尝试重连');
} }
}; };
} catch (error) { } catch (error) {
@ -150,10 +206,16 @@ export const subscribeToNotifications = () => {
*/ */
export const sendPing = () => { export const sendPing = () => {
if (socket && socket.readyState === WebSocket.OPEN) { if (socket && socket.readyState === WebSocket.OPEN) {
const pingMessage = { try {
type: 'ping', const pingMessage = {
}; type: 'ping',
socket.send(JSON.stringify(pingMessage)); timestamp: new Date().toISOString(),
};
socket.send(JSON.stringify(pingMessage));
console.debug('已发送 ping 消息');
} catch (error) {
console.error('发送 ping 消息失败:', error);
}
} }
}; };
@ -176,53 +238,91 @@ export const acknowledgeNotification = (notificationId) => {
/** /**
* 关闭WebSocket连接 * 关闭WebSocket连接
* @param {boolean} [permanent=false] 是否永久关闭不再重连
*/ */
export const closeWebSocket = () => { export const closeWebSocket = (permanent = false) => {
if (socket) { // 停止重连尝试
socket.close(1000, 'Normal closure');
socket = null;
}
if (reconnectTimer) { if (reconnectTimer) {
clearTimeout(reconnectTimer); clearTimeout(reconnectTimer);
reconnectTimer = null; reconnectTimer = null;
} }
// 停止ping
if (pingInterval) { if (pingInterval) {
clearInterval(pingInterval); clearInterval(pingInterval);
pingInterval = null; pingInterval = null;
} }
// 如果是永久关闭,重置重连计数器
if (permanent) {
globalReconnectAttempts = MAX_GLOBAL_RECONNECT_ATTEMPTS; // 设置为最大值阻止重连
}
// 关闭连接
if (socket) {
if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
console.log(`手动关闭WebSocket连接${permanent ? '(永久)' : ''}`);
socket.close(1000, '用户主动关闭');
}
socket = null;
}
// 更新Redux中的连接状态 // 更新Redux中的连接状态
store.dispatch(setWebSocketConnected(false)); store.dispatch(setWebSocketConnected(false));
}; };
/**
* 重置WebSocket连接状态允许重新尝试连接
*/
export const resetWebSocketState = () => {
closeWebSocket(true); // 先关闭当前连接
// 重置所有计数器和状态
reconnectAttempts = 0;
globalReconnectAttempts = 0;
console.log('WebSocket连接状态已重置可以重新尝试连接');
};
/** /**
* 处理接收到的WebSocket消息 * 处理接收到的WebSocket消息
* @param {Object} data 解析后的消息数据 * @param {Object} data 解析后的消息数据
*/ */
const handleWebSocketMessage = (data) => { const handleWebSocketMessage = (data) => {
switch (data.type) { if (!data || typeof data !== 'object') {
case 'connection_established': console.warn('收到无效的WebSocket消息:', data);
console.log(`Connection established for user: ${data.user_id}`); return;
break; }
case 'notification': try {
console.log('Received notification:', data); switch (data.type) {
// 将通知添加到Redux store case 'connection_established':
store.dispatch(addNotification(processNotification(data))); console.log(`WebSocket连接已建立用户ID: ${data.user_id}`);
break; break;
case 'pong': case 'notification':
console.log(`Received pong at ${data.timestamp}`); if (!data.data) {
break; console.warn('收到无效的通知数据:', data);
return;
}
console.log('收到新通知:', data.data.title);
// 将通知添加到Redux store
store.dispatch(addNotification(processNotification(data)));
break;
case 'error': case 'pong':
console.error(`WebSocket error: ${data.code} - ${data.message}`); console.debug(`收到pong响应时间戳: ${data.timestamp}`);
break; break;
default: case 'error':
console.log('Received unknown message type:', data); console.error(`WebSocket错误: ${data.code} - ${data.message}`);
break;
default:
console.log('收到未知类型的消息:', data.type, data);
}
} catch (error) {
console.error('处理WebSocket消息时发生错误:', error, 'Message:', data);
} }
}; };
@ -271,3 +371,182 @@ const processNotification = (data) => {
metadata: notificationData.metadata || {}, metadata: notificationData.metadata || {},
}; };
}; };
/**
* 初始化聊天WebSocket连接
* @returns {Promise<WebSocket>} WebSocket连接实例
*/
export const initChatWebSocket = () => {
return new Promise((resolve, reject) => {
// 如果已经有一个连接,先关闭它
if (chatSocket && chatSocket.readyState !== WebSocket.CLOSED) {
console.log('关闭已有Chat WebSocket连接');
chatSocket.close(1000, 'Normal closure, reconnecting');
}
try {
// 从sessionStorage获取token
const encryptedToken = sessionStorage.getItem('token');
if (!encryptedToken) {
console.error('No token found, cannot connect to chat service');
reject(new Error('No token found'));
return;
}
let token = '';
try {
token = CryptoJS.AES.decrypt(encryptedToken, secretKey).toString(CryptoJS.enc.Utf8);
if (!token) {
throw new Error('Token decryption resulted in empty string');
}
} catch (e) {
console.error('Failed to decrypt token:', e);
reject(new Error('Invalid token'));
return;
}
// 构建WebSocket URL
const wsUrl = `${WS_CHAT_URL}?token=${token}`;
console.log('正在连接Chat WebSocket...', wsUrl.substring(0, wsUrl.indexOf('?')));
// 创建WebSocket连接
chatSocket = new WebSocket(wsUrl);
// 设置超时
const connectionTimeout = setTimeout(() => {
if (chatSocket.readyState !== WebSocket.OPEN) {
console.error('Chat WebSocket连接超时');
chatSocket.close();
reject(new Error('Connection timeout'));
}
}, 10000); // 10秒超时
// 连接建立时的处理
chatSocket.onopen = () => {
console.log('Chat WebSocket 连接成功!');
clearTimeout(connectionTimeout);
resolve(chatSocket);
};
// 接收消息的处理
chatSocket.onmessage = (event) => {
try {
// 解析消息并传递给回调
const message = JSON.parse(event.data);
if (chatMessageCallback) {
chatMessageCallback(message);
}
} catch (error) {
console.error('处理Chat WebSocket消息失败:', error, 'Raw message:', event.data);
}
};
// 错误处理
chatSocket.onerror = (error) => {
console.error('Chat WebSocket连接错误:', error);
clearTimeout(connectionTimeout);
reject(error);
};
// 连接关闭时的处理
chatSocket.onclose = (event) => {
console.log(
`Chat WebSocket连接关闭: 代码=${event.code} 原因="${event.reason || '未知'}" 是否干净=${
event.wasClean
}`
);
clearTimeout(connectionTimeout);
};
} catch (error) {
console.error('Error initializing Chat WebSocket:', error);
reject(error);
}
});
};
/**
* 通过WebSocket发送聊天消息
* @param {Object} message 聊天消息
* @param {Function} callback 接收消息的回调函数
*/
export const sendChatMessageViaWebSocket = (message, callback) => {
// 保存回调函数
chatMessageCallback = callback;
// 确保WebSocket连接已建立
if (!chatSocket || chatSocket.readyState !== WebSocket.OPEN) {
console.log('WebSocket未连接正在初始化连接...');
return initChatWebSocket()
.then(() => {
console.log('WebSocket连接已建立发送消息:', message);
chatSocket.send(JSON.stringify(message));
})
.catch((error) => {
console.error('Chat WebSocket连接失败:', error);
// 如果连接失败,调用回调传递错误
if (callback) {
callback({
code: 500,
message: `WebSocket连接失败: ${error.message}`,
data: {
content: `连接失败: ${error.message}`,
is_end: true,
},
});
}
});
} else {
// 如果连接已建立,直接发送消息
console.log('WebSocket已连接直接发送消息:', message);
chatSocket.send(JSON.stringify(message));
}
};
/**
* 处理聊天WebSocket消息
* 流式内容结构类似于:
* {
* "code": 200,
* "message": "开始流式传输" | "partial" | "完成",
* "data": {
* "content": "消息内容",
* "is_end": true/false
* }
* }
* @param {string} data 原始消息数据
* @param {Function} callback 处理消息的回调函数
*/
export const processChatWebSocketMessage = (data, callback) => {
try {
const message = JSON.parse(data);
// 调用回调处理消息
if (callback) {
callback(message);
}
return message;
} catch (error) {
console.error('解析Chat WebSocket消息失败:', error);
}
};
/**
* 关闭聊天WebSocket连接
*/
export const closeChatWebSocket = () => {
// 清除回调
chatMessageCallback = null;
// 关闭连接
if (chatSocket) {
if (chatSocket.readyState === WebSocket.OPEN || chatSocket.readyState === WebSocket.CONNECTING) {
console.log('手动关闭Chat WebSocket连接');
chatSocket.close(1000, '用户主动关闭');
}
chatSocket = null;
}
console.log('Chat WebSocket连接已关闭');
};

View File

@ -148,7 +148,12 @@ const chatSlice = createSlice({
const messageIndex = state.list.items[chatIndex].messages.findIndex((msg) => msg.id === id); const messageIndex = state.list.items[chatIndex].messages.findIndex((msg) => msg.id === id);
if (messageIndex !== -1) { if (messageIndex !== -1) {
// 更新现有消息 // 更新现有消息 - 确保完全替换内容以触发React更新
if (updates.content !== undefined) {
state.list.items[chatIndex].messages[messageIndex].content = updates.content;
}
// 更新其他字段
state.list.items[chatIndex].messages[messageIndex] = { state.list.items[chatIndex].messages[messageIndex] = {
...state.list.items[chatIndex].messages[messageIndex], ...state.list.items[chatIndex].messages[messageIndex],
...updates, ...updates,
@ -158,6 +163,15 @@ const chatSlice = createSlice({
if (updates.is_streaming === false) { if (updates.is_streaming === false) {
state.sendMessage.status = 'succeeded'; state.sendMessage.status = 'succeeded';
} }
// 如果是最后一条消息且是助手消息更新会话的last_message
if (state.list.items[chatIndex].messages[messageIndex].role === 'assistant') {
state.list.items[chatIndex].last_message =
updates.content || state.list.items[chatIndex].messages[messageIndex].content;
}
} else {
// 如果找不到消息,尝试创建一个新消息
console.warn(`消息 ID ${id} 不存在,无法更新`);
} }
} }
} }

View File

@ -590,3 +590,25 @@
} }
} }
} }
// ResourceList 组件样式
.resource-list {
font-size: 0.9rem;
.resource-item {
transition: background-color 0.2s ease;
&:hover {
background-color: #f0f0f0 !important;
}
}
.resource-title {
font-size: 0.9rem;
line-height: 1.2;
}
.resource-source {
font-size: 0.8rem;
}
}