log-center/docs/pr-review-and-retry-workflow.md
zyc 5611839fd8
Some checks failed
Build and Deploy Log Center / build-and-deploy (push) Failing after 1m55s
fix git pr
2026-02-25 10:55:26 +08:00

898 lines
33 KiB
Markdown
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.

# PR 审核与重试修复完整流程
> 文档版本: v1.0
> 创建日期: 2026-02-25
> 用途: 日志中台直接操作 PR + Close 后重新修复流程设计
---
## 📋 目录
- [Gitea API 支持情况](#gitea-api-支持情况)
- [日志中台直接操作 PR](#日志中台直接操作-pr)
- [Close PR 后重新修复流程](#close-pr-后重新修复流程)
- [技术实现](#技术实现)
- [测试验证](#测试验证)
---
## ✅ Gitea API 支持情况
### 测试结论
经过 API 测试,**Gitea 完全支持在日志中台直接操作 PR**
| 功能 | API 端点 | 是否支持 | 说明 |
|------|---------|---------|------|
| **创建 PR** | `POST /repos/{owner}/{repo}/pulls` | ✅ 支持 | 可自动创建 PR |
| **合并 PR** | `POST /repos/{owner}/{repo}/pulls/{index}/merge` | ✅ 支持 | **可在日志中台直接 merge** |
| **关闭 PR** | `PATCH /repos/{owner}/{repo}/pulls/{index}` | ✅ 支持 | **可在日志中台直接 close** |
| **添加评论** | `POST /repos/{owner}/{repo}/issues/{index}/comments` | ✅ 支持 | 记录 close 原因 |
| **获取评论** | `GET /repos/{owner}/{repo}/issues/{index}/comments` | ✅ 支持 | 读取 close 原因 |
| **重新打开** | `PATCH /repos/{owner}/{repo}/pulls/{index}` | ✅ 支持 | 可重新激活 PR |
### 关键发现
1.**无需跳转到 Gitea**:所有操作都可以通过 API 在日志中台完成
2.**支持添加原因**Close PR 时可以通过评论记录详细原因
3.**可以重新打开**Close 后可以重新激活 PR如果需要
---
## 🎯 日志中台直接操作 PR
### 方案 B在日志中台内审核推荐
由于 Gitea API 完全支持,**推荐使用方案 B**,用户无需跳转到 Gitea。
### Web 界面设计
#### Bug 详情页 - PR 审核区
```
┌─────────────────────────────────────────────────────────────┐
│ Bug #123: TypeError in user_login │
├─────────────────────────────────────────────────────────────┤
│ 状态: 🟡 PENDING_REVIEW (待审核) │
│ │
│ 📋 修复报告 │
│ ├─ AI 分析: request.user 可能为 None │
│ ├─ 修改文件: 2 个 │
│ ├─ 测试结果: ✅ 通过 │
│ ├─ 严重等级: 🟠 8/10 (高风险 - 核心业务逻辑) │
│ └─ PR: #45 │
│ │
│ 📊 风险评估详情 │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ **风险类别:** 核心业务逻辑 │ │
│ │ **业务影响:** 可能导致用户登录失败 │ │
│ │ **回滚难度:** 中等 │ │
│ │ │ │
│ │ **判定理由:** │ │
│ │ • 修改了用户登录流程,属于核心业务 │ │
│ │ • 添加了 null 检查,降低了 TypeError 风险 │ │
│ │ • 测试覆盖充分,包含边界条件 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ 🔗 Pull Request #45 │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ fix/auto-20260225-1430 → main │ │
│ │ 修改: 2 files (+15, -1) │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ 📄 代码变更(展开查看) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ app/views.py [展开 ▼] │ │
│ │ ───────────────────────────────────── │ │
│ │ 23 │ def user_login(request): │ │
│ │ -24 │ for item in request.user: │ │
│ │ +24 │ if request.user is not None: │ │
│ │ +25 │ for item in request.user: │ │
│ │ 26 │ # ... rest of code │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ 💬 审核意见(可选) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ [输入框:添加审核意见或拒绝原因...] │ │
│ │ │ │
│ │ 常用模板: │ │
│ │ • 测试覆盖不足 │ │
│ │ • 业务逻辑需要调整 │ │
│ │ • 建议添加更多边界条件测试 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ⚡ 审核操作 │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ [ ✓ 批准并合并 ] [ ✗ 拒绝修复 ] │ │ ← 核心操作
│ │ │ │
│ │ 提示:拒绝后 Bug 将回到待修复状态, │ │
│ │ Agent 会结合拒绝原因进行二次修复 │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### 用户操作流程
#### 场景 1批准并合并
```
1. 用户在 Bug 详情页查看修复报告
2. 查看代码 diff展开查看
3. 确认修复正确
4. 点击 "批准并合并" 按钮
后端操作:
5. 调用 Gitea API 合并 PR
6. 更新 Bug 状态: PENDING_REVIEW → MERGED
7. 触发 CI/CD 自动部署
```
#### 场景 2拒绝修复重点
```
1. 用户在 Bug 详情页查看修复报告
2. 发现问题(例如:测试不足、业务逻辑有误)
3. 在审核意见框输入拒绝原因:
"测试覆盖不足,缺少边界条件测试。
建议添加以下测试场景:
1. request.user 为 None 的情况
2. request.user 为空列表的情况"
4. 点击 "拒绝修复" 按钮
后端操作:
5. 调用 Gitea API 添加评论(记录拒绝原因)
6. 调用 Gitea API 关闭 PR
7. 更新 Bug 状态: PENDING_REVIEW → PENDING_FIX
8. 记录拒绝原因到数据库
Agent 自动检测:
9. Agent 定时扫描发现 PENDING_FIX 状态的 Bug
10. 读取拒绝原因
11. 结合 Bug 本身 + 拒绝原因进行二次修复
12. 创建新的 PR
```
---
## 🔄 Close PR 后重新修复流程
### 完整流程图
```
┌─────────────────────────────────────────────────────────────┐
│ 第一次修复尝试 │
├─────────────────────────────────────────────────────────────┤
│ NEW → FIXING → PENDING_REVIEW (PR #45) │
│ ↓ │
│ 【人工审核】 │
│ ↓ │
│ 发现问题,拒绝 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 拒绝处理流程 │
├─────────────────────────────────────────────────────────────┤
│ 1. 添加拒绝原因评论到 PR │
│ "测试覆盖不足,缺少边界条件测试" │
│ │
│ 2. Close PR #45 │
│ │
│ 3. 更新 Bug 状态: PENDING_REVIEW → PENDING_FIX │
│ │
│ 4. 记录拒绝原因到 error_logs.rejection_reason │
│ │
│ 5. 删除本地 fix 分支git branch -D fix/auto-xxx
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Agent 二次修复 │
├─────────────────────────────────────────────────────────────┤
│ 1. Agent 定时扫描,发现 Bug #123 状态为 PENDING_FIX │
│ │
│ 2. 检查是否有拒绝原因rejection_reason 不为空) │
│ │
│ 3. 构造增强 Prompt: │
│ """ │
│ 你是 Bug 修复代理。这个 Bug 之前修复过一次,但被拒绝。 │
│ │
│ 【原始 Bug 信息】 │
│ - 错误类型: TypeError │
│ - 错误消息: 'NoneType' object is not iterable │
│ - 文件: app/views.py:24 │
│ │
│ 【第一次修复被拒原因】 │
│ 测试覆盖不足,缺少边界条件测试。 │
│ 建议添加以下测试场景: │
│ 1. request.user 为 None 的情况 │
│ 2. request.user 为空列表的情况 │
│ │
│ 【第一次修复内容】(供参考) │
│ - 修改了 app/views.py添加了 null 检查 │
│ - 但测试用例只覆盖了正常场景 │
│ │
│ 请针对上述拒绝原因,重新修复 Bug
│ 1. 修复原始 BugTypeError
│ 2. 补充被指出缺失的测试场景 │
│ 3. 确保测试覆盖充分 │
│ """ │
│ │
│ 4. 调用 Claude Code CLI 进行二次修复 │
│ │
│ 5. 运行测试验证 │
│ │
│ 6. AI 评估严重等级 │
│ │
│ 7. 创建新的 PR #46附带改进说明
│ │
│ 8. 更新状态: PENDING_FIX → PENDING_REVIEW │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 第二次人工审核 │
├─────────────────────────────────────────────────────────────┤
│ 查看 PR #46: │
│ - 看到改进说明:"已根据反馈补充测试" │
│ - 查看新增的测试用例 │
│ - 确认修复质量 │
│ │
│ 审核结果: │
│ ├─ 通过 → 合并 PR → MERGED → DEPLOYED │
│ └─ 拒绝 → 再次回到 PENDING_FIX可设置最大重试次数
└─────────────────────────────────────────────────────────────┘
```
### 关键设计点
#### 1. 拒绝原因记录
**数据库字段:**
```sql
ALTER TABLE error_logs ADD COLUMN rejection_reason TEXT;
ALTER TABLE error_logs ADD COLUMN rejection_count INT DEFAULT 0;
ALTER TABLE error_logs ADD COLUMN last_rejected_at TIMESTAMP;
```
**记录内容:**
```json
{
"rejected_at": "2026-02-25T15:30:00Z",
"rejected_by": "张三",
"reason": "测试覆盖不足,缺少边界条件测试。建议添加以下测试场景...",
"previous_pr": {
"pr_number": 45,
"pr_url": "https://gitea.xxx/owner/repo/pulls/45",
"branch": "fix/auto-20260225-1430",
"modified_files": ["app/views.py", "tests/test_views.py"],
"diff": "..."
}
}
```
#### 2. 二次修复 Prompt 增强
**Prompt 模板:**
```python
def build_retry_prompt_with_rejection(bug, rejection_info):
"""构造包含拒绝原因的重试 Prompt"""
prompt = f"""你是一个 Bug 修复代理。这个 Bug 之前修复过 {rejection_info['rejection_count']} 次,但被审核人员拒绝。
## 原始 Bug 信息
{bug.format_for_prompt()}
## 第 {rejection_info['rejection_count']} 次修复被拒原因
**拒绝时间:** {rejection_info['rejected_at']}
**审核人员:** {rejection_info['rejected_by']}
**拒绝原因:**
{rejection_info['reason']}
## 上次修复内容(供参考)
**修改的文件:**
{', '.join(rejection_info['previous_pr']['modified_files'])}
**代码变更:**
```diff
{rejection_info['previous_pr']['diff'][:1000]}
```
**上次修复的问题:**
根据审核人员的反馈,上次修复存在以下问题:
{parse_rejection_issues(rejection_info['reason'])}
## 本次修复要求
请针对审核人员指出的问题,重新修复 Bug
1. **修复原始 Bug**{bug.error.type} - {bug.error.message}
2. **解决被指出的问题**
{generate_fix_requirements(rejection_info['reason'])}
3. **测试要求**
- 必须覆盖审核人员提出的测试场景
- 确保测试用例能够验证修复效果
- 测试必须通过
4. **代码质量**
- 参考上次修复的方向,但避免同样的错误
- 代码应该更加健壮和完善
- 添加必要的注释说明改进点
## 修复说明要求
修复完成后,请在测试文件顶部添加注释说明:
- 本次修复针对哪些审核意见进行了改进
- 新增了哪些测试场景
- 与上次修复的主要区别
请立即开始修复。
"""
return prompt
```
#### 3. PR 标题和描述增强
**第二次 PR 标题:**
```
fix: auto repair bug #123 (重试 #2 - 已补充测试)
```
**PR 描述:**
```markdown
## 🔄 二次修复
本 PR 是针对 Bug #123 的第 2 次修复尝试。
### 📌 第一次修复被拒原因
> 时间: 2026-02-25 15:30
> 审核人: 张三
> PR: #45 (已关闭)
**拒绝原因:**
测试覆盖不足,缺少边界条件测试。建议添加以下测试场景:
1. request.user 为 None 的情况
2. request.user 为空列表的情况
### ✅ 本次改进
**1. 代码修复(保持不变)**
- 添加了 `if request.user is not None` 检查(与上次相同)
**2. 测试增强(新增)**
- ✅ 新增测试:`test_user_login_with_none_user`
- ✅ 新增测试:`test_user_login_with_empty_user_list`
- ✅ 新增测试:`test_user_login_with_anonymous_user`
**3. 测试覆盖率**
- 上次60% (仅正常场景)
- 本次95% (包含边界条件)
### 📄 修改文件
- `app/views.py` (+3, -1) - 添加 null 检查
- `tests/test_views.py` (+45, -0) - 新增 3 个测试用例
### 🧪 测试结果
```
test_user_login_normal ................ PASSED
test_user_login_with_none_user ........ PASSED ← 新增
test_user_login_with_empty_list ....... PASSED ← 新增
test_user_login_with_anonymous ........ PASSED ← 新增
----------------------------------------------
4 tests passed
```
---
**请审核:** 已根据反馈补充测试,请重新审核。
```
#### 4. 防止无限重试
**配置项:**
```python
max_rejection_retry_count: int = 3 # 最多拒绝 3 次
```
**逻辑:**
```python
if bug.rejection_count >= settings.max_rejection_retry_count:
logger.warning(f"Bug #{bug.id} 已被拒绝 {bug.rejection_count} 次,标记为 FAILED")
task_manager.update_status(bug.id, BugStatus.FAILED, "多次修复被拒,需人工处理")
return
```
---
## 💻 技术实现
### 1. 日志中台 API新增接口
**文件:`log_center/app/api/bugs.py`**
```python
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
import httpx
router = APIRouter()
@router.post("/bugs/{bug_id}/approve-pr")
async def approve_and_merge_pr(
bug_id: int,
db: Session = Depends(get_db),
):
"""
批准并合并 PR
流程:
1. 调用 Gitea API 合并 PR
2. 更新 Bug 状态 → MERGED
"""
bug = db.query(ErrorLog).filter(ErrorLog.id == bug_id).first()
if not bug:
raise HTTPException(status_code=404, detail="Bug not found")
if bug.status != BugStatus.PENDING_REVIEW.value:
raise HTTPException(status_code=400, detail="Bug 状态不是待审核")
if not bug.pr_number:
raise HTTPException(status_code=400, detail="Bug 没有关联的 PR")
# 调用 Gitea API 合并 PR
gitea_client = GiteaClient(
gitea_url=settings.gitea_url,
token=settings.gitea_token,
)
success, message = gitea_client.merge_pr(
owner=bug.project_info.owner,
repo=bug.project_info.repo,
pr_number=bug.pr_number,
)
if success:
# 更新 Bug 状态
bug.status = BugStatus.MERGED.value
bug.merged_at = datetime.now()
db.commit()
return {"message": "PR 已合并", "pr_url": bug.pr_url}
else:
raise HTTPException(status_code=500, detail=f"合并失败: {message}")
@router.post("/bugs/{bug_id}/reject-pr")
async def reject_and_close_pr(
bug_id: int,
reason: str,
db: Session = Depends(get_db),
current_user = Depends(get_current_user), # 获取审核人
):
"""
拒绝修复并关闭 PR
流程:
1. 添加拒绝原因评论到 PR
2. 调用 Gitea API 关闭 PR
3. 更新 Bug 状态 → PENDING_FIX
4. 记录拒绝原因
5. Agent 会自动检测并重新修复
"""
bug = db.query(ErrorLog).filter(ErrorLog.id == bug_id).first()
if not bug:
raise HTTPException(status_code=404, detail="Bug not found")
if bug.status != BugStatus.PENDING_REVIEW.value:
raise HTTPException(status_code=400, detail="Bug 状态不是待审核")
if not bug.pr_number:
raise HTTPException(status_code=400, detail="Bug 没有关联的 PR")
gitea_client = GiteaClient(
gitea_url=settings.gitea_url,
token=settings.gitea_token,
)
# Step 1: 添加拒绝原因评论
comment_body = f"""## ❌ 修复被拒绝
**审核人:** {current_user.username}
**时间:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
**拒绝原因:**
{reason}
---
**操作:** 系统将自动重新修复此 Bug并根据上述反馈改进。
"""
gitea_client.add_pr_comment(
owner=bug.project_info.owner,
repo=bug.project_info.repo,
pr_number=bug.pr_number,
comment=comment_body,
)
# Step 2: 关闭 PR
success, message = gitea_client.close_pr(
owner=bug.project_info.owner,
repo=bug.project_info.repo,
pr_number=bug.pr_number,
)
if not success:
raise HTTPException(status_code=500, detail=f"关闭 PR 失败: {message}")
# Step 3: 更新 Bug 状态
bug.status = BugStatus.PENDING_FIX.value
bug.rejection_count = (bug.rejection_count or 0) + 1
bug.last_rejected_at = datetime.now()
# Step 4: 记录拒绝原因
rejection_info = {
"rejected_at": datetime.now().isoformat(),
"rejected_by": current_user.username,
"reason": reason,
"previous_pr": {
"pr_number": bug.pr_number,
"pr_url": bug.pr_url,
"branch": bug.branch_name,
},
}
bug.rejection_reason = json.dumps(rejection_info, ensure_ascii=False)
db.commit()
return {
"message": "PR 已拒绝Bug 将重新修复",
"rejection_count": bug.rejection_count,
}
```
---
### 2. Gitea Client 封装
**文件:`log_center/app/utils/gitea_client.py`**
```python
import httpx
from typing import Tuple
class GiteaClient:
"""Gitea API 客户端"""
def __init__(self, gitea_url: str, token: str):
self.gitea_url = gitea_url.rstrip("/")
self.token = token
self.base_api_url = f"{self.gitea_url}/api/v1"
self.client = httpx.Client(timeout=30)
def _headers(self) -> dict:
return {
"Authorization": f"token {self.token}",
"Content-Type": "application/json",
}
def merge_pr(self, owner: str, repo: str, pr_number: int) -> Tuple[bool, str]:
"""
合并 PR
Returns:
(是否成功, 消息)
"""
url = f"{self.base_api_url}/repos/{owner}/{repo}/pulls/{pr_number}/merge"
payload = {
"Do": "merge",
"MergeMessageField": f"Merge PR #{pr_number} (approved via Log Center)",
}
try:
response = self.client.post(url, json=payload, headers=self._headers())
response.raise_for_status()
return True, "合并成功"
except httpx.HTTPStatusError as e:
return False, f"HTTP {e.response.status_code}: {e.response.text}"
except Exception as e:
return False, str(e)
def close_pr(self, owner: str, repo: str, pr_number: int) -> Tuple[bool, str]:
"""
关闭 PR
Returns:
(是否成功, 消息)
"""
url = f"{self.base_api_url}/repos/{owner}/{repo}/pulls/{pr_number}"
payload = {"state": "closed"}
try:
response = self.client.patch(url, json=payload, headers=self._headers())
response.raise_for_status()
return True, "关闭成功"
except Exception as e:
return False, str(e)
def add_pr_comment(
self, owner: str, repo: str, pr_number: int, comment: str
) -> Tuple[bool, str]:
"""
添加 PR 评论
Returns:
(是否成功, 消息)
"""
url = f"{self.base_api_url}/repos/{owner}/{repo}/issues/{pr_number}/comments"
payload = {"body": comment}
try:
response = self.client.post(url, json=payload, headers=self._headers())
response.raise_for_status()
return True, "评论添加成功"
except Exception as e:
return False, str(e)
def get_pr_comments(
self, owner: str, repo: str, pr_number: int
) -> list[dict]:
"""获取 PR 所有评论"""
url = f"{self.base_api_url}/repos/{owner}/{repo}/issues/{pr_number}/comments"
try:
response = self.client.get(url, headers=self._headers())
response.raise_for_status()
return response.json()
except Exception:
return []
```
---
### 3. Repair Agent 二次修复逻辑
**文件:`log_center/repair_agent/agent/core.py`**
`fix_project()` 中检测拒绝原因:
```python
def fix_project(self, project_id: str, ...) -> BatchFixResult:
"""修复项目的所有待修复 Bug"""
# 获取待修复的 Bug
bugs = self.task_manager.fetch_pending_bugs(project_id)
# 分类:首次修复 vs 重试修复
first_time_bugs = []
retry_bugs = []
for bug in bugs:
if bug.rejection_reason:
# 有拒绝原因 → 重试修复
retry_bugs.append(bug)
else:
# 无拒绝原因 → 首次修复
first_time_bugs.append(bug)
# 先处理首次修复
if first_time_bugs:
logger.info(f"首次修复 {len(first_time_bugs)} 个 Bug")
self._fix_bugs_batch(first_time_bugs, project_id, ...)
# 再处理重试修复
if retry_bugs:
logger.info(f"重试修复 {len(retry_bugs)} 个 Bug")
self._retry_fix_bugs_with_rejection(retry_bugs, project_id, ...)
def _retry_fix_bugs_with_rejection(
self, bugs: list[Bug], project_id: str, ...
):
"""针对被拒绝的 Bug 进行二次修复"""
for bug in bugs:
# 检查重试次数
if bug.rejection_count >= settings.max_rejection_retry_count:
logger.warning(
f"Bug #{bug.id} 已被拒绝 {bug.rejection_count} 次,标记为 FAILED"
)
self.task_manager.update_status(
bug.id, BugStatus.FAILED, "多次修复被拒,需人工处理"
)
continue
# 解析拒绝原因
rejection_info = json.loads(bug.rejection_reason)
logger.info(f"Bug #{bug.id}{bug.rejection_count + 1} 次修复")
logger.info(f"上次拒绝原因: {rejection_info['reason'][:100]}...")
# 构造增强 Prompt
prompt = self._build_retry_prompt_with_rejection(bug, rejection_info)
# 调用 Claude 修复
success, output = self.claude_service.execute_prompt(
prompt=prompt,
cwd=project_path,
)
# 后续流程与首次修复相同
# ...
```
---
## 🧪 测试验证
### 测试脚本
已创建测试脚本:`log_center/repair_agent/test_gitea_api.py`
**运行测试:**
```bash
cd log_center/repair_agent
python test_gitea_api.py \
--gitea-url https://gitea.airlabs.art \
--token YOUR_TOKEN \
--owner airlabs \
--repo rtc_backend \
--pr-number 45
```
**测试内容:**
1. ✅ 创建 PR
2. ✅ 合并 PR验证可在日志中台直接操作
3. ✅ 关闭 PR验证可在日志中台直接操作
4. ✅ 添加拒绝原因评论
5. ✅ 获取评论(验证可读取拒绝原因)
6. ✅ 重新打开 PR可选
---
## 📊 完整数据流
### 数据库变更
```sql
-- 新增字段
ALTER TABLE error_logs ADD COLUMN rejection_reason TEXT COMMENT '拒绝原因JSON';
ALTER TABLE error_logs ADD COLUMN rejection_count INT DEFAULT 0 COMMENT '拒绝次数';
ALTER TABLE error_logs ADD COLUMN last_rejected_at TIMESTAMP COMMENT '最后拒绝时间';
ALTER TABLE error_logs ADD COLUMN merged_at TIMESTAMP COMMENT '合并时间';
```
### rejection_reason JSON 结构
```json
{
"rejected_at": "2026-02-25T15:30:00Z",
"rejected_by": "张三",
"reason": "测试覆盖不足,缺少边界条件测试...",
"previous_pr": {
"pr_number": 45,
"pr_url": "https://gitea.xxx/owner/repo/pulls/45",
"branch": "fix/auto-20260225-1430",
"modified_files": ["app/views.py"],
"diff": "..."
}
}
```
---
## 🎯 实施计划
### Phase 1日志中台 PR 操作(必须)
**时间2 天**
- [ ] 后端
- [ ] 创建 `GiteaClient` 工具类
- [ ] 新增 `/bugs/{id}/approve-pr` 接口
- [ ] 新增 `/bugs/{id}/reject-pr` 接口
- [ ] 数据库添加拒绝相关字段
- [ ] 前端
- [ ] Bug 详情页添加"批准并合并"按钮
- [ ] Bug 详情页添加"拒绝修复"按钮
- [ ] 添加拒绝原因输入框
- [ ] 添加常用拒绝理由模板
- [ ] 测试
- [ ] 运行 `test_gitea_api.py` 验证 API
- [ ] 端到端测试完整流程
### Phase 2二次修复流程必须
**时间3 天**
- [ ] Repair Agent
- [ ] 修改 `fix_project()` 区分首次/重试
- [ ] 新增 `_retry_fix_bugs_with_rejection()` 方法
- [ ] 新增 `_build_retry_prompt_with_rejection()` 方法
- [ ] 添加重试次数限制(默认 3 次)
- [ ] 配置
- [ ] 添加 `max_rejection_retry_count` 配置
- [ ] 添加 Gitea URL 和 Token 配置
- [ ] 测试
- [ ] 完整测试拒绝 → 重新修复 → 再次审核流程
---
## 🔍 FAQ
### Q1: 为什么要在日志中台操作 PR而不是跳转到 Gitea
**A:**
1. **用户体验更好**:无需在两个系统间跳转
2. **操作更直观**:直接在 Bug 详情页操作,上下文清晰
3. **可扩展性强**:可以添加更多审核功能(如批量审核)
4. **统一管理**:所有操作记录在日志中台
### Q2: 拒绝后 Agent 会立即重新修复吗?
**A:** 取决于配置:
- **定时扫描模式**:等待下一次扫描(如 1 小时后)
- **实时模式**:可以配置 Webhook拒绝后立即触发修复
- **手动触发**:提供"立即重新修复"按钮
### Q3: 如果 Agent 三次修复都被拒绝怎么办?
**A:**
- Bug 状态标记为 `FAILED`
- 需要人工介入处理
- 可配置最大重试次数(默认 3 次)
### Q4: 拒绝原因会显示在新的 PR 中吗?
**A:** 会!新 PR 的描述会包含:
- 上次拒绝原因
- 本次改进说明
- 对比改进前后的差异
---
## 📋 总结
### ✅ Gitea API 完全支持
| 操作 | 支持情况 | 说明 |
|------|---------|------|
| 在日志中台 merge PR | ✅ 支持 | 无需跳转到 Gitea |
| 在日志中台 close PR | ✅ 支持 | 可添加拒绝原因 |
| 获取拒绝原因 | ✅ 支持 | 通过评论获取 |
| 二次修复 | ✅ 支持 | Agent 自动检测并重试 |
### 🔄 完整闭环
```
首次修复 → PR 审核 → 拒绝 → 记录原因 → Agent 检测
↓ ↓ ↓
通过 close PR 结合原因二次修复
↓ ↓
MERGED 新 PR改进版
再次审核
```
### 🎯 核心价值
1. **提升审核效率**:在日志中台直接操作,无需跳转
2. **智能重试**Agent 根据拒绝原因针对性改进
3. **可追溯**:完整记录拒绝原因和改进过程
4. **防止死循环**:最多重试 3 次,超过则人工介入
---
**下一步:是否开始实施?**