From 2929097ef8e6d67247c3e9be803fe6a66444986c Mon Sep 17 00:00:00 2001 From: Hao OUYANG Date: Tue, 7 Jul 2026 23:52:47 +0800 Subject: [PATCH] fix: suppress idle and empty simulation actions --- backend/app/api/simulation.py | 14 ++- backend/scripts/run_parallel_simulation.py | 2 +- frontend/src/components/Step3Simulation.vue | 117 +++++++++++++++++--- 3 files changed, 114 insertions(+), 19 deletions(-) diff --git a/backend/app/api/simulation.py b/backend/app/api/simulation.py index 126732c5..48664540 100644 --- a/backend/app/api/simulation.py +++ b/backend/app/api/simulation.py @@ -307,8 +307,9 @@ def _check_simulation_prepared(simulation_id: str) -> tuple: # - running: 正在运行,说明准备早就完成了 # - completed: 运行完成,说明准备早就完成了 # - stopped: 已停止,说明准备早就完成了 + # - paused: 手动停止后会写入 paused,配置仍然可复用 # - failed: 运行失败(但准备是完成的) - prepared_statuses = ["ready", "preparing", "running", "completed", "stopped", "failed"] + prepared_statuses = ["ready", "preparing", "running", "completed", "stopped", "paused", "failed"] if status in prepared_statuses and config_generated: # 获取文件统计信息 profiles_file = os.path.join(simulation_dir, "reddit_profiles.json") @@ -1599,6 +1600,17 @@ def start_simulation(): }), 400 logger.info(f"启用图谱记忆更新: simulation_id={simulation_id}, graph_id={graph_id}") + + existing_run_state = SimulationRunner.get_run_state(simulation_id) + if existing_run_state and existing_run_state.runner_status.value not in ["starting", "running"]: + logger.info( + f"清理已结束的旧运行记录后重新启动: " + f"simulation_id={simulation_id}, runner_status={existing_run_state.runner_status.value}" + ) + cleanup_result = SimulationRunner.cleanup_simulation_logs(simulation_id) + if not cleanup_result.get("success"): + logger.warning(f"清理旧运行记录时出现警告: {cleanup_result.get('errors')}") + force_restarted = True # 启动模拟 run_state = SimulationRunner.start_simulation( diff --git a/backend/scripts/run_parallel_simulation.py b/backend/scripts/run_parallel_simulation.py index 9ec72f72..a6411931 100644 --- a/backend/scripts/run_parallel_simulation.py +++ b/backend/scripts/run_parallel_simulation.py @@ -608,7 +608,7 @@ def load_config(config_path: str) -> Dict[str, Any]: # 需要过滤掉的非核心动作类型(这些动作对分析价值较低) -FILTERED_ACTIONS = {'refresh', 'sign_up'} +FILTERED_ACTIONS = {'refresh', 'sign_up', 'do_nothing'} # 动作类型映射表(数据库中的名称 -> 标准名称) ACTION_TYPE_MAP = { diff --git a/frontend/src/components/Step3Simulation.vue b/frontend/src/components/Step3Simulation.vue index 5b0f968c..d62c3117 100644 --- a/frontend/src/components/Step3Simulation.vue +++ b/frontend/src/components/Step3Simulation.vue @@ -106,9 +106,9 @@
-
+
- TOTAL EVENTS: {{ allActions.length }} + TOTAL EVENTS: {{ chronologicalActions.length }}
- - -
+
{{ action.action_args.content }}
@@ -262,7 +279,7 @@
-
+
Waiting for agent actions...
@@ -326,19 +343,21 @@ const allActions = ref([]) // 所有动作(增量累积) const actionIds = ref(new Set()) // 用于去重的动作ID集合 const scrollContainer = ref(null) +const isDisplayableAction = (action) => action?.action_type !== 'DO_NOTHING' + // Computed // 按时间顺序显示动作(最新的在最后面,即底部) const chronologicalActions = computed(() => { - return allActions.value + return allActions.value.filter(isDisplayableAction) }) // 各平台动作计数 const twitterActionsCount = computed(() => { - return allActions.value.filter(a => a.platform === 'twitter').length + return chronologicalActions.value.filter(a => a.platform === 'twitter').length }) const redditActionsCount = computed(() => { - return allActions.value.filter(a => a.platform === 'reddit').length + return chronologicalActions.value.filter(a => a.platform === 'reddit').length }) // 格式化模拟流逝时间(根据轮次和每轮分钟数计算) @@ -379,15 +398,75 @@ const resetAllState = () => { stopPolling() // 停止之前可能存在的轮询 } +const hasExistingRunData = (data) => { + if (!data) return false + const runnerStatus = data.runner_status + const actionsCount = Number(data.total_actions_count || 0) + + Number(data.twitter_actions_count || 0) + + Number(data.reddit_actions_count || 0) + return Boolean(data.started_at) + || actionsCount > 0 + || ['starting', 'running', 'completed', 'stopped', 'paused', 'failed'].includes(runnerStatus) +} + +const isActiveRun = (data) => { + return data?.runner_status === 'starting' + || data?.runner_status === 'running' + || data?.twitter_running + || data?.reddit_running +} + +const restoreExistingRun = async (data) => { + runStatus.value = data + prevTwitterRound.value = data.twitter_current_round || 0 + prevRedditRound.value = data.reddit_current_round || 0 + await fetchRunStatusDetail() + + if (isActiveRun(data)) { + phase.value = 1 + emit('update-status', 'processing') + startStatusPolling() + startDetailPolling() + return + } + + phase.value = 2 + emit('update-status', data.runner_status === 'failed' ? 'error' : 'completed') + if (data.error) { + addLog(t('log.startFailed', { error: data.error })) + } +} + +const initializeSimulationView = async () => { + if (!props.simulationId) { + addLog(t('log.errorMissingSimId')) + return + } + + resetAllState() + + try { + const res = await getRunStatus(props.simulationId) + if (res.success && hasExistingRunData(res.data)) { + await restoreExistingRun(res.data) + return + } + } catch (err) { + console.warn('恢复模拟运行状态失败:', err) + } + + await doStartSimulation({ reset: false }) +} + // 启动模拟 -const doStartSimulation = async () => { +const doStartSimulation = async ({ reset = true } = {}) => { if (!props.simulationId) { addLog(t('log.errorMissingSimId')) return } // 先重置所有状态,确保不会受到上一次模拟的影响 - resetAllState() + if (reset) resetAllState() isStarting.value = true startError.value = null @@ -398,7 +477,7 @@ const doStartSimulation = async () => { const params = { simulation_id: props.simulationId, platform: 'parallel', - force: true, // 强制重新开始 + force: false, enable_graph_memory_update: true // 开启动态图谱更新 } @@ -597,8 +676,10 @@ const getActionTypeLabel = (type) => { 'CREATE_POST': 'POST', 'REPOST': 'REPOST', 'LIKE_POST': 'LIKE', + 'DISLIKE_POST': 'DISLIKE', 'CREATE_COMMENT': 'COMMENT', 'LIKE_COMMENT': 'LIKE', + 'DISLIKE_COMMENT': 'DISLIKE', 'DO_NOTHING': 'IDLE', 'FOLLOW': 'FOLLOW', 'SEARCH_POSTS': 'SEARCH', @@ -614,8 +695,10 @@ const getActionTypeClass = (type) => { 'CREATE_POST': 'badge-post', 'REPOST': 'badge-action', 'LIKE_POST': 'badge-action', + 'DISLIKE_POST': 'badge-action', 'CREATE_COMMENT': 'badge-comment', 'LIKE_COMMENT': 'badge-action', + 'DISLIKE_COMMENT': 'badge-action', 'QUOTE_POST': 'badge-post', 'FOLLOW': 'badge-meta', 'SEARCH_POSTS': 'badge-meta', @@ -690,7 +773,7 @@ watch(() => props.systemLogs?.length, () => { onMounted(() => { addLog(t('log.step3Init')) if (props.simulationId) { - doStartSimulation() + initializeSimulationView() } }) @@ -1124,7 +1207,7 @@ onUnmounted(() => { } /* Info Blocks (Quote, Repost, etc) */ -.quoted-block, .repost-content { +.quoted-block, .repost-content, .liked-content, .voted-content { background: #F9F9F9; border: 1px solid #EEE; padding: 10px 12px; @@ -1264,4 +1347,4 @@ onUnmounted(() => { animation: spin 0.8s linear infinite; margin-right: 6px; } - \ No newline at end of file +