fix: handle json string chunk (#8896)

* fix(ai客户端): 处理非JSON格式的响应数据块

添加对非JSON格式响应数据块的解析处理,当解析失败时抛出包含本地化错误信息的异常

* feat(i18n): 添加聊天响应无效数据格式的错误提示和多语言支持

为聊天响应添加无效数据格式的错误提示,并更新多个语言文件以支持该功能
This commit is contained in:
Phantom 2025-08-07 00:12:52 +08:00 committed by GitHub
parent ddbf710727
commit 3c5fa06d57
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 172 additions and 36 deletions

View File

@ -70,6 +70,7 @@ import {
mcpToolsToAnthropicTools
} from '@renderer/utils/mcp-tools'
import { findFileBlocks, findImageBlocks } from '@renderer/utils/messageUtils/find'
import { t } from 'i18next'
import { BaseApiClient } from '../BaseApiClient'
import { AnthropicStreamListener, RawStreamListener, RequestTransformer, ResponseChunkTransformer } from '../types'
@ -520,6 +521,14 @@ export class AnthropicAPIClient extends BaseApiClient<
const toolCalls: Record<number, ToolUseBlock> = {}
return {
async transform(rawChunk: AnthropicSdkRawChunk, controller: TransformStreamDefaultController<GenericChunk>) {
if (typeof rawChunk === 'string') {
try {
rawChunk = JSON.parse(rawChunk)
} catch (error) {
logger.error('invalid chunk', { rawChunk, error })
throw new Error(t('error.chat.chunk.non_json'))
}
}
switch (rawChunk.type) {
case 'message': {
let i = 0

View File

@ -42,6 +42,7 @@ import {
mcpToolsToAwsBedrockTools
} from '@renderer/utils/mcp-tools'
import { findImageBlocks } from '@renderer/utils/messageUtils/find'
import { t } from 'i18next'
import { BaseApiClient } from '../BaseApiClient'
import { RequestTransformer, ResponseChunkTransformer } from '../types'
@ -436,6 +437,15 @@ export class AwsBedrockAPIClient extends BaseApiClient<
async transform(rawChunk: AwsBedrockSdkRawChunk, controller: TransformStreamDefaultController<GenericChunk>) {
logger.silly('Processing AWS Bedrock chunk:', rawChunk)
if (typeof rawChunk === 'string') {
try {
rawChunk = JSON.parse(rawChunk)
} catch (error) {
logger.error('invalid chunk', { rawChunk, error })
throw new Error(t('error.chat.chunk.non_json'))
}
}
// 处理消息开始事件
if (rawChunk.messageStart) {
controller.enqueue({

View File

@ -60,6 +60,7 @@ import {
} from '@renderer/utils/mcp-tools'
import { findFileBlocks, findImageBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
import { defaultTimeout, MB } from '@shared/config/constant'
import { t } from 'i18next'
import { BaseApiClient } from '../BaseApiClient'
import { RequestTransformer, ResponseChunkTransformer } from '../types'
@ -557,6 +558,14 @@ export class GeminiAPIClient extends BaseApiClient<
return () => ({
async transform(chunk: GeminiSdkRawChunk, controller: TransformStreamDefaultController<GenericChunk>) {
logger.silly('chunk', chunk)
if (typeof chunk === 'string') {
try {
chunk = JSON.parse(chunk)
} catch (error) {
logger.error('invalid chunk', { chunk, error })
throw new Error(t('error.chat.chunk.non_json'))
}
}
if (chunk.candidates && chunk.candidates.length > 0) {
for (const candidate of chunk.candidates) {
if (candidate.content) {

View File

@ -63,6 +63,7 @@ import {
openAIToolsToMcpTool
} from '@renderer/utils/mcp-tools'
import { findFileBlocks, findImageBlocks } from '@renderer/utils/messageUtils/find'
import { t } from 'i18next'
import OpenAI, { AzureOpenAI } from 'openai'
import { ChatCompletionContentPart, ChatCompletionContentPartRefusal, ChatCompletionTool } from 'openai/resources'
@ -758,6 +759,15 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
return
}
if (typeof chunk === 'string') {
try {
chunk = JSON.parse(chunk)
} catch (error) {
logger.error('invalid chunk', { chunk, error })
throw new Error(t('error.chat.chunk.non_json'))
}
}
// 处理chunk
if ('choices' in chunk && chunk.choices && chunk.choices.length > 0) {
for (const choice of chunk.choices) {

View File

@ -1,3 +1,4 @@
import { loggerService } from '@logger'
import { GenericChunk } from '@renderer/aiCore/middleware/schemas'
import { CompletionsContext } from '@renderer/aiCore/middleware/types'
import {
@ -38,6 +39,7 @@ import {
} from '@renderer/utils/mcp-tools'
import { findFileBlocks, findImageBlocks } from '@renderer/utils/messageUtils/find'
import { MB } from '@shared/config/constant'
import { t } from 'i18next'
import { isEmpty } from 'lodash'
import OpenAI, { AzureOpenAI } from 'openai'
import { ResponseInput } from 'openai/resources/responses/responses'
@ -46,6 +48,7 @@ import { RequestTransformer, ResponseChunkTransformer } from '../types'
import { OpenAIAPIClient } from './OpenAIApiClient'
import { OpenAIBaseClient } from './OpenAIBaseClient'
const logger = loggerService.withContext('OpenAIResponseAPIClient')
export class OpenAIResponseAPIClient extends OpenAIBaseClient<
OpenAI,
OpenAIResponseSdkParams,
@ -477,6 +480,14 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient<
let isFirstTextChunk = true
return () => ({
async transform(chunk: OpenAIResponseSdkRawChunk, controller: TransformStreamDefaultController<GenericChunk>) {
if (typeof chunk === 'string') {
try {
chunk = JSON.parse(chunk)
} catch (error) {
logger.error('invalid chunk', { chunk, error })
throw new Error(t('error.chat.chunk.non_json'))
}
}
// 处理chunk
if ('output' in chunk) {
if (ctx._internal?.toolProcessingState) {

View File

@ -748,6 +748,9 @@
"file_format": "Backup file format error"
},
"chat": {
"chunk": {
"non_json": "Returned an invalid data format"
},
"response": "Something went wrong. Please check if you have set your API key in the Settings > Providers"
},
"http": {

View File

@ -748,6 +748,9 @@
"file_format": "バックアップファイルの形式エラー"
},
"chat": {
"chunk": {
"non_json": "無効なデータ形式が返されました"
},
"response": "エラーが発生しました。APIキーが設定されていない場合は、設定 > プロバイダーでキーを設定してください"
},
"http": {

View File

@ -748,6 +748,9 @@
"file_format": "Ошибка формата файла резервной копии"
},
"chat": {
"chunk": {
"non_json": "Вернулся недопустимый формат данных"
},
"response": "Что-то пошло не так. Пожалуйста, проверьте, установлен ли ваш ключ API в Настройки > Провайдеры"
},
"http": {

View File

@ -748,6 +748,9 @@
"file_format": "备份文件格式错误"
},
"chat": {
"chunk": {
"non_json": "返回了无效的数据格式"
},
"response": "出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥"
},
"http": {

View File

@ -748,6 +748,9 @@
"file_format": "備份檔案格式錯誤"
},
"chat": {
"chunk": {
"non_json": "返回了無效的資料格式"
},
"response": "出現錯誤。如果尚未設定 API 金鑰,請前往設定 > 模型提供者中設定金鑰"
},
"http": {

View File

@ -505,6 +505,7 @@
"tip": "Στη γραμμή εργαλείων των εκτελέσιμων blocks κώδικα θα εμφανίζεται το κουμπί εκτέλεσης· προσέξτε να μην εκτελέσετε επικίνδυνο κώδικα!",
"title": "Εκτέλεση Κώδικα"
},
"code_image_tools": "Ενεργοποίηση εργαλείου προεπισκόπησης",
"code_wrappable": "Οι κώδικες μπορούν να γράφονται σε διαφορετική γραμμή",
"context_count": {
"label": "Πλήθος ενδιάμεσων",
@ -648,15 +649,6 @@
},
"expand": "επιλογή",
"more": "Περισσότερα",
"preview": {
"copy": {
"image": "Αντιγραφή ως εικόνα"
},
"label": "Προεπισκόπηση",
"source": "Προβολή πηγαίου κώδικα",
"zoom_in": "Μεγέθυνση",
"zoom_out": "Σμίκρυνση"
},
"run": "Εκτέλεση κώδικα",
"split": {
"label": "Διαχωρισμός προβολής",
@ -756,6 +748,9 @@
"file_format": "Λάθος μορφή αρχείου που επιστρέφεται"
},
"chat": {
"chunk": {
"non_json": "Επέστρεψε μη έγκυρη μορφή δεδομένων"
},
"response": "Σφάλμα. Εάν δεν έχετε ρυθμίσει το κλειδί API, πηγαίνετε στο ρυθμισμένα > παρέχοντας το πρόσωπο του μοντέλου"
},
"http": {
@ -1159,6 +1154,9 @@
"failed": "Η διαγραφή απέτυχε",
"success": "Η διαγραφή ήταν επιτυχής"
},
"dialog": {
"failed": "Η προεπισκόπηση απέτυχε"
},
"download": {
"failed": "Αποτυχία λήψης",
"success": "Λήψη ολοκληρώθηκε"
@ -1561,6 +1559,7 @@
"mode": {
"edit": "Επεξεργασία",
"generate": "Δημιουργία",
"merge": "συγχώνευση",
"remix": "Ανάμειξη",
"upscale": "Μεγέθυνση"
},
@ -1653,6 +1652,22 @@
"seed_tip": "Ελέγχει την τυχαιότητα του αποτελέσματος μεγέθυνσης"
}
},
"preview": {
"copy": {
"image": "Αντιγραφή ως εικόνα"
},
"dialog": "Άνοιγμα παραθύρου προεπισκόπησης",
"label": "Προεπισκόπηση",
"pan": "Μετακίνηση",
"pan_down": "Μετακίνηση προς τα κάτω",
"pan_left": "Μετακίνηση προς τα αριστερά",
"pan_right": "μετακίνηση προς τα δεξιά",
"pan_up": "Μετακίνηση προς τα πάνω",
"reset": "Επαναφορά",
"source": "Προβολή πηγαίου κώδικα",
"zoom_in": "Μεγέθυνση",
"zoom_out": "συρρίκνωση"
},
"prompts": {
"explanation": "Με βοηθήστε να εξηγήσετε αυτό το όρισμα",
"summarize": "Με βοηθήστε να συνοψίσετε αυτό το κείμενο",
@ -2715,6 +2730,8 @@
"jsonSaveError": "Αποτυχία αποθήκευσης της διαμορφωτικής ρύθμισης JSON",
"jsonSaveSuccess": "Η διαμορφωτική ρύθμιση JSON αποθηκεύτηκε επιτυχώς",
"logoUrl": "URL Λογότυπου",
"longRunning": "Μακροχρόνια λειτουργία",
"longRunningTooltip": "Όταν ενεργοποιηθεί, ο διακομιστής υποστηρίζει μακροχρόνιες εργασίες, επαναφέρει το χρονικό όριο μετά από λήψη ειδοποίησης προόδου και επεκτείνει το μέγιστο χρονικό όριο σε 10 λεπτά.",
"missingDependencies": "Απο缺失, παρακαλώ εγκαταστήστε το για να συνεχίσετε",
"more": {
"awesome": "Επιλεγμένος κατάλογος διακομιστών MCP",
@ -2987,6 +3004,7 @@
"select_api_key": "Επιλέξτε το API key που θέλετε να χρησιμοποιήσετε:",
"single": "Μόνο",
"start": "Έναρξη",
"timeout": "Χρονική καθυστέρηση",
"title": "Ελεγχος υγείας μοντέλου",
"use_all_keys": "Χρήση όλων των κλειδιών"
},

View File

@ -505,6 +505,7 @@
"tip": "En la barra de herramientas de bloques de código ejecutables se mostrará un botón de ejecución. ¡Tenga cuidado en no ejecutar código peligroso!",
"title": "Ejecución de Código"
},
"code_image_tools": "Activar herramientas de vista previa",
"code_wrappable": "Bloques de código reemplazables",
"context_count": {
"label": "Número de contextos",
@ -648,15 +649,6 @@
},
"expand": "Expandir",
"more": "Más",
"preview": {
"copy": {
"image": "Copiar como imagen"
},
"label": "Vista previa",
"source": "Ver código fuente",
"zoom_in": "Acercar",
"zoom_out": "Alejar"
},
"run": "Ejecutar código",
"split": {
"label": "Dividir vista",
@ -756,6 +748,9 @@
"file_format": "Formato de archivo de copia de seguridad incorrecto"
},
"chat": {
"chunk": {
"non_json": "Devuelve un formato de datos no válido"
},
"response": "Ha ocurrido un error, si no ha configurado la clave API, vaya a Configuración > Proveedor de modelos para configurar la clave"
},
"http": {
@ -1159,6 +1154,9 @@
"failed": "Eliminación fallida",
"success": "Eliminación exitosa"
},
"dialog": {
"failed": "Error de vista previa"
},
"download": {
"failed": "Descarga fallida",
"success": "Descarga exitosa"
@ -1561,6 +1559,7 @@
"mode": {
"edit": "Editar",
"generate": "Generar imagen",
"merge": "combinar",
"remix": "Mezclar",
"upscale": "Ampliar"
},
@ -1653,6 +1652,22 @@
"seed_tip": "Controla la aleatoriedad del resultado de la ampliación"
}
},
"preview": {
"copy": {
"image": "Copiar como imagen"
},
"dialog": "Abrir la ventana de vista previa",
"label": "Vista previa",
"pan": "moverse",
"pan_down": "Mover hacia abajo",
"pan_left": "Desplazarse hacia la izquierda",
"pan_right": "Desplazarse hacia la derecha",
"pan_up": "Mover hacia arriba",
"reset": "Restablecer",
"source": "Ver código fuente",
"zoom_in": "ampliar",
"zoom_out": "reducir"
},
"prompts": {
"explanation": "Ayúdame a explicar este concepto",
"summarize": "Ayúdame a resumir este párrafo",
@ -2715,6 +2730,8 @@
"jsonSaveError": "Fallo al guardar la configuración JSON",
"jsonSaveSuccess": "Configuración JSON guardada exitosamente",
"logoUrl": "URL del logotipo",
"longRunning": "Modo de ejecución prolongada",
"longRunningTooltip": "Una vez habilitado, el servidor admite tareas de larga duración, reinicia el temporizador de tiempo de espera al recibir notificaciones de progreso y amplía el tiempo máximo de espera hasta 10 minutos.",
"missingDependencies": "Faltan, instalelas para continuar",
"more": {
"awesome": "Lista seleccionada de servidores MCP",
@ -2987,6 +3004,7 @@
"select_api_key": "Seleccionar clave API a usar:",
"single": "Individual",
"start": "Iniciar",
"timeout": "Tiempo de espera agotado",
"title": "Verificación de salud del modelo",
"use_all_keys": "Usar todas las claves"
},

View File

@ -505,6 +505,7 @@
"tip": "Une bouton d'exécution s'affichera dans la barre d'outils des blocs de code exécutables. Attention à ne pas exécuter de code dangereux !",
"title": "Exécution de code"
},
"code_image_tools": "Activer l'outil d'aperçu",
"code_wrappable": "Blocs de code avec retours à la ligne",
"context_count": {
"label": "Nombre de contextes",
@ -648,15 +649,6 @@
},
"expand": "Développer",
"more": "Plus",
"preview": {
"copy": {
"image": "Copier comme image"
},
"label": "Aperçu",
"source": "Voir le code source",
"zoom_in": "Agrandir",
"zoom_out": "Réduire"
},
"run": "Exécuter le code",
"split": {
"label": "Fractionner la vue",
@ -756,6 +748,9 @@
"file_format": "Le format du fichier de sauvegarde est incorrect"
},
"chat": {
"chunk": {
"non_json": "a renvoyé un format de données invalide"
},
"response": "Une erreur s'est produite, si l'API n'est pas configurée, veuillez aller dans Paramètres > Fournisseurs de modèles pour configurer la clé"
},
"http": {
@ -1159,6 +1154,9 @@
"failed": "Échec de la suppression",
"success": "Suppression réussie"
},
"dialog": {
"failed": "Échec de l'aperçu"
},
"download": {
"failed": "Échec du téléchargement",
"success": "Téléchargement réussi"
@ -1561,6 +1559,7 @@
"mode": {
"edit": "Редактировать",
"generate": "Создать изображение",
"merge": "fusionner",
"remix": "Смешать",
"upscale": "Увеличить"
},
@ -1653,6 +1652,22 @@
"seed_tip": "Contrôle la randomisation du résultat d'agrandissement"
}
},
"preview": {
"copy": {
"image": "Copier en tant qu'image"
},
"dialog": "Ouvrir la fenêtre d'aperçu",
"label": "Aperçu",
"pan": "déplacer",
"pan_down": "Déplacer vers le bas",
"pan_left": "Déplacement vers la gauche",
"pan_right": "Décalage vers la droite",
"pan_up": "Déplacer vers le haut",
"reset": "Réinitialiser",
"source": "Voir le code source",
"zoom_in": "agrandir",
"zoom_out": "réduire"
},
"prompts": {
"explanation": "Aidez-moi à expliquer ce concept",
"summarize": "Aidez-moi à résumer ce passage",
@ -2715,6 +2730,8 @@
"jsonSaveError": "Échec de la sauvegarde de la configuration JSON",
"jsonSaveSuccess": "Configuration JSON sauvegardée",
"logoUrl": "Адрес логотипа",
"longRunning": "Mode d'exécution prolongée",
"longRunningTooltip": "Activé, le serveur prend en charge les tâches de longue durée, réinitialise le minuteur de délai d'attente lorsqu'il reçoit une notification de progression et prolonge le délai d'expiration maximal à 10 minutes.",
"missingDependencies": "Manquantes, veuillez les installer pour continuer",
"more": {
"awesome": "Liste sélectionnée de serveurs MCP",
@ -2987,6 +3004,7 @@
"select_api_key": "Sélectionner la clé API à utiliser :",
"single": "Unique",
"start": "Commencer",
"timeout": "Délai dépassé",
"title": "Test de santé des modèles",
"use_all_keys": "Utiliser toutes les clés"
},

View File

@ -505,6 +505,7 @@
"tip": "A barra de ferramentas de blocos de código executáveis exibirá um botão de execução; atenção para não executar códigos perigosos!",
"title": "Execução de Código"
},
"code_image_tools": "Ativar ferramenta de pré-visualização",
"code_wrappable": "Bloco de código com quebra de linha",
"context_count": {
"label": "Número de contexto",
@ -648,15 +649,6 @@
},
"expand": "Expandir",
"more": "Mais",
"preview": {
"copy": {
"image": "Copiar como imagem"
},
"label": "Pré-visualizar",
"source": "Ver código-fonte",
"zoom_in": "Ampliar",
"zoom_out": "Reduzir"
},
"run": "Executar código",
"split": {
"label": "Dividir visualização",
@ -756,6 +748,9 @@
"file_format": "Formato do arquivo de backup está incorreto"
},
"chat": {
"chunk": {
"non_json": "Devolveu um formato de dados inválido"
},
"response": "Ocorreu um erro, se a chave da API não foi configurada, por favor vá para Configurações > Provedores de Modelo para configurar a chave"
},
"http": {
@ -1159,6 +1154,9 @@
"failed": "Falha ao excluir",
"success": "Excluído com sucesso"
},
"dialog": {
"failed": "A pré-visualização falhou"
},
"download": {
"failed": "Falha no download",
"success": "Download bem-sucedido"
@ -1561,6 +1559,7 @@
"mode": {
"edit": "Editar",
"generate": "Gerar imagem",
"merge": "fundir",
"remix": "Misturar",
"upscale": "Aumentar"
},
@ -1653,6 +1652,22 @@
"seed_tip": "Controla a aleatoriedade do resultado de ampliação"
}
},
"preview": {
"copy": {
"image": "Copiar como imagem"
},
"dialog": "Abrir janela de pré-visualização",
"label": "Pré-visualização",
"pan": "mover",
"pan_down": "mover para baixo",
"pan_left": "Deslocar para a esquerda",
"pan_right": "Deslocar para a direita",
"pan_up": "Mover para cima",
"reset": "repor",
"source": "Ver código-fonte",
"zoom_in": "ampliar",
"zoom_out": "reduzir"
},
"prompts": {
"explanation": "Ajude-me a explicar este conceito",
"summarize": "Ajude-me a resumir este parágrafo",
@ -2715,6 +2730,8 @@
"jsonSaveError": "Falha ao salvar configuração JSON",
"jsonSaveSuccess": "Configuração JSON salva com sucesso",
"logoUrl": "URL do Logotipo",
"longRunning": "Modo de execução prolongada",
"longRunningTooltip": "Ativado, o servidor suporta tarefas de longa duração, reiniciando o temporizador de tempo limite ao receber notificações de progresso e prolongando o tempo máximo de tempo limite para 10 minutos.",
"missingDependencies": "Ausente, instale para continuar",
"more": {
"awesome": "Lista selecionada de servidores MCP",
@ -2987,6 +3004,7 @@
"select_api_key": "Selecione a chave API a ser usada:",
"single": "Individual",
"start": "Começar",
"timeout": "tempo expirado",
"title": "Verificação de saúde do modelo",
"use_all_keys": "Use chaves"
},