mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-05 12:29:44 +08:00
feat: more abort control (#9267)
* feat(abort): 添加 abortKey 参数支持以自定义中止控制 支持通过参数传入 abortKey 来替代 messageId,提供更灵活的中止控制方式 * fix(aiCore): 修复checkApi时未正确abort的问题 * feat(翻译): 添加翻译中止功能 支持在翻译过程中中止操作,包括添加中止键状态管理、中止错误处理和界面停止按钮 添加相关国际化文案 * feat(i18n): 添加翻译中止相关文本和多语言支持 * style(translate): 调整停止按钮图标大小以保持视觉一致性 * fix(TranslateService): 改进翻译错误处理逻辑 正确处理翻译中止和失败的情况,统一错误信息格式化方式 * fix(aiCore): 去除不必要的类型断言 * style(TabContainer): 移除多余的空格并保持代码整洁 * fix(translate): 添加翻译前校验并修复文件处理异步问题 在翻译前添加couldTranslate校验,防止无效操作 将processFile改为异步调用以正确处理文件处理流程
This commit is contained in:
parent
aaa0eb7140
commit
7407bb335d
@ -112,7 +112,7 @@ export default class AiProvider {
|
||||
builder.remove(ToolUseExtractionMiddlewareName)
|
||||
logger.silly('ToolUseExtractionMiddleware is removed')
|
||||
}
|
||||
if (params.callType !== 'chat') {
|
||||
if (params.callType !== 'chat' && params.callType !== 'check' && params.callType !== 'translate') {
|
||||
logger.silly('AbortHandlerMiddleware is removed')
|
||||
builder.remove(AbortHandlerMiddlewareName)
|
||||
}
|
||||
|
||||
@ -21,32 +21,38 @@ export const AbortHandlerMiddleware: CompletionsMiddleware =
|
||||
return result
|
||||
}
|
||||
|
||||
// 获取当前消息的ID用于abort管理
|
||||
// 优先使用处理过的消息,如果没有则使用原始消息
|
||||
let messageId: string | undefined
|
||||
|
||||
if (typeof params.messages === 'string') {
|
||||
messageId = `message-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
|
||||
} else {
|
||||
const processedMessages = params.messages
|
||||
const lastUserMessage = processedMessages.findLast((m) => m.role === 'user')
|
||||
messageId = lastUserMessage?.id
|
||||
}
|
||||
|
||||
if (!messageId) {
|
||||
logger.warn(`No messageId found, abort functionality will not be available.`)
|
||||
return next(ctx, params)
|
||||
}
|
||||
|
||||
const abortController = new AbortController()
|
||||
const abortFn = (): void => abortController.abort()
|
||||
|
||||
addAbortController(messageId, abortFn)
|
||||
|
||||
let abortSignal: AbortSignal | null = abortController.signal
|
||||
let abortKey: string
|
||||
|
||||
// 如果参数中传入了abortKey则优先使用
|
||||
if (params.abortKey) {
|
||||
abortKey = params.abortKey
|
||||
} else {
|
||||
// 获取当前消息的ID用于abort管理
|
||||
// 优先使用处理过的消息,如果没有则使用原始消息
|
||||
let messageId: string | undefined
|
||||
|
||||
if (typeof params.messages === 'string') {
|
||||
messageId = `message-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
|
||||
} else {
|
||||
const processedMessages = params.messages
|
||||
const lastUserMessage = processedMessages.findLast((m) => m.role === 'user')
|
||||
messageId = lastUserMessage?.id
|
||||
}
|
||||
|
||||
if (!messageId) {
|
||||
logger.warn(`No messageId found, abort functionality will not be available.`)
|
||||
return next(ctx, params)
|
||||
}
|
||||
|
||||
abortKey = messageId
|
||||
}
|
||||
|
||||
addAbortController(abortKey, abortFn)
|
||||
const cleanup = (): void => {
|
||||
removeAbortController(messageId as string, abortFn)
|
||||
removeAbortController(abortKey, abortFn)
|
||||
if (ctx._internal?.flowControl) {
|
||||
ctx._internal.flowControl.abortController = undefined
|
||||
ctx._internal.flowControl.abortSignal = undefined
|
||||
|
||||
@ -59,6 +59,9 @@ export interface CompletionsParams {
|
||||
contextCount?: number
|
||||
topicId?: string // 主题ID,用于关联上下文
|
||||
|
||||
// abort 控制
|
||||
abortKey?: string
|
||||
|
||||
_internal?: ProcessingState
|
||||
}
|
||||
|
||||
|
||||
@ -789,6 +789,7 @@
|
||||
"label": "Sort by Pinyin"
|
||||
}
|
||||
},
|
||||
"stop": "Stop",
|
||||
"success": "Success",
|
||||
"swap": "Swap",
|
||||
"topics": "Topics",
|
||||
@ -3805,6 +3806,9 @@
|
||||
},
|
||||
"title": "Translation History"
|
||||
},
|
||||
"info": {
|
||||
"aborted": "Translation aborted"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "Enter text to translate"
|
||||
},
|
||||
|
||||
@ -789,6 +789,7 @@
|
||||
"label": "ピンインでソート"
|
||||
}
|
||||
},
|
||||
"stop": "停止",
|
||||
"success": "成功",
|
||||
"swap": "交換",
|
||||
"topics": "トピック",
|
||||
@ -3805,6 +3806,9 @@
|
||||
},
|
||||
"title": "翻訳履歴"
|
||||
},
|
||||
"info": {
|
||||
"aborted": "翻訳中止"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "翻訳するテキストを入力"
|
||||
},
|
||||
|
||||
@ -789,6 +789,7 @@
|
||||
"label": "Сортировать по пиньинь"
|
||||
}
|
||||
},
|
||||
"stop": "остановить",
|
||||
"success": "Успешно",
|
||||
"swap": "Поменять местами",
|
||||
"topics": "Топики",
|
||||
@ -3805,6 +3806,9 @@
|
||||
},
|
||||
"title": "История переводов"
|
||||
},
|
||||
"info": {
|
||||
"aborted": "Перевод прерван"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "Введите текст для перевода"
|
||||
},
|
||||
|
||||
@ -789,6 +789,7 @@
|
||||
"label": "按拼音排序"
|
||||
}
|
||||
},
|
||||
"stop": "停止",
|
||||
"success": "成功",
|
||||
"swap": "交换",
|
||||
"topics": "话题",
|
||||
@ -3805,6 +3806,9 @@
|
||||
},
|
||||
"title": "翻译历史"
|
||||
},
|
||||
"info": {
|
||||
"aborted": "翻译中止"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "输入文本进行翻译"
|
||||
},
|
||||
|
||||
@ -789,6 +789,7 @@
|
||||
"label": "按拼音排序"
|
||||
}
|
||||
},
|
||||
"stop": "停止",
|
||||
"success": "成功",
|
||||
"swap": "交換",
|
||||
"topics": "話題",
|
||||
@ -3805,6 +3806,9 @@
|
||||
},
|
||||
"title": "翻譯歷史"
|
||||
},
|
||||
"info": {
|
||||
"aborted": "翻譯中止"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "輸入文字進行翻譯"
|
||||
},
|
||||
|
||||
@ -789,6 +789,7 @@
|
||||
"label": "Ταξινόμηση κατά Πινγίν"
|
||||
}
|
||||
},
|
||||
"stop": "σταματήστε",
|
||||
"success": "Επιτυχία",
|
||||
"swap": "Εναλλαγή",
|
||||
"topics": "Θέματα",
|
||||
@ -3804,6 +3805,9 @@
|
||||
},
|
||||
"title": "Ιστορικό μετάφρασης"
|
||||
},
|
||||
"info": {
|
||||
"aborted": "Η μετάφραση διακόπηκε"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "Εισαγάγετε κείμενο για μετάφραση"
|
||||
},
|
||||
|
||||
@ -789,6 +789,7 @@
|
||||
"label": "Ordenar por pinyin"
|
||||
}
|
||||
},
|
||||
"stop": "Detener",
|
||||
"success": "Éxito",
|
||||
"swap": "Intercambiar",
|
||||
"topics": "Temas",
|
||||
@ -3804,6 +3805,9 @@
|
||||
},
|
||||
"title": "Historial de traducciones"
|
||||
},
|
||||
"info": {
|
||||
"aborted": "Traducción cancelada"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "Ingrese el texto para traducir"
|
||||
},
|
||||
|
||||
@ -789,6 +789,7 @@
|
||||
"label": "Сортировать по пиньинь"
|
||||
}
|
||||
},
|
||||
"stop": "Arrêter",
|
||||
"success": "Succès",
|
||||
"swap": "Échanger",
|
||||
"topics": "Sujets",
|
||||
@ -3804,6 +3805,9 @@
|
||||
},
|
||||
"title": "Historique des traductions"
|
||||
},
|
||||
"info": {
|
||||
"aborted": "Traduction annulée"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "entrez le texte à traduire"
|
||||
},
|
||||
|
||||
@ -789,6 +789,7 @@
|
||||
"label": "Ordenar por Pinyin"
|
||||
}
|
||||
},
|
||||
"stop": "Parar",
|
||||
"success": "Sucesso",
|
||||
"swap": "Trocar",
|
||||
"topics": "Tópicos",
|
||||
@ -3804,6 +3805,9 @@
|
||||
},
|
||||
"title": "Histórico de Tradução"
|
||||
},
|
||||
"info": {
|
||||
"aborted": "Tradução interrompida"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "Digite o texto para traduzir"
|
||||
},
|
||||
|
||||
@ -17,7 +17,7 @@ import useTranslate from '@renderer/hooks/useTranslate'
|
||||
import { estimateTextTokens } from '@renderer/services/TokenService'
|
||||
import { saveTranslateHistory, translateText } from '@renderer/services/TranslateService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setTranslating as setTranslatingAction } from '@renderer/store/runtime'
|
||||
import { setTranslateAbortKey, setTranslating as setTranslatingAction } from '@renderer/store/runtime'
|
||||
import { setTranslatedContent as setTranslatedContentAction, setTranslateInput } from '@renderer/store/translate'
|
||||
import {
|
||||
type AutoDetectionMethod,
|
||||
@ -27,7 +27,9 @@ import {
|
||||
type TranslateHistory,
|
||||
type TranslateLanguage
|
||||
} from '@renderer/types'
|
||||
import { getFileExtension, runAsyncFunction } from '@renderer/utils'
|
||||
import { getFileExtension, runAsyncFunction, uuid } from '@renderer/utils'
|
||||
import { abortCompletion } from '@renderer/utils/abortController'
|
||||
import { isAbortError } from '@renderer/utils/error'
|
||||
import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import { getFilesFromDropEvent, getTextFromDropEvent } from '@renderer/utils/input'
|
||||
import {
|
||||
@ -40,7 +42,7 @@ import { imageExts, MB, textExts } from '@shared/config/constant'
|
||||
import { Button, Flex, FloatButton, Popover, Tooltip, Typography } from 'antd'
|
||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||
import { isEmpty, throttle } from 'lodash'
|
||||
import { Check, FolderClock, Settings2, UploadIcon } from 'lucide-react'
|
||||
import { Check, CirclePause, FolderClock, Settings2, UploadIcon } from 'lucide-react'
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -86,6 +88,7 @@ const TranslatePage: FC = () => {
|
||||
const text = useAppSelector((state) => state.translate.translateInput)
|
||||
const translatedContent = useAppSelector((state) => state.translate.translatedContent)
|
||||
const translating = useAppSelector((state) => state.runtime.translating)
|
||||
const abortKey = useAppSelector((state) => state.runtime.translateAbortKey)
|
||||
|
||||
// ref
|
||||
const contentContainerRef = useRef<HTMLDivElement>(null)
|
||||
@ -144,11 +147,16 @@ const TranslatePage: FC = () => {
|
||||
}
|
||||
|
||||
let translated: string
|
||||
const abortKey = uuid()
|
||||
dispatch(setTranslateAbortKey(abortKey))
|
||||
|
||||
try {
|
||||
translated = await translateText(text, actualTargetLanguage, throttle(setTranslatedContent, 100))
|
||||
translated = await translateText(text, actualTargetLanguage, throttle(setTranslatedContent, 100), abortKey)
|
||||
} catch (e) {
|
||||
logger.error('Failed to translate text', e as Error)
|
||||
window.message.error(t('translate.error.failed' + ': ' + (e as Error).message))
|
||||
if (!isAbortError(e)) {
|
||||
logger.error('Failed to translate text', e as Error)
|
||||
window.message.error(t('translate.error.failed' + ': ' + (e as Error).message))
|
||||
}
|
||||
setTranslating(false)
|
||||
return
|
||||
}
|
||||
@ -166,11 +174,12 @@ const TranslatePage: FC = () => {
|
||||
window.message.error(t('translate.error.unknown') + ': ' + (e as Error).message)
|
||||
}
|
||||
},
|
||||
[setTranslatedContent, setTranslating, t, translating]
|
||||
[dispatch, setTranslatedContent, setTranslating, t, translating]
|
||||
)
|
||||
|
||||
// 控制翻译按钮,翻译前进行校验
|
||||
const onTranslate = useCallback(async () => {
|
||||
if (!couldTranslate) return
|
||||
if (!text.trim()) return
|
||||
if (!translateModel) {
|
||||
window.message.error({
|
||||
@ -237,6 +246,15 @@ const TranslatePage: FC = () => {
|
||||
translateModel
|
||||
])
|
||||
|
||||
// 控制停止翻译
|
||||
const onAbort = async () => {
|
||||
if (!abortKey || !abortKey.trim()) {
|
||||
logger.error('Failed to abort. Invalid abortKey.')
|
||||
return
|
||||
}
|
||||
abortCompletion(abortKey)
|
||||
}
|
||||
|
||||
// 控制双向翻译切换
|
||||
const toggleBidirectional = (value: boolean) => {
|
||||
setIsBidirectional(value)
|
||||
@ -607,7 +625,7 @@ const TranslatePage: FC = () => {
|
||||
})
|
||||
return
|
||||
}
|
||||
processFile(selectedFile)
|
||||
await processFile(selectedFile)
|
||||
} catch (error) {
|
||||
logger.error('onPaste:', error as Error)
|
||||
window.message.error(t('chat.input.file_error'))
|
||||
@ -672,7 +690,12 @@ const TranslatePage: FC = () => {
|
||||
/>
|
||||
</Tooltip>
|
||||
{getLanguageDisplay()}
|
||||
<TranslateButton translating={translating} onTranslate={onTranslate} couldTranslate={couldTranslate} />
|
||||
<TranslateButton
|
||||
translating={translating}
|
||||
onTranslate={onTranslate}
|
||||
couldTranslate={couldTranslate}
|
||||
onAbort={onAbort}
|
||||
/>
|
||||
</InnerOperationBar>
|
||||
<InnerOperationBar style={{ justifyContent: 'flex-end' }}>
|
||||
<ModelSelectButton
|
||||
@ -903,11 +926,13 @@ const OutputText = styled.div`
|
||||
const TranslateButton = ({
|
||||
translating,
|
||||
onTranslate,
|
||||
couldTranslate
|
||||
couldTranslate,
|
||||
onAbort
|
||||
}: {
|
||||
translating: boolean
|
||||
onTranslate: () => void
|
||||
couldTranslate: boolean
|
||||
onAbort: () => void
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
@ -922,14 +947,16 @@ const TranslateButton = ({
|
||||
Shift + Enter: {t('translate.tooltip.newline')}
|
||||
</div>
|
||||
}>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={translating}
|
||||
onClick={onTranslate}
|
||||
disabled={!couldTranslate}
|
||||
icon={<SendOutlined />}>
|
||||
{t('translate.button.translate')}
|
||||
</Button>
|
||||
{!translating && (
|
||||
<Button type="primary" onClick={onTranslate} disabled={!couldTranslate} icon={<SendOutlined />}>
|
||||
{t('translate.button.translate')}
|
||||
</Button>
|
||||
)}
|
||||
{translating && (
|
||||
<Button danger type="primary" onClick={onAbort} icon={<CirclePause size={14} />}>
|
||||
{t('common.stop')}
|
||||
</Button>
|
||||
)}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@ -39,12 +39,7 @@ import { type Chunk, ChunkType } from '@renderer/types/chunk'
|
||||
import { Message } from '@renderer/types/newMessage'
|
||||
import { SdkModel } from '@renderer/types/sdk'
|
||||
import { removeSpecialCharactersForTopicName, uuid } from '@renderer/utils'
|
||||
import {
|
||||
abortCompletion,
|
||||
addAbortController,
|
||||
createAbortPromise,
|
||||
removeAbortController
|
||||
} from '@renderer/utils/abortController'
|
||||
import { abortCompletion } from '@renderer/utils/abortController'
|
||||
import { isAbortError } from '@renderer/utils/error'
|
||||
import { extractInfoFromXML, ExtractResults } from '@renderer/utils/extract'
|
||||
import { filterAdjacentUserMessaegs, filterLastAssistantMessage } from '@renderer/utils/messageUtils/filters'
|
||||
@ -874,10 +869,7 @@ export function checkApiProvider(provider: Provider): void {
|
||||
export async function checkApi(provider: Provider, model: Model, timeout = 15000): Promise<void> {
|
||||
checkApiProvider(provider)
|
||||
|
||||
const controller = new AbortController()
|
||||
const abortFn = () => controller.abort()
|
||||
const taskId = uuid()
|
||||
addAbortController(taskId, abortFn)
|
||||
|
||||
const ai = new AiProvider(provider)
|
||||
|
||||
@ -890,7 +882,6 @@ export async function checkApi(provider: Provider, model: Model, timeout = 15000
|
||||
const timerPromise = new Promise((_, reject) => setTimeout(() => reject('Timeout'), timeout))
|
||||
await Promise.race([ai.getEmbeddingDimensions(model), timerPromise])
|
||||
} else {
|
||||
// 通过该状态判断abort原因
|
||||
let streamError: Error | undefined = undefined
|
||||
|
||||
// 15s超时
|
||||
@ -905,31 +896,25 @@ export async function checkApi(provider: Provider, model: Model, timeout = 15000
|
||||
assistant,
|
||||
streamOutput: true,
|
||||
enableReasoning: false,
|
||||
onChunk: () => {
|
||||
// 接收到任意chunk都直接abort
|
||||
onChunk: (chunk: Chunk) => {
|
||||
if (chunk.type === ChunkType.ERROR && !isAbortError(chunk.error)) {
|
||||
streamError = new Error(JSON.stringify(chunk.error))
|
||||
}
|
||||
abortCompletion(taskId)
|
||||
},
|
||||
onError: (e) => {
|
||||
// 捕获stream error
|
||||
streamError = e
|
||||
abortCompletion(taskId)
|
||||
}
|
||||
shouldThrow: true,
|
||||
abortKey: taskId
|
||||
}
|
||||
|
||||
// Try streaming check first
|
||||
try {
|
||||
await createAbortPromise(controller.signal, ai.completions(params))
|
||||
} catch (e: any) {
|
||||
if (isAbortError(e)) {
|
||||
if (streamError) {
|
||||
throw streamError
|
||||
}
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
await ai.completions(params)
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
if (streamError) {
|
||||
throw streamError
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
// FIXME: 这种判断方法无法严格保证错误是流式引起的
|
||||
@ -947,8 +932,6 @@ export async function checkApi(provider: Provider, model: Model, timeout = 15000
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
} finally {
|
||||
removeAbortController(taskId, abortFn)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,9 @@ import {
|
||||
import { db } from '@renderer/databases'
|
||||
import { CustomTranslateLanguage, TranslateHistory, TranslateLanguage, TranslateLanguageCode } from '@renderer/types'
|
||||
import { TranslateAssistant } from '@renderer/types'
|
||||
import { ChunkType } from '@renderer/types/chunk'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { formatErrorMessage, isAbortError } from '@renderer/utils/error'
|
||||
import { t } from 'i18next'
|
||||
|
||||
import { hasApiKey } from './ApiService'
|
||||
@ -24,9 +26,10 @@ const logger = loggerService.withContext('TranslateService')
|
||||
interface FetchTranslateProps {
|
||||
assistant: TranslateAssistant
|
||||
onResponse?: (text: string, isComplete: boolean) => void
|
||||
abortKey?: string
|
||||
}
|
||||
|
||||
async function fetchTranslate({ assistant, onResponse }: FetchTranslateProps) {
|
||||
async function fetchTranslate({ assistant, onResponse, abortKey }: FetchTranslateProps) {
|
||||
const model = getTranslateModel() || assistant.model || getDefaultModel()
|
||||
|
||||
if (!model) {
|
||||
@ -51,6 +54,7 @@ async function fetchTranslate({ assistant, onResponse }: FetchTranslateProps) {
|
||||
((isSupportedThinkingTokenModel(model) || isSupportedReasoningEffortModel(model)) &&
|
||||
assistant.settings?.reasoning_effort !== undefined) ||
|
||||
(isReasoningModel(model) && (!isSupportedThinkingTokenModel(model) || !isSupportedReasoningEffortModel(model)))
|
||||
let abortError
|
||||
|
||||
const params: CompletionsParams = {
|
||||
callType: 'translate',
|
||||
@ -58,12 +62,22 @@ async function fetchTranslate({ assistant, onResponse }: FetchTranslateProps) {
|
||||
assistant: { ...assistant, model },
|
||||
streamOutput: stream,
|
||||
enableReasoning,
|
||||
onResponse
|
||||
onResponse,
|
||||
onChunk: (chunk) => {
|
||||
if (chunk.type === ChunkType.ERROR && isAbortError(chunk.error)) {
|
||||
abortError = chunk.error
|
||||
}
|
||||
},
|
||||
abortKey
|
||||
}
|
||||
|
||||
const AI = new AiProvider(provider)
|
||||
|
||||
return (await AI.completionsForTrace(params)).getText().trim()
|
||||
const result = (await AI.completionsForTrace(params)).getText().trim()
|
||||
if (abortError) {
|
||||
throw abortError
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@ -71,18 +85,20 @@ async function fetchTranslate({ assistant, onResponse }: FetchTranslateProps) {
|
||||
* @param text - 需要翻译的文本内容
|
||||
* @param targetLanguage - 目标语言
|
||||
* @param onResponse - 流式输出的回调函数,用于实时获取翻译结果
|
||||
* @param abortKey - 用于控制 abort 的键
|
||||
* @returns 返回翻译后的文本
|
||||
* @throws {Error} 当翻译模型未配置或翻译失败时抛出错误
|
||||
* @throws {Error} 翻译中止或失败时抛出异常
|
||||
*/
|
||||
export const translateText = async (
|
||||
text: string,
|
||||
targetLanguage: TranslateLanguage,
|
||||
onResponse?: (text: string, isComplete: boolean) => void
|
||||
onResponse?: (text: string, isComplete: boolean) => void,
|
||||
abortKey?: string
|
||||
) => {
|
||||
try {
|
||||
const assistant = getDefaultTranslateAssistant(targetLanguage, text)
|
||||
|
||||
const translatedText = await fetchTranslate({ assistant, onResponse })
|
||||
const translatedText = await fetchTranslate({ assistant, onResponse, abortKey })
|
||||
|
||||
const trimmedText = translatedText.trim()
|
||||
|
||||
@ -92,10 +108,13 @@ export const translateText = async (
|
||||
|
||||
return trimmedText
|
||||
} 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 ''
|
||||
if (isAbortError(e)) {
|
||||
window.message.info(t('translate.info.aborted'))
|
||||
} else {
|
||||
logger.error('Failed to translate', e as Error)
|
||||
window.message.error(t('translate.error.failed' + ': ' + formatErrorMessage(e)))
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -30,6 +30,7 @@ export interface RuntimeState {
|
||||
avatar: string
|
||||
generating: boolean
|
||||
translating: boolean
|
||||
translateAbortKey?: string
|
||||
/** whether the minapp popup is shown */
|
||||
minappShow: boolean
|
||||
/** the minapps that are opened and should be keep alive */
|
||||
@ -98,6 +99,9 @@ const runtimeSlice = createSlice({
|
||||
setTranslating: (state, action: PayloadAction<boolean>) => {
|
||||
state.translating = action.payload
|
||||
},
|
||||
setTranslateAbortKey: (state, action: PayloadAction<string>) => {
|
||||
state.translateAbortKey = action.payload
|
||||
},
|
||||
setMinappShow: (state, action: PayloadAction<boolean>) => {
|
||||
state.minappShow = action.payload
|
||||
},
|
||||
@ -162,6 +166,7 @@ export const {
|
||||
setAvatar,
|
||||
setGenerating,
|
||||
setTranslating,
|
||||
setTranslateAbortKey,
|
||||
setMinappShow,
|
||||
setOpenedKeepAliveMinapps,
|
||||
setOpenedOneOffMinapp,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user