diff --git a/config.yaml b/config.yaml index 086e24f..b23ea73 100644 --- a/config.yaml +++ b/config.yaml @@ -31,6 +31,8 @@ mcp: - web_search - file_reader - code_executor + - static_analyzer + - ssh_docker # ── 工具配置 ─────────────────────────────────────────────────── tools: @@ -50,6 +52,51 @@ tools: calculator: precision: 10 + # ── C/C++ 静态分析 ────────────────────────────────────────── + static_analyzer: + default_tool: "cppcheck" # cppcheck | clang-tidy | infer + default_std: "c++17" # c89 | c99 | c11 | c++11 | c++14 | c++17 | c++20 + timeout: 120 # 分析超时(秒) + jobs: 4 # 并行线程数(cppcheck -j 参数) + output_format: "summary" # summary | json | full + max_issues: 500 # 最多返回问题条数 + # 允许分析的目录白名单,空列表表示不限制 + allowed_roots: [ ] + # 各工具的额外默认参数 + tool_extra_args: + cppcheck: "--suppress=missingIncludeSystem --suppress=unmatchedSuppression" + clang-tidy: "--checks=*,-fuchsia-*,-google-*,-zircon-*" + infer: "" + + # ── SSH Docker 部署 ───────────────────────────────────────── + ssh_docker: + default_ssh_port: 22 + default_username: "root" + connect_timeout: 30 # SSH 连接超时(秒) + cmd_timeout: 120 # 单条命令执行超时(秒) + deploy_timeout: 300 # 镜像拉取/部署超时(秒) + default_restart_policy: "unless-stopped" + default_tail_lines: 100 + # 安全:允许操作的服务器白名单,空列表表示不限制 + allowed_hosts: [ ] + # 安全:禁止使用的镜像前缀 + blocked_images: [ ] + # 是否允许 --privileged 模式 + allow_privileged: false + # 已知服务器预设(可选,避免每次传入认证信息) + servers: { } + # 示例: + # servers: + # prod: + # host: "192.168.1.100" + # port: 22 + # username: "deploy" + # key_path: "/home/ci/.ssh/id_rsa" + # staging: + # host: "192.168.1.200" + # port: 22 + # username: "ubuntu" + # password: "" # 留空则读取环境变量 SSH_STAGING_PASSWORD # ── 记忆配置 ─────────────────────────────────────────────────── memory: diff --git a/config/settings.py b/config/settings.py index 6fbfece..5f6b1df 100644 --- a/config/settings.py +++ b/config/settings.py @@ -1,6 +1,6 @@ """ config/settings.py -配置加载与管理模块(新增 OpenAI 专用字段) +配置加载与管理 —— 使用纯字典存储工具配置,通过 settings.tools['tool_name']['key'] 访问 """ import os @@ -16,12 +16,147 @@ except ImportError: # ════════════════════════════════════════════════════════════════ -# 配置数据类 +# 默认配置(与 config.yaml 结构完全对应,作为 fallback) +# ════════════════════════════════════════════════════════════════ + +_DEFAULTS: dict[str, Any] = { + "llm": { + "provider": "openai", + "model_name": "gpt-4o", + "api_key": "", + "api_base_url": "", + "max_tokens": 4096, + "temperature": 0.7, + "timeout": 60, + "max_retries": 3, + "function_calling": True, + "stream": False, + "model_path": "", + "ollama_host": "http://localhost:11434", + }, + "mcp": { + "server_name": "DemoMCPServer", + "transport": "stdio", + "host": "localhost", + "port": 3000, + "enabled_tools": [ + "calculator", "web_search", "file_reader", + "code_executor", "static_analyzer", "ssh_docker", + ], + }, + "tools": { + "calculator": { + "precision": 10, + }, + "web_search": { + "max_results": 5, + "timeout": 10, + "api_key": "", + "engine": "mock", + }, + "file_reader": { + "allowed_root": "./workspace", + "max_file_size_kb": 512, + }, + "code_executor": { + "timeout": 5, + "sandbox": True, + }, + "static_analyzer": { + "default_tool": "cppcheck", + "default_std": "c++17", + "timeout": 120, + "jobs": 4, + "output_format": "summary", + "max_issues": 500, + "allowed_roots": [], + "tool_extra_args": { + "cppcheck": "--suppress=missingIncludeSystem --suppress=unmatchedSuppression", + "clang-tidy": "--checks=*,-fuchsia-*,-google-*,-zircon-*", + "infer": "", + }, + }, + "ssh_docker": { + "default_ssh_port": 22, + "default_username": "root", + "connect_timeout": 30, + "cmd_timeout": 120, + "deploy_timeout": 300, + "default_restart_policy": "unless-stopped", + "default_tail_lines": 100, + "allowed_hosts": [], + "blocked_images": [], + "allow_privileged": False, + "servers": {}, + }, + }, + "memory": { + "max_history": 20, + "enable_long_term": False, + "vector_db_url": "", + }, + "logging": { + "level": "DEBUG", + "enable_file": True, + "log_dir": "./logs", + "log_file": "agent.log", + }, + "agent": { + "max_chain_steps": 10, + "enable_multi_step": True, + "session_timeout": 3600, + "fallback_to_rules": True, + }, +} + + +# ════════════════════════════════════════════════════════════════ +# 工具配置字典视图(支持 settings.tools['web_search']['timeout']) +# ════════════════════════════════════════════════════════════════ + +class ToolsView: + """ + 工具配置字典视图 + + 用法: + settings.tools['web_search']['timeout'] → 10 + settings.tools['static_analyzer']['jobs'] → 4 + settings.tools['ssh_docker']['connect_timeout']→ 30 + settings.tools['ssh_docker']['servers'] → {...} + 'web_search' in settings.tools → True + """ + + def __init__(self, data: dict[str, dict]): + self._data = data + + def __getitem__(self, tool_name: str) -> dict[str, Any]: + if tool_name not in self._data: + raise KeyError( + f"工具 '{tool_name}' 未在配置中定义。" + f"可用工具: {list(self._data.keys())}" + ) + return self._data[tool_name] + + def __contains__(self, tool_name: str) -> bool: + return tool_name in self._data + + def __repr__(self) -> str: + return f"ToolsView({list(self._data.keys())})" + + def get(self, tool_name: str, default: Any = None) -> Any: + return self._data.get(tool_name, default) + + def keys(self): + return self._data.keys() + + +# ════════════════════════════════════════════════════════════════ +# LLM / MCP / Memory / Logging / Agent 轻量配置对象 +# (保留 dataclass 方便属性访问,非工具类配置) # ════════════════════════════════════════════════════════════════ @dataclass class LLMConfig: - """LLM 模型配置(含 OpenAI 专用字段)""" provider: str = "openai" model_name: str = "gpt-4o" api_key: str = "" @@ -30,10 +165,8 @@ class LLMConfig: temperature: float = 0.7 timeout: int = 60 max_retries: int = 3 - # OpenAI 专用 function_calling: bool = True stream: bool = False - # Ollama / 本地模型 model_path: str = "" ollama_host: str = "http://localhost:11434" @@ -41,7 +174,6 @@ class LLMConfig: self.api_key = os.getenv("LLM_API_KEY", self.api_key) self.api_base_url = os.getenv("LLM_API_BASE_URL", self.api_base_url) self.model_name = os.getenv("LLM_MODEL_NAME", self.model_name) - self.model_path = os.getenv("LLM_MODEL_PATH", self.model_path) @dataclass @@ -51,46 +183,11 @@ class MCPConfig: host: str = "localhost" port: int = 3000 enabled_tools: list[str] = field(default_factory=lambda: [ - "calculator", "web_search", "file_reader", "code_executor" + "calculator", "web_search", "file_reader", + "code_executor", "static_analyzer", "ssh_docker", ]) -@dataclass -class WebSearchToolConfig: - max_results: int = 5 - timeout: int = 10 - api_key: str = "" - engine: str = "mock" - - def __post_init__(self): - self.api_key = os.getenv("SEARCH_API_KEY", self.api_key) - - -@dataclass -class FileReaderToolConfig: - allowed_root: str = "./workspace" - max_file_size_kb: int = 512 - - -@dataclass -class CodeExecutorToolConfig: - timeout: int = 5 - sandbox: bool = True - - -@dataclass -class CalculatorToolConfig: - precision: int = 10 - - -@dataclass -class ToolsConfig: - web_search: WebSearchToolConfig = field(default_factory=WebSearchToolConfig) - file_reader: FileReaderToolConfig = field(default_factory=FileReaderToolConfig) - code_executor: CodeExecutorToolConfig = field(default_factory=CodeExecutorToolConfig) - calculator: CalculatorToolConfig = field(default_factory=CalculatorToolConfig) - - @dataclass class MemoryConfig: max_history: int = 20 @@ -114,39 +211,82 @@ class AgentConfig: max_chain_steps: int = 10 enable_multi_step: bool = True session_timeout: int = 3600 - fallback_to_rules: bool = True # API 失败时降级到规则引擎 + fallback_to_rules: bool = True -@dataclass +# ════════════════════════════════════════════════════════════════ +# 顶层 AppConfig +# ════════════════════════════════════════════════════════════════ + class AppConfig: - llm: LLMConfig = field(default_factory=LLMConfig) - mcp: MCPConfig = field(default_factory=MCPConfig) - tools: ToolsConfig = field(default_factory=ToolsConfig) - memory: MemoryConfig = field(default_factory=MemoryConfig) - logging: LoggingConfig = field(default_factory=LoggingConfig) - agent: AgentConfig = field(default_factory=AgentConfig) + """ + 全局配置单例 + + 访问方式: + settings.llm.model_name + settings.mcp.enabled_tools + settings.tools['web_search']['timeout'] + settings.tools['static_analyzer']['tool_extra_args']['cppcheck'] + settings.tools['ssh_docker']['servers']['prod']['host'] + settings.memory.max_history + settings.agent.fallback_to_rules + settings.logging.level + """ + + def __init__( + self, + llm: LLMConfig, + mcp: MCPConfig, + tools: ToolsView, + memory: MemoryConfig, + logging: LoggingConfig, + agent: AgentConfig, + ): + self.llm = llm + self.mcp = mcp + self.tools = tools + self.memory = memory + self.logging = logging + self.agent = agent def display(self) -> str: + sa = self.tools['static_analyzer'] + ssh = self.tools['ssh_docker'] + ws = self.tools['web_search'] + fr = self.tools['file_reader'] + ce = self.tools['code_executor'] + calc= self.tools['calculator'] lines = [ - "─" * 52, + "─" * 62, " 📋 当前配置", - "─" * 52, - f" [LLM] provider = {self.llm.provider}", - f" [LLM] model_name = {self.llm.model_name}", - f" [LLM] api_key = {'***' if self.llm.api_key else '(未设置)'}", - f" [LLM] api_base_url = {self.llm.api_base_url or '(默认)'}", - f" [LLM] temperature = {self.llm.temperature}", - f" [LLM] max_tokens = {self.llm.max_tokens}", - f" [LLM] function_calling = {self.llm.function_calling}", - f" [LLM] stream = {self.llm.stream}", - f" [LLM] max_retries = {self.llm.max_retries}", - f" [MCP] server_name = {self.mcp.server_name}", - f" [MCP] enabled_tools = {self.mcp.enabled_tools}", - f" [MEMORY] max_history = {self.memory.max_history}", - f" [AGENT] multi_step = {self.agent.enable_multi_step}", - f" [AGENT] fallback_rules = {self.agent.fallback_to_rules}", - f" [LOG] level = {self.logging.level}", - "─" * 52, + "─" * 62, + f" [LLM] provider = {self.llm.provider}", + f" [LLM] model_name = {self.llm.model_name}", + f" [LLM] api_key = {'***' + self.llm.api_key[-4:] if len(self.llm.api_key) > 4 else '(未设置)'}", + f" [LLM] api_base_url = {self.llm.api_base_url or '(默认)'}", + f" [LLM] function_calling = {self.llm.function_calling}", + f" [LLM] temperature = {self.llm.temperature}", + f" [MCP] enabled_tools = {self.mcp.enabled_tools}", + f" [TOOL] calculator.precision= {calc['precision']}", + f" [TOOL] web_search.engine = {ws['engine']}", + f" [TOOL] web_search.timeout = {ws['timeout']}s", + f" [TOOL] file_reader.root = {fr['allowed_root']}", + f" [TOOL] code_executor.timeout={ce['timeout']}s", + f" [TOOL] static_analyzer.tool = {sa['default_tool']}", + f" [TOOL] static_analyzer.std = {sa['default_std']}", + f" [TOOL] static_analyzer.timeout = {sa['timeout']}s", + f" [TOOL] static_analyzer.jobs = {sa['jobs']}", + f" [TOOL] static_analyzer.roots = {sa['allowed_roots'] or '(不限制)'}", + f" [TOOL] ssh_docker.port = {ssh['default_ssh_port']}", + f" [TOOL] ssh_docker.user = {ssh['default_username']}", + f" [TOOL] ssh_docker.conn_timeout = {ssh['connect_timeout']}s", + f" [TOOL] ssh_docker.deploy_timeout= {ssh['deploy_timeout']}s", + f" [TOOL] ssh_docker.allowed_hosts = {ssh['allowed_hosts'] or '(不限制)'}", + f" [TOOL] ssh_docker.servers = {list(ssh['servers'].keys()) or '(无预设)'}", + f" [MEM] max_history = {self.memory.max_history}", + f" [AGT] fallback_rules = {self.agent.fallback_to_rules}", + f" [LOG] level = {self.logging.level}", + "─" * 62, ] return "\n".join(lines) @@ -156,8 +296,8 @@ class AppConfig: # ════════════════════════════════════════════════════════════════ class ConfigLoader: - _CONFIG_SEARCH_PATHS = [ - Path(os.getenv("AGENT_CONFIG_PATH", "./config.yaml")), + _SEARCH_PATHS = [ + Path(os.getenv("AGENT_CONFIG_PATH", "__none__")), Path("config") / "config.yaml", Path("config.yaml"), ] @@ -165,15 +305,15 @@ class ConfigLoader: @classmethod def load(cls) -> AppConfig: raw = cls._read_yaml() - return cls._parse(raw) if raw else AppConfig() + return cls._build(raw if raw is not None else {}) @classmethod def _read_yaml(cls) -> dict[str, Any] | None: if not _YAML_AVAILABLE: print("⚠️ PyYAML 未安装(pip install pyyaml),使用默认配置") return None - for path in cls._CONFIG_SEARCH_PATHS: - if path and path.exists(): + for path in cls._SEARCH_PATHS: + if path and path.exists() and path.suffix in (".yaml", ".yml"): with open(path, encoding="utf-8") as f: data = yaml.safe_load(f) print(f"✅ 已加载配置文件: {path.resolve()}") @@ -182,97 +322,129 @@ class ConfigLoader: return None @classmethod - def _parse(cls, raw: dict[str, Any]) -> AppConfig: + def _build(cls, raw: dict[str, Any]) -> AppConfig: return AppConfig( - llm=cls._parse_llm(raw.get("llm", {})), - mcp=cls._parse_mcp(raw.get("mcp", {})), - tools=cls._parse_tools(raw.get("tools", {})), - memory=cls._parse_memory(raw.get("memory", {})), - logging=cls._parse_logging(raw.get("logging", {})), - agent=cls._parse_agent(raw.get("agent", {})), + llm=cls._build_llm(raw.get("llm", {})), + mcp=cls._build_mcp(raw.get("mcp", {})), + tools=cls._build_tools(raw.get("tools", {})), + memory=cls._build_memory(raw.get("memory", {})), + logging=cls._build_logging(raw.get("logging", {})), + agent=cls._build_agent(raw.get("agent", {})), ) + # ── LLM ─────────────────────────────────────────────────── + @staticmethod - def _parse_llm(d: dict) -> LLMConfig: + def _build_llm(d: dict) -> LLMConfig: + df = _DEFAULTS["llm"] return LLMConfig( - provider=d.get("provider", "openai"), - model_name=d.get("model_name", "gpt-4o"), - api_key=d.get("api_key", ""), - api_base_url=d.get("api_base_url", ""), - max_tokens=int(d.get("max_tokens", 4096)), - temperature=float(d.get("temperature", 0.7)), - timeout=int(d.get("timeout", 60)), - max_retries=int(d.get("max_retries", 3)), - function_calling=bool(d.get("function_calling", True)), - stream=bool(d.get("stream", False)), - model_path=d.get("model_path", ""), - ollama_host=d.get("ollama_host", "http://localhost:11434"), + provider=d.get("provider", df["provider"]), + model_name=d.get("model_name", df["model_name"]), + api_key=d.get("api_key", df["api_key"]), + api_base_url=d.get("api_base_url", df["api_base_url"]), + max_tokens=int(d.get("max_tokens", df["max_tokens"])), + temperature=float(d.get("temperature", df["temperature"])), + timeout=int(d.get("timeout", df["timeout"])), + max_retries=int(d.get("max_retries", df["max_retries"])), + function_calling=bool(d.get("function_calling", df["function_calling"])), + stream=bool(d.get("stream", df["stream"])), + model_path=d.get("model_path", df["model_path"]), + ollama_host=d.get("ollama_host", df["ollama_host"]), ) + # ── MCP ─────────────────────────────────────────────────── + @staticmethod - def _parse_mcp(d: dict) -> MCPConfig: + def _build_mcp(d: dict) -> MCPConfig: + df = _DEFAULTS["mcp"] return MCPConfig( - server_name=d.get("server_name", "DemoMCPServer"), - transport=d.get("transport", "stdio"), - host=d.get("host", "localhost"), - port=int(d.get("port", 3000)), - enabled_tools=d.get("enabled_tools", [ - "calculator", "web_search", "file_reader", "code_executor" - ]), + server_name=d.get("server_name", df["server_name"]), + transport=d.get("transport", df["transport"]), + host=d.get("host", df["host"]), + port=int(d.get("port", df["port"])), + enabled_tools=d.get("enabled_tools", df["enabled_tools"]), ) - @staticmethod - def _parse_tools(d: dict) -> ToolsConfig: - ws = d.get("web_search", {}) - fr = d.get("file_reader", {}) - ce = d.get("code_executor", {}) - ca = d.get("calculator", {}) - return ToolsConfig( - web_search=WebSearchToolConfig( - max_results=int(ws.get("max_results", 5)), - timeout=int(ws.get("timeout", 10)), - api_key=ws.get("api_key", ""), - engine=ws.get("engine", "mock"), - ), - file_reader=FileReaderToolConfig( - allowed_root=fr.get("allowed_root", "./workspace"), - max_file_size_kb=int(fr.get("max_file_size_kb", 512)), - ), - code_executor=CodeExecutorToolConfig( - timeout=int(ce.get("timeout", 5)), - sandbox=bool(ce.get("sandbox", True)), - ), - calculator=CalculatorToolConfig( - precision=int(ca.get("precision", 10)), - ), - ) + # ── Tools(纯字典,深度合并默认值)──────────────────────── + + @classmethod + def _build_tools(cls, d: dict) -> ToolsView: + df = _DEFAULTS["tools"] + merged: dict[str, dict] = {} + # 遍历所有已知工具,深度合并 yaml 值与默认值 + for tool_name, tool_defaults in df.items(): + yaml_tool = d.get(tool_name, {}) + merged[tool_name] = cls._deep_merge(tool_defaults, yaml_tool) + # 处理 yaml 中额外定义的工具(不在默认列表中) + for tool_name, tool_cfg in d.items(): + if tool_name not in merged: + merged[tool_name] = tool_cfg if isinstance(tool_cfg, dict) else {} + # 环境变量覆盖 + cls._apply_env_overrides(merged) + return ToolsView(merged) @staticmethod - def _parse_memory(d: dict) -> MemoryConfig: + def _deep_merge(base: dict, override: dict) -> dict: + """ + 深度合并两个字典:override 中的值覆盖 base 中的值 + 对于嵌套字典递归合并,其他类型直接覆盖 + """ + result = dict(base) + for key, val in override.items(): + if ( + key in result + and isinstance(result[key], dict) + and isinstance(val, dict) + ): + result[key] = ConfigLoader._deep_merge(result[key], val) + else: + result[key] = val + return result + + @staticmethod + def _apply_env_overrides(tools: dict[str, dict]) -> None: + """从环境变量覆盖特定工具配置""" + # web_search.api_key + if api_key := os.getenv("SEARCH_API_KEY"): + tools["web_search"]["api_key"] = api_key + # ssh_docker servers 密码(格式: SSH__PASSWORD) + for server_name, srv in tools.get("ssh_docker", {}).get("servers", {}).items(): + if isinstance(srv, dict) and not srv.get("password"): + env_key = f"SSH_{server_name.upper()}_PASSWORD" + if pw := os.getenv(env_key): + srv["password"] = pw + + # ── Memory / Logging / Agent ────────────────────────────── + + @staticmethod + def _build_memory(d: dict) -> MemoryConfig: + df = _DEFAULTS["memory"] return MemoryConfig( - max_history=int(d.get("max_history", 20)), - enable_long_term=bool(d.get("enable_long_term", False)), - vector_db_url=d.get("vector_db_url", ""), + max_history=int(d.get("max_history", df["max_history"])), + enable_long_term=bool(d.get("enable_long_term",df["enable_long_term"])), + vector_db_url=d.get("vector_db_url", df["vector_db_url"]), ) @staticmethod - def _parse_logging(d: dict) -> LoggingConfig: + def _build_logging(d: dict) -> LoggingConfig: + df = _DEFAULTS["logging"] return LoggingConfig( - level=d.get("level", "DEBUG"), - enable_file=bool(d.get("enable_file", True)), - log_dir=d.get("log_dir", "./logs"), - log_file=d.get("log_file", "agent.log"), + level=d.get("level", df["level"]), + enable_file=bool(d.get("enable_file", df["enable_file"])), + log_dir=d.get("log_dir", df["log_dir"]), + log_file=d.get("log_file", df["log_file"]), ) @staticmethod - def _parse_agent(d: dict) -> AgentConfig: + def _build_agent(d: dict) -> AgentConfig: + df = _DEFAULTS["agent"] return AgentConfig( - max_chain_steps=int(d.get("max_chain_steps", 10)), - enable_multi_step=bool(d.get("enable_multi_step", True)), - session_timeout=int(d.get("session_timeout", 3600)), - fallback_to_rules=bool(d.get("fallback_to_rules", True)), + max_chain_steps=int(d.get("max_chain_steps", df["max_chain_steps"])), + enable_multi_step=bool(d.get("enable_multi_step", df["enable_multi_step"])), + session_timeout=int(d.get("session_timeout", df["session_timeout"])), + fallback_to_rules=bool(d.get("fallback_to_rules", df["fallback_to_rules"])), ) -# 全局单例 +# ── 全局单例 ────────────────────────────────────────────────── settings: AppConfig = ConfigLoader.load() \ No newline at end of file diff --git a/logs/agent.log b/logs/agent.log index aeda5bb..f076894 100644 --- a/logs/agent.log +++ b/logs/agent.log @@ -1589,3 +1589,420 @@ The function `get_system_name()` uses `platform.system()` to determine the syste 34*56 = 190... [2026-03-09 13:35:15,930] [agent.CLIENT] INFO: 🎉 [CLIENT] 流程完成,回复已返回 +[2026-03-09 13:39:06,494] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统(OpenAI Function Calling 模式)... +[2026-03-09 13:39:06,494] [agent.SYSTEM] INFO: ──────────────────────────────────────────────────── + 📋 当前配置 +──────────────────────────────────────────────────── + [LLM] provider = openai + [LLM] model_name = gpt-4o + [LLM] api_key = *** + [LLM] api_base_url = https://openapi.monica.im/v1 + [LLM] temperature = 0.7 + [LLM] max_tokens = 4096 + [LLM] function_calling = True + [LLM] stream = False + [LLM] max_retries = 3 + [MCP] server_name = DemoMCPServer + [MCP] enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor'] + [MEMORY] max_history = 20 + [AGENT] multi_step = True + [AGENT] fallback_rules = True + [LOG] level = DEBUG +──────────────────────────────────────────────────── +[2026-03-09 13:39:06,495] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-03-09 13:39:06,495] [agent.MCP] INFO: transport = stdio +[2026-03-09 13:39:06,496] [agent.MCP] INFO: enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor'] +[2026-03-09 13:39:06,497] [agent.TOOL] DEBUG: ⚙️ Calculator 精度: 10 +[2026-03-09 13:39:06,498] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等 +[2026-03-09 13:39:06,498] [agent.TOOL] DEBUG: ⚙️ WebSearch engine=mock, max_results=5, api_key=(未设置) +[2026-03-09 13:39:06,498] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要 +[2026-03-09 13:39:06,498] [agent.TOOL] DEBUG: ⚙️ FileReader root=workspace, max_size=512KB +[2026-03-09 13:39:06,498] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限配置的 allowed_root 目录 +[2026-03-09 13:39:06,499] [agent.TOOL] DEBUG: ⚙️ CodeExecutor timeout=5s, sandbox=True +[2026-03-09 13:39:06,499] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出 +[2026-03-09 13:39:06,499] [agent.LLM] INFO: 🏭 Provider 工厂: 创建 [openai] Provider +[2026-03-09 13:39:08,884] [agent.LLM] INFO: 🔗 使用自定义 API 地址: https://openapi.monica.im/v1 +[2026-03-09 13:39:09,057] [agent.LLM] INFO: ✅ OpenAI 客户端初始化完成 + model = gpt-4o + base_url = https://openapi.monica.im/v1 + max_retries= 3 +[2026-03-09 13:39:09,058] [agent.LLM] INFO: 🧠 LLM 引擎初始化完成 +[2026-03-09 13:39:09,058] [agent.LLM] INFO: provider = openai +[2026-03-09 13:39:09,058] [agent.LLM] INFO: model_name = gpt-4o +[2026-03-09 13:39:09,058] [agent.LLM] INFO: function_calling = True +[2026-03-09 13:39:09,058] [agent.LLM] INFO: temperature = 0.7 +[2026-03-09 13:39:09,059] [agent.LLM] INFO: fallback_rules = True +[2026-03-09 13:39:09,059] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条 +[2026-03-09 13:39:09,060] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成(OpenAI Function Calling 模式) +[2026-03-09 13:39:09,061] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor'] +[2026-03-09 13:39:25,884] [agent.CLIENT] INFO: ════════════════════════════════════════════════════════════ +[2026-03-09 13:39:25,884] [agent.CLIENT] INFO: 📨 收到用户输入: 先计算34乘以12,再将结果乘以56 +[2026-03-09 13:39:25,884] [agent.CLIENT] INFO: ════════════════════════════════════════════════════════════ +[2026-03-09 13:39:25,885] [agent.MEMORY] DEBUG: 💬 [USER] 先计算34乘以12,再将结果乘以56... +[2026-03-09 13:39:25,885] [agent.CLIENT] INFO: 🗺 [LLM] 规划工具调用链... +[2026-03-09 13:39:25,885] [agent.LLM] INFO: 🗺 规划工具调用链: 先计算34乘以12,再将结果乘以56... +[2026-03-09 13:39:25,886] [agent.LLM] DEBUG: 📤 发送规划请求,tools 数量: 4 +[2026-03-09 13:39:25,886] [agent.LLM] DEBUG: 📤 消息历史长度: 3 +[2026-03-09 13:39:28,752] [agent.LLM] INFO: 📊 Token 用量: prompt=405, completion=17 +[2026-03-09 13:39:28,752] [agent.LLM] INFO: 📋 解析到 1 个工具调用步骤 +[2026-03-09 13:39:28,753] [agent.LLM] INFO: 📋 OpenAI 规划完成: 1 步 +[2026-03-09 13:39:28,753] [agent.LLM] INFO: Step 1: [calculator] args={'expression': '34*12'} +[2026-03-09 13:39:28,753] [agent.CLIENT] INFO: +──────────────────────────────────────────────────────────── + 🔗 开始执行工具调用链 + 目标: calculator + 步骤: 1 步 +──────────────────────────────────────────────────────────── +[2026-03-09 13:39:28,753] [agent.CLIENT] DEBUG: 🔑 预生成 tool_call_ids: {1: 'call_d656a0cfe9ab'} +[2026-03-09 13:39:28,753] [agent.CLIENT] INFO: + ▶ Step 1 执行中 + 工具 : [calculator] + 说明 : 调用 calculator(由 OpenAI Function Calling 规划) + 参数 : {'expression': '34*12'} + call_id : call_d656a0cfe9ab +[2026-03-09 13:39:28,754] [agent.MCP] INFO: 📨 收到请求 id=df72b664 method=tools/call transport=stdio +[2026-03-09 13:39:28,754] [agent.TOOL] INFO: ▶ 执行工具 [calculator],参数: {'expression': '34*12'} +[2026-03-09 13:39:28,754] [agent.TOOL] INFO: ✅ 工具 [calculator] 执行成功 +[2026-03-09 13:39:28,754] [agent.CLIENT] INFO: ✅ Step 1 成功: 34*12 = 408... +[2026-03-09 13:39:28,754] [agent.MEMORY] DEBUG: 💬 [TOOL] 34*12 = 408... +[2026-03-09 13:39:28,754] [agent.CLIENT] DEBUG: 📦 OpenAI 消息块结构: +[2026-03-09 13:39:28,754] [agent.CLIENT] DEBUG: [0] assistant tool_calls.ids = ['call_d656a0cfe9ab'] +[2026-03-09 13:39:28,755] [agent.CLIENT] DEBUG: [1] tool tool_call_id = call_d656a0cfe9ab content = 34*12 = 408... +[2026-03-09 13:39:28,755] [agent.CLIENT] INFO: ──────────────────────────────────────────────────────────── + ✅ 调用链执行完成 + 完成: 1/1 步 +──────────────────────────────────────────────────────────── +[2026-03-09 13:39:28,755] [agent.CLIENT] INFO: ✍️ [LLM] 调用 OpenAI 生成最终回复... +[2026-03-09 13:39:28,755] [agent.LLM] INFO: ✍️ 生成最终回复(工具调用链模式)... +[2026-03-09 13:39:28,755] [agent.LLM] DEBUG: 📤 发送回复请求,消息数: 4 +[2026-03-09 13:39:28,755] [agent.LLM] DEBUG: 📋 消息序列结构: +[2026-03-09 13:39:28,755] [agent.LLM] DEBUG: [0] system 你是一个友好、专业的 AI 助手。 +请基于已执行的工具调用结果,用清晰、自然的语言回答用户的问题。 +... +[2026-03-09 13:39:28,755] [agent.LLM] DEBUG: [1] user 先计算34乘以12,再将结果乘以56... +[2026-03-09 13:39:28,755] [agent.LLM] DEBUG: [2] assistant tool_calls=['calculator'] ids=['call_d656a0cfe9ab'] +[2026-03-09 13:39:28,755] [agent.LLM] DEBUG: [3] tool tool_call_id=call_d656a0cfe9ab content=34*12 = 408... +[2026-03-09 13:39:28,756] [agent.LLM] DEBUG: 📤 发送回复生成请求,消息长度: 4 +[2026-03-09 13:39:30,293] [agent.LLM] INFO: ✅ 回复生成成功,长度: 27 chars,Token: 18 +[2026-03-09 13:39:30,294] [agent.LLM] INFO: ✅ OpenAI 回复生成成功 (27 chars) +[2026-03-09 13:39:30,294] [agent.MEMORY] DEBUG: 💬 [ASSISTANT] 34乘以12的结果是408。接下来计算408乘以56。... +[2026-03-09 13:39:30,294] [agent.CLIENT] INFO: 🎉 流程完成,回复已返回 +[2026-03-09 14:03:41,261] [agent.TOOL.SSHDocker] WARNING: ⚠️ paramiko 未安装,请执行: pip install paramiko>=3.0.0 +[2026-03-09 14:03:41,271] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统(OpenAI Function Calling 模式)... +[2026-03-09 14:03:41,271] [agent.SYSTEM] INFO: ────────────────────────────────────────────────────────────── + 📋 当前配置 +────────────────────────────────────────────────────────────── + [LLM] provider = openai + [LLM] model_name = gpt-4o + [LLM] api_key = ***ACjR + [LLM] api_base_url = https://openapi.monica.im/v1 + [LLM] function_calling = True + [LLM] temperature = 0.7 + [MCP] enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor', 'static_analyzer', 'ssh_docker'] + [TOOL] calculator.precision= 10 + [TOOL] web_search.engine = mock + [TOOL] web_search.timeout = 10s + [TOOL] file_reader.root = ./workspace + [TOOL] code_executor.timeout=5s + [TOOL] static_analyzer.tool = cppcheck + [TOOL] static_analyzer.std = c++17 + [TOOL] static_analyzer.timeout = 120s + [TOOL] static_analyzer.jobs = 4 + [TOOL] static_analyzer.roots = (不限制) + [TOOL] ssh_docker.port = 22 + [TOOL] ssh_docker.user = root + [TOOL] ssh_docker.conn_timeout = 30s + [TOOL] ssh_docker.deploy_timeout= 300s + [TOOL] ssh_docker.allowed_hosts = (不限制) + [TOOL] ssh_docker.servers = (无预设) + [MEM] max_history = 20 + [AGT] fallback_rules = True + [LOG] level = DEBUG +────────────────────────────────────────────────────────────── +[2026-03-09 14:03:41,276] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-03-09 14:03:41,278] [agent.MCP] INFO: transport = stdio +[2026-03-09 14:03:41,278] [agent.MCP] INFO: enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor', 'static_analyzer', 'ssh_docker'] +[2026-03-09 14:05:16,524] [agent.TOOL.SSHDocker] WARNING: ⚠️ paramiko 未安装,请执行: pip install paramiko>=3.0.0 +[2026-03-09 14:05:16,529] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统(OpenAI Function Calling 模式)... +[2026-03-09 14:05:16,529] [agent.SYSTEM] INFO: ────────────────────────────────────────────────────────────── + 📋 当前配置 +────────────────────────────────────────────────────────────── + [LLM] provider = openai + [LLM] model_name = gpt-4o + [LLM] api_key = ***ACjR + [LLM] api_base_url = https://openapi.monica.im/v1 + [LLM] function_calling = True + [LLM] temperature = 0.7 + [MCP] enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor', 'static_analyzer', 'ssh_docker'] + [TOOL] calculator.precision= 10 + [TOOL] web_search.engine = mock + [TOOL] web_search.timeout = 10s + [TOOL] file_reader.root = ./workspace + [TOOL] code_executor.timeout=5s + [TOOL] static_analyzer.tool = cppcheck + [TOOL] static_analyzer.std = c++17 + [TOOL] static_analyzer.timeout = 120s + [TOOL] static_analyzer.jobs = 4 + [TOOL] static_analyzer.roots = (不限制) + [TOOL] ssh_docker.port = 22 + [TOOL] ssh_docker.user = root + [TOOL] ssh_docker.conn_timeout = 30s + [TOOL] ssh_docker.deploy_timeout= 300s + [TOOL] ssh_docker.allowed_hosts = (不限制) + [TOOL] ssh_docker.servers = (无预设) + [MEM] max_history = 20 + [AGT] fallback_rules = True + [LOG] level = DEBUG +────────────────────────────────────────────────────────────── +[2026-03-09 14:05:16,530] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-03-09 14:05:16,530] [agent.MCP] INFO: transport = stdio +[2026-03-09 14:05:16,531] [agent.MCP] INFO: enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor', 'static_analyzer', 'ssh_docker'] +[2026-03-09 14:06:01,148] [agent.TOOL.SSHDocker] WARNING: ⚠️ paramiko 未安装,请执行: pip install paramiko>=3.0.0 +[2026-03-09 14:06:01,156] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统(OpenAI Function Calling 模式)... +[2026-03-09 14:06:01,157] [agent.SYSTEM] INFO: ────────────────────────────────────────────────────────────── + 📋 当前配置 +────────────────────────────────────────────────────────────── + [LLM] provider = openai + [LLM] model_name = gpt-4o + [LLM] api_key = ***ACjR + [LLM] api_base_url = https://openapi.monica.im/v1 + [LLM] function_calling = True + [LLM] temperature = 0.7 + [MCP] enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor', 'static_analyzer', 'ssh_docker'] + [TOOL] calculator.precision= 10 + [TOOL] web_search.engine = mock + [TOOL] web_search.timeout = 10s + [TOOL] file_reader.root = ./workspace + [TOOL] code_executor.timeout=5s + [TOOL] static_analyzer.tool = cppcheck + [TOOL] static_analyzer.std = c++17 + [TOOL] static_analyzer.timeout = 120s + [TOOL] static_analyzer.jobs = 4 + [TOOL] static_analyzer.roots = (不限制) + [TOOL] ssh_docker.port = 22 + [TOOL] ssh_docker.user = root + [TOOL] ssh_docker.conn_timeout = 30s + [TOOL] ssh_docker.deploy_timeout= 300s + [TOOL] ssh_docker.allowed_hosts = (不限制) + [TOOL] ssh_docker.servers = (无预设) + [MEM] max_history = 20 + [AGT] fallback_rules = True + [LOG] level = DEBUG +────────────────────────────────────────────────────────────── +[2026-03-09 14:06:01,161] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-03-09 14:06:01,161] [agent.MCP] INFO: transport = stdio +[2026-03-09 14:06:01,161] [agent.MCP] INFO: enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor', 'static_analyzer', 'ssh_docker'] +[2026-03-09 14:06:11,760] [agent.TOOL.SSHDocker] WARNING: ⚠️ paramiko 未安装,请执行: pip install paramiko>=3.0.0 +[2026-03-09 14:06:11,766] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统(OpenAI Function Calling 模式)... +[2026-03-09 14:06:11,766] [agent.SYSTEM] INFO: ────────────────────────────────────────────────────────────── + 📋 当前配置 +────────────────────────────────────────────────────────────── + [LLM] provider = openai + [LLM] model_name = gpt-4o + [LLM] api_key = ***ACjR + [LLM] api_base_url = https://openapi.monica.im/v1 + [LLM] function_calling = True + [LLM] temperature = 0.7 + [MCP] enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor', 'static_analyzer', 'ssh_docker'] + [TOOL] calculator.precision= 10 + [TOOL] web_search.engine = mock + [TOOL] web_search.timeout = 10s + [TOOL] file_reader.root = ./workspace + [TOOL] code_executor.timeout=5s + [TOOL] static_analyzer.tool = cppcheck + [TOOL] static_analyzer.std = c++17 + [TOOL] static_analyzer.timeout = 120s + [TOOL] static_analyzer.jobs = 4 + [TOOL] static_analyzer.roots = (不限制) + [TOOL] ssh_docker.port = 22 + [TOOL] ssh_docker.user = root + [TOOL] ssh_docker.conn_timeout = 30s + [TOOL] ssh_docker.deploy_timeout= 300s + [TOOL] ssh_docker.allowed_hosts = (不限制) + [TOOL] ssh_docker.servers = (无预设) + [MEM] max_history = 20 + [AGT] fallback_rules = True + [LOG] level = DEBUG +────────────────────────────────────────────────────────────── +[2026-03-09 14:06:11,767] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-03-09 14:06:11,767] [agent.MCP] INFO: transport = stdio +[2026-03-09 14:06:11,768] [agent.MCP] INFO: enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor', 'static_analyzer', 'ssh_docker'] +[2026-03-09 14:06:11,768] [agent.TOOL] DEBUG: ⚙️ Calculator 精度: 10 +[2026-03-09 14:06:11,769] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等 +[2026-03-09 14:06:22,925] [agent.TOOL.SSHDocker] WARNING: ⚠️ paramiko 未安装,请执行: pip install paramiko>=3.0.0 +[2026-03-09 14:06:22,939] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统(OpenAI Function Calling 模式)... +[2026-03-09 14:06:22,940] [agent.SYSTEM] INFO: ────────────────────────────────────────────────────────────── + 📋 当前配置 +────────────────────────────────────────────────────────────── + [LLM] provider = openai + [LLM] model_name = gpt-4o + [LLM] api_key = ***ACjR + [LLM] api_base_url = https://openapi.monica.im/v1 + [LLM] function_calling = True + [LLM] temperature = 0.7 + [MCP] enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor', 'static_analyzer', 'ssh_docker'] + [TOOL] calculator.precision= 10 + [TOOL] web_search.engine = mock + [TOOL] web_search.timeout = 10s + [TOOL] file_reader.root = ./workspace + [TOOL] code_executor.timeout=5s + [TOOL] static_analyzer.tool = cppcheck + [TOOL] static_analyzer.std = c++17 + [TOOL] static_analyzer.timeout = 120s + [TOOL] static_analyzer.jobs = 4 + [TOOL] static_analyzer.roots = (不限制) + [TOOL] ssh_docker.port = 22 + [TOOL] ssh_docker.user = root + [TOOL] ssh_docker.conn_timeout = 30s + [TOOL] ssh_docker.deploy_timeout= 300s + [TOOL] ssh_docker.allowed_hosts = (不限制) + [TOOL] ssh_docker.servers = (无预设) + [MEM] max_history = 20 + [AGT] fallback_rules = True + [LOG] level = DEBUG +────────────────────────────────────────────────────────────── +[2026-03-09 14:06:22,942] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-03-09 14:06:22,942] [agent.MCP] INFO: transport = stdio +[2026-03-09 14:06:22,942] [agent.MCP] INFO: enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor', 'static_analyzer', 'ssh_docker'] +[2026-03-09 14:06:22,943] [agent.TOOL] DEBUG: ⚙️ Calculator 精度: 10 +[2026-03-09 14:06:22,943] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等 +[2026-03-09 14:06:44,043] [agent.TOOL.SSHDocker] WARNING: ⚠️ paramiko 未安装,请执行: pip install paramiko>=3.0.0 +[2026-03-09 14:06:44,046] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统(OpenAI Function Calling 模式)... +[2026-03-09 14:06:44,046] [agent.SYSTEM] INFO: ────────────────────────────────────────────────────────────── + 📋 当前配置 +────────────────────────────────────────────────────────────── + [LLM] provider = openai + [LLM] model_name = gpt-4o + [LLM] api_key = ***ACjR + [LLM] api_base_url = https://openapi.monica.im/v1 + [LLM] function_calling = True + [LLM] temperature = 0.7 + [MCP] enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor', 'static_analyzer', 'ssh_docker'] + [TOOL] calculator.precision= 10 + [TOOL] web_search.engine = mock + [TOOL] web_search.timeout = 10s + [TOOL] file_reader.root = ./workspace + [TOOL] code_executor.timeout=5s + [TOOL] static_analyzer.tool = cppcheck + [TOOL] static_analyzer.std = c++17 + [TOOL] static_analyzer.timeout = 120s + [TOOL] static_analyzer.jobs = 4 + [TOOL] static_analyzer.roots = (不限制) + [TOOL] ssh_docker.port = 22 + [TOOL] ssh_docker.user = root + [TOOL] ssh_docker.conn_timeout = 30s + [TOOL] ssh_docker.deploy_timeout= 300s + [TOOL] ssh_docker.allowed_hosts = (不限制) + [TOOL] ssh_docker.servers = (无预设) + [MEM] max_history = 20 + [AGT] fallback_rules = True + [LOG] level = DEBUG +────────────────────────────────────────────────────────────── +[2026-03-09 14:06:44,048] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-03-09 14:06:44,048] [agent.MCP] INFO: transport = stdio +[2026-03-09 14:06:44,048] [agent.MCP] INFO: enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor', 'static_analyzer', 'ssh_docker'] +[2026-03-09 14:06:44,049] [agent.TOOL] DEBUG: ⚙️ Calculator 精度: 10 +[2026-03-09 14:06:44,049] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等 +[2026-03-09 14:06:44,049] [agent.TOOL] DEBUG: ⚙️ WebSearch engine=mock, max_results=5, api_key=(未设置) +[2026-03-09 14:06:44,050] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要 +[2026-03-09 14:07:04,527] [agent.TOOL.SSHDocker] WARNING: ⚠️ paramiko 未安装,请执行: pip install paramiko>=3.0.0 +[2026-03-09 14:07:04,531] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统(OpenAI Function Calling 模式)... +[2026-03-09 14:07:04,532] [agent.SYSTEM] INFO: ────────────────────────────────────────────────────────────── + 📋 当前配置 +────────────────────────────────────────────────────────────── + [LLM] provider = openai + [LLM] model_name = gpt-4o + [LLM] api_key = ***ACjR + [LLM] api_base_url = https://openapi.monica.im/v1 + [LLM] function_calling = True + [LLM] temperature = 0.7 + [MCP] enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor', 'static_analyzer', 'ssh_docker'] + [TOOL] calculator.precision= 10 + [TOOL] web_search.engine = mock + [TOOL] web_search.timeout = 10s + [TOOL] file_reader.root = ./workspace + [TOOL] code_executor.timeout=5s + [TOOL] static_analyzer.tool = cppcheck + [TOOL] static_analyzer.std = c++17 + [TOOL] static_analyzer.timeout = 120s + [TOOL] static_analyzer.jobs = 4 + [TOOL] static_analyzer.roots = (不限制) + [TOOL] ssh_docker.port = 22 + [TOOL] ssh_docker.user = root + [TOOL] ssh_docker.conn_timeout = 30s + [TOOL] ssh_docker.deploy_timeout= 300s + [TOOL] ssh_docker.allowed_hosts = (不限制) + [TOOL] ssh_docker.servers = (无预设) + [MEM] max_history = 20 + [AGT] fallback_rules = True + [LOG] level = DEBUG +────────────────────────────────────────────────────────────── +[2026-03-09 14:07:04,532] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-03-09 14:07:04,533] [agent.MCP] INFO: transport = stdio +[2026-03-09 14:07:04,533] [agent.MCP] INFO: enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor', 'static_analyzer', 'ssh_docker'] +[2026-03-09 14:07:04,533] [agent.TOOL] DEBUG: ⚙️ Calculator 精度: 10 +[2026-03-09 14:07:04,533] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等 +[2026-03-09 14:07:04,533] [agent.TOOL] DEBUG: ⚙️ WebSearch engine=mock, max_results=5, api_key=(未设置) +[2026-03-09 14:07:04,533] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要 +[2026-03-09 14:07:04,534] [agent.TOOL] DEBUG: ⚙️ FileReader root=workspace, max_size=512KB +[2026-03-09 14:07:04,534] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限配置的 allowed_root 目录 +[2026-03-09 14:07:40,023] [agent.TOOL.SSHDocker] WARNING: ⚠️ paramiko 未安装,请执行: pip install paramiko>=3.0.0 +[2026-03-09 14:07:40,029] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统(OpenAI Function Calling 模式)... +[2026-03-09 14:07:40,029] [agent.SYSTEM] INFO: ────────────────────────────────────────────────────────────── + 📋 当前配置 +────────────────────────────────────────────────────────────── + [LLM] provider = openai + [LLM] model_name = gpt-4o + [LLM] api_key = ***ACjR + [LLM] api_base_url = https://openapi.monica.im/v1 + [LLM] function_calling = True + [LLM] temperature = 0.7 + [MCP] enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor', 'static_analyzer', 'ssh_docker'] + [TOOL] calculator.precision= 10 + [TOOL] web_search.engine = mock + [TOOL] web_search.timeout = 10s + [TOOL] file_reader.root = ./workspace + [TOOL] code_executor.timeout=5s + [TOOL] static_analyzer.tool = cppcheck + [TOOL] static_analyzer.std = c++17 + [TOOL] static_analyzer.timeout = 120s + [TOOL] static_analyzer.jobs = 4 + [TOOL] static_analyzer.roots = (不限制) + [TOOL] ssh_docker.port = 22 + [TOOL] ssh_docker.user = root + [TOOL] ssh_docker.conn_timeout = 30s + [TOOL] ssh_docker.deploy_timeout= 300s + [TOOL] ssh_docker.allowed_hosts = (不限制) + [TOOL] ssh_docker.servers = (无预设) + [MEM] max_history = 20 + [AGT] fallback_rules = True + [LOG] level = DEBUG +────────────────────────────────────────────────────────────── +[2026-03-09 14:07:40,032] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动 +[2026-03-09 14:07:40,032] [agent.MCP] INFO: transport = stdio +[2026-03-09 14:07:40,032] [agent.MCP] INFO: enabled_tools = ['calculator', 'web_search', 'file_reader', 'code_executor', 'static_analyzer', 'ssh_docker'] +[2026-03-09 14:07:40,033] [agent.TOOL] DEBUG: ⚙️ Calculator 精度: 10 +[2026-03-09 14:07:40,034] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等 +[2026-03-09 14:07:40,034] [agent.TOOL] DEBUG: ⚙️ WebSearch engine=mock, max_results=5, api_key=(未设置) +[2026-03-09 14:07:40,035] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要 +[2026-03-09 14:07:40,035] [agent.TOOL] DEBUG: ⚙️ FileReader root=workspace, max_size=512KB +[2026-03-09 14:07:40,035] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限配置的 allowed_root 目录 +[2026-03-09 14:07:40,036] [agent.TOOL] DEBUG: ⚙️ CodeExecutor timeout=5s, sandbox=True +[2026-03-09 14:07:40,036] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出 +[2026-03-09 14:07:40,036] [agent.MCP] INFO: 📌 注册工具: [static_analyzer] — 对指定目录下的 C/C++ 工程调用外部静态分析工具(cppcheck/clang-tidy/infer)进行代码质量检查,返回错误、警告及代码风格问题 +[2026-03-09 14:07:40,036] [agent.MCP] INFO: 📌 注册工具: [ssh_docker] — 通过 SSH 连接到远程服务器,使用 Docker 部署和管理容器应用。支持: deploy | start | stop | restart | status | logs | remove | compose_up | compose_down | compose_ps | pull | inspect | stats +[2026-03-09 14:07:40,037] [agent.LLM] INFO: 🏭 Provider 工厂: 创建 [openai] Provider +[2026-03-09 14:07:42,643] [agent.LLM] INFO: 🔗 使用自定义 API 地址: https://openapi.monica.im/v1 +[2026-03-09 14:07:42,832] [agent.LLM] INFO: ✅ OpenAI 客户端初始化完成 + model = gpt-4o + base_url = https://openapi.monica.im/v1 + max_retries= 3 +[2026-03-09 14:07:42,833] [agent.LLM] INFO: 🧠 LLM 引擎初始化完成 +[2026-03-09 14:07:42,833] [agent.LLM] INFO: provider = openai +[2026-03-09 14:07:42,833] [agent.LLM] INFO: model_name = gpt-4o +[2026-03-09 14:07:42,833] [agent.LLM] INFO: function_calling = True +[2026-03-09 14:07:42,834] [agent.LLM] INFO: temperature = 0.7 +[2026-03-09 14:07:42,834] [agent.LLM] INFO: fallback_rules = True +[2026-03-09 14:07:42,834] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条 +[2026-03-09 14:07:42,834] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成(OpenAI Function Calling 模式) +[2026-03-09 14:07:42,835] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor', 'static_analyzer', 'ssh_docker'] diff --git a/main.py b/main.py index 3b68b98..800f6d0 100644 --- a/main.py +++ b/main.py @@ -23,6 +23,8 @@ from tools.calculator import CalculatorTool from tools.code_executor import CodeExecutorTool from tools.file_reader import FileReaderTool from tools.web_search import WebSearchTool +from tools.static_analyzer import StaticAnalyzerTool +from tools.ssh_docker import SSHDockerTool from utils.logger import get_logger logger = get_logger("SYSTEM") @@ -32,6 +34,8 @@ _ALL_TOOLS = { "web_search": WebSearchTool, "file_reader": FileReaderTool, "code_executor": CodeExecutorTool, + "static_analyzer": StaticAnalyzerTool, + "ssh_docker": SSHDockerTool } diff --git a/tools/calculator.py b/tools/calculator.py index 6521aa3..ede95ac 100644 --- a/tools/calculator.py +++ b/tools/calculator.py @@ -27,7 +27,7 @@ class CalculatorTool(BaseTool): def __init__(self): super().__init__() # 从配置读取精度 - self._precision = settings.tools.calculator.precision + self._precision = settings.tools['calculator']['precision'] self.logger.debug(f"⚙️ Calculator 精度: {self._precision}") def execute(self, expression: str, **_) -> ToolResult: diff --git a/tools/code_executor.py b/tools/code_executor.py index 9f78d88..88d05f0 100644 --- a/tools/code_executor.py +++ b/tools/code_executor.py @@ -29,9 +29,9 @@ class CodeExecutorTool(BaseTool): def __init__(self): super().__init__() - cfg = settings.tools.code_executor - self._timeout = cfg.timeout - self._sandbox = cfg.sandbox + cfg = settings.tools['code_executor'] + self._timeout = cfg['timeout'] + self._sandbox = cfg['sandbox'] self.logger.debug( f"⚙️ CodeExecutor timeout={self._timeout}s, sandbox={self._sandbox}" ) diff --git a/tools/file_reader.py b/tools/file_reader.py index 8f5288d..5dcd1e6 100644 --- a/tools/file_reader.py +++ b/tools/file_reader.py @@ -18,9 +18,9 @@ class FileReaderTool(BaseTool): def __init__(self): super().__init__() - cfg = settings.tools.file_reader - self._allowed_root = Path(cfg.allowed_root) - self._max_size_kb = cfg.max_file_size_kb + cfg = settings.tools['file_reader'] + self._allowed_root = Path(cfg['allowed_root']) + self._max_size_kb = cfg['max_file_size_kb'] self.logger.debug( f"⚙️ FileReader root={self._allowed_root}, " f"max_size={self._max_size_kb}KB" diff --git a/tools/ssh_docker.py b/tools/ssh_docker.py new file mode 100644 index 0000000..3b35bda --- /dev/null +++ b/tools/ssh_docker.py @@ -0,0 +1,732 @@ +""" +tools/ssh_docker.py +SSH 远程 Docker 部署工具 —— 所有配置通过 settings.tools['ssh_docker'][key] 获取 +依赖: pip install paramiko>=3.0.0 +""" + +import json +import re +import time +from dataclasses import dataclass, field + +from config.settings import settings +from utils.logger import get_logger + +logger = get_logger("TOOL.SSHDocker") + +try: + import paramiko + _PARAMIKO_AVAILABLE = True +except ImportError: + _PARAMIKO_AVAILABLE = False + logger.warning("⚠️ paramiko 未安装,请执行: pip install paramiko>=3.0.0") + + +# ════════════════════════════════════════════════════════════════ +# 配置访问快捷函数 +# ════════════════════════════════════════════════════════════════ + +def _cfg(key: str, fallback=None): + """读取 ssh_docker 工具配置,不存在时返回 fallback""" + return settings.tools['ssh_docker'].get(key, fallback) + + +# ════════════════════════════════════════════════════════════════ +# 数据结构 +# ════════════════════════════════════════════════════════════════ + +@dataclass +class SSHConfig: + host: str + port: int = 22 + username: str = "root" + password: str = "" + key_path: str = "" + timeout: int = 30 + cmd_timeout: int = 120 + + @classmethod + def from_kwargs(cls, kwargs: dict) -> "SSHConfig": + """ + 从调用参数构造 SSHConfig + 支持通过 server 名称引用 config.yaml 中的预设 + 缺省值全部来自 config.yaml → tools.ssh_docker + """ + server_name = kwargs.get("server", "") + if server_name: + servers = _cfg('servers', {}) + preset = servers.get(server_name) + if not preset: + raise ValueError( + f"服务器预设 '{server_name}' 未在 config.yaml " + f"tools.ssh_docker.servers 中定义\n" + f"已有预设: {list(servers.keys())}" + ) + logger.info(f"📋 使用服务器预设: {server_name} → {preset.get('host')}") + return cls( + host=preset.get("host", ""), + port=int(preset.get("port", _cfg('default_ssh_port', 22))), + username=preset.get("username", _cfg('default_username', 'root')), + password=preset.get("password", ""), + key_path=preset.get("key_path", ""), + timeout=_cfg('connect_timeout', 30), + cmd_timeout=_cfg('cmd_timeout', 120), + ) + + return cls( + host=kwargs.get("host", ""), + port=int(kwargs.get("port", _cfg('default_ssh_port', 22))), + username=kwargs.get("username", _cfg('default_username', 'root')), + password=kwargs.get("password", ""), + key_path=kwargs.get("key_path", ""), + timeout=_cfg('connect_timeout', 30), + cmd_timeout=_cfg('cmd_timeout', 120), + ) + + +@dataclass +class CommandResult: + command: str + stdout: str + stderr: str + exit_code: int + success: bool = True + + @property + def output(self) -> str: + return self.stdout.strip() or self.stderr.strip() + + +@dataclass +class DeployConfig: + image: str + container_name: str + action: str = "deploy" + ports: list[str] = field(default_factory=list) + volumes: list[str] = field(default_factory=list) + env_vars: dict[str, str] = field(default_factory=dict) + network: str = "" + restart_policy: str = "" + command: str = "" + compose_file: str = "" + pull_latest: bool = True + extra_args: str = "" + + def __post_init__(self): + # 重启策略缺省值来自 config.yaml + if not self.restart_policy: + self.restart_policy = _cfg('default_restart_policy', 'unless-stopped') + + +# ════════════════════════════════════════════════════════════════ +# SSH 连接管理器 +# ════════════════════════════════════════════════════════════════ + +class SSHManager: + def __init__(self, cfg: SSHConfig): + self.cfg = cfg + self.client: "paramiko.SSHClient | None" = None + + def connect(self) -> None: + if not _PARAMIKO_AVAILABLE: + raise RuntimeError("paramiko 未安装,请执行: pip install paramiko>=3.0.0") + + self.client = paramiko.SSHClient() + self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + connect_kwargs: dict = { + "hostname": self.cfg.host, + "port": self.cfg.port, + "username": self.cfg.username, + "timeout": self.cfg.timeout, + } + if self.cfg.key_path: + logger.info(f"🔑 使用密钥认证: {self.cfg.key_path}") + connect_kwargs["key_filename"] = self.cfg.key_path + elif self.cfg.password: + logger.info("🔐 使用密码认证") + connect_kwargs["password"] = self.cfg.password + else: + logger.info("🔓 尝试 SSH Agent / 默认密钥认证") + + self.client.connect(**connect_kwargs) + logger.info( + f"✅ SSH 连接成功: {self.cfg.username}@{self.cfg.host}:{self.cfg.port}\n" + f" 连接超时: {self.cfg.timeout}s " + f"[config.yaml connect_timeout={_cfg('connect_timeout')}s]\n" + f" 命令超时: {self.cfg.cmd_timeout}s " + f"[config.yaml cmd_timeout={_cfg('cmd_timeout')}s]" + ) + + def exec(self, command: str, timeout: int | None = None) -> CommandResult: + if not self.client: + raise RuntimeError("SSH 未连接,请先调用 connect()") + t = timeout or self.cfg.cmd_timeout + logger.debug(f"🖥 执行命令 (timeout={t}s): {command}") + + _, stdout, stderr = self.client.exec_command(command, timeout=t) + exit_code = stdout.channel.recv_exit_status() + out = stdout.read().decode("utf-8", errors="replace") + err = stderr.read().decode("utf-8", errors="replace") + + result = CommandResult( + command=command, stdout=out, stderr=err, + exit_code=exit_code, success=(exit_code == 0), + ) + logger.debug(f" exit={exit_code} out={out[:80]} err={err[:80]}") + return result + + def close(self) -> None: + if self.client: + self.client.close() + self.client = None + + def __enter__(self): + self.connect() + return self + + def __exit__(self, *_): + self.close() + + +# ════════════════════════════════════════════════════════════════ +# Docker 操作执行器 +# ════════════════════════════════════════════════════════════════ + +class DockerExecutor: + ALLOWED_ACTIONS = { + "deploy", "start", "stop", "restart", + "status", "logs", "remove", + "compose_up", "compose_down", "compose_ps", + "pull", "inspect", "stats", + } + + def __init__(self, ssh: SSHManager): + self.ssh = ssh + + def check_docker(self) -> CommandResult: + return self.ssh.exec( + "docker --version && docker info --format '{{.ServerVersion}}'" + ) + + def pull_image(self, image: str) -> CommandResult: + logger.info(f"📥 拉取镜像: {image}") + return self.ssh.exec( + f"docker pull {image}", + timeout=_cfg('deploy_timeout', 300), + ) + + def deploy(self, cfg: DeployConfig) -> list[CommandResult]: + results = [] + if cfg.pull_latest: + results.append(self.pull_image(cfg.image)) + results.append(self.ssh.exec( + f"docker stop {cfg.container_name} 2>/dev/null || true" + )) + results.append(self.ssh.exec( + f"docker rm {cfg.container_name} 2>/dev/null || true" + )) + cmd = self._build_run_command(cfg) + logger.info(f"🚀 启动容器: {cmd}") + results.append(self.ssh.exec(cmd, timeout=_cfg('deploy_timeout', 300))) + return results + + def start(self, name: str) -> CommandResult: return self.ssh.exec(f"docker start {name}") + def stop(self, name: str) -> CommandResult: return self.ssh.exec(f"docker stop {name}") + def restart(self, name: str) -> CommandResult: return self.ssh.exec(f"docker restart {name}") + + def remove(self, name: str, force: bool = True) -> CommandResult: + return self.ssh.exec(f"docker rm {'-f' if force else ''} {name}") + + def status(self, name: str) -> CommandResult: + cmd = ( + f"docker inspect {name} " + f"--format '{{{{.Name}}}} | {{{{.State.Status}}}} | " + f"Started: {{{{.State.StartedAt}}}} | Image: {{{{.Config.Image}}}}'" + f" 2>/dev/null || echo 'Container {name} not found'" + ) + return self.ssh.exec(cmd) + + def logs(self, name: str, tail: int | None = None) -> CommandResult: + n = tail if tail is not None else _cfg('default_tail_lines', 100) + logger.info( + f"📋 获取日志: {name} tail={n} " + f"[config.yaml default_tail_lines={_cfg('default_tail_lines')}]" + ) + return self.ssh.exec(f"docker logs --tail={n} --timestamps {name} 2>&1") + + def inspect(self, name: str) -> CommandResult: + return self.ssh.exec(f"docker inspect {name}") + + def stats(self, name: str) -> CommandResult: + return self.ssh.exec( + f"docker stats {name} --no-stream " + f"--format 'table {{{{.Name}}}}\t{{{{.CPUPerc}}}}\t" + f"{{{{.MemUsage}}}}\t{{{{.NetIO}}}}'" + ) + + def compose_up(self, compose_file: str, detach: bool = True) -> CommandResult: + work_dir = compose_file.rsplit("/", 1)[0] if "/" in compose_file else "." + logger.info(f"🐙 Compose Up: {compose_file}") + return self.ssh.exec( + f"cd {work_dir} && docker compose -f {compose_file} " + f"up {'-d' if detach else ''} --pull always", + timeout=_cfg('deploy_timeout', 300), + ) + + def compose_down(self, compose_file: str) -> CommandResult: + work_dir = compose_file.rsplit("/", 1)[0] if "/" in compose_file else "." + return self.ssh.exec( + f"cd {work_dir} && docker compose -f {compose_file} down" + ) + + def compose_ps(self, compose_file: str) -> CommandResult: + work_dir = compose_file.rsplit("/", 1)[0] if "/" in compose_file else "." + return self.ssh.exec( + f"cd {work_dir} && docker compose -f {compose_file} ps" + ) + + @staticmethod + def _build_run_command(cfg: DeployConfig) -> str: + """ + 构造 docker run 命令 + 安全检查:config.yaml allow_privileged=false 时拒绝 --privileged + """ + if "--privileged" in cfg.extra_args and not _cfg('allow_privileged', False): + logger.warning( + "⚠️ 已移除 --privileged 参数\n" + " 如需启用请在 config.yaml → " + "tools.ssh_docker.allow_privileged 设置为 true" + ) + cfg.extra_args = cfg.extra_args.replace("--privileged", "").strip() + + parts = ["docker", "run", "-d", f"--name {cfg.container_name}"] + if cfg.restart_policy: + parts.append(f"--restart {cfg.restart_policy}") + for p in cfg.ports: + parts.append(f"-p {p}") + for v in cfg.volumes: + parts.append(f"-v {v}") + for k, val in cfg.env_vars.items(): + safe_val = str(val).replace('"', '\\"') + parts.append(f'-e {k}="{safe_val}"') + if cfg.network: + parts.append(f"--network {cfg.network}") + if cfg.extra_args: + parts.append(cfg.extra_args) + parts.append(cfg.image) + if cfg.command: + parts.append(cfg.command) + return " ".join(parts) + + +# ════════════════════════════════════════════════════════════════ +# 主工具类 +# ════════════════════════════════════════════════════════════════ + +class SSHDockerTool: + """ + SSH 远程 Docker 部署工具 + 所有配置均通过 settings.tools['ssh_docker'][key] 读取 + """ + + name = "ssh_docker" + description = ( + "通过 SSH 连接到远程服务器,使用 Docker 部署和管理容器应用。" + "支持: deploy | start | stop | restart | status | logs | " + "remove | compose_up | compose_down | compose_ps | pull | inspect | stats" + ) + parameters = { + "host": { + "type": "string", + "description": "远程服务器 IP 或域名(与 server 参数二选一)", + }, + "server": { + "type": "string", + "description": ( + "使用 config.yaml tools.ssh_docker.servers 中预设的服务器名称" + "(与 host 二选一),例如 'prod' 或 'staging'" + ), + }, + "username": { + "type": "string", + "description": "SSH 用户名(不传则使用 config.yaml default_username)", + }, + "action": { + "type": "string", + "description": ( + "Docker 操作类型: deploy(部署)| start | stop | restart | " + "status(查看状态)| logs(查看日志)| remove(删除)| " + "compose_up | compose_down | compose_ps | pull | inspect | stats" + ), + "enum": sorted(DockerExecutor.ALLOWED_ACTIONS), + }, + "image": { + "type": "string", + "description": "Docker 镜像名称,例如 nginx:latest(deploy/pull 时必填)", + }, + "container_name": { + "type": "string", + "description": "容器名称,例如 my-nginx", + }, + "port": { + "type": "integer", + "description": "SSH 端口(不传则使用 config.yaml default_ssh_port)", + }, + "password": { + "type": "string", + "description": "SSH 密码(与 key_path 二选一)", + }, + "key_path": { + "type": "string", + "description": "SSH 私钥路径,例如 /home/user/.ssh/id_rsa", + }, + "ports": { + "type": "string", + "description": "端口映射,逗号分隔,例如 '8080:80,443:443'", + }, + "volumes": { + "type": "string", + "description": "数据卷挂载,逗号分隔,例如 '/data:/app/data,/logs:/var/log'", + }, + "env_vars": { + "type": "string", + "description": "环境变量 JSON 字符串,例如 '{\"DB_HOST\":\"localhost\",\"DB_PORT\":\"5432\"}'", + }, + "network": { + "type": "string", + "description": "Docker 网络名称,例如 bridge 或 my-network", + }, + "restart_policy": { + "type": "string", + "description": "重启策略(不传则使用 config.yaml default_restart_policy): no | always | unless-stopped | on-failure", + }, + "compose_file": { + "type": "string", + "description": "docker-compose.yml 在远程服务器上的绝对路径", + }, + "pull_latest": { + "type": "boolean", + "description": "部署前是否拉取最新镜像,默认 true", + }, + "tail_lines": { + "type": "integer", + "description": "查看日志时返回的行数(不传则使用 config.yaml default_tail_lines)", + }, + "extra_args": { + "type": "string", + "description": "传递给 docker run 的额外参数,例如 '--memory=512m --cpus=1'", + }, + } + + def execute(self, **kwargs) -> str: + # ── 解析参数,缺省值全部来自 config.yaml ────────────── + action = kwargs.get("action", "status").lower() + image = kwargs.get("image", "") + container_name = kwargs.get("container_name", "") + ports_str = kwargs.get("ports", "") + volumes_str = kwargs.get("volumes", "") + env_vars_str = kwargs.get("env_vars", "{}") + network = kwargs.get("network", "") + restart_policy = kwargs.get("restart_policy", "") # 空→由 DeployConfig.__post_init__ 填充 + compose_file = kwargs.get("compose_file", "") + pull_latest = bool(kwargs.get("pull_latest", True)) + tail_lines_raw = kwargs.get("tail_lines", None) + tail_lines = int(tail_lines_raw) if tail_lines_raw is not None else None + extra_args = kwargs.get("extra_args", "") + + logger.info( + f"🐳 SSH Docker 操作启动\n" + f" 操作 : {action}\n" + f" 容器 : {container_name or '(未指定)'}\n" + f" 镜像 : {image or '(未指定)'}\n" + f" server预设: {kwargs.get('server', '(无)')} " + f"host: {kwargs.get('host', '(无)')}\n" + f" deploy_timeout : {_cfg('deploy_timeout')}s " + f"[config.yaml]\n" + f" allow_privileged: {_cfg('allow_privileged')} " + f"[config.yaml]" + ) + + # ── 参数校验 ────────────────────────────────────────── + err = self._validate(kwargs, action, image, container_name, compose_file) + if err: + return err + + # ── 解析复合参数 ────────────────────────────────────── + ports = [p.strip() for p in ports_str.split(",") if p.strip()] + volumes = [v.strip() for v in volumes_str.split(",") if v.strip()] + try: + env_vars: dict = json.loads(env_vars_str) if env_vars_str.strip() else {} + except json.JSONDecodeError: + return f"❌ env_vars 格式错误,请使用 JSON 格式: {env_vars_str}" + + # ── 镜像黑名单检查(来自 config.yaml blocked_images)── + if image: + blocked = _cfg('blocked_images', []) + if any(image.startswith(b) for b in blocked): + return ( + f"❌ 安全限制: 镜像 '{image}' 在黑名单中\n" + f" 黑名单: {blocked}\n" + f" 请在 config.yaml → tools.ssh_docker.blocked_images 中移除" + ) + + # ── 构造配置对象 ────────────────────────────────────── + try: + ssh_cfg = SSHConfig.from_kwargs(kwargs) + except ValueError as e: + return f"❌ SSH 配置错误: {e}" + + deploy_cfg = DeployConfig( + image=image, + container_name=container_name, + action=action, + ports=ports, + volumes=volumes, + env_vars=env_vars, + network=network, + restart_policy=restart_policy, + compose_file=compose_file, + pull_latest=pull_latest, + extra_args=extra_args, + ) + + # ── 执行操作 ────────────────────────────────────────── + try: + with SSHManager(ssh_cfg) as ssh: + executor = DockerExecutor(ssh) + return self._dispatch(action, executor, deploy_cfg, tail_lines) + except Exception as e: + error_msg = str(e) + logger.error(f"❌ SSH Docker 操作失败: {error_msg}") + return self._format_error(action, ssh_cfg.host, error_msg) + + # ── 操作分发 ────────────────────────────────────────────── + + def _dispatch( + self, + action: str, + executor: DockerExecutor, + cfg: DeployConfig, + tail_lines: int | None, + ) -> str: + # 先检查 Docker 环境 + check = executor.check_docker() + if not check.success: + return ( + f"❌ 远程服务器 Docker 不可用\n" + f" 错误: {check.stderr[:200]}\n" + f" 请确认 Docker 已安装并运行: sudo systemctl start docker" + ) + + match action: + case "deploy": + return self._do_deploy(executor, cfg) + case "start": + return self._fmt_single(executor.start(cfg.container_name), "start") + case "stop": + return self._fmt_single(executor.stop(cfg.container_name), "stop") + case "restart": + return self._fmt_single(executor.restart(cfg.container_name), "restart") + case "status": + return self._do_status(executor, cfg.container_name) + case "logs": + return self._do_logs(executor, cfg.container_name, tail_lines) + case "remove": + return self._fmt_single(executor.remove(cfg.container_name), "remove") + case "pull": + return self._fmt_single(executor.pull_image(cfg.image), "pull") + case "inspect": + return self._do_inspect(executor, cfg.container_name) + case "stats": + return self._fmt_single(executor.stats(cfg.container_name), "stats") + case "compose_up": + return self._fmt_single(executor.compose_up(cfg.compose_file), "compose_up") + case "compose_down": + return self._fmt_single(executor.compose_down(cfg.compose_file), "compose_down") + case "compose_ps": + return self._fmt_single(executor.compose_ps(cfg.compose_file), "compose_ps") + case _: + return f"❌ 不支持的操作: {action}" + + def _do_deploy(self, executor: DockerExecutor, cfg: DeployConfig) -> str: + if cfg.compose_file: + result = executor.compose_up(cfg.compose_file) + icon = "✅" if result.success else "❌" + return ( + f"{icon} Compose 部署{'成功' if result.success else '失败'}\n" + f"{'─' * 50}\n" + f" Compose 文件: {cfg.compose_file}\n" + f"{'─' * 50}\n" + f"{result.output[:1500]}" + ) + results = executor.deploy(cfg) + return self._fmt_deploy(results, cfg) + + def _do_status(self, executor: DockerExecutor, container_name: str) -> str: + status_r = executor.status(container_name) + stats_r = executor.stats(container_name) + lines = [ + f"📊 容器状态: {container_name}", + "─" * 50, + status_r.output or "容器不存在或未运行", + ] + if stats_r.success and stats_r.output: + lines += ["", "📈 资源使用:", stats_r.output] + return "\n".join(lines) + + def _do_logs( + self, executor: DockerExecutor, container_name: str, tail: int | None + ) -> str: + result = executor.logs(container_name, tail) + n = tail if tail is not None else _cfg('default_tail_lines', 100) + if result.success: + return ( + f"📋 容器日志: {container_name} (最近 {n} 行)\n" + f"{'─' * 50}\n" + f"{result.output or '(无日志输出)'}" + ) + return f"❌ 获取日志失败: {result.stderr[:300]}" + + def _do_inspect(self, executor: DockerExecutor, container_name: str) -> str: + result = executor.inspect(container_name) + if result.success: + try: + data = json.loads(result.stdout) + if data: + c = data[0] + info = { + "Name": c.get("Name", ""), + "Status": c.get("State", {}).get("Status", ""), + "Image": c.get("Config", {}).get("Image", ""), + "Ports": c.get("NetworkSettings", {}).get("Ports", {}), + "Mounts": [m.get("Source") for m in c.get("Mounts", [])], + "Created": c.get("Created", ""), + } + return ( + f"🔍 容器详情: {container_name}\n" + f"{'─' * 50}\n" + f"{json.dumps(info, ensure_ascii=False, indent=2)}" + ) + except json.JSONDecodeError: + pass + return f"❌ 获取容器详情失败: {result.stderr[:300]}" + + # ── 格式化输出 ───────────────────────────────────────────── + + @staticmethod + def _fmt_deploy(results: list[CommandResult], cfg: DeployConfig) -> str: + lines = [ + "🚀 容器部署结果", + "─" * 50, + f" 镜像 : {cfg.image}", + f" 容器名 : {cfg.container_name}", + f" 端口 : {', '.join(cfg.ports) or '(无)'}", + f" 数据卷 : {', '.join(cfg.volumes) or '(无)'}", + f" 重启策略: {cfg.restart_policy} " + f"[config.yaml default_restart_policy=" + f"{_cfg('default_restart_policy')}]", + "─" * 50, + ] + all_ok = True + for r in results: + icon = "✅" if r.success else "❌" + lines.append(f" {icon} $ {r.command[:70]}") + if r.output: + lines.append(f" └─ {r.output[:150]}") + if not r.success: + all_ok = False + lines.append(f" └─ 错误: {r.stderr[:150]}") + lines.append("─" * 50) + lines.append( + f"✅ 部署成功!容器 [{cfg.container_name}] 已启动" + if all_ok else + "⚠️ 部署过程中有步骤失败,请检查上方错误信息" + ) + return "\n".join(lines) + + @staticmethod + def _fmt_single(result: CommandResult, action: str) -> str: + icon = "✅" if result.success else "❌" + status = "成功" if result.success else "失败" + return ( + f"{icon} {action} {status}\n" + f"{'─' * 40}\n" + f"{result.output[:500] or '(无输出)'}" + ) + + @staticmethod + def _format_error(action: str, host: str, error: str) -> str: + lines = [ + f"❌ SSH Docker [{action}] 操作失败", + "─" * 50, + f" 服务器: {host}", + f" 错误 : {error}", + "─" * 50, + "💡 排查建议:", + ] + el = error.lower() + if "authentication" in el or "auth" in el: + lines += [ + " • 检查用户名/密码是否正确", + " • 检查 SSH 密钥路径和权限(chmod 600 ~/.ssh/id_rsa)", + " • 或在 config.yaml tools.ssh_docker.servers 中配置预设", + ] + elif "connection" in el or "timed out" in el: + lines += [ + " • 检查服务器 IP 和 SSH 端口是否正确", + " • 检查防火墙是否开放 SSH 端口", + f" • config.yaml connect_timeout={_cfg('connect_timeout')}s,可适当增大", + ] + elif "docker" in el: + lines += [ + " • 确认 Docker 已安装: docker --version", + " • 确认 Docker 服务运行: sudo systemctl start docker", + " • 确认用户有 Docker 权限: sudo usermod -aG docker $USER", + ] + return "\n".join(lines) + + # ── 参数校验 ────────────────────────────────────────────── + + @staticmethod + def _validate( + kwargs: dict, + action: str, + image: str, + container_name: str, + compose_file: str, + ) -> str | None: + # 必须提供 host 或 server 之一 + if not kwargs.get("host") and not kwargs.get("server"): + return "❌ 参数错误: 必须提供 host(服务器地址)或 server(预设名称)之一" + + if action not in DockerExecutor.ALLOWED_ACTIONS: + return ( + f"❌ 不支持的操作: {action}\n" + f" 可选值: {', '.join(sorted(DockerExecutor.ALLOWED_ACTIONS))}" + ) + + # 主机白名单检查(来自 config.yaml allowed_hosts) + host = kwargs.get("host", "") + allowed_hosts = _cfg('allowed_hosts', []) + if host and allowed_hosts and host not in allowed_hosts: + return ( + f"❌ 安全限制: 服务器 '{host}' 不在白名单中\n" + f" 白名单: {allowed_hosts}\n" + f" 请在 config.yaml → tools.ssh_docker.allowed_hosts 中添加" + ) + + if action == "deploy" and not image and not compose_file: + return "❌ deploy 操作需要指定 image(镜像名)或 compose_file(Compose 文件路径)" + + needs_container = { + "start", "stop", "restart", "logs", "remove", "inspect", "stats" + } + if action in needs_container and not container_name: + return f"❌ {action} 操作需要指定 container_name(容器名称)" + + needs_compose = {"compose_up", "compose_down", "compose_ps"} + if action in needs_compose and not compose_file: + return f"❌ {action} 操作需要指定 compose_file(docker-compose.yml 路径)" + + return None \ No newline at end of file diff --git a/tools/static_analyzer.py b/tools/static_analyzer.py new file mode 100644 index 0000000..33eadaa --- /dev/null +++ b/tools/static_analyzer.py @@ -0,0 +1,482 @@ +""" +tools/static_analyzer.py +C/C++ 静态分析工具 —— 所有配置通过 settings.tools['static_analyzer'][key] 获取 +""" + +import json +import os +import re +import shutil +import subprocess +import time +from dataclasses import dataclass, field +from pathlib import Path + +from config.settings import settings +from utils.logger import get_logger + +logger = get_logger("TOOL.StaticAnalyzer") + + +# ════════════════════════════════════════════════════════════════ +# 配置访问快捷函数(统一入口,便于调试) +# ════════════════════════════════════════════════════════════════ + +def _cfg(key: str, fallback=None): + """读取 static_analyzer 工具配置,不存在时返回 fallback""" + return settings.tools['static_analyzer'].get(key, fallback) + + +# ════════════════════════════════════════════════════════════════ +# 数据结构 +# ════════════════════════════════════════════════════════════════ + +@dataclass +class AnalysisIssue: + file: str + line: int + column: int + severity: str # error | warning | style | performance | information + rule_id: str + message: str + tool: str + + def to_dict(self) -> dict: + return { + "file": self.file, "line": self.line, "column": self.column, + "severity": self.severity, "rule_id": self.rule_id, + "message": self.message, "tool": self.tool, + } + + def __str__(self) -> str: + return ( + f"[{self.severity.upper():12s}] {self.file}:{self.line}:{self.column}" + f" ({self.rule_id}) {self.message}" + ) + + +@dataclass +class AnalysisResult: + project_dir: str + tool: str + success: bool + issues: list[AnalysisIssue] = field(default_factory=list) + raw_output: str = "" + error: str = "" + elapsed_sec: float = 0.0 + + @property + def error_count(self) -> int: return sum(1 for i in self.issues if i.severity == "error") + @property + def warning_count(self) -> int: return sum(1 for i in self.issues if i.severity == "warning") + @property + def style_count(self) -> int: return sum(1 for i in self.issues if i.severity in ("style", "performance")) + @property + def total_count(self) -> int: return len(self.issues) + + def summary(self) -> str: + max_show = min(20, _cfg('max_issues', 500)) + if not self.success: + return f"❌ 分析失败: {self.error}" + lines = [ + f"📊 静态分析完成 [{self.tool}] 耗时: {self.elapsed_sec:.1f}s", + f" 工程目录 : {self.project_dir}", + f" 问题总计 : {self.total_count} 条", + f" ├─ 错误 (error) : {self.error_count} 条", + f" ├─ 警告 (warning): {self.warning_count} 条", + f" └─ 风格 (style) : {self.style_count} 条", + ] + if self.issues: + lines.append(f"\n📋 问题详情(最多显示 {max_show} 条):") + for issue in self.issues[:max_show]: + lines.append(f" {issue}") + if self.total_count > max_show: + lines.append(f" ... 还有 {self.total_count - max_show} 条") + else: + lines.append(" ✅ 未发现任何问题!") + return "\n".join(lines) + + def to_dict(self) -> dict: + max_issues = _cfg('max_issues', 500) + return { + "project_dir": self.project_dir, + "tool": self.tool, + "success": self.success, + "elapsed_sec": round(self.elapsed_sec, 2), + "stats": { + "total": self.total_count, + "error": self.error_count, + "warning": self.warning_count, + "style": self.style_count, + }, + "issues": [i.to_dict() for i in self.issues[:max_issues]], + "error": self.error, + } + + +# ════════════════════════════════════════════════════════════════ +# 各工具解析器 +# ════════════════════════════════════════════════════════════════ + +class CppcheckParser: + SEVERITY_MAP = { + "error": "error", "warning": "warning", "style": "style", + "performance": "performance", "portability": "style", + "information": "information", + } + + @classmethod + def build_command(cls, project_dir: str, standard: str, extra_args: str) -> list[str]: + jobs = _cfg('jobs', 4) + cfg_extra = _cfg('tool_extra_args', {}).get('cppcheck', '') + full_args = f"{cfg_extra} {extra_args}".strip() + + cmd = [ + "cppcheck", + "--enable=all", + "--xml", "--xml-version=2", + f"--std={standard}", + f"-j{jobs}", + ] + if full_args: + cmd.extend(full_args.split()) + cmd.append(project_dir) + return cmd + + @classmethod + def parse(cls, output: str, tool: str = "cppcheck") -> list[AnalysisIssue]: + issues: list[AnalysisIssue] = [] + try: + import xml.etree.ElementTree as ET + root = ET.fromstring(output) + for error in root.iter("error"): + severity = cls.SEVERITY_MAP.get(error.get("severity", "warning"), "warning") + rule_id = error.get("id", "unknown") + message = error.get("msg", "") + loc = error.find("location") + if loc is not None: + file_path = loc.get("file", "unknown") + line = int(loc.get("line", 0)) + column = int(loc.get("column", 0)) + else: + file_path, line, column = "unknown", 0, 0 + issues.append(AnalysisIssue( + file=file_path, line=line, column=column, + severity=severity, rule_id=rule_id, + message=message, tool=tool, + )) + except Exception as e: + logger.warning(f"⚠️ XML 解析失败,回退文本解析: {e}") + issues = cls._parse_text(output, tool) + return issues + + @staticmethod + def _parse_text(output: str, tool: str) -> list[AnalysisIssue]: + issues = [] + pattern = re.compile( + r"^(.+?):(\d+):(\d+):\s+(error|warning|style|performance|information):\s+" + r"(.+?)(?:\s+\[(\w+)\])?$", re.MULTILINE, + ) + for m in pattern.finditer(output): + issues.append(AnalysisIssue( + file=m.group(1), line=int(m.group(2)), column=int(m.group(3)), + severity=m.group(4), rule_id=m.group(6) or "unknown", + message=m.group(5), tool=tool, + )) + return issues + + +class ClangTidyParser: + @classmethod + def build_command(cls, project_dir: str, standard: str, extra_args: str) -> list[str]: + cfg_extra = _cfg('tool_extra_args', {}).get('clang-tidy', '') + full_extra = f"{cfg_extra} {extra_args}".strip() + + # 从 extra 中提取 --checks 值 + m = re.search(r"--checks=(\S+)", full_extra) + checks = m.group(1) if m else "*" + + if shutil.which("run-clang-tidy"): + cmd = [ + "run-clang-tidy", + f"-checks={checks}", + "-p", os.path.join(project_dir, "build"), + ] + else: + cmd = ["clang-tidy"] + if full_extra: + cmd.extend(full_extra.split()) + src_files = [] + for ext in ("*.cpp", "*.c", "*.cc", "*.cxx"): + src_files.extend(Path(project_dir).rglob(ext)) + cmd.extend(str(f) for f in src_files[:50]) + return cmd + + @classmethod + def parse(cls, output: str, tool: str = "clang-tidy") -> list[AnalysisIssue]: + issues = [] + pattern = re.compile( + r"^(.+?):(\d+):(\d+):\s+(error|warning|note):\s+(.+?)(?:\s+\[([\w\-\.]+)\])?$", + re.MULTILINE, + ) + for m in pattern.finditer(output): + if m.group(4) == "note": + continue + issues.append(AnalysisIssue( + file=m.group(1), line=int(m.group(2)), column=int(m.group(3)), + severity=m.group(4), rule_id=m.group(6) or "unknown", + message=m.group(5), tool=tool, + )) + return issues + + +class InferParser: + @classmethod + def build_command(cls, project_dir: str, standard: str, extra_args: str) -> list[str]: + cfg_extra = _cfg('tool_extra_args', {}).get('infer', '') + full_extra = f"{cfg_extra} {extra_args}".strip() + cmd = [ + "infer", "run", + "--results-dir", os.path.join(project_dir, "infer-out"), + ] + if full_extra: + cmd.extend(full_extra.split()) + cmd += ["--", "make", "-C", project_dir] + return cmd + + @classmethod + def parse(cls, output: str, tool: str = "infer") -> list[AnalysisIssue]: + issues = [] + try: + data = json.loads(output) + for item in data: + issues.append(AnalysisIssue( + file=item.get("file", "unknown"), + line=item.get("line", 0), + column=0, + severity="error" if item.get("severity") == "ERROR" else "warning", + rule_id=item.get("bug_type", "unknown"), + message=item.get("qualifier", ""), + tool=tool, + )) + except json.JSONDecodeError: + pattern = re.compile(r"(.+\.(?:cpp|c|cc|h)):(\d+):\s+(?:error|warning):\s+(.+)") + for m in pattern.finditer(output): + issues.append(AnalysisIssue( + file=m.group(1), line=int(m.group(2)), column=0, + severity="warning", rule_id="infer", + message=m.group(3), tool=tool, + )) + return issues + + +_TOOL_REGISTRY: dict[str, type] = { + "cppcheck": CppcheckParser, + "clang-tidy": ClangTidyParser, + "infer": InferParser, +} + + +# ════════════════════════════════════════════════════════════════ +# 主工具类 +# ════════════════════════════════════════════════════════════════ + +class StaticAnalyzerTool: + """ + C/C++ 静态分析工具 + 所有配置均通过 settings.tools['static_analyzer'][key] 读取 + """ + + name = "static_analyzer" + description = ( + "对指定目录下的 C/C++ 工程调用外部静态分析工具(cppcheck/clang-tidy/infer)" + "进行代码质量检查,返回错误、警告及代码风格问题" + ) + parameters = { + "project_dir": { + "type": "string", + "description": "C/C++ 工程根目录的绝对路径,例如 /home/user/myproject", + }, + "tool": { + "type": "string", + "description": "静态分析工具: cppcheck(默认)| clang-tidy | infer", + "enum": ["cppcheck", "clang-tidy", "infer"], + }, + "standard": { + "type": "string", + "description": "C/C++ 语言标准: c89 | c99 | c11 | c++11 | c++14 | c++17 | c++20", + }, + "extra_args": { + "type": "string", + "description": "额外命令行参数(追加到 config.yaml tool_extra_args 之后)", + }, + "output_format": { + "type": "string", + "description": "输出格式: summary(默认)| json | full", + "enum": ["summary", "json", "full"], + }, + "timeout": { + "type": "integer", + "description": "分析超时秒数(不传则使用 config.yaml 中的 timeout)", + }, + } + + def execute(self, **kwargs) -> str: + # ── 读取参数,未提供时使用 config.yaml 中的默认值 ────── + project_dir = kwargs.get("project_dir", "") + tool_name = kwargs.get("tool", _cfg('default_tool', 'cppcheck')).lower() + standard = kwargs.get("standard", _cfg('default_std', 'c++17')) + extra_args = kwargs.get("extra_args", "") + output_format = kwargs.get("output_format", _cfg('output_format', 'summary')) + timeout = int(kwargs.get("timeout", _cfg('timeout', 120))) + + logger.info( + f"🔍 静态分析启动\n" + f" 工程目录 : {project_dir}\n" + f" 分析工具 : {tool_name} " + f"[config default_tool={_cfg('default_tool')}]\n" + f" 语言标准 : {standard} " + f"[config default_std={_cfg('default_std')}]\n" + f" 超时 : {timeout}s " + f"[config timeout={_cfg('timeout')}s]\n" + f" 并行数 : {_cfg('jobs')} " + f"[config jobs={_cfg('jobs')}]\n" + f" 最大问题数: {_cfg('max_issues')}" + ) + + # ── 参数校验 ────────────────────────────────────────── + err = self._validate(project_dir, tool_name) + if err: + return err + + # ── 构造并执行命令 ──────────────────────────────────── + parser_cls = _TOOL_REGISTRY[tool_name] + try: + cmd = parser_cls.build_command(project_dir, standard, extra_args) + except Exception as e: + return f"❌ 构造分析命令失败: {e}" + + logger.info(f"🚀 执行命令: {' '.join(cmd)}") + result = self._run_command(cmd, project_dir, timeout, tool_name) + + # 截断超过 max_issues 的问题 + max_issues = _cfg('max_issues', 500) + if len(result.issues) > max_issues: + logger.info(f"⚠️ 问题数 {len(result.issues)} 超过上限 {max_issues},已截断") + result.issues = result.issues[:max_issues] + + return self._format_output(result, output_format) + + # ── 私有方法 ────────────────────────────────────────────── + + @staticmethod + def _validate(project_dir: str, tool_name: str) -> str | None: + if not project_dir: + return "❌ 参数错误: project_dir 不能为空" + + path = Path(project_dir) + if not path.exists(): + return f"❌ 目录不存在: {project_dir}" + if not path.is_dir(): + return f"❌ 路径不是目录: {project_dir}" + + # 白名单校验(来自 config.yaml allowed_roots) + allowed_roots = _cfg('allowed_roots', []) + if allowed_roots and not any( + project_dir.startswith(r) for r in allowed_roots + ): + return ( + f"❌ 安全限制: {project_dir} 不在白名单中\n" + f" 白名单: {allowed_roots}\n" + f" 请在 config.yaml → tools.static_analyzer.allowed_roots 中添加" + ) + + # 检查是否包含 C/C++ 源文件 + src_files = ( + list(path.rglob("*.cpp")) + list(path.rglob("*.c")) + + list(path.rglob("*.cc")) + list(path.rglob("*.h")) + ) + if not src_files: + return f"❌ 目录中未找到 C/C++ 源文件: {project_dir}" + + if tool_name not in _TOOL_REGISTRY: + return ( + f"❌ 不支持的分析工具: {tool_name}\n" + f" 可选值: {', '.join(_TOOL_REGISTRY.keys())}" + ) + + exe = "run-clang-tidy" if tool_name == "clang-tidy" else tool_name + if not shutil.which(exe) and not shutil.which(tool_name): + return ( + f"❌ 分析工具未安装: {tool_name}\n" + f" 安装方式:\n" + f" cppcheck : sudo apt install cppcheck\n" + f" clang-tidy: sudo apt install clang-tidy\n" + f" infer : https://fbinfer.com/docs/getting-started" + ) + return None + + @staticmethod + def _run_command( + cmd: list[str], project_dir: str, timeout: int, tool_name: str, + ) -> AnalysisResult: + start = time.time() + try: + proc = subprocess.run( + cmd, cwd=project_dir, + capture_output=True, text=True, + timeout=timeout, encoding="utf-8", errors="replace", + ) + elapsed = time.time() - start + raw_output = proc.stderr if proc.stderr.strip() else proc.stdout + logger.debug(f"📄 原始输出(前 500 字符):\n{raw_output[:500]}") + + parser_cls = _TOOL_REGISTRY[tool_name] + issues = parser_cls.parse(raw_output, tool_name) + + if tool_name == "infer": + report_path = Path(project_dir) / "infer-out" / "report.json" + if report_path.exists(): + issues = InferParser.parse( + report_path.read_text(encoding="utf-8"), "infer" + ) + + logger.info(f"✅ 分析完成: {len(issues)} 个问题,耗时 {elapsed:.1f}s") + return AnalysisResult( + project_dir=project_dir, tool=tool_name, + success=True, issues=issues, + raw_output=raw_output, elapsed_sec=elapsed, + ) + + except subprocess.TimeoutExpired: + elapsed = time.time() - start + msg = ( + f"分析超时(>{timeout}s)\n" + f" 请增大 config.yaml → tools.static_analyzer.timeout" + ) + logger.error(f"⏰ {msg}") + return AnalysisResult( + project_dir=project_dir, tool=tool_name, + success=False, error=msg, elapsed_sec=elapsed, + ) + except FileNotFoundError: + return AnalysisResult( + project_dir=project_dir, tool=tool_name, + success=False, error=f"命令未找到: {cmd[0]}", + ) + except Exception as e: + return AnalysisResult( + project_dir=project_dir, tool=tool_name, + success=False, error=str(e), + ) + + @staticmethod + def _format_output(result: AnalysisResult, fmt: str) -> str: + if fmt == "json": + return json.dumps(result.to_dict(), ensure_ascii=False, indent=2) + if fmt == "full": + return ( + f"{result.summary()}\n\n{'─' * 60}\n" + f"📄 原始输出:\n{result.raw_output[:3000]}" + ) + return result.summary() \ No newline at end of file diff --git a/tools/web_search.py b/tools/web_search.py index 54cad38..10f776a 100644 --- a/tools/web_search.py +++ b/tools/web_search.py @@ -29,11 +29,11 @@ class WebSearchTool(BaseTool): def __init__(self): super().__init__() - cfg = settings.tools.web_search - self._default_max = cfg.max_results - self._engine = cfg.engine - self._api_key = cfg.api_key - self._timeout = cfg.timeout + cfg = settings.tools['web_search'] + self._default_max = cfg['max_results'] + self._engine = cfg['engine'] + self._api_key = cfg['api_key'] + self._timeout = cfg['timeout'] self.logger.debug( f"⚙️ WebSearch engine={self._engine}, " f"max_results={self._default_max}, "