fix bug
All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 2m7s
All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 2m7s
This commit is contained in:
parent
20a3b0b374
commit
61cbcfc4c4
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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",
|
||||
]
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
"""拉取最新代码"""
|
||||
|
||||
201
repair_agent/agent/scheduler.py
Normal file
201
repair_agent/agent/scheduler.py
Normal 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: 扫描间隔(秒),默认 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
|
||||
@ -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()]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user