feat(ai-chat): 实现文档分片功能与SSE连接优化
- 新增文档分片相关API接口定义及请求方法 - 文档分片VO接口补充,调整项目ID等类型为字符串 - ai-chat视图中用新的带鉴权Header的SSE工具替换EventSource - SSE连接支持事件处理及错误处理完善,提供连接关闭回调 - 知识库视图中添加文档分片查看功能,包括分片列表与详情对话框 - 界面增加分片列表操作按钮及分页数据显示 - 路由枚举调整,修正 AI Chat 相关命名混淆 - 增加SSE连接工具函数chatSSE.ts,实现带鉴权Header的SSE连接管理
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
type SSEErrorData
|
||||
} from "@/api/ai-chat";
|
||||
import { getProjectList, type ProjectItem } from "@/api/project";
|
||||
import { createSSEConnection } from "@/utils/sse/chatSSE";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import SendIcon from "~icons/ri/send-plane-fill";
|
||||
@@ -41,8 +42,8 @@ const projects = ref<ProjectItem[]>([]);
|
||||
const projectLoading = ref(false);
|
||||
const showProjectSelect = ref(false);
|
||||
|
||||
// SSE连接
|
||||
let eventSource: EventSource | null = null;
|
||||
// SSE连接关闭函数
|
||||
let abortSSE: (() => void) | null = null;
|
||||
|
||||
// 消息容器引用
|
||||
const messagesContainer = ref<HTMLElement | null>(null);
|
||||
@@ -67,11 +68,11 @@ async function loadProjects() {
|
||||
}
|
||||
|
||||
// 加载会话列表
|
||||
async function loadSessions(projectId?: number) {
|
||||
async function loadSessions(projectId?: string) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getChatSessions(
|
||||
projectId || currentSession.value?.projectId || 0
|
||||
projectId || currentSession.value?.projectId || ""
|
||||
);
|
||||
if (res.code === 200) {
|
||||
sessions.value = res.data || [];
|
||||
@@ -116,7 +117,7 @@ async function handleProjectSelect(project: ProjectItem) {
|
||||
currentSession.value = {
|
||||
sessionId: "",
|
||||
sessionTitle: "新对话",
|
||||
projectId: Number(project.id),
|
||||
projectId: String(project.id),
|
||||
projectName: project.projectName || "",
|
||||
lastMessageTime: new Date().toISOString(),
|
||||
messageCount: 0,
|
||||
@@ -163,81 +164,86 @@ async function sendMessage() {
|
||||
});
|
||||
|
||||
// 关闭之前的连接
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
if (abortSSE) {
|
||||
abortSSE();
|
||||
abortSSE = null;
|
||||
}
|
||||
|
||||
eventSource = new EventSource(sseUrl);
|
||||
// 使用带鉴权Header的SSE连接
|
||||
abortSSE = await createSSEConnection({
|
||||
url: sseUrl,
|
||||
onEvent: (eventName: string, data: any) => {
|
||||
switch (eventName) {
|
||||
case "start": {
|
||||
const startData = data as SSEStartData;
|
||||
if (startData.isNewSession || !currentSession.value?.sessionId) {
|
||||
currentSession.value = {
|
||||
...currentSession.value!,
|
||||
sessionId: startData.sessionId,
|
||||
sessionTitle: message.slice(0, 20)
|
||||
};
|
||||
loadSessions(currentSession.value.projectId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "chunk": {
|
||||
const chunkData = data as SSEChunkData;
|
||||
streamingMessage.value += chunkData.content;
|
||||
scrollToBottom();
|
||||
break;
|
||||
}
|
||||
case "references": {
|
||||
const refData = data as SSEReferencesData;
|
||||
streamingReferences.value = refData.docs || [];
|
||||
break;
|
||||
}
|
||||
case "complete": {
|
||||
const completeData = data as SSECompleteData;
|
||||
|
||||
eventSource.addEventListener("start", (e: MessageEvent) => {
|
||||
const data: SSEStartData = JSON.parse(e.data);
|
||||
if (data.isNewSession || !currentSession.value.sessionId) {
|
||||
currentSession.value = {
|
||||
...currentSession.value,
|
||||
sessionId: data.sessionId,
|
||||
sessionTitle: message.slice(0, 20)
|
||||
};
|
||||
loadSessions(currentSession.value.projectId);
|
||||
// 添加AI回复消息
|
||||
const assistantMessage: ChatMessageVO = {
|
||||
id: completeData.messageId,
|
||||
role: "assistant",
|
||||
content: streamingMessage.value,
|
||||
referencedDocs: streamingReferences.value,
|
||||
tokensUsed: completeData.tokensUsed,
|
||||
messageIndex: messages.value.length + 1,
|
||||
createTime: new Date().toISOString()
|
||||
};
|
||||
messages.value.push(assistantMessage);
|
||||
|
||||
// 重置状态
|
||||
streamingMessage.value = "";
|
||||
streamingReferences.value = [];
|
||||
sending.value = false;
|
||||
abortSSE = null;
|
||||
|
||||
loadSessions(currentSession.value?.projectId);
|
||||
scrollToBottom();
|
||||
break;
|
||||
}
|
||||
case "error": {
|
||||
const errorData = data as SSEErrorData;
|
||||
ElMessage.error(errorData.message || "对话发生错误");
|
||||
sending.value = false;
|
||||
streamingMessage.value = "";
|
||||
streamingReferences.value = [];
|
||||
if (abortSSE) {
|
||||
abortSSE();
|
||||
abortSSE = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
ElMessage.error("连接服务器失败: " + error.message);
|
||||
sending.value = false;
|
||||
streamingMessage.value = "";
|
||||
streamingReferences.value = [];
|
||||
abortSSE = null;
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener("chunk", (e: MessageEvent) => {
|
||||
const data: SSEChunkData = JSON.parse(e.data);
|
||||
streamingMessage.value += data.content;
|
||||
scrollToBottom();
|
||||
});
|
||||
|
||||
eventSource.addEventListener("references", (e: MessageEvent) => {
|
||||
const data: SSEReferencesData = JSON.parse(e.data);
|
||||
streamingReferences.value = data.docs || [];
|
||||
});
|
||||
|
||||
eventSource.addEventListener("complete", (e: MessageEvent) => {
|
||||
const data: SSECompleteData = JSON.parse(e.data);
|
||||
|
||||
// 添加AI回复消息
|
||||
const assistantMessage: ChatMessageVO = {
|
||||
id: data.messageId,
|
||||
role: "assistant",
|
||||
content: streamingMessage.value,
|
||||
referencedDocs: streamingReferences.value,
|
||||
tokensUsed: data.tokensUsed,
|
||||
messageIndex: messages.value.length + 1,
|
||||
createTime: new Date().toISOString()
|
||||
};
|
||||
messages.value.push(assistantMessage);
|
||||
|
||||
// 重置状态
|
||||
streamingMessage.value = "";
|
||||
streamingReferences.value = [];
|
||||
sending.value = false;
|
||||
|
||||
eventSource?.close();
|
||||
eventSource = null;
|
||||
|
||||
loadSessions(currentSession.value?.projectId);
|
||||
scrollToBottom();
|
||||
});
|
||||
|
||||
eventSource.addEventListener("error", (e: MessageEvent) => {
|
||||
const data: SSEErrorData = JSON.parse(e.data);
|
||||
ElMessage.error(data.message || "对话发生错误");
|
||||
sending.value = false;
|
||||
streamingMessage.value = "";
|
||||
streamingReferences.value = [];
|
||||
eventSource?.close();
|
||||
eventSource = null;
|
||||
});
|
||||
|
||||
eventSource.onerror = () => {
|
||||
ElMessage.error("连接服务器失败");
|
||||
sending.value = false;
|
||||
streamingMessage.value = "";
|
||||
streamingReferences.value = [];
|
||||
eventSource?.close();
|
||||
eventSource = null;
|
||||
};
|
||||
}
|
||||
|
||||
// 删除会话
|
||||
@@ -289,9 +295,9 @@ function formatFileSize(bytes: number) {
|
||||
|
||||
// 组件卸载时关闭SSE连接
|
||||
onUnmounted(() => {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
if (abortSSE) {
|
||||
abortSSE();
|
||||
abortSSE = null;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user