From cdb68636bff29e95c1e45e43d1d4f4bca9ecd17f Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 17 Jul 2026 01:01:23 +0800 Subject: [PATCH] =?UTF-8?q?fix(ci):=20=E4=BF=AE=E5=A4=8DAI=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=AE=A1=E6=9F=A53=E4=B8=AAbug=20-=20=E8=BF=87?= =?UTF-8?q?=E6=BB=A4=E7=94=9F=E6=95=88/=E8=AF=84=E8=AE=BA=E5=8E=BB?= =?UTF-8?q?=E9=87=8D/=E5=BC=82=E5=B8=B8=E9=80=80=E5=87=BA=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bug1: 过滤逻辑实际从diff中移除跳过的文件(lock/图片等),节省token - Bug2: 评论去重接上,每次新审查先删除旧的AI评论,避免刷屏 - Bug3: 异常情况sys.exit(1)替代sys.exit(0),配合continue-on-error也能看到失败 --- scripts/ci_code_review.py | 143 ++++++++++++++++++-------------------- 1 file changed, 69 insertions(+), 74 deletions(-) diff --git a/scripts/ci_code_review.py b/scripts/ci_code_review.py index 006e8220b..6bb625f35 100644 --- a/scripts/ci_code_review.py +++ b/scripts/ci_code_review.py @@ -150,70 +150,34 @@ class GiteaClient: def get_existing_review_comments(self, pr_number: int, marker: str) -> list: """ - 获取 PR 上已有的审查评论(带标识),用于后续更新或删除旧评论。 + 获取 PR 上已有的 AI 审查评论 ID 列表(带标识 marker)。 """ url = self._api_url(f"issues/{pr_number}/comments") resp = self.session.get(url, timeout=GITEA_TIMEOUT) if resp.status_code != 200: + logger.warning(f"获取评论列表失败: HTTP {resp.status_code}") return [] comments = resp.json() - return [c for c in comments if marker in c.get("body", "")] + review_comment_ids = [] + for c in comments: + body = c.get("body", "") + if marker in body: + review_comment_ids.append(c.get("id")) + logger.info(f"找到 {len(review_comment_ids)} 条旧的 AI 审查评论") + return review_comment_ids + def delete_pr_comment(self, pr_number: int, comment_id: int) -> bool: + """ + 删除 PR 上的指定评论。 + """ + url = self._api_url(f"issues/comments/{comment_id}") + resp = self.session.delete(url, timeout=GITEA_TIMEOUT) + if resp.status_code not in (200, 204): + logger.warning(f"删除评论 {comment_id} 失败: HTTP {resp.status_code}") + return False + return True -# ============== LLM 调用 ============== -def build_review_prompt(diff_text: str, pr_number: int, file_list: list) -> str: - """构建代码审查的 Prompt""" - file_names = [f.get("filename", "") for f in file_list] if file_list else [] - files_summary = ", ".join(file_names[:10]) if file_names else "未知" - if len(file_names) > 10: - files_summary += f" 等 {len(file_names)} 个文件" - - prompt = f"""你是一位资深代码审查专家,请对以下 Pull Request 的代码变更进行严格审查。 - -**PR 信息:** -- PR 编号:#{pr_number} -- 修改文件:{files_summary} - -**审查重点:** -1. **严重问题**:逻辑错误、潜在 Bug、安全漏洞、数据不一致、空指针、资源泄漏、并发问题等 -2. **代码质量**:边界条件处理、错误处理是否完善、异常场景覆盖 -3. **性能隐患**:明显的性能问题、低效算法、不必要的重复计算 -4. **最佳实践**:代码规范、可读性、可维护性、命名是否清晰 - -**审查原则:** -- 只针对变更的代码(diff)进行审查,不要审查未改动的代码 -- 严重问题必须指出具体文件名和大致行号(根据 diff 中的行号推断) -- 给出明确、可操作的建议,不要空泛 -- 如果代码质量很好、没有明显问题,也请如实说明 -- 用中文回复 - -**输出格式要求(严格遵守,不要输出格式以外的内容):** - -## 代码审查结果 - PR #{pr_number} - -### ⚠️ 问题(N个需要修改) -1. **文件名 第X行**:问题描述(说明原因和可能的影响) -2. **文件名 第X行**:问题描述 - -### 💡 建议(N个可选) -1. 建议描述(可选优化、代码风格等) - ---- -✅ 格式检查通过 | ❌ 逻辑审查需修改 | ⚠️ 建议关注性能 - -**说明:** 底部的三个状态标签,根据审查结果勾选或取消对应标记(用 ✅/❌/⚠️ 表示): -- 格式检查:代码格式、命名规范等是否达标 -- 逻辑审查:是否存在必须修改的逻辑问题 -- 性能:是否存在需要关注的性能问题 - -**以下是代码 diff 内容:** - -```diff -{diff_text} -``` -""" - return prompt def call_llm_openai( @@ -509,9 +473,7 @@ def main(): if missing: logger.error(f"缺少必要配置: {', '.join(missing)}") - # 审查失败不阻断 CI,返回 0 - logger.info("审查脚本因配置缺失而跳过,退出码 0") - sys.exit(0) + sys.exit(1) logger.info(f"开始审查 PR #{pr_number},仓库: {repo_name}") logger.info(f"Gitea: {gitea_url}") @@ -527,17 +489,45 @@ def main(): file_list = gitea.get_pr_files(pr_number) except Exception as e: logger.error(f"获取 PR 信息失败: {e}") - logger.info("审查脚本异常退出,退出码 0(不阻断 CI)") - sys.exit(0) + sys.exit(1) - # 3. 过滤掉不需要审查的文件(如 lock 文件、生成的文件等) + # 3. 过滤掉不需要审查的文件(如 lock 文件、生成的文件、二进制文件等) skip_extensions = (".lock", ".sum", ".min.js", ".min.css", ".map", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".woff", ".woff2", ".ttf", ".eot") + skipped_files = [] if file_list: - skipped = [f.get("filename") for f in file_list - if f.get("filename", "").endswith(skip_extensions) - or f.get("status") == "removed"] - if skipped: - logger.info(f"跳过 {len(skipped)} 个非文本/已删除文件: {', '.join(skipped[:5])}...") + skipped_files = [f.get("filename") for f in file_list + if f.get("filename", "").endswith(skip_extensions) + or f.get("status") == "removed"] + if skipped_files: + logger.info(f"跳过 {len(skipped_files)} 个非文本/已删除文件: {', '.join(skipped_files[:5])}...") + + # 实际从 diff 中移除跳过的文件(按文件边界切割) + if skipped_files: + diff_lines = diff_text.split("\n") + filtered_lines = [] + current_file = None + skip_current = False + i = 0 + while i < len(diff_lines): + line = diff_lines[i] + # 检测新文件开始: diff --git a/xxx b/xxx + if line.startswith("diff --git "): + # 提取文件名 + parts = line.split(" ") + if len(parts) >= 4: + # b/ 后面的是目标文件名 + current_file = parts[3][2:] if parts[3].startswith("b/") else parts[3] + skip_current = any(current_file == sf for sf in skipped_files) or any( + current_file.endswith(ext) for ext in skip_extensions + ) + else: + skip_current = False + if not skip_current: + filtered_lines.append(line) + i += 1 + original_len = len(diff_text) + diff_text = "\n".join(filtered_lines) + logger.info(f"Diff 过滤后: {original_len} -> {len(diff_text)} 字符 (减少 {original_len - len(diff_text)})") # 4. 截断过大的 diff diff_text, was_truncated = truncate_diff(diff_text, MAX_DIFF_CHARS) @@ -561,9 +551,8 @@ def main(): ) if not review_result: - logger.error("LLM 审查失败,跳过发布评论") - logger.info("审查脚本异常退出,退出码 0(不阻断 CI)") - sys.exit(0) + logger.error("LLM 审查失败") + sys.exit(1) # 7. 加上审查时间和标识(便于识别是自动审查) from datetime import datetime @@ -586,14 +575,22 @@ def main(): logger.info(f"... 共 {len(review_result.split(chr(10)))} 行") logger.info("=" * 60) - # 9. 发布评论 + # 9. 发布评论(先删除旧的审查评论,避免刷屏) if args.dry_run: logger.info("--dry-run 模式,跳过发布评论") print(full_comment) else: + # 去重:删除之前的 AI 审查评论 + old_comments = gitea.get_existing_review_comments(pr_number, marker) + if old_comments: + logger.info(f"找到 {len(old_comments)} 条旧的 AI 审查评论,先删除") + for cid in old_comments: + gitea.delete_pr_comment(pr_number, cid) + # 发布新评论 success = gitea.post_pr_comment(pr_number, full_comment) if not success: - logger.warning("评论发布失败,但不影响 CI 通过") + logger.error("评论发布失败") + sys.exit(1) # 10. 判断是否有严重问题(可选阻断) # 目前只做建议,不阻断合并,始终返回 0 @@ -606,9 +603,7 @@ def main(): except Exception as e: logger.exception(f"审查脚本发生未预期的异常: {e}") - # 任何异常都不阻断 CI - logger.info("审查脚本异常退出,退出码 0(不阻断 CI)") - sys.exit(0) + sys.exit(1) if __name__ == "__main__":