fix: clarify ai runtime errors

This commit is contained in:
2026-06-21 20:31:49 +08:00
parent 75f8ffd47f
commit 0c52bf0641
7 changed files with 170 additions and 54 deletions
@@ -112,7 +112,12 @@ class LLMService:
raise ValueError(f'不支持的提供商类型: {provider.provider_type}') raise ValueError(f'不支持的提供商类型: {provider.provider_type}')
self._provider_cache[cache_key] = provider_instance 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: def _get_provider_sync(self, model_id: str, model_data: dict) -> tuple:
""" """
@@ -145,7 +150,70 @@ class LLMService:
raise ValueError(f'不支持的提供商类型: {provider_type}') raise ValueError(f'不支持的提供商类型: {provider_type}')
self._provider_cache[cache_key] = provider_instance 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( def chat_with_provider(
self, self,
@@ -197,7 +265,10 @@ class LLMService:
**kwargs **kwargs
) )
try:
return provider.chat(llm_messages, config) 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]: def _convert_messages(self, messages: List[Dict]) -> List[LLMMessage]:
"""转换消息格式,支持 tool 消息""" """转换消息格式,支持 tool 消息"""
@@ -263,7 +334,10 @@ class LLMService:
**kwargs **kwargs
) )
try:
return await provider.chat_async(llm_messages, config) 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( async def chat_stream(
self, self,
@@ -315,8 +389,11 @@ class LLMService:
**kwargs **kwargs
) )
try:
async for chunk in provider.chat_stream(llm_messages, config): async for chunk in provider.chat_stream(llm_messages, config):
yield chunk yield chunk
except Exception as exc:
raise RuntimeError(self._format_provider_error(exc, provider, model_name)) from exc
def chat_stream_sync( def chat_stream_sync(
self, self,
@@ -428,8 +505,11 @@ class LLMService:
**kwargs **kwargs
) )
try:
for chunk in provider.chat_stream_sync(llm_messages, config): for chunk in provider.chat_stream_sync(llm_messages, config):
yield chunk yield chunk
except Exception as exc:
raise RuntimeError(self._format_provider_error(exc, provider, model_name)) from exc
@staticmethod @staticmethod
def get_available_providers() -> List[Dict]: def get_available_providers() -> List[Dict]:
@@ -21,6 +21,11 @@ function formatDuration(ms?: number) {
if (ms < 1000) return `${ms}ms`; if (ms < 1000) return `${ms}ms`;
return `${(ms / 1000).toFixed(2)}s`; return `${(ms / 1000).toFixed(2)}s`;
} }
function getLogError(log: ExecutionLogEntry) {
const item = log as any;
return item.error || item.error_message || item.message || '';
}
</script> </script>
<template> <template>
@@ -52,10 +57,10 @@ function formatDuration(ms?: number) {
{{ log.node_type }} {{ log.node_type }}
</div> </div>
<div <div
v-if="log.error" v-if="getLogError(log)"
class="text-destructive mt-1 line-clamp-2 text-xs" class="text-destructive mt-1 line-clamp-2 text-xs"
> >
{{ log.error }} {{ getLogError(log) }}
</div> </div>
</button> </button>
</ElTimelineItem> </ElTimelineItem>
@@ -178,7 +178,7 @@ const collaborationTimeline = computed(() => {
subtitle: log.node_label || log.node_id, subtitle: log.node_label || log.node_id,
status: log.status, status: log.status,
summary: summaryParts.join(' · '), summary: summaryParts.join(' · '),
message: communication.message || previewValue(log.error || log.output), message: communication.message || previewValue(getLogError(log) || log.output),
elapsed: log.elapsed_time, elapsed: log.elapsed_time,
timestamp, timestamp,
tokens: log.tokens_used || 0, tokens: log.tokens_used || 0,
@@ -233,7 +233,7 @@ const collaborationSupportSummary = computed(() => {
log.node_type, log.node_type,
log.branch_label, log.branch_label,
previewValue(log.output), previewValue(log.output),
previewValue(log.error), previewValue(getLogError(log)),
].join(' '), ].join(' '),
) )
.join(' ') .join(' ')
@@ -432,6 +432,21 @@ function getCollaboration(log: any) {
return getLogMeta(log).collaboration || {}; return getLogMeta(log).collaboration || {};
} }
function getLogError(log: any) {
const meta = getLogMeta(log);
const communication = meta.communication || {};
return (
log?.error ||
log?.error_message ||
log?.message ||
meta.error ||
meta.error_message ||
communication.error ||
communication.message ||
''
);
}
function getActorName(log: any) { function getActorName(log: any) {
const communication = getCommunication(log); const communication = getCommunication(log);
const actor = communication.actor || {}; const actor = communication.actor || {};
@@ -747,8 +762,8 @@ defineExpose({ open });
</template> </template>
<div class="space-y-3 text-xs"> <div class="space-y-3 text-xs">
<div v-if="log.error" class="text-destructive break-words"> <div v-if="getLogError(log)" class="text-destructive break-words">
{{ log.error }} {{ getLogError(log) }}
</div> </div>
<div <div
v-if="getCollaboration(log).agent_code || getCommunication(log).channel" v-if="getCollaboration(log).agent_code || getCommunication(log).channel"
@@ -89,6 +89,15 @@ watch(
const generateId = () => const generateId = () =>
`msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const getStreamErrorMessage = (
event: Error | Partial<WorkflowStreamEvent> | any,
) =>
event?.error_message ||
event?.message ||
event?.error ||
event?.content ||
$t('ai-platform.workflow.editor.chatPanel.execFailed');
// 发送消息 // 发送消息
const handleSend = (text: string) => { const handleSend = (text: string) => {
if (!text || running.value) return; if (!text || running.value) return;
@@ -222,17 +231,13 @@ const runWorkflow = (userMessage: string, inputs: Record<string, any>) => {
}, },
(error) => { (error) => {
console.error('Stream error:', error); console.error('Stream error:', error);
const errorMessage = getStreamErrorMessage(error);
updateAssistantMessage(assistantMsgId, { updateAssistantMessage(assistantMsgId, {
status: 'failed', status: 'failed',
error_message: error_message: errorMessage,
error.message ||
$t('ai-platform.workflow.editor.chatPanel.execFailed'),
}); });
running.value = false; running.value = false;
ElMessage.error( ElMessage.error(errorMessage);
error.message ||
$t('ai-platform.workflow.editor.chatPanel.workflowExecFailed'),
);
}, },
() => { () => {
cancelStream = null; cancelStream = null;
@@ -355,11 +360,10 @@ const handleStreamEvent = (event: WorkflowStreamEvent, msgId: string) => {
} }
case 'error': { case 'error': {
const errorMessage = getStreamErrorMessage(event);
updateAssistantMessage(msgId, { updateAssistantMessage(msgId, {
status: 'failed', status: 'failed',
error_message: error_message: errorMessage,
event.message ||
$t('ai-platform.workflow.editor.chatPanel.execFailed'),
}); });
running.value = false; running.value = false;
break; break;
@@ -698,17 +702,13 @@ const resumeWorkflow = (userInput: any) => {
}, },
(error) => { (error) => {
console.error('Resume stream error:', error); console.error('Resume stream error:', error);
const errorMessage = getStreamErrorMessage(error);
updateAssistantMessage(assistantMsgId, { updateAssistantMessage(assistantMsgId, {
status: 'failed', status: 'failed',
error_message: error_message: errorMessage,
error.message ||
$t('ai-platform.workflow.editor.chatPanel.resumeFailed'),
}); });
running.value = false; running.value = false;
ElMessage.error( ElMessage.error(errorMessage);
error.message ||
$t('ai-platform.workflow.editor.chatPanel.resumeWorkflowFailed'),
);
}, },
() => { () => {
cancelStream = null; cancelStream = null;
@@ -152,6 +152,15 @@ const formatTime = (ms: number | undefined) => {
return `${(ms / 1000).toFixed(1)}s`; return `${(ms / 1000).toFixed(1)}s`;
}; };
const getStreamErrorMessage = (
event: Error | Partial<WorkflowStreamEvent> | any,
) =>
event?.error_message ||
event?.message ||
event?.error ||
event?.content ||
$t('ai-platform.workflow.editor.runPanel.execFailed');
const handleRun = () => { const handleRun = () => {
if (!props.workflowId) return; if (!props.workflowId) return;
running.value = true; running.value = true;
@@ -185,15 +194,14 @@ const handleRun = () => {
case 'error': { case 'error': {
stopTimer(); stopTimer();
const errorMessage = getStreamErrorMessage(event);
// 将当前运行中的节点标记为失败(优先使用事件中的 node_id) // 将当前运行中的节点标记为失败(优先使用事件中的 node_id)
const failedNodeId = event.node_id || currentNodeId.value; const failedNodeId = event.node_id || currentNodeId.value;
if (failedNodeId) { if (failedNodeId) {
const state = nodeStates.value.get(failedNodeId); const state = nodeStates.value.get(failedNodeId);
if (state && state.status === 'running') { if (state && state.status === 'running') {
state.status = 'failed'; state.status = 'failed';
state.error_message = state.error_message = errorMessage;
event.message ||
$t('ai-platform.workflow.editor.runPanel.execFailed');
state.elapsed_time = state.start_time state.elapsed_time = state.start_time
? Date.now() - state.start_time ? Date.now() - state.start_time
: 0; : 0;
@@ -201,16 +209,14 @@ const handleRun = () => {
node_id: failedNodeId, node_id: failedNodeId,
node_type: state.node_type, node_type: state.node_type,
status: 'failed', status: 'failed',
error_message: error_message: errorMessage,
event.message ||
$t('ai-platform.workflow.editor.runPanel.execFailed'),
}); });
} }
currentNodeId.value = ''; currentNodeId.value = '';
} }
result.value = { result.value = {
status: 'failed', status: 'failed',
error_message: event.message, error_message: errorMessage,
execution_log: nodeStateList.value, execution_log: nodeStateList.value,
}; };
running.value = false; running.value = false;
@@ -400,32 +406,26 @@ const handleRun = () => {
(error) => { (error) => {
console.error('Stream error:', error); console.error('Stream error:', error);
stopTimer(); stopTimer();
const errorMessage = getStreamErrorMessage(error);
// 将当前运行中的节点标记为失败 // 将当前运行中的节点标记为失败
if (currentNodeId.value) { if (currentNodeId.value) {
const state = nodeStates.value.get(currentNodeId.value); const state = nodeStates.value.get(currentNodeId.value);
if (state && state.status === 'running') { if (state && state.status === 'running') {
state.status = 'failed'; state.status = 'failed';
state.error_message = state.error_message = errorMessage;
error.message ||
$t('ai-platform.workflow.editor.runPanel.execFailed');
emit('node-complete', { emit('node-complete', {
node_id: currentNodeId.value, node_id: currentNodeId.value,
node_type: state.node_type, node_type: state.node_type,
status: 'failed', status: 'failed',
error_message: error_message: errorMessage,
error.message ||
$t('ai-platform.workflow.editor.runPanel.execFailed'),
}); });
} }
currentNodeId.value = ''; currentNodeId.value = '';
} }
ElMessage.error( ElMessage.error(errorMessage);
error.message ||
$t('ai-platform.workflow.editor.runPanel.workflowExecFailed'),
);
result.value = { result.value = {
status: 'failed', status: 'failed',
error_message: error.message, error_message: errorMessage,
execution_log: nodeStateList.value, execution_log: nodeStateList.value,
}; };
running.value = false; running.value = false;
@@ -314,6 +314,18 @@ function getWorkflowFinalOutput(outputs?: Record<string, any>) {
return previewRunValue(nodeValues.at(-1) || outputs, 800); return previewRunValue(nodeValues.at(-1) || outputs, 800);
} }
function getWorkflowErrorMessage(
event: Error | Partial<WorkflowStreamEvent> | any,
) {
return (
event?.error_message ||
event?.message ||
event?.error ||
event?.content ||
'流程运行失败'
);
}
function pushRunEvent( function pushRunEvent(
title: string, title: string,
status: 'failed' | 'info' | 'running' | 'success' | 'warning' = 'info', status: 'failed' | 'info' | 'running' | 'success' | 'warning' = 'info',
@@ -451,8 +463,7 @@ function handleWorkflowRunEvent(event: WorkflowStreamEvent) {
} }
case 'error': { case 'error': {
runStatus.value = 'failed'; runStatus.value = 'failed';
runError.value = runError.value = getWorkflowErrorMessage(event);
event.message || event.error_message || event.error || '流程运行失败';
pushRunEvent('流程运行失败', 'failed', runError.value); pushRunEvent('流程运行失败', 'failed', runError.value);
ElMessage.error(runError.value); ElMessage.error(runError.value);
cancelWorkflowRunStream = null; cancelWorkflowRunStream = null;
@@ -477,7 +488,7 @@ function handleRunWorkflow(inputs: Record<string, any>) {
handleWorkflowRunEvent, handleWorkflowRunEvent,
(error) => { (error) => {
runStatus.value = 'failed'; runStatus.value = 'failed';
runError.value = error.message || '流程运行失败'; runError.value = getWorkflowErrorMessage(error);
pushRunEvent('流程运行失败', 'failed', runError.value); pushRunEvent('流程运行失败', 'failed', runError.value);
ElMessage.error(runError.value); ElMessage.error(runError.value);
cancelWorkflowRunStream = null; cancelWorkflowRunStream = null;
@@ -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( function buildReplayInputs(
logIndex: number, logIndex: number,
logs: ExecutionLogEntry[], logs: ExecutionLogEntry[],
@@ -122,13 +127,13 @@ export function applyRunReplay(
status: isWaiting ? 'running' : isSuccess ? 'success' : 'failed', status: isWaiting ? 'running' : isSuccess ? 'success' : 'failed',
elapsed_time: log.elapsed_time, elapsed_time: log.elapsed_time,
tokens_used: log.tokens_used, tokens_used: log.tokens_used,
error: log.error, error: getLogError(log),
}, },
executionResult: { executionResult: {
status: mapLogStatus(log.status || 'completed'), status: mapLogStatus(log.status || 'completed'),
elapsed_time: log.elapsed_time, elapsed_time: log.elapsed_time,
tokens_used: log.tokens_used, tokens_used: log.tokens_used,
error_message: log.error, error_message: getLogError(log),
output: nodeOutput, output: nodeOutput,
inputs: nodeInputs, inputs: nodeInputs,
}, },