From 0577ecdae84bad972a7f82ac0e08711f8a3d5b58 Mon Sep 17 00:00:00 2001
From: 666ghj <670939375@qq.com>
Date: Fri, 12 Dec 2025 16:13:08 +0800
Subject: [PATCH] Add new JSON data file and enhance simulation management
features
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Introduced a new JSON data file containing detailed actions and quotes related to the 武大声誉修复基金 initiative.
- Updated the OasisProfileGenerator to ensure compatibility with the new JSON format, emphasizing the inclusion of user_id.
- Modified simulation management to support independent tracking of Twitter and Reddit platforms, including completion status and round information.
- Enhanced the SimulationRunner to accurately reflect the completion state of each platform and added checks for overall simulation completion.
- Improved the GraphPanel and Step3Simulation components to provide real-time updates and better user feedback during simulations.
---
.../app/services/oasis_profile_generator.py | 32 +-
backend/app/services/simulation_manager.py | 3 +-
backend/app/services/simulation_runner.py | 96 +-
frontend/src/components/GraphPanel.vue | 57 +-
frontend/src/components/Step3Simulation.vue | 1490 +++----
frontend/src/views/SimulationRunView.vue | 46 +-
模拟详情接口返回示例.json | 3832 +++++++++++++++++
7 files changed, 4628 insertions(+), 928 deletions(-)
create mode 100644 模拟详情接口返回示例.json
diff --git a/backend/app/services/oasis_profile_generator.py b/backend/app/services/oasis_profile_generator.py
index ac3b7e3..57836c5 100644
--- a/backend/app/services/oasis_profile_generator.py
+++ b/backend/app/services/oasis_profile_generator.py
@@ -1142,27 +1142,31 @@ class OasisProfileGenerator:
"""
保存Reddit Profile为JSON格式
- OASIS Reddit支持两种JSON格式:
- 1. 基础格式: user_id, user_name, name, bio, karma, created_at
- 2. 详细格式: realname, username, bio, persona, age, gender, mbti, country, profession, interested_topics
+ 使用与 to_reddit_format() 一致的格式,确保 OASIS 能正确读取。
+ 必须包含 user_id 字段,这是 OASIS agent_graph.get_agent() 匹配的关键!
- 我们使用详细格式,与用户示例数据(36个简单人设.json)保持一致
-
- OASIS要求所有字段都必须存在:
- - age: 整数
+ 必需字段:
+ - user_id: 用户ID(整数,用于匹配 initial_posts 中的 poster_agent_id)
+ - username: 用户名
+ - name: 显示名称
+ - bio: 简介
+ - persona: 详细人设
+ - age: 年龄(整数)
- gender: "male", "female", 或 "other"
- - mbti: MBTI类型字符串
- - country: 国家字符串
+ - mbti: MBTI类型
+ - country: 国家
"""
data = []
- for profile in profiles:
- # 使用详细格式(与用户示例兼容)
- # 确保所有必需字段都有有效值
+ for idx, profile in enumerate(profiles):
+ # 使用与 to_reddit_format() 一致的格式
item = {
- "realname": profile.name,
+ "user_id": profile.user_id if profile.user_id is not None else idx, # 关键:必须包含 user_id
"username": profile.user_name,
+ "name": profile.name,
"bio": profile.bio[:150] if profile.bio else f"{profile.name}",
"persona": profile.persona or f"{profile.name} is a participant in social discussions.",
+ "karma": profile.karma if profile.karma else 1000,
+ "created_at": profile.created_at,
# OASIS必需字段 - 确保都有默认值
"age": profile.age if profile.age else 30,
"gender": self._normalize_gender(profile.gender),
@@ -1181,7 +1185,7 @@ class OasisProfileGenerator:
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
- logger.info(f"已保存 {len(profiles)} 个Reddit Profile到 {file_path} (JSON详细格式,已标准化gender字段)")
+ logger.info(f"已保存 {len(profiles)} 个Reddit Profile到 {file_path} (JSON格式,包含user_id字段)")
# 保留旧方法名作为别名,保持向后兼容
def save_profiles_to_json(
diff --git a/backend/app/services/simulation_manager.py b/backend/app/services/simulation_manager.py
index 31dfab6..830762b 100644
--- a/backend/app/services/simulation_manager.py
+++ b/backend/app/services/simulation_manager.py
@@ -28,7 +28,8 @@ class SimulationStatus(str, Enum):
READY = "ready"
RUNNING = "running"
PAUSED = "paused"
- COMPLETED = "completed"
+ STOPPED = "stopped" # 模拟被手动停止
+ COMPLETED = "completed" # 模拟自然完成
FAILED = "failed"
diff --git a/backend/app/services/simulation_runner.py b/backend/app/services/simulation_runner.py
index 2985a50..997fbb4 100644
--- a/backend/app/services/simulation_runner.py
+++ b/backend/app/services/simulation_runner.py
@@ -106,12 +106,22 @@ class SimulationRunState:
simulated_hours: int = 0
total_simulation_hours: int = 0
+ # 各平台独立轮次和模拟时间(用于双平台并行显示)
+ twitter_current_round: int = 0
+ reddit_current_round: int = 0
+ twitter_simulated_hours: int = 0
+ reddit_simulated_hours: int = 0
+
# 平台状态
twitter_running: bool = False
reddit_running: bool = False
twitter_actions_count: int = 0
reddit_actions_count: int = 0
+ # 平台完成状态(通过检测 actions.jsonl 中的 simulation_end 事件)
+ twitter_completed: bool = False
+ reddit_completed: bool = False
+
# 每轮摘要
rounds: List[RoundSummary] = field(default_factory=list)
@@ -152,8 +162,15 @@ class SimulationRunState:
"simulated_hours": self.simulated_hours,
"total_simulation_hours": self.total_simulation_hours,
"progress_percent": round(self.current_round / max(self.total_rounds, 1) * 100, 1),
+ # 各平台独立轮次和时间
+ "twitter_current_round": self.twitter_current_round,
+ "reddit_current_round": self.reddit_current_round,
+ "twitter_simulated_hours": self.twitter_simulated_hours,
+ "reddit_simulated_hours": self.reddit_simulated_hours,
"twitter_running": self.twitter_running,
"reddit_running": self.reddit_running,
+ "twitter_completed": self.twitter_completed,
+ "reddit_completed": self.reddit_completed,
"twitter_actions_count": self.twitter_actions_count,
"reddit_actions_count": self.reddit_actions_count,
"total_actions_count": self.twitter_actions_count + self.reddit_actions_count,
@@ -236,8 +253,15 @@ class SimulationRunner:
total_rounds=data.get("total_rounds", 0),
simulated_hours=data.get("simulated_hours", 0),
total_simulation_hours=data.get("total_simulation_hours", 0),
+ # 各平台独立轮次和时间
+ twitter_current_round=data.get("twitter_current_round", 0),
+ reddit_current_round=data.get("reddit_current_round", 0),
+ twitter_simulated_hours=data.get("twitter_simulated_hours", 0),
+ reddit_simulated_hours=data.get("reddit_simulated_hours", 0),
twitter_running=data.get("twitter_running", False),
reddit_running=data.get("reddit_running", False),
+ twitter_completed=data.get("twitter_completed", False),
+ reddit_completed=data.get("reddit_completed", False),
twitter_actions_count=data.get("twitter_actions_count", 0),
reddit_actions_count=data.get("reddit_actions_count", 0),
started_at=data.get("started_at"),
@@ -575,8 +599,51 @@ class SimulationRunner:
try:
action_data = json.loads(line)
- # 跳过事件类型的条目(如 simulation_start, round_start 等)
+ # 处理事件类型的条目
if "event_type" in action_data:
+ event_type = action_data.get("event_type")
+
+ # 检测 simulation_end 事件,标记平台已完成
+ if event_type == "simulation_end":
+ if platform == "twitter":
+ state.twitter_completed = True
+ state.twitter_running = False
+ logger.info(f"Twitter 模拟已完成: {state.simulation_id}, total_rounds={action_data.get('total_rounds')}, total_actions={action_data.get('total_actions')}")
+ elif platform == "reddit":
+ state.reddit_completed = True
+ state.reddit_running = False
+ logger.info(f"Reddit 模拟已完成: {state.simulation_id}, total_rounds={action_data.get('total_rounds')}, total_actions={action_data.get('total_actions')}")
+
+ # 检查是否所有启用的平台都已完成
+ # 如果只运行了一个平台,只检查那个平台
+ # 如果运行了两个平台,需要两个都完成
+ all_completed = cls._check_all_platforms_completed(state)
+ if all_completed:
+ state.runner_status = RunnerStatus.COMPLETED
+ state.completed_at = datetime.now().isoformat()
+ logger.info(f"所有平台模拟已完成: {state.simulation_id}")
+
+ # 更新轮次信息(从 round_end 事件)
+ elif event_type == "round_end":
+ round_num = action_data.get("round", 0)
+ simulated_hours = action_data.get("simulated_hours", 0)
+
+ # 更新各平台独立的轮次和时间
+ if platform == "twitter":
+ if round_num > state.twitter_current_round:
+ state.twitter_current_round = round_num
+ state.twitter_simulated_hours = simulated_hours
+ elif platform == "reddit":
+ if round_num > state.reddit_current_round:
+ state.reddit_current_round = round_num
+ state.reddit_simulated_hours = simulated_hours
+
+ # 总体轮次取两个平台的最大值
+ if round_num > state.current_round:
+ state.current_round = round_num
+ # 总体时间取两个平台的最大值
+ state.simulated_hours = max(state.twitter_simulated_hours, state.reddit_simulated_hours)
+
continue
action = AgentAction(
@@ -607,6 +674,33 @@ class SimulationRunner:
logger.warning(f"读取动作日志失败: {log_path}, error={e}")
return position
+ @classmethod
+ def _check_all_platforms_completed(cls, state: SimulationRunState) -> bool:
+ """
+ 检查所有启用的平台是否都已完成模拟
+
+ 通过检查对应的 actions.jsonl 文件是否存在来判断平台是否被启用
+
+ Returns:
+ True 如果所有启用的平台都已完成
+ """
+ sim_dir = os.path.join(cls.RUN_STATE_DIR, state.simulation_id)
+ twitter_log = os.path.join(sim_dir, "twitter", "actions.jsonl")
+ reddit_log = os.path.join(sim_dir, "reddit", "actions.jsonl")
+
+ # 检查哪些平台被启用(通过文件是否存在判断)
+ twitter_enabled = os.path.exists(twitter_log)
+ reddit_enabled = os.path.exists(reddit_log)
+
+ # 如果平台被启用但未完成,则返回 False
+ if twitter_enabled and not state.twitter_completed:
+ return False
+ if reddit_enabled and not state.reddit_completed:
+ return False
+
+ # 至少有一个平台被启用且已完成
+ return twitter_enabled or reddit_enabled
+
@classmethod
def stop_simulation(cls, simulation_id: str) -> SimulationRunState:
"""停止模拟"""
diff --git a/frontend/src/components/GraphPanel.vue b/frontend/src/components/GraphPanel.vue
index e42b85b..95ea121 100644
--- a/frontend/src/components/GraphPanel.vue
+++ b/frontend/src/components/GraphPanel.vue
@@ -19,10 +19,15 @@
-
-
-
- 实时更新中...
+
+
+
+ {{ isSimulating ? 'GraphRAG长短期记忆实时更新中' : '实时更新中...' }}
@@ -219,7 +224,8 @@ import * as d3 from 'd3'
const props = defineProps({
graphData: Object,
loading: Boolean,
- currentPhase: Number
+ currentPhase: Number,
+ isSimulating: Boolean
})
const emit = defineEmits(['refresh', 'toggle-maximize'])
@@ -1153,30 +1159,41 @@ input:checked + .slider:before {
/* Building hint */
.graph-building-hint {
position: absolute;
- bottom: 80px;
+ bottom: 160px; /* Moved up from 80px */
left: 50%;
transform: translateX(-50%);
- background: rgba(0,0,0,0.75);
+ background: rgba(0, 0, 0, 0.65);
+ backdrop-filter: blur(8px);
color: #fff;
- padding: 8px 16px;
- border-radius: 20px;
- font-size: 12px;
+ padding: 10px 20px;
+ border-radius: 30px;
+ font-size: 13px;
display: flex;
align-items: center;
- gap: 8px;
+ gap: 10px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ font-weight: 500;
+ letter-spacing: 0.5px;
+ z-index: 100;
}
-.building-dot {
- width: 8px;
- height: 8px;
- background: #4CAF50;
- border-radius: 50%;
- animation: pulse 1.5s ease-in-out infinite;
+.memory-icon-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ animation: breathe 2s ease-in-out infinite;
}
-@keyframes pulse {
- 0%, 100% { opacity: 1; transform: scale(1); }
- 50% { opacity: 0.5; transform: scale(0.8); }
+.memory-icon {
+ width: 18px;
+ height: 18px;
+ color: #4CAF50;
+}
+
+@keyframes breathe {
+ 0%, 100% { opacity: 0.7; transform: scale(1); filter: drop-shadow(0 0 2px rgba(76, 175, 80, 0.3)); }
+ 50% { opacity: 1; transform: scale(1.15); filter: drop-shadow(0 0 8px rgba(76, 175, 80, 0.6)); }
}
/* Loading spinner */
diff --git a/frontend/src/components/Step3Simulation.vue b/frontend/src/components/Step3Simulation.vue
index 4020809..b9ffc78 100644
--- a/frontend/src/components/Step3Simulation.vue
+++ b/frontend/src/components/Step3Simulation.vue
@@ -1,292 +1,184 @@