diff --git a/backend/src/services/knowledge_base_service.py b/backend/src/services/knowledge_base_service.py
index 23f1e28..0c6bb06 100644
--- a/backend/src/services/knowledge_base_service.py
+++ b/backend/src/services/knowledge_base_service.py
@@ -161,14 +161,16 @@ class KnowledgeBaseService:
# 处理文档(向量化)- 在后台异步处理,不阻塞主流程
try:
- # 使用同步方法,但不等待完成(在后台处理)
import threading
+ import asyncio as _asyncio
def process_in_background():
try:
- self.document_service.process_document(document.id)
+ loop = _asyncio.new_event_loop()
+ loop.run_until_complete(self.document_service.process_document(document.id))
+ loop.close()
except Exception as e:
print(f"后台处理文档 {document.id} 失败: {e}")
-
+
thread = threading.Thread(target=process_in_background, daemon=True)
thread.start()
except Exception as e:
@@ -198,12 +200,15 @@ class KnowledgeBaseService:
# 重新处理文档 - 在后台异步处理
try:
import threading
+ import asyncio as _asyncio
def process_in_background():
try:
- self.document_service.process_document(document.id)
+ loop = _asyncio.new_event_loop()
+ loop.run_until_complete(self.document_service.process_document(document.id))
+ loop.close()
except Exception as e:
print(f"后台处理文档 {document.id} 失败: {e}")
-
+
thread = threading.Thread(target=process_in_background, daemon=True)
thread.start()
except Exception as e:
@@ -340,7 +345,10 @@ class KnowledgeBaseService:
self.db.query(DocumentChunk).filter(DocumentChunk.document_id == document_id).delete()
# 重新处理文档
- success = self.document_service.process_document(document_id)
+ import asyncio as _asyncio
+ success = _asyncio.get_event_loop().run_until_complete(
+ self.document_service.process_document(document_id)
+ )
if success:
return {"success": True, "message": "文档重新索引成功"}
diff --git a/web/src/app/(main)/chat/page.tsx b/web/src/app/(main)/chat/page.tsx
index e313653..14286b2 100644
--- a/web/src/app/(main)/chat/page.tsx
+++ b/web/src/app/(main)/chat/page.tsx
@@ -14,9 +14,10 @@ import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
export default function ChatPage() {
const router = useRouter();
const { isAuthenticated, user, isLoading: authLoading } = useAuthStore();
- const { loadSessions, isLoading: chatLoading } = useChatStore();
+ const { loadSessions, sessions } = useChatStore();
const [isInitialized, setIsInitialized] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(true);
+ const [initialLoading, setInitialLoading] = useState(true);
useEffect(() => {
if (!authLoading && !isAuthenticated) {
@@ -25,12 +26,12 @@ export default function ChatPage() {
}
if (isAuthenticated && !isInitialized) {
- loadSessions();
+ loadSessions().then(() => setInitialLoading(false));
setIsInitialized(true);
}
}, [isAuthenticated, authLoading, isInitialized, router, loadSessions]);
- if (authLoading || chatLoading) {
+ if (authLoading || initialLoading) {
return (
diff --git a/web/src/app/(main)/forum/page.tsx b/web/src/app/(main)/forum/page.tsx
index 33b64f6..a67e972 100644
--- a/web/src/app/(main)/forum/page.tsx
+++ b/web/src/app/(main)/forum/page.tsx
@@ -25,11 +25,11 @@ const CATEGORY_ORDER = ["公告", "通知", "学习", "讨论", "课程", "反
function getCategoryIcon(name: string) {
if (name.includes("公告") || name.includes("通知"))
- return
;
+ return
;
if (name.includes("学习") || name.includes("讨论") || name.includes("课程"))
- return
;
+ return
;
if (name.includes("反馈") || name.includes("使用"))
- return
;
+ return
;
return CATEGORY_ICONS.default;
}
@@ -106,110 +106,112 @@ export default function ForumHomePage() {
}, []);
return (
-
- {isLoading ? (
-
-
- 正在加载社区...
-
- ) : error ? (
-
- ) : (
-
- {[...categories]
- .sort((a, b) => {
- const getOrder = (name: string) => {
- for (let i = 0; i < CATEGORY_ORDER.length; i++) {
- if (name.includes(CATEGORY_ORDER[i])) return i;
- }
- return CATEGORY_ORDER.length;
- };
- return getOrder(a.name) - getOrder(b.name);
- })
- .map((category) => {
- const posts = categoryPosts[category.id] || [];
- return (
-
- {/* Category header */}
-
-
-
- {getCategoryIcon(category.name)}
-
-
-
- {category.name}
-
- {category.description && (
-
- {category.description}
-
- )}
-
-
-
-
- {category.post_count} 条讨论
-
-
- 查看全部
-
-
-
-
-
- {/* Posts list */}
-
- {posts.length > 0 ? (
- posts.map((post) => (
-
-
-
-
-
-
- {post.title}
+
+
+ {isLoading ? (
+
+
+ 正在加载社区...
+
+ ) : error ? (
+
+ ) : (
+
+ {[...categories]
+ .sort((a, b) => {
+ const getOrder = (name: string) => {
+ for (let i = 0; i < CATEGORY_ORDER.length; i++) {
+ if (name.includes(CATEGORY_ORDER[i])) return i;
+ }
+ return CATEGORY_ORDER.length;
+ };
+ return getOrder(a.name) - getOrder(b.name);
+ })
+ .map((category) => {
+ const posts = categoryPosts[category.id] || [];
+ return (
+
+ {/* Category header */}
+
+
+
+ {getCategoryIcon(category.name)}
+
+
+
+ {category.name}
+
+ {category.description && (
+
+ {category.description}
-
- {post.author_name}
-
-
- {formatRelativeTime(post.created_at)}
-
-
-
- {post.reply_count}
-
-
-
-
-
- ))
- ) : (
-
- 暂无讨论,成为第一个发帖的人
+ )}
+
- )}
+
+
+ {category.post_count} 条讨论
+
+
+ 查看全部
+
+
+
+
+
+ {/* Posts list */}
+
+ {posts.length > 0 ? (
+ posts.map((post) => (
+
+
+
+
+
+
+ {post.title}
+
+
+ {post.author_name}
+
+
+ {formatRelativeTime(post.created_at)}
+
+
+
+ {post.reply_count}
+
+
+
+
+
+ ))
+ ) : (
+
+ 暂无讨论,成为第一个发帖的人
+
+ )}
+
-
- );
- })}
-
- )}
+ );
+ })}
+
+ )}
+
);
}
diff --git a/web/src/components/chat/chat-interface.tsx b/web/src/components/chat/chat-interface.tsx
index f46cca5..9c970bc 100644
--- a/web/src/components/chat/chat-interface.tsx
+++ b/web/src/components/chat/chat-interface.tsx
@@ -4,10 +4,9 @@ import { useState, useRef, useEffect } from "react";
import { useChatStore } from "@/store/chat";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
-import { Send, Square, Bot, ChevronDown, Settings2 } from "lucide-react";
+import { Send, Square, Bot, ChevronDown, Settings2, Database } from "lucide-react";
import MessageList from "./message-list";
import QuickQuestions from "./quick-questions";
-import ModeSelector, { ChatMode } from "./mode-selector";
import ModelSelector from "./model-selector";
import KnowledgeSelector, { KnowledgeBase } from "./knowledge-selector";
import { cn } from "@/lib/utils";
@@ -16,7 +15,6 @@ import { knowledgeBaseAPI } from "@/lib/api";
export default function ChatInterface() {
const [inputMessage, setInputMessage] = useState("");
const [isComposing, setIsComposing] = useState(false);
- const [chatMode, setChatMode] = useState
("normal");
const [selectedModel, setSelectedModel] = useState("deepseek-ai/DeepSeek-V3");
const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState([]);
const [systemKnowledgeBases, setSystemKnowledgeBases] = useState([]);
@@ -72,7 +70,8 @@ export default function ChatInterface() {
await selectSession(newSession.id);
}
}
- await streamMessage(message, chatMode, selectedKnowledgeBases, selectedModel);
+ const mode = selectedKnowledgeBases.length > 0 ? "rag" : "normal";
+ await streamMessage(message, mode, selectedKnowledgeBases, selectedModel);
};
const handleKeyPress = (e: React.KeyboardEvent) => {
@@ -124,7 +123,6 @@ export default function ChatInterface() {
{/* Settings panel — collapsible */}
{showSettings && (
-
+ {selectedKnowledgeBases.length > 0 && (
+
+
+ 知识库检索模式
+
+ )}
0 && "pt-4",
"bg-muted/40 border border-border/30",
"placeholder:text-muted-foreground/60",
"focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary/30",
diff --git a/web/src/store/chat.ts b/web/src/store/chat.ts
index fdb3495..ce4db14 100644
--- a/web/src/store/chat.ts
+++ b/web/src/store/chat.ts
@@ -54,14 +54,12 @@ export const useChatStore = create
((set, get) => ({
// 加载会话列表
loadSessions: async () => {
- set({ isLoading: true, error: null });
-
try {
const sessions = await chatAPI.getSessions();
- set({ sessions, isLoading: false });
+ set({ sessions });
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "加载会话失败";
- set({ error: errorMessage, isLoading: false });
+ set({ error: errorMessage });
}
},
@@ -320,7 +318,7 @@ export const useChatStore = create((set, get) => ({
});
}
},
- (sessionId: number, messageId?: number, userMessageId?: number) => {
+ (_sessionId: number, messageId?: number, userMessageId?: number) => {
if (messageId) {
set((state) => ({
messages: state.messages.map(msg => {
@@ -332,15 +330,9 @@ export const useChatStore = create((set, get) => ({
abortController: null,
}));
} else {
- const { currentSession } = get();
- if (currentSession) {
- get().loadMessages(currentSession.id);
- }
+ // 前端已通过流式获得完整内容,不需要从服务端重新加载
set({ isStreaming: false, abortController: null });
}
-
- // 刷新会话列表以确保新会话显示在顶部
- get().loadSessions();
},
(error: string) => {
// 流式错误
@@ -414,36 +406,121 @@ export const useChatStore = create((set, get) => ({
}
},
- // 重新生成消息(支持 assistant 消息 ID)
+ // 重新生成消息(流式)
regenerateMessage: async (messageId: number, model?: string) => {
+ const { currentSession, messages } = get();
+ if (!currentSession) return;
+
+ const messageIndex = messages.findIndex(msg => msg.id === messageId);
+ if (messageIndex === -1) return;
+
+ // 找到该 assistant 消息之前的 user 消息内容
+ const userContent = messages
+ .slice(0, messageIndex)
+ .reverse()
+ .find(msg => msg.role === "user")?.content;
+ if (!userContent) return;
+
+ // 删除该 assistant 消息及之后的所有消息,替换为流式占位符
+ const trimmed = messages.slice(0, messageIndex);
+ const assistantPlaceholder: ChatMessage = {
+ id: Date.now() + 1,
+ role: "assistant",
+ content: "",
+ created_at: new Date().toISOString(),
+ };
+
+ const abortController = new AbortController();
+ set({
+ messages: [...trimmed, assistantPlaceholder],
+ isStreaming: true,
+ error: null,
+ abortController,
+ });
+
+ // 判断模式:检查之前的消息中是否有知识库相关内容
+ const mode = "normal";
+
+ let thinkingSteps: any[] = [];
+
try {
- set({ isLoading: true, error: null });
- const response = await chatAPI.regenerateMessage(messageId, model);
-
- // 找到该消息,删除它及之后的所有消息
- const messageIndex = get().messages.findIndex(msg => msg.id === messageId);
- if (messageIndex !== -1) {
- set((state) => ({
- messages: state.messages.slice(0, messageIndex),
- }));
- }
-
- // 添加新的助手回复
- const newMessage: ChatMessage = {
- id: response.new_message_id,
- role: "assistant",
- content: response.content,
- created_at: new Date().toISOString(),
- };
-
- set((state) => ({
- messages: [...state.messages, newMessage],
- isLoading: false,
- }));
+ await chatAPI.streamMessage(
+ userContent,
+ currentSession.id,
+ mode,
+ undefined, // knowledgeBaseIds
+ abortController.signal,
+ (chunk: string) => {
+ // 处理 chunk
+ try {
+ const data = JSON.parse(chunk);
+ if (data.type === 'thinking') {
+ thinkingSteps.push({
+ stage: data.stage,
+ message: data.message,
+ doc_count: data.doc_count,
+ time: data.time
+ });
+ requestAnimationFrame(() => {
+ set((state) => ({
+ messages: state.messages.map(msg =>
+ msg.id === assistantPlaceholder.id
+ ? { ...msg, thinking: [...thinkingSteps] }
+ : msg
+ ),
+ }));
+ });
+ } else if (data.type === 'chunk') {
+ requestAnimationFrame(() => {
+ set((state) => ({
+ messages: state.messages.map(msg =>
+ msg.id === assistantPlaceholder.id
+ ? { ...msg, content: msg.content + data.content }
+ : msg
+ ),
+ }));
+ });
+ }
+ } catch {
+ // 纯文本 chunk
+ requestAnimationFrame(() => {
+ set((state) => ({
+ messages: state.messages.map(msg =>
+ msg.id === assistantPlaceholder.id
+ ? { ...msg, content: msg.content + chunk }
+ : msg
+ ),
+ }));
+ });
+ }
+ },
+ (_sessionId: number, messageId?: number) => {
+ // onComplete: 只替换 assistant 占位符 ID,不重新加载
+ if (messageId) {
+ set((state) => ({
+ messages: state.messages.map(msg =>
+ msg.id === assistantPlaceholder.id
+ ? { ...msg, id: messageId }
+ : msg
+ ),
+ isStreaming: false,
+ abortController: null,
+ }));
+ } else {
+ set({ isStreaming: false, abortController: null });
+ }
+ },
+ (error: string) => {
+ set({ error, isStreaming: false, abortController: null });
+ },
+ undefined, // onStatus
+ undefined, // onThinking
+ undefined, // onThinkingContent
+ model
+ );
} catch (error) {
- console.error("重新生成消息失败:", error);
- const errorMessage = error instanceof Error ? error.message : "重新生成消息失败";
- set({ error: errorMessage, isLoading: false });
+ const errorMessage = error instanceof Error ? error.message : "重新生成失败";
+ set({ error: errorMessage, isStreaming: false, abortController: null });
}
},