Add real-time simulation configuration endpoint and update frontend components
- Introduced a new API endpoint for retrieving real-time simulation configuration, allowing users to view progress and metadata during simulation generation. - Updated frontend API service to include the new real-time configuration method. - Enhanced Step2EnvSetup.vue to support real-time polling for configuration updates, improving user experience during simulation setup. - Revised display logic to show detailed configuration summaries and orchestration content, enriching the simulation setup process.
This commit is contained in:
parent
ceb1399144
commit
8b5d082fb1
3 changed files with 390 additions and 20 deletions
|
|
@ -936,6 +936,126 @@ def get_simulation_profiles_realtime(simulation_id: str):
|
|||
}), 500
|
||||
|
||||
|
||||
@simulation_bp.route('/<simulation_id>/config/realtime', methods=['GET'])
|
||||
def get_simulation_config_realtime(simulation_id: str):
|
||||
"""
|
||||
实时获取模拟配置(用于在生成过程中实时查看进度)
|
||||
|
||||
与 /config 接口的区别:
|
||||
- 直接读取文件,不经过 SimulationManager
|
||||
- 适用于生成过程中的实时查看
|
||||
- 返回额外的元数据(如文件修改时间、是否正在生成等)
|
||||
- 即使配置还没生成完也能返回部分信息
|
||||
|
||||
返回:
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"simulation_id": "sim_xxxx",
|
||||
"file_exists": true,
|
||||
"file_modified_at": "2025-12-04T18:20:00",
|
||||
"is_generating": true, // 是否正在生成
|
||||
"generation_stage": "generating_config", // 当前生成阶段
|
||||
"config": {...} // 配置内容(如果存在)
|
||||
}
|
||||
}
|
||||
"""
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
# 获取模拟目录
|
||||
sim_dir = os.path.join(Config.OASIS_SIMULATION_DATA_DIR, simulation_id)
|
||||
|
||||
if not os.path.exists(sim_dir):
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"模拟不存在: {simulation_id}"
|
||||
}), 404
|
||||
|
||||
# 配置文件路径
|
||||
config_file = os.path.join(sim_dir, "simulation_config.json")
|
||||
|
||||
# 检查文件是否存在
|
||||
file_exists = os.path.exists(config_file)
|
||||
config = None
|
||||
file_modified_at = None
|
||||
|
||||
if file_exists:
|
||||
# 获取文件修改时间
|
||||
file_stat = os.stat(config_file)
|
||||
file_modified_at = datetime.fromtimestamp(file_stat.st_mtime).isoformat()
|
||||
|
||||
try:
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
except (json.JSONDecodeError, Exception) as e:
|
||||
logger.warning(f"读取 config 文件失败(可能正在写入中): {e}")
|
||||
config = None
|
||||
|
||||
# 检查是否正在生成(通过 state.json 判断)
|
||||
is_generating = False
|
||||
generation_stage = None
|
||||
config_generated = False
|
||||
|
||||
state_file = os.path.join(sim_dir, "state.json")
|
||||
if os.path.exists(state_file):
|
||||
try:
|
||||
with open(state_file, 'r', encoding='utf-8') as f:
|
||||
state_data = json.load(f)
|
||||
status = state_data.get("status", "")
|
||||
is_generating = status == "preparing"
|
||||
config_generated = state_data.get("config_generated", False)
|
||||
|
||||
# 判断当前阶段
|
||||
if is_generating:
|
||||
if state_data.get("profiles_generated", False):
|
||||
generation_stage = "generating_config"
|
||||
else:
|
||||
generation_stage = "generating_profiles"
|
||||
elif status == "ready":
|
||||
generation_stage = "completed"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 构建返回数据
|
||||
response_data = {
|
||||
"simulation_id": simulation_id,
|
||||
"file_exists": file_exists,
|
||||
"file_modified_at": file_modified_at,
|
||||
"is_generating": is_generating,
|
||||
"generation_stage": generation_stage,
|
||||
"config_generated": config_generated,
|
||||
"config": config
|
||||
}
|
||||
|
||||
# 如果配置存在,提取一些关键统计信息
|
||||
if config:
|
||||
response_data["summary"] = {
|
||||
"total_agents": len(config.get("agent_configs", [])),
|
||||
"simulation_hours": config.get("time_config", {}).get("total_simulation_hours"),
|
||||
"initial_posts_count": len(config.get("event_config", {}).get("initial_posts", [])),
|
||||
"hot_topics_count": len(config.get("event_config", {}).get("hot_topics", [])),
|
||||
"has_twitter_config": "twitter_config" in config,
|
||||
"has_reddit_config": "reddit_config" in config,
|
||||
"generated_at": config.get("generated_at"),
|
||||
"llm_model": config.get("llm_model")
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": response_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"实时获取Config失败: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"traceback": traceback.format_exc()
|
||||
}), 500
|
||||
|
||||
|
||||
@simulation_bp.route('/<simulation_id>/config', methods=['GET'])
|
||||
def get_simulation_config(simulation_id: str):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -58,6 +58,15 @@ export const getSimulationConfig = (simulationId) => {
|
|||
return service.get(`/api/simulation/${simulationId}/config`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 实时获取生成中的模拟配置
|
||||
* @param {string} simulationId
|
||||
* @returns {Promise} 返回配置信息,包含元数据和配置内容
|
||||
*/
|
||||
export const getSimulationConfigRealtime = (simulationId) => {
|
||||
return service.get(`/api/simulation/${simulationId}/config/realtime`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有模拟
|
||||
* @param {string} projectId - 可选,按项目ID过滤
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 03: 生成模拟配置 -->
|
||||
<!-- Step 03: 生成双平台模拟配置 -->
|
||||
<div class="step-card" :class="{ 'active': phase === 2, 'completed': phase > 2 }">
|
||||
<div class="card-header">
|
||||
<div class="step-info">
|
||||
|
|
@ -137,38 +137,98 @@
|
|||
<div v-if="simulationConfig" class="config-preview">
|
||||
<div class="config-section">
|
||||
<span class="config-label">模拟时长</span>
|
||||
<span class="config-value">{{ simulationConfig.time_config?.simulation_hours || '-' }} 小时</span>
|
||||
<span class="config-value">{{ simulationConfig.time_config?.total_simulation_hours || '-' }} 小时</span>
|
||||
</div>
|
||||
<div class="config-section">
|
||||
<span class="config-label">模拟轮次</span>
|
||||
<span class="config-value">{{ simulationConfig.time_config?.max_rounds || '-' }} 轮</span>
|
||||
<span class="config-label">总轮次</span>
|
||||
<span class="config-value">{{ (simulationConfig.time_config?.total_simulation_hours * 60 / simulationConfig.time_config?.minutes_per_round) || '-' }} 轮</span>
|
||||
</div>
|
||||
<div class="config-section">
|
||||
<span class="config-label">平台</span>
|
||||
<span class="config-label">平台配置</span>
|
||||
<span class="config-value">
|
||||
<span v-if="simulationConfig.platform_configs?.twitter" class="platform-tag">Twitter</span>
|
||||
<span v-if="simulationConfig.platform_configs?.reddit" class="platform-tag">Reddit</span>
|
||||
<span v-if="simulationConfig.twitter_config" class="platform-tag">Twitter</span>
|
||||
<span v-if="simulationConfig.reddit_config" class="platform-tag">Reddit</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- LLM Reasoning -->
|
||||
<div v-if="simulationConfig.generation_reasoning" class="reasoning-section">
|
||||
<span class="reasoning-label">LLM 配置推理</span>
|
||||
<p class="reasoning-text">{{ simulationConfig.generation_reasoning }}</p>
|
||||
<p class="reasoning-text">{{ simulationConfig.generation_reasoning.split('|')[0] }} ...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 04: 准备完成 -->
|
||||
<div class="step-card" :class="{ 'active': phase === 3 }">
|
||||
<!-- Step 04: 初始激活编排 -->
|
||||
<div class="step-card" :class="{ 'active': phase === 3, 'completed': phase > 3 }">
|
||||
<div class="card-header">
|
||||
<div class="step-info">
|
||||
<span class="step-num">04</span>
|
||||
<span class="step-title">初始激活编排</span>
|
||||
</div>
|
||||
<div class="step-status">
|
||||
<span v-if="phase > 3" class="badge success">已完成</span>
|
||||
<span v-else-if="phase === 3" class="badge processing">编排中</span>
|
||||
<span v-else class="badge pending">等待</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p class="api-note">Event Orchestration</p>
|
||||
<p class="description">
|
||||
基于叙事方向,自动生成初始激活事件与热点话题,引导模拟世界的初始状态
|
||||
</p>
|
||||
|
||||
<div v-if="simulationConfig?.event_config" class="orchestration-content">
|
||||
<!-- 叙事方向 -->
|
||||
<div class="narrative-box">
|
||||
<span class="box-label">叙事引导方向</span>
|
||||
<p class="narrative-text">{{ simulationConfig.event_config.narrative_direction }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 热点话题 -->
|
||||
<div class="topics-section">
|
||||
<span class="box-label">初始热点话题</span>
|
||||
<div class="hot-topics-grid">
|
||||
<span v-for="topic in simulationConfig.event_config.hot_topics.slice(0, 8)" :key="topic" class="hot-topic-tag">
|
||||
# {{ topic }}
|
||||
</span>
|
||||
<span v-if="simulationConfig.event_config.hot_topics.length > 8" class="hot-topic-more">
|
||||
+{{ simulationConfig.event_config.hot_topics.length - 8 }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 初始帖子流 -->
|
||||
<div class="initial-posts-section">
|
||||
<span class="box-label">初始激活序列 ({{ simulationConfig.event_config.initial_posts.length }})</span>
|
||||
<div class="posts-timeline">
|
||||
<div v-for="(post, idx) in simulationConfig.event_config.initial_posts" :key="idx" class="timeline-item">
|
||||
<div class="timeline-marker"></div>
|
||||
<div class="timeline-content">
|
||||
<div class="post-header">
|
||||
<span class="post-role">{{ post.poster_type }}</span>
|
||||
<span class="post-id">Agent {{ post.poster_agent_id }}</span>
|
||||
</div>
|
||||
<p class="post-text">{{ post.content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 05: 准备完成 -->
|
||||
<div class="step-card" :class="{ 'active': phase === 4 }">
|
||||
<div class="card-header">
|
||||
<div class="step-info">
|
||||
<span class="step-num">05</span>
|
||||
<span class="step-title">准备完成</span>
|
||||
</div>
|
||||
<div class="step-status">
|
||||
<span v-if="phase >= 3" class="badge processing">进行中</span>
|
||||
<span v-if="phase >= 4" class="badge processing">进行中</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -183,7 +243,7 @@
|
|||
</button>
|
||||
<button
|
||||
class="action-btn primary"
|
||||
:disabled="phase < 3"
|
||||
:disabled="phase < 4"
|
||||
@click="$emit('next-step')"
|
||||
>
|
||||
开始模拟 ➝
|
||||
|
|
@ -300,7 +360,8 @@ import {
|
|||
prepareSimulation,
|
||||
getPrepareStatus,
|
||||
getSimulationProfilesRealtime,
|
||||
getSimulationConfig
|
||||
getSimulationConfig,
|
||||
getSimulationConfigRealtime
|
||||
} from '../api/simulation'
|
||||
|
||||
const props = defineProps({
|
||||
|
|
@ -331,6 +392,11 @@ watch(currentStage, (newStage) => {
|
|||
phase.value = 1
|
||||
} else if (newStage === '生成模拟配置' || newStage === 'generating_config') {
|
||||
phase.value = 2
|
||||
// 进入配置生成阶段,开始轮询配置
|
||||
if (!configTimer) {
|
||||
addLog('开始生成双平台模拟配置...')
|
||||
startConfigPolling()
|
||||
}
|
||||
} else if (newStage === '准备模拟脚本' || newStage === 'copying_scripts') {
|
||||
phase.value = 2 // 仍属于配置阶段
|
||||
}
|
||||
|
|
@ -339,6 +405,7 @@ watch(currentStage, (newStage) => {
|
|||
// Polling timer
|
||||
let pollTimer = null
|
||||
let profilesTimer = null
|
||||
let configTimer = null
|
||||
|
||||
// Computed
|
||||
const displayProfiles = computed(() => {
|
||||
|
|
@ -504,6 +571,48 @@ const fetchProfilesRealtime = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
// 配置轮询
|
||||
const startConfigPolling = () => {
|
||||
configTimer = setInterval(fetchConfigRealtime, 2000)
|
||||
}
|
||||
|
||||
const stopConfigPolling = () => {
|
||||
if (configTimer) {
|
||||
clearInterval(configTimer)
|
||||
configTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
const fetchConfigRealtime = async () => {
|
||||
if (!props.simulationId) return
|
||||
|
||||
try {
|
||||
const res = await getSimulationConfigRealtime(props.simulationId)
|
||||
|
||||
if (res.success && res.data) {
|
||||
const data = res.data
|
||||
|
||||
// 如果配置已生成
|
||||
if (data.config_generated && data.config) {
|
||||
simulationConfig.value = data.config
|
||||
addLog('模拟配置生成完成')
|
||||
|
||||
// 显示配置摘要
|
||||
if (data.summary) {
|
||||
addLog(`配置摘要: ${data.summary.total_agents}个Agent, ${data.summary.simulation_hours}小时, ${data.summary.initial_posts_count}条初始帖子`)
|
||||
}
|
||||
|
||||
stopConfigPolling()
|
||||
phase.value = 4
|
||||
addLog('环境搭建完成,可以开始模拟')
|
||||
emit('update-status', 'completed')
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('获取 Config 失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const loadPreparedData = async () => {
|
||||
phase.value = 2
|
||||
addLog('正在加载配置数据...')
|
||||
|
|
@ -511,15 +620,27 @@ const loadPreparedData = async () => {
|
|||
// 最后获取一次 Profiles
|
||||
await fetchProfilesRealtime()
|
||||
|
||||
// 获取配置
|
||||
// 获取配置(使用实时接口)
|
||||
try {
|
||||
const res = await getSimulationConfig(props.simulationId)
|
||||
const res = await getSimulationConfigRealtime(props.simulationId)
|
||||
if (res.success && res.data) {
|
||||
simulationConfig.value = res.data
|
||||
addLog('模拟配置加载成功')
|
||||
addLog('环境搭建完成,可以开始模拟')
|
||||
phase.value = 3
|
||||
emit('update-status', 'completed')
|
||||
if (res.data.config_generated && res.data.config) {
|
||||
simulationConfig.value = res.data.config
|
||||
addLog('模拟配置加载成功')
|
||||
|
||||
// 显示配置摘要
|
||||
if (res.data.summary) {
|
||||
addLog(`配置摘要: ${res.data.summary.total_agents}个Agent, ${res.data.summary.simulation_hours}小时`)
|
||||
}
|
||||
|
||||
addLog('环境搭建完成,可以开始模拟')
|
||||
phase.value = 4
|
||||
emit('update-status', 'completed')
|
||||
} else {
|
||||
// 配置尚未生成,开始轮询
|
||||
addLog('配置生成中,等待完成...')
|
||||
startConfigPolling()
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载配置失败: ${err.message}`)
|
||||
|
|
@ -548,6 +669,7 @@ onMounted(() => {
|
|||
onUnmounted(() => {
|
||||
stopPolling()
|
||||
stopProfilesPolling()
|
||||
stopConfigPolling()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -1254,5 +1376,124 @@ onUnmounted(() => {
|
|||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
/* Orchestration Content */
|
||||
.orchestration-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.box-label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #999;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.narrative-box {
|
||||
background: #F9F9F9;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #FF5722;
|
||||
}
|
||||
|
||||
.narrative-text {
|
||||
font-size: 13px;
|
||||
color: #444;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.topics-section {
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
.hot-topics-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.hot-topic-tag {
|
||||
font-size: 12px;
|
||||
color: #FF5722;
|
||||
background: #FFF3E0;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hot-topic-more {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.initial-posts-section {
|
||||
border-top: 1px solid #EAEAEA;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.posts-timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding-left: 8px;
|
||||
border-left: 2px solid #F0F0F0;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.timeline-marker {
|
||||
position: absolute;
|
||||
left: -5px;
|
||||
top: 6px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #CCC;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #FFF;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
background: #F9F9F9;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #EEE;
|
||||
}
|
||||
|
||||
.post-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.post-role {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.post-id {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.post-text {
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue