first commit
This commit is contained in:
commit
7fe37768c4
1
.codebuddy/.gitignore
vendored
Normal file
1
.codebuddy/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
db/
|
||||
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
*.egg
|
||||
|
||||
# Virtual environment
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Project specific
|
||||
*.db
|
||||
1
mcp_tools/__init__.py
Normal file
1
mcp_tools/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# mcp_tools package
|
||||
1
mcp_tools/command_executor/__init__.py
Normal file
1
mcp_tools/command_executor/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# command_executor package
|
||||
127
mcp_tools/command_executor/server.py
Normal file
127
mcp_tools/command_executor/server.py
Normal file
@ -0,0 +1,127 @@
|
||||
"""
|
||||
终端命令执行 MCP Server
|
||||
通过 MCP 协议暴露 run_command 命令,供 Dev Agent 使用
|
||||
"""
|
||||
import asyncio
|
||||
import subprocess
|
||||
import shlex
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from mcp.server import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
from mcp.types import Tool, TextContent
|
||||
|
||||
|
||||
def log(msg):
|
||||
"""写日志到 stderr,不污染 MCP stdio 协议通道"""
|
||||
print(msg, file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
app = Server("command-executor")
|
||||
|
||||
|
||||
@app.list_tools()
|
||||
async def list_tools():
|
||||
return [
|
||||
Tool(
|
||||
name="run_command",
|
||||
description="在终端中执行一条命令,返回输出结果。适用于编译、构建、运行测试等场景。",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "要执行的命令,如 'npm run build'、'npm test' 等",
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string",
|
||||
"description": "命令执行的工作目录,默认使用当前目录",
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "超时时间(秒),默认 300 秒",
|
||||
"default": 300,
|
||||
},
|
||||
},
|
||||
"required": ["command"],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@app.call_tool()
|
||||
async def call_tool(name: str, arguments: dict):
|
||||
if name != "run_command":
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
|
||||
cmd = arguments.get("command", "")
|
||||
cwd = arguments.get("cwd") or os.getcwd()
|
||||
timeout = int(arguments.get("timeout", 300))
|
||||
|
||||
if not cmd:
|
||||
return [TextContent(type="text", text="错误:命令不能为空")]
|
||||
|
||||
log(f" [command] {cmd}")
|
||||
log(f" [cwd] {cwd}")
|
||||
log(f" [timeout] {timeout}s")
|
||||
log(f" [PATH] {os.environ.get('PATH', 'N/A')[:200]}")
|
||||
log(f" [where npm] {__import__('shutil').which('npm.cmd')}")
|
||||
|
||||
try:
|
||||
# 使用异步 subprocess,避免阻塞 asyncio 事件循环
|
||||
# 在 Windows 上,npm 是 .cmd 文件,需要通过 shell 执行
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
||||
proc.communicate(),
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
stdout = stdout_bytes.decode("utf-8", errors="replace")
|
||||
stderr = stderr_bytes.decode("utf-8", errors="replace")
|
||||
|
||||
output_parts = []
|
||||
if stdout:
|
||||
output_parts.append(f"[stdout]\n{stdout}")
|
||||
if stderr:
|
||||
output_parts.append(f"[stderr]\n{stderr}")
|
||||
|
||||
output = "\n".join(output_parts) if output_parts else "(无输出)"
|
||||
|
||||
max_len = 10000
|
||||
if len(output) > max_len:
|
||||
output = output[:max_len] + f"\n\n...(输出已截断,共 {len(output)} 字符)"
|
||||
|
||||
status = "成功" if proc.returncode == 0 else f"失败 (退出码 {proc.returncode})"
|
||||
log(f" [result] {status}")
|
||||
return [TextContent(type="text", text=f"命令执行{status}\n{output}")]
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
# 超时后杀进程
|
||||
proc.kill()
|
||||
await proc.wait()
|
||||
log(f" [error] 超时")
|
||||
return [TextContent(type="text", text=f"命令执行超时({timeout}秒): {cmd}")]
|
||||
except Exception as e:
|
||||
log(f" [error] {e}")
|
||||
return [TextContent(type="text", text=f"命令执行出错: {e}")]
|
||||
|
||||
|
||||
async def main():
|
||||
log("MCP 命令执行服务器启动中...")
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await app.run(
|
||||
read_stream, write_stream,
|
||||
app.create_initialization_options()
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
2
my_agent/__init__.py
Normal file
2
my_agent/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# my_agent package
|
||||
from . import agent
|
||||
146
my_agent/agent.py
Normal file
146
my_agent/agent.py
Normal file
@ -0,0 +1,146 @@
|
||||
from google.adk.agents import LlmAgent
|
||||
from google.adk.models.lite_llm import LiteLlm
|
||||
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
|
||||
from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams
|
||||
from google.adk.tools.function_tool import FunctionTool
|
||||
from mcp.client.stdio import StdioServerParameters
|
||||
import os
|
||||
import asyncio
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# --- 使用 vLLM 端点的智能体 ---
|
||||
api_base_url = os.getenv("VLLM_API_BASE", "https://9router.aqroid.cn/v1")
|
||||
model_name = os.getenv("VLLM_MODEL", "")
|
||||
api_key = os.getenv("VLLM_API_KEY", "")
|
||||
|
||||
# Agent 可访问的工作目录
|
||||
WORKSPACE_DIR = os.getenv("AGENT_WORKSPACE_DIR", r"D:\nzy\workspace_git")
|
||||
|
||||
|
||||
# --- 文件系统 MCP 工具 ---
|
||||
filesystem_mcp = McpToolset(
|
||||
connection_params=StdioConnectionParams(
|
||||
server_params=StdioServerParameters(
|
||||
command="npx",
|
||||
args=[
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-filesystem",
|
||||
os.path.abspath(WORKSPACE_DIR),
|
||||
],
|
||||
),
|
||||
timeout=30.0,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# --- 终端命令执行工具(Python 原生,绕开 MCP 通信问题)---
|
||||
async def run_command(command: str, cwd: str = None, timeout: int = 300) -> str:
|
||||
"""
|
||||
在终端中执行一条命令,返回输出结果。
|
||||
|
||||
Args:
|
||||
command: 要执行的命令,如 'npm run build'、'python -m pytest' 等
|
||||
cwd: 命令执行的工作目录,默认使用 AGENT_WORKSPACE_DIR
|
||||
timeout: 超时时间(秒),默认 300
|
||||
|
||||
Returns:
|
||||
命令执行结果(stdout + stderr + 状态)
|
||||
"""
|
||||
if not command:
|
||||
return "错误:命令不能为空"
|
||||
|
||||
work_dir = cwd or os.path.abspath(WORKSPACE_DIR)
|
||||
if not os.path.isdir(work_dir):
|
||||
return f"错误:工作目录不存在 {work_dir}"
|
||||
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
command,
|
||||
cwd=work_dir,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
||||
proc.communicate(), timeout=timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
proc.kill()
|
||||
await proc.wait()
|
||||
return f"命令执行超时({timeout}秒): {command}"
|
||||
except Exception as e:
|
||||
return f"命令执行出错: {e}"
|
||||
|
||||
stdout = stdout_bytes.decode("utf-8", errors="replace")
|
||||
stderr = stderr_bytes.decode("utf-8", errors="replace")
|
||||
|
||||
parts = []
|
||||
if stdout:
|
||||
parts.append(f"[stdout]\n{stdout}")
|
||||
if stderr:
|
||||
parts.append(f"[stderr]\n{stderr}")
|
||||
|
||||
output = "\n".join(parts) if parts else "(无输出)"
|
||||
|
||||
max_len = 10000
|
||||
if len(output) > max_len:
|
||||
output = output[:max_len] + f"\n\n...(输出已截断,共 {len(output)} 字符)"
|
||||
|
||||
status = "成功" if proc.returncode == 0 else f"失败 (退出码 {proc.returncode})"
|
||||
return f"命令执行{status}\n{output}"
|
||||
|
||||
|
||||
# 注册为 ADK 工具
|
||||
run_command_tool = FunctionTool(run_command)
|
||||
|
||||
|
||||
root_agent = LlmAgent(
|
||||
model=LiteLlm(
|
||||
model=model_name,
|
||||
api_base=api_base_url,
|
||||
api_key=api_key if api_key else None,
|
||||
custom_llm_provider="openai",
|
||||
),
|
||||
name="dev_agent",
|
||||
description="全栈开发子 Agent,可以读写文件、浏览目录、执行开发任务。",
|
||||
instruction=(
|
||||
"你是一个全栈开发子 Agent(Dev Agent),由主控调度执行具体的开发任务。\n"
|
||||
"\n"
|
||||
"## 工作流程\n"
|
||||
"1. 先理解任务需求和项目上下文\n"
|
||||
"2. 使用文件系统工具浏览项目结构、读取相关文件\n"
|
||||
"3. 编写或修改代码\n"
|
||||
"4. 使用 run_command 工具运行编译/构建/测试,确保代码可正常工作\n"
|
||||
"5. 验证结果后,按指定格式报告完成情况\n"
|
||||
"\n"
|
||||
"## 工作边界\n"
|
||||
"- 所有文件操作限定在分配的工作目录范围内\n"
|
||||
"- 你拥有的工具:文件系统操作(读/写/列目录)、终端命令执行\n"
|
||||
"- 你可以自主完成:代码编写、bug 修复、样式调整、接口修改、简单重构\n"
|
||||
"- 需要上报的情况:\n"
|
||||
" • 架构设计或重大技术选型决策\n"
|
||||
" • 依赖包版本不兼容导致的编译/运行时错误(需要升级/降级依赖时)\n"
|
||||
" • 工具调用异常、环境配置问题、命令超时等非代码问题\n"
|
||||
" • 超出你能力范围或不确定的问题\n"
|
||||
"\n"
|
||||
"## 编译/构建守则\n"
|
||||
"- 写完代码后,优先运行编译或构建命令验证\n"
|
||||
"- 编译报错时,先判断错误类型:\n"
|
||||
" • 代码语法/逻辑错误 → 自行修复后重试\n"
|
||||
" • 依赖缺失或版本不兼容 → 上报,由主控决定处理方式\n"
|
||||
" • 环境/工具问题 → 上报\n"
|
||||
"- 连续修复 3 次仍无法通过编译时,上报当前状态和所有错误信息\n"
|
||||
"- 只有编译通过后才算任务完成\n"
|
||||
"\n"
|
||||
"## 报告格式\n"
|
||||
"完成任务后,结构化报告:\n"
|
||||
"**状态**:成功 / 部分完成 / 失败(需上报)\n"
|
||||
"**修改的文件**:列出所有修改的文件路径\n"
|
||||
"**变更摘要**:简述做了什么\n"
|
||||
"**验证结果**:编译/测试是否通过,如有警告需列出\n"
|
||||
"**需要主控关注**:如有需要上报的问题,详细说明"
|
||||
),
|
||||
tools=[filesystem_mcp, run_command_tool],
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user