From bc25791fd9a04052068688cf8650e846f05c9e37 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Sat, 7 Mar 2026 12:32:26 +0800 Subject: [PATCH] =?UTF-8?q?ai=E9=97=AE=E7=89=87=E7=A7=BB=E9=99=A4think?= =?UTF-8?q?=EF=BC=8C=E6=A0=87=E9=A2=98=E5=8F=AF=E7=82=B9=E5=87=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/ai/chat/route.ts | 48 +++++++++++++++++++++---- src/components/AIChatPanel.tsx | 64 ++++++++++++++++++++++++++++++---- 2 files changed, 99 insertions(+), 13 deletions(-) diff --git a/src/app/api/ai/chat/route.ts b/src/app/api/ai/chat/route.ts index 5a68901..94072e8 100644 --- a/src/app/api/ai/chat/route.ts +++ b/src/app/api/ai/chat/route.ts @@ -74,6 +74,9 @@ function transformToSSE( return new ReadableStream({ async start(controller) { let buffer = ''; // 缓冲区,用于保存不完整的行 + let contentBuffer = ''; // 累积的内容,用于处理跨chunk的thinking标签 + let inThinkingBlock = false; // 是否在thinking块内 + try { while (true) { const { done, value } = await reader.read(); @@ -122,9 +125,32 @@ function transformToSSE( } if (text) { - controller.enqueue( - new TextEncoder().encode(`data: ${JSON.stringify({ text })}\n\n`) - ); + // 累积内容并处理thinking标签 + contentBuffer += text; + + // 检查是否进入thinking块 + if (contentBuffer.includes('')) { + inThinkingBlock = true; + } + + // 检查是否退出thinking块 + if (inThinkingBlock && contentBuffer.includes('')) { + // 移除thinking块内容 + contentBuffer = contentBuffer.replace(/[\s\S]*?<\/think>/g, ''); + inThinkingBlock = false; + } + + // 只有在不在thinking块内时才输出内容 + if (!inThinkingBlock) { + // 输出非thinking部分的内容 + const outputText = contentBuffer; + if (outputText) { + controller.enqueue( + new TextEncoder().encode(`data: ${JSON.stringify({ text: outputText })}\n\n`) + ); + contentBuffer = ''; // 清空已输出的内容 + } + } } } catch (e) { // 只在非空数据解析失败时打印错误 @@ -153,9 +179,14 @@ function transformToSSE( text = json.choices?.[0]?.delta?.content || ''; } if (text) { - controller.enqueue( - new TextEncoder().encode(`data: ${JSON.stringify({ text })}\n\n`) - ); + contentBuffer += text; + // 最后清理一次thinking标签 + contentBuffer = contentBuffer.replace(/[\s\S]*?<\/think>/g, ''); + if (contentBuffer) { + controller.enqueue( + new TextEncoder().encode(`data: ${JSON.stringify({ text: contentBuffer })}\n\n`) + ); + } } } catch (e) { console.error('Parse final buffer error:', e); @@ -298,7 +329,10 @@ export async function POST(request: NextRequest) { // 非流式响应:等待完整响应后返回JSON const response = result as Response; const data = await response.json(); - const content = data.choices?.[0]?.message?.content || ''; + let content = data.choices?.[0]?.message?.content || ''; + + // 移除thinking标签内容 + content = content.replace(/[\s\S]*?<\/think>/g, ''); return NextResponse.json({ content }); } diff --git a/src/components/AIChatPanel.tsx b/src/components/AIChatPanel.tsx index 9299b1a..517dba0 100644 --- a/src/components/AIChatPanel.tsx +++ b/src/components/AIChatPanel.tsx @@ -2,6 +2,8 @@ 'use client'; import { Bot, Loader2, Send, Sparkles, Trash2,X } from 'lucide-react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import ReactMarkdown from 'react-markdown'; @@ -33,6 +35,8 @@ export default function AIChatPanel({ useDrawer = false, drawerWidth = 'w-full md:w-[25%]', }: AIChatPanelProps) { + const pathname = usePathname(); + // 使用 useMemo 稳定 storage key,只在实际内容变化时才改变 const storageKey = useMemo(() => { if (context?.title) { @@ -53,6 +57,14 @@ export default function AIChatPanel({ const abortControllerRef = useRef(null); const hasLoadedRef = useRef(false); + // 将《》包裹的影视名称转换为链接 + const convertTitleToLink = (content: string): string => { + return content.replace(/《([^》]+)》/g, (match, title) => { + const encodedTitle = encodeURIComponent(title); + return `[《${title}》](/play?title=${encodedTitle})`; + }); + }; + // 自动滚动到底部 const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); @@ -424,9 +436,29 @@ export default function AIChatPanel({ {message.content}

) : ( -
- - {message.content} +
+ { + // 如果是内部链接(以 / 开头),使用 Next.js Link + if (href?.startsWith('/')) { + // 如果当前在 /play 页面且链接也是 /play,不做处理(返回纯文本) + if (pathname === '/play' && href.startsWith('/play')) { + return {children}; + } + return ( + + {children} + + ); + } + // 外部链接使用普通 a 标签 + return {children}; + } + }} + > + {convertTitleToLink(message.content)}
)} @@ -610,9 +642,29 @@ export default function AIChatPanel({ {message.content}

) : ( -
- - {message.content} +
+ { + // 如果是内部链接(以 / 开头),使用 Next.js Link + if (href?.startsWith('/')) { + // 如果当前在 /play 页面且链接也是 /play,不做处理(返回纯文本) + if (pathname === '/play' && href.startsWith('/play')) { + return {children}; + } + return ( + + {children} + + ); + } + // 外部链接使用普通 a 标签 + return {children}; + } + }} + > + {convertTitleToLink(message.content)}
)}