From f83a2034a8df8c2d3481590f72eff0ad2e4f30af Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 22 Jun 2026 10:27:20 +0000 Subject: [PATCH] =?UTF-8?q?fix(ai-review=20comment):=20=E9=A1=AF=E7=A4=BA?= =?UTF-8?q?=E5=AF=A9=E6=9F=A5=E5=95=8F=E9=A1=8C=E5=8E=9F=E5=9B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- app/comments.js | 2 +- app/comments.test.js | 15 ++++++++++++++- app/findings.js | 2 +- app/findings.test.js | 3 ++- app/roles.js | 1 + app/roles.test.js | 2 ++ 7 files changed, 23 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0f2b80b..cc0cec9 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ 3. 檢查是否為 AI 助理自動提交;若不是,選定 LLM provider/model、載入角色、取得 PR diff,將服務名稱、模型名稱與角色資訊 Comment 到 Pull Request,並讓每個角色個別分析 Git Diff 產生新問題表格(問題等級、角色名稱、問題位置或行數、修改建議) 4. 讀取來源分支中的所有未解決舊問題(問題檔案 `.gitea/ai-review/findings.json`)加上新問題後,去除重複產生本次 PR 的問題表格(PR問題表格)覆蓋問題檔案 5. 讀取來源分支中的排除問題檔案(`.gitea/ai-review/exclusions.json`),用來過濾 PR 問題表格中不需要處理的問題 -6. 將 PR 問題表格寫入 `.gitea/ai-review/findings.json`,並發布一個 Gitea Review:Review 本文用「嚴重/警告/建議」三欄統計各等級數量;之後將可找出檔案與行數的問題依照嚴重等級排序後加入 Review Comments 內,每個 Comment 包含嚴重等級/審查員/問題/建議 +6. 將 PR 問題表格寫入 `.gitea/ai-review/findings.json`,並發布一個 Gitea Review:Review 本文用「嚴重/警告/建議」三欄統計各等級數量;之後將可找出檔案與行數的問題依照嚴重等級排序後加入 Review Comments 內,每個 Comment 包含嚴重等級/審查員/問題/建議,其中「問題」是審查員判斷該處有問題的原因,不是檔案路徑或行號 7. 驗證來源分支中的 `findings.json` 與 `exclusions.json` 是否為合法 JSON array;格式錯誤時先嘗試透過 AI 修正內容,再重新驗證;修正後仍不合法才 exit 1;檔案不存在則建立並寫入 `[]` 8. Commit 問題檔案,只將 workspace 中實際存在的 `.gitea/ai-review/findings.json` 與 `.gitea/ai-review/exclusions.json` 覆蓋到記憶區;workspace 沒有的問題檔就略過。自動提交的 commit message 會帶上 `[ai-review-bot]`,供 workflow 判斷是否要跳過重跑 9. 如果 PR 問題表格中有嚴重問題,則不要讓 workflow 執行成功(exit 1) @@ -29,7 +29,7 @@ 6. API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,全部失敗則 exit 1 7. 讀取 Git Diff 時排除 `.gitea/`、`.github/` 資料夾,以及 `TODO.md`、`README.md`,避免 AI 分析 workflow 設定與文件等非業務程式碼 8. 階段七驗證來源分支中的 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,格式錯誤時先嘗試透過 AI 修正內容,再重新驗證;修正後仍不合法才 exit 1;之後才檢查檔案是否存在,不存在則建立並寫入 `[]` -9. 傳給 AI 的 findings 只保留必要欄位(level、role、location、suggestion),排除 `is_new` 等內部欄位;system prompt 精簡為指令核心;exclusions hint 只傳 location 與 suggestion,減少 token 用量 +9. 傳給 AI 的 findings 只保留必要欄位(level、role、location、problem、suggestion),排除 `is_new` 等內部欄位;system prompt 精簡為指令核心;exclusions hint 只傳 location 與 suggestion,減少 token 用量 10. 執行時會額外記錄來源分支狀態、`findings.json` / `exclusions.json` 的檔案路徑、大小、mtime 與 raw/normalized 筆數,方便追查讀檔與分支內容不一致的問題 11. action 一啟動就先做「前置驗證」(流程第 2 點):集中檢查 Gitea REST API token、comment token、git push 認證與 LLM 的所有驗證相關設定是否可用,全部通過才往下跑。驗證邏輯獨立成 `app/preflight.js`(git push 驗證委派給 `app/git.js` 的 `verifyRemoteAccess`),由 `main.js` 在 Step1 之後、其餘步驟之前呼叫;任何一項失敗都印出是哪一項、原因為何後 `exit 1`,避免在分析到一半、發 comment 或最後 push 時才因 token / key / 認證無效而中斷 diff --git a/app/comments.js b/app/comments.js index 399f870..587dc54 100644 --- a/app/comments.js +++ b/app/comments.js @@ -44,7 +44,7 @@ function inlineCommentBody(f) { } function problemText(f) { - return f.problem || f.title || f.message || f.location || '未提供問題位置'; + return f.problem || f.reason || f.description || f.detail || f.title || f.message || '未提供問題原因'; } function reviewCommentBody(f) { diff --git a/app/comments.test.js b/app/comments.test.js index aa945e6..a6de0d8 100644 --- a/app/comments.test.js +++ b/app/comments.test.js @@ -212,7 +212,8 @@ describe('postFindingsReview', () => { ); assert.match(reviewCalls[0].comments[0].body, /嚴重等級/); assert.match(reviewCalls[0].comments[0].body, /審查員.*Rex/s); - assert.match(reviewCalls[0].comments[0].body, /問題.*app\/a\.js:10/s); + assert.match(reviewCalls[0].comments[0].body, /問題.*未提供問題原因/s); + assert.doesNotMatch(reviewCalls[0].comments[0].body, /問題.*app\/a\.js:10/s); assert.match(reviewCalls[0].comments[0].body, /建議.*C/s); }); @@ -242,4 +243,16 @@ describe('postFindingsReview', () => { assert.match(reviewCalls[0].comments[0].body, /問題.*命名不清楚/s); assert.match(reviewCalls[0].comments[0].body, /建議.*改成具體名稱/s); }); + + it('uses reviewer reason fields as the problem text instead of the location', async () => { + const reviewCalls = []; + await postFindingsReview([ + { level: 'warning', role: 'Leo', location: 'app/a.js:5', description: '這裡缺少空值檢查', suggestion: '先判斷 null 再使用' }, + ], { + postReview: async (args) => { reviewCalls.push(args); }, + }); + + assert.match(reviewCalls[0].comments[0].body, /問題.*這裡缺少空值檢查/s); + assert.doesNotMatch(reviewCalls[0].comments[0].body, /問題.*app\/a\.js:5/s); + }); }); diff --git a/app/findings.js b/app/findings.js index f03b0c6..0a7a6f2 100644 --- a/app/findings.js +++ b/app/findings.js @@ -246,7 +246,7 @@ function fallback(label, findings, e) { /** 只保留 AI 需要的欄位,減少 token 用量 */ function toAIPayload(findings) { - return findings.map(({ level, role, location, suggestion }) => ({ level, role, location, suggestion })); + return findings.map(({ level, role, location, problem, suggestion }) => ({ level, role, location, problem, suggestion })); } /** diff --git a/app/findings.test.js b/app/findings.test.js index c1e2284..33c5b2e 100644 --- a/app/findings.test.js +++ b/app/findings.test.js @@ -115,7 +115,7 @@ describe('findings exclusions', () => { it('builds a compact exclusion hint for AI', async () => { const findings = [ - { level: 'warning', role: 'Maya', location: 'src/app.cs:12', suggestion: 'update tests' }, + { level: 'warning', role: 'Maya', location: 'src/app.cs:12', problem: '缺少測試驗證', suggestion: 'update tests' }, ]; const exclusions = [ { location: 'src/app.cs:1', original_finding: '更新套件後請補上測試驗證' }, @@ -138,6 +138,7 @@ describe('findings exclusions', () => { assert.ok(capturedSystemPrompt.includes('paths=src/app.cs, src/service.cs')); assert.ok(capturedSystemPrompt.includes('請確認安全性變更')); assert.ok(capturedUserContent.includes('"location":"src/app.cs:12"')); + assert.ok(capturedUserContent.includes('"problem":"缺少測試驗證"')); assert.ok(capturedUserContent.includes('"suggestion":"update tests"')); }); diff --git a/app/roles.js b/app/roles.js index 9c48b62..bcec715 100644 --- a/app/roles.js +++ b/app/roles.js @@ -71,6 +71,7 @@ export function buildAnalysisPrompt(role) { ' "level": "critical|warning|info",', ` "role": "${role.name}",`, ' "location": "檔案路徑:行號 或 檔案路徑",', + ' "problem": "繁體中文(台灣用語)說明審查員認為這裡有問題的原因,不要只填檔案路徑或行號",', ' "suggestion": "繁體中文(台灣用語)的具體修改建議"', '}', '', diff --git a/app/roles.test.js b/app/roles.test.js index b700a38..d06a502 100644 --- a/app/roles.test.js +++ b/app/roles.test.js @@ -68,6 +68,8 @@ describe('buildAnalysisPrompt', () => { it('embeds the role name in the JSON contract and persona/body', () => { const prompt = buildAnalysisPrompt(parseRoleFile(SAMPLE)); assert.match(prompt, /"role": "Tester"/); + assert.match(prompt, /"problem":/); + assert.match(prompt, /有問題的原因/); assert.match(prompt, /冷靜嚴謹/); assert.match(prompt, /審查重點:邊界與空值/); assert.match(prompt, /只回傳 JSON 陣列/);