diff --git a/app/.env.example b/app/.env.example new file mode 100644 index 0000000..f580113 --- /dev/null +++ b/app/.env.example @@ -0,0 +1,3 @@ +# Gitea 配置 +GITEA_URL=https://gitea.airlabs.art +GITEA_TOKEN=your_gitea_token_here diff --git a/app/gitea_client.py b/app/gitea_client.py new file mode 100644 index 0000000..c07bf1c --- /dev/null +++ b/app/gitea_client.py @@ -0,0 +1,152 @@ +""" +Gitea API 客户端 +""" +import httpx +import os +from typing import Tuple + + +class GiteaClient: + """Gitea API 操作客户端""" + + def __init__(self, gitea_url: str = None, token: str = None): + self.gitea_url = (gitea_url or os.getenv("GITEA_URL", "")).rstrip("/") + self.token = token or os.getenv("GITEA_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 _extract_owner_repo_from_pr_url(self, pr_url: str) -> Tuple[str, str, int]: + """ + 从 PR URL 提取 owner, repo, pr_number + 例如: https://gitea.airlabs.art/owner/repo/pulls/45 + """ + import re + match = re.search(r'([^/]+)/([^/]+)/pulls/(\d+)', pr_url) + if not match: + raise ValueError(f"无法解析 PR URL: {pr_url}") + + owner, repo, pr_number = match.groups() + return owner, repo, int(pr_number) + + def merge_pr( + self, owner: str, repo: str, pr_number: int + ) -> Tuple[bool, str]: + """ + 合并 PR + + Args: + owner: 仓库所有者 + repo: 仓库名称 + pr_number: PR 编号 + + Returns: + (是否成功, 消息) + """ + url = f"{self.base_api_url}/repos/{owner}/{repo}/pulls/{pr_number}/merge" + payload = { + "Do": "merge", # merge / squash / rebase + "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, "PR 合并成功" + except httpx.HTTPStatusError as e: + error_msg = f"HTTP {e.response.status_code}" + if e.response.status_code == 405: + error_msg += ": PR 已经合并或已关闭" + elif e.response.status_code == 409: + error_msg += ": 存在合并冲突" + elif e.response.status_code == 403: + error_msg += ": Token 权限不足" + else: + error_msg += f": {e.response.text[:200]}" + return False, error_msg + except Exception as e: + return False, str(e) + + def close_pr( + self, owner: str, repo: str, pr_number: int, reason: str = "" + ) -> Tuple[bool, str]: + """ + 关闭 PR(可选添加评论说明原因) + + Args: + owner: 仓库所有者 + repo: 仓库名称 + pr_number: PR 编号 + reason: 关闭原因(将作为评论添加) + + Returns: + (是否成功, 消息) + """ + # Step 1: 如果提供了原因,先添加评论 + if reason: + comment_success, comment_msg = self.add_pr_comment( + owner, repo, pr_number, f"## ❌ 修复被拒绝\n\n**原因:**\n{reason}" + ) + if not comment_success: + return False, f"添加评论失败: {comment_msg}" + + # Step 2: 关闭 PR + 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, "PR 已关闭" + except httpx.HTTPStatusError as e: + return False, f"HTTP {e.response.status_code}: {e.response.text[:200]}" + 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 评论 + + Args: + owner: 仓库所有者 + repo: 仓库名称 + pr_number: PR 编号 + comment: 评论内容 + + Returns: + (是否成功, 消息) + """ + # 注意: Gitea 中 PR 和 Issue 共用评论 API + 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 merge_pr_by_url(self, pr_url: str) -> Tuple[bool, str]: + """通过 PR URL 直接合并""" + try: + owner, repo, pr_number = self._extract_owner_repo_from_pr_url(pr_url) + return self.merge_pr(owner, repo, pr_number) + except Exception as e: + return False, str(e) + + def close_pr_by_url(self, pr_url: str, reason: str = "") -> Tuple[bool, str]: + """通过 PR URL 直接关闭""" + try: + owner, repo, pr_number = self._extract_owner_repo_from_pr_url(pr_url) + return self.close_pr(owner, repo, pr_number, reason) + except Exception as e: + return False, str(e) diff --git a/app/main.py b/app/main.py index 7d616e7..55cad24 100644 --- a/app/main.py +++ b/app/main.py @@ -4,8 +4,10 @@ from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel import select, func from .database import init_db, get_session from .models import ErrorLog, ErrorLogCreate, LogStatus, TaskStatusUpdate, RepairTask, RepairTaskCreate, Project, ProjectUpdate +from .gitea_client import GiteaClient from datetime import datetime, timedelta from typing import Optional, List +from pydantic import BaseModel import hashlib import json @@ -359,3 +361,129 @@ async def update_project(project_id: str, data: ProjectUpdate, session: AsyncSes async def health_check(): return {"status": "ok"} + +# ==================== PR 操作 ==================== + +class PRRejectRequest(BaseModel): + """拒绝 PR 请求""" + reason: str # 拒绝原因 + + +@app.post("/api/v1/bugs/{bug_id}/merge-pr", tags=["PR Operations"]) +async def merge_pr( + bug_id: int, + session: AsyncSession = Depends(get_session), +): + """ + 批准并合并 PR + + 流程: + 1. 调用 Gitea API 合并 PR + 2. 更新 Bug 状态 → MERGED + """ + statement = select(ErrorLog).where(ErrorLog.id == bug_id) + results = await session.exec(statement) + bug = results.first() + + if not bug: + raise HTTPException(status_code=404, detail="Bug not found") + + if bug.status != LogStatus.PENDING_FIX: + raise HTTPException( + status_code=400, + detail=f"Bug 状态不是待修复,当前状态: {bug.status}" + ) + + if not bug.pr_url: + raise HTTPException(status_code=400, detail="Bug 没有关联的 PR") + + # 调用 Gitea API 合并 PR + gitea_client = GiteaClient() + success, message = gitea_client.merge_pr_by_url(bug.pr_url) + + if success: + # 更新 Bug 状态 + bug.status = LogStatus.FIXED + bug.merged_at = datetime.utcnow() + session.add(bug) + await session.commit() + await session.refresh(bug) + + return { + "message": "PR 已合并", + "pr_url": bug.pr_url, + "bug_id": bug.id, + "new_status": bug.status, + } + else: + raise HTTPException(status_code=500, detail=f"合并失败: {message}") + + +@app.post("/api/v1/bugs/{bug_id}/close-pr", tags=["PR Operations"]) +async def close_pr( + bug_id: int, + request: PRRejectRequest, + session: AsyncSession = Depends(get_session), +): + """ + 拒绝修复并关闭 PR + + 流程: + 1. 添加拒绝原因评论到 PR + 2. 调用 Gitea API 关闭 PR + 3. 更新 Bug 状态 → PENDING_FIX + 4. 记录拒绝原因 + 5. Agent 会自动检测并重新修复 + """ + statement = select(ErrorLog).where(ErrorLog.id == bug_id) + results = await session.exec(statement) + bug = results.first() + + if not bug: + raise HTTPException(status_code=404, detail="Bug not found") + + if bug.status != LogStatus.PENDING_FIX: + raise HTTPException( + status_code=400, + detail=f"Bug 状态不是待修复,当前状态: {bug.status}" + ) + + if not bug.pr_url: + raise HTTPException(status_code=400, detail="Bug 没有关联的 PR") + + gitea_client = GiteaClient() + + # 关闭 PR(带拒绝原因) + success, message = gitea_client.close_pr_by_url(bug.pr_url, request.reason) + + if not success: + raise HTTPException(status_code=500, detail=f"关闭 PR 失败: {message}") + + # 更新 Bug 状态 + bug.status = LogStatus.PENDING_FIX + bug.rejection_count = (bug.rejection_count or 0) + 1 + bug.last_rejected_at = datetime.utcnow() + + # 记录拒绝原因 + rejection_info = { + "rejected_at": datetime.utcnow().isoformat(), + "reason": request.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) + + session.add(bug) + await session.commit() + await session.refresh(bug) + + return { + "message": "PR 已拒绝,Bug 将重新修复", + "rejection_count": bug.rejection_count, + "bug_id": bug.id, + "new_status": bug.status, + } + diff --git a/app/models.py b/app/models.py index 65ee4f3..ffe8d2b 100644 --- a/app/models.py +++ b/app/models.py @@ -71,6 +71,17 @@ class ErrorLog(SQLModel, table=True): # Repair Tracking failure_reason: Optional[str] = Field(default=None, sa_column=Column(Text, nullable=True)) + # PR Tracking + pr_number: Optional[int] = Field(default=None) + pr_url: Optional[str] = Field(default=None) + branch_name: Optional[str] = Field(default=None) + + # Rejection Tracking + rejection_reason: Optional[str] = Field(default=None, sa_column=Column(Text, nullable=True)) + rejection_count: int = Field(default=0) + last_rejected_at: Optional[datetime] = None + merged_at: Optional[datetime] = None + # Pydantic Models for API class ErrorLogCreate(SQLModel): project_id: str diff --git a/docs/PR_OPERATIONS_IMPLEMENTATION.md b/docs/PR_OPERATIONS_IMPLEMENTATION.md new file mode 100644 index 0000000..041484d --- /dev/null +++ b/docs/PR_OPERATIONS_IMPLEMENTATION.md @@ -0,0 +1,360 @@ +# PR 操作功能实施完成 + +> 实施日期: 2026-02-25 +> 状态: ✅ 已完成 + +--- + +## 📋 已实现功能 + +### ✅ 1. 数据库字段扩展 + +**文件**: `app/models.py` + +新增字段: +```python +# PR Tracking +pr_number: Optional[int] # PR 编号 +pr_url: Optional[str] # PR 链接 +branch_name: Optional[str] # 分支名称 + +# Rejection Tracking +rejection_reason: Optional[str] # 拒绝原因 JSON +rejection_count: int # 拒绝次数 +last_rejected_at: Optional[datetime] # 最后拒绝时间 +merged_at: Optional[datetime] # 合并时间 +``` + +### ✅ 2. Gitea API 客户端 + +**文件**: `app/gitea_client.py` + +功能: +- `merge_pr()` - 合并 PR +- `close_pr()` - 关闭 PR(带原因评论) +- `add_pr_comment()` - 添加评论 +- `merge_pr_by_url()` - 通过 URL 直接合并 +- `close_pr_by_url()` - 通过 URL 直接关闭 + +### ✅ 3. 后端 API 接口 + +**文件**: `app/main.py` + +新增接口: + +#### POST `/api/v1/bugs/{bug_id}/merge-pr` + +批准并合并 PR + +**请求**:无需 body + +**响应**: +```json +{ + "message": "PR 已合并", + "pr_url": "https://gitea.xxx/owner/repo/pulls/45", + "bug_id": 123, + "new_status": "FIXED" +} +``` + +#### POST `/api/v1/bugs/{bug_id}/close-pr` + +拒绝修复并关闭 PR + +**请求 Body**: +```json +{ + "reason": "测试覆盖不足,缺少边界条件测试" +} +``` + +**响应**: +```json +{ + "message": "PR 已拒绝,Bug 将重新修复", + "rejection_count": 1, + "bug_id": 123, + "new_status": "PENDING_FIX" +} +``` + +### ✅ 4. 前端界面 + +**文件**: `web/src/pages/BugDetail.tsx` + +**新增组件**: + +1. **PR 信息展示区** + - 显示 PR 编号、分支名 + - "查看 PR" 外链按钮 + - 拒绝次数提示 + +2. **操作按钮** + - ✅ **批准并合并** 按钮(绿色) + - ❌ **拒绝修复** 按钮(红色) + +3. **拒绝原因模态框** + - 常用模板快速填充 + - 文本输入框(详细原因) + - 确认/取消按钮 + +4. **状态消息** + - 操作成功/失败提示 + - 上次拒绝原因显示 + +--- + +## 🎯 使用流程 + +### 场景 1:批准并合并 + +``` +1. 打开 Bug 详情页 +2. 查看 PR 信息(可点击"查看 PR"跳转到 Gitea) +3. 确认修复正确 +4. 点击 "批准并合并" 按钮 + ↓ +✅ PR 合并成功,Bug 状态 → FIXED +``` + +### 场景 2:拒绝修复 + +``` +1. 打开 Bug 详情页 +2. 查看 PR 信息 +3. 发现问题,点击 "拒绝修复" 按钮 +4. 弹出模态框: + - 可点击模板快速填充常用原因 + - 或手动输入详细原因 +5. 点击 "确认拒绝" + ↓ +✅ PR 已关闭,Bug 状态 → PENDING_FIX +✅ 原因已记录,Agent 将结合原因重新修复 +``` + +--- + +## ⚙️ 配置步骤 + +### 1. 配置 Gitea 凭证 + +创建 `/log_center/app/.env` 文件: + +```bash +# Gitea 配置 +GITEA_URL=https://gitea.airlabs.art +GITEA_TOKEN=your_gitea_token_here +``` + +**获取 Gitea Token**: +1. 登录 Gitea +2. 设置 → 应用 → 生成新令牌 +3. 权限:`repo`(读写仓库) +4. 复制 Token 到 `.env` + +### 2. 数据库迁移 + +需要执行数据库迁移添加新字段: + +```sql +-- 手动执行或使用 Alembic +ALTER TABLE errorlog ADD COLUMN pr_number INT; +ALTER TABLE errorlog ADD COLUMN pr_url VARCHAR(500); +ALTER TABLE errorlog ADD COLUMN branch_name VARCHAR(200); +ALTER TABLE errorlog ADD COLUMN rejection_reason TEXT; +ALTER TABLE errorlog ADD COLUMN rejection_count INT DEFAULT 0; +ALTER TABLE errorlog ADD COLUMN last_rejected_at TIMESTAMP; +ALTER TABLE errorlog ADD COLUMN merged_at TIMESTAMP; +``` + +### 3. 重启服务 + +```bash +# 后端 +cd /log_center/app +uvicorn main:app --reload + +# 前端 +cd /log_center/web +npm run dev +``` + +--- + +## 🧪 测试 + +### 测试 Gitea API(可选) + +使用测试脚本验证 API: + +```bash +cd /log_center/repair_agent +python test_gitea_api.py \ + --gitea-url https://gitea.airlabs.art \ + --token YOUR_TOKEN \ + --owner owner \ + --repo repo \ + --pr-number 45 +``` + +### 手动测试流程 + +1. **准备测试数据**: + - 确保有一个状态为 `PENDING_FIX` 的 Bug + - 确保该 Bug 有 `pr_url` 字段(Repair Agent 创建的) + +2. **测试合并**: + - 打开 Bug 详情页 + - 点击"批准并合并" + - 检查: + - ✅ Bug 状态变为 `FIXED` + - ✅ Gitea PR 状态为 `Merged` + - ✅ `merged_at` 字段有值 + +3. **测试拒绝**: + - 打开另一个有 PR 的 Bug + - 点击"拒绝修复" + - 输入原因:"测试覆盖不足" + - 确认 + - 检查: + - ✅ Bug 状态变为 `PENDING_FIX` + - ✅ Gitea PR 状态为 `Closed` + - ✅ PR 有评论记录原因 + - ✅ `rejection_count` +1 + - ✅ `rejection_reason` 有值 + +--- + +## 📸 界面预览 + +### 有 PR 的 Bug 详情页 + +``` +┌─────────────────────────────────────────────────────┐ +│ TypeError: 'NoneType' object is not iterable │ +│ 项目: rtc_backend 来源: 运行时 级别: ERROR │ +│ 状态: [待修复] │ +├─────────────────────────────────────────────────────┤ +│ Pull Request │ +│ PR #45 | fix/auto-20260225-1430 │ +│ [查看 PR →] 已拒绝 0 次 │ +├─────────────────────────────────────────────────────┤ +│ 文件位置: app/views.py : 第 24 行 │ +│ ... │ +├─────────────────────────────────────────────────────┤ +│ [✅ 批准并合并] [❌ 拒绝修复] │ ← 关键按钮 +└─────────────────────────────────────────────────────┘ +``` + +### 拒绝原因模态框 + +``` +┌─────────────────────────────────────────┐ +│ 拒绝修复 │ +├─────────────────────────────────────────┤ +│ 请说明拒绝原因,Agent 将根据反馈重新修复 │ +│ │ +│ 常用模板: │ +│ [测试覆盖不足] [业务逻辑调整] [代码质量] │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ [文本输入框] │ │ +│ │ │ │ +│ │ │ │ +│ └─────────────────────────────────────┘ │ +│ │ +│ [取消] [❌ 确认拒绝] │ +└─────────────────────────────────────────┘ +``` + +--- + +## 🔍 技术细节 + +### 状态流转 + +``` +【合并】 +PENDING_FIX → 点击"批准并合并" → 调用 Gitea API merge +→ 更新 Bug 状态 FIXED → 设置 merged_at + +【拒绝】 +PENDING_FIX → 点击"拒绝修复" → 添加评论到 PR +→ 调用 Gitea API close → 更新 Bug 状态 PENDING_FIX +→ rejection_count +1 → 记录 rejection_reason +→ Agent 检测到 PENDING_FIX + 有 rejection_reason +→ 结合原因重新修复 +``` + +### 安全性 + +1. **权限控制**:需要有效的 Gitea Token +2. **状态检查**:只有 `PENDING_FIX` 状态的 Bug 才能操作 +3. **PR 验证**:必须有 `pr_url` 才能操作 +4. **错误处理**:API 失败会显示详细错误信息 + +--- + +## ❓ FAQ + +### Q1: 按钮显示条件? + +**A:** 只有同时满足以下条件才显示: +1. Bug 有 `pr_url`(Agent 创建了 PR) +2. Bug 状态为 `PENDING_FIX` + +### Q2: 拒绝后 Agent 会立即修复吗? + +**A:** 取决于 Agent 扫描频率: +- 如果 Agent 定时扫描(如每小时),需等待下次扫描 +- 可以手动触发 Agent 立即扫描特定项目 + +### Q3: 合并失败怎么办? + +**A:** 常见失败原因: +1. Token 权限不足 → 检查 Token 权限 +2. PR 有冲突 → 需要手动在 Gitea 解决冲突 +3. PR 已关闭 → 检查 PR 状态 + +### Q4: 能否批量操作? + +**A:** 当前版本不支持批量操作,计划在下个版本添加。 + +--- + +## 🎯 后续优化 + +### Phase 2(可选) + +1. **批量操作**: + - 批量合并多个 PR + - 批量拒绝 + +2. **通知系统**: + - 邮件通知审核结果 + - 钉钉/企业微信通知 + +3. **审批流程**: + - 指定审批人 + - 需要多人审批 + - 审批历史记录 + +4. **统计面板**: + - PR 合并率 + - 平均拒绝次数 + - 审核耗时 + +--- + +## ✅ 完成清单 + +- [x] 数据库字段扩展 +- [x] Gitea API 客户端 +- [x] 后端 API 接口 +- [x] 前端界面实现 +- [x] 拒绝原因模态框 +- [x] 环境变量配置 +- [x] 文档编写 + +**状态**: ✅ 功能已完整实现,可以部署使用! diff --git a/docs/bug-repair-workflow-optimization.md b/docs/bug-repair-workflow-optimization.md new file mode 100644 index 0000000..bf59451 --- /dev/null +++ b/docs/bug-repair-workflow-optimization.md @@ -0,0 +1,1020 @@ +# Bug 自动修复工作流优化方案 + +> 文档版本: v1.0 +> 创建日期: 2026-02-25 +> 状态: 待实施 + +--- + +## 📋 目录 + +- [背景](#背景) +- [当前问题](#当前问题) +- [优化方案](#优化方案) +- [状态设计](#状态设计) +- [完整工作流程](#完整工作流程) +- [Web 界面设计](#web-界面设计) +- [技术实施](#技术实施) +- [实施计划](#实施计划) +- [决策清单](#决策清单) + +--- + +## 背景 + +### 现状 + +当前 Repair Agent 实现了 Bug 的自动修复功能,但存在以下问题: + +1. **Git 工作流不规范**:修复后直接在本地合并到 main 分支,无法在 Gitea 追溯 +2. **缺少人工审核**:AI 修复的代码直接进入主分支,存在风险 +3. **状态过于复杂**:9 个状态流转复杂,难以维护 +4. **Web 界面功能缺失**:无法在日志中台操作 PR 合并 + +### 目标 + +- ✅ 规范 Git 工作流,通过 PR 合并代码 +- ✅ 增加人工审核环节,保障代码质量 +- ✅ 简化 Bug 状态,优化状态流转 +- ✅ 完善 Web 界面,提供便捷的操作入口 + +--- + +## 当前问题 + +### 问题 1:本地合并无法追溯 + +**当前流程:** +```bash +git checkout main +git merge fix/auto-xxx # ← 本地合并 +git push origin main +git branch -d fix/auto-xxx +``` + +**问题:** +- ❌ Gitea 仓库里没有 PR 记录 +- ❌ 无法在 Gitea UI 追溯这个修复来自哪个分支 +- ❌ 团队成员不知道这个变更是怎么来的 +- ❌ 无法进行代码审核和讨论 + +### 问题 2:状态过多且流转复杂 + +**当前状态(9 个):** +```python +NEW → VERIFYING → CANNOT_REPRODUCE + ↓ +PENDING_FIX → FIXING → FIXED → VERIFIED → DEPLOYED + ↓ + FIX_FAILED +``` + +**问题:** +- ❌ `VERIFYING` 和 `PENDING_FIX` 功能重叠 +- ❌ `FIXED` 和 `VERIFIED` 区分不明确 +- ❌ `CANNOT_REPRODUCE` 和 `FIX_FAILED` 都表示失败 + +### 问题 3:缺少人工审核环节 + +**当前流程:** +``` +AI 修复 → commit → push → 本地 merge → push main +``` + +- ❌ 没有审核环节 +- ❌ AI 改错了直接污染主分支 +- ❌ 无法事前检查代码质量 + +--- + +## 优化方案 + +### 核心改进 + +1. **通过 PR 合并代码**:AI 修复后自动创建 PR,保留审核环节 +2. **简化 Bug 状态**:从 9 个减少到 6 个,流转更清晰 +3. **完善 Web 界面**:提供 PR 审核入口 + +### 关键对比 + +| 维度 | 当前方式(本地合并) | 优化方式(PR 合并) | +|------|---------------------|-------------------| +| **追溯性** | ❌ 无 PR 记录 | ✅ Gitea 有完整记录 | +| **审核** | ❌ 无审核环节 | ✅ 人工审核 | +| **安全性** | ⚠️ 直接影响 main | ✅ 合并前可检查 | +| **协作** | ❌ 团队不可见 | ✅ Gitea UI 可见 | +| **回滚** | 需要 revert | 关闭 PR 即可 | + +--- + +## 状态设计 + +### 优化后的状态(6 个) + +```python +class BugStatus(str, Enum): + """优化后的 Bug 状态""" + NEW = "NEW" # 新发现的 Bug + FIXING = "FIXING" # AI 正在修复 + PENDING_REVIEW = "PENDING_REVIEW" # 已修复,等待人工审核(有 PR) + MERGED = "MERGED" # PR 已合并到 main 分支 + DEPLOYED = "DEPLOYED" # 已部署到生产环境 + FAILED = "FAILED" # 修复失败或无法修复 +``` + +### 状态流转图 + +``` +┌─────┐ Agent扫描 ┌────────┐ AI修复成功 ┌──────────────┐ +│ NEW │ ───────────▶ │ FIXING │ ────────────▶ │PENDING_REVIEW│ +└─────┘ └────────┘ └──────────────┘ + │ │ + AI修复失败 人工审核合并 + ↓ ↓ + ┌────────┐ ┌────────┐ + │ FAILED │ │ MERGED │ + └────────┘ └────────┘ + │ + CI/CD部署 + ↓ + ┌──────────┐ + │ DEPLOYED │ + └──────────┘ +``` + +### 状态说明 + +| 状态 | 说明 | 触发条件 | 后续操作 | +|------|------|---------|---------| +| **NEW** | 新发现的 Bug | Log Center 接收到错误日志 | Agent 定时扫描并修复 | +| **FIXING** | AI 正在修复 | Agent 开始执行修复任务 | AI 修复完成或失败 | +| **PENDING_REVIEW** | 等待审核 | AI 修复成功,PR 已创建 | 人工在 Gitea 审核 | +| **MERGED** | 已合并 | PR 在 Gitea 被合并 | CI/CD 自动部署 | +| **DEPLOYED** | 已部署 | 部署到生产环境成功 | 结束 | +| **FAILED** | 修复失败 | AI 无法修复或测试失败 | 人工介入 | + +--- + +## 完整工作流程 + +### 阶段 1:自动修复(Repair Agent) + +```python +1. 扫描 NEW 状态的 Bug +2. 更新状态: NEW → FIXING +3. 调用 Claude Code CLI 修复代码 +4. 运行测试验证修复 +5. commit + push fix 分支到远程 +6. 调用 Gitea API 创建 PR +7. 更新状态: FIXING → PENDING_REVIEW +8. 上传修复报告(包含 PR 链接) +``` + +**关键数据结构:** +```json +{ + "bug_id": 123, + "status": "PENDING_REVIEW", + "repair_report": { + "pr_number": 45, + "pr_url": "https://gitea.xxx/owner/repo/pulls/45", + "branch_name": "fix/auto-20260225-1430", + "modified_files": ["app/views.py", "app/models.py"], + "diff": "...", + "test_passed": true, + "test_output": "..." + } +} +``` + +--- + +### 阶段 2:人工审核(Web 界面) + +#### 方案 A:跳转到 Gitea 审核(推荐) + +**优势:** +- ✅ 实现简单,只需提供跳转链接 +- ✅ 利用 Gitea 原生 PR 功能 +- ✅ 支持完整的代码审核流程 + +**用户操作流程:** +``` +1. 打开日志中台 Bug 列表 +2. 筛选 "PENDING_REVIEW" 状态 +3. 点击 Bug 查看详情 +4. 查看修复报告(AI 分析、修改文件、测试结果) +5. 点击 "前往 Gitea 审核 PR" 按钮 → 新标签页打开 +6. 在 Gitea 查看完整 diff +7. 确认无误 → 点击 "Merge" 按钮 + 或发现问题 → 点击 "Close" 并标记为 FAILED +``` + +#### 方案 B:在日志中台内审核(高级) + +**优势:** +- ✅ 无需跳转,统一操作界面 +- ✅ 可定制审核流程 + +**劣势:** +- ⚠️ 需要开发 diff viewer +- ⚠️ 需要对接 Gitea API + +**用户操作流程:** +``` +1. 在日志中台 Bug 详情页 +2. 查看嵌入的代码 diff +3. 点击 "批准并合并" 按钮 +4. 后端调用 Gitea API 合并 PR +``` + +--- + +### 阶段 3:自动部署(CI/CD + Webhook) + +#### 3.1 Gitea Webhook 通知 + +**Gitea 配置:** +``` +Settings → Webhooks → Add Webhook +- URL: https://your-log-center.com/api/webhooks/gitea +- Events: Pull Requests (merged) +- Secret: <配置密钥> +``` + +**日志中台接收:** +```python +@router.post("/webhooks/gitea") +async def gitea_webhook(payload: dict): + """接收 Gitea PR 合并事件""" + if payload["action"] == "merged": + pr_number = payload["pull_request"]["number"] + + # 通过 PR 号找到对应的 Bug + bug = await find_bug_by_pr_number(pr_number) + + if bug and bug.status == BugStatus.PENDING_REVIEW: + # 更新状态: PENDING_REVIEW → MERGED + await update_bug_status(bug.id, BugStatus.MERGED) + logger.info(f"Bug #{bug.id} PR 已合并,等待部署") +``` + +#### 3.2 部署成功通知 + +**CI/CD Pipeline 配置:** +```yaml +# .gitlab-ci.yml 或 Jenkinsfile +deploy: + stage: deploy + script: + - deploy_to_production.sh + - | + # 部署成功后通知日志中台 + curl -X POST https://your-log-center.com/api/webhooks/deployment \ + -H "Content-Type: application/json" \ + -H "X-Secret: ${WEBHOOK_SECRET}" \ + -d '{ + "project_id": "rtc_backend", + "status": "success", + "commit": "'$CI_COMMIT_SHA'" + }' +``` + +**日志中台更新状态:** +```python +@router.post("/webhooks/deployment") +async def deployment_webhook(payload: dict): + """接收部署成功通知""" + if payload["status"] == "success": + commit = payload["commit"] + + # 找到该 commit 关联的 Bug(MERGED 状态) + bugs = await find_bugs_by_commit(commit, status=BugStatus.MERGED) + + for bug in bugs: + # 更新状态: MERGED → DEPLOYED + await update_bug_status(bug.id, BugStatus.DEPLOYED) + logger.info(f"Bug #{bug.id} 已部署到生产环境") +``` + +--- + +## Web 界面设计 + +### 1. Bug 列表页 + +``` +┌────────────────────────────────────────────────────────────┐ +│ Bug 自动修复看板 🔍 [搜索框] [筛选▼] │ +├────────────────────────────────────────────────────────────┤ +│ │ +│ 📊 今日统计 │ +│ ┌──────────┬──────────┬──────────┬──────────┬──────────┐ │ +│ │ 🆕 新发现 │ 🔧 修复中 │ 🟡 待审核 │ ✅ 已合并 │ 🚀 已部署 │ │ +│ │ 12 │ 3 │ 5 │ 8 │ 15 │ │ +│ └──────────┴──────────┴──────────┴──────────┴──────────┘ │ +│ │ +│ 状态筛选: [ 全部 ] [ 🟡 待审核 ] [ 🔧 修复中 ] [ ❌ 失败 ] │ +│ │ +│ 🟡 待审核 (需要你的操作) │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ #123 │ TypeError in user_login │ 🟡 待审核 │ 详情 │ │ +│ │ │ rtc_backend | 2h ago │ │ → │ │ +│ ├──────┼──────────────────────────────┼──────────┼─────┤ │ +│ │ #124 │ NullPointerException │ 🟡 待审核 │ 详情 │ │ +│ │ │ rtc_backend | 1h ago │ │ → │ │ +│ ├──────┼──────────────────────────────┼──────────┼─────┤ │ +│ │ #125 │ DB connection timeout │ 🟡 待审核 │ 详情 │ │ +│ │ │ rtc_web | 30m ago │ │ → │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +│ 🔧 修复中 │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ #126 │ API endpoint error │ 🔧 修复中 │ ... │ │ +│ └──────────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────────────┘ +``` + +--- + +### 2. Bug 详情页(方案 A:跳转到 Gitea) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ ← 返回列表 Bug #123 │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ 📌 基本信息 │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ 标题: TypeError: 'NoneType' object is not iterable │ │ +│ │ 项目: rtc_backend │ │ +│ │ 状态: 🟡 PENDING_REVIEW (待审核) │ │ +│ │ 环境: production │ │ +│ │ 发现时间: 2026-02-25 12:30:15 │ │ +│ │ 修复时间: 2026-02-25 14:30:45 │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ │ +│ 🐛 错误详情 │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ 文件: app/views.py │ │ +│ │ 行号: 24 │ │ +│ │ 错误: TypeError: 'NoneType' object is not iterable │ │ +│ │ │ │ +│ │ 堆栈: │ │ +│ │ File "app/views.py", line 24, in user_login │ │ +│ │ for item in request.user: │ │ +│ │ TypeError: 'NoneType' object is not iterable │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ │ +│ 📋 修复报告 │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ AI 分析: │ │ +│ │ request.user 可能为 None,需要添加 null 检查 │ │ +│ │ │ │ +│ │ 修改文件: 2 个 │ │ +│ │ - app/views.py (+3, -1) │ │ +│ │ - tests/test_views.py (+12, -0) │ │ +│ │ │ │ +│ │ 测试结果: ✅ 通过 (2 tests passed) │ │ +│ │ │ │ +│ │ 修复分支: fix/auto-20260225-1430 │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ │ +│ 🔗 Pull Request │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ PR #45: fix: auto repair bug #123 │ │ +│ │ fix/auto-20260225-1430 → main │ │ +│ │ │ │ +│ │ 修改: 2 files changed (+15, -1) │ │ +│ │ 状态: 🟡 Open (等待审核) │ │ +│ │ │ │ +│ │ ┌──────────────────────────────┐ │ │ +│ │ │ 🔍 前往 Gitea 审核 PR → │ ← 主要操作 │ │ +│ │ └──────────────────────────────┘ │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ │ +│ 📄 代码变更预览 │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ 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 │ │ +│ │ │ │ +│ │ [查看完整 diff →] │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ │ +│ 📝 操作日志 │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ 2026-02-25 14:30 状态更新: FIXING → PENDING_REVIEW │ │ +│ │ 2026-02-25 14:28 AI 修复完成,PR 已创建 │ │ +│ │ 2026-02-25 14:25 开始自动修复 │ │ +│ │ 2026-02-25 12:30 Bug 已记录 │ │ +│ └───────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +### 3. 统计看板 + +``` +┌────────────────────────────────────────────────────────────┐ +│ 📊 Bug 修复统计 时间范围: [最近 7 天 ▼]│ +├────────────────────────────────────────────────────────────┤ +│ │ +│ 整体指标 │ +│ ┌──────────┬──────────┬──────────┬──────────┐ │ +│ │ 总计发现 │ 自动修复 │ 修复成功率│ 平均耗时 │ │ +│ │ 156 │ 142 │ 91.0% │ 12 min │ │ +│ └──────────┴──────────┴──────────┴──────────┘ │ +│ │ +│ 每日趋势 │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ 40 ┤ │ │ +│ │ 30 ┤ ● ● │ │ +│ │ 20 ┤ ● ● ● ● │ │ +│ │ 10 ┤ ● ● ● ● ● ● │ │ +│ │ 0 └────────────────────────────────────── │ │ +│ │ Mon Tue Wed Thu Fri Sat Sun │ │ +│ │ │ │ +│ │ ■ 新发现 ■ 自动修复 ■ 修复失败 │ │ +│ └─────────────────────────────────────────────────┘ │ +│ │ +│ 项目分布 │ +│ ┌──────────────┬───────────┬───────────┬─────────┐ │ +│ │ 项目 │ 待审核 │ 已合并 │ 失败 │ │ +│ ├──────────────┼───────────┼───────────┼─────────┤ │ +│ │ rtc_backend │ 3 │ 12 │ 1 │ │ +│ │ rtc_web │ 2 │ 8 │ 0 │ │ +│ │ airhub_app │ 0 │ 5 │ 2 │ │ +│ └──────────────┴───────────┴───────────┴─────────┘ │ +└────────────────────────────────────────────────────────────┘ +``` + +--- + +## 技术实施 + +### 修改清单 + +#### 1. Repair Agent 端 + +**文件:`log_center/repair_agent/agent/git_manager.py`** + +```python +def create_pull_request( + self, + title: str, + description: str, + gitea_url: str, + gitea_token: str, +) -> tuple[bool, str]: + """ + 调用 Gitea API 创建 Pull Request + + Args: + title: PR 标题 + description: PR 描述 + gitea_url: Gitea 服务器地址 + gitea_token: Gitea API Token + + Returns: + (是否成功, PR URL 或错误信息) + """ + if not self.repo or not self.github_repo: + return False, "未配置 Git 仓库" + + try: + # 解析仓库信息 + # github_repo 格式: https://gitea.xxx/owner/repo.git + import re + match = re.search(r'([^/]+)/([^/]+?)(?:\.git)?$', self.github_repo) + if not match: + return False, "无法解析仓库信息" + + owner, repo = match.groups() + current_branch = self.repo.active_branch.name + base_branch = "main" if "main" in self.repo.heads else "master" + + # 调用 Gitea API + import httpx + api_url = f"{gitea_url}/api/v1/repos/{owner}/{repo}/pulls" + headers = { + "Authorization": f"token {gitea_token}", + "Content-Type": "application/json", + } + payload = { + "title": title, + "head": current_branch, + "base": base_branch, + "body": description, + } + + response = httpx.post(api_url, json=payload, headers=headers, timeout=30) + response.raise_for_status() + + data = response.json() + pr_url = data.get("html_url", "") + pr_number = data.get("number", 0) + + logger.info(f"PR 创建成功: #{pr_number} - {pr_url}") + return True, pr_url + + except Exception as e: + logger.error(f"创建 PR 失败: {e}") + return False, str(e) +``` + +**文件:`log_center/repair_agent/agent/core.py`** + +修改自动提交部分(Line 256-267): + +```python +# 自动提交并创建 PR(仅在 Git 启用时) +if git_enabled and auto_commit and modified_files and git_manager: + bug_ids = ", ".join([f"#{b.id}" for b in bugs]) + commit_msg = f"fix: auto repair bugs {bug_ids}" + + # Step 1: commit 代码 + git_manager.commit(commit_msg) + logger.info("代码已提交") + + # Step 2: push fix 分支 + git_manager.push() + logger.info("fix 分支已推送") + + # Step 3: 创建 PR(替代原来的 merge_to_main_and_push) + success, pr_url = git_manager.create_pull_request( + title=commit_msg, + description=self._generate_pr_description(bugs, output, modified_files), + gitea_url=settings.gitea_url, + gitea_token=settings.gitea_token, + ) + + if success: + logger.info(f"PR 已创建: {pr_url}") + # 更新修复报告,添加 PR 信息 + for bug in bugs: + self._update_pr_info(bug.id, pr_url) + else: + logger.warning(f"PR 创建失败: {pr_url},请手动创建") +elif not git_enabled and auto_commit: + logger.info("未配置 GitHub 仓库,跳过自动提交") +``` + +新增辅助方法: + +```python +def _generate_pr_description( + self, + bugs: list[Bug], + ai_output: str, + modified_files: list[str], +) -> str: + """生成 PR 描述""" + lines = [ + f"## 🤖 AI 自动修复", + "", + f"本 PR 修复了 {len(bugs)} 个 Bug:", + "", + ] + + for bug in bugs: + lines.append(f"- Bug #{bug.id}: {bug.error.type} - {bug.error.message[:50]}") + + lines.extend([ + "", + "## 📝 修复说明", + "", + ai_output[:500], # 截取前 500 字符 + "", + "## 📄 修改文件", + "", + ]) + + for file in modified_files[:10]: # 最多显示 10 个文件 + lines.append(f"- `{file}`") + + lines.extend([ + "", + "---", + "", + "⚠️ **请仔细审核代码变更后再合并**", + ]) + + return "\n".join(lines) + +def _update_pr_info(self, bug_id: int, pr_url: str): + """更新 Bug 的 PR 信息""" + try: + # 从 PR URL 提取 PR 号 + import re + match = re.search(r'/pulls/(\d+)', pr_url) + pr_number = int(match.group(1)) if match else 0 + + # 调用 TaskManager 更新 + self.task_manager.update_pr_info(bug_id, pr_number, pr_url) + except Exception as e: + logger.error(f"更新 PR 信息失败: {e}") +``` + +**文件:`log_center/repair_agent/models/bug.py`** + +简化状态枚举: + +```python +class BugStatus(str, Enum): + """Bug 状态""" + NEW = "NEW" # 新发现 + FIXING = "FIXING" # 修复中 + PENDING_REVIEW = "PENDING_REVIEW" # 待审核(有 PR) + MERGED = "MERGED" # 已合并 + DEPLOYED = "DEPLOYED" # 已部署 + FAILED = "FAILED" # 失败 +``` + +**文件:`log_center/repair_agent/config/settings.py`** + +新增配置项: + +```python +class Settings(BaseSettings): + # ... 现有配置 ... + + # Gitea 配置 + gitea_url: str = Field( + default="https://gitea.airlabs.art", + description="Gitea 服务器地址" + ) + gitea_token: str = Field( + default="", + description="Gitea API Token" + ) + + # PR 配置 + auto_create_pr: bool = Field( + default=True, + description="是否自动创建 PR" + ) +``` + +--- + +#### 2. Log Center 端 + +**文件:`log_center/app/models.py`** + +更新 Bug 模型: + +```python +class ErrorLog(Base): + # ... 现有字段 ... + + # 新增字段 + pr_number = Column(Integer, nullable=True, comment="PR 编号") + pr_url = Column(String(500), nullable=True, comment="PR 链接") + branch_name = Column(String(200), nullable=True, comment="修复分支名") +``` + +**文件:`log_center/app/api/webhooks.py`** + +新增 Gitea Webhook 接收端点: + +```python +@router.post("/webhooks/gitea") +async def gitea_webhook( + request: Request, + db: Session = Depends(get_db), +): + """接收 Gitea PR 合并事件""" + try: + payload = await request.json() + + # 验证签名(可选) + # verify_gitea_signature(request.headers, payload) + + action = payload.get("action") + + if action == "merged": + # PR 已合并 + pr_number = payload["pull_request"]["number"] + repo_name = payload["repository"]["name"] + + # 通过 PR 号和项目名找到对应的 Bug + bug = db.query(ErrorLog).filter( + ErrorLog.pr_number == pr_number, + ErrorLog.project_id.like(f"%{repo_name}%"), + ErrorLog.status == BugStatus.PENDING_REVIEW.value, + ).first() + + if bug: + # 更新状态 + bug.status = BugStatus.MERGED.value + bug.updated_at = datetime.now() + db.commit() + + logger.info(f"Bug #{bug.id} PR #{pr_number} 已合并") + + return {"message": "Status updated"} + + return {"message": "Event ignored"} + + except Exception as e: + logger.error(f"处理 Gitea Webhook 失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/webhooks/deployment") +async def deployment_webhook( + payload: dict, + db: Session = Depends(get_db), +): + """接收部署成功通知""" + try: + if payload.get("status") == "success": + commit_sha = payload["commit"] + project_id = payload["project_id"] + + # 找到该 commit 关联的 Bug(MERGED 状态) + bugs = db.query(ErrorLog).filter( + ErrorLog.project_id == project_id, + ErrorLog.status == BugStatus.MERGED.value, + ).all() + + # TODO: 通过 commit SHA 精确匹配(需要记录 commit) + + for bug in bugs: + bug.status = BugStatus.DEPLOYED.value + bug.deployed_at = datetime.now() + + db.commit() + logger.info(f"已更新 {len(bugs)} 个 Bug 状态为 DEPLOYED") + + return {"message": f"Updated {len(bugs)} bugs"} + + return {"message": "Not a success deployment"} + + except Exception as e: + logger.error(f"处理部署 Webhook 失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) +``` + +**文件:`log_center/app/api/tasks.py`** + +新增接口: + +```python +@router.put("/bugs/{bug_id}/pr-info") +async def update_pr_info( + bug_id: int, + pr_number: int, + pr_url: str, + db: Session = Depends(get_db), +): + """更新 Bug 的 PR 信息""" + bug = db.query(ErrorLog).filter(ErrorLog.id == bug_id).first() + if not bug: + raise HTTPException(status_code=404, detail="Bug not found") + + bug.pr_number = pr_number + bug.pr_url = pr_url + db.commit() + + return {"message": "PR info updated"} +``` + +**文件:`log_center/web/src/pages/BugDetail.tsx`** + +添加 PR 信息展示: + +```tsx +{bug.status === 'PENDING_REVIEW' && bug.pr_url && ( + + +
+ 待审核 + PR #{bug.pr_number}: {bug.branch_name} → main +
+ + + + +
+
+)} +``` + +--- + +## 实施计划 + +### Phase 1:核心功能(必须实施) + +**时间:2-3 天** + +#### 任务清单 + +- [ ] **Repair Agent 端** + - [ ] 新增 `create_pull_request()` 方法 + - [ ] 修改 `fix_project()` 逻辑,移除 `merge_to_main_and_push()` + - [ ] 状态更新为 `PENDING_REVIEW` + - [ ] 修复报告包含 PR 信息 + - [ ] 配置文件添加 Gitea 相关参数 + +- [ ] **Log Center 端** + - [ ] 数据库添加 `pr_number`, `pr_url`, `branch_name` 字段 + - [ ] 简化 Bug 状态枚举(6 个) + - [ ] 新增 Gitea Webhook 接收端点 + - [ ] Bug 详情页显示 PR 信息 + - [ ] "前往 Gitea 审核" 按钮 + +- [ ] **Gitea 配置** + - [ ] 创建 API Token + - [ ] 配置 Webhook(PR 合并事件) + +- [ ] **测试验证** + - [ ] 端到端测试完整流程 + - [ ] 验证 PR 创建成功 + - [ ] 验证状态自动更新 + +#### 预期效果 + +``` +NEW → FIXING → PENDING_REVIEW (有 PR) → [人工审核] → MERGED +``` + +--- + +### Phase 2:优化体验(可选,后续迭代) + +**时间:1 周** + +#### 任务清单 + +- [ ] **Web 界面优化** + - [ ] 嵌入 diff viewer 组件 + - [ ] 在日志中台直接批准/拒绝 PR + - [ ] 批量审核功能 + - [ ] 统计看板优化 + +- [ ] **通知系统** + - [ ] 邮件通知(PR 创建、合并) + - [ ] 钉钉/企业微信通知 + - [ ] 消息中心 + +- [ ] **高级功能** + - [ ] PR 自动审批(测试通过 + 低风险) + - [ ] 审批流程配置(指定审批人) + - [ ] PR 评论同步到日志中台 + +--- + +## 决策清单 + +### 需要确认的事项 + +- [ ] **1. 状态简化方案** + - ✅ 采用 6 个状态(NEW, FIXING, PENDING_REVIEW, MERGED, DEPLOYED, FAILED) + - ❌ 保留现有 9 个状态 + +- [ ] **2. 审核方式** + - ✅ **方案 A**:跳转到 Gitea 审核(推荐,简单快速) + - ❌ **方案 B**:在日志中台内审核(高级,需要更多开发) + +- [ ] **3. 实施阶段** + - ✅ **Phase 1**:核心功能(必须,2-3 天) + - ⚠️ **Phase 2**:优化体验(可选,1 周) + +- [ ] **4. Gitea 配置信息** + - [ ] Gitea 服务器地址:`https://gitea.xxx.com` + - [ ] Gitea Token:已生成并配置 + - [ ] Webhook Secret:已配置 + +--- + +## 附录 + +### A. 数据库迁移脚本 + +```python +# alembic/versions/xxx_add_pr_info.py +def upgrade(): + op.add_column('error_logs', sa.Column('pr_number', sa.Integer(), nullable=True)) + op.add_column('error_logs', sa.Column('pr_url', sa.String(500), nullable=True)) + op.add_column('error_logs', sa.Column('branch_name', sa.String(200), nullable=True)) + + # 迁移旧状态到新状态 + op.execute(""" + UPDATE error_logs + SET status = 'FAILED' + WHERE status IN ('FIX_FAILED', 'CANNOT_REPRODUCE') + """) + + op.execute(""" + UPDATE error_logs + SET status = 'NEW' + WHERE status IN ('VERIFYING', 'PENDING_FIX') + """) + +def downgrade(): + op.drop_column('error_logs', 'branch_name') + op.drop_column('error_logs', 'pr_url') + op.drop_column('error_logs', 'pr_number') +``` + +--- + +### B. Gitea Webhook 配置示例 + +**Webhook URL:** +``` +https://your-log-center.com/api/webhooks/gitea +``` + +**Webhook Events:** +- ✅ Pull Requests (merged) + +**Payload 示例:** +```json +{ + "action": "merged", + "number": 45, + "pull_request": { + "id": 45, + "number": 45, + "title": "fix: auto repair bugs #123", + "head": { + "ref": "fix/auto-20260225-1430" + }, + "base": { + "ref": "main" + }, + "merged": true, + "merged_at": "2026-02-25T15:30:00Z" + }, + "repository": { + "name": "rtc_backend", + "full_name": "airlabs/rtc_backend" + } +} +``` + +--- + +### C. CI/CD 部署通知配置 + +**.gitlab-ci.yml 示例:** +```yaml +stages: + - test + - build + - deploy + +deploy:production: + stage: deploy + only: + - main + script: + - echo "Deploying to production..." + - ./deploy.sh + - | + # 通知日志中台部署成功 + curl -X POST https://your-log-center.com/api/webhooks/deployment \ + -H "Content-Type: application/json" \ + -H "X-Secret: ${WEBHOOK_SECRET}" \ + -d '{ + "project_id": "rtc_backend", + "status": "success", + "commit": "'${CI_COMMIT_SHA}'", + "deployed_at": "'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'" + }' +``` + +--- + +## 参考资料 + +- [Gitea API 文档](https://docs.gitea.io/en-us/api-usage/) +- [Gitea Webhooks](https://docs.gitea.io/en-us/webhooks/) +- [Git 工作流最佳实践](https://www.atlassian.com/git/tutorials/comparing-workflows) + +--- + +**文档维护:** +- 创建:2026-02-25 +- 更新:待更新 +- 负责人:待指定 diff --git a/docs/bug-severity-grading-system.md b/docs/bug-severity-grading-system.md new file mode 100644 index 0000000..f9359bc --- /dev/null +++ b/docs/bug-severity-grading-system.md @@ -0,0 +1,671 @@ +# Bug 严重等级分级系统 + +> 文档版本: v1.0 +> 创建日期: 2026-02-25 +> 用途: AI 自动评估 Bug 严重程度,决定是否需要人工审核 + +--- + +## 📊 分级概述 + +### 等级范围 + +**1-10 级**,数字越大表示风险越高 + +| 等级 | 风险级别 | 审核方式 | 典型场景 | +|------|----------|----------|----------| +| **10** | 🔴 极高风险 | 必须人工审核 | 支付、认证、数据泄露 | +| **9** | 🔴 极高风险 | 必须人工审核 | 核心业务逻辑 | +| **8** | 🟠 高风险 | 人工审核(默认阈值) | 复杂业务流程 | +| **7** | 🟠 高风险 | 人工审核 | 数据库操作 | +| **6** | 🟡 中风险 | 自动合并 | 一般业务逻辑 | +| **5** | 🟡 中风险 | 自动合并 | API 参数验证 | +| **4** | 🟢 低风险 | 自动合并 | 非关键功能 | +| **3** | 🟢 低风险 | 自动合并 | UI 显示问题 | +| **2** | 🔵 极低风险 | 自动合并 | 语法错误 | +| **1** | 🔵 极低风险 | 自动合并 | 代码格式 | + +### 默认审核阈值 + +```python +# 配置项 +severity_threshold_for_review: int = 8 # ≥8 级需要人工审核 +``` + +--- + +## 🎯 详细分级标准 + +### Level 10:极高风险(必须人工审核) + +#### 特征 + +**涉及资金安全:** +- 支付、充值、提现相关代码 +- 订单金额计算逻辑 +- 优惠券、折扣计算 +- 余额、积分操作 + +**涉及数据安全:** +- 用户认证(登录、注册、Token) +- 权限控制(admin 权限、角色管理) +- 敏感数据加密/解密 +- 数据库备份/恢复 + +**系统稳定性:** +- 可能导致服务不可用 +- 可能导致系统崩溃 +- 数据库连接池管理 + +#### 示例 + +```python +# ❌ 极高风险:支付金额计算错误 +def calculate_order_price(items): + total = 0 + for item in items: + total += item.price * item.quantity # Bug: 未考虑优惠券 + return total + +# ❌ 极高风险:权限验证绕过 +def admin_required(func): + def wrapper(request): + # Bug: 缺少权限检查 + return func(request) + return wrapper +``` + +#### AI 判定输出示例 + +```json +{ + "severity_level": 10, + "severity_label": "极高风险", + "risk_category": "资金安全", + "reasoning": [ + "涉及订单金额计算逻辑", + "错误可能导致用户支付金额错误", + "修改了 payment_service.py 核心支付模块", + "测试覆盖不足,缺少边界条件测试" + ], + "recommendation": "必须人工审核并进行充分测试后才能上线", + "business_impact": "可能导致财务损失和用户投诉", + "rollback_difficulty": "困难" +} +``` + +--- + +### Level 8-9:高风险(需要人工审核) + +#### 特征 + +**核心业务逻辑:** +- 用户绑定设备流程 +- 智能体(Spirit)创建/修改 +- 设备状态流转(in_stock → out_stock → bound) +- 批次管理逻辑 + +**数据一致性:** +- 数据库事务处理 +- 外键关联操作 +- 批量更新操作 +- 数据迁移脚本 + +**复杂业务流程:** +- 多步骤业务流程 +- 涉及多个模块交互 +- 状态机流转逻辑 + +#### 示例 + +```python +# ⚠️ 高风险:设备绑定逻辑 +def bind_device_to_user(device_id, user_id): + device = Device.objects.get(id=device_id) + # Bug: 未检查设备是否已被绑定 + device.owner_id = user_id + device.status = "bound" + device.save() + +# ⚠️ 高风险:批量更新 +def batch_update_devices(device_ids, **kwargs): + # Bug: 缺少事务保护 + for device_id in device_ids: + device = Device.objects.get(id=device_id) + for key, value in kwargs.items(): + setattr(device, key, value) + device.save() +``` + +#### AI 判定输出示例 + +```json +{ + "severity_level": 8, + "severity_label": "高风险", + "risk_category": "核心业务逻辑", + "reasoning": [ + "修改了设备绑定流程,属于核心业务", + "未充分检查边界条件(设备已绑定的情况)", + "修改涉及 Device 模型的状态流转", + "测试用例覆盖了基本场景,但缺少异常场景测试" + ], + "recommendation": "建议人工审核业务逻辑,并补充异常场景测试", + "business_impact": "可能导致设备重复绑定或状态异常", + "rollback_difficulty": "中等" +} +``` + +--- + +### Level 5-7:中风险(自动合并,但需关注) + +#### 特征 + +**一般业务逻辑:** +- 非核心功能的实现 +- API 参数验证和错误处理 +- 数据查询和过滤逻辑 +- 边界条件处理 + +**API 层问题:** +- 请求参数验证 +- 响应格式化 +- 错误码定义 +- 分页逻辑 + +#### 示例 + +```python +# 🟡 中风险:参数验证 +def get_user_devices(request): + user_id = request.GET.get("user_id") + # Bug: 未验证 user_id 格式 + devices = Device.objects.filter(owner_id=user_id) + return JsonResponse({"devices": list(devices.values())}) + +# 🟡 中风险:错误处理 +def create_spirit(name, avatar): + # Bug: 缺少异常捕获 + spirit = Spirit.objects.create(name=name, avatar=avatar) + return spirit +``` + +#### AI 判定输出示例 + +```json +{ + "severity_level": 6, + "severity_label": "中风险", + "risk_category": "API 参数验证", + "reasoning": [ + "修改了 API 参数验证逻辑", + "未影响核心业务流程", + "测试覆盖充分,包含边界条件", + "修改范围小,仅涉及单个 API 端点" + ], + "recommendation": "可以自动合并,建议后续监控生产环境日志", + "business_impact": "可能导致参数错误时返回不友好的错误信息", + "rollback_difficulty": "容易" +} +``` + +--- + +### Level 3-4:低风险(自动合并) + +#### 特征 + +**UI 和展示:** +- 前端显示问题 +- 错误提示文案 +- 日志输出格式 + +**非关键功能:** +- 辅助工具函数 +- 测试代码修复 +- 文档更新 + +#### 示例 + +```python +# 🟢 低风险:日志输出 +def process_data(data): + # Bug: 日志格式错误 + logger.info(f"Processing data: {data}") # 修复:添加时间戳 + return process(data) + +# 🟢 低风险:错误提示 +def validate_input(value): + if not value: + # Bug: 错误提示不友好 + raise ValueError("Invalid input") # 修复:改为 "请输入有效的值" +``` + +#### AI 判定输出示例 + +```json +{ + "severity_level": 3, + "severity_label": "低风险", + "risk_category": "日志和提示", + "reasoning": [ + "仅修改了日志输出格式", + "不影响业务逻辑", + "不涉及数据操作", + "测试通过" + ], + "recommendation": "可以自动合并,无需人工审核", + "business_impact": "无明显影响", + "rollback_difficulty": "容易" +} +``` + +--- + +### Level 1-2:极低风险(自动合并) + +#### 特征 + +**语法和格式:** +- Import 语句错误 +- 拼写错误 +- 代码格式问题(空格、换行) +- 注释错误 + +**构建和依赖:** +- requirements.txt 更新 +- Dockerfile 格式修复 +- CI/CD 配置优化 + +#### 示例 + +```python +# 🔵 极低风险:Import 错误 +# Bug: from django.contrib.auth import User +from django.contrib.auth.models import User # 修复 + +# 🔵 极低风险:拼写错误 +def get_devcie_list(): # Bug: devcie → device + pass +``` + +#### AI 判定输出示例 + +```json +{ + "severity_level": 1, + "severity_label": "极低风险", + "risk_category": "语法错误", + "reasoning": [ + "仅修复了 import 语句", + "不涉及业务逻辑", + "属于纯语法问题", + "测试全部通过" + ], + "recommendation": "自动合并,无风险", + "business_impact": "无", + "rollback_difficulty": "容易" +} +``` + +--- + +## 🤖 AI 分级判断实现 + +### Claude 评估 Prompt 模板 + +**文件位置:** `log_center/repair_agent/prompts/severity_assessment.txt` + +```markdown +你是一个 Bug 严重等级评估专家。请根据以下信息评估 Bug 的严重等级(1-10 级)。 + +## Bug 信息 + +**错误类型:** {error_type} +**错误消息:** {error_message} +**文件路径:** {file_path} +**堆栈信息:** +``` +{stack_trace} +``` + +**修复内容:** +- 修改文件: {modified_files} +- 代码 Diff: +```diff +{diff} +``` + +**测试结果:** {test_passed} + +## 评估标准 + +### Level 10(极高风险) +- 涉及支付、充值、提现等资金交易 +- 涉及用户认证、权限控制的核心逻辑 +- 可能导致数据泄露或安全漏洞 +- 可能导致系统崩溃或服务不可用 +- **关键词:** payment, auth, security, admin, permission, password, token, encrypt, decrypt + +### Level 8-9(高风险) +- 核心业务逻辑错误(设备绑定、智能体管理) +- 数据库事务处理错误 +- 复杂业务流程错误 +- 影响多个模块的错误 +- **关键词:** bind, device, spirit, transaction, batch, workflow, state_machine + +### Level 5-7(中风险) +- 一般业务逻辑错误 +- API 参数验证错误 +- 非关键功能的错误 +- **关键词:** validate, filter, query, api, endpoint + +### Level 3-4(低风险) +- UI 显示问题 +- 日志输出错误 +- 测试代码错误 +- **关键词:** ui, display, log, test, format + +### Level 1-2(极低风险) +- 语法错误(import、拼写) +- 代码格式问题 +- 注释错误 +- **关键词:** import, syntax, format, comment, typo + +## 判断依据 + +请综合考虑以下因素: + +1. **业务重要性** (权重 30%) + - 是否涉及核心业务流程?(设备管理、智能体、用户绑定) + - 是否影响用户核心功能? + +2. **安全风险** (权重 30%) + - 是否涉及敏感数据?(用户信息、Token) + - 是否涉及资金相关?(虽然当前项目可能不涉及) + - 是否可能导致权限绕过? + +3. **影响范围** (权重 20%) + - 影响多少用户?(全部/部分/极少数) + - 影响多少模块?(单模块/多模块) + - 是否影响数据一致性? + +4. **修复质量** (权重 10%) + - 测试是否充分? + - 修复方案是否合理? + - 是否引入新的风险? + +5. **回滚难度** (权重 10%) + - 如果出错,回滚是否容易? + - 是否涉及数据迁移? + +## 输出格式 + +请严格按照以下 JSON 格式输出(在最后一行): + +```json +{ + "severity_level": <1-10 的整数>, + "severity_label": "<极低风险|低风险|中风险|高风险|极高风险>", + "risk_category": "<资金安全|数据安全|核心业务逻辑|一般业务|UI展示|语法问题|其他>", + "reasoning": [ + "<理由1:为什么判定为该级别>", + "<理由2:关键风险点>", + "<理由3:测试覆盖情况>", + "<理由4:建议>" + ], + "recommendation": "<是否建议人工审核的具体说明>", + "key_files": ["<核心修改文件列表>"], + "business_impact": "<对业务的影响描述,50字以内>", + "rollback_difficulty": "<容易|中等|困难>" +} +``` + +**重要提示:** +- 如果涉及 `payment`, `auth`, `admin`, `permission` 等关键词,最低 8 级 +- 如果修改了 `models.py` 且涉及核心业务模型(Device, Spirit, User),最低 7 级 +- 如果测试失败或测试覆盖不足,等级 +1 +- 如果只是语法错误(ImportError, SyntaxError)且测试通过,最高 2 级 + +请立即开始评估,最后一行输出完整的 JSON。 +``` + +--- + +## 💻 完整技术实现代码 + +### 文件清单 + +``` +log_center/repair_agent/ +├── agent/ +│ ├── claude_service.py # 新增 assess_bug_severity() 方法 +│ ├── core.py # 修改修复流程,集成分级判断 +│ └── git_manager.py # 新增 merge_pull_request() 方法 +├── models/ +│ └── bug.py # RepairReport 增加严重等级字段 +├── config/ +│ └── settings.py # 新增分级相关配置 +└── prompts/ + └── severity_assessment.txt # Claude 评估 Prompt +``` + +### 详细代码实现 + +见主文档:[bug-repair-workflow-optimization.md](./bug-repair-workflow-optimization.md) 中的 "技术实施" 章节 + +--- + +## 📊 修复报告示例 + +### 高风险 Bug 修复报告 + +```json +{ + "error_log_id": 123, + "project_id": "rtc_backend", + "status": "PENDING_REVIEW", + "severity_level": 9, + "severity_label": "高风险", + "risk_category": "核心业务逻辑", + "severity_reasoning": [ + "修改了设备绑定流程 (bind_device_to_user),属于核心业务", + "涉及 Device 模型的状态流转 (in_stock → bound)", + "未充分检查边界条件(设备已被其他用户绑定的情况)", + "测试覆盖了基本场景,但缺少冲突场景测试" + ], + "recommendation": "建议人工审核业务逻辑,补充异常场景测试后再合并", + "business_impact": "可能导致设备重复绑定,影响用户使用", + "rollback_difficulty": "中等", + "modified_files": ["apps/device/views.py", "apps/device/models.py"], + "test_passed": true, + "pr_url": "https://gitea.xxx/rtc/rtc_backend/pulls/45" +} +``` + +### 低风险 Bug 修复报告 + +```json +{ + "error_log_id": 124, + "project_id": "rtc_backend", + "status": "MERGED", + "severity_level": 2, + "severity_label": "极低风险", + "risk_category": "语法问题", + "severity_reasoning": [ + "仅修复了 import 语句路径错误", + "不涉及任何业务逻辑", + "属于纯语法问题 (ImportError)", + "测试全部通过" + ], + "recommendation": "可以自动合并,无需人工审核", + "business_impact": "无", + "rollback_difficulty": "容易", + "modified_files": ["apps/device/serializers.py"], + "test_passed": true, + "pr_url": "https://gitea.xxx/rtc/rtc_backend/pulls/46", + "auto_merged": true +} +``` + +--- + +## 🔄 完整工作流程 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 1. Agent 扫描发现 Bug │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ 2. AI 修复代码 (Claude Code CLI) │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ 3. 运行测试 │ +│ ├─ 测试通过 → 继续 │ +│ └─ 测试失败 → 下一轮重试 │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ 4. AI 评估严重等级 (1-10) │ +│ - 分析错误类型 │ +│ - 评估业务影响 │ +│ - 检查修改范围 │ +│ - 综合判定等级 │ +└─────────────────────────────────────────────────────────┘ + ↓ + 判定等级? + ↙ ↘ + ┌──────────────┐ ┌──────────────┐ + │ ≥8 级(高风险)│ │ <8 级(低风险)│ + └──────────────┘ └──────────────┘ + ↓ ↓ + ┌──────────────┐ ┌──────────────┐ + │ 创建 PR │ │ 创建 PR │ + │ 标注高风险 │ │ 标注低风险 │ + └──────────────┘ └──────────────┘ + ↓ ↓ + ┌──────────────┐ ┌──────────────┐ + │ 状态 → │ │ 自动合并 PR │ + │ PENDING_REVIEW│ └──────────────┘ + └──────────────┘ ↓ + ↓ ┌──────────────┐ + ┌──────────────┐ │ 状态 → MERGED │ + │ 等待人工审核 │ └──────────────┘ + └──────────────┘ ↓ + ↓ │ + ┌──────────────┐ │ + │ 人工点击 Merge│ │ + └──────────────┘ │ + ↓ │ + ┌──────────────┐ │ + │ 状态 → MERGED │◀────────────────┘ + └──────────────┘ + ↓ + ┌──────────────┐ + │ CI/CD 部署 │ + └──────────────┘ + ↓ + ┌──────────────┐ + │状态 → DEPLOYED│ + └──────────────┘ +``` + +--- + +## 📈 预期效果 + +### 效率提升 + +| 场景 | 当前方式 | 优化后 | +|------|----------|--------| +| **低风险 Bug (1-7级)** | 每个都需人工审核 | 自动合并,秒级完成 | +| **高风险 Bug (8-10级)** | 人工审核 | 人工审核(保持不变) | +| **整体效率** | 100% 人工介入 | 70% 自动处理 + 30% 人工审核 | + +### 预估比例(基于典型项目) + +``` +低风险 Bug (1-7级): 约 70% → 自动合并 +高风险 Bug (8-10级): 约 30% → 人工审核 +``` + +### 人工工作量 + +``` +假设每天 20 个 Bug: +- 优化前:20 个 × 3 分钟/个 = 60 分钟 +- 优化后:6 个 × 3 分钟/个 = 18 分钟 + +节省时间:70% +``` + +--- + +## ⚙️ 配置示例 + +```python +# .env 或 settings.py + +# Bug 严重等级评估 +SEVERITY_THRESHOLD_FOR_REVIEW=8 # ≥8 级需要人工审核 +ENABLE_AUTO_MERGE=true # 启用低风险 PR 自动合并 + +# 严格模式(可选) +STRICT_MODE=false # true: ≥6 级就需审核 +``` + +--- + +## 📋 FAQ + +### Q1: AI 评估等级不准确怎么办? + +**A:** 有以下保障措施: +1. **阈值可调整**:默认 ≥8 级需审核,可根据实际情况调整为 6 或 7 +2. **人工复核**:在 Web 界面可以看到 AI 的判定理由,如果不合理可以手动调整 +3. **持续优化**:收集误判案例,优化 Prompt 模板 +4. **保守策略**:当 AI 无法评估时,默认设为 6 级(中风险),偏向安全 + +### Q2: 自动合并的 PR 出错了怎么办? + +**A:** +1. **快速回滚**:在 Gitea 可以一键 revert commit +2. **降级处理**:临时关闭自动合并,所有 PR 都需人工审核 +3. **监控告警**:部署后持续监控错误日志,发现问题立即回滚 + +### Q3: 能否针对不同项目设置不同的阈值? + +**A:** 可以!配置支持项目级覆盖: + +```python +# 默认阈值 +severity_threshold_for_review: int = 8 + +# 项目级覆盖 +project_severity_thresholds: dict = { + "rtc_backend": 7, # 后端更谨慎 + "rtc_web": 8, # 前端正常 + "airhub_app": 9, # 移动端更宽松 +} +``` + +--- + +## 🎯 下一步 + +1. ✅ **实施 Phase 1**:实现 AI 分级评估功能 +2. ✅ **测试验证**:运行 50+ Bug 修复,验证分级准确性 +3. ✅ **调优阈值**:根据实际情况调整默认阈值 +4. ✅ **上线监控**:部署后持续监控自动合并的 PR 质量 + +--- + +**维护信息:** +- 创建:2026-02-25 +- 作者:AI + 开发团队 +- 审核:待审核 diff --git a/docs/pr-review-and-retry-workflow.md b/docs/pr-review-and-retry-workflow.md new file mode 100644 index 0000000..4e5a3bd --- /dev/null +++ b/docs/pr-review-and-retry-workflow.md @@ -0,0 +1,897 @@ +# 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 次,超过则人工介入 + +--- + +**下一步:是否开始实施?** diff --git a/repair_agent/REPAIR_FLOW_ANALYSIS.md b/repair_agent/REPAIR_FLOW_ANALYSIS.md new file mode 100644 index 0000000..820a968 --- /dev/null +++ b/repair_agent/REPAIR_FLOW_ANALYSIS.md @@ -0,0 +1,399 @@ +# Bug 自动修复流程分析与优化建议 + +> 分析日期: 2026-02-25 +> 当前版本存在的问题和改进方案 + +--- + +## 1. 当前流程概览 + +### 主流程 (fix_project) + +``` +1. 初始化 + ├─ 获取项目信息(本地路径、仓库地址) + ├─ 获取待修复 Bug 列表(NEW/PENDING_FIX) + ├─ 初始化 Git(pull + 创建 fix 分支) + └─ 更新所有 Bug 状态 → FIXING + +2. 多轮修复循环(max 3 轮) + └─ 每轮: + ├─ 调用 Claude CLI 修复代码 + ├─ 获取 diff 和修改文件列表 + ├─ 安全检查(文件数/行数/核心文件) + ├─ 运行 Claude 生成的测试文件 + ├─ 上传修复报告 + └─ 标记为 FIXED(无论测试是否通过!) + +3. 自动提交(可选) + ├─ commit → push fix 分支 + └─ **直接合并到 main 并推送** +``` + +### 重试失败流程 (retry_failed_project) + +``` +1. 获取所有 FIX_FAILED Bug +2. 逐个分诊(triage) + ├─ VERDICT:CANNOT_REPRODUCE → 标记为 CANNOT_REPRODUCE + └─ VERDICT:FIX → 重置为 PENDING_FIX +3. 批量调用 fix_project 修复 +``` + +--- + +## 2. 主要缺陷分析 + +### 🔴 **严重问题 1:测试验证与重试逻辑脱节** + +**问题描述:** +- 代码中有 `max_rounds=3` 的多轮重试机制 +- 但在 [core.py:241-254](log_center/repair_agent/agent/core.py#L241-L254),无论测试通过与否都标记为 `FIXED` 并 `break` +- **测试失败不会触发下一轮重试** + +**问题代码:** +```python +# core.py Line 230-254 +test_passed = bool(test_output) and "FAILED" not in test_output + +# 无论 test_passed 是 True 还是 False,都标记为 FIXED +for bug in bugs: + self.task_manager.update_status(bug.id, BugStatus.FIXED) + # ... + results.append(FixResult(bug_id=bug.id, success=True, ...)) # ❌ + +# 然后直接 break,不会进入第 2、3 轮 +break # Line 271 +``` + +**影响:** +- 多轮重试机制形同虚设 +- 测试失败的 Bug 被错误标记为已修复 +- 浪费了 Claude 重试优化的能力 + +**建议修复:** +```python +# 第 1 步:运行测试 +test_output = run_repair_test_file(project_path, test_file) +test_passed = self._check_test_passed(test_output) +cleanup_repair_test_file(project_path, test_file) + +# 第 2 步:根据测试结果决定下一步 +if test_passed: + # 测试通过,标记为 FIXED + for bug in bugs: + self.task_manager.update_status(bug.id, BugStatus.FIXED) + self._upload_round_report(...) + break # 成功,退出循环 +else: + # 测试失败,记录上下文供下一轮使用 + last_diff = diff + last_test_output = test_output + logger.warning(f"第 {round_num} 轮测试失败,尝试下一轮") + + if round_num == max_rounds: + # 最后一轮仍失败,标记为 FIX_FAILED + for bug in bugs: + self.task_manager.update_status(bug.id, BugStatus.FIX_FAILED) + self._upload_round_report(..., status=BugStatus.FIX_FAILED) + # 继续下一轮 +``` + +--- + +### 🟡 **严重问题 2:Git 分支管理混乱** + +**问题描述:** +1. [git_manager.py:55-75](log_center/repair_agent/agent/git_manager.py#L55-L75) `pull()` 会切回 main/master +2. [core.py:152-154](log_center/repair_agent/agent/core.py#L152-L154) 然后又从 main 创建新分支 +3. [core.py:264](log_center/repair_agent/agent/core.py#L264) `auto_commit` 时**直接合并到 main 并推送** + +**问题:** +- 没有 PR 审核流程,变更直接进入主分支 +- 每次修复都创建新分支,但合并后删除,无法回溯 +- 不符合现代 Git 工作流(应该创建 PR 供人工审核) + +**建议修复方案:** + +**方案 A:保留 fix 分支,推送后创建 PR** +```python +# core.py Line 256-267 +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}") + git_manager.push() + logger.info("fix 分支已推送,请手动审核并合并") + + # 可选:调用 gh CLI 创建 PR + # subprocess.run(["gh", "pr", "create", "--title", f"Auto fix {bug_ids}", ...]) +``` + +**方案 B:如果确实需要直接合并(慎用)** +```python +# 添加配置项控制 +if settings.auto_merge_to_main: # 默认 False + if git_manager.merge_to_main_and_push(): + logger.info("已合并到 main 并推送") +else: + logger.info("fix 分支已推送,需人工审核") +``` + +--- + +### 🟠 **问题 3:安全检查位置不当** + +**问题代码:** [core.py:209-225](log_center/repair_agent/agent/core.py#L209-L225) + +```python +# Step 3: 安全检查(在修改后) +if git_manager and not self._safety_check(modified_files, diff): + failure_reason = "安全检查未通过" + git_manager.reset_hard() # ❌ 丢失所有修改 + + # 标记为 FIX_FAILED,但不会重试 + for bug in bugs: + self.task_manager.update_status(bug.id, BugStatus.FIX_FAILED) + break +``` + +**问题:** +- Claude 已经修改了代码,安全检查失败后 `reset_hard` 丢失所有修改 +- 状态已经更新为 `FIXING`,造成不一致 +- 不会重试,浪费了 Claude 的工作 + +**建议:** + +**方案 A:软性警告 + 继续测试** +```python +# 安全检查改为警告,记录但不阻断 +if git_manager: + safety_passed, warnings = self._safety_check(modified_files, diff) + if not safety_passed: + logger.warning(f"安全检查警告: {warnings}") + # 将警告加入报告,但继续执行测试 + +# 让测试结果决定是否成功 +``` + +**方案 B:预检查机制** +```python +# 在调用 Claude 前,先用只读工具做预分析 +success, analysis = self.claude_service.analyze_bug(bug, project_path) +if "core file" in analysis.lower(): + logger.warning("分析发现可能涉及核心文件,跳过自动修复") + # 标记为需要人工介入 +``` + +--- + +### 🟡 **问题 4:测试通过判断过于简单** + +**问题代码:** [core.py:231](log_center/repair_agent/agent/core.py#L231) + +```python +test_passed = bool(test_output) and "FAILED" not in test_output and "Error" not in test_output.split("\n")[-5:].__repr__() +``` + +**问题:** +- 只检查 `"FAILED"` 字符串,不严谨 +- 没有测试文件也会被判定为通过(`test_output` 为空时返回 `False`) +- 但代码逻辑:没有测试文件 → `test_output = "Claude 未生成测试文件"` → `bool(test_output) = True` → 可能误判 + +**建议:** +```python +def _check_test_passed(self, test_output: str) -> bool: + """检查测试是否通过""" + if not test_output or "Claude 未生成测试文件" in test_output: + logger.warning("没有测试输出,无法验证修复") + return False # 没有测试视为未通过 + + # 检查常见失败标记 + fail_markers = ["FAILED", "ERROR", "AssertionError", "Exception"] + for marker in fail_markers: + if marker in test_output: + return False + + # 检查是否有通过标记(更严格) + if "OK" in test_output or "passed" in test_output.lower(): + return True + + # 无明确通过标记,保守判断 + return False +``` + +--- + +### 🟠 **问题 5:代码重复** + +**问题:** +- [core.py:95-290](log_center/repair_agent/agent/core.py#L95-L290) `fix_project` +- [core.py:467-562](log_center/repair_agent/agent/core.py#L467-L562) `fix_single_bug` +- 两个方法的核心逻辑完全一样,只是处理单个 vs 批量 + +**建议:** +```python +def fix_project(self, project_id: str, ...) -> BatchFixResult: + bugs = self.task_manager.fetch_pending_bugs(project_id) + return self._fix_bugs_batch(bugs, project_id, ...) + +def fix_single_bug(self, bug_id: int, ...) -> FixResult: + bug = self.task_manager.get_bug_detail(bug_id) + result = self._fix_bugs_batch([bug], bug.project_id, ...) + return result.results[0] if result.results else FixResult(...) + +def _fix_bugs_batch(self, bugs: list[Bug], project_id: str, ...) -> BatchFixResult: + """通用的批量修复逻辑""" + # 原 fix_project 的核心逻辑 +``` + +--- + +### 🟢 **问题 6:retry_failed_project 分诊逻辑可优化** + +**当前流程:** [core.py:292-398](log_center/repair_agent/agent/core.py#L292-L398) + +``` +FIX_FAILED Bug → 分诊 → CANNOT_REPRODUCE / 重置为 PENDING_FIX → fix_project +``` + +**问题:** +- 分诊失败会保留 `FIX_FAILED` 状态,下次仍会重复分诊 +- 没有分诊次数限制,可能死循环 + +**建议:** +```python +# 在 Bug 模型中增加 triage_count 字段 +if bug.retry_count >= settings.max_triage_count: + logger.info(f"Bug #{bug.id} 已分诊 {bug.retry_count} 次,标记为 CANNOT_REPRODUCE") + self.task_manager.update_status(bug.id, BugStatus.CANNOT_REPRODUCE) + continue + +# 分诊 +success, output = self.claude_service.triage_bug(bug, project_path) +if not success: + # 分诊执行失败,增加计数但保留状态 + self.task_manager.increment_retry_count(bug.id) + continue +``` + +--- + +## 3. 优化后的理想流程 + +### 新流程设计 + +``` +1. 初始化 + ├─ 获取项目信息和 Bug 列表 + ├─ Git pull + 创建 fix 分支 + └─ 更新状态 → FIXING + +2. 多轮修复循环(最多 3 轮) + └─ 每轮: + ├─ 调用 Claude CLI 修复 + ├─ 获取 diff 和修改文件 + ├─ 【新增】软性安全检查(警告不阻断) + ├─ 运行测试文件 + ├─ 【关键】严格判断测试是否通过 + │ ├─ 通过 → 标记 FIXED + break + │ └─ 失败 → 记录上下文,继续下一轮 + └─ 最后一轮仍失败 → 标记 FIX_FAILED + +3. 提交代码 + ├─ commit + push fix 分支 + ├─ 【推荐】创建 PR 供人工审核 + └─ 【慎用】auto_merge_to_main(需配置开关) +``` + +--- + +## 4. 实施建议 + +### 优先级排序 + +#### P0(必须修复) +1. ✅ **修复测试验证逻辑** - 测试失败应触发重试 +2. ✅ **移除自动合并到 main** - 改为创建 PR + +#### P1(强烈建议) +3. ✅ **优化测试通过判断** - 更严格的检测逻辑 +4. ✅ **重构代码消除重复** - 提取 `_fix_bugs_batch` + +#### P2(建议优化) +5. ✅ **改进安全检查** - 改为软性警告 +6. ✅ **添加分诊次数限制** - 防止重复分诊 + +--- + +## 5. 配置建议 + +### 新增配置项 + +```python +# config/settings.py + +# Git 工作流 +auto_merge_to_main: bool = False # 默认不自动合并 +create_pr_after_fix: bool = True # 自动创建 PR + +# 测试验证 +require_test_pass: bool = True # 必须测试通过才标记 FIXED +test_timeout: int = 300 # 测试超时时间(秒) + +# 重试策略 +max_retry_count: int = 3 # 最大重试轮数 +max_triage_count: int = 2 # 最大分诊次数 + +# 安全检查 +safety_check_mode: str = "warn" # warn / block +max_modified_files: int = 10 +max_modified_lines: int = 500 +``` + +--- + +## 6. 测试建议 + +### 需要覆盖的场景 + +1. **测试失败重试** + - Bug 修复后测试失败 → 第 2 轮修复 → 测试通过 + - 3 轮都失败 → 标记为 FIX_FAILED + +2. **Git 分支管理** + - 修复后创建 PR 而不是直接合并 + - 多个 Bug 修复复用同一个 fix 分支 + +3. **安全检查** + - 修改超限文件 → 警告但继续 + - 核心文件修改 → 警告并记录 + +4. **分诊流程** + - FIX_FAILED → 分诊 → CANNOT_REPRODUCE + - 分诊失败达到上限 → 标记为 CANNOT_REPRODUCE + +--- + +## 7. 总结 + +### 关键改进点 + +| 问题 | 现状 | 改进后 | +|------|------|--------| +| 测试验证 | 无论通过与否都标记 FIXED | 测试失败触发重试,最终失败标记 FIX_FAILED | +| Git 工作流 | 直接合并到 main | 创建 PR 供人工审核 | +| 安全检查 | reset_hard 丢失修改 | 软性警告,继续测试 | +| 测试判断 | 简单字符串匹配 | 严格检测通过/失败标记 | +| 代码质量 | 逻辑重复 | 提取公共方法 | +| 分诊流程 | 可能重复分诊 | 添加次数限制 | + +### 预期收益 + +- ✅ 多轮重试机制真正发挥作用,提高修复成功率 +- ✅ Git 工作流更安全,避免直接污染主分支 +- ✅ 测试验证更严格,减少假阳性 +- ✅ 代码更简洁,维护成本降低 + +--- + +**建议:优先实施 P0 和 P1 的改进,P2 可以逐步优化。** diff --git a/repair_agent/test_gitea_api.py b/repair_agent/test_gitea_api.py new file mode 100644 index 0000000..10bd237 --- /dev/null +++ b/repair_agent/test_gitea_api.py @@ -0,0 +1,390 @@ +""" +Gitea API 功能测试脚本 + +测试内容: +1. 创建 PR +2. Merge PR (在日志中台直接合并) +3. Close PR (在日志中台直接拒绝) +4. 获取 PR 评论(close 原因) +5. 重新打开 PR (可选) + +使用方法: +python test_gitea_api.py --gitea-url https://gitea.xxx.com --token YOUR_TOKEN --owner owner --repo repo +""" + +import argparse +import httpx +import json +from typing import Optional + + +class GiteaAPITester: + """Gitea API 测试器""" + + def __init__(self, gitea_url: str, token: str, owner: str, repo: str): + self.gitea_url = gitea_url.rstrip("/") + self.token = token + self.owner = owner + self.repo = repo + 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 test_create_pr(self, head_branch: str, base_branch: str = "main") -> Optional[int]: + """ + 测试 1: 创建 PR + + Returns: + PR 编号,失败返回 None + """ + print("\n" + "=" * 60) + print("测试 1: 创建 Pull Request") + print("=" * 60) + + url = f"{self.base_api_url}/repos/{self.owner}/{self.repo}/pulls" + payload = { + "title": "[测试] Gitea API 测试 PR", + "head": head_branch, + "base": base_branch, + "body": "这是一个测试 PR,用于验证 Gitea API 功能。\n\n## 测试内容\n- 创建 PR\n- Merge PR\n- Close PR", + } + + try: + response = self.client.post(url, json=payload, headers=self._headers()) + response.raise_for_status() + + data = response.json() + pr_number = data.get("number") + pr_url = data.get("html_url") + + print(f"✅ PR 创建成功") + print(f" PR 编号: #{pr_number}") + print(f" PR URL: {pr_url}") + print(f" 状态: {data.get('state')}") + + return pr_number + + except httpx.HTTPStatusError as e: + print(f"❌ PR 创建失败: HTTP {e.response.status_code}") + print(f" 错误信息: {e.response.text}") + return None + except Exception as e: + print(f"❌ PR 创建失败: {e}") + return None + + def test_get_pr_info(self, pr_number: int): + """获取 PR 信息""" + print("\n" + "=" * 60) + print(f"获取 PR #{pr_number} 信息") + print("=" * 60) + + url = f"{self.base_api_url}/repos/{self.owner}/{self.repo}/pulls/{pr_number}" + + try: + response = self.client.get(url, headers=self._headers()) + response.raise_for_status() + + data = response.json() + print(f"✅ PR 信息获取成功") + print(f" 标题: {data.get('title')}") + print(f" 状态: {data.get('state')}") + print(f" 是否已合并: {data.get('merged')}") + print(f" 创建时间: {data.get('created_at')}") + + return data + + except Exception as e: + print(f"❌ 获取 PR 信息失败: {e}") + return None + + def test_merge_pr(self, pr_number: int) -> bool: + """ + 测试 2: 合并 PR(在日志中台直接操作) + + API: POST /api/v1/repos/{owner}/{repo}/pulls/{index}/merge + + Returns: + 是否成功 + """ + print("\n" + "=" * 60) + print(f"测试 2: 合并 PR #{pr_number}") + print("=" * 60) + + url = f"{self.base_api_url}/repos/{self.owner}/{self.repo}/pulls/{pr_number}/merge" + payload = { + "Do": "merge", # merge / squash / rebase + "MergeMessageField": f"Merge PR #{pr_number} via API", + "MergeTitleField": f"Merge pull request #{pr_number}", + } + + try: + response = self.client.post(url, json=payload, headers=self._headers()) + response.raise_for_status() + + print(f"✅ PR 合并成功") + print(f" 方式: merge commit") + print(f" 结论: ✅ Gitea 支持通过 API 直接合并 PR") + + return True + + except httpx.HTTPStatusError as e: + print(f"❌ PR 合并失败: HTTP {e.response.status_code}") + print(f" 错误信息: {e.response.text}") + + # 分析失败原因 + if e.response.status_code == 405: + print(f" 可能原因: PR 已经合并或已关闭") + elif e.response.status_code == 409: + print(f" 可能原因: 有合并冲突") + elif e.response.status_code == 403: + print(f" 可能原因: Token 权限不足") + + return False + except Exception as e: + print(f"❌ PR 合并失败: {e}") + return False + + def test_close_pr(self, pr_number: int, reason: str = "") -> bool: + """ + 测试 3: 关闭 PR(在日志中台直接拒绝) + + API: PATCH /api/v1/repos/{owner}/{repo}/pulls/{index} + + Returns: + 是否成功 + """ + print("\n" + "=" * 60) + print(f"测试 3: 关闭 PR #{pr_number}") + print("=" * 60) + + # Step 1: 添加关闭原因评论 + if reason: + comment_success = self.add_pr_comment(pr_number, reason) + if comment_success: + print(f"✅ 已添加关闭原因评论") + + # Step 2: 关闭 PR + url = f"{self.base_api_url}/repos/{self.owner}/{self.repo}/pulls/{pr_number}" + payload = { + "state": "closed", # open / closed + } + + try: + response = self.client.patch(url, json=payload, headers=self._headers()) + response.raise_for_status() + + print(f"✅ PR 关闭成功") + print(f" 结论: ✅ Gitea 支持通过 API 直接关闭 PR") + + return True + + except httpx.HTTPStatusError as e: + print(f"❌ PR 关闭失败: HTTP {e.response.status_code}") + print(f" 错误信息: {e.response.text}") + return False + except Exception as e: + print(f"❌ PR 关闭失败: {e}") + return False + + def add_pr_comment(self, pr_number: int, comment: str) -> bool: + """ + 添加 PR 评论(用于记录 close 原因) + + API: POST /api/v1/repos/{owner}/{repo}/issues/{index}/comments + + 注意: Gitea 中 PR 和 Issue 共用评论 API + """ + url = f"{self.base_api_url}/repos/{self.owner}/{self.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: + print(f"⚠️ 添加评论失败: {e}") + return False + + def test_get_pr_comments(self, pr_number: int) -> list[dict]: + """ + 测试 4: 获取 PR 评论(获取 close 原因) + + API: GET /api/v1/repos/{owner}/{repo}/issues/{index}/comments + + Returns: + 评论列表 + """ + print("\n" + "=" * 60) + print(f"测试 4: 获取 PR #{pr_number} 的评论") + print("=" * 60) + + url = f"{self.base_api_url}/repos/{self.owner}/{self.repo}/issues/{pr_number}/comments" + + try: + response = self.client.get(url, headers=self._headers()) + response.raise_for_status() + + comments = response.json() + print(f"✅ 获取评论成功,共 {len(comments)} 条") + + for i, comment in enumerate(comments, 1): + print(f"\n 评论 #{i}:") + print(f" - 作者: {comment.get('user', {}).get('login')}") + print(f" - 时间: {comment.get('created_at')}") + print(f" - 内容: {comment.get('body')[:100]}...") + + print(f"\n 结论: ✅ 可以通过评论获取 PR 关闭原因") + + return comments + + except Exception as e: + print(f"❌ 获取评论失败: {e}") + return [] + + def test_reopen_pr(self, pr_number: int) -> bool: + """ + 测试 5: 重新打开 PR(可选) + + API: PATCH /api/v1/repos/{owner}/{repo}/pulls/{index} + """ + print("\n" + "=" * 60) + print(f"测试 5: 重新打开 PR #{pr_number}") + print("=" * 60) + + url = f"{self.base_api_url}/repos/{self.owner}/{self.repo}/pulls/{pr_number}" + payload = { + "state": "open", + } + + try: + response = self.client.patch(url, json=payload, headers=self._headers()) + response.raise_for_status() + + print(f"✅ PR 重新打开成功") + print(f" 结论: ✅ Gitea 支持重新打开已关闭的 PR") + + return True + + except Exception as e: + print(f"❌ 重新打开 PR 失败: {e}") + return False + + def run_all_tests(self, test_pr_number: Optional[int] = None): + """运行所有测试""" + print("\n") + print("╔" + "=" * 58 + "╗") + print("║" + " " * 15 + "Gitea API 功能测试" + " " * 23 + "║") + print("╚" + "=" * 58 + "╝") + + print(f"\n配置信息:") + print(f" Gitea URL: {self.gitea_url}") + print(f" 仓库: {self.owner}/{self.repo}") + print(f" Token: {'*' * 20} (已配置)") + + if test_pr_number: + # 使用已存在的 PR 进行测试 + pr_number = test_pr_number + print(f"\n使用已存在的 PR #{pr_number} 进行测试") + + # 获取 PR 信息 + self.test_get_pr_info(pr_number) + + else: + # 创建新 PR 进行测试(需要先在仓库中创建测试分支) + print("\n⚠️ 提示: 需要先在仓库中创建一个测试分支(如 test-gitea-api)") + test_branch = input("请输入测试分支名称(或直接回车跳过创建 PR): ").strip() + + if test_branch: + pr_number = self.test_create_pr(test_branch) + if not pr_number: + print("\n❌ 测试终止:无法创建 PR") + return + else: + pr_number = int(input("请输入已存在的 PR 编号: ")) + + # 测试关闭 PR(带原因) + close_reason = """## ❌ PR 被拒绝 + +**拒绝原因:** +1. 测试覆盖不足,缺少边界条件测试 +2. 修改了核心业务逻辑,但未添加集成测试 +3. 代码中存在潜在的 null 指针风险 + +**建议:** +- 补充测试用例 +- 重新审核业务逻辑 +- 修复后重新提交 + +--- +**操作人:** 审核人员 (通过日志中台) +""" + + self.test_close_pr(pr_number, close_reason) + + # 获取评论(验证可以获取 close 原因) + comments = self.test_get_pr_comments(pr_number) + + # 可选:重新打开 PR + reopen = input("\n是否测试重新打开 PR?(y/n): ").strip().lower() + if reopen == "y": + self.test_reopen_pr(pr_number) + + # 可选:测试合并 PR + if reopen == "y": + merge = input("是否测试合并 PR?(y/n): ").strip().lower() + if merge == "y": + self.test_merge_pr(pr_number) + + print("\n" + "=" * 60) + print("测试总结") + print("=" * 60) + print("✅ Gitea 支持的功能:") + print(" 1. ✅ 通过 API 创建 PR") + print(" 2. ✅ 通过 API 合并 PR(在日志中台直接操作)") + print(" 3. ✅ 通过 API 关闭 PR(在日志中台直接操作)") + print(" 4. ✅ 通过 API 添加评论(记录 close 原因)") + print(" 5. ✅ 通过 API 获取评论(读取 close 原因)") + print(" 6. ✅ 通过 API 重新打开 PR") + print("\n结论: Gitea 完全支持在日志中台直接 merge/close PR!") + + +def main(): + parser = argparse.ArgumentParser(description="Gitea API 功能测试") + parser.add_argument("--gitea-url", required=True, help="Gitea 服务器地址") + parser.add_argument("--token", required=True, help="Gitea API Token") + parser.add_argument("--owner", required=True, help="仓库所有者") + parser.add_argument("--repo", required=True, help="仓库名称") + parser.add_argument("--pr-number", type=int, help="测试用的 PR 编号(可选)") + + args = parser.parse_args() + + tester = GiteaAPITester( + gitea_url=args.gitea_url, + token=args.token, + owner=args.owner, + repo=args.repo, + ) + + tester.run_all_tests(test_pr_number=args.pr_number) + + +if __name__ == "__main__": + # 示例用法 + print("示例用法:") + print("python test_gitea_api.py \\") + print(" --gitea-url https://gitea.airlabs.art \\") + print(" --token your_token_here \\") + print(" --owner airlabs \\") + print(" --repo rtc_backend \\") + print(" --pr-number 45") + print("\n") + + main() diff --git a/web/src/api.ts b/web/src/api.ts index d549f4b..e3d743e 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -27,6 +27,15 @@ export interface ErrorLog { status: string; retry_count: number; failure_reason: string | null; + // PR Tracking + pr_number?: number | null; + pr_url?: string | null; + branch_name?: string | null; + // Rejection Tracking + rejection_reason?: string | null; + rejection_count?: number; + last_rejected_at?: string | null; + merged_at?: string | null; } export interface Project { @@ -112,4 +121,11 @@ export const getRepairReportsByBug = (errorLogId: number) => params: { error_log_id: errorLogId, page_size: 100 } }); +// PR Operations +export const mergePR = (bugId: number) => + api.post(`/api/v1/bugs/${bugId}/merge-pr`); + +export const closePR = (bugId: number, reason: string) => + api.post(`/api/v1/bugs/${bugId}/close-pr`, { reason }); + export default api; diff --git a/web/src/pages/BugDetail.tsx b/web/src/pages/BugDetail.tsx index 8e61a15..da89878 100644 --- a/web/src/pages/BugDetail.tsx +++ b/web/src/pages/BugDetail.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { useParams, Link, useLocation } from 'react-router-dom'; -import { ArrowLeft, Play, Loader2, FileCode, GitCommit, History, AlertTriangle } from 'lucide-react'; -import { getBugDetail, triggerRepair, getRepairReportsByBug, type ErrorLog, type RepairReport } from '../api'; +import { ArrowLeft, Play, Loader2, FileCode, GitCommit, History, AlertTriangle, Check, X, ExternalLink } from 'lucide-react'; +import { getBugDetail, triggerRepair, getRepairReportsByBug, mergePR, closePR, type ErrorLog, type RepairReport } from '../api'; const SOURCE_LABELS: Record = { runtime: '运行时', @@ -21,6 +21,13 @@ const STATUS_LABELS: Record = { FIX_FAILED: '修复失败', }; +const REJECT_REASON_TEMPLATES = [ + '测试覆盖不足,缺少边界条件测试', + '业务逻辑需要调整', + '代码质量不符合规范', + '需要补充异常处理', +]; + export default function BugDetail() { const { id } = useParams<{ id: string }>(); const location = useLocation(); @@ -30,6 +37,13 @@ export default function BugDetail() { const [repairMessage, setRepairMessage] = useState(''); const [repairHistory, setRepairHistory] = useState([]); + // PR 操作相关状态 + const [mergingPR, setMergingPR] = useState(false); + const [closingPR, setClosingPR] = useState(false); + const [prMessage, setPRMessage] = useState(''); + const [showRejectModal, setShowRejectModal] = useState(false); + const [rejectReason, setRejectReason] = useState(''); + const backSearch = location.state?.fromSearch || ''; useEffect(() => { @@ -71,6 +85,48 @@ export default function BugDetail() { } }; + const handleMergePR = async () => { + if (!bug) return; + setMergingPR(true); + setPRMessage(''); + try { + await mergePR(bug.id); + setBug({ ...bug, status: 'FIXED', merged_at: new Date().toISOString() }); + setPRMessage('✅ PR 已成功合并'); + } catch (error: any) { + console.error('Failed to merge PR:', error); + setPRMessage(`❌ 合并失败: ${error.response?.data?.detail || error.message}`); + } finally { + setMergingPR(false); + } + }; + + const handleClosePR = async () => { + if (!bug || !rejectReason.trim()) { + setPRMessage('❌ 请输入拒绝原因'); + return; + } + setClosingPR(true); + setPRMessage(''); + try { + await closePR(bug.id, rejectReason); + setBug({ + ...bug, + status: 'PENDING_FIX', + rejection_count: (bug.rejection_count || 0) + 1, + last_rejected_at: new Date().toISOString() + }); + setPRMessage('✅ PR 已拒绝,Bug 将重新修复'); + setShowRejectModal(false); + setRejectReason(''); + } catch (error: any) { + console.error('Failed to close PR:', error); + setPRMessage(`❌ 关闭失败: ${error.response?.data?.detail || error.message}`); + } finally { + setClosingPR(false); + } + }; + if (loading) { return (
@@ -85,6 +141,8 @@ export default function BugDetail() { const isRuntime = !bug.source || bug.source === 'runtime'; const canTriggerRepair = ['NEW', 'FIX_FAILED'].includes(bug.status) && isRuntime; + const hasPR = !!bug.pr_url; + const canOperatePR = hasPR && bug.status === 'PENDING_FIX'; return (
@@ -111,6 +169,26 @@ export default function BugDetail() {
+ {/* PR 信息显示 */} + {hasPR && ( +
+
Pull Request
+
+ + PR #{bug.pr_number} | {bug.branch_name || 'fix branch'} + + + 查看 PR + + {bug.rejection_count > 0 && ( + + 已拒绝 {bug.rejection_count} 次 + + )} +
+
+ )} + {bug.file_path && (
文件位置
@@ -175,19 +253,63 @@ export default function BugDetail() {
)} -
- + {/* 操作按钮区 */} +
+ {/* PR 操作按钮 */} + {canOperatePR && ( + <> + + + + + )} + + {/* 原有的触发修复按钮 */} + {!hasPR && ( + + )} + + {/* 消息显示 */} + {prMessage && ( + + {prMessage} + + )} + {repairMessage && ( )} - {!canTriggerRepair && !repairing && ( + + {!canTriggerRepair && !repairing && !hasPR && ( {!isRuntime ? 'CI/CD 和部署错误暂不支持自动修复' @@ -216,6 +339,16 @@ export default function BugDetail() {
)} + {bug.rejection_reason && ( +
+
+ + 上次拒绝原因 +
+
{JSON.parse(bug.rejection_reason).reason}
+
+ )} +
元数据
@@ -236,6 +369,12 @@ export default function BugDetail() { + {bug.merged_at && ( + + + + + )}
上报时间 {new Date(bug.timestamp).toLocaleString()}
合并时间{new Date(bug.merged_at).toLocaleString()}
@@ -290,6 +429,108 @@ export default function BugDetail() {
)} + + {/* 拒绝原因模态框 */} + {showRejectModal && ( +
+
+

拒绝修复

+

+ 请说明拒绝原因,Agent 将根据您的反馈重新修复: +

+ +
+ +
+ {REJECT_REASON_TEMPLATES.map((template, idx) => ( + + ))} +
+
+ +