From 192e8a95bfe91059c28fe102860f563766ea3bee Mon Sep 17 00:00:00 2001 From: elipitc Date: Fri, 27 Mar 2026 19:44:08 +0800 Subject: [PATCH] feat: add CI/CD workflow capabilities to code_ai_agent Add Git, SSH, and K8s deploy tools enabling code_ai_agent to act as an autonomous DevOps worker for the agent-manager project: - tools/workspace.py: isolated /workspace/{task_id} per task - tools/git_tools.py: clone, branch, status, write-file, commit-push - tools/ssh_tools.py: SSH exec via paramiko (Azure VM support) - tools/deploy_tools.py: K8s rollout restart/status via kubectl - api_server.py: new routes /api/v1/git/*, /api/v1/ssh/*, /api/v1/deploy/k8s - Dockerfile: add git, openssh-client, sshpass; pip install paramiko gitpython kubernetes - requirements.txt: add paramiko, gitpython, kubernetes Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../agents/code_ai_agent/Dockerfile | 24 +- .../agents/code_ai_agent/requirements.txt | 8 +- .../code_ai_agent/src/server/api_server.py | 309 ++++++++++++++++-- .../src/server/tools/__init__.py | 0 .../src/server/tools/deploy_tools.py | 67 ++++ .../src/server/tools/git_tools.py | 135 ++++++++ .../src/server/tools/ssh_tools.py | 58 ++++ .../src/server/tools/workspace.py | 49 +++ 8 files changed, 602 insertions(+), 48 deletions(-) create mode 100644 agent_templates/agents/code_ai_agent/src/server/tools/__init__.py create mode 100644 agent_templates/agents/code_ai_agent/src/server/tools/deploy_tools.py create mode 100644 agent_templates/agents/code_ai_agent/src/server/tools/git_tools.py create mode 100644 agent_templates/agents/code_ai_agent/src/server/tools/ssh_tools.py create mode 100644 agent_templates/agents/code_ai_agent/src/server/tools/workspace.py diff --git a/agent_templates/agents/code_ai_agent/Dockerfile b/agent_templates/agents/code_ai_agent/Dockerfile index 29a7cb9..d56e2f4 100644 --- a/agent_templates/agents/code_ai_agent/Dockerfile +++ b/agent_templates/agents/code_ai_agent/Dockerfile @@ -8,25 +8,33 @@ WORKDIR /app ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 -# 安装系统依赖 +# 安装系统依赖(含 git、ssh) RUN apt-get update && apt-get install -y \ gcc \ + git \ + openssh-client \ + sshpass \ + curl \ && rm -rf /var/lib/apt/lists/* # 复制依赖文件 -COPY requirements.txt . +COPY agent_templates/agents/code_ai_agent/requirements.txt . # 安装 Python 依赖 -RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir -r requirements.txt requests paramiko gitpython # 复制应用代码 -COPY . . +COPY agent_templates/agents/code_ai_agent/ /app/ +COPY agent_templates/common/agent_callback_utils.py /app/common/ +RUN touch /app/common/__init__.py -# 创建项目存储目录 -RUN mkdir -p /tmp/projects +# 创建项目存储目录和工作空间 +RUN mkdir -p /tmp/projects /workspace -# 安装curl用于健康检查 -RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* +# 配置 git 全局设置 +RUN git config --global user.email "code-ai-agent@taijiagnet.com" \ + && git config --global user.name "Code AI Agent" \ + && git config --global credential.helper store # 暴露端口 EXPOSE 8000 8001 diff --git a/agent_templates/agents/code_ai_agent/requirements.txt b/agent_templates/agents/code_ai_agent/requirements.txt index 63ce91f..89a0d08 100644 --- a/agent_templates/agents/code_ai_agent/requirements.txt +++ b/agent_templates/agents/code_ai_agent/requirements.txt @@ -2,9 +2,9 @@ pydantic-ai httpx mcp fastmcp -fastapi -uvicorn[standard] -python-multipart fastapi>=0.104.0 uvicorn[standard]>=0.24.0 -python-multipart \ No newline at end of file +python-multipart +paramiko +gitpython +kubernetes \ No newline at end of file diff --git a/agent_templates/agents/code_ai_agent/src/server/api_server.py b/agent_templates/agents/code_ai_agent/src/server/api_server.py index 67b748a..0e0897f 100644 --- a/agent_templates/agents/code_ai_agent/src/server/api_server.py +++ b/agent_templates/agents/code_ai_agent/src/server/api_server.py @@ -24,17 +24,36 @@ from .mcp_server import ( analyze_project, create_code_file ) +# DevOps 工具 +from .tools.workspace import create_workspace, delete_workspace, list_workspaces, workspace_exists +from .tools.git_tools import git_clone, git_branch, git_status, git_diff, git_commit_push, git_write_file +from .tools.ssh_tools import ssh_exec, ssh_test_connection +from .tools.deploy_tools import deploy_rollout_restart, deploy_rollout_status, get_deployment_status + +try: + from common.agent_callback_utils import AgentCallbackHandler, CallbackContextManager + CALLBACK_ENABLED = True +except ImportError: + CALLBACK_ENABLED = False + AgentCallbackHandler = None + CallbackContextManager = None # 配置 API_VERSION = "v1" SERVER_NAME = "代码助手 Agent API" +POD_NAME = os.getenv("POD_NAME", "code-ai-agent") +USER_ID = os.getenv("USER_ID", "") +callback_handler: Optional[AgentCallbackHandler] = None # 创建 FastAPI 应用 @asynccontextmanager async def lifespan(app: FastAPI): """应用生命周期管理""" # 启动时初始化 + global callback_handler print(f"🚀 {SERVER_NAME} 启动中...") + if CALLBACK_ENABLED and AgentCallbackHandler: + callback_handler = AgentCallbackHandler(agent_name=POD_NAME, user_id=USER_ID) yield # 关闭时清理 print(f"🛑 {SERVER_NAME} 关闭中...") @@ -407,7 +426,16 @@ async def handle_mcp_request(request_data: Dict[str, Any], session_id: Optional[ tool_name = params.get("name") arguments = params.get("arguments", {}) - result = await call_mcp_tool(tool_name, arguments, api_key=api_key) + if CALLBACK_ENABLED and callback_handler: + with CallbackContextManager( + handler=callback_handler, + user_id=USER_ID, + request_id=f"codeai-mcp-{tool_name}-{request_id or uuid.uuid4().hex}" + ) as ctx: + ctx.add_tool(tool_name) + result = await call_mcp_tool(tool_name, arguments, api_key=api_key) + else: + result = await call_mcp_tool(tool_name, arguments, api_key=api_key) return { "jsonrpc": "2.0", @@ -518,12 +546,26 @@ async def api_generate_code(request: GenerateCodeRequest, api_key: str = Depends 根据自然语言需求生成高质量的代码 """ try: - result = await call_mcp_tool('generate_code', { - 'requirement': request.requirement, - 'language': request.language, - 'style': request.style, - 'project_root': request.project_root - }, api_key=api_key) + if CALLBACK_ENABLED and callback_handler: + with CallbackContextManager( + handler=callback_handler, + user_id=USER_ID, + request_id=f"codeai-generate-{uuid.uuid4().hex}" + ) as ctx: + ctx.add_tool("generate_code") + result = await call_mcp_tool('generate_code', { + 'requirement': request.requirement, + 'language': request.language, + 'style': request.style, + 'project_root': request.project_root + }, api_key=api_key) + else: + result = await call_mcp_tool('generate_code', { + 'requirement': request.requirement, + 'language': request.language, + 'style': request.style, + 'project_root': request.project_root + }, api_key=api_key) return APIResponse( success=True, data={"result": result}, @@ -544,10 +586,22 @@ async def api_refactor_code(request: RefactorCodeRequest, api_key: str = Depends 改进代码质量、性能和可维护性 """ try: - result = await call_mcp_tool('refactor_code', { - 'code_content': request.code_content, - 'refactoring_goal': request.refactoring_goal - }, api_key=api_key) + if CALLBACK_ENABLED and callback_handler: + with CallbackContextManager( + handler=callback_handler, + user_id=USER_ID, + request_id=f"codeai-refactor-{uuid.uuid4().hex}" + ) as ctx: + ctx.add_tool("refactor_code") + result = await call_mcp_tool('refactor_code', { + 'code_content': request.code_content, + 'refactoring_goal': request.refactoring_goal + }, api_key=api_key) + else: + result = await call_mcp_tool('refactor_code', { + 'code_content': request.code_content, + 'refactoring_goal': request.refactoring_goal + }, api_key=api_key) return APIResponse( success=True, data={"result": result}, @@ -568,10 +622,22 @@ async def api_review_code(request: ReviewCodeRequest, api_key: str = Depends(ver 发现潜在问题、bug 和改进建议 """ try: - result = await call_mcp_tool('review_code', { - 'code_content': request.code_content, - 'file_path': request.file_path - }, api_key=api_key) + if CALLBACK_ENABLED and callback_handler: + with CallbackContextManager( + handler=callback_handler, + user_id=USER_ID, + request_id=f"codeai-review-{uuid.uuid4().hex}" + ) as ctx: + ctx.add_tool("review_code") + result = await call_mcp_tool('review_code', { + 'code_content': request.code_content, + 'file_path': request.file_path + }, api_key=api_key) + else: + result = await call_mcp_tool('review_code', { + 'code_content': request.code_content, + 'file_path': request.file_path + }, api_key=api_key) return APIResponse( success=True, data={"result": result}, @@ -592,11 +658,24 @@ async def api_organize_code(request: OrganizeCodeRequest, api_key: str = Depends 智能分析代码并自动组织到合适的文件夹中 """ try: - result = await call_mcp_tool('organize_code', { - 'code_content': request.code_content, - 'code_type': request.code_type, - 'project_root': request.project_root - }, api_key=api_key) + if CALLBACK_ENABLED and callback_handler: + with CallbackContextManager( + handler=callback_handler, + user_id=USER_ID, + request_id=f"codeai-organize-{uuid.uuid4().hex}" + ) as ctx: + ctx.add_tool("organize_code") + result = await call_mcp_tool('organize_code', { + 'code_content': request.code_content, + 'code_type': request.code_type, + 'project_root': request.project_root + }, api_key=api_key) + else: + result = await call_mcp_tool('organize_code', { + 'code_content': request.code_content, + 'code_type': request.code_type, + 'project_root': request.project_root + }, api_key=api_key) return APIResponse( success=True, data={"result": result}, @@ -617,9 +696,20 @@ async def api_classify_code(request: ClassifyCodeRequest, api_key: str = Depends 分析代码内容,确定其应该属于哪个类别/文件夹 """ try: - result = await call_mcp_tool('classify_code', { - 'code_content': request.code_content - }, api_key=api_key) + if CALLBACK_ENABLED and callback_handler: + with CallbackContextManager( + handler=callback_handler, + user_id=USER_ID, + request_id=f"codeai-classify-{uuid.uuid4().hex}" + ) as ctx: + ctx.add_tool("classify_code") + result = await call_mcp_tool('classify_code', { + 'code_content': request.code_content + }, api_key=api_key) + else: + result = await call_mcp_tool('classify_code', { + 'code_content': request.code_content + }, api_key=api_key) return APIResponse( success=True, data={"result": result}, @@ -640,10 +730,22 @@ async def api_analyze_project(request: AnalyzeProjectRequest, api_key: str = Dep 分析项目结构,提供项目概览和改进建议 """ try: - result = await call_mcp_tool('analyze_project', { - 'project_root': request.project_root, - 'max_depth': request.max_depth - }, api_key=api_key) + if CALLBACK_ENABLED and callback_handler: + with CallbackContextManager( + handler=callback_handler, + user_id=USER_ID, + request_id=f"codeai-analyze-{uuid.uuid4().hex}" + ) as ctx: + ctx.add_tool("analyze_project") + result = await call_mcp_tool('analyze_project', { + 'project_root': request.project_root, + 'max_depth': request.max_depth + }, api_key=api_key) + else: + result = await call_mcp_tool('analyze_project', { + 'project_root': request.project_root, + 'max_depth': request.max_depth + }, api_key=api_key) return APIResponse( success=True, data={"result": result}, @@ -664,9 +766,20 @@ async def api_suggest_structure(request: SuggestStructureRequest, api_key: str = 根据项目描述,建议合理的文件夹结构 """ try: - result = await call_mcp_tool('suggest_folder_structure', { - 'project_description': request.project_description - }, api_key=api_key) + if CALLBACK_ENABLED and callback_handler: + with CallbackContextManager( + handler=callback_handler, + user_id=USER_ID, + request_id=f"codeai-structure-{uuid.uuid4().hex}" + ) as ctx: + ctx.add_tool("suggest_folder_structure") + result = await call_mcp_tool('suggest_folder_structure', { + 'project_description': request.project_description + }, api_key=api_key) + else: + result = await call_mcp_tool('suggest_folder_structure', { + 'project_description': request.project_description + }, api_key=api_key) return APIResponse( success=True, data={"result": result}, @@ -687,12 +800,26 @@ async def api_create_file(request: CreateFileRequest, api_key: str = Depends(ver 在指定文件夹中创建代码文件 """ try: - result = await call_mcp_tool('create_code_file', { - 'code_content': request.code_content, - 'folder_path': request.folder_path, - 'file_name': request.file_name, - 'project_root': request.project_root - }, api_key=api_key) + if CALLBACK_ENABLED and callback_handler: + with CallbackContextManager( + handler=callback_handler, + user_id=USER_ID, + request_id=f"codeai-create-file-{uuid.uuid4().hex}" + ) as ctx: + ctx.add_tool("create_code_file") + result = await call_mcp_tool('create_code_file', { + 'code_content': request.code_content, + 'folder_path': request.folder_path, + 'file_name': request.file_name, + 'project_root': request.project_root + }, api_key=api_key) + else: + result = await call_mcp_tool('create_code_file', { + 'code_content': request.code_content, + 'folder_path': request.folder_path, + 'file_name': request.file_name, + 'project_root': request.project_root + }, api_key=api_key) return APIResponse( success=True, data={"result": result}, @@ -705,6 +832,116 @@ async def api_create_file(request: CreateFileRequest, api_key: str = Depends(ver ) +# ==================== DevOps 请求模型 ==================== + +class GitCloneRequest(BaseModel): + repo_url: str = Field(default="http://gitee.ath.cx:3000/zhanggangyong/agent_management.git") + task_id: Optional[str] = None + branch: str = "master" + depth: int = 1 + +class GitBranchRequest(BaseModel): + task_id: str + branch_name: str + base_branch: str = "master" + +class GitCommitPushRequest(BaseModel): + task_id: str + message: str + branch: str + token: Optional[str] = None + username: Optional[str] = None + +class GitWriteFileRequest(BaseModel): + task_id: str + relative_path: str + content: str + +class SshExecRequest(BaseModel): + command: str + host: Optional[str] = None + user: Optional[str] = None + password: Optional[str] = None + timeout: int = 120 + +class DeployRequest(BaseModel): + deployment: str = "agent-manager" + namespace: str = "agent-manager" + context: Optional[str] = None + wait: bool = True + +# ==================== DevOps 路由 ==================== + +@app.post("/api/v1/git/clone") +async def api_git_clone(request: GitCloneRequest, api_key: str = Depends(verify_api_key)): + result = git_clone(request.repo_url, request.task_id or str(uuid.uuid4())[:8], request.branch, request.depth) + if not result.get("success"): + raise HTTPException(status_code=500, detail=result.get("stderr", "clone 失败")) + return APIResponse(success=True, data=result, message="Clone 成功") + +@app.post("/api/v1/git/branch") +async def api_git_branch(request: GitBranchRequest, api_key: str = Depends(verify_api_key)): + result = git_branch(request.task_id, request.branch_name, request.base_branch) + if not result.get("success"): + raise HTTPException(status_code=500, detail=result.get("stderr", "创建分支失败")) + return APIResponse(success=True, data=result, message=f"分支 {request.branch_name} 创建成功") + +@app.post("/api/v1/git/status") +async def api_git_status(request: dict, api_key: str = Depends(verify_api_key)): + task_id = request.get("task_id") + if not task_id: + raise HTTPException(status_code=400, detail="task_id 必填") + result = git_status(task_id) + return APIResponse(success=True, data=result, message="状态获取成功") + +@app.post("/api/v1/git/write-file") +async def api_git_write_file(request: GitWriteFileRequest, api_key: str = Depends(verify_api_key)): + result = git_write_file(request.task_id, request.relative_path, request.content) + if not result.get("success"): + raise HTTPException(status_code=500, detail="写入文件失败") + return APIResponse(success=True, data=result, message="文件写入成功") + +@app.post("/api/v1/git/commit-push") +async def api_git_commit_push(request: GitCommitPushRequest, api_key: str = Depends(verify_api_key)): + result = git_commit_push(request.task_id, request.message, request.branch, + token=request.token, username=request.username) + if not result.get("success"): + raise HTTPException(status_code=500, detail=result.get("stderr", "commit/push 失败")) + return APIResponse(success=True, data=result, message="Push 成功") + +@app.post("/api/v1/ssh/exec") +async def api_ssh_exec(request: SshExecRequest, api_key: str = Depends(verify_api_key)): + result = ssh_exec(request.command, host=request.host, user=request.user, + password=request.password, timeout=request.timeout) + return APIResponse(success=result["success"], data=result, + message="命令执行成功" if result["success"] else "命令执行失败") + +@app.post("/api/v1/ssh/test") +async def api_ssh_test(api_key: str = Depends(verify_api_key)): + result = ssh_test_connection() + return APIResponse(success=result["success"], data=result, + message="SSH 连接正常" if result["success"] else "SSH 连接失败") + +@app.post("/api/v1/deploy/k8s") +async def api_deploy_k8s(request: DeployRequest, api_key: str = Depends(verify_api_key)): + result = deploy_rollout_restart(request.deployment, request.namespace, request.context) + if not result.get("success"): + raise HTTPException(status_code=500, detail=result.get("stderr", "部署触发失败")) + if request.wait: + status = deploy_rollout_status(request.deployment, request.namespace, request.context) + result["rollout_status"] = status + return APIResponse(success=True, data=result, message="部署成功") + +@app.get("/api/v1/workspace") +async def api_list_workspaces(api_key: str = Depends(verify_api_key)): + return APIResponse(success=True, data={"workspaces": list_workspaces()}, message="获取成功") + +@app.delete("/api/v1/workspace/{task_id}") +async def api_delete_workspace(task_id: str, api_key: str = Depends(verify_api_key)): + ok = delete_workspace(task_id) + return APIResponse(success=ok, data={"task_id": task_id}, message="工作空间已删除" if ok else "工作空间不存在") + + if __name__ == '__main__': import uvicorn diff --git a/agent_templates/agents/code_ai_agent/src/server/tools/__init__.py b/agent_templates/agents/code_ai_agent/src/server/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agent_templates/agents/code_ai_agent/src/server/tools/deploy_tools.py b/agent_templates/agents/code_ai_agent/src/server/tools/deploy_tools.py new file mode 100644 index 0000000..54573ca --- /dev/null +++ b/agent_templates/agents/code_ai_agent/src/server/tools/deploy_tools.py @@ -0,0 +1,67 @@ +""" +部署工具 - 触发 K8s 滚动更新 +""" +import os +import subprocess +import logging +from typing import Optional + +logger = logging.getLogger(__name__) + +K8S_CONTEXT = os.getenv("K8S_CONTEXT", "") +K8S_NAMESPACE = os.getenv("K8S_NAMESPACE", "agent-manager") + + +def _run_kubectl(cmd: list, timeout: int = 60) -> dict: + """执行 kubectl 命令""" + try: + result = subprocess.run( + cmd, capture_output=True, text=True, timeout=timeout + ) + return { + "returncode": result.returncode, + "stdout": result.stdout.strip(), + "stderr": result.stderr.strip(), + "success": result.returncode == 0, + } + except subprocess.TimeoutExpired: + return {"returncode": -1, "stdout": "", "stderr": "kubectl 命令超时", "success": False} + except Exception as e: + return {"returncode": -1, "stdout": "", "stderr": str(e), "success": False} + + +def deploy_rollout_restart(deployment: str, namespace: Optional[str] = None, + context: Optional[str] = None) -> dict: + """触发 Deployment 滚动重启""" + ns = namespace or K8S_NAMESPACE + cmd = ["kubectl", "rollout", "restart", f"deployment/{deployment}", "-n", ns] + if context or K8S_CONTEXT: + cmd = ["kubectl", "--context", context or K8S_CONTEXT] + cmd[1:] + result = _run_kubectl(cmd) + if result["success"]: + logger.info(f"✅ 触发滚动重启: {deployment} in {ns}") + return result + + +def deploy_rollout_status(deployment: str, namespace: Optional[str] = None, + context: Optional[str] = None, timeout: int = 120) -> dict: + """等待 Deployment 滚动更新完成""" + ns = namespace or K8S_NAMESPACE + cmd = ["kubectl", "rollout", "status", f"deployment/{deployment}", + "-n", ns, f"--timeout={timeout}s"] + if context or K8S_CONTEXT: + cmd = ["kubectl", "--context", context or K8S_CONTEXT] + cmd[1:] + result = _run_kubectl(cmd, timeout=timeout + 10) + if result["success"]: + logger.info(f"✅ 滚动更新完成: {deployment}") + return result + + +def get_deployment_status(deployment: str, namespace: Optional[str] = None, + context: Optional[str] = None) -> dict: + """获取 Deployment 当前状态""" + ns = namespace or K8S_NAMESPACE + cmd = ["kubectl", "get", "deployment", deployment, "-n", ns, "-o", "json"] + if context or K8S_CONTEXT: + cmd = ["kubectl", "--context", context or K8S_CONTEXT] + cmd[1:] + return _run_kubectl(cmd) diff --git a/agent_templates/agents/code_ai_agent/src/server/tools/git_tools.py b/agent_templates/agents/code_ai_agent/src/server/tools/git_tools.py new file mode 100644 index 0000000..d42f808 --- /dev/null +++ b/agent_templates/agents/code_ai_agent/src/server/tools/git_tools.py @@ -0,0 +1,135 @@ +""" +Git 操作工具 - 支持 clone、branch、commit、push、status、diff +""" +import os +import subprocess +import logging +from typing import Optional +from .workspace import get_workspace_path, create_workspace, workspace_exists + +logger = logging.getLogger(__name__) + +GITEE_TOKEN = os.getenv("GITEE_TOKEN", "") +GITEE_USERNAME = os.getenv("GITEE_USERNAME", "") + + +def _run_git(cmd: list, cwd: str, timeout: int = 60) -> dict: + """执行 git 命令,返回 stdout/stderr/returncode""" + try: + result = subprocess.run( + cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout + ) + return { + "returncode": result.returncode, + "stdout": result.stdout.strip(), + "stderr": result.stderr.strip(), + "success": result.returncode == 0, + } + except subprocess.TimeoutExpired: + return {"returncode": -1, "stdout": "", "stderr": "命令超时", "success": False} + except Exception as e: + return {"returncode": -1, "stdout": "", "stderr": str(e), "success": False} + + +def _inject_credentials(repo_url: str) -> str: + """将 token 注入到 git URL(不落盘)""" + if GITEE_TOKEN and "://" in repo_url: + proto, rest = repo_url.split("://", 1) + # 移除已有的凭证 + if "@" in rest: + rest = rest.split("@", 1)[1] + return f"{proto}://{GITEE_USERNAME}:{GITEE_TOKEN}@{rest}" + return repo_url + + +def git_clone(repo_url: str, task_id: str, branch: str = "master", depth: int = 1) -> dict: + """Clone 仓库到工作空间""" + if not workspace_exists(task_id): + create_workspace(task_id) + workspace = get_workspace_path(task_id) + repo_dir = os.path.join(workspace, "repo") + if os.path.exists(repo_dir): + return {"success": True, "message": "仓库已存在", "repo_dir": repo_dir, "task_id": task_id} + + auth_url = _inject_credentials(repo_url) + cmd = ["git", "clone", "--branch", branch, "--depth", str(depth), auth_url, "repo"] + result = _run_git(cmd, cwd=workspace, timeout=120) + if result["success"]: + # 替换 remote URL 为无凭证版本(安全) + _run_git(["git", "remote", "set-url", "origin", repo_url], cwd=repo_dir) + # 配置 credential helper 使用 token + _run_git(["git", "config", "credential.helper", f"!echo password={GITEE_TOKEN}; echo username={GITEE_USERNAME}"], cwd=repo_dir) + result["repo_dir"] = repo_dir + result["task_id"] = task_id + logger.info(f"✅ Clone 成功: {repo_url} -> {repo_dir}") + else: + logger.error(f"❌ Clone 失败: {result['stderr']}") + return result + +def git_branch(task_id: str, branch_name: str, base_branch: str = "master") -> dict: + """创建并切换到新分支""" + repo_dir = os.path.join(get_workspace_path(task_id), "repo") + # 先确保在 base_branch + _run_git(["git", "checkout", base_branch], cwd=repo_dir) + result = _run_git(["git", "checkout", "-b", branch_name], cwd=repo_dir) + if result["success"]: + logger.info(f"✅ 创建分支: {branch_name}") + return result + + +def git_status(task_id: str) -> dict: + """查看工作区状态""" + repo_dir = os.path.join(get_workspace_path(task_id), "repo") + result = _run_git(["git", "status", "--short"], cwd=repo_dir) + if result["success"]: + result["current_branch"] = _run_git(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=repo_dir)["stdout"] + return result + + +def git_diff(task_id: str) -> dict: + """查看变更内容""" + repo_dir = os.path.join(get_workspace_path(task_id), "repo") + return _run_git(["git", "diff"], cwd=repo_dir) + + +def git_commit_push(task_id: str, message: str, branch: str, + token: Optional[str] = None, username: Optional[str] = None) -> dict: + """Commit 并 Push 到远端""" + repo_dir = os.path.join(get_workspace_path(task_id), "repo") + # 暂存所有变更 + add_result = _run_git(["git", "add", "-A"], cwd=repo_dir) + if not add_result["success"]: + return add_result + # Commit + commit_result = _run_git(["git", "commit", "-m", message], cwd=repo_dir) + if not commit_result["success"]: + return commit_result + # Push(使用 token 注入 URL) + origin_url = _run_git(["git", "remote", "get-url", "origin"], cwd=repo_dir)["stdout"] + # 优先使用请求中传入的 token + _token = token or GITEE_TOKEN + _username = username or GITEE_USERNAME + if _token and "://" in origin_url: + proto, rest = origin_url.split("://", 1) + if "@" in rest: + rest = rest.split("@", 1)[1] + auth_url = f"{proto}://{_username}:{_token}@{rest}" + else: + auth_url = _inject_credentials(origin_url) + push_result = _run_git(["git", "push", auth_url, branch], cwd=repo_dir, timeout=120) + if push_result["success"]: + logger.info(f"✅ Push 成功: {branch}") + else: + logger.error(f"❌ Push 失败: {push_result['stderr']}") + return push_result + + +def git_write_file(task_id: str, relative_path: str, content: str) -> dict: + """在仓库内写入文件内容""" + repo_dir = os.path.join(get_workspace_path(task_id), "repo") + full_path = os.path.join(repo_dir, relative_path) + os.makedirs(os.path.dirname(full_path), exist_ok=True) + with open(full_path, "w", encoding="utf-8") as f: + f.write(content) + return {"success": True, "path": full_path, "bytes": len(content.encode())} + diff --git a/agent_templates/agents/code_ai_agent/src/server/tools/ssh_tools.py b/agent_templates/agents/code_ai_agent/src/server/tools/ssh_tools.py new file mode 100644 index 0000000..1626868 --- /dev/null +++ b/agent_templates/agents/code_ai_agent/src/server/tools/ssh_tools.py @@ -0,0 +1,58 @@ +""" +SSH 工具 - 连接 Azure VM 执行命令 +""" +import os +import logging +from typing import Optional + +logger = logging.getLogger(__name__) + +SSH_HOST = os.getenv("SSH_HOST", "") +SSH_USER = os.getenv("SSH_USER", "") +SSH_PASSWORD = os.getenv("SSH_PASSWORD", "") +SSH_PORT = int(os.getenv("SSH_PORT", "22")) + + +def ssh_exec(command: str, host: Optional[str] = None, user: Optional[str] = None, + password: Optional[str] = None, port: int = 22, timeout: int = 120) -> dict: + """SSH 连接执行命令""" + try: + import paramiko + except ImportError: + return {"success": False, "stdout": "", "stderr": "paramiko 未安装", "returncode": -1} + + _host = host or SSH_HOST + _user = user or SSH_USER + _password = password or SSH_PASSWORD + _port = port or SSH_PORT + + if not _host or not _user: + return {"success": False, "stdout": "", "stderr": "SSH_HOST 或 SSH_USER 未配置", "returncode": -1} + + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + client.connect(_host, port=_port, username=_user, password=_password, timeout=30) + stdin, stdout, stderr = client.exec_command(command, timeout=timeout) + stdout_str = stdout.read().decode("utf-8", errors="replace").strip() + stderr_str = stderr.read().decode("utf-8", errors="replace").strip() + returncode = stdout.channel.recv_exit_status() + logger.info(f"✅ SSH exec 完成 (rc={returncode}): {command[:80]}") + return { + "success": returncode == 0, + "stdout": stdout_str, + "stderr": stderr_str, + "returncode": returncode, + "host": _host, + } + except Exception as e: + logger.error(f"❌ SSH 连接失败: {e}") + return {"success": False, "stdout": "", "stderr": str(e), "returncode": -1} + finally: + client.close() + + +def ssh_test_connection(host: Optional[str] = None, user: Optional[str] = None, + password: Optional[str] = None) -> dict: + """测试 SSH 连接是否正常""" + return ssh_exec("echo 'SSH_OK'", host=host, user=user, password=password, timeout=10) diff --git a/agent_templates/agents/code_ai_agent/src/server/tools/workspace.py b/agent_templates/agents/code_ai_agent/src/server/tools/workspace.py new file mode 100644 index 0000000..aaf2fca --- /dev/null +++ b/agent_templates/agents/code_ai_agent/src/server/tools/workspace.py @@ -0,0 +1,49 @@ +""" +工作空间管理 - 为每个任务提供隔离的工作目录 +""" +import os +import shutil +import uuid +from pathlib import Path +from typing import Optional + +WORKSPACE_ROOT = os.getenv("WORKSPACE_ROOT", "/workspace") + + +def get_workspace_path(task_id: str) -> str: + """获取任务工作空间路径""" + return os.path.join(WORKSPACE_ROOT, task_id) + + +def create_workspace(task_id: Optional[str] = None) -> str: + """创建隔离工作空间,返回 task_id""" + if not task_id: + task_id = str(uuid.uuid4())[:8] + path = get_workspace_path(task_id) + os.makedirs(path, exist_ok=True) + return task_id + + +def delete_workspace(task_id: str) -> bool: + """删除工作空间""" + path = get_workspace_path(task_id) + if os.path.exists(path): + shutil.rmtree(path) + return True + return False + + +def list_workspaces() -> list: + """列出所有工作空间""" + root = Path(WORKSPACE_ROOT) + if not root.exists(): + return [] + return [ + {"task_id": d.name, "path": str(d), "size_mb": round(sum(f.stat().st_size for f in d.rglob("*") if f.is_file()) / 1024 / 1024, 2)} + for d in root.iterdir() if d.is_dir() + ] + + +def workspace_exists(task_id: str) -> bool: + """检查工作空间是否存在""" + return os.path.exists(get_workspace_path(task_id))