563 lines
15 KiB
Plaintext
563 lines
15 KiB
Plaintext
// AI翻译服务
|
||
// filepath: h:\blews\akmon\uni_modules\ak-ai-news\services\ai-translation-service.uts
|
||
|
||
import { AkReq } from '@/uni_modules/ak-req/index.uts'
|
||
import type {
|
||
TranslationResult,
|
||
TranslationOptions,
|
||
AIProvider,
|
||
AIResponse,
|
||
AIServiceConfig
|
||
} from '../types/ai-types.uts'
|
||
|
||
export class AITranslationService {
|
||
private config: AIServiceConfig
|
||
private req: AkReq
|
||
private cache: Map<string, TranslationResult> = new Map()
|
||
|
||
constructor(config: AIServiceConfig) {
|
||
this.config = config
|
||
this.req = new AkReq()
|
||
}
|
||
|
||
/**
|
||
* 翻译文本 - 智能选择最佳AI服务
|
||
*/
|
||
async translateText(
|
||
text: string,
|
||
targetLang: string,
|
||
sourceLang?: string,
|
||
options?: TranslationOptions
|
||
): Promise<AIResponse<TranslationResult>> {
|
||
try {
|
||
// 检查缓存
|
||
const cacheKey = this.generateCacheKey(text, targetLang, sourceLang)
|
||
const cached = this.cache.get(cacheKey)
|
||
if (cached && this.isCacheValid(cached)) {
|
||
return {
|
||
success: true,
|
||
data: cached,
|
||
processingTimeMs: 0,
|
||
costUSD: 0
|
||
}
|
||
}
|
||
|
||
// 智能选择提供商
|
||
const provider = this.selectOptimalProvider(text, targetLang, options)
|
||
|
||
let result: TranslationResult
|
||
const startTime = Date.now()
|
||
|
||
switch (provider) {
|
||
case 'openai':
|
||
result = await this.translateWithOpenAI(text, targetLang, sourceLang, options)
|
||
break
|
||
case 'google':
|
||
result = await this.translateWithGoogle(text, targetLang, sourceLang, options)
|
||
break
|
||
case 'baidu':
|
||
result = await this.translateWithBaidu(text, targetLang, sourceLang, options)
|
||
break
|
||
default:
|
||
throw new Error(`不支持的AI提供商: ${provider}`)
|
||
}
|
||
|
||
result.processingTimeMs = Date.now() - startTime
|
||
|
||
// 质量检查
|
||
if (result.qualityScore < (options?.qualityThreshold ?? 0.7)) {
|
||
// 尝试使用备用提供商
|
||
const fallbackProvider = this.getFallbackProvider(provider)
|
||
if (fallbackProvider) {
|
||
const fallbackResult = await this.translateWithProvider(
|
||
text, targetLang, sourceLang, fallbackProvider, options
|
||
)
|
||
if (fallbackResult.qualityScore > result.qualityScore) {
|
||
result = fallbackResult
|
||
}
|
||
}
|
||
}
|
||
|
||
// 缓存结果
|
||
this.cache.set(cacheKey, result)
|
||
|
||
// 记录使用统计
|
||
await this.recordUsage(result)
|
||
|
||
return {
|
||
success: true,
|
||
data: result,
|
||
processingTimeMs: result.processingTimeMs,
|
||
costUSD: result.costUSD,
|
||
provider: result.provider
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('翻译失败:', error)
|
||
return {
|
||
success: false,
|
||
error: error instanceof Error ? error.message : '翻译服务异常',
|
||
errorCode: 'TRANSLATION_FAILED'
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 批量翻译
|
||
*/
|
||
async batchTranslate(
|
||
texts: string[],
|
||
targetLang: string,
|
||
sourceLang?: string,
|
||
options?: TranslationOptions
|
||
): Promise<AIResponse<TranslationResult[]>> {
|
||
try {
|
||
const results: TranslationResult[] = []
|
||
const batchSize = 10 // 批处理大小
|
||
|
||
for (let i = 0; i < texts.length; i += batchSize) {
|
||
const batch = texts.slice(i, i + batchSize)
|
||
const batchPromises = batch.map(text =>
|
||
this.translateText(text, targetLang, sourceLang, options)
|
||
)
|
||
|
||
const batchResults = await Promise.all(batchPromises)
|
||
|
||
for (const result of batchResults) {
|
||
if (result.success && result.data) {
|
||
results.push(result.data)
|
||
}
|
||
}
|
||
|
||
// 避免API限流
|
||
if (i + batchSize < texts.length) {
|
||
await this.delay(100)
|
||
}
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
data: results,
|
||
processingTimeMs: results.reduce((sum, r) => sum + r.processingTimeMs, 0),
|
||
costUSD: results.reduce((sum, r) => sum + r.costUSD, 0)
|
||
}
|
||
|
||
} catch (error) {
|
||
return {
|
||
success: false,
|
||
error: error instanceof Error ? error.message : '批量翻译失败',
|
||
errorCode: 'BATCH_TRANSLATION_FAILED'
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* OpenAI翻译实现
|
||
*/
|
||
private async translateWithOpenAI(
|
||
text: string,
|
||
targetLang: string,
|
||
sourceLang?: string,
|
||
options?: TranslationOptions
|
||
): Promise<TranslationResult> {
|
||
const openaiConfig = this.config.openai
|
||
if (!openaiConfig) {
|
||
throw new Error('OpenAI配置未找到')
|
||
}
|
||
|
||
const prompt = this.buildOpenAIPrompt(text, targetLang, sourceLang, options)
|
||
|
||
const response = await this.req.post<any>({
|
||
url: `${openaiConfig.baseURL || 'https://api.openai.com'}/v1/chat/completions`,
|
||
headers: {
|
||
'Authorization': `Bearer ${openaiConfig.apiKey}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
data: {
|
||
model: options?.model || openaiConfig.model,
|
||
messages: [
|
||
{
|
||
role: 'system',
|
||
content: '你是一个专业的翻译助手,能够提供高质量的多语言翻译服务。'
|
||
},
|
||
{
|
||
role: 'user',
|
||
content: prompt
|
||
}
|
||
],
|
||
max_tokens: options?.maxTokens || openaiConfig.maxTokens,
|
||
temperature: options?.temperature || openaiConfig.temperature
|
||
}
|
||
})
|
||
|
||
if (!response.success || !response.data) {
|
||
throw new Error('OpenAI API调用失败')
|
||
}
|
||
|
||
const choice = response.data.choices?.[0]
|
||
if (!choice) {
|
||
throw new Error('OpenAI响应格式错误')
|
||
}
|
||
|
||
return this.parseOpenAIResponse(
|
||
choice.message.content,
|
||
text,
|
||
targetLang,
|
||
sourceLang || 'auto',
|
||
response.data.usage
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Google翻译实现
|
||
*/
|
||
private async translateWithGoogle(
|
||
text: string,
|
||
targetLang: string,
|
||
sourceLang?: string,
|
||
options?: TranslationOptions
|
||
): Promise<TranslationResult> {
|
||
const googleConfig = this.config.google
|
||
if (!googleConfig) {
|
||
throw new Error('Google翻译配置未找到')
|
||
}
|
||
|
||
const startTime = Date.now()
|
||
|
||
const response = await this.req.post<any>({
|
||
url: 'https://translation.googleapis.com/language/translate/v2',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
data: {
|
||
key: googleConfig.apiKey,
|
||
q: text,
|
||
target: this.convertLanguageCode(targetLang, 'google'),
|
||
source: sourceLang ? this.convertLanguageCode(sourceLang, 'google') : undefined,
|
||
format: 'text'
|
||
}
|
||
})
|
||
|
||
if (!response.success || !response.data) {
|
||
throw new Error('Google翻译API调用失败')
|
||
}
|
||
|
||
const translation = response.data.data?.translations?.[0]
|
||
if (!translation) {
|
||
throw new Error('Google翻译响应格式错误')
|
||
}
|
||
|
||
return {
|
||
translatedText: translation.translatedText,
|
||
originalText: text,
|
||
sourceLang: translation.detectedSourceLanguage || sourceLang || 'auto',
|
||
targetLang,
|
||
confidence: 0.9, // Google翻译通常质量较高
|
||
qualityScore: 0.85,
|
||
provider: 'google',
|
||
tokensUsed: Math.ceil(text.length / 4), // 估算token数
|
||
processingTimeMs: Date.now() - startTime,
|
||
costUSD: this.calculateGoogleCost(text.length)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 百度翻译实现
|
||
*/
|
||
private async translateWithBaidu(
|
||
text: string,
|
||
targetLang: string,
|
||
sourceLang?: string,
|
||
options?: TranslationOptions
|
||
): Promise<TranslationResult> {
|
||
const baiduConfig = this.config.baidu
|
||
if (!baiduConfig) {
|
||
throw new Error('百度翻译配置未找到')
|
||
}
|
||
|
||
const startTime = Date.now()
|
||
const salt = Date.now().toString()
|
||
const sign = this.generateBaiduSign(text, salt, baiduConfig.apiKey, baiduConfig.secretKey)
|
||
|
||
const response = await this.req.post<any>({
|
||
url: 'https://fanyi-api.baidu.com/api/trans/vip/translate',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded'
|
||
},
|
||
data: {
|
||
q: text,
|
||
from: sourceLang ? this.convertLanguageCode(sourceLang, 'baidu') : 'auto',
|
||
to: this.convertLanguageCode(targetLang, 'baidu'),
|
||
appid: baiduConfig.apiKey,
|
||
salt: salt,
|
||
sign: sign
|
||
}
|
||
})
|
||
|
||
if (!response.success || !response.data) {
|
||
throw new Error('百度翻译API调用失败')
|
||
}
|
||
|
||
const result = response.data.trans_result?.[0]
|
||
if (!result) {
|
||
throw new Error('百度翻译响应格式错误')
|
||
}
|
||
|
||
return {
|
||
translatedText: result.dst,
|
||
originalText: text,
|
||
sourceLang: response.data.from || sourceLang || 'auto',
|
||
targetLang,
|
||
confidence: 0.85,
|
||
qualityScore: 0.8,
|
||
provider: 'baidu',
|
||
tokensUsed: Math.ceil(text.length / 4),
|
||
processingTimeMs: Date.now() - startTime,
|
||
costUSD: this.calculateBaiduCost(text.length)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 选择最优提供商
|
||
*/
|
||
private selectOptimalProvider(
|
||
text: string,
|
||
targetLang: string,
|
||
options?: TranslationOptions
|
||
): AIProvider {
|
||
if (options?.provider) {
|
||
return options.provider
|
||
}
|
||
|
||
// 根据文本长度和语言选择最佳提供商
|
||
const textLength = text.length
|
||
const isChineseTarget = targetLang.startsWith('zh')
|
||
const isChineseSource = /[\u4e00-\u9fff]/.test(text)
|
||
|
||
// 中文相关翻译优先使用百度
|
||
if (isChineseTarget || isChineseSource) {
|
||
return 'baidu'
|
||
}
|
||
|
||
// 长文本使用OpenAI(更好的上下文理解)
|
||
if (textLength > 1000) {
|
||
return 'openai'
|
||
}
|
||
|
||
// 短文本使用Google(速度快,成本低)
|
||
return 'google'
|
||
}
|
||
|
||
/**
|
||
* 获取备用提供商
|
||
*/
|
||
private getFallbackProvider(primary: AIProvider): AIProvider | null {
|
||
switch (primary) {
|
||
case 'openai':
|
||
return 'google'
|
||
case 'google':
|
||
return 'baidu'
|
||
case 'baidu':
|
||
return 'google'
|
||
default:
|
||
return null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成缓存键
|
||
*/
|
||
private generateCacheKey(text: string, targetLang: string, sourceLang?: string): string {
|
||
const source = sourceLang || 'auto'
|
||
const hash = this.simpleHash(text)
|
||
return `${source}-${targetLang}-${hash}`
|
||
}
|
||
|
||
/**
|
||
* 检查缓存是否有效
|
||
*/
|
||
private isCacheValid(result: TranslationResult): boolean {
|
||
// 简单的缓存有效性检查,可以根据需要扩展
|
||
return result.qualityScore > 0.7
|
||
}
|
||
|
||
/**
|
||
* 构建OpenAI提示词
|
||
*/
|
||
private buildOpenAIPrompt(
|
||
text: string,
|
||
targetLang: string,
|
||
sourceLang?: string,
|
||
options?: TranslationOptions
|
||
): string {
|
||
let prompt = `请将以下文本翻译成${this.getLanguageName(targetLang)}:\n\n${text}\n\n`
|
||
|
||
if (options?.culturalAdaptation) {
|
||
prompt += '请考虑文化差异,进行适当的本地化调整。\n'
|
||
}
|
||
|
||
if (options?.preserveFormatting) {
|
||
prompt += '请保持原文的格式和结构。\n'
|
||
}
|
||
|
||
prompt += '只返回翻译结果,不要包含其他解释。'
|
||
|
||
return prompt
|
||
}
|
||
|
||
/**
|
||
* 解析OpenAI响应
|
||
*/
|
||
private parseOpenAIResponse(
|
||
content: string,
|
||
originalText: string,
|
||
targetLang: string,
|
||
sourceLang: string,
|
||
usage: any
|
||
): TranslationResult {
|
||
return {
|
||
translatedText: content.trim(),
|
||
originalText,
|
||
sourceLang,
|
||
targetLang,
|
||
confidence: 0.9,
|
||
qualityScore: 0.9,
|
||
provider: 'openai',
|
||
tokensUsed: usage?.total_tokens || 0,
|
||
processingTimeMs: 0,
|
||
costUSD: this.calculateOpenAICost(usage?.total_tokens || 0)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 转换语言代码
|
||
*/
|
||
private convertLanguageCode(code: string, provider: AIProvider): string {
|
||
const codeMap: Record<AIProvider, Record<string, string>> = {
|
||
openai: {
|
||
'zh-CN': 'Chinese',
|
||
'en-US': 'English',
|
||
'ja-JP': 'Japanese',
|
||
'ko-KR': 'Korean'
|
||
},
|
||
google: {
|
||
'zh-CN': 'zh',
|
||
'en-US': 'en',
|
||
'ja-JP': 'ja',
|
||
'ko-KR': 'ko'
|
||
},
|
||
baidu: {
|
||
'zh-CN': 'zh',
|
||
'en-US': 'en',
|
||
'ja-JP': 'jp',
|
||
'ko-KR': 'kor'
|
||
},
|
||
custom: {}
|
||
}
|
||
|
||
return codeMap[provider]?.[code] || code
|
||
}
|
||
|
||
/**
|
||
* 获取语言名称
|
||
*/
|
||
private getLanguageName(code: string): string {
|
||
const nameMap: Record<string, string> = {
|
||
'zh-CN': '中文',
|
||
'en-US': 'English',
|
||
'ja-JP': '日语',
|
||
'ko-KR': '韩语',
|
||
'es-ES': 'Spanish',
|
||
'fr-FR': 'French',
|
||
'de-DE': 'German'
|
||
}
|
||
return nameMap[code] || code
|
||
}
|
||
|
||
/**
|
||
* 计算成本
|
||
*/
|
||
private calculateOpenAICost(tokens: number): number {
|
||
// GPT-4 pricing: $0.03 per 1K tokens (input + output)
|
||
return (tokens / 1000) * 0.03
|
||
}
|
||
|
||
private calculateGoogleCost(textLength: number): number {
|
||
// Google Translate: $20 per 1M characters
|
||
return (textLength / 1000000) * 20
|
||
}
|
||
|
||
private calculateBaiduCost(textLength: number): number {
|
||
// 百度翻译:较低成本,假设$5 per 1M characters
|
||
return (textLength / 1000000) * 5
|
||
}
|
||
|
||
/**
|
||
* 生成百度签名
|
||
*/
|
||
private generateBaiduSign(text: string, salt: string, appid: string, key: string): string {
|
||
// 简化的签名生成,实际应用中需要使用MD5
|
||
const str = appid + text + salt + key
|
||
return this.simpleHash(str)
|
||
}
|
||
|
||
/**
|
||
* 简单哈希函数
|
||
*/
|
||
private simpleHash(str: string): string {
|
||
let hash = 0
|
||
for (let i = 0; i < str.length; i++) {
|
||
const char = str.charCodeAt(i)
|
||
hash = ((hash << 5) - hash) + char
|
||
hash = hash & hash // Convert to 32-bit integer
|
||
}
|
||
return Math.abs(hash).toString(36)
|
||
}
|
||
|
||
/**
|
||
* 延迟函数
|
||
*/
|
||
private delay(ms: number): Promise<void> {
|
||
return new Promise(resolve => setTimeout(resolve, ms))
|
||
}
|
||
|
||
/**
|
||
* 记录使用统计
|
||
*/
|
||
private async recordUsage(result: TranslationResult): Promise<void> {
|
||
try {
|
||
// 这里可以将使用统计发送到数据库
|
||
console.log('翻译使用统计:', {
|
||
provider: result.provider,
|
||
tokensUsed: result.tokensUsed,
|
||
costUSD: result.costUSD,
|
||
processingTimeMs: result.processingTimeMs
|
||
})
|
||
} catch (error) {
|
||
console.error('记录使用统计失败:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根据提供商翻译
|
||
*/
|
||
private async translateWithProvider(
|
||
text: string,
|
||
targetLang: string,
|
||
sourceLang: string | undefined,
|
||
provider: AIProvider,
|
||
options?: TranslationOptions
|
||
): Promise<TranslationResult> {
|
||
switch (provider) {
|
||
case 'openai':
|
||
return await this.translateWithOpenAI(text, targetLang, sourceLang, options)
|
||
case 'google':
|
||
return await this.translateWithGoogle(text, targetLang, sourceLang, options)
|
||
case 'baidu':
|
||
return await this.translateWithBaidu(text, targetLang, sourceLang, options)
|
||
default:
|
||
throw new Error(`不支持的提供商: ${provider}`)
|
||
}
|
||
}
|
||
}
|