From 61cbcfc4c4231610ac6ae5fd798bc11ac36445b0 Mon Sep 17 00:00:00 2001 From: zyc <1439655764@qq.com> Date: Thu, 12 Feb 2026 10:42:39 +0800 Subject: [PATCH] fix bug --- repair_agent/.env.example | 5 + repair_agent/__main__.py | 56 ++++++++- repair_agent/agent/__init__.py | 2 + repair_agent/agent/core.py | 70 ++++++----- repair_agent/agent/git_manager.py | 28 ++++- repair_agent/agent/scheduler.py | 201 ++++++++++++++++++++++++++++++ repair_agent/config/settings.py | 21 ++++ 7 files changed, 346 insertions(+), 37 deletions(-) create mode 100644 repair_agent/agent/scheduler.py diff --git a/repair_agent/.env.example b/repair_agent/.env.example index a990a46..8303705 100644 --- a/repair_agent/.env.example +++ b/repair_agent/.env.example @@ -10,6 +10,11 @@ GIT_USER_NAME=repair-agent GIT_USER_EMAIL=agent@airlabs.art GITEA_TOKEN=your_token_here +# GitHub 仓库地址(为空则不执行 git 操作) +GITHUB_REPO_RTC_BACKEND= +GITHUB_REPO_RTC_WEB= +GITHUB_REPO_AIRHUB_APP= + # 项目路径映射 (project_id -> 本地路径) PATH_rtc_backend=/Users/maidong/Desktop/zyc/qy_gitlab/rtc_backend PATH_rtc_web=/Users/maidong/Desktop/zyc/qy_gitlab/rtc_web diff --git a/repair_agent/__main__.py b/repair_agent/__main__.py index 497325c..c40eee9 100644 --- a/repair_agent/__main__.py +++ b/repair_agent/__main__.py @@ -1,12 +1,15 @@ """ Repair Agent CLI - 命令行入口 """ +from typing import List, Optional + import typer from loguru import logger from rich.console import Console from rich.table import Table from .agent import RepairEngine, TaskManager +from .agent.scheduler import RepairScheduler from .config import settings app = typer.Typer( @@ -118,10 +121,14 @@ def status(): console.print(f"最大修改文件数: [cyan]{settings.max_modified_files}[/cyan]") console.print(f"核心文件关键词: [cyan]{settings.critical_files}[/cyan]") - console.print("\n[bold]项目路径映射:[/bold]") - console.print(f" rtc_backend: [blue]{settings.path_rtc_backend}[/blue]") - console.print(f" rtc_web: [blue]{settings.path_rtc_web}[/blue]") - console.print(f" airhub_app: [blue]{settings.path_airhub_app}[/blue]") + console.print("\n[bold]项目配置:[/bold]") + for pid in ["rtc_backend", "rtc_web", "airhub_app"]: + path = settings.get_project_path(pid) + repo = settings.get_github_repo(pid) + git_status = f"[green]{repo}[/green]" if repo else "[yellow]未配置(Git 已禁用)[/yellow]" + console.print(f" {pid}:") + console.print(f" 路径: [blue]{path}[/blue]") + console.print(f" GitHub: {git_status}") @app.command() @@ -155,6 +162,47 @@ def analyze( engine.close() +@app.command() +def watch( + interval: int = typer.Option( + None, "--interval", "-i", + help="扫描间隔(秒),默认读取 WATCH_INTERVAL 环境变量或 3600", + ), + project: Optional[List[str]] = typer.Option( + None, "--project", "-p", + help="监控的项目ID,可多次指定,默认监控所有已配置项目", + ), + test: bool = typer.Option(True, "--test/--no-test", help="修复后是否运行测试"), + commit: bool = typer.Option(False, "--commit", "-c", help="是否自动提交代码"), +): + """ + 启动定时扫描守护进程,每隔固定时间扫描新 Bug 并自动修复。 + + 示例: + python -m repair_agent watch # 默认每小时扫描所有项目 + python -m repair_agent watch -i 1800 # 每 30 分钟扫描 + python -m repair_agent watch -p rtc_backend # 只监控 rtc_backend + python -m repair_agent watch -p rtc_backend -p rtc_web -c # 监控两个项目并自动提交 + """ + scan_interval = interval or settings.watch_interval + projects = list(project) if project else None + + console.print("[bold blue]启动 Repair Scheduler[/bold blue]") + console.print(f" 扫描间隔: [cyan]{scan_interval}s[/cyan]") + console.print(f" 监控项目: [cyan]{', '.join(projects) if projects else '全部已配置项目'}[/cyan]") + console.print(f" 运行测试: [cyan]{test}[/cyan]") + console.print(f" 自动提交: [cyan]{commit}[/cyan]") + console.print("\n[dim]按 Ctrl+C 停止[/dim]\n") + + scheduler = RepairScheduler( + interval=scan_interval, + run_tests=test, + auto_commit=commit, + projects=projects, + ) + scheduler.start() + + def main(): """入口函数""" app() diff --git a/repair_agent/agent/__init__.py b/repair_agent/agent/__init__.py index 649c0ee..569e251 100644 --- a/repair_agent/agent/__init__.py +++ b/repair_agent/agent/__init__.py @@ -3,6 +3,7 @@ from .task_manager import TaskManager from .git_manager import GitManager from .claude_service import ClaudeService from .test_runner import TestRunner +from .scheduler import RepairScheduler __all__ = [ "RepairEngine", @@ -10,4 +11,5 @@ __all__ = [ "GitManager", "ClaudeService", "TestRunner", + "RepairScheduler", ] diff --git a/repair_agent/agent/core.py b/repair_agent/agent/core.py index 6c357c5..7e7ba29 100644 --- a/repair_agent/agent/core.py +++ b/repair_agent/agent/core.py @@ -64,17 +64,21 @@ class RepairEngine: ) logger.info(f"获取到 {len(bugs)} 个待修复 Bug") - - # 初始化 Git 管理器 - git_manager = GitManager(project_path) - - # 拉取最新代码 - git_manager.pull() - - # 创建修复分支 - branch_name = f"fix/auto-{datetime.now().strftime('%Y%m%d-%H%M%S')}" - if auto_commit: - git_manager.create_branch(branch_name) + + # 检查是否启用 Git + git_enabled = settings.is_git_enabled(project_id) + git_manager = None + + if git_enabled: + github_repo = settings.get_github_repo(project_id) + git_manager = GitManager(project_path, github_repo=github_repo) + git_manager.pull() + + branch_name = f"fix/auto-{datetime.now().strftime('%Y%m%d-%H%M%S')}" + if auto_commit: + git_manager.create_branch(branch_name) + else: + logger.info(f"项目 {project_id} 未配置 GitHub 仓库,跳过 Git 操作") # 更新所有 Bug 状态为 FIXING for bug in bugs: @@ -86,19 +90,22 @@ class RepairEngine: results = [] if success: # 获取修改的文件 - modified_files = git_manager.get_modified_files() - diff = git_manager.get_diff() - + modified_files = [] + diff = "" + if git_manager: + modified_files = git_manager.get_modified_files() + diff = git_manager.get_diff() + logger.info(f"修复完成,修改了 {len(modified_files)} 个文件") - - # 安全检查 - if not self._safety_check(modified_files, diff): + + # 安全检查(仅在 Git 启用时) + if git_manager and not self._safety_check(modified_files, diff): logger.warning("安全检查未通过,回滚更改") git_manager.reset_hard() - + for bug in bugs: self.task_manager.update_status( - bug.id, + bug.id, BugStatus.FIX_FAILED, "安全检查未通过" ) @@ -107,7 +114,7 @@ class RepairEngine: success=False, message="安全检查未通过", )) - + return BatchFixResult( project_id=project_id, total=len(bugs), @@ -115,17 +122,18 @@ class RepairEngine: failed_count=len(bugs), results=results, ) - + # 运行测试 test_result = None if run_tests: test_runner = TestRunner(project_path, project_id) test_result = test_runner.run_full_suite() - + if not test_result.success: logger.error("测试未通过,回滚更改") - git_manager.reset_hard() - + if git_manager: + git_manager.reset_hard() + for bug in bugs: self.task_manager.update_status( bug.id, @@ -137,7 +145,7 @@ class RepairEngine: success=False, message="测试未通过", )) - + return BatchFixResult( project_id=project_id, total=len(bugs), @@ -145,11 +153,11 @@ class RepairEngine: failed_count=len(bugs), results=results, ) - + # 标记成功并上传报告 for bug in bugs: self.task_manager.update_status(bug.id, BugStatus.FIXED) - + # 上传修复报告 try: report = RepairReport( @@ -174,12 +182,14 @@ class RepairEngine: modified_files=modified_files, diff=diff, )) - - # 自动提交 - if auto_commit and modified_files: + + # 自动提交(仅在 Git 启用时) + if git_enabled and auto_commit and modified_files and git_manager: bug_ids = ", ".join([f"#{b.id}" for b in bugs]) git_manager.commit(f"fix: auto repair bugs {bug_ids}") logger.info("代码已提交") + elif not git_enabled and auto_commit: + logger.info("未配置 GitHub 仓库,跳过自动提交") else: logger.error(f"修复失败: {output}") for bug in bugs: diff --git a/repair_agent/agent/git_manager.py b/repair_agent/agent/git_manager.py index fb20482..06b46bf 100644 --- a/repair_agent/agent/git_manager.py +++ b/repair_agent/agent/git_manager.py @@ -11,12 +11,13 @@ from ..config import settings class GitManager: """负责 Git 操作""" - - def __init__(self, project_path: str): + + def __init__(self, project_path: str, github_repo: str = ""): self.project_path = project_path + self.github_repo = github_repo.strip() self.repo: Optional[Repo] = None self._init_repo() - + def _init_repo(self): """初始化 Git 仓库""" try: @@ -25,10 +26,31 @@ class GitManager: with self.repo.config_writer() as config: config.set_value("user", "name", settings.git_user_name) config.set_value("user", "email", settings.git_user_email) + + # 如果指定了 GitHub 仓库地址,确保 origin 指向正确的仓库 + if self.github_repo: + self._ensure_remote(self.github_repo) + logger.info(f"Git 仓库初始化成功: {self.project_path}") except InvalidGitRepositoryError: logger.error(f"无效的 Git 仓库: {self.project_path}") self.repo = None + + def _ensure_remote(self, repo_url: str): + """确保 origin remote 指向指定的仓库地址""" + if not self.repo: + return + try: + if "origin" in [r.name for r in self.repo.remotes]: + current_url = self.repo.remotes.origin.url + if current_url != repo_url: + self.repo.remotes.origin.set_url(repo_url) + logger.info(f"更新 origin 地址: {repo_url}") + else: + self.repo.create_remote("origin", repo_url) + logger.info(f"添加 origin 地址: {repo_url}") + except Exception as e: + logger.error(f"配置 remote 失败: {e}") def pull(self) -> bool: """拉取最新代码""" diff --git a/repair_agent/agent/scheduler.py b/repair_agent/agent/scheduler.py new file mode 100644 index 0000000..f030c9c --- /dev/null +++ b/repair_agent/agent/scheduler.py @@ -0,0 +1,201 @@ +""" +Scheduler - 定时扫描并自动修复 Bug +""" +import signal +import threading +import time +from datetime import datetime +from typing import Optional + +from loguru import logger + +from ..config import settings +from .core import RepairEngine +from .task_manager import TaskManager + + +class RepairScheduler: + """定时扫描 Bug 并自动触发修复""" + + def __init__( + self, + interval: int = 3600, + run_tests: bool = True, + auto_commit: bool = False, + projects: Optional[list[str]] = None, + ): + """ + Args: + interval: 扫描间隔(秒),默认 3600(1 小时) + run_tests: 修复后是否运行测试 + auto_commit: 是否自动提交代码 + projects: 要监控的项目列表,None 表示所有已配置项目 + """ + self.interval = interval + self.run_tests = run_tests + self.auto_commit = auto_commit + self.projects = projects or self._get_configured_projects() + self._stop_event = threading.Event() + self._repairing = False + + @staticmethod + def _get_configured_projects() -> list[str]: + """获取所有有路径配置的项目""" + candidates = ["rtc_backend", "rtc_web", "airhub_app"] + return [p for p in candidates if settings.get_project_path(p)] + + def start(self): + """启动定时任务(阻塞式,Ctrl+C 退出)""" + signal.signal(signal.SIGINT, self._handle_signal) + signal.signal(signal.SIGTERM, self._handle_signal) + + logger.info("=" * 60) + logger.info("Repair Scheduler 启动") + logger.info(f" 扫描间隔: {self.interval}s ({self.interval // 60} min)") + logger.info(f" 监控项目: {', '.join(self.projects)}") + logger.info(f" 运行测试: {self.run_tests}") + logger.info(f" 自动提交: {self.auto_commit}") + logger.info("=" * 60) + + # 启动后立即执行一次 + self._tick() + + while not self._stop_event.is_set(): + self._stop_event.wait(self.interval) + if not self._stop_event.is_set(): + self._tick() + + logger.info("Repair Scheduler 已停止") + + def stop(self): + """停止定时任务""" + logger.info("正在停止 Scheduler...") + self._stop_event.set() + + def _handle_signal(self, signum, frame): + logger.info(f"收到信号 {signum},准备停止") + self.stop() + + def _tick(self): + """执行一次扫描和修复""" + ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + logger.info(f"[{ts}] 开始扫描...") + + task_manager = TaskManager() + total_found = 0 + total_fixed = 0 + + try: + for project_id in self.projects: + bugs = task_manager.fetch_pending_bugs(project_id) + # 也拉取 UI 触发的 PENDING_FIX 状态 + pending_bugs = self._fetch_pending_fix_bugs(task_manager, project_id) + all_bugs = bugs + pending_bugs + + if not all_bugs: + logger.info(f" [{project_id}] 无新 Bug") + continue + + count = len(all_bugs) + total_found += count + logger.info(f" [{project_id}] 发现 {count} 个待修复 Bug") + + # 检查项目路径是否存在 + project_path = settings.get_project_path(project_id) + if not project_path: + logger.warning(f" [{project_id}] 未配置项目路径,跳过") + continue + + # 执行修复 + fixed = self._run_repair(project_id) + total_fixed += fixed + + logger.info( + f"[{ts}] 扫描完成: " + f"发现 {total_found} 个 Bug, 成功修复 {total_fixed} 个" + ) + logger.info(f"下次扫描: {self.interval}s 后") + except Exception as e: + logger.error(f"扫描过程出错: {e}") + finally: + task_manager.close() + + @staticmethod + def _fetch_pending_fix_bugs( + task_manager: TaskManager, project_id: str + ) -> list: + """获取 PENDING_FIX 状态的 Bug(由 UI 触发)""" + try: + import httpx + + params = {"status": "PENDING_FIX", "project_id": project_id} + response = task_manager.client.get( + f"{task_manager.base_url}/api/v1/bugs", params=params + ) + response.raise_for_status() + data = response.json() + + from ..models import Bug, BugStatus + + bugs = [] + for item in data.get("items", []): + stack_trace = item.get("stack_trace") + if isinstance(stack_trace, str): + stack_trace = stack_trace.split("\n") + + bug = Bug( + id=item["id"], + project_id=item["project_id"], + environment=item.get("environment", "production"), + level=item.get("level", "ERROR"), + error={ + "type": item.get("error_type", "Unknown"), + "message": item.get("error_message", ""), + "file_path": item.get("file_path"), + "line_number": item.get("line_number"), + "stack_trace": stack_trace, + }, + context=item.get("context"), + status=BugStatus(item.get("status", "PENDING_FIX")), + retry_count=item.get("retry_count", 0), + ) + bugs.append(bug) + return bugs + except Exception as e: + logger.debug(f"获取 PENDING_FIX Bug 失败: {e}") + return [] + + def _run_repair(self, project_id: str) -> int: + """对指定项目执行修复,返回成功修复的数量""" + if self._repairing: + logger.warning(f" [{project_id}] 上一次修复尚未完成,跳过") + return 0 + + self._repairing = True + try: + engine = RepairEngine() + result = engine.fix_project( + project_id=project_id, + run_tests=self.run_tests, + auto_commit=self.auto_commit, + ) + engine.close() + + if result.total == 0: + return 0 + + logger.info( + f" [{project_id}] 修复结果: " + f"{result.success_count}/{result.total} 成功" + ) + for r in result.results: + icon = "✓" if r.success else "✗" + logger.info(f" {icon} Bug #{r.bug_id}: {r.message}") + + return result.success_count + + except Exception as e: + logger.error(f" [{project_id}] 修复过程出错: {e}") + return 0 + finally: + self._repairing = False diff --git a/repair_agent/config/settings.py b/repair_agent/config/settings.py index 106df72..d27397a 100644 --- a/repair_agent/config/settings.py +++ b/repair_agent/config/settings.py @@ -24,7 +24,15 @@ class Settings(BaseSettings): git_user_name: str = Field(default="repair-agent", description="Git 用户名") git_user_email: str = Field(default="agent@airlabs.art", description="Git 邮箱") gitea_token: Optional[str] = Field(default=None, description="Gitea Token") + + # GitHub 仓库地址(为空则不执行 git 操作) + github_repo_rtc_backend: str = Field(default="", description="rtc_backend GitHub 仓库地址") + github_repo_rtc_web: str = Field(default="", description="rtc_web GitHub 仓库地址") + github_repo_airhub_app: str = Field(default="", description="airhub_app GitHub 仓库地址") + # 定时任务 + watch_interval: int = Field(default=3600, description="定时扫描间隔(秒)") + # 安全配置 max_retry_count: int = Field(default=3, description="最大重试次数") max_modified_lines: int = Field(default=500, description="最大修改行数") @@ -62,6 +70,19 @@ class Settings(BaseSettings): } return path_map.get(project_id) + def get_github_repo(self, project_id: str) -> str: + """获取项目的 GitHub 仓库地址,为空表示未配置""" + repo_map = { + "rtc_backend": self.github_repo_rtc_backend, + "rtc_web": self.github_repo_rtc_web, + "airhub_app": self.github_repo_airhub_app, + } + return repo_map.get(project_id, "").strip() + + def is_git_enabled(self, project_id: str) -> bool: + """检查项目是否启用了 Git 操作(GitHub 仓库地址不为空)""" + return bool(self.get_github_repo(project_id)) + def get_critical_files(self) -> list[str]: """获取核心文件关键词列表""" return [f.strip() for f in self.critical_files.split(",") if f.strip()]