Skip to content

Commit 54e066f

Browse files
committed
refactor: migrate services package under pure_auto_codeql
Move LLM/LSP/path-selection and related services into pure_auto_codeql.services with nested top-level re-export shims. Resolve repo-root paths via get_repo_root for MCP and language config.
1 parent d76de8c commit 54e066f

61 files changed

Lines changed: 6256 additions & 6044 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/package_architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ migration window.
1717
| `prompts` | `pure_auto_codeql.prompts` | Migrated (top-level re-export shim kept; `.md` assets co-located) |
1818
| `utils` | `pure_auto_codeql.utils` | Migrated (top-level re-export shim kept) |
1919
| `tools` | `pure_auto_codeql.tools` | Migrated Python modules (top-level re-export shim kept; `tools/mcp_ripgrep` stays at repo root) |
20+
| `services` | `pure_auto_codeql.services` | Migrated (top-level re-export shim kept) |
2021
| `api` | `pure_auto_codeql.api` | Planned staged migration |
2122
| `core` | `pure_auto_codeql.core` | Planned staged migration |
22-
| `services` | `pure_auto_codeql.services` | Planned staged migration |
2323

2424
## Compatibility Surface
2525

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""服务层模块
2+
3+
提供各种业务服务,包括LLM服务、LSP服务、分析服务等。
4+
"""
5+
6+
from .lsp_service import CodeQLLSPService
7+
from .codeql_prompt import build_placeholder_map, apply_placeholders
8+
from .codeql_syntax import CodeQLSyntaxSession
9+
from .codeql_execution import CodeQLExecutionService, CodeQLExecutionResult
10+
from .llm_service import MultiAgentAnalyzer, AgentResult
11+
from .language_detector import LanguageDetector
12+
from .knowledge_base import (
13+
KnowledgeBaseFactory,
14+
PythonKnowledgeBase,
15+
)
16+
17+
__all__ = [
18+
"CodeQLLSPService",
19+
"KnowledgeBaseFactory",
20+
"PythonKnowledgeBase",
21+
"build_placeholder_map",
22+
"apply_placeholders",
23+
"CodeQLSyntaxSession",
24+
"CodeQLExecutionService",
25+
"CodeQLExecutionResult",
26+
"MultiAgentAnalyzer",
27+
"AgentResult",
28+
"LanguageDetector"
29+
]
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
from pathlib import Path
2+
from typing import Dict, Optional, TYPE_CHECKING
3+
4+
from langchain_mcp_adapters.client import MultiServerMCPClient # noqa: F401 # 预留给未来可能的直接使用
5+
6+
from utils.logger import get_logger
7+
from .mcp_language_config import MCPLanguageConfigService
8+
from pure_auto_codeql.paths import prompts_dir
9+
from pure_auto_codeql.paths import get_repo_root
10+
11+
if TYPE_CHECKING:
12+
from pure_auto_codeql.agents.cve_analysis_agent import CVEAnalysisAgent as _CVEAnalysisAgent
13+
from pure_auto_codeql.agents.unified_sink_path_agent import UnifiedSinkPathAgent as _UnifiedSinkPathAgent
14+
from pure_auto_codeql.agents.unified_source_analysis_agent import UnifiedSourceAnalysisAgent as _UnifiedSourceAnalysisAgent
15+
from pure_auto_codeql.agents.path_analysis_agent import PathAnalysisAgent as _PathAnalysisAgent
16+
from pure_auto_codeql.agents.codeql_gen_agents.codeql_gen_agent import CodeQLGenAgent as _CodeQLGenAgent
17+
from pure_auto_codeql.agents.codeql_gen_agents.codeql_error_agent import CodeQLErrorAgent as _CodeQLErrorAgent
18+
from pure_auto_codeql.agents.codeql_gen_agents.codeql_fix_inplace_agent import (
19+
CodeQLFixInplaceAgent as _CodeQLFixInplaceAgent,
20+
)
21+
from pure_auto_codeql.agents.codeql_gen_agents.codeql_breakpoint_detect_agent import (
22+
CodeQLBreakpointAgent as _CodeQLBreakpointAgent,
23+
)
24+
25+
26+
logger = get_logger(__name__)
27+
28+
29+
AGENT_TYPES: Dict[str, str] = {
30+
"cve_analysis": "pure_auto_codeql.agents.cve_analysis_agent.CVEAnalysisAgent",
31+
"unified_sink_path": "pure_auto_codeql.agents.unified_sink_path_agent.UnifiedSinkPathAgent",
32+
"unified_source_analysis": "pure_auto_codeql.agents.unified_source_analysis_agent.UnifiedSourceAnalysisAgent",
33+
"path_analysis": "pure_auto_codeql.agents.path_analysis_agent.PathAnalysisAgent",
34+
"codeql_gen": "pure_auto_codeql.agents.codeql_gen_agents.codeql_gen_agent.CodeQLGenAgent",
35+
"codeql_error": "pure_auto_codeql.agents.codeql_gen_agents.codeql_error_agent.CodeQLErrorAgent",
36+
"codeql_fix_inplace": "pure_auto_codeql.agents.codeql_gen_agents.codeql_fix_inplace_agent.CodeQLFixInplaceAgent",
37+
"codeql_breakpoint_detect": "pure_auto_codeql.agents.codeql_gen_agents.codeql_breakpoint_detect_agent.CodeQLBreakpointAgent",
38+
"template_refinement": "pure_auto_codeql.agents.codeql_gen_agents.template_refinement_agent.TemplateRefinementAgent",
39+
"sink_verification": "pure_auto_codeql.agents.sink_verification_agent.SinkVerificationAgent",
40+
"source_verification": "pure_auto_codeql.agents.source_verification_agent.SourceVerificationAgent",
41+
}
42+
43+
AGENT_MCP_PROFILES: Dict[str, list[str]] = {
44+
"cve_analysis": [],
45+
"unified_sink_path": ["filesystem", "ripgrep", "language-server"],
46+
# "unified_source_analysis": ["language-server"],
47+
"unified_source_analysis": ["tree_sitter", "language-server", "filesystem"],
48+
"source_analysis": ["tree_sitter", "language-server", "filesystem"],
49+
"path_analysis": ["tree_sitter", "ripgrep"],
50+
"codeql_gen": [],
51+
"codeql_generation": ["filesystem", "ripgrep", "language-server"],
52+
"codeql_error": ["filesystem", "ripgrep", "language-server"],
53+
"codeql_fix_inplace": ["filesystem", "ripgrep", "language-server"],
54+
"codeql_breakpoint_detect": ["tree_sitter", "ripgrep"],
55+
"template_refinement": ["filesystem"],
56+
"source_sink_fallback": ["ripgrep"], # 需要 ripgrep 以支持 lsplookup 工具
57+
"sink_verification": ["ripgrep", "language-server"], # 需要 ripgrep 和 LSP 以支持 LSPFunctionLookupTool
58+
"source_verification": ["ripgrep", "language-server"], # 需要 ripgrep 和 LSP 以支持 LSPFunctionLookupTool
59+
"default": [],
60+
}
61+
62+
63+
class AgentMCPConfigService:
64+
"""
65+
Agent 级别的 MCP 配置管理服务。
66+
"""
67+
68+
def __init__(self, language_config_service: Optional[MCPLanguageConfigService] = None):
69+
"""
70+
初始化配置服务。
71+
"""
72+
self._language_config_service = language_config_service or MCPLanguageConfigService()
73+
74+
def get_config_for_agent(
75+
self,
76+
agent_type: str,
77+
language: Optional[str] = None,
78+
workspace_path: Optional[str] = None,
79+
) -> Dict[str, Dict]:
80+
# 获取Agent对应的MCP配置文件,如果不存在则使用默认配置
81+
profile = AGENT_MCP_PROFILES.get(agent_type, AGENT_MCP_PROFILES["default"])
82+
83+
# 针对 Source 分析类 Agent,根据语言调整 MCP 组合策略
84+
normalized_language = (language or "").lower()
85+
if agent_type in {"unified_source_analysis", "source_analysis"} and normalized_language:
86+
if normalized_language == "java":
87+
# Java: 使用 ripgrep + language-server + filesystem
88+
profile = ["ripgrep", "language-server"]
89+
elif normalized_language == "python":
90+
# Python: 使用 tree_sitter + language-server + filesystem
91+
profile = ["language-server", "ripgrep", "tree_sitter"]
92+
elif normalized_language in {"c", "cpp", "c++"}:
93+
# C/C++: 使用 tree_sitter + filesystem
94+
profile = ["tree_sitter", "ripgrep"]
95+
96+
# 构建完整的MCP服务器配置
97+
mcp_servers: Dict[str, Dict] = {}
98+
99+
# 添加文件系统MCP
100+
if "filesystem" in profile:
101+
if not workspace_path:
102+
logger.warning("⚠️ filesystem MCP 需要 workspace_path 参数")
103+
else:
104+
workspace_path_str = str(workspace_path).replace("source_code", "")
105+
workspace_path_obj = Path(workspace_path_str)
106+
parents = workspace_path_obj.parents
107+
repo_root = parents[1] if len(parents) > 1 else workspace_path_obj.parent
108+
109+
# 添加对于错误整理Agent的MCP配置
110+
if agent_type == "template_refinement":
111+
prompts_path = prompts_dir()
112+
mcp_servers["filesystem"] = {
113+
"command": "npx",
114+
"args": [
115+
"-y",
116+
"@modelcontextprotocol/server-filesystem",
117+
str(prompts_path),
118+
],
119+
"transport": "stdio",
120+
}
121+
else:
122+
temp_codeql_path = repo_root / "temp" / "codeql_temp"
123+
prompts_path = prompts_dir()
124+
mcp_servers["filesystem"] = {
125+
"command": "npx",
126+
"args": [
127+
"-y",
128+
"@modelcontextprotocol/server-filesystem",
129+
workspace_path_str,
130+
str(temp_codeql_path),
131+
str(prompts_path),
132+
],
133+
"transport": "stdio",
134+
}
135+
136+
# 添加ripgrep MCP
137+
if "ripgrep" in profile:
138+
mcp_servers["ripgrep"] = {
139+
"command": "node",
140+
"args": [
141+
str(
142+
get_repo_root()
143+
/ "tools"
144+
/ "mcp_ripgrep"
145+
/ "dist"
146+
/ "index.js"
147+
)
148+
],
149+
"transport": "stdio",
150+
}
151+
152+
# 添加 tree_sitter MCP
153+
if "tree_sitter" in profile:
154+
if not workspace_path:
155+
logger.warning("⚠️ tree_sitter MCP 需要 workspace_path 参数")
156+
else:
157+
mcp_servers["tree_sitter"] = {
158+
"command": "uv",
159+
"args": [
160+
"--directory",
161+
str(workspace_path),
162+
"run",
163+
"-m",
164+
"mcp_server_tree_sitter.server",
165+
],
166+
"transport": "stdio",
167+
}
168+
169+
# 添加语言服务器MCP
170+
if "language-server" in profile and language and workspace_path:
171+
try:
172+
language_config = self._language_config_service
173+
if language_config.is_language_supported(language):
174+
lsp_config = language_config.get_language_server_config(language, workspace_path)
175+
mcp_servers["language-server"] = lsp_config
176+
logger.info(f"✓ 已添加 {language} LSP MCP 配置")
177+
else:
178+
logger.info(f"ℹ️ 语言 {language} 不支持 LSP MCP")
179+
except Exception as e: # pragma: no cover - 防御性日志记录
180+
logger.warning(f"⚠️ LSP MCP 配置失败,继续使用基础 MCP: {e}")
181+
elif "language-server" in profile:
182+
logger.warning("⚠️ language-server MCP 需要 language 和 workspace_path 参数")
183+
184+
return mcp_servers
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
"""CodeQL 查询执行与结果整理服务。"""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass
6+
from pathlib import Path
7+
from typing import Callable, Optional
8+
9+
from utils.sarif_config import get_sarif2json_config
10+
from utils.sarif_utils import write_paths_json
11+
12+
13+
@dataclass
14+
class CodeQLExecutionResult:
15+
success: bool
16+
output: str
17+
sarif_path: Optional[str] = None
18+
json_path: Optional[str] = None
19+
paths_count: Optional[int] = None
20+
result_file: Optional[str] = None
21+
preview: Optional[str] = None
22+
23+
@property
24+
def has_results(self) -> bool:
25+
"""检测查询结果是否为空。"""
26+
# 检查paths_count(analyze模式)
27+
if self.paths_count is not None:
28+
return self.paths_count > 0
29+
30+
# 检查output内容(run模式)
31+
if self.output and self.output.strip():
32+
# 检查常见的空结果模式
33+
empty_indicators = [
34+
'No results.',
35+
'No results found',
36+
'0 results',
37+
'Empty result set',
38+
'查询结果为空',
39+
'未找到结果'
40+
]
41+
42+
output_lower = self.output.lower()
43+
for indicator in empty_indicators:
44+
if indicator.lower() in output_lower:
45+
return False
46+
47+
# 检查是否有实际的数据行(非表头、非空行)
48+
lines = [line.strip() for line in self.output.splitlines() if line.strip()]
49+
if len(lines) <= 2: # 只有表头或很少的行
50+
return False
51+
52+
# 检查是否有数据行(包含实际数据)
53+
data_lines = [line for line in lines if not line.startswith('|') or '---' not in line]
54+
return len(data_lines) > 1
55+
56+
return False
57+
58+
59+
class CodeQLExecutionService:
60+
"""包装 CodeQL CLI 调用及 SARIF 后处理。"""
61+
62+
def __init__(
63+
self,
64+
*,
65+
database_path: str,
66+
language: str,
67+
execute_fn: Callable[[str, str, Optional[str]], dict],
68+
decode_fn: Optional[Callable[[str, str, Optional[str]], dict]] = None,
69+
) -> None:
70+
self._database_path = database_path
71+
self._language = language
72+
self._execute_fn = execute_fn
73+
self._decode_fn = decode_fn
74+
75+
def execute(self, query: str, exec_mode: str = "analyze") -> CodeQLExecutionResult:
76+
mode = (exec_mode or "analyze").lower()
77+
78+
if mode == "run" and self._decode_fn:
79+
return self._execute_run_mode(query)
80+
81+
return self._execute_analyze_mode(query)
82+
83+
def _execute_analyze_mode(self, query: str) -> CodeQLExecutionResult:
84+
try:
85+
raw_result = self._execute_fn(query, self._database_path, self._language)
86+
except Exception as exc: # pylint: disable=broad-except
87+
return CodeQLExecutionResult(success=False, output=f"Execution failed: {exc}")
88+
89+
if not raw_result.get("success"):
90+
return CodeQLExecutionResult(
91+
success=False,
92+
output=raw_result.get("output", "Unknown execution error"),
93+
)
94+
95+
sarif_path = raw_result.get("sarif_path")
96+
json_path: Optional[str] = None
97+
paths_count: Optional[int] = None
98+
99+
if sarif_path:
100+
try:
101+
config = get_sarif2json_config()
102+
json_file = Path(sarif_path).with_suffix(".json")
103+
paths_count = write_paths_json(
104+
sarif_path,
105+
str(json_file),
106+
max_results=config.max_results,
107+
threadflow_index=config.threadflow_index,
108+
rule_filter=config.rule_filter,
109+
relative_to=None,
110+
)
111+
json_path = str(json_file)
112+
except Exception:
113+
# SARIF 转换失败时继续返回原结果
114+
json_path = None
115+
paths_count = None
116+
117+
result = CodeQLExecutionResult(
118+
success=True,
119+
output=raw_result.get("output", ""),
120+
sarif_path=sarif_path,
121+
json_path=json_path,
122+
paths_count=paths_count,
123+
)
124+
125+
# 检测空结果并添加提示
126+
result = self._handle_empty_results(result)
127+
128+
return result
129+
130+
def _handle_empty_results(self, result: CodeQLExecutionResult) -> CodeQLExecutionResult:
131+
"""处理空结果检测和用户交互提示。"""
132+
if not result.has_results:
133+
result.output = f"⚠️ 查询执行成功,但未找到匹配结果。\n\n原始输出:\n{result.output}\n\n💡 请检查查询条件或选择是否继续优化查询。"
134+
return result
135+
136+
def _execute_run_mode(self, query: str) -> CodeQLExecutionResult:
137+
try:
138+
raw_result = self._decode_fn(query, self._database_path, self._language) # type: ignore[misc]
139+
except Exception as exc: # pylint: disable=broad-except
140+
return CodeQLExecutionResult(success=False, output=f"Execution failed: {exc}")
141+
142+
if not raw_result.get("success"):
143+
return CodeQLExecutionResult(
144+
success=False,
145+
output=raw_result.get("output", "Unknown execution error"),
146+
)
147+
148+
full_text = raw_result.get("output", "") or ""
149+
result_file = raw_result.get("result_file")
150+
lines = full_text.splitlines()
151+
preview = "\n".join(lines[:40]).strip()
152+
if len(lines) > 40:
153+
preview = f"{preview}\n..."
154+
155+
result = CodeQLExecutionResult(
156+
success=True,
157+
output=full_text,
158+
result_file=result_file,
159+
preview=preview if preview else None,
160+
)
161+
162+
# 检测空结果并添加提示
163+
result = self._handle_empty_results(result)
164+
165+
return result
166+
167+
168+
__all__ = ["CodeQLExecutionService", "CodeQLExecutionResult"]

0 commit comments

Comments
 (0)