Initial commit of akmon project
This commit is contained in:
562
uni_modules/ak-ai-news/services/ai-translation-service.uts
Normal file
562
uni_modules/ak-ai-news/services/ai-translation-service.uts
Normal file
@@ -0,0 +1,562 @@
|
||||
// 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}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user