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.
This commit is contained in:
MrBeanDev 2026-08-04 07:03:40 +05:30
parent b5b53acc57
commit 5da9cc3d76
3 changed files with 66 additions and 2 deletions

View File

@ -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()
}
})

View File

@ -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",

View File

@ -525,6 +525,9 @@
"loadConfigFailed": "加载配置失败: {error}",
"step2Init": "Step2 环境搭建初始化",
"step3Init": "Step3 模拟运行初始化",
"resumeCheckFailed": "✗ 无法获取运行状态: {error}。已跳过启动,避免丢弃已有的模拟数据。",
"resumedRunningSim": "↻ 已接管正在运行的模拟(第 {round} 轮)——未重新开始",
"loadedExistingSim": "↻ 已载入既有的模拟运行({actions} 条事件)——未重新开始",
"startingDualSim": "正在启动双平台并行模拟...",
"setMaxRounds": "设置最大模拟轮数: {rounds}",
"graphMemoryUpdateEnabled": "已开启动态图谱更新模式",