This commit is contained in:
Cyrus Auyeung 2026-07-07 23:53:06 +08:00 committed by GitHub
commit 86afdbba77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 249 additions and 71 deletions

View File

@ -295,7 +295,7 @@ def build_graph():
}), 500
# 解析请求
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
project_id = data.get('project_id')
logger.debug(f"请求参数: project_id={project_id}")

View File

@ -48,7 +48,7 @@ def generate_report():
}
"""
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id')
if not simulation_id:
@ -223,7 +223,7 @@ def get_generate_status():
}
"""
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
task_id = data.get('task_id')
simulation_id = data.get('simulation_id')
@ -497,7 +497,7 @@ def chat_with_report_agent():
}
"""
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id')
message = data.get('message')
@ -945,7 +945,7 @@ def search_graph_tool():
}
"""
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
graph_id = data.get('graph_id')
query = data.get('query')
@ -991,7 +991,7 @@ def get_graph_statistics_tool():
}
"""
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
graph_id = data.get('graph_id')

View File

@ -192,7 +192,7 @@ def create_simulation():
}
"""
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
project_id = data.get('project_id')
if not project_id:
@ -291,7 +291,7 @@ def _check_simulation_prepared(simulation_id: str) -> tuple:
state_file = os.path.join(simulation_dir, "state.json")
try:
import json
with open(state_file, 'r', encoding='utf-8') as f:
with open(state_file, 'r', encoding='utf-8-sig') as f:
state_data = json.load(f)
status = state_data.get("status", "")
@ -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")
@ -403,7 +404,7 @@ def prepare_simulation():
from ..config import Config
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id')
if not simulation_id:
@ -670,7 +671,7 @@ def get_prepare_status():
from ..models.task import TaskManager
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
task_id = data.get('task_id')
simulation_id = data.get('simulation_id')
@ -1104,7 +1105,7 @@ def get_simulation_profiles_realtime(simulation_id: str):
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:
with open(state_file, 'r', encoding='utf-8-sig') as f:
state_data = json.load(f)
status = state_data.get("status", "")
is_generating = status == "preparing"
@ -1200,7 +1201,7 @@ def get_simulation_config_realtime(simulation_id: str):
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:
with open(state_file, 'r', encoding='utf-8-sig') as f:
state_data = json.load(f)
status = state_data.get("status", "")
is_generating = status == "preparing"
@ -1388,7 +1389,7 @@ def generate_profiles():
}
"""
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
graph_id = data.get('graph_id')
if not graph_id:
@ -1490,7 +1491,7 @@ def start_simulation():
}
"""
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id')
if not simulation_id:
@ -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(
@ -1662,7 +1674,7 @@ def stop_simulation():
}
"""
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id')
if not simulation_id:
@ -2191,7 +2203,7 @@ def interview_agent():
}
"""
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id')
agent_id = data.get('agent_id')
@ -2313,7 +2325,7 @@ def interview_agents_batch():
}
"""
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id')
interviews = data.get('interviews')
@ -2440,7 +2452,7 @@ def interview_all_agents():
}
"""
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id')
prompt = data.get('prompt')
@ -2544,7 +2556,7 @@ def get_interview_history():
}
"""
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id')
platform = data.get('platform') # 不指定则返回两个平台的历史
@ -2606,7 +2618,7 @@ def get_env_status():
}
"""
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id')
@ -2673,7 +2685,7 @@ def close_simulation_env():
}
"""
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
simulation_id = data.get('simulation_id')
timeout = data.get('timeout', 30)

View File

@ -195,7 +195,8 @@ class OasisProfileGenerator:
self.client = OpenAI(
api_key=self.api_key,
base_url=self.base_url
base_url=self.base_url,
default_headers={"User-Agent": "python-requests/2.32.5"}
)
# Zep客户端用于检索丰富上下文
@ -315,6 +316,7 @@ class OasisProfileGenerator:
return results
comprehensive_query = t('progress.zepSearchQuery', name=entity_name)
zep_query = comprehensive_query[:400]
def search_edges():
"""搜索边(事实/关系)- 带重试机制"""
@ -325,7 +327,7 @@ class OasisProfileGenerator:
for attempt in range(max_retries):
try:
return self.zep_client.graph.search(
query=comprehensive_query,
query=zep_query,
graph_id=self.graph_id,
limit=30,
scope="edges",
@ -350,7 +352,7 @@ class OasisProfileGenerator:
for attempt in range(max_retries):
try:
return self.zep_client.graph.search(
query=comprehensive_query,
query=zep_query,
graph_id=self.graph_id,
limit=20,
scope="nodes",

View File

@ -237,7 +237,8 @@ class SimulationConfigGenerator:
self.client = OpenAI(
api_key=self.api_key,
base_url=self.base_url
base_url=self.base_url,
default_headers={"User-Agent": "python-requests/2.32.5"}
)
def generate_config(

View File

@ -278,7 +278,7 @@ class SimulationIPCClient:
return False
try:
with open(status_file, 'r', encoding='utf-8') as f:
with open(status_file, 'r', encoding='utf-8-sig') as f:
status = json.load(f)
return status.get("status") == "alive"
except (json.JSONDecodeError, OSError):

View File

@ -165,7 +165,7 @@ class SimulationManager:
if not os.path.exists(state_file):
return None
with open(state_file, 'r', encoding='utf-8') as f:
with open(state_file, 'r', encoding='utf-8-sig') as f:
data = json.load(f)
state = SimulationState(

View File

@ -12,6 +12,7 @@ import threading
import subprocess
import signal
import atexit
import psutil
from typing import Dict, Any, List, Optional, Union
from dataclasses import dataclass, field
from datetime import datetime
@ -247,7 +248,7 @@ class SimulationRunner:
return None
try:
with open(state_file, 'r', encoding='utf-8') as f:
with open(state_file, 'r', encoding='utf-8-sig') as f:
data = json.load(f)
state = SimulationRunState(
@ -1232,12 +1233,18 @@ class SimulationRunner:
# 更新 run_state.json
state = cls.get_run_state(simulation_id)
keep_completed = bool(
state and (
state.runner_status == RunnerStatus.COMPLETED or
(state.progress_percent >= 100 and state.twitter_completed and state.reddit_completed)
)
)
if state:
state.runner_status = RunnerStatus.STOPPED
state.runner_status = RunnerStatus.COMPLETED if keep_completed else RunnerStatus.STOPPED
state.twitter_running = False
state.reddit_running = False
state.completed_at = datetime.now().isoformat()
state.error = "服务器关闭,模拟被终止"
state.completed_at = state.completed_at or datetime.now().isoformat()
state.error = None if keep_completed else "服务器关闭,模拟被终止"
cls._save_run_state(state)
# 同时更新 state.json将状态设为 stopped
@ -1246,13 +1253,13 @@ class SimulationRunner:
state_file = os.path.join(sim_dir, "state.json")
logger.info(f"尝试更新 state.json: {state_file}")
if os.path.exists(state_file):
with open(state_file, 'r', encoding='utf-8') as f:
with open(state_file, 'r', encoding='utf-8-sig') as f:
state_data = json.load(f)
state_data['status'] = 'stopped'
state_data['status'] = 'completed' if keep_completed else 'stopped'
state_data['updated_at'] = datetime.now().isoformat()
with open(state_file, 'w', encoding='utf-8') as f:
json.dump(state_data, f, indent=2, ensure_ascii=False)
logger.info(f"已更新 state.json 状态为 stopped: {simulation_id}")
logger.info(f"已更新 state.json 状态为 {state_data['status']}: {simulation_id}")
else:
logger.warning(f"state.json 不存在: {state_file}")
except Exception as state_err:
@ -1386,7 +1393,23 @@ class SimulationRunner:
return False
ipc_client = SimulationIPCClient(sim_dir)
return ipc_client.check_env_alive()
if not ipc_client.check_env_alive():
return False
process = cls._processes.get(simulation_id)
if process is not None:
return process.poll() is None
state = cls.get_run_state(simulation_id)
pid = state.process_pid if state else None
if not pid:
return False
try:
proc = psutil.Process(pid)
return proc.is_running() and proc.status() != psutil.STATUS_ZOMBIE
except psutil.Error:
return False
@classmethod
def get_env_status_detail(cls, simulation_id: str) -> Dict[str, Any]:
@ -1413,12 +1436,13 @@ class SimulationRunner:
return default_status
try:
with open(status_file, 'r', encoding='utf-8') as f:
with open(status_file, 'r', encoding='utf-8-sig') as f:
status = json.load(f)
env_alive = cls.check_env_alive(simulation_id)
return {
"status": status.get("status", "stopped"),
"twitter_available": status.get("twitter_available", False),
"reddit_available": status.get("reddit_available", False),
"status": status.get("status", "stopped") if env_alive else "stopped",
"twitter_available": status.get("twitter_available", False) if env_alive else False,
"reddit_available": status.get("reddit_available", False) if env_alive else False,
"timestamp": status.get("timestamp")
}
except (json.JSONDecodeError, OSError):
@ -1459,7 +1483,7 @@ class SimulationRunner:
ipc_client = SimulationIPCClient(sim_dir)
if not ipc_client.check_env_alive():
if not cls.check_env_alive(simulation_id):
raise ValueError(f"模拟环境未运行或已关闭无法执行Interview: {simulation_id}")
logger.info(f"发送Interview命令: simulation_id={simulation_id}, agent_id={agent_id}, platform={platform}")
@ -1521,7 +1545,7 @@ class SimulationRunner:
ipc_client = SimulationIPCClient(sim_dir)
if not ipc_client.check_env_alive():
if not cls.check_env_alive(simulation_id):
raise ValueError(f"模拟环境未运行或已关闭无法执行Interview: {simulation_id}")
logger.info(f"发送批量Interview命令: simulation_id={simulation_id}, count={len(interviews)}, platform={platform}")
@ -1631,7 +1655,7 @@ class SimulationRunner:
ipc_client = SimulationIPCClient(sim_dir)
if not ipc_client.check_env_alive():
if not cls.check_env_alive(simulation_id):
return {
"success": True,
"message": "环境已经关闭"

View File

@ -484,13 +484,14 @@ class ZepToolsService:
SearchResult: 搜索结果
"""
logger.info(t("console.graphSearch", graphId=graph_id, query=query[:50]))
zep_query = query[:400]
# 尝试使用Zep Cloud Search API
try:
search_results = self._call_with_retry(
func=lambda: self.client.graph.search(
graph_id=graph_id,
query=query,
query=zep_query,
limit=limit,
scope=scope,
reranker="cross_encoder"

View File

@ -29,7 +29,8 @@ class LLMClient:
self.client = OpenAI(
api_key=self.api_key,
base_url=self.base_url
base_url=self.base_url,
default_headers={"User-Agent": "python-requests/2.32.5"}
)
def chat(

View File

@ -32,6 +32,7 @@ dependencies = [
# 工具库
"python-dotenv>=1.0.0",
"pydantic>=2.0.0",
"psutil>=5.9.0",
]
[project.optional-dependencies]

View File

@ -33,3 +33,4 @@ python-dotenv>=1.0.0
# 数据验证
pydantic>=2.0.0
psutil>=5.9.0

View File

@ -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 = {
@ -678,8 +678,9 @@ def fetch_new_actions_from_db(
if not os.path.exists(db_path):
return actions, new_last_rowid
conn = None
try:
conn = sqlite3.connect(db_path)
conn = sqlite3.connect(db_path, timeout=5)
cursor = conn.cursor()
# 使用 rowid 来追踪已处理的记录rowid 是 SQLite 的内置自增字段)
@ -739,13 +740,35 @@ def fetch_new_actions_from_db(
'action_args': simplified_args,
})
conn.close()
except Exception as e:
print(f"读取数据库动作失败: {e}")
finally:
if conn is not None:
conn.close()
return actions, new_last_rowid
def get_latest_trace_rowid(db_path: str) -> int:
"""Return the latest processed trace row id for a platform DB."""
if not os.path.exists(db_path):
return 0
conn = None
try:
conn = sqlite3.connect(db_path, timeout=5)
cursor = conn.cursor()
cursor.execute("SELECT COALESCE(MAX(rowid), 0) FROM trace")
rowid = int(cursor.fetchone()[0] or 0)
return rowid
except Exception as e:
print(f"读取最新动作游标失败: {e}")
return 0
finally:
if conn is not None:
conn.close()
def _enrich_action_context(
cursor,
action_type: str,
@ -1034,6 +1057,9 @@ def create_model(config: Dict[str, Any], use_boost: bool = False):
return ModelFactory.create(
model_platform=ModelPlatformType.OPENAI,
model_type=llm_model,
api_key=llm_api_key or None,
url=llm_base_url or None,
default_headers={"User-Agent": "python-requests/2.32.5"},
)
@ -1184,10 +1210,16 @@ async def run_twitter_simulation(
content = post.get("content", "")
try:
agent = result.env.agent_graph.get_agent(agent_id)
initial_actions[agent] = ManualAction(
manual_action = ManualAction(
action_type=ActionType.CREATE_POST,
action_args={"content": content}
)
if agent in initial_actions:
if not isinstance(initial_actions[agent], list):
initial_actions[agent] = [initial_actions[agent]]
initial_actions[agent].append(manual_action)
else:
initial_actions[agent] = manual_action
if action_logger:
action_logger.log_action(
@ -1204,7 +1236,9 @@ async def run_twitter_simulation(
if initial_actions:
await result.env.step(initial_actions)
log_info(f"已发布 {len(initial_actions)} 条初始帖子")
last_rowid = get_latest_trace_rowid(db_path)
posted_count = sum(len(action) if isinstance(action, list) else 1 for action in initial_actions.values())
log_info(f"已发布 {posted_count} 条初始帖子")
# 记录 round 0 结束
if action_logger:
@ -1403,7 +1437,9 @@ async def run_reddit_simulation(
if initial_actions:
await result.env.step(initial_actions)
log_info(f"已发布 {len(initial_actions)} 条初始帖子")
last_rowid = get_latest_trace_rowid(db_path)
posted_count = sum(len(action) if isinstance(action, list) else 1 for action in initial_actions.values())
log_info(f"已发布 {posted_count} 条初始帖子")
# 记录 round 0 结束
if action_logger:

View File

@ -464,6 +464,9 @@ class RedditSimulationRunner:
return ModelFactory.create(
model_platform=ModelPlatformType.OPENAI,
model_type=llm_model,
api_key=llm_api_key or None,
url=llm_base_url or None,
default_headers={"User-Agent": "python-requests/2.32.5"},
)
def _get_active_agents_for_round(
@ -617,7 +620,8 @@ class RedditSimulationRunner:
if initial_actions:
await self.env.step(initial_actions)
print(f" 已发布 {len(initial_actions)} 条初始帖子")
posted_count = sum(len(action) if isinstance(action, list) else 1 for action in initial_actions.values())
print(f" 已发布 {posted_count} 条初始帖子")
# 主模拟循环
print("\n开始模拟循环...")

View File

@ -457,6 +457,9 @@ class TwitterSimulationRunner:
return ModelFactory.create(
model_platform=ModelPlatformType.OPENAI,
model_type=llm_model,
api_key=llm_api_key or None,
url=llm_base_url or None,
default_headers={"User-Agent": "python-requests/2.32.5"},
)
def _get_active_agents_for_round(
@ -615,16 +618,23 @@ class TwitterSimulationRunner:
content = post.get("content", "")
try:
agent = self.env.agent_graph.get_agent(agent_id)
initial_actions[agent] = ManualAction(
manual_action = ManualAction(
action_type=ActionType.CREATE_POST,
action_args={"content": content}
)
if agent in initial_actions:
if not isinstance(initial_actions[agent], list):
initial_actions[agent] = [initial_actions[agent]]
initial_actions[agent].append(manual_action)
else:
initial_actions[agent] = manual_action
except Exception as e:
print(f" 警告: 无法为Agent {agent_id}创建初始帖子: {e}")
if initial_actions:
await self.env.step(initial_actions)
print(f" 已发布 {len(initial_actions)} 条初始帖子")
posted_count = sum(len(action) if isinstance(action, list) else 1 for action in initial_actions.values())
print(f" 已发布 {posted_count} 条初始帖子")
# 主模拟循环
print("\n开始模拟循环...")

View File

@ -995,6 +995,7 @@ dependencies = [
{ name = "flask" },
{ name = "flask-cors" },
{ name = "openai" },
{ name = "psutil" },
{ name = "pydantic" },
{ name = "pymupdf" },
{ name = "python-dotenv" },
@ -1024,6 +1025,7 @@ requires-dist = [
{ name = "flask-cors", specifier = ">=6.0.0" },
{ name = "openai", specifier = ">=1.0.0" },
{ name = "pipreqs", marker = "extra == 'dev'", specifier = ">=0.5.0" },
{ name = "psutil", specifier = ">=5.9.0" },
{ name = "pydantic", specifier = ">=2.0.0" },
{ name = "pymupdf", specifier = ">=1.24.0" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },

View File

@ -106,9 +106,9 @@
<!-- Main Content: Dual Timeline -->
<div class="main-content-area" ref="scrollContainer">
<!-- Timeline Header -->
<div class="timeline-header" v-if="allActions.length > 0">
<div class="timeline-header" v-if="chronologicalActions.length > 0">
<div class="timeline-stats">
<span class="total-count">TOTAL EVENTS: <span class="mono">{{ allActions.length }}</span></span>
<span class="total-count">TOTAL EVENTS: <span class="mono">{{ chronologicalActions.length }}</span></span>
<span class="platform-breakdown">
<span class="breakdown-item twitter">
<svg class="mini-icon" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
@ -189,15 +189,32 @@
</div>
</template>
<!-- LIKE_POST: 点赞帖子 -->
<template v-if="action.action_type === 'LIKE_POST'">
<!-- LIKE_POST / DISLIKE_POST: 点赞/帖子 -->
<template v-if="action.action_type === 'LIKE_POST' || action.action_type === 'DISLIKE_POST'">
<div class="like-info">
<svg class="icon-small filled" viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
<span class="like-label">Liked @{{ action.action_args?.post_author_name || 'User' }}'s post</span>
<span class="like-label">{{ action.action_type === 'DISLIKE_POST' ? 'Disliked' : 'Liked' }} @{{ action.action_args?.post_author_name || 'User' }}'s post</span>
</div>
<div v-if="action.action_args?.post_content" class="liked-content">
"{{ truncateContent(action.action_args.post_content, 120) }}"
</div>
<div v-else-if="action.action_args?.post_id" class="comment-context">
<span>Post #{{ action.action_args.post_id }}</span>
</div>
</template>
<!-- LIKE_COMMENT / DISLIKE_COMMENT: 点赞/踩评论 -->
<template v-if="action.action_type === 'LIKE_COMMENT' || action.action_type === 'DISLIKE_COMMENT'">
<div class="like-info">
<svg class="icon-small filled" viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
<span class="like-label">{{ action.action_type === 'DISLIKE_COMMENT' ? 'Disliked' : 'Liked' }} @{{ action.action_args?.comment_author_name || 'User' }}'s comment</span>
</div>
<div v-if="action.action_args?.comment_content" class="liked-content">
"{{ truncateContent(action.action_args.comment_content, 120) }}"
</div>
<div v-else-if="action.action_args?.comment_id" class="comment-context">
<span>Comment #{{ action.action_args.comment_id }}</span>
</div>
</template>
<!-- CREATE_COMMENT: 发表评论 -->
@ -224,7 +241,7 @@
<template v-if="action.action_type === 'FOLLOW'">
<div class="follow-info">
<svg class="icon-small" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg>
<span class="follow-label">Followed @{{ action.action_args?.target_user || action.action_args?.user_id || 'User' }}</span>
<span class="follow-label">Followed @{{ action.action_args?.target_user_name || action.action_args?.target_user || action.action_args?.user_id || 'User' }}</span>
</div>
</template>
@ -249,7 +266,7 @@
</template>
<!-- 通用回退未知类型或有 content 但未被上述处理 -->
<div v-if="!['CREATE_POST', 'QUOTE_POST', 'REPOST', 'LIKE_POST', 'CREATE_COMMENT', 'SEARCH_POSTS', 'FOLLOW', 'UPVOTE_POST', 'DOWNVOTE_POST', 'DO_NOTHING'].includes(action.action_type) && action.action_args?.content" class="content-text">
<div v-if="!['CREATE_POST', 'QUOTE_POST', 'REPOST', 'LIKE_POST', 'DISLIKE_POST', 'LIKE_COMMENT', 'DISLIKE_COMMENT', 'CREATE_COMMENT', 'SEARCH_POSTS', 'FOLLOW', 'UPVOTE_POST', 'DOWNVOTE_POST', 'DO_NOTHING'].includes(action.action_type) && action.action_args?.content" class="content-text">
{{ action.action_args.content }}
</div>
</div>
@ -262,7 +279,7 @@
</div>
</TransitionGroup>
<div v-if="allActions.length === 0" class="waiting-state">
<div v-if="chronologicalActions.length === 0" class="waiting-state">
<div class="pulse-ring"></div>
<span>Waiting for agent actions...</span>
</div>
@ -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;
}
</style>
</style>