From 0c52bf0641fb914c40d260d89519297b7b138ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cls=5F=E5=AE=81=E6=B3=A2=E6=9C=AC=E6=9C=BA?= <908705107@qq.com> Date: Sun, 21 Jun 2026 20:31:49 +0800 Subject: [PATCH] fix: clarify ai runtime errors --- .../ai_platform/services/llm_service.py | 96 +++++++++++++++++-- .../components/RunLogTimeline.vue | 9 +- .../workflow-runs/modules/detail-dialog.vue | 23 ++++- .../workflow/editor/components/ChatPanel.vue | 34 +++---- .../workflow/editor/components/RunPanel.vue | 36 +++---- .../src/views/ai-platform/workflow/index.vue | 17 +++- .../workflow/shared/useRunReplay.ts | 9 +- 7 files changed, 170 insertions(+), 54 deletions(-) diff --git a/backend-fastapi/ai_platform/services/llm_service.py b/backend-fastapi/ai_platform/services/llm_service.py index 12e034a..03fbfa7 100644 --- a/backend-fastapi/ai_platform/services/llm_service.py +++ b/backend-fastapi/ai_platform/services/llm_service.py @@ -112,7 +112,12 @@ class LLMService: raise ValueError(f'不支持的提供商类型: {provider.provider_type}') self._provider_cache[cache_key] = provider_instance - return self._provider_cache[cache_key], model.model_name + provider_instance = self._provider_cache[cache_key] + setattr(provider_instance, 'config_name', provider.name or provider.provider_type) + setattr(provider_instance, 'config_id', str(provider.id)) + setattr(provider_instance, 'config_api_base', provider.api_base or '') + + return provider_instance, model.model_name def _get_provider_sync(self, model_id: str, model_data: dict) -> tuple: """ @@ -145,8 +150,71 @@ class LLMService: raise ValueError(f'不支持的提供商类型: {provider_type}') self._provider_cache[cache_key] = provider_instance - return self._provider_cache[cache_key], model_name + provider_instance = self._provider_cache[cache_key] + setattr(provider_instance, 'config_name', model_data.get('provider_name') or provider_type) + setattr(provider_instance, 'config_id', str(provider_id)) + setattr(provider_instance, 'config_api_base', api_base or '') + + return provider_instance, model_name + @staticmethod + def _extract_upstream_message(error_text: str) -> str: + import re + + for pattern in ( + r"'message'\s*:\s*'([^']+)'", + r'"message"\s*:\s*"([^"]+)"', + ): + match = re.search(pattern, error_text) + if match: + return match.group(1) + return error_text[:240] + + def _format_provider_error( + self, + exc: Exception, + provider: Optional[BaseLLMProvider] = None, + model_name: str = '', + ) -> str: + error_text = str(exc) + lower_text = error_text.lower() + provider_name = ( + getattr(provider, 'config_name', '') + or getattr(provider, 'provider_name', '') + or '未知提供商' + ) + model_text = model_name or '未知模型' + + if ( + '401' in lower_text + or 'unauthorized' in lower_text + or 'invalid api key' in lower_text + or 'incorrect api key' in lower_text + or '无效的令牌' in error_text + or '鉴权' in error_text + ): + reason = '上游模型鉴权失败,API Key 无效或已过期' + elif ( + '404' in lower_text + or 'model_not_found' in lower_text + or 'model not found' in lower_text + or 'does not exist' in lower_text + ): + reason = '上游模型不存在或当前账号无权访问该模型' + elif 'timeout' in lower_text or 'timed out' in lower_text or '超时' in error_text: + reason = '上游模型请求超时' + elif 'rate limit' in lower_text or '429' in lower_text or 'quota' in lower_text: + reason = '上游模型限流或额度不足' + else: + reason = '上游模型调用失败' + + upstream_message = self._extract_upstream_message(error_text) + return ( + f'{reason}:提供商 {provider_name},模型 {model_text}。' + f'请在 AI 平台的模型提供商配置中检查 Base URL、API Key 和模型名称。' + f'上游返回:{upstream_message}' + ) + def chat_with_provider( self, provider: BaseLLMProvider, @@ -197,7 +265,10 @@ class LLMService: **kwargs ) - return provider.chat(llm_messages, config) + try: + return provider.chat(llm_messages, config) + except Exception as exc: + raise RuntimeError(self._format_provider_error(exc, provider, model_name)) from exc def _convert_messages(self, messages: List[Dict]) -> List[LLMMessage]: """转换消息格式,支持 tool 消息""" @@ -263,7 +334,10 @@ class LLMService: **kwargs ) - return await provider.chat_async(llm_messages, config) + try: + return await provider.chat_async(llm_messages, config) + except Exception as exc: + raise RuntimeError(self._format_provider_error(exc, provider, model_name)) from exc async def chat_stream( self, @@ -315,8 +389,11 @@ class LLMService: **kwargs ) - async for chunk in provider.chat_stream(llm_messages, config): - yield chunk + try: + async for chunk in provider.chat_stream(llm_messages, config): + yield chunk + except Exception as exc: + raise RuntimeError(self._format_provider_error(exc, provider, model_name)) from exc def chat_stream_sync( self, @@ -428,8 +505,11 @@ class LLMService: **kwargs ) - for chunk in provider.chat_stream_sync(llm_messages, config): - yield chunk + try: + for chunk in provider.chat_stream_sync(llm_messages, config): + yield chunk + except Exception as exc: + raise RuntimeError(self._format_provider_error(exc, provider, model_name)) from exc @staticmethod def get_available_providers() -> List[Dict]: diff --git a/web/apps/web-ele/src/views/ai-platform/workflow-runs/components/RunLogTimeline.vue b/web/apps/web-ele/src/views/ai-platform/workflow-runs/components/RunLogTimeline.vue index c5a2213..6b5ad92 100644 --- a/web/apps/web-ele/src/views/ai-platform/workflow-runs/components/RunLogTimeline.vue +++ b/web/apps/web-ele/src/views/ai-platform/workflow-runs/components/RunLogTimeline.vue @@ -21,6 +21,11 @@ function formatDuration(ms?: number) { if (ms < 1000) return `${ms}ms`; return `${(ms / 1000).toFixed(2)}s`; } + +function getLogError(log: ExecutionLogEntry) { + const item = log as any; + return item.error || item.error_message || item.message || ''; +}
-
- {{ log.error }} +
+ {{ getLogError(log) }}
`msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; +const getStreamErrorMessage = ( + event: Error | Partial | any, +) => + event?.error_message || + event?.message || + event?.error || + event?.content || + $t('ai-platform.workflow.editor.chatPanel.execFailed'); + // 发送消息 const handleSend = (text: string) => { if (!text || running.value) return; @@ -222,17 +231,13 @@ const runWorkflow = (userMessage: string, inputs: Record) => { }, (error) => { console.error('Stream error:', error); + const errorMessage = getStreamErrorMessage(error); updateAssistantMessage(assistantMsgId, { status: 'failed', - error_message: - error.message || - $t('ai-platform.workflow.editor.chatPanel.execFailed'), + error_message: errorMessage, }); running.value = false; - ElMessage.error( - error.message || - $t('ai-platform.workflow.editor.chatPanel.workflowExecFailed'), - ); + ElMessage.error(errorMessage); }, () => { cancelStream = null; @@ -355,11 +360,10 @@ const handleStreamEvent = (event: WorkflowStreamEvent, msgId: string) => { } case 'error': { + const errorMessage = getStreamErrorMessage(event); updateAssistantMessage(msgId, { status: 'failed', - error_message: - event.message || - $t('ai-platform.workflow.editor.chatPanel.execFailed'), + error_message: errorMessage, }); running.value = false; break; @@ -698,17 +702,13 @@ const resumeWorkflow = (userInput: any) => { }, (error) => { console.error('Resume stream error:', error); + const errorMessage = getStreamErrorMessage(error); updateAssistantMessage(assistantMsgId, { status: 'failed', - error_message: - error.message || - $t('ai-platform.workflow.editor.chatPanel.resumeFailed'), + error_message: errorMessage, }); running.value = false; - ElMessage.error( - error.message || - $t('ai-platform.workflow.editor.chatPanel.resumeWorkflowFailed'), - ); + ElMessage.error(errorMessage); }, () => { cancelStream = null; diff --git a/web/apps/web-ele/src/views/ai-platform/workflow/editor/components/RunPanel.vue b/web/apps/web-ele/src/views/ai-platform/workflow/editor/components/RunPanel.vue index ea84922..3493370 100644 --- a/web/apps/web-ele/src/views/ai-platform/workflow/editor/components/RunPanel.vue +++ b/web/apps/web-ele/src/views/ai-platform/workflow/editor/components/RunPanel.vue @@ -152,6 +152,15 @@ const formatTime = (ms: number | undefined) => { return `${(ms / 1000).toFixed(1)}s`; }; +const getStreamErrorMessage = ( + event: Error | Partial | any, +) => + event?.error_message || + event?.message || + event?.error || + event?.content || + $t('ai-platform.workflow.editor.runPanel.execFailed'); + const handleRun = () => { if (!props.workflowId) return; running.value = true; @@ -185,15 +194,14 @@ const handleRun = () => { case 'error': { stopTimer(); + const errorMessage = getStreamErrorMessage(event); // 将当前运行中的节点标记为失败(优先使用事件中的 node_id) const failedNodeId = event.node_id || currentNodeId.value; if (failedNodeId) { const state = nodeStates.value.get(failedNodeId); if (state && state.status === 'running') { state.status = 'failed'; - state.error_message = - event.message || - $t('ai-platform.workflow.editor.runPanel.execFailed'); + state.error_message = errorMessage; state.elapsed_time = state.start_time ? Date.now() - state.start_time : 0; @@ -201,16 +209,14 @@ const handleRun = () => { node_id: failedNodeId, node_type: state.node_type, status: 'failed', - error_message: - event.message || - $t('ai-platform.workflow.editor.runPanel.execFailed'), + error_message: errorMessage, }); } currentNodeId.value = ''; } result.value = { status: 'failed', - error_message: event.message, + error_message: errorMessage, execution_log: nodeStateList.value, }; running.value = false; @@ -400,32 +406,26 @@ const handleRun = () => { (error) => { console.error('Stream error:', error); stopTimer(); + const errorMessage = getStreamErrorMessage(error); // 将当前运行中的节点标记为失败 if (currentNodeId.value) { const state = nodeStates.value.get(currentNodeId.value); if (state && state.status === 'running') { state.status = 'failed'; - state.error_message = - error.message || - $t('ai-platform.workflow.editor.runPanel.execFailed'); + state.error_message = errorMessage; emit('node-complete', { node_id: currentNodeId.value, node_type: state.node_type, status: 'failed', - error_message: - error.message || - $t('ai-platform.workflow.editor.runPanel.execFailed'), + error_message: errorMessage, }); } currentNodeId.value = ''; } - ElMessage.error( - error.message || - $t('ai-platform.workflow.editor.runPanel.workflowExecFailed'), - ); + ElMessage.error(errorMessage); result.value = { status: 'failed', - error_message: error.message, + error_message: errorMessage, execution_log: nodeStateList.value, }; running.value = false; diff --git a/web/apps/web-ele/src/views/ai-platform/workflow/index.vue b/web/apps/web-ele/src/views/ai-platform/workflow/index.vue index 9df3172..6c352a2 100644 --- a/web/apps/web-ele/src/views/ai-platform/workflow/index.vue +++ b/web/apps/web-ele/src/views/ai-platform/workflow/index.vue @@ -314,6 +314,18 @@ function getWorkflowFinalOutput(outputs?: Record) { return previewRunValue(nodeValues.at(-1) || outputs, 800); } +function getWorkflowErrorMessage( + event: Error | Partial | any, +) { + return ( + event?.error_message || + event?.message || + event?.error || + event?.content || + '流程运行失败' + ); +} + function pushRunEvent( title: string, status: 'failed' | 'info' | 'running' | 'success' | 'warning' = 'info', @@ -451,8 +463,7 @@ function handleWorkflowRunEvent(event: WorkflowStreamEvent) { } case 'error': { runStatus.value = 'failed'; - runError.value = - event.message || event.error_message || event.error || '流程运行失败'; + runError.value = getWorkflowErrorMessage(event); pushRunEvent('流程运行失败', 'failed', runError.value); ElMessage.error(runError.value); cancelWorkflowRunStream = null; @@ -477,7 +488,7 @@ function handleRunWorkflow(inputs: Record) { handleWorkflowRunEvent, (error) => { runStatus.value = 'failed'; - runError.value = error.message || '流程运行失败'; + runError.value = getWorkflowErrorMessage(error); pushRunEvent('流程运行失败', 'failed', runError.value); ElMessage.error(runError.value); cancelWorkflowRunStream = null; diff --git a/web/apps/web-ele/src/views/ai-platform/workflow/shared/useRunReplay.ts b/web/apps/web-ele/src/views/ai-platform/workflow/shared/useRunReplay.ts index b95fce9..489504c 100644 --- a/web/apps/web-ele/src/views/ai-platform/workflow/shared/useRunReplay.ts +++ b/web/apps/web-ele/src/views/ai-platform/workflow/shared/useRunReplay.ts @@ -50,6 +50,11 @@ function resolveNodeOutput(log: ExecutionLogEntry, run: WorkflowRun) { }; } +function getLogError(log: ExecutionLogEntry) { + const item = log as any; + return item.error || item.error_message || item.message || ''; +} + function buildReplayInputs( logIndex: number, logs: ExecutionLogEntry[], @@ -122,13 +127,13 @@ export function applyRunReplay( status: isWaiting ? 'running' : isSuccess ? 'success' : 'failed', elapsed_time: log.elapsed_time, tokens_used: log.tokens_used, - error: log.error, + error: getLogError(log), }, executionResult: { status: mapLogStatus(log.status || 'completed'), elapsed_time: log.elapsed_time, tokens_used: log.tokens_used, - error_message: log.error, + error_message: getLogError(log), output: nodeOutput, inputs: nodeInputs, },