diff --git a/pipeline_service/agent_loop.py b/pipeline_service/agent_loop.py index 10b0ac9..bd549c6 100644 --- a/pipeline_service/agent_loop.py +++ b/pipeline_service/agent_loop.py @@ -167,6 +167,7 @@ AGENT_TOOLS = [ {"name":"write_file","description":"写入文件(自动创建父目录)","params":{"path":"相对路径","content":"文件内容"}}, {"name":"list_files","description":"列出目录内容","params":{"path":"相对路径(可选,默认工作空间根)"}}, {"name":"run_shell","description":"在工作空间中执行shell命令","params":{"command":"命令"}}, + {"name":"git_clone","description":"克隆git仓库到工作空间repos/下","params":{"repo_url":"仓库URL","repo_name":"仓库目录名(可选,默认从URL推断)","branch":"分支(可选,默认main)"}}, {"name":"git_status","description":"查看git仓库状态","params":{"repo_dir":"仓库子目录(可选,默认repos下第一个)"}}, {"name":"git_commit_push","description":"git add + commit + push","params":{"message":"提交信息","repo_dir":"仓库子目录(可选)"}}, {"name":"ask_question","description":"向用户提问(缺少信息时使用)","params":{"question":"问题"}}, @@ -473,6 +474,21 @@ def _parse_agent_action(raw): return {"action": "deliver", "result": raw} +def _resolve_repo_target(workspace_dir, repo_dir): + """解析仓库目录:空→repos/下第一个git仓库;'repos/xxx'→直接;'xxx'→repos/xxx。""" + repos_dir = os.path.join(workspace_dir, 'repos') + if repo_dir: + if repo_dir.startswith('repos/') or repo_dir.startswith('repos\\'): + return os.path.join(workspace_dir, repo_dir) + return os.path.join(repos_dir, repo_dir) + if os.path.isdir(repos_dir): + dirs = [d for d in os.listdir(repos_dir) + if os.path.isdir(os.path.join(repos_dir, d, '.git'))] + if dirs: + return os.path.join(repos_dir, dirs[0]) + return workspace_dir + + async def _exec_agent_tool(tool, params, workspace_dir): p = params or {} try: @@ -512,26 +528,21 @@ async def _exec_agent_tool(tool, params, workspace_dir): if not cmd: return 'FAIL: 需要命令' r = await _run_shell(cmd, workspace_dir, timeout=120) return f"rc={r['rc']}\nSTDOUT:\n{r['stdout'][:2000]}\nSTDERR:\n{r['stderr'][:1000]}" + elif tool == 'git_clone': + url = p.get('repo_url', '') + if not url: return 'FAIL: 需要仓库URL' + name = p.get('repo_name', '') or url.rstrip('/').split('/')[-1].replace('.git', '') + target = os.path.join(workspace_dir, 'repos', name) + r = await _git_clone(url, target, p.get('branch', 'main')) + return f"rc={r['rc']} {r['message']}" elif tool == 'git_status': - repo = p.get('repo_dir', '') - repos_dir = os.path.join(workspace_dir, 'repos') - if repo: - target = os.path.join(workspace_dir, repo) - else: - dirs = [d for d in os.listdir(repos_dir) if os.path.isdir(os.path.join(repos_dir, d, '.git'))] if os.path.isdir(repos_dir) else [] - target = os.path.join(repos_dir, dirs[0]) if dirs else workspace_dir + target = _resolve_repo_target(workspace_dir, p.get('repo_dir', '')) r = await _run_shell('git status --short', target, 10) r2 = await _run_shell('git log --oneline -3', target, 10) return f"Status:\n{r['stdout'][:1000] or '(clean)'}\nRecent:\n{r2['stdout'][:500]}" elif tool == 'git_commit_push': msg = p.get('message', '') or 'agent update' - repo = p.get('repo_dir', '') - repos_dir = os.path.join(workspace_dir, 'repos') - if repo: - target = os.path.join(workspace_dir, repo) - else: - dirs = [d for d in os.listdir(repos_dir) if os.path.isdir(os.path.join(repos_dir, d, '.git'))] if os.path.isdir(repos_dir) else [] - target = os.path.join(repos_dir, dirs[0]) if dirs else workspace_dir + target = _resolve_repo_target(workspace_dir, p.get('repo_dir', '')) r = await _git_commit_push(target, msg) return f"rc={r['rc']} {r['message']}" return f'未实现: {tool}' @@ -565,6 +576,13 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None): repos_dir = os.path.join(workspace_dir, 'repos') os.makedirs(repos_dir, exist_ok=True) + # 确保项目关联仓库已 clone(幂等:已存在则 pull)——否则源码写不进 git 仓库 + try: + clone_results = await _setup_repos(sor, workspace_dir, project_id) + logger.info(f"role_agent_run setup_repos: {clone_results}") + except Exception as e: + logger.warning(f"role_agent_run setup_repos failed: {e}") + role_specific = ROLE_SPECIFICS.get(role, ROLE_SPECIFICS.get('develop', '')) qna_section = await _build_qna_section(sor, task_id) tools_text = json.dumps(AGENT_TOOLS, ensure_ascii=False) diff --git a/pipeline_service/agent_loop_v2.py b/pipeline_service/agent_loop_v2.py index 68077f4..a140b99 100644 --- a/pipeline_service/agent_loop_v2.py +++ b/pipeline_service/agent_loop_v2.py @@ -563,6 +563,7 @@ class AgentExecutor: "answer_question": self._t_answer_question, "add_repo": self._t_add_repo, "list_repos": self._t_list_repos, + "clone_repo": self._t_clone_repo, "run_command": self._t_run_command, "check_progress": self._t_check_progress, "add_bug": self._t_add_bug, @@ -919,6 +920,30 @@ class AgentExecutor: lines.append(f"- {getattr(r, 'repo_name', '')}: {getattr(r, 'repo_url', '')}") return "\n".join(lines) + async def _t_clone_repo(self, sor, p, pid): + if not pid: + return "请先切换到项目" + + import os + from pipeline_service.agent_loop import ( + _setup_repos, _get_workspace_dir, _git_clone) + + workspace_dir = await _get_workspace_dir(sor, pid) + url = (p.get("repo_url", "") or "").strip() + + if url: + name = url.rstrip("/").split("/")[-1].replace(".git", "") + target = os.path.join(workspace_dir, "repos", name) + r = await _git_clone(url, target, p.get("branch", "main")) + return f"rc={r['rc']} {r['message']}" + + results = await _setup_repos(sor, workspace_dir, pid) + if not results: + return "无关联仓库可克隆" + return "\n".join( + f"- {x['repo']}: rc={x.get('rc', '?')} {x.get('message', '')}" + for x in results) + async def _t_run_command(self, sor, p, pid): cmd = p.get("command", "") if not cmd: