mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 10:40:07 +08:00
Fix/translate selection (#8943)
* refactor(translate): 重构翻译窗口使用翻译服务接口 * refactor(translate): 重构翻译功能,提取翻译服务为独立模块 将翻译相关逻辑从ApiService中提取到独立的TranslateService模块 简化组件中翻译功能的调用方式,移除重复代码 * fix(selection): 防止流式输出完成后的重复处理 添加finished标志位,在收到LLM_RESPONSE_COMPLETE时标记完成,避免后续chunk继续处理 * fix(TranslateService): 修复翻译失败时的错误处理和日志记录 改进翻译服务的错误处理逻辑,添加日志记录以便排查问题 * fix(翻译服务): 修正未配置模型时的错误提示信息
This commit is contained in:
parent
5647d6e6d4
commit
c97ece946a
@ -1,9 +1,7 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { fetchTranslate } from '@renderer/services/ApiService'
|
||||
import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import { getLanguageByLangcode } from '@renderer/utils/translate'
|
||||
import { Modal, ModalProps } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
@ -43,7 +41,6 @@ const PopupContainer: React.FC<Props> = ({
|
||||
const [textValue, setTextValue] = useState(text)
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
const textareaRef = useRef<TextAreaRef>(null)
|
||||
const { translateModel } = useDefaultModel()
|
||||
const { targetLanguage, showTranslateConfirm } = useSettings()
|
||||
const isMounted = useRef(true)
|
||||
|
||||
@ -103,21 +100,12 @@ const PopupContainer: React.FC<Props> = ({
|
||||
if (!confirmed) return
|
||||
}
|
||||
|
||||
if (!translateModel) {
|
||||
window.message.error({
|
||||
content: t('translate.error.not_configured'),
|
||||
key: 'translate-message'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (isMounted.current) {
|
||||
setIsTranslating(true)
|
||||
}
|
||||
|
||||
try {
|
||||
const assistant = getDefaultTranslateAssistant(getLanguageByLangcode(targetLanguage), textValue)
|
||||
const translatedText = await fetchTranslate({ content: textValue, assistant })
|
||||
const translatedText = await translateText(textValue, getLanguageByLangcode(targetLanguage))
|
||||
if (isMounted.current) {
|
||||
setTextValue(translatedText)
|
||||
}
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { fetchTranslate } from '@renderer/services/ApiService'
|
||||
import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import { getLanguageByLangcode } from '@renderer/utils/translate'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import { Languages } from 'lucide-react'
|
||||
@ -23,7 +21,6 @@ const logger = loggerService.withContext('TranslateButton')
|
||||
|
||||
const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style, isLoading }) => {
|
||||
const { t } = useTranslation()
|
||||
const { translateModel } = useDefaultModel()
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
const { targetLanguage, showTranslateConfirm } = useSettings()
|
||||
|
||||
@ -45,21 +42,12 @@ const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style, isLoa
|
||||
return
|
||||
}
|
||||
|
||||
if (!translateModel) {
|
||||
window.message.error({
|
||||
content: t('translate.error.not_configured'),
|
||||
key: 'translate-message'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 先复制原文到剪贴板
|
||||
await navigator.clipboard.writeText(text)
|
||||
|
||||
setIsTranslating(true)
|
||||
try {
|
||||
const assistant = getDefaultTranslateAssistant(getLanguageByLangcode(targetLanguage), text)
|
||||
const translatedText = await fetchTranslate({ content: text, assistant })
|
||||
const translatedText = await translateText(text, getLanguageByLangcode(targetLanguage))
|
||||
onTranslated(translatedText)
|
||||
} catch (error) {
|
||||
logger.error('Translation failed:', error as Error)
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import db from '@renderer/databases'
|
||||
import { fetchTranslate } from '@renderer/services/ApiService'
|
||||
import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setTranslating as _setTranslating } from '@renderer/store/runtime'
|
||||
import { setTranslatedContent as _setTranslatedContent } from '@renderer/store/translate'
|
||||
import { Language, LanguageCode, TranslateAssistant, TranslateHistory } from '@renderer/types'
|
||||
import { Language, LanguageCode, TranslateHistory } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { t } from 'i18next'
|
||||
import { throttle } from 'lodash'
|
||||
@ -55,24 +54,8 @@ export default function useTranslate() {
|
||||
|
||||
setTranslating(true)
|
||||
|
||||
let assistant: TranslateAssistant
|
||||
try {
|
||||
assistant = getDefaultTranslateAssistant(actualTargetLanguage, text)
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
window.message.error(e.message)
|
||||
return
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await fetchTranslate({
|
||||
content: text,
|
||||
assistant,
|
||||
onResponse: throttle(setTranslatedContent, 100)
|
||||
})
|
||||
await translateText(text, actualTargetLanguage, throttle(setTranslatedContent, 100))
|
||||
} catch (e) {
|
||||
logger.error('Failed to translate text', e as Error)
|
||||
window.message.error(t('translate.error.failed'))
|
||||
|
||||
@ -30,7 +30,6 @@ import {
|
||||
MemoryItem,
|
||||
Model,
|
||||
Provider,
|
||||
TranslateAssistant,
|
||||
WebSearchResponse,
|
||||
WebSearchSource
|
||||
} from '@renderer/types'
|
||||
@ -62,8 +61,7 @@ import {
|
||||
getDefaultAssistant,
|
||||
getDefaultModel,
|
||||
getProviderByModel,
|
||||
getTopNamingModel,
|
||||
getTranslateModel
|
||||
getTopNamingModel
|
||||
} from './AssistantService'
|
||||
import { processKnowledgeSearch } from './KnowledgeService'
|
||||
import { MemoryProcessor } from './MemoryProcessor'
|
||||
@ -606,56 +604,6 @@ async function processConversationMemory(messages: Message[], assistant: Assista
|
||||
}
|
||||
}
|
||||
|
||||
interface FetchTranslateProps {
|
||||
content: string
|
||||
assistant: TranslateAssistant
|
||||
onResponse?: (text: string, isComplete: boolean) => void
|
||||
}
|
||||
|
||||
export async function fetchTranslate({ content, assistant, onResponse }: FetchTranslateProps) {
|
||||
const model = getTranslateModel() || assistant.model || getDefaultModel()
|
||||
|
||||
if (!model) {
|
||||
throw new Error(i18n.t('error.provider_disabled'))
|
||||
}
|
||||
|
||||
const provider = getProviderByModel(model)
|
||||
|
||||
if (!hasApiKey(provider)) {
|
||||
throw new Error(i18n.t('error.no_api_key'))
|
||||
}
|
||||
|
||||
const isSupportedStreamOutput = () => {
|
||||
if (!onResponse) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const stream = isSupportedStreamOutput()
|
||||
const enableReasoning =
|
||||
((isSupportedThinkingTokenModel(model) || isSupportedReasoningEffortModel(model)) &&
|
||||
assistant.settings?.reasoning_effort !== undefined) ||
|
||||
(isReasoningModel(model) && (!isSupportedThinkingTokenModel(model) || !isSupportedReasoningEffortModel(model)))
|
||||
|
||||
const params: CompletionsParams = {
|
||||
callType: 'translate',
|
||||
messages: content,
|
||||
assistant: { ...assistant, model },
|
||||
streamOutput: stream,
|
||||
enableReasoning,
|
||||
onResponse
|
||||
}
|
||||
|
||||
const AI = new AiProvider(provider)
|
||||
|
||||
try {
|
||||
return (await AI.completions(params)).getText() || ''
|
||||
} catch (error: any) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchMessagesSummary({ messages, assistant }: { messages: Message[]; assistant: Assistant }) {
|
||||
let prompt = (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.title')
|
||||
const model = getTopNamingModel() || assistant.model || getDefaultModel()
|
||||
@ -789,7 +737,7 @@ export async function fetchGenerate({
|
||||
}
|
||||
}
|
||||
|
||||
function hasApiKey(provider: Provider) {
|
||||
export function hasApiKey(provider: Provider) {
|
||||
if (!provider) return false
|
||||
if (provider.id === 'ollama' || provider.id === 'lmstudio' || provider.type === 'vertexai') return true
|
||||
return !isEmpty(provider.apiKey)
|
||||
|
||||
@ -1,34 +1,93 @@
|
||||
import { loggerService } from '@logger'
|
||||
import AiProvider from '@renderer/aiCore'
|
||||
import { CompletionsParams } from '@renderer/aiCore/middleware/schemas'
|
||||
import {
|
||||
isReasoningModel,
|
||||
isSupportedReasoningEffortModel,
|
||||
isSupportedThinkingTokenModel
|
||||
} from '@renderer/config/models'
|
||||
import i18n from '@renderer/i18n'
|
||||
import store from '@renderer/store'
|
||||
import { Language } from '@renderer/types'
|
||||
import { Language, TranslateAssistant } from '@renderer/types'
|
||||
import { t } from 'i18next'
|
||||
|
||||
import { fetchTranslate } from './ApiService'
|
||||
import { getDefaultTranslateAssistant } from './AssistantService'
|
||||
import { hasApiKey } from './ApiService'
|
||||
import {
|
||||
getDefaultModel,
|
||||
getDefaultTranslateAssistant,
|
||||
getProviderByModel,
|
||||
getTranslateModel
|
||||
} from './AssistantService'
|
||||
|
||||
const logger = loggerService.withContext('TranslateService')
|
||||
interface FetchTranslateProps {
|
||||
content: string
|
||||
assistant: TranslateAssistant
|
||||
onResponse?: (text: string, isComplete: boolean) => void
|
||||
}
|
||||
|
||||
async function fetchTranslate({ content, assistant, onResponse }: FetchTranslateProps) {
|
||||
const model = getTranslateModel() || assistant.model || getDefaultModel()
|
||||
|
||||
if (!model) {
|
||||
throw new Error(i18n.t('translate.error.not_configured'))
|
||||
}
|
||||
|
||||
const provider = getProviderByModel(model)
|
||||
|
||||
if (!hasApiKey(provider)) {
|
||||
throw new Error(i18n.t('error.no_api_key'))
|
||||
}
|
||||
|
||||
const isSupportedStreamOutput = () => {
|
||||
if (!onResponse) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const stream = isSupportedStreamOutput()
|
||||
const enableReasoning =
|
||||
((isSupportedThinkingTokenModel(model) || isSupportedReasoningEffortModel(model)) &&
|
||||
assistant.settings?.reasoning_effort !== undefined) ||
|
||||
(isReasoningModel(model) && (!isSupportedThinkingTokenModel(model) || !isSupportedReasoningEffortModel(model)))
|
||||
|
||||
const params: CompletionsParams = {
|
||||
callType: 'translate',
|
||||
messages: content,
|
||||
assistant: { ...assistant, model },
|
||||
streamOutput: stream,
|
||||
enableReasoning,
|
||||
onResponse
|
||||
}
|
||||
|
||||
const AI = new AiProvider(provider)
|
||||
|
||||
return (await AI.completions(params)).getText().trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译文本到目标语言
|
||||
* @param text - 需要翻译的文本内容
|
||||
* @param targetLanguage - 目标语言
|
||||
* @param onResponse - 流式输出的回调函数,用于实时获取翻译结果
|
||||
* @returns 返回翻译后的文本
|
||||
* @throws {Error} 当翻译模型未配置或翻译失败时抛出错误
|
||||
*/
|
||||
export const translateText = async (
|
||||
text: string,
|
||||
targetLanguage: Language,
|
||||
onResponse?: (text: string, isComplete: boolean) => void
|
||||
) => {
|
||||
const translateModel = store.getState().llm.translateModel
|
||||
try {
|
||||
const assistant = getDefaultTranslateAssistant(targetLanguage, text)
|
||||
|
||||
if (!translateModel) {
|
||||
window.message.error({
|
||||
content: i18n.t('translate.error.not_configured'),
|
||||
key: 'translate-message'
|
||||
})
|
||||
return Promise.reject(new Error(i18n.t('translate.error.not_configured')))
|
||||
const translatedText = await fetchTranslate({ content: text, assistant, onResponse })
|
||||
|
||||
return translatedText
|
||||
} catch (e) {
|
||||
logger.error('Failed to translate', e as Error)
|
||||
const message = e instanceof Error ? e.message : String(e)
|
||||
window.message.error(t('translate.error.failed' + ': ' + message))
|
||||
return ''
|
||||
}
|
||||
|
||||
const assistant = getDefaultTranslateAssistant(targetLanguage, text)
|
||||
|
||||
const translatedText = await fetchTranslate({ content: text, assistant, onResponse })
|
||||
|
||||
const trimmedText = translatedText.trim()
|
||||
|
||||
if (!trimmedText) {
|
||||
return Promise.reject(new Error(i18n.t('translate.error.failed')))
|
||||
}
|
||||
|
||||
return trimmedText
|
||||
}
|
||||
|
||||
@ -4,9 +4,8 @@ import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { LanguagesEnum, translateLanguageOptions } from '@renderer/config/translate'
|
||||
import db from '@renderer/databases'
|
||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { fetchTranslate } from '@renderer/services/ApiService'
|
||||
import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
||||
import { Assistant, Language } from '@renderer/types'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import { Language } from '@renderer/types'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { getLanguageByLangcode } from '@renderer/utils/translate'
|
||||
import { Select } from 'antd'
|
||||
@ -41,20 +40,7 @@ const Translate: FC<Props> = ({ text }) => {
|
||||
try {
|
||||
translatingRef.current = true
|
||||
|
||||
const assistant: Assistant = getDefaultTranslateAssistant(targetLanguage, text)
|
||||
// const message: Message = {
|
||||
// id: uuid(),
|
||||
// role: 'user',
|
||||
// content: '',
|
||||
// assistantId: assistant.id,
|
||||
// topicId: uuid(),
|
||||
// model: translateModel,
|
||||
// createdAt: new Date().toISOString(),
|
||||
// type: 'text',
|
||||
// status: 'sending'
|
||||
// }
|
||||
|
||||
await fetchTranslate({ content: text, assistant, onResponse: setResult })
|
||||
await translateText(text, targetLanguage, setResult)
|
||||
|
||||
translatingRef.current = false
|
||||
} catch (error) {
|
||||
|
||||
@ -51,10 +51,15 @@ export const processMessages = async (
|
||||
})
|
||||
)
|
||||
|
||||
let finished = false
|
||||
|
||||
await fetchChatCompletion({
|
||||
messages: [userMessage],
|
||||
assistant: { ...assistant, settings: { streamOutput: true } },
|
||||
onChunkReceived: (chunk: Chunk) => {
|
||||
if (finished) {
|
||||
return
|
||||
}
|
||||
switch (chunk.type) {
|
||||
case ChunkType.THINKING_START:
|
||||
{
|
||||
@ -163,6 +168,9 @@ export const processMessages = async (
|
||||
)
|
||||
}
|
||||
break
|
||||
case ChunkType.LLM_RESPONSE_COMPLETE:
|
||||
finished = true
|
||||
break
|
||||
case ChunkType.ERROR:
|
||||
{
|
||||
const blockId = textBlockId || thinkingBlockId
|
||||
|
||||
Loading…
Reference in New Issue
Block a user