From 14fe5eec626e74959047bdb757d076151b25ce27 Mon Sep 17 00:00:00 2001 From: tt-a1i <2801884530@qq.com> Date: Thu, 2 Apr 2026 22:43:19 +0800 Subject: [PATCH] fix: stop frontend polling when backend simulation has failed (#444) The frontend kept polling /config/realtime and /run-status indefinitely even after the backend had already failed, causing an infinite loading state for users. - Step3: detect runner_status === 'failed' in fetchRunStatus and stop polling - Backend /config/realtime: detect status == "failed" in state.json, return failed flag and error message in response - Step2: detect failure in fetchConfigRealtime response and stop polling - Both steps: add consecutive error counter (max 10) to stop polling on persistent network/API errors --- backend/app/api/simulation.py | 19 ++++++++++++---- frontend/src/components/Step2EnvSetup.vue | 24 +++++++++++++++++++-- frontend/src/components/Step3Simulation.vue | 24 ++++++++++++++++++--- 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/backend/app/api/simulation.py b/backend/app/api/simulation.py index 3a8e1e3fc..74285dc42 100644 --- a/backend/app/api/simulation.py +++ b/backend/app/api/simulation.py @@ -1196,7 +1196,8 @@ def get_simulation_config_realtime(simulation_id: str): is_generating = False generation_stage = None config_generated = False - + state_data = None + state_file = os.path.join(sim_dir, "state.json") if os.path.exists(state_file): try: @@ -1205,9 +1206,11 @@ def get_simulation_config_realtime(simulation_id: str): status = state_data.get("status", "") is_generating = status == "preparing" config_generated = state_data.get("config_generated", False) - + # 判断当前阶段 - if is_generating: + if status == "failed": + generation_stage = "failed" + elif is_generating: if state_data.get("profiles_generated", False): generation_stage = "generating_config" else: @@ -1217,6 +1220,12 @@ def get_simulation_config_realtime(simulation_id: str): except Exception: pass + # 如果状态为失败,提取错误信息 + failed = generation_stage == "failed" + error_message = None + if failed and state_data: + error_message = state_data.get("error", None) + # 构建返回数据 response_data = { "simulation_id": simulation_id, @@ -1225,7 +1234,9 @@ def get_simulation_config_realtime(simulation_id: str): "is_generating": is_generating, "generation_stage": generation_stage, "config_generated": config_generated, - "config": config + "config": config, + "failed": failed, + "error": error_message } # 如果配置存在,提取一些关键统计信息 diff --git a/frontend/src/components/Step2EnvSetup.vue b/frontend/src/components/Step2EnvSetup.vue index a27ba347c..21c2d44cf 100644 --- a/frontend/src/components/Step2EnvSetup.vue +++ b/frontend/src/components/Step2EnvSetup.vue @@ -953,7 +953,11 @@ const fetchProfilesRealtime = async () => { } // 配置轮询 +let configErrorCount = 0 +const MAX_CONFIG_ERRORS = 10 + const startConfigPolling = () => { + configErrorCount = 0 configTimer = setInterval(fetchConfigRealtime, 2000) } @@ -972,7 +976,17 @@ const fetchConfigRealtime = async () => { if (res.success && res.data) { const data = res.data - + configErrorCount = 0 + + // 检测是否失败 + if (data.failed || data.generation_stage === 'failed') { + const errorMsg = data.error || t('common.unknownError') + addLog(`❌ ${t('log.configGenerationFailed') || '配置生成失败'}: ${errorMsg}`) + stopConfigPolling() + emit('update-status', 'failed') + return + } + // 输出配置生成阶段日志(避免重复) if (data.generation_stage && data.generation_stage !== lastLoggedConfigStage) { lastLoggedConfigStage = data.generation_stage @@ -982,7 +996,7 @@ const fetchConfigRealtime = async () => { addLog(t('log.generatingLLMConfig')) } } - + // 如果配置已生成 if (data.config_generated && data.config) { simulationConfig.value = data.config @@ -1017,6 +1031,12 @@ const fetchConfigRealtime = async () => { } } catch (err) { console.warn('获取 Config 失败:', err) + configErrorCount++ + if (configErrorCount >= MAX_CONFIG_ERRORS) { + addLog(`❌ ${t('log.configGenerationFailed') || '配置生成失败'}: ${t('log.pollErrorLimit') || '连续多次获取状态失败,已停止轮询'}`) + stopConfigPolling() + emit('update-status', 'failed') + } } } diff --git a/frontend/src/components/Step3Simulation.vue b/frontend/src/components/Step3Simulation.vue index 5b0f968c6..bf41d5431 100644 --- a/frontend/src/components/Step3Simulation.vue +++ b/frontend/src/components/Step3Simulation.vue @@ -465,8 +465,11 @@ const handleStopSimulation = async () => { // 轮询状态 let statusTimer = null let detailTimer = null +let statusErrorCount = 0 +const MAX_STATUS_ERRORS = 10 const startStatusPolling = () => { + statusErrorCount = 0 statusTimer = setInterval(fetchRunStatus, 2000) } @@ -497,7 +500,7 @@ const fetchRunStatus = async () => { if (res.success && res.data) { const data = res.data - + statusErrorCount = 0 runStatus.value = data // 分别检测各平台的轮次变化并输出日志 @@ -511,13 +514,22 @@ const fetchRunStatus = async () => { prevRedditRound.value = data.reddit_current_round } + // 检测模拟是否失败 + if (data.runner_status === 'failed') { + const errorMsg = data.error || t('common.unknownError') + addLog(`❌ ${t('log.simFailed') || '模拟运行失败'}: ${errorMsg}`) + stopPolling() + emit('update-status', 'failed') + return + } + // 检测模拟是否已完成(通过 runner_status 或平台完成状态判断) const isCompleted = data.runner_status === 'completed' || data.runner_status === 'stopped' - + // 额外检查:如果后端还没来得及更新 runner_status,但平台已经报告完成 // 通过检测 twitter_completed 和 reddit_completed 状态判断 const platformsCompleted = checkPlatformsCompleted(data) - + if (isCompleted || platformsCompleted) { if (platformsCompleted && !isCompleted) { addLog(t('log.allPlatformsCompleted')) @@ -530,6 +542,12 @@ const fetchRunStatus = async () => { } } catch (err) { console.warn('获取运行状态失败:', err) + statusErrorCount++ + if (statusErrorCount >= MAX_STATUS_ERRORS) { + addLog(`❌ ${t('log.simFailed') || '模拟运行失败'}: ${t('log.pollErrorLimit') || '连续多次获取状态失败,已停止轮询'}`) + stopPolling() + emit('update-status', 'failed') + } } }