fix bug
All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 2m7s

This commit is contained in:
zyc 2026-02-12 10:42:39 +08:00
parent 20a3b0b374
commit 61cbcfc4c4
7 changed files with 346 additions and 37 deletions

View File

@ -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

View File

@ -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()

View File

@ -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",
]

View File

@ -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:

View File

@ -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:
"""拉取最新代码"""

View File

@ -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: 扫描间隔默认 36001 小时
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

View File

@ -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()]