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:
2026-03-30 18:26:06 +08:00
parent 8c09689c1c
commit 86330be8f5
6 changed files with 606 additions and 94 deletions

View File

@@ -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;
}
});