From 70776ee5fd66263d0a55e91edbff530ac6ea5cb3 Mon Sep 17 00:00:00 2001 From: lintsinghua Date: Thu, 11 Dec 2025 23:29:04 +0800 Subject: [PATCH] feat: Introduce structured agent collaboration with `TaskHandoff` and `analysis_v2` agent, updating core agent logic, tools, and audit UI. --- backend/app/api/v1/endpoints/agent_tasks.py | 124 ++++- backend/app/services/agent/agents/__init__.py | 7 +- backend/app/services/agent/agents/analysis.py | 187 ++++---- .../app/services/agent/agents/analysis_v2.py | 0 backend/app/services/agent/agents/base.py | 445 ++++++++++++++++-- .../app/services/agent/agents/orchestrator.py | 34 +- .../app/services/agent/agents/react_agent.py | 35 +- backend/app/services/agent/agents/recon.py | 139 +++--- .../app/services/agent/agents/verification.py | 175 ++++--- backend/app/services/agent/event_manager.py | 5 +- .../app/services/agent/graph/audit_graph.py | 13 + backend/app/services/agent/graph/nodes.py | 168 ++++++- backend/app/services/agent/graph/runner.py | 339 ++++++------- backend/app/services/agent/json_parser.py | 251 ++++++++++ .../agent/tools/code_analysis_tool.py | 20 +- .../services/agent/tools/external_tools.py | 42 +- .../app/services/agent/tools/pattern_tool.py | 11 + backend/app/services/agent/tools/rag_tool.py | 25 +- .../services/llm/adapters/litellm_adapter.py | 79 ++++ backend/app/services/llm/service.py | 105 ++++- .../data_level0.bin | Bin 0 -> 628400 bytes .../header.bin | Bin 0 -> 100 bytes .../length.bin | Bin 0 -> 400 bytes .../link_lists.bin | 0 frontend/src/pages/AgentAudit.tsx | 52 +- 25 files changed, 1657 insertions(+), 599 deletions(-) create mode 100644 backend/app/services/agent/agents/analysis_v2.py create mode 100644 backend/app/services/agent/json_parser.py create mode 100644 backend/data/vector_db/ef6dc788-cc23-4a4d-b1a9-5ce4b32248b8/data_level0.bin create mode 100644 backend/data/vector_db/ef6dc788-cc23-4a4d-b1a9-5ce4b32248b8/header.bin create mode 100644 backend/data/vector_db/ef6dc788-cc23-4a4d-b1a9-5ce4b32248b8/length.bin create mode 100644 backend/data/vector_db/ef6dc788-cc23-4a4d-b1a9-5ce4b32248b8/link_lists.bin diff --git a/backend/app/api/v1/endpoints/agent_tasks.py b/backend/app/api/v1/endpoints/agent_tasks.py index 502d4fa..66a58a1 100644 --- a/backend/app/api/v1/endpoints/agent_tasks.py +++ b/backend/app/api/v1/endpoints/agent_tasks.py @@ -28,6 +28,7 @@ from app.models.agent_task import ( ) from app.models.project import Project from app.models.user import User +from app.models.user_config import UserConfig from app.services.agent import AgentRunner, EventManager, run_agent_task from app.services.agent.streaming import StreamHandler, StreamEvent, StreamEventType @@ -199,7 +200,7 @@ class TaskSummaryResponse(BaseModel): # ============ 后台任务执行 ============ -async def _execute_agent_task(task_id: str, project_root: str): +async def _execute_agent_task(task_id: str): """在后台执行 Agent 任务""" async with async_session_factory() as db: try: @@ -209,14 +210,57 @@ async def _execute_agent_task(task_id: str, project_root: str): logger.error(f"Task {task_id} not found") return + # 获取项目 + project = task.project + if not project: + logger.error(f"Project not found for task {task_id}") + return + + # 🔥 获取项目根目录(解压 ZIP 或克隆仓库) + project_root = await _get_project_root(project, task_id) + + # 🔥 获取用户配置(从系统配置页面) + # 优先级:1. 数据库用户配置 > 2. 环境变量配置 + user_config = None + if task.created_by: + from app.api.v1.endpoints.config import ( + decrypt_config, + SENSITIVE_LLM_FIELDS, SENSITIVE_OTHER_FIELDS + ) + import json + + result = await db.execute( + select(UserConfig).where(UserConfig.user_id == task.created_by) + ) + config = result.scalar_one_or_none() + + if config and config.llm_config: + # 🔥 有数据库配置:使用数据库配置(优先) + user_llm_config = json.loads(config.llm_config) if config.llm_config else {} + user_other_config = json.loads(config.other_config) if config.other_config else {} + + # 解密敏感字段 + user_llm_config = decrypt_config(user_llm_config, SENSITIVE_LLM_FIELDS) + user_other_config = decrypt_config(user_other_config, SENSITIVE_OTHER_FIELDS) + + user_config = { + "llmConfig": user_llm_config, # 直接使用数据库配置,不合并默认值 + "otherConfig": user_other_config, + } + logger.info(f"✅ Using database user config for task {task_id}, LLM provider: {user_llm_config.get('llmProvider', 'N/A')}") + else: + # 🔥 无数据库配置:传递 None,让 LLMService 使用环境变量 + user_config = None + logger.info(f"⚠️ No database config found for user {task.created_by}, will use environment variables for task {task_id}") + # 更新状态为运行中 task.status = AgentTaskStatus.RUNNING task.started_at = datetime.now(timezone.utc) await db.commit() logger.info(f"Task {task_id} started") - # 创建 Runner - runner = AgentRunner(db, task, project_root) + # 创建 Runner(传入用户配置) + runner = AgentRunner(db, task, project_root, user_config=user_config) _running_tasks[task_id] = runner # 执行 @@ -296,11 +340,8 @@ async def create_agent_task( await db.commit() await db.refresh(task) - # 确定项目根目录 - project_root = _get_project_root(project, task.id) - - # 在后台启动任务 - background_tasks.add_task(_execute_agent_task, task.id, project_root) + # 在后台启动任务(项目根目录在任务内部获取) + background_tasks.add_task(_execute_agent_task, task.id) logger.info(f"Created agent task {task.id} for project {project.name}") @@ -897,24 +938,73 @@ async def update_finding_status( # ============ Helper Functions ============ -def _get_project_root(project: Project, task_id: str) -> str: +async def _get_project_root(project: Project, task_id: str) -> str: """ 获取项目根目录 - TODO: 实际实现中需要: - - 对于 ZIP 项目:解压到临时目录 - - 对于 Git 仓库:克隆到临时目录 + 支持两种项目类型: + - ZIP 项目:解压 ZIP 文件到临时目录 + - 仓库项目:克隆仓库到临时目录 """ + import zipfile + import subprocess + base_path = f"/tmp/deepaudit/{task_id}" # 确保目录存在 os.makedirs(base_path, exist_ok=True) - # 如果项目有存储路径,复制过来 - if hasattr(project, 'storage_path') and project.storage_path: - if os.path.exists(project.storage_path): - # 复制项目文件 - shutil.copytree(project.storage_path, base_path, dirs_exist_ok=True) + # 根据项目类型处理 + if project.source_type == "zip": + # 🔥 ZIP 项目:解压 ZIP 文件 + from app.services.zip_storage import load_project_zip + + zip_path = await load_project_zip(project.id) + + if zip_path and os.path.exists(zip_path): + try: + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + zip_ref.extractall(base_path) + logger.info(f"✅ Extracted ZIP project {project.id} to {base_path}") + except Exception as e: + logger.error(f"Failed to extract ZIP {zip_path}: {e}") + else: + logger.warning(f"⚠️ ZIP file not found for project {project.id}") + + elif project.source_type == "repository" and project.repository_url: + # 🔥 仓库项目:克隆仓库 + try: + branch = project.default_branch or "main" + repo_url = project.repository_url + + # 克隆仓库 + result = subprocess.run( + ["git", "clone", "--depth", "1", "--branch", branch, repo_url, base_path], + capture_output=True, + text=True, + timeout=300, + ) + + if result.returncode == 0: + logger.info(f"✅ Cloned repository {repo_url} (branch: {branch}) to {base_path}") + else: + logger.warning(f"Failed to clone branch {branch}, trying default branch: {result.stderr}") + # 如果克隆失败,尝试使用默认分支 + if branch != "main": + result = subprocess.run( + ["git", "clone", "--depth", "1", repo_url, base_path], + capture_output=True, + text=True, + timeout=300, + ) + if result.returncode == 0: + logger.info(f"✅ Cloned repository {repo_url} (default branch) to {base_path}") + else: + logger.error(f"Failed to clone repository: {result.stderr}") + except subprocess.TimeoutExpired: + logger.error(f"Git clone timeout for {project.repository_url}") + except Exception as e: + logger.error(f"Failed to clone repository {project.repository_url}: {e}") return base_path diff --git a/backend/app/services/agent/agents/__init__.py b/backend/app/services/agent/agents/__init__.py index 3009b64..fafec70 100644 --- a/backend/app/services/agent/agents/__init__.py +++ b/backend/app/services/agent/agents/__init__.py @@ -1,9 +1,13 @@ """ 混合 Agent 架构 包含 Orchestrator、Recon、Analysis 和 Verification Agent + +协作机制: +- Agent 之间通过 TaskHandoff 传递结构化上下文 +- 每个 Agent 完成后生成 handoff 给下一个 Agent """ -from .base import BaseAgent, AgentConfig, AgentResult +from .base import BaseAgent, AgentConfig, AgentResult, TaskHandoff from .orchestrator import OrchestratorAgent from .recon import ReconAgent from .analysis import AnalysisAgent @@ -13,6 +17,7 @@ __all__ = [ "BaseAgent", "AgentConfig", "AgentResult", + "TaskHandoff", "OrchestratorAgent", "ReconAgent", "AnalysisAgent", diff --git a/backend/app/services/agent/agents/analysis.py b/backend/app/services/agent/agents/analysis.py index 8b0b86e..ec9ef14 100644 --- a/backend/app/services/agent/agents/analysis.py +++ b/backend/app/services/agent/agents/analysis.py @@ -10,6 +10,7 @@ LLM 是真正的安全分析大脑! 类型: ReAct (真正的!) """ +import asyncio import json import logging import re @@ -17,6 +18,7 @@ from typing import List, Dict, Any, Optional from dataclasses import dataclass from .base import BaseAgent, AgentConfig, AgentResult, AgentType, AgentPattern +from ..json_parser import AgentJsonParser logger = logging.getLogger(__name__) @@ -33,18 +35,13 @@ ANALYSIS_SYSTEM_PROMPT = """你是 DeepAudit 的漏洞分析 Agent,一个**自 ## 你可以使用的工具 -### 外部扫描工具 -- **semgrep_scan**: Semgrep 静态分析(推荐首先使用) - 参数: rules (str), max_results (int) -- **bandit_scan**: Python 安全扫描 - -### RAG 语义搜索 -- **rag_query**: 语义代码搜索 - 参数: query (str), top_k (int) -- **security_search**: 安全相关代码搜索 - 参数: vulnerability_type (str), top_k (int) -- **function_context**: 函数上下文分析 - 参数: function_name (str) +### 文件操作 +- **read_file**: 读取文件内容 + 参数: file_path (str), start_line (int), end_line (int) +- **list_files**: 列出目录文件 + 参数: directory (str), pattern (str) +- **search_code**: 代码关键字搜索 + 参数: keyword (str), max_results (int) ### 深度分析 - **pattern_match**: 危险模式匹配 @@ -53,16 +50,28 @@ ANALYSIS_SYSTEM_PROMPT = """你是 DeepAudit 的漏洞分析 Agent,一个**自 参数: code (str), file_path (str), focus (str) - **dataflow_analysis**: 数据流追踪 参数: source (str), sink (str) -- **vulnerability_validation**: 漏洞验证 - 参数: code (str), vulnerability_type (str) -### 文件操作 -- **read_file**: 读取文件内容 - 参数: file_path (str), start_line (int), end_line (int) -- **search_code**: 代码关键字搜索 - 参数: keyword (str), max_results (int) -- **list_files**: 列出目录文件 - 参数: directory (str), pattern (str) +### 外部静态分析工具 +- **semgrep_scan**: Semgrep 静态分析(推荐首先使用) + 参数: rules (str), max_results (int) +- **bandit_scan**: Python 安全扫描 + 参数: target (str) +- **gitleaks_scan**: Git 密钥泄露扫描 + 参数: target (str) +- **trufflehog_scan**: 敏感信息扫描 + 参数: target (str) +- **npm_audit**: NPM 依赖漏洞扫描 + 参数: target (str) +- **safety_scan**: Python 依赖安全扫描 + 参数: target (str) +- **osv_scan**: OSV 漏洞数据库扫描 + 参数: target (str) + +### RAG 语义搜索 +- **security_search**: 安全相关代码搜索 + 参数: vulnerability_type (str), top_k (int) +- **function_context**: 函数上下文分析 + 参数: function_name (str) ## 工作方式 每一步,你需要输出: @@ -168,15 +177,7 @@ class AnalysisAgent(BaseAgent): self._conversation_history: List[Dict[str, str]] = [] self._steps: List[AnalysisStep] = [] - def _get_tools_description(self) -> str: - """生成工具描述""" - tools_info = [] - for name, tool in self.tools.items(): - if name.startswith("_"): - continue - desc = f"- {name}: {getattr(tool, 'description', 'No description')}" - tools_info.append(desc) - return "\n".join(tools_info) + def _parse_llm_response(self, response: str) -> AnalysisStep: """解析 LLM 响应""" @@ -191,13 +192,20 @@ class AnalysisAgent(BaseAgent): final_match = re.search(r'Final Answer:\s*(.*?)$', response, re.DOTALL) if final_match: step.is_final = True - try: - answer_text = final_match.group(1).strip() - answer_text = re.sub(r'```json\s*', '', answer_text) - answer_text = re.sub(r'```\s*', '', answer_text) - step.final_answer = json.loads(answer_text) - except json.JSONDecodeError: - step.final_answer = {"findings": [], "raw_answer": final_match.group(1).strip()} + answer_text = final_match.group(1).strip() + answer_text = re.sub(r'```json\s*', '', answer_text) + answer_text = re.sub(r'```\s*', '', answer_text) + # 使用增强的 JSON 解析器 + step.final_answer = AgentJsonParser.parse( + answer_text, + default={"findings": [], "raw_answer": answer_text} + ) + # 确保 findings 格式正确 + if "findings" in step.final_answer: + step.final_answer["findings"] = [ + f for f in step.final_answer["findings"] + if isinstance(f, dict) + ] return step # 提取 Action @@ -211,51 +219,15 @@ class AnalysisAgent(BaseAgent): input_text = input_match.group(1).strip() input_text = re.sub(r'```json\s*', '', input_text) input_text = re.sub(r'```\s*', '', input_text) - try: - step.action_input = json.loads(input_text) - except json.JSONDecodeError: - step.action_input = {"raw_input": input_text} + # 使用增强的 JSON 解析器 + step.action_input = AgentJsonParser.parse( + input_text, + default={"raw_input": input_text} + ) return step - async def _execute_tool(self, tool_name: str, tool_input: Dict) -> str: - """执行工具""" - tool = self.tools.get(tool_name) - - if not tool: - return f"错误: 工具 '{tool_name}' 不存在。可用工具: {list(self.tools.keys())}" - - try: - self._tool_calls += 1 - await self.emit_tool_call(tool_name, tool_input) - - import time - start = time.time() - - result = await tool.execute(**tool_input) - - duration_ms = int((time.time() - start) * 1000) - await self.emit_tool_result(tool_name, str(result.data)[:200], duration_ms) - - if result.success: - output = str(result.data) - - # 如果是代码分析工具,也包含 metadata - if result.metadata: - if "issues" in result.metadata: - output += f"\n\n发现的问题:\n{json.dumps(result.metadata['issues'], ensure_ascii=False, indent=2)}" - if "findings" in result.metadata: - output += f"\n\n发现:\n{json.dumps(result.metadata['findings'][:10], ensure_ascii=False, indent=2)}" - - if len(output) > 6000: - output = output[:6000] + f"\n\n... [输出已截断,共 {len(str(result.data))} 字符]" - return output - else: - return f"工具执行失败: {result.error}" - - except Exception as e: - logger.error(f"Tool execution error: {e}") - return f"工具执行错误: {str(e)}" + async def run(self, input_data: Dict[str, Any]) -> AgentResult: """ @@ -271,6 +243,14 @@ class AnalysisAgent(BaseAgent): task = input_data.get("task", "") task_context = input_data.get("task_context", "") + # 🔥 处理交接信息 + handoff = input_data.get("handoff") + if handoff: + from .base import TaskHandoff + if isinstance(handoff, dict): + handoff = TaskHandoff.from_dict(handoff) + self.receive_handoff(handoff) + # 从 Recon 结果获取上下文 recon_data = previous_results.get("recon", {}) if isinstance(recon_data, dict) and "data" in recon_data: @@ -281,7 +261,9 @@ class AnalysisAgent(BaseAgent): high_risk_areas = recon_data.get("high_risk_areas", plan.get("high_risk_areas", [])) initial_findings = recon_data.get("initial_findings", []) - # 构建初始消息 + # 🔥 构建包含交接上下文的初始消息 + handoff_context = self.get_handoff_context() + initial_message = f"""请开始对项目进行安全漏洞分析。 ## 项目信息 @@ -289,7 +271,7 @@ class AnalysisAgent(BaseAgent): - 语言: {tech_stack.get('languages', [])} - 框架: {tech_stack.get('frameworks', [])} -## 上下文信息 +{handoff_context if handoff_context else f'''## 上下文信息 ### 高风险区域 {json.dumps(high_risk_areas[:20], ensure_ascii=False)} @@ -297,7 +279,7 @@ class AnalysisAgent(BaseAgent): {json.dumps(entry_points[:10], ensure_ascii=False, indent=2)} ### 初步发现 (如果有) -{json.dumps(initial_findings[:5], ensure_ascii=False, indent=2) if initial_findings else '无'} +{json.dumps(initial_findings[:5], ensure_ascii=False, indent=2) if initial_findings else "无"}'''} ## 任务 {task_context or task or '进行全面的安全漏洞分析,发现代码中的安全问题。'} @@ -306,9 +288,12 @@ class AnalysisAgent(BaseAgent): {config.get('target_vulnerabilities', ['all'])} ## 可用工具 -{self._get_tools_description()} +{self.get_tools_description()} 请开始你的安全分析。首先思考分析策略,然后选择合适的工具开始分析。""" + + # 🔥 记录工作开始 + self.record_work("开始安全漏洞分析") # 初始化对话历史 self._conversation_history = [ @@ -328,18 +313,22 @@ class AnalysisAgent(BaseAgent): self._iteration = iteration + 1 - # 🔥 发射 LLM 开始思考事件 - await self.emit_llm_start(iteration + 1) + # 🔥 再次检查取消标志(在LLM调用之前) + if self.is_cancelled: + await self.emit_thinking("🛑 任务已取消,停止执行") + break - # 🔥 调用 LLM 进行思考和决策 - response = await self.llm_service.chat_completion_raw( - messages=self._conversation_history, - temperature=0.1, - max_tokens=2048, - ) + # 调用 LLM 进行思考和决策(流式输出) + try: + llm_output, tokens_this_round = await self.stream_llm_call( + self._conversation_history, + temperature=0.1, + max_tokens=2048, + ) + except asyncio.CancelledError: + logger.info(f"[{self.name}] LLM call cancelled") + break - llm_output = response.get("content", "") - tokens_this_round = response.get("usage", {}).get("total_tokens", 0) self._total_tokens += tokens_this_round # 解析 LLM 响应 @@ -369,6 +358,14 @@ class AnalysisAgent(BaseAgent): finding.get("vulnerability_type", "other"), finding.get("file_path", "") ) + # 🔥 记录洞察 + self.add_insight( + f"发现 {finding.get('severity', 'medium')} 级别漏洞: {finding.get('title', 'Unknown')}" + ) + + # 🔥 记录工作完成 + self.record_work(f"完成安全分析,发现 {len(all_findings)} 个潜在漏洞") + await self.emit_llm_complete( f"分析完成,发现 {len(all_findings)} 个潜在漏洞", self._total_tokens @@ -380,7 +377,7 @@ class AnalysisAgent(BaseAgent): # 🔥 发射 LLM 动作决策事件 await self.emit_llm_action(step.action, step.action_input or {}) - observation = await self._execute_tool( + observation = await self.execute_tool( step.action, step.action_input or {} ) @@ -427,7 +424,7 @@ class AnalysisAgent(BaseAgent): await self.emit_event( "info", - f"🎯 Analysis Agent 完成: {len(standardized_findings)} 个发现, {self._iteration} 轮迭代, {self._tool_calls} 次工具调用" + f"Analysis Agent 完成: {len(standardized_findings)} 个发现, {self._iteration} 轮迭代, {self._tool_calls} 次工具调用" ) return AgentResult( diff --git a/backend/app/services/agent/agents/analysis_v2.py b/backend/app/services/agent/agents/analysis_v2.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/services/agent/agents/base.py b/backend/app/services/agent/agents/base.py index d526c24..ca7b37c 100644 --- a/backend/app/services/agent/agents/base.py +++ b/backend/app/services/agent/agents/base.py @@ -2,14 +2,20 @@ Agent 基类 定义 Agent 的基本接口和通用功能 -核心原则:LLM 是 Agent 的大脑,所有日志应该反映 LLM 的参与! +核心原则: +1. LLM 是 Agent 的大脑,全程参与决策 +2. Agent 之间通过 TaskHandoff 传递结构化上下文 +3. 事件分为流式事件(前端展示)和持久化事件(数据库记录) """ from abc import ABC, abstractmethod -from typing import List, Dict, Any, Optional, AsyncGenerator +from typing import List, Dict, Any, Optional, AsyncGenerator, Tuple from dataclasses import dataclass, field from enum import Enum +from datetime import datetime, timezone +import asyncio import logging +import uuid logger = logging.getLogger(__name__) @@ -73,6 +79,9 @@ class AgentResult: # 元数据 metadata: Dict[str, Any] = field(default_factory=dict) + # 🔥 协作信息 - Agent 传递给下一个 Agent 的结构化信息 + handoff: Optional["TaskHandoff"] = None + def to_dict(self) -> Dict[str, Any]: return { "success": self.success, @@ -83,9 +92,139 @@ class AgentResult: "tokens_used": self.tokens_used, "duration_ms": self.duration_ms, "metadata": self.metadata, + "handoff": self.handoff.to_dict() if self.handoff else None, } +@dataclass +class TaskHandoff: + """ + 任务交接协议 - Agent 之间传递的结构化信息 + + 设计原则: + 1. 包含足够的上下文让下一个 Agent 理解前序工作 + 2. 提供明确的建议和关注点 + 3. 可直接转换为 LLM 可理解的 prompt + """ + # 基本信息 + from_agent: str + to_agent: str + + # 工作摘要 + summary: str + work_completed: List[str] = field(default_factory=list) + + # 关键发现和洞察 + key_findings: List[Dict[str, Any]] = field(default_factory=list) + insights: List[str] = field(default_factory=list) + + # 建议和关注点 + suggested_actions: List[Dict[str, Any]] = field(default_factory=list) + attention_points: List[str] = field(default_factory=list) + priority_areas: List[str] = field(default_factory=list) + + # 上下文数据 + context_data: Dict[str, Any] = field(default_factory=dict) + + # 置信度 + confidence: float = 0.8 + + # 时间戳 + timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + + def to_dict(self) -> Dict[str, Any]: + return { + "from_agent": self.from_agent, + "to_agent": self.to_agent, + "summary": self.summary, + "work_completed": self.work_completed, + "key_findings": self.key_findings, + "insights": self.insights, + "suggested_actions": self.suggested_actions, + "attention_points": self.attention_points, + "priority_areas": self.priority_areas, + "context_data": self.context_data, + "confidence": self.confidence, + "timestamp": self.timestamp.isoformat(), + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "TaskHandoff": + return cls( + from_agent=data.get("from_agent", ""), + to_agent=data.get("to_agent", ""), + summary=data.get("summary", ""), + work_completed=data.get("work_completed", []), + key_findings=data.get("key_findings", []), + insights=data.get("insights", []), + suggested_actions=data.get("suggested_actions", []), + attention_points=data.get("attention_points", []), + priority_areas=data.get("priority_areas", []), + context_data=data.get("context_data", {}), + confidence=data.get("confidence", 0.8), + ) + + def to_prompt_context(self) -> str: + """ + 转换为 LLM 可理解的上下文格式 + 这是关键!让 LLM 能够理解前序 Agent 的工作 + """ + lines = [ + f"## 来自 {self.from_agent} Agent 的任务交接", + "", + f"### 工作摘要", + self.summary, + "", + ] + + if self.work_completed: + lines.append("### 已完成的工作") + for work in self.work_completed: + lines.append(f"- {work}") + lines.append("") + + if self.key_findings: + lines.append("### 关键发现") + for i, finding in enumerate(self.key_findings[:15], 1): + severity = finding.get("severity", "medium") + title = finding.get("title", "Unknown") + file_path = finding.get("file_path", "") + lines.append(f"{i}. [{severity.upper()}] {title}") + if file_path: + lines.append(f" 位置: {file_path}:{finding.get('line_start', '')}") + if finding.get("description"): + lines.append(f" 描述: {finding['description'][:100]}") + lines.append("") + + if self.insights: + lines.append("### 洞察和分析") + for insight in self.insights: + lines.append(f"- {insight}") + lines.append("") + + if self.suggested_actions: + lines.append("### 建议的下一步行动") + for action in self.suggested_actions: + action_type = action.get("type", "general") + description = action.get("description", "") + priority = action.get("priority", "medium") + lines.append(f"- [{priority.upper()}] {action_type}: {description}") + lines.append("") + + if self.attention_points: + lines.append("### ⚠️ 需要特别关注") + for point in self.attention_points: + lines.append(f"- {point}") + lines.append("") + + if self.priority_areas: + lines.append("### 优先分析区域") + for area in self.priority_areas: + lines.append(f"- {area}") + + return "\n".join(lines) + + class BaseAgent(ABC): """ Agent 基类 @@ -94,6 +233,11 @@ class BaseAgent(ABC): 1. LLM 是 Agent 的大脑,全程参与决策 2. 所有日志应该反映 LLM 的思考过程 3. 工具调用是 LLM 的决策结果 + + 协作原则: + 1. 通过 TaskHandoff 接收前序 Agent 的上下文 + 2. 执行完成后生成 TaskHandoff 传递给下一个 Agent + 3. 洞察和发现应该结构化记录 """ def __init__( @@ -122,6 +266,11 @@ class BaseAgent(ABC): self._total_tokens = 0 self._tool_calls = 0 self._cancelled = False + + # 🔥 协作状态 + self._incoming_handoff: Optional[TaskHandoff] = None + self._insights: List[str] = [] # 收集的洞察 + self._work_completed: List[str] = [] # 完成的工作记录 @property def name(self) -> str: @@ -152,6 +301,103 @@ class BaseAgent(ABC): def is_cancelled(self) -> bool: return self._cancelled + # ============ 协作方法 ============ + + def receive_handoff(self, handoff: TaskHandoff): + """ + 接收来自前序 Agent 的任务交接 + + Args: + handoff: 任务交接对象 + """ + self._incoming_handoff = handoff + logger.info( + f"[{self.name}] Received handoff from {handoff.from_agent}: " + f"{handoff.summary[:50]}..." + ) + + def get_handoff_context(self) -> str: + """ + 获取交接上下文(用于构建 LLM prompt) + + Returns: + 格式化的上下文字符串 + """ + if not self._incoming_handoff: + return "" + return self._incoming_handoff.to_prompt_context() + + def add_insight(self, insight: str): + """记录洞察""" + self._insights.append(insight) + + def record_work(self, work: str): + """记录完成的工作""" + self._work_completed.append(work) + + def create_handoff( + self, + to_agent: str, + summary: str, + key_findings: List[Dict[str, Any]] = None, + suggested_actions: List[Dict[str, Any]] = None, + attention_points: List[str] = None, + priority_areas: List[str] = None, + context_data: Dict[str, Any] = None, + ) -> TaskHandoff: + """ + 创建任务交接 + + Args: + to_agent: 目标 Agent + summary: 工作摘要 + key_findings: 关键发现 + suggested_actions: 建议的行动 + attention_points: 需要关注的点 + priority_areas: 优先分析区域 + context_data: 上下文数据 + + Returns: + TaskHandoff 对象 + """ + return TaskHandoff( + from_agent=self.name, + to_agent=to_agent, + summary=summary, + work_completed=self._work_completed.copy(), + key_findings=key_findings or [], + insights=self._insights.copy(), + suggested_actions=suggested_actions or [], + attention_points=attention_points or [], + priority_areas=priority_areas or [], + context_data=context_data or {}, + ) + + def build_prompt_with_handoff(self, base_prompt: str) -> str: + """ + 构建包含交接上下文的 prompt + + Args: + base_prompt: 基础 prompt + + Returns: + 增强后的 prompt + """ + handoff_context = self.get_handoff_context() + if not handoff_context: + return base_prompt + + return f"""{base_prompt} + +--- +## 前序 Agent 交接信息 + +{handoff_context} + +--- +请基于以上来自前序 Agent 的信息,结合你的专业能力开展工作。 +""" + # ============ 核心事件发射方法 ============ async def emit_event( @@ -173,13 +419,13 @@ class BaseAgent(ABC): async def emit_thinking(self, message: str): """发射 LLM 思考事件""" - await self.emit_event("thinking", f"🧠 [{self.name}] {message}") + await self.emit_event("thinking", f"[{self.name}] {message}") async def emit_llm_start(self, iteration: int): """发射 LLM 开始思考事件""" await self.emit_event( "llm_start", - f"🤔 [{self.name}] LLM 开始第 {iteration} 轮思考...", + f"[{self.name}] 第 {iteration} 轮迭代开始", metadata={"iteration": iteration} ) @@ -189,31 +435,62 @@ class BaseAgent(ABC): display_thought = thought[:500] + "..." if len(thought) > 500 else thought await self.emit_event( "llm_thought", - f"💭 [{self.name}] LLM 思考:\n{display_thought}", + f"[{self.name}] 思考: {display_thought}", metadata={ "thought": thought, "iteration": iteration, } ) + async def emit_thinking_start(self): + """发射开始思考事件(流式输出用)""" + await self.emit_event("thinking_start", f"[{self.name}] 开始思考...") + + async def emit_thinking_token(self, token: str, accumulated: str): + """发射思考 token 事件(流式输出用)""" + await self.emit_event( + "thinking_token", + "", # 不需要 message,前端从 metadata 获取 + metadata={ + "token": token, + "accumulated": accumulated, + } + ) + + async def emit_thinking_end(self, full_response: str): + """发射思考结束事件(流式输出用)""" + await self.emit_event( + "thinking_end", + f"[{self.name}] 思考完成", + metadata={"accumulated": full_response} + ) + async def emit_llm_decision(self, decision: str, reason: str = ""): """发射 LLM 决策事件 - 展示 LLM 做了什么决定""" await self.emit_event( "llm_decision", - f"💡 [{self.name}] LLM 决策: {decision}" + (f" (理由: {reason})" if reason else ""), + f"[{self.name}] 决策: {decision}" + (f" ({reason})" if reason else ""), metadata={ "decision": decision, "reason": reason, } ) + async def emit_llm_complete(self, result_summary: str, tokens_used: int): + """发射 LLM 完成事件""" + await self.emit_event( + "llm_complete", + f"[{self.name}] 完成: {result_summary} (消耗 {tokens_used} tokens)", + metadata={ + "tokens_used": tokens_used, + } + ) + async def emit_llm_action(self, action: str, action_input: Dict): - """发射 LLM 动作事件 - LLM 决定执行什么动作""" - import json - input_str = json.dumps(action_input, ensure_ascii=False)[:200] + """发射 LLM 动作决策事件""" await self.emit_event( "llm_action", - f"⚡ [{self.name}] LLM 动作: {action}\n 参数: {input_str}", + f"[{self.name}] 执行动作: {action}", metadata={ "action": action, "action_input": action_input, @@ -221,43 +498,33 @@ class BaseAgent(ABC): ) async def emit_llm_observation(self, observation: str): - """发射 LLM 观察事件 - LLM 看到了什么""" + """发射 LLM 观察事件""" + # 截断过长的观察结果 display_obs = observation[:300] + "..." if len(observation) > 300 else observation await self.emit_event( "llm_observation", - f"👁️ [{self.name}] LLM 观察到:\n{display_obs}", - metadata={"observation": observation[:2000]} - ) - - async def emit_llm_complete(self, result_summary: str, tokens_used: int): - """发射 LLM 完成事件""" - await self.emit_event( - "llm_complete", - f"✅ [{self.name}] LLM 完成: {result_summary} (消耗 {tokens_used} tokens)", + f"[{self.name}] 观察结果: {display_obs}", metadata={ - "tokens_used": tokens_used, + "observation": observation[:2000], # 限制存储长度 } ) # ============ 工具调用相关事件 ============ async def emit_tool_call(self, tool_name: str, tool_input: Dict): - """发射工具调用事件 - LLM 决定调用工具""" - import json - input_str = json.dumps(tool_input, ensure_ascii=False)[:300] + """发射工具调用事件""" await self.emit_event( "tool_call", - f"🔧 [{self.name}] LLM 调用工具: {tool_name}\n 输入: {input_str}", + f"[{self.name}] 调用工具: {tool_name}", tool_name=tool_name, tool_input=tool_input, ) async def emit_tool_result(self, tool_name: str, result: str, duration_ms: int): """发射工具结果事件""" - result_preview = result[:200] + "..." if len(result) > 200 else result await self.emit_event( "tool_result", - f"📤 [{self.name}] 工具 {tool_name} 返回 ({duration_ms}ms):\n {result_preview}", + f"[{self.name}] 工具 {tool_name} 完成 ({duration_ms}ms)", tool_name=tool_name, tool_duration_ms=duration_ms, ) @@ -332,9 +599,6 @@ class BaseAgent(ABC): """ self._iteration += 1 - # 发射 LLM 开始事件 - await self.emit_llm_start(self._iteration) - try: response = await self.llm_service.chat_completion( messages=messages, @@ -385,3 +649,124 @@ class BaseAgent(ABC): "tool_calls": self._tool_calls, "tokens_used": self._total_tokens, } + + # ============ 统一的流式 LLM 调用 ============ + + async def stream_llm_call( + self, + messages: List[Dict[str, str]], + temperature: float = 0.1, + max_tokens: int = 2048, + ) -> Tuple[str, int]: + """ + 统一的流式 LLM 调用方法 + + 所有 Agent 共用此方法,避免重复代码 + + Args: + messages: 消息列表 + temperature: 温度 + max_tokens: 最大 token 数 + + Returns: + (完整响应内容, token数量) + """ + accumulated = "" + total_tokens = 0 + + await self.emit_thinking_start() + + try: + async for chunk in self.llm_service.chat_completion_stream( + messages=messages, + temperature=temperature, + max_tokens=max_tokens, + ): + # 检查取消 + if self.is_cancelled: + break + + if chunk["type"] == "token": + token = chunk["content"] + accumulated = chunk["accumulated"] + await self.emit_thinking_token(token, accumulated) + + elif chunk["type"] == "done": + accumulated = chunk["content"] + if chunk.get("usage"): + total_tokens = chunk["usage"].get("total_tokens", 0) + break + + elif chunk["type"] == "error": + accumulated = chunk.get("accumulated", "") + logger.error(f"Stream error: {chunk.get('error')}") + break + + except asyncio.CancelledError: + logger.info(f"[{self.name}] LLM call cancelled") + raise + finally: + await self.emit_thinking_end(accumulated) + + return accumulated, total_tokens + + async def execute_tool(self, tool_name: str, tool_input: Dict) -> str: + """ + 统一的工具执行方法 + + Args: + tool_name: 工具名称 + tool_input: 工具参数 + + Returns: + 工具执行结果字符串 + """ + tool = self.tools.get(tool_name) + + if not tool: + return f"错误: 工具 '{tool_name}' 不存在。可用工具: {list(self.tools.keys())}" + + try: + self._tool_calls += 1 + await self.emit_tool_call(tool_name, tool_input) + + import time + start = time.time() + + result = await tool.execute(**tool_input) + + duration_ms = int((time.time() - start) * 1000) + await self.emit_tool_result(tool_name, str(result.data)[:200], duration_ms) + + if result.success: + output = str(result.data) + + # 包含 metadata 中的额外信息 + if result.metadata: + if "issues" in result.metadata: + import json + output += f"\n\n发现的问题:\n{json.dumps(result.metadata['issues'], ensure_ascii=False, indent=2)}" + if "findings" in result.metadata: + import json + output += f"\n\n发现:\n{json.dumps(result.metadata['findings'][:10], ensure_ascii=False, indent=2)}" + + # 截断过长输出 + if len(output) > 6000: + output = output[:6000] + f"\n\n... [输出已截断,共 {len(str(result.data))} 字符]" + return output + else: + return f"工具执行失败: {result.error}" + + except Exception as e: + logger.error(f"Tool execution error: {e}") + return f"工具执行错误: {str(e)}" + + def get_tools_description(self) -> str: + """生成工具描述文本(用于 prompt)""" + tools_info = [] + for name, tool in self.tools.items(): + if name.startswith("_"): + continue + desc = f"- {name}: {getattr(tool, 'description', 'No description')}" + tools_info.append(desc) + return "\n".join(tools_info) diff --git a/backend/app/services/agent/agents/orchestrator.py b/backend/app/services/agent/agents/orchestrator.py index 9354ac5..b5ee923 100644 --- a/backend/app/services/agent/agents/orchestrator.py +++ b/backend/app/services/agent/agents/orchestrator.py @@ -18,6 +18,7 @@ from typing import List, Dict, Any, Optional from dataclasses import dataclass from .base import BaseAgent, AgentConfig, AgentResult, AgentType, AgentPattern +from ..json_parser import AgentJsonParser logger = logging.getLogger(__name__) @@ -178,18 +179,22 @@ class OrchestratorAgent(BaseAgent): self._iteration = iteration + 1 - # 🔥 发射 LLM 开始思考事件 - await self.emit_llm_start(iteration + 1) + # 🔥 再次检查取消标志(在LLM调用之前) + if self.is_cancelled: + await self.emit_thinking("🛑 任务已取消,停止执行") + break - # 🔥 调用 LLM 进行思考和决策 - response = await self.llm_service.chat_completion_raw( - messages=self._conversation_history, - temperature=0.1, - max_tokens=2048, - ) + # 调用 LLM 进行思考和决策(流式输出) + try: + llm_output, tokens_this_round = await self.stream_llm_call( + self._conversation_history, + temperature=0.1, + max_tokens=2048, + ) + except asyncio.CancelledError: + logger.info(f"[{self.name}] LLM call cancelled") + break - llm_output = response.get("content", "") - tokens_this_round = response.get("usage", {}).get("total_tokens", 0) self._total_tokens += tokens_this_round # 解析 LLM 的决策 @@ -348,10 +353,11 @@ class OrchestratorAgent(BaseAgent): input_text = re.sub(r'```json\s*', '', input_text) input_text = re.sub(r'```\s*', '', input_text) - try: - action_input = json.loads(input_text) - except json.JSONDecodeError: - action_input = {"raw": input_text} + # 使用增强的 JSON 解析器 + action_input = AgentJsonParser.parse( + input_text, + default={"raw": input_text} + ) return AgentStep( thought=thought, diff --git a/backend/app/services/agent/agents/react_agent.py b/backend/app/services/agent/agents/react_agent.py index e7f5631..37e8e0e 100644 --- a/backend/app/services/agent/agents/react_agent.py +++ b/backend/app/services/agent/agents/react_agent.py @@ -16,6 +16,7 @@ from typing import List, Dict, Any, Optional, Tuple from dataclasses import dataclass from .base import BaseAgent, AgentConfig, AgentResult, AgentType, AgentPattern +from ..json_parser import AgentJsonParser logger = logging.getLogger(__name__) @@ -182,15 +183,20 @@ class ReActAgent(BaseAgent): final_match = re.search(r'Final Answer:\s*(.*?)$', response, re.DOTALL) if final_match: step.is_final = True - try: - # 尝试提取 JSON - answer_text = final_match.group(1).strip() - # 移除 markdown 代码块 - answer_text = re.sub(r'```json\s*', '', answer_text) - answer_text = re.sub(r'```\s*', '', answer_text) - step.final_answer = json.loads(answer_text) - except json.JSONDecodeError: - step.final_answer = {"raw_answer": final_match.group(1).strip()} + answer_text = final_match.group(1).strip() + answer_text = re.sub(r'```json\s*', '', answer_text) + answer_text = re.sub(r'```\s*', '', answer_text) + # 使用增强的 JSON 解析器 + step.final_answer = AgentJsonParser.parse( + answer_text, + default={"raw_answer": answer_text} + ) + # 确保 findings 格式正确 + if "findings" in step.final_answer: + step.final_answer["findings"] = [ + f for f in step.final_answer["findings"] + if isinstance(f, dict) + ] return step # 提取 Action @@ -202,14 +208,13 @@ class ReActAgent(BaseAgent): input_match = re.search(r'Action Input:\s*(.*?)(?=Thought:|Action:|Observation:|$)', response, re.DOTALL) if input_match: input_text = input_match.group(1).strip() - # 移除 markdown 代码块 input_text = re.sub(r'```json\s*', '', input_text) input_text = re.sub(r'```\s*', '', input_text) - try: - step.action_input = json.loads(input_text) - except json.JSONDecodeError: - # 尝试简单解析 - step.action_input = {"raw_input": input_text} + # 使用增强的 JSON 解析器 + step.action_input = AgentJsonParser.parse( + input_text, + default={"raw_input": input_text} + ) return step diff --git a/backend/app/services/agent/agents/recon.py b/backend/app/services/agent/agents/recon.py index 8ddc609..5279a99 100644 --- a/backend/app/services/agent/agents/recon.py +++ b/backend/app/services/agent/agents/recon.py @@ -10,6 +10,7 @@ LLM 是真正的大脑! 类型: ReAct (真正的!) """ +import asyncio import json import logging import re @@ -17,22 +18,24 @@ from typing import List, Dict, Any, Optional from dataclasses import dataclass from .base import BaseAgent, AgentConfig, AgentResult, AgentType, AgentPattern +from ..json_parser import AgentJsonParser logger = logging.getLogger(__name__) -RECON_SYSTEM_PROMPT = """你是 DeepAudit 的信息收集 Agent,负责在安全审计前**自主**收集项目信息。 +RECON_SYSTEM_PROMPT = """你是 DeepAudit 的信息收集 Agent,负责在安全审计前收集项目信息。 -## 你的角色 -你是信息收集的**大脑**,不是机械执行者。你需要: -1. 自主思考需要收集什么信息 -2. 选择合适的工具获取信息 -3. 根据发现动态调整策略 -4. 判断何时信息收集足够 +## 你的职责 +你专注于**信息收集**,为后续的漏洞分析提供基础数据: +1. 分析项目结构和目录布局 +2. 识别技术栈(语言、框架、数据库) +3. 找出入口点(API、路由、用户输入处理) +4. 标记高风险区域(认证、数据库操作、文件处理) +5. 收集依赖信息 ## 你可以使用的工具 -### 文件系统 +### 文件系统工具 - **list_files**: 列出目录内容 参数: directory (str), recursive (bool), pattern (str), max_files (int) @@ -42,12 +45,14 @@ RECON_SYSTEM_PROMPT = """你是 DeepAudit 的信息收集 Agent,负责在安 - **search_code**: 代码关键字搜索 参数: keyword (str), max_results (int) -### 安全扫描 -- **semgrep_scan**: Semgrep 静态分析扫描 -- **npm_audit**: npm 依赖漏洞审计 -- **safety_scan**: Python 依赖漏洞审计 -- **gitleaks_scan**: 密钥/敏感信息泄露扫描 -- **osv_scan**: OSV 通用依赖漏洞扫描 +### 语义搜索工具 +- **rag_query**: 语义代码搜索(如果可用) + 参数: query (str), top_k (int) + +## 注意 +- 你只负责信息收集,不要进行漏洞分析 +- 漏洞分析由 Analysis Agent 负责 +- 专注于收集项目结构、技术栈、入口点等信息 ## 工作方式 每一步,你需要输出: @@ -142,15 +147,7 @@ class ReconAgent(BaseAgent): self._conversation_history: List[Dict[str, str]] = [] self._steps: List[ReconStep] = [] - def _get_tools_description(self) -> str: - """生成工具描述""" - tools_info = [] - for name, tool in self.tools.items(): - if name.startswith("_"): - continue - desc = f"- {name}: {getattr(tool, 'description', 'No description')}" - tools_info.append(desc) - return "\n".join(tools_info) + def _parse_llm_response(self, response: str) -> ReconStep: """解析 LLM 响应""" @@ -165,13 +162,14 @@ class ReconAgent(BaseAgent): final_match = re.search(r'Final Answer:\s*(.*?)$', response, re.DOTALL) if final_match: step.is_final = True - try: - answer_text = final_match.group(1).strip() - answer_text = re.sub(r'```json\s*', '', answer_text) - answer_text = re.sub(r'```\s*', '', answer_text) - step.final_answer = json.loads(answer_text) - except json.JSONDecodeError: - step.final_answer = {"raw_answer": final_match.group(1).strip()} + answer_text = final_match.group(1).strip() + answer_text = re.sub(r'```json\s*', '', answer_text) + answer_text = re.sub(r'```\s*', '', answer_text) + # 使用增强的 JSON 解析器 + step.final_answer = AgentJsonParser.parse( + answer_text, + default={"raw_answer": answer_text} + ) return step # 提取 Action @@ -185,43 +183,15 @@ class ReconAgent(BaseAgent): input_text = input_match.group(1).strip() input_text = re.sub(r'```json\s*', '', input_text) input_text = re.sub(r'```\s*', '', input_text) - try: - step.action_input = json.loads(input_text) - except json.JSONDecodeError: - step.action_input = {"raw_input": input_text} + # 使用增强的 JSON 解析器 + step.action_input = AgentJsonParser.parse( + input_text, + default={"raw_input": input_text} + ) return step - async def _execute_tool(self, tool_name: str, tool_input: Dict) -> str: - """执行工具""" - tool = self.tools.get(tool_name) - - if not tool: - return f"错误: 工具 '{tool_name}' 不存在。可用工具: {list(self.tools.keys())}" - - try: - self._tool_calls += 1 - await self.emit_tool_call(tool_name, tool_input) - - import time - start = time.time() - - result = await tool.execute(**tool_input) - - duration_ms = int((time.time() - start) * 1000) - await self.emit_tool_result(tool_name, str(result.data)[:200], duration_ms) - - if result.success: - output = str(result.data) - if len(output) > 4000: - output = output[:4000] + f"\n\n... [输出已截断,共 {len(str(result.data))} 字符]" - return output - else: - return f"工具执行失败: {result.error}" - - except Exception as e: - logger.error(f"Tool execution error: {e}") - return f"工具执行错误: {str(e)}" + async def run(self, input_data: Dict[str, Any]) -> AgentResult: """ @@ -246,7 +216,7 @@ class ReconAgent(BaseAgent): {task_context or task or '进行全面的信息收集,为安全审计做准备。'} ## 可用工具 -{self._get_tools_description()} +{self.get_tools_description()} 请开始你的信息收集工作。首先思考应该收集什么信息,然后选择合适的工具。""" @@ -259,7 +229,7 @@ class ReconAgent(BaseAgent): self._steps = [] final_result = None - await self.emit_thinking("🔍 Recon Agent 启动,LLM 开始自主收集信息...") + await self.emit_thinking("Recon Agent 启动,LLM 开始自主收集信息...") try: for iteration in range(self.config.max_iterations): @@ -268,18 +238,22 @@ class ReconAgent(BaseAgent): self._iteration = iteration + 1 - # 🔥 发射 LLM 开始思考事件 - await self.emit_llm_start(iteration + 1) + # 🔥 再次检查取消标志(在LLM调用之前) + if self.is_cancelled: + await self.emit_thinking("🛑 任务已取消,停止执行") + break - # 🔥 调用 LLM 进行思考和决策 - response = await self.llm_service.chat_completion_raw( - messages=self._conversation_history, - temperature=0.1, - max_tokens=2048, - ) + # 调用 LLM 进行思考和决策(使用基类统一方法) + try: + llm_output, tokens_this_round = await self.stream_llm_call( + self._conversation_history, + temperature=0.1, + max_tokens=2048, + ) + except asyncio.CancelledError: + logger.info(f"[{self.name}] LLM call cancelled") + break - llm_output = response.get("content", "") - tokens_this_round = response.get("usage", {}).get("total_tokens", 0) self._total_tokens += tokens_this_round # 解析 LLM 响应 @@ -311,7 +285,7 @@ class ReconAgent(BaseAgent): # 🔥 发射 LLM 动作决策事件 await self.emit_llm_action(step.action, step.action_input or {}) - observation = await self._execute_tool( + observation = await self.execute_tool( step.action, step.action_input or {} ) @@ -341,9 +315,18 @@ class ReconAgent(BaseAgent): if not final_result: final_result = self._summarize_from_steps() + # 🔥 记录工作和洞察 + self.record_work(f"完成项目信息收集,发现 {len(final_result.get('entry_points', []))} 个入口点") + self.record_work(f"识别技术栈: {final_result.get('tech_stack', {})}") + + if final_result.get("high_risk_areas"): + self.add_insight(f"发现 {len(final_result['high_risk_areas'])} 个高风险区域需要重点分析") + if final_result.get("initial_findings"): + self.add_insight(f"初步发现 {len(final_result['initial_findings'])} 个潜在问题") + await self.emit_event( "info", - f"🎯 Recon Agent 完成: {self._iteration} 轮迭代, {self._tool_calls} 次工具调用" + f"Recon Agent 完成: {self._iteration} 轮迭代, {self._tool_calls} 次工具调用" ) return AgentResult( diff --git a/backend/app/services/agent/agents/verification.py b/backend/app/services/agent/agents/verification.py index e4c17bb..02360af 100644 --- a/backend/app/services/agent/agents/verification.py +++ b/backend/app/services/agent/agents/verification.py @@ -10,6 +10,7 @@ LLM 是验证的大脑! 类型: ReAct (真正的!) """ +import asyncio import json import logging import re @@ -18,6 +19,7 @@ from dataclasses import dataclass from datetime import datetime, timezone from .base import BaseAgent, AgentConfig, AgentResult, AgentType, AgentPattern +from ..json_parser import AgentJsonParser logger = logging.getLogger(__name__) @@ -34,15 +36,17 @@ VERIFICATION_SYSTEM_PROMPT = """你是 DeepAudit 的漏洞验证 Agent,一个* ## 你可以使用的工具 -### 代码分析 +### 文件操作 - **read_file**: 读取更多代码上下文 参数: file_path (str), start_line (int), end_line (int) -- **function_context**: 分析函数调用关系 - 参数: function_name (str) -- **dataflow_analysis**: 追踪数据流 - 参数: source (str), sink (str), file_path (str) +- **list_files**: 列出目录文件 + 参数: directory (str), pattern (str) + +### 验证分析 - **vulnerability_validation**: LLM 深度验证 ⭐ 参数: code (str), vulnerability_type (str), context (str) +- **dataflow_analysis**: 追踪数据流 + 参数: source (str), sink (str), file_path (str) ### 沙箱验证 - **sandbox_exec**: 在沙箱中执行命令 @@ -157,16 +161,6 @@ class VerificationAgent(BaseAgent): self._conversation_history: List[Dict[str, str]] = [] self._steps: List[VerificationStep] = [] - def _get_tools_description(self) -> str: - """生成工具描述""" - tools_info = [] - for name, tool in self.tools.items(): - if name.startswith("_"): - continue - desc = f"- {name}: {getattr(tool, 'description', 'No description')}" - tools_info.append(desc) - return "\n".join(tools_info) - def _parse_llm_response(self, response: str) -> VerificationStep: """解析 LLM 响应""" step = VerificationStep(thought="") @@ -180,13 +174,20 @@ class VerificationAgent(BaseAgent): final_match = re.search(r'Final Answer:\s*(.*?)$', response, re.DOTALL) if final_match: step.is_final = True - try: - answer_text = final_match.group(1).strip() - answer_text = re.sub(r'```json\s*', '', answer_text) - answer_text = re.sub(r'```\s*', '', answer_text) - step.final_answer = json.loads(answer_text) - except json.JSONDecodeError: - step.final_answer = {"findings": [], "raw_answer": final_match.group(1).strip()} + answer_text = final_match.group(1).strip() + answer_text = re.sub(r'```json\s*', '', answer_text) + answer_text = re.sub(r'```\s*', '', answer_text) + # 使用增强的 JSON 解析器 + step.final_answer = AgentJsonParser.parse( + answer_text, + default={"findings": [], "raw_answer": answer_text} + ) + # 确保 findings 格式正确 + if "findings" in step.final_answer: + step.final_answer["findings"] = [ + f for f in step.final_answer["findings"] + if isinstance(f, dict) + ] return step # 提取 Action @@ -200,50 +201,14 @@ class VerificationAgent(BaseAgent): input_text = input_match.group(1).strip() input_text = re.sub(r'```json\s*', '', input_text) input_text = re.sub(r'```\s*', '', input_text) - try: - step.action_input = json.loads(input_text) - except json.JSONDecodeError: - step.action_input = {"raw_input": input_text} + # 使用增强的 JSON 解析器 + step.action_input = AgentJsonParser.parse( + input_text, + default={"raw_input": input_text} + ) return step - async def _execute_tool(self, tool_name: str, tool_input: Dict) -> str: - """执行工具""" - tool = self.tools.get(tool_name) - - if not tool: - return f"错误: 工具 '{tool_name}' 不存在。可用工具: {list(self.tools.keys())}" - - try: - self._tool_calls += 1 - await self.emit_tool_call(tool_name, tool_input) - - import time - start = time.time() - - result = await tool.execute(**tool_input) - - duration_ms = int((time.time() - start) * 1000) - await self.emit_tool_result(tool_name, str(result.data)[:200], duration_ms) - - if result.success: - output = str(result.data) - - # 包含 metadata - if result.metadata: - if "validation" in result.metadata: - output += f"\n\n验证结果:\n{json.dumps(result.metadata['validation'], ensure_ascii=False, indent=2)}" - - if len(output) > 4000: - output = output[:4000] + f"\n\n... [输出已截断]" - return output - else: - return f"工具执行失败: {result.error}" - - except Exception as e: - logger.error(f"Tool execution error: {e}") - return f"工具执行错误: {str(e)}" - async def run(self, input_data: Dict[str, Any]) -> AgentResult: """ 执行漏洞验证 - LLM 全程参与! @@ -256,20 +221,32 @@ class VerificationAgent(BaseAgent): task = input_data.get("task", "") task_context = input_data.get("task_context", "") + # 🔥 处理交接信息 + handoff = input_data.get("handoff") + if handoff: + from .base import TaskHandoff + if isinstance(handoff, dict): + handoff = TaskHandoff.from_dict(handoff) + self.receive_handoff(handoff) + # 收集所有待验证的发现 findings_to_verify = [] - for phase_name, result in previous_results.items(): - if isinstance(result, dict): - data = result.get("data", {}) - else: - data = result.data if hasattr(result, 'data') else {} - - if isinstance(data, dict): - phase_findings = data.get("findings", []) - for f in phase_findings: - if f.get("needs_verification", True): - findings_to_verify.append(f) + # 🔥 优先从交接信息获取发现 + if self._incoming_handoff and self._incoming_handoff.key_findings: + findings_to_verify = self._incoming_handoff.key_findings.copy() + else: + for phase_name, result in previous_results.items(): + if isinstance(result, dict): + data = result.get("data", {}) + else: + data = result.data if hasattr(result, 'data') else {} + + if isinstance(data, dict): + phase_findings = data.get("findings", []) + for f in phase_findings: + if f.get("needs_verification", True): + findings_to_verify.append(f) # 去重 findings_to_verify = self._deduplicate(findings_to_verify) @@ -289,7 +266,12 @@ class VerificationAgent(BaseAgent): f"开始验证 {len(findings_to_verify)} 个发现" ) - # 构建初始消息 + # 🔥 记录工作开始 + self.record_work(f"开始验证 {len(findings_to_verify)} 个漏洞发现") + + # 🔥 构建包含交接上下文的初始消息 + handoff_context = self.get_handoff_context() + findings_summary = [] for i, f in enumerate(findings_to_verify): findings_summary.append(f""" @@ -306,6 +288,8 @@ class VerificationAgent(BaseAgent): initial_message = f"""请验证以下 {len(findings_to_verify)} 个安全发现。 +{handoff_context if handoff_context else ''} + ## 待验证发现 {''.join(findings_summary)} @@ -313,9 +297,10 @@ class VerificationAgent(BaseAgent): - 验证级别: {config.get('verification_level', 'standard')} ## 可用工具 -{self._get_tools_description()} +{self.get_tools_description()} -请开始验证。对于每个发现,思考如何验证它,使用合适的工具获取更多信息,然后判断是否为真实漏洞。""" +请开始验证。对于每个发现,思考如何验证它,使用合适的工具获取更多信息,然后判断是否为真实漏洞。 +{f"特别注意 Analysis Agent 提到的关注点。" if handoff_context else ""}""" # 初始化对话历史 self._conversation_history = [ @@ -335,18 +320,22 @@ class VerificationAgent(BaseAgent): self._iteration = iteration + 1 - # 🔥 发射 LLM 开始思考事件 - await self.emit_llm_start(iteration + 1) + # 🔥 再次检查取消标志(在LLM调用之前) + if self.is_cancelled: + await self.emit_thinking("🛑 任务已取消,停止执行") + break - # 🔥 调用 LLM 进行思考和决策 - response = await self.llm_service.chat_completion_raw( - messages=self._conversation_history, - temperature=0.1, - max_tokens=3000, - ) + # 调用 LLM 进行思考和决策(流式输出) + try: + llm_output, tokens_this_round = await self.stream_llm_call( + self._conversation_history, + temperature=0.1, + max_tokens=3000, + ) + except asyncio.CancelledError: + logger.info(f"[{self.name}] LLM call cancelled") + break - llm_output = response.get("content", "") - tokens_this_round = response.get("usage", {}).get("total_tokens", 0) self._total_tokens += tokens_this_round # 解析 LLM 响应 @@ -367,6 +356,14 @@ class VerificationAgent(BaseAgent): if step.is_final: await self.emit_llm_decision("完成漏洞验证", "LLM 判断验证已充分") final_result = step.final_answer + + # 🔥 记录洞察和工作 + if final_result and "findings" in final_result: + verified_count = len([f for f in final_result["findings"] if f.get("is_verified")]) + fp_count = len([f for f in final_result["findings"] if f.get("verdict") == "false_positive"]) + self.add_insight(f"验证了 {len(final_result['findings'])} 个发现,{verified_count} 个确认,{fp_count} 个误报") + self.record_work(f"完成漏洞验证: {verified_count} 个确认, {fp_count} 个误报") + await self.emit_llm_complete( f"验证完成", self._total_tokens @@ -378,7 +375,7 @@ class VerificationAgent(BaseAgent): # 🔥 发射 LLM 动作决策事件 await self.emit_llm_action(step.action, step.action_input or {}) - observation = await self._execute_tool( + observation = await self.execute_tool( step.action, step.action_input or {} ) @@ -438,7 +435,7 @@ class VerificationAgent(BaseAgent): await self.emit_event( "info", - f"🎯 Verification Agent 完成: {confirmed_count} 确认, {likely_count} 可能, {false_positive_count} 误报" + f"Verification Agent 完成: {confirmed_count} 确认, {likely_count} 可能, {false_positive_count} 误报" ) return AgentResult( diff --git a/backend/app/services/agent/event_manager.py b/backend/app/services/agent/event_manager.py index b1ead15..fe99e6e 100644 --- a/backend/app/services/agent/event_manager.py +++ b/backend/app/services/agent/event_manager.py @@ -299,8 +299,9 @@ class EventManager: "timestamp": timestamp.isoformat(), } - # 保存到数据库 - if self.db_session_factory: + # 保存到数据库(跳过高频事件如 thinking_token) + skip_db_events = {"thinking_token", "thinking_start", "thinking_end"} + 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: diff --git a/backend/app/services/agent/graph/audit_graph.py b/backend/app/services/agent/graph/audit_graph.py index 8f62338..90e0e8d 100644 --- a/backend/app/services/agent/graph/audit_graph.py +++ b/backend/app/services/agent/graph/audit_graph.py @@ -69,6 +69,11 @@ class AuditState(TypedDict): llm_next_action: Optional[str] # LLM 建议的下一步: "continue_analysis", "verify", "report", "end" llm_routing_reason: Optional[str] # LLM 的决策理由 + # 🔥 新增:Agent 间协作的任务交接信息 + recon_handoff: Optional[Dict[str, Any]] # Recon -> Analysis 的交接 + analysis_handoff: Optional[Dict[str, Any]] # Analysis -> Verification 的交接 + verification_handoff: Optional[Dict[str, Any]] # Verification -> Report 的交接 + # 消息和事件 messages: Annotated[List[Dict], operator.add] events: Annotated[List[Dict], operator.add] @@ -146,6 +151,9 @@ class LLMRouter: # 统计发现 severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0} for f in findings: + # 跳过非字典类型的 finding + if not isinstance(f, dict): + continue sev = f.get("severity", "medium") severity_counts[sev] = severity_counts.get(sev, 0) + 1 @@ -243,6 +251,11 @@ def route_after_recon(state: AuditState) -> Literal["analysis", "end"]: Recon 后的路由决策 优先使用 LLM 的决策,否则使用默认逻辑 """ + # 🔥 检查是否有错误 + if state.get("error") or state.get("current_phase") == "error": + logger.error(f"Recon phase has error, routing to end: {state.get('error')}") + return "end" + # 检查 LLM 是否有决策 llm_action = state.get("llm_next_action") if llm_action: diff --git a/backend/app/services/agent/graph/nodes.py b/backend/app/services/agent/graph/nodes.py index bcf5825..4b750ef 100644 --- a/backend/app/services/agent/graph/nodes.py +++ b/backend/app/services/agent/graph/nodes.py @@ -1,6 +1,8 @@ """ LangGraph 节点实现 每个节点封装一个 Agent 的执行逻辑 + +协作增强:节点之间通过 TaskHandoff 传递结构化的上下文和洞察 """ from typing import Dict, Any, List, Optional @@ -28,6 +30,14 @@ class BaseNode: await self.event_emitter.emit_info(message) except Exception as e: logger.warning(f"Failed to emit event: {e}") + + def _extract_handoff_from_state(self, state: Dict[str, Any], from_phase: str): + """从状态中提取前序 Agent 的 handoff""" + handoff_data = state.get(f"{from_phase}_handoff") + if handoff_data: + from ..agents.base import TaskHandoff + return TaskHandoff.from_dict(handoff_data) + return None class ReconNode(BaseNode): @@ -35,7 +45,7 @@ class ReconNode(BaseNode): 信息收集节点 输入: project_root, project_info, config - 输出: tech_stack, entry_points, high_risk_areas, dependencies + 输出: tech_stack, entry_points, high_risk_areas, dependencies, recon_handoff """ async def __call__(self, state: Dict[str, Any]) -> Dict[str, Any]: @@ -52,6 +62,35 @@ class ReconNode(BaseNode): if result.success and result.data: data = result.data + # 🔥 创建交接信息给 Analysis Agent + handoff = self.agent.create_handoff( + to_agent="Analysis", + summary=f"项目信息收集完成。发现 {len(data.get('entry_points', []))} 个入口点,{len(data.get('high_risk_areas', []))} 个高风险区域。", + key_findings=data.get("initial_findings", []), + suggested_actions=[ + { + "type": "deep_analysis", + "description": f"深入分析高风险区域: {', '.join(data.get('high_risk_areas', [])[:5])}", + "priority": "high", + }, + { + "type": "entry_point_audit", + "description": "审计所有入口点的输入验证", + "priority": "high", + }, + ], + attention_points=[ + f"技术栈: {data.get('tech_stack', {}).get('frameworks', [])}", + f"主要语言: {data.get('tech_stack', {}).get('languages', [])}", + ], + priority_areas=data.get("high_risk_areas", [])[:10], + context_data={ + "tech_stack": data.get("tech_stack", {}), + "entry_points": data.get("entry_points", []), + "dependencies": data.get("dependencies", {}), + }, + ) + await self.emit_event( "phase_complete", f"✅ 信息收集完成: 发现 {len(data.get('entry_points', []))} 个入口点" @@ -63,12 +102,15 @@ class ReconNode(BaseNode): "high_risk_areas": data.get("high_risk_areas", []), "dependencies": data.get("dependencies", {}), "current_phase": "recon_complete", - "findings": data.get("initial_findings", []), # 初步发现 + "findings": data.get("initial_findings", []), + # 🔥 保存交接信息 + "recon_handoff": handoff.to_dict(), "events": [{ "type": "recon_complete", "data": { "entry_points_count": len(data.get("entry_points", [])), "high_risk_areas_count": len(data.get("high_risk_areas", [])), + "handoff_summary": handoff.summary, } }], } @@ -90,8 +132,8 @@ class AnalysisNode(BaseNode): """ 漏洞分析节点 - 输入: tech_stack, entry_points, high_risk_areas, previous findings - 输出: findings (累加), should_continue_analysis + 输入: tech_stack, entry_points, high_risk_areas, recon_handoff + 输出: findings (累加), should_continue_analysis, analysis_handoff """ async def __call__(self, state: Dict[str, Any]) -> Dict[str, Any]: @@ -104,6 +146,15 @@ class AnalysisNode(BaseNode): ) try: + # 🔥 提取 Recon 的交接信息 + recon_handoff = self._extract_handoff_from_state(state, "recon") + if recon_handoff: + self.agent.receive_handoff(recon_handoff) + await self.emit_event( + "handoff_received", + f"📨 收到 Recon Agent 交接: {recon_handoff.summary[:50]}..." + ) + # 构建分析输入 analysis_input = { "phase_name": "analysis", @@ -121,6 +172,8 @@ class AnalysisNode(BaseNode): } } }, + # 🔥 传递交接信息 + "handoff": recon_handoff, } # 调用 Analysis Agent @@ -130,27 +183,70 @@ class AnalysisNode(BaseNode): new_findings = result.data.get("findings", []) # 判断是否需要继续分析 - # 如果这一轮发现了很多问题,可能还有更多 should_continue = ( len(new_findings) >= 5 and iteration < state.get("max_iterations", 3) ) + # 🔥 创建交接信息给 Verification Agent + # 统计严重程度 + severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0} + for f in new_findings: + if isinstance(f, dict): + sev = f.get("severity", "medium") + severity_counts[sev] = severity_counts.get(sev, 0) + 1 + + handoff = self.agent.create_handoff( + to_agent="Verification", + summary=f"漏洞分析完成。发现 {len(new_findings)} 个潜在漏洞 (Critical: {severity_counts['critical']}, High: {severity_counts['high']}, Medium: {severity_counts['medium']}, Low: {severity_counts['low']})", + key_findings=new_findings[:20], # 传递前20个发现 + suggested_actions=[ + { + "type": "verify_critical", + "description": "优先验证 Critical 和 High 级别的漏洞", + "priority": "critical", + }, + { + "type": "poc_generation", + "description": "为确认的漏洞生成 PoC", + "priority": "high", + }, + ], + attention_points=[ + f"共 {severity_counts['critical']} 个 Critical 级别漏洞需要立即验证", + f"共 {severity_counts['high']} 个 High 级别漏洞需要优先验证", + "注意检查是否有误报,特别是静态分析工具的结果", + ], + priority_areas=[ + f.get("file_path", "") for f in new_findings + if f.get("severity") in ["critical", "high"] + ][:10], + context_data={ + "severity_distribution": severity_counts, + "total_findings": len(new_findings), + "iteration": iteration, + }, + ) + await self.emit_event( "phase_complete", f"✅ 分析迭代 {iteration} 完成: 发现 {len(new_findings)} 个潜在漏洞" ) return { - "findings": new_findings, # 会自动累加 + "findings": new_findings, "iteration": iteration, "should_continue_analysis": should_continue, "current_phase": "analysis_complete", + # 🔥 保存交接信息 + "analysis_handoff": handoff.to_dict(), "events": [{ "type": "analysis_iteration", "data": { "iteration": iteration, "findings_count": len(new_findings), + "severity_distribution": severity_counts, + "handoff_summary": handoff.summary, } }], } @@ -174,8 +270,8 @@ class VerificationNode(BaseNode): """ 漏洞验证节点 - 输入: findings - 输出: verified_findings, false_positives + 输入: findings, analysis_handoff + 输出: verified_findings, false_positives, verification_handoff """ async def __call__(self, state: Dict[str, Any]) -> Dict[str, Any]: @@ -195,6 +291,15 @@ class VerificationNode(BaseNode): ) try: + # 🔥 提取 Analysis 的交接信息 + analysis_handoff = self._extract_handoff_from_state(state, "analysis") + if analysis_handoff: + self.agent.receive_handoff(analysis_handoff) + await self.emit_event( + "handoff_received", + f"📨 收到 Analysis Agent 交接: {analysis_handoff.summary[:50]}..." + ) + # 构建验证输入 verification_input = { "previous_results": { @@ -205,16 +310,49 @@ class VerificationNode(BaseNode): } }, "config": state["config"], + # 🔥 传递交接信息 + "handoff": analysis_handoff, } # 调用 Verification Agent result = await self.agent.run(verification_input) if result.success and result.data: - verified = [f for f in result.data.get("findings", []) if f.get("is_verified")] - false_pos = [f["id"] for f in result.data.get("findings", []) + all_verified_findings = result.data.get("findings", []) + verified = [f for f in all_verified_findings if f.get("is_verified")] + false_pos = [f.get("id", f.get("title", "unknown")) for f in all_verified_findings if f.get("verdict") == "false_positive"] + # 🔥 创建交接信息给 Report 节点 + handoff = self.agent.create_handoff( + to_agent="Report", + summary=f"漏洞验证完成。{len(verified)} 个漏洞已确认,{len(false_pos)} 个误报已排除。", + key_findings=verified, + suggested_actions=[ + { + "type": "generate_report", + "description": "生成详细的安全审计报告", + "priority": "high", + }, + { + "type": "remediation_plan", + "description": "为确认的漏洞制定修复计划", + "priority": "high", + }, + ], + attention_points=[ + f"共 {len(verified)} 个漏洞已确认存在", + f"共 {len(false_pos)} 个误报已排除", + "建议按严重程度优先修复 Critical 和 High 级别漏洞", + ], + context_data={ + "verified_count": len(verified), + "false_positive_count": len(false_pos), + "total_analyzed": len(findings), + "verification_rate": len(verified) / len(findings) if findings else 0, + }, + ) + await self.emit_event( "phase_complete", f"✅ 验证完成: {len(verified)} 已确认, {len(false_pos)} 误报" @@ -224,11 +362,14 @@ class VerificationNode(BaseNode): "verified_findings": verified, "false_positives": false_pos, "current_phase": "verification_complete", + # 🔥 保存交接信息 + "verification_handoff": handoff.to_dict(), "events": [{ "type": "verification_complete", "data": { "verified_count": len(verified), "false_positive_count": len(false_pos), + "handoff_summary": handoff.summary, } }], } @@ -269,6 +410,11 @@ class ReportNode(BaseNode): type_counts = {} for finding in findings: + # 跳过非字典类型的 finding(防止数据格式异常) + if not isinstance(finding, dict): + logger.warning(f"Skipping invalid finding (not a dict): {type(finding)}") + continue + sev = finding.get("severity", "medium") severity_counts[sev] = severity_counts.get(sev, 0) + 1 @@ -300,7 +446,7 @@ class ReportNode(BaseNode): await self.emit_event( "phase_complete", - f"✅ 报告生成完成: 安全评分 {security_score}/100" + f"报告生成完成: 安全评分 {security_score}/100" ) return { diff --git a/backend/app/services/agent/graph/runner.py b/backend/app/services/agent/graph/runner.py index 304121d..3299c2e5 100644 --- a/backend/app/services/agent/graph/runner.py +++ b/backend/app/services/agent/graph/runner.py @@ -39,167 +39,8 @@ from .nodes import ReconNode, AnalysisNode, VerificationNode, ReportNode logger = logging.getLogger(__name__) -class LLMService: - """ - LLM 服务封装 - 提供代码分析、漏洞检测等 AI 功能 - """ - - def __init__(self, model: Optional[str] = None, api_key: Optional[str] = None): - self.model = model or settings.LLM_MODEL or "gpt-4o-mini" - self.api_key = api_key or settings.LLM_API_KEY - self.base_url = settings.LLM_BASE_URL - - async def chat_completion_raw( - self, - messages: List[Dict[str, str]], - temperature: float = 0.1, - max_tokens: int = 4096, - ) -> Dict[str, Any]: - """调用 LLM 生成响应""" - try: - import litellm - - response = await litellm.acompletion( - model=self.model, - messages=messages, - temperature=temperature, - max_tokens=max_tokens, - api_key=self.api_key, - base_url=self.base_url, - ) - - return { - "content": response.choices[0].message.content, - "usage": { - "prompt_tokens": response.usage.prompt_tokens, - "completion_tokens": response.usage.completion_tokens, - "total_tokens": response.usage.total_tokens, - } if response.usage else {}, - } - - except Exception as e: - logger.error(f"LLM call failed: {e}") - raise - - async def analyze_code(self, code: str, language: str) -> Dict[str, Any]: - """ - 分析代码安全问题 - - Args: - code: 代码内容 - language: 编程语言 - - Returns: - 分析结果,包含 issues 列表 - """ - prompt = f"""请分析以下 {language} 代码的安全问题。 - -代码: -```{language} -{code[:8000]} -``` - -请识别所有潜在的安全漏洞,包括但不限于: -- SQL 注入 -- XSS (跨站脚本) -- 命令注入 -- 路径遍历 -- 不安全的反序列化 -- 硬编码密钥/密码 -- 不安全的加密 -- SSRF -- 认证/授权问题 - -对于每个发现的问题,请提供: -1. 漏洞类型 -2. 严重程度 (critical/high/medium/low) -3. 问题描述 -4. 具体行号 -5. 修复建议 - -请以 JSON 格式返回结果: -{{ - "issues": [ - {{ - "type": "漏洞类型", - "severity": "严重程度", - "title": "问题标题", - "description": "详细描述", - "line": 行号, - "code_snippet": "相关代码片段", - "suggestion": "修复建议" - }} - ], - "quality_score": 0-100 -}} - -如果没有发现安全问题,返回空的 issues 数组和较高的 quality_score。""" - - try: - result = await self.chat_completion_raw( - messages=[ - {"role": "system", "content": "你是一位专业的代码安全审计专家,擅长发现代码中的安全漏洞。请只返回 JSON 格式的结果,不要包含其他内容。"}, - {"role": "user", "content": prompt}, - ], - temperature=0.1, - max_tokens=4096, - ) - - content = result.get("content", "{}") - - # 尝试提取 JSON - import json - import re - - # 尝试直接解析 - try: - return json.loads(content) - except json.JSONDecodeError: - pass - - # 尝试从 markdown 代码块提取 - json_match = re.search(r'```(?:json)?\s*([\s\S]*?)\s*```', content) - if json_match: - try: - return json.loads(json_match.group(1)) - except json.JSONDecodeError: - pass - - # 返回空结果 - return {"issues": [], "quality_score": 80} - - except Exception as e: - logger.error(f"Code analysis failed: {e}") - return {"issues": [], "quality_score": 0, "error": str(e)} - - async def analyze_code_with_custom_prompt( - self, - code: str, - language: str, - prompt: str, - **kwargs - ) -> Dict[str, Any]: - """使用自定义提示词分析代码""" - full_prompt = prompt.replace("{code}", code).replace("{language}", language) - - try: - result = await self.chat_completion_raw( - messages=[ - {"role": "system", "content": "你是一位专业的代码安全审计专家。"}, - {"role": "user", "content": full_prompt}, - ], - temperature=0.1, - ) - - return { - "analysis": result.get("content", ""), - "usage": result.get("usage", {}), - } - - except Exception as e: - logger.error(f"Custom analysis failed: {e}") - return {"analysis": "", "error": str(e)} +# 🔥 使用系统统一的 LLMService(支持用户配置) +from app.services.llm.service import LLMService class AgentRunner: @@ -217,18 +58,22 @@ class AgentRunner: db: AsyncSession, task: AgentTask, project_root: str, + user_config: Optional[Dict[str, Any]] = None, ): self.db = db self.task = task self.project_root = project_root + # 🔥 保存用户配置,供 RAG 初始化使用 + self.user_config = user_config or {} + # 事件管理 - 传入 db_session_factory 以持久化事件 from app.db.session import async_session_factory self.event_manager = EventManager(db_session_factory=async_session_factory) self.event_emitter = AgentEventEmitter(task.id, self.event_manager) - # LLM 服务 - self.llm_service = LLMService() + # 🔥 LLM 服务 - 使用用户配置(从系统配置页面获取) + self.llm_service = LLMService(user_config=self.user_config) # 工具集 self.tools: Dict[str, Any] = {} @@ -248,14 +93,26 @@ class AgentRunner: self._cancelled = False self._running_task: Optional[asyncio.Task] = None + # Agent 引用(用于取消传播) + self._agents: List[Any] = [] + # 流式处理器 self.stream_handler = StreamHandler(task.id) def cancel(self): """取消任务""" self._cancelled = True + + # 🔥 取消所有 Agent + for agent in self._agents: + if hasattr(agent, 'cancel'): + agent.cancel() + logger.debug(f"Cancelled agent: {agent.name if hasattr(agent, 'name') else 'unknown'}") + + # 取消运行中的任务 if self._running_task and not self._running_task.done(): self._running_task.cancel() + logger.info(f"Task {self.task.id} cancellation requested") @property @@ -283,11 +140,33 @@ class AgentRunner: await self.event_emitter.emit_info("📚 初始化 RAG 代码检索系统...") try: + # 🔥 从用户配置中获取 LLM 配置(用于 Embedding API Key) + # 优先级:用户配置 > 环境变量 + user_llm_config = self.user_config.get('llmConfig', {}) + + # 获取 Embedding 配置(优先使用用户配置的 LLM API Key) + embedding_provider = getattr(settings, 'EMBEDDING_PROVIDER', 'openai') + embedding_model = getattr(settings, 'EMBEDDING_MODEL', 'text-embedding-3-small') + + # 🔥 API Key 优先级:用户配置 > 环境变量 + embedding_api_key = ( + user_llm_config.get('llmApiKey') or + getattr(settings, 'LLM_API_KEY', '') or + '' + ) + + # 🔥 Base URL 优先级:用户配置 > 环境变量 + embedding_base_url = ( + user_llm_config.get('llmBaseUrl') or + getattr(settings, 'LLM_BASE_URL', None) or + None + ) + embedding_service = EmbeddingService( - provider=settings.EMBEDDING_PROVIDER, - model=settings.EMBEDDING_MODEL, - api_key=settings.LLM_API_KEY, - base_url=settings.LLM_BASE_URL, + provider=embedding_provider, + model=embedding_model, + api_key=embedding_api_key, + base_url=embedding_base_url, ) self.indexer = CodeIndexer( @@ -308,35 +187,59 @@ class AgentRunner: async def _initialize_tools(self): """初始化工具集""" - await self.event_emitter.emit_info("🔧 初始化 Agent 工具集...") + await self.event_emitter.emit_info("初始化 Agent 工具集...") - # 文件工具 - self.tools["read_file"] = FileReadTool(self.project_root) - self.tools["search_code"] = FileSearchTool(self.project_root) - self.tools["list_files"] = ListFilesTool(self.project_root) + # ============ 基础工具(所有 Agent 共享)============ + base_tools = { + "read_file": FileReadTool(self.project_root), + "list_files": ListFilesTool(self.project_root), + } - # RAG 工具 + # ============ Recon Agent 专属工具 ============ + # 职责:信息收集、项目结构分析、技术栈识别 + self.recon_tools = { + **base_tools, + "search_code": FileSearchTool(self.project_root), + } + + # RAG 工具(Recon 用于语义搜索) if self.retriever: - self.tools["rag_query"] = RAGQueryTool(self.retriever) - self.tools["security_search"] = SecurityCodeSearchTool(self.retriever) - self.tools["function_context"] = FunctionContextTool(self.retriever) + self.recon_tools["rag_query"] = RAGQueryTool(self.retriever) - # 分析工具 - self.tools["pattern_match"] = PatternMatchTool(self.project_root) - self.tools["code_analysis"] = CodeAnalysisTool(self.llm_service) - self.tools["dataflow_analysis"] = DataFlowAnalysisTool(self.llm_service) - self.tools["vulnerability_validation"] = VulnerabilityValidationTool(self.llm_service) + # ============ Analysis Agent 专属工具 ============ + # 职责:漏洞分析、代码审计、模式匹配 + self.analysis_tools = { + **base_tools, + "search_code": FileSearchTool(self.project_root), + # 模式匹配和代码分析 + "pattern_match": PatternMatchTool(self.project_root), + "code_analysis": CodeAnalysisTool(self.llm_service), + "dataflow_analysis": DataFlowAnalysisTool(self.llm_service), + # 外部静态分析工具 + "semgrep_scan": SemgrepTool(self.project_root), + "bandit_scan": BanditTool(self.project_root), + "gitleaks_scan": GitleaksTool(self.project_root), + "trufflehog_scan": TruffleHogTool(self.project_root), + "npm_audit": NpmAuditTool(self.project_root), + "safety_scan": SafetyTool(self.project_root), + "osv_scan": OSVScannerTool(self.project_root), + } - # 外部安全工具 - self.tools["semgrep_scan"] = SemgrepTool(self.project_root) - self.tools["bandit_scan"] = BanditTool(self.project_root) - self.tools["gitleaks_scan"] = GitleaksTool(self.project_root) - self.tools["trufflehog_scan"] = TruffleHogTool(self.project_root) - self.tools["npm_audit"] = NpmAuditTool(self.project_root) - self.tools["safety_scan"] = SafetyTool(self.project_root) - self.tools["osv_scan"] = OSVScannerTool(self.project_root) + # RAG 工具(Analysis 用于安全相关代码搜索) + if self.retriever: + self.analysis_tools["security_search"] = SecurityCodeSearchTool(self.retriever) + self.analysis_tools["function_context"] = FunctionContextTool(self.retriever) - # 沙箱工具 + # ============ Verification Agent 专属工具 ============ + # 职责:漏洞验证、PoC 执行、误报排除 + self.verification_tools = { + **base_tools, + # 验证工具 + "vulnerability_validation": VulnerabilityValidationTool(self.llm_service), + "dataflow_analysis": DataFlowAnalysisTool(self.llm_service), + } + + # 沙箱工具(仅 Verification Agent 可用) try: self.sandbox_manager = SandboxManager( image=settings.SANDBOX_IMAGE, @@ -344,14 +247,20 @@ class AgentRunner: cpu_limit=settings.SANDBOX_CPU_LIMIT, ) - self.tools["sandbox_exec"] = SandboxTool(self.sandbox_manager) - self.tools["sandbox_http"] = SandboxHttpTool(self.sandbox_manager) - self.tools["verify_vulnerability"] = VulnerabilityVerifyTool(self.sandbox_manager) + self.verification_tools["sandbox_exec"] = SandboxTool(self.sandbox_manager) + self.verification_tools["sandbox_http"] = SandboxHttpTool(self.sandbox_manager) + self.verification_tools["verify_vulnerability"] = VulnerabilityVerifyTool(self.sandbox_manager) except Exception as e: logger.warning(f"Sandbox initialization failed: {e}") - await self.event_emitter.emit_info(f"✅ 已加载 {len(self.tools)} 个工具") + # 统计总工具数 + total_tools = len(set( + list(self.recon_tools.keys()) + + list(self.analysis_tools.keys()) + + list(self.verification_tools.keys()) + )) + await self.event_emitter.emit_info(f"已加载 {total_tools} 个工具") async def _build_graph(self): """构建 LangGraph 审计图""" @@ -360,25 +269,28 @@ class AgentRunner: # 导入 Agent from app.services.agent.agents import ReconAgent, AnalysisAgent, VerificationAgent - # 创建 Agent 实例 + # 创建 Agent 实例(每个 Agent 使用专属工具集) recon_agent = ReconAgent( llm_service=self.llm_service, - tools=self.tools, + tools=self.recon_tools, # Recon 专属工具 event_emitter=self.event_emitter, ) analysis_agent = AnalysisAgent( llm_service=self.llm_service, - tools=self.tools, + tools=self.analysis_tools, # Analysis 专属工具 event_emitter=self.event_emitter, ) verification_agent = VerificationAgent( llm_service=self.llm_service, - tools=self.tools, + tools=self.verification_tools, # Verification 专属工具 event_emitter=self.event_emitter, ) + # 🔥 保存 Agent 引用以便取消时传播信号 + self._agents = [recon_agent, analysis_agent, verification_agent] + # 创建节点 recon_node = ReconNode(recon_agent, self.event_emitter) analysis_node = AnalysisNode(analysis_agent, self.event_emitter) @@ -481,6 +393,10 @@ class AgentRunner: "iteration": 0, "max_iterations": self.task.max_iterations or 50, "should_continue_analysis": False, + # 🔥 Agent 协作交接信息 + "recon_handoff": None, + "analysis_handoff": None, + "verification_handoff": None, "messages": [], "events": [], "summary": None, @@ -556,6 +472,33 @@ class AgentRunner: graph_state = self.graph.get_state(run_config) final_state = graph_state.values if graph_state else {} + # 🔥 检查是否有错误 + error = final_state.get("error") + if error: + # 检查是否是 LLM 认证错误 + error_str = str(error) + if "AuthenticationError" in error_str or "API key" in error_str or "invalid_api_key" in error_str: + error_message = "LLM API 密钥配置错误。请检查环境变量 LLM_API_KEY 或配置中的 API 密钥是否正确。" + logger.error(f"LLM authentication error: {error}") + else: + error_message = error_str + + duration_ms = int((time.time() - start_time) * 1000) + + # 标记任务为失败 + await self._update_task_status(AgentTaskStatus.FAILED, error_message) + await self.event_emitter.emit_task_error(error_message) + + yield StreamEvent( + event_type=StreamEventType.TASK_ERROR, + sequence=self.stream_handler._next_sequence(), + data={ + "error": error_message, + "message": f"❌ 任务失败: {error_message}", + }, + ) + return + # 6. 保存发现 findings = final_state.get("findings", []) await self._save_findings(findings) diff --git a/backend/app/services/agent/json_parser.py b/backend/app/services/agent/json_parser.py new file mode 100644 index 0000000..ec9c2cd --- /dev/null +++ b/backend/app/services/agent/json_parser.py @@ -0,0 +1,251 @@ +""" +Agent JSON 解析工具 +从 LLM 响应中安全地解析 JSON,参考 llm/service.py 的实现 +""" + +import json +import re +import logging +from typing import Dict, Any, List, Optional, Union + +logger = logging.getLogger(__name__) + +# 尝试导入 json-repair 库 +try: + from json_repair import repair_json + JSON_REPAIR_AVAILABLE = True +except ImportError: + JSON_REPAIR_AVAILABLE = False + logger.debug("json-repair library not available") + + +class AgentJsonParser: + """Agent 专用的 JSON 解析器""" + + @staticmethod + def clean_text(text: str) -> str: + """清理文本中的控制字符""" + if not text: + return "" + # 移除 BOM 和零宽字符 + text = text.replace('\ufeff', '').replace('\u200b', '').replace('\u200c', '').replace('\u200d', '') + return text + + @staticmethod + def fix_json_format(text: str) -> str: + """修复常见的 JSON 格式问题""" + text = text.strip() + # 移除尾部逗号 + text = re.sub(r',(\s*[}\]])', r'\1', text) + # 修复未转义的换行符(在字符串值中) + text = re.sub(r':\s*"([^"]*)\n([^"]*)"', r': "\1\\n\2"', text) + return text + + @classmethod + def extract_from_markdown(cls, text: str) -> Dict[str, Any]: + """从 markdown 代码块提取 JSON""" + match = re.search(r'```(?:json)?\s*(\{[\s\S]*?\})\s*```', text) + if match: + return json.loads(match.group(1)) + raise ValueError("No markdown code block found") + + @classmethod + def extract_json_object(cls, text: str) -> Dict[str, Any]: + """智能提取 JSON 对象""" + start_idx = text.find('{') + if start_idx == -1: + raise ValueError("No JSON object found") + + # 考虑字符串内的花括号和转义字符 + brace_count = 0 + in_string = False + escape_next = False + end_idx = -1 + + for i in range(start_idx, len(text)): + char = text[i] + + if escape_next: + escape_next = False + continue + + if char == '\\': + escape_next = True + continue + + if char == '"' and not escape_next: + in_string = not in_string + continue + + if not in_string: + if char == '{': + brace_count += 1 + elif char == '}': + brace_count -= 1 + if brace_count == 0: + end_idx = i + 1 + break + + if end_idx == -1: + # 如果找不到完整的 JSON,尝试使用最后一个 } + last_brace = text.rfind('}') + if last_brace > start_idx: + end_idx = last_brace + 1 + else: + raise ValueError("Incomplete JSON object") + + json_str = text[start_idx:end_idx] + # 修复格式问题 + json_str = re.sub(r',(\s*[}\]])', r'\1', json_str) + + return json.loads(json_str) + + @classmethod + def fix_truncated_json(cls, text: str) -> Dict[str, Any]: + """修复截断的 JSON""" + start_idx = text.find('{') + if start_idx == -1: + raise ValueError("Cannot fix truncated JSON") + + json_str = text[start_idx:] + + # 计算缺失的闭合符号 + open_braces = json_str.count('{') + close_braces = json_str.count('}') + open_brackets = json_str.count('[') + close_brackets = json_str.count(']') + + # 补全缺失的闭合符号 + json_str += ']' * max(0, open_brackets - close_brackets) + json_str += '}' * max(0, open_braces - close_braces) + + # 修复格式 + json_str = re.sub(r',(\s*[}\]])', r'\1', json_str) + return json.loads(json_str) + + @classmethod + def repair_with_library(cls, text: str) -> Dict[str, Any]: + """使用 json-repair 库修复损坏的 JSON""" + if not JSON_REPAIR_AVAILABLE: + raise ValueError("json-repair library not available") + + start_idx = text.find('{') + if start_idx == -1: + raise ValueError("No JSON object found for repair") + + end_idx = text.rfind('}') + if end_idx > start_idx: + json_str = text[start_idx:end_idx + 1] + else: + json_str = text[start_idx:] + + repaired = repair_json(json_str, return_objects=True) + + if isinstance(repaired, dict): + return repaired + + raise ValueError(f"json-repair returned unexpected type: {type(repaired)}") + + @classmethod + def parse(cls, text: str, default: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + 从 LLM 响应中解析 JSON(增强版) + + Args: + text: LLM 响应文本 + default: 解析失败时返回的默认值,如果为 None 则抛出异常 + + Returns: + 解析后的字典 + """ + if not text or not text.strip(): + if default is not None: + logger.warning("LLM 响应为空,返回默认值") + return default + raise ValueError("LLM 响应内容为空") + + clean = cls.clean_text(text) + + # 尝试多种方式解析 + attempts = [ + ("直接解析", lambda: json.loads(text)), + ("清理后解析", lambda: json.loads(cls.fix_json_format(clean))), + ("Markdown 提取", lambda: cls.extract_from_markdown(text)), + ("智能提取", lambda: cls.extract_json_object(clean)), + ("截断修复", lambda: cls.fix_truncated_json(clean)), + ("json-repair", lambda: cls.repair_with_library(text)), + ] + + last_error = None + for name, attempt in attempts: + try: + result = attempt() + if result and isinstance(result, dict): + if name != "直接解析": + logger.debug(f"✅ JSON 解析成功(方法: {name})") + return result + except Exception as e: + last_error = e + logger.debug(f"JSON 解析方法 '{name}' 失败: {e}") + + # 所有尝试都失败 + if default is not None: + logger.warning(f"JSON 解析失败,返回默认值。原始内容: {text[:200]}...") + return default + + logger.error(f"❌ 无法解析 JSON,原始内容: {text[:500]}...") + raise ValueError(f"无法解析 JSON: {last_error}") + + @classmethod + def parse_findings(cls, text: str) -> List[Dict[str, Any]]: + """ + 专门解析 findings 列表 + + Args: + text: LLM 响应文本 + + Returns: + findings 列表(每个元素都是字典) + """ + try: + result = cls.parse(text, default={"findings": []}) + findings = result.get("findings", []) + + # 确保每个 finding 都是字典 + valid_findings = [] + for f in findings: + if isinstance(f, dict): + valid_findings.append(f) + elif isinstance(f, str): + # 尝试将字符串解析为 JSON + try: + parsed = json.loads(f) + if isinstance(parsed, dict): + valid_findings.append(parsed) + except json.JSONDecodeError: + logger.warning(f"跳过无效的 finding(字符串): {f[:100]}...") + else: + logger.warning(f"跳过无效的 finding(类型: {type(f)})") + + return valid_findings + + except Exception as e: + logger.error(f"解析 findings 失败: {e}") + return [] + + @classmethod + def safe_get(cls, data: Union[Dict, str, Any], key: str, default: Any = None) -> Any: + """ + 安全地从数据中获取值 + + Args: + data: 可能是字典或其他类型 + key: 要获取的键 + default: 默认值 + + Returns: + 获取的值或默认值 + """ + if isinstance(data, dict): + return data.get(key, default) + return default diff --git a/backend/app/services/agent/tools/code_analysis_tool.py b/backend/app/services/agent/tools/code_analysis_tool.py index c97e51e..6d50391 100644 --- a/backend/app/services/agent/tools/code_analysis_tool.py +++ b/backend/app/services/agent/tools/code_analysis_tool.py @@ -79,9 +79,25 @@ class CodeAnalysisTool(AgentTool): **kwargs ) -> ToolResult: """执行代码分析""" + import asyncio + try: - # 构建分析结果 - analysis = await self.llm_service.analyze_code(code, language) + # 限制代码长度,避免超时 + max_code_length = 50000 # 约 50KB + if len(code) > max_code_length: + code = code[:max_code_length] + "\n\n... (代码已截断,仅分析前 50000 字符)" + + # 添加超时保护(5分钟) + try: + analysis = await asyncio.wait_for( + self.llm_service.analyze_code(code, language), + timeout=300.0 # 5分钟超时 + ) + except asyncio.TimeoutError: + return ToolResult( + success=False, + error="代码分析超时(超过5分钟)。代码可能过长或过于复杂,请尝试分析较小的代码片段。", + ) issues = analysis.get("issues", []) diff --git a/backend/app/services/agent/tools/external_tools.py b/backend/app/services/agent/tools/external_tools.py index 8fb3caf..e863ef7 100644 --- a/backend/app/services/agent/tools/external_tools.py +++ b/backend/app/services/agent/tools/external_tools.py @@ -109,10 +109,14 @@ Semgrep 是业界领先的静态分析工具,支持 30+ 种编程语言。 """执行 Semgrep 扫描""" # 检查 semgrep 是否可用 if not await self._check_semgrep(): - return ToolResult( - success=False, - error="Semgrep 未安装。请使用 'pip install semgrep' 安装。", - ) + # 尝试自动安装 + logger.info("Semgrep 未安装,尝试自动安装...") + install_success = await self._try_install_semgrep() + if not install_success: + return ToolResult( + success=False, + error="Semgrep 未安装。请使用 'pip install semgrep' 安装,或联系管理员安装。", + ) # 构建完整路径 full_path = os.path.normpath(os.path.join(self.project_root, target_path)) @@ -216,6 +220,30 @@ Semgrep 是业界领先的静态分析工具,支持 30+ 种编程语言。 return proc.returncode == 0 except: return False + + async def _try_install_semgrep(self) -> bool: + """尝试自动安装 Semgrep""" + try: + logger.info("正在安装 Semgrep...") + proc = await asyncio.create_subprocess_exec( + "pip", "install", "semgrep", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=120) + if proc.returncode == 0: + logger.info("Semgrep 安装成功") + # 验证安装 + return await self._check_semgrep() + else: + logger.warning(f"Semgrep 安装失败: {stderr.decode()[:200]}") + return False + except asyncio.TimeoutError: + logger.warning("Semgrep 安装超时") + return False + except Exception as e: + logger.warning(f"Semgrep 安装出错: {e}") + return False # ============ Bandit 工具 (Python) ============ @@ -422,7 +450,11 @@ Gitleaks 是专业的密钥检测工具,支持 150+ 种密钥类型。 if not await self._check_gitleaks(): return ToolResult( success=False, - error="Gitleaks 未安装。请从 https://github.com/gitleaks/gitleaks 安装。", + error="Gitleaks 未安装。Gitleaks 需要手动安装,请参考: https://github.com/gitleaks/gitleaks/releases\n" + "安装方法:\n" + "- macOS: brew install gitleaks\n" + "- Linux: 下载二进制文件并添加到 PATH\n" + "- Windows: 下载二进制文件并添加到 PATH", ) full_path = os.path.normpath(os.path.join(self.project_root, target_path)) diff --git a/backend/app/services/agent/tools/pattern_tool.py b/backend/app/services/agent/tools/pattern_tool.py index 09a7da8..37e22ed 100644 --- a/backend/app/services/agent/tools/pattern_tool.py +++ b/backend/app/services/agent/tools/pattern_tool.py @@ -291,8 +291,19 @@ class PatternMatchTool(AgentTool): return f"""快速扫描代码中的危险模式和常见漏洞。 使用正则表达式检测已知的不安全代码模式。 +⚠️ 重要:此工具需要代码内容作为输入,不是目录路径! +使用步骤: +1. 先用 read_file 工具读取文件内容 +2. 然后将读取的代码内容传递给此工具的 code 参数 + 支持的漏洞类型: {vuln_types} +输入参数: +- code (必需): 要扫描的代码内容(字符串) +- file_path (可选): 文件路径,用于上下文 +- pattern_types (可选): 要检测的漏洞类型列表,如 ['sql_injection', 'xss'] +- language (可选): 编程语言,如 'python', 'php', 'javascript' + 这是一个快速扫描工具,可以在分析开始时使用来快速发现潜在问题。 发现的问题需要进一步分析确认。""" diff --git a/backend/app/services/agent/tools/rag_tool.py b/backend/app/services/agent/tools/rag_tool.py index 7c8b9c6..527b95d 100644 --- a/backend/app/services/agent/tools/rag_tool.py +++ b/backend/app/services/agent/tools/rag_tool.py @@ -189,10 +189,27 @@ class SecurityCodeSearchTool(AgentTool): ) except Exception as e: - return ToolResult( - success=False, - error=f"安全代码搜索失败: {str(e)}", - ) + error_msg = str(e) + # 提供更友好的错误信息 + if "401" in error_msg or "Unauthorized" in error_msg: + return ToolResult( + success=False, + error=f"安全代码搜索失败: API 认证失败(401 Unauthorized)。\n" + f"请检查系统配置中的 LLM API Key 是否正确设置。\n" + f"错误详情: {error_msg[:200]}", + ) + elif "403" in error_msg or "Forbidden" in error_msg: + return ToolResult( + success=False, + error=f"安全代码搜索失败: API 访问被拒绝(403 Forbidden)。\n" + f"请检查 API Key 是否有足够的权限。\n" + f"错误详情: {error_msg[:200]}", + ) + else: + return ToolResult( + success=False, + error=f"安全代码搜索失败: {error_msg[:500]}", + ) class FunctionContextInput(BaseModel): diff --git a/backend/app/services/llm/adapters/litellm_adapter.py b/backend/app/services/llm/adapters/litellm_adapter.py index 81a9d90..ea104e6 100644 --- a/backend/app/services/llm/adapters/litellm_adapter.py +++ b/backend/app/services/llm/adapters/litellm_adapter.py @@ -177,6 +177,85 @@ class LiteLLMAdapter(BaseLLMAdapter): finish_reason=choice.finish_reason, ) + async def stream_complete(self, request: LLMRequest): + """ + 流式调用 LLM,逐 token 返回 + + Yields: + dict: {"type": "token", "content": str} 或 {"type": "done", "content": str, "usage": dict} + """ + import litellm + + await self.validate_config() + + litellm.cache = None + litellm.drop_params = True + + messages = [{"role": msg.role, "content": msg.content} for msg in request.messages] + + kwargs = { + "model": self._litellm_model, + "messages": messages, + "temperature": request.temperature if request.temperature is not None else self.config.temperature, + "max_tokens": request.max_tokens if request.max_tokens is not None else self.config.max_tokens, + "top_p": request.top_p if request.top_p is not None else self.config.top_p, + "stream": True, # 启用流式输出 + } + + if self.config.api_key and self.config.api_key != "ollama": + kwargs["api_key"] = self.config.api_key + + if self._api_base: + kwargs["api_base"] = self._api_base + + kwargs["timeout"] = self.config.timeout + + accumulated_content = "" + + try: + response = await litellm.acompletion(**kwargs) + + async for chunk in response: + if not chunk.choices: + continue + + delta = chunk.choices[0].delta + content = getattr(delta, "content", "") or "" + finish_reason = chunk.choices[0].finish_reason + + if content: + accumulated_content += content + yield { + "type": "token", + "content": content, + "accumulated": accumulated_content, + } + + if finish_reason: + # 流式完成 + usage = None + if hasattr(chunk, "usage") and chunk.usage: + usage = { + "prompt_tokens": chunk.usage.prompt_tokens or 0, + "completion_tokens": chunk.usage.completion_tokens or 0, + "total_tokens": chunk.usage.total_tokens or 0, + } + + yield { + "type": "done", + "content": accumulated_content, + "usage": usage, + "finish_reason": finish_reason, + } + break + + except Exception as e: + yield { + "type": "error", + "error": str(e), + "accumulated": accumulated_content, + } + async def validate_config(self) -> bool: """验证配置""" # Ollama 不需要 API Key diff --git a/backend/app/services/llm/service.py b/backend/app/services/llm/service.py index 610eaef..dee4eec 100644 --- a/backend/app/services/llm/service.py +++ b/backend/app/services/llm/service.py @@ -6,7 +6,7 @@ LLM服务 - 代码分析核心服务 import json import re import logging -from typing import Dict, Any, Optional +from typing import Dict, Any, Optional, List from .types import LLMConfig, LLMProvider, LLMMessage, LLMRequest, DEFAULT_MODELS from .factory import LLMFactory from app.core.config import settings @@ -36,15 +36,23 @@ class LLMService: @property def config(self) -> LLMConfig: - """获取LLM配置(优先使用用户配置,然后使用系统配置)""" + """ + 获取LLM配置 + + 🔥 优先级(从高到低): + 1. 数据库用户配置(系统配置页面保存的配置) + 2. 环境变量配置(.env 文件中的配置) + + 如果用户配置中某个字段为空,则自动回退到环境变量。 + """ if self._config is None: user_llm_config = self._user_config.get('llmConfig', {}) - # 优先使用用户配置的provider,否则使用系统配置 + # 🔥 Provider 优先级:用户配置 > 环境变量 provider_str = user_llm_config.get('llmProvider') or getattr(settings, 'LLM_PROVIDER', 'openai') provider = self._parse_provider(provider_str) - # 获取API Key - 优先级:用户配置 > 系统通用配置 > 系统平台专属配置 + # 🔥 API Key 优先级:用户配置 > 环境变量通用配置 > 环境变量平台专属配置 api_key = ( user_llm_config.get('llmApiKey') or getattr(settings, 'LLM_API_KEY', '') or @@ -52,33 +60,33 @@ class LLMService: self._get_provider_api_key(provider) ) - # 获取Base URL + # 🔥 Base URL 优先级:用户配置 > 环境变量 base_url = ( user_llm_config.get('llmBaseUrl') or getattr(settings, 'LLM_BASE_URL', None) or self._get_provider_base_url(provider) ) - # 获取模型 + # 🔥 Model 优先级:用户配置 > 环境变量 > 默认模型 model = ( user_llm_config.get('llmModel') or getattr(settings, 'LLM_MODEL', '') or DEFAULT_MODELS.get(provider, 'gpt-4o-mini') ) - # 获取超时时间(用户配置是毫秒,系统配置是秒) + # 🔥 Timeout 优先级:用户配置(毫秒) > 环境变量(秒) timeout_ms = user_llm_config.get('llmTimeout') if timeout_ms: # 用户配置是毫秒,转换为秒 timeout = int(timeout_ms / 1000) if timeout_ms > 1000 else int(timeout_ms) else: - # 系统配置是秒 + # 环境变量是秒 timeout = int(getattr(settings, 'LLM_TIMEOUT', 150)) - # 获取温度 + # 🔥 Temperature 优先级:用户配置 > 环境变量 temperature = user_llm_config.get('llmTemperature') if user_llm_config.get('llmTemperature') is not None else float(getattr(settings, 'LLM_TEMPERATURE', 0.1)) - # 获取最大token数 + # 🔥 Max Tokens 优先级:用户配置 > 环境变量 max_tokens = user_llm_config.get('llmMaxTokens') or int(getattr(settings, 'LLM_MAX_TOKENS', 4096)) self._config = LLMConfig( @@ -394,6 +402,83 @@ Please analyze the following code: # 重新抛出异常,让调用者处理 raise + async def chat_completion_raw( + self, + messages: List[Dict[str, str]], + temperature: float = 0.1, + max_tokens: int = 4096, + ) -> Dict[str, Any]: + """ + 🔥 Agent 使用的原始聊天完成接口(兼容旧接口) + + Args: + messages: 消息列表,格式为 [{"role": "user", "content": "..."}] + temperature: 温度参数 + max_tokens: 最大token数 + + Returns: + 包含 content 和 usage 的字典 + """ + # 转换消息格式 + llm_messages = [ + LLMMessage(role=msg["role"], content=msg["content"]) + for msg in messages + ] + + request = LLMRequest( + messages=llm_messages, + temperature=temperature, + max_tokens=max_tokens, + ) + + adapter = LLMFactory.create_adapter(self.config) + response = await adapter.complete(request) + + return { + "content": response.content, + "usage": { + "prompt_tokens": response.usage.prompt_tokens if response.usage else 0, + "completion_tokens": response.usage.completion_tokens if response.usage else 0, + "total_tokens": response.usage.total_tokens if response.usage else 0, + }, + } + + async def chat_completion_stream( + self, + messages: List[Dict[str, str]], + temperature: float = 0.1, + max_tokens: int = 4096, + ): + """ + 流式聊天完成接口,逐 token 返回 + + Args: + messages: 消息列表 + temperature: 温度参数 + max_tokens: 最大token数 + + Yields: + dict: {"type": "token", "content": str} 或 {"type": "done", ...} + """ + from .adapters.litellm_adapter import LiteLLMAdapter + + llm_messages = [ + LLMMessage(role=msg["role"], content=msg["content"]) + for msg in messages + ] + + request = LLMRequest( + messages=llm_messages, + temperature=temperature, + max_tokens=max_tokens, + ) + + # 使用 LiteLLM adapter 进行流式调用 + adapter = LiteLLMAdapter(self.config) + + async for chunk in adapter.stream_complete(request): + yield chunk + def _parse_json(self, text: str) -> Dict[str, Any]: """从LLM响应中解析JSON(增强版)""" 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 new file mode 100644 index 0000000000000000000000000000000000000000..c95e62eda5e3d74c69650418a987c8971eadc0c6 GIT binary patch literal 628400 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⪚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 z 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 = { - // 🧠 LLM 核心事件 - 最重要! + // LLM 核心事件 llm_start: , llm_thought: , llm_decision: , - llm_action: , - llm_observation: , llm_complete: , // 阶段相关 @@ -43,7 +41,7 @@ const eventTypeIcons: Record = { phase_complete: , thinking: , - // 工具相关 - LLM 决定的工具调用 + // 工具相关 tool_call: , tool_result: , tool_error: , @@ -65,14 +63,12 @@ const eventTypeIcons: Record = { task_cancel: , }; -// 事件类型颜色映射 - 🔥 LLM 事件突出显示 +// 事件类型颜色映射 const eventTypeColors: Record = { - // 🧠 LLM 核心事件 - 使用紫色系突出 + // 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_action: "text-orange-300 font-medium", - llm_observation: "text-blue-300", + 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", // 阶段相关 @@ -411,7 +407,7 @@ export default function AgentAuditPage() { {/* 左侧:执行日志 */}

- {/* 🧠 LLM 思考过程展示区域 - 核心!展示 LLM 的大脑活动 */} + {/* LLM 思考过程展示区域 */} {(isThinking || thinking) && showThinking && (
- 🧠 LLM Thinking - Agent 的大脑正在工作 + LLM Thinking + Agent 思考过程
{isThinking && ( @@ -438,7 +434,7 @@ export default function AgentAuditPage() {
- {thinking || "🤔 正在思考下一步..."} + {thinking || "正在思考下一步..."} {isThinking && }
@@ -446,7 +442,7 @@ export default function AgentAuditPage() {
)} - {/* 🔧 LLM 工具调用展示区域 - LLM 决定调用的工具 */} + {/* 工具调用展示区域 */} {toolCalls.length > 0 && showToolDetails && (
- 🔧 LLM Tool Calls - LLM 决定调用的工具 + Tool Calls + 工具调用记录
{toolCalls.length} 次调用 @@ -488,9 +484,9 @@ export default function AgentAuditPage() {
- LLM Execution Log + Execution Log
- LLM 思考 & 工具调用记录 + 执行日志 {(isStreaming || isStreamConnected) && ( @@ -708,7 +704,7 @@ function StatusBadge({ status }: { status: string }) { ); } -// 事件行组件 - 增强 LLM 事件展示 +// 事件行组件 function EventLine({ event }: { event: AgentEvent }) { const icon = eventTypeIcons[event.event_type] || ; const colorClass = eventTypeColors[event.event_type] || "text-gray-400"; @@ -717,19 +713,19 @@ function EventLine({ event }: { event: AgentEvent }) { ? new Date(event.timestamp).toLocaleTimeString("zh-CN", { hour12: false }) : ""; - // LLM 思考事件特殊处理 - 展示多行内容 + // 特殊事件处理 const isLLMThought = event.event_type === "llm_thought"; const isLLMDecision = event.event_type === "llm_decision"; - const isLLMAction = event.event_type === "llm_action"; - const isImportantLLMEvent = isLLMThought || isLLMDecision || isLLMAction; + const isToolCall = event.event_type === "tool_call"; + const isToolResult = event.event_type === "tool_result"; - // LLM 事件背景色 + // 背景色 const bgClass = isLLMThought ? "bg-purple-950/40 border-l-2 border-purple-600" : isLLMDecision ? "bg-yellow-950/30 border-l-2 border-yellow-600" - : isLLMAction - ? "bg-orange-950/30 border-l-2 border-orange-600" + : isToolCall || isToolResult + ? "bg-gray-900/30" : ""; return ( @@ -738,7 +734,7 @@ function EventLine({ event }: { event: AgentEvent }) { {timestamp} {icon} - + {event.message} {event.tool_duration_ms && ( ({event.tool_duration_ms}ms)