ai问片支持非流式响应

This commit is contained in:
mtvpls
2026-01-13 21:39:15 +08:00
parent 93688bb8da
commit 59e3685e65
5 changed files with 119 additions and 56 deletions

View File

@@ -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 = ({
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
</label>
<textarea
value={systemPrompt}
value={systemPrompt}
onChange={(e) => setSystemPrompt(e.target.value)}
rows={4}
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'
/>
</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>
</details>

View File

@@ -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,
};

View File

@@ -34,8 +34,9 @@ async function streamOpenAIChat(
model: string;
temperature: number;
maxTokens: number;
}
): Promise<ReadableStream> {
},
enableStreaming: boolean = true
): Promise<ReadableStream | Response> {
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(

View File

@@ -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);

View File

@@ -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} 替换符)