diff --git a/backend/app/api/v1/endpoints/agent_tasks.py b/backend/app/api/v1/endpoints/agent_tasks.py index 66a58a1..79fbb33 100644 --- a/backend/app/api/v1/endpoints/agent_tasks.py +++ b/backend/app/api/v1/endpoints/agent_tasks.py @@ -601,25 +601,13 @@ async def stream_agent_with_thinking( 增强版事件流 (SSE) 支持: - - LLM 思考过程的 Token 级流式输出 + - LLM 思考过程的 Token 级流式输出 (仅运行时) - 工具调用的详细输入/输出 - 节点执行状态 - 发现事件 - 事件类型: - - thinking_start: LLM 开始思考 - - thinking_token: LLM 输出 Token - - thinking_end: LLM 思考结束 - - tool_call_start: 工具调用开始 - - tool_call_end: 工具调用结束 - - node_start: 节点开始 - - node_end: 节点结束 - - finding_new: 新发现 - - finding_verified: 验证通过 - - progress: 进度更新 - - task_complete: 任务完成 - - task_error: 任务错误 - - heartbeat: 心跳 + 优先使用内存中的事件队列 (支持 thinking_token), + 如果任务未在运行,则回退到数据库轮询 (不支持 thinking_token 复盘)。 """ task = await db.get(AgentTask, task_id) if not task: @@ -629,119 +617,156 @@ async def stream_agent_with_thinking( if not project or project.owner_id != current_user.id: raise HTTPException(status_code=403, detail="无权访问此任务") + # 定义 SSE 格式化函数 + def format_sse_event(event_data: Dict[str, Any]) -> str: + """格式化为 SSE 事件""" + event_type = event_data.get("event_type") or event_data.get("type") + + # 统一字段 + if "type" not in event_data: + event_data["type"] = event_type + + return f"event: {event_type}\ndata: {json.dumps(event_data, ensure_ascii=False)}\n\n" + async def enhanced_event_generator(): """生成增强版 SSE 事件流""" - last_sequence = after_sequence - poll_interval = 0.3 # 更短的轮询间隔以支持流式 - heartbeat_interval = 15 # 心跳间隔 - max_idle = 600 # 10 分钟无事件后关闭 - idle_time = 0 - last_heartbeat = 0 + # 1. 检查任务是否在运行中 (内存) + runner = _running_tasks.get(task_id) - # 事件类型过滤 - skip_types = set() - if not include_thinking: - skip_types.update(["thinking_start", "thinking_token", "thinking_end"]) - if not include_tool_calls: - skip_types.update(["tool_call_start", "tool_call_input", "tool_call_output", "tool_call_end"]) - - while True: + if runner: + logger.info(f"Stream {task_id}: Using in-memory event manager") try: - async with async_session_factory() as session: - # 查询新事件 - result = await session.execute( - select(AgentEvent) - .where(AgentEvent.task_id == task_id) - .where(AgentEvent.sequence > last_sequence) - .order_by(AgentEvent.sequence) - .limit(100) - ) - events = result.scalars().all() + # 使用 EventManager 的流式接口 + # 过滤选项 + skip_types = set() + if not include_thinking: + skip_types.update(["thinking_start", "thinking_token", "thinking_end"]) + if not include_tool_calls: + skip_types.update(["tool_call_start", "tool_call_input", "tool_call_output", "tool_call_end"]) + + async for event in runner.event_manager.stream_events(task_id, after_sequence=after_sequence): + event_type = event.get("event_type") - # 获取任务状态 - current_task = await session.get(AgentTask, task_id) - task_status = current_task.status if current_task else None - - if events: - idle_time = 0 - for event in events: - last_sequence = event.sequence + if event_type in skip_types: + continue + + # 🔥 Debug: 记录 thinking_token 事件 + if event_type == "thinking_token": + token = event.get("metadata", {}).get("token", "")[:20] + logger.debug(f"Stream {task_id}: Sending thinking_token: '{token}...'") - # 获取事件类型字符串(event_type 已经是字符串) - event_type = str(event.event_type) - - # 过滤事件 - if event_type in skip_types: - continue - - # 构建事件数据 - data = { - "id": event.id, - "type": event_type, - "phase": str(event.phase) if event.phase else None, - "message": event.message, - "sequence": event.sequence, - "timestamp": event.created_at.isoformat() if event.created_at else None, - } - - # 添加工具调用详情 - if include_tool_calls and event.tool_name: - data["tool"] = { - "name": event.tool_name, - "input": event.tool_input, - "output": event.tool_output, - "duration_ms": event.tool_duration_ms, - } - - # 添加元数据 - if event.event_metadata: - data["metadata"] = event.event_metadata - - # 添加 Token 使用 - if event.tokens_used: - data["tokens_used"] = event.tokens_used - - # 使用标准 SSE 格式 - yield f"event: {event_type}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n" - else: - idle_time += poll_interval - - # 检查任务是否结束 - if task_status: - status_str = str(task_status) - if status_str in ["completed", "failed", "cancelled"]: - end_data = { - "type": "task_end", - "status": status_str, - "message": f"任务{'完成' if status_str == 'completed' else '结束'}", - } - yield f"event: task_end\ndata: {json.dumps(end_data, ensure_ascii=False)}\n\n" - break - - # 发送心跳 - last_heartbeat += poll_interval - if last_heartbeat >= heartbeat_interval: - last_heartbeat = 0 - heartbeat_data = { - "type": "heartbeat", - "timestamp": datetime.now(timezone.utc).isoformat(), - "last_sequence": last_sequence, - } - yield f"event: heartbeat\ndata: {json.dumps(heartbeat_data)}\n\n" - - # 检查空闲超时 - if idle_time >= max_idle: - timeout_data = {"type": "timeout", "message": "连接超时"} - yield f"event: timeout\ndata: {json.dumps(timeout_data)}\n\n" - break - - await asyncio.sleep(poll_interval) - + # 格式化并 yield + yield format_sse_event(event) + + # 🔥 CRITICAL: 为 thinking_token 添加微小延迟 + # 确保事件在不同的 TCP 包中发送,让前端能够逐个处理 + # 没有这个延迟,所有 token 会在一次 read() 中被接收,导致 React 批量更新 + if event_type == "thinking_token": + await asyncio.sleep(0.01) # 10ms 延迟 + except Exception as e: - logger.error(f"Stream error: {e}") - error_data = {"type": "error", "message": str(e)} - yield f"event: error\ndata: {json.dumps(error_data)}\n\n" - break + logger.error(f"In-memory stream error: {e}") + err_data = {"type": "error", "message": str(e)} + yield format_sse_event(err_data) + + else: + logger.info(f"Stream {task_id}: Task not running, falling back to DB polling") + # 2. 回退到数据库轮询 (无法获取 thinking_token) + last_sequence = after_sequence + poll_interval = 2.0 # 完成的任务轮询可以慢一点 + heartbeat_interval = 15 + max_idle = 60 # 1分钟无事件关闭 + idle_time = 0 + last_heartbeat = 0 + + skip_types = set() + if not include_thinking: + skip_types.update(["thinking_start", "thinking_token", "thinking_end"]) + + while True: + try: + async with async_session_factory() as session: + # 查询新事件 + result = await session.execute( + select(AgentEvent) + .where(AgentEvent.task_id == task_id) + .where(AgentEvent.sequence > last_sequence) + .order_by(AgentEvent.sequence) + .limit(100) + ) + events = result.scalars().all() + + # 获取任务状态 + current_task = await session.get(AgentTask, task_id) + task_status = current_task.status if current_task else None + + if events: + idle_time = 0 + for event in events: + last_sequence = event.sequence + event_type = str(event.event_type) + + if event_type in skip_types: + continue + + # 构建数据 + data = { + "id": event.id, + "type": event_type, + "phase": str(event.phase) if event.phase else None, + "message": event.message, + "sequence": event.sequence, + "timestamp": event.created_at.isoformat() if event.created_at else None, + } + + # 添加详情 + if include_tool_calls and event.tool_name: + data["tool"] = { + "name": event.tool_name, + "input": event.tool_input, + "output": event.tool_output, + "duration_ms": event.tool_duration_ms, + } + + if event.event_metadata: + data["metadata"] = event.event_metadata + + if event.tokens_used: + data["tokens_used"] = event.tokens_used + + yield format_sse_event(data) + else: + idle_time += poll_interval + + # 检查是否应该结束 + if task_status: + status_str = str(task_status) + # 如果任务已完成且没有新事件,结束流 + if status_str in ["completed", "failed", "cancelled"]: + end_data = { + "type": "task_end", + "status": status_str, + "message": f"任务已{status_str}" + } + yield format_sse_event(end_data) + break + + # 心跳 + last_heartbeat += poll_interval + if last_heartbeat >= heartbeat_interval: + last_heartbeat = 0 + yield format_sse_event({"type": "heartbeat", "timestamp": datetime.now(timezone.utc).isoformat()}) + + # 超时 + if idle_time >= max_idle: + break + + await asyncio.sleep(poll_interval) + + except Exception as e: + logger.error(f"DB poll stream error: {e}") + yield format_sse_event({"type": "error", "message": str(e)}) + break return StreamingResponse( enhanced_event_generator(), diff --git a/backend/app/services/agent/agents/analysis.py b/backend/app/services/agent/agents/analysis.py index ec9ef14..2e6a2d3 100644 --- a/backend/app/services/agent/agents/analysis.py +++ b/backend/app/services/agent/agents/analysis.py @@ -187,6 +187,10 @@ class AnalysisAgent(BaseAgent): thought_match = re.search(r'Thought:\s*(.*?)(?=Action:|Final Answer:|$)', response, re.DOTALL) if thought_match: step.thought = thought_match.group(1).strip() + elif not re.search(r'Action:|Final Answer:', response): + # 🔥 Fallback: If no markers found, treat the whole response as Thought + if response.strip(): + step.thought = response.strip() # 检查是否是最终答案 final_match = re.search(r'Final Answer:\s*(.*?)$', response, re.DOTALL) @@ -330,7 +334,17 @@ class AnalysisAgent(BaseAgent): break self._total_tokens += tokens_this_round - + + # 🔥 Handle empty LLM response to prevent loops + if not llm_output or not llm_output.strip(): + logger.warning(f"[{self.name}] Empty LLM response in iteration {self._iteration}") + await self.emit_llm_decision("收到空响应", "LLM 返回内容为空,尝试重试通过提示") + self._conversation_history.append({ + "role": "user", + "content": "Received empty response. Please output your Thought and Action.", + }) + continue + # 解析 LLM 响应 step = self._parse_llm_response(llm_output) self._steps.append(step) @@ -406,6 +420,11 @@ class AnalysisAgent(BaseAgent): # 标准化发现 standardized_findings = [] for finding in all_findings: + # 确保 finding 是字典 + if not isinstance(finding, dict): + logger.warning(f"Skipping invalid finding (not a dict): {finding}") + continue + standardized = { "vulnerability_type": finding.get("vulnerability_type", "other"), "severity": finding.get("severity", "medium"), diff --git a/backend/app/services/agent/agents/base.py b/backend/app/services/agent/agents/base.py index ca7b37c..84b0878 100644 --- a/backend/app/services/agent/agents/base.py +++ b/backend/app/services/agent/agents/base.py @@ -409,17 +409,38 @@ class BaseAgent(ABC): """发射事件""" if self.event_emitter: from ..event_manager import AgentEventData + + # 准备 metadata + metadata = kwargs.get("metadata", {}) or {} + if "agent_name" not in metadata: + metadata["agent_name"] = self.name + + # 分离已知字段和未知字段 + known_fields = { + "phase", "tool_name", "tool_input", "tool_output", + "tool_duration_ms", "finding_id", "tokens_used" + } + + event_kwargs = {} + for k, v in kwargs.items(): + if k in known_fields: + event_kwargs[k] = v + elif k != "metadata": + # 将未知字段放入 metadata + metadata[k] = v + await self.event_emitter.emit(AgentEventData( event_type=event_type, message=message, - **kwargs + metadata=metadata, + **event_kwargs )) # ============ LLM 思考相关事件 ============ async def emit_thinking(self, message: str): """发射 LLM 思考事件""" - await self.emit_event("thinking", f"[{self.name}] {message}") + await self.emit_event("thinking", message) async def emit_llm_start(self, iteration: int): """发射 LLM 开始思考事件""" @@ -444,7 +465,7 @@ class BaseAgent(ABC): async def emit_thinking_start(self): """发射开始思考事件(流式输出用)""" - await self.emit_event("thinking_start", f"[{self.name}] 开始思考...") + await self.emit_event("thinking_start", "开始思考...") async def emit_thinking_token(self, token: str, accumulated: str): """发射思考 token 事件(流式输出用)""" @@ -461,7 +482,7 @@ class BaseAgent(ABC): """发射思考结束事件(流式输出用)""" await self.emit_event( "thinking_end", - f"[{self.name}] 思考完成", + "思考完成", metadata={"accumulated": full_response} ) @@ -690,6 +711,9 @@ class BaseAgent(ABC): token = chunk["content"] accumulated = chunk["accumulated"] await self.emit_thinking_token(token, accumulated) + # 🔥 CRITICAL: 让出控制权给事件循环,让 SSE 有机会发送事件 + # 如果不这样做,所有 token 会在循环结束后一起发送 + await asyncio.sleep(0) elif chunk["type"] == "done": accumulated = chunk["content"] diff --git a/backend/app/services/agent/agents/orchestrator.py b/backend/app/services/agent/agents/orchestrator.py index b5ee923..f6c1030 100644 --- a/backend/app/services/agent/agents/orchestrator.py +++ b/backend/app/services/agent/agents/orchestrator.py @@ -421,6 +421,9 @@ class OrchestratorAgent(BaseAgent): ### 发现摘要 """ for i, f in enumerate(findings[:10]): # 最多显示 10 个 + if not isinstance(f, dict): + continue + observation += f""" {i+1}. [{f.get('severity', 'unknown')}] {f.get('title', 'Unknown')} - 类型: {f.get('vulnerability_type', 'unknown')} @@ -452,6 +455,9 @@ class OrchestratorAgent(BaseAgent): type_counts = {} for f in self._all_findings: + if not isinstance(f, dict): + continue + sev = f.get("severity", "low") severity_counts[sev] = severity_counts.get(sev, 0) + 1 @@ -475,7 +481,8 @@ class OrchestratorAgent(BaseAgent): summary += "\n### 详细列表\n" for i, f in enumerate(self._all_findings): - summary += f"{i+1}. [{f.get('severity')}] {f.get('title')} ({f.get('file_path')})\n" + if isinstance(f, dict): + summary += f"{i+1}. [{f.get('severity')}] {f.get('title')} ({f.get('file_path')})\n" return summary @@ -484,8 +491,9 @@ class OrchestratorAgent(BaseAgent): severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0} for f in self._all_findings: - sev = f.get("severity", "low") - severity_counts[sev] = severity_counts.get(sev, 0) + 1 + if isinstance(f, dict): + sev = f.get("severity", "low") + severity_counts[sev] = severity_counts.get(sev, 0) + 1 return { "total_findings": len(self._all_findings), diff --git a/backend/app/services/agent/agents/recon.py b/backend/app/services/agent/agents/recon.py index 5279a99..1850c52 100644 --- a/backend/app/services/agent/agents/recon.py +++ b/backend/app/services/agent/agents/recon.py @@ -157,6 +157,11 @@ class ReconAgent(BaseAgent): thought_match = re.search(r'Thought:\s*(.*?)(?=Action:|Final Answer:|$)', response, re.DOTALL) if thought_match: step.thought = thought_match.group(1).strip() + elif not re.search(r'Action:|Final Answer:', response): + # 🔥 Fallback: If no markers found, treat the whole response as Thought + # This prevents empty steps loops "Decision: Continue Thinking" + if response.strip(): + step.thought = response.strip() # 检查是否是最终答案 final_match = re.search(r'Final Answer:\s*(.*?)$', response, re.DOTALL) @@ -170,6 +175,12 @@ class ReconAgent(BaseAgent): answer_text, default={"raw_answer": answer_text} ) + # 确保 findings 格式正确 + if "initial_findings" in step.final_answer: + step.final_answer["initial_findings"] = [ + f for f in step.final_answer["initial_findings"] + if isinstance(f, dict) + ] return step # 提取 Action @@ -256,6 +267,16 @@ class ReconAgent(BaseAgent): self._total_tokens += tokens_this_round + # 🔥 Handle empty LLM response to prevent loops + if not llm_output or not llm_output.strip(): + logger.warning(f"[{self.name}] Empty LLM response in iteration {self._iteration}") + await self.emit_llm_decision("收到空响应", "LLM 返回内容为空,尝试重试通过提示") + self._conversation_history.append({ + "role": "user", + "content": "Received empty response. Please output your Thought and Action.", + }) + continue + # 解析 LLM 响应 step = self._parse_llm_response(llm_output) self._steps.append(step) diff --git a/backend/app/services/agent/agents/verification.py b/backend/app/services/agent/agents/verification.py index 02360af..919618b 100644 --- a/backend/app/services/agent/agents/verification.py +++ b/backend/app/services/agent/agents/verification.py @@ -169,6 +169,10 @@ class VerificationAgent(BaseAgent): thought_match = re.search(r'Thought:\s*(.*?)(?=Action:|Final Answer:|$)', response, re.DOTALL) if thought_match: step.thought = thought_match.group(1).strip() + elif not re.search(r'Action:|Final Answer:', response): + # 🔥 Fallback: If no markers found, treat the whole response as Thought + if response.strip(): + step.thought = response.strip() # 检查是否是最终答案 final_match = re.search(r'Final Answer:\s*(.*?)$', response, re.DOTALL) @@ -337,7 +341,17 @@ class VerificationAgent(BaseAgent): break self._total_tokens += tokens_this_round - + + # 🔥 Handle empty LLM response to prevent loops + if not llm_output or not llm_output.strip(): + logger.warning(f"[{self.name}] Empty LLM response in iteration {self._iteration}") + await self.emit_llm_decision("收到空响应", "LLM 返回内容为空,尝试重试通过提示") + self._conversation_history.append({ + "role": "user", + "content": "Received empty response. Please output your Thought and Action.", + }) + continue + # 解析 LLM 响应 step = self._parse_llm_response(llm_output) self._steps.append(step) diff --git a/backend/app/services/agent/event_manager.py b/backend/app/services/agent/event_manager.py index fe99e6e..d7f5d85 100644 --- a/backend/app/services/agent/event_manager.py +++ b/backend/app/services/agent/event_manager.py @@ -300,16 +300,19 @@ class EventManager: } # 保存到数据库(跳过高频事件如 thinking_token) - skip_db_events = {"thinking_token", "thinking_start", "thinking_end"} + skip_db_events = {"thinking_token"} if self.db_session_factory and event_type not in skip_db_events: try: await self._save_event_to_db(event_data) except Exception as e: logger.error(f"Failed to save event to database: {e}") - # 推送到队列 + # 推送到队列(非阻塞) if task_id in self._event_queues: - await self._event_queues[task_id].put(event_data) + try: + self._event_queues[task_id].put_nowait(event_data) + except asyncio.QueueFull: + logger.warning(f"Event queue full for task {task_id}, dropping event: {event_type}") # 调用回调 if task_id in self._event_callbacks: @@ -348,9 +351,10 @@ class EventManager: await db.commit() def create_queue(self, task_id: str) -> asyncio.Queue: - """创建事件队列""" + """创建或获取事件队列""" if task_id not in self._event_queues: - self._event_queues[task_id] = asyncio.Queue() + # 🔥 使用较大的队列容量,缓存更多 token 事件 + self._event_queues[task_id] = asyncio.Queue(maxsize=1000) return self._event_queues[task_id] def remove_queue(self, task_id: str): @@ -398,13 +402,43 @@ class EventManager: task_id: str, after_sequence: int = 0, ) -> AsyncGenerator[Dict, None]: - """流式获取事件""" - queue = self.create_queue(task_id) + """流式获取事件 - # 先发送历史事件 - history = await self.get_events(task_id, after_sequence) - for event in history: - yield event + 🔥 重要: 此方法会先排空队列中已缓存的事件(在 SSE 连接前产生的), + 然后继续实时推送新事件。 + """ + # 获取现有队列(由 AgentRunner 在初始化时创建) + queue = self._event_queues.get(task_id) + + if not queue: + # 如果队列不存在,创建一个新的(回退逻辑) + queue = self.create_queue(task_id) + logger.warning(f"Queue not found for task {task_id}, created new one") + + # 🔥 先排空队列中已缓存的事件(这些是在 SSE 连接前产生的) + buffered_count = 0 + while not queue.empty(): + try: + buffered_event = queue.get_nowait() + buffered_count += 1 + yield buffered_event + + # 🔥 为所有缓存事件添加延迟,确保不会一起输出 + event_type = buffered_event.get("event_type") + if event_type == "thinking_token": + await asyncio.sleep(0.015) # 15ms for tokens + else: + await asyncio.sleep(0.005) # 5ms for other events + + # 检查是否是结束事件 + if event_type in ["task_complete", "task_error", "task_cancel"]: + logger.info(f"Task {task_id} already completed, sent {buffered_count} buffered events") + return + except asyncio.QueueEmpty: + break + + if buffered_count > 0: + logger.info(f"Drained {buffered_count} buffered events for task {task_id}") # 然后实时推送新事件 try: @@ -413,6 +447,10 @@ class EventManager: event = await asyncio.wait_for(queue.get(), timeout=30) yield event + # 🔥 为 thinking_token 添加微延迟确保流式效果 + if event.get("event_type") == "thinking_token": + await asyncio.sleep(0.01) # 10ms + # 检查是否是结束事件 if event.get("event_type") in ["task_complete", "task_error", "task_cancel"]: break @@ -421,8 +459,10 @@ class EventManager: # 发送心跳 yield {"event_type": "heartbeat", "timestamp": datetime.now(timezone.utc).isoformat()} - finally: - self.remove_queue(task_id) + except GeneratorExit: + # SSE 连接断开 + logger.debug(f"SSE stream closed for task {task_id}") + # 🔥 不要移除队列,让 AgentRunner 管理队列的生命周期 def create_emitter(self, task_id: str) -> AgentEventEmitter: """创建事件发射器""" diff --git a/backend/app/services/agent/graph/runner.py b/backend/app/services/agent/graph/runner.py index 3299c2e5..563678a 100644 --- a/backend/app/services/agent/graph/runner.py +++ b/backend/app/services/agent/graph/runner.py @@ -72,6 +72,10 @@ class AgentRunner: self.event_manager = EventManager(db_session_factory=async_session_factory) self.event_emitter = AgentEventEmitter(task.id, self.event_manager) + # 🔥 CRITICAL: 立即创建事件队列,确保在 Agent 开始执行前队列就存在 + # 这样即使前端 SSE 连接稍晚,token 事件也不会丢失 + self.event_manager.create_queue(task.id) + # 🔥 LLM 服务 - 使用用户配置(从系统配置页面获取) self.llm_service = LLMService(user_config=self.user_config) @@ -708,6 +712,11 @@ class AgentRunner: for finding in findings: try: + # 确保 finding 是字典 + if not isinstance(finding, dict): + logger.warning(f"Skipping invalid finding (not a dict): {finding}") + continue + db_finding = AgentFinding( id=str(uuid.uuid4()), task_id=self.task.id, diff --git a/backend/data/vector_db/ef6dc788-cc23-4a4d-b1a9-5ce4b32248b8/data_level0.bin b/backend/data/vector_db/ef6dc788-cc23-4a4d-b1a9-5ce4b32248b8/data_level0.bin index c95e62e..e89c95a 100644 Binary files a/backend/data/vector_db/ef6dc788-cc23-4a4d-b1a9-5ce4b32248b8/data_level0.bin and b/backend/data/vector_db/ef6dc788-cc23-4a4d-b1a9-5ce4b32248b8/data_level0.bin differ diff --git a/bandit_results.json b/bandit_results.json new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/hooks/useAgentStream.ts b/frontend/src/hooks/useAgentStream.ts index 3770ad7..b635e18 100644 --- a/frontend/src/hooks/useAgentStream.ts +++ b/frontend/src/hooks/useAgentStream.ts @@ -12,7 +12,7 @@ import { AgentStreamState, } from '../shared/api/agentStream'; -export interface UseAgentStreamOptions extends Omit { +export interface UseAgentStreamOptions extends StreamOptions { autoConnect?: boolean; maxEvents?: number; } @@ -73,6 +73,10 @@ export function useAgentStream( ...callbackOptions } = options; + // 🔥 使用 ref 存储 callback options,避免 connect 函数依赖变化导致重连 + const callbackOptionsRef = useRef(callbackOptions); + callbackOptionsRef.current = callbackOptions; + // 状态 const [events, setEvents] = useState([]); const [thinking, setThinking] = useState(''); @@ -115,8 +119,17 @@ export function useAgentStream( includeThinking, includeToolCalls, afterSequence, - + onEvent: (event) => { + // Pass to custom callback first (important for capturing metadata like agent_name) + callbackOptionsRef.current.onEvent?.(event); + + // 忽略 thinking 事件,防止污染日志列表 (它们会通过 onThinking* 回调单独处理) + if ( + event.type === 'thinking_token' || + event.type === 'thinking_start' || + event.type === 'thinking_end' + ) return; setEvents((prev) => [...prev.slice(-maxEvents + 1), event]); }, @@ -124,20 +137,20 @@ export function useAgentStream( thinkingBufferRef.current = []; setIsThinking(true); setThinking(''); - callbackOptions.onThinkingStart?.(); + callbackOptionsRef.current.onThinkingStart?.(); }, onThinkingToken: (token, accumulated) => { thinkingBufferRef.current.push(token); setThinking(accumulated); - callbackOptions.onThinkingToken?.(token, accumulated); + callbackOptionsRef.current.onThinkingToken?.(token, accumulated); }, onThinkingEnd: (response) => { setIsThinking(false); setThinking(response); thinkingBufferRef.current = []; - callbackOptions.onThinkingEnd?.(response); + callbackOptionsRef.current.onThinkingEnd?.(response); }, onToolStart: (name, input) => { @@ -145,7 +158,7 @@ export function useAgentStream( ...prev, { name, input, status: 'running' as const }, ]); - callbackOptions.onToolStart?.(name, input); + callbackOptionsRef.current.onToolStart?.(name, input); }, onToolEnd: (name, output, durationMs) => { @@ -156,16 +169,16 @@ export function useAgentStream( : tc ) ); - callbackOptions.onToolEnd?.(name, output, durationMs); + callbackOptionsRef.current.onToolEnd?.(name, output, durationMs); }, onNodeStart: (nodeName, phase) => { setCurrentPhase(phase); - callbackOptions.onNodeStart?.(nodeName, phase); + callbackOptionsRef.current.onNodeStart?.(nodeName, phase); }, onNodeEnd: (nodeName, summary) => { - callbackOptions.onNodeEnd?.(nodeName, summary); + callbackOptionsRef.current.onNodeEnd?.(nodeName, summary); }, onProgress: (current, total, message) => { @@ -174,35 +187,35 @@ export function useAgentStream( total, percentage: total > 0 ? Math.round((current / total) * 100) : 0, }); - callbackOptions.onProgress?.(current, total, message); + callbackOptionsRef.current.onProgress?.(current, total, message); }, onFinding: (finding, isVerified) => { setFindings((prev) => [...prev, finding]); - callbackOptions.onFinding?.(finding, isVerified); + callbackOptionsRef.current.onFinding?.(finding, isVerified); }, onComplete: (data) => { setIsComplete(true); setIsConnected(false); - callbackOptions.onComplete?.(data); + callbackOptionsRef.current.onComplete?.(data); }, onError: (err) => { setError(err); setIsComplete(true); setIsConnected(false); - callbackOptions.onError?.(err); + callbackOptionsRef.current.onError?.(err); }, onHeartbeat: () => { - callbackOptions.onHeartbeat?.(); + callbackOptionsRef.current.onHeartbeat?.(); }, }); handlerRef.current.connect(); setIsConnected(true); - }, [taskId, includeThinking, includeToolCalls, afterSequence, maxEvents, callbackOptions]); + }, [taskId, includeThinking, includeToolCalls, afterSequence, maxEvents]); // 🔥 移除 callbackOptions 依赖 // 断开连接 const disconnect = useCallback(() => { @@ -261,7 +274,7 @@ export function useAgentThinking(taskId: string | null) { const { thinking, isThinking, connect, disconnect } = useAgentStream(taskId, { includeToolCalls: false, }); - + return { thinking, isThinking, connect, disconnect }; } @@ -272,9 +285,8 @@ export function useAgentToolCalls(taskId: string | null) { const { toolCalls, connect, disconnect } = useAgentStream(taskId, { includeThinking: false, }); - + return { toolCalls, connect, disconnect }; } export default useAgentStream; - diff --git a/frontend/src/pages/AgentAudit.tsx b/frontend/src/pages/AgentAudit.tsx index 94450c9..8195a77 100644 --- a/frontend/src/pages/AgentAudit.tsx +++ b/frontend/src/pages/AgentAudit.tsx @@ -1,873 +1,512 @@ /** - * Agent 审计页面 - * 机械终端风格的 AI Agent 审计界面 - * 支持 LLM 思考过程和工具调用的实时流式展示 + * Agent Audit Page - Simplified Professional Version */ import { useState, useEffect, useRef, useCallback, useMemo } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { - Terminal, Bot, Shield, AlertTriangle, CheckCircle2, - Loader2, Code, Zap, Activity, ChevronRight, XCircle, - FileCode, Search, Bug, Square, RefreshCw, - ArrowLeft, ExternalLink, Brain, Wrench, - ChevronDown, ChevronUp, Clock, Sparkles + Terminal, Bot, CheckCircle2, Loader2, XCircle, + Bug, Square, ArrowLeft, Brain, Wrench, + ChevronDown, ChevronUp, Clock, Eye, EyeOff, Target } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; -import { ScrollArea } from "@/components/ui/scroll-area"; import { toast } from "sonner"; import { useAgentStream } from "@/hooks/useAgentStream"; import { type AgentTask, - type AgentEvent, type AgentFinding, getAgentTask, - getAgentEvents, getAgentFindings, cancelAgentTask, } from "@/shared/api/agentTasks"; -// 事件类型图标映射 -const eventTypeIcons: Record = { - // LLM 核心事件 - llm_start: , - llm_thought: , - llm_decision: , - llm_complete: , - - // 阶段相关 - phase_start: , - phase_complete: , - thinking: , - - // 工具相关 - tool_call: , - tool_result: , - tool_error: , - - // 发现相关 - finding: , - finding_new: , - finding_verified: , - - // 状态相关 - info: , - warning: , - error: , - progress: , - - // 任务相关 - task_complete: , - task_error: , - task_cancel: , +// ============ Types ============ + +interface LogItem { + id: string; + time: string; + type: 'thinking' | 'tool' | 'phase' | 'finding' | 'info' | 'error'; + title: string; + content?: string; + isStreaming?: boolean; + tool?: { name: string; duration?: number; status?: 'running' | 'completed' | 'failed' }; + severity?: string; + agentName?: string; +} + +// ============ Constants ============ + +const SEVERITY_COLORS: Record = { + critical: "text-red-400 bg-red-950/50", + high: "text-orange-400 bg-orange-950/50", + medium: "text-yellow-400 bg-yellow-950/50", + low: "text-blue-400 bg-blue-950/50", + info: "text-gray-400 bg-gray-900/50", }; -// 事件类型颜色映射 -const eventTypeColors: Record = { - // LLM 核心事件 - llm_start: "text-purple-400 font-semibold", - llm_thought: "text-purple-300 bg-purple-950/30 rounded px-1", - llm_decision: "text-yellow-300 font-semibold", - llm_complete: "text-green-400 font-semibold", - - // 阶段相关 - phase_start: "text-cyan-400 font-bold", - phase_complete: "text-green-400", - thinking: "text-purple-300", - - // 工具相关 - tool_call: "text-yellow-300", - tool_result: "text-green-300", - tool_error: "text-red-400", - - // 发现相关 - finding: "text-orange-300 font-medium", - finding_new: "text-orange-300", - finding_verified: "text-red-300 font-medium", - - // 状态相关 - info: "text-gray-300", - warning: "text-yellow-300", - error: "text-red-400", - progress: "text-cyan-300", - - // 任务相关 - task_complete: "text-green-400 font-bold", - task_error: "text-red-400 font-bold", - task_cancel: "text-yellow-400", +const AGENT_COLORS: Record = { + Orchestrator: "text-purple-400 border-purple-500/30 bg-purple-950/20", + Recon: "text-green-400 border-green-500/30 bg-green-950/20", + Analysis: "text-blue-400 border-blue-500/30 bg-blue-950/20", + Verification: "text-red-400 border-red-500/30 bg-red-950/20", + default: "text-gray-400 border-gray-500/30 bg-gray-950/20", }; -// 严重程度颜色 -const severityColors: Record = { - critical: "bg-red-900/50 border-red-500 text-red-300", - high: "bg-orange-900/50 border-orange-500 text-orange-300", - medium: "bg-yellow-900/50 border-yellow-500 text-yellow-300", - low: "bg-blue-900/50 border-blue-500 text-blue-300", - info: "bg-gray-900/50 border-gray-500 text-gray-300", -}; +// ============ Components ============ -const severityIcons: Record = { - critical: "🔴", - high: "🟠", - medium: "🟡", - low: "🟢", - info: "⚪", -}; +function StatusBadge({ status }: { status: string }) { + const config: Record = { + pending: { bg: "bg-gray-700", icon: }, + running: { bg: "bg-blue-700", icon: }, + completed: { bg: "bg-green-700", icon: }, + failed: { bg: "bg-red-700", icon: }, + cancelled: { bg: "bg-yellow-700", icon: }, + }; + const c = config[status] || config.pending; + return ( + + {c.icon} + {status.toUpperCase()} + + ); +} +function LogEntry({ item, isExpanded, onToggle }: { + item: LogItem; + isExpanded: boolean; + onToggle: () => void; +}) { + const icons: Record = { + thinking: , + tool: , + phase: , + finding: , + info: , + error: , + }; + + const borderColors: Record = { + thinking: "border-l-purple-500", + tool: "border-l-amber-500", + phase: "border-l-cyan-500", + finding: "border-l-red-500", + info: "border-l-gray-600", + error: "border-l-red-600", + }; + + // Thinking content is always shown, others are collapsible + const isThinking = item.type === 'thinking'; + const showContent = isThinking || isExpanded; + const isCollapsible = !isThinking && item.content; + + return ( +
+
+
+ {icons[item.type]} + {item.time} + {!isThinking && {item.title}} + {item.isStreaming && } + {item.tool?.status === 'running' && } + {item.agentName && ( + + {item.agentName} + + )} +
+
+ {item.tool?.duration !== undefined && ( + {item.tool.duration}ms + )} + {item.severity && ( + + {item.severity.charAt(0).toUpperCase()} + + )} + {isCollapsible && ( + isExpanded ? : + )} +
+
+ + {showContent && item.content && ( +
+ {item.content} +
+ )} +
+ ); +} + +// ============ Main Component ============ export default function AgentAuditPage() { const { taskId } = useParams<{ taskId: string }>(); const navigate = useNavigate(); - + const [task, setTask] = useState(null); - const [events, setEvents] = useState([]); - const [findings, setFindings] = useState([]); + const [_findings, setFindings] = useState([]); // Loaded for future use const [isLoading, setIsLoading] = useState(true); - const [isStreaming, setIsStreaming] = useState(false); - const [currentTime, setCurrentTime] = useState(new Date().toLocaleTimeString("zh-CN", { hour12: false })); - const [showThinking, setShowThinking] = useState(true); - const [showToolDetails, setShowToolDetails] = useState(true); - - const eventsEndRef = useRef(null); - const thinkingEndRef = useRef(null); - - // 是否完成 + + const [logs, setLogs] = useState([]); + const [expandedIds, setExpandedIds] = useState>(new Set()); + const [isAutoScroll, setIsAutoScroll] = useState(true); + + const logEndRef = useRef(null); + const logIdCounter = useRef(0); + const currentThinkingId = useRef(null); + const currentAgentName = useRef(null); + + const isRunning = task?.status === "running"; const isComplete = task?.status === "completed" || task?.status === "failed" || task?.status === "cancelled"; - - // 加载任务信息 + + // Helper to add log + const addLog = useCallback((item: Omit) => { + const newItem: LogItem = { + ...item, + id: `log-${++logIdCounter.current}`, + time: new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }), + }; + setLogs(prev => [...prev, newItem]); + return newItem.id; + }, []); + + // Load functions const loadTask = useCallback(async () => { if (!taskId) return; - try { - const taskData = await getAgentTask(taskId); - setTask(taskData); - } catch (error: any) { - console.error("Failed to load task:", error); - const errorMessage = error?.response?.data?.detail || error?.message || "未知错误"; - toast.error(`加载任务失败: ${errorMessage}`); - // 如果是 404,可能是任务不存在 - if (error?.response?.status === 404) { - setTimeout(() => navigate("/tasks"), 2000); - } - } - }, [taskId, navigate]); - - // 加载事件 - const loadEvents = useCallback(async () => { - if (!taskId) return; - - try { - const eventsData = await getAgentEvents(taskId, { limit: 500 }); - setEvents(eventsData); - } catch (error) { - console.error("Failed to load events:", error); + const data = await getAgentTask(taskId); + setTask(data); + } catch (err: unknown) { + toast.error("Failed to load task"); } }, [taskId]); - - // 加载发现 + const loadFindings = useCallback(async () => { if (!taskId) return; - try { - const findingsData = await getAgentFindings(taskId); - setFindings(findingsData); - } catch (error) { - console.error("Failed to load findings:", error); + const data = await getAgentFindings(taskId); + setFindings(data); + } catch (err) { + console.error(err); } }, [taskId]); - - // 🔥 稳定化回调函数,避免重复创建 connect/disconnect + + // Stream options - SIMPLIFIED const streamOptions = useMemo(() => ({ includeThinking: true, includeToolCalls: true, - onFinding: () => loadFindings(), + + onEvent: (event: any) => { + if (event.agent_name) { + currentAgentName.current = event.agent_name; + } + }, + + onThinkingStart: () => { + // Ensure previous thinking is finalized + if (currentThinkingId.current) { + setLogs(prev => prev.map(log => + log.id === currentThinkingId.current + ? { ...log, isStreaming: false } + : log + )); + } + currentThinkingId.current = null; + }, + + onThinkingToken: (_token: string, accumulated: string) => { + if (!accumulated || accumulated.trim() === '') return; // Skip empty content + + // User Request: Action and Action Input should not be in Thinking box + // Filter out "Action:" and everything after from the thinking log + const cleanContent = accumulated.replace(/\nAction\s*:[\s\S]*$/, "").trim(); + + if (!cleanContent) return; + + if (!currentThinkingId.current) { + // Create new thinking entry on first non-empty token + const id = addLog({ + type: 'thinking', + title: 'Thinking...', + content: cleanContent, + isStreaming: true, + agentName: currentAgentName.current || undefined, + }); + currentThinkingId.current = id; + } else { + // Update existing entry + setLogs(prev => prev.map(log => + log.id === currentThinkingId.current + ? { ...log, content: cleanContent } + : log + )); + } + }, + + onThinkingEnd: (response: string) => { + const cleanResponse = (response || "").replace(/\nAction\s*:[\s\S]*$/, "").trim(); + + if (!cleanResponse || cleanResponse === '') { + // No content, remove the entry if it exists + if (currentThinkingId.current) { + setLogs(prev => prev.filter(log => log.id !== currentThinkingId.current)); + } + currentThinkingId.current = null; + return; + } + + if (currentThinkingId.current) { + setLogs(prev => prev.map(log => + log.id === currentThinkingId.current + ? { + ...log, + title: cleanResponse.slice(0, 80) + (cleanResponse.length > 80 ? '...' : ''), + content: cleanResponse, + isStreaming: false + } + : log + )); + currentThinkingId.current = null; + } else if (cleanResponse.trim()) { + // No existing entry but we have content - create one + addLog({ + type: 'thinking', + title: cleanResponse.slice(0, 80) + (cleanResponse.length > 80 ? '...' : ''), + content: cleanResponse, + agentName: currentAgentName.current || undefined, + }); + } + }, + + onToolStart: (name: string, input: Record) => { + // Force finalize any pending thinking log when a tool starts + if (currentThinkingId.current) { + setLogs(prev => prev.map(log => + log.id === currentThinkingId.current + ? { ...log, isStreaming: false } + : log + )); + currentThinkingId.current = null; + } + + addLog({ + type: 'tool', + title: `Action: ${name}`, + content: `Input:\n${JSON.stringify(input, null, 2)}`, + tool: { name, status: 'running' }, + agentName: currentAgentName.current || undefined, + }); + }, + + onToolEnd: (name: string, output: unknown, duration: number) => { + // Update the last tool log with duration and output + setLogs(prev => { + // Find last matching tool (reverse search for compatibility) + let idx = -1; + for (let i = prev.length - 1; i >= 0; i--) { + if (prev[i].type === 'tool' && prev[i].tool?.name === name) { + idx = i; + break; + } + } + if (idx >= 0) { + const newLogs = [...prev]; + // Preserve existing input content and append output + const previousContent = newLogs[idx].content || ''; + const outputStr = typeof output === 'string' ? output : JSON.stringify(output, null, 2); + + newLogs[idx] = { + ...newLogs[idx], + title: `Completed: ${name}`, + content: `${previousContent}\n\nOutput:\n${outputStr}`, + tool: { name, duration, status: 'completed' }, + }; + return newLogs; + } + return prev; + }); + }, + + onFinding: (finding: Record) => { + addLog({ + type: 'finding', + title: (finding.title as string) || 'Vulnerability found', + severity: (finding.severity as string) || 'medium', + }); + loadFindings(); + }, + onComplete: () => { + addLog({ type: 'info', title: 'Audit completed' }); loadTask(); loadFindings(); }, + onError: (err: string) => { - console.error("Stream error:", err); + addLog({ type: 'error', title: `Error: ${err}` }); }, - }), [loadFindings, loadTask]); - - // 使用增强版流式 Hook + }), [addLog, loadTask, loadFindings]); + const { - thinking, - isThinking, - toolCalls, - // currentPhase: streamPhase, // 暂未使用 - // progress: streamProgress, // 暂未使用 connect: connectStream, disconnect: disconnectStream, - isConnected: isStreamConnected, + isConnected, } = useAgentStream(taskId || null, streamOptions); - - // 初始化加载 + + // Init useEffect(() => { const init = async () => { setIsLoading(true); - await Promise.all([loadTask(), loadEvents(), loadFindings()]); + await Promise.all([loadTask(), loadFindings()]); setIsLoading(false); }; - init(); - }, [loadTask, loadEvents, loadFindings]); - - // 🔥 使用增强版流式 API(优先) + }, [loadTask, loadFindings]); + + // Connect useEffect(() => { - if (!taskId || isComplete || isLoading) return; - - // 连接流式 API + if (!taskId || isComplete) return; connectStream(); - setIsStreaming(true); - - return () => { - disconnectStream(); - setIsStreaming(false); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [taskId, isComplete, isLoading]); // connectStream/disconnectStream 是稳定的,不需要作为依赖 - - // 🔥 后备:如果流式连接失败,使用轮询获取事件(仅作为后备) + return () => disconnectStream(); + }, [taskId, isComplete, connectStream, disconnectStream]); + + // Auto-scroll useEffect(() => { - if (!taskId || isComplete || isLoading) return; - - // 如果流式连接已建立,不需要轮询 - if (isStreamConnected) { - return; + if (isAutoScroll && logEndRef.current) { + logEndRef.current.scrollIntoView({ behavior: 'smooth' }); } - - // 每 5 秒轮询一次事件(作为后备机制) - const pollInterval = setInterval(async () => { - try { - const lastSequence = events.length > 0 ? Math.max(...events.map(e => e.sequence)) : 0; - const newEvents = await getAgentEvents(taskId, { after_sequence: lastSequence, limit: 50 }); - - if (newEvents.length > 0) { - setEvents(prev => { - // 合并新事件,避免重复 - const existingIds = new Set(prev.map(e => e.id)); - const uniqueNew = newEvents.filter(e => !existingIds.has(e.id)); - return [...prev, ...uniqueNew]; - }); - - // 如果有发现事件,刷新发现列表 - if (newEvents.some(e => e.event_type.startsWith("finding_"))) { - loadFindings(); - } - } - } catch (error) { - console.error("Failed to poll events:", error); - } - }, 5000); - - return () => clearInterval(pollInterval); - }, [taskId, isComplete, isLoading, isStreamConnected, events.length, loadFindings]); - - // 自动滚动 - useEffect(() => { - eventsEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [events]); - - // 更新时间 - useEffect(() => { - const interval = setInterval(() => { - setCurrentTime(new Date().toLocaleTimeString("zh-CN", { hour12: false })); - }, 1000); - - return () => clearInterval(interval); - }, []); - - // 定期轮询任务状态(作为 SSE 的后备机制)- 🔥 增加间隔,避免资源耗尽 - useEffect(() => { - if (!taskId || isComplete || isLoading) return; - - // 🔥 每 10 秒轮询一次(而不是 3 秒),减少资源消耗 - const pollInterval = setInterval(async () => { - try { - const taskData = await getAgentTask(taskId); - setTask(taskData); - - // 如果任务已完成/失败/取消,刷新其他数据并停止轮询 - if (taskData.status === "completed" || taskData.status === "failed" || taskData.status === "cancelled") { - await loadEvents(); - await loadFindings(); - clearInterval(pollInterval); - } - } catch (error) { - console.error("Failed to poll task status:", error); - // 🔥 如果连续失败,停止轮询避免资源耗尽 - clearInterval(pollInterval); - } - }, 10000); // 🔥 改为 10 秒 - - return () => clearInterval(pollInterval); - }, [taskId, isComplete, isLoading]); // 🔥 移除函数依赖 - - // 取消任务 + }, [logs, isAutoScroll]); + const handleCancel = async () => { if (!taskId) return; - - if (!confirm("确定要取消此任务吗?已分析的结果将被保留。")) { - return; - } - try { await cancelAgentTask(taskId); - toast.success("任务已取消"); + toast.success("Task cancelled"); loadTask(); - } catch (error) { - toast.error("取消失败"); + } catch { + toast.error("Failed to cancel"); } }; - - if (isLoading) { + + const toggleExpand = (id: string) => { + setExpandedIds(prev => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }; + + if (isLoading || !task) { return (
-
- -

正在加载...

-
+
); } - - if (!task) { - return ( -
-
- -

任务不存在

- -
-
- ); - } - + return ( -
- {/* 顶部状态栏 */} -
+
+ {/* Header */} +
- - -
-
- -
-
- AGENT_AUDIT - - {task.name || `任务 ${task.id.slice(0, 8)}`} - -
+
+ + Security Audit + {taskId?.slice(0, 8)}
- -
- {/* 阶段指示器 */} - - - {/* 状态徽章 */} - - - {/* 时间 */} - {currentTime} -
-
- - {/* 错误提示 */} - {task.status === "failed" && task.error_message && ( -
-
- -
-

任务执行失败

-

{task.error_message}

-
-
-
- )} - -
- {/* 左侧:执行日志 */} -
- - {/* LLM 思考过程展示区域 */} - {(isThinking || thinking) && showThinking && ( -
-
setShowThinking(!showThinking)} - > -
-
- -
-
- LLM Thinking - Agent 思考过程 -
- {isThinking && ( - - - 思考中... - - )} -
- {showThinking ? : } -
- -
-
- {thinking || "正在思考下一步..."} - {isThinking && } -
-
-
-
- )} - - {/* 工具调用展示区域 */} - {toolCalls.length > 0 && showToolDetails && ( -
-
setShowToolDetails(!showToolDetails)} - > -
-
- -
-
- Tool Calls - 工具调用记录 -
- - {toolCalls.length} 次调用 - -
- {showToolDetails ? : } -
- -
-
- {toolCalls.slice(-5).map((tc, idx) => ( - | undefined - }} - /> - ))} -
-
-
- )} - -
-
-
- - Execution Log -
- 执行日志 - {(isStreaming || isStreamConnected) && ( - - - LIVE - - )} -
- {events.length} 条记录 -
- - {/* 终端窗口 */} -
- {/* CRT 效果 */} -
- - -
- {events.map((event) => ( - - ))} - - {/* 光标 */} - {!isComplete && ( -
- {currentTime} - -
- )} - -
-
- -
- - {/* 底部控制栏 */} -
-
- {/* 进度 */} -
- Progress -
-
-
- {(task.progress_percentage ?? 0).toFixed(0)}% -
- - {/* Token 消耗 */} - {task.total_chunks > 0 && ( -
- Chunks: {task.total_chunks} -
- )} -
- -
- {!isComplete && ( - - )} - - {isComplete && ( - - )} -
-
-
- - {/* 右侧:发现面板 */} -
-
-
-
- - Findings -
- - {findings.length} - -
- - {/* 严重程度统计 */} -
- {task.critical_count > 0 && ( - 🔴 {task.critical_count} - )} - {task.high_count > 0 && ( - 🟠 {task.high_count} - )} - {task.medium_count > 0 && ( - 🟡 {task.medium_count} - )} - {task.low_count > 0 && ( - 🟢 {task.low_count} - )} -
-
- - -
- {findings.length === 0 ? ( -
- -

暂无发现

-
- ) : ( - findings.map((finding) => ( - - )) - )} -
-
- - {/* 评分 */} - {isComplete && ( -
-
- 安全评分 - = 80 ? "text-green-400" : - (task.security_score ?? 0) >= 60 ? "text-yellow-400" : - "text-red-400" - }`}> - {(task.security_score ?? 0).toFixed(0)}/100 - -
-
- 已验证 - {task.verified_count}/{task.findings_count} -
-
- )} -
-
-
- ); -} -// 阶段指示器组件 -function PhaseIndicator({ phase, status }: { phase: string | null; status: string }) { - const phases = ["planning", "indexing", "analysis", "verification", "reporting"]; - const currentIndex = phase ? phases.indexOf(phase) : -1; - const isComplete = status === "completed"; - const isFailed = status === "failed"; - - return ( -
- {phases.map((p, idx) => { - const isActive = p === phase; - const isPast = isComplete || (currentIndex >= 0 && idx < currentIndex); - - return ( -
- ); - })} - {phase && ( - {phase} - )} -
- ); -} - -// 状态徽章组件 -function StatusBadge({ status }: { status: string }) { - const statusConfig: Record = { - pending: { text: "PENDING", className: "bg-gray-800 text-gray-400 border-gray-600" }, - initializing: { text: "INIT", className: "bg-blue-900/50 text-blue-400 border-blue-600 animate-pulse" }, - running: { text: "RUNNING", className: "bg-green-900/50 text-green-400 border-green-600 animate-pulse" }, - planning: { text: "PLANNING", className: "bg-purple-900/50 text-purple-400 border-purple-600 animate-pulse" }, - indexing: { text: "INDEXING", className: "bg-cyan-900/50 text-cyan-400 border-cyan-600 animate-pulse" }, - analyzing: { text: "ANALYZING", className: "bg-yellow-900/50 text-yellow-400 border-yellow-600 animate-pulse" }, - verifying: { text: "VERIFYING", className: "bg-orange-900/50 text-orange-400 border-orange-600 animate-pulse" }, - reporting: { text: "REPORTING", className: "bg-indigo-900/50 text-indigo-400 border-indigo-600 animate-pulse" }, - completed: { text: "COMPLETED", className: "bg-green-900/50 text-green-400 border-green-600" }, - failed: { text: "FAILED", className: "bg-red-900/50 text-red-400 border-red-600" }, - cancelled: { text: "CANCELLED", className: "bg-yellow-900/50 text-yellow-400 border-yellow-600" }, - }; - - const config = statusConfig[status] || statusConfig.pending; - - return ( - - {config.text} - - ); -} - -// 事件行组件 -function EventLine({ event }: { event: AgentEvent }) { - const icon = eventTypeIcons[event.event_type] || ; - const colorClass = eventTypeColors[event.event_type] || "text-gray-400"; - - const timestamp = event.timestamp - ? new Date(event.timestamp).toLocaleTimeString("zh-CN", { hour12: false }) - : ""; - - // 特殊事件处理 - const isLLMThought = event.event_type === "llm_thought"; - const isLLMDecision = event.event_type === "llm_decision"; - const isToolCall = event.event_type === "tool_call"; - const isToolResult = event.event_type === "tool_result"; - - // 背景色 - const bgClass = isLLMThought - ? "bg-purple-950/40 border-l-2 border-purple-600" - : isLLMDecision - ? "bg-yellow-950/30 border-l-2 border-yellow-600" - : isToolCall || isToolResult - ? "bg-gray-900/30" - : ""; - - return ( -
- - {timestamp} - - {icon} - - {event.message} - {event.tool_duration_ms && ( - ({event.tool_duration_ms}ms) - )} - -
- ); -} - -// 工具调用卡片组件 -interface ToolCallProps { - toolCall: { - name: string; - input: Record; - output?: string | Record; - durationMs?: number; - status: 'running' | 'success' | 'error'; - }; -} - -function ToolCallCard({ toolCall }: ToolCallProps) { - const [expanded, setExpanded] = useState(false); - - const statusConfig = { - running: { - icon: , - badge: "bg-yellow-900/30 border-yellow-700 text-yellow-400", - text: "Running", - }, - success: { - icon: , - badge: "bg-green-900/30 border-green-700 text-green-400", - text: "Done", - }, - error: { - icon: , - badge: "bg-red-900/30 border-red-700 text-red-400", - text: "Error", - }, - }; - - const config = statusConfig[toolCall.status]; - - return ( -
-
setExpanded(!expanded)} - > -
- {config.icon} - {toolCall.name} -
-
- {toolCall.durationMs && ( - - - {toolCall.durationMs}ms +
+ {isConnected && ( + + + LIVE )} - - {config.text} - - {expanded ? : } -
-
- - {expanded && ( -
- {/* 输入 */} - {toolCall.input && Object.keys(toolCall.input).length > 0 && ( -
- Input: -
-                {JSON.stringify(toolCall.input, null, 2).slice(0, 500)}
-              
-
- )} - - {/* 输出 */} - {toolCall.output && ( -
- Output: -
-                {typeof toolCall.output === 'string' 
-                  ? toolCall.output.slice(0, 500)
-                  : JSON.stringify(toolCall.output, null, 2).slice(0, 500)
-                }
-              
-
+ + {isRunning && ( + )}
- )} -
- ); -} +
-// 发现卡片组件 -function FindingCard({ finding }: { finding: AgentFinding }) { - const colorClass = severityColors[finding.severity] || severityColors.info; - const icon = severityIcons[finding.severity] || "⚪"; - - return ( -
-
- {icon} -
-

{finding.title}

-

{finding.vulnerability_type}

- {finding.file_path && ( -

- - {finding.file_path}:{finding.line_start} -

+ {/* Main */} +
+ {/* Left: Activity Log */} +
+ {/* Toolbar */} +
+
+ + Activity Log + {logs.length} +
+ +
+ + {/* Logs */} +
+ {logs.length === 0 ? ( +
+ +

Waiting for agent activity...

+
+ ) : ( + logs + .filter(item => { + // Filter out empty/placeholder entries + if (item.title === 'Thinking...' && (!item.content || item.content.trim() === '')) { + return false; + } + return true; + }) + .map(item => ( + toggleExpand(item.id)} + /> + )) + )} +
+
+ + {/* Progress */} + {isRunning && ( +
+
+ Progress + {task.progress_percentage?.toFixed(0) || 0}% +
+
+
+
+
)}
- -
- {finding.is_verified && ( - - - 已验证 - - )} - {finding.has_poc && ( - - - PoC - - )} -
); } - diff --git a/frontend/src/shared/api/agentStream.ts b/frontend/src/shared/api/agentStream.ts index ba92fc3..0ada1da 100644 --- a/frontend/src/shared/api/agentStream.ts +++ b/frontend/src/shared/api/agentStream.ts @@ -10,6 +10,7 @@ // 事件类型定义 export type StreamEventType = // LLM 相关 + | 'thinking' // General thinking event | 'thinking_start' | 'thinking_token' | 'thinking_end' @@ -19,6 +20,8 @@ export type StreamEventType = | 'tool_call_output' | 'tool_call_end' | 'tool_call_error' + | 'tool_call' // Backend sends this + | 'tool_result' // Backend sends this // 节点相关 | 'node_start' | 'node_end' @@ -69,6 +72,12 @@ export interface StreamEventData { error?: string; // task_error findings_count?: number; // task_complete security_score?: number; // task_complete + // Backend tool event fields + tool_name?: string; // tool_call, tool_result + tool_input?: Record; // tool_call + tool_output?: unknown; // tool_result + tool_duration_ms?: number; // tool_result + agent_name?: string; // Extracted from metadata } // 事件回调类型 @@ -191,19 +200,24 @@ export class AgentStreamHandler { } const { done, value } = await this.reader.read(); - + if (done) { break; } buffer += decoder.decode(value, { stream: true }); - + // 解析 SSE 事件 const events = this.parseSSE(buffer); buffer = events.remaining; - + + // 🔥 逐个处理事件,添加微延迟确保 React 能逐个渲染 for (const event of events.parsed) { this.handleEvent(event); + // 为 thinking_token 添加微延迟确保打字效果 + if (event.type === 'thinking_token') { + await new Promise(resolve => setTimeout(resolve, 5)); + } } } @@ -220,7 +234,7 @@ export class AgentStreamHandler { this.isConnected = false; console.error('Stream connection error:', error); - + // 🔥 只有在未断开时才尝试重连 if (!this.isDisconnecting && this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; @@ -253,10 +267,10 @@ export class AgentStreamHandler { const lines = buffer.split('\n'); let remaining = ''; let currentEvent: Partial = {}; - + for (let i = 0; i < lines.length; i++) { const line = lines[i]; - + // 空行表示事件结束 if (line === '') { if (currentEvent.type) { @@ -265,13 +279,13 @@ export class AgentStreamHandler { } continue; } - + // 检查是否是最后一行(可能不完整) if (i === lines.length - 1 && !buffer.endsWith('\n')) { remaining = line; break; } - + // 解析 event: 行 if (line.startsWith('event:')) { currentEvent.type = line.slice(6).trim() as StreamEventType; @@ -286,7 +300,7 @@ export class AgentStreamHandler { } } } - + return { parsed, remaining }; } @@ -294,6 +308,11 @@ export class AgentStreamHandler { * 处理事件 */ private handleEvent(event: StreamEventData): void { + // Extract agent_name from metadata if present + if (event.metadata?.agent_name && !event.agent_name) { + event.agent_name = event.metadata.agent_name as string; + } + // 通用回调 this.options.onEvent?.(event); @@ -304,19 +323,23 @@ export class AgentStreamHandler { this.thinkingBuffer = []; this.options.onThinkingStart?.(); break; - + case 'thinking_token': - if (event.token) { - this.thinkingBuffer.push(event.token); + // 兼容处理:token 可能在顶层,也可能在 metadata 中 + const token = event.token || (event.metadata?.token as string); + const accumulated = event.accumulated || (event.metadata?.accumulated as string); + + if (token) { + this.thinkingBuffer.push(token); this.options.onThinkingToken?.( - event.token, - event.accumulated || this.thinkingBuffer.join('') + token, + accumulated || this.thinkingBuffer.join('') ); } break; - + case 'thinking_end': - const fullResponse = event.accumulated || this.thinkingBuffer.join(''); + const fullResponse = event.accumulated || (event.metadata?.accumulated as string) || this.thinkingBuffer.join(''); this.thinkingBuffer = []; this.options.onThinkingEnd?.(fullResponse); break; @@ -330,7 +353,7 @@ export class AgentStreamHandler { ); } break; - + case 'tool_call_end': if (event.tool) { this.options.onToolEnd?.( @@ -341,6 +364,22 @@ export class AgentStreamHandler { } break; + // Alternative event names (backend sends these) + case 'tool_call': + this.options.onToolStart?.( + event.tool_name || 'unknown', + event.tool_input || {} + ); + break; + + case 'tool_result': + this.options.onToolEnd?.( + event.tool_name || 'unknown', + event.tool_output, + event.tool_duration_ms || 0 + ); + break; + // 节点 case 'node_start': this.options.onNodeStart?.( @@ -348,7 +387,7 @@ export class AgentStreamHandler { event.phase || '' ); break; - + case 'node_end': this.options.onNodeEnd?.( event.metadata?.node as string || 'unknown', @@ -407,13 +446,13 @@ export class AgentStreamHandler { // 🔥 标记正在断开,防止重连 this.isDisconnecting = true; this.isConnected = false; - + // 🔥 取消 fetch 请求 if (this.abortController) { this.abortController.abort(); this.abortController = null; } - + // 🔥 清理 reader if (this.reader) { try { @@ -424,13 +463,13 @@ export class AgentStreamHandler { } this.reader = null; } - + // 清理 EventSource(如果使用) if (this.eventSource) { this.eventSource.close(); this.eventSource = null; } - + // 重置重连计数 this.reconnectAttempts = 0; } @@ -469,6 +508,7 @@ export interface AgentStreamState { events: StreamEventData[]; thinking: string; isThinking: boolean; + thinkingAgent?: string; // Who is thinking toolCalls: Array<{ name: string; input: Record; @@ -494,6 +534,7 @@ export function createAgentStreamWithState( events: [], thinking: '', isThinking: false, + thinkingAgent: undefined, toolCalls: [], currentPhase: '', progress: { current: 0, total: 100, percentage: 0 }, @@ -509,9 +550,16 @@ export function createAgentStreamWithState( return new AgentStreamHandler(taskId, { onEvent: (event) => { - updateState({ - events: [...state.events, event].slice(-500), // 保留最近 500 条 - }); + const updates: Partial = { + events: [...state.events, event].slice(-500), + }; + + // Update thinking agent if available + if (event.agent_name && (event.type === 'thinking' || event.type === 'thinking_start' || event.type === 'thinking_token')) { + updates.thinkingAgent = event.agent_name; + } + + updateState(updates); }, onThinkingStart: () => { updateState({ isThinking: true, thinking: '' }); diff --git a/semgrep_results.json b/semgrep_results.json new file mode 100644 index 0000000..60eb658 --- /dev/null +++ b/semgrep_results.json @@ -0,0 +1 @@ +{"version":"1.145.0","results":[{"check_id":"dockerfile.security.missing-user.missing-user","path":"/Users/lintsinghua/XCodeReviewer/backend/Dockerfile","start":{"line":57,"col":1,"offset":1424},"end":{"line":57,"col":71,"offset":1494},"extra":{"message":"By not specifying a USER, a program in the container may run as 'root'. This is a security hazard. If an attacker can control a process running as root, they may have control over the container. Ensure that the last USER in a Dockerfile is a USER other than 'root'.","fix":"USER non-root\nCMD [\"uvicorn\", \"app.main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\"]","metadata":{"cwe":["CWE-250: Execution with Unnecessary Privileges"],"category":"security","technology":["dockerfile"],"confidence":"MEDIUM","owasp":["A04:2021 - Insecure Design"],"references":["https://owasp.org/Top10/A04_2021-Insecure_Design"],"subcategory":["audit"],"likelihood":"LOW","impact":"MEDIUM","license":"Semgrep Rules License v1.0. For more details, visit semgrep.dev/legal/rules-license","vulnerability_class":["Improper Authorization"],"source":"https://semgrep.dev/r/dockerfile.security.missing-user.missing-user","shortlink":"https://sg.run/Gbvn"},"severity":"ERROR","fingerprint":"requires login","lines":"requires login","validation_state":"NO_VALIDATOR","engine_kind":"OSS"}},{"check_id":"python.sqlalchemy.performance.performance-improvements.len-all-count","path":"/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/database.py","start":{"line":624,"col":34,"offset":23582},"end":{"line":626,"col":36,"offset":23719},"extra":{"message":"Using QUERY.count() instead of len(QUERY.all()) sends less data to the client since the SQLAlchemy method is performed server-side.","metadata":{"category":"performance","technology":["sqlalchemy"],"license":"Semgrep Rules License v1.0. For more details, visit semgrep.dev/legal/rules-license","source":"https://semgrep.dev/r/python.sqlalchemy.performance.performance-improvements.len-all-count","shortlink":"https://sg.run/4y8g"},"severity":"WARNING","fingerprint":"requires login","lines":"requires login","validation_state":"NO_VALIDATOR","engine_kind":"OSS"}},{"check_id":"python.sqlalchemy.performance.performance-improvements.len-all-count","path":"/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/database.py","start":{"line":628,"col":31,"offset":23767},"end":{"line":630,"col":36,"offset":23910},"extra":{"message":"Using QUERY.count() instead of len(QUERY.all()) sends less data to the client since the SQLAlchemy method is performed server-side.","metadata":{"category":"performance","technology":["sqlalchemy"],"license":"Semgrep Rules License v1.0. For more details, visit semgrep.dev/legal/rules-license","source":"https://semgrep.dev/r/python.sqlalchemy.performance.performance-improvements.len-all-count","shortlink":"https://sg.run/4y8g"},"severity":"WARNING","fingerprint":"requires login","lines":"requires login","validation_state":"NO_VALIDATOR","engine_kind":"OSS"}},{"check_id":"python.sqlalchemy.performance.performance-improvements.len-all-count","path":"/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/database.py","start":{"line":632,"col":34,"offset":23961},"end":{"line":634,"col":36,"offset":24113},"extra":{"message":"Using QUERY.count() instead of len(QUERY.all()) sends less data to the client since the SQLAlchemy method is performed server-side.","metadata":{"category":"performance","technology":["sqlalchemy"],"license":"Semgrep Rules License v1.0. For more details, visit semgrep.dev/legal/rules-license","source":"https://semgrep.dev/r/python.sqlalchemy.performance.performance-improvements.len-all-count","shortlink":"https://sg.run/4y8g"},"severity":"WARNING","fingerprint":"requires login","lines":"requires login","validation_state":"NO_VALIDATOR","engine_kind":"OSS"}},{"check_id":"python.fastapi.security.wildcard-cors.wildcard-cors","path":"/Users/lintsinghua/XCodeReviewer/backend/app/main.py","start":{"line":59,"col":19,"offset":1793},"end":{"line":59,"col":24,"offset":1798},"extra":{"message":"CORS policy allows any origin (using wildcard '*'). This is insecure and should be avoided.","metadata":{"cwe":["CWE-942: Permissive Cross-domain Policy with Untrusted Domains"],"owasp":["A05:2021 - Security Misconfiguration"],"category":"security","technology":["python","fastapi"],"references":["https://owasp.org/Top10/A05_2021-Security_Misconfiguration","https://cwe.mitre.org/data/definitions/942.html"],"likelihood":"HIGH","impact":"LOW","confidence":"MEDIUM","vulnerability_class":["Configuration"],"subcategory":["vuln"],"license":"Semgrep Rules License v1.0. For more details, visit semgrep.dev/legal/rules-license","source":"https://semgrep.dev/r/python.fastapi.security.wildcard-cors.wildcard-cors","shortlink":"https://sg.run/KxApY"},"severity":"WARNING","fingerprint":"requires login","lines":"requires login","validation_state":"NO_VALIDATOR","engine_kind":"OSS"}},{"check_id":"python.flask.security.xss.audit.direct-use-of-jinja2.direct-use-of-jinja2","path":"/Users/lintsinghua/XCodeReviewer/backend/app/services/report_generator.py","start":{"line":432,"col":24,"offset":14717},"end":{"line":432,"col":50,"offset":14743},"extra":{"message":"Detected direct use of jinja2. If not done properly, this may bypass HTML escaping which opens up the application to cross-site scripting (XSS) vulnerabilities. Prefer using the Flask method 'render_template()' and templates with a '.html' extension in order to prevent XSS.","metadata":{"cwe":["CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')"],"owasp":["A07:2017 - Cross-Site Scripting (XSS)","A03:2021 - Injection"],"references":["https://jinja.palletsprojects.com/en/2.11.x/api/#basics"],"category":"security","technology":["flask"],"cwe2022-top25":true,"cwe2021-top25":true,"subcategory":["audit"],"likelihood":"LOW","impact":"MEDIUM","confidence":"LOW","license":"Semgrep Rules License v1.0. For more details, visit semgrep.dev/legal/rules-license","vulnerability_class":["Cross-Site-Scripting (XSS)"],"source":"https://semgrep.dev/r/python.flask.security.xss.audit.direct-use-of-jinja2.direct-use-of-jinja2","shortlink":"https://sg.run/RoKe"},"severity":"WARNING","fingerprint":"requires login","lines":"requires login","validation_state":"NO_VALIDATOR","engine_kind":"OSS"}}],"errors":[],"paths":{"scanned":["/Users/lintsinghua/XCodeReviewer/backend/.dockerignore","/Users/lintsinghua/XCodeReviewer/backend/.gitignore","/Users/lintsinghua/XCodeReviewer/backend/.python-version","/Users/lintsinghua/XCodeReviewer/backend/Dockerfile","/Users/lintsinghua/XCodeReviewer/backend/README_UV.md","/Users/lintsinghua/XCodeReviewer/backend/UV_MIGRATION.md","/Users/lintsinghua/XCodeReviewer/backend/alembic/env.py","/Users/lintsinghua/XCodeReviewer/backend/alembic/script.py.mako","/Users/lintsinghua/XCodeReviewer/backend/alembic/versions/001_initial.py","/Users/lintsinghua/XCodeReviewer/backend/alembic/versions/004_add_prompts_and_rules.py","/Users/lintsinghua/XCodeReviewer/backend/alembic/versions/006_add_agent_tables.py","/Users/lintsinghua/XCodeReviewer/backend/alembic/versions/5fc1cc05d5d0_add_missing_user_fields.py","/Users/lintsinghua/XCodeReviewer/backend/alembic/versions/73889a94a455_add_is_active_to_projects.py","/Users/lintsinghua/XCodeReviewer/backend/alembic/versions/add_source_type_to_projects.py","/Users/lintsinghua/XCodeReviewer/backend/alembic.ini","/Users/lintsinghua/XCodeReviewer/backend/app/__init__.py","/Users/lintsinghua/XCodeReviewer/backend/app/api/__init__.py","/Users/lintsinghua/XCodeReviewer/backend/app/api/deps.py","/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/__init__.py","/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/api.py","/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/__init__.py","/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/agent_tasks.py","/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/auth.py","/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/config.py","/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/database.py","/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/embedding_config.py","/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/members.py","/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/projects.py","/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/prompts.py","/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/rules.py","/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/scan.py","/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/tasks.py","/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/users.py","/Users/lintsinghua/XCodeReviewer/backend/app/core/__init__.py","/Users/lintsinghua/XCodeReviewer/backend/app/core/config.py","/Users/lintsinghua/XCodeReviewer/backend/app/core/encryption.py","/Users/lintsinghua/XCodeReviewer/backend/app/core/security.py","/Users/lintsinghua/XCodeReviewer/backend/app/db/__init__.py","/Users/lintsinghua/XCodeReviewer/backend/app/db/base.py","/Users/lintsinghua/XCodeReviewer/backend/app/db/init_db.py","/Users/lintsinghua/XCodeReviewer/backend/app/db/session.py","/Users/lintsinghua/XCodeReviewer/backend/app/main.py","/Users/lintsinghua/XCodeReviewer/backend/app/models/__init__.py","/Users/lintsinghua/XCodeReviewer/backend/app/models/agent_task.py","/Users/lintsinghua/XCodeReviewer/backend/app/models/analysis.py","/Users/lintsinghua/XCodeReviewer/backend/app/models/audit.py","/Users/lintsinghua/XCodeReviewer/backend/app/models/audit_rule.py","/Users/lintsinghua/XCodeReviewer/backend/app/models/project.py","/Users/lintsinghua/XCodeReviewer/backend/app/models/prompt_template.py","/Users/lintsinghua/XCodeReviewer/backend/app/models/user.py","/Users/lintsinghua/XCodeReviewer/backend/app/models/user_config.py","/Users/lintsinghua/XCodeReviewer/backend/app/schemas/__init__.py","/Users/lintsinghua/XCodeReviewer/backend/app/schemas/audit_rule.py","/Users/lintsinghua/XCodeReviewer/backend/app/schemas/prompt_template.py","/Users/lintsinghua/XCodeReviewer/backend/app/schemas/token.py","/Users/lintsinghua/XCodeReviewer/backend/app/schemas/user.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/__init__.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/agents/__init__.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/agents/analysis.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/agents/analysis_v2.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/agents/base.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/agents/orchestrator.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/agents/react_agent.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/agents/recon.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/agents/verification.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/event_manager.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/graph/__init__.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/graph/audit_graph.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/graph/nodes.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/graph/runner.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/json_parser.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/prompts/__init__.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/prompts/system_prompts.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/streaming/__init__.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/streaming/stream_handler.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/streaming/token_streamer.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/streaming/tool_stream.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/__init__.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/base.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/code_analysis_tool.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/external_tools.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/file_tool.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/pattern_tool.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/rag_tool.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/sandbox_tool.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/init_templates.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/llm/__init__.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/llm/adapters/__init__.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/llm/adapters/baidu_adapter.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/llm/adapters/doubao_adapter.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/llm/adapters/litellm_adapter.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/llm/adapters/minimax_adapter.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/llm/base_adapter.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/llm/factory.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/llm/service.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/llm/types.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/rag/__init__.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/rag/embeddings.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/rag/indexer.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/rag/retriever.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/rag/splitter.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/report_generator.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/scanner.py","/Users/lintsinghua/XCodeReviewer/backend/app/services/zip_storage.py","/Users/lintsinghua/XCodeReviewer/backend/data/vector_db/ef6dc788-cc23-4a4d-b1a9-5ce4b32248b8/data_level0.bin","/Users/lintsinghua/XCodeReviewer/backend/data/vector_db/ef6dc788-cc23-4a4d-b1a9-5ce4b32248b8/header.bin","/Users/lintsinghua/XCodeReviewer/backend/data/vector_db/ef6dc788-cc23-4a4d-b1a9-5ce4b32248b8/length.bin","/Users/lintsinghua/XCodeReviewer/backend/data/vector_db/ef6dc788-cc23-4a4d-b1a9-5ce4b32248b8/link_lists.bin","/Users/lintsinghua/XCodeReviewer/backend/env.example","/Users/lintsinghua/XCodeReviewer/backend/main.py","/Users/lintsinghua/XCodeReviewer/backend/pyproject.toml","/Users/lintsinghua/XCodeReviewer/backend/requirements-lock.txt","/Users/lintsinghua/XCodeReviewer/backend/requirements.txt","/Users/lintsinghua/XCodeReviewer/backend/start.sh","/Users/lintsinghua/XCodeReviewer/backend/static/images/logo_nobg.png","/Users/lintsinghua/XCodeReviewer/backend/test_logo.py","/Users/lintsinghua/XCodeReviewer/backend/uploads/.gitkeep","/Users/lintsinghua/XCodeReviewer/backend/uv.lock"]},"time":{"rules":[],"rules_parse_time":1.2000598907470703,"profiling_times":{"config_time":3.0274291038513184,"core_time":37.23275899887085,"ignores_time":0.0010230541229248047,"total_time":40.26207113265991},"parsing_time":{"total_time":0.0,"per_file_time":{"mean":0.0,"std_dev":0.0},"very_slow_stats":{"time_ratio":0.0,"count_ratio":0.0},"very_slow_files":[]},"scanning_time":{"total_time":234.07624554634094,"per_file_time":{"mean":0.6966554926974439,"std_dev":4.675806630950063},"very_slow_stats":{"time_ratio":0.8731978438340042,"count_ratio":0.10416666666666667},"very_slow_files":[{"fpath":"/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/scan.py","ftime":7.5774359703063965},{"fpath":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/external_tools.py","ftime":8.510899066925049},{"fpath":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/agents/analysis.py","ftime":9.324252128601074},{"fpath":"/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/agent_tasks.py","ftime":10.199949026107788},{"fpath":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/pattern_tool.py","ftime":10.646106958389282},{"fpath":"/Users/lintsinghua/XCodeReviewer/backend/app/services/init_templates.py","ftime":11.258774042129517},{"fpath":"/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/prompts.py","ftime":11.770168781280518},{"fpath":"/Users/lintsinghua/XCodeReviewer/backend/app/services/rag/splitter.py","ftime":11.997308015823364},{"fpath":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/agents/react_agent.py","ftime":12.751168012619019},{"fpath":"/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/projects.py","ftime":16.807862043380737}]},"matching_time":{"total_time":0.0,"per_file_and_rule_time":{"mean":0.0,"std_dev":0.0},"very_slow_stats":{"time_ratio":0.0,"count_ratio":0.0},"very_slow_rules_on_files":[]},"tainting_time":{"total_time":0.0,"per_def_and_rule_time":{"mean":0.0,"std_dev":0.0},"very_slow_stats":{"time_ratio":0.0,"count_ratio":0.0},"very_slow_rules_on_defs":[]},"fixpoint_timeouts":[{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/alembic/versions/006_add_agent_tables.py:19:4 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/alembic/versions/006_add_agent_tables.py","start":{"line":19,"col":5,"offset":370},"end":{"line":19,"col":12,"offset":377}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/agent_tasks.py:203:10 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/agent_tasks.py","start":{"line":203,"col":11,"offset":5475},"end":{"line":203,"col":30,"offset":5494}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/database.py:202:10 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/database.py","start":{"line":202,"col":11,"offset":7486},"end":{"line":202,"col":26,"offset":7501}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/database.py:37:10 [rules: 1, first: python.flask.security.injection.tainted-url-host.tainted-url-host]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/database.py","start":{"line":37,"col":11,"offset":975},"end":{"line":37,"col":26,"offset":990}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/database.py:488:10 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/database.py","start":{"line":488,"col":11,"offset":18788},"end":{"line":488,"col":29,"offset":18806}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/scan.py:47:10 [rules: 2, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/api/v1/endpoints/scan.py","start":{"line":47,"col":11,"offset":1499},"end":{"line":47,"col":27,"offset":1515}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/db/init_db.py:51:10 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/db/init_db.py","start":{"line":51,"col":11,"offset":1548},"end":{"line":51,"col":27,"offset":1564}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/services/agent/agents/analysis.py:236:14 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/agents/analysis.py","start":{"line":236,"col":15,"offset":7437},"end":{"line":236,"col":18,"offset":7440}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/services/agent/agents/orchestrator.py:144:14 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/agents/orchestrator.py","start":{"line":144,"col":15,"offset":4111},"end":{"line":144,"col":18,"offset":4114}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/services/agent/agents/react_agent.py:253:14 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/agents/react_agent.py","start":{"line":253,"col":15,"offset":8497},"end":{"line":253,"col":18,"offset":8500}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/services/agent/agents/recon.py:207:14 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/agents/recon.py","start":{"line":207,"col":15,"offset":6233},"end":{"line":207,"col":18,"offset":6236}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/services/agent/agents/verification.py:216:14 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/agents/verification.py","start":{"line":216,"col":15,"offset":6905},"end":{"line":216,"col":18,"offset":6908}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/services/agent/graph/audit_graph.py:580:14 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/graph/audit_graph.py","start":{"line":580,"col":15,"offset":18800},"end":{"line":580,"col":18,"offset":18803}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/services/agent/graph/nodes.py:139:14 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/graph/nodes.py","start":{"line":139,"col":15,"offset":5316},"end":{"line":139,"col":23,"offset":5324}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/services/agent/graph/nodes.py:277:14 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/graph/nodes.py","start":{"line":277,"col":15,"offset":11241},"end":{"line":277,"col":23,"offset":11249}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/services/agent/json_parser.py:150:8 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/json_parser.py","start":{"line":150,"col":9,"offset":4774},"end":{"line":150,"col":14,"offset":4779}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/code_analysis_tool.py:342:14 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/code_analysis_tool.py","start":{"line":342,"col":15,"offset":11823},"end":{"line":342,"col":23,"offset":11831}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/code_analysis_tool.py:72:14 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/code_analysis_tool.py","start":{"line":72,"col":15,"offset":1932},"end":{"line":72,"col":23,"offset":1940}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/external_tools.py:101:14 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/external_tools.py","start":{"line":101,"col":15,"offset":2731},"end":{"line":101,"col":23,"offset":2739}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/external_tools.py:300:14 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/external_tools.py","start":{"line":300,"col":15,"offset":10031},"end":{"line":300,"col":23,"offset":10039}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/external_tools.py:585:14 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/external_tools.py","start":{"line":585,"col":15,"offset":20431},"end":{"line":585,"col":23,"offset":20439}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/external_tools.py:803:14 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/external_tools.py","start":{"line":803,"col":15,"offset":28487},"end":{"line":803,"col":23,"offset":28495}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/file_tool.py:217:14 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/file_tool.py","start":{"line":217,"col":15,"offset":6991},"end":{"line":217,"col":23,"offset":6999}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/pattern_tool.py:38:6 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/services/agent/tools/pattern_tool.py","start":{"line":38,"col":7,"offset":963},"end":{"line":38,"col":23,"offset":979}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/services/rag/splitter.py:482:8 [rules: 1, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/services/rag/splitter.py","start":{"line":482,"col":9,"offset":16235},"end":{"line":482,"col":33,"offset":16259}}},{"error_type":"Fixpoint timeout","severity":"warn","message":"Fixpoint timeout while performing taint analysis at /Users/lintsinghua/XCodeReviewer/backend/app/services/scanner.py:238:10 [rules: 2, first: python.boto3.security.hardcoded-token.hardcoded-token]","location":{"path":"/Users/lintsinghua/XCodeReviewer/backend/app/services/scanner.py","start":{"line":238,"col":11,"offset":8735},"end":{"line":238,"col":25,"offset":8749}}}],"prefiltering":{"project_level_time":0.0,"file_level_time":0.0,"rules_with_project_prefilters_ratio":0.0,"rules_with_file_prefilters_ratio":0.9899620184481823,"rules_selected_ratio":0.0529028757460662,"rules_matched_ratio":0.0529028757460662},"targets":[],"total_bytes":0,"max_memory_bytes":1613084800},"engine_requested":"OSS","skipped_rules":[],"profiling_results":[]}