Commit bda91a10 authored by tongzifang's avatar tongzifang

Initial commit: CI Code Viewer

parents
.DS_Store
__pycache__/
*.pyc
*.pyo
.env
# AI Code Review
GitLab CI 集成的 AI 代码审查工具,自动分析代码变更风险并通过飞书通知审查结果。
## 特性
- **三级智能分流**:LLM 自动判定审查级别(skip / standard / deep),节省 API 调用成本
- **双重安全闸门**:实质代码变更不可跳过 + 安全关键词自动升级为深度审查
- **多平台预设**:内置 Android / iOS / Frontend 专属审查规则,开箱即用
- **飞书通知**:审查结果自动推送至飞书群
- **零侵入接入**:项目只需在 `.gitlab-ci.yml` 中添加几行配置
## 快速接入
### 方式一:include 模板(推荐)
在项目的 `.gitlab-ci.yml` 中添加:
```yaml
include:
- project: 'devops/ai-code-review'
file: '/templates/ai-review.yml'
```
### 方式二:直接引用脚本
```yaml
stages:
- review
ai-code-review:
stage: review
tags:
- code-review
variables:
GIT_DEPTH: "50"
GIT_STRATEGY: fetch
script:
- echo "Starting AI code review..."
- rm -rf /tmp/ai-code-review-${CI_JOB_ID}
- git clone --depth 1 https://gitlab-ci-token:${CI_JOB_TOKEN}@git.zmcms.cn/fht/ai-code-review.git /tmp/ai-code-review-${CI_JOB_ID}
- python3 /tmp/ai-code-review-${CI_JOB_ID}/ci_review.py
after_script:
- rm -rf /tmp/ai-code-review-${CI_JOB_ID}
allow_failure: true
```
### 必要配置
在 GitLab **Settings > CI/CD > Variables** 中添加:
| 变量 | 必填 | 说明 |
|------|------|------|
| `AI_REVIEW_API_KEY` | ✅ | LLM API 密钥 |
| `FEISHU_WEBHOOK_URL` | 否 | 飞书机器人 Webhook 地址 |
## 环境变量
### 核心配置
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `AI_REVIEW_API_URL` | `https://coding.dashscope.aliyuncs.com/v1/chat/completions` | LLM API 地址 |
| `AI_REVIEW_API_KEY` | - | LLM API 密钥 |
| `AI_REVIEW_MODEL` | `qwen3.5-plus` | 深度审查模型 |
| `AI_REVIEW_TRIAGE_MODEL` | `qwen3-coder-plus` | 分流判定 & 标准审查模型 |
| `AI_REVIEW_PLATFORM` | - | 平台预设:`android` / `ios` / `frontend` |
### 审查参数
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `AI_REVIEW_MAX_DIFF_LINES` | `2000` | 审查 diff 最大行数 |
| `AI_REVIEW_TRIAGE_MAX_DIFF_LINES` | `300` | 分流判定 diff 最大行数 |
| `AI_REVIEW_STANDARD_MAX_TOKENS` | `4096` | 标准审查最大 token |
| `AI_REVIEW_DEEP_MAX_TOKENS` | `8192` | 深度审查最大 token |
| `AI_REVIEW_STANDARD_TIMEOUT` | `180` | 标准审查超时(秒) |
| `AI_REVIEW_DEEP_TIMEOUT` | `300` | 深度审查超时(秒) |
### 精细覆盖
| 变量 | 说明 |
|------|------|
| `AI_REVIEW_SKIP_PATTERNS` | 跳过文件模式,逗号分隔(如 `*.lock,*.json`) |
| `AI_REVIEW_FOCUS_AREAS` | 审查关注点,`\|` 分隔(覆盖平台预设) |
## 审查流程
```
git diff → 文件过滤 → LLM 分流判定 → 审查 → 飞书通知
┌─────┼─────┐
▼ ▼ ▼
skip standard deep
(跳过) (轻量模型) (重量模型+thinking)
│ │
安全闸门检查:
• 有实质代码变更 → 不可 skip
• 命中安全关键词 → 升级为 deep
```
## 项目结构
```
ai_review/
├── __init__.py # 版本号
├── __main__.py # python -m ai_review 入口
├── config.py # 环境变量、平台预设
├── git.py # Git diff 获取、过滤、统计
├── llm.py # LLM 调用与 prompt 构建
├── triage.py # 分流判定与安全闸门
├── notify.py # 飞书通知
└── main.py # 主流程编排
ci_review.py # 薄包装入口(兼容现有 CI 调用)
templates/
└── ai-review.yml # GitLab CI 模板
```
## 平台预设
设置 `AI_REVIEW_PLATFORM` 环境变量可启用对应平台的预设规则:
- **android**:关注空指针、内存泄漏、WebView 漏洞等,跳过 `.xml``.properties` 等配置文件
- **ios**:关注强制解包、循环引用、Data Race 等,跳过 `.pbxproj``Pods/*` 等文件
- **frontend**:关注 XSS、状态管理、异步问题等,跳过 `dist/*``node_modules/*` 等目录
不设置则使用通用审查规则。
__version__ = "1.0.0"
from ai_review.main import main
main()
# -*- coding: utf-8 -*-
"""所有配置通过环境变量注入,不依赖项目内任何文件。"""
import os
# ─── 配置(全部从环境变量读取) ──────────────────────────────
API_URL = os.environ.get("AI_REVIEW_API_URL", "https://coding.dashscope.aliyuncs.com/v1/chat/completions")
API_KEY = os.environ.get("AI_REVIEW_API_KEY", "")
MODEL = os.environ.get("AI_REVIEW_MODEL", "qwen3.5-plus")
TRIAGE_MODEL = os.environ.get("AI_REVIEW_TRIAGE_MODEL", "qwen3-coder-plus")
FEISHU_WEBHOOK = os.environ.get("FEISHU_WEBHOOK_URL", "")
MAX_DIFF_LINES = int(os.environ.get("AI_REVIEW_MAX_DIFF_LINES", "2000"))
STANDARD_MAX_TOKENS = int(os.environ.get("AI_REVIEW_STANDARD_MAX_TOKENS", "4096"))
DEEP_MAX_TOKENS = int(os.environ.get("AI_REVIEW_DEEP_MAX_TOKENS", "8192"))
STANDARD_TIMEOUT = int(os.environ.get("AI_REVIEW_STANDARD_TIMEOUT", "180"))
DEEP_TIMEOUT = int(os.environ.get("AI_REVIEW_DEEP_TIMEOUT", "300"))
# ─── 平台预设(android / ios / frontend,留空或其他值则使用通用配置) ───
PLATFORM = os.environ.get("AI_REVIEW_PLATFORM", "").strip().lower()
_PLATFORM_PROFILES = {
"android": {
"skip": "*.lock,*.json,*.toml,*.xml,*.properties,*.pro,*.txt,*.md",
"focus": [
"安全漏洞(硬编码密钥、SQL注入、XSS、WebView漏洞等)",
"空指针风险(NullPointerException)",
"内存泄漏(Activity/Fragment/Context泄漏)",
"线程安全问题(Handler/AsyncTask/协程misuse)",
"资源未关闭(Cursor/Stream/数据库连接等)",
"逻辑错误",
"性能问题(主线程阻塞、频繁GC、过度绘制等)",
],
},
"ios": {
"skip": "*.lock,*.json,*.toml,*.xml,*.txt,*.md,"
"*.pbxproj,*.xcscheme,*.xcworkspacedata,Pods/*,*.xib.nib",
"focus": [
"安全漏洞(硬编码密钥、ATS配置、Keychain误用等)",
"空值风险(强制解包 Force Unwrap、隐式解包)",
"内存泄漏(循环引用、delegate强引用、闭包捕获self)",
"线程安全问题(Data Race、非主线程更新UI)",
"资源未关闭(FileHandle/URLSession/Timer未invalidate)",
"逻辑错误",
"性能问题(主线程阻塞、离屏渲染、AutoLayout性能等)",
],
},
"frontend": {
"skip": "*.lock,*.json,*.md,*.txt,*.svg,*.png,*.jpg,*.ico,"
"*.css,*.scss,*.less,*.map,dist/*,node_modules/*,.next/*,build/*",
"focus": [
"安全漏洞(XSS、硬编码密钥/Token、innerHTML滥用、eval使用、CSRF等)",
"类型安全问题(any滥用、类型断言不当、缺少空值检查)",
"内存泄漏(未清理的定时器/事件监听/订阅、组件卸载未清理副作用)",
"异步问题(未处理的Promise rejection、竞态条件、async/await误用)",
"状态管理问题(不必要的重渲染、状态更新逻辑错误、props drilling)",
"逻辑错误",
"性能问题(大列表未虚拟化、频繁重渲染、bundle体积过大、图片未优化等)",
],
},
}
_profile = _PLATFORM_PROFILES.get(PLATFORM, {})
_default_skip = _profile.get("skip",
"*.lock,*.json,*.toml,*.xml,*.properties,*.pro,*.txt,*.md,"
"*.pbxproj,*.xcscheme,*.xcworkspacedata,Pods/*")
_default_focus = _profile.get("focus", [
"安全漏洞(硬编码密钥、SQL注入、XSS等)",
"空指针/空值风险",
"内存泄漏(循环引用、资源未释放等)",
"线程安全问题",
"资源未关闭(文件句柄/数据库连接/网络会话等)",
"逻辑错误",
"性能问题(主线程阻塞、不必要的重复计算等)",
])
# 环境变量仍可精细覆盖(优先级最高)
SKIP_PATTERNS = os.environ.get("AI_REVIEW_SKIP_PATTERNS", _default_skip).split(",")
FOCUS_AREAS = os.environ.get("AI_REVIEW_FOCUS_AREAS", "").split("|") \
if os.environ.get("AI_REVIEW_FOCUS_AREAS") else _default_focus
def format_duration(seconds):
if seconds < 60:
return f"{seconds:.1f}s"
return f"{int(seconds // 60)}m{int(seconds % 60)}s"
# -*- coding: utf-8 -*-
"""Git diff 获取、过滤、统计。"""
import fnmatch
import os
import subprocess
from ai_review.config import SKIP_PATTERNS
def get_ci_diff():
before_sha = os.environ.get("CI_COMMIT_BEFORE_SHA", "")
commit_sha = os.environ.get("CI_COMMIT_SHA", "HEAD")
if not before_sha or before_sha == "0000000000000000000000000000000000000000":
default_branch = os.environ.get("CI_DEFAULT_BRANCH", "master")
subprocess.run(["git", "fetch", "origin", default_branch], capture_output=True)
before_sha = f"origin/{default_branch}"
cmd = ["git", "diff", before_sha, commit_sha, "--diff-filter=ACMR", "--no-color"]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"git diff 失败: {result.stderr}")
cmd = ["git", "diff", "HEAD~1", "HEAD", "--diff-filter=ACMR", "--no-color"]
result = subprocess.run(cmd, capture_output=True, text=True)
return filter_diff(result.stdout)
def filter_diff(diff_text):
if not diff_text:
return ""
filtered_lines = []
skip_current = False
for line in diff_text.splitlines(True):
if line.startswith("diff --git"):
parts = line.split(" b/")
if len(parts) > 1:
filepath = parts[-1].strip()
skip_current = any(
fnmatch.fnmatch(filepath, p.strip()) or fnmatch.fnmatch(os.path.basename(filepath), p.strip())
for p in SKIP_PATTERNS
)
else:
skip_current = False
if not skip_current:
filtered_lines.append(line)
return "".join(filtered_lines)
def truncate_diff(diff_text, max_lines):
lines = diff_text.splitlines(True)
if len(lines) <= max_lines:
return diff_text, False
truncated = "".join(lines[:max_lines])
truncated += f"\n\n... [已截断,共 {len(lines)} 行,仅展示前 {max_lines} 行] ...\n"
return truncated, True
def diff_stats(diff_text):
lines = diff_text.splitlines()
added = sum(1 for l in lines if l.startswith("+") and not l.startswith("+++"))
removed = sum(1 for l in lines if l.startswith("-") and not l.startswith("---"))
files = sum(1 for l in lines if l.startswith("diff --git"))
return files, added, removed
# -*- coding: utf-8 -*-
"""LLM 调用与 prompt 构建。"""
import json
import urllib.request
import urllib.error
from ai_review.config import API_URL, API_KEY, MODEL, FOCUS_AREAS
def build_standard_prompt(diff_text):
focus_text = "\n".join(f" - {a}" for a in FOCUS_AREAS)
system_msg = "你是一位资深开发工程师,专门负责代码审查。请用中文回复,结论要可执行、可定位。"
user_msg = f"""请审查以下 git diff 代码变更,重点关注以下风险点:
{focus_text}
审查规则:
- 只基于 diff 中可见内容下结论,不要猜测未展示代码
- 每个问题必须给出定位(文件路径 + 尽量精确的行号范围)
- 每个问题必须包含:风险原因、触发条件、修复建议
- 无法确认的问题标记为"需补充上下文",不要当作高风险定论
- 优先发现真实风险,避免泛泛建议
请严格按以下格式回复:
## 🔴 高风险
(必须修复;如无写"无")
- [标题]
- 位置: path:line
- 问题: ...
- 风险: ...
- 建议: ...
## 🟡 中风险
(建议修复;如无写"无")
- [标题]
- 位置: path:line
- 问题: ...
- 风险: ...
- 建议: ...
## 🟢 低风险/建议
(可优化项;如无写"无")
## 📋 总结
(一句话总结 + 风险等级:✅ 安全 / ⚠️ 需关注 / 🚨 有风险)
## ✅ 建议动作
- [ ] 必须立即修复(最多 3 条)
- [ ] 可在后续迭代修复(最多 3 条)
如果没有发现明显问题,直接回复"✅ 代码审查通过,未发现明显风险点。"
---
以下是 diff 内容:
```diff
{diff_text}
```"""
return system_msg, user_msg
def build_deep_prompt(diff_text):
focus_text = "\n".join(f" - {a}" for a in FOCUS_AREAS)
system_msg = (
"你是一位资深安全与架构工程师,专门负责深度代码审查。"
"请充分运用推理能力,逐步分析代码变更的安全性、逻辑正确性和架构影响。"
"请用中文回复,结论要可执行、可定位。"
)
user_msg = f"""请对以下 git diff 代码变更进行深度审查,重点关注以下风险点:
{focus_text}
- 跨文件联动影响(接口变更、数据流变化)
- 安全攻击面评估(输入校验、权限控制、敏感数据处理)
- 并发与线程安全(竞态条件、死锁、数据竞争)
深度审查规则:
- 只基于 diff 中可见内容下结论,不要猜测未展示代码
- 每个问题必须给出定位(文件路径 + 尽量精确的行号范围)
- 每个问题必须包含:风险原因、触发条件、影响范围、修复建议
- 无法确认的问题标记为"需补充上下文",不要当作高风险定论
- 优先发现真实风险,避免泛泛建议
- 请逐步推理:先理解变更意图,再分析可能的风险路径
- 对安全相关变更,请考虑潜在的攻击向量和利用方式
请严格按以下格式回复:
## 🔴 高风险
(必须修复;如无写"无")
- [标题]
- 位置: path:line
- 问题: ...
- 风险: ...
- 影响范围: ...
- 建议: ...
## 🟡 中风险
(建议修复;如无写"无")
- [标题]
- 位置: path:line
- 问题: ...
- 风险: ...
- 影响范围: ...
- 建议: ...
## 🟢 低风险/建议
(可优化项;如无写"无")
## 🔍 深度分析
- 变更意图: (一句话概括此次变更的目的)
- 影响面: (列出可能受影响的模块/功能)
- 安全评估: (是否引入新的攻击面或安全风险)
## 📋 总结
(一句话总结 + 风险等级:✅ 安全 / ⚠️ 需关注 / 🚨 有风险)
## ✅ 建议动作
- [ ] 必须立即修复(最多 3 条)
- [ ] 可在后续迭代修复(最多 3 条)
如果没有发现明显问题,直接回复"✅ 代码审查通过,未发现明显风险点。"
---
以下是 diff 内容:
```diff
{diff_text}
```"""
return system_msg, user_msg
def call_llm(system_msg, user_msg, max_tokens=4096, timeout=300, model=None, enable_thinking=False):
payload = {
"model": model or MODEL,
"messages": [
{"role": "system", "content": system_msg},
{"role": "user", "content": user_msg}
],
"temperature": 0.1,
"max_tokens": max_tokens
}
if enable_thinking:
payload["enable_thinking"] = True
payload["thinking_budget"] = 8192
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {API_KEY}"
}
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(API_URL, data=data, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
result = json.loads(resp.read().decode("utf-8"))
choices = result.get("choices", [])
if not choices:
return "⚠️ API 返回了空结果,可能触发了内容安全策略。"
return choices[0].get("message", {}).get("content", "⚠️ API 返回格式异常")
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8") if e.fp else ""
return f"❌ API 请求失败 (HTTP {e.code}): {body}"
except urllib.error.URLError as e:
return f"❌ 网络错误: {e.reason}"
except Exception as e:
return f"❌ 调用失败: {e}"
# -*- coding: utf-8 -*-
"""主流程编排。"""
import os
import sys
import time
from ai_review.config import (
API_KEY, MODEL, TRIAGE_MODEL,
MAX_DIFF_LINES, STANDARD_MAX_TOKENS, DEEP_MAX_TOKENS,
STANDARD_TIMEOUT, DEEP_TIMEOUT,
format_duration,
)
from ai_review.git import get_ci_diff, truncate_diff, diff_stats
from ai_review.llm import call_llm, build_standard_prompt, build_deep_prompt
from ai_review.triage import (
build_triage_prompt, parse_triage_decision,
has_meaningful_code_change, security_keyword_gate,
)
from ai_review.notify import send_feishu
def main():
total_start = time.time()
if not API_KEY:
print("错误: 未设置 AI_REVIEW_API_KEY 环境变量")
print("请在 GitLab Settings > CI/CD > Variables 中添加。")
sys.exit(0)
branch = os.environ.get("CI_COMMIT_BRANCH", os.environ.get("CI_COMMIT_REF_NAME", "unknown"))
committer = os.environ.get("CI_COMMIT_AUTHOR", os.environ.get("GITLAB_USER_NAME", "unknown"))
commit_msg = os.environ.get("CI_COMMIT_MESSAGE", "")[:100]
project_name = os.environ.get("CI_PROJECT_NAME", "unknown")
commit_sha_short = os.environ.get("CI_COMMIT_SHORT_SHA", "")
pipeline_url = os.environ.get("CI_PIPELINE_URL", "")
print(f"🤖 AI 代码审查 - {project_name}/{branch}")
print(f" 提交者: {committer}")
print(f" 提交信息: {commit_msg}")
t_diff = time.time()
diff = get_ci_diff()
diff_elapsed = time.time() - t_diff
print(f" ⏱️ 阶段耗时 - 获取Diff: {format_duration(diff_elapsed)}")
if not diff.strip():
print("没有需要审查的代码变更,跳过。")
t_feishu = time.time()
send_feishu(
f"🤖 代码审查 - {project_name}/{branch}",
[
f"提交者: {committer}",
f"提交: {commit_sha_short} {commit_msg}",
f"分流模型: {TRIAGE_MODEL}",
"",
"✅ 没有需要审查的代码变更。",
]
)
print(f" ⏱️ 阶段耗时 - 飞书通知: {format_duration(time.time() - t_feishu)}")
print(f" ⏱️ 总耗时: {format_duration(time.time() - total_start)}")
return
files, added, removed = diff_stats(diff)
print(f" 变更文件: {files} 个, +{added} -{removed} 行")
analysis_start = time.time()
# 先由 LLM 做快速分流,决定是否需要完整审查
triage_max_lines = int(os.environ.get("AI_REVIEW_TRIAGE_MAX_DIFF_LINES", "300"))
triage_diff, _ = truncate_diff(diff, triage_max_lines)
print(" ⏳ 正在调用大模型分流判定...")
t_triage = time.time()
triage_system_msg, triage_user_msg = build_triage_prompt(triage_diff)
triage_result = call_llm(
triage_system_msg,
triage_user_msg,
max_tokens=256,
timeout=120,
model=TRIAGE_MODEL,
)
triage_elapsed = time.time() - t_triage
print(f" ⏱️ 阶段耗时 - 分流判定: {format_duration(triage_elapsed)}")
triage_decision, triage_reason, triage_category = parse_triage_decision(triage_result)
# 安全闸:只要检测到实质代码变更,就不允许 skip。
if triage_decision == "skip" and has_meaningful_code_change(triage_diff):
triage_decision = "standard"
triage_reason = "检测到实质代码变更,强制标准审查"
# 安全关键词闸:standard 命中安全关键词则提升为 deep
if triage_decision == "standard" and security_keyword_gate(diff):
triage_decision = "deep"
triage_reason = "检测到安全敏感关键词,提升为深度审查"
review_level = triage_decision # skip / standard / deep
print(f" 📌 分流决策: {review_level} | 类型: {triage_category} | 原因: {triage_reason}")
if triage_decision == "skip":
review_result = f"✅ 代码审查通过,LLM 判定可跳过完整审查:{triage_reason}"
print(f" ✅ 分流判定:跳过完整审查({triage_reason})")
elif triage_decision == "standard":
diff, was_truncated = truncate_diff(diff, MAX_DIFF_LINES)
if was_truncated:
print(f" ⚠️ diff 过长,已截断至 {MAX_DIFF_LINES} 行")
print(f" ⏳ 正在调用标准审查 ({TRIAGE_MODEL})...")
t_review = time.time()
system_msg, user_msg = build_standard_prompt(diff)
review_result = call_llm(
system_msg, user_msg,
max_tokens=STANDARD_MAX_TOKENS,
timeout=STANDARD_TIMEOUT,
model=TRIAGE_MODEL,
)
review_elapsed = time.time() - t_review
print(f" ⏱️ 阶段耗时 - 标准审查: {format_duration(review_elapsed)}")
else: # deep
diff, was_truncated = truncate_diff(diff, MAX_DIFF_LINES)
if was_truncated:
print(f" ⚠️ diff 过长,已截断至 {MAX_DIFF_LINES} 行")
print(f" ⏳ 正在调用深度审查 ({MODEL}, thinking mode)...")
t_review = time.time()
system_msg, user_msg = build_deep_prompt(diff)
review_result = call_llm(
system_msg, user_msg,
max_tokens=DEEP_MAX_TOKENS,
timeout=DEEP_TIMEOUT,
model=MODEL,
enable_thinking=True,
)
review_elapsed = time.time() - t_review
print(f" ⏱️ 阶段耗时 - 深度审查: {format_duration(review_elapsed)}")
elapsed = time.time() - analysis_start
elapsed_str = format_duration(elapsed)
print(f" ⏱️ 阶段耗时 - AI分析总计: {elapsed_str}")
print("\n" + "=" * 60)
print("📋 审查结果:")
print("=" * 60)
print(review_result)
print("=" * 60)
has_high_risk = "🔴" in review_result or "高风险" in review_result
is_pass = "✅" in review_result and not has_high_risk
risk_emoji = "✅" if is_pass else ("🚨" if has_high_risk else "⚠️")
# 构造 diff 链接
project_url = os.environ.get("CI_PROJECT_URL", "")
commit_sha_full = os.environ.get("CI_COMMIT_SHA", "")
diff_link = f"{project_url}/commit/{commit_sha_full}" if project_url and commit_sha_full else ""
_level_labels = {
"skip": "跳过",
"standard": f"标准审查 ({TRIAGE_MODEL})",
"deep": f"深度审查 ({MODEL}, thinking)",
}
review_level_label = _level_labels.get(review_level, review_level)
feishu_title = f"🤖 代码审查报告 - {project_name}/{branch} {risk_emoji}"
feishu_lines = [
f"提交者: {committer}",
f"分支: {branch}",
f"提交: {commit_sha_short} {commit_msg}",
f"变更: {files} 个文件, +{added} -{removed} 行",
f"分流模型: {TRIAGE_MODEL}",
f"审查层级: {review_level_label}",
f"变更类型: {triage_category}",
f"分析耗时: {elapsed_str}",
]
if diff_link:
feishu_lines.append(f"Diff: {diff_link}")
if pipeline_url:
feishu_lines.append(f"流水线: {pipeline_url}")
feishu_lines.append("")
feishu_lines.append("────── 代码审查结果 ──────")
feishu_lines.append("")
for line in review_result.splitlines():
feishu_lines.append(line)
t_feishu = time.time()
send_feishu(feishu_title, feishu_lines)
print(f" ⏱️ 阶段耗时 - 飞书通知: {format_duration(time.time() - t_feishu)}")
print(f" ⏱️ 总耗时: {format_duration(time.time() - total_start)}")
print("\n✅ 审查完成。")
# -*- coding: utf-8 -*-
"""飞书 Webhook 通知。"""
import json
import urllib.request
from ai_review.config import FEISHU_WEBHOOK
def send_feishu(title, content_lines):
if not FEISHU_WEBHOOK:
print("未配置 FEISHU_WEBHOOK_URL,跳过飞书通知。")
return
elements = []
for line in content_lines:
if isinstance(line, list):
elements.append(line)
else:
elements.append([{"tag": "text", "text": line}])
payload = {
"msg_type": "post",
"content": {
"post": {
"zh_cn": {
"title": title,
"content": elements
}
}
}
}
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
FEISHU_WEBHOOK,
data=data,
headers={"Content-Type": "application/json"},
method="POST"
)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
result = json.loads(resp.read().decode("utf-8"))
if result.get("code") == 0 or result.get("StatusCode") == 0:
print("✅ 飞书通知发送成功")
else:
print(f"⚠️ 飞书通知返回: {result}")
except Exception as e:
print(f"⚠️ 飞书通知发送失败: {e}")
# -*- coding: utf-8 -*-
"""分流判定:决定审查级别(skip / standard / deep)。"""
import json
import re
def build_triage_prompt(diff_text):
system_msg = "你是代码审查分流器。你的默认策略是保守:有任何不确定即 deep。"
user_msg = f"""请判断下面的 git diff 应该进行哪种级别的审查。
三级决策规则:
1. decision=skip: 仅当变更为纯空白/空行/注释/格式化,且不影响行为时
2. decision=standard: 简单重构(重命名、提取方法)、单文件bugfix、配置项变更、UI/样式调整、测试代码修改
3. decision=deep: 以下任一情况:
- 安全相关(密钥、认证、权限、加密、SQL、XSS、CSRF)
- 多文件联动逻辑变更(≥3个文件有逻辑关联)
- 新增或修改依赖
- 核心业务逻辑改动(支付、用户数据、数据库schema)
- 并发/线程安全相关
- 不确定归属哪个级别
变更类型 category 取值:refactor / bugfix / feature / security / config / dependency / test / other
只输出 JSON,不要输出其他内容:
{{"decision":"skip|standard|deep","reason":"一句话原因","category":"变更类型","evidence":"最多20字说明依据"}}
```diff
{diff_text}
```"""
return system_msg, user_msg
def parse_triage_decision(content):
if not content:
return "deep", "分流结果为空,默认深度审查", "unknown"
match = re.search(r"\{[\s\S]*\}", content)
raw_json = match.group(0) if match else content
try:
data = json.loads(raw_json)
decision = str(data.get("decision", "deep")).strip().lower()
reason = str(data.get("reason", "")).strip() or "未提供原因"
category = str(data.get("category", "other")).strip().lower()
if decision not in ("skip", "standard", "deep"):
return "deep", "分流结果非法,默认深度审查", category
return decision, reason, category
except Exception:
return "deep", "分流结果无法解析,默认深度审查", "unknown"
def has_meaningful_code_change(diff_text):
"""保守判定:只要存在非空白、非注释的增删行,就认为是实质代码变更。"""
comment_prefixes = ("#", "//", "/*", "*", "*/", "--")
for line in diff_text.splitlines():
if line.startswith("+++") or line.startswith("---"):
continue
if not (line.startswith("+") or line.startswith("-")):
continue
content = line[1:].strip()
if not content:
continue
if content.startswith(comment_prefixes):
continue
return True
return False
def security_keyword_gate(diff_text):
"""检测 diff 中是否包含安全敏感关键词,用于将 standard 提升为 deep。"""
pattern = re.compile(
r"(password|passwd|secret|private.?key|credential|"
r"access.?key|api.?key|auth.?token|bearer|"
r"encrypt|decrypt|cipher|hmac|"
r"injection|xss|csrf|ssrf|"
r"eval\s*\(|exec\s*\(|innerHTML|dangerouslySetInnerHTML|"
r"subprocess|os\.system|shell.?exec|"
r"chmod|chown|setuid|sudo)",
re.IGNORECASE
)
for line in diff_text.splitlines():
if not (line.startswith("+") or line.startswith("-")):
continue
if line.startswith("+++") or line.startswith("---"):
continue
if pattern.search(line):
return True
return False
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GitLab CI AI Code Review + 飞书通知
薄包装入口:兼容现有 CI 调用方式 `python3 ci_review.py`
实际逻辑已拆分至 ai_review/ 包。
"""
from ai_review.main import main
if __name__ == "__main__":
main()
# AI Code Review CI 模板
# 其他项目通过 include 引用此文件即可自动接入 AI 代码审查
#
# 使用方法:在项目的 .gitlab-ci.yml 中添加:
# include:
# - project: 'devops/ai-code-review'
# file: '/templates/ai-review.yml'
stages:
- review
ai-code-review:
stage: review
tags:
- code-review
variables:
GIT_DEPTH: "50"
GIT_STRATEGY: fetch
before_script:
# 从中央仓库拉取审查脚本(使用 CI_JOB_TOKEN 自动鉴权,无需额外配置)
- rm -rf /tmp/ai-code-review-${CI_JOB_ID}
- git clone --depth 1 https://gitlab-ci-token:${CI_JOB_TOKEN}@git.zmcms.cn/fht/ai-code-review.git /tmp/ai-code-review-${CI_JOB_ID}
script:
- python3 /tmp/ai-code-review-${CI_JOB_ID}/ci_review.py || python /tmp/ai-code-review-${CI_JOB_ID}/ci_review.py
after_script:
- rm -rf /tmp/ai-code-review-${CI_JOB_ID}
allow_failure: true
rules:
- if: $CI_PIPELINE_SOURCE == "push"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment