ai问片支持非流式响应
This commit is contained in:
+29
-2
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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} 替换符)
|
||||||
|
|||||||
Reference in New Issue
Block a user