Some checks failed
Build and Deploy Log Center / build-and-deploy (push) Failing after 1m55s
898 lines
33 KiB
Markdown
898 lines
33 KiB
Markdown
# 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. 修复原始 Bug(TypeError) │
|
||
│ 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 次,超过则人工介入
|
||
|
||
---
|
||
|
||
**下一步:是否开始实施?**
|