From 246b2a485f0e4359906cda560fafbbf27a000417 Mon Sep 17 00:00:00 2001 From: sontolau Date: Sat, 6 Jun 2026 16:42:07 +0800 Subject: [PATCH] in dev --- agent/agent.py | 20 +- client/agent_client.py | 12 +- config.yaml | 170 --------- config/settings.py | 20 ++ device.py | 30 ++ envs | 7 - install.sh | 19 - logs/agent.log | 1 + main.py | 159 --------- mcp/mcp_server.py | 6 +- mcp/mcp_skill_client.py | 4 +- mcp/skill_loader.py | 2 +- mcp/skill_registry.py | 14 +- run.sh | 36 -- stop.sh | 23 -- tools/base_tool.py | 5 +- tools/calculator.py | 113 ------ tools/code_executor.py | 149 -------- tools/file_reader.py | 179 ---------- tools/ssh_docker.py | 733 --------------------------------------- tools/static_analyzer.py | 483 -------------------------- tools/uav_control.py | 42 +++ tools/uav_get_state.py | 37 ++ tools/uav_self_check.py | 123 +++++++ tools/web_search.py | 6 +- 25 files changed, 283 insertions(+), 2110 deletions(-) delete mode 100644 config.yaml create mode 100644 device.py delete mode 100644 envs delete mode 100755 install.sh delete mode 100644 main.py delete mode 100755 run.sh delete mode 100755 stop.sh delete mode 100644 tools/calculator.py delete mode 100644 tools/code_executor.py delete mode 100644 tools/file_reader.py delete mode 100644 tools/ssh_docker.py delete mode 100644 tools/static_analyzer.py create mode 100644 tools/uav_control.py create mode 100644 tools/uav_get_state.py create mode 100644 tools/uav_self_check.py diff --git a/agent/agent.py b/agent/agent.py index 112e678..024e7c2 100644 --- a/agent/agent.py +++ b/agent/agent.py @@ -8,9 +8,9 @@ import time from dataclasses import dataclass, field from typing import Any -from config.settings import settings -from mcp.skill_registry import DispatchResult, SkillRegistry -from utils.logger import get_logger +from agent.config.settings import settings +from agent.mcp.skill_registry import DispatchResult, SkillRegistry +from agent.utils.logger import get_logger logger = get_logger("Agent") @@ -210,14 +210,6 @@ class Agent: 可用工具如下: {tools} - - 请严格按照以下格式进行回应: - - Thought: 你的思考过程,用于分析问题、拆解任务和规划下一步行动。 - Action: 你决定采取的行动,必须是以下格式之一: - - `{{tool_name}}[{{tool_input}}]`:调用一个可用工具。 - - `Finish[最终答案]`:当你认为已经获得最终答案时。 - - 当你收集到足够的信息,能够回答用户的最终问题时,你必须在Action:字段后使用 Finish[最终答案] 来输出最终答案。 """ def __init__( @@ -380,7 +372,7 @@ class Agent: # ── 消息构造 ────────────────────────────────────────────── def _build_messages(self, tools, history: list[Message]) -> list[dict]: - prompt = self.system_prompt.format(tools="\n".join(f"{tool['name']}: {tool['description']}" for tool in tools)) + prompt = self.system_prompt messages = [{"role": "system", "content":prompt}] messages += [m.to_api_dict() for m in history] return messages @@ -404,7 +396,7 @@ class Agent: # Demo 入口 # ════════════════════════════════════════════════════════════════ -def create_agent() -> tuple[Agent, SkillRegistry]: +def create_agent(agent_prompt=None) -> tuple[Agent, SkillRegistry]: """ 工厂函数:创建并初始化 Agent + SkillRegistry @@ -416,7 +408,7 @@ def create_agent() -> tuple[Agent, SkillRegistry]: registry.load_local_tools() # 连接在线 MCP Skill(来自 config.yaml mcp_skills) registry.connect_mcp_skills() - agent = Agent(registry) + agent = Agent(registry, system_prompt=agent_prompt) return agent, registry diff --git a/client/agent_client.py b/client/agent_client.py index 7e1d33e..e76fe26 100644 --- a/client/agent_client.py +++ b/client/agent_client.py @@ -8,16 +8,14 @@ client/agent_client.py import json import uuid from dataclasses import dataclass, field - -from config.settings import settings -from llm.llm_engine import LLMEngine -from mcp.mcp_protocol import ( +from agent.llm.llm_engine import LLMEngine +from agent.mcp.mcp_protocol import ( ChainPlan, ChainResult, MCPRequest, MCPResponse, StepResult, ToolStep, ) -from mcp.mcp_server import MCPServer -from memory.memory_store import MemoryStore -from utils.logger import get_logger +from agent.mcp.mcp_server import MCPServer +from agent.memory.memory_store import MemoryStore +from agent.utils.logger import get_logger @dataclass diff --git a/config.yaml b/config.yaml deleted file mode 100644 index bbd4153..0000000 --- a/config.yaml +++ /dev/null @@ -1,170 +0,0 @@ -# ════════════════════════════════════════════════════════════════ -# config/config.yaml — Agent 系统全局配置文件 -# ════════════════════════════════════════════════════════════════ - -# ── LLM 模型配置 ─────────────────────────────────────────────── -llm: - provider: "openai" - model_name: "gpt-4o" - api_key: "sk-AUmOuFI731Ty5Nob38jY26d8lydfDT-QkE2giqb0sCuPCAE2JH6zjLM4lZLpvL5WMYPOocaMe2FwVDmqM_9KimmKACjR" # 优先读取环境变量 LLM_API_KEY - api_base_url: "https://openapi.monica.im/v1" # 自定义代理地址,留空使用官方 - max_tokens: 4096 - temperature: 0.7 - timeout: 60 - max_retries: 3 - function_calling: true - stream: false - model_path: "" - ollama_host: "http://localhost:11434" -database: - type: "sqlite" - url: "sqlite:///skills.db" -skills_directory: "./" # 新增:SKILL.md 文件所在目录 -# ── 本地 MCP Server 配置 ─────────────────────────────────────── -mcp: - server_name: "MCPServer" - transport: "stdio" - host: "localhost" - port: 3000 - # 本地注册的工具列表 - enabled_tools: -# - calculator - - web_search -# - file_reader -# - code_executor - -# ── 在线 MCP Skill 配置 ──────────────────────────────────────── -# 每一项代表一个远端 MCP Server,其暴露的所有工具将作为 skill 注册到 Agent -mcp_skills: - - # 示例一:SSE 传输(最常见的在线 MCP Server 形式) -# - name: "everything" # skill 组名称(用于日志/调试) -# enabled: true -# transport: "sse" # sse | http | stdio -# url: "http://localhost:3001/sse" -# # 请求头(可用于 API Key 认证) -# headers: -# Authorization: "" # 优先读取环境变量 MCP_EVERYTHING_TOKEN -# timeout: 30 # 连接超时(秒) -# retry: 2 # 失败重试次数 -# # 只暴露指定工具(空列表=全部暴露) -# include_tools: [] -# # 排除指定工具 -# exclude_tools: [] - - # 示例二:Streamable HTTP 传输 -# - name: "remote-tools" -# enabled: false -# transport: "http" -# url: "http://api.example.com/mcp" -# headers: -# Authorization: "Bearer your_token_here" -# X-Client-ID: "agent-demo" -# timeout: 30 -# retry: 2 -# include_tools: [] -# exclude_tools: [] - - # 示例三:stdio 子进程(本地可执行文件作为 MCP Server) -# - name: "filesystem" -# enabled: false -# transport: "stdio" -# # stdio 模式使用 command 启动子进程,不需要 url -# command: "npx" -# args: -# - "-y" -# - "@modelcontextprotocol/server-filesystem" -# - "/tmp" -# env: -# NODE_ENV: "production" -# timeout: 30 -# retry: 1 -# include_tools: [] -# exclude_tools: [] - - name: "hexstrike-ai" - enabled: true - transport: "stdio" - command: "python3" - args: - - "/Users/sontolau/Applications/hexstrike-ai/hexstrike_mcp.py" - - "--server" - - "http://localhost:8999" - description: "HexStrike AI v6.0 - Advanced Cybersecurity Automation Platform" - timeout: 300 - # 示例四:带鉴权的在线 MCP SaaS 服务 -# - name: "brave-search" -# enabled: false -# transport: "sse" -# url: "https://mcp.brave.com/sse" -# headers: -# Authorization: "" # 优先读取环境变量 MCP_BRAVE_SEARCH_TOKEN -# timeout: 20 -# retry: 2 -# include_tools: -# - "brave_web_search" -# - "brave_local_search" -# exclude_tools: [] - -# ── 工具配置 ─────────────────────────────────────────────────── -tools: - calculator: - precision: 10 - - web_search: - max_results: 5 - timeout: 10 - api_key: "7917bef5e46044af5209fdb78518be98be394f3fe763bbce3fbb503280408bd9" - - 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 行为配置 ───────────────────────────────────────────── -agent: - max_chain_steps: 10 - enable_multi_step: true - session_timeout: 3600 - fallback_to_rules: true \ No newline at end of file diff --git a/config/settings.py b/config/settings.py index 36da518..1bc1b13 100644 --- a/config/settings.py +++ b/config/settings.py @@ -221,6 +221,7 @@ class MCPConfig: transport: str = "stdio" host: str = "localhost" port: int = 3000 + tools_directory: str = "tools" enabled_tools: list[str] = field(default_factory=lambda: [ "calculator", "web_search", "file_reader", "code_executor", "static_analyzer", "ssh_docker", @@ -257,6 +258,12 @@ class DatabaseConfig: type: str = "sqlite" url: str = None +@dataclass +class DeviceConfig: + type: str + device_id: str + protocol: str + params: dict # ════════════════════════════════════════════════════════════════ # 顶层 AppConfig # ════════════════════════════════════════════════════════════════ @@ -287,6 +294,7 @@ class AppConfig: memory: MemoryConfig, logging: LoggingConfig, agent: AgentConfig, + device: DeviceConfig, skills_directory: str = "./skills", database: DatabaseConfig = DatabaseConfig() ): @@ -297,6 +305,7 @@ class AppConfig: self.memory = memory self.logging = logging self.agent = agent + self.device = device self.skills_directory = skills_directory self.database = database @@ -382,12 +391,22 @@ class ConfigLoader: logging=cls._build_logging(raw.get("logging", {})), agent=cls._build_agent(raw.get("agent", {})), skills_directory=raw.get("skills_directory", ""), + device=cls._build_device(raw.get("device", {})), database=DatabaseConfig( type=raw.get("database", {}).get("type", "sqlite"), url=raw.get("database", {}).get("url", "sqlite:///skills.db"), ) ) + @staticmethod + def _build_device(d: dict): + return DeviceConfig( + type=d.get("type", None), + device_id=d.get("device_id", None), + protocol=d.get("protocol", None), + params=d.get("params", {}) + ) + # ── LLM ─────────────────────────────────────────────────── @staticmethod @@ -418,6 +437,7 @@ class ConfigLoader: transport=d.get("transport", df["transport"]), host=d.get("host", df["host"]), port=int(d.get("port", df["port"])), + tools_directory=d.get("tools_directory", "tools"), enabled_tools=d.get("enabled_tools", df["enabled_tools"]), ) diff --git a/device.py b/device.py new file mode 100644 index 0000000..c2ce6fa --- /dev/null +++ b/device.py @@ -0,0 +1,30 @@ +from agent.config.settings import settings +from core.uas_control.controllers.robot_dog_controller import RobotDogController +from core.uas_control.controllers.uav_controller import UAVController +from core.uas_control.protocols.mavlink_adapter import MAVLinkAdapter +from core.uas_control.protocols.ros_adapter import ROSAdapter +from core.uas_control.protocols.simulation_adapter import SimulationAdapter + +uas_type = settings.device.type +protocol = settings.device.protocol +device_id = settings.device.device_id + +if protocol == "mavlink": + adapter = MAVLinkAdapter(device_id=device_id, **settings.device.params) +elif protocol == "ros": + adapter = ROSAdapter(device_id=device_id, **settings.device.params) +else: + adapter = SimulationAdapter(device_id=device_id, **settings.device.params) + +if uas_type == "uav": + controller = UAVController(device_id=device_id, adapter=adapter) +elif uas_type == "robot_dog": + controller = RobotDogController(device_id=device_id, adapter=adapter) +else: + controller = UAVController(device_id=device_id, adapter=adapter) + +# results, errors = [], [] +# controller.command_result.connect(lambda r: results.append(r)) +# controller.error_occurred.connect(lambda m: errors.append(m)) +# assert controller.connect(), "连接应成功" + diff --git a/envs b/envs deleted file mode 100644 index 12a845a..0000000 --- a/envs +++ /dev/null @@ -1,7 +0,0 @@ -LOG_DIR="./logs" # 日志目录 - -# --- HEXSTRIKE_AI 配置项 --- -HEXSTRIKE_AI_ENTRY="hexstrike_server:app" # WSGI 入口,格式为 模块名:变量名 -HEXSTRIKE_AI_BIND_ADDRESS="127.0.0.1:8000" # 监听地址 -HEXSTRIKE_AI_WORKERS=4 # 工作进程数 -HEXSTRIKE_AI_PID_FILE="./hexstrike_ai.pid" # PID 文件保存位置 \ No newline at end of file diff --git a/install.sh b/install.sh deleted file mode 100755 index fe2136b..0000000 --- a/install.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -SKILLS_DIR="./skills" - -function install () { - name=$1 - url=$2 - - if ! [ -d "${SKILLS_DIR}/$name" ]; then - git clone $url ${SKILLS_DIR}/$name - fi - requirements_file="${SKILLS_DIR}/$name/requirements.txt" - if [ -f ${requirements_file} ]; then - pip install -r $requirements_file - fi -} - -pip install -r ./requirements.txt -install "hexstrike_ai" "https://github.com/0x4m4/hexstrike-ai.git" \ No newline at end of file diff --git a/logs/agent.log b/logs/agent.log index af1d0c3..901affe 100644 --- a/logs/agent.log +++ b/logs/agent.log @@ -7577,3 +7577,4 @@ The function `get_system_name()` uses `platform.system()` to determine the syste 时间, 08:00, 11:00 ; 天气 ; 气温, 23.9℃, 2026-06-01 15:52:17 [INFO ] agent.Agent │ 🔁 推理步骤 2/10 2026-06-01 15:52:19 [DEBUG ] agent.Agent │ LLM 响应: finish=stop tool_calls=0 content=今天成都双流区的天气为多云,气温范围大约在22℃到32℃,风力较轻,适合外出活动。请注意根据具体天气情况,适当增减衣物。 +2026-06-01 15:54:14 [INFO ] agent.MCP.SkillRegistry │ 🔌 SkillRegistry 已关闭所有连接 diff --git a/main.py b/main.py deleted file mode 100644 index 90844e0..0000000 --- a/main.py +++ /dev/null @@ -1,159 +0,0 @@ -""" -main.py -项目入口 —— 启动 Agent 交互式对话 或 MCP Server stdio 模式 - -用法: - python main.py # 启动 Agent 交互式对话(默认) - python main.py --mode agent # 同上 - python main.py --mode mcp # 启动本地 MCP Server(stdio 模式) - python main.py --mode check # 检查配置和依赖 -""" - -import argparse -import atexit -import sys - - -def run_agent() -> None: - """启动 Agent 交互式对话""" - from agent.agent import create_agent - from config.settings import settings - - print(settings.display()) - - agent, registry = create_agent() - atexit.register(registry.close) - - print(agent.show_tools()) - print("─" * 60) - print("💡 命令: exit=退出 reset=清空历史 tools=查看工具列表") - print("─" * 60) - - while True: - try: - user_input = input("\n🧑 You: ").strip() - except (EOFError, KeyboardInterrupt): - print("\n👋 再见!") - break - - if not user_input: - continue - - match user_input.lower(): - case "exit" | "quit": - print("👋 再见!") - break - case "reset": - agent.reset() - print("🔄 对话历史已清空") - case "tools": - print(agent.show_tools()) - case _: - reply = agent.chat(user_input) - print(f"\n🤖 Agent: {reply}") - - -def run_mcp_server() -> None: - """启动本地 MCP Server(stdio 模式)""" - from mcp.mcp_server import MCPServer - with MCPServer() as server: - server.run_stdio() - - -# def run_check() -> None: -# """检查配置和依赖完整性""" -# print("=" * 60) -# print(" 🔍 项目依赖检查") -# print("=" * 60) -# -# checks = [ -# ("pyyaml", "yaml", "pip install pyyaml"), -# ("openai", "openai", "pip install openai>=1.0.0"), -# ("httpx", "httpx", "pip install httpx>=0.27.0"), -# ("httpx-sse", "httpx_sse", "pip install httpx-sse>=0.4.0"), -# ("paramiko", "paramiko", "pip install paramiko>=3.0.0"), -# ] -# -# all_ok = True -# for pkg_name, import_name, install_cmd in checks: -# try: -# __import__(import_name) -# print(f" ✅ {pkg_name:<15} 已安装") -# except ImportError: -# print(f" ❌ {pkg_name:<15} 未安装 → {install_cmd}") -# all_ok = False -# -# print() -# -# # 配置检查 -# try: -# from config.settings import settings -# print(" ✅ config/settings.py 加载成功") -# print(f" LLM : {settings.llm.provider} / {settings.llm.model_name}") -# print(f" 本地工具: {settings.mcp.enabled_tools}") -# skills = settings.enabled_mcp_skills -# if skills: -# print(f" 在线Skill: {[s.name for s in skills]}") -# else: -# print(" 在线Skill: (未配置)") -# except Exception as e: -# print(f" ❌ 配置加载失败: {e}") -# all_ok = False -# -# print() -# -# # 工具注册检查 -# try: -# from mcp.skill_registry import SkillRegistry -# from tools.calculator import CalculatorTool -# from tools.code_executor import CodeExecutorTool -# from tools.file_reader import FileReaderTool -# from tools.ssh_docker import SSHDockerTool -# from tools.static_analyzer import StaticAnalyzerTool -# from tools.web_search import WebSearchTool -# -# registry = SkillRegistry() -# registry.register_local_many( -# CalculatorTool(), WebSearchTool(), FileReaderTool(), -# CodeExecutorTool(), StaticAnalyzerTool(), SSHDockerTool(), -# ) -# tools = registry.list_all_tools() -# print(f" ✅ 本地工具注册 共 {len(tools)} 个:") -# for t in tools: -# print(f" 🔵 {t['name']}: {t['description'][:50]}") -# except Exception as e: -# print(f" ❌ 工具注册失败: {e}") -# all_ok = False -# -# print() -# print("=" * 60) -# if all_ok: -# print(" ✅ 所有检查通过,项目可正常运行") -# else: -# print(" ⚠️ 存在问题,请按提示安装缺失依赖") -# print("=" * 60) - - -def main() -> None: - parser = argparse.ArgumentParser( - description="Agent Demo —— 支持本地工具 + 在线 MCP Skill" - ) - parser.add_argument( - "--mode", - choices=["agent", "mcp", "check"], - default="agent", - help="运行模式: agent(交互对话)| mcp(MCP Server)| check(依赖检查)", - ) - args = parser.parse_args() - - match args.mode: - case "agent": - run_agent() - case "mcp": - run_mcp_server() - # case "check": - # run_check() - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/mcp/mcp_server.py b/mcp/mcp_server.py index 860924d..9eb73ee 100644 --- a/mcp/mcp_server.py +++ b/mcp/mcp_server.py @@ -6,9 +6,9 @@ mcp/mcp_server.py import json import sys from typing import Any -from config.settings import settings -from mcp.skill_registry import SkillRegistry -from utils.logger import get_logger +from agent.config.settings import settings +from agent.mcp.skill_registry import SkillRegistry +from agent.utils.logger import get_logger logger = get_logger("MCP.Server") diff --git a/mcp/mcp_skill_client.py b/mcp/mcp_skill_client.py index ff03a32..a7f8d23 100644 --- a/mcp/mcp_skill_client.py +++ b/mcp/mcp_skill_client.py @@ -21,8 +21,8 @@ import uuid from dataclasses import dataclass, field from typing import Any, Iterator -from config.settings import MCPSkillConfig -from utils.logger import get_logger +from agent.config.settings import MCPSkillConfig +from agent.utils.logger import get_logger logger = get_logger("MCP.SkillClient") diff --git a/mcp/skill_loader.py b/mcp/skill_loader.py index d2655a5..c9ac318 100644 --- a/mcp/skill_loader.py +++ b/mcp/skill_loader.py @@ -2,7 +2,7 @@ import os import json import re from typing import Any, Dict, List, Tuple -from utils.logger import get_logger +from agent.utils.logger import get_logger import yaml logger = get_logger("mcp.SkillLoader") diff --git a/mcp/skill_registry.py b/mcp/skill_registry.py index 1790e41..2a308ed 100644 --- a/mcp/skill_registry.py +++ b/mcp/skill_registry.py @@ -14,11 +14,11 @@ import time from dataclasses import dataclass from typing import Any -from config.settings import settings -from mcp.mcp_skill_client import MCPSkillClient, RemoteTool, ToolCallResult -from mcp.skill_loader import SkillLoader -from tools.base_tool import BaseTool -from utils.logger import get_logger +from agent.config.settings import settings +from agent.mcp.mcp_skill_client import MCPSkillClient, RemoteTool, ToolCallResult +from agent.mcp.skill_loader import SkillLoader +from agent.tools.base_tool import BaseTool +from agent.utils.logger import get_logger logger = get_logger("MCP.SkillRegistry") @@ -111,8 +111,10 @@ class SkillRegistry: enabled = settings.mcp.enabled_tools logger.info(f"🔧 注册本地工具: {enabled}") for tool_name in enabled: - tool_path = f"tools/{tool_name}.py" + tool_path =f"{settings.mcp.tools_directory}/{tool_name}.py" + # tool_path = f"tools/{tool_name}.py" if not os.path.exists(tool_path): + logger.warning(f"未找到工具:{tool_path}") continue # 动态加载模块 spec = importlib.util.spec_from_file_location(tool_name, tool_path) diff --git a/run.sh b/run.sh deleted file mode 100755 index d27cdba..0000000 --- a/run.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash - -if [ -f "./envs" ]; then - source ./envs -fi - -# 创建日志目录 -if ! [ -d ${LOG_DIR} ]; then - mkdir -p $LOG_DIR -fi - -function start_hexstrike_ai() { - if [ -f "$HEXSTRIKE_AI_PID_FILE" ]; then - echo "错误: hexstrike_ai 似乎已在运行 (PID: $(cat $HEXSTRIKE_AI_PID_FILE))" - exit 1 - fi - # 启动 Gunicorn - echo "正在启动 hexstrike_ai" - cd skills/hexstrike_ai && gunicorn -w ${HEXSTRIKE_AI_WORKERS} \ - -b ${HEXSTRIKE_AI_BIND_ADDRESS} \ - --pid ${HEXSTRIKE_AI_PID_FILE} \ - --access-logfile "$LOG_DIR/access.log" \ - --error-logfile "$LOG_DIR/error.log" \ - -D \ - ${HEXSTRIKE_AI_ENTRY} - - if [ $? -eq 0 ]; then - echo "启动成功!PID: $(cat $PID_FILE)" - else - echo "启动失败,请检查日志。" - fi -} - - - -start_hexstrike_ai \ No newline at end of file diff --git a/stop.sh b/stop.sh deleted file mode 100755 index e54465b..0000000 --- a/stop.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -if [ -f "./envs" ]; then - source ./envs -fi - -function stop() { - PID_FILE=$1 - echo $PID_FILE - if [ -f "$PID_FILE" ]; then - PID=$(cat $PID_FILE) - echo "正在停止进程 $PID..." - kill $PID - # 循环等待进程结束并清理 PID 文件 - while ps -p $PID > /dev/null; do sleep 1; done - rm $PID_FILE - echo "服务已停止。" - else - echo "未发现正在运行的服务 (未找到 PID 文件)。" - fi -} - -stop ${HEXSTRIKE_AI_PID_FILE} \ No newline at end of file diff --git a/tools/base_tool.py b/tools/base_tool.py index ef4f48a..7a4ad48 100644 --- a/tools/base_tool.py +++ b/tools/base_tool.py @@ -6,9 +6,8 @@ tools/base_tool.py from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Any - -from mcp.mcp_protocol import ToolSchema -from utils.logger import get_logger +from agent.mcp.mcp_protocol import ToolSchema +from agent.utils.logger import get_logger @dataclass diff --git a/tools/calculator.py b/tools/calculator.py deleted file mode 100644 index 32659a8..0000000 --- a/tools/calculator.py +++ /dev/null @@ -1,113 +0,0 @@ -""" -tools/calculator.py -数学计算工具 —— 支持基本四则运算及常用数学函数 -配置通过 settings.tools['calculator'] 读取 -""" - -import math -import operator -from typing import Any - -from config.settings import settings -from tools.base_tool import BaseTool -from utils.logger import get_logger - -logger = get_logger("TOOL.Calculator") - - -def _cfg(key: str, fallback=None): - return settings.tools['calculator'].get(key, fallback) - - -class Tool(BaseTool): - name = "calculator" - description = ( - "执行数学计算,支持四则运算、幂运算、开方、三角函数、对数等。" - "输入数学表达式字符串,返回计算结果。" - ) - parameters = { - "type": "object", - "properties": { - "expression": { - "type": "string", - "description": ( - "数学表达式,例如: '2 + 3 * 4', 'sqrt(16)', " - "'sin(3.14159/2)', 'log(100, 10)', '2 ** 10'" - ), - }, - }, - "required": ["expression"], - } - - # 安全的内置函数白名单 - _SAFE_GLOBALS: dict[str, Any] = { - "__builtins__": {}, - # 基本数学 - "abs": abs, - "round": round, - "pow": pow, - "min": min, - "max": max, - # math 模块常用函数 - "sqrt": math.sqrt, - "ceil": math.ceil, - "floor": math.floor, - "log": math.log, - "log2": math.log2, - "log10": math.log10, - "exp": math.exp, - "sin": math.sin, - "cos": math.cos, - "tan": math.tan, - "asin": math.asin, - "acos": math.acos, - "atan": math.atan, - "atan2": math.atan2, - "pi": math.pi, - "e": math.e, - "inf": math.inf, - "factorial": math.factorial, - "gcd": math.gcd, - "hypot": math.hypot, - } - - def execute(self, expression: str = "", **_) -> str: - if not expression or not expression.strip(): - return "❌ 参数错误: expression 不能为空" - - expr = expression.strip() - logger.info(f"🔢 计算表达式: {expr}") - - # 安全检查:禁止危险关键字 - forbidden = ["import", "exec", "eval", "open", "os", "sys", - "__", "compile", "globals", "locals", "getattr"] - for kw in forbidden: - if kw in expr: - return f"❌ 安全限制: 表达式包含禁止关键字 '{kw}'" - - try: - precision = _cfg('precision', 10) - result = eval(expr, self._SAFE_GLOBALS, {}) # noqa: S307 - - # 格式化输出 - if isinstance(result, float): - # 去除多余尾零 - formatted = f"{result:.{precision}f}".rstrip("0").rstrip(".") - elif isinstance(result, complex): - formatted = str(result) - else: - formatted = str(result) - - logger.info(f"✅ 计算结果: {expr} = {formatted}") - return f"{expr} = {formatted}" - - except ZeroDivisionError: - return f"❌ 计算错误: 除零错误 表达式: {expr}" - except OverflowError: - return f"❌ 计算错误: 数值溢出 表达式: {expr}" - except ValueError as e: - return f"❌ 计算错误: {e} 表达式: {expr}" - except SyntaxError: - return f"❌ 语法错误: 无法解析表达式 '{expr}'" - except Exception as e: - return f"❌ 计算失败: {e} 表达式: {expr}" \ No newline at end of file diff --git a/tools/code_executor.py b/tools/code_executor.py deleted file mode 100644 index 419a66b..0000000 --- a/tools/code_executor.py +++ /dev/null @@ -1,149 +0,0 @@ -""" -tools/code_executor.py -代码执行工具 —— 在沙箱中执行 Python 代码片段 -配置通过 settings.tools['code_executor'] 读取 -""" - -import io -import sys -import textwrap -import time -import traceback -from contextlib import redirect_stderr, redirect_stdout - -from config.settings import settings -from tools.base_tool import BaseTool -from utils.logger import get_logger - -logger = get_logger("TOOL.CodeExecutor") - - -def _cfg(key: str, fallback=None): - return settings.tools['code_executor'].get(key, fallback) - - -class Tool(BaseTool): - name = "code_executor" - description = ( - "在安全沙箱中执行 Python 代码片段,返回标准输出和执行结果。" - "适用于数据处理、计算、格式转换等任务。" - "注意:沙箱模式下禁止文件系统写入、网络访问和系统调用。" - ) - parameters = { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "要执行的 Python 代码字符串", - }, - "timeout": { - "type": "integer", - "description": "执行超时秒数(默认来自 config.yaml code_executor.timeout)", - }, - }, - "required": ["code"], - } - - # 沙箱模式下禁止的模块和函数 - _FORBIDDEN_SANDBOX = [ - "import os", "import sys", "import subprocess", - "import socket", "import requests", "import httpx", - "import shutil", "open(", "__import__", - "exec(", "eval(", "compile(", - ] - - def execute(self, code: str = "", timeout: int | None = None, **_) -> str: - if not code or not code.strip(): - return "❌ 参数错误: code 不能为空" - - sandbox = _cfg('sandbox', True) - t = timeout or _cfg('timeout', 5) - code = textwrap.dedent(code) - - logger.info( - f"🐍 执行代码 sandbox={sandbox} timeout={t}s " - f"[config timeout={_cfg('timeout')}s sandbox={_cfg('sandbox')}]\n" - f" 代码预览: {code[:100]}" - ) - - # 沙箱安全检查 - if sandbox: - err = self._sandbox_check(code) - if err: - return err - - # 使用线程超时执行 - return self._run_with_timeout(code, t) - - def _run_with_timeout(self, code: str, timeout: int) -> str: - """在独立线程中执行代码,超时则终止""" - import threading - - result_box: list[str] = [] - error_box: list[str] = [] - - def _run(): - stdout_buf = io.StringIO() - stderr_buf = io.StringIO() - local_ns: dict = {} - start = time.time() - try: - with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf): - exec(code, {"__builtins__": __builtins__}, local_ns) # noqa: S102 - elapsed = time.time() - start - stdout = stdout_buf.getvalue() - stderr = stderr_buf.getvalue() - # 尝试获取最后一个表达式的值 - last_val = "" - lines = [l.strip() for l in code.strip().splitlines() if l.strip()] - if lines: - last_line = lines[-1] - if not last_line.startswith(("#", "print", "import", "from", - "def ", "class ", "if ", "for ", - "while ", "try:", "with ")): - try: - val = eval(last_line, {"__builtins__": __builtins__}, local_ns) # noqa: S307 - if val is not None: - last_val = f"\n返回值: {repr(val)}" - except Exception: - pass - - output = stdout + (f"\n[stderr]\n{stderr}" if stderr else "") + last_val - result_box.append( - f"✅ 执行成功 耗时={elapsed:.3f}s\n" - f"{'─' * 40}\n" - f"{output.strip() or '(无输出)'}" - ) - except Exception: - elapsed = time.time() - start - tb = traceback.format_exc() - error_box.append( - f"❌ 执行错误 耗时={elapsed:.3f}s\n" - f"{'─' * 40}\n{tb}" - ) - - thread = threading.Thread(target=_run, daemon=True) - thread.start() - thread.join(timeout=timeout) - - if thread.is_alive(): - return ( - f"⏰ 执行超时(>{timeout}s)\n" - f" 请增大 config.yaml → tools.code_executor.timeout\n" - f" 或优化代码逻辑" - ) - - if error_box: - return error_box[0] - return result_box[0] if result_box else "❌ 执行失败(未知错误)" - - def _sandbox_check(self, code: str) -> str | None: - """沙箱模式下的静态安全检查""" - for forbidden in self._FORBIDDEN_SANDBOX: - if forbidden in code: - return ( - f"❌ 沙箱限制: 代码包含禁止操作 '{forbidden}'\n" - f" 如需完整权限请在 config.yaml → " - f"tools.code_executor.sandbox 设置为 false" - ) - return None \ No newline at end of file diff --git a/tools/file_reader.py b/tools/file_reader.py deleted file mode 100644 index f4c9184..0000000 --- a/tools/file_reader.py +++ /dev/null @@ -1,179 +0,0 @@ -""" -tools/file_reader.py -文件读取工具 —— 读取本地文件内容,支持文本/JSON/CSV -配置通过 settings.tools['file_reader'] 读取 -""" - -import csv -import io -import json -from pathlib import Path - -from config.settings import settings -from tools.base_tool import BaseTool -from utils.logger import get_logger - -logger = get_logger("TOOL.FileReader") - - -def _cfg(key: str, fallback=None): - return settings.tools['file_reader'].get(key, fallback) - - -class Tool(BaseTool): - name = "file_reader" - description = ( - "读取本地文件内容,支持 .txt / .md / .py / .json / .csv / .yaml / .log 等文本文件。" - "文件必须位于 config.yaml file_reader.allowed_root 目录下。" - ) - parameters = { - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "文件路径(相对于 allowed_root 或绝对路径)", - }, - "encoding": { - "type": "string", - "description": "文件编码,默认 utf-8", - }, - "max_lines": { - "type": "integer", - "description": "最多读取行数,0 表示全部读取", - }, - }, - "required": ["file_path"], - } - - _TEXT_EXTENSIONS = { - ".txt", ".md", ".py", ".js", ".ts", ".java", ".c", ".cpp", - ".h", ".hpp", ".go", ".rs", ".rb", ".php", ".sh", ".bash", - ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf", - ".json", ".csv", ".log", ".xml", ".html", ".css", ".sql", - ".env", ".gitignore", ".dockerfile", - } - - def execute( - self, - file_path: str = "", - encoding: str = "utf-8", - max_lines: int = 0, - **_, - ) -> str: - if not file_path or not file_path.strip(): - return "❌ 参数错误: file_path 不能为空" - - allowed_root = Path(_cfg('allowed_root', './workspace')).resolve() - max_size_kb = _cfg('max_file_size_kb', 512) - - # 路径解析 - path = Path(file_path) - if not path.is_absolute(): - path = allowed_root / path - path = path.resolve() - - logger.info( - f"📄 读取文件: {path}\n" - f" allowed_root={allowed_root} " - f"max_size={max_size_kb}KB [config]" - ) - - # 安全检查:必须在 allowed_root 内 - try: - path.relative_to(allowed_root) - except ValueError: - return ( - f"❌ 安全限制: 文件路径超出允许范围\n" - f" 路径: {path}\n" - f" 允许范围: {allowed_root}\n" - f" 请在 config.yaml → tools.file_reader.allowed_root 中调整" - ) - - if not path.exists(): - return f"❌ 文件不存在: {path}" - if not path.is_file(): - return f"❌ 路径不是文件: {path}" - - # 文件大小检查 - size_kb = path.stat().st_size / 1024 - if size_kb > max_size_kb: - return ( - f"❌ 文件过大: {size_kb:.1f} KB > 限制 {max_size_kb} KB\n" - f" 请在 config.yaml → tools.file_reader.max_file_size_kb 中调整" - ) - - # 扩展名检查 - suffix = path.suffix.lower() - if suffix not in self._TEXT_EXTENSIONS: - return ( - f"❌ 不支持的文件类型: {suffix}\n" - f" 支持类型: {', '.join(sorted(self._TEXT_EXTENSIONS))}" - ) - - # 读取文件 - try: - if suffix == ".json": - return self._read_json(path, encoding) - if suffix == ".csv": - return self._read_csv(path, encoding, max_lines) - return self._read_text(path, encoding, max_lines) - except UnicodeDecodeError: - return ( - f"❌ 编码错误: 无法以 {encoding} 解码文件\n" - f" 请尝试指定 encoding 参数,例如 'gbk' 或 'latin-1'" - ) - except Exception as e: - return f"❌ 读取失败: {e}" - - @staticmethod - def _read_text(path: Path, encoding: str, max_lines: int) -> str: - content = path.read_text(encoding=encoding) - lines = content.splitlines() - total = len(lines) - if max_lines and max_lines < total: - shown = lines[:max_lines] - omitted = total - max_lines - text = "\n".join(shown) - return ( - f"📄 {path.name} ({total} 行,显示前 {max_lines} 行)\n" - f"{'─' * 50}\n{text}\n" - f"{'─' * 50}\n... 还有 {omitted} 行未显示" - ) - return f"📄 {path.name} ({total} 行)\n{'─' * 50}\n{content}" - - @staticmethod - def _read_json(path: Path, encoding: str) -> str: - content = path.read_text(encoding=encoding) - try: - data = json.loads(content) - formatted = json.dumps(data, ensure_ascii=False, indent=2) - return f"📄 {path.name} (JSON)\n{'─' * 50}\n{formatted}" - except json.JSONDecodeError as e: - return f"⚠️ JSON 解析失败: {e}\n原始内容:\n{content[:500]}" - - @staticmethod - def _read_csv(path: Path, encoding: str, max_lines: int) -> str: - content = path.read_text(encoding=encoding) - reader = csv.reader(io.StringIO(content)) - rows = list(reader) - total = len(rows) - limit = max_lines if max_lines else min(total, 50) - shown = rows[:limit] - - # 计算列宽 - if not shown: - return f"📄 {path.name} (CSV,空文件)" - col_widths = [ - max(len(str(row[i])) if i < len(row) else 0 for row in shown) - for i in range(len(shown[0])) - ] - lines = [f"📄 {path.name} (CSV,{total} 行)", "─" * 50] - for row in shown: - cells = [ - str(row[i]).ljust(col_widths[i]) if i < len(row) else "" - for i in range(len(shown[0])) - ] - lines.append(" | ".join(cells)) - if total > limit: - lines.append(f"... 还有 {total - limit} 行未显示") - return "\n".join(lines) \ No newline at end of file diff --git a/tools/ssh_docker.py b/tools/ssh_docker.py deleted file mode 100644 index 4d303ad..0000000 --- a/tools/ssh_docker.py +++ /dev/null @@ -1,733 +0,0 @@ -""" -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 tools.base_tool import BaseTool -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 Tool(BaseTool): - """ - 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 deleted file mode 100644 index 2577f0c..0000000 --- a/tools/static_analyzer.py +++ /dev/null @@ -1,483 +0,0 @@ -""" -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 tools.base_tool import BaseTool -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 Tool(BaseTool): - """ - 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/uav_control.py b/tools/uav_control.py new file mode 100644 index 0000000..fcd6253 --- /dev/null +++ b/tools/uav_control.py @@ -0,0 +1,42 @@ +from typing import List +from agent.device import controller +from agent.tools.base_tool import BaseTool, ToolResult + + + +class Tool(BaseTool): + name = "uav_control" + description = ( + "该工具用于对无人系统进行控制并在飞行控制中完成一系列的活动(Action),例如起飞、移动、悬停、照相等" + ) + parameters = { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "无人系统飞行控制命令,例如arm、disarm、takeoff、rtl、goto等", + }, + "params": { + "type": "object", + "description": "控制命令参数" + }, + "actions": { + "type": "array", + "description": "过程中执行的一些列的动作,例如pause、capture_phone、standby等" + } + }, + "required": ["command"], + } + + + def execute(self, command: str, params: dict={}, actions: List[object]=()) -> ToolResult: + if not controller.connect(): + return ToolResult(success=False, output="连接无人机失败") + try: + command_handler = getattr(controller, command) + try: + return ToolResult(success=True, output=str(command_handler(**params))) + except Exception as e: + return ToolResult(success=False, output=str(e)) + finally: + controller.disconnect() \ No newline at end of file diff --git a/tools/uav_get_state.py b/tools/uav_get_state.py new file mode 100644 index 0000000..f5de66e --- /dev/null +++ b/tools/uav_get_state.py @@ -0,0 +1,37 @@ +import json +from dataclasses import asdict +from typing import List + +from agent.config.settings import settings +from agent.device import controller +from agent.tools.base_tool import BaseTool, ToolResult +from core.uas_control import command_registry +from core.uas_control.controllers.robot_dog_controller import RobotDogController +from core.uas_control.controllers.uav_controller import UAVController +from core.uas_control.protocols.mavlink_adapter import MAVLinkAdapter +from core.uas_control.protocols.ros_adapter import ROSAdapter +from core.uas_control.protocols.simulation_adapter import SimulationAdapter + + +class Tool(BaseTool): + name = "uav_get_state" + description = ( + "获取无人系统当前状态, 例如当前位置等信息" + ) + parameters = { + "type": "object", + "properties": { + + }, + "required": [""], + } + + def execute(self) -> ToolResult: + if not controller.connect(): + return ToolResult(success=False, output="连接无人机失败") + try: + telemetry = controller.get_telemetry() + return ToolResult(success=True, output=json.dumps(telemetry.to_dict(), ensure_ascii=True)) + finally: + controller.disconnect() + diff --git a/tools/uav_self_check.py b/tools/uav_self_check.py new file mode 100644 index 0000000..1efd5eb --- /dev/null +++ b/tools/uav_self_check.py @@ -0,0 +1,123 @@ +import time +from pymavlink import mavutil +from pymavlink.dialects.v20.all import MAVLink_sys_status_message + +from agent.device import controller +from agent.tools.base_tool import BaseTool, ToolResult + + +class Tool(BaseTool): + name = "uav_self_check" + description = ("对无人机设备进行使用前自检") + parameters = {} + + def execute(self, **kwargs) -> ToolResult: + """ + MAVLink 飞控系统健康自检 + 解决:Arming denied: Resolve system health failures first + """ + # 等待健康数据 + + assert controller.connect(), "连接应成功" + try: + connection = controller.adapter.get_connection() + check_list = [] + + health = connection.wait_heartbeat(timeout=5) + if not health: + return ToolResult(success=False, output="❌ 无法获取飞控状态,连接异常") + + # 2. 获取系统健康状态(关键:判断解锁失败原因) + check_list.append("\n===== 系统健康自检结果 =====") + system_status = health.system_status + check_list.append(f"系统状态码:{system_status}") + + # 系统状态解释 + status_map = { + 0: "未初始化", + 1: "初始化中", + 2: "测试模式", + 3: "待机", + 4: "活动", + 5: "临界", + 6: "紧急", + 7: "故障" + } + check_list.append(f"系统状态:{status_map.get(system_status, '未知')}") + # 3. 读取详细传感器健康(核心自检) + check_list.append("\n===== 传感器健康检测 =====") + for _ in range(30): + msg = connection.recv_match(type="SYS_STATUS", blocking=True) + if msg: + # 传感器是否存在 + sensors_present = msg.onboard_control_sensors_present + sensors_enable = msg.onboard_control_sensors_enabled + + # 传感器列表 + sensor_list = [ + ("3D陀螺仪", 0), + ("3D加速度计", 1), + ("3D磁力计", 2), + ("气压计", 3), + ("GPS", 4), + ("光学流量", 5), + ("视觉定位", 6), + ("测距传感器", 7), + ("电机控制", 8), + ("电池", 9), + ("遥控信号", 10), + ("飞行控制", 11), + ("避障系统", 12), + ("云台", 13), + ] + + # 逐个检测 + for name, bit in sensor_list: + present = (sensors_present >> bit) & 1 + enable = (sensors_enable >> bit) & 1 + + if present: + if not enable: + check_list.append(f"❌ {name}:故障") + else: + check_list.append(f"✅ {name}:正常") + else: + check_list.append(f"ℹ️ {name}:未安装") + break + time.sleep(0.1) + + # 4. 解锁能力判断(解决 Arming denied) + check_list.append("\n===== 解锁能力检测 =====") + can_arm = False + for _ in range(20): + msg = connection.recv_match(type="HEARTBEAT", blocking=True) + if msg: + base_mode = msg.base_mode + custom_mode = msg.custom_mode + + # 0b10000000 = 已解锁 + if (base_mode & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED) != 0: + check_list.append("✅ 当前状态:已解锁") + can_arm = True + else: + check_list.append("⚠️ 当前状态:未解锁") + + # 判断是否禁止解锁 + if system_status in [5, 6, 7]: + check_list.append("❌ 禁止解锁原因:系统处于临界/紧急/故障状态") + else: + check_list.append("✅ 系统允许解锁,可执行 arm 指令") + can_arm = True + break + + print("\n===== 自检完成 =====") + if can_arm: + check_list.append("🎉 结论:无健康故障,可以执行解锁(arm)") + else: + check_list.append("⚠️ 结论:存在系统故障,必须修复后才能 arm") + + return ToolResult(success=True, output="\n".join(check_list)) + except Exception as e: + return ToolResult(success=False, output=str(e)) + finally: + controller.disconnect() \ No newline at end of file diff --git a/tools/web_search.py b/tools/web_search.py index 8122c55..d48e088 100644 --- a/tools/web_search.py +++ b/tools/web_search.py @@ -6,9 +6,9 @@ tools/web_search.py from dataclasses import dataclass from serpapi import SerpApiClient -from config.settings import settings -from tools.base_tool import BaseTool -from utils.logger import get_logger +from agent.config.settings import settings +from agent.tools.base_tool import BaseTool +from agent.utils.logger import get_logger logger = get_logger("TOOL.WebSearch")