From aacb1172411e4811c84e2b947d6dbaff7a0c5733 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Tue, 30 Dec 2025 00:16:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96ai=E5=86=B3=E7=AD=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/ai/chat/route.ts | 15 +- src/components/AIChatPanel.tsx | 86 +++++++++- src/lib/ai-orchestrator.ts | 287 +++++++++++++++++++++++---------- 3 files changed, 295 insertions(+), 93 deletions(-) diff --git a/src/app/api/ai/chat/route.ts b/src/app/api/ai/chat/route.ts index 3007818..6cf50a1 100644 --- a/src/app/api/ai/chat/route.ts +++ b/src/app/api/ai/chat/route.ts @@ -121,7 +121,12 @@ function transformToSSE( for (const line of lines) { if (line.startsWith('data: ')) { - const data = line.slice(6); + const data = line.slice(6).trim(); + + // 跳过空数据 + if (!data) { + continue; + } if (data === '[DONE]') { controller.enqueue( @@ -151,7 +156,10 @@ function transformToSSE( ); } } catch (e) { - console.error('Parse stream chunk error:', e); + // 只在非空数据解析失败时打印错误 + if (data.length > 0) { + console.error('Parse stream chunk error:', e, 'Data:', data.substring(0, 100)); + } } } } @@ -228,6 +236,9 @@ export async function POST(request: NextRequest) { tavilyApiKey: aiConfig.TavilyApiKey, serperApiKey: aiConfig.SerperApiKey, serpApiKey: aiConfig.SerpApiKey, + // TMDB 配置 + tmdbApiKey: adminConfig.SiteConfig.TMDBApiKey, + tmdbProxy: adminConfig.SiteConfig.TMDBProxy, // 决策模型配置(固定使用自定义provider,复用主模型的API配置) enableDecisionModel: aiConfig.EnableDecisionModel, decisionProvider: 'custom', diff --git a/src/components/AIChatPanel.tsx b/src/components/AIChatPanel.tsx index 7323292..5122dc6 100644 --- a/src/components/AIChatPanel.tsx +++ b/src/components/AIChatPanel.tsx @@ -3,7 +3,7 @@ import React, { useState, useRef, useEffect } from 'react'; import { createPortal } from 'react-dom'; -import { X, Send, Bot, Loader2, Sparkles } from 'lucide-react'; +import { X, Send, Bot, Loader2, Sparkles, Trash2 } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { VideoContext } from '@/lib/ai-orchestrator'; @@ -26,13 +26,23 @@ export default function AIChatPanel({ context, welcomeMessage = '你好!我是MoonTVPlus的AI影视助手,有什么可以帮你的吗?', }: AIChatPanelProps) { + // 生成sessionStorage的key,基于视频上下文 + const getStorageKey = () => { + if (context?.title) { + return `ai-chat-${context.title}-${context.year || ''}-${context.type || ''}`; + } + return 'ai-chat-general'; + }; + const [messages, setMessages] = useState([ { role: 'assistant', content: welcomeMessage }, ]); const [input, setInput] = useState(''); const [isStreaming, setIsStreaming] = useState(false); + const [isMobile, setIsMobile] = useState(false); const messagesEndRef = useRef(null); const inputRef = useRef(null); + const contextKeyRef = useRef(getStorageKey()); // 自动滚动到底部 const scrollToBottom = () => { @@ -43,9 +53,59 @@ export default function AIChatPanel({ scrollToBottom(); }, [messages]); + // 从sessionStorage加载消息记录 + useEffect(() => { + if (typeof window === 'undefined') return; + + const storageKey = getStorageKey(); + const savedMessages = sessionStorage.getItem(storageKey); + + if (savedMessages) { + try { + const parsed = JSON.parse(savedMessages); + if (Array.isArray(parsed) && parsed.length > 0) { + setMessages(parsed); + } + } catch (error) { + console.error('加载聊天记录失败:', error); + } + } + }, []); // 只在组件挂载时加载一次 + + // 保存消息记录到sessionStorage + useEffect(() => { + if (typeof window === 'undefined') return; + + const storageKey = getStorageKey(); + try { + sessionStorage.setItem(storageKey, JSON.stringify(messages)); + } catch (error) { + console.error('保存聊天记录失败:', error); + } + }, [messages, context]); // 消息变化时保存 + + // 检测VideoContext变化,清除旧的聊天记录 + useEffect(() => { + if (typeof window === 'undefined') return; + + const newKey = getStorageKey(); + if (contextKeyRef.current !== newKey) { + // 上下文变化了,清除消息并重置为欢迎消息 + console.log('视频上下文变化,清除聊天记录'); + setMessages([{ role: 'assistant', content: welcomeMessage }]); + contextKeyRef.current = newKey; + } + }, [context, welcomeMessage]); // 监听context变化 + // 自动聚焦输入框和防止背景滚动 useEffect(() => { if (isOpen) { + // 检测是否为移动设备 + const checkMobile = () => { + setIsMobile(window.innerWidth < 768); + }; + checkMobile(); + // 聚焦输入框 if (inputRef.current) { inputRef.current.focus(); @@ -174,6 +234,20 @@ export default function AIChatPanel({ } }; + // 清空聊天上下文 + const handleClearContext = () => { + if (typeof window === 'undefined') return; + + // 清除sessionStorage + const storageKey = getStorageKey(); + sessionStorage.removeItem(storageKey); + + // 重置消息为欢迎消息 + setMessages([{ role: 'assistant', content: welcomeMessage }]); + + console.log('已清空聊天上下文'); + }; + if (!isOpen) return null; const modalContent = ( @@ -289,12 +363,20 @@ export default function AIChatPanel({ {/* 输入区域 */}
+