/** * 有道翻译工具服务 * 使用有道智云API实现高质量多语言翻译 * * API文档: https://ai.youdao.com/DOCSIRMA/html/trans/api/wbfy/index.html */ import crypto from 'crypto'; // ============== 类型定义 ============== export interface TranslateInput { /** 待翻译文本 */ text: string; /** 源语言代码,默认auto自动检测 */ from?: string; /** 目标语言代码,默认zh-CHS简体中文 */ to?: string; } export interface TranslateResponse { /** 是否成功 */ success: boolean; /** 翻译结果 */ translation?: string; /** 检测到的源语言 */ from?: string; /** 目标语言 */ to?: string; /** 错误信息 */ error?: string; /** 原始API响应 */ rawResponse?: YoudaoApiResponse; } /** 有道API原始响应 */ interface YoudaoApiResponse { errorCode: string; query?: string; translation?: string[]; l?: string; dict?: { url: string }; webdict?: { url: string }; tSpeakUrl?: string; speakUrl?: string; } // ============== 有道API配置 ============== const YOUDAO_APP_KEY = process.env.YOUDAO_APP_KEY || ''; const YOUDAO_APP_SECRET = process.env.YOUDAO_APP_SECRET || ''; const YOUDAO_API_URL = 'https://openapi.youdao.com/api'; // ============== 错误码映射 ============== const ERROR_CODE_MAP: Record = { '101': '缺少必填参数', '102': '不支持的语言类型', '103': '翻译文本过长(最大5000字符)', '108': '应用ID无效', '110': '无相关服务的有效应用', '111': '开发者账号无效', '112': '请求服务无效', '113': '翻译文本不能为空', '202': '签名检验失败', '203': '访问IP地址不在可访问IP列表', '206': '时间戳无效导致签名校验失败', '207': '重放请求', '301': '辞典查询失败', '302': '翻译查询失败', '303': '服务端异常', '401': '账户已欠费', '411': '访问频率受限', '412': '长请求过于频繁', }; // ============== 工具函数 ============== /** * 生成SHA256签名 */ function generateSign( appKey: string, input: string, salt: string, curtime: string, appSecret: string ): string { const signStr = appKey + input + salt + curtime + appSecret; return crypto.createHash('sha256').update(signStr).digest('hex'); } /** * 计算input参数(处理长文本截断) * 规则:文本长度>20时,取前10字符+长度+后10字符 */ function truncate(q: string): string { const len = q.length; if (len <= 20) return q; return q.substring(0, 10) + len + q.substring(len - 10, len); } /** * 获取错误信息 */ function getErrorMessage(errorCode: string): string { return ERROR_CODE_MAP[errorCode] || `未知错误 (错误码: ${errorCode})`; } // ============== 语言代码映射 ============== /** 常用语言代码对照表 */ export const LANGUAGE_MAP: Record = { // 自动检测 'auto': '自动检测', // 中文 'zh-CHS': '简体中文', 'zh-CHT': '繁体中文', // 主要语言 'en': '英语', 'ja': '日语', 'ko': '韩语', 'fr': '法语', 'de': '德语', 'es': '西班牙语', 'pt': '葡萄牙语', 'it': '意大利语', 'ru': '俄语', 'ar': '阿拉伯语', // 其他常用语言 'vi': '越南语', 'th': '泰语', 'id': '印度尼西亚语', 'ms': '马来语', 'hi': '印地语', 'nl': '荷兰语', 'pl': '波兰语', 'tr': '土耳其语', 'uk': '乌克兰语', 'sv': '瑞典语', 'da': '丹麦语', 'no': '挪威语', 'fi': '芬兰语', 'el': '希腊语', 'cs': '捷克语', 'hu': '匈牙利语', 'ro': '罗马尼亚语', 'bg': '保加利亚语', 'he': '希伯来语', 'yue': '粤语', }; /** * 获取语言名称 */ export function getLanguageName(code: string): string { return LANGUAGE_MAP[code] || code; } // ============== 主翻译函数 ============== /** * 执行翻译 * @param input 翻译输入参数 * @returns 翻译响应 */ export async function translate(input: TranslateInput): Promise { const { text, from = 'auto', to = 'zh-CHS' } = input; // API Key 验证 if (!YOUDAO_APP_KEY || !YOUDAO_APP_SECRET) { return { success: false, error: '有道翻译 API 未配置,请在环境变量中设置 YOUDAO_APP_KEY 和 YOUDAO_APP_SECRET', }; } // 参数验证 if (!text || text.trim().length === 0) { return { success: false, error: '翻译文本不能为空', }; } if (text.length > 5000) { return { success: false, error: '翻译文本过长,最大支持5000字符', }; } try { // 生成请求参数 const salt = crypto.randomUUID(); const curtime = Math.round(Date.now() / 1000).toString(); const sign = generateSign( YOUDAO_APP_KEY, truncate(text), salt, curtime, YOUDAO_APP_SECRET ); // 构建请求体 const params = new URLSearchParams({ q: text, from, to, appKey: YOUDAO_APP_KEY, salt, sign, signType: 'v3', curtime, }); // 发送请求 const response = await fetch(YOUDAO_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: params.toString(), }); if (!response.ok) { return { success: false, error: `HTTP请求失败: ${response.status} ${response.statusText}`, }; } const data: YoudaoApiResponse = await response.json(); // 检查API返回结果 if (data.errorCode === '0') { // 解析语言方向 (如 "en2zh-CHS") const langParts = data.l?.split('2') || [from, to]; const detectedFrom = langParts[0] || from; const detectedTo = langParts[1] || to; return { success: true, translation: data.translation?.join('\n') || '', from: detectedFrom, to: detectedTo, rawResponse: data, }; } else { return { success: false, error: getErrorMessage(data.errorCode), rawResponse: data, }; } } catch (error) { console.error('Translate API error:', error); return { success: false, error: error instanceof Error ? error.message : '翻译请求失败', }; } } // ============== 格式化函数 ============== /** * 格式化翻译结果(完整版 - 发送给AI) */ export function formatTranslateResult( response: TranslateResponse, originalText: string ): string { if (!response.success) { return `翻译失败: ${response.error}`; } const fromLang = getLanguageName(response.from || 'auto'); const toLang = getLanguageName(response.to || 'zh-CHS'); let result = `## 翻译结果\n\n`; result += `**源语言**: ${fromLang}\n`; result += `**目标语言**: ${toLang}\n\n`; result += `### 原文\n${originalText}\n\n`; result += `### 译文\n${response.translation}`; return result; } /** * 格式化翻译结果(简短版 - 显示给用户) */ export function formatTranslateResultShort( response: TranslateResponse, originalText: string ): string { if (!response.success) { return `翻译失败: ${response.error}`; } const fromLang = getLanguageName(response.from || 'auto'); const toLang = getLanguageName(response.to || 'zh-CHS'); // 截取原文预览(最多30字符) const textPreview = originalText.length > 30 ? originalText.substring(0, 30) + '...' : originalText; let result = `> 🌐 已翻译「${textPreview}」\n`; result += `> **${fromLang}** → **${toLang}**\n\n`; result += `${response.translation}`; return result; }