From 5da9cc3d7692fd9979a8f3c66ce56e6114b0831f Mon Sep 17 00:00:00 2001 From: MrBeanDev Date: Tue, 4 Aug 2026 07:03:40 +0530 Subject: [PATCH] fix(simulation): stop discarding an existing run when Step 3 remounts Step3Simulation mounted with a simulation id and immediately called doStartSimulation() with force: true. Because the id lives in the route path (/simulation/:simulationId/start), a browser refresh remounted the component and re-sent that request, so the backend stopped the running subprocess and cleaned the run directory: run_state.json, the twitter and reddit actions.jsonl action logs, and both *_simulation.db files. The timeline, posts and comments views read those files directly, so the data was unrecoverable. Probe the run status before deciding what to do. A simulation that has never run (runner_status "idle" with no recorded actions) still starts exactly as before. Anything else is adopted instead of restarted: live runs resume polling, finished runs load their existing timeline read-only. If the status probe itself fails we now do nothing rather than risk overwriting a run we could not verify. force is also no longer hardcoded to true. The remaining call site only runs when there is nothing to lose, and the flag should never be an implicit default. --- frontend/src/components/Step3Simulation.vue | 62 ++++++++++++++++++++- locales/en.json | 3 + locales/zh.json | 3 + 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Step3Simulation.vue b/frontend/src/components/Step3Simulation.vue index e9dd3dc3..5425cf14 100644 --- a/frontend/src/components/Step3Simulation.vue +++ b/frontend/src/components/Step3Simulation.vue @@ -398,7 +398,9 @@ const doStartSimulation = async () => { const params = { simulation_id: props.simulationId, platform: 'parallel', - force: true, // 强制重新开始 + // 只在确认没有可丢失的运行数据后才会走到这里,因此无需强制重启。 + // force=true 会让后端删除 actions.jsonl 与 *_simulation.db,绝不能作为隐式默认值。 + force: false, enable_graph_memory_update: true // 开启动态图谱更新 } @@ -437,6 +439,62 @@ const doStartSimulation = async () => { } } +// 判断后端是否已存在不可再生的运行数据 +// run_state 缺失时后端返回 runner_status='idle',此时才认为是全新的模拟 +const hasRecoverableRun = (status) => { + if (!status) return false + if (status.runner_status && status.runner_status !== 'idle') return true + return (status.total_actions_count || 0) > 0 +} + +// 组件挂载入口:先探测后端真实状态,再决定是接管还是新建 +// 页面刷新会重新挂载本组件,直接启动会让后端删除既有的模拟数据 +const resumeOrStartSimulation = async () => { + if (!props.simulationId) { + addLog(t('log.errorMissingSimId')) + return + } + + let status = null + try { + const res = await getRunStatus(props.simulationId) + if (!res.success || !res.data) { + throw new Error(res.error || t('common.unknownError')) + } + status = res.data + } catch (err) { + // 探测失败时保持观望:宁可什么都不做,也不能覆盖可能存在的运行数据 + startError.value = err.message + addLog(t('log.resumeCheckFailed', { error: err.message })) + emit('update-status', 'error') + return + } + + if (!hasRecoverableRun(status)) { + await doStartSimulation() + return + } + + // 已有运行数据:接管现有运行,不重启、不清理 + runStatus.value = status + prevTwitterRound.value = status.twitter_current_round || 0 + prevRedditRound.value = status.reddit_current_round || 0 + await fetchRunStatusDetail() + + const isLive = ['starting', 'running', 'paused', 'stopping'].includes(status.runner_status) + if (isLive) { + phase.value = 1 + addLog(t('log.resumedRunningSim', { round: status.current_round || 0 })) + emit('update-status', 'processing') + startStatusPolling() + startDetailPolling() + } else { + phase.value = 2 + addLog(t('log.loadedExistingSim', { actions: status.total_actions_count || 0 })) + emit('update-status', status.runner_status === 'failed' ? 'error' : 'completed') + } +} + // 停止模拟 const handleStopSimulation = async () => { if (!props.simulationId) return @@ -691,7 +749,7 @@ watch(() => props.systemLogs?.length, () => { onMounted(() => { addLog(t('log.step3Init')) if (props.simulationId) { - doStartSimulation() + resumeOrStartSimulation() } }) diff --git a/locales/en.json b/locales/en.json index 02679955..050b9c52 100644 --- a/locales/en.json +++ b/locales/en.json @@ -525,6 +525,9 @@ "loadConfigFailed": "Failed to load config: {error}", "step2Init": "Step 2 environment setup initialized", "step3Init": "Step 3 simulation run initialized", + "resumeCheckFailed": "✗ Could not read run status: {error}. Not starting, to avoid discarding an existing run.", + "resumedRunningSim": "↻ Reattached to the running simulation (round {round}) — not restarted", + "loadedExistingSim": "↻ Loaded the existing simulation run ({actions} events) — not restarted", "startingDualSim": "Starting dual-platform parallel simulation...", "setMaxRounds": "Max simulation rounds set to: {rounds}", "graphMemoryUpdateEnabled": "Dynamic graph memory update enabled", diff --git a/locales/zh.json b/locales/zh.json index c5937dcc..645afa93 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -525,6 +525,9 @@ "loadConfigFailed": "加载配置失败: {error}", "step2Init": "Step2 环境搭建初始化", "step3Init": "Step3 模拟运行初始化", + "resumeCheckFailed": "✗ 无法获取运行状态: {error}。已跳过启动,避免丢弃已有的模拟数据。", + "resumedRunningSim": "↻ 已接管正在运行的模拟(第 {round} 轮)——未重新开始", + "loadedExistingSim": "↻ 已载入既有的模拟运行({actions} 条事件)——未重新开始", "startingDualSim": "正在启动双平台并行模拟...", "setMaxRounds": "设置最大模拟轮数: {rounds}", "graphMemoryUpdateEnabled": "已开启动态图谱更新模式",