claude-code-cchui/src/services/tools/translate.ts
gaoziman d16f72c035 feat(工具): 添加有道智云翻译功能
- 新增 translate.ts: 实现有道翻译API调用
  - 支持100+种语言互译
  - 自动语言检测
  - SHA256签名验证
  - 完善的错误码处理

- executor.ts: 添加翻译工具执行器
  - 支持源语言/目标语言参数
  - 格式化翻译结果输出

- route.ts: 添加翻译工具定义
  - Claude/OpenAI/Codex三种格式支持
2025-12-23 14:33:00 +08:00

315 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 有道翻译工具服务
* 使用有道智云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<string, string> = {
'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<string, string> = {
// 自动检测
'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<TranslateResponse> {
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;
}