From bb7fa425db9512daf9a4f9e3371eff2914b79c99 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 01:35:57 +0000 Subject: [PATCH] fix: align flow with README, add Step4 exclusions filter, fix step numbers --- TODO.md | 6 ++++-- app/config.js | 1 + app/findings.js | 39 ++++++++++++++++++++++++++++++++++++++- app/main.js | 38 +++++++++++++++++++++----------------- 4 files changed, 64 insertions(+), 20 deletions(-) diff --git a/TODO.md b/TODO.md index bb6edcd..a6fd2e7 100644 --- a/TODO.md +++ b/TODO.md @@ -8,14 +8,17 @@ ## 階段二:Findings 產生與合併 - 目標:各角色(style/security/performance/maintainability/testing)能產生 findings,並正確合併新舊 findings。 - 驗收:log 中能看到每個角色 findings 數量、合併後 findings 統計,並有「Step3: merged findings total=...」等訊息。 +- 完成 ## 階段三:AI 去重與角色確認 - 目標:嘗試呼叫 LLM 進行 findings 去重與角色確認,API 額度不足時要有降級處理 log。 - 驗收:log 中能看到 deduplication/resolution confirmation 成功或失敗(如 402),降級時有「保留所有問題」等明確訊息。 +- 完成 ## 階段四:排除問題過濾 - 目標:讀取排除問題檔案,過濾 PR 問題表格中不需要處理的問題。 - 驗收:log 中能看到排除問題檔案讀取成功或不存在的訊息,以及過濾後 findings 數量變化。 +- 完成 ## 階段五:findings 寫入與 comment 發布 - 目標:findings.jsonl 正確寫入,comment 發布順序正確(舊問題→非嚴重→嚴重),每步有 log。 @@ -31,7 +34,6 @@ --- - 每個階段都會加上明確的 log,並確保即使部分功能未完成也能降級執行、不會中斷 pipeline。 -每次執行後請貼 log,我會協助 debug。 \ No newline at end of file +每次執行後請貼 log,我會協助 debug。 diff --git a/app/config.js b/app/config.js index ea20e2c..c5e7caa 100644 --- a/app/config.js +++ b/app/config.js @@ -6,6 +6,7 @@ export const PR_HEAD_BRANCH = process.env.PR_HEAD_BRANCH || ''; export const PR_BASE_BRANCH = process.env.PR_BASE_BRANCH || ''; export const FINDINGS_PATH = '.gitea/ai-review/findings.json'; +export const EXCLUSIONS_PATH = '.gitea/ai-review/exclusions.json'; export function getLLMConfig() { const checks = [ diff --git a/app/findings.js b/app/findings.js index 7c38515..cb65dbd 100644 --- a/app/findings.js +++ b/app/findings.js @@ -1,7 +1,7 @@ import fs from 'fs'; import path from 'path'; import { chatJSON } from './llm.js'; -import { FINDINGS_PATH } from './config.js'; +import { FINDINGS_PATH, EXCLUSIONS_PATH } from './config.js'; const LEVELS = ['critical', 'warning', 'info']; @@ -93,3 +93,40 @@ export async function deduplicateWithAI(findings) { return findings; } } + +/** + * 讀取排除問題檔案(從 workspace 的 EXCLUSIONS_PATH) + * 格式:[{ role, location, suggestion }],欄位可部分省略,省略表示萬用 + */ +export function loadExclusions(workspace) { + const fullPath = path.join(workspace, EXCLUSIONS_PATH); + if (!fs.existsSync(fullPath)) { + console.log(' 排除問題檔案不存在,跳過過濾'); + return []; + } + try { + const data = JSON.parse(fs.readFileSync(fullPath, 'utf8')); + const exclusions = Array.isArray(data) ? data : []; + console.log(` 讀取排除問題: ${exclusions.length} 筆`); + return exclusions; + } catch (e) { + console.log(` ⚠️ 讀取排除問題失敗: ${e.message},跳過過濾`); + return []; + } +} + +/** + * 套用排除規則,過濾掉符合排除條件的 findings + * 排除條件:role/location/suggestion 皆符合(省略的欄位視為萬用) + */ +export function applyExclusions(findings, exclusions) { + if (exclusions.length === 0) return findings; + const before = findings.length; + const filtered = findings.filter(f => !exclusions.some(ex => + (!ex.role || ex.role === f.role) && + (!ex.location || ex.location === f.location) && + (!ex.suggestion || String(f.suggestion).startsWith(String(ex.suggestion).slice(0, 50))) + )); + console.log(` 排除過濾: ${before} -> ${filtered.length} 筆(排除 ${before - filtered.length} 筆)`); + return filtered; +} diff --git a/app/main.js b/app/main.js index 5e13dc8..ef0b173 100644 --- a/app/main.js +++ b/app/main.js @@ -1,7 +1,7 @@ import { GITEA_REPOSITORY, PR_NUMBER, PR_HEAD_BRANCH, PR_BASE_BRANCH, getLLMConfig } from './config.js'; import { loadRoles, getRoleIntro } from './roles.js'; import { getPRDiff, postComment } from './gitea.js'; -import { analyzeWithRole, loadOldFindings, mergeFindings, sortByLevel, deduplicateWithAI } from './findings.js'; +import { analyzeWithRole, loadOldFindings, mergeFindings, sortByLevel, deduplicateWithAI, loadExclusions, applyExclusions } from './findings.js'; import { saveFindings, postOldFindingsComment, postNewNonCriticalComment, postNewCriticalComments } from './comments.js'; import { commitAndPush } from './git.js'; @@ -26,7 +26,6 @@ async function main() { console.log(` 已載入 ${roles.length} 個角色: [${roles.map(r => r.name).join(', ')}]`); // 取得 PR diff - console.log('\n📋 Step1: 取得 PR Diff'); let diff; try { diff = await getPRDiff(); @@ -42,7 +41,6 @@ async function main() { } // 發布角色介紹 comment - console.log('\n💬 Step1: 發布角色介紹 Comment'); try { const intro = getRoleIntro(roles) + `\n\n> 🔍 服務:${provider} 模型:${model}`; await postComment(intro); @@ -50,6 +48,7 @@ async function main() { } catch (e) { console.log(` ⚠️ comment 發布失敗(繼續執行): ${e.message}`); } + console.log(' Step1 完成'); // Step2: 各角色分析 diff 產生新 findings console.log('\n📊 Step2: Findings 產生'); @@ -64,38 +63,43 @@ async function main() { } console.log(` Step2 完成: 新 findings 總計 ${newFindings.length} 筆`); - // Step3: 讀取舊 findings,合併去重 + // Step3: 讀取舊 findings,合併去重(含 AI 語意去重) console.log('\n🔀 Step3: Findings 合併'); const oldFindings = loadOldFindings(WORKSPACE); const mergedFindings = mergeFindings(oldFindings, newFindings); console.log(` Step3 merged findings total=${mergedFindings.length}`); - // Step3b: AI 語意去重 console.log('\n🤖 Step3b: AI 語意去重'); const deduped = await deduplicateWithAI(mergedFindings); const sorted = sortByLevel(deduped); console.log(` Step3b dedup findings total=${sorted.length} (critical=${sorted.filter(f=>f.level==='critical').length} warning=${sorted.filter(f=>f.level==='warning').length} info=${sorted.filter(f=>f.level==='info').length})`); - // Step4: 寫入 findings.json,依序發布 comment - console.log('\n📝 Step4: Findings 寫入與 Comment 發布'); - saveFindings(WORKSPACE, sorted); + // Step4: 讀取排除問題檔案,過濾 PR 問題表格 + console.log('\n🚫 Step4: 排除問題過濾'); + const exclusions = loadExclusions(WORKSPACE); + const filtered = applyExclusions(sorted, exclusions); + console.log(` Step4 完成: findings total=${filtered.length}`); + + // Step5: 寫入 findings.json,依序發布 comment + console.log('\n📝 Step5: Findings 寫入與 Comment 發布'); + saveFindings(WORKSPACE, filtered); try { - await postOldFindingsComment(sorted); - await postNewNonCriticalComment(sorted); - await postNewCriticalComments(sorted); - console.log(' Step4 完成'); + await postOldFindingsComment(filtered); + await postNewNonCriticalComment(filtered); + await postNewCriticalComments(filtered); + console.log(' Step5 完成'); } catch (e) { console.log(` ⚠️ comment 發布失敗(繼續執行): ${e.message}`); } - // Step5: commit/push findings.json 到來源分支 - console.log('\n💾 Step5: 記憶區 Commit/Push'); + // Step6: commit/push findings.json 到來源分支 + console.log('\n💾 Step6: 記憶區 Commit/Push'); await commitAndPush(WORKSPACE); - // Step6: 有 critical 問題則 exit 1 - console.log('\n🚦 Step6: 嚴重問題檢查'); - const criticalCount = sorted.filter(f => f.level === 'critical').length; + // Step7: 有 critical 問題則 exit 1 + console.log('\n🚦 Step7: 嚴重問題檢查'); + const criticalCount = filtered.filter(f => f.level === 'critical').length; if (criticalCount > 0) { console.log(` ❌ 發現 ${criticalCount} 個嚴重問題,workflow 結束(exit 1)`); console.log('='.repeat(60));