Initial commit of akmon project
This commit is contained in:
866
uni_modules/ak-ai-news/services/AIChatService.uts
Normal file
866
uni_modules/ak-ai-news/services/AIChatService.uts
Normal file
@@ -0,0 +1,866 @@
|
||||
// AI Chat Service - Multilingual news assistant and conversation management
|
||||
|
||||
import {
|
||||
ChatMessage,
|
||||
ChatSession,
|
||||
AIProvider,
|
||||
AIResponse,
|
||||
AIServiceConfig,
|
||||
AIServiceError,
|
||||
ContentInfo
|
||||
} from '../types/ai-types.uts'
|
||||
|
||||
// 聊天配置选项
|
||||
type ChatOptions = {
|
||||
provider?: AIProvider
|
||||
model?: string
|
||||
temperature?: number
|
||||
maxTokens?: number
|
||||
language?: string
|
||||
systemPrompt?: string
|
||||
contextWindow?: number
|
||||
streamResponse?: boolean
|
||||
}
|
||||
|
||||
// 会话上下文
|
||||
type SessionContext = {
|
||||
recentNews?: ContentInfo[]
|
||||
userPreferences?: UserPreferences
|
||||
conversationHistory: ChatMessage[]
|
||||
currentTopic?: string
|
||||
activeLanguage: string
|
||||
}
|
||||
|
||||
// 用户偏好
|
||||
type UserPreferences = {
|
||||
preferredLanguages: string[]
|
||||
preferredCategories: string[]
|
||||
preferredSources: string[]
|
||||
newsStyle: 'brief' | 'detailed' | 'analytical'
|
||||
updateFrequency: 'realtime' | 'hourly' | 'daily'
|
||||
}
|
||||
|
||||
// 聊天统计
|
||||
type ChatStats = {
|
||||
totalSessions: number
|
||||
totalMessages: number
|
||||
avgSessionLength: number
|
||||
avgResponseTime: number
|
||||
totalCost: number
|
||||
languageDistribution: Record<string, number>
|
||||
topQuestionTypes: Record<string, number>
|
||||
}
|
||||
|
||||
// 预定义的聊天模板
|
||||
type ChatTemplate = {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
systemPrompt: string
|
||||
language: string
|
||||
category: string
|
||||
}
|
||||
|
||||
/**
|
||||
* AI聊天服务类
|
||||
* 提供多语言新闻助手功能,包括会话管理、上下文理解、个性化回复等
|
||||
*/
|
||||
export class AIChatService {
|
||||
private config: AIServiceConfig
|
||||
private activeSessions: Map<string, SessionContext> = new Map()
|
||||
private chatTemplates: ChatTemplate[] = []
|
||||
private stats: ChatStats = {
|
||||
totalSessions: 0,
|
||||
totalMessages: 0,
|
||||
avgSessionLength: 0,
|
||||
avgResponseTime: 0,
|
||||
totalCost: 0,
|
||||
languageDistribution: {},
|
||||
topQuestionTypes: {}
|
||||
}
|
||||
|
||||
constructor(config: AIServiceConfig) {
|
||||
this.config = config
|
||||
this.initializeChatTemplates()
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新的聊天会话
|
||||
* @param userId 用户ID
|
||||
* @param language 语言
|
||||
* @param options 聊天选项
|
||||
*/
|
||||
async createChatSession(
|
||||
userId: string,
|
||||
language: string = 'zh-CN',
|
||||
options: ChatOptions = {}
|
||||
): Promise<AIResponse<ChatSession>> {
|
||||
try {
|
||||
const sessionId = this.generateSessionId(userId)
|
||||
const session: ChatSession = {
|
||||
id: sessionId,
|
||||
userId,
|
||||
name: this.generateSessionName(language),
|
||||
language,
|
||||
aiModel: options.model || this.getDefaultModel(options.provider),
|
||||
contextSettings: {
|
||||
temperature: options.temperature || 0.7,
|
||||
maxTokens: options.maxTokens || 1000,
|
||||
contextWindow: options.contextWindow || 10
|
||||
},
|
||||
totalMessages: 0,
|
||||
totalTokensUsed: 0,
|
||||
totalCostUSD: 0,
|
||||
isActive: true,
|
||||
startedAt: Date.now(),
|
||||
lastMessageAt: Date.now()
|
||||
}
|
||||
|
||||
// 初始化会话上下文
|
||||
const context: SessionContext = {
|
||||
conversationHistory: [],
|
||||
activeLanguage: language,
|
||||
currentTopic: undefined,
|
||||
recentNews: [],
|
||||
userPreferences: await this.loadUserPreferences(userId)
|
||||
}
|
||||
|
||||
this.activeSessions.set(sessionId, context)
|
||||
this.stats.totalSessions++
|
||||
|
||||
// 发送欢迎消息
|
||||
const welcomeMessage = await this.generateWelcomeMessage(language)
|
||||
await this.addSystemMessage(sessionId, welcomeMessage, language)
|
||||
|
||||
return { success: true, data: session }
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Failed to create chat session'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
* @param sessionId 会话ID
|
||||
* @param message 用户消息
|
||||
* @param options 聊天选项
|
||||
*/
|
||||
async sendMessage(
|
||||
sessionId: string,
|
||||
message: string,
|
||||
options: ChatOptions = {}
|
||||
): Promise<AIResponse<ChatMessage>> {
|
||||
try {
|
||||
const context = this.activeSessions.get(sessionId)
|
||||
if (!context) {
|
||||
throw new Error('Session not found')
|
||||
}
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
// 添加用户消息到历史
|
||||
const userMessage: ChatMessage = {
|
||||
id: this.generateMessageId(),
|
||||
sessionId,
|
||||
type: 'user',
|
||||
content: message,
|
||||
language: context.activeLanguage,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
context.conversationHistory.push(userMessage)
|
||||
this.stats.totalMessages++
|
||||
|
||||
// 分析用户意图
|
||||
const intent = await this.analyzeUserIntent(message, context)
|
||||
|
||||
// 生成AI回复
|
||||
const provider = options.provider || this.selectBestProvider()
|
||||
const response = await this.generateAIResponse(message, context, intent, provider, options)
|
||||
|
||||
const processingTime = Date.now() - startTime
|
||||
|
||||
// 创建助手消息
|
||||
const assistantMessage: ChatMessage = {
|
||||
id: this.generateMessageId(),
|
||||
sessionId,
|
||||
type: 'assistant',
|
||||
content: response.content,
|
||||
language: context.activeLanguage,
|
||||
timestamp: Date.now(),
|
||||
responseTimeMs: processingTime,
|
||||
tokensUsed: response.tokensUsed,
|
||||
costUSD: response.cost
|
||||
}
|
||||
|
||||
// 添加到历史并更新上下文
|
||||
context.conversationHistory.push(assistantMessage)
|
||||
this.updateSessionContext(context, message, response.content, intent)
|
||||
|
||||
// 保持上下文窗口大小
|
||||
this.trimContextHistory(context, options.contextWindow || 10)
|
||||
|
||||
// 更新统计
|
||||
this.updateChatStats(processingTime, response.tokensUsed || 0, response.cost || 0, context.activeLanguage, intent)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: assistantMessage,
|
||||
processingTimeMs: processingTime,
|
||||
tokensUsed: response.tokensUsed,
|
||||
costUSD: response.cost,
|
||||
provider
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage: ChatMessage = {
|
||||
id: this.generateMessageId(),
|
||||
sessionId,
|
||||
type: 'error',
|
||||
content: this.getErrorMessage(error.message, this.activeSessions.get(sessionId)?.activeLanguage || 'zh-CN'),
|
||||
language: this.activeSessions.get(sessionId)?.activeLanguage || 'zh-CN',
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
data: errorMessage,
|
||||
error: error.message || 'Failed to send message'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话历史
|
||||
* @param sessionId 会话ID
|
||||
* @param limit 消息数量限制
|
||||
*/
|
||||
getChatHistory(sessionId: string, limit: number = 50): ChatMessage[] {
|
||||
const context = this.activeSessions.get(sessionId)
|
||||
if (!context) return []
|
||||
|
||||
return context.conversationHistory.slice(-limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话语言
|
||||
* @param sessionId 会话ID
|
||||
* @param language 新语言
|
||||
*/
|
||||
async switchLanguage(sessionId: string, language: string): Promise<AIResponse<boolean>> {
|
||||
try {
|
||||
const context = this.activeSessions.get(sessionId)
|
||||
if (!context) {
|
||||
throw new Error('Session not found')
|
||||
}
|
||||
|
||||
const oldLanguage = context.activeLanguage
|
||||
context.activeLanguage = language
|
||||
|
||||
// 发送语言切换确认消息
|
||||
const confirmMessage = this.getLanguageSwitchMessage(oldLanguage, language)
|
||||
await this.addSystemMessage(sessionId, confirmMessage, language)
|
||||
|
||||
return { success: true, data: true }
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Failed to switch language'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置会话的新闻上下文
|
||||
* @param sessionId 会话ID
|
||||
* @param newsItems 新闻列表
|
||||
*/
|
||||
setNewsContext(sessionId: string, newsItems: ContentInfo[]): void {
|
||||
const context = this.activeSessions.get(sessionId)
|
||||
if (context) {
|
||||
context.recentNews = newsItems.slice(0, 10) // 保留最近10条新闻
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取推荐问题
|
||||
* @param sessionId 会话ID
|
||||
* @param category 新闻分类
|
||||
*/
|
||||
getSuggestedQuestions(sessionId: string, category?: string): string[] {
|
||||
const context = this.activeSessions.get(sessionId)
|
||||
if (!context) return []
|
||||
|
||||
const language = context.activeLanguage
|
||||
const baseQuestions = this.getBaseQuestions(language)
|
||||
|
||||
if (category && context.recentNews) {
|
||||
const categoryNews = context.recentNews.filter(news => news.categoryId === category)
|
||||
if (categoryNews.length > 0) {
|
||||
return this.generateCategoryQuestions(categoryNews, language)
|
||||
}
|
||||
}
|
||||
|
||||
return baseQuestions
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束会话
|
||||
* @param sessionId 会话ID
|
||||
*/
|
||||
async endChatSession(sessionId: string): Promise<AIResponse<ChatSession>> {
|
||||
try {
|
||||
const context = this.activeSessions.get(sessionId)
|
||||
if (!context) {
|
||||
throw new Error('Session not found')
|
||||
}
|
||||
|
||||
// 生成会话总结
|
||||
const summary = await this.generateSessionSummary(context)
|
||||
|
||||
// 添加结束消息
|
||||
const endMessage = this.getSessionEndMessage(context.activeLanguage)
|
||||
await this.addSystemMessage(sessionId, endMessage, context.activeLanguage)
|
||||
|
||||
// 更新会话状态
|
||||
const session: ChatSession = {
|
||||
id: sessionId,
|
||||
userId: '', // 需要从其他地方获取
|
||||
name: this.generateSessionName(context.activeLanguage),
|
||||
language: context.activeLanguage,
|
||||
aiModel: '',
|
||||
contextSettings: {},
|
||||
totalMessages: context.conversationHistory.length,
|
||||
totalTokensUsed: this.calculateTotalTokens(context),
|
||||
totalCostUSD: this.calculateTotalCost(context),
|
||||
isActive: false,
|
||||
startedAt: context.conversationHistory[0]?.timestamp || Date.now(),
|
||||
lastMessageAt: Date.now(),
|
||||
endedAt: Date.now()
|
||||
}
|
||||
|
||||
// 清理活动会话
|
||||
this.activeSessions.delete(sessionId)
|
||||
|
||||
return { success: true, data: session }
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Failed to end chat session'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天统计
|
||||
*/
|
||||
getChatStatistics(): ChatStats {
|
||||
return { ...this.stats }
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
private async analyzeUserIntent(message: string, context: SessionContext): Promise<string> {
|
||||
// 简单的意图识别
|
||||
const lowerMessage = message.toLowerCase()
|
||||
|
||||
if (lowerMessage.includes('新闻') || lowerMessage.includes('news')) {
|
||||
if (lowerMessage.includes('最新') || lowerMessage.includes('latest')) return 'latest_news'
|
||||
if (lowerMessage.includes('推荐') || lowerMessage.includes('recommend')) return 'recommend_news'
|
||||
if (lowerMessage.includes('搜索') || lowerMessage.includes('search')) return 'search_news'
|
||||
return 'general_news'
|
||||
}
|
||||
|
||||
if (lowerMessage.includes('翻译') || lowerMessage.includes('translate')) return 'translate'
|
||||
if (lowerMessage.includes('总结') || lowerMessage.includes('summary')) return 'summarize'
|
||||
if (lowerMessage.includes('分析') || lowerMessage.includes('analyze')) return 'analyze'
|
||||
if (lowerMessage.includes('解释') || lowerMessage.includes('explain')) return 'explain'
|
||||
if (lowerMessage.includes('比较') || lowerMessage.includes('compare')) return 'compare'
|
||||
|
||||
return 'general_chat'
|
||||
}
|
||||
|
||||
private async generateAIResponse(
|
||||
message: string,
|
||||
context: SessionContext,
|
||||
intent: string,
|
||||
provider: AIProvider,
|
||||
options: ChatOptions
|
||||
): Promise<{ content: string, tokensUsed?: number, cost?: number }> {
|
||||
|
||||
const systemPrompt = this.buildSystemPrompt(context, intent)
|
||||
const contextMessages = this.buildContextMessages(context, options.contextWindow || 10)
|
||||
|
||||
switch (provider) {
|
||||
case 'openai':
|
||||
return await this.generateResponseWithOpenAI(message, systemPrompt, contextMessages, options)
|
||||
case 'google':
|
||||
return await this.generateResponseWithGoogle(message, systemPrompt, contextMessages, options)
|
||||
case 'baidu':
|
||||
return await this.generateResponseWithBaidu(message, systemPrompt, contextMessages, options)
|
||||
default:
|
||||
return await this.generateBasicResponse(message, context, intent)
|
||||
}
|
||||
}
|
||||
|
||||
private buildSystemPrompt(context: SessionContext, intent: string): string {
|
||||
const language = context.activeLanguage
|
||||
const newsContext = context.recentNews?.length ? `当前有${context.recentNews.length}条相关新闻可供参考。` : ''
|
||||
|
||||
let basePrompt = ''
|
||||
|
||||
if (language === 'zh-CN') {
|
||||
basePrompt = `你是一个专业的多语言新闻助手。你可以帮助用户了解最新新闻、分析新闻内容、回答相关问题,并提供翻译服务。${newsContext}`
|
||||
|
||||
switch (intent) {
|
||||
case 'latest_news':
|
||||
basePrompt += '用户想了解最新新闻,请提供简洁准确的新闻摘要。'
|
||||
break
|
||||
case 'recommend_news':
|
||||
basePrompt += '用户需要新闻推荐,请根据用户偏好推荐相关新闻。'
|
||||
break
|
||||
case 'search_news':
|
||||
basePrompt += '用户想搜索特定新闻,请帮助找到相关内容。'
|
||||
break
|
||||
case 'translate':
|
||||
basePrompt += '用户需要翻译服务,请提供准确的翻译。'
|
||||
break
|
||||
case 'analyze':
|
||||
basePrompt += '用户需要新闻分析,请提供客观深入的分析。'
|
||||
break
|
||||
default:
|
||||
basePrompt += '请提供有帮助的回复,保持友好和专业。'
|
||||
}
|
||||
} else {
|
||||
basePrompt = `You are a professional multilingual news assistant. You can help users understand the latest news, analyze news content, answer related questions, and provide translation services. ${newsContext}`
|
||||
}
|
||||
|
||||
return basePrompt
|
||||
}
|
||||
|
||||
private buildContextMessages(context: SessionContext, windowSize: number): ChatMessage[] {
|
||||
return context.conversationHistory.slice(-windowSize * 2) // 用户和助手消息
|
||||
}
|
||||
|
||||
private async generateResponseWithOpenAI(
|
||||
message: string,
|
||||
systemPrompt: string,
|
||||
contextMessages: ChatMessage[],
|
||||
options: ChatOptions
|
||||
): Promise<{ content: string, tokensUsed: number, cost: number }> {
|
||||
|
||||
// 构建消息数组
|
||||
const messages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
...contextMessages.map(msg => ({
|
||||
role: msg.type === 'user' ? 'user' : 'assistant',
|
||||
content: msg.content
|
||||
})),
|
||||
{ role: 'user', content: message }
|
||||
]
|
||||
|
||||
// 模拟OpenAI API调用
|
||||
await this.delay(Math.random() * 1000 + 500)
|
||||
|
||||
const responseContent = await this.generateMockResponse(message, systemPrompt, options.language || 'zh-CN')
|
||||
const tokensUsed = this.estimateTokens(messages) + this.estimateTokens([{ content: responseContent }])
|
||||
const cost = this.calculateOpenAICost(tokensUsed, options.model || 'gpt-3.5-turbo')
|
||||
|
||||
return {
|
||||
content: responseContent,
|
||||
tokensUsed,
|
||||
cost
|
||||
}
|
||||
}
|
||||
|
||||
private async generateResponseWithGoogle(
|
||||
message: string,
|
||||
systemPrompt: string,
|
||||
contextMessages: ChatMessage[],
|
||||
options: ChatOptions
|
||||
): Promise<{ content: string, tokensUsed: number, cost: number }> {
|
||||
|
||||
await this.delay(Math.random() * 800 + 400)
|
||||
|
||||
const responseContent = await this.generateMockResponse(message, systemPrompt, options.language || 'zh-CN')
|
||||
const tokensUsed = this.estimateTokens([{ content: message }, { content: responseContent }])
|
||||
const cost = this.calculateGoogleCost(tokensUsed)
|
||||
|
||||
return {
|
||||
content: responseContent,
|
||||
tokensUsed,
|
||||
cost
|
||||
}
|
||||
}
|
||||
|
||||
private async generateResponseWithBaidu(
|
||||
message: string,
|
||||
systemPrompt: string,
|
||||
contextMessages: ChatMessage[],
|
||||
options: ChatOptions
|
||||
): Promise<{ content: string, tokensUsed: number, cost: number }> {
|
||||
|
||||
await this.delay(Math.random() * 600 + 300)
|
||||
|
||||
const responseContent = await this.generateMockResponse(message, systemPrompt, options.language || 'zh-CN')
|
||||
const tokensUsed = this.estimateTokens([{ content: message }, { content: responseContent }])
|
||||
const cost = this.calculateBaiduCost(tokensUsed)
|
||||
|
||||
return {
|
||||
content: responseContent,
|
||||
tokensUsed,
|
||||
cost
|
||||
}
|
||||
}
|
||||
|
||||
private async generateBasicResponse(
|
||||
message: string,
|
||||
context: SessionContext,
|
||||
intent: string
|
||||
): Promise<{ content: string }> {
|
||||
|
||||
const language = context.activeLanguage
|
||||
|
||||
// 基础响应生成
|
||||
switch (intent) {
|
||||
case 'latest_news':
|
||||
return { content: this.getLatestNewsResponse(context) }
|
||||
case 'recommend_news':
|
||||
return { content: this.getRecommendNewsResponse(context) }
|
||||
case 'general_chat':
|
||||
return { content: this.getGeneralChatResponse(message, language) }
|
||||
default:
|
||||
return { content: this.getDefaultResponse(language) }
|
||||
}
|
||||
}
|
||||
|
||||
private async generateMockResponse(message: string, systemPrompt: string, language: string): Promise<string> {
|
||||
// 模拟AI响应生成
|
||||
const responses = {
|
||||
'zh-CN': [
|
||||
'我理解您的问题。根据最新的新闻信息,我可以为您提供以下回复:',
|
||||
'这是一个很有趣的问题。让我为您分析一下相关情况:',
|
||||
'基于当前的新闻数据和分析,我的建议是:',
|
||||
'感谢您的提问。关于这个话题,我可以分享以下见解:'
|
||||
],
|
||||
'en': [
|
||||
'I understand your question. Based on the latest news information, I can provide the following response:',
|
||||
'That\'s an interesting question. Let me analyze the relevant situation for you:',
|
||||
'Based on current news data and analysis, my recommendation is:',
|
||||
'Thank you for your question. Regarding this topic, I can share the following insights:'
|
||||
]
|
||||
}
|
||||
|
||||
const langResponses = responses[language] || responses['zh-CN']
|
||||
const baseResponse = langResponses[Math.floor(Math.random() * langResponses.length)]
|
||||
|
||||
// 添加针对消息的具体回复
|
||||
return `${baseResponse}\n\n关于"${message.substring(0, 50)}${message.length > 50 ? '...' : ''}",我建议您关注相关的最新发展和官方信息。如果您需要更具体的信息或有其他问题,请随时告诉我。`
|
||||
}
|
||||
|
||||
private getLatestNewsResponse(context: SessionContext): string {
|
||||
if (!context.recentNews || context.recentNews.length === 0) {
|
||||
return context.activeLanguage === 'zh-CN'
|
||||
? '抱歉,目前没有最新的新闻信息。请稍后再试或询问其他问题。'
|
||||
: 'Sorry, there is no latest news information available at the moment. Please try again later or ask other questions.'
|
||||
}
|
||||
|
||||
const language = context.activeLanguage
|
||||
const news = context.recentNews.slice(0, 3)
|
||||
|
||||
if (language === 'zh-CN') {
|
||||
let response = '以下是最新的新闻信息:\n\n'
|
||||
news.forEach((item, index) => {
|
||||
response += `${index + 1}. ${item.title}\n${item.summary || item.content.substring(0, 100)}...\n\n`
|
||||
})
|
||||
return response
|
||||
} else {
|
||||
let response = 'Here is the latest news information:\n\n'
|
||||
news.forEach((item, index) => {
|
||||
response += `${index + 1}. ${item.title}\n${item.summary || item.content.substring(0, 100)}...\n\n`
|
||||
})
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
private getRecommendNewsResponse(context: SessionContext): string {
|
||||
const language = context.activeLanguage
|
||||
|
||||
if (language === 'zh-CN') {
|
||||
return '根据您的兴趣和阅读历史,我为您推荐以下新闻:\n\n1. 科技领域的最新发展\n2. 国际时事动态\n3. 经济政策解读\n\n如果您想了解特定领域的新闻,请告诉我您感兴趣的类别。'
|
||||
} else {
|
||||
return 'Based on your interests and reading history, I recommend the following news for you:\n\n1. Latest developments in technology\n2. International current affairs\n3. Economic policy analysis\n\nIf you want to know about news in a specific field, please tell me the category you are interested in.'
|
||||
}
|
||||
}
|
||||
|
||||
private getGeneralChatResponse(message: string, language: string): string {
|
||||
if (language === 'zh-CN') {
|
||||
return '我是您的新闻助手,可以帮助您了解最新新闻、分析新闻内容、提供翻译服务等。请告诉我您想了解什么,我会尽力为您提供帮助。'
|
||||
} else {
|
||||
return 'I am your news assistant and can help you understand the latest news, analyze news content, provide translation services, etc. Please tell me what you would like to know and I will do my best to help you.'
|
||||
}
|
||||
}
|
||||
|
||||
private getDefaultResponse(language: string): string {
|
||||
if (language === 'zh-CN') {
|
||||
return '感谢您的问题。作为您的新闻助手,我可以帮助您获取最新新闻、分析新闻内容、翻译文本等。请告诉我您需要什么帮助。'
|
||||
} else {
|
||||
return 'Thank you for your question. As your news assistant, I can help you get the latest news, analyze news content, translate text, etc. Please tell me what help you need.'
|
||||
}
|
||||
}
|
||||
|
||||
private async addSystemMessage(sessionId: string, content: string, language: string): Promise<void> {
|
||||
const context = this.activeSessions.get(sessionId)
|
||||
if (!context) return
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
id: this.generateMessageId(),
|
||||
sessionId,
|
||||
type: 'system',
|
||||
content,
|
||||
language,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
context.conversationHistory.push(systemMessage)
|
||||
}
|
||||
|
||||
private updateSessionContext(context: SessionContext, userMessage: string, assistantResponse: string, intent: string): void {
|
||||
// 更新当前话题
|
||||
if (intent !== 'general_chat') {
|
||||
context.currentTopic = intent
|
||||
}
|
||||
|
||||
// 分析和更新用户偏好(简化版)
|
||||
if (userMessage.includes('科技') || userMessage.includes('technology')) {
|
||||
if (!context.userPreferences) context.userPreferences = { preferredLanguages: [], preferredCategories: [], preferredSources: [], newsStyle: 'brief', updateFrequency: 'daily' }
|
||||
if (!context.userPreferences.preferredCategories.includes('technology')) {
|
||||
context.userPreferences.preferredCategories.push('technology')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private trimContextHistory(context: SessionContext, windowSize: number): void {
|
||||
if (context.conversationHistory.length > windowSize * 2) {
|
||||
// 保留系统消息和最近的对话
|
||||
const systemMessages = context.conversationHistory.filter(msg => msg.type === 'system')
|
||||
const recentMessages = context.conversationHistory.slice(-windowSize * 2)
|
||||
context.conversationHistory = [...systemMessages.slice(-2), ...recentMessages]
|
||||
}
|
||||
}
|
||||
|
||||
private async generateWelcomeMessage(language: string): Promise<string> {
|
||||
if (language === 'zh-CN') {
|
||||
return '您好!我是您的智能新闻助手。我可以帮助您:\n\n🔍 获取最新新闻\n📊 分析新闻内容\n🌐 翻译新闻文本\n💡 推荐相关新闻\n\n请告诉我您想了解什么,让我们开始对话吧!'
|
||||
} else {
|
||||
return 'Hello! I am your intelligent news assistant. I can help you:\n\n🔍 Get the latest news\n📊 Analyze news content\n🌐 Translate news text\n💡 Recommend related news\n\nPlease tell me what you want to know and let\'s start the conversation!'
|
||||
}
|
||||
}
|
||||
|
||||
private generateSessionName(language: string): string {
|
||||
const now = new Date()
|
||||
const timeStr = `${now.getHours()}:${now.getMinutes().toString().padStart(2, '0')}`
|
||||
|
||||
if (language === 'zh-CN') {
|
||||
return `新闻对话 ${timeStr}`
|
||||
} else {
|
||||
return `News Chat ${timeStr}`
|
||||
}
|
||||
}
|
||||
|
||||
private getLanguageSwitchMessage(oldLang: string, newLang: string): string {
|
||||
if (newLang === 'zh-CN') {
|
||||
return '语言已切换为中文。我会用中文继续为您服务。'
|
||||
} else {
|
||||
return 'Language has been switched to English. I will continue to serve you in English.'
|
||||
}
|
||||
}
|
||||
|
||||
private getSessionEndMessage(language: string): string {
|
||||
if (language === 'zh-CN') {
|
||||
return '感谢您使用新闻助手服务!如果您还有其他问题,随时可以开始新的对话。祝您有美好的一天!'
|
||||
} else {
|
||||
return 'Thank you for using the news assistant service! If you have any other questions, you can start a new conversation at any time. Have a great day!'
|
||||
}
|
||||
}
|
||||
|
||||
private getErrorMessage(error: string, language: string): string {
|
||||
if (language === 'zh-CN') {
|
||||
return '抱歉,处理您的请求时出现了问题。请稍后重试或联系技术支持。'
|
||||
} else {
|
||||
return 'Sorry, there was a problem processing your request. Please try again later or contact technical support.'
|
||||
}
|
||||
}
|
||||
|
||||
private getBaseQuestions(language: string): string[] {
|
||||
if (language === 'zh-CN') {
|
||||
return [
|
||||
'今天有什么重要新闻?',
|
||||
'请推荐一些科技新闻',
|
||||
'最新的经济动态如何?',
|
||||
'有什么国际新闻值得关注?',
|
||||
'帮我分析这条新闻的影响',
|
||||
'翻译这段英文新闻'
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
'What important news is there today?',
|
||||
'Please recommend some technology news',
|
||||
'How are the latest economic developments?',
|
||||
'What international news is worth paying attention to?',
|
||||
'Help me analyze the impact of this news',
|
||||
'Translate this Chinese news'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
private generateCategoryQuestions(news: ContentInfo[], language: string): string[] {
|
||||
if (language === 'zh-CN') {
|
||||
return [
|
||||
`分析一下"${news[0].title}"这条新闻`,
|
||||
`这类新闻的趋势如何?`,
|
||||
`相关的新闻还有哪些?`,
|
||||
`这对普通人有什么影响?`
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
`Analyze the news "${news[0].title}"`,
|
||||
`What are the trends in this type of news?`,
|
||||
`What other related news are there?`,
|
||||
`What impact does this have on ordinary people?`
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
private async generateSessionSummary(context: SessionContext): Promise<string> {
|
||||
const messageCount = context.conversationHistory.length
|
||||
const topics = new Set(context.conversationHistory
|
||||
.filter(msg => msg.type === 'user')
|
||||
.map(msg => this.extractTopicFromMessage(msg.content))
|
||||
)
|
||||
|
||||
const language = context.activeLanguage
|
||||
|
||||
if (language === 'zh-CN') {
|
||||
return `本次对话共${messageCount}条消息,主要讨论了${Array.from(topics).join('、')}等话题。`
|
||||
} else {
|
||||
return `This conversation had ${messageCount} messages and mainly discussed topics such as ${Array.from(topics).join(', ')}.`
|
||||
}
|
||||
}
|
||||
|
||||
private extractTopicFromMessage(message: string): string {
|
||||
// 简单的话题提取
|
||||
if (message.includes('新闻') || message.includes('news')) return '新闻'
|
||||
if (message.includes('科技') || message.includes('technology')) return '科技'
|
||||
if (message.includes('经济') || message.includes('economy')) return '经济'
|
||||
if (message.includes('政治') || message.includes('politics')) return '政治'
|
||||
return '一般话题'
|
||||
}
|
||||
|
||||
private calculateTotalTokens(context: SessionContext): number {
|
||||
return context.conversationHistory
|
||||
.filter(msg => msg.tokensUsed)
|
||||
.reduce((total, msg) => total + (msg.tokensUsed || 0), 0)
|
||||
}
|
||||
|
||||
private calculateTotalCost(context: SessionContext): number {
|
||||
return context.conversationHistory
|
||||
.filter(msg => msg.costUSD)
|
||||
.reduce((total, msg) => total + (msg.costUSD || 0), 0)
|
||||
}
|
||||
|
||||
private updateChatStats(processingTime: number, tokens: number, cost: number, language: string, intent: string): void {
|
||||
this.stats.totalMessages++
|
||||
this.stats.avgResponseTime = (this.stats.avgResponseTime * (this.stats.totalMessages - 1) + processingTime) / this.stats.totalMessages
|
||||
this.stats.totalCost += cost
|
||||
|
||||
this.stats.languageDistribution[language] = (this.stats.languageDistribution[language] || 0) + 1
|
||||
this.stats.topQuestionTypes[intent] = (this.stats.topQuestionTypes[intent] || 0) + 1
|
||||
}
|
||||
|
||||
private estimateTokens(messages: Array<{ content: string }>): number {
|
||||
return messages.reduce((total, msg) => total + Math.ceil(msg.content.length / 4), 0)
|
||||
}
|
||||
|
||||
private calculateOpenAICost(tokens: number, model: string): number {
|
||||
const pricing: Record<string, { input: number, output: number }> = {
|
||||
'gpt-3.5-turbo': { input: 0.0015, output: 0.002 },
|
||||
'gpt-4': { input: 0.03, output: 0.06 }
|
||||
}
|
||||
const modelPricing = pricing[model] || pricing['gpt-3.5-turbo']
|
||||
return (tokens / 1000) * ((modelPricing.input + modelPricing.output) / 2)
|
||||
}
|
||||
|
||||
private calculateGoogleCost(tokens: number): number {
|
||||
return (tokens / 1000) * 0.01 // 估算
|
||||
}
|
||||
|
||||
private calculateBaiduCost(tokens: number): number {
|
||||
return (tokens / 1000) * 0.008 // 估算
|
||||
}
|
||||
|
||||
private selectBestProvider(): AIProvider {
|
||||
if (this.config.openai?.apiKey) return 'openai'
|
||||
if (this.config.google?.apiKey) return 'google'
|
||||
if (this.config.baidu?.apiKey) return 'baidu'
|
||||
return 'openai'
|
||||
}
|
||||
|
||||
private getDefaultModel(provider?: AIProvider): string {
|
||||
switch (provider) {
|
||||
case 'openai': return 'gpt-3.5-turbo'
|
||||
case 'google': return 'gemini-pro'
|
||||
case 'baidu': return 'ernie-bot'
|
||||
default: return 'gpt-3.5-turbo'
|
||||
}
|
||||
}
|
||||
|
||||
private generateSessionId(userId: string): string {
|
||||
return `session_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
|
||||
private generateMessageId(): string {
|
||||
return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
|
||||
private async loadUserPreferences(userId: string): Promise<UserPreferences | undefined> {
|
||||
// 模拟加载用户偏好
|
||||
return {
|
||||
preferredLanguages: ['zh-CN'],
|
||||
preferredCategories: ['technology', 'economy'],
|
||||
preferredSources: [],
|
||||
newsStyle: 'brief',
|
||||
updateFrequency: 'daily'
|
||||
}
|
||||
}
|
||||
|
||||
private async delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
private initializeChatTemplates(): void {
|
||||
this.chatTemplates = [
|
||||
{
|
||||
id: 'news_assistant',
|
||||
name: '新闻助手',
|
||||
description: '专业的新闻分析和推荐助手',
|
||||
systemPrompt: '你是一个专业的新闻助手,可以帮助用户了解最新新闻、分析新闻内容、提供翻译服务。',
|
||||
language: 'zh-CN',
|
||||
category: 'news'
|
||||
},
|
||||
{
|
||||
id: 'tech_news',
|
||||
name: '科技新闻专家',
|
||||
description: '专注于科技新闻的分析和解读',
|
||||
systemPrompt: '你是一个科技新闻专家,专门分析科技行业动态、创新趋势和技术发展。',
|
||||
language: 'zh-CN',
|
||||
category: 'technology'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
806
uni_modules/ak-ai-news/services/AIContentAnalysisService.uts
Normal file
806
uni_modules/ak-ai-news/services/AIContentAnalysisService.uts
Normal file
@@ -0,0 +1,806 @@
|
||||
// AI Content Analysis Service - Content classification, sentiment analysis, and quality assessment
|
||||
|
||||
import {
|
||||
ContentAnalysisResult,
|
||||
EntityResult,
|
||||
TopicResult,
|
||||
CategoryResult,
|
||||
AIProvider,
|
||||
AIResponse,
|
||||
AIServiceConfig,
|
||||
BatchProcessingOptions,
|
||||
AIServiceError,
|
||||
ContentInfo
|
||||
} from '../types/ai-types.uts'
|
||||
|
||||
// 分析类型枚举
|
||||
type AnalysisType = 'sentiment' | 'entities' | 'topics' | 'categories' | 'readability' | 'credibility' | 'toxicity' | 'summary' | 'keywords'
|
||||
|
||||
// 分析选项
|
||||
type AnalysisOptions = {
|
||||
types: AnalysisType[]
|
||||
provider?: AIProvider
|
||||
model?: string
|
||||
includeScores?: boolean
|
||||
detailedResults?: boolean
|
||||
language?: string
|
||||
customCategories?: string[]
|
||||
}
|
||||
|
||||
// 内容质量评估结果
|
||||
type QualityAssessment = {
|
||||
overallScore: number
|
||||
factualAccuracy: number
|
||||
sourceReliability: number
|
||||
writingQuality: number
|
||||
objectivity: number
|
||||
completeness: number
|
||||
timeliness: number
|
||||
relevance: number
|
||||
}
|
||||
|
||||
// 关键词提取结果
|
||||
type KeywordResult = {
|
||||
keyword: string
|
||||
frequency: number
|
||||
importance: number
|
||||
type: 'noun' | 'verb' | 'adjective' | 'entity' | 'concept'
|
||||
}
|
||||
|
||||
// 分析统计
|
||||
type AnalysisStats = {
|
||||
totalAnalyses: number
|
||||
successCount: number
|
||||
errorCount: number
|
||||
avgProcessingTimeMs: number
|
||||
totalCost: number
|
||||
byProvider: Record<AIProvider, number>
|
||||
byAnalysisType: Record<AnalysisType, number>
|
||||
}
|
||||
|
||||
/**
|
||||
* AI内容分析服务类
|
||||
* 提供情感分析、实体识别、主题提取、内容分类、质量评估等功能
|
||||
*/
|
||||
export class AIContentAnalysisService {
|
||||
private config: AIServiceConfig
|
||||
private stats: AnalysisStats = {
|
||||
totalAnalyses: 0,
|
||||
successCount: 0,
|
||||
errorCount: 0,
|
||||
avgProcessingTimeMs: 0,
|
||||
totalCost: 0,
|
||||
byProvider: {} as Record<AIProvider, number>,
|
||||
byAnalysisType: {} as Record<AnalysisType, number>
|
||||
}
|
||||
|
||||
// 预定义的新闻分类
|
||||
private readonly NEWS_CATEGORIES = [
|
||||
{ id: 'politics', name: '政治', keywords: ['政府', '政策', '选举', '法律', '议会', '总统', '部长'] },
|
||||
{ id: 'economy', name: '经济', keywords: ['经济', '金融', '股市', '投资', '银行', '贸易', 'GDP'] },
|
||||
{ id: 'technology', name: '科技', keywords: ['科技', '人工智能', '互联网', '软件', '硬件', '创新', '数字化'] },
|
||||
{ id: 'sports', name: '体育', keywords: ['体育', '足球', '篮球', '奥运', '比赛', '运动员', '锦标赛'] },
|
||||
{ id: 'entertainment', name: '娱乐', keywords: ['娱乐', '电影', '音乐', '明星', '综艺', '演出', '艺术'] },
|
||||
{ id: 'health', name: '健康', keywords: ['健康', '医疗', '病毒', '疫苗', '医院', '药物', '疾病'] },
|
||||
{ id: 'education', name: '教育', keywords: ['教育', '学校', '大学', '学生', '教师', '考试', '学习'] },
|
||||
{ id: 'environment', name: '环境', keywords: ['环境', '气候', '污染', '环保', '生态', '绿色', '可持续'] },
|
||||
{ id: 'international', name: '国际', keywords: ['国际', '外交', '战争', '和平', '联合国', '条约', '全球'] },
|
||||
{ id: 'social', name: '社会', keywords: ['社会', '社区', '公益', '慈善', '志愿者', '文化', '传统'] }
|
||||
]
|
||||
|
||||
constructor(config: AIServiceConfig) {
|
||||
this.config = config
|
||||
this.initializeStats()
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析内容
|
||||
* @param content 内容文本
|
||||
* @param options 分析选项
|
||||
*/
|
||||
async analyzeContent(
|
||||
content: string,
|
||||
options: AnalysisOptions = {
|
||||
types: ['sentiment', 'entities', 'topics', 'categories', 'readability', 'summary', 'keywords']
|
||||
}
|
||||
): Promise<AIResponse<ContentAnalysisResult>> {
|
||||
try {
|
||||
this.stats.totalAnalyses++
|
||||
const startTime = Date.now()
|
||||
|
||||
// 选择提供商
|
||||
const provider = options.provider || this.selectBestProvider()
|
||||
|
||||
// 执行各种分析
|
||||
const results = await Promise.allSettled([
|
||||
this.analyzeSentiment(content, provider, options),
|
||||
this.extractEntities(content, provider, options),
|
||||
this.extractTopics(content, provider, options),
|
||||
this.classifyContent(content, options),
|
||||
this.assessReadability(content, options.language),
|
||||
this.assessCredibility(content),
|
||||
this.assessToxicity(content, provider),
|
||||
this.generateSummary(content, provider, options),
|
||||
this.extractKeywords(content, options)
|
||||
])
|
||||
|
||||
const processingTime = Date.now() - startTime
|
||||
|
||||
// 合并结果
|
||||
const analysisResult: ContentAnalysisResult = {
|
||||
contentId: this.generateContentId(content),
|
||||
sentimentScore: this.extractResult(results[0], 0),
|
||||
sentimentLabel: this.getSentimentLabel(this.extractResult(results[0], 0)),
|
||||
readabilityScore: this.extractResult(results[4], 0.5),
|
||||
credibilityScore: this.extractResult(results[5], 0.5),
|
||||
toxicityScore: this.extractResult(results[6], 0),
|
||||
keywords: this.extractResult(results[8], []),
|
||||
entities: this.extractResult(results[1], []),
|
||||
topics: this.extractResult(results[2], []),
|
||||
categories: this.extractResult(results[3], []),
|
||||
summary: this.extractResult(results[7], ''),
|
||||
keyPhrases: this.extractKeyPhrases(content),
|
||||
language: options.language || await this.detectLanguage(content),
|
||||
processingTimeMs: processingTime,
|
||||
provider
|
||||
}
|
||||
|
||||
// 更新统计
|
||||
this.updateStats(provider, options.types, processingTime)
|
||||
this.stats.successCount++
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: analysisResult,
|
||||
processingTimeMs: processingTime,
|
||||
provider
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.stats.errorCount++
|
||||
const aiError: AIServiceError = {
|
||||
code: 'ANALYSIS_ERROR',
|
||||
message: error.message || 'Content analysis failed',
|
||||
provider: options.provider,
|
||||
retryable: this.isRetryableError(error)
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: aiError.message,
|
||||
errorCode: aiError.code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量内容分析
|
||||
* @param contents 内容数组
|
||||
* @param options 分析选项
|
||||
* @param batchOptions 批处理选项
|
||||
*/
|
||||
async analyzeContentBatch(
|
||||
contents: string[],
|
||||
options: AnalysisOptions = { types: ['sentiment', 'categories', 'summary'] },
|
||||
batchOptions: BatchProcessingOptions = {
|
||||
batchSize: 5,
|
||||
concurrency: 2,
|
||||
retryCount: 2,
|
||||
delayMs: 1000
|
||||
}
|
||||
): Promise<AIResponse<ContentAnalysisResult[]>> {
|
||||
try {
|
||||
const results: ContentAnalysisResult[] = []
|
||||
const batches = this.createBatches(contents, batchOptions.batchSize)
|
||||
|
||||
for (let i = 0; i < batches.length; i++) {
|
||||
const batch = batches[i]
|
||||
const batchPromises = batch.map(async (content) => {
|
||||
try {
|
||||
const response = await this.analyzeContent(content, options)
|
||||
if (response.success && response.data) {
|
||||
return response.data
|
||||
}
|
||||
throw new Error(response.error || 'Analysis failed')
|
||||
} catch (error) {
|
||||
if (batchOptions.onError) {
|
||||
batchOptions.onError(error, content)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
const batchResults = await Promise.allSettled(batchPromises)
|
||||
|
||||
for (const result of batchResults) {
|
||||
if (result.status === 'fulfilled') {
|
||||
results.push(result.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 进度回调
|
||||
if (batchOptions.onProgress) {
|
||||
batchOptions.onProgress(results.length, contents.length)
|
||||
}
|
||||
|
||||
// 批次间延迟
|
||||
if (i < batches.length - 1 && batchOptions.delayMs > 0) {
|
||||
await this.delay(batchOptions.delayMs)
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, data: results }
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Batch analysis failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 质量评估
|
||||
* @param content 内容文本
|
||||
* @param metadata 内容元数据
|
||||
*/
|
||||
async assessQuality(content: string, metadata?: Partial<ContentInfo>): Promise<AIResponse<QualityAssessment>> {
|
||||
try {
|
||||
const [
|
||||
factualScore,
|
||||
sourceScore,
|
||||
writingScore,
|
||||
objectivityScore,
|
||||
completenessScore,
|
||||
timelinessScore,
|
||||
relevanceScore
|
||||
] = await Promise.all([
|
||||
this.assessFactualAccuracy(content),
|
||||
this.assessSourceReliability(metadata?.sourceUrl || ''),
|
||||
this.assessWritingQuality(content),
|
||||
this.assessObjectivity(content),
|
||||
this.assessCompleteness(content),
|
||||
this.assessTimeliness(metadata?.publishedAt || Date.now()),
|
||||
this.assessRelevance(content, metadata?.categoryId)
|
||||
])
|
||||
|
||||
const overallScore = (
|
||||
factualScore + sourceScore + writingScore + objectivityScore +
|
||||
completenessScore + timelinessScore + relevanceScore
|
||||
) / 7
|
||||
|
||||
const assessment: QualityAssessment = {
|
||||
overallScore,
|
||||
factualAccuracy: factualScore,
|
||||
sourceReliability: sourceScore,
|
||||
writingQuality: writingScore,
|
||||
objectivity: objectivityScore,
|
||||
completeness: completenessScore,
|
||||
timeliness: timelinessScore,
|
||||
relevance: relevanceScore
|
||||
}
|
||||
|
||||
return { success: true, data: assessment }
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Quality assessment failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*/
|
||||
getStatistics(): AnalysisStats {
|
||||
return { ...this.stats }
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
private async analyzeSentiment(content: string, provider: AIProvider, options: AnalysisOptions): Promise<number> {
|
||||
if (!options.types.includes('sentiment')) return 0
|
||||
|
||||
switch (provider) {
|
||||
case 'openai':
|
||||
return await this.analyzeSentimentWithOpenAI(content)
|
||||
case 'google':
|
||||
return await this.analyzeSentimentWithGoogle(content)
|
||||
case 'baidu':
|
||||
return await this.analyzeSentimentWithBaidu(content)
|
||||
default:
|
||||
return this.analyzeSentimentBasic(content)
|
||||
}
|
||||
}
|
||||
|
||||
private async extractEntities(content: string, provider: AIProvider, options: AnalysisOptions): Promise<EntityResult[]> {
|
||||
if (!options.types.includes('entities')) return []
|
||||
|
||||
switch (provider) {
|
||||
case 'openai':
|
||||
return await this.extractEntitiesWithOpenAI(content)
|
||||
case 'google':
|
||||
return await this.extractEntitiesWithGoogle(content)
|
||||
default:
|
||||
return this.extractEntitiesBasic(content)
|
||||
}
|
||||
}
|
||||
|
||||
private async extractTopics(content: string, provider: AIProvider, options: AnalysisOptions): Promise<TopicResult[]> {
|
||||
if (!options.types.includes('topics')) return []
|
||||
|
||||
switch (provider) {
|
||||
case 'openai':
|
||||
return await this.extractTopicsWithOpenAI(content)
|
||||
default:
|
||||
return this.extractTopicsBasic(content)
|
||||
}
|
||||
}
|
||||
|
||||
private async classifyContent(content: string, options: AnalysisOptions): Promise<CategoryResult[]> {
|
||||
if (!options.types.includes('categories')) return []
|
||||
|
||||
const categories: CategoryResult[] = []
|
||||
|
||||
// 基于关键词的分类
|
||||
for (const category of this.NEWS_CATEGORIES) {
|
||||
const matches = category.keywords.filter(keyword =>
|
||||
content.toLowerCase().includes(keyword.toLowerCase())
|
||||
)
|
||||
|
||||
if (matches.length > 0) {
|
||||
const confidence = Math.min(matches.length / category.keywords.length, 1)
|
||||
categories.push({
|
||||
categoryId: category.id,
|
||||
categoryName: category.name,
|
||||
confidence,
|
||||
level: 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 按置信度排序
|
||||
return categories.sort((a, b) => b.confidence - a.confidence).slice(0, 3)
|
||||
}
|
||||
|
||||
private assessReadability(content: string, language?: string): number {
|
||||
// 简化的可读性评估
|
||||
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 0)
|
||||
const words = content.split(/\s+/).filter(w => w.length > 0)
|
||||
const characters = content.replace(/\s/g, '').length
|
||||
|
||||
if (sentences.length === 0 || words.length === 0) return 0
|
||||
|
||||
const avgWordsPerSentence = words.length / sentences.length
|
||||
const avgCharsPerWord = characters / words.length
|
||||
|
||||
// 基于句子长度和词汇复杂度的评分
|
||||
let score = 1.0
|
||||
|
||||
// 句子长度惩罚
|
||||
if (avgWordsPerSentence > 20) score -= 0.2
|
||||
if (avgWordsPerSentence > 30) score -= 0.3
|
||||
|
||||
// 词汇复杂度惩罚
|
||||
if (avgCharsPerWord > 6) score -= 0.1
|
||||
if (avgCharsPerWord > 8) score -= 0.2
|
||||
|
||||
return Math.max(0, Math.min(1, score))
|
||||
}
|
||||
|
||||
private assessCredibility(content: string): number {
|
||||
let score = 0.5 // 基础分
|
||||
|
||||
// 包含引用或来源
|
||||
if (content.includes('据') || content.includes('根据') || content.includes('来源')) {
|
||||
score += 0.2
|
||||
}
|
||||
|
||||
// 包含具体数据
|
||||
if (/\d+%|\d+万|\d+亿|\d{4}年/.test(content)) {
|
||||
score += 0.15
|
||||
}
|
||||
|
||||
// 避免极端词汇
|
||||
const extremeWords = ['绝对', '必然', '完全', '永远', '从来', '所有']
|
||||
const extremeCount = extremeWords.filter(word => content.includes(word)).length
|
||||
score -= extremeCount * 0.05
|
||||
|
||||
// 避免情绪化表达
|
||||
const emotionalWords = ['震惊', '愤怒', '可怕', '惊人', '令人发指']
|
||||
const emotionalCount = emotionalWords.filter(word => content.includes(word)).length
|
||||
score -= emotionalCount * 0.03
|
||||
|
||||
return Math.max(0, Math.min(1, score))
|
||||
}
|
||||
|
||||
private async assessToxicity(content: string, provider: AIProvider): Promise<number> {
|
||||
// 基础毒性检测
|
||||
const toxicWords = ['仇恨', '歧视', '暴力', '威胁', '诽谤', '侮辱']
|
||||
const toxicCount = toxicWords.filter(word => content.includes(word)).length
|
||||
|
||||
return Math.min(toxicCount / 10, 1)
|
||||
}
|
||||
|
||||
private async generateSummary(content: string, provider: AIProvider, options: AnalysisOptions): Promise<string> {
|
||||
if (!options.types.includes('summary')) return ''
|
||||
|
||||
// 简单的摘要生成:提取前两句
|
||||
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10)
|
||||
return sentences.slice(0, 2).join('。') + (sentences.length > 2 ? '。' : '')
|
||||
}
|
||||
|
||||
private extractKeywords(content: string, options: AnalysisOptions): string[] {
|
||||
if (!options.types.includes('keywords')) return []
|
||||
|
||||
// 简单的关键词提取
|
||||
const words = content
|
||||
.replace(/[^\u4e00-\u9fa5\w\s]/g, '') // 保留中文、英文和空格
|
||||
.split(/\s+/)
|
||||
.filter(word => word.length > 1)
|
||||
|
||||
// 统计词频
|
||||
const wordCount: Record<string, number> = {}
|
||||
words.forEach(word => {
|
||||
const lower = word.toLowerCase()
|
||||
wordCount[lower] = (wordCount[lower] || 0) + 1
|
||||
})
|
||||
|
||||
// 按频率排序并返回前10个
|
||||
return Object.entries(wordCount)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 10)
|
||||
.map(([word]) => word)
|
||||
}
|
||||
|
||||
private extractKeyPhrases(content: string): string[] {
|
||||
// 提取2-3个词的短语
|
||||
const phrases: string[] = []
|
||||
const words = content.split(/\s+/)
|
||||
|
||||
for (let i = 0; i < words.length - 1; i++) {
|
||||
const twoWordPhrase = words.slice(i, i + 2).join(' ')
|
||||
if (twoWordPhrase.length > 4) {
|
||||
phrases.push(twoWordPhrase)
|
||||
}
|
||||
|
||||
if (i < words.length - 2) {
|
||||
const threeWordPhrase = words.slice(i, i + 3).join(' ')
|
||||
if (threeWordPhrase.length > 6) {
|
||||
phrases.push(threeWordPhrase)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 去重并返回前5个
|
||||
return [...new Set(phrases)].slice(0, 5)
|
||||
}
|
||||
|
||||
private async detectLanguage(content: string): Promise<string> {
|
||||
// 基础语言检测
|
||||
const chineseRegex = /[\u4e00-\u9fff]/
|
||||
const englishRegex = /[a-zA-Z]/
|
||||
|
||||
const chineseMatches = content.match(chineseRegex)?.length || 0
|
||||
const englishMatches = content.match(englishRegex)?.length || 0
|
||||
|
||||
if (chineseMatches > englishMatches) return 'zh-CN'
|
||||
if (englishMatches > chineseMatches) return 'en'
|
||||
return 'auto'
|
||||
}
|
||||
|
||||
private getSentimentLabel(score: number): 'positive' | 'negative' | 'neutral' {
|
||||
if (score > 0.1) return 'positive'
|
||||
if (score < -0.1) return 'negative'
|
||||
return 'neutral'
|
||||
}
|
||||
|
||||
private generateContentId(content: string): string {
|
||||
// 简单的内容ID生成
|
||||
return `content_${Date.now()}_${content.substring(0, 10).replace(/\s/g, '_')}`
|
||||
}
|
||||
|
||||
private extractResult<T>(result: PromiseSettledResult<T>, defaultValue: T): T {
|
||||
return result.status === 'fulfilled' ? result.value : defaultValue
|
||||
}
|
||||
|
||||
private selectBestProvider(): AIProvider {
|
||||
if (this.config.openai?.apiKey) return 'openai'
|
||||
if (this.config.google?.apiKey) return 'google'
|
||||
if (this.config.baidu?.apiKey) return 'baidu'
|
||||
return 'openai'
|
||||
}
|
||||
|
||||
private createBatches<T>(items: T[], batchSize: number): T[][] {
|
||||
const batches: T[][] = []
|
||||
for (let i = 0; i < items.length; i += batchSize) {
|
||||
batches.push(items.slice(i, i + batchSize))
|
||||
}
|
||||
return batches
|
||||
}
|
||||
|
||||
private async delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
private initializeStats(): void {
|
||||
const providers: AIProvider[] = ['openai', 'google', 'baidu', 'custom']
|
||||
const analysisTypes: AnalysisType[] = ['sentiment', 'entities', 'topics', 'categories', 'readability', 'credibility', 'toxicity', 'summary', 'keywords']
|
||||
|
||||
providers.forEach(provider => {
|
||||
this.stats.byProvider[provider] = 0
|
||||
})
|
||||
|
||||
analysisTypes.forEach(type => {
|
||||
this.stats.byAnalysisType[type] = 0
|
||||
})
|
||||
}
|
||||
|
||||
private updateStats(provider: AIProvider, types: AnalysisType[], processingTime: number): void {
|
||||
this.stats.byProvider[provider]++
|
||||
types.forEach(type => {
|
||||
this.stats.byAnalysisType[type]++
|
||||
})
|
||||
|
||||
this.stats.avgProcessingTimeMs = (this.stats.avgProcessingTimeMs * (this.stats.totalAnalyses - 1) + processingTime) / this.stats.totalAnalyses
|
||||
}
|
||||
|
||||
private isRetryableError(error: any): boolean {
|
||||
const retryableCodes = ['TIMEOUT', 'RATE_LIMIT', 'SERVER_ERROR']
|
||||
return retryableCodes.includes(error.code) || error.status >= 500
|
||||
}
|
||||
|
||||
// Quality assessment methods
|
||||
private async assessFactualAccuracy(content: string): Promise<number> {
|
||||
// 检查是否包含可验证的事实
|
||||
let score = 0.5
|
||||
|
||||
// 包含日期
|
||||
if (/\d{4}年|\d{1,2}月|\d{1,2}日/.test(content)) score += 0.1
|
||||
|
||||
// 包含具体数字
|
||||
if (/\d+\.?\d*%|\d+万|\d+亿|\d+千/.test(content)) score += 0.1
|
||||
|
||||
// 包含地点
|
||||
if (/市|省|县|区|国|州/.test(content)) score += 0.1
|
||||
|
||||
// 包含人名或机构名
|
||||
if (/先生|女士|部长|主席|公司|集团|大学|医院/.test(content)) score += 0.1
|
||||
|
||||
return Math.min(1, score)
|
||||
}
|
||||
|
||||
private async assessSourceReliability(sourceUrl: string): Promise<number> {
|
||||
if (!sourceUrl) return 0.3
|
||||
|
||||
// 简单的源可靠性评估
|
||||
const reliableDomains = ['gov.cn', 'edu.cn', 'xinhuanet.com', 'people.com.cn', 'cctv.com']
|
||||
const domain = sourceUrl.toLowerCase()
|
||||
|
||||
for (const reliableDomain of reliableDomains) {
|
||||
if (domain.includes(reliableDomain)) return 0.9
|
||||
}
|
||||
|
||||
if (domain.includes('.gov') || domain.includes('.edu')) return 0.8
|
||||
if (domain.includes('news') || domain.includes('media')) return 0.6
|
||||
|
||||
return 0.4
|
||||
}
|
||||
|
||||
private async assessWritingQuality(content: string): Promise<number> {
|
||||
let score = 0.5
|
||||
|
||||
// 检查语法和结构
|
||||
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 0)
|
||||
if (sentences.length > 2) score += 0.1
|
||||
|
||||
// 检查段落结构
|
||||
const paragraphs = content.split('\n\n').filter(p => p.trim().length > 0)
|
||||
if (paragraphs.length > 1) score += 0.1
|
||||
|
||||
// 检查词汇丰富度
|
||||
const words = content.split(/\s+/).filter(w => w.length > 0)
|
||||
const uniqueWords = new Set(words.map(w => w.toLowerCase()))
|
||||
const diversity = uniqueWords.size / words.length
|
||||
score += diversity * 0.3
|
||||
|
||||
return Math.min(1, score)
|
||||
}
|
||||
|
||||
private async assessObjectivity(content: string): Promise<number> {
|
||||
let score = 0.7 // 基础客观性分数
|
||||
|
||||
// 主观词汇惩罚
|
||||
const subjectiveWords = ['我认为', '个人觉得', '显然', '明显', '无疑', '肯定']
|
||||
const subjectiveCount = subjectiveWords.filter(word => content.includes(word)).length
|
||||
score -= subjectiveCount * 0.1
|
||||
|
||||
// 情感词汇惩罚
|
||||
const emotionalWords = ['愤怒', '激动', '兴奋', '失望', '震惊', '惊喜']
|
||||
const emotionalCount = emotionalWords.filter(word => content.includes(word)).length
|
||||
score -= emotionalCount * 0.05
|
||||
|
||||
return Math.max(0, Math.min(1, score))
|
||||
}
|
||||
|
||||
private async assessCompleteness(content: string): Promise<number> {
|
||||
let score = 0.3
|
||||
|
||||
// 基于内容长度
|
||||
if (content.length > 200) score += 0.2
|
||||
if (content.length > 500) score += 0.2
|
||||
if (content.length > 1000) score += 0.2
|
||||
|
||||
// 包含关键新闻要素(5W1H)
|
||||
const hasWho = /人|者|员|家|国|公司|组织/.test(content)
|
||||
const hasWhat = /事件|活动|发生|进行|宣布|决定/.test(content)
|
||||
const hasWhen = /\d{4}年|\d{1,2}月|\d{1,2}日|今天|昨天|明天/.test(content)
|
||||
const hasWhere = /市|省|县|区|国|地区|地点/.test(content)
|
||||
const hasWhy = /因为|由于|原因|目的|为了/.test(content)
|
||||
|
||||
const elements = [hasWho, hasWhat, hasWhen, hasWhere, hasWhy].filter(Boolean).length
|
||||
score += elements * 0.06
|
||||
|
||||
return Math.min(1, score)
|
||||
}
|
||||
|
||||
private async assessTimeliness(publishedAt: number): Promise<number> {
|
||||
const now = Date.now()
|
||||
const ageHours = (now - publishedAt) / (1000 * 60 * 60)
|
||||
|
||||
// 新闻越新,时效性越高
|
||||
if (ageHours < 1) return 1.0
|
||||
if (ageHours < 6) return 0.9
|
||||
if (ageHours < 24) return 0.7
|
||||
if (ageHours < 72) return 0.5
|
||||
if (ageHours < 168) return 0.3
|
||||
return 0.1
|
||||
}
|
||||
|
||||
private async assessRelevance(content: string, categoryId?: string): Promise<number> {
|
||||
if (!categoryId) return 0.5
|
||||
|
||||
// 根据分类检查相关性
|
||||
const category = this.NEWS_CATEGORIES.find(c => c.id === categoryId)
|
||||
if (!category) return 0.5
|
||||
|
||||
const matches = category.keywords.filter(keyword =>
|
||||
content.toLowerCase().includes(keyword.toLowerCase())
|
||||
).length
|
||||
|
||||
return Math.min(1, matches / category.keywords.length + 0.3)
|
||||
}
|
||||
|
||||
// Mock AI service methods
|
||||
private async analyzeSentimentWithOpenAI(content: string): Promise<number> {
|
||||
// 模拟OpenAI情感分析
|
||||
await this.delay(Math.random() * 500 + 200)
|
||||
|
||||
// 简单的情感检测
|
||||
const positiveWords = ['好', '棒', '优秀', '成功', '胜利', '喜悦', '高兴', '满意']
|
||||
const negativeWords = ['坏', '糟糕', '失败', '问题', '困难', '悲伤', '愤怒', '失望']
|
||||
|
||||
const positiveCount = positiveWords.filter(word => content.includes(word)).length
|
||||
const negativeCount = negativeWords.filter(word => content.includes(word)).length
|
||||
|
||||
const score = (positiveCount - negativeCount) / Math.max(positiveCount + negativeCount, 1)
|
||||
return Math.max(-1, Math.min(1, score))
|
||||
}
|
||||
|
||||
private async analyzeSentimentWithGoogle(content: string): Promise<number> {
|
||||
await this.delay(Math.random() * 400 + 150)
|
||||
return Math.random() * 2 - 1 // -1 to 1
|
||||
}
|
||||
|
||||
private async analyzeSentimentWithBaidu(content: string): Promise<number> {
|
||||
await this.delay(Math.random() * 300 + 100)
|
||||
return Math.random() * 2 - 1
|
||||
}
|
||||
|
||||
private analyzeSentimentBasic(content: string): number {
|
||||
// 基础情感分析
|
||||
const positiveWords = ['好', '棒', '优秀', '成功', '胜利', 'great', 'good', 'excellent']
|
||||
const negativeWords = ['坏', '糟糕', '失败', '问题', 'bad', 'terrible', 'awful']
|
||||
|
||||
const positiveCount = positiveWords.filter(word => content.toLowerCase().includes(word)).length
|
||||
const negativeCount = negativeWords.filter(word => content.toLowerCase().includes(word)).length
|
||||
|
||||
return (positiveCount - negativeCount) / Math.max(positiveCount + negativeCount, 1)
|
||||
}
|
||||
|
||||
private async extractEntitiesWithOpenAI(content: string): Promise<EntityResult[]> {
|
||||
await this.delay(Math.random() * 600 + 300)
|
||||
|
||||
// 模拟实体提取
|
||||
const entities: EntityResult[] = []
|
||||
const patterns = [
|
||||
{ regex: /[\u4e00-\u9fa5]{2,4}(公司|集团|企业|机构)/g, type: 'organization' as const },
|
||||
{ regex: /[\u4e00-\u9fa5]{2,3}(市|省|县|区)/g, type: 'location' as const },
|
||||
{ regex: /[\u4e00-\u9fa5]{2,4}(先生|女士|部长|主席|总裁|经理)/g, type: 'person' as const },
|
||||
{ regex: /\d{4}年\d{1,2}月\d{1,2}日/g, type: 'date' as const },
|
||||
{ regex: /\d+\.?\d*(万|亿|千)?(元|美元|英镑)/g, type: 'money' as const }
|
||||
]
|
||||
|
||||
patterns.forEach(pattern => {
|
||||
const matches = content.matchAll(pattern.regex)
|
||||
for (const match of matches) {
|
||||
entities.push({
|
||||
text: match[0],
|
||||
type: pattern.type,
|
||||
confidence: 0.8 + Math.random() * 0.2,
|
||||
startPosition: match.index || 0,
|
||||
endPosition: (match.index || 0) + match[0].length
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return entities.slice(0, 10)
|
||||
}
|
||||
|
||||
private async extractEntitiesWithGoogle(content: string): Promise<EntityResult[]> {
|
||||
await this.delay(Math.random() * 500 + 250)
|
||||
return this.extractEntitiesBasic(content)
|
||||
}
|
||||
|
||||
private extractEntitiesBasic(content: string): EntityResult[] {
|
||||
// 基础实体提取
|
||||
const entities: EntityResult[] = []
|
||||
|
||||
// 提取组织
|
||||
const orgMatches = content.matchAll(/[\u4e00-\u9fa5]{2,4}(公司|集团)/g)
|
||||
for (const match of orgMatches) {
|
||||
entities.push({
|
||||
text: match[0],
|
||||
type: 'organization',
|
||||
confidence: 0.7,
|
||||
startPosition: match.index || 0,
|
||||
endPosition: (match.index || 0) + match[0].length
|
||||
})
|
||||
}
|
||||
|
||||
return entities
|
||||
}
|
||||
|
||||
private async extractTopicsWithOpenAI(content: string): Promise<TopicResult[]> {
|
||||
await this.delay(Math.random() * 400 + 200)
|
||||
|
||||
// 基于关键词聚类的主题提取
|
||||
const topics: TopicResult[] = []
|
||||
|
||||
for (const category of this.NEWS_CATEGORIES.slice(0, 3)) {
|
||||
const matches = category.keywords.filter(keyword =>
|
||||
content.toLowerCase().includes(keyword.toLowerCase())
|
||||
)
|
||||
|
||||
if (matches.length > 0) {
|
||||
topics.push({
|
||||
name: category.name,
|
||||
confidence: matches.length / category.keywords.length,
|
||||
keywords: matches
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return topics.sort((a, b) => b.confidence - a.confidence)
|
||||
}
|
||||
|
||||
private extractTopicsBasic(content: string): TopicResult[] {
|
||||
// 基础主题提取
|
||||
const topics: TopicResult[] = []
|
||||
|
||||
// 检查科技相关关键词
|
||||
const techKeywords = ['科技', '技术', '互联网', 'AI', '人工智能']
|
||||
const techMatches = techKeywords.filter(keyword => content.includes(keyword))
|
||||
|
||||
if (techMatches.length > 0) {
|
||||
topics.push({
|
||||
name: '科技',
|
||||
confidence: techMatches.length / techKeywords.length,
|
||||
keywords: techMatches
|
||||
})
|
||||
}
|
||||
|
||||
return topics
|
||||
}
|
||||
}
|
||||
761
uni_modules/ak-ai-news/services/AIErrorHandler.uts
Normal file
761
uni_modules/ak-ai-news/services/AIErrorHandler.uts
Normal file
@@ -0,0 +1,761 @@
|
||||
// Advanced Error Handling and Retry Mechanism System
|
||||
// Comprehensive error recovery, circuit breaker, and resilience patterns
|
||||
|
||||
import { type AIProvider, type AIResponse } from '../types/ai-types.uts'
|
||||
|
||||
/**
|
||||
* Error classification and handling configuration
|
||||
*/
|
||||
export type ErrorHandlingConfig = {
|
||||
retryPolicy: {
|
||||
maxAttempts: number
|
||||
baseDelayMs: number
|
||||
maxDelayMs: number
|
||||
backoffMultiplier: number
|
||||
jitterEnabled: boolean
|
||||
}
|
||||
circuitBreaker: {
|
||||
failureThreshold: number
|
||||
recoveryTimeoutMs: number
|
||||
halfOpenMaxCalls: number
|
||||
monitoringWindowMs: number
|
||||
}
|
||||
rateLimit: {
|
||||
maxRequestsPerSecond: number
|
||||
burstSize: number
|
||||
enabled: boolean
|
||||
}
|
||||
fallback: {
|
||||
enabled: boolean
|
||||
fallbackProviders: AIProvider[]
|
||||
gracefulDegradation: boolean
|
||||
}
|
||||
monitoring: {
|
||||
enableMetrics: boolean
|
||||
alertOnPatterns: boolean
|
||||
maxErrorHistorySize: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error categories for different handling strategies
|
||||
*/
|
||||
export enum ErrorCategory {
|
||||
TRANSIENT = 'transient', // Network timeouts, temporary unavailability
|
||||
AUTHENTICATION = 'auth', // API key issues, token expiration
|
||||
RATE_LIMIT = 'rate_limit', // API rate limiting
|
||||
QUOTA_EXCEEDED = 'quota', // API quota exceeded
|
||||
INVALID_REQUEST = 'invalid', // Bad request data
|
||||
SERVICE_ERROR = 'service', // Internal service errors
|
||||
NETWORK = 'network', // Network connectivity issues
|
||||
PERMANENT = 'permanent' // Permanent failures that shouldn't be retried
|
||||
}
|
||||
|
||||
/**
|
||||
* Detailed error information
|
||||
*/
|
||||
export type ErrorInfo = {
|
||||
category: ErrorCategory
|
||||
code?: string
|
||||
message: string
|
||||
provider?: AIProvider
|
||||
operation: string
|
||||
timestamp: number
|
||||
retryCount: number
|
||||
context?: Record<string, any>
|
||||
isRetryable: boolean
|
||||
suggestedAction?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Circuit breaker states
|
||||
*/
|
||||
export enum CircuitBreakerState {
|
||||
CLOSED = 'closed', // Normal operation
|
||||
OPEN = 'open', // Circuit is open, failing fast
|
||||
HALF_OPEN = 'half_open' // Testing if service has recovered
|
||||
}
|
||||
|
||||
/**
|
||||
* Circuit breaker status
|
||||
*/
|
||||
export type CircuitBreakerStatus = {
|
||||
state: CircuitBreakerState
|
||||
failureCount: number
|
||||
successCount: number
|
||||
lastFailureTime?: number
|
||||
nextAttemptTime?: number
|
||||
halfOpenAttempts: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limiter status
|
||||
*/
|
||||
export type RateLimiterStatus = {
|
||||
requestsRemaining: number
|
||||
resetTime: number
|
||||
isLimited: boolean
|
||||
queueSize: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry attempt information
|
||||
*/
|
||||
export type RetryAttempt = {
|
||||
attemptNumber: number
|
||||
timestamp: number
|
||||
error?: ErrorInfo
|
||||
delayMs: number
|
||||
success: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Operation result with retry information
|
||||
*/
|
||||
export type OperationResult<T> = {
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: ErrorInfo
|
||||
attempts: RetryAttempt[]
|
||||
totalDuration: number
|
||||
finalProvider?: AIProvider
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced error handler and retry manager
|
||||
*/
|
||||
export class AIErrorHandler {
|
||||
private config: ErrorHandlingConfig
|
||||
private circuitBreakers = new Map<string, CircuitBreakerStatus>()
|
||||
private rateLimiters = new Map<string, RateLimiterStatus>()
|
||||
private errorHistory: ErrorInfo[] = []
|
||||
private requestQueues = new Map<string, Array<() => Promise<any>>>()
|
||||
|
||||
constructor(config: ErrorHandlingConfig) {
|
||||
this.config = config
|
||||
this.initializeCircuitBreakers()
|
||||
this.initializeRateLimiters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute operation with advanced error handling and retry logic
|
||||
*/
|
||||
async executeWithRetry<T>(
|
||||
operation: () => Promise<T>,
|
||||
context: {
|
||||
operationName: string
|
||||
provider?: AIProvider
|
||||
retryable?: boolean
|
||||
metadata?: Record<string, any>
|
||||
}
|
||||
): Promise<OperationResult<T>> {
|
||||
const startTime = Date.now()
|
||||
const attempts: RetryAttempt[] = []
|
||||
let lastError: ErrorInfo | undefined
|
||||
|
||||
// Check circuit breaker
|
||||
const breakerKey = this.getBreakerKey(context.operationName, context.provider)
|
||||
if (this.isCircuitOpen(breakerKey)) {
|
||||
const error = this.createError(
|
||||
ErrorCategory.SERVICE_ERROR,
|
||||
`Circuit breaker is open for ${breakerKey}`,
|
||||
context.operationName,
|
||||
context.provider
|
||||
)
|
||||
return {
|
||||
success: false,
|
||||
error,
|
||||
attempts: [],
|
||||
totalDuration: Date.now() - startTime
|
||||
}
|
||||
}
|
||||
|
||||
// Check rate limits
|
||||
if (this.config.rateLimit.enabled && context.provider) {
|
||||
const rateLimitResult = await this.checkRateLimit(context.provider)
|
||||
if (!rateLimitResult.allowed) {
|
||||
const error = this.createError(
|
||||
ErrorCategory.RATE_LIMIT,
|
||||
'Rate limit exceeded',
|
||||
context.operationName,
|
||||
context.provider
|
||||
)
|
||||
return {
|
||||
success: false,
|
||||
error,
|
||||
attempts: [],
|
||||
totalDuration: Date.now() - startTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute with retry logic
|
||||
for (let attempt = 1; attempt <= this.config.retryPolicy.maxAttempts; attempt++) {
|
||||
const attemptStart = Date.now()
|
||||
|
||||
try {
|
||||
// Add delay for retry attempts
|
||||
if (attempt > 1) {
|
||||
const delay = this.calculateRetryDelay(attempt - 1)
|
||||
await this.sleep(delay)
|
||||
attempts[attempts.length - 1].delayMs = delay
|
||||
}
|
||||
|
||||
// Execute the operation
|
||||
const result = await operation()
|
||||
|
||||
// Record successful attempt
|
||||
const attemptInfo: RetryAttempt = {
|
||||
attemptNumber: attempt,
|
||||
timestamp: attemptStart,
|
||||
delayMs: 0,
|
||||
success: true
|
||||
}
|
||||
attempts.push(attemptInfo)
|
||||
|
||||
// Update circuit breaker on success
|
||||
this.recordSuccess(breakerKey)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
attempts,
|
||||
totalDuration: Date.now() - startTime,
|
||||
finalProvider: context.provider
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const errorInfo = this.analyzeError(error, context.operationName, context.provider, attempt - 1)
|
||||
lastError = errorInfo
|
||||
|
||||
// Record failed attempt
|
||||
const attemptInfo: RetryAttempt = {
|
||||
attemptNumber: attempt,
|
||||
timestamp: attemptStart,
|
||||
error: errorInfo,
|
||||
delayMs: 0,
|
||||
success: false
|
||||
}
|
||||
attempts.push(attemptInfo)
|
||||
|
||||
// Update error history
|
||||
this.recordError(errorInfo)
|
||||
|
||||
// Update circuit breaker on failure
|
||||
this.recordFailure(breakerKey)
|
||||
|
||||
// Check if we should retry
|
||||
if (!this.shouldRetry(errorInfo, attempt)) {
|
||||
break
|
||||
}
|
||||
|
||||
// Try fallback provider if available
|
||||
if (this.config.fallback.enabled && attempt === this.config.retryPolicy.maxAttempts) {
|
||||
const fallbackResult = await this.tryFallbackProviders(
|
||||
operation,
|
||||
context,
|
||||
startTime,
|
||||
attempts
|
||||
)
|
||||
if (fallbackResult) {
|
||||
return fallbackResult
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: lastError,
|
||||
attempts,
|
||||
totalDuration: Date.now() - startTime
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle bulk operations with advanced error recovery
|
||||
*/
|
||||
async executeBulkWithRetry<T, R>(
|
||||
items: T[],
|
||||
operation: (item: T) => Promise<R>,
|
||||
options: {
|
||||
operationName: string
|
||||
batchSize?: number
|
||||
concurrency?: number
|
||||
failFast?: boolean
|
||||
partialFailureThreshold?: number
|
||||
}
|
||||
): Promise<{
|
||||
results: Array<{ item: T; result?: R; error?: ErrorInfo }>
|
||||
summary: {
|
||||
successful: number
|
||||
failed: number
|
||||
totalTime: number
|
||||
throughput: number
|
||||
}
|
||||
}> {
|
||||
const startTime = Date.now()
|
||||
const batchSize = options.batchSize || 10
|
||||
const concurrency = options.concurrency || 3
|
||||
const results: Array<{ item: T; result?: R; error?: ErrorInfo }> = []
|
||||
|
||||
// Process in batches
|
||||
for (let i = 0; i < items.length; i += batchSize) {
|
||||
const batch = items.slice(i, i + batchSize)
|
||||
|
||||
// Process batch with controlled concurrency
|
||||
const batchPromises = batch.map(async (item) => {
|
||||
const operationResult = await this.executeWithRetry(
|
||||
() => operation(item),
|
||||
{
|
||||
operationName: options.operationName,
|
||||
metadata: { batchIndex: Math.floor(i / batchSize), itemIndex: i + batch.indexOf(item) }
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
item,
|
||||
result: operationResult.data,
|
||||
error: operationResult.error
|
||||
}
|
||||
})
|
||||
|
||||
// Execute with concurrency control
|
||||
const batchResults = await this.executeConcurrently(batchPromises, concurrency)
|
||||
results.push(...batchResults)
|
||||
|
||||
// Check failure threshold
|
||||
const failedCount = results.filter(r => r.error).length
|
||||
const failureRate = failedCount / results.length
|
||||
|
||||
if (options.failFast && failureRate > (options.partialFailureThreshold || 0.5)) {
|
||||
console.log(`⚠️ Bulk operation failing fast due to high failure rate: ${(failureRate * 100).toFixed(1)}%`)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const endTime = Date.now()
|
||||
const successful = results.filter(r => !r.error).length
|
||||
const failed = results.filter(r => r.error).length
|
||||
const totalTime = endTime - startTime
|
||||
const throughput = results.length / (totalTime / 1000)
|
||||
|
||||
return {
|
||||
results,
|
||||
summary: {
|
||||
successful,
|
||||
failed,
|
||||
totalTime,
|
||||
throughput
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current error handling status
|
||||
*/
|
||||
getErrorHandlingStatus(): {
|
||||
circuitBreakers: Array<{ key: string; status: CircuitBreakerStatus }>
|
||||
rateLimiters: Array<{ key: string; status: RateLimiterStatus }>
|
||||
recentErrors: ErrorInfo[]
|
||||
errorPatterns: Array<{ pattern: string; count: number; lastSeen: number }>
|
||||
} {
|
||||
const recentErrors = this.errorHistory.slice(-50) // Last 50 errors
|
||||
const errorPatterns = this.analyzeErrorPatterns(recentErrors)
|
||||
|
||||
return {
|
||||
circuitBreakers: Array.from(this.circuitBreakers.entries()).map(([key, status]) => ({ key, status })),
|
||||
rateLimiters: Array.from(this.rateLimiters.entries()).map(([key, status]) => ({ key, status })),
|
||||
recentErrors,
|
||||
errorPatterns
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset circuit breakers and error state
|
||||
*/
|
||||
resetErrorState(): void {
|
||||
this.circuitBreakers.clear()
|
||||
this.rateLimiters.clear()
|
||||
this.errorHistory = []
|
||||
this.requestQueues.clear()
|
||||
|
||||
this.initializeCircuitBreakers()
|
||||
this.initializeRateLimiters()
|
||||
|
||||
console.log('🔄 Error handling state reset')
|
||||
}
|
||||
|
||||
/**
|
||||
* Update configuration
|
||||
*/
|
||||
updateConfig(newConfig: Partial<ErrorHandlingConfig>): void {
|
||||
this.config = { ...this.config, ...newConfig }
|
||||
console.log('⚙️ Error handling configuration updated')
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
private initializeCircuitBreakers(): void {
|
||||
const providers: AIProvider[] = ['openai', 'google', 'baidu']
|
||||
const operations = ['translate', 'analyze', 'chat', 'recommend']
|
||||
|
||||
providers.forEach(provider => {
|
||||
operations.forEach(operation => {
|
||||
const key = this.getBreakerKey(operation, provider)
|
||||
this.circuitBreakers.set(key, {
|
||||
state: CircuitBreakerState.CLOSED,
|
||||
failureCount: 0,
|
||||
successCount: 0,
|
||||
halfOpenAttempts: 0
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private initializeRateLimiters(): void {
|
||||
const providers: AIProvider[] = ['openai', 'google', 'baidu']
|
||||
|
||||
providers.forEach(provider => {
|
||||
this.rateLimiters.set(provider, {
|
||||
requestsRemaining: this.config.rateLimit.maxRequestsPerSecond,
|
||||
resetTime: Date.now() + 1000,
|
||||
isLimited: false,
|
||||
queueSize: 0
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private getBreakerKey(operation: string, provider?: AIProvider): string {
|
||||
return provider ? `${provider}:${operation}` : operation
|
||||
}
|
||||
|
||||
private isCircuitOpen(breakerKey: string): boolean {
|
||||
const breaker = this.circuitBreakers.get(breakerKey)
|
||||
if (!breaker) return false
|
||||
|
||||
if (breaker.state === CircuitBreakerState.OPEN) {
|
||||
// Check if we should transition to half-open
|
||||
const now = Date.now()
|
||||
if (breaker.lastFailureTime &&
|
||||
now - breaker.lastFailureTime > this.config.circuitBreaker.recoveryTimeoutMs) {
|
||||
breaker.state = CircuitBreakerState.HALF_OPEN
|
||||
breaker.halfOpenAttempts = 0
|
||||
console.log(`🔄 Circuit breaker ${breakerKey} transitioning to half-open`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private recordSuccess(breakerKey: string): void {
|
||||
const breaker = this.circuitBreakers.get(breakerKey)
|
||||
if (!breaker) return
|
||||
|
||||
breaker.successCount++
|
||||
|
||||
if (breaker.state === CircuitBreakerState.HALF_OPEN) {
|
||||
breaker.halfOpenAttempts++
|
||||
if (breaker.halfOpenAttempts >= this.config.circuitBreaker.halfOpenMaxCalls) {
|
||||
breaker.state = CircuitBreakerState.CLOSED
|
||||
breaker.failureCount = 0
|
||||
console.log(`✅ Circuit breaker ${breakerKey} closed after successful recovery`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private recordFailure(breakerKey: string): void {
|
||||
const breaker = this.circuitBreakers.get(breakerKey)
|
||||
if (!breaker) return
|
||||
|
||||
breaker.failureCount++
|
||||
breaker.lastFailureTime = Date.now()
|
||||
|
||||
if (breaker.state === CircuitBreakerState.CLOSED) {
|
||||
if (breaker.failureCount >= this.config.circuitBreaker.failureThreshold) {
|
||||
breaker.state = CircuitBreakerState.OPEN
|
||||
console.log(`⚠️ Circuit breaker ${breakerKey} opened due to ${breaker.failureCount} failures`)
|
||||
}
|
||||
} else if (breaker.state === CircuitBreakerState.HALF_OPEN) {
|
||||
breaker.state = CircuitBreakerState.OPEN
|
||||
console.log(`❌ Circuit breaker ${breakerKey} re-opened after failed recovery attempt`)
|
||||
}
|
||||
}
|
||||
|
||||
private async checkRateLimit(provider: AIProvider): Promise<{ allowed: boolean; waitTime?: number }> {
|
||||
const limiter = this.rateLimiters.get(provider)
|
||||
if (!limiter) return { allowed: true }
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
// Reset if time window has passed
|
||||
if (now >= limiter.resetTime) {
|
||||
limiter.requestsRemaining = this.config.rateLimit.maxRequestsPerSecond
|
||||
limiter.resetTime = now + 1000
|
||||
limiter.isLimited = false
|
||||
}
|
||||
|
||||
if (limiter.requestsRemaining <= 0) {
|
||||
limiter.isLimited = true
|
||||
return {
|
||||
allowed: false,
|
||||
waitTime: limiter.resetTime - now
|
||||
}
|
||||
}
|
||||
|
||||
limiter.requestsRemaining--
|
||||
return { allowed: true }
|
||||
}
|
||||
|
||||
private analyzeError(
|
||||
error: any,
|
||||
operation: string,
|
||||
provider?: AIProvider,
|
||||
retryCount: number = 0
|
||||
): ErrorInfo {
|
||||
const errorMessage = error?.message || String(error)
|
||||
const errorCode = error?.code || error?.status
|
||||
|
||||
let category = ErrorCategory.PERMANENT
|
||||
let isRetryable = false
|
||||
let suggestedAction = 'Review error and fix manually'
|
||||
|
||||
// Analyze error to determine category and retry strategy
|
||||
if (errorMessage.toLowerCase().includes('timeout') ||
|
||||
errorMessage.toLowerCase().includes('network')) {
|
||||
category = ErrorCategory.TRANSIENT
|
||||
isRetryable = true
|
||||
suggestedAction = 'Retry with exponential backoff'
|
||||
} else if (errorMessage.toLowerCase().includes('rate limit') || errorCode === 429) {
|
||||
category = ErrorCategory.RATE_LIMIT
|
||||
isRetryable = true
|
||||
suggestedAction = 'Wait and retry, consider implementing rate limiting'
|
||||
} else if (errorMessage.toLowerCase().includes('quota') ||
|
||||
errorMessage.toLowerCase().includes('exceeded')) {
|
||||
category = ErrorCategory.QUOTA_EXCEEDED
|
||||
isRetryable = false
|
||||
suggestedAction = 'Check API quota and billing'
|
||||
} else if (errorMessage.toLowerCase().includes('auth') ||
|
||||
errorMessage.toLowerCase().includes('unauthorized') ||
|
||||
errorCode === 401) {
|
||||
category = ErrorCategory.AUTHENTICATION
|
||||
isRetryable = false
|
||||
suggestedAction = 'Check API keys and authentication'
|
||||
} else if (errorCode >= 400 && errorCode < 500) {
|
||||
category = ErrorCategory.INVALID_REQUEST
|
||||
isRetryable = false
|
||||
suggestedAction = 'Review request parameters'
|
||||
} else if (errorCode >= 500) {
|
||||
category = ErrorCategory.SERVICE_ERROR
|
||||
isRetryable = true
|
||||
suggestedAction = 'Retry or use fallback provider'
|
||||
}
|
||||
|
||||
return {
|
||||
category,
|
||||
code: String(errorCode || 'unknown'),
|
||||
message: errorMessage,
|
||||
provider,
|
||||
operation,
|
||||
timestamp: Date.now(),
|
||||
retryCount,
|
||||
isRetryable,
|
||||
suggestedAction,
|
||||
context: {
|
||||
originalError: error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private shouldRetry(error: ErrorInfo, attemptNumber: number): boolean {
|
||||
if (attemptNumber >= this.config.retryPolicy.maxAttempts) {
|
||||
return false
|
||||
}
|
||||
|
||||
return error.isRetryable && [
|
||||
ErrorCategory.TRANSIENT,
|
||||
ErrorCategory.RATE_LIMIT,
|
||||
ErrorCategory.SERVICE_ERROR,
|
||||
ErrorCategory.NETWORK
|
||||
].includes(error.category)
|
||||
}
|
||||
|
||||
private calculateRetryDelay(attemptNumber: number): number {
|
||||
const baseDelay = this.config.retryPolicy.baseDelayMs
|
||||
const maxDelay = this.config.retryPolicy.maxDelayMs
|
||||
const multiplier = this.config.retryPolicy.backoffMultiplier
|
||||
|
||||
let delay = baseDelay * Math.pow(multiplier, attemptNumber)
|
||||
delay = Math.min(delay, maxDelay)
|
||||
|
||||
// Add jitter if enabled
|
||||
if (this.config.retryPolicy.jitterEnabled) {
|
||||
const jitter = delay * 0.1 * Math.random()
|
||||
delay += jitter
|
||||
}
|
||||
|
||||
return Math.floor(delay)
|
||||
}
|
||||
|
||||
private async tryFallbackProviders<T>(
|
||||
operation: () => Promise<T>,
|
||||
context: any,
|
||||
startTime: number,
|
||||
existingAttempts: RetryAttempt[]
|
||||
): Promise<OperationResult<T> | null> {
|
||||
if (!this.config.fallback.enabled || !context.provider) {
|
||||
return null
|
||||
}
|
||||
|
||||
const fallbackProviders = this.config.fallback.fallbackProviders.filter(
|
||||
p => p !== context.provider
|
||||
)
|
||||
|
||||
for (const fallbackProvider of fallbackProviders) {
|
||||
try {
|
||||
console.log(`🔄 Attempting fallback to provider: ${fallbackProvider}`)
|
||||
|
||||
const result = await operation() // Note: In real implementation, this would use the fallback provider
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
attempts: existingAttempts,
|
||||
totalDuration: Date.now() - startTime,
|
||||
finalProvider: fallbackProvider
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`❌ Fallback provider ${fallbackProvider} also failed:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private recordError(error: ErrorInfo): void {
|
||||
this.errorHistory.push(error)
|
||||
|
||||
// Maintain history size limit
|
||||
if (this.errorHistory.length > this.config.monitoring.maxErrorHistorySize) {
|
||||
this.errorHistory = this.errorHistory.slice(-this.config.monitoring.maxErrorHistorySize)
|
||||
}
|
||||
|
||||
// Alert on error patterns if enabled
|
||||
if (this.config.monitoring.alertOnPatterns) {
|
||||
this.checkErrorPatterns(error)
|
||||
}
|
||||
}
|
||||
|
||||
private checkErrorPatterns(error: ErrorInfo): void {
|
||||
const recentErrors = this.errorHistory.filter(
|
||||
e => Date.now() - e.timestamp < 300000 // Last 5 minutes
|
||||
)
|
||||
|
||||
// Check for repeated errors from same provider
|
||||
if (error.provider) {
|
||||
const providerErrors = recentErrors.filter(e => e.provider === error.provider)
|
||||
if (providerErrors.length >= 5) {
|
||||
console.log(`🚨 High error rate detected for provider ${error.provider}: ${providerErrors.length} errors in 5 minutes`)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for repeated error categories
|
||||
const categoryErrors = recentErrors.filter(e => e.category === error.category)
|
||||
if (categoryErrors.length >= 10) {
|
||||
console.log(`🚨 High error rate detected for category ${error.category}: ${categoryErrors.length} errors in 5 minutes`)
|
||||
}
|
||||
}
|
||||
|
||||
private analyzeErrorPatterns(errors: ErrorInfo[]): Array<{ pattern: string; count: number; lastSeen: number }> {
|
||||
const patterns = new Map<string, { count: number; lastSeen: number }>()
|
||||
|
||||
errors.forEach(error => {
|
||||
const pattern = `${error.category}:${error.provider || 'unknown'}`
|
||||
const existing = patterns.get(pattern) || { count: 0, lastSeen: 0 }
|
||||
patterns.set(pattern, {
|
||||
count: existing.count + 1,
|
||||
lastSeen: Math.max(existing.lastSeen, error.timestamp)
|
||||
})
|
||||
})
|
||||
|
||||
return Array.from(patterns.entries())
|
||||
.map(([pattern, data]) => ({ pattern, ...data }))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
}
|
||||
|
||||
private async executeConcurrently<T>(promises: Promise<T>[], concurrency: number): Promise<T[]> {
|
||||
const results: T[] = []
|
||||
const executing: Promise<void>[] = []
|
||||
|
||||
for (const promise of promises) {
|
||||
const p = promise.then(result => {
|
||||
results.push(result)
|
||||
})
|
||||
|
||||
executing.push(p)
|
||||
|
||||
if (executing.length >= concurrency) {
|
||||
await Promise.race(executing)
|
||||
executing.splice(executing.findIndex(x => x === p), 1)
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(executing)
|
||||
return results
|
||||
}
|
||||
|
||||
private createError(
|
||||
category: ErrorCategory,
|
||||
message: string,
|
||||
operation: string,
|
||||
provider?: AIProvider
|
||||
): ErrorInfo {
|
||||
return {
|
||||
category,
|
||||
message,
|
||||
operation,
|
||||
provider,
|
||||
timestamp: Date.now(),
|
||||
retryCount: 0,
|
||||
isRetryable: category !== ErrorCategory.PERMANENT
|
||||
}
|
||||
}
|
||||
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
}
|
||||
|
||||
// Default configuration
|
||||
export const defaultErrorHandlingConfig: ErrorHandlingConfig = {
|
||||
retryPolicy: {
|
||||
maxAttempts: 3,
|
||||
baseDelayMs: 1000, // 1 second
|
||||
maxDelayMs: 30000, // 30 seconds
|
||||
backoffMultiplier: 2,
|
||||
jitterEnabled: true
|
||||
},
|
||||
circuitBreaker: {
|
||||
failureThreshold: 5,
|
||||
recoveryTimeoutMs: 60000, // 1 minute
|
||||
halfOpenMaxCalls: 3,
|
||||
monitoringWindowMs: 300000 // 5 minutes
|
||||
},
|
||||
rateLimit: {
|
||||
maxRequestsPerSecond: 10,
|
||||
burstSize: 5,
|
||||
enabled: true
|
||||
},
|
||||
fallback: {
|
||||
enabled: true,
|
||||
fallbackProviders: ['openai', 'google', 'baidu'],
|
||||
gracefulDegradation: true
|
||||
},
|
||||
monitoring: {
|
||||
enableMetrics: true,
|
||||
alertOnPatterns: true,
|
||||
maxErrorHistorySize: 1000
|
||||
}
|
||||
}
|
||||
758
uni_modules/ak-ai-news/services/AIPerformanceMonitor.uts
Normal file
758
uni_modules/ak-ai-news/services/AIPerformanceMonitor.uts
Normal file
@@ -0,0 +1,758 @@
|
||||
// Performance Monitor and Optimization System
|
||||
// Real-time monitoring, metrics collection, and automatic optimization
|
||||
|
||||
import { type AIProvider, type AIServiceConfig } from '../types/ai-types.uts'
|
||||
|
||||
/**
|
||||
* Performance metrics data structure
|
||||
*/
|
||||
export type PerformanceMetrics = {
|
||||
timestamp: number
|
||||
service: string
|
||||
operation: string
|
||||
provider?: AIProvider
|
||||
duration: number
|
||||
success: boolean
|
||||
error?: string
|
||||
tokensUsed?: number
|
||||
costUSD?: number
|
||||
cacheHit?: boolean
|
||||
memoryUsage?: number
|
||||
cpuUsage?: number
|
||||
networkLatency?: number
|
||||
queueSize?: number
|
||||
throughput?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* System health status
|
||||
*/
|
||||
export type SystemHealth = {
|
||||
status: 'healthy' | 'warning' | 'critical'
|
||||
score: number // 0-100
|
||||
checks: {
|
||||
apiConnectivity: boolean
|
||||
memoryUsage: number
|
||||
errorRate: number
|
||||
responseTime: number
|
||||
costBudget: number
|
||||
cacheEfficiency: number
|
||||
}
|
||||
alerts: HealthAlert[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Health alert
|
||||
*/
|
||||
export type HealthAlert = {
|
||||
id: string
|
||||
severity: 'info' | 'warning' | 'error' | 'critical'
|
||||
message: string
|
||||
timestamp: number
|
||||
source: string
|
||||
resolved: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimization recommendation
|
||||
*/
|
||||
export type OptimizationRecommendation = {
|
||||
type: 'cache' | 'provider' | 'batch' | 'model' | 'timeout' | 'retry'
|
||||
priority: 'low' | 'medium' | 'high' | 'critical'
|
||||
description: string
|
||||
expectedImpact: {
|
||||
performanceGain?: string
|
||||
costSaving?: string
|
||||
reliabilityImprovement?: string
|
||||
}
|
||||
implementation: {
|
||||
action: string
|
||||
parameters: Record<string, any>
|
||||
estimatedEffort: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performance statistics aggregation
|
||||
*/
|
||||
export type PerformanceStats = {
|
||||
timeRange: {
|
||||
start: number
|
||||
end: number
|
||||
duration: number
|
||||
}
|
||||
requests: {
|
||||
total: number
|
||||
successful: number
|
||||
failed: number
|
||||
successRate: number
|
||||
}
|
||||
timing: {
|
||||
averageLatency: number
|
||||
medianLatency: number
|
||||
p95Latency: number
|
||||
p99Latency: number
|
||||
}
|
||||
costs: {
|
||||
total: number
|
||||
average: number
|
||||
byProvider: Record<string, number>
|
||||
}
|
||||
cache: {
|
||||
hitRate: number
|
||||
totalRequests: number
|
||||
hits: number
|
||||
misses: number
|
||||
}
|
||||
errors: {
|
||||
byType: Record<string, number>
|
||||
byProvider: Record<string, number>
|
||||
topErrors: Array<{ error: string; count: number }>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performance monitoring and optimization service
|
||||
*/
|
||||
export class AIPerformanceMonitor {
|
||||
private metrics: PerformanceMetrics[] = []
|
||||
private alerts: HealthAlert[] = []
|
||||
private isMonitoring = false
|
||||
private monitoringInterval?: number
|
||||
private maxMetricsHistory = 10000
|
||||
private alertThresholds = {
|
||||
errorRate: 0.05, // 5%
|
||||
responseTime: 5000, // 5 seconds
|
||||
memoryUsage: 0.8, // 80%
|
||||
costBudget: 0.9, // 90% of daily budget
|
||||
cacheHitRate: 0.3 // 30% minimum
|
||||
}
|
||||
|
||||
constructor(
|
||||
private config: {
|
||||
monitoringInterval: number
|
||||
maxHistory: number
|
||||
alertWebhook?: string
|
||||
enableAutoOptimization: boolean
|
||||
}
|
||||
) {
|
||||
this.maxMetricsHistory = config.maxHistory
|
||||
}
|
||||
|
||||
/**
|
||||
* Start performance monitoring
|
||||
*/
|
||||
startMonitoring(): void {
|
||||
if (this.isMonitoring) {
|
||||
console.log('⚠️ Performance monitoring is already running')
|
||||
return
|
||||
}
|
||||
|
||||
this.isMonitoring = true
|
||||
console.log('🚀 Starting performance monitoring...')
|
||||
|
||||
this.monitoringInterval = setInterval(() => {
|
||||
this.collectSystemMetrics()
|
||||
this.checkSystemHealth()
|
||||
this.generateOptimizationRecommendations()
|
||||
}, this.config.monitoringInterval)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop performance monitoring
|
||||
*/
|
||||
stopMonitoring(): void {
|
||||
if (this.monitoringInterval) {
|
||||
clearInterval(this.monitoringInterval)
|
||||
this.monitoringInterval = undefined
|
||||
}
|
||||
this.isMonitoring = false
|
||||
console.log('🛑 Performance monitoring stopped')
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a performance metric
|
||||
*/
|
||||
recordMetric(metric: PerformanceMetrics): void {
|
||||
metric.timestamp = metric.timestamp || Date.now()
|
||||
this.metrics.push(metric)
|
||||
|
||||
// Maintain history limit
|
||||
if (this.metrics.length > this.maxMetricsHistory) {
|
||||
this.metrics = this.metrics.slice(-this.maxMetricsHistory)
|
||||
}
|
||||
|
||||
// Real-time analysis for critical metrics
|
||||
if (!metric.success || (metric.duration && metric.duration > 10000)) {
|
||||
this.checkForImmedateAlerts(metric)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current system health
|
||||
*/
|
||||
getSystemHealth(): SystemHealth {
|
||||
const now = Date.now()
|
||||
const recentMetrics = this.metrics.filter(m => now - m.timestamp < 300000) // Last 5 minutes
|
||||
|
||||
if (recentMetrics.length === 0) {
|
||||
return {
|
||||
status: 'warning',
|
||||
score: 50,
|
||||
checks: {
|
||||
apiConnectivity: false,
|
||||
memoryUsage: 0,
|
||||
errorRate: 0,
|
||||
responseTime: 0,
|
||||
costBudget: 0,
|
||||
cacheEfficiency: 0
|
||||
},
|
||||
alerts: this.getActiveAlerts()
|
||||
}
|
||||
}
|
||||
|
||||
const errorRate = recentMetrics.filter(m => !m.success).length / recentMetrics.length
|
||||
const avgResponseTime = recentMetrics.reduce((sum, m) => sum + m.duration, 0) / recentMetrics.length
|
||||
const cacheHitRate = this.calculateCacheHitRate(recentMetrics)
|
||||
const memoryUsage = this.getMemoryUsage()
|
||||
const costBudget = this.calculateCostBudgetUsage()
|
||||
|
||||
const checks = {
|
||||
apiConnectivity: errorRate < 0.1,
|
||||
memoryUsage,
|
||||
errorRate,
|
||||
responseTime: avgResponseTime,
|
||||
costBudget,
|
||||
cacheEfficiency: cacheHitRate
|
||||
}
|
||||
|
||||
const score = this.calculateHealthScore(checks)
|
||||
const status = this.determineHealthStatus(score, checks)
|
||||
|
||||
return {
|
||||
status,
|
||||
score,
|
||||
checks,
|
||||
alerts: this.getActiveAlerts()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get performance statistics for a time range
|
||||
*/
|
||||
getPerformanceStats(
|
||||
startTime: number,
|
||||
endTime: number
|
||||
): PerformanceStats {
|
||||
const rangeMetrics = this.metrics.filter(
|
||||
m => m.timestamp >= startTime && m.timestamp <= endTime
|
||||
)
|
||||
|
||||
if (rangeMetrics.length === 0) {
|
||||
return this.getEmptyStats(startTime, endTime)
|
||||
}
|
||||
|
||||
const successful = rangeMetrics.filter(m => m.success)
|
||||
const failed = rangeMetrics.filter(m => !m.success)
|
||||
const successRate = successful.length / rangeMetrics.length
|
||||
|
||||
// Calculate timing statistics
|
||||
const durations = rangeMetrics.map(m => m.duration).sort((a, b) => a - b)
|
||||
const averageLatency = durations.reduce((sum, d) => sum + d, 0) / durations.length
|
||||
const medianLatency = durations[Math.floor(durations.length / 2)]
|
||||
const p95Latency = durations[Math.floor(durations.length * 0.95)]
|
||||
const p99Latency = durations[Math.floor(durations.length * 0.99)]
|
||||
|
||||
// Calculate cost statistics
|
||||
const totalCost = rangeMetrics.reduce((sum, m) => sum + (m.costUSD || 0), 0)
|
||||
const averageCost = totalCost / rangeMetrics.length
|
||||
const costByProvider = this.groupCostsByProvider(rangeMetrics)
|
||||
|
||||
// Calculate cache statistics
|
||||
const cacheRequests = rangeMetrics.filter(m => m.cacheHit !== undefined)
|
||||
const cacheHits = cacheRequests.filter(m => m.cacheHit).length
|
||||
const cacheMisses = cacheRequests.length - cacheHits
|
||||
const cacheHitRate = cacheRequests.length > 0 ? cacheHits / cacheRequests.length : 0
|
||||
|
||||
// Calculate error statistics
|
||||
const errorsByType = this.groupErrorsByType(failed)
|
||||
const errorsByProvider = this.groupErrorsByProvider(failed)
|
||||
const topErrors = this.getTopErrors(failed)
|
||||
|
||||
return {
|
||||
timeRange: {
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
duration: endTime - startTime
|
||||
},
|
||||
requests: {
|
||||
total: rangeMetrics.length,
|
||||
successful: successful.length,
|
||||
failed: failed.length,
|
||||
successRate
|
||||
},
|
||||
timing: {
|
||||
averageLatency,
|
||||
medianLatency,
|
||||
p95Latency,
|
||||
p99Latency
|
||||
},
|
||||
costs: {
|
||||
total: totalCost,
|
||||
average: averageCost,
|
||||
byProvider: costByProvider
|
||||
},
|
||||
cache: {
|
||||
hitRate: cacheHitRate,
|
||||
totalRequests: cacheRequests.length,
|
||||
hits: cacheHits,
|
||||
misses: cacheMisses
|
||||
},
|
||||
errors: {
|
||||
byType: errorsByType,
|
||||
byProvider: errorsByProvider,
|
||||
topErrors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get optimization recommendations
|
||||
*/
|
||||
getOptimizationRecommendations(): OptimizationRecommendation[] {
|
||||
const recommendations: OptimizationRecommendation[] = []
|
||||
const recentStats = this.getPerformanceStats(
|
||||
Date.now() - 3600000, // Last hour
|
||||
Date.now()
|
||||
)
|
||||
|
||||
// Cache optimization recommendations
|
||||
if (recentStats.cache.hitRate < 0.4) {
|
||||
recommendations.push({
|
||||
type: 'cache',
|
||||
priority: 'high',
|
||||
description: `Cache hit rate is low (${(recentStats.cache.hitRate * 100).toFixed(1)}%). Consider increasing cache size or TTL.`,
|
||||
expectedImpact: {
|
||||
performanceGain: '30-50% faster response times',
|
||||
costSaving: '20-40% reduction in AI API costs'
|
||||
},
|
||||
implementation: {
|
||||
action: 'increase_cache_size',
|
||||
parameters: {
|
||||
maxSize: Math.max(1000, recentStats.cache.totalRequests * 2),
|
||||
ttl: 3600000 // 1 hour
|
||||
},
|
||||
estimatedEffort: 'Low - Configuration change'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Provider optimization recommendations
|
||||
const providerErrors = recentStats.errors.byProvider
|
||||
const worstProvider = Object.entries(providerErrors)
|
||||
.sort(([, a], [, b]) => b - a)[0]
|
||||
|
||||
if (worstProvider && worstProvider[1] > recentStats.requests.total * 0.1) {
|
||||
recommendations.push({
|
||||
type: 'provider',
|
||||
priority: 'medium',
|
||||
description: `Provider ${worstProvider[0]} has high error rate (${worstProvider[1]} errors). Consider switching primary provider.`,
|
||||
expectedImpact: {
|
||||
reliabilityImprovement: '80-90% reduction in errors'
|
||||
},
|
||||
implementation: {
|
||||
action: 'switch_primary_provider',
|
||||
parameters: {
|
||||
newPrimary: this.recommendBestProvider(recentStats),
|
||||
fallbackProviders: ['openai', 'google', 'baidu']
|
||||
},
|
||||
estimatedEffort: 'Medium - Code changes required'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Batch processing recommendations
|
||||
if (recentStats.timing.averageLatency > 3000 && recentStats.requests.total > 100) {
|
||||
recommendations.push({
|
||||
type: 'batch',
|
||||
priority: 'medium',
|
||||
description: 'High latency with significant request volume. Consider implementing batch processing.',
|
||||
expectedImpact: {
|
||||
performanceGain: '50-70% improvement in throughput',
|
||||
costSaving: '15-25% cost reduction'
|
||||
},
|
||||
implementation: {
|
||||
action: 'enable_batch_processing',
|
||||
parameters: {
|
||||
batchSize: 10,
|
||||
batchTimeout: 1000,
|
||||
concurrency: 3
|
||||
},
|
||||
estimatedEffort: 'High - Significant code changes'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Model optimization recommendations
|
||||
if (recentStats.costs.average > 0.01) { // More than 1 cent per request
|
||||
recommendations.push({
|
||||
type: 'model',
|
||||
priority: 'low',
|
||||
description: 'Request costs are high. Consider using smaller/cheaper models for simple tasks.',
|
||||
expectedImpact: {
|
||||
costSaving: '40-60% cost reduction'
|
||||
},
|
||||
implementation: {
|
||||
action: 'implement_model_selection',
|
||||
parameters: {
|
||||
simpleTaskModel: 'gpt-3.5-turbo',
|
||||
complexTaskModel: 'gpt-4',
|
||||
costThreshold: 0.005
|
||||
},
|
||||
estimatedEffort: 'Medium - Logic implementation required'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return recommendations
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply automatic optimization
|
||||
*/
|
||||
async applyOptimizations(
|
||||
recommendations: OptimizationRecommendation[]
|
||||
): Promise<{ applied: number; failed: number; results: any[] }> {
|
||||
if (!this.config.enableAutoOptimization) {
|
||||
console.log('⚠️ Auto-optimization is disabled')
|
||||
return { applied: 0, failed: 0, results: [] }
|
||||
}
|
||||
|
||||
const results: any[] = []
|
||||
let applied = 0
|
||||
let failed = 0
|
||||
|
||||
for (const rec of recommendations) {
|
||||
try {
|
||||
const result = await this.applyOptimization(rec)
|
||||
if (result.success) {
|
||||
applied++
|
||||
console.log(`✅ Applied optimization: ${rec.description}`)
|
||||
} else {
|
||||
failed++
|
||||
console.log(`❌ Failed to apply optimization: ${rec.description} - ${result.error}`)
|
||||
}
|
||||
results.push(result)
|
||||
} catch (error) {
|
||||
failed++
|
||||
console.log(`💥 Error applying optimization: ${rec.description} - ${error}`)
|
||||
results.push({ success: false, error: String(error) })
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🔧 Auto-optimization completed: ${applied} applied, ${failed} failed`)
|
||||
return { applied, failed, results }
|
||||
}
|
||||
|
||||
/**
|
||||
* Export performance data for external analysis
|
||||
*/
|
||||
exportPerformanceData(format: 'json' | 'csv'): string {
|
||||
if (format === 'json') {
|
||||
return JSON.stringify({
|
||||
exportTime: Date.now(),
|
||||
metrics: this.metrics,
|
||||
alerts: this.alerts,
|
||||
systemHealth: this.getSystemHealth(),
|
||||
stats: this.getPerformanceStats(Date.now() - 86400000, Date.now()) // Last 24h
|
||||
}, null, 2)
|
||||
} else {
|
||||
// CSV format
|
||||
const headers = [
|
||||
'timestamp', 'service', 'operation', 'provider', 'duration',
|
||||
'success', 'tokensUsed', 'costUSD', 'cacheHit', 'error'
|
||||
]
|
||||
|
||||
const rows = this.metrics.map(m => [
|
||||
m.timestamp,
|
||||
m.service,
|
||||
m.operation,
|
||||
m.provider || '',
|
||||
m.duration,
|
||||
m.success,
|
||||
m.tokensUsed || 0,
|
||||
m.costUSD || 0,
|
||||
m.cacheHit || false,
|
||||
m.error || ''
|
||||
])
|
||||
|
||||
return [headers, ...rows].map(row => row.join(',')).join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
// Private methods for internal calculations and operations
|
||||
|
||||
private collectSystemMetrics(): void {
|
||||
const memoryUsage = this.getMemoryUsage()
|
||||
const cpuUsage = this.getCpuUsage()
|
||||
|
||||
this.recordMetric({
|
||||
timestamp: Date.now(),
|
||||
service: 'system',
|
||||
operation: 'health_check',
|
||||
duration: 0,
|
||||
success: true,
|
||||
memoryUsage,
|
||||
cpuUsage
|
||||
})
|
||||
}
|
||||
|
||||
private checkSystemHealth(): void {
|
||||
const health = this.getSystemHealth()
|
||||
|
||||
if (health.status === 'critical') {
|
||||
this.createAlert({
|
||||
severity: 'critical',
|
||||
message: `System health is critical (score: ${health.score})`,
|
||||
source: 'health_monitor',
|
||||
resolved: false
|
||||
})
|
||||
} else if (health.status === 'warning') {
|
||||
this.createAlert({
|
||||
severity: 'warning',
|
||||
message: `System health degraded (score: ${health.score})`,
|
||||
source: 'health_monitor',
|
||||
resolved: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private generateOptimizationRecommendations(): void {
|
||||
const recommendations = this.getOptimizationRecommendations()
|
||||
|
||||
if (recommendations.length > 0 && this.config.enableAutoOptimization) {
|
||||
console.log(`🔧 Found ${recommendations.length} optimization opportunities`)
|
||||
this.applyOptimizations(recommendations)
|
||||
}
|
||||
}
|
||||
|
||||
private checkForImmedateAlerts(metric: PerformanceMetrics): void {
|
||||
if (!metric.success) {
|
||||
this.createAlert({
|
||||
severity: 'error',
|
||||
message: `${metric.service} ${metric.operation} failed: ${metric.error}`,
|
||||
source: metric.service,
|
||||
resolved: false
|
||||
})
|
||||
}
|
||||
|
||||
if (metric.duration && metric.duration > 10000) {
|
||||
this.createAlert({
|
||||
severity: 'warning',
|
||||
message: `High latency detected: ${metric.service} ${metric.operation} took ${metric.duration}ms`,
|
||||
source: metric.service,
|
||||
resolved: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private createAlert(alert: Omit<HealthAlert, 'id' | 'timestamp'>): void {
|
||||
const newAlert: HealthAlert = {
|
||||
id: `alert-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
timestamp: Date.now(),
|
||||
...alert
|
||||
}
|
||||
|
||||
this.alerts.push(newAlert)
|
||||
|
||||
// Keep only last 100 alerts
|
||||
if (this.alerts.length > 100) {
|
||||
this.alerts = this.alerts.slice(-100)
|
||||
}
|
||||
|
||||
console.log(`🚨 ${alert.severity.toUpperCase()}: ${alert.message}`)
|
||||
}
|
||||
|
||||
private getActiveAlerts(): HealthAlert[] {
|
||||
return this.alerts.filter(a => !a.resolved && Date.now() - a.timestamp < 3600000) // Last hour
|
||||
}
|
||||
|
||||
private calculateHealthScore(checks: any): number {
|
||||
let score = 100
|
||||
|
||||
if (checks.errorRate > this.alertThresholds.errorRate) {
|
||||
score -= (checks.errorRate - this.alertThresholds.errorRate) * 500
|
||||
}
|
||||
|
||||
if (checks.responseTime > this.alertThresholds.responseTime) {
|
||||
score -= Math.min(30, (checks.responseTime - this.alertThresholds.responseTime) / 1000 * 5)
|
||||
}
|
||||
|
||||
if (checks.memoryUsage > this.alertThresholds.memoryUsage) {
|
||||
score -= (checks.memoryUsage - this.alertThresholds.memoryUsage) * 100
|
||||
}
|
||||
|
||||
if (checks.cacheEfficiency < this.alertThresholds.cacheHitRate) {
|
||||
score -= (this.alertThresholds.cacheHitRate - checks.cacheEfficiency) * 50
|
||||
}
|
||||
|
||||
return Math.max(0, Math.min(100, score))
|
||||
}
|
||||
|
||||
private determineHealthStatus(score: number, checks: any): 'healthy' | 'warning' | 'critical' {
|
||||
if (score < 30 || checks.errorRate > 0.2 || !checks.apiConnectivity) {
|
||||
return 'critical'
|
||||
} else if (score < 70 || checks.errorRate > 0.1 || checks.responseTime > 5000) {
|
||||
return 'warning'
|
||||
} else {
|
||||
return 'healthy'
|
||||
}
|
||||
}
|
||||
|
||||
private calculateCacheHitRate(metrics: PerformanceMetrics[]): number {
|
||||
const cacheMetrics = metrics.filter(m => m.cacheHit !== undefined)
|
||||
if (cacheMetrics.length === 0) return 0
|
||||
return cacheMetrics.filter(m => m.cacheHit).length / cacheMetrics.length
|
||||
}
|
||||
|
||||
private getMemoryUsage(): number {
|
||||
// Simulated memory usage - in real implementation, use actual system metrics
|
||||
return Math.random() * 0.8 + 0.2
|
||||
}
|
||||
|
||||
private getCpuUsage(): number {
|
||||
// Simulated CPU usage - in real implementation, use actual system metrics
|
||||
return Math.random() * 0.6 + 0.1
|
||||
}
|
||||
|
||||
private calculateCostBudgetUsage(): number {
|
||||
const todayStart = new Date().setHours(0, 0, 0, 0)
|
||||
const todayMetrics = this.metrics.filter(m => m.timestamp >= todayStart)
|
||||
const todayCost = todayMetrics.reduce((sum, m) => sum + (m.costUSD || 0), 0)
|
||||
const dailyBudget = 100 // $100 daily budget - should be configurable
|
||||
return todayCost / dailyBudget
|
||||
}
|
||||
|
||||
private groupCostsByProvider(metrics: PerformanceMetrics[]): Record<string, number> {
|
||||
const costs: Record<string, number> = {}
|
||||
metrics.forEach(m => {
|
||||
if (m.provider && m.costUSD) {
|
||||
costs[m.provider] = (costs[m.provider] || 0) + m.costUSD
|
||||
}
|
||||
})
|
||||
return costs
|
||||
}
|
||||
|
||||
private groupErrorsByType(failedMetrics: PerformanceMetrics[]): Record<string, number> {
|
||||
const errors: Record<string, number> = {}
|
||||
failedMetrics.forEach(m => {
|
||||
if (m.error) {
|
||||
const errorType = this.categorizeError(m.error)
|
||||
errors[errorType] = (errors[errorType] || 0) + 1
|
||||
}
|
||||
})
|
||||
return errors
|
||||
}
|
||||
|
||||
private groupErrorsByProvider(failedMetrics: PerformanceMetrics[]): Record<string, number> {
|
||||
const errors: Record<string, number> = {}
|
||||
failedMetrics.forEach(m => {
|
||||
if (m.provider) {
|
||||
errors[m.provider] = (errors[m.provider] || 0) + 1
|
||||
}
|
||||
})
|
||||
return errors
|
||||
}
|
||||
|
||||
private getTopErrors(failedMetrics: PerformanceMetrics[]): Array<{ error: string; count: number }> {
|
||||
const errorCounts: Record<string, number> = {}
|
||||
failedMetrics.forEach(m => {
|
||||
if (m.error) {
|
||||
errorCounts[m.error] = (errorCounts[m.error] || 0) + 1
|
||||
}
|
||||
})
|
||||
|
||||
return Object.entries(errorCounts)
|
||||
.map(([error, count]) => ({ error, count }))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 5)
|
||||
}
|
||||
|
||||
private categorizeError(error: string): string {
|
||||
const lowerError = error.toLowerCase()
|
||||
if (lowerError.includes('timeout')) return 'timeout'
|
||||
if (lowerError.includes('rate limit')) return 'rate_limit'
|
||||
if (lowerError.includes('auth')) return 'authentication'
|
||||
if (lowerError.includes('network')) return 'network'
|
||||
if (lowerError.includes('quota')) return 'quota_exceeded'
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
private recommendBestProvider(stats: PerformanceStats): AIProvider {
|
||||
const providerPerformance = {
|
||||
openai: 0,
|
||||
google: 0,
|
||||
baidu: 0
|
||||
}
|
||||
|
||||
// Simple scoring based on error rates
|
||||
Object.entries(stats.errors.byProvider).forEach(([provider, errors]) => {
|
||||
const errorRate = errors / stats.requests.total
|
||||
providerPerformance[provider as AIProvider] = 1 - errorRate
|
||||
})
|
||||
|
||||
return Object.entries(providerPerformance)
|
||||
.sort(([, a], [, b]) => b - a)[0][0] as AIProvider
|
||||
}
|
||||
|
||||
private getEmptyStats(startTime: number, endTime: number): PerformanceStats {
|
||||
return {
|
||||
timeRange: { start: startTime, end: endTime, duration: endTime - startTime },
|
||||
requests: { total: 0, successful: 0, failed: 0, successRate: 0 },
|
||||
timing: { averageLatency: 0, medianLatency: 0, p95Latency: 0, p99Latency: 0 },
|
||||
costs: { total: 0, average: 0, byProvider: {} },
|
||||
cache: { hitRate: 0, totalRequests: 0, hits: 0, misses: 0 },
|
||||
errors: { byType: {}, byProvider: {}, topErrors: [] }
|
||||
}
|
||||
}
|
||||
|
||||
private async applyOptimization(recommendation: OptimizationRecommendation): Promise<{ success: boolean; error?: string }> {
|
||||
// Simulated optimization application
|
||||
// In real implementation, this would apply actual configuration changes
|
||||
|
||||
try {
|
||||
switch (recommendation.type) {
|
||||
case 'cache':
|
||||
// Apply cache optimization
|
||||
console.log(`🔧 Applying cache optimization: ${JSON.stringify(recommendation.implementation.parameters)}`)
|
||||
break
|
||||
case 'provider':
|
||||
// Switch provider
|
||||
console.log(`🔧 Switching to provider: ${recommendation.implementation.parameters.newPrimary}`)
|
||||
break
|
||||
case 'batch':
|
||||
// Enable batch processing
|
||||
console.log(`🔧 Enabling batch processing: batch size ${recommendation.implementation.parameters.batchSize}`)
|
||||
break
|
||||
case 'model':
|
||||
// Implement model selection
|
||||
console.log(`🔧 Implementing intelligent model selection`)
|
||||
break
|
||||
default:
|
||||
return { success: false, error: 'Unknown optimization type' }
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return { success: false, error: String(error) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export default configuration
|
||||
export const defaultPerformanceConfig = {
|
||||
monitoringInterval: 30000, // 30 seconds
|
||||
maxHistory: 10000,
|
||||
enableAutoOptimization: false, // Disabled by default for safety
|
||||
alertWebhook: undefined
|
||||
}
|
||||
1027
uni_modules/ak-ai-news/services/AIRecommendationService.uts
Normal file
1027
uni_modules/ak-ai-news/services/AIRecommendationService.uts
Normal file
File diff suppressed because it is too large
Load Diff
563
uni_modules/ak-ai-news/services/AIServiceManager.uts
Normal file
563
uni_modules/ak-ai-news/services/AIServiceManager.uts
Normal file
@@ -0,0 +1,563 @@
|
||||
// AI Service Manager - Unified coordinator for all AI services
|
||||
|
||||
import {
|
||||
AIServiceConfig,
|
||||
AIProvider,
|
||||
AIResponse,
|
||||
AIServiceError,
|
||||
UsageStatistics,
|
||||
CacheOptions
|
||||
} from '../types/ai-types.uts'
|
||||
|
||||
import { AITranslationService } from './AITranslationService.uts'
|
||||
import { AIContentAnalysisService } from './AIContentAnalysisService.uts'
|
||||
import { AIChatService } from './AIChatService.uts'
|
||||
import { AIRecommendationService } from './AIRecommendationService.uts'
|
||||
import { ContentProcessingPipeline } from './ContentProcessingPipeline.uts'
|
||||
|
||||
// 服务状态枚举
|
||||
type ServiceStatus = 'initializing' | 'ready' | 'busy' | 'error' | 'maintenance'
|
||||
|
||||
// 服务健康状态
|
||||
type ServiceHealth = {
|
||||
status: ServiceStatus
|
||||
lastChecked: number
|
||||
responseTime: number
|
||||
errorRate: number
|
||||
uptime: number
|
||||
version: string
|
||||
capabilities: string[]
|
||||
}
|
||||
|
||||
// 负载均衡策略
|
||||
type LoadBalanceStrategy = 'round_robin' | 'least_connections' | 'weighted' | 'random'
|
||||
|
||||
// 服务监控配置
|
||||
type MonitoringConfig = {
|
||||
healthCheckInterval: number // 健康检查间隔(毫秒)
|
||||
maxErrorRate: number // 最大错误率
|
||||
maxResponseTime: number // 最大响应时间(毫秒)
|
||||
alertThresholds: {
|
||||
errorRate: number
|
||||
responseTime: number
|
||||
dailyCost: number
|
||||
}
|
||||
}
|
||||
|
||||
// 成本控制配置
|
||||
type CostControlConfig = {
|
||||
dailyLimit: number // 每日成本限制(USD)
|
||||
monthlyLimit: number // 每月成本限制(USD)
|
||||
perRequestLimit: number // 单次请求成本限制(USD)
|
||||
alertThresholds: {
|
||||
daily: number // 每日预警阈值
|
||||
monthly: number // 每月预警阈值
|
||||
}
|
||||
}
|
||||
|
||||
// 管理器统计
|
||||
type ManagerStats = {
|
||||
totalRequests: number
|
||||
successfulRequests: number
|
||||
failedRequests: number
|
||||
totalCost: number
|
||||
avgResponseTime: number
|
||||
servicesHealth: Record<string, ServiceHealth>
|
||||
dailyUsage: UsageStatistics[]
|
||||
costBreakdown: Record<AIProvider, number>
|
||||
lastReset: number
|
||||
}
|
||||
|
||||
/**
|
||||
* AI服务管理器
|
||||
* 统一管理所有AI服务,提供负载均衡、监控、成本控制等功能
|
||||
*/
|
||||
export class AIServiceManager {
|
||||
private config: AIServiceConfig
|
||||
private monitoringConfig: MonitoringConfig
|
||||
private costControlConfig: CostControlConfig
|
||||
private cacheOptions: CacheOptions
|
||||
|
||||
// 服务实例
|
||||
private translationService: AITranslationService
|
||||
private analysisService: AIContentAnalysisService
|
||||
private chatService: AIChatService
|
||||
private recommendationService: AIRecommendationService
|
||||
private processingPipeline: ContentProcessingPipeline
|
||||
|
||||
// 状态管理
|
||||
private servicesHealth: Map<string, ServiceHealth> = new Map()
|
||||
private loadBalanceState: Map<AIProvider, number> = new Map()
|
||||
private stats: ManagerStats
|
||||
private healthCheckInterval: any
|
||||
private isInitialized: boolean = false
|
||||
|
||||
constructor(
|
||||
config: AIServiceConfig,
|
||||
monitoringConfig: Partial<MonitoringConfig> = {},
|
||||
costControlConfig: Partial<CostControlConfig> = {},
|
||||
cacheOptions: Partial<CacheOptions> = {}
|
||||
) {
|
||||
this.config = config
|
||||
this.monitoringConfig = this.createDefaultMonitoringConfig(monitoringConfig)
|
||||
this.costControlConfig = this.createDefaultCostControlConfig(costControlConfig)
|
||||
this.cacheOptions = this.createDefaultCacheOptions(cacheOptions)
|
||||
this.stats = this.initializeStats()
|
||||
|
||||
this.initializeServices()
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化所有服务
|
||||
*/
|
||||
async initialize(): Promise<AIResponse<boolean>> {
|
||||
try {
|
||||
console.log('Initializing AI Service Manager...')
|
||||
|
||||
// 初始化各个服务
|
||||
this.translationService = new AITranslationService(this.config, this.cacheOptions)
|
||||
this.analysisService = new AIContentAnalysisService(this.config)
|
||||
this.chatService = new AIChatService(this.config)
|
||||
this.recommendationService = new AIRecommendationService(this.config)
|
||||
this.processingPipeline = new ContentProcessingPipeline(this.config)
|
||||
|
||||
// 初始化服务健康状态
|
||||
await this.initializeHealthStatus()
|
||||
|
||||
// 启动健康检查
|
||||
this.startHealthMonitoring()
|
||||
|
||||
// 初始化负载均衡状态
|
||||
this.initializeLoadBalancing()
|
||||
|
||||
this.isInitialized = true
|
||||
console.log('AI Service Manager initialized successfully')
|
||||
|
||||
return { success: true, data: true }
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize AI Service Manager:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Initialization failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取翻译服务
|
||||
*/
|
||||
getTranslationService(): AITranslationService {
|
||||
this.ensureInitialized()
|
||||
return this.translationService
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内容分析服务
|
||||
*/
|
||||
getAnalysisService(): AIContentAnalysisService {
|
||||
this.ensureInitialized()
|
||||
return this.analysisService
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天服务
|
||||
*/
|
||||
getChatService(): AIChatService {
|
||||
this.ensureInitialized()
|
||||
return this.chatService
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取推荐服务
|
||||
*/
|
||||
getRecommendationService(): AIRecommendationService {
|
||||
this.ensureInitialized()
|
||||
return this.recommendationService
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内容处理管道
|
||||
*/
|
||||
getProcessingPipeline(): ContentProcessingPipeline {
|
||||
this.ensureInitialized()
|
||||
return this.processingPipeline
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择最佳提供商
|
||||
* @param serviceType 服务类型
|
||||
*/
|
||||
selectBestProvider(serviceType: string = 'general'): AIProvider {
|
||||
const availableProviders = this.getAvailableProviders()
|
||||
|
||||
if (availableProviders.length === 0) {
|
||||
return 'openai' // 默认提供商
|
||||
}
|
||||
|
||||
// 基于健康状态和负载均衡策略选择
|
||||
return this.applyLoadBalancing(availableProviders, serviceType)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查成本限制
|
||||
* @param estimatedCost 预估成本
|
||||
*/
|
||||
checkCostLimits(estimatedCost: number): boolean {
|
||||
const now = new Date()
|
||||
const today = now.toISOString().split('T')[0]
|
||||
const currentMonth = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}`
|
||||
|
||||
// 检查每日限制
|
||||
const dailyCost = this.getDailyCost(today)
|
||||
if (dailyCost + estimatedCost > this.costControlConfig.dailyLimit) {
|
||||
console.warn(`Daily cost limit exceeded: ${dailyCost + estimatedCost} > ${this.costControlConfig.dailyLimit}`)
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查每月限制
|
||||
const monthlyCost = this.getMonthlyCost(currentMonth)
|
||||
if (monthlyCost + estimatedCost > this.costControlConfig.monthlyLimit) {
|
||||
console.warn(`Monthly cost limit exceeded: ${monthlyCost + estimatedCost} > ${this.costControlConfig.monthlyLimit}`)
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查单次请求限制
|
||||
if (estimatedCost > this.costControlConfig.perRequestLimit) {
|
||||
console.warn(`Per-request cost limit exceeded: ${estimatedCost} > ${this.costControlConfig.perRequestLimit}`)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录使用统计
|
||||
* @param provider 提供商
|
||||
* @param serviceType 服务类型
|
||||
* @param stats 统计信息
|
||||
*/
|
||||
recordUsage(provider: AIProvider, serviceType: string, stats: Partial<UsageStatistics>): void {
|
||||
this.stats.totalRequests++
|
||||
|
||||
if (stats.requestsCount && stats.requestsCount > 0) {
|
||||
this.stats.successfulRequests++
|
||||
} else {
|
||||
this.stats.failedRequests++
|
||||
}
|
||||
|
||||
this.stats.totalCost += stats.costUSD || 0
|
||||
this.stats.costBreakdown[provider] = (this.stats.costBreakdown[provider] || 0) + (stats.costUSD || 0)
|
||||
|
||||
if (stats.avgResponseTimeMs) {
|
||||
this.stats.avgResponseTime = (
|
||||
this.stats.avgResponseTime * (this.stats.totalRequests - 1) + stats.avgResponseTimeMs
|
||||
) / this.stats.totalRequests
|
||||
}
|
||||
|
||||
// 记录每日使用情况
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const hour = new Date().getHours()
|
||||
|
||||
const dailyStats: UsageStatistics = {
|
||||
provider,
|
||||
serviceType,
|
||||
tokensUsed: stats.tokensUsed || 0,
|
||||
requestsCount: stats.requestsCount || 0,
|
||||
costUSD: stats.costUSD || 0,
|
||||
successCount: stats.successCount || 0,
|
||||
errorCount: stats.errorCount || 0,
|
||||
avgResponseTimeMs: stats.avgResponseTimeMs || 0,
|
||||
date: today,
|
||||
hour
|
||||
}
|
||||
|
||||
this.stats.dailyUsage.push(dailyStats)
|
||||
|
||||
// 保持最近30天的数据
|
||||
const cutoffDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]
|
||||
this.stats.dailyUsage = this.stats.dailyUsage.filter(usage => usage.date >= cutoffDate)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务健康状态
|
||||
*/
|
||||
getServicesHealth(): Record<string, ServiceHealth> {
|
||||
const health: Record<string, ServiceHealth> = {}
|
||||
for (const [serviceName, serviceHealth] of this.servicesHealth.entries()) {
|
||||
health[serviceName] = { ...serviceHealth }
|
||||
}
|
||||
return health
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取管理器统计
|
||||
*/
|
||||
getManagerStatistics(): ManagerStats {
|
||||
return {
|
||||
...this.stats,
|
||||
servicesHealth: this.getServicesHealth()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置统计数据
|
||||
*/
|
||||
resetStatistics(): void {
|
||||
this.stats = this.initializeStats()
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止所有服务
|
||||
*/
|
||||
async shutdown(): Promise<void> {
|
||||
console.log('Shutting down AI Service Manager...')
|
||||
|
||||
// 停止健康检查
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval)
|
||||
}
|
||||
|
||||
// 清理缓存
|
||||
if (this.translationService) {
|
||||
this.translationService.clearCache()
|
||||
}
|
||||
|
||||
this.isInitialized = false
|
||||
console.log('AI Service Manager shut down completed')
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
private initializeServices(): void {
|
||||
// 初始化负载均衡状态
|
||||
const providers: AIProvider[] = ['openai', 'google', 'baidu', 'custom']
|
||||
providers.forEach(provider => {
|
||||
this.loadBalanceState.set(provider, 0)
|
||||
})
|
||||
}
|
||||
|
||||
private async initializeHealthStatus(): Promise<void> {
|
||||
const services = ['translation', 'analysis', 'chat', 'recommendation', 'pipeline']
|
||||
|
||||
for (const serviceName of services) {
|
||||
const health: ServiceHealth = {
|
||||
status: 'ready',
|
||||
lastChecked: Date.now(),
|
||||
responseTime: 0,
|
||||
errorRate: 0,
|
||||
uptime: Date.now(),
|
||||
version: '1.0.0',
|
||||
capabilities: this.getServiceCapabilities(serviceName)
|
||||
}
|
||||
|
||||
this.servicesHealth.set(serviceName, health)
|
||||
}
|
||||
}
|
||||
|
||||
private getServiceCapabilities(serviceName: string): string[] {
|
||||
const capabilities: Record<string, string[]> = {
|
||||
translation: ['text_translation', 'language_detection', 'batch_translation'],
|
||||
analysis: ['sentiment_analysis', 'entity_extraction', 'content_classification', 'quality_assessment'],
|
||||
chat: ['conversation', 'multilingual_support', 'context_awareness'],
|
||||
recommendation: ['personalized_recommendations', 'trending_content', 'similarity_matching'],
|
||||
pipeline: ['automated_processing', 'batch_processing', 'workflow_management']
|
||||
}
|
||||
|
||||
return capabilities[serviceName] || []
|
||||
}
|
||||
|
||||
private startHealthMonitoring(): void {
|
||||
this.healthCheckInterval = setInterval(() => {
|
||||
this.performHealthCheck()
|
||||
}, this.monitoringConfig.healthCheckInterval)
|
||||
}
|
||||
|
||||
private async performHealthCheck(): Promise<void> {
|
||||
for (const [serviceName, health] of this.servicesHealth.entries()) {
|
||||
try {
|
||||
const startTime = Date.now()
|
||||
|
||||
// 执行简单的健康检查
|
||||
await this.checkServiceHealth(serviceName)
|
||||
|
||||
const responseTime = Date.now() - startTime
|
||||
|
||||
// 更新健康状态
|
||||
health.lastChecked = Date.now()
|
||||
health.responseTime = responseTime
|
||||
health.status = responseTime > this.monitoringConfig.maxResponseTime ? 'error' : 'ready'
|
||||
|
||||
// 检查错误率
|
||||
if (health.errorRate > this.monitoringConfig.maxErrorRate) {
|
||||
health.status = 'error'
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Health check failed for ${serviceName}:`, error)
|
||||
this.servicesHealth.get(serviceName)!.status = 'error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async checkServiceHealth(serviceName: string): Promise<void> {
|
||||
// 简单的健康检查实现
|
||||
switch (serviceName) {
|
||||
case 'translation':
|
||||
// 可以测试一个简单的翻译
|
||||
break
|
||||
case 'analysis':
|
||||
// 可以测试一个简单的分析
|
||||
break
|
||||
case 'chat':
|
||||
// 可以检查会话状态
|
||||
break
|
||||
case 'recommendation':
|
||||
// 可以检查推荐算法状态
|
||||
break
|
||||
case 'pipeline':
|
||||
// 可以检查处理管道状态
|
||||
break
|
||||
}
|
||||
|
||||
// 模拟健康检查延迟
|
||||
await this.delay(Math.random() * 100 + 50)
|
||||
}
|
||||
|
||||
private initializeLoadBalancing(): void {
|
||||
const providers = this.getAvailableProviders()
|
||||
providers.forEach(provider => {
|
||||
this.loadBalanceState.set(provider, 0)
|
||||
})
|
||||
}
|
||||
|
||||
private getAvailableProviders(): AIProvider[] {
|
||||
const providers: AIProvider[] = []
|
||||
|
||||
if (this.config.openai?.apiKey) providers.push('openai')
|
||||
if (this.config.google?.apiKey) providers.push('google')
|
||||
if (this.config.baidu?.apiKey) providers.push('baidu')
|
||||
|
||||
return providers
|
||||
}
|
||||
|
||||
private applyLoadBalancing(providers: AIProvider[], serviceType: string): AIProvider {
|
||||
// 过滤健康的提供商
|
||||
const healthyProviders = providers.filter(provider => {
|
||||
const serviceName = this.getServiceNameForProvider(provider, serviceType)
|
||||
const health = this.servicesHealth.get(serviceName)
|
||||
return health && health.status === 'ready'
|
||||
})
|
||||
|
||||
if (healthyProviders.length === 0) {
|
||||
return providers[0] // 回退到第一个可用提供商
|
||||
}
|
||||
|
||||
// 轮询策略
|
||||
const providerCounts = healthyProviders.map(provider => ({
|
||||
provider,
|
||||
count: this.loadBalanceState.get(provider) || 0
|
||||
}))
|
||||
|
||||
// 选择使用次数最少的提供商
|
||||
const selectedProvider = providerCounts.reduce((min, current) =>
|
||||
current.count < min.count ? current : min
|
||||
).provider
|
||||
|
||||
// 更新计数
|
||||
this.loadBalanceState.set(selectedProvider, (this.loadBalanceState.get(selectedProvider) || 0) + 1)
|
||||
|
||||
return selectedProvider
|
||||
}
|
||||
|
||||
private getServiceNameForProvider(provider: AIProvider, serviceType: string): string {
|
||||
// 根据提供商和服务类型映射到内部服务名称
|
||||
const serviceMap: Record<string, string> = {
|
||||
'translation': 'translation',
|
||||
'analysis': 'analysis',
|
||||
'chat': 'chat',
|
||||
'recommendation': 'recommendation'
|
||||
}
|
||||
|
||||
return serviceMap[serviceType] || 'translation'
|
||||
}
|
||||
|
||||
private getDailyCost(date: string): number {
|
||||
return this.stats.dailyUsage
|
||||
.filter(usage => usage.date === date)
|
||||
.reduce((total, usage) => total + usage.costUSD, 0)
|
||||
}
|
||||
|
||||
private getMonthlyCost(month: string): number {
|
||||
return this.stats.dailyUsage
|
||||
.filter(usage => usage.date.startsWith(month))
|
||||
.reduce((total, usage) => total + usage.costUSD, 0)
|
||||
}
|
||||
|
||||
private ensureInitialized(): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error('AI Service Manager not initialized. Call initialize() first.')
|
||||
}
|
||||
}
|
||||
|
||||
private createDefaultMonitoringConfig(overrides: Partial<MonitoringConfig>): MonitoringConfig {
|
||||
return {
|
||||
healthCheckInterval: 60000, // 1分钟
|
||||
maxErrorRate: 0.1, // 10%
|
||||
maxResponseTime: 5000, // 5秒
|
||||
alertThresholds: {
|
||||
errorRate: 0.05, // 5%
|
||||
responseTime: 3000, // 3秒
|
||||
dailyCost: 100 // $100
|
||||
},
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
private createDefaultCostControlConfig(overrides: Partial<CostControlConfig>): CostControlConfig {
|
||||
return {
|
||||
dailyLimit: 200, // $200
|
||||
monthlyLimit: 5000, // $5000
|
||||
perRequestLimit: 10, // $10
|
||||
alertThresholds: {
|
||||
daily: 150, // $150
|
||||
monthly: 4000 // $4000
|
||||
},
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
private createDefaultCacheOptions(overrides: Partial<CacheOptions>): CacheOptions {
|
||||
return {
|
||||
enabled: true,
|
||||
ttlHours: 24,
|
||||
maxSize: 10000,
|
||||
strategy: 'lru',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
private initializeStats(): ManagerStats {
|
||||
const providers: AIProvider[] = ['openai', 'google', 'baidu', 'custom']
|
||||
const costBreakdown: Record<AIProvider, number> = {} as Record<AIProvider, number>
|
||||
providers.forEach(provider => {
|
||||
costBreakdown[provider] = 0
|
||||
})
|
||||
|
||||
return {
|
||||
totalRequests: 0,
|
||||
successfulRequests: 0,
|
||||
failedRequests: 0,
|
||||
totalCost: 0,
|
||||
avgResponseTime: 0,
|
||||
servicesHealth: {},
|
||||
dailyUsage: [],
|
||||
costBreakdown,
|
||||
lastReset: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
private async delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
}
|
||||
701
uni_modules/ak-ai-news/services/AITranslationService.uts
Normal file
701
uni_modules/ak-ai-news/services/AITranslationService.uts
Normal file
@@ -0,0 +1,701 @@
|
||||
// AI Translation Service - Multi-provider translation implementation
|
||||
|
||||
import {
|
||||
TranslationResult,
|
||||
TranslationOptions,
|
||||
AIProvider,
|
||||
AIResponse,
|
||||
AIServiceConfig,
|
||||
BatchProcessingOptions,
|
||||
CacheOptions,
|
||||
AIServiceError
|
||||
} from '../types/ai-types.uts'
|
||||
|
||||
// 翻译缓存条目
|
||||
type TranslationCacheEntry = {
|
||||
key: string
|
||||
result: TranslationResult
|
||||
createdAt: number
|
||||
ttl: number
|
||||
}
|
||||
|
||||
// 翻译统计
|
||||
type TranslationStats = {
|
||||
totalRequests: number
|
||||
successCount: number
|
||||
errorCount: number
|
||||
totalTokens: number
|
||||
totalCost: number
|
||||
avgQuality: number
|
||||
cacheHitRate: number
|
||||
}
|
||||
|
||||
/**
|
||||
* AI翻译服务类
|
||||
* 支持多种AI提供商的翻译服务,包括缓存、批处理、质量评估等功能
|
||||
*/
|
||||
export class AITranslationService {
|
||||
private config: AIServiceConfig
|
||||
private cache: Map<string, TranslationCacheEntry> = new Map()
|
||||
private cacheOptions: CacheOptions
|
||||
private stats: TranslationStats = {
|
||||
totalRequests: 0,
|
||||
successCount: 0,
|
||||
errorCount: 0,
|
||||
totalTokens: 0,
|
||||
totalCost: 0,
|
||||
avgQuality: 0,
|
||||
cacheHitRate: 0
|
||||
}
|
||||
|
||||
constructor(config: AIServiceConfig, cacheOptions: CacheOptions = {
|
||||
enabled: true,
|
||||
ttlHours: 24,
|
||||
maxSize: 10000,
|
||||
strategy: 'lru'
|
||||
}) {
|
||||
this.config = config
|
||||
this.cacheOptions = cacheOptions
|
||||
this.initializeCache()
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译文本
|
||||
* @param text 原文本
|
||||
* @param targetLang 目标语言
|
||||
* @param sourceLang 源语言(可选,自动检测)
|
||||
* @param options 翻译选项
|
||||
*/
|
||||
async translateText(
|
||||
text: string,
|
||||
targetLang: string,
|
||||
sourceLang?: string,
|
||||
options: TranslationOptions = {}
|
||||
): Promise<AIResponse<TranslationResult>> {
|
||||
try {
|
||||
this.stats.totalRequests++
|
||||
|
||||
// 检查缓存
|
||||
const cacheKey = this.generateCacheKey(text, targetLang, sourceLang, options)
|
||||
const cached = this.getFromCache(cacheKey)
|
||||
if (cached) {
|
||||
return { success: true, data: cached }
|
||||
}
|
||||
|
||||
// 选择提供商
|
||||
const provider = options.provider || this.selectBestProvider()
|
||||
|
||||
// 执行翻译
|
||||
let result: TranslationResult
|
||||
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(`Unsupported translation provider: ${provider}`)
|
||||
}
|
||||
|
||||
// 质量检查
|
||||
if (result.qualityScore < (this.config.qualityThresholds?.translation || 0.7)) {
|
||||
console.warn(`Translation quality below threshold: ${result.qualityScore}`)
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
this.addToCache(cacheKey, result)
|
||||
|
||||
// 更新统计
|
||||
this.updateStats(result)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
tokensUsed: result.tokensUsed,
|
||||
processingTimeMs: result.processingTimeMs,
|
||||
costUSD: result.costUSD,
|
||||
provider: result.provider
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.stats.errorCount++
|
||||
const aiError: AIServiceError = {
|
||||
code: 'TRANSLATION_ERROR',
|
||||
message: error.message || 'Translation failed',
|
||||
provider: options.provider,
|
||||
retryable: this.isRetryableError(error)
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: aiError.message,
|
||||
errorCode: aiError.code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量翻译
|
||||
* @param texts 文本数组
|
||||
* @param targetLang 目标语言
|
||||
* @param sourceLang 源语言
|
||||
* @param options 翻译选项
|
||||
* @param batchOptions 批处理选项
|
||||
*/
|
||||
async translateBatch(
|
||||
texts: string[],
|
||||
targetLang: string,
|
||||
sourceLang?: string,
|
||||
options: TranslationOptions = {},
|
||||
batchOptions: BatchProcessingOptions = {
|
||||
batchSize: 10,
|
||||
concurrency: 3,
|
||||
retryCount: 2,
|
||||
delayMs: 1000
|
||||
}
|
||||
): Promise<AIResponse<TranslationResult[]>> {
|
||||
try {
|
||||
const results: TranslationResult[] = []
|
||||
const batches = this.createBatches(texts, batchOptions.batchSize)
|
||||
|
||||
for (let i = 0; i < batches.length; i++) {
|
||||
const batch = batches[i]
|
||||
const batchPromises = batch.map(async (text, index) => {
|
||||
try {
|
||||
const response = await this.translateText(text, targetLang, sourceLang, options)
|
||||
if (response.success && response.data) {
|
||||
return response.data
|
||||
}
|
||||
throw new Error(response.error || 'Translation failed')
|
||||
} catch (error) {
|
||||
if (batchOptions.onError) {
|
||||
batchOptions.onError(error, text)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
const batchResults = await Promise.allSettled(batchPromises)
|
||||
|
||||
for (const result of batchResults) {
|
||||
if (result.status === 'fulfilled') {
|
||||
results.push(result.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 进度回调
|
||||
if (batchOptions.onProgress) {
|
||||
batchOptions.onProgress(results.length, texts.length)
|
||||
}
|
||||
|
||||
// 批次间延迟
|
||||
if (i < batches.length - 1 && batchOptions.delayMs > 0) {
|
||||
await this.delay(batchOptions.delayMs)
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, data: results }
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Batch translation failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测语言
|
||||
* @param text 文本
|
||||
*/
|
||||
async detectLanguage(text: string): Promise<AIResponse<string>> {
|
||||
try {
|
||||
// 使用正则表达式和字符集进行基础检测
|
||||
const basicDetection = this.basicLanguageDetection(text)
|
||||
if (basicDetection.confidence > 0.8) {
|
||||
return { success: true, data: basicDetection.language }
|
||||
}
|
||||
|
||||
// 使用AI提供商进行检测
|
||||
const provider = this.selectBestProvider()
|
||||
let detectedLang: string
|
||||
|
||||
switch (provider) {
|
||||
case 'google':
|
||||
detectedLang = await this.detectLanguageWithGoogle(text)
|
||||
break
|
||||
case 'baidu':
|
||||
detectedLang = await this.detectLanguageWithBaidu(text)
|
||||
break
|
||||
default:
|
||||
detectedLang = basicDetection.language
|
||||
}
|
||||
|
||||
return { success: true, data: detectedLang, provider }
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Language detection failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支持的语言列表
|
||||
*/
|
||||
getSupportedLanguages(): string[] {
|
||||
return [
|
||||
'zh-CN', 'zh-TW', 'en', 'ja', 'ko', 'es', 'fr', 'de', 'it', 'pt',
|
||||
'ru', 'ar', 'hi', 'th', 'vi', 'id', 'ms', 'tl', 'tr', 'nl'
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取翻译统计
|
||||
*/
|
||||
getStatistics(): TranslationStats {
|
||||
this.updateCacheHitRate()
|
||||
return { ...this.stats }
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理缓存
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.cache.clear()
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
private async translateWithOpenAI(
|
||||
text: string,
|
||||
targetLang: string,
|
||||
sourceLang?: string,
|
||||
options: TranslationOptions = {}
|
||||
): Promise<TranslationResult> {
|
||||
const startTime = Date.now()
|
||||
|
||||
const systemPrompt = this.buildOpenAISystemPrompt(targetLang, sourceLang, options)
|
||||
const userPrompt = `请翻译以下文本到${this.getLanguageName(targetLang)}:\n\n${text}`
|
||||
|
||||
const requestBody = {
|
||||
model: options.model || this.config.openai?.model || 'gpt-3.5-turbo',
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt }
|
||||
],
|
||||
temperature: options.temperature || this.config.openai?.temperature || 0.3,
|
||||
max_tokens: options.maxTokens || this.config.openai?.maxTokens || 2000
|
||||
}
|
||||
|
||||
// 模拟API调用(实际项目中替换为真实的HTTP请求)
|
||||
const response = await this.mockOpenAIRequest(requestBody)
|
||||
|
||||
const processingTime = Date.now() - startTime
|
||||
const tokensUsed = response.usage.total_tokens
|
||||
const cost = this.calculateOpenAICost(tokensUsed, requestBody.model)
|
||||
|
||||
return {
|
||||
translatedText: response.choices[0].message.content.trim(),
|
||||
originalText: text,
|
||||
sourceLang: sourceLang || 'auto',
|
||||
targetLang,
|
||||
confidence: 0.95,
|
||||
qualityScore: this.evaluateTranslationQuality(text, response.choices[0].message.content),
|
||||
provider: 'openai',
|
||||
tokensUsed,
|
||||
processingTimeMs: processingTime,
|
||||
costUSD: cost
|
||||
}
|
||||
}
|
||||
|
||||
private async translateWithGoogle(
|
||||
text: string,
|
||||
targetLang: string,
|
||||
sourceLang?: string,
|
||||
options: TranslationOptions = {}
|
||||
): Promise<TranslationResult> {
|
||||
const startTime = Date.now()
|
||||
|
||||
// 模拟Google Translate API调用
|
||||
const response = await this.mockGoogleRequest({
|
||||
q: text,
|
||||
target: this.convertToGoogleLangCode(targetLang),
|
||||
source: sourceLang ? this.convertToGoogleLangCode(sourceLang) : undefined,
|
||||
format: 'text'
|
||||
})
|
||||
|
||||
const processingTime = Date.now() - startTime
|
||||
const tokensUsed = Math.ceil(text.length / 4) // 估算
|
||||
const cost = this.calculateGoogleCost(text.length)
|
||||
|
||||
return {
|
||||
translatedText: response.data.translations[0].translatedText,
|
||||
originalText: text,
|
||||
sourceLang: response.data.translations[0].detectedSourceLanguage || sourceLang || 'auto',
|
||||
targetLang,
|
||||
confidence: 0.92,
|
||||
qualityScore: this.evaluateTranslationQuality(text, response.data.translations[0].translatedText),
|
||||
provider: 'google',
|
||||
tokensUsed,
|
||||
processingTimeMs: processingTime,
|
||||
costUSD: cost
|
||||
}
|
||||
}
|
||||
|
||||
private async translateWithBaidu(
|
||||
text: string,
|
||||
targetLang: string,
|
||||
sourceLang?: string,
|
||||
options: TranslationOptions = {}
|
||||
): Promise<TranslationResult> {
|
||||
const startTime = Date.now()
|
||||
|
||||
// 模拟百度翻译API调用
|
||||
const response = await this.mockBaiduRequest({
|
||||
q: text,
|
||||
from: sourceLang ? this.convertToBaiduLangCode(sourceLang) : 'auto',
|
||||
to: this.convertToBaiduLangCode(targetLang),
|
||||
appid: this.config.baidu?.apiKey || '',
|
||||
salt: Date.now().toString(),
|
||||
sign: 'mock_sign'
|
||||
})
|
||||
|
||||
const processingTime = Date.now() - startTime
|
||||
const tokensUsed = Math.ceil(text.length / 4)
|
||||
const cost = this.calculateBaiduCost(text.length)
|
||||
|
||||
return {
|
||||
translatedText: response.trans_result[0].dst,
|
||||
originalText: text,
|
||||
sourceLang: response.from || sourceLang || 'auto',
|
||||
targetLang,
|
||||
confidence: 0.90,
|
||||
qualityScore: this.evaluateTranslationQuality(text, response.trans_result[0].dst),
|
||||
provider: 'baidu',
|
||||
tokensUsed,
|
||||
processingTimeMs: processingTime,
|
||||
costUSD: cost
|
||||
}
|
||||
}
|
||||
|
||||
private selectBestProvider(): AIProvider {
|
||||
// 根据配置和可用性选择最佳提供商
|
||||
if (this.config.openai?.apiKey) return 'openai'
|
||||
if (this.config.google?.apiKey) return 'google'
|
||||
if (this.config.baidu?.apiKey) return 'baidu'
|
||||
return 'openai' // 默认
|
||||
}
|
||||
|
||||
private generateCacheKey(
|
||||
text: string,
|
||||
targetLang: string,
|
||||
sourceLang?: string,
|
||||
options: TranslationOptions = {}
|
||||
): string {
|
||||
const optionsStr = JSON.stringify({
|
||||
provider: options.provider,
|
||||
temperature: options.temperature,
|
||||
culturalAdaptation: options.culturalAdaptation
|
||||
})
|
||||
return `${text}_${sourceLang || 'auto'}_${targetLang}_${optionsStr}`.replace(/\s+/g, '_')
|
||||
}
|
||||
|
||||
private getFromCache(key: string): TranslationResult | null {
|
||||
if (!this.cacheOptions.enabled) return null
|
||||
|
||||
const entry = this.cache.get(key)
|
||||
if (!entry) return null
|
||||
|
||||
// 检查TTL
|
||||
const now = Date.now()
|
||||
if (now > entry.createdAt + (entry.ttl * 60 * 60 * 1000)) {
|
||||
this.cache.delete(key)
|
||||
return null
|
||||
}
|
||||
|
||||
return entry.result
|
||||
}
|
||||
|
||||
private addToCache(key: string, result: TranslationResult): void {
|
||||
if (!this.cacheOptions.enabled) return
|
||||
|
||||
// 检查缓存大小限制
|
||||
if (this.cache.size >= this.cacheOptions.maxSize) {
|
||||
this.evictCache()
|
||||
}
|
||||
|
||||
this.cache.set(key, {
|
||||
key,
|
||||
result,
|
||||
createdAt: Date.now(),
|
||||
ttl: this.cacheOptions.ttlHours
|
||||
})
|
||||
}
|
||||
|
||||
private evictCache(): void {
|
||||
// LRU策略:删除最早的条目
|
||||
const oldestKey = this.cache.keys().next().value
|
||||
if (oldestKey) {
|
||||
this.cache.delete(oldestKey)
|
||||
}
|
||||
}
|
||||
|
||||
private initializeCache(): void {
|
||||
// 初始化缓存清理定时器
|
||||
setInterval(() => {
|
||||
this.cleanupExpiredCache()
|
||||
}, 60 * 60 * 1000) // 每小时清理一次
|
||||
}
|
||||
|
||||
private cleanupExpiredCache(): void {
|
||||
const now = Date.now()
|
||||
for (const [key, entry] of this.cache.entries()) {
|
||||
if (now > entry.createdAt + (entry.ttl * 60 * 60 * 1000)) {
|
||||
this.cache.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createBatches<T>(items: T[], batchSize: number): T[][] {
|
||||
const batches: T[][] = []
|
||||
for (let i = 0; i < items.length; i += batchSize) {
|
||||
batches.push(items.slice(i, i + batchSize))
|
||||
}
|
||||
return batches
|
||||
}
|
||||
|
||||
private async delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
private updateStats(result: TranslationResult): void {
|
||||
this.stats.successCount++
|
||||
this.stats.totalTokens += result.tokensUsed
|
||||
this.stats.totalCost += result.costUSD
|
||||
this.stats.avgQuality = (this.stats.avgQuality * (this.stats.successCount - 1) + result.qualityScore) / this.stats.successCount
|
||||
}
|
||||
|
||||
private updateCacheHitRate(): void {
|
||||
if (this.stats.totalRequests > 0) {
|
||||
const cacheHits = this.stats.totalRequests - this.stats.successCount - this.stats.errorCount
|
||||
this.stats.cacheHitRate = cacheHits / this.stats.totalRequests
|
||||
}
|
||||
}
|
||||
|
||||
private evaluateTranslationQuality(original: string, translated: string): number {
|
||||
// 简单的质量评估算法
|
||||
if (!translated || translated.length === 0) return 0
|
||||
|
||||
const lengthRatio = translated.length / original.length
|
||||
const lengthScore = lengthRatio > 0.5 && lengthRatio < 2 ? 1 : 0.7
|
||||
|
||||
// 检查是否包含原文(可能翻译失败)
|
||||
const similarityScore = original.toLowerCase() === translated.toLowerCase() ? 0.3 : 1
|
||||
|
||||
return (lengthScore + similarityScore) / 2
|
||||
}
|
||||
|
||||
private basicLanguageDetection(text: string): { language: string, confidence: number } {
|
||||
// 基于字符集的语言检测
|
||||
const chineseRegex = /[\u4e00-\u9fff]/
|
||||
const japaneseRegex = /[\u3040-\u309f\u30a0-\u30ff]/
|
||||
const koreanRegex = /[\uac00-\ud7af]/
|
||||
const arabicRegex = /[\u0600-\u06ff]/
|
||||
const russianRegex = /[\u0400-\u04ff]/
|
||||
|
||||
if (chineseRegex.test(text)) return { language: 'zh-CN', confidence: 0.9 }
|
||||
if (japaneseRegex.test(text)) return { language: 'ja', confidence: 0.9 }
|
||||
if (koreanRegex.test(text)) return { language: 'ko', confidence: 0.9 }
|
||||
if (arabicRegex.test(text)) return { language: 'ar', confidence: 0.8 }
|
||||
if (russianRegex.test(text)) return { language: 'ru', confidence: 0.8 }
|
||||
|
||||
return { language: 'en', confidence: 0.5 }
|
||||
}
|
||||
|
||||
private async detectLanguageWithGoogle(text: string): Promise<string> {
|
||||
// 模拟Google语言检测API
|
||||
const response = await this.mockGoogleDetectRequest({ q: text })
|
||||
return this.convertFromGoogleLangCode(response.data.detections[0][0].language)
|
||||
}
|
||||
|
||||
private async detectLanguageWithBaidu(text: string): Promise<string> {
|
||||
// 模拟百度语言检测API
|
||||
const response = await this.mockBaiduDetectRequest({ q: text })
|
||||
return this.convertFromBaiduLangCode(response.lan)
|
||||
}
|
||||
|
||||
private buildOpenAISystemPrompt(targetLang: string, sourceLang?: string, options: TranslationOptions = {}): string {
|
||||
let prompt = `你是一个专业的翻译助手。请将文本翻译成${this.getLanguageName(targetLang)}。`
|
||||
|
||||
if (options.culturalAdaptation) {
|
||||
prompt += ' 请注意文化适应性,确保翻译符合目标文化的表达习惯。'
|
||||
}
|
||||
|
||||
if (options.preserveFormatting) {
|
||||
prompt += ' 请保持原文的格式和结构。'
|
||||
}
|
||||
|
||||
prompt += ' 只返回翻译结果,不需要其他说明。'
|
||||
|
||||
return prompt
|
||||
}
|
||||
|
||||
private getLanguageName(langCode: string): string {
|
||||
const languageNames: Record<string, string> = {
|
||||
'zh-CN': '简体中文',
|
||||
'zh-TW': '繁体中文',
|
||||
'en': 'English',
|
||||
'ja': '日本語',
|
||||
'ko': '한국어',
|
||||
'es': 'Español',
|
||||
'fr': 'Français',
|
||||
'de': 'Deutsch',
|
||||
'it': 'Italiano',
|
||||
'pt': 'Português',
|
||||
'ru': 'Русский',
|
||||
'ar': 'العربية',
|
||||
'hi': 'हिन्दी',
|
||||
'th': 'ไทย',
|
||||
'vi': 'Tiếng Việt'
|
||||
}
|
||||
return languageNames[langCode] || langCode
|
||||
}
|
||||
|
||||
private convertToGoogleLangCode(langCode: string): string {
|
||||
const mapping: Record<string, string> = {
|
||||
'zh-CN': 'zh',
|
||||
'zh-TW': 'zh-tw'
|
||||
}
|
||||
return mapping[langCode] || langCode
|
||||
}
|
||||
|
||||
private convertFromGoogleLangCode(langCode: string): string {
|
||||
const mapping: Record<string, string> = {
|
||||
'zh': 'zh-CN',
|
||||
'zh-tw': 'zh-TW'
|
||||
}
|
||||
return mapping[langCode] || langCode
|
||||
}
|
||||
|
||||
private convertToBaiduLangCode(langCode: string): string {
|
||||
const mapping: Record<string, string> = {
|
||||
'zh-CN': 'zh',
|
||||
'zh-TW': 'cht',
|
||||
'en': 'en',
|
||||
'ja': 'jp',
|
||||
'ko': 'kor',
|
||||
'es': 'spa',
|
||||
'fr': 'fra',
|
||||
'de': 'de',
|
||||
'ru': 'ru',
|
||||
'ar': 'ara'
|
||||
}
|
||||
return mapping[langCode] || 'en'
|
||||
}
|
||||
|
||||
private convertFromBaiduLangCode(langCode: string): string {
|
||||
const mapping: Record<string, string> = {
|
||||
'zh': 'zh-CN',
|
||||
'cht': 'zh-TW',
|
||||
'en': 'en',
|
||||
'jp': 'ja',
|
||||
'kor': 'ko',
|
||||
'spa': 'es',
|
||||
'fra': 'fr',
|
||||
'de': 'de',
|
||||
'ru': 'ru',
|
||||
'ara': 'ar'
|
||||
}
|
||||
return mapping[langCode] || langCode
|
||||
}
|
||||
|
||||
private calculateOpenAICost(tokens: number, model: string): number {
|
||||
const pricing: Record<string, { input: number, output: number }> = {
|
||||
'gpt-3.5-turbo': { input: 0.0015, output: 0.002 },
|
||||
'gpt-4': { input: 0.03, output: 0.06 },
|
||||
'gpt-4-turbo': { input: 0.01, output: 0.03 }
|
||||
}
|
||||
const modelPricing = pricing[model] || pricing['gpt-3.5-turbo']
|
||||
return (tokens / 1000) * ((modelPricing.input + modelPricing.output) / 2)
|
||||
}
|
||||
|
||||
private calculateGoogleCost(textLength: number): number {
|
||||
// Google Translate pricing: $20 per 1M characters
|
||||
return (textLength / 1000000) * 20
|
||||
}
|
||||
|
||||
private calculateBaiduCost(textLength: number): number {
|
||||
// 百度翻译定价较低
|
||||
return (textLength / 1000000) * 10
|
||||
}
|
||||
|
||||
private isRetryableError(error: any): boolean {
|
||||
// 判断错误是否可重试
|
||||
const retryableCodes = ['TIMEOUT', 'RATE_LIMIT', 'SERVER_ERROR']
|
||||
return retryableCodes.includes(error.code) || error.status >= 500
|
||||
}
|
||||
|
||||
// Mock API methods (在实际项目中替换为真实的HTTP请求)
|
||||
private async mockOpenAIRequest(requestBody: any): Promise<any> {
|
||||
await this.delay(Math.random() * 1000 + 500) // 模拟网络延迟
|
||||
return {
|
||||
choices: [{
|
||||
message: {
|
||||
content: `[Translated by OpenAI] ${requestBody.messages[1].content.split(':\n\n')[1] || 'Translation result'}`
|
||||
}
|
||||
}],
|
||||
usage: {
|
||||
total_tokens: Math.ceil(requestBody.messages[1].content.length / 4) + 50
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async mockGoogleRequest(params: any): Promise<any> {
|
||||
await this.delay(Math.random() * 800 + 400)
|
||||
return {
|
||||
data: {
|
||||
translations: [{
|
||||
translatedText: `[Translated by Google] ${params.q}`,
|
||||
detectedSourceLanguage: 'zh'
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async mockBaiduRequest(params: any): Promise<any> {
|
||||
await this.delay(Math.random() * 600 + 300)
|
||||
return {
|
||||
trans_result: [{
|
||||
src: params.q,
|
||||
dst: `[Translated by Baidu] ${params.q}`
|
||||
}],
|
||||
from: params.from
|
||||
}
|
||||
}
|
||||
|
||||
private async mockGoogleDetectRequest(params: any): Promise<any> {
|
||||
await this.delay(200)
|
||||
return {
|
||||
data: {
|
||||
detections: [[{
|
||||
language: 'zh',
|
||||
confidence: 0.95
|
||||
}]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async mockBaiduDetectRequest(params: any): Promise<any> {
|
||||
await this.delay(200)
|
||||
return {
|
||||
lan: 'zh',
|
||||
confidence: 0.92
|
||||
}
|
||||
}
|
||||
}
|
||||
755
uni_modules/ak-ai-news/services/ContentProcessingPipeline.uts
Normal file
755
uni_modules/ak-ai-news/services/ContentProcessingPipeline.uts
Normal file
@@ -0,0 +1,755 @@
|
||||
// Content Processing Pipeline - Automated news content workflow
|
||||
|
||||
import {
|
||||
ContentInfo,
|
||||
ProcessingStep,
|
||||
AIProvider,
|
||||
AIResponse,
|
||||
AIServiceConfig,
|
||||
AIServiceError,
|
||||
TranslationResult,
|
||||
ContentAnalysisResult,
|
||||
BatchProcessingOptions
|
||||
} from '../types/ai-types.uts'
|
||||
|
||||
import { AITranslationService } from './AITranslationService.uts'
|
||||
import { AIContentAnalysisService } from './AIContentAnalysisService.uts'
|
||||
|
||||
// 处理阶段枚举
|
||||
type ProcessingStage =
|
||||
| 'fetching'
|
||||
| 'validation'
|
||||
| 'analysis'
|
||||
| 'translation'
|
||||
| 'categorization'
|
||||
| 'quality_check'
|
||||
| 'storage'
|
||||
| 'indexing'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
|
||||
// 处理状态
|
||||
type ProcessingStatus = {
|
||||
contentId: string
|
||||
stage: ProcessingStage
|
||||
progress: number // 0-100
|
||||
startTime: number
|
||||
lastUpdateTime: number
|
||||
completedSteps: string[]
|
||||
errors: Array<{ step: string, error: string, timestamp: number }>
|
||||
metadata: UTSJSONObject
|
||||
}
|
||||
|
||||
// 管道配置
|
||||
type PipelineConfig = {
|
||||
enabledSteps: string[]
|
||||
parallelProcessing: boolean
|
||||
maxConcurrency: number
|
||||
retryCount: number
|
||||
timeoutMs: number
|
||||
qualityThreshold: number
|
||||
targetLanguages: string[]
|
||||
categorization: {
|
||||
enabled: boolean
|
||||
threshold: number
|
||||
maxCategories: number
|
||||
}
|
||||
translation: {
|
||||
enabled: boolean
|
||||
targetLanguages: string[]
|
||||
qualityThreshold: number
|
||||
}
|
||||
analysis: {
|
||||
enabled: boolean
|
||||
types: string[]
|
||||
includeScores: boolean
|
||||
}
|
||||
}
|
||||
|
||||
// 处理结果
|
||||
type ProcessingResult = {
|
||||
contentId: string
|
||||
originalContent: ContentInfo
|
||||
processedContent: ContentInfo
|
||||
translations: Record<string, TranslationResult>
|
||||
analysis: ContentAnalysisResult
|
||||
categories: string[]
|
||||
qualityScore: number
|
||||
processingTime: number
|
||||
totalCost: number
|
||||
status: ProcessingStage
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
// 管道统计
|
||||
type PipelineStats = {
|
||||
totalProcessed: number
|
||||
successCount: number
|
||||
errorCount: number
|
||||
avgProcessingTime: number
|
||||
totalCost: number
|
||||
stageStats: Record<ProcessingStage, {
|
||||
count: number
|
||||
avgTime: number
|
||||
errorRate: number
|
||||
}>
|
||||
dailyThroughput: number
|
||||
lastProcessedAt: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 内容处理管道服务
|
||||
* 自动化新闻内容的获取、分析、翻译、分类等全流程处理
|
||||
*/
|
||||
export class ContentProcessingPipeline {
|
||||
private config: AIServiceConfig
|
||||
private pipelineConfig: PipelineConfig
|
||||
private translationService: AITranslationService
|
||||
private analysisService: AIContentAnalysisService
|
||||
private processingQueue: Map<string, ProcessingStatus> = new Map()
|
||||
private processingSteps: Map<string, ProcessingStep> = new Map()
|
||||
private stats: PipelineStats
|
||||
|
||||
constructor(
|
||||
aiConfig: AIServiceConfig,
|
||||
pipelineConfig: Partial<PipelineConfig> = {}
|
||||
) {
|
||||
this.config = aiConfig
|
||||
this.pipelineConfig = this.createDefaultPipelineConfig(pipelineConfig)
|
||||
this.translationService = new AITranslationService(aiConfig)
|
||||
this.analysisService = new AIContentAnalysisService(aiConfig)
|
||||
this.stats = this.initializeStats()
|
||||
this.initializeProcessingSteps()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个内容
|
||||
* @param content 原始内容
|
||||
*/
|
||||
async processContent(content: ContentInfo): Promise<AIResponse<ProcessingResult>> {
|
||||
try {
|
||||
const contentId = content.id
|
||||
const startTime = Date.now()
|
||||
|
||||
// 初始化处理状态
|
||||
const status: ProcessingStatus = {
|
||||
contentId,
|
||||
stage: 'validation',
|
||||
progress: 0,
|
||||
startTime,
|
||||
lastUpdateTime: startTime,
|
||||
completedSteps: [],
|
||||
errors: [],
|
||||
metadata: {}
|
||||
}
|
||||
|
||||
this.processingQueue.set(contentId, status)
|
||||
|
||||
// 执行处理步骤
|
||||
const result = await this.executeProcessingPipeline(content, status)
|
||||
|
||||
// 清理处理队列
|
||||
this.processingQueue.delete(contentId)
|
||||
|
||||
// 更新统计
|
||||
this.updateStats(result)
|
||||
|
||||
return { success: true, data: result }
|
||||
|
||||
} catch (error) {
|
||||
const aiError: AIServiceError = {
|
||||
code: 'PIPELINE_ERROR',
|
||||
message: error.message || 'Content processing failed',
|
||||
retryable: this.isRetryableError(error)
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: aiError.message,
|
||||
errorCode: aiError.code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量处理内容
|
||||
* @param contents 内容列表
|
||||
* @param batchOptions 批处理选项
|
||||
*/
|
||||
async processBatch(
|
||||
contents: ContentInfo[],
|
||||
batchOptions: BatchProcessingOptions = {
|
||||
batchSize: 5,
|
||||
concurrency: 3,
|
||||
retryCount: 2,
|
||||
delayMs: 1000
|
||||
}
|
||||
): Promise<AIResponse<ProcessingResult[]>> {
|
||||
try {
|
||||
const results: ProcessingResult[] = []
|
||||
const batches = this.createBatches(contents, batchOptions.batchSize)
|
||||
|
||||
for (let i = 0; i < batches.length; i++) {
|
||||
const batch = batches[i]
|
||||
|
||||
if (this.pipelineConfig.parallelProcessing) {
|
||||
// 并行处理
|
||||
const batchPromises = batch.map(async (content) => {
|
||||
try {
|
||||
const response = await this.processContent(content)
|
||||
if (response.success && response.data) {
|
||||
return response.data
|
||||
}
|
||||
throw new Error(response.error || 'Processing failed')
|
||||
} catch (error) {
|
||||
if (batchOptions.onError) {
|
||||
batchOptions.onError(error, content)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
const batchResults = await Promise.allSettled(batchPromises)
|
||||
|
||||
for (const result of batchResults) {
|
||||
if (result.status === 'fulfilled') {
|
||||
results.push(result.value)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 串行处理
|
||||
for (const content of batch) {
|
||||
try {
|
||||
const response = await this.processContent(content)
|
||||
if (response.success && response.data) {
|
||||
results.push(response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
if (batchOptions.onError) {
|
||||
batchOptions.onError(error, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 进度回调
|
||||
if (batchOptions.onProgress) {
|
||||
batchOptions.onProgress(results.length, contents.length)
|
||||
}
|
||||
|
||||
// 批次间延迟
|
||||
if (i < batches.length - 1 && batchOptions.delayMs > 0) {
|
||||
await this.delay(batchOptions.delayMs)
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, data: results }
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Batch processing failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取处理状态
|
||||
* @param contentId 内容ID
|
||||
*/
|
||||
getProcessingStatus(contentId: string): ProcessingStatus | null {
|
||||
return this.processingQueue.get(contentId) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有处理中的内容状态
|
||||
*/
|
||||
getAllProcessingStatus(): ProcessingStatus[] {
|
||||
return Array.from(this.processingQueue.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加自定义处理步骤
|
||||
* @param step 处理步骤
|
||||
*/
|
||||
addProcessingStep(step: ProcessingStep): void {
|
||||
this.processingSteps.set(step.name, step)
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除处理步骤
|
||||
* @param stepName 步骤名称
|
||||
*/
|
||||
removeProcessingStep(stepName: string): void {
|
||||
this.processingSteps.delete(stepName)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新管道配置
|
||||
* @param config 新配置
|
||||
*/
|
||||
updatePipelineConfig(config: Partial<PipelineConfig>): void {
|
||||
this.pipelineConfig = { ...this.pipelineConfig, ...config }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取管道统计
|
||||
*/
|
||||
getPipelineStatistics(): PipelineStats {
|
||||
return { ...this.stats }
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置统计数据
|
||||
*/
|
||||
resetStatistics(): void {
|
||||
this.stats = this.initializeStats()
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
private async executeProcessingPipeline(
|
||||
content: ContentInfo,
|
||||
status: ProcessingStatus
|
||||
): Promise<ProcessingResult> {
|
||||
|
||||
const result: ProcessingResult = {
|
||||
contentId: content.id,
|
||||
originalContent: content,
|
||||
processedContent: { ...content },
|
||||
translations: {},
|
||||
analysis: {} as ContentAnalysisResult,
|
||||
categories: [],
|
||||
qualityScore: 0,
|
||||
processingTime: 0,
|
||||
totalCost: 0,
|
||||
status: 'fetching',
|
||||
errors: []
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 内容验证
|
||||
await this.executeStep('validation', content, result, status)
|
||||
|
||||
// 2. 内容分析
|
||||
if (this.pipelineConfig.analysis.enabled) {
|
||||
await this.executeStep('analysis', content, result, status)
|
||||
}
|
||||
|
||||
// 3. 内容翻译
|
||||
if (this.pipelineConfig.translation.enabled && this.pipelineConfig.translation.targetLanguages.length > 0) {
|
||||
await this.executeStep('translation', content, result, status)
|
||||
}
|
||||
|
||||
// 4. 内容分类
|
||||
if (this.pipelineConfig.categorization.enabled) {
|
||||
await this.executeStep('categorization', content, result, status)
|
||||
}
|
||||
|
||||
// 5. 质量检查
|
||||
await this.executeStep('quality_check', content, result, status)
|
||||
|
||||
// 6. 存储处理
|
||||
await this.executeStep('storage', content, result, status)
|
||||
|
||||
// 7. 索引构建
|
||||
await this.executeStep('indexing', content, result, status)
|
||||
|
||||
// 完成处理
|
||||
result.status = 'completed'
|
||||
result.processingTime = Date.now() - status.startTime
|
||||
this.updateProcessingStatus(status, 'completed', 100)
|
||||
|
||||
} catch (error) {
|
||||
result.status = 'failed'
|
||||
result.errors.push(error.message || 'Unknown error')
|
||||
this.updateProcessingStatus(status, 'failed', status.progress, error.message)
|
||||
throw error
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private async executeStep(
|
||||
stepName: string,
|
||||
content: ContentInfo,
|
||||
result: ProcessingResult,
|
||||
status: ProcessingStatus
|
||||
): Promise<void> {
|
||||
|
||||
const step = this.processingSteps.get(stepName)
|
||||
if (!step) {
|
||||
throw new Error(`Processing step '${stepName}' not found`)
|
||||
}
|
||||
|
||||
try {
|
||||
// 验证前置条件
|
||||
if (step.validate && !step.validate(result)) {
|
||||
throw new Error(`Validation failed for step '${stepName}'`)
|
||||
}
|
||||
|
||||
// 执行步骤
|
||||
const stepResult = await step.execute(result)
|
||||
|
||||
// 更新结果
|
||||
if (stepResult) {
|
||||
Object.assign(result, stepResult)
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
status.completedSteps.push(stepName)
|
||||
const progress = (status.completedSteps.length / 7) * 100 // 7个主要步骤
|
||||
this.updateProcessingStatus(status, this.getStageFromStep(stepName), progress)
|
||||
|
||||
} catch (error) {
|
||||
// 记录错误
|
||||
status.errors.push({
|
||||
step: stepName,
|
||||
error: error.message || 'Unknown error',
|
||||
timestamp: Date.now()
|
||||
})
|
||||
|
||||
// 尝试回滚
|
||||
if (step.rollback) {
|
||||
try {
|
||||
await step.rollback(result)
|
||||
} catch (rollbackError) {
|
||||
console.error(`Rollback failed for step '${stepName}':`, rollbackError)
|
||||
}
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private updateProcessingStatus(
|
||||
status: ProcessingStatus,
|
||||
stage: ProcessingStage,
|
||||
progress: number,
|
||||
error?: string
|
||||
): void {
|
||||
status.stage = stage
|
||||
status.progress = progress
|
||||
status.lastUpdateTime = Date.now()
|
||||
|
||||
if (error) {
|
||||
status.errors.push({
|
||||
step: stage,
|
||||
error,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private getStageFromStep(stepName: string): ProcessingStage {
|
||||
const stageMap: Record<string, ProcessingStage> = {
|
||||
'validation': 'validation',
|
||||
'analysis': 'analysis',
|
||||
'translation': 'translation',
|
||||
'categorization': 'categorization',
|
||||
'quality_check': 'quality_check',
|
||||
'storage': 'storage',
|
||||
'indexing': 'indexing'
|
||||
}
|
||||
|
||||
return stageMap[stepName] || 'validation'
|
||||
}
|
||||
|
||||
private initializeProcessingSteps(): void {
|
||||
// 内容验证步骤
|
||||
this.processingSteps.set('validation', {
|
||||
name: 'validation',
|
||||
order: 1,
|
||||
execute: async (data: ProcessingResult) => {
|
||||
const content = data.originalContent
|
||||
|
||||
// 验证必需字段
|
||||
if (!content.title || content.title.trim().length === 0) {
|
||||
throw new Error('Content title is required')
|
||||
}
|
||||
|
||||
if (!content.content || content.content.trim().length < 50) {
|
||||
throw new Error('Content is too short (minimum 50 characters)')
|
||||
}
|
||||
|
||||
// 验证内容质量
|
||||
if (content.quality !== undefined && content.quality < this.pipelineConfig.qualityThreshold) {
|
||||
throw new Error(`Content quality (${content.quality}) below threshold (${this.pipelineConfig.qualityThreshold})`)
|
||||
}
|
||||
|
||||
return data
|
||||
},
|
||||
validate: (data: ProcessingResult) => {
|
||||
return data.originalContent && data.originalContent.title && data.originalContent.content
|
||||
}
|
||||
})
|
||||
|
||||
// 内容分析步骤
|
||||
this.processingSteps.set('analysis', {
|
||||
name: 'analysis',
|
||||
order: 2,
|
||||
execute: async (data: ProcessingResult) => {
|
||||
const response = await this.analysisService.analyzeContent(data.originalContent.content, {
|
||||
types: this.pipelineConfig.analysis.types as any,
|
||||
includeScores: this.pipelineConfig.analysis.includeScores,
|
||||
language: data.originalContent.originalLanguage
|
||||
})
|
||||
|
||||
if (response.success && response.data) {
|
||||
data.analysis = response.data
|
||||
data.processedContent.sentiment = response.data.sentimentScore
|
||||
data.processedContent.readability = response.data.readabilityScore
|
||||
data.processedContent.credibility = response.data.credibilityScore
|
||||
data.processedContent.keywords = response.data.keywords
|
||||
data.totalCost += (response.costUSD || 0)
|
||||
} else {
|
||||
throw new Error(response.error || 'Content analysis failed')
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
})
|
||||
|
||||
// 内容翻译步骤
|
||||
this.processingSteps.set('translation', {
|
||||
name: 'translation',
|
||||
order: 3,
|
||||
execute: async (data: ProcessingResult) => {
|
||||
const sourceContent = data.originalContent
|
||||
const targetLanguages = this.pipelineConfig.translation.targetLanguages
|
||||
|
||||
for (const targetLang of targetLanguages) {
|
||||
if (targetLang === sourceContent.originalLanguage) continue
|
||||
|
||||
// 翻译标题
|
||||
const titleResponse = await this.translationService.translateText(
|
||||
sourceContent.title,
|
||||
targetLang,
|
||||
sourceContent.originalLanguage,
|
||||
{ qualityThreshold: this.pipelineConfig.translation.qualityThreshold }
|
||||
)
|
||||
|
||||
// 翻译内容
|
||||
const contentResponse = await this.translationService.translateText(
|
||||
sourceContent.content,
|
||||
targetLang,
|
||||
sourceContent.originalLanguage,
|
||||
{ qualityThreshold: this.pipelineConfig.translation.qualityThreshold }
|
||||
)
|
||||
|
||||
if (titleResponse.success && contentResponse.success && titleResponse.data && contentResponse.data) {
|
||||
data.translations[targetLang] = {
|
||||
...contentResponse.data,
|
||||
translatedText: `${titleResponse.data.translatedText}\n\n${contentResponse.data.translatedText}`
|
||||
}
|
||||
data.totalCost += (titleResponse.costUSD || 0) + (contentResponse.costUSD || 0)
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
})
|
||||
|
||||
// 内容分类步骤
|
||||
this.processingSteps.set('categorization', {
|
||||
name: 'categorization',
|
||||
order: 4,
|
||||
execute: async (data: ProcessingResult) => {
|
||||
if (data.analysis && data.analysis.categories) {
|
||||
const validCategories = data.analysis.categories
|
||||
.filter(cat => cat.confidence >= this.pipelineConfig.categorization.threshold)
|
||||
.slice(0, this.pipelineConfig.categorization.maxCategories)
|
||||
.map(cat => cat.categoryId)
|
||||
|
||||
data.categories = validCategories
|
||||
|
||||
// 设置主分类
|
||||
if (validCategories.length > 0) {
|
||||
data.processedContent.categoryId = validCategories[0]
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
})
|
||||
|
||||
// 质量检查步骤
|
||||
this.processingSteps.set('quality_check', {
|
||||
name: 'quality_check',
|
||||
order: 5,
|
||||
execute: async (data: ProcessingResult) => {
|
||||
let qualityScore = 0
|
||||
let factors = 0
|
||||
|
||||
// 基于分析结果的质量评估
|
||||
if (data.analysis) {
|
||||
if (data.analysis.readabilityScore !== undefined) {
|
||||
qualityScore += data.analysis.readabilityScore
|
||||
factors++
|
||||
}
|
||||
|
||||
if (data.analysis.credibilityScore !== undefined) {
|
||||
qualityScore += data.analysis.credibilityScore
|
||||
factors++
|
||||
}
|
||||
|
||||
// 毒性检查
|
||||
if (data.analysis.toxicityScore !== undefined) {
|
||||
qualityScore += (1 - data.analysis.toxicityScore) // 毒性越低质量越高
|
||||
factors++
|
||||
}
|
||||
}
|
||||
|
||||
// 内容长度评估
|
||||
const contentLength = data.originalContent.content.length
|
||||
const lengthScore = contentLength > 500 ? 1 : contentLength / 500
|
||||
qualityScore += lengthScore
|
||||
factors++
|
||||
|
||||
// 翻译质量评估
|
||||
if (Object.keys(data.translations).length > 0) {
|
||||
const translationQualities = Object.values(data.translations).map(t => t.qualityScore)
|
||||
const avgTranslationQuality = translationQualities.reduce((sum, q) => sum + q, 0) / translationQualities.length
|
||||
qualityScore += avgTranslationQuality
|
||||
factors++
|
||||
}
|
||||
|
||||
data.qualityScore = factors > 0 ? qualityScore / factors : 0.5
|
||||
data.processedContent.quality = data.qualityScore
|
||||
|
||||
// 质量阈值检查
|
||||
if (data.qualityScore < this.pipelineConfig.qualityThreshold) {
|
||||
console.warn(`Content quality (${data.qualityScore}) below threshold (${this.pipelineConfig.qualityThreshold})`)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
})
|
||||
|
||||
// 存储步骤
|
||||
this.processingSteps.set('storage', {
|
||||
name: 'storage',
|
||||
order: 6,
|
||||
execute: async (data: ProcessingResult) => {
|
||||
// 模拟存储操作
|
||||
await this.delay(100)
|
||||
|
||||
// 在实际实现中,这里会将处理后的内容保存到数据库
|
||||
data.processedContent.status = 'published'
|
||||
data.processedContent.tags = [...(data.processedContent.tags || []), ...data.categories]
|
||||
|
||||
return data
|
||||
}
|
||||
})
|
||||
|
||||
// 索引构建步骤
|
||||
this.processingSteps.set('indexing', {
|
||||
name: 'indexing',
|
||||
order: 7,
|
||||
execute: async (data: ProcessingResult) => {
|
||||
// 模拟索引构建
|
||||
await this.delay(50)
|
||||
|
||||
// 在实际实现中,这里会更新搜索索引
|
||||
console.log(`Content indexed: ${data.contentId}`)
|
||||
|
||||
return data
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private createDefaultPipelineConfig(overrides: Partial<PipelineConfig>): PipelineConfig {
|
||||
return {
|
||||
enabledSteps: ['validation', 'analysis', 'translation', 'categorization', 'quality_check', 'storage', 'indexing'],
|
||||
parallelProcessing: true,
|
||||
maxConcurrency: 3,
|
||||
retryCount: 2,
|
||||
timeoutMs: 300000, // 5分钟
|
||||
qualityThreshold: 0.7,
|
||||
targetLanguages: ['zh-CN', 'en'],
|
||||
categorization: {
|
||||
enabled: true,
|
||||
threshold: 0.6,
|
||||
maxCategories: 3
|
||||
},
|
||||
translation: {
|
||||
enabled: true,
|
||||
targetLanguages: ['zh-CN', 'en'],
|
||||
qualityThreshold: 0.7
|
||||
},
|
||||
analysis: {
|
||||
enabled: true,
|
||||
types: ['sentiment', 'entities', 'topics', 'categories', 'readability', 'credibility', 'summary', 'keywords'],
|
||||
includeScores: true
|
||||
},
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
private initializeStats(): PipelineStats {
|
||||
const stages: ProcessingStage[] = [
|
||||
'fetching', 'validation', 'analysis', 'translation',
|
||||
'categorization', 'quality_check', 'storage', 'indexing',
|
||||
'completed', 'failed'
|
||||
]
|
||||
|
||||
const stageStats: Record<ProcessingStage, any> = {} as Record<ProcessingStage, any>
|
||||
stages.forEach(stage => {
|
||||
stageStats[stage] = {
|
||||
count: 0,
|
||||
avgTime: 0,
|
||||
errorRate: 0
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
totalProcessed: 0,
|
||||
successCount: 0,
|
||||
errorCount: 0,
|
||||
avgProcessingTime: 0,
|
||||
totalCost: 0,
|
||||
stageStats,
|
||||
dailyThroughput: 0,
|
||||
lastProcessedAt: 0
|
||||
}
|
||||
}
|
||||
|
||||
private updateStats(result: ProcessingResult): void {
|
||||
this.stats.totalProcessed++
|
||||
this.stats.lastProcessedAt = Date.now()
|
||||
|
||||
if (result.status === 'completed') {
|
||||
this.stats.successCount++
|
||||
} else {
|
||||
this.stats.errorCount++
|
||||
}
|
||||
|
||||
// 更新平均处理时间
|
||||
this.stats.avgProcessingTime = (
|
||||
this.stats.avgProcessingTime * (this.stats.totalProcessed - 1) + result.processingTime
|
||||
) / this.stats.totalProcessed
|
||||
|
||||
// 更新总成本
|
||||
this.stats.totalCost += result.totalCost
|
||||
|
||||
// 更新阶段统计
|
||||
this.stats.stageStats[result.status].count++
|
||||
}
|
||||
|
||||
private createBatches<T>(items: T[], batchSize: number): T[][] {
|
||||
const batches: T[][] = []
|
||||
for (let i = 0; i < items.length; i += batchSize) {
|
||||
batches.push(items.slice(i, i + batchSize))
|
||||
}
|
||||
return batches
|
||||
}
|
||||
|
||||
private async delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
private isRetryableError(error: any): boolean {
|
||||
const retryableCodes = ['TIMEOUT', 'RATE_LIMIT', 'SERVER_ERROR', 'NETWORK_ERROR']
|
||||
return retryableCodes.includes(error.code) || error.status >= 500
|
||||
}
|
||||
}
|
||||
563
uni_modules/ak-ai-news/services/ai-content-analysis-service.uts
Normal file
563
uni_modules/ak-ai-news/services/ai-content-analysis-service.uts
Normal file
@@ -0,0 +1,563 @@
|
||||
// AI内容分析服务
|
||||
// filepath: h:\blews\akmon\uni_modules\ak-ai-news\services\ai-content-analysis-service.uts
|
||||
|
||||
import { AkReq } from '@/uni_modules/ak-req/index.uts'
|
||||
import type {
|
||||
ContentAnalysisResult,
|
||||
EntityResult,
|
||||
TopicResult,
|
||||
CategoryResult,
|
||||
AIProvider,
|
||||
AIResponse,
|
||||
AIServiceConfig,
|
||||
ContentInfo
|
||||
} from '../types/ai-types.uts'
|
||||
|
||||
export class AIContentAnalysisService {
|
||||
private config: AIServiceConfig
|
||||
private req: AkReq
|
||||
private cache: Map<string, ContentAnalysisResult> = new Map()
|
||||
|
||||
constructor(config: AIServiceConfig) {
|
||||
this.config = config
|
||||
this.req = new AkReq()
|
||||
}
|
||||
|
||||
/**
|
||||
* 综合内容分析
|
||||
*/
|
||||
async analyzeContent(
|
||||
content: ContentInfo,
|
||||
options?: {
|
||||
includeEntities?: boolean
|
||||
includeTopics?: boolean
|
||||
includeSentiment?: boolean
|
||||
includeReadability?: boolean
|
||||
includeCredibility?: boolean
|
||||
language?: string
|
||||
}
|
||||
): Promise<AIResponse<ContentAnalysisResult>> {
|
||||
try {
|
||||
const startTime = Date.now()
|
||||
|
||||
// 检查缓存
|
||||
const cacheKey = this.generateContentCacheKey(content.id, options)
|
||||
const cached = this.cache.get(cacheKey)
|
||||
if (cached) {
|
||||
return {
|
||||
success: true,
|
||||
data: cached,
|
||||
processingTimeMs: 0
|
||||
}
|
||||
}
|
||||
|
||||
const analysisPromises: Promise<any>[] = []
|
||||
|
||||
// 情感分析
|
||||
if (options?.includeSentiment !== false) {
|
||||
analysisPromises.push(this.analyzeSentiment(content.content, content.title))
|
||||
}
|
||||
|
||||
// 实体识别
|
||||
if (options?.includeEntities !== false) {
|
||||
analysisPromises.push(this.extractEntities(content.content))
|
||||
}
|
||||
|
||||
// 主题提取
|
||||
if (options?.includeTopics !== false) {
|
||||
analysisPromises.push(this.extractTopics(content.content))
|
||||
}
|
||||
|
||||
// 可读性分析
|
||||
if (options?.includeReadability !== false) {
|
||||
analysisPromises.push(this.analyzeReadability(content.content))
|
||||
}
|
||||
|
||||
// 可信度分析
|
||||
if (options?.includeCredibility !== false) {
|
||||
analysisPromises.push(this.analyzeCredibility(content))
|
||||
}
|
||||
|
||||
// 并行执行所有分析
|
||||
const results = await Promise.all(analysisPromises)
|
||||
|
||||
// 生成摘要
|
||||
const summary = await this.generateSummary(content.content)
|
||||
|
||||
// 提取关键词
|
||||
const keywords = await this.extractKeywords(content.content)
|
||||
|
||||
// 分类内容
|
||||
const categories = await this.classifyContent(content)
|
||||
|
||||
const analysisResult: ContentAnalysisResult = {
|
||||
contentId: content.id,
|
||||
sentimentScore: results[0]?.score || 0,
|
||||
sentimentLabel: results[0]?.label || 'neutral',
|
||||
readabilityScore: results[3] || 0.5,
|
||||
credibilityScore: results[4] || 0.5,
|
||||
toxicityScore: 0, // 可以添加毒性检测
|
||||
keywords: keywords || [],
|
||||
entities: results[1] || [],
|
||||
topics: results[2] || [],
|
||||
categories: categories || [],
|
||||
summary: summary || '',
|
||||
keyPhrases: this.extractKeyPhrases(content.content),
|
||||
language: options?.language || content.originalLanguage,
|
||||
processingTimeMs: Date.now() - startTime,
|
||||
provider: 'openai'
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
this.cache.set(cacheKey, analysisResult)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: analysisResult,
|
||||
processingTimeMs: analysisResult.processingTimeMs
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('内容分析失败:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '内容分析服务异常',
|
||||
errorCode: 'CONTENT_ANALYSIS_FAILED'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 情感分析
|
||||
*/
|
||||
private async analyzeSentiment(content: string, title?: string): Promise<{score: number, label: string}> {
|
||||
try {
|
||||
const openaiConfig = this.config.openai
|
||||
if (!openaiConfig) {
|
||||
throw new Error('OpenAI配置未找到')
|
||||
}
|
||||
|
||||
const text = title ? `${title}\n\n${content}` : content
|
||||
const prompt = `请分析以下文本的情感倾向,返回一个-1到1之间的数值(-1表示非常负面,0表示中性,1表示非常正面)和对应的标签(positive/negative/neutral)。
|
||||
|
||||
文本:${text.substring(0, 2000)}
|
||||
|
||||
请以JSON格式返回:{"score": 数值, "label": "标签"}`
|
||||
|
||||
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: openaiConfig.model,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: '你是一个专业的文本情感分析助手。'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt
|
||||
}
|
||||
],
|
||||
max_tokens: 100,
|
||||
temperature: 0.1
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.success || !response.data?.choices?.[0]) {
|
||||
throw new Error('情感分析API调用失败')
|
||||
}
|
||||
|
||||
const result = JSON.parse(response.data.choices[0].message.content)
|
||||
return {
|
||||
score: Math.max(-1, Math.min(1, parseFloat(result.score) || 0)),
|
||||
label: result.label || 'neutral'
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('情感分析失败:', error)
|
||||
return { score: 0, label: 'neutral' }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体识别
|
||||
*/
|
||||
private async extractEntities(content: string): Promise<EntityResult[]> {
|
||||
try {
|
||||
const openaiConfig = this.config.openai
|
||||
if (!openaiConfig) {
|
||||
return []
|
||||
}
|
||||
|
||||
const prompt = `请从以下文本中识别出人名、地名、机构名、日期、金额等实体。
|
||||
|
||||
文本:${content.substring(0, 2000)}
|
||||
|
||||
请以JSON数组格式返回,每个实体包含:text(实体文本)、type(类型:person/location/organization/date/money/other)、confidence(置信度0-1)。`
|
||||
|
||||
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: openaiConfig.model,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: '你是一个专业的命名实体识别助手。'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt
|
||||
}
|
||||
],
|
||||
max_tokens: 500,
|
||||
temperature: 0.1
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.success || !response.data?.choices?.[0]) {
|
||||
return []
|
||||
}
|
||||
|
||||
const entities = JSON.parse(response.data.choices[0].message.content)
|
||||
return entities.map((entity: any, index: number) => ({
|
||||
text: entity.text || '',
|
||||
type: entity.type || 'other',
|
||||
confidence: entity.confidence || 0.8,
|
||||
startPosition: 0, // 简化处理
|
||||
endPosition: entity.text?.length || 0
|
||||
})) as EntityResult[]
|
||||
|
||||
} catch (error) {
|
||||
console.error('实体识别失败:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题提取
|
||||
*/
|
||||
private async extractTopics(content: string): Promise<TopicResult[]> {
|
||||
try {
|
||||
const openaiConfig = this.config.openai
|
||||
if (!openaiConfig) {
|
||||
return []
|
||||
}
|
||||
|
||||
const prompt = `请分析以下文本的主要主题,提取3-5个核心主题。
|
||||
|
||||
文本:${content.substring(0, 2000)}
|
||||
|
||||
请以JSON数组格式返回,每个主题包含:name(主题名称)、confidence(置信度0-1)、keywords(相关关键词数组)。`
|
||||
|
||||
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: openaiConfig.model,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: '你是一个专业的文本主题分析助手。'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt
|
||||
}
|
||||
],
|
||||
max_tokens: 400,
|
||||
temperature: 0.2
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.success || !response.data?.choices?.[0]) {
|
||||
return []
|
||||
}
|
||||
|
||||
const topics = JSON.parse(response.data.choices[0].message.content)
|
||||
return topics.map((topic: any) => ({
|
||||
name: topic.name || '',
|
||||
confidence: topic.confidence || 0.8,
|
||||
keywords: topic.keywords || []
|
||||
})) as TopicResult[]
|
||||
|
||||
} catch (error) {
|
||||
console.error('主题提取失败:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 可读性分析
|
||||
*/
|
||||
private async analyzeReadability(content: string): Promise<number> {
|
||||
try {
|
||||
// 简化的可读性计算
|
||||
const sentences = content.split(/[.!?。!?]/).length
|
||||
const words = content.split(/\s+/).length
|
||||
const avgWordsPerSentence = words / sentences
|
||||
|
||||
// 基于平均句长计算可读性分数
|
||||
let score = 1.0
|
||||
if (avgWordsPerSentence > 30) score = 0.3
|
||||
else if (avgWordsPerSentence > 20) score = 0.5
|
||||
else if (avgWordsPerSentence > 15) score = 0.7
|
||||
else if (avgWordsPerSentence > 10) score = 0.9
|
||||
|
||||
return score
|
||||
|
||||
} catch (error) {
|
||||
console.error('可读性分析失败:', error)
|
||||
return 0.5
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 可信度分析
|
||||
*/
|
||||
private async analyzeCredibility(content: ContentInfo): Promise<number> {
|
||||
try {
|
||||
let score = 0.5 // 基础分数
|
||||
|
||||
// 来源可信度
|
||||
if (content.sourceUrl) {
|
||||
const domain = this.extractDomain(content.sourceUrl)
|
||||
const credibleDomains = ['reuters.com', 'bbc.com', 'xinhuanet.com', 'nhk.or.jp']
|
||||
if (credibleDomains.some(d => domain.includes(d))) {
|
||||
score += 0.2
|
||||
}
|
||||
}
|
||||
|
||||
// 作者信息
|
||||
if (content.author && content.author.length > 0) {
|
||||
score += 0.1
|
||||
}
|
||||
|
||||
// 内容长度和结构
|
||||
if (content.content.length > 500) {
|
||||
score += 0.1
|
||||
}
|
||||
|
||||
// 时效性
|
||||
const daysSincePublished = (Date.now() - content.publishedAt) / (1000 * 60 * 60 * 24)
|
||||
if (daysSincePublished < 1) {
|
||||
score += 0.1
|
||||
}
|
||||
|
||||
return Math.min(1.0, score)
|
||||
|
||||
} catch (error) {
|
||||
console.error('可信度分析失败:', error)
|
||||
return 0.5
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成摘要
|
||||
*/
|
||||
private async generateSummary(content: string): Promise<string> {
|
||||
try {
|
||||
const openaiConfig = this.config.openai
|
||||
if (!openaiConfig) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (content.length < 200) {
|
||||
return content
|
||||
}
|
||||
|
||||
const prompt = `请为以下文本生成一个简洁的摘要(100字以内):\n\n${content.substring(0, 2000)}`
|
||||
|
||||
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: openaiConfig.model,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: '你是一个专业的文本摘要助手。'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt
|
||||
}
|
||||
],
|
||||
max_tokens: 200,
|
||||
temperature: 0.3
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.success || !response.data?.choices?.[0]) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return response.data.choices[0].message.content.trim()
|
||||
|
||||
} catch (error) {
|
||||
console.error('摘要生成失败:', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取关键词
|
||||
*/
|
||||
private async extractKeywords(content: string): Promise<string[]> {
|
||||
try {
|
||||
const openaiConfig = this.config.openai
|
||||
if (!openaiConfig) {
|
||||
return []
|
||||
}
|
||||
|
||||
const prompt = `请从以下文本中提取5-10个关键词:\n\n${content.substring(0, 1500)}\n\n请以JSON数组格式返回关键词。`
|
||||
|
||||
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: openaiConfig.model,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: '你是一个专业的关键词提取助手。'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt
|
||||
}
|
||||
],
|
||||
max_tokens: 200,
|
||||
temperature: 0.1
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.success || !response.data?.choices?.[0]) {
|
||||
return []
|
||||
}
|
||||
|
||||
const keywords = JSON.parse(response.data.choices[0].message.content)
|
||||
return Array.isArray(keywords) ? keywords : []
|
||||
|
||||
} catch (error) {
|
||||
console.error('关键词提取失败:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 内容分类
|
||||
*/
|
||||
private async classifyContent(content: ContentInfo): Promise<CategoryResult[]> {
|
||||
try {
|
||||
// 预定义分类
|
||||
const categories = [
|
||||
{ id: 'politics', name: '政治', keywords: ['政治', '政府', '选举', '政策', 'politics', 'government'] },
|
||||
{ id: 'technology', name: '科技', keywords: ['科技', '技术', '人工智能', 'AI', 'technology', 'tech'] },
|
||||
{ id: 'business', name: '商业', keywords: ['商业', '经济', '金融', '市场', 'business', 'economy'] },
|
||||
{ id: 'sports', name: '体育', keywords: ['体育', '运动', '比赛', '足球', 'sports', 'game'] },
|
||||
{ id: 'entertainment', name: '娱乐', keywords: ['娱乐', '电影', '音乐', '明星', 'entertainment', 'movie'] },
|
||||
{ id: 'health', name: '健康', keywords: ['健康', '医疗', '疾病', '医院', 'health', 'medical'] }
|
||||
]
|
||||
|
||||
const text = `${content.title} ${content.content}`.toLowerCase()
|
||||
const results: CategoryResult[] = []
|
||||
|
||||
for (const category of categories) {
|
||||
let score = 0
|
||||
for (const keyword of category.keywords) {
|
||||
const matches = (text.match(new RegExp(keyword.toLowerCase(), 'g')) || []).length
|
||||
score += matches
|
||||
}
|
||||
|
||||
if (score > 0) {
|
||||
results.push({
|
||||
categoryId: category.id,
|
||||
categoryName: category.name,
|
||||
confidence: Math.min(1.0, score / 10),
|
||||
level: 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return results.sort((a, b) => b.confidence - a.confidence).slice(0, 3)
|
||||
|
||||
} catch (error) {
|
||||
console.error('内容分类失败:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取关键短语
|
||||
*/
|
||||
private extractKeyPhrases(content: string): string[] {
|
||||
try {
|
||||
// 简单的关键短语提取
|
||||
const sentences = content.split(/[.!?。!?]/)
|
||||
const phrases: string[] = []
|
||||
|
||||
for (const sentence of sentences) {
|
||||
const words = sentence.trim().split(/\s+/)
|
||||
if (words.length >= 2 && words.length <= 5) {
|
||||
phrases.push(sentence.trim())
|
||||
}
|
||||
}
|
||||
|
||||
return phrases.slice(0, 10)
|
||||
|
||||
} catch (error) {
|
||||
console.error('关键短语提取失败:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取域名
|
||||
*/
|
||||
private extractDomain(url: string): string {
|
||||
try {
|
||||
const matches = url.match(/https?:\/\/([^\/]+)/)
|
||||
return matches ? matches[1] : ''
|
||||
} catch (error) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成缓存键
|
||||
*/
|
||||
private generateContentCacheKey(contentId: string, options?: any): string {
|
||||
const optionsStr = JSON.stringify(options || {})
|
||||
return `content-${contentId}-${this.simpleHash(optionsStr)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单哈希函数
|
||||
*/
|
||||
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
|
||||
}
|
||||
return Math.abs(hash).toString(36)
|
||||
}
|
||||
}
|
||||
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