fix 百分比
All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 2m38s

This commit is contained in:
zyc 2026-02-24 17:04:39 +08:00
parent bc63d580ac
commit f9c84b211b
4 changed files with 462 additions and 199 deletions

View File

@ -235,9 +235,14 @@ async def get_dashboard_stats(source: Optional[str] = None, session: AsyncSessio
count_result = await session.exec(count_query)
status_counts[status.value] = count_result.one()
# Fixed rate = (FIXED + VERIFIED + DEPLOYED) / Total
fixed_count = status_counts.get("FIXED", 0) + status_counts.get("VERIFIED", 0) + status_counts.get("DEPLOYED", 0)
fix_rate = round((fixed_count / total_bugs * 100), 2) if total_bugs > 0 else 0
# 修复率 = (FIXED + VERIFIED + DEPLOYED + CANNOT_REPRODUCE) / Total
resolved_count = (
status_counts.get("FIXED", 0)
+ status_counts.get("VERIFIED", 0)
+ status_counts.get("DEPLOYED", 0)
+ status_counts.get("CANNOT_REPRODUCE", 0)
)
fix_rate = round((resolved_count / total_bugs * 100), 2) if total_bugs > 0 else 0
# Source distribution
from .models import LogSource

View File

@ -1,51 +1,226 @@
# Repair Agent - 自动化 Bug 修复代理
本地运行的自动化 Bug 修复工具,从 Log Center 获取 Bug使用 Claude Code CLI 进行修复
从 Log Center 获取 Bug使用 Claude Code CLI 自动修复,支持多轮重试、测试验证、自动提交
## 安装
## 前置条件
- Python 3.12+
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) 已安装并登录(终端运行 `claude` 可用)
- 目标项目代码已 clone 到本地
## 快速开始
### 1. 安装依赖
```bash
cd log_center/repair_agent
pip install -r requirements.txt
```
## 配置
复制 `.env.example``.env` 并配置:
### 2. 配置环境变量
```bash
cp .env.example .env
```
## 使用
编辑 `.env`,必须配置的项:
```bash
# 查看待修复的 Bug
python -m repair_agent list
# Log Center API 地址(默认已配置线上地址)
LOG_CENTER_URL=https://qiyuan-log-center-api.airlabs.art
# 修复指定项目的所有 Bug
python -m repair_agent fix rtc_backend
# Claude CLI 路径(如果 claude 不在 PATH 中需要指定完整路径)
CLAUDE_CLI_PATH=claude
CLAUDE_TIMEOUT=1000 # Claude 执行超时(秒)
# 修复单个 Bug
python -m repair_agent fix-one <bug_id>
# 项目本地路径(修改为你的实际路径)
PATH_RTC_BACKEND=/Users/maidong/Desktop/zyc/qy_gitlab/rtc_backend
PATH_RTC_WEB=/Users/maidong/Desktop/zyc/qy_gitlab/rtc_web
PATH_AIRHUB_APP=/Users/maidong/Desktop/zyc/qiyuan_gitea/rtc_prd/airhub_app
```
# 查看状态
可选配置:
```bash
# Git 自动提交(配置后 --commit 才生效)
GITHUB_REPO_RTC_BACKEND=https://gitea.example.com/org/rtc_backend.git
GITEA_TOKEN=your_token_here
# 安全限制
MAX_RETRY_COUNT=3 # 最大修复轮次
MAX_MODIFIED_LINES=50 # 单次最大修改行数
MAX_MODIFIED_FILES=5 # 单次最大修改文件数
CRITICAL_FILES=payment,auth,security # 禁止修改的核心文件关键词
```
### 3. 验证配置
```bash
python -m repair_agent status
```
输出会显示 Log Center 连接地址、Claude CLI 路径、各项目路径等,确认无误即可。
## 命令大全
### 查看待修复 Bug
```bash
python -m repair_agent list # 所有项目
python -m repair_agent list -p rtc_backend # 指定项目
```
### 修复 Bug
```bash
# 修复指定项目的所有待修复 BugNEW / PENDING_FIX 状态)
python -m repair_agent fix rtc_backend
# 修复但不运行测试
python -m repair_agent fix rtc_backend --no-test
# 修复并自动提交推送(需配置 Git 仓库地址)
python -m repair_agent fix rtc_backend --commit
```
### 修复单个 Bug
```bash
python -m repair_agent fix-one 11 # 按 Bug ID 修复
python -m repair_agent fix-one 11 --no-test # 不运行测试
```
### 重试失败的 Bug
`FIX_FAILED` 状态的 Bug 重新处理(先分诊判断是否为代码缺陷,再决定修复或标记为无法复现):
```bash
python -m repair_agent retry # 所有项目
python -m repair_agent retry -p rtc_backend # 指定项目
python -m repair_agent retry --commit # 修复后自动提交
```
### 分析 Bug不修复
```bash
python -m repair_agent analyze 11 # 只分析,不修改代码
```
### 定时守护模式
启动后台守护进程,定时扫描新 Bug 并自动修复:
```bash
# 默认每小时扫描一次所有项目
python -m repair_agent watch
# 每 30 分钟扫描,只监控指定项目
python -m repair_agent watch -i 1800 -p rtc_backend
# 监控多个项目,自动提交
python -m repair_agent watch -p rtc_backend -p rtc_web --commit
python -m repair_agent watch -i 60 -c
# Ctrl+C 停止
```
## 修复流程
```
获取 NEW/PENDING_FIX 的 Bug
|
状态改为 FIXING
|
┌─────v─────┐
│ Claude CLI │ ← 修复代码 + 运行针对性测试
│ 修复代码 │
└─────┬─────┘
|
获取 Git diff
|
安全检查(文件数/行数/核心文件)
|
┌───v───┐
│ 测试? │──── 跳过测试 ──→ 从 Claude 输出提取验证结果
└───┬───┘
|
TestRunner 运行测试
|
┌───v───┐
│ 通过? │── 否 ──→ 回滚代码,进入下一轮(最多 3 轮)
└───┬───┘ 最终失败 → FIX_FAILED
|
FIXED ──→ 上传修复报告(含测试输出)
|
自动提交推送(可选)
```
## Bug 状态流转
| 状态 | 含义 | 触发条件 |
|------|------|---------|
| `NEW` | 新发现 | 日志上报 |
| `FIXING` | 修复中 | 开始修复 |
| `FIXED` | 已修复 | 修复成功 + 测试通过 |
| `FIX_FAILED` | 修复失败 | 测试不通过 / Claude 执行失败 / 异常终止 |
| `CANNOT_REPRODUCE` | 无法复现 | 分诊判定非代码缺陷 |
| `PENDING_FIX` | 待修复 | retry 分诊后判定可修复 |
## 修复报告
每轮修复都会上传报告到 Log Center包含
| 字段 | 内容 |
|------|------|
| `ai_analysis` | Claude 的完整分析和修复过程 |
| `code_diff` | 代码变更 diff |
| `test_output` | 测试/验证命令的实际执行输出 |
| `test_passed` | 测试是否通过 |
| `repair_round` | 第几轮修复 |
| `failure_reason` | 失败原因(成功时为空) |
## 架构
```
repair_agent/
├── agent/
│ ├── core.py # 核心引擎
│ ├── task_manager.py # Log Center 交互
│ ├── git_manager.py # Git 操作
│ ├── claude_service.py # Claude CLI 调用
│ └── test_runner.py # 测试执行
│ ├── core.py # 核心修复引擎(多轮重试、异常兜底)
│ ├── task_manager.py # 与 Log Center API 交互
│ ├── git_manager.py # Git 操作(分支、提交、推送)
│ ├── claude_service.py # Claude Code CLI 调用(提示词编排)
│ ├── test_runner.py # 测试执行(自动检测 Django/pytest/npm
│ └── scheduler.py # 定时扫描守护进程
├── config/
│ └── settings.py # 配置管理
│ └── settings.py # 配置管理pydantic-settings + .env
├── models/
│ └── bug.py # 数据模型
└── __main__.py # CLI 入口
│ └── bug.py # 数据模型Bug、RepairReport 等)
├── __main__.py # CLI 入口typer
├── .env.example # 环境变量模板
└── requirements.txt # Python 依赖
```
## 常见问题
**Q: Bug 卡在 FIXING 状态怎么办?**
流程中已有异常兜底,会自动标记为 `FIX_FAILED`。如果仍有残留,可通过 API 手动重置:
```bash
curl -X PUT "https://qiyuan-log-center-api.airlabs.art/api/v1/tasks/{bug_id}/status" \
-H "Content-Type: application/json" \
-d '{"status": "NEW", "message": "手动重置"}'
```
**Q: Claude CLI 超时怎么办?**
调大 `.env` 中的 `CLAUDE_TIMEOUT`(默认 1000 秒)。
**Q: 如何只修复不提交?**
不加 `--commit` 参数即可,默认只修改本地代码不提交。
**Q: watch 模式会不会重复修复同一个 Bug**
不会。Bug 被拾取后状态改为 `FIXING`,不在 `NEW/PENDING_FIX` 范围内,不会被重复拾取。

View File

@ -93,6 +93,9 @@ class ClaudeService:
if not bugs:
return False, "没有需要修复的 Bug"
bug_ids = "_".join(str(b.id) for b in bugs)
test_file = f"repair_test_bug_{bug_ids}.py"
# 构造批量修复 Prompt
prompt_parts = [
f"你是一个自动化 Bug 修复代理。请直接修复以下 {len(bugs)} 个 Bug。",
@ -113,7 +116,22 @@ class ClaudeService:
"3. 用 Edit 或 Write 直接修改代码来修复 Bug",
"4. 每个 Bug 只做最小必要的改动",
"5. 确保不破坏现有功能",
"6. 修复完成后简要说明每个 Bug 的修复方式",
"",
"## 测试用例要求(必须严格遵守)",
"",
"**如果是代码逻辑 Bug运行时错误、TypeError、AttributeError 等):**",
f"- 修复完成后,你必须针对本次修复编写测试用例",
f"- 将测试用例保存到项目根目录下的 `{test_file}` 文件中",
"- 测试文件必须是可独立运行的(包含必要的 import",
"- 对于 Django 项目,测试类应继承 `django.test.TestCase`",
"- **不要运行测试,只写测试文件。测试会由系统自动运行。**",
"",
"**如果是 CI/CD 构建或部署 BugDocker build 失败、依赖错误、语法错误等):**",
f"- 编写一个简单的验证脚本保存到 `{test_file}`",
"- 脚本内容:验证项目能正常加载(如 import 检查、`manage.py check` 等)",
"- **不要运行脚本,只写文件。**",
"",
"最后简要说明每个 Bug 的修复方式。",
"",
"请立即开始修复,直接编辑文件。",
])
@ -144,6 +162,9 @@ class ClaudeService:
Returns:
(成功与否, Claude 输出)
"""
bug_ids = "_".join(str(b.id) for b in bugs)
test_file = f"repair_test_bug_{bug_ids}.py"
prompt_parts = [
f"你是一个自动化 Bug 修复代理。这是第 {round_num} 次修复尝试。",
"",
@ -175,7 +196,22 @@ class ClaudeService:
"4. 用 Edit 或 Write 直接修改代码来修复 Bug",
"5. 每个 Bug 只做最小必要的改动",
"6. 确保不破坏现有功能",
"7. 修复完成后简要说明每个 Bug 的修复方式和与上次的区别",
"",
"## 测试用例要求(必须严格遵守)",
"",
"**如果是代码逻辑 Bug运行时错误、TypeError、AttributeError 等):**",
f"- 修复完成后,你必须针对本次修复编写测试用例",
f"- 将测试用例保存到项目根目录下的 `{test_file}` 文件中",
"- 测试文件必须是可独立运行的(包含必要的 import",
"- 对于 Django 项目,测试类应继承 `django.test.TestCase`",
"- **不要运行测试,只写测试文件。测试会由系统自动运行。**",
"",
"**如果是 CI/CD 构建或部署 BugDocker build 失败、依赖错误、语法错误等):**",
f"- 编写一个简单的验证脚本保存到 `{test_file}`",
"- 脚本内容:验证项目能正常加载(如 import 检查、`manage.py check` 等)",
"- **不要运行脚本,只写文件。**",
"",
"最后简要说明每个 Bug 的修复方式和与上次的区别。",
"",
"请立即开始修复,直接编辑文件。",
])

View File

@ -1,6 +1,9 @@
"""
Core Engine - 核心修复引擎
"""
import os
import re
import subprocess
from datetime import datetime
from typing import Optional
from loguru import logger
@ -10,7 +13,76 @@ from ..models import Bug, BugStatus, FixResult, BatchFixResult, RepairReport
from .task_manager import TaskManager
from .git_manager import GitManager
from .claude_service import ClaudeService
from .test_runner import TestRunner
def run_repair_test_file(project_path: str, test_file: str, timeout: int = 120) -> str:
"""
运行 Claude 生成的测试文件返回测试输出
Args:
project_path: 项目根目录
test_file: 测试文件名 repair_test_bug_28.py
timeout: 超时秒数
Returns:
测试输出文本包含命令和结果
"""
test_path = os.path.join(project_path, test_file)
if not os.path.exists(test_path):
logger.warning(f"测试文件不存在: {test_path}")
return ""
# 检测项目类型,选择运行方式
manage_py = os.path.join(project_path, "manage.py")
if os.path.exists(manage_py):
# Django 项目:用 --pattern 精确匹配,只运行该文件中的测试
cmd = [
"python", "manage.py", "test",
"--pattern", test_file,
"--top-level-directory", ".",
"--keepdb", "-v", "2",
]
else:
# 其他项目:直接用 pytest 运行指定文件
cmd = ["python", "-m", "pytest", test_file, "-v"]
cmd_str = " ".join(cmd)
logger.info(f"运行测试: {cmd_str}")
try:
result = subprocess.run(
cmd,
cwd=project_path,
capture_output=True,
text=True,
timeout=timeout,
)
output = f"$ {cmd_str}\n{result.stdout}{result.stderr}"
if result.returncode == 0:
logger.info(f"测试通过: {test_file}")
else:
logger.warning(f"测试失败 (returncode={result.returncode}): {test_file}")
return output.strip()
except subprocess.TimeoutExpired:
logger.error(f"测试超时 ({timeout}s): {test_file}")
return f"$ {cmd_str}\n测试执行超时 ({timeout}秒)"
except Exception as e:
logger.error(f"测试执行异常: {e}")
return f"$ {cmd_str}\n测试执行异常: {e}"
def cleanup_repair_test_file(project_path: str, test_file: str):
"""删除 Claude 生成的临时测试文件"""
test_path = os.path.join(project_path, test_file)
try:
if os.path.exists(test_path):
os.remove(test_path)
logger.debug(f"已清理测试文件: {test_file}")
except Exception as e:
logger.warning(f"清理测试文件失败: {e}")
class RepairEngine:
@ -93,6 +165,7 @@ class RepairEngine:
last_test_output = ""
last_diff = ""
try:
for round_num in range(1, max_rounds + 1):
logger.info(f"=== 第 {round_num}/{max_rounds} 轮修复 ===")
@ -151,50 +224,26 @@ class RepairEngine:
results.append(FixResult(bug_id=bug.id, success=False, message=failure_reason))
break
# Step 4: 运行测试
test_result = None
if run_tests:
test_runner = TestRunner(project_path, project_id)
test_result = test_runner.run_full_suite()
# Step 4: 运行 Claude 生成的测试文件
bug_ids_str = "_".join(str(b.id) for b in bugs)
test_file = f"repair_test_bug_{bug_ids_str}.py"
test_output = run_repair_test_file(project_path, test_file)
test_passed = bool(test_output) and "FAILED" not in test_output and "Error" not in test_output.split("\n")[-5:].__repr__()
if not test_result.success:
last_test_output = test_result.output
last_diff = diff
is_last_round = (round_num == max_rounds)
failure_reason = f"测试未通过 (第 {round_num}/{max_rounds} 轮)"
if not test_output:
test_output = "Claude 未生成测试文件"
logger.warning(f"测试文件 {test_file} 不存在,跳过测试验证")
# 上传本轮报告
for bug in bugs:
self._upload_round_report(
bug=bug, project_id=project_id, round_num=round_num,
ai_analysis=output, diff=diff, modified_files=modified_files,
test_output=test_result.output, test_passed=False,
failure_reason=failure_reason,
status=BugStatus.FIX_FAILED if is_last_round else BugStatus.FIXING,
)
# 清理临时测试文件
cleanup_repair_test_file(project_path, test_file)
# 回滚准备下一轮或最终失败
if git_manager:
git_manager.reset_hard()
if is_last_round:
final_msg = f"经过 {max_rounds} 轮修复仍未通过测试"
for bug in bugs:
self.task_manager.update_status(bug.id, BugStatus.FIX_FAILED, final_msg)
results.append(FixResult(bug_id=bug.id, success=False, message=final_msg))
else:
logger.info(f"{round_num} 轮测试未通过,准备第 {round_num + 1} 轮重试...")
continue # 进入下一轮
# Step 5: 测试通过(或跳过测试)— 成功!
for bug in bugs:
self.task_manager.update_status(bug.id, BugStatus.FIXED)
self._upload_round_report(
bug=bug, project_id=project_id, round_num=round_num,
ai_analysis=output, diff=diff, modified_files=modified_files,
test_output=test_result.output if test_result else "Tests skipped",
test_passed=test_result.success if test_result else True,
test_output=test_output,
test_passed=test_passed,
failure_reason=None,
status=BugStatus.FIXED,
)
@ -221,6 +270,15 @@ class RepairEngine:
break # 成功,退出循环
except Exception as e:
# 兜底:标记为 FIX_FAILED防止死循环可通过 retry 命令重新处理)
failure_reason = f"修复流程异常终止: {str(e)[:500]}"
logger.exception(failure_reason)
for bug in bugs:
if bug.id not in {r.bug_id for r in results}:
self.task_manager.update_status(bug.id, BugStatus.FIX_FAILED, failure_reason)
results.append(FixResult(bug_id=bug.id, success=False, message=failure_reason))
success_count = sum(1 for r in results if r.success)
return BatchFixResult(
@ -439,6 +497,7 @@ class RepairEngine:
last_test_output = ""
last_diff = ""
try:
for round_num in range(1, max_rounds + 1):
logger.info(f"=== Bug #{bug_id}{round_num}/{max_rounds} 轮修复 ===")
@ -467,43 +526,24 @@ class RepairEngine:
modified_files = git_manager.get_modified_files()
diff = git_manager.get_diff()
# 运行测试
test_result = None
if run_tests:
test_runner = TestRunner(project_path, bug.project_id)
test_result = test_runner.run_full_suite()
# 运行 Claude 生成的测试文件
test_file = f"repair_test_bug_{bug_id}.py"
test_output = run_repair_test_file(project_path, test_file)
test_passed = bool(test_output) and "FAILED" not in test_output
if not test_result.success:
last_test_output = test_result.output
last_diff = diff
is_last_round = (round_num == max_rounds)
failure_reason = f"测试未通过 (第 {round_num}/{max_rounds} 轮)"
if not test_output:
test_output = "Claude 未生成测试文件"
logger.warning(f"测试文件 {test_file} 不存在,跳过测试验证")
self._upload_round_report(
bug=bug, project_id=bug.project_id, round_num=round_num,
ai_analysis=output, diff=diff, modified_files=modified_files,
test_output=test_result.output, test_passed=False,
failure_reason=failure_reason,
status=BugStatus.FIX_FAILED if is_last_round else BugStatus.FIXING,
)
# 清理临时测试文件
cleanup_repair_test_file(project_path, test_file)
git_manager.reset_hard()
if is_last_round:
final_msg = f"经过 {max_rounds} 轮修复仍未通过测试"
self.task_manager.update_status(bug_id, BugStatus.FIX_FAILED, final_msg)
return FixResult(bug_id=bug_id, success=False, message=final_msg)
logger.info(f"{round_num} 轮测试未通过,准备第 {round_num + 1} 轮重试...")
continue
# 测试通过 — 成功
self.task_manager.update_status(bug_id, BugStatus.FIXED)
self._upload_round_report(
bug=bug, project_id=bug.project_id, round_num=round_num,
ai_analysis=output, diff=diff, modified_files=modified_files,
test_output=test_result.output if test_result else "Tests skipped",
test_passed=True, failure_reason=None, status=BugStatus.FIXED,
test_output=test_output,
test_passed=test_passed, failure_reason=None, status=BugStatus.FIXED,
)
return FixResult(
bug_id=bug_id, success=True,
@ -511,6 +551,13 @@ class RepairEngine:
modified_files=modified_files, diff=diff,
)
except Exception as e:
# 兜底:标记为 FIX_FAILED防止死循环可通过 retry 命令重新处理)
failure_reason = f"修复流程异常终止: {str(e)[:500]}"
logger.exception(failure_reason)
self.task_manager.update_status(bug_id, BugStatus.FIX_FAILED, failure_reason)
return FixResult(bug_id=bug_id, success=False, message=failure_reason)
# 不应到达这里,但做安全兜底
return FixResult(bug_id=bug_id, success=False, message="修复流程异常结束")