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)}
)}