Preserve completed simulation replay flow

This commit is contained in:
liyizhouAI 2026-06-08 19:51:36 +08:00
parent e1467b53e2
commit 57263d5337
3 changed files with 115 additions and 10 deletions

View File

@ -32,6 +32,21 @@ def allowed_file(filename: str) -> bool:
return ext in Config.ALLOWED_EXTENSIONS
def _derive_project_name(raw_name: str, uploaded_files) -> str:
"""Use a readable project name when the client did not provide one."""
name = (raw_name or "").strip()
if name and name.lower() not in {"unnamed project", "untitled", "new project"}:
return name
filenames = [getattr(f, "filename", "") for f in (uploaded_files or []) if getattr(f, "filename", "")]
if filenames:
stem = os.path.splitext(os.path.basename(filenames[0]))[0].strip()
if stem:
return stem
return "Foresight 演示项目"
def _prepare_graph_for_visualization(graph_data: dict) -> dict:
"""Keep graph visualization dense and readable by showing the top connected nodes."""
if not isinstance(graph_data, dict):
@ -205,24 +220,26 @@ def generate_ontology():
# 获取参数
simulation_requirement = request.form.get('simulation_requirement', '')
project_name = request.form.get('project_name', 'Unnamed Project')
raw_project_name = request.form.get('project_name', 'Unnamed Project')
additional_context = request.form.get('additional_context', '')
# token 追踪:标记当前 stage
from ..utils import token_tracker
token_tracker.set_stage("step1_ontology")
# 获取上传的文件
uploaded_files = request.files.getlist('files')
project_name = _derive_project_name(raw_project_name, uploaded_files)
logger.debug(f"项目名称: {project_name}")
logger.debug(f"模拟需求: {simulation_requirement[:100]}...")
if not simulation_requirement:
return jsonify({
"success": False,
"error": t('api.requireSimulationRequirement')
}), 400
# 获取上传的文件
uploaded_files = request.files.getlist('files')
if not uploaded_files or all(not f.filename for f in uploaded_files):
return jsonify({
"success": False,

View File

@ -38,6 +38,37 @@ logger = get_logger('foresight.api.simulation')
INTERVIEW_PROMPT_PREFIX = "结合你的人设、所有的过往记忆与行动,不调用任何工具直接用文本回复我:"
def _runner_status_value(run_state) -> str:
if not run_state:
return ""
status = getattr(run_state, "runner_status", "")
return status.value if hasattr(status, "value") else str(status)
def _has_completed_run(run_state) -> bool:
if not run_state:
return False
status = _runner_status_value(run_state)
total_rounds = int(getattr(run_state, "total_rounds", 0) or 0)
current_round = int(getattr(run_state, "current_round", 0) or 0)
return status in {"completed", "stopped"} or (total_rounds > 0 and current_round >= total_rounds)
def _display_project_name(project) -> str:
if not project:
return "未命名项目"
name = (getattr(project, "name", "") or "").strip()
if name and name.lower() not in {"unnamed project", "untitled", "new project"}:
return name
files = getattr(project, "files", []) or []
if files:
filename = files[0].get("filename") or files[0].get("original_filename") or ""
stem = os.path.splitext(os.path.basename(filename))[0].strip()
if stem:
return stem
return "未命名项目"
def optimize_interview_prompt(prompt: str) -> str:
"""
优化Interview提问添加前缀避免Agent调用工具
@ -1063,9 +1094,13 @@ def get_simulation_history():
run_state = SimulationRunner.get_run_state(sim.simulation_id)
if run_state:
sim_dict["current_round"] = run_state.current_round
sim_dict["runner_status"] = run_state.runner_status.value
sim_dict["runner_status"] = _runner_status_value(run_state)
# 使用用户设置的 total_rounds若无则使用推荐轮数
sim_dict["total_rounds"] = run_state.total_rounds if run_state.total_rounds > 0 else recommended_rounds
if _has_completed_run(run_state):
sim_dict["status"] = "completed"
sim_dict["current_round"] = sim_dict["total_rounds"]
sim_dict["has_replay"] = True
else:
sim_dict["current_round"] = 0
sim_dict["runner_status"] = "idle"
@ -1074,11 +1109,13 @@ def get_simulation_history():
# 获取关联项目的文件列表最多3个
project = ProjectManager.get_project(sim.project_id)
if project and hasattr(project, 'files') and project.files:
sim_dict["project_name"] = _display_project_name(project)
sim_dict["files"] = [
{"filename": f.get("filename", "未知文件")}
for f in project.files[:3]
]
else:
sim_dict["project_name"] = sim_dict.get("project_name") or "未命名项目"
sim_dict["files"] = []
# 获取关联的 report_id查找该 simulation 最新的 report
@ -2279,7 +2316,7 @@ def get_simulation_replay(simulation_id: str):
if project:
project_info = {
"project_id": project.project_id,
"name": project.name,
"name": _display_project_name(project),
"status": project.status.value if hasattr(project.status, 'value') else str(project.status),
"graph_id": project.graph_id,
"simulation_requirement": project.simulation_requirement,
@ -2421,6 +2458,9 @@ def get_simulation_replay(simulation_id: str):
# ---------- 6. Workflow 时间线 ----------
status_str = state.status.value if hasattr(state.status, 'value') else str(state.status)
run_state = SimulationRunner.get_run_state(simulation_id)
if _has_completed_run(run_state):
status_str = "completed"
workflow = [
{
"step": 1,

View File

@ -287,7 +287,7 @@
<script setup>
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import {
startSimulation,
@ -314,6 +314,7 @@ const props = defineProps({
const emit = defineEmits(['go-back', 'next-step', 'add-log', 'update-status'])
const router = useRouter()
const route = useRoute()
// State
const isGeneratingReport = ref(false)
@ -325,6 +326,7 @@ const runStatus = ref({})
const allActions = ref([]) //
const actionIds = ref(new Set()) // ID
const scrollContainer = ref(null)
const reviewMode = ref(false)
// Computed
//
@ -368,6 +370,7 @@ const addLog = (msg) => {
//
const resetAllState = () => {
phase.value = 0
reviewMode.value = false
runStatus.value = {}
allActions.value = []
actionIds.value = new Set()
@ -379,6 +382,50 @@ const resetAllState = () => {
stopPolling() //
}
const hasCompletedRun = (data) => {
if (!data) return false
const totalRounds = Number(data.total_rounds || 0)
const currentRound = Number(data.current_round || 0)
return data.runner_status === 'completed' ||
data.runner_status === 'stopped' ||
(totalRounds > 0 && currentRound >= totalRounds)
}
const loadExistingRunForReview = async (statusData = null) => {
reviewMode.value = true
phase.value = 2
emit('update-status', 'completed')
if (statusData) {
runStatus.value = statusData
}
await fetchRunStatusDetail()
await fetchRunStatus()
addLog(`Replay mode: using completed dual-world result (${runStatus.value.total_rounds || props.maxRounds || '-'} rounds · ${runStatus.value.total_actions_count || allActions.value.length} actions).`)
}
const startOrReviewSimulation = async () => {
if (!props.simulationId) {
addLog(t('log.errorMissingSimId'))
return
}
if (route.query.rerun !== '1') {
try {
const statusRes = await getRunStatus(props.simulationId)
if (statusRes.success && hasCompletedRun(statusRes.data)) {
await loadExistingRunForReview(statusRes.data)
return
}
} catch (err) {
addLog(`Completed replay check failed, falling back to live start: ${err.message}`)
}
}
await doStartSimulation()
}
//
const doStartSimulation = async () => {
if (!props.simulationId) {
@ -533,9 +580,10 @@ const fetchRunStatus = async () => {
if (isCompleted || platformsCompleted) {
if (platformsCompleted && !isCompleted) {
addLog(t('log.allPlatformsCompleted'))
addLog(t('log.allPlatformsCompleted'))
}
addLog(t('log.simCompleted'))
reviewMode.value = true
phase.value = 2
stopPolling()
emit('update-status', 'completed')
@ -710,7 +758,7 @@ watch(() => props.systemLogs?.length, () => {
onMounted(() => {
addLog(t('log.step3Init'))
if (props.simulationId) {
doStartSimulation()
startOrReviewSimulation()
}
})