fix: async document processing in background threads, improve chat UX

- Fix knowledge_base_service calling async process_document synchronously
  in background threads — use asyncio.new_event_loop().run_until_complete()
  instead of bare call that returned unawaited coroutine
- Same fix for _update_document and reindex_document
- Replace ModeSelector with auto-detect: RAG mode when knowledge bases selected
- Convert regenerateMessage to streaming (was synchronous API call)
- Show "知识库检索模式" indicator above input when KB selected
- Remove loadSessions side-effect from streaming onComplete
- Fix chat page loading state to avoid flash
- Improve forum page spacing and sizing

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 11:33:32 +08:00
parent 50faf7ad4e
commit 62da8b3c58
5 changed files with 250 additions and 157 deletions
+116 -39
View File
@@ -54,14 +54,12 @@ export const useChatStore = create<ChatStore>((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<ChatStore>((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<ChatStore>((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<ChatStore>((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 });
}
},