Skip to content

Commit 5e2e508

Browse files
committed
fix: cherry-pick 5 high-priority fixes from upstream PRs
- fix(runner): prevent action data loss from partial line reads (666ghj#460) Track safe_position; only advance past newline-terminated lines to avoid permanent data loss when the writer is mid-flush. - fix(build): restrict Python to <3.13 to avoid tiktoken failure (666ghj#453) tiktoken wheels are not available for Python 3.13+. - fix(security): default FLASK_DEBUG to False (666ghj#445) Debug mode was on by default, exposing the Werkzeug debugger in production deployments on 0.0.0.0. - fix(polling): stop frontend polling when backend fails (666ghj#448) Step2 and Step3 now detect failed status and error-count limits, stopping the polling interval instead of looping indefinitely. - fix(simulation): stop Step2 hanging after prepare failures (666ghj#336) Reset state flags on each prepare run; raise ValueError instead of returning silently so the task manager marks the task as failed. Co-Authored-By: koyouko <koyouko@users.noreply.github.com>
1 parent c037a62 commit 5e2e508

File tree

6 files changed

+67
-9
lines changed

6 files changed

+67
-9
lines changed

backend/app/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class Config:
2222

2323
# ── Flask ────────────────────────────────────────────────────────────────
2424
SECRET_KEY = os.environ.get('SECRET_KEY', 'mirofish-secret-key')
25-
DEBUG = os.environ.get('FLASK_DEBUG', 'True').lower() == 'true'
25+
DEBUG = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true'
2626

2727
# Disable ASCII escaping so non-ASCII characters display correctly in JSON
2828
JSON_AS_ASCII = False

backend/app/services/simulation_manager.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class SimulationState:
5959
entity_types: List[str] = field(default_factory=list)
6060

6161
# 配置生成信息
62+
profiles_generated: bool = False
6263
config_generated: bool = False
6364
config_reasoning: str = ""
6465

@@ -86,6 +87,7 @@ def to_dict(self) -> Dict[str, Any]:
8687
"entities_count": self.entities_count,
8788
"profiles_count": self.profiles_count,
8889
"entity_types": self.entity_types,
90+
"profiles_generated": self.profiles_generated,
8991
"config_generated": self.config_generated,
9092
"config_reasoning": self.config_reasoning,
9193
"current_round": self.current_round,
@@ -106,6 +108,7 @@ def to_simple_dict(self) -> Dict[str, Any]:
106108
"entities_count": self.entities_count,
107109
"profiles_count": self.profiles_count,
108110
"entity_types": self.entity_types,
111+
"profiles_generated": self.profiles_generated,
109112
"config_generated": self.config_generated,
110113
"error": self.error,
111114
}
@@ -177,6 +180,7 @@ def _load_simulation_state(self, simulation_id: str) -> Optional[SimulationState
177180
entities_count=data.get("entities_count", 0),
178181
profiles_count=data.get("profiles_count", 0),
179182
entity_types=data.get("entity_types", []),
183+
profiles_generated=data.get("profiles_generated", False),
180184
config_generated=data.get("config_generated", False),
181185
config_reasoning=data.get("config_reasoning", ""),
182186
current_round=data.get("current_round", 0),
@@ -264,6 +268,10 @@ def prepare_simulation(
264268

265269
try:
266270
state.status = SimulationStatus.PREPARING
271+
state.error = None
272+
state.profiles_generated = False
273+
state.config_generated = False
274+
state.config_reasoning = ""
267275
self._save_simulation_state(state)
268276

269277
sim_dir = self._get_simulation_dir(simulation_id)
@@ -298,7 +306,7 @@ def prepare_simulation(
298306
state.status = SimulationStatus.FAILED
299307
state.error = "没有找到符合条件的实体,请检查图谱是否正确构建"
300308
self._save_simulation_state(state)
301-
return state
309+
raise ValueError(state.error)
302310

303311
# ========== 阶段2: 生成Agent Profile ==========
304312
total_entities = len(filtered.entities)
@@ -346,7 +354,9 @@ def profile_progress(current, total, msg):
346354
)
347355

348356
state.profiles_count = len(profiles)
349-
357+
state.profiles_generated = len(profiles) > 0
358+
self._save_simulation_state(state)
359+
350360
# 保存Profile文件(注意:Twitter使用CSV格式,Reddit使用JSON格式)
351361
# Reddit 已经在生成过程中实时保存了,这里再保存一次确保完整性
352362
if progress_callback:

backend/app/services/simulation_runner.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,17 @@ def _read_action_log(
608608
try:
609609
with open(log_path, 'r', encoding='utf-8') as f:
610610
f.seek(position)
611-
for line in f:
611+
# Only advance safe_position after reading a complete
612+
# newline-terminated line. Partial lines (writer mid-flush)
613+
# are left unread for the next poll cycle to prevent data loss.
614+
safe_position = position
615+
while True:
616+
line = f.readline()
617+
if not line:
618+
break # EOF
619+
if not line.endswith('\n'):
620+
break # incomplete line — retry next poll
621+
safe_position = f.tell()
612622
line = line.strip()
613623
if line:
614624
try:
@@ -684,7 +694,7 @@ def _read_action_log(
684694

685695
except json.JSONDecodeError:
686696
pass
687-
return f.tell()
697+
return safe_position
688698
except Exception as e:
689699
logger.warning(f"读取动作日志失败: {log_path}, error={e}")
690700
return position

backend/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "mirofish-backend"
33
version = "0.1.0"
44
description = "MiroFish - 简洁通用的群体智能引擎,预测万物"
5-
requires-python = ">=3.11"
5+
requires-python = ">=3.11,<3.13"
66
license = { text = "AGPL-3.0" }
77
authors = [
88
{ name = "MiroFish Team" }

frontend/src/components/Step2EnvSetup.vue

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -952,7 +952,11 @@ const fetchProfilesRealtime = async () => {
952952
}
953953
954954
// 配置轮询
955+
let configErrorCount = 0
956+
const MAX_CONFIG_ERRORS = 10
957+
955958
const startConfigPolling = () => {
959+
configErrorCount = 0
956960
configTimer = setInterval(fetchConfigRealtime, 2000)
957961
}
958962
@@ -971,7 +975,17 @@ const fetchConfigRealtime = async () => {
971975
972976
if (res.success && res.data) {
973977
const data = res.data
974-
978+
configErrorCount = 0
979+
980+
// Stop polling if backend reported a failure
981+
if (data.failed || data.generation_stage === 'failed') {
982+
const errorMsg = data.error || 'Unknown error'
983+
addLog(`❌ Config generation failed: ${errorMsg}`)
984+
stopConfigPolling()
985+
emit('update-status', 'failed')
986+
return
987+
}
988+
975989
// 输出配置生成阶段日志(避免重复)
976990
if (data.generation_stage && data.generation_stage !== lastLoggedConfigStage) {
977991
lastLoggedConfigStage = data.generation_stage
@@ -1016,6 +1030,12 @@ const fetchConfigRealtime = async () => {
10161030
}
10171031
} catch (err) {
10181032
console.warn('获取 Config 失败:', err)
1033+
configErrorCount++
1034+
if (configErrorCount >= MAX_CONFIG_ERRORS) {
1035+
addLog(`❌ Config generation failed: Too many consecutive polling errors`)
1036+
stopConfigPolling()
1037+
emit('update-status', 'failed')
1038+
}
10191039
}
10201040
}
10211041

frontend/src/components/Step3Simulation.vue

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,8 +465,11 @@ const handleStopSimulation = async () => {
465465
// 轮询状态
466466
let statusTimer = null
467467
let detailTimer = null
468+
let statusErrorCount = 0
469+
const MAX_STATUS_ERRORS = 10
468470
469471
const startStatusPolling = () => {
472+
statusErrorCount = 0
470473
statusTimer = setInterval(fetchRunStatus, 2000)
471474
}
472475
@@ -497,9 +500,18 @@ const fetchRunStatus = async () => {
497500
498501
if (res.success && res.data) {
499502
const data = res.data
500-
503+
statusErrorCount = 0
501504
runStatus.value = data
502-
505+
506+
// Stop polling if backend reported a failure
507+
if (data.runner_status === 'failed') {
508+
const errorMsg = data.error || 'Unknown error'
509+
addLog(`❌ Simulation failed: ${errorMsg}`)
510+
stopPolling()
511+
emit('update-status', 'failed')
512+
return
513+
}
514+
503515
// 分别检测各平台的轮次变化并输出日志
504516
if (data.twitter_current_round > prevTwitterRound.value) {
505517
addLog(`[Plaza] R${data.twitter_current_round}/${data.total_rounds} | T:${data.twitter_simulated_hours || 0}h | A:${data.twitter_actions_count}`)
@@ -530,6 +542,12 @@ const fetchRunStatus = async () => {
530542
}
531543
} catch (err) {
532544
console.warn('获取运行状态失败:', err)
545+
statusErrorCount++
546+
if (statusErrorCount >= MAX_STATUS_ERRORS) {
547+
addLog(`❌ Simulation failed: Too many consecutive polling errors`)
548+
stopPolling()
549+
emit('update-status', 'failed')
550+
}
533551
}
534552
}
535553

0 commit comments

Comments
 (0)