From da19858c2d50ddc71975c79cdc57b9a2c1dbf5ca Mon Sep 17 00:00:00 2001 From: gaoziman <2942894660@qq.com> Date: Sat, 20 Dec 2025 01:04:28 +0800 Subject: [PATCH] =?UTF-8?q?refactor(API):=20=E9=87=8D=E6=9E=84=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E6=8E=A5=E5=8F=A3=E6=94=AF=E6=8C=81=E5=A4=9A=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 拆分 Claude 和 Codex 模型的处理逻辑为独立函数 - 新增 handleClaudeChat 函数处理 Claude 系列模型 - 新增 handleCodexChat 函数处理 Codex 系列模型(OpenAI Response API 格式) - 添加 isCodexModel 工具函数用于模型类型判断 - 优化代码结构,提高可维护性 --- src/app/api/chat/route.ts | 1070 +++++++++++++++++++++++++++---------- 1 file changed, 782 insertions(+), 288 deletions(-) diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 001714d..5634bf2 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -13,7 +13,7 @@ interface ChatRequest { enableThinking?: boolean; } -// 消息内容块类型 +// 消息内容块类型(Claude) interface ContentBlock { type: 'text' | 'tool_use' | 'tool_result' | 'thinking'; text?: string; @@ -25,12 +25,35 @@ interface ContentBlock { content?: string; } -// API 消息类型 +// API 消息类型(Claude) interface APIMessage { role: 'user' | 'assistant'; 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 助手。请遵循以下规则来回复用户: @@ -148,38 +171,22 @@ export async function POST(request: Request) { status: 'completed', }); - // 构建消息历史 - const messageHistory: APIMessage[] = historyMessages.map((msg) => ({ - role: msg.role as 'user' | 'assistant', - content: msg.content, - })); - - // 添加当前用户消息 - messageHistory.push({ - role: 'user', - content: message, - }); - // 准备 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 { - // 调用 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', @@ -188,280 +195,52 @@ export async function POST(request: Request) { }); const systemPrompt = baseSystemPrompt.replace('{{CURRENT_DATE}}', currentDate); - // 获取温度参数(优先级:对话级别 > 全局设置 > 默认 0.7) + // 获取温度参数 const temperature = parseFloat(conversation.temperature || settings.temperature || '0.7'); - // 工具执行循环 - let currentMessages = [...messageHistory]; let fullContent = ''; let thinkingContent = ''; let totalInputTokens = 0; let totalOutputTokens = 0; - let loopCount = 0; - const maxLoops = 10; // 防止无限循环 - let hasToolResults = false; // 标记是否有工具结果 - while (loopCount < maxLoops) { - loopCount++; - - // 构建请求体 - const requestBody: Record = { + if (isCodex) { + // ==================== Codex 模型处理(OpenAI 格式) ==================== + const result = await handleCodexChat({ + cchUrl, + apiKey: settings.cchApiKey!, model: useModel, - max_tokens: 8192, - stream: true, - system: systemPrompt, - messages: currentMessages, - }; - - // 添加工具 - if (toolDefinitions.length > 0) { - requestBody.tools = toolDefinitions; - } - - // 添加思考模式 - // 重要:thinking 模式和工具调用的 tool_result 不兼容 - // 当消息中包含 tool_result 时,不能启用 thinking 模式 - if (useThinking && !hasToolResults) { - requestBody.thinking = { - type: 'enabled', - budget_tokens: 4096, - }; - } else { - requestBody.temperature = temperature; - } - - const response = await fetch(`${cchUrl}/v1/messages`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': settings.cchApiKey!, - 'anthropic-version': '2023-06-01', - }, - body: JSON.stringify(requestBody), + systemPrompt, + temperature, + historyMessages, + message, + tools: tools || (conversation.tools as string[]) || [], + controller, + encoder, }); - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`CCH API error: ${response.status} - ${errorText}`); - } + 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, + }); - const reader = response.body?.getReader(); - if (!reader) { - throw new Error('No response body'); - } - - // 收集当前轮次的内容 - let currentTextContent = ''; - let currentThinkingContent = ''; - const toolCalls: { id: string; name: string; input: Record }[] = []; - let currentToolUse: { id: string; name: string; inputJson: string } | null = null; - let stopReason: string | null = null; - - const decoder = new TextDecoder(); - let buffer = ''; - - // 处理流式响应 - 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) { - if (line.startsWith('data: ')) { - const data = line.slice(6); - if (data === '[DONE]') continue; - - try { - const event = JSON.parse(data); - - if (event.type === 'content_block_delta') { - const delta = event.delta; - - if (delta.type === 'thinking_delta') { - currentThinkingContent += delta.thinking || ''; - thinkingContent += delta.thinking || ''; - controller.enqueue(encoder.encode(`data: ${JSON.stringify({ - type: 'thinking', - content: delta.thinking || '', - })}\n\n`)); - } else if (delta.type === 'text_delta') { - currentTextContent += delta.text || ''; - fullContent += delta.text || ''; - controller.enqueue(encoder.encode(`data: ${JSON.stringify({ - type: 'text', - content: delta.text || '', - })}\n\n`)); - } else if (delta.type === 'input_json_delta') { - if (currentToolUse) { - currentToolUse.inputJson += delta.partial_json || ''; - } - } - } else if (event.type === 'message_delta') { - if (event.usage) { - totalOutputTokens += event.usage.output_tokens || 0; - } - if (event.delta?.stop_reason) { - stopReason = event.delta.stop_reason; - } - } else if (event.type === 'message_start') { - if (event.message?.usage) { - totalInputTokens += event.message.usage.input_tokens || 0; - } - } else if (event.type === 'content_block_start') { - if (event.content_block?.type === 'tool_use') { - currentToolUse = { - id: event.content_block.id, - name: event.content_block.name, - inputJson: '', - }; - controller.enqueue(encoder.encode(`data: ${JSON.stringify({ - type: 'tool_use_start', - id: event.content_block.id, - name: event.content_block.name, - })}\n\n`)); - } - } else if (event.type === 'content_block_stop') { - if (currentToolUse) { - try { - const toolInput = JSON.parse(currentToolUse.inputJson || '{}'); - toolCalls.push({ - id: currentToolUse.id, - name: currentToolUse.name, - input: toolInput, - }); - controller.enqueue(encoder.encode(`data: ${JSON.stringify({ - type: 'tool_use_complete', - id: currentToolUse.id, - name: currentToolUse.name, - input: toolInput, - })}\n\n`)); - } catch (e) { - console.error('Failed to parse tool input:', e); - } - currentToolUse = null; - } - } - } catch (e) { - console.error('Parse error:', e); - } - } - } - } - - // 检查是否需要执行工具 - if (stopReason === 'tool_use' && toolCalls.length > 0) { - // 构建助手消息的内容块 - const assistantContent: ContentBlock[] = []; - - // 如果有 thinking 内容,需要先添加(Claude API 要求) - if (currentThinkingContent) { - assistantContent.push({ - type: 'thinking', - thinking: currentThinkingContent, - }); - } - - if (currentTextContent) { - assistantContent.push({ - type: 'text', - text: currentTextContent, - }); - } - - // 添加工具调用 - for (const tc of toolCalls) { - assistantContent.push({ - type: 'tool_use', - id: tc.id, - name: tc.name, - input: tc.input, - }); - } - - // 将助手消息添加到历史 - currentMessages.push({ - role: 'assistant', - content: assistantContent, - }); - - // 执行所有工具并收集结果 - const toolResults: ContentBlock[] = []; - - for (const tc of toolCalls) { - // 发送工具执行开始事件 - controller.enqueue(encoder.encode(`data: ${JSON.stringify({ - type: 'tool_execution_start', - id: tc.id, - name: tc.name, - })}\n\n`)); - - // 执行工具 - const result = await executeTool(tc.name, tc.input); - - // 检查是否需要浏览器端 Pyodide 执行 - if (result.requiresPyodide) { - // 发送 Pyodide 执行请求事件 - controller.enqueue(encoder.encode(`data: ${JSON.stringify({ - type: 'pyodide_execution_required', - id: tc.id, - name: tc.name, - code: result.code, - language: result.language, - })}\n\n`)); - - // 将占位工具结果发送给 AI(稍后会被前端执行结果替换) - toolResults.push({ - type: 'tool_result', - tool_use_id: tc.id, - content: '代码正在浏览器端执行中,请稍候...', - }); - continue; - } - - // 发送工具执行结果事件(使用简短版本) - controller.enqueue(encoder.encode(`data: ${JSON.stringify({ - type: 'tool_execution_result', - id: tc.id, - name: tc.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`)); - - // 将完整的工具结果发送给 AI - toolResults.push({ - type: 'tool_result', - tool_use_id: tc.id, - content: result.fullResult, - }); - } - - // 将工具结果添加到消息历史 - currentMessages.push({ - role: 'user', - content: toolResults, - }); - - // 标记已有工具结果,下一轮请求需要禁用 thinking 模式 - hasToolResults = true; - - // 继续循环,让 AI 基于工具结果继续回复 - continue; - } - - // 如果没有工具调用或停止原因不是 tool_use,则结束循环 - break; + fullContent = result.fullContent; + thinkingContent = result.thinkingContent; + totalInputTokens = result.inputTokens; + totalOutputTokens = result.outputTokens; } // 保存 AI 回复到数据库 @@ -527,8 +306,599 @@ export async function POST(request: Request) { } } -// 构建工具定义 -function buildToolDefinitions(toolIds: string[]) { +// ==================== 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 = { + 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 = {}; + 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) => ({ + role: msg.role as 'user' | 'assistant', + content: msg.content, + })); + + // 添加当前用户消息 + messageHistory.push({ + role: 'user', + content: message, + }); + + // 构建工具定义 + const toolDefinitions = buildClaudeToolDefinitions(tools); + + let currentMessages = [...messageHistory]; + let fullContent = ''; + let thinkingContent = ''; + let totalInputTokens = 0; + let totalOutputTokens = 0; + let loopCount = 0; + const maxLoops = 10; + let hasToolResults = false; + + while (loopCount < maxLoops) { + loopCount++; + + // 构建请求体 + const requestBody: Record = { + model, + max_tokens: 8192, + stream: true, + system: systemPrompt, + messages: currentMessages, + }; + + if (toolDefinitions.length > 0) { + requestBody.tools = toolDefinitions; + } + + // 添加思考模式(Codex 不支持) + if (enableThinking && !hasToolResults) { + requestBody.thinking = { + type: 'enabled', + budget_tokens: 4096, + }; + } else { + requestBody.temperature = temperature; + } + + const response = await fetch(`${cchUrl}/v1/messages`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01', + }, + 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 = ''; + let currentThinkingContent = ''; + const toolCalls: { id: string; name: string; input: Record }[] = []; + let currentToolUse: { id: string; name: string; inputJson: string } | null = null; + let stopReason: string | null = null; + + const decoder = new TextDecoder(); + let buffer = ''; + + // 处理流式响应 + 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) { + if (line.startsWith('data: ')) { + const data = line.slice(6); + if (data === '[DONE]') continue; + + try { + const event = JSON.parse(data); + + if (event.type === 'content_block_delta') { + const delta = event.delta; + + if (delta.type === 'thinking_delta') { + currentThinkingContent += delta.thinking || ''; + thinkingContent += delta.thinking || ''; + controller.enqueue(encoder.encode(`data: ${JSON.stringify({ + type: 'thinking', + content: delta.thinking || '', + })}\n\n`)); + } else if (delta.type === 'text_delta') { + currentTextContent += delta.text || ''; + fullContent += delta.text || ''; + controller.enqueue(encoder.encode(`data: ${JSON.stringify({ + type: 'text', + content: delta.text || '', + })}\n\n`)); + } else if (delta.type === 'input_json_delta') { + if (currentToolUse) { + currentToolUse.inputJson += delta.partial_json || ''; + } + } + } else if (event.type === 'message_delta') { + if (event.usage) { + totalOutputTokens += event.usage.output_tokens || 0; + } + if (event.delta?.stop_reason) { + stopReason = event.delta.stop_reason; + } + } else if (event.type === 'message_start') { + if (event.message?.usage) { + totalInputTokens += event.message.usage.input_tokens || 0; + } + } else if (event.type === 'content_block_start') { + if (event.content_block?.type === 'tool_use') { + currentToolUse = { + id: event.content_block.id, + name: event.content_block.name, + inputJson: '', + }; + controller.enqueue(encoder.encode(`data: ${JSON.stringify({ + type: 'tool_use_start', + id: event.content_block.id, + name: event.content_block.name, + })}\n\n`)); + } + } else if (event.type === 'content_block_stop') { + if (currentToolUse) { + try { + const toolInput = JSON.parse(currentToolUse.inputJson || '{}'); + toolCalls.push({ + id: currentToolUse.id, + name: currentToolUse.name, + input: toolInput, + }); + controller.enqueue(encoder.encode(`data: ${JSON.stringify({ + type: 'tool_use_complete', + id: currentToolUse.id, + name: currentToolUse.name, + input: toolInput, + })}\n\n`)); + } catch (e) { + console.error('Failed to parse tool input:', e); + } + currentToolUse = null; + } + } + } catch (e) { + console.error('Parse error:', e); + } + } + } + } + + // 检查是否需要执行工具 + if (stopReason === 'tool_use' && toolCalls.length > 0) { + // 构建助手消息的内容块 + const assistantContent: ContentBlock[] = []; + + if (currentThinkingContent) { + assistantContent.push({ + type: 'thinking', + thinking: currentThinkingContent, + }); + } + + if (currentTextContent) { + assistantContent.push({ + type: 'text', + text: currentTextContent, + }); + } + + for (const tc of toolCalls) { + assistantContent.push({ + type: 'tool_use', + id: tc.id, + name: tc.name, + input: tc.input, + }); + } + + currentMessages.push({ + role: 'assistant', + content: assistantContent, + }); + + // 执行所有工具并收集结果 + const toolResults: ContentBlock[] = []; + + for (const tc of toolCalls) { + controller.enqueue(encoder.encode(`data: ${JSON.stringify({ + type: 'tool_execution_start', + id: tc.id, + name: tc.name, + })}\n\n`)); + + const result = await executeTool(tc.name, tc.input); + + if (result.requiresPyodide) { + controller.enqueue(encoder.encode(`data: ${JSON.stringify({ + type: 'pyodide_execution_required', + id: tc.id, + name: tc.name, + code: result.code, + language: result.language, + })}\n\n`)); + + toolResults.push({ + type: 'tool_result', + tool_use_id: tc.id, + content: '代码正在浏览器端执行中,请稍候...', + }); + continue; + } + + controller.enqueue(encoder.encode(`data: ${JSON.stringify({ + type: 'tool_execution_result', + id: tc.id, + name: tc.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`)); + + toolResults.push({ + type: 'tool_result', + tool_use_id: tc.id, + content: result.fullResult, + }); + } + + currentMessages.push({ + role: 'user', + content: toolResults, + }); + + hasToolResults = true; + continue; + } + + break; + } + + return { + fullContent, + thinkingContent, + inputTokens: totalInputTokens, + outputTokens: totalOutputTokens, + }; +} + +// 构建 Claude 工具定义 +function buildClaudeToolDefinitions(toolIds: string[]) { const toolMap: Record = { web_search: { name: 'web_search', @@ -582,3 +952,127 @@ function buildToolDefinitions(toolIds: string[]) { .filter((id) => toolMap[id]) .map((id) => toolMap[id]); } + +// 构建 OpenAI 工具定义 +function buildOpenAIToolDefinitions(toolIds: string[]) { + const toolMap: Record = { + 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 = { + 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]); +}