mirror of
https://github.com/Funkoala14/knowledgebase_law.git
synced 2025-06-08 07:48:16 +08:00
[dev]resources & chat
This commit is contained in:
parent
f2a6029076
commit
89d42b9e10
8
package-lock.json
generated
8
package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
// 其次使用URL中传入的知识库ID
|
// 其次使用URL中传入的知识库ID
|
||||||
else if (knowledgeBaseId) {
|
else if (knowledgeBaseId) {
|
||||||
// 可能是单个ID或以逗号分隔的多个ID
|
// 可能是单个ID或以逗号分隔的多个ID
|
||||||
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) {
|
||||||
// 如果是新会话,使用当前选择的知识库
|
// 如果是新会话,使用当前选择的知识库
|
||||||
// 可能是单个ID或以逗号分隔的多个ID
|
// 可能是单个ID或以逗号分隔的多个ID
|
||||||
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);
|
||||||
|
|
||||||
|
// 创建WebSocket测试的AI回复消息ID - 必须先初始化
|
||||||
|
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} />
|
||||||
)}
|
)}
|
||||||
|
@ -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连接已关闭');
|
||||||
|
};
|
||||||
|
@ -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} 不存在,无法更新`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user