diff --git a/app/main.py b/app/main.py index f876a0b..7d616e7 100644 --- a/app/main.py +++ b/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 diff --git a/repair_agent/README.md b/repair_agent/README.md index 45cc3bb..a954573 100644 --- a/repair_agent/README.md +++ b/repair_agent/README.md @@ -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 +# 项目本地路径(修改为你的实际路径) +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` 范围内,不会被重复拾取。 diff --git a/repair_agent/agent/claude_service.py b/repair_agent/agent/claude_service.py index ec9c0bc..c35fde6 100644 --- a/repair_agent/agent/claude_service.py +++ b/repair_agent/agent/claude_service.py @@ -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 的修复方式和与上次的区别。", "", "请立即开始修复,直接编辑文件。", ]) diff --git a/repair_agent/agent/core.py b/repair_agent/agent/core.py index 98b66d8..97c78e3 100644 --- a/repair_agent/agent/core.py +++ b/repair_agent/agent/core.py @@ -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,133 +165,119 @@ class RepairEngine: last_test_output = "" last_diff = "" - for round_num in range(1, max_rounds + 1): - logger.info(f"=== 第 {round_num}/{max_rounds} 轮修复 ===") + try: + for round_num in range(1, max_rounds + 1): + logger.info(f"=== 第 {round_num}/{max_rounds} 轮修复 ===") - # Step 1: 调用 Claude 修复 - if round_num == 1: - success, output = self.claude_service.batch_fix_bugs(bugs, project_path) - else: - success, output = self.claude_service.retry_fix_bugs( - bugs, project_path, - previous_diff=last_diff, - previous_test_output=last_test_output, - round_num=round_num, - ) - - if not success: - # Claude CLI 本身执行失败,不重试 - failure_reason = f"Claude CLI 执行失败: {output[:500]}" - logger.error(f"{failure_reason} (round {round_num})") - - for bug in bugs: - self._upload_round_report( - bug=bug, project_id=project_id, round_num=round_num, - ai_analysis=output, diff="", modified_files=[], - test_output="", test_passed=False, - failure_reason=failure_reason, - status=BugStatus.FIX_FAILED, + # Step 1: 调用 Claude 修复 + if round_num == 1: + success, output = self.claude_service.batch_fix_bugs(bugs, project_path) + else: + success, output = self.claude_service.retry_fix_bugs( + bugs, project_path, + previous_diff=last_diff, + previous_test_output=last_test_output, + round_num=round_num, ) - self.task_manager.update_status(bug.id, BugStatus.FIX_FAILED, failure_reason) - results.append(FixResult(bug_id=bug.id, success=False, message=failure_reason)) - break - # Step 2: 获取变更 - modified_files = [] - diff = "" - if git_manager: - modified_files = git_manager.get_modified_files() - diff = git_manager.get_diff() + if not success: + # Claude CLI 本身执行失败,不重试 + failure_reason = f"Claude CLI 执行失败: {output[:500]}" + logger.error(f"{failure_reason} (round {round_num})") - logger.info(f"第 {round_num} 轮修复完成,修改了 {len(modified_files)} 个文件") + for bug in bugs: + self._upload_round_report( + bug=bug, project_id=project_id, round_num=round_num, + ai_analysis=output, diff="", modified_files=[], + test_output="", test_passed=False, + failure_reason=failure_reason, + status=BugStatus.FIX_FAILED, + ) + self.task_manager.update_status(bug.id, BugStatus.FIX_FAILED, failure_reason) + results.append(FixResult(bug_id=bug.id, success=False, message=failure_reason)) + break - # Step 3: 安全检查(仅在 Git 启用时,不重试) - if git_manager and not self._safety_check(modified_files, diff): - failure_reason = "安全检查未通过" - logger.warning(f"{failure_reason} (round {round_num})") - git_manager.reset_hard() + # Step 2: 获取变更 + modified_files = [] + diff = "" + if git_manager: + modified_files = git_manager.get_modified_files() + diff = git_manager.get_diff() - 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_passed=False, - failure_reason=failure_reason, - status=BugStatus.FIX_FAILED, - ) - self.task_manager.update_status(bug.id, BugStatus.FIX_FAILED, failure_reason) - results.append(FixResult(bug_id=bug.id, success=False, message=failure_reason)) - break + logger.info(f"第 {round_num} 轮修复完成,修改了 {len(modified_files)} 个文件") - # Step 4: 运行测试 - test_result = None - if run_tests: - test_runner = TestRunner(project_path, project_id) - test_result = test_runner.run_full_suite() + # Step 3: 安全检查(仅在 Git 启用时,不重试) + if git_manager and not self._safety_check(modified_files, diff): + failure_reason = "安全检查未通过" + logger.warning(f"{failure_reason} (round {round_num})") + git_manager.reset_hard() - 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} 轮)" - - # 上传本轮报告 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, + test_output="", test_passed=False, failure_reason=failure_reason, - status=BugStatus.FIX_FAILED if is_last_round else BugStatus.FIXING, + status=BugStatus.FIX_FAILED, ) + self.task_manager.update_status(bug.id, BugStatus.FIX_FAILED, failure_reason) + results.append(FixResult(bug_id=bug.id, success=False, message=failure_reason)) + break - # 回滚准备下一轮或最终失败 - if git_manager: - git_manager.reset_hard() + # 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 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)) + if not test_output: + test_output = "Claude 未生成测试文件" + logger.warning(f"测试文件 {test_file} 不存在,跳过测试验证") + + # 清理临时测试文件 + cleanup_repair_test_file(project_path, test_file) + + 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_output, + test_passed=test_passed, + failure_reason=None, + status=BugStatus.FIXED, + ) + results.append(FixResult( + bug_id=bug.id, success=True, + message=f"修复成功 (第 {round_num} 轮)", + modified_files=modified_files, diff=diff, + )) + + # 自动提交、合并到 main 并推送(仅在 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("代码已提交") + git_manager.push() + logger.info("fix 分支已推送") + # 合并 fix 分支到 main 并推送,触发 CI/CD + if git_manager.merge_to_main_and_push(): + logger.info("已合并到 main 并推送") else: - logger.info(f"第 {round_num} 轮测试未通过,准备第 {round_num + 1} 轮重试...") + logger.warning("合并到 main 失败,请手动合并") + elif not git_enabled and auto_commit: + logger.info("未配置 GitHub 仓库,跳过自动提交") - continue # 进入下一轮 + break # 成功,退出循环 - # Step 5: 测试通过(或跳过测试)— 成功! + except Exception as e: + # 兜底:标记为 FIX_FAILED,防止死循环(可通过 retry 命令重新处理) + failure_reason = f"修复流程异常终止: {str(e)[:500]}" + logger.exception(failure_reason) 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, - failure_reason=None, - status=BugStatus.FIXED, - ) - results.append(FixResult( - bug_id=bug.id, success=True, - message=f"修复成功 (第 {round_num} 轮)", - modified_files=modified_files, diff=diff, - )) - - # 自动提交、合并到 main 并推送(仅在 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("代码已提交") - git_manager.push() - logger.info("fix 分支已推送") - # 合并 fix 分支到 main 并推送,触发 CI/CD - if git_manager.merge_to_main_and_push(): - logger.info("已合并到 main 并推送") - else: - logger.warning("合并到 main 失败,请手动合并") - elif not git_enabled and auto_commit: - logger.info("未配置 GitHub 仓库,跳过自动提交") - - break # 成功,退出循环 + 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) @@ -439,77 +497,66 @@ class RepairEngine: last_test_output = "" last_diff = "" - for round_num in range(1, max_rounds + 1): - logger.info(f"=== Bug #{bug_id} 第 {round_num}/{max_rounds} 轮修复 ===") + try: + for round_num in range(1, max_rounds + 1): + logger.info(f"=== Bug #{bug_id} 第 {round_num}/{max_rounds} 轮修复 ===") - # 调用 Claude - if round_num == 1: - success, output = self.claude_service.batch_fix_bugs([bug], project_path) - else: - success, output = self.claude_service.retry_fix_bugs( - [bug], project_path, - previous_diff=last_diff, - previous_test_output=last_test_output, - round_num=round_num, - ) - - if not success: - failure_reason = f"Claude CLI 执行失败: {output[:500]}" - self._upload_round_report( - bug=bug, project_id=bug.project_id, round_num=round_num, - ai_analysis=output, diff="", modified_files=[], - test_output="", test_passed=False, - failure_reason=failure_reason, status=BugStatus.FIX_FAILED, - ) - self.task_manager.update_status(bug_id, BugStatus.FIX_FAILED, failure_reason) - return FixResult(bug_id=bug_id, success=False, message=failure_reason) - - 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() - - 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} 轮)" - - 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, + # 调用 Claude + if round_num == 1: + success, output = self.claude_service.batch_fix_bugs([bug], project_path) + else: + success, output = self.claude_service.retry_fix_bugs( + [bug], project_path, + previous_diff=last_diff, + previous_test_output=last_test_output, + round_num=round_num, ) - git_manager.reset_hard() + if not success: + failure_reason = f"Claude CLI 执行失败: {output[:500]}" + self._upload_round_report( + bug=bug, project_id=bug.project_id, round_num=round_num, + ai_analysis=output, diff="", modified_files=[], + test_output="", test_passed=False, + failure_reason=failure_reason, status=BugStatus.FIX_FAILED, + ) + self.task_manager.update_status(bug_id, BugStatus.FIX_FAILED, failure_reason) + return FixResult(bug_id=bug_id, success=False, message=failure_reason) - 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) + modified_files = git_manager.get_modified_files() + diff = git_manager.get_diff() - logger.info(f"第 {round_num} 轮测试未通过,准备第 {round_num + 1} 轮重试...") - continue + # 运行 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 - # 测试通过 — 成功 - 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, - ) - return FixResult( - bug_id=bug_id, success=True, - message=f"修复成功 (第 {round_num} 轮)", - modified_files=modified_files, diff=diff, - ) + if not test_output: + test_output = "Claude 未生成测试文件" + logger.warning(f"测试文件 {test_file} 不存在,跳过测试验证") + + # 清理临时测试文件 + cleanup_repair_test_file(project_path, test_file) + + 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_output, + test_passed=test_passed, failure_reason=None, status=BugStatus.FIXED, + ) + return FixResult( + bug_id=bug_id, success=True, + message=f"修复成功 (第 {round_num} 轮)", + 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="修复流程异常结束")