Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions .github/CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,14 @@ comment(汇总报告,发表/更新 PR 评论)

### Secrets(必须)

- `GEMINI_API_KEY`:Gemini API Key
- `OPENAI_API_KEY`:OpenAI 兼容 API Key(备用)
- `OPENAI_API_KEY`:OpenAI 兼容 API Key(推荐,支持 DeepSeek / OpenAI / 国产模型)
- `GEMINI_API_KEY`:Gemini API Key(备用)

### Variables(可选)

- `ENABLE_AI_REVIEW`:设为 `false` 关闭 AI 审查
- `OPENAI_BASE_URL`:API 地址。**DeepSeek** 填 `https://api.deepseek.com/v1`
- `OPENAI_MODEL`:模型名。**DeepSeek** 填 `deepseek-chat` 或 `deepseek-coder`
- `GEMINI_MODEL_FALLBACK`:Gemini 模型,默认 `gemini-2.5-flash`
- `OPENAI_BASE_URL`:OpenAI 兼容 API 地址
- `OPENAI_MODEL`:OpenAI 模型,默认 `gpt-4o-mini`

### Labels(需预先创建)

Expand Down
25 changes: 17 additions & 8 deletions .github/scripts/ai_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,28 @@ def call_gemini(prompt: str) -> str | None:
model=model,
contents=prompt,
)
if response and response.text:
return response.text.strip()
if response:
text = getattr(response, "text", None)
if text:
return str(text).strip()
if hasattr(response, "candidates") and response.candidates:
c = response.candidates[0]
if hasattr(c, "content") and c.content and hasattr(c.content, "parts"):
for p in c.content.parts:
if hasattr(p, "text") and p.text:
return str(p.text).strip()
return None
except Exception:
return None


def call_openai_compatible(prompt: str) -> str | None:
"""Call OpenAI-compatible API, return response text or None on failure."""
"""Call OpenAI-compatible API (DeepSeek/OpenAI/国产模型等)."""
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
return None
base_url = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1")
model = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
base_url = os.environ.get("OPENAI_BASE_URL") or "https://api.openai.com/v1"
model = os.environ.get("OPENAI_MODEL") or "gpt-4o-mini"
try:
from openai import OpenAI

Expand Down Expand Up @@ -139,12 +147,13 @@ def main() -> int:

prompt = build_prompt(files, diff)

result = call_gemini(prompt)
# 优先使用 OpenAI 兼容接口(DeepSeek/OpenAI/国产模型等),否则用 Gemini
result = call_openai_compatible(prompt)
if result is None:
result = call_openai_compatible(prompt)
result = call_gemini(prompt)
if result is None:
result = (
"⚠️ AI 审查未执行:请配置 `GEMINI_API_KEY` 或 `OPENAI_API_KEY`。"
"⚠️ AI 审查未执行:请配置 `OPENAI_API_KEY`(DeepSeek 等)或 `GEMINI_API_KEY`。"
)

write_result(result)
Expand Down
76 changes: 51 additions & 25 deletions .github/workflows/pr-review.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
name: PR Review

on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
paths:
- 'backend/**/*.py'
- 'backend/requirements.txt'
- 'Dockerfile'
- 'docker-compose.yml'
- '.github/workflows/**'
- '.github/scripts/**'
pull_request_target:
branches: [main]
types: [opened, synchronize, reopened]
Expand All @@ -10,6 +20,7 @@ on:
- 'Dockerfile'
- 'docker-compose.yml'
- '.github/workflows/**'
- '.github/scripts/**'
workflow_dispatch:

permissions:
Expand All @@ -21,10 +32,17 @@ env:
BASE_REF: ${{ github.event.pull_request.base.ref || github.ref_name }}

jobs:
guard:
name: Guard
runs-on: ubuntu-latest
if: github.event.pull_request == null
steps:
- run: echo "No PR context, skipping PR Review"

security-check:
name: Security Check
runs-on: ubuntu-latest
if: github.event.pull_request
if: github.event.pull_request != null
outputs:
safe_to_run: ${{ steps.check.outputs.safe_to_run }}
steps:
Expand Down Expand Up @@ -104,8 +122,7 @@ jobs:
always() &&
needs.security-check.outputs.safe_to_run == 'true' &&
needs.auto-check.outputs.has_py_changes == 'true' &&
needs.auto-check.result == 'success' &&
(vars.ENABLE_AI_REVIEW != 'false')
needs.auto-check.result == 'success'
steps:
- name: Checkout scripts from default branch
uses: actions/checkout@v4
Expand Down Expand Up @@ -138,10 +155,10 @@ jobs:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GEMINI_MODEL_FALLBACK: ${{ vars.GEMINI_MODEL_FALLBACK || 'gemini-2.5-flash' }}
OPENAI_BASE_URL: ${{ vars.OPENAI_BASE_URL || '' }}
OPENAI_MODEL: ${{ vars.OPENAI_MODEL || 'gpt-4o-mini' }}
OPENAI_BASE_URL: ${{ vars.OPENAI_BASE_URL }}
OPENAI_MODEL: ${{ vars.OPENAI_MODEL }}
run: |
python ../main-scripts/.github/scripts/ai_review.py
python ../main-scripts/.github/scripts/ai_review.py || echo "⚠️ AI 审查未执行:请配置 GEMINI_API_KEY 或 OPENAI_API_KEY" > ai_review_result.txt

- name: Upload AI review result
uses: actions/upload-artifact@v4
Expand All @@ -152,12 +169,13 @@ jobs:
labeler:
name: Labeler
runs-on: ubuntu-latest
if: github.event.pull_request
if: github.event.pull_request != null
steps:
- name: Apply labels
uses: actions/github-script@v7
with:
script: |
try {
const pr = context.payload.pull_request;
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
Expand Down Expand Up @@ -190,14 +208,15 @@ jobs:
labels: allLabels,
});
}
} catch (e) {
console.log('Labeler skipped or failed:', e.message);
}

comment:
name: Comment Report
runs-on: ubuntu-latest
needs: [security-check, auto-check, ai-review]
if: |
always() &&
needs.security-check.outputs.safe_to_run == 'true'
if: always() && github.event.pull_request != null
steps:
- name: Download AI review artifact
uses: actions/download-artifact@v4
Expand All @@ -210,6 +229,7 @@ jobs:
with:
script: |
const fs = require('fs');
const safeToRun = '${{ needs.security-check.outputs.safe_to_run }}' === 'true';
let aiResult = 'AI 审查未执行或未产生结果。';
if (fs.existsSync('ai_review_result.txt')) {
aiResult = fs.readFileSync('ai_review_result.txt', 'utf8');
Expand All @@ -224,21 +244,27 @@ jobs:
const additions = files.reduce((a, f) => a + (f.additions || 0), 0);
const deletions = files.reduce((a, f) => a + (f.deletions || 0), 0);
const autoOk = '${{ needs.auto-check.outputs.syntax_ok }}' === 'true';
const body = `## 🤖 自动审查报告

### 变更统计
- 变更文件:${files.length}
- 新增行:+${additions}
- 删除行:-${deletions}

### 静态检查
${autoOk ? '✅ 通过' : '❌ 未通过'}

### AI 代码审查
${aiResult}

---
*本报告由 CI 自动生成*`;
if (!safeToRun) {
aiResult = '⚠️ 本 PR 修改了 `.github/workflows` 或 `.github/scripts`,出于安全考虑已跳过 AI 审查。合并后,后续仅修改业务代码的 PR 将获得完整审查。';
}
const staticStatus = !safeToRun ? '⏭️ 已跳过(敏感文件变更)' : (autoOk ? '✅ 通过' : '❌ 未通过');
const body = [
'## 🤖 自动审查报告',
'',
'### 变更统计',
'- 变更文件:' + files.length,
'- 新增行:+' + additions,
'- 删除行:-' + deletions,
'',
'### 静态检查',
staticStatus,
'',
'### AI 代码审查',
aiResult,
'',
'\u002D\u002D\u002D',
'*本报告由 CI 自动生成*',
].join('\n');

const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
Expand Down
1 change: 1 addition & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
ChatRaw - Minimalist AI Chat Interface
Python + FastAPI Backend
CI test: verify automation workflows (trigger AI review).
"""

import os
Expand Down
Loading