diff --git a/components/OpenAISettings.tsx b/components/OpenAISettings.tsx new file mode 100644 index 0000000..59d94ac --- /dev/null +++ b/components/OpenAISettings.tsx @@ -0,0 +1,149 @@ +import { useState, useEffect } from "react"; +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, + Button, + FormControl, + FormLabel, + Input, + VStack, + useToast, + Text, + Box, + Divider, +} from "@chakra-ui/react"; + +interface OpenAISettingsProps { + isOpen: boolean; + onClose: () => void; +} + +const OpenAISettings = ({ isOpen, onClose }: OpenAISettingsProps) => { + const [apiKey, setApiKey] = useState(""); + const [model, setModel] = useState("gpt-4o-mini"); + const [endpoint, setEndpoint] = useState("https://api.openai.com/v1/chat/completions"); + const toast = useToast(); + + // 加载保存的设置 + useEffect(() => { + if (isOpen) { + const savedApiKey = localStorage.getItem("openai_api_key") || ""; + const savedModel = localStorage.getItem("openai_model") || "gpt-4o-mini"; + const savedEndpoint = localStorage.getItem("openai_endpoint") || "https://api.openai.com/v1/chat/completions"; + + setApiKey(savedApiKey); + setModel(savedModel); + setEndpoint(savedEndpoint); + } + }, [isOpen]); + + const handleSave = () => { + // 保存到localStorage + localStorage.setItem("openai_api_key", apiKey); + localStorage.setItem("openai_model", model); + localStorage.setItem("openai_endpoint", endpoint); + + // 更新环境变量(通过前端无法直接修改环境变量,这里只是模拟) + // 实际上这些值会在API请求时从localStorage中读取 + window.sessionStorage.setItem("OPENAI_API_KEY", apiKey); + window.sessionStorage.setItem("OPENAI_MODEL", model); + window.sessionStorage.setItem("OPENAI_ENDPOINT", endpoint); + + toast({ + title: "设置已保存", + description: "OpenAI设置已成功保存。", + status: "success", + duration: 3000, + isClosable: true, + position: "top", + }); + + onClose(); + }; + + return ( + + + + OpenAI设置 + + + + + 配置OpenAI翻译服务的API密钥、模型和端点。这些设置将保存在浏览器中,不会上传到服务器。 + + + + + + API密钥 + setApiKey(e.target.value)} + bg="gray.700" + borderColor="gray.600" + /> + + + + 模型 + setModel(e.target.value)} + bg="gray.700" + borderColor="gray.600" + /> + + + + API端点 + setEndpoint(e.target.value)} + bg="gray.700" + borderColor="gray.600" + /> + + + + + 常用模型: + + + - gpt-4o-mini (OpenAI) + + + - gpt-3.5-turbo (OpenAI) + + + - gpt-4o (OpenAI) + + + - claude-3-opus-20240229 (Anthropic) + + + + + + + + + + + ); +}; + +export default OpenAISettings; \ No newline at end of file diff --git a/components/TranslationInput.tsx b/components/TranslationInput.tsx index 0768fba..dd47d4a 100644 --- a/components/TranslationInput.tsx +++ b/components/TranslationInput.tsx @@ -1,6 +1,6 @@ "use client"; -import { Box, VStack, Textarea, Select, Button, Spinner, HStack, Grid, GridItem, Collapse, Flex, useToast, Text, Accordion, AccordionItem, AccordionButton, AccordionPanel, AccordionIcon } from "@chakra-ui/react"; +import { Box, VStack, Textarea, Select, Button, Spinner, HStack, Grid, GridItem, Collapse, Flex, useToast, Text, Accordion, AccordionItem, AccordionButton, AccordionPanel, AccordionIcon, Switch, FormControl, FormLabel } from "@chakra-ui/react"; import { useState, useEffect } from "react"; import axios from "axios"; import { useTranslationContext } from "../contexts/TranslationContext"; @@ -14,10 +14,20 @@ const TranslationInput = () => { const [sourceLang, setSourceLang] = useState("auto"); const [targetLang, setTargetLang] = useState("en"); const { services, customAPIs, addHistory } = useTranslationContext(); - const [translations, setTranslations] = useState<{ service: string; text: string; name: string }[]>([]); + const [translations, setTranslations] = useState<{ service: string; text: string; name: string; streaming?: boolean }[]>([]); const [loading, setLoading] = useState(false); + const [useStreaming, setUseStreaming] = useState(true); const toast = useToast(); + // 获取OpenAI配置 + const getOpenAIConfig = () => { + return { + apiKey: localStorage.getItem("openai_api_key") || "", + model: localStorage.getItem("openai_model") || "gpt-4o-mini", + endpoint: localStorage.getItem("openai_endpoint") || "https://api.openai.com/v1/chat/completions" + }; + }; + const handleTranslate = async () => { if (services.length === 0) { toast({ @@ -34,58 +44,173 @@ const TranslationInput = () => { setLoading(true); setTranslations([]); - try { - const promises = services.map(service => - axios.post("/api/translate", { - text, - sourceLang: sourceLang === "auto" ? "" : sourceLang, - targetLang, - service, - customAPIs, - }) - ); - - const results = await Promise.all(promises); - const newTranslations = results.map((res, index) => ({ - service: services[index], - text: res.data.translatedText, - name: getServiceName(services[index]), - })); - setTranslations(newTranslations); - - // 添加到历史记录 - newTranslations.forEach(t => { - addHistory({ - id: uuidv4(), - inputText: text, - sourceLang, - targetLang, - service: t.service, - translatedText: t.text, - timestamp: new Date().toISOString(), + // 初始化每个服务的翻译结果 + const initialTranslations = services.map(service => ({ + service, + text: useStreaming ? "" : "翻译中...", + name: getServiceName(service), + streaming: useStreaming + })); + setTranslations(initialTranslations); + + // 获取OpenAI配置 + const openAIConfig = getOpenAIConfig(); + + if (useStreaming) { + // 使用流式输出 + services.forEach((service, index) => { + const eventSource = new EventSource(`/api/translate-stream?dummy=${Date.now()}`); + + // 发送初始请求数据 + fetch('/api/translate-stream', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + text, + sourceLang: sourceLang === "auto" ? "" : sourceLang, + targetLang, + service, + customAPIs, + openAIConfig, + }), + }).catch(error => { + console.error('流式请求发送失败:', error); + setTranslations(prev => + prev.map((t, i) => + i === index ? { ...t, text: `翻译失败: ${error.message}` } : t + ) + ); + eventSource.close(); }); - }); - toast({ - title: "翻译成功", - description: "您的文本已成功翻译。", - status: "success", - duration: 3000, - isClosable: true, - position: "top", + let fullText = ""; + + eventSource.onmessage = (event) => { + if (event.data === "[DONE]") { + eventSource.close(); + + // 添加到历史记录 + addHistory({ + id: uuidv4(), + inputText: text, + sourceLang, + targetLang, + service, + translatedText: fullText, + timestamp: new Date().toISOString(), + }); + + if (index === services.length - 1) { + setLoading(false); + toast({ + title: "翻译成功", + description: "您的文本已成功翻译。", + status: "success", + duration: 3000, + isClosable: true, + position: "top", + }); + } + return; + } + + try { + const data = JSON.parse(event.data); + if (data.error) { + setTranslations(prev => + prev.map((t, i) => + i === index ? { ...t, text: `翻译失败: ${data.error}` } : t + ) + ); + eventSource.close(); + return; + } + + if (data.text) { + fullText += data.text; + setTranslations(prev => + prev.map((t, i) => + i === index ? { ...t, text: fullText } : t + ) + ); + } + } catch (error) { + console.error('解析流数据失败:', error); + } + }; + + eventSource.onerror = (error) => { + console.error('流式请求错误:', error); + setTranslations(prev => + prev.map((t, i) => + i === index ? { ...t, text: `翻译失败: 连接错误` } : t + ) + ); + eventSource.close(); + + if (index === services.length - 1) { + setLoading(false); + } + }; }); - } catch (error: any) { - console.error(error); - toast({ - title: "翻译失败", - description: error.response?.data?.error || "翻译失败,请稍后重试。", - status: "error", - duration: 3000, - isClosable: true, - position: "top", - }); - } finally { - setLoading(false); + } else { + // 使用普通请求 + try { + const promises = services.map(service => + axios.post("/api/translate", { + text, + sourceLang: sourceLang === "auto" ? "" : sourceLang, + targetLang, + service, + customAPIs, + openAIConfig, + }) + ); + + const results = await Promise.all(promises); + const newTranslations = results.map((res, index) => ({ + service: services[index], + text: res.data.translatedText, + name: getServiceName(services[index]), + })); + setTranslations(newTranslations); + + // 添加到历史记录 + newTranslations.forEach(t => { + addHistory({ + id: uuidv4(), + inputText: text, + sourceLang, + targetLang, + service: t.service, + translatedText: t.text, + timestamp: new Date().toISOString(), + }); + }); + + toast({ + title: "翻译成功", + description: "您的文本已成功翻译。", + status: "success", + duration: 3000, + isClosable: true, + position: "top", + }); + } catch (error: any) { + console.error(error); + toast({ + title: "翻译失败", + description: error.response?.data?.error || "翻译失败,请稍后重试。", + status: "error", + duration: 3000, + isClosable: true, + position: "top", + }); + } finally { + setLoading(false); + } } }; @@ -180,6 +305,20 @@ const TranslationInput = () => { p={4} /> + + + + 流式输出 + + setUseStreaming(e.target.checked)} + colorScheme="brand" + /> + + + + + {/* OpenAI设置模态框 */} + ); }; diff --git a/next.config.mjs b/next.config.mjs index 61cd5ed..2371a33 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,40 @@ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: false, + async rewrites() { + return [ + { + source: '/api/:path*', + destination: '/api/:path*', + }, + ]; + }, + // 允许在iframe中使用和跨域请求 + headers: async () => { + return [ + { + source: '/:path*', + headers: [ + { + key: 'X-Frame-Options', + value: 'ALLOWALL', + }, + { + key: 'Access-Control-Allow-Origin', + value: '*', + }, + { + key: 'Access-Control-Allow-Methods', + value: 'GET, POST, PUT, DELETE, OPTIONS', + }, + { + key: 'Access-Control-Allow-Headers', + value: 'X-Requested-With, Content-Type, Authorization', + }, + ], + }, + ]; + }, }; export default nextConfig; diff --git a/pages/api/translate-stream.ts b/pages/api/translate-stream.ts new file mode 100644 index 0000000..a75ff41 --- /dev/null +++ b/pages/api/translate-stream.ts @@ -0,0 +1,580 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import axios from "axios"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "POST") { + res.status(405).json({ error: "Method not allowed" }); + return; + } + + const { text, sourceLang, targetLang, service, customAPIs, openAIConfig } = req.body; + + if (!text || !targetLang || !service) { + res.status(400).json({ error: "Missing parameters" }); + return; + } + + // 设置响应头,支持流式输出 + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + + try { + if (service.startsWith("custom_")) { + const customAPI = customAPIs.find((api: any) => api.id === service); + if (customAPI) { + await streamWithCustomAPI(text, sourceLang, targetLang, customAPI, res); + } else { + throw new Error("未找到自定义API配置"); + } + } else if (service === "openai") { + await streamWithOpenAI(text, sourceLang, targetLang, res, openAIConfig); + } else { + // 对于不支持流式输出的服务,使用普通方式获取结果后模拟流式输出 + let translatedText = ""; + + switch (service) { + case "google": + translatedText = await translateWithGoogle(text, sourceLang, targetLang); + break; + case "tongyi": + translatedText = await translateWithTongyi(text, sourceLang, targetLang); + break; + case "deepl": + translatedText = await translateWithDeepL(text, sourceLang, targetLang); + break; + case "siliconflow": + translatedText = await translateWithSiliconFlow(text, sourceLang, targetLang); + break; + case "deepseek": + translatedText = await translateWithDeepSeek(text, sourceLang, targetLang); + break; + default: + throw new Error("未知的翻译服务"); + } + + // 模拟流式输出 + simulateStreamOutput(translatedText, res); + } + } catch (error: any) { + // 发送错误消息 + res.write(`data: ${JSON.stringify({ error: error.message || "内部服务器错误" })}\n\n`); + res.end(); + } +} + +// 模拟流式输出 +const simulateStreamOutput = (text: string, res: NextApiResponse) => { + const chunks = text.split(' '); + let index = 0; + + // 每100ms发送一个单词,模拟流式输出 + const interval = setInterval(() => { + if (index < chunks.length) { + const chunk = chunks[index]; + res.write(`data: ${JSON.stringify({ text: chunk + ' ' })}\n\n`); + index++; + } else { + clearInterval(interval); + res.write(`data: [DONE]\n\n`); + res.end(); + } + }, 100); + + // 确保请求关闭时清除定时器 + res.on('close', () => { + clearInterval(interval); + res.end(); + }); +}; + +// OpenAI流式输出 +const streamWithOpenAI = async (text: string, source: string, target: string, res: NextApiResponse, openAIConfig?: any) => { + // 从环境变量或配置中获取API密钥和设置 + let apiKey = process.env.OPENAI_API_KEY || ""; + let model = process.env.OPENAI_MODEL || "gpt-4o-mini"; + let endpoint = process.env.OPENAI_ENDPOINT || "https://api.openai.com/v1/chat/completions"; + + // 如果提供了配置,则使用配置中的值 + if (openAIConfig) { + if (openAIConfig.apiKey) apiKey = openAIConfig.apiKey; + if (openAIConfig.model) model = openAIConfig.model; + if (openAIConfig.endpoint) endpoint = openAIConfig.endpoint; + } + + if (!apiKey) { + throw new Error("OpenAI API 密钥未配置"); + } + + const data = { + model: model, + messages: [ + { + role: "system", + content: "你是一个专业的翻译助手,请准确翻译用户提供的文本,保持原文的意思、风格和格式。只返回翻译后的文本,不要添加解释或其他内容。" + }, + { + role: "user", + content: `请将以下文本从${source || "自动检测"}翻译成${target}语言:\n\n${text}`, + }, + ], + stream: true, + max_tokens: 4000, // 增加最大token数,避免长文本被截断 + temperature: 0.1, // 降低温度,使翻译更准确 + }; + + try { + const response = await axios.post(endpoint, data, { + headers: { + 'Accept': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + responseType: 'stream', + timeout: 120000 // 增加超时时间到2分钟 + }); + + response.data.on('data', (chunk: Buffer) => { + const lines = chunk.toString().split('\n').filter(line => line.trim() !== ''); + + for (const line of lines) { + if (line.includes('[DONE]')) { + res.write(`data: [DONE]\n\n`); + continue; + } + + if (line.startsWith('data: ')) { + try { + // 处理特殊情况:有些API返回的不是JSON + if (line.substring(6).trim() === '[DONE]') { + res.write(`data: [DONE]\n\n`); + continue; + } + + const data = JSON.parse(line.substring(6)); + + // 处理标准OpenAI流式响应格式 + if (data.choices && data.choices.length > 0) { + const choice = data.choices[0]; + + // 处理delta格式 + if (choice.delta && choice.delta.content) { + res.write(`data: ${JSON.stringify({ text: choice.delta.content })}\n\n`); + } + // 处理message格式 + else if (choice.message && choice.message.content) { + res.write(`data: ${JSON.stringify({ text: choice.message.content })}\n\n`); + } + // 处理text格式 + else if (choice.text) { + res.write(`data: ${JSON.stringify({ text: choice.text })}\n\n`); + } + // 处理content格式 + else if (choice.content) { + res.write(`data: ${JSON.stringify({ text: choice.content })}\n\n`); + } + } + // 处理其他可能的流式响应格式 + else if (data.text) { + res.write(`data: ${JSON.stringify({ text: data.text })}\n\n`); + } + else if (data.content) { + res.write(`data: ${JSON.stringify({ text: data.content })}\n\n`); + } + else if (data.chunk) { + res.write(`data: ${JSON.stringify({ text: data.chunk })}\n\n`); + } + else if (data.token) { + res.write(`data: ${JSON.stringify({ text: data.token })}\n\n`); + } + else if (typeof data === 'string') { + res.write(`data: ${JSON.stringify({ text: data })}\n\n`); + } + } catch (e) { + console.error('解析流数据失败:', e, '原始数据:', line.substring(6)); + // 尝试直接发送文本 + try { + res.write(`data: ${JSON.stringify({ text: line.substring(6) })}\n\n`); + } catch (err) { + console.error('发送流数据失败:', err); + } + } + } + } + }); + + response.data.on('end', () => { + // 确保发送结束标记 + res.write(`data: [DONE]\n\n`); + res.end(); + }); + + response.data.on('error', (err: Error) => { + console.error('流处理错误:', err); + res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`); + res.write(`data: [DONE]\n\n`); + res.end(); + }); + + // 设置超时处理 + setTimeout(() => { + if (!res.writableEnded) { + console.error('流处理超时'); + res.write(`data: ${JSON.stringify({ error: "请求超时,请尝试减少文本长度或使用非流式输出" })}\n\n`); + res.write(`data: [DONE]\n\n`); + res.end(); + } + }, 110000); // 设置比请求超时略短的时间 + + } catch (error: any) { + console.error('OpenAI流式请求失败:', error.response?.data || error.message); + throw new Error(`翻译失败: ${error.response?.data?.error?.message || error.message || '请求失败,请检查API配置'}`); + } +}; + +// 自定义API流式输出 +const streamWithCustomAPI = async (text: string, source: string, target: string, customAPI: any, res: NextApiResponse) => { + const data = { + model: customAPI.model || "gpt-3.5-turbo", + messages: [ + { + role: "system", + content: "你是一个专业的翻译助手,请准确翻译用户提供的文本,保持原文的意思、风格和格式。只返回翻译后的文本,不要添加解释或其他内容。" + }, + { + role: "user", + content: `请将以下文本从${source || "自动检测"}翻译成${target}语言:\n\n${text}`, + }, + ], + stream: true, + max_tokens: 4000, // 增加最大token数,避免长文本被截断 + temperature: 0.1, // 降低温度,使翻译更准确 + }; + + try { + const response = await axios.post(customAPI.endpoint, data, { + headers: { + 'Accept': 'application/json', + 'Authorization': `Bearer ${customAPI.apiKey}`, + 'Content-Type': 'application/json' + }, + responseType: 'stream', + timeout: 120000 // 增加超时时间到2分钟 + }); + + response.data.on('data', (chunk: Buffer) => { + const lines = chunk.toString().split('\n').filter(line => line.trim() !== ''); + + for (const line of lines) { + if (line.includes('[DONE]')) { + res.write(`data: [DONE]\n\n`); + continue; + } + + if (line.startsWith('data: ')) { + try { + // 处理特殊情况:有些API返回的不是JSON + if (line.substring(6).trim() === '[DONE]') { + res.write(`data: [DONE]\n\n`); + continue; + } + + const data = JSON.parse(line.substring(6)); + + // 处理标准OpenAI流式响应格式 + if (data.choices && data.choices.length > 0) { + const choice = data.choices[0]; + + // 处理delta格式 + if (choice.delta && choice.delta.content) { + res.write(`data: ${JSON.stringify({ text: choice.delta.content })}\n\n`); + } + // 处理message格式 + else if (choice.message && choice.message.content) { + res.write(`data: ${JSON.stringify({ text: choice.message.content })}\n\n`); + } + // 处理text格式 + else if (choice.text) { + res.write(`data: ${JSON.stringify({ text: choice.text })}\n\n`); + } + // 处理content格式 + else if (choice.content) { + res.write(`data: ${JSON.stringify({ text: choice.content })}\n\n`); + } + } + // 处理其他可能的流式响应格式 + else if (data.text) { + res.write(`data: ${JSON.stringify({ text: data.text })}\n\n`); + } + else if (data.content) { + res.write(`data: ${JSON.stringify({ text: data.content })}\n\n`); + } + else if (data.chunk) { + res.write(`data: ${JSON.stringify({ text: data.chunk })}\n\n`); + } + else if (data.token) { + res.write(`data: ${JSON.stringify({ text: data.token })}\n\n`); + } + else if (typeof data === 'string') { + res.write(`data: ${JSON.stringify({ text: data })}\n\n`); + } + } catch (e) { + console.error('解析流数据失败:', e, '原始数据:', line.substring(6)); + // 尝试直接发送文本 + try { + res.write(`data: ${JSON.stringify({ text: line.substring(6) })}\n\n`); + } catch (err) { + console.error('发送流数据失败:', err); + } + } + } + } + }); + + response.data.on('end', () => { + // 确保发送结束标记 + res.write(`data: [DONE]\n\n`); + res.end(); + }); + + response.data.on('error', (err: Error) => { + console.error('流处理错误:', err); + res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`); + res.write(`data: [DONE]\n\n`); + res.end(); + }); + + // 设置超时处理 + setTimeout(() => { + if (!res.writableEnded) { + console.error('流处理超时'); + res.write(`data: ${JSON.stringify({ error: "请求超时,请尝试减少文本长度或使用非流式输出" })}\n\n`); + res.write(`data: [DONE]\n\n`); + res.end(); + } + }, 110000); // 设置比请求超时略短的时间 + + } catch (error: any) { + console.error('自定义API流式请求失败:', error.response?.data || error.message); + throw new Error(`翻译失败: ${error.response?.data?.error?.message || error.message || '请求失败,请检查API配置'}`); + } +}; + +// 以下是从translate.ts复制的非流式翻译函数 +// 渠道: 谷歌翻译 +const translateWithGoogle = async (text: string, source: string, target: string): Promise => { + const { Translate } = require('@google-cloud/translate').v2; + + const projectId = process.env.GOOGLE_CLOUD_PROJECT_ID; + const keyFilename = process.env.GOOGLE_CLOUD_KEYFILE; + + if (!projectId || !keyFilename) { + throw new Error("Google Cloud 项目ID或密钥文件路径未配置"); + } + + const translate = new Translate({ projectId, keyFilename }); + + try { + const [translation] = await translate.translate(text, { + from: source, + to: target, + }); + return translation; + } catch (error: any) { + console.error('谷歌翻译请求失败:', error); + throw new Error(`翻译失败: ${error.message}`); + } +}; + +// 渠道: 通义千问 +const translateWithTongyi = async (text: string, source: string, target: string): Promise => { + const apiKey = process.env.TONGYI_API_KEY; + + if (!apiKey) { + throw new Error("通义千问 API 密钥未配置"); + } + + const data = { + model: "qwen-turbo", + input: { + messages: [ + { + role: "user", + content: `请将以下文本从${source}翻译成${target}语言:${text}`, + }, + ], + }, + }; + + const config = { + method: 'post', + url: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation', + headers: { + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + data: data, + }; + + try { + const response = await axios(config); + + if ( + response.data && + response.data.output && + response.data.output.text + ) { + const translatedText = response.data.output.text.trim(); + return translatedText; + } else { + throw new Error("无效的响应结构"); + } + } catch (error: any) { + console.error('通义千问翻译请求失败:', error.response?.data || error.message); + throw new Error(`翻译失败: ${error.response?.data?.error?.message || error.message}`); + } +}; + +// 渠道: DeepL +const translateWithDeepL = async (text: string, source: string, target: string): Promise => { + const apiKey = process.env.DEEPL_API_KEY; + + if (!apiKey) { + throw new Error("DeepL API 密钥未配置"); + } + + const data = new URLSearchParams({ + text: text, + source_lang: source.toUpperCase(), + target_lang: target.toUpperCase(), + }); + + const config = { + method: 'post', + url: 'https://api-free.deepl.com/v2/translate', + headers: { + 'Authorization': `DeepL-Auth-Key ${apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data: data, + }; + + try { + const response = await axios(config); + + if ( + response.data && + response.data.translations && + response.data.translations.length > 0 && + response.data.translations[0].text + ) { + const translatedText = response.data.translations[0].text.trim(); + return translatedText; + } else { + throw new Error("无效的响应结构"); + } + } catch (error: any) { + console.error('DeepL翻译请求失败:', error.response?.data || error.message); + throw new Error(`翻译失败: ${error.response?.data?.message || error.message}`); + } +}; + +// 渠道: 硅基流动 +const translateWithSiliconFlow = async (text: string, source: string, target: string): Promise => { + const apiKey = process.env.SILICONFLOW_API_KEY; + + if (!apiKey) { + throw new Error("硅基流动 API 密钥未配置"); + } + + const data = { + model: "Qwen/Qwen2.5-32B-Instruct", + messages: [ + { + role: "user", + content: `请将以下文本从${source}翻译成${target}语言:${text}`, + }, + ], + }; + + const config = { + method: 'post', + url: 'https://api.siliconflow.cn/v1/chat/completions', + headers: { + 'Accept': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + data: data, + }; + + try { + const response = await axios(config); + + if ( + response.data && + response.data.choices && + response.data.choices.length > 0 && + response.data.choices[0].message && + response.data.choices[0].message.content + ) { + const translatedText = response.data.choices[0].message.content.trim(); + return translatedText; + } else { + throw new Error("无效的响应结构"); + } + } catch (error: any) { + console.error('硅基流动翻译请求失败:', error.response?.data || error.message); + throw new Error(`翻译失败: ${error.response?.data?.error?.message || error.message}`); + } +}; + +// 渠道: 深度求索 +const translateWithDeepSeek = async (text: string, source: string, target: string): Promise => { + const apiKey = process.env.DEEPSEEK_API_KEY; + + if (!apiKey) { + throw new Error("DeepSeek API 密钥未配置"); + } + + const data = { + model: "deepseek-chat", + messages: [ + { + role: "user", + content: `请将以下文本从${source}翻译成${target}语言:${text}`, + }, + ], + }; + + const config = { + method: 'post', + url: 'https://api.deepseek.com/v1/chat/completions', + headers: { + 'Accept': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + data: data, + }; + + try { + const response = await axios(config); + + if ( + response.data && + response.data.choices && + response.data.choices.length > 0 && + response.data.choices[0].message && + response.data.choices[0].message.content + ) { + const translatedText = response.data.choices[0].message.content.trim(); + return translatedText; + } else { + throw new Error("无效的响应结构"); + } + } catch (error: any) { + console.error('DeepSeek翻译请求失败:', error.response?.data || error.message); + throw new Error(`翻译失败: ${error.response?.data?.error?.message || error.message}`); + } +}; \ No newline at end of file diff --git a/pages/api/translate.ts b/pages/api/translate.ts index 94dbed2..9846054 100644 --- a/pages/api/translate.ts +++ b/pages/api/translate.ts @@ -15,7 +15,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< return; } - const { text, sourceLang, targetLang, service, customAPIs } = req.body; + const { text, sourceLang, targetLang, service, customAPIs, openAIConfig } = req.body; if (!text || !targetLang || !service) { res.status(400).json({ error: "Missing parameters" }); @@ -38,7 +38,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< translatedText = await translateWithGoogle(text, sourceLang, targetLang); break; case "openai": - translatedText = await translateWithOpenAI(text, sourceLang, targetLang); + translatedText = await translateWithOpenAI(text, sourceLang, targetLang, req); break; case "tongyi": translatedText = await translateWithTongyi(text, sourceLang, targetLang); @@ -90,9 +90,19 @@ const translateWithGoogle = async (text: string, source: string, target: string) }; // 渠道: OpenAI -const translateWithOpenAI = async (text: string, source: string, target: string): Promise => { - // 从环境变量中获取 API 密钥 - const apiKey = process.env.OPENAI_API_KEY; +const translateWithOpenAI = async (text: string, source: string, target: string, req?: any): Promise => { + // 从环境变量中获取默认值 + let apiKey = process.env.OPENAI_API_KEY || ""; + let model = process.env.OPENAI_MODEL || "gpt-4o-mini"; + let endpoint = process.env.OPENAI_ENDPOINT || "https://api.openai.com/v1/chat/completions"; + + // 如果请求中包含OpenAI配置,则使用请求中的配置 + if (req && req.body && req.body.openAIConfig) { + const { apiKey: reqApiKey, model: reqModel, endpoint: reqEndpoint } = req.body.openAIConfig; + if (reqApiKey) apiKey = reqApiKey; + if (reqModel) model = reqModel; + if (reqEndpoint) endpoint = reqEndpoint; + } if (!apiKey) { throw new Error("OpenAI API 密钥未配置"); @@ -100,48 +110,80 @@ const translateWithOpenAI = async (text: string, source: string, target: string) // 构建请求数据 const data = { - model: "gpt-4o-mini", + model: model, messages: [ + { + role: "system", + content: "你是一个专业的翻译助手,请准确翻译用户提供的文本,保持原文的意思、风格和格式。只返回翻译后的文本,不要添加解释或其他内容。" + }, { role: "user", - content: `请将以下文本从${source}翻译成${target}语言:${text}`, + content: `请将以下文本从${source || "自动检测"}翻译成${target}语言:\n\n${text}`, }, ], + stream: false, + max_tokens: 4000, // 增加最大token数,避免长文本被截断 + temperature: 0.1, // 降低温度,使翻译更准确 }; // 配置请求参数 const config = { method: 'post', - url: 'https://api.openai.com/v1/chat/completions', + url: endpoint, headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, data: data, + timeout: 120000, // 增加超时时间到2分钟,避免大文本翻译超时 }; try { // 发送请求 const response = await axios(config); - // 检查响应结构 - if ( - response.data && - response.data.choices && - response.data.choices.length > 0 && - response.data.choices[0].message && - response.data.choices[0].message.content - ) { - // 提取并返回翻译结果 - const translatedText = response.data.choices[0].message.content.trim(); - return translatedText; - } else { - throw new Error("无效的响应结构"); + // 增强响应结构检查 + if (response.data) { + // 标准OpenAI格式 + if ( + response.data.choices && + response.data.choices.length > 0 + ) { + if (response.data.choices[0].message && response.data.choices[0].message.content) { + return response.data.choices[0].message.content.trim(); + } else if (response.data.choices[0].text) { + // 兼容旧版OpenAI API + return response.data.choices[0].text.trim(); + } else if (response.data.choices[0].content) { + // 兼容某些API格式 + return response.data.choices[0].content.trim(); + } + } + // 兼容其他可能的API格式 + else if (response.data.response) { + return response.data.response.trim(); + } + else if (response.data.result) { + return response.data.result.trim(); + } + else if (response.data.text) { + return response.data.text.trim(); + } + else if (response.data.content) { + return response.data.content.trim(); + } + else if (typeof response.data === 'string') { + return response.data.trim(); + } } + + // 如果无法识别响应格式,返回错误 + console.error('无法识别的OpenAI响应格式:', JSON.stringify(response.data)); + throw new Error("无效的响应结构"); } catch (error: any) { - console.error('翻译请求失败:', error.response?.data || error.message); - throw new Error(`翻译失败: ${error.response?.data?.error?.message || error.message}`); + console.error('OpenAI翻译请求失败:', error.response?.data || error.message); + throw new Error(`翻译失败: ${error.response?.data?.error?.message || error.message || '请求失败,请检查API配置'}`); } }; @@ -308,16 +350,25 @@ const translateWithSiliconFlow = async (text: string, source: string, target: st // 渠道: 自定义API const translateWithCustomAPI = async (text: string, source: string, target: string, customAPI: any): Promise => { + // 构建请求数据 - 确保兼容OpenAI API格式 const data = { - model: customAPI.model, + model: customAPI.model || "gpt-3.5-turbo", // 提供默认模型 messages: [ + { + role: "system", + content: "你是一个专业的翻译助手,请准确翻译用户提供的文本,保持原文的意思、风格和格式。只返回翻译后的文本,不要添加解释或其他内容。" + }, { role: "user", - content: `请将以下文本从${source}翻译成${target}语言:${text}`, + content: `请将以下文本从${source || "自动检测"}翻译成${target}语言:\n\n${text}`, }, ], + stream: false, // 默认不使用流式输出 + max_tokens: 4000, // 增加最大token数,避免长文本被截断 + temperature: 0.1, // 降低温度,使翻译更准确 }; + // 配置请求参数 const config = { method: 'post', url: customAPI.endpoint, @@ -327,26 +378,74 @@ const translateWithCustomAPI = async (text: string, source: string, target: stri 'Content-Type': 'application/json' }, data: data, + // 增加超时时间,避免大文本翻译超时 + timeout: 120000, // 增加到2分钟 }; try { const response = await axios(config); - if ( - response.data && - response.data.choices && - response.data.choices.length > 0 && - response.data.choices[0].message && - response.data.choices[0].message.content - ) { - const translatedText = response.data.choices[0].message.content.trim(); - return translatedText; - } else { - throw new Error("无效的响应结构"); + // 增强响应结构检查,支持更多API格式 + if (response.data) { + // 标准OpenAI格式 + if ( + response.data.choices && + response.data.choices.length > 0 + ) { + if (response.data.choices[0].message && response.data.choices[0].message.content) { + return response.data.choices[0].message.content.trim(); + } else if (response.data.choices[0].text) { + // 兼容旧版OpenAI API + return response.data.choices[0].text.trim(); + } else if (response.data.choices[0].content) { + // 兼容某些API格式 + return response.data.choices[0].content.trim(); + } else if (response.data.choices[0].delta && response.data.choices[0].delta.content) { + // 兼容流式输出格式 + return response.data.choices[0].delta.content.trim(); + } + } + // 兼容其他可能的API格式 + else if (response.data.response) { + return response.data.response.trim(); + } + else if (response.data.result) { + return response.data.result.trim(); + } + else if (response.data.translation) { + return response.data.translation.trim(); + } + else if (response.data.output) { + if (typeof response.data.output === 'string') { + return response.data.output.trim(); + } else if (response.data.output.text) { + return response.data.output.text.trim(); + } else if (response.data.output.content) { + return response.data.output.content.trim(); + } else if (response.data.output.translation) { + return response.data.output.translation.trim(); + } + } + else if (response.data.text) { + return response.data.text.trim(); + } + else if (response.data.content) { + return response.data.content.trim(); + } + else if (response.data.generated_text) { + return response.data.generated_text.trim(); + } + else if (typeof response.data === 'string') { + return response.data.trim(); + } } + + // 如果无法识别响应格式,返回原始响应以便调试 + console.error('无法识别的API响应格式:', JSON.stringify(response.data)); + throw new Error(`无法识别的API响应格式: ${JSON.stringify(response.data).substring(0, 200)}...`); } catch (error: any) { console.error('自定义API翻译请求失败:', error.response?.data || error.message); - throw new Error(`翻译失败: ${error.response?.data?.error?.message || error.message}`); + throw new Error(`翻译失败: ${error.response?.data?.error?.message || error.message || '请求失败,请检查API配置'}`); } };