feat: 新增 DecisionSignal 反馈、后验验证与统计 P5 #3332
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # PR 自动审查 - 语法检查 + AI 语义审查 | |
| # 当有 PR 创建或更新时自动触发 | |
| name: PR Review | |
| on: | |
| pull_request_target: | |
| types: [opened, synchronize, reopened] | |
| paths: | |
| - '**.py' | |
| - '**.md' | |
| - '**.ts' | |
| - '**.tsx' | |
| - 'docs/**' | |
| - 'README.md' | |
| - 'AGENTS.md' | |
| - 'apps/dsa-web/**' | |
| - 'requirements.txt' | |
| - '.github/requirements-ci.txt' | |
| - 'pyproject.toml' | |
| - 'setup.cfg' | |
| - '.github/PULL_REQUEST_TEMPLATE.md' | |
| - '.github/workflows/**' | |
| - '.github/scripts/**' | |
| - 'docker/Dockerfile' | |
| - 'docker-compose.yml' | |
| # 支持手动触发(用于重新审查) | |
| workflow_dispatch: | |
| # 限制并发,避免同一 PR 多次触发时重复评论 | |
| concurrency: | |
| group: pr-review-${{ github.event.pull_request.number || github.run_id }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| jobs: | |
| # ==================== 安全检查(检测敏感文件修改)==================== | |
| security-check: | |
| name: 🔒 安全检查 | |
| runs-on: ubuntu-latest | |
| outputs: | |
| safe_to_run: ${{ steps.check_sensitive.outputs.safe_to_run }} | |
| sensitive_files_changed: ${{ steps.check_sensitive.outputs.sensitive_files_changed }} | |
| steps: | |
| - name: 📥 检出代码 | |
| uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| fetch-depth: 0 | |
| - name: 🔄 获取 base 分支 | |
| run: git fetch origin ${{ github.base_ref || 'main' }}:refs/remotes/origin/${{ github.base_ref || 'main' }} | |
| - name: 🔒 检查敏感文件修改 | |
| id: check_sensitive | |
| run: | | |
| BASE_REF="${{ github.base_ref || 'main' }}" | |
| SENSITIVE_FILES=$(git diff --name-only origin/$BASE_REF...HEAD | grep -E '^(\.github/workflows/.*\.yml|\.github/scripts/.*\.py)$' || echo "") | |
| if [ -n "$SENSITIVE_FILES" ]; then | |
| echo "⚠️ **检测到敏感文件修改,需要人工审核!**" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "修改的敏感文件:" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| echo "$SENSITIVE_FILES" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "已标记为敏感变更,请重点人工复核;自动流程继续执行。" >> $GITHUB_STEP_SUMMARY | |
| echo "sensitive_files_changed=true" >> $GITHUB_OUTPUT | |
| echo "safe_to_run=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "✅ 未检测到敏感文件修改" >> $GITHUB_STEP_SUMMARY | |
| echo "sensitive_files_changed=false" >> $GITHUB_OUTPUT | |
| echo "safe_to_run=true" >> $GITHUB_OUTPUT | |
| fi | |
| # ==================== 静态检查(先于 AI 审查)==================== | |
| auto-check: | |
| name: 🔍 静态检查 | |
| runs-on: ubuntu-latest | |
| needs: [security-check] | |
| if: needs.security-check.outputs.safe_to_run == 'true' | |
| outputs: | |
| syntax_ok: ${{ steps.syntax.outputs.syntax_ok }} | |
| has_py_changes: ${{ steps.check_files.outputs.has_py_changes }} | |
| has_reviewable_changes: ${{ steps.check_files.outputs.has_reviewable_changes }} | |
| steps: | |
| - name: 📥 检出代码 | |
| uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| fetch-depth: 0 | |
| - name: 🔄 获取 base 分支 | |
| run: git fetch origin ${{ github.base_ref || 'main' }}:refs/remotes/origin/${{ github.base_ref || 'main' }} | |
| - name: 🐍 设置 Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.11' | |
| cache: 'pip' | |
| - name: 📦 安装依赖 | |
| run: | | |
| pip install --upgrade pip | |
| pip install flake8 | |
| - name: 📋 检查变更文件 | |
| id: check_files | |
| run: | | |
| BASE_REF="${{ github.base_ref || 'main' }}" | |
| CHANGED_FILES=$(git diff --name-only origin/$BASE_REF...HEAD -- '*.py' 2>/dev/null || echo "") | |
| REVIEWABLE_FILES=$(git diff --name-only origin/$BASE_REF...HEAD -- '*.py' '*.md' 'docs/**' 'README.md' 'AGENTS.md' 'requirements.txt' '.github/requirements-ci.txt' 'pyproject.toml' 'setup.cfg' '.github/PULL_REQUEST_TEMPLATE.md' '.github/workflows/**' '.github/scripts/**' 2>/dev/null || echo "") | |
| if [ -z "$REVIEWABLE_FILES" ]; then | |
| echo "has_reviewable_changes=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "has_reviewable_changes=true" >> $GITHUB_OUTPUT | |
| fi | |
| if [ -z "$CHANGED_FILES" ]; then | |
| echo "has_py_changes=false" >> $GITHUB_OUTPUT | |
| echo "✅ 没有修改 Python 文件" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "has_py_changes=true" >> $GITHUB_OUTPUT | |
| echo "CHANGED_FILES<<EOF" >> $GITHUB_ENV | |
| echo "$CHANGED_FILES" >> $GITHUB_ENV | |
| echo "EOF" >> $GITHUB_ENV | |
| fi | |
| - name: 🐍 Python 语法检查 | |
| id: syntax | |
| if: steps.check_files.outputs.has_py_changes == 'true' | |
| run: | | |
| echo "## 🐍 语法检查" >> $GITHUB_STEP_SUMMARY | |
| echo "检查文件: $CHANGED_FILES" | |
| ERRORS="" | |
| SYNTAX_OK="true" | |
| for file in $CHANGED_FILES; do | |
| if [ -f "$file" ]; then | |
| if ! python -m py_compile "$file" 2>&1; then | |
| ERRORS="$ERRORS\n❌ $file 语法错误" | |
| SYNTAX_OK="false" | |
| fi | |
| fi | |
| done | |
| echo "syntax_ok=$SYNTAX_OK" >> $GITHUB_OUTPUT | |
| if [ "$SYNTAX_OK" = "false" ]; then | |
| echo -e "$ERRORS" >> $GITHUB_STEP_SUMMARY | |
| exit 1 | |
| else | |
| echo "✅ 所有文件语法正确" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| - name: 🔎 Flake8 检查(严重错误) | |
| if: steps.check_files.outputs.has_py_changes == 'true' | |
| run: | | |
| echo "## 🔎 代码质量检查" >> $GITHUB_STEP_SUMMARY | |
| RESULT=$(flake8 $CHANGED_FILES --select=E9,F63,F7,F82 --format='%(path)s:%(row)d: %(code)s %(text)s' 2>/dev/null || true) | |
| if [ -n "$RESULT" ]; then | |
| echo "⚠️ 发现以下问题:" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| echo "$RESULT" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| exit 1 | |
| else | |
| echo "✅ 未发现严重代码问题" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| - name: 📊 变更统计 | |
| run: | | |
| BASE_REF="${{ github.base_ref || 'main' }}" | |
| echo "## 📊 变更统计" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| STATS=$(git diff --stat origin/$BASE_REF...HEAD 2>/dev/null | tail -1) | |
| echo "$STATS" >> $GITHUB_STEP_SUMMARY | |
| # ==================== AI 代码审查(依赖静态检查通过)==================== | |
| ai-review: | |
| name: 🤖 AI 代码审查 | |
| runs-on: ubuntu-latest | |
| needs: [security-check, auto-check] | |
| if: | | |
| needs.security-check.outputs.safe_to_run == 'true' && | |
| needs.auto-check.result == 'success' && | |
| needs.auto-check.outputs.has_reviewable_changes == 'true' && | |
| vars.ENABLE_AI_REVIEW != 'false' | |
| steps: | |
| # 先检出主分支(获取最新的 .github/scripts) | |
| - name: 📥 检出主分支脚本 | |
| uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ github.event.repository.default_branch }} | |
| sparse-checkout: | | |
| .github/scripts | |
| sparse-checkout-cone-mode: false | |
| path: main-scripts | |
| # 再检出 PR 代码(用于 diff 分析) | |
| - name: 📥 检出 PR 代码 | |
| uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| fetch-depth: 0 | |
| path: pr-code | |
| - name: 🔄 获取 base 分支 | |
| working-directory: pr-code | |
| run: git fetch origin ${{ github.base_ref || 'main' }}:refs/remotes/origin/${{ github.base_ref || 'main' }} | |
| - name: 🐍 设置 Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.11' | |
| - name: 📦 安装依赖 | |
| run: pip install google-genai openai httpx | |
| - name: 🤖 AI 审查代码变更 | |
| working-directory: pr-code | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GITHUB_BASE_REF: ${{ github.base_ref || 'main' }} | |
| GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} | |
| GEMINI_MODEL: ${{ vars.GEMINI_MODEL || 'gemini-2.5-flash' }} | |
| GEMINI_MODEL_FALLBACK: ${{ vars.GEMINI_MODEL_FALLBACK || 'gemini-2.5-flash' }} | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| OPENAI_BASE_URL: ${{ vars.OPENAI_BASE_URL }} | |
| OPENAI_MODEL: ${{ vars.OPENAI_MODEL }} | |
| AI_REVIEW_STRICT: ${{ vars.AI_REVIEW_STRICT || 'false' }} | |
| CI_SYNTAX_OK: ${{ needs.auto-check.outputs.syntax_ok || '' }} | |
| CI_HAS_PY_CHANGES: ${{ needs.auto-check.outputs.has_py_changes || 'false' }} | |
| CI_AUTO_CHECK_RESULT: ${{ needs.auto-check.result || '' }} | |
| run: python ../main-scripts/.github/scripts/ai_review.py | |
| - name: 📤 上传审查结果 | |
| uses: actions/upload-artifact@v6 | |
| if: always() | |
| with: | |
| name: ai-review-result | |
| path: pr-code/ai_review_result.txt | |
| if-no-files-found: ignore | |
| # ==================== PR 标签自动分类 ==================== | |
| labeler: | |
| name: 🏷️ 自动标签 | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: 📥 检出代码 | |
| uses: actions/checkout@v5 | |
| - name: 🏷️ 添加标签 | |
| if: github.event_name == 'pull_request_target' | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const { data: files } = await github.rest.pulls.listFiles({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.issue.number | |
| }); | |
| const labels = new Set(); | |
| for (const file of files) { | |
| const filename = file.filename.toLowerCase(); | |
| if (filename.includes('notification') || filename.includes('webhook')) { | |
| labels.add('notification'); | |
| } | |
| if (filename.includes('feishu')) { | |
| labels.add('feishu'); | |
| } | |
| if (filename.includes('data_provider') || filename.includes('fetcher')) { | |
| labels.add('data-source'); | |
| } | |
| if (filename.includes('analyzer') || filename.includes('ai')) { | |
| labels.add('ai'); | |
| } | |
| if (filename.endsWith('.md') || filename.includes('doc')) { | |
| labels.add('documentation'); | |
| } | |
| if (filename.includes('workflow') || filename.includes('.github')) { | |
| labels.add('ci/cd'); | |
| } | |
| if (filename.includes('config')) { | |
| labels.add('configuration'); | |
| } | |
| if (filename.includes('test')) { | |
| labels.add('testing'); | |
| } | |
| } | |
| const additions = files.reduce((sum, f) => sum + f.additions, 0); | |
| const deletions = files.reduce((sum, f) => sum + f.deletions, 0); | |
| const totalChanges = additions + deletions; | |
| if (totalChanges < 50) { | |
| labels.add('size/S'); | |
| } else if (totalChanges < 200) { | |
| labels.add('size/M'); | |
| } else if (totalChanges < 500) { | |
| labels.add('size/L'); | |
| } else { | |
| labels.add('size/XL'); | |
| } | |
| if (labels.size > 0) { | |
| try { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| labels: Array.from(labels) | |
| }); | |
| console.log(`Added labels: ${Array.from(labels).join(', ')}`); | |
| } catch (e) { | |
| console.log(`Failed to add labels: ${e.message}`); | |
| } | |
| } | |
| # ==================== 自动评论检查结果 ==================== | |
| comment: | |
| name: 💬 审查报告 | |
| runs-on: ubuntu-latest | |
| needs: [security-check, auto-check, ai-review] | |
| if: always() && github.event_name == 'pull_request_target' && needs.security-check.outputs.safe_to_run == 'true' | |
| steps: | |
| - name: 📥 检出代码 | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: 📥 下载 AI 审查结果 | |
| uses: actions/download-artifact@v7 | |
| if: needs.ai-review.result == 'success' | |
| continue-on-error: true | |
| with: | |
| name: ai-review-result | |
| path: . | |
| - name: 💬 生成审查报告 | |
| env: | |
| AUTO_CHECK_RESULT: ${{ needs.auto-check.result }} | |
| AI_REVIEW_RESULT: ${{ needs.ai-review.result }} | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const { data: files } = await github.rest.pulls.listFiles({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.issue.number | |
| }); | |
| const additions = files.reduce((sum, f) => sum + f.additions, 0); | |
| const deletions = files.reduce((sum, f) => sum + f.deletions, 0); | |
| const changedFiles = files.length; | |
| let aiReview = ''; | |
| try { | |
| aiReview = fs.readFileSync('ai_review_result.txt', 'utf8'); | |
| } catch (e) { | |
| console.log('No AI review result found'); | |
| } | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number | |
| }); | |
| const botComment = comments.find(c => | |
| c.user.type === 'Bot' && | |
| c.body.includes('## 🤖 自动审查报告') | |
| ); | |
| const checkStatus = process.env.AUTO_CHECK_RESULT === 'success' ? '✅ 通过' : '⚠️ 有问题'; | |
| const aiStatus = process.env.AI_REVIEW_RESULT === 'success' ? '✅ 已完成' : | |
| process.env.AI_REVIEW_RESULT === 'skipped' ? '⏭️ 跳过' : '⚠️ 失败'; | |
| let report = `## 🤖 自动审查报告 | |
| | 项目 | 结果 | | |
| |------|------| | |
| | 📊 变更文件 | ${changedFiles} 个 | | |
| | ➕ 新增行数 | ${additions} 行 | | |
| | ➖ 删除行数 | ${deletions} 行 | | |
| | 🔍 静态检查 | ${checkStatus} | | |
| | 🧠 AI 审查 | ${aiStatus} | | |
| ### 📁 修改的文件 | |
| `; | |
| for (const file of files.slice(0, 20)) { | |
| const status = file.status === 'added' ? '🆕' : | |
| file.status === 'removed' ? '🗑️' : '📝'; | |
| report += `- ${status} \`${file.filename}\` (+${file.additions}/-${file.deletions})\n`; | |
| } | |
| if (files.length > 20) { | |
| report += `\n... 还有 ${files.length - 20} 个文件\n`; | |
| } | |
| if (aiReview) { | |
| report += ` | |
| --- | |
| ### 🧠 AI 代码审查意见 | |
| ${aiReview} | |
| `; | |
| } | |
| report += ` | |
| --- | |
| > 💡 **提示**: 请确保代码已通过本地测试,并遵循项目代码规范。 | |
| `; | |
| if (botComment) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: botComment.id, | |
| body: report | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: report | |
| }); | |
| } |