From a0e69b4e826e9327020ca7bbedd3cae9f958d20b Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 02:12:26 +0000 Subject: [PATCH] feat: add AI false positive filtering in Step4 --- app/findings.js | 34 ++++++++++++++++++++++++++++++++++ app/main.js | 7 ++++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/app/findings.js b/app/findings.js index 118b9e4..7dcf060 100644 --- a/app/findings.js +++ b/app/findings.js @@ -130,3 +130,37 @@ export function applyExclusions(findings, exclusions) { console.log(` 排除過濾: ${before} -> ${filtered.length} 筆(排除 ${before - filtered.length} 筆)`); return filtered; } + +/** + * 呼叫 AI 判斷哪些問題是誤報或不需處理,回傳需保留的 findings + * 失敗時降級回傳原始 findings + */ +export async function filterFalsePositivesWithAI(findings) { + if (findings.length === 0) return findings; + + const systemPrompt = `你是一位資深程式碼審查專家,負責判斷審查問題是否為誤報或不需處理。 +給你一份問題清單(JSON 陣列),每筆包含 level、role、location、suggestion。 +請移除以下類型的問題: +1. 誤報:問題描述與實際程式碼不符(例如:程式碼已正確使用環境變數或 secrets,卻被標記為硬編碼敏感資料) +2. 不適用:問題在此專案情境下不需處理(例如:CI/CD action 本來就需要透過環境變數傳遞 token) +只回傳需要保留的問題 JSON 陣列,不要有其他文字。`; + + const userContent = `請判斷以下問題清單,移除誤報或不需處理的問題:\n\n${JSON.stringify(findings, null, 2)}`; + + try { + const result = await chatJSON(systemPrompt, userContent); + if (Array.isArray(result)) { + console.log(` AI 誤報過濾: ${findings.length} -> ${result.length} 筆`); + return result; + } + throw new Error('AI 回傳非陣列'); + } catch (e) { + const status = e.response?.status; + if (status === 402 || status === 429) { + console.log(` ⚠️ AI 誤報過濾失敗(${status} 額度/限流),降級:保留所有問題`); + } else { + console.log(` ⚠️ AI 誤報過濾失敗(${e.message}),降級:保留所有問題`); + } + return findings; + } +} diff --git a/app/main.js b/app/main.js index b3bd670..de40363 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, loadExclusions, applyExclusions } from './findings.js'; +import { analyzeWithRole, loadOldFindings, mergeFindings, sortByLevel, deduplicateWithAI, loadExclusions, applyExclusions, filterFalsePositivesWithAI } from './findings.js'; import { saveFindings, postOldFindingsComment, postNewNonCriticalComment, postNewCriticalComments } from './comments.js'; import { cloneRepo, commitAndPush } from './git.js'; @@ -81,10 +81,11 @@ async function main() { 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: 讀取排除問題檔案,過濾 PR 問題表格 + // Step4: 讀取排除問題檔案,過濾 PR 問題表格,並請 AI 判斷誤報 console.log('\n🚫 Step4: 排除問題過濾'); const exclusions = loadExclusions(repoDir || WORKSPACE); - const filtered = applyExclusions(sorted, exclusions); + const ruleFiltered = applyExclusions(sorted, exclusions); + const filtered = await filterFalsePositivesWithAI(ruleFiltered); console.log(` Step4 完成: findings total=${filtered.length}`); // Step5: 寫入 findings.json,依序發布 comment