fix 百分比
All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 2m38s
All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 2m38s
This commit is contained in:
parent
bc63d580ac
commit
f9c84b211b
11
app/main.py
11
app/main.py
@ -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
|
||||
|
||||
@ -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
|
||||
# 修复指定项目的所有待修复 Bug(NEW / 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` 范围内,不会被重复拾取。
|
||||
|
||||
@ -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 构建或部署 Bug(Docker 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 构建或部署 Bug(Docker build 失败、依赖错误、语法错误等):**",
|
||||
f"- 编写一个简单的验证脚本保存到 `{test_file}`",
|
||||
"- 脚本内容:验证项目能正常加载(如 import 检查、`manage.py check` 等)",
|
||||
"- **不要运行脚本,只写文件。**",
|
||||
"",
|
||||
"最后简要说明每个 Bug 的修复方式和与上次的区别。",
|
||||
"",
|
||||
"请立即开始修复,直接编辑文件。",
|
||||
])
|
||||
|
||||
@ -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="修复流程异常结束")
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user