From 897fad95eb00a76e1061adf9213d9ac6612026ea Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Mon, 1 Jun 2026 10:49:38 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=A0=87=E9=A2=98=E7=94=9F?= =?UTF-8?q?=E6=88=90=E9=87=8D=E6=9E=84=E3=80=81UI=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E3=80=81=E6=B5=8B=E8=AF=95=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将标题生成逻辑提取为独立函数,提前到Agent调用前非阻塞执行 - 修复模型/供应商不存在时的HTTP状态码 500→400 - ChatPanel: 分离模型选择useEffect、CSS类替代内联样式、按钮样式统一 - use-conversations: fetchConversations/fetchMessages改用handleResponse去重 - 聊天面板滚动优化(scroll-behavior: smooth, overflow-anchor: auto) - 测试: mock支持onFinish回调,新增首次消息标题生成测试 - 移除未使用的SendMessageRequest接口 --- src/server/routes/chat/send.ts | 99 +++++++++---------- src/shared/api.ts | 5 - .../workbench/components/chat/ChatPanel.tsx | 59 ++++++++--- src/web/hooks/use-conversations.ts | 12 +-- src/web/styles.css | 15 ++- tests/server/routes/chat.test.ts | 72 +++++++++++++- 6 files changed, 170 insertions(+), 92 deletions(-) diff --git a/src/server/routes/chat/send.ts b/src/server/routes/chat/send.ts index e952f98..527d705 100644 --- a/src/server/routes/chat/send.ts +++ b/src/server/routes/chat/send.ts @@ -79,12 +79,12 @@ export async function handleSendChat(req: Request, db: Database, mode: RuntimeMo const d = wrap(db); const modelRow = d.select().from(models).where(eq(models.id, conversation.modelId)).get(); if (!modelRow) { - return jsonResponse(createApiError(`模型不存在: ${conversation.modelId}`, 500), { mode, status: 500 }); + return jsonResponse(createApiError(`模型不存在: ${conversation.modelId}`, 400), { mode, status: 400 }); } const providerRow = d.select().from(providers).where(eq(providers.id, modelRow.providerId)).get(); if (!providerRow) { - return jsonResponse(createApiError(`供应商不存在: ${modelRow.providerId}`, 500), { mode, status: 500 }); + return jsonResponse(createApiError(`供应商不存在: ${modelRow.providerId}`, 400), { mode, status: 400 }); } const registry = buildProviderRegistry(db); @@ -95,6 +95,17 @@ export async function handleSendChat(req: Request, db: Database, mode: RuntimeMo } try { + const firstUserMsg = body.messages.find((m) => m.role === "user"); + const firstUserText = + firstUserMsg?.parts + ?.filter((p) => p.type === "text") + .map((p) => p.text) + .join("") ?? ""; + + if (conversation.title === "新会话" && firstUserText) { + generateConversationTitle(firstUserText, model, db, conversation.id, logger); + } + const agent = createAlfredAgent(model); return await createAgentUIStreamResponse({ agent, @@ -110,58 +121,6 @@ export async function handleSendChat(req: Request, db: Database, mode: RuntimeMo role: "assistant", }); updateConversationTimestamp(db, conversation.id); - - try { - if (conversation.title === "新会话") { - const firstUserText = - body.messages - ?.find((m) => m.role === "user") - ?.parts?.filter((p) => p.type === "text") - ?.map((p) => p.text) - ?.join("") ?? ""; - - if (firstUserText) { - if (firstUserText.length <= 5) { - updateConversation(db, conversation.id, { title: firstUserText }); - } else { - void generateText({ - model, - prompt: `请根据以下对话开头生成一个简短标题(不超过10个字):${firstUserText}`, - system: "你是一个标题生成助手,只返回标题文本,不要解释。", - }) - .then((result) => { - const title = result.text.trim().slice(0, 10); - updateConversation(db, conversation.id, { title: title || firstUserText.slice(0, 10) }); - }) - .catch((titleError: unknown) => { - const titleMsg = titleError instanceof Error ? titleError.message : String(titleError); - logger.error({ conversationId: conversation.id, error: titleMsg }, "标题生成失败"); - try { - updateConversation(db, conversation.id, { title: firstUserText.slice(0, 10) }); - } catch { - logger.error({ conversationId: conversation.id }, "标题兜底更新失败"); - } - }); - } - } - } - } catch (titleError: unknown) { - const titleMsg = titleError instanceof Error ? titleError.message : String(titleError); - logger.error({ conversationId: conversation.id, error: titleMsg }, "标题生成失败"); - try { - const fallbackTitle = - body.messages - ?.find((m) => m.role === "user") - ?.parts?.filter((p) => p.type === "text") - ?.map((p) => p.text) - ?.join("") - ?.slice(0, 10) ?? "新会话"; - updateConversation(db, conversation.id, { title: fallbackTitle }); - } catch (fallbackError: unknown) { - const fbMsg = fallbackError instanceof Error ? fallbackError.message : String(fallbackError); - logger.error({ conversationId: conversation.id, error: fbMsg }, "标题兜底更新失败"); - } - } }, uiMessages: body.messages, }); @@ -170,3 +129,35 @@ export async function handleSendChat(req: Request, db: Database, mode: RuntimeMo return jsonResponse(createApiError(`AI 调用失败:${msg}`, 500), { mode, status: 500 }); } } + +function generateConversationTitle( + firstUserText: string, + model: ReturnType["languageModel"]>, + db: Database, + conversationId: string, + logger: Logger, +): void { + if (firstUserText.length <= 5) { + updateConversation(db, conversationId, { title: firstUserText }); + return; + } + + void generateText({ + model, + prompt: `请根据以下对话开头生成一个简短标题(不超过10个字):${firstUserText}`, + system: "你是一个标题生成助手,只返回标题文本,不要解释。", + }) + .then((result) => { + const title = result.text.trim().slice(0, 10); + updateConversation(db, conversationId, { title: title || firstUserText.slice(0, 10) }); + }) + .catch((titleError: unknown) => { + const titleMsg = titleError instanceof Error ? titleError.message : String(titleError); + logger.error({ conversationId, error: titleMsg }, "标题生成失败"); + try { + updateConversation(db, conversationId, { title: firstUserText.slice(0, 10) }); + } catch { + logger.error({ conversationId }, "标题兜底更新失败"); + } + }); +} diff --git a/src/shared/api.ts b/src/shared/api.ts index 8aeb850..fbafd77 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -99,11 +99,6 @@ export type ModelCapability = | "video-generation" | "video-recognition"; -export interface SendMessageRequest { - conversationId: string; - messages: Array<{ content: string; role: "assistant" | "system" | "user" }>; -} - export interface UpdateConversationRequest { modelId?: string; title?: string; diff --git a/src/web/consoles/workbench/components/chat/ChatPanel.tsx b/src/web/consoles/workbench/components/chat/ChatPanel.tsx index 7d2d5d7..e43928b 100644 --- a/src/web/consoles/workbench/components/chat/ChatPanel.tsx +++ b/src/web/consoles/workbench/components/chat/ChatPanel.tsx @@ -70,21 +70,11 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }: setInput(""); setMessages([]); try { - const convPromise = fetchConversation(projectId, conversationId); const msgPromise = fetchRef.current(projectId, conversationId); - const conv = await convPromise; const data = await msgPromise; if (cancelled) return; - const firstTextId = textModels[0]?.id; - if (firstTextId && textModels.every((m) => m.id !== conv.modelId)) { - setSelectedModelId(firstTextId); - void updateConversation(projectId, conversationId, { modelId: firstTextId }); - } else { - setSelectedModelId(conv.modelId); - } - const history = data.items .filter((m: { role: string }) => m.role === "user" || m.role === "assistant") .reverse() @@ -109,7 +99,22 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }: return () => { cancelled = true; }; - }, [conversationId, projectId, setMessages, message, textModels]); + }, [conversationId, projectId, setMessages, message]); + + useEffect(() => { + if (!conversationId) return; + const firstTextId = textModels[0]?.id; + if (!firstTextId) return; + + void fetchConversation(projectId, conversationId).then((conv) => { + if (textModels.every((m) => m.id !== conv.modelId)) { + setSelectedModelId(firstTextId); + void updateConversation(projectId, conversationId, { modelId: firstTextId }); + } else { + setSelectedModelId(conv.modelId); + } + }); + }, [conversationId, textModels, projectId]); useEffect(() => { scrollRef.current?.scrollTo({ behavior: "smooth", top: scrollRef.current.scrollHeight }); @@ -213,18 +218,39 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }: const buttons: React.ReactNode[] = []; buttons.push( -