From ff9f45858bd645d848b98e0a615f2e95cc6c8f53 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 1 Feb 2026 02:00:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=99=BA=E8=83=BD=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E5=85=88=E5=AF=B9=E8=AF=9D=E5=86=8D=E7=94=9F?= =?UTF-8?q?=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 流程: 1. 用户发送请求(图片+文字) 2. AI立即回复对话('好的,我来帮你...') 3. 显示占位图(正在生成中...) 4. 图片生成后显示 修改: - 后端:先发送对话消息,再调用生图API - 前端:检查用户消息后所有assistant消息是否有图片 - 只有图片出现才隐藏占位图,对话文字不影响 Co-authored-by: Cursor --- app/(chat)/api/chat/route.ts | 90 ++++++++++++++++++++++++++++-------- components/messages.tsx | 43 +++++++++++------ 2 files changed, 100 insertions(+), 33 deletions(-) diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 88dd8eda6..115366aa9 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -986,6 +986,36 @@ export async function POST(request: Request) { const alibabaStream = createUIMessageStream({ execute: async ({ writer }) => { try { + // 🆕 如果是智能模式且有对话回复,先发送对话消息让用户看到 + if (smartAutoFriendlyMessage) { + const chatMessageId = generateUUID(); + console.log("[Chat API] 💬 Sending conversational reply first:", smartAutoFriendlyMessage); + + // 立即发送对话消息 + writer.write({ + type: "data-appendMessage", + data: JSON.stringify({ + id: chatMessageId, + role: "assistant", + parts: [{ type: "text", text: smartAutoFriendlyMessage }], + }), + }); + + // 保存对话消息到数据库 + await saveMessages({ + messages: [ + { + id: chatMessageId, + chatId: id, + role: "assistant", + parts: [{ type: "text", text: smartAutoFriendlyMessage }], + attachments: [], + createdAt: new Date(), + }, + ], + }); + } + // 只传递最后一条用户消息 const lastUserMessage = uiMessages.filter((m) => m.role === "user").pop(); let messagesForAlibaba = lastUserMessage ? [lastUserMessage] : uiMessages; @@ -1045,25 +1075,20 @@ export async function POST(request: Request) { const imageMarkdown = result.text || "图片生成失败"; console.log("[Chat API] Alibaba generated, length:", imageMarkdown.length); - // 生成消息 ID + // 生成消息 ID(图片消息) const messageId = generateUUID(); - // 合并对话和图片的完整内容 - const fullContent = smartAutoFriendlyMessage - ? `${smartAutoFriendlyMessage}\n\n${imageMarkdown}` - : imageMarkdown; - - // 写入完整消息到流 + // 写入图片消息到流 writer.write({ type: "data-appendMessage", data: JSON.stringify({ id: messageId, role: "assistant", - parts: [{ type: "text", text: fullContent }], + parts: [{ type: "text", text: imageMarkdown }], }), }); - // 保存完整消息到数据库 + // 保存图片消息到数据库 try { await saveMessages({ messages: [ @@ -1071,7 +1096,7 @@ export async function POST(request: Request) { id: messageId, chatId: id, role: "assistant", - parts: [{ type: "text", text: fullContent }], + parts: [{ type: "text", text: imageMarkdown }], attachments: [], createdAt: new Date(), videoStorageStatus: null, @@ -1175,6 +1200,36 @@ export async function POST(request: Request) { const volcanoStream = createUIMessageStream({ execute: async ({ writer }) => { try { + // 🆕 如果是智能模式且有对话回复,先发送对话消息让用户看到 + if (smartAutoFriendlyMessage) { + const chatMessageId = generateUUID(); + console.log("[Chat API] 💬 Volcano: Sending conversational reply first:", smartAutoFriendlyMessage); + + // 立即发送对话消息 + writer.write({ + type: "data-appendMessage", + data: JSON.stringify({ + id: chatMessageId, + role: "assistant", + parts: [{ type: "text", text: smartAutoFriendlyMessage }], + }), + }); + + // 保存对话消息到数据库 + await saveMessages({ + messages: [ + { + id: chatMessageId, + chatId: id, + role: "assistant", + parts: [{ type: "text", text: smartAutoFriendlyMessage }], + attachments: [], + createdAt: new Date(), + }, + ], + }); + } + // 转换消息,火山引擎支持 URL 和 base64 const modelMessages = await convertMessagesWithImages(messagesForVolcano, "volcano"); @@ -1187,25 +1242,20 @@ export async function POST(request: Request) { console.log("[Chat API] Volcano Vision generated, length:", imageMarkdown.length); console.log("[Chat API] Volcano Vision image markdown:", imageMarkdown.substring(0, 200)); - // 生成消息 ID + // 生成消息 ID(图片消息) const messageId = generateUUID(); - // 合并对话和图片的完整内容 - const fullContent = smartAutoFriendlyMessage - ? `${smartAutoFriendlyMessage}\n\n${imageMarkdown}` - : imageMarkdown; - - // 写入完整消息到流 + // 写入图片消息到流 writer.write({ type: "data-appendMessage", data: JSON.stringify({ id: messageId, role: "assistant", - parts: [{ type: "text", text: fullContent }], + parts: [{ type: "text", text: imageMarkdown }], }), }); - // 保存完整消息到数据库 + // 保存图片消息到数据库 try { await saveMessages({ messages: [ @@ -1213,7 +1263,7 @@ export async function POST(request: Request) { id: messageId, chatId: id, role: "assistant", - parts: [{ type: "text", text: fullContent }], + parts: [{ type: "text", text: imageMarkdown }], attachments: [], // 必须提供 attachments 字段(数据库 notNull) createdAt: new Date(), videoStorageStatus: null, diff --git a/components/messages.tsx b/components/messages.tsx index a880bcc99..2262d1515 100644 --- a/components/messages.tsx +++ b/components/messages.tsx @@ -152,14 +152,37 @@ function PureMessages({ }); } + // 🆕 智能自动模式:检查用户最后一条消息是否包含图片,如果有则视为图片编辑场景 + // 注意:需要找用户的最后一条消息,而不是整个对话的最后一条 + const lastUserMessage = [...messages].reverse().find(m => m.role === "user"); + const isAutoModeWithImage = selectedModelId === "auto" && + lastUserMessage?.parts?.some((p: any) => p.type === "file" || p.type === "image"); + // 对于图片模型,检查是否有图片内容(包含 ![thumb| 或 ![thumb||| 标记) - const hasImageContent = lastMessage?.parts && lastMessage.parts.some( - (part: any) => part.type === "text" && part.text && ( - part.text.includes("![thumb|") || - part.text.includes("![thumb|||") || - part.text.includes("![Generated Image") - ) - ); + // 🆕 在智能自动模式下,需要检查最后一条用户消息之后的所有 assistant 消息 + let hasImageContent = false; + if (isAutoModeWithImage) { + // 找到最后一条用户消息的索引 + const lastUserIndex = messages.findLastIndex(m => m.role === "user"); + // 检查之后所有的 assistant 消息是否包含图片 + const assistantMessagesAfterUser = messages.slice(lastUserIndex + 1).filter(m => m.role === "assistant"); + hasImageContent = assistantMessagesAfterUser.some(msg => + msg.parts?.some((part: any) => part.type === "text" && part.text && ( + part.text.includes("![thumb|") || + part.text.includes("![thumb|||") || + part.text.includes("![Generated Image") + )) + ); + } else { + // 普通模式:只检查最后一条消息 + hasImageContent = lastMessage?.parts && lastMessage.parts.some( + (part: any) => part.type === "text" && part.text && ( + part.text.includes("![thumb|") || + part.text.includes("![thumb|||") || + part.text.includes("![Generated Image") + ) + ) || false; + } // 对于其他模型,检查是否有实际的文本内容 const hasTextContent = lastMessage?.parts && lastMessage.parts.some( @@ -169,12 +192,6 @@ function PureMessages({ // 判断是否是图片生成模型(包括 Gemini、火山、阿里) const isImageGenModel = isGeminiModel || isVolcanoVisionModel || isAlibabaImageModel; - // 🆕 智能自动模式:检查用户最后一条消息是否包含图片,如果有则视为图片编辑场景 - // 注意:需要找用户的最后一条消息,而不是整个对话的最后一条 - const lastUserMessage = [...messages].reverse().find(m => m.role === "user"); - const isAutoModeWithImage = selectedModelId === "auto" && - lastUserMessage?.parts?.some((p: any) => p.type === "file" || p.type === "image"); - // 根据模型类型判断是否有有效内容 let hasContent = false; if (isQianwenVideoModel || isVideoModel) {