diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index f82a746..8e88a61 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -9108,6 +9108,7 @@ const AIConfigComponent = ({
const [temperature, setTemperature] = useState(0.7);
const [maxTokens, setMaxTokens] = useState(1000);
const [systemPrompt, setSystemPrompt] = useState('');
+ const [enableStreaming, setEnableStreaming] = useState(true);
// AI默认消息配置
const [defaultMessageNoVideo, setDefaultMessageNoVideo] = useState('');
@@ -9133,6 +9134,7 @@ const AIConfigComponent = ({
setTemperature(config.AIConfig.Temperature ?? 0.7);
setMaxTokens(config.AIConfig.MaxTokens ?? 1000);
setSystemPrompt(config.AIConfig.SystemPrompt || '');
+ setEnableStreaming(config.AIConfig.EnableStreaming !== false);
setDefaultMessageNoVideo(config.AIConfig.DefaultMessageNoVideo || '');
setDefaultMessageWithVideo(config.AIConfig.DefaultMessageWithVideo || '');
}
@@ -9165,6 +9167,7 @@ const AIConfigComponent = ({
Temperature: temperature,
MaxTokens: maxTokens,
SystemPrompt: systemPrompt,
+ EnableStreaming: enableStreaming,
DefaultMessageNoVideo: defaultMessageNoVideo,
DefaultMessageWithVideo: defaultMessageWithVideo,
}),
@@ -9517,16 +9520,40 @@ const AIConfigComponent = ({
+
+ {/* 流式响应开关 */}
+
+
+
+
+ 启用后AI消息将实时流式显示,关闭后将等待完整响应后一次性显示
+
+
+
+
diff --git a/src/app/api/admin/ai/route.ts b/src/app/api/admin/ai/route.ts
index 9d99001..07e7425 100644
--- a/src/app/api/admin/ai/route.ts
+++ b/src/app/api/admin/ai/route.ts
@@ -61,6 +61,7 @@ export async function POST(request: NextRequest) {
Temperature,
MaxTokens,
SystemPrompt,
+ EnableStreaming,
DefaultMessageNoVideo,
DefaultMessageWithVideo,
} = body as {
@@ -96,6 +97,7 @@ export async function POST(request: NextRequest) {
Temperature?: number;
MaxTokens?: number;
SystemPrompt?: string;
+ EnableStreaming?: boolean;
DefaultMessageNoVideo?: string;
DefaultMessageWithVideo?: string;
};
@@ -133,7 +135,8 @@ export async function POST(request: NextRequest) {
typeof AllowRegularUsers !== 'boolean' ||
(Temperature !== undefined && typeof Temperature !== 'number') ||
(MaxTokens !== undefined && typeof MaxTokens !== 'number') ||
- (SystemPrompt !== undefined && typeof SystemPrompt !== 'string')
+ (SystemPrompt !== undefined && typeof SystemPrompt !== 'string') ||
+ (EnableStreaming !== undefined && typeof EnableStreaming !== 'boolean')
) {
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
}
@@ -182,6 +185,7 @@ export async function POST(request: NextRequest) {
Temperature,
MaxTokens,
SystemPrompt,
+ EnableStreaming,
DefaultMessageNoVideo,
DefaultMessageWithVideo,
};
diff --git a/src/app/api/ai/chat/route.ts b/src/app/api/ai/chat/route.ts
index 6cf50a1..736d02d 100644
--- a/src/app/api/ai/chat/route.ts
+++ b/src/app/api/ai/chat/route.ts
@@ -34,8 +34,9 @@ async function streamOpenAIChat(
model: string;
temperature: number;
maxTokens: number;
- }
-): Promise {
+ },
+ enableStreaming: boolean = true
+): Promise {
const response = await fetch(`${config.baseURL}/chat/completions`, {
method: 'POST',
headers: {
@@ -47,7 +48,7 @@ async function streamOpenAIChat(
messages,
temperature: config.temperature,
max_tokens: config.maxTokens,
- stream: true,
+ stream: enableStreaming,
}),
});
@@ -57,7 +58,7 @@ async function streamOpenAIChat(
);
}
- return response.body!;
+ return enableStreaming ? response.body! : response;
}
/**
@@ -265,6 +266,7 @@ export async function POST(request: NextRequest) {
// 6. 调用自定义API
const temperature = aiConfig.Temperature ?? 0.7;
const maxTokens = aiConfig.MaxTokens ?? 1000;
+ const enableStreaming = aiConfig.EnableStreaming !== false; // 默认启用流式响应
if (!aiConfig.CustomApiKey || !aiConfig.CustomBaseURL) {
return NextResponse.json(
@@ -273,24 +275,34 @@ export async function POST(request: NextRequest) {
);
}
- const stream = await streamOpenAIChat(messages, {
+ const result = await streamOpenAIChat(messages, {
apiKey: aiConfig.CustomApiKey,
baseURL: aiConfig.CustomBaseURL,
model: aiConfig.CustomModel || 'gpt-3.5-turbo',
temperature,
maxTokens,
- });
+ }, enableStreaming);
- // 7. 转换为SSE格式并返回
- const sseStream = transformToSSE(stream, 'openai');
+ // 7. 根据是否启用流式响应返回不同格式
+ if (enableStreaming) {
+ // 流式响应:转换为SSE格式并返回
+ const sseStream = transformToSSE(result as ReadableStream, 'openai');
- return new NextResponse(sseStream, {
- headers: {
- 'Content-Type': 'text/event-stream',
- 'Cache-Control': 'no-cache',
- Connection: 'keep-alive',
- },
- });
+ return new NextResponse(sseStream, {
+ headers: {
+ 'Content-Type': 'text/event-stream',
+ 'Cache-Control': 'no-cache',
+ Connection: 'keep-alive',
+ },
+ });
+ } else {
+ // 非流式响应:等待完整响应后返回JSON
+ const response = result as Response;
+ const data = await response.json();
+ const content = data.choices?.[0]?.message?.content || '';
+
+ return NextResponse.json({ content });
+ }
} catch (error) {
console.error('❌ AI聊天API错误:', error);
return NextResponse.json(
diff --git a/src/components/AIChatPanel.tsx b/src/components/AIChatPanel.tsx
index e0a1c43..c278e54 100644
--- a/src/components/AIChatPanel.tsx
+++ b/src/components/AIChatPanel.tsx
@@ -152,63 +152,82 @@ export default function AIChatPanel({
body: JSON.stringify({
message: userMessage,
context,
- history: messages.filter((m) => m.role !== 'assistant' || m.content !== welcomeMessage),
+ history: messages.filter((m) => m.role !== 'assistant' || m.content !== welcomeMessage),
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
- const errorMsg = errorData.error || errorData.details || `请求失败 (${response.status})`;
+ const errorMsg = errorData.error || errorData.details || `请求失败 (${response.status})`;
throw new Error(errorMsg);
}
- // 处理流式响应
- const reader = response.body?.getReader();
- const decoder = new TextDecoder();
+ // 检查响应类型:流式(text/event-stream)或非流式(application/json)
+ const contentType = response.headers.get('content-type');
- if (!reader) {
- throw new Error('无法读取响应流');
- }
+ if (contentType?.includes('text/event-stream')) {
+ // 处理流式响应
+ const reader = response.body?.getReader();
+ const decoder = new TextDecoder();
- let assistantMessage = '';
+ if (!reader) {
+ throw new Error('无法读取响应流');
+ }
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
+ let assistantMessage = '';
- const chunk = decoder.decode(value, { stream: true });
- const lines = chunk.split('\n').filter((line) => line.trim() !== '');
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
- for (const line of lines) {
- if (line.startsWith('data: ')) {
- const data = line.slice(6);
+ const chunk = decoder.decode(value, { stream: true });
+ const lines = chunk.split('\n').filter((line) => line.trim() !== '');
- if (data === '[DONE]') {
- break;
- }
+ for (const line of lines) {
+ if (line.startsWith('data: ')) {
+ const data = line.slice(6);
- try {
- const json = JSON.parse(data);
- const text = json.text || '';
-
- if (text) {
- assistantMessage += text;
-
- // 更新最后一条消息
- setMessages((prev) => {
- const newMessages = [...prev];
- newMessages[newMessages.length - 1] = {
- role: 'assistant',
- content: assistantMessage,
- };
- return newMessages;
- });
+ if (data === '[DONE]') {
+ break;
+ }
+
+ try {
+ const json = JSON.parse(data);
+ const text = json.text || '';
+
+ if (text) {
+ assistantMessage += text;
+
+ // 更新最后一条消息
+ setMessages((prev) => {
+ const newMessages = [...prev];
+ newMessages[newMessages.length - 1] = {
+ role: 'assistant',
+ content: assistantMessage,
+ };
+ return newMessages;
+ });
+ }
+ } catch (e) {
+ console.error('解析SSE数据失败:', e);
}
- } catch (e) {
- console.error('解析SSE数据失败:', e);
}
}
}
+ } else {
+ // 处理非流式响应
+ const data = await response.json();
+ const content = data.content || '';
+
+ // 更新最后一条消息为完整响应
+ setMessages((prev) => {
+ const newMessages = [...prev];
+ newMessages[newMessages.length - 1] = {
+ role: 'assistant',
+ content: content,
+ };
+ return newMessages;
+ });
}
} catch (error) {
console.error('发送消息失败:', error);
diff --git a/src/lib/admin.types.ts b/src/lib/admin.types.ts
index 18a04cd..8dfb3a1 100644
--- a/src/lib/admin.types.ts
+++ b/src/lib/admin.types.ts
@@ -158,6 +158,7 @@ export interface AdminConfig {
Temperature?: number; // AI温度参数(0-2),默认0.7
MaxTokens?: number; // 最大回复token数,默认1000
SystemPrompt?: string; // 自定义系统提示词
+ EnableStreaming?: boolean; // 是否启用流式响应,默认true
// AI问片默认消息配置
DefaultMessageNoVideo?: string; // 无视频时的默认消息
DefaultMessageWithVideo?: string; // 有视频时的默认消息(支持 {title} 替换符)