refactor(API): 重构聊天接口支持多模型类型
- 拆分 Claude 和 Codex 模型的处理逻辑为独立函数 - 新增 handleClaudeChat 函数处理 Claude 系列模型 - 新增 handleCodexChat 函数处理 Codex 系列模型(OpenAI Response API 格式) - 添加 isCodexModel 工具函数用于模型类型判断 - 优化代码结构,提高可维护性
This commit is contained in:
parent
55dcb618af
commit
da19858c2d
@ -13,7 +13,7 @@ interface ChatRequest {
|
|||||||
enableThinking?: boolean;
|
enableThinking?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 消息内容块类型
|
// 消息内容块类型(Claude)
|
||||||
interface ContentBlock {
|
interface ContentBlock {
|
||||||
type: 'text' | 'tool_use' | 'tool_result' | 'thinking';
|
type: 'text' | 'tool_use' | 'tool_result' | 'thinking';
|
||||||
text?: string;
|
text?: string;
|
||||||
@ -25,12 +25,35 @@ interface ContentBlock {
|
|||||||
content?: string;
|
content?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// API 消息类型
|
// API 消息类型(Claude)
|
||||||
interface APIMessage {
|
interface APIMessage {
|
||||||
role: 'user' | 'assistant';
|
role: 'user' | 'assistant';
|
||||||
content: string | ContentBlock[];
|
content: string | ContentBlock[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenAI 消息类型
|
||||||
|
interface OpenAIMessage {
|
||||||
|
role: 'system' | 'user' | 'assistant' | 'tool';
|
||||||
|
content: string | null;
|
||||||
|
tool_calls?: OpenAIToolCall[];
|
||||||
|
tool_call_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenAI 工具调用
|
||||||
|
interface OpenAIToolCall {
|
||||||
|
id: string;
|
||||||
|
type: 'function';
|
||||||
|
function: {
|
||||||
|
name: string;
|
||||||
|
arguments: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否为 Codex 模型
|
||||||
|
function isCodexModel(modelId: string): boolean {
|
||||||
|
return modelId.startsWith('gpt-') && modelId.includes('codex');
|
||||||
|
}
|
||||||
|
|
||||||
// 默认系统提示词 - 用于生成更详细、更有结构的回复
|
// 默认系统提示词 - 用于生成更详细、更有结构的回复
|
||||||
const DEFAULT_SYSTEM_PROMPT = `你是一个专业、友好的 AI 助手。请遵循以下规则来回复用户:
|
const DEFAULT_SYSTEM_PROMPT = `你是一个专业、友好的 AI 助手。请遵循以下规则来回复用户:
|
||||||
|
|
||||||
@ -148,6 +171,456 @@ export async function POST(request: Request) {
|
|||||||
status: 'completed',
|
status: 'completed',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 准备 AI 消息 ID
|
||||||
|
const assistantMessageId = nanoid();
|
||||||
|
|
||||||
|
// 判断使用的模型类型
|
||||||
|
const useModel = model || conversation.model;
|
||||||
|
const isCodex = isCodexModel(useModel);
|
||||||
|
|
||||||
|
// 创建 SSE 响应
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
async start(controller) {
|
||||||
|
try {
|
||||||
|
const cchUrl = settings.cchUrl || 'http://localhost:13500';
|
||||||
|
|
||||||
|
// 获取系统提示词
|
||||||
|
const baseSystemPrompt = conversation.systemPrompt || settings.systemPrompt || DEFAULT_SYSTEM_PROMPT;
|
||||||
|
const currentDate = new Date().toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
weekday: 'long',
|
||||||
|
});
|
||||||
|
const systemPrompt = baseSystemPrompt.replace('{{CURRENT_DATE}}', currentDate);
|
||||||
|
|
||||||
|
// 获取温度参数
|
||||||
|
const temperature = parseFloat(conversation.temperature || settings.temperature || '0.7');
|
||||||
|
|
||||||
|
let fullContent = '';
|
||||||
|
let thinkingContent = '';
|
||||||
|
let totalInputTokens = 0;
|
||||||
|
let totalOutputTokens = 0;
|
||||||
|
|
||||||
|
if (isCodex) {
|
||||||
|
// ==================== Codex 模型处理(OpenAI 格式) ====================
|
||||||
|
const result = await handleCodexChat({
|
||||||
|
cchUrl,
|
||||||
|
apiKey: settings.cchApiKey!,
|
||||||
|
model: useModel,
|
||||||
|
systemPrompt,
|
||||||
|
temperature,
|
||||||
|
historyMessages,
|
||||||
|
message,
|
||||||
|
tools: tools || (conversation.tools as string[]) || [],
|
||||||
|
controller,
|
||||||
|
encoder,
|
||||||
|
});
|
||||||
|
|
||||||
|
fullContent = result.fullContent;
|
||||||
|
totalInputTokens = result.inputTokens;
|
||||||
|
totalOutputTokens = result.outputTokens;
|
||||||
|
} else {
|
||||||
|
// ==================== Claude 模型处理(原有逻辑) ====================
|
||||||
|
const result = await handleClaudeChat({
|
||||||
|
cchUrl,
|
||||||
|
apiKey: settings.cchApiKey!,
|
||||||
|
model: useModel,
|
||||||
|
systemPrompt,
|
||||||
|
temperature,
|
||||||
|
historyMessages,
|
||||||
|
message,
|
||||||
|
tools: tools || (conversation.tools as string[]) || [],
|
||||||
|
enableThinking: enableThinking ?? conversation.enableThinking ?? false,
|
||||||
|
controller,
|
||||||
|
encoder,
|
||||||
|
});
|
||||||
|
|
||||||
|
fullContent = result.fullContent;
|
||||||
|
thinkingContent = result.thinkingContent;
|
||||||
|
totalInputTokens = result.inputTokens;
|
||||||
|
totalOutputTokens = result.outputTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存 AI 回复到数据库
|
||||||
|
await db.insert(messages).values({
|
||||||
|
messageId: assistantMessageId,
|
||||||
|
conversationId,
|
||||||
|
role: 'assistant',
|
||||||
|
content: fullContent,
|
||||||
|
thinkingContent: thinkingContent || null,
|
||||||
|
inputTokens: totalInputTokens,
|
||||||
|
outputTokens: totalOutputTokens,
|
||||||
|
status: 'completed',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新对话信息
|
||||||
|
await db
|
||||||
|
.update(conversations)
|
||||||
|
.set({
|
||||||
|
messageCount: (conversation.messageCount || 0) + 2,
|
||||||
|
totalTokens: (conversation.totalTokens || 0) + totalInputTokens + totalOutputTokens,
|
||||||
|
lastMessageAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
title: (conversation.messageCount || 0) === 0
|
||||||
|
? message.slice(0, 50) + (message.length > 50 ? '...' : '')
|
||||||
|
: conversation.title,
|
||||||
|
})
|
||||||
|
.where(eq(conversations.conversationId, conversationId));
|
||||||
|
|
||||||
|
// 发送完成事件
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
|
type: 'done',
|
||||||
|
messageId: assistantMessageId,
|
||||||
|
inputTokens: totalInputTokens,
|
||||||
|
outputTokens: totalOutputTokens,
|
||||||
|
})}\n\n`));
|
||||||
|
|
||||||
|
controller.close();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Stream error:', error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
|
type: 'error',
|
||||||
|
error: errorMessage,
|
||||||
|
})}\n\n`));
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(stream, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Chat API error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to process chat request' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Codex 模型处理函数(使用 Codex Response API 格式)====================
|
||||||
|
interface CodexChatParams {
|
||||||
|
cchUrl: string;
|
||||||
|
apiKey: string;
|
||||||
|
model: string;
|
||||||
|
systemPrompt: string;
|
||||||
|
temperature: number;
|
||||||
|
historyMessages: { role: string; content: string }[];
|
||||||
|
message: string;
|
||||||
|
tools: string[];
|
||||||
|
controller: ReadableStreamDefaultController;
|
||||||
|
encoder: TextEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Codex Response API 的输入项类型
|
||||||
|
interface CodexInputItem {
|
||||||
|
type: 'message';
|
||||||
|
role: 'user' | 'assistant' | 'system';
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Codex Response API 的工具调用类型
|
||||||
|
interface CodexFunctionCall {
|
||||||
|
call_id: string;
|
||||||
|
name: string;
|
||||||
|
arguments: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCodexChat(params: CodexChatParams): Promise<{
|
||||||
|
fullContent: string;
|
||||||
|
inputTokens: number;
|
||||||
|
outputTokens: number;
|
||||||
|
}> {
|
||||||
|
const {
|
||||||
|
cchUrl,
|
||||||
|
apiKey,
|
||||||
|
model,
|
||||||
|
systemPrompt,
|
||||||
|
temperature,
|
||||||
|
historyMessages,
|
||||||
|
message,
|
||||||
|
tools,
|
||||||
|
controller,
|
||||||
|
encoder,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
// 构建 Codex Response API 格式的输入
|
||||||
|
const inputItems: CodexInputItem[] = [
|
||||||
|
...historyMessages.map((msg) => ({
|
||||||
|
type: 'message' as const,
|
||||||
|
role: msg.role as 'user' | 'assistant',
|
||||||
|
content: msg.content,
|
||||||
|
})),
|
||||||
|
{ type: 'message' as const, role: 'user' as const, content: message },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 构建 Codex Response API 格式的工具定义
|
||||||
|
const codexTools = buildCodexToolDefinitions(tools);
|
||||||
|
|
||||||
|
let fullContent = '';
|
||||||
|
let totalInputTokens = 0;
|
||||||
|
let totalOutputTokens = 0;
|
||||||
|
let loopCount = 0;
|
||||||
|
const maxLoops = 10;
|
||||||
|
|
||||||
|
while (loopCount < maxLoops) {
|
||||||
|
loopCount++;
|
||||||
|
|
||||||
|
// 构建 Codex Response API 请求体
|
||||||
|
const requestBody: Record<string, unknown> = {
|
||||||
|
model,
|
||||||
|
input: inputItems,
|
||||||
|
stream: true,
|
||||||
|
instructions: systemPrompt,
|
||||||
|
temperature,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (codexTools.length > 0) {
|
||||||
|
requestBody.tools = codexTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 Codex Response API 端点
|
||||||
|
const response = await fetch(`${cchUrl}/v1/responses`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestBody),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`CCH API error: ${response.status} - ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
if (!reader) {
|
||||||
|
throw new Error('No response body');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集当前轮次的内容
|
||||||
|
let currentTextContent = '';
|
||||||
|
const functionCalls: CodexFunctionCall[] = [];
|
||||||
|
let currentFunctionCall: { call_id: string; name: string; arguments: string } | null = null;
|
||||||
|
let hasToolUse = false;
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = '';
|
||||||
|
|
||||||
|
// 处理 Codex Response API 流式响应
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
buffer = lines.pop() || '';
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// Codex Response API 使用 "event: xxx" 和 "data: xxx" 格式
|
||||||
|
if (line.startsWith('event: ')) {
|
||||||
|
// 事件类型行,继续读取下一行的 data
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('data: ')) {
|
||||||
|
const data = line.slice(6);
|
||||||
|
if (data === '[DONE]') continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const event = JSON.parse(data);
|
||||||
|
|
||||||
|
// 处理不同的事件类型
|
||||||
|
if (event.type === 'response.output_text.delta') {
|
||||||
|
// 文本增量
|
||||||
|
const delta = event.delta || '';
|
||||||
|
currentTextContent += delta;
|
||||||
|
fullContent += delta;
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
|
type: 'text',
|
||||||
|
content: delta,
|
||||||
|
})}\n\n`));
|
||||||
|
} else if (event.type === 'response.content_part.delta') {
|
||||||
|
// 内容部分增量(另一种格式)
|
||||||
|
const delta = event.delta?.text || event.delta || '';
|
||||||
|
if (delta) {
|
||||||
|
currentTextContent += delta;
|
||||||
|
fullContent += delta;
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
|
type: 'text',
|
||||||
|
content: delta,
|
||||||
|
})}\n\n`));
|
||||||
|
}
|
||||||
|
} else if (event.type === 'response.function_call_arguments.delta') {
|
||||||
|
// 函数调用参数增量
|
||||||
|
if (currentFunctionCall) {
|
||||||
|
currentFunctionCall.arguments += event.delta || '';
|
||||||
|
}
|
||||||
|
} else if (event.type === 'response.output_item.added') {
|
||||||
|
// 新输出项添加
|
||||||
|
const item = event.item;
|
||||||
|
if (item?.type === 'function_call') {
|
||||||
|
currentFunctionCall = {
|
||||||
|
call_id: item.call_id || '',
|
||||||
|
name: item.name || '',
|
||||||
|
arguments: '',
|
||||||
|
};
|
||||||
|
hasToolUse = true;
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
|
type: 'tool_use_start',
|
||||||
|
id: item.call_id,
|
||||||
|
name: item.name,
|
||||||
|
})}\n\n`));
|
||||||
|
}
|
||||||
|
} else if (event.type === 'response.output_item.done') {
|
||||||
|
// 输出项完成
|
||||||
|
const item = event.item;
|
||||||
|
if (item?.type === 'function_call' && currentFunctionCall) {
|
||||||
|
// 更新函数调用信息
|
||||||
|
currentFunctionCall.call_id = item.call_id || currentFunctionCall.call_id;
|
||||||
|
currentFunctionCall.name = item.name || currentFunctionCall.name;
|
||||||
|
currentFunctionCall.arguments = item.arguments || currentFunctionCall.arguments;
|
||||||
|
functionCalls.push({ ...currentFunctionCall });
|
||||||
|
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
|
type: 'tool_use_complete',
|
||||||
|
id: currentFunctionCall.call_id,
|
||||||
|
name: currentFunctionCall.name,
|
||||||
|
input: JSON.parse(currentFunctionCall.arguments || '{}'),
|
||||||
|
})}\n\n`));
|
||||||
|
currentFunctionCall = null;
|
||||||
|
}
|
||||||
|
} else if (event.type === 'response.completed') {
|
||||||
|
// 响应完成,提取 usage 信息
|
||||||
|
const usage = event.response?.usage;
|
||||||
|
if (usage) {
|
||||||
|
totalInputTokens = usage.input_tokens || 0;
|
||||||
|
totalOutputTokens = usage.output_tokens || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Parse error:', e, 'Line:', line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否需要执行工具
|
||||||
|
if (hasToolUse && functionCalls.length > 0) {
|
||||||
|
// 将助手消息添加到输入历史
|
||||||
|
if (currentTextContent) {
|
||||||
|
inputItems.push({
|
||||||
|
type: 'message',
|
||||||
|
role: 'assistant',
|
||||||
|
content: currentTextContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行所有工具并收集结果
|
||||||
|
for (const fc of functionCalls) {
|
||||||
|
// 发送工具执行开始事件
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
|
type: 'tool_execution_start',
|
||||||
|
id: fc.call_id,
|
||||||
|
name: fc.name,
|
||||||
|
})}\n\n`));
|
||||||
|
|
||||||
|
// 解析工具参数
|
||||||
|
let toolInput: Record<string, unknown> = {};
|
||||||
|
try {
|
||||||
|
toolInput = JSON.parse(fc.arguments || '{}');
|
||||||
|
} catch {
|
||||||
|
console.error('Failed to parse tool arguments');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行工具
|
||||||
|
const result = await executeTool(fc.name, toolInput);
|
||||||
|
|
||||||
|
// 发送工具执行结果事件
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
|
type: 'tool_execution_result',
|
||||||
|
id: fc.call_id,
|
||||||
|
name: fc.name,
|
||||||
|
success: result.success,
|
||||||
|
result: result.displayResult,
|
||||||
|
images: result.images,
|
||||||
|
})}\n\n`));
|
||||||
|
|
||||||
|
// 将工具结果显示给用户
|
||||||
|
const toolDisplayText = `\n\n${result.displayResult}\n\n`;
|
||||||
|
fullContent += toolDisplayText;
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
|
type: 'text',
|
||||||
|
content: toolDisplayText,
|
||||||
|
})}\n\n`));
|
||||||
|
|
||||||
|
// 将工具结果添加到输入历史(Codex 格式)
|
||||||
|
inputItems.push({
|
||||||
|
type: 'message',
|
||||||
|
role: 'user',
|
||||||
|
content: `Function ${fc.name} result: ${result.fullResult}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 继续循环,让 AI 基于工具结果继续回复
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有工具调用,则结束循环
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fullContent,
|
||||||
|
inputTokens: totalInputTokens,
|
||||||
|
outputTokens: totalOutputTokens,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Claude 模型处理函数 ====================
|
||||||
|
interface ClaudeChatParams {
|
||||||
|
cchUrl: string;
|
||||||
|
apiKey: string;
|
||||||
|
model: string;
|
||||||
|
systemPrompt: string;
|
||||||
|
temperature: number;
|
||||||
|
historyMessages: { role: string; content: string }[];
|
||||||
|
message: string;
|
||||||
|
tools: string[];
|
||||||
|
enableThinking: boolean;
|
||||||
|
controller: ReadableStreamDefaultController;
|
||||||
|
encoder: TextEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleClaudeChat(params: ClaudeChatParams): Promise<{
|
||||||
|
fullContent: string;
|
||||||
|
thinkingContent: string;
|
||||||
|
inputTokens: number;
|
||||||
|
outputTokens: number;
|
||||||
|
}> {
|
||||||
|
const {
|
||||||
|
cchUrl,
|
||||||
|
apiKey,
|
||||||
|
model,
|
||||||
|
systemPrompt,
|
||||||
|
temperature,
|
||||||
|
historyMessages,
|
||||||
|
message,
|
||||||
|
tools,
|
||||||
|
enableThinking,
|
||||||
|
controller,
|
||||||
|
encoder,
|
||||||
|
} = params;
|
||||||
|
|
||||||
// 构建消息历史
|
// 构建消息历史
|
||||||
const messageHistory: APIMessage[] = historyMessages.map((msg) => ({
|
const messageHistory: APIMessage[] = historyMessages.map((msg) => ({
|
||||||
role: msg.role as 'user' | 'assistant',
|
role: msg.role as 'user' | 'assistant',
|
||||||
@ -160,68 +633,36 @@ export async function POST(request: Request) {
|
|||||||
content: message,
|
content: message,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 准备 AI 消息 ID
|
// 构建工具定义
|
||||||
const assistantMessageId = nanoid();
|
const toolDefinitions = buildClaudeToolDefinitions(tools);
|
||||||
|
|
||||||
// 创建 SSE 响应
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const stream = new ReadableStream({
|
|
||||||
async start(controller) {
|
|
||||||
try {
|
|
||||||
// 调用 CCH API
|
|
||||||
const cchUrl = settings.cchUrl || 'http://localhost:13500';
|
|
||||||
const useThinking = enableThinking ?? conversation.enableThinking;
|
|
||||||
const useModel = model || conversation.model;
|
|
||||||
|
|
||||||
// 构建工具定义(如果需要)
|
|
||||||
const toolDefinitions = buildToolDefinitions(tools || (conversation.tools as string[]) || []);
|
|
||||||
|
|
||||||
// 获取系统提示词(优先级:对话级别 > 全局设置 > 默认)
|
|
||||||
const baseSystemPrompt = conversation.systemPrompt || settings.systemPrompt || DEFAULT_SYSTEM_PROMPT;
|
|
||||||
|
|
||||||
// 替换日期占位符为当前日期
|
|
||||||
const currentDate = new Date().toLocaleDateString('zh-CN', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
weekday: 'long',
|
|
||||||
});
|
|
||||||
const systemPrompt = baseSystemPrompt.replace('{{CURRENT_DATE}}', currentDate);
|
|
||||||
|
|
||||||
// 获取温度参数(优先级:对话级别 > 全局设置 > 默认 0.7)
|
|
||||||
const temperature = parseFloat(conversation.temperature || settings.temperature || '0.7');
|
|
||||||
|
|
||||||
// 工具执行循环
|
|
||||||
let currentMessages = [...messageHistory];
|
let currentMessages = [...messageHistory];
|
||||||
let fullContent = '';
|
let fullContent = '';
|
||||||
let thinkingContent = '';
|
let thinkingContent = '';
|
||||||
let totalInputTokens = 0;
|
let totalInputTokens = 0;
|
||||||
let totalOutputTokens = 0;
|
let totalOutputTokens = 0;
|
||||||
let loopCount = 0;
|
let loopCount = 0;
|
||||||
const maxLoops = 10; // 防止无限循环
|
const maxLoops = 10;
|
||||||
let hasToolResults = false; // 标记是否有工具结果
|
let hasToolResults = false;
|
||||||
|
|
||||||
while (loopCount < maxLoops) {
|
while (loopCount < maxLoops) {
|
||||||
loopCount++;
|
loopCount++;
|
||||||
|
|
||||||
// 构建请求体
|
// 构建请求体
|
||||||
const requestBody: Record<string, unknown> = {
|
const requestBody: Record<string, unknown> = {
|
||||||
model: useModel,
|
model,
|
||||||
max_tokens: 8192,
|
max_tokens: 8192,
|
||||||
stream: true,
|
stream: true,
|
||||||
system: systemPrompt,
|
system: systemPrompt,
|
||||||
messages: currentMessages,
|
messages: currentMessages,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加工具
|
|
||||||
if (toolDefinitions.length > 0) {
|
if (toolDefinitions.length > 0) {
|
||||||
requestBody.tools = toolDefinitions;
|
requestBody.tools = toolDefinitions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加思考模式
|
// 添加思考模式(Codex 不支持)
|
||||||
// 重要:thinking 模式和工具调用的 tool_result 不兼容
|
if (enableThinking && !hasToolResults) {
|
||||||
// 当消息中包含 tool_result 时,不能启用 thinking 模式
|
|
||||||
if (useThinking && !hasToolResults) {
|
|
||||||
requestBody.thinking = {
|
requestBody.thinking = {
|
||||||
type: 'enabled',
|
type: 'enabled',
|
||||||
budget_tokens: 4096,
|
budget_tokens: 4096,
|
||||||
@ -234,7 +675,7 @@ export async function POST(request: Request) {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'x-api-key': settings.cchApiKey!,
|
'x-api-key': apiKey,
|
||||||
'anthropic-version': '2023-06-01',
|
'anthropic-version': '2023-06-01',
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestBody),
|
body: JSON.stringify(requestBody),
|
||||||
@ -356,7 +797,6 @@ export async function POST(request: Request) {
|
|||||||
// 构建助手消息的内容块
|
// 构建助手消息的内容块
|
||||||
const assistantContent: ContentBlock[] = [];
|
const assistantContent: ContentBlock[] = [];
|
||||||
|
|
||||||
// 如果有 thinking 内容,需要先添加(Claude API 要求)
|
|
||||||
if (currentThinkingContent) {
|
if (currentThinkingContent) {
|
||||||
assistantContent.push({
|
assistantContent.push({
|
||||||
type: 'thinking',
|
type: 'thinking',
|
||||||
@ -371,7 +811,6 @@ export async function POST(request: Request) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加工具调用
|
|
||||||
for (const tc of toolCalls) {
|
for (const tc of toolCalls) {
|
||||||
assistantContent.push({
|
assistantContent.push({
|
||||||
type: 'tool_use',
|
type: 'tool_use',
|
||||||
@ -381,7 +820,6 @@ export async function POST(request: Request) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将助手消息添加到历史
|
|
||||||
currentMessages.push({
|
currentMessages.push({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: assistantContent,
|
content: assistantContent,
|
||||||
@ -391,19 +829,15 @@ export async function POST(request: Request) {
|
|||||||
const toolResults: ContentBlock[] = [];
|
const toolResults: ContentBlock[] = [];
|
||||||
|
|
||||||
for (const tc of toolCalls) {
|
for (const tc of toolCalls) {
|
||||||
// 发送工具执行开始事件
|
|
||||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
type: 'tool_execution_start',
|
type: 'tool_execution_start',
|
||||||
id: tc.id,
|
id: tc.id,
|
||||||
name: tc.name,
|
name: tc.name,
|
||||||
})}\n\n`));
|
})}\n\n`));
|
||||||
|
|
||||||
// 执行工具
|
|
||||||
const result = await executeTool(tc.name, tc.input);
|
const result = await executeTool(tc.name, tc.input);
|
||||||
|
|
||||||
// 检查是否需要浏览器端 Pyodide 执行
|
|
||||||
if (result.requiresPyodide) {
|
if (result.requiresPyodide) {
|
||||||
// 发送 Pyodide 执行请求事件
|
|
||||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
type: 'pyodide_execution_required',
|
type: 'pyodide_execution_required',
|
||||||
id: tc.id,
|
id: tc.id,
|
||||||
@ -412,7 +846,6 @@ export async function POST(request: Request) {
|
|||||||
language: result.language,
|
language: result.language,
|
||||||
})}\n\n`));
|
})}\n\n`));
|
||||||
|
|
||||||
// 将占位工具结果发送给 AI(稍后会被前端执行结果替换)
|
|
||||||
toolResults.push({
|
toolResults.push({
|
||||||
type: 'tool_result',
|
type: 'tool_result',
|
||||||
tool_use_id: tc.id,
|
tool_use_id: tc.id,
|
||||||
@ -421,7 +854,6 @@ export async function POST(request: Request) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送工具执行结果事件(使用简短版本)
|
|
||||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
type: 'tool_execution_result',
|
type: 'tool_execution_result',
|
||||||
id: tc.id,
|
id: tc.id,
|
||||||
@ -431,7 +863,6 @@ export async function POST(request: Request) {
|
|||||||
images: result.images,
|
images: result.images,
|
||||||
})}\n\n`));
|
})}\n\n`));
|
||||||
|
|
||||||
// 将简短的工具结果显示给用户
|
|
||||||
const toolDisplayText = `\n\n${result.displayResult}\n\n`;
|
const toolDisplayText = `\n\n${result.displayResult}\n\n`;
|
||||||
fullContent += toolDisplayText;
|
fullContent += toolDisplayText;
|
||||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
@ -439,7 +870,6 @@ export async function POST(request: Request) {
|
|||||||
content: toolDisplayText,
|
content: toolDisplayText,
|
||||||
})}\n\n`));
|
})}\n\n`));
|
||||||
|
|
||||||
// 将完整的工具结果发送给 AI
|
|
||||||
toolResults.push({
|
toolResults.push({
|
||||||
type: 'tool_result',
|
type: 'tool_result',
|
||||||
tool_use_id: tc.id,
|
tool_use_id: tc.id,
|
||||||
@ -447,88 +877,28 @@ export async function POST(request: Request) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将工具结果添加到消息历史
|
|
||||||
currentMessages.push({
|
currentMessages.push({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: toolResults,
|
content: toolResults,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 标记已有工具结果,下一轮请求需要禁用 thinking 模式
|
|
||||||
hasToolResults = true;
|
hasToolResults = true;
|
||||||
|
|
||||||
// 继续循环,让 AI 基于工具结果继续回复
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有工具调用或停止原因不是 tool_use,则结束循环
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存 AI 回复到数据库
|
return {
|
||||||
await db.insert(messages).values({
|
fullContent,
|
||||||
messageId: assistantMessageId,
|
thinkingContent,
|
||||||
conversationId,
|
|
||||||
role: 'assistant',
|
|
||||||
content: fullContent,
|
|
||||||
thinkingContent: thinkingContent || null,
|
|
||||||
inputTokens: totalInputTokens,
|
inputTokens: totalInputTokens,
|
||||||
outputTokens: totalOutputTokens,
|
outputTokens: totalOutputTokens,
|
||||||
status: 'completed',
|
};
|
||||||
});
|
|
||||||
|
|
||||||
// 更新对话信息
|
|
||||||
await db
|
|
||||||
.update(conversations)
|
|
||||||
.set({
|
|
||||||
messageCount: (conversation.messageCount || 0) + 2,
|
|
||||||
totalTokens: (conversation.totalTokens || 0) + totalInputTokens + totalOutputTokens,
|
|
||||||
lastMessageAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
title: (conversation.messageCount || 0) === 0
|
|
||||||
? message.slice(0, 50) + (message.length > 50 ? '...' : '')
|
|
||||||
: conversation.title,
|
|
||||||
})
|
|
||||||
.where(eq(conversations.conversationId, conversationId));
|
|
||||||
|
|
||||||
// 发送完成事件
|
|
||||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
|
||||||
type: 'done',
|
|
||||||
messageId: assistantMessageId,
|
|
||||||
inputTokens: totalInputTokens,
|
|
||||||
outputTokens: totalOutputTokens,
|
|
||||||
})}\n\n`));
|
|
||||||
|
|
||||||
controller.close();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Stream error:', error);
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
|
||||||
type: 'error',
|
|
||||||
error: errorMessage,
|
|
||||||
})}\n\n`));
|
|
||||||
controller.close();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Response(stream, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'text/event-stream',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Chat API error:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to process chat request' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建工具定义
|
// 构建 Claude 工具定义
|
||||||
function buildToolDefinitions(toolIds: string[]) {
|
function buildClaudeToolDefinitions(toolIds: string[]) {
|
||||||
const toolMap: Record<string, object> = {
|
const toolMap: Record<string, object> = {
|
||||||
web_search: {
|
web_search: {
|
||||||
name: 'web_search',
|
name: 'web_search',
|
||||||
@ -582,3 +952,127 @@ function buildToolDefinitions(toolIds: string[]) {
|
|||||||
.filter((id) => toolMap[id])
|
.filter((id) => toolMap[id])
|
||||||
.map((id) => toolMap[id]);
|
.map((id) => toolMap[id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 构建 OpenAI 工具定义
|
||||||
|
function buildOpenAIToolDefinitions(toolIds: string[]) {
|
||||||
|
const toolMap: Record<string, object> = {
|
||||||
|
web_search: {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'web_search',
|
||||||
|
description: '搜索互联网获取最新信息。当用户询问时事、新闻、天气、实时数据等需要最新信息的问题时,请使用此工具。',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
query: {
|
||||||
|
type: 'string',
|
||||||
|
description: '搜索查询关键词',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['query'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
code_execution: {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'code_execution',
|
||||||
|
description: '执行代码并返回结果。支持 Python、JavaScript、TypeScript、Java、C、C++、Go、Rust 等多种语言。当需要验证代码、进行计算或演示代码运行结果时,请使用此工具。',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
code: {
|
||||||
|
type: 'string',
|
||||||
|
description: '要执行的代码',
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
type: 'string',
|
||||||
|
description: '编程语言 (python, javascript, typescript, java, c, cpp, go, rust, ruby, php 等)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['code', 'language'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
web_fetch: {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'web_fetch',
|
||||||
|
description: '获取指定 URL 的网页内容。当用户提供了具体的网址并想了解该页面的内容时,请使用此工具。',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
description: '要获取内容的完整 URL',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['url'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return toolIds
|
||||||
|
.filter((id) => toolMap[id])
|
||||||
|
.map((id) => toolMap[id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建 Codex Response API 工具定义
|
||||||
|
function buildCodexToolDefinitions(toolIds: string[]) {
|
||||||
|
const toolMap: Record<string, object> = {
|
||||||
|
web_search: {
|
||||||
|
type: 'function',
|
||||||
|
name: 'web_search',
|
||||||
|
description: '搜索互联网获取最新信息。当用户询问时事、新闻、天气、实时数据等需要最新信息的问题时,请使用此工具。',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
query: {
|
||||||
|
type: 'string',
|
||||||
|
description: '搜索查询关键词',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['query'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
code_execution: {
|
||||||
|
type: 'function',
|
||||||
|
name: 'code_execution',
|
||||||
|
description: '执行代码并返回结果。支持 Python、JavaScript、TypeScript、Java、C、C++、Go、Rust 等多种语言。当需要验证代码、进行计算或演示代码运行结果时,请使用此工具。',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
code: {
|
||||||
|
type: 'string',
|
||||||
|
description: '要执行的代码',
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
type: 'string',
|
||||||
|
description: '编程语言 (python, javascript, typescript, java, c, cpp, go, rust, ruby, php 等)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['code', 'language'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
web_fetch: {
|
||||||
|
type: 'function',
|
||||||
|
name: 'web_fetch',
|
||||||
|
description: '获取指定 URL 的网页内容。当用户提供了具体的网址并想了解该页面的内容时,请使用此工具。',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
description: '要获取内容的完整 URL',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['url'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return toolIds
|
||||||
|
.filter((id) => toolMap[id])
|
||||||
|
.map((id) => toolMap[id]);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user