ai问片支持非流式响应

This commit is contained in:
mtvpls
2026-01-13 21:39:15 +08:00
parent 93688bb8da
commit 59e3685e65
5 changed files with 117 additions and 54 deletions
+29 -2
View File
@@ -9108,6 +9108,7 @@ const AIConfigComponent = ({
const [temperature, setTemperature] = useState(0.7); const [temperature, setTemperature] = useState(0.7);
const [maxTokens, setMaxTokens] = useState(1000); const [maxTokens, setMaxTokens] = useState(1000);
const [systemPrompt, setSystemPrompt] = useState(''); const [systemPrompt, setSystemPrompt] = useState('');
const [enableStreaming, setEnableStreaming] = useState(true);
// AI默认消息配置 // AI默认消息配置
const [defaultMessageNoVideo, setDefaultMessageNoVideo] = useState(''); const [defaultMessageNoVideo, setDefaultMessageNoVideo] = useState('');
@@ -9133,6 +9134,7 @@ const AIConfigComponent = ({
setTemperature(config.AIConfig.Temperature ?? 0.7); setTemperature(config.AIConfig.Temperature ?? 0.7);
setMaxTokens(config.AIConfig.MaxTokens ?? 1000); setMaxTokens(config.AIConfig.MaxTokens ?? 1000);
setSystemPrompt(config.AIConfig.SystemPrompt || ''); setSystemPrompt(config.AIConfig.SystemPrompt || '');
setEnableStreaming(config.AIConfig.EnableStreaming !== false);
setDefaultMessageNoVideo(config.AIConfig.DefaultMessageNoVideo || ''); setDefaultMessageNoVideo(config.AIConfig.DefaultMessageNoVideo || '');
setDefaultMessageWithVideo(config.AIConfig.DefaultMessageWithVideo || ''); setDefaultMessageWithVideo(config.AIConfig.DefaultMessageWithVideo || '');
} }
@@ -9165,6 +9167,7 @@ const AIConfigComponent = ({
Temperature: temperature, Temperature: temperature,
MaxTokens: maxTokens, MaxTokens: maxTokens,
SystemPrompt: systemPrompt, SystemPrompt: systemPrompt,
EnableStreaming: enableStreaming,
DefaultMessageNoVideo: defaultMessageNoVideo, DefaultMessageNoVideo: defaultMessageNoVideo,
DefaultMessageWithVideo: defaultMessageWithVideo, DefaultMessageWithVideo: defaultMessageWithVideo,
}), }),
@@ -9517,16 +9520,40 @@ const AIConfigComponent = ({
<div> <div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'> <label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
</label> </label>
<textarea <textarea
value={systemPrompt} value={systemPrompt}
onChange={(e) => setSystemPrompt(e.target.value)} onChange={(e) => setSystemPrompt(e.target.value)}
rows={4} rows={4}
placeholder='可自定义AI的角色和行为规则...' placeholder='可自定义AI的角色和行为规则...'
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100' className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100'
/> />
</div> </div>
{/* 流式响应开关 */}
<div className='flex items-center justify-between py-3 border-t border-gray-200 dark:border-gray-700'>
<div className='flex-1'>
<label className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</label>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
AI消息将实时流式显示
</p>
</div>
<button
onClick={() => setEnableStreaming(!enableStreaming)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
enableStreaming ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-700'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
enableStreaming ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
</div> </div>
</details> </details>
+5 -1
View File
@@ -61,6 +61,7 @@ export async function POST(request: NextRequest) {
Temperature, Temperature,
MaxTokens, MaxTokens,
SystemPrompt, SystemPrompt,
EnableStreaming,
DefaultMessageNoVideo, DefaultMessageNoVideo,
DefaultMessageWithVideo, DefaultMessageWithVideo,
} = body as { } = body as {
@@ -96,6 +97,7 @@ export async function POST(request: NextRequest) {
Temperature?: number; Temperature?: number;
MaxTokens?: number; MaxTokens?: number;
SystemPrompt?: string; SystemPrompt?: string;
EnableStreaming?: boolean;
DefaultMessageNoVideo?: string; DefaultMessageNoVideo?: string;
DefaultMessageWithVideo?: string; DefaultMessageWithVideo?: string;
}; };
@@ -133,7 +135,8 @@ export async function POST(request: NextRequest) {
typeof AllowRegularUsers !== 'boolean' || typeof AllowRegularUsers !== 'boolean' ||
(Temperature !== undefined && typeof Temperature !== 'number') || (Temperature !== undefined && typeof Temperature !== 'number') ||
(MaxTokens !== undefined && typeof MaxTokens !== '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 }); return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
} }
@@ -182,6 +185,7 @@ export async function POST(request: NextRequest) {
Temperature, Temperature,
MaxTokens, MaxTokens,
SystemPrompt, SystemPrompt,
EnableStreaming,
DefaultMessageNoVideo, DefaultMessageNoVideo,
DefaultMessageWithVideo, DefaultMessageWithVideo,
}; };
+27 -15
View File
@@ -34,8 +34,9 @@ async function streamOpenAIChat(
model: string; model: string;
temperature: number; temperature: number;
maxTokens: number; maxTokens: number;
} },
): Promise<ReadableStream> { enableStreaming: boolean = true
): Promise<ReadableStream | Response> {
const response = await fetch(`${config.baseURL}/chat/completions`, { const response = await fetch(`${config.baseURL}/chat/completions`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -47,7 +48,7 @@ async function streamOpenAIChat(
messages, messages,
temperature: config.temperature, temperature: config.temperature,
max_tokens: config.maxTokens, 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 // 6. 调用自定义API
const temperature = aiConfig.Temperature ?? 0.7; const temperature = aiConfig.Temperature ?? 0.7;
const maxTokens = aiConfig.MaxTokens ?? 1000; const maxTokens = aiConfig.MaxTokens ?? 1000;
const enableStreaming = aiConfig.EnableStreaming !== false; // 默认启用流式响应
if (!aiConfig.CustomApiKey || !aiConfig.CustomBaseURL) { if (!aiConfig.CustomApiKey || !aiConfig.CustomBaseURL) {
return NextResponse.json( 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, apiKey: aiConfig.CustomApiKey,
baseURL: aiConfig.CustomBaseURL, baseURL: aiConfig.CustomBaseURL,
model: aiConfig.CustomModel || 'gpt-3.5-turbo', model: aiConfig.CustomModel || 'gpt-3.5-turbo',
temperature, temperature,
maxTokens, maxTokens,
}); }, enableStreaming);
// 7. 转换为SSE格式并返回 // 7. 根据是否启用流式响应返回不同格式
const sseStream = transformToSSE(stream, 'openai'); if (enableStreaming) {
// 流式响应:转换为SSE格式并返回
const sseStream = transformToSSE(result as ReadableStream, 'openai');
return new NextResponse(sseStream, { return new NextResponse(sseStream, {
headers: { headers: {
'Content-Type': 'text/event-stream', 'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache', 'Cache-Control': 'no-cache',
Connection: 'keep-alive', 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) { } catch (error) {
console.error('❌ AI聊天API错误:', error); console.error('❌ AI聊天API错误:', error);
return NextResponse.json( return NextResponse.json(
+55 -36
View File
@@ -152,63 +152,82 @@ export default function AIChatPanel({
body: JSON.stringify({ body: JSON.stringify({
message: userMessage, message: userMessage,
context, context,
history: messages.filter((m) => m.role !== 'assistant' || m.content !== welcomeMessage), history: messages.filter((m) => m.role !== 'assistant' || m.content !== welcomeMessage),
}), }),
}); });
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); 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); throw new Error(errorMsg);
} }
// 处理流式响应 // 检查响应类型:流式(text/event-stream)或非流式(application/json)
const reader = response.body?.getReader(); const contentType = response.headers.get('content-type');
const decoder = new TextDecoder();
if (!reader) { if (contentType?.includes('text/event-stream')) {
throw new Error('无法读取响应流'); // 处理流式响应
} const reader = response.body?.getReader();
const decoder = new TextDecoder();
let assistantMessage = ''; if (!reader) {
throw new Error('无法读取响应流');
}
while (true) { let assistantMessage = '';
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true }); while (true) {
const lines = chunk.split('\n').filter((line) => line.trim() !== ''); const { done, value } = await reader.read();
if (done) break;
for (const line of lines) { const chunk = decoder.decode(value, { stream: true });
if (line.startsWith('data: ')) { const lines = chunk.split('\n').filter((line) => line.trim() !== '');
const data = line.slice(6);
if (data === '[DONE]') { for (const line of lines) {
break; if (line.startsWith('data: ')) {
} const data = line.slice(6);
try { if (data === '[DONE]') {
const json = JSON.parse(data); break;
const text = json.text || ''; }
if (text) { try {
assistantMessage += text; const json = JSON.parse(data);
const text = json.text || '';
// 更新最后一条消息 if (text) {
setMessages((prev) => { assistantMessage += text;
const newMessages = [...prev];
newMessages[newMessages.length - 1] = { // 更新最后一条消息
role: 'assistant', setMessages((prev) => {
content: assistantMessage, const newMessages = [...prev];
}; newMessages[newMessages.length - 1] = {
return newMessages; 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) { } catch (error) {
console.error('发送消息失败:', error); console.error('发送消息失败:', error);
+1
View File
@@ -158,6 +158,7 @@ export interface AdminConfig {
Temperature?: number; // AI温度参数(0-2),默认0.7 Temperature?: number; // AI温度参数(0-2),默认0.7
MaxTokens?: number; // 最大回复token数,默认1000 MaxTokens?: number; // 最大回复token数,默认1000
SystemPrompt?: string; // 自定义系统提示词 SystemPrompt?: string; // 自定义系统提示词
EnableStreaming?: boolean; // 是否启用流式响应,默认true
// AI问片默认消息配置 // AI问片默认消息配置
DefaultMessageNoVideo?: string; // 无视频时的默认消息 DefaultMessageNoVideo?: string; // 无视频时的默认消息
DefaultMessageWithVideo?: string; // 有视频时的默认消息(支持 {title} 替换符) DefaultMessageWithVideo?: string; // 有视频时的默认消息(支持 {title} 替换符)