All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 2m7s
202 lines
6.8 KiB
Python
202 lines
6.8 KiB
Python
"""
|
||
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
|