log-center/repair_agent/agent/claude_service.py
repair-agent 25c9b2d18e
All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 1m39s
fix bug
2026-03-02 17:46:33 +08:00

325 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Claude Service - 调用 Claude Code CLI
"""
import subprocess
from typing import Optional
from loguru import logger
from ..config import settings
from ..models import Bug
class ClaudeService:
"""通过 CLI 调用 Claude Code"""
def __init__(self):
self.cli_path = settings.claude_cli_path
self.timeout = settings.claude_timeout
def execute_prompt(
self,
prompt: str,
cwd: str,
allowed_tools: Optional[str] = None,
) -> tuple[bool, str]:
"""
执行 Claude CLI 命令
Args:
prompt: 提示词
cwd: 工作目录
allowed_tools: 可选,限制可用工具(逗号分隔)。
为 None 时配合 --dangerously-skip-permissions 允许所有工具。
Returns:
(成功与否, 输出内容)
"""
try:
cmd = [
self.cli_path,
"-p", prompt,
"--model", settings.claude_model,
"--dangerously-skip-permissions",
]
if allowed_tools:
cmd.extend(["--allowedTools", allowed_tools])
logger.debug(f"执行 Claude CLI (cwd={cwd})")
logger.debug(f"Prompt 长度: {len(prompt)} 字符")
result = subprocess.run(
cmd,
cwd=cwd,
capture_output=True,
text=True,
timeout=self.timeout,
)
output = result.stdout + result.stderr
if result.returncode == 0:
logger.info("Claude CLI 执行成功")
logger.debug(f"输出前 500 字符: {output[:500]}")
return True, output.strip()
else:
logger.error(f"Claude CLI 执行失败 (code={result.returncode}): {output[:500]}")
return False, output.strip()
except subprocess.TimeoutExpired:
logger.error(f"Claude CLI 超时 ({self.timeout}秒)")
return False, "执行超时"
except FileNotFoundError:
logger.error(f"Claude CLI 未找到: {self.cli_path}")
return False, "Claude CLI 未安装"
except Exception as e:
logger.error(f"Claude CLI 执行异常: {e}")
return False, str(e)
def batch_fix_bugs(
self,
bugs: list[Bug],
project_path: str,
) -> tuple[bool, str]:
"""
批量修复多个 Bug
Args:
bugs: Bug 列表
project_path: 项目路径
Returns:
(成功与否, Claude 输出)
"""
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。",
"",
"重要你必须使用工具Read、Edit、Write 等)直接修改源代码文件来修复 Bug。",
"不要只是分析问题或给出建议,你需要实际定位文件并编辑代码。",
"",
]
for bug in bugs:
prompt_parts.append(bug.format_for_prompt())
prompt_parts.append("")
prompt_parts.extend([
"## 修复要求",
"1. 先用 Grep/Glob 定位相关源代码文件",
"2. 用 Read 读取文件内容,理解上下文",
"3. 用 Edit 或 Write 直接修改代码来修复 Bug",
"4. 每个 Bug 只做最小必要的改动",
"5. 确保不破坏现有功能",
"",
"## 测试用例要求(必须严格遵守)",
"",
"**如果是代码逻辑 Bug运行时错误、TypeError、AttributeError 等):**",
f"- 修复完成后,你必须针对本次修复编写测试用例",
f"- 将测试用例保存到项目根目录下的 `{test_file}` 文件中",
"- 测试文件必须是可独立运行的(包含必要的 import",
"- 对于 Django 项目,测试类应继承 `django.test.TestCase`",
"- **不要运行测试,只写测试文件。测试会由系统自动运行。**",
"",
"**如果是 CI/CD 构建或部署 BugDocker build 失败、依赖错误、语法错误等):**",
f"- 编写一个简单的验证脚本保存到 `{test_file}`",
"- 脚本内容:验证项目能正常加载(如 import 检查、`manage.py check` 等)",
"- **不要运行脚本,只写文件。**",
"",
"最后简要说明每个 Bug 的修复方式。",
"",
"请立即开始修复,直接编辑文件。",
])
prompt = "\n".join(prompt_parts)
logger.info(f"开始批量修复 {len(bugs)} 个 Bug...")
return self.execute_prompt(prompt, project_path)
def retry_fix_bugs(
self,
bugs: list[Bug],
project_path: str,
previous_diff: str,
previous_test_output: str,
round_num: int,
) -> tuple[bool, str]:
"""
重试修复 Bug带上次失败的上下文
Args:
bugs: Bug 列表
project_path: 项目路径
previous_diff: 上轮修复产生的 diff
previous_test_output: 上轮测试输出
round_num: 当前轮次
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} 次修复尝试。",
"",
"## 上次修复尝试失败",
"",
"上次你进行了如下代码修改:",
"```diff",
previous_diff[:3000],
"```",
"",
"但是测试未通过,测试输出如下:",
"```",
previous_test_output[:3000],
"```",
"",
"请分析上次修复失败的原因,避免同样的错误,重新修复以下 Bug",
"",
]
for bug in bugs:
prompt_parts.append(bug.format_for_prompt())
prompt_parts.append("")
prompt_parts.extend([
"## 修复要求",
"1. 先分析上次测试失败的原因",
"2. 用 Grep/Glob 定位相关源代码文件",
"3. 用 Read 读取文件内容,理解上下文",
"4. 用 Edit 或 Write 直接修改代码来修复 Bug",
"5. 每个 Bug 只做最小必要的改动",
"6. 确保不破坏现有功能",
"",
"## 测试用例要求(必须严格遵守)",
"",
"**如果是代码逻辑 Bug运行时错误、TypeError、AttributeError 等):**",
f"- 修复完成后,你必须针对本次修复编写测试用例",
f"- 将测试用例保存到项目根目录下的 `{test_file}` 文件中",
"- 测试文件必须是可独立运行的(包含必要的 import",
"- 对于 Django 项目,测试类应继承 `django.test.TestCase`",
"- **不要运行测试,只写测试文件。测试会由系统自动运行。**",
"",
"**如果是 CI/CD 构建或部署 BugDocker build 失败、依赖错误、语法错误等):**",
f"- 编写一个简单的验证脚本保存到 `{test_file}`",
"- 脚本内容:验证项目能正常加载(如 import 检查、`manage.py check` 等)",
"- **不要运行脚本,只写文件。**",
"",
"最后简要说明每个 Bug 的修复方式和与上次的区别。",
"",
"请立即开始修复,直接编辑文件。",
])
prompt = "\n".join(prompt_parts)
logger.info(f"开始第 {round_num} 轮修复 {len(bugs)} 个 Bug...")
return self.execute_prompt(prompt, project_path)
def triage_bug(self, bug: Bug, project_path: str) -> tuple[bool, str]:
"""
分诊 Bug判断是否为可修复的代码缺陷。
输出中包含 VERDICT:FIX 或 VERDICT:CANNOT_REPRODUCE。
"""
prompt = f"""你是一个 Bug 分诊专家。请分析以下 Bug判断它是否是一个需要修复的**代码缺陷**。
{bug.format_for_prompt()}
## 判断规则
### 运行时错误runtime
属于 **无法复现 / 不需要修复** 的情况CANNOT_REPRODUCE
1. JWT Token 过期、认证失败 — 正常认证流程,不是代码 Bug
2. HTTP 405 Method Not Allowed — 客户端请求了错误的方法
3. 第三方库内部错误且 file_path 指向 site-packages / sdk — 非项目代码
4. 瞬态网络错误、加载中断(如 PlatformException: Loading interrupted
5. 客户端传参错误导致的验证失败
6. 错误堆栈中没有项目代码帧(全在框架/三方库中)
属于 **需要修复** 的情况FIX
1. 堆栈中有项目代码apps/ 或 lib/ 开头)且错误原因明确
2. 数据库约束错误IntegrityError由项目代码逻辑引起
3. TypeError / AttributeError 出现在项目视图或模型中
### CI/CD 构建错误CICDFailure
属于 **需要修复** 的情况FIX
1. Docker build 失败且日志中包含代码编译/语法错误SyntaxError、ImportError、ModuleNotFoundError 等)
2. npm/pip install 成功但 build 阶段因代码问题失败
3. Dockerfile 中 RUN 命令因项目代码问题报错
属于 **无法复现 / 不需要修复** 的情况CANNOT_REPRODUCE
1. Docker 镜像仓库认证失败、推送失败 — 基础设施问题
2. kubectl 部署失败、K8s 资源不足 — 运维问题
3. 网络超时、镜像拉取失败 — 瞬态错误
4. 日志中没有具体代码错误,只有通用构建/部署失败信息
请先用 Grep/Read 查看相关源文件确认当前代码状态,然后给出判断。
**最后一行必须输出以下格式之一(只输出一个):**
VERDICT:FIX
VERDICT:CANNOT_REPRODUCE
"""
return self.execute_prompt(prompt, project_path, allowed_tools="Read,Grep,Glob")
def analyze_bug(self, bug: Bug, project_path: str) -> tuple[bool, str]:
"""
分析单个 Bug不修复
Args:
bug: Bug 对象
project_path: 项目路径
Returns:
(成功与否, 分析结果)
"""
prompt = f"""
请分析以下 Bug但不要修改任何代码
{bug.format_for_prompt()}
请分析:
1. 错误的根本原因
2. 建议的修复方案
3. 可能影响的其他文件
"""
return self.execute_prompt(prompt, project_path, allowed_tools="Read,Grep,Glob")
def review_changes(self, diff: str, project_path: str) -> tuple[bool, str]:
"""
审核代码变更
Args:
diff: Git diff 内容
project_path: 项目路径
Returns:
(是否通过, 审核意见)
"""
prompt = f"""
请审核以下代码变更是否安全:
```diff
{diff}
```
审核要点:
1. 是否修复了目标问题
2. 是否引入了新的 Bug
3. 是否破坏了原有业务逻辑
4. 是否有安全风险
如果审核通过,回复 "APPROVED"。否则说明问题。
"""
return self.execute_prompt(prompt, project_path, allowed_tools="Read,Grep,Glob")