From d95213334b9070bb9b5f2fbec864908ef941c881 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 18:17:14 +0800 Subject: [PATCH 01/29] feat: add concurrency settings and branch ignore for pull request workflows --- README.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e1717ef..bd29c03 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,13 @@ ### 1. OpenAI ```yaml name: AI +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true on: pull_request: + branches-ignore: + - master types: [opened, synchronize] jobs: code-review: @@ -55,8 +60,13 @@ jobs: ### 2. OpenRouter ```yaml name: AI +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true on: pull_request: + branches-ignore: + - master types: [opened, synchronize] jobs: code-review: @@ -78,8 +88,13 @@ jobs: ### 3. Anthropic Claude ```yaml name: AI +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true on: pull_request: + branches-ignore: + - master types: [opened, synchronize] jobs: code-review: @@ -100,8 +115,13 @@ jobs: ### 4. Google Gemini ```yaml name: AI +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true on: pull_request: + branches-ignore: + - master types: [opened, synchronize] jobs: code-review: @@ -123,8 +143,13 @@ jobs: ### 5. Amazon Q ```yaml name: AI +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true on: pull_request: + branches-ignore: + - master types: [opened, synchronize] jobs: code-review: @@ -146,8 +171,13 @@ jobs: ```yaml name: AI +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true on: pull_request: + branches-ignore: + - master types: [opened, synchronize] jobs: code-review: @@ -162,6 +192,5 @@ jobs: permissions: contents: write pull-requests: write - issues: write ``` \ No newline at end of file From 818342d27bcff12d1a8fc50ef16e91c390175748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B3=BB=E7=B5=B1=E7=AE=A1=E7=90=86=E5=93=A1?= Date: Tue, 12 May 2026 10:19:10 +0000 Subject: [PATCH 02/29] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bd29c03..c7d0e9e 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ on: types: [opened, synchronize] jobs: code-review: - name: 'Code Review' + name: Code Review runs-on: ubuntu steps: - name: AI Code Review @@ -70,7 +70,7 @@ on: types: [opened, synchronize] jobs: code-review: - name: 'Code Review' + name: Code Review runs-on: ubuntu steps: - name: AI Code Review @@ -98,7 +98,7 @@ on: types: [opened, synchronize] jobs: code-review: - name: 'Code Review' + name: Code Review runs-on: ubuntu steps: - name: AI Code Review @@ -125,7 +125,7 @@ on: types: [opened, synchronize] jobs: code-review: - name: 'Code Review' + name: Code Review runs-on: ubuntu steps: - name: AI Code Review @@ -153,7 +153,7 @@ on: types: [opened, synchronize] jobs: code-review: - name: 'Code Review' + name: Code Review runs-on: ubuntu steps: - name: AI Code Review @@ -181,7 +181,7 @@ on: types: [opened, synchronize] jobs: code-review: - name: 'Code Review' + name: Code Review runs-on: ubuntu steps: - name: AI Code Review From 8878165a81ae9019bfc130df057c8a02b6a24779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B3=BB=E7=B5=B1=E7=AE=A1=E7=90=86=E5=93=A1?= Date: Tue, 12 May 2026 10:19:32 +0000 Subject: [PATCH 03/29] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20.gitea/workflows/rev?= =?UTF-8?q?iew.yaml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/review.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 5c36cc4..7928d34 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -26,7 +26,7 @@ jobs: tag_name: v${{ steps.version.outputs.version }} target_commitish: ${{ github.head_ref }} code-review: - name: 'Code Review' + name: Code Review runs-on: ubuntu needs: [version] steps: From abfd594bb2a20a6794ad932fe67cec4dcd686662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B3=BB=E7=B5=B1=E7=AE=A1=E7=90=86=E5=93=A1?= Date: Tue, 12 May 2026 10:21:27 +0000 Subject: [PATCH 04/29] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20.gitea/workflows/rev?= =?UTF-8?q?iew.yaml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/review.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 7928d34..d7fcbcc 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -31,7 +31,7 @@ jobs: needs: [version] steps: - name: AI Code Review - uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} + uses: https://gitea.jsc.idv.tw/actions/code-review@v${{ needs.version.outputs.version }} with: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }},${{ secrets.GEMINI_API_KEY_1 }},${{ secrets.GEMINI_API_KEY_2 }},${{ secrets.GEMINI_API_KEY_3 }},${{ secrets.GEMINI_API_KEY_4 }},${{ secrets.GEMINI_API_KEY_5 }},${{ secrets.GEMINI_API_KEY_6 }},${{ secrets.GEMINI_API_KEY_7 }},${{ secrets.GEMINI_API_KEY_8 }},${{ secrets.GEMINI_API_KEY_9 }},${{ secrets.GEMINI_API_KEY_10 }},${{ secrets.GEMINI_API_KEY_11 }},${{ secrets.GEMINI_API_KEY_12 }},${{ secrets.GEMINI_API_KEY_13 }},${{ secrets.GEMINI_API_KEY_14 }},${{ secrets.GEMINI_API_KEY_15 }},${{ secrets.GEMINI_API_KEY_16 }},${{ secrets.GEMINI_API_KEY_17 }},${{ secrets.GEMINI_API_KEY_18 }},${{ secrets.GEMINI_API_KEY_19 }} GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta From fe7381c36e75bfd550429cdb5fc4463e2020fc83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B3=BB=E7=B5=B1=E7=AE=A1=E7=90=86=E5=93=A1?= Date: Tue, 12 May 2026 10:22:02 +0000 Subject: [PATCH 05/29] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c7d0e9e..9de144f 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ jobs: runs-on: ubuntu steps: - name: AI Code Review - uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} + uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} with: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} # 支援逗號分隔多個 Key OPENAI_BASE_URL: https://api.openai.com/v1 @@ -74,7 +74,7 @@ jobs: runs-on: ubuntu steps: - name: AI Code Review - uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} + uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} with: OPENAI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }},${{ secrets.OPENROUTER_API_KEY_1 }} OPENAI_BASE_URL: https://openrouter.ai/api/v1 @@ -102,7 +102,7 @@ jobs: runs-on: ubuntu steps: - name: AI Code Review - uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} + uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} with: CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} # 支援逗號分隔多個 Key CLAUDE_BASE_URL: https://api.anthropic.com/v1 @@ -129,7 +129,7 @@ jobs: runs-on: ubuntu steps: - name: AI Code Review - uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} + uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} with: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }},${{ secrets.GEMINI_API_KEY_1 }},${{ secrets.GEMINI_API_KEY_2 }},${{ secrets.GEMINI_API_KEY_3 }},${{ secrets.GEMINI_API_KEY_4 }},${{ secrets.GEMINI_API_KEY_5 }},${{ secrets.GEMINI_API_KEY_6 }},${{ secrets.GEMINI_API_KEY_7 }},${{ secrets.GEMINI_API_KEY_8 }},${{ secrets.GEMINI_API_KEY_9 }},${{ secrets.GEMINI_API_KEY_10 }},${{ secrets.GEMINI_API_KEY_11 }},${{ secrets.GEMINI_API_KEY_12 }},${{ secrets.GEMINI_API_KEY_13 }},${{ secrets.GEMINI_API_KEY_14 }},${{ secrets.GEMINI_API_KEY_15 }},${{ secrets.GEMINI_API_KEY_16 }},${{ secrets.GEMINI_API_KEY_17 }},${{ secrets.GEMINI_API_KEY_18 }},${{ secrets.GEMINI_API_KEY_19 }} GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta @@ -157,7 +157,7 @@ jobs: runs-on: ubuntu steps: - name: AI Code Review - uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} + uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} with: AMAZONQ_API_KEY: ${{ secrets.AMAZONQ_API_KEY }} # 支援逗號分隔多個 Key AMAZONQ_BASE_URL: https://q.api.aws @@ -185,7 +185,7 @@ jobs: runs-on: ubuntu steps: - name: AI Code Review - uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} + uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} with: OLLAMA_BASE_URL: https://ollama.jsc.idv.me/v1 OLLAMA_MODEL: ${{ vars.OLLAMA_MODEL }} From de8de251ba3259d3405787ed82710e143a0b0ee7 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 13 May 2026 00:41:12 +0000 Subject: [PATCH 06/29] feat: exclude .gitea/ directory from Git Diff analysis and update TODO --- README.md | 1 + TODO.md | 5 +++++ app/gitea.js | 9 ++++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9de144f..f6b6658 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ 4. 盡量將應用程式放在 ./app,修改 entrypoint.sh 與 Dockerfile 讓程式可以正常運行 5. 將提示詞放到 ./app/prompts 內供程式讀取 6. API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,全部失敗則 exit 1 +7. 讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,避免 AI 分析 workflow 設定等非業務程式碼 # 使用說明 diff --git a/TODO.md b/TODO.md index 225c458..255c9ce 100644 --- a/TODO.md +++ b/TODO.md @@ -40,6 +40,11 @@ - 驗收:log 中能看到「key[N/M] 失敗」等訊息,換 key 後繼續執行;傳入單一 Key 時行為與原本相同;全部 Key 失敗時 log「所有 API Key 均失敗,終止流程」且 workflow 狀態為失敗。 - 完成 +## 階段九:Git Diff 排除 .gitea/ 資料夾 +- 目標:讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,避免 AI 分析 workflow 設定等非業務程式碼。 +- 驗收:PR 中有 `.gitea/` 路徑的變更時,diff 內容不包含該路徑的區塊,AI 分析結果不含 `.gitea/` 相關問題。 +- 完成 + --- 所有階段驗收通過。 diff --git a/app/gitea.js b/app/gitea.js index 904b456..8c319b0 100644 --- a/app/gitea.js +++ b/app/gitea.js @@ -8,7 +8,14 @@ const api = (path) => `${GITEA_SERVER_URL.replace(/\/$/, '')}/api/v1${path}`; export async function getPRDiff() { const resp = await axios.get(api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}.diff`), { headers: headers(), timeout: 60000, httpsAgent }); - return resp.data; + return filterDiff(resp.data, ['.gitea/']); +} + +function filterDiff(diff, excludePrefixes) { + const blocks = diff.split(/(?=^diff --git )/m); + return blocks + .filter(block => !excludePrefixes.some(prefix => block.match(new RegExp(`^diff --git a/${prefix.replace(/\//g, '\\/')}`)))) + .join(''); } export async function postComment(body) { From fd854649dbe6af6e604747359565b0cdf4ff9dde Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 13 May 2026 00:49:23 +0000 Subject: [PATCH 07/29] refactor: improve comment formatting and streamline AI handling in findings processing --- app/comments.js | 2 +- app/findings.js | 47 +++++++++++----------------- app/git.js | 81 +++++++++++++++++++++---------------------------- app/gitea.js | 5 ++- app/llm.js | 7 ++--- app/main.js | 8 ----- 6 files changed, 58 insertions(+), 92 deletions(-) diff --git a/app/comments.js b/app/comments.js index 779a177..a8f2198 100644 --- a/app/comments.js +++ b/app/comments.js @@ -63,7 +63,7 @@ export async function postNewCriticalComments(findings) { return; } for (const f of criticals) { - const body = `## 🚨 嚴重問題\n\n| 審查員 | 位置 | 建議 |\n|--------|------|------|\n| ${f.role} | ${f.location} | ${f.suggestion} |`; + const body = `## 🚨 嚴重問題\n\n${buildTable([f])}`; await postComment(body); console.log(` ✅ 嚴重問題 comment 發布: [${f.role}] ${f.location}`); } diff --git a/app/findings.js b/app/findings.js index f381cfa..8a6a5cb 100644 --- a/app/findings.js +++ b/app/findings.js @@ -11,7 +11,6 @@ const LEVELS = ['critical', 'warning', 'info']; export async function analyzeWithRole(role, diff) { console.log(` [${role.name}] 開始分析...`); const findings = await chatJSON(role.system_prompt, `以下是 Git Diff 內容:\n\n${diff}`); - // 確保每筆都有必要欄位,並標記為新問題 const valid = findings.filter(f => f.level && f.role && f.location && f.suggestion) .map(f => ({ ...f, is_new: true })); console.log(` [${role.name}] 找到 ${valid.length} 個問題`); @@ -40,7 +39,6 @@ export function loadOldFindings(workspace) { /** * 合併新舊 findings,以 (role + location + suggestion前50字) 為 key 去除重複 - * 舊問題保留,新問題若與舊問題重複則捨棄 */ export function mergeFindings(oldFindings, newFindings) { const key = f => `${f.role}|${f.location}|${String(f.suggestion).slice(0, 50)}`; @@ -63,8 +61,17 @@ export function sortByLevel(findings) { } /** - * 呼叫 LLM 進行語意去重,回傳去重後的 findings - * 失敗時降級回傳原始 findings + * AI 呼叫失敗時的統一降級處理 + */ +function fallback(label, findings, e) { + const status = e.response?.status; + const reason = (status === 402 || status === 429) ? `${status} 額度/限流` : e.message; + console.log(` ⚠️ ${label}失敗(${reason}),降級:保留所有問題`); + return findings; +} + +/** + * 呼叫 LLM 進行語意去重,失敗時降級回傳原始 findings */ export async function deduplicateWithAI(findings) { if (findings.length === 0) return findings; @@ -74,29 +81,20 @@ export async function deduplicateWithAI(findings) { 保留等級較高的版本,優先保留 critical > warning > info。 只回傳去重後的 JSON 陣列,不要有其他文字。`; - const userContent = `以下是問題清單,請去除語意重複的項目:\n\n${JSON.stringify(findings, null, 2)}`; - try { - const result = await chatJSON(systemPrompt, userContent); + const result = await chatJSON(systemPrompt, `以下是問題清單,請去除語意重複的項目:\n\n${JSON.stringify(findings, null, 2)}`); if (Array.isArray(result) && result.length > 0) { 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; + return fallback('AI 去重', findings, e); } } /** * 讀取排除問題檔案(從 workspace 的 EXCLUSIONS_PATH) - * 格式:[{ role, location, suggestion }],欄位可部分省略,省略表示萬用 */ export function loadExclusions(workspace) { const fullPath = path.join(workspace, EXCLUSIONS_PATH); @@ -125,17 +123,14 @@ export function applyExclusions(findings, exclusions) { const filtered = findings.filter(f => !exclusions.some(ex => { const fPath = String(f.location).split(':')[0]; const exPath = ex.location ? String(ex.location).split(':')[0] : null; - return (!exPath || fPath === exPath) && - (!ex.role || ex.role === f.role); + return (!exPath || fPath === exPath) && (!ex.role || ex.role === f.role); })); console.log(` 排除過濾: ${before} -> ${filtered.length} 筆(排除 ${before - filtered.length} 筆)`); return filtered; } /** - * 呼叫 AI 判斷哪些問題是誤報或不需處理,回傳需保留的 findings - * exclusions 為已知誤報清單,供 AI 參考判斷 - * 失敗時降級回傳原始 findings + * 呼叫 AI 判斷哪些問題是誤報或不需處理,失敗時降級回傳原始 findings */ export async function filterFalsePositivesWithAI(findings, exclusions = []) { if (findings.length === 0) return findings; @@ -152,22 +147,14 @@ export async function filterFalsePositivesWithAI(findings, exclusions = []) { 3. 與已知誤報清單語意相近的問題(檔案路徑相同且建議內容相似) 只回傳需要保留的問題 JSON 陣列,不要有其他文字。${exclusionHint}`; - const userContent = `請判斷以下問題清單,移除誤報或不需處理的問題:\n\n${JSON.stringify(findings, null, 2)}`; - try { - const result = await chatJSON(systemPrompt, userContent); + const result = await chatJSON(systemPrompt, `請判斷以下問題清單,移除誤報或不需處理的問題:\n\n${JSON.stringify(findings, null, 2)}`); if (Array.isArray(result) && result.length > 0) { 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; + return fallback('AI 誤報過濾', findings, e); } } diff --git a/app/git.js b/app/git.js index 5006d88..b182ba0 100644 --- a/app/git.js +++ b/app/git.js @@ -14,20 +14,26 @@ function makeRunner(spawn) { }; } +function withAskpass(workspace, fn) { + const askpassScript = path.join(workspace, '.git-askpass.sh'); + fs.writeFileSync(askpassScript, '#!/bin/sh\necho "$GIT_TOKEN"\n', { mode: 0o700 }); + const credEnv = { ...process.env, GIT_ASKPASS: askpassScript, GIT_USERNAME: 'x-token', GIT_TOKEN: GITEA_TOKEN }; + try { + return fn(credEnv); + } finally { + try { fs.unlinkSync(askpassScript); } catch {} + } +} + /** * Clone PR head branch to workspace/repo (idempotent) */ export function cloneRepo(workspace, _spawnSync = spawnSync) { const run = makeRunner(_spawnSync); - const baseUrl = GITEA_SERVER_URL.replace(/\/$/, ''); - const remoteUrl = `${baseUrl}/${GITEA_REPOSITORY}.git`; + const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.git`; const repoDir = path.join(workspace, 'repo'); - const askpassScript = path.join(workspace, '.git-askpass.sh'); - fs.writeFileSync(askpassScript, '#!/bin/sh\necho "$GIT_TOKEN"\n', { mode: 0o700 }); - const credEnv = { ...process.env, GIT_ASKPASS: askpassScript, GIT_USERNAME: 'x-token', GIT_TOKEN: GITEA_TOKEN }; - - try { + return withAskpass(workspace, credEnv => { if (!fs.existsSync(repoDir)) { run(['clone', '--depth=1', '--branch', PR_HEAD_BRANCH, remoteUrl, repoDir], workspace, credEnv); console.log(` ✅ repo cloned to ${repoDir}`); @@ -36,57 +42,40 @@ export function cloneRepo(workspace, _spawnSync = spawnSync) { run(['checkout', PR_HEAD_BRANCH], repoDir); console.log(` ✅ repo already exists, fetched latest`); } - } finally { - try { fs.unlinkSync(askpassScript); } catch {} - } - return repoDir; + return repoDir; + }); } export async function commitAndPush(workspace, _spawnSync = spawnSync) { const run = makeRunner(_spawnSync); - - const baseUrl = GITEA_SERVER_URL.replace(/\/$/, ''); - const remoteUrl = `${baseUrl}/${GITEA_REPOSITORY}.git`; - const repoDir = path.join(workspace, 'repo'); - - // Write a temporary askpass script that reads the token from an env var, - // so the token value never appears in the script file itself - const askpassScript = path.join(workspace, '.git-askpass.sh'); - fs.writeFileSync(askpassScript, '#!/bin/sh\necho "$GIT_TOKEN"\n', { mode: 0o700 }); - - const credEnv = { ...process.env, GIT_ASKPASS: askpassScript, GIT_USERNAME: 'x-token', GIT_TOKEN: GITEA_TOKEN }; + const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.git`; try { - if (!fs.existsSync(repoDir)) { - run(['clone', '--depth=1', '--branch', PR_HEAD_BRANCH, remoteUrl, repoDir], workspace, credEnv); - } + const repoDir = cloneRepo(workspace, _spawnSync); - run(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir); - run(['config', 'user.name', 'AI Review Bot'], repoDir); - run(['fetch', 'origin', PR_HEAD_BRANCH], repoDir, credEnv); - run(['checkout', PR_HEAD_BRANCH], repoDir); + await withAskpass(workspace, async credEnv => { + run(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir); + run(['config', 'user.name', 'AI Review Bot'], repoDir); - // 將 findings.json 從 workspace 複製到 clone 的 repo - const srcFindings = path.join(workspace, FINDINGS_PATH); - const destFindings = path.join(repoDir, FINDINGS_PATH); - fs.mkdirSync(path.dirname(destFindings), { recursive: true }); - fs.copyFileSync(srcFindings, destFindings); + const srcFindings = path.join(workspace, FINDINGS_PATH); + const destFindings = path.join(repoDir, FINDINGS_PATH); + fs.mkdirSync(path.dirname(destFindings), { recursive: true }); + fs.copyFileSync(srcFindings, destFindings); - run(['add', FINDINGS_PATH], repoDir); + run(['add', FINDINGS_PATH], repoDir); - const status = run(['status', '--porcelain'], repoDir); - if (!status) { - console.log(' findings.json 無變更,跳過 commit'); - return; - } + const status = run(['status', '--porcelain'], repoDir); + if (!status) { + console.log(' findings.json 無變更,跳過 commit'); + return; + } - const out = run(['commit', '-m', 'chore: update ai-review findings [skip ci]'], repoDir); - const commitHash = out.match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown'; - run(['push', remoteUrl, PR_HEAD_BRANCH], repoDir, credEnv); - console.log(` ✅ persisted findings commit=${commitHash} push=${PR_HEAD_BRANCH}`); + const out = run(['commit', '-m', 'chore: update ai-review findings [skip ci]'], repoDir); + const commitHash = out.match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown'; + run(['push', remoteUrl, PR_HEAD_BRANCH], repoDir, credEnv); + console.log(` ✅ persisted findings commit=${commitHash} push=${PR_HEAD_BRANCH}`); + }); } catch (e) { console.log(` ⚠️ Runner failed: commit/push 失敗: ${e.message}`); - } finally { - try { fs.unlinkSync(askpassScript); } catch {} } } diff --git a/app/gitea.js b/app/gitea.js index 8c319b0..e35ccab 100644 --- a/app/gitea.js +++ b/app/gitea.js @@ -12,9 +12,8 @@ export async function getPRDiff() { } function filterDiff(diff, excludePrefixes) { - const blocks = diff.split(/(?=^diff --git )/m); - return blocks - .filter(block => !excludePrefixes.some(prefix => block.match(new RegExp(`^diff --git a/${prefix.replace(/\//g, '\\/')}`)))) + return diff.split(/(?=^diff --git )/m) + .filter(block => !excludePrefixes.some(p => block.startsWith(`diff --git a/${p}`))) .join(''); } diff --git a/app/llm.js b/app/llm.js index 4bf932f..87ace9d 100644 --- a/app/llm.js +++ b/app/llm.js @@ -29,12 +29,11 @@ export async function chat(systemPrompt, userContent) { } export async function chatJSON(systemPrompt, userContent) { + const text = await chat(systemPrompt, userContent); try { - let text = await chat(systemPrompt, userContent); - text = text.trim().replace(/^```[^\n]*\n?/, '').replace(/```$/, '').trim(); - return JSON.parse(text); + return JSON.parse(text.trim().replace(/^```[^\n]*\n?/, '').replace(/```$/, '').trim()); } catch (e) { - console.log(` [LLM] 解析失敗: ${e.message}`); + console.log(` [LLM] JSON 解析失敗: ${e.message}`); return []; } } diff --git a/app/main.js b/app/main.js index 67a797d..e25b839 100644 --- a/app/main.js +++ b/app/main.js @@ -13,7 +13,6 @@ async function main() { console.log(` repo=${GITEA_REPOSITORY} PR=#${PR_NUMBER}`); console.log(` ${PR_HEAD_BRANCH} -> ${PR_BASE_BRANCH}`); - // 偵測 LLM const { provider, baseURL, model } = getLLMConfig(); if (!provider) { console.error('❌ 未設定任何 LLM API Key,請檢查 action inputs'); @@ -21,11 +20,9 @@ async function main() { } console.log(` LLM: provider=${provider} model=${model} base_url=${baseURL}`); - // 載入角色 const roles = loadRoles(); console.log(` 已載入 ${roles.length} 個角色: [${roles.map(r => r.name).join(', ')}]`); - // 取得 PR diff let diff; try { diff = await getPRDiff(); @@ -40,7 +37,6 @@ async function main() { process.exit(0); } - // 發布角色介紹 comment try { const intro = getRoleIntro(roles) + `\n\n> 🔍 服務:${provider} 模型:${model}`; await postComment(intro); @@ -48,7 +44,6 @@ async function main() { } catch (e) { console.log(` ⚠️ comment 發布失敗(繼續執行): ${e.message}`); } - console.log(' Step1 完成'); // Step2: 各角色分析 diff 產生新 findings console.log('\n📊 Step2: Findings 產生'); @@ -65,7 +60,6 @@ async function main() { // Step3: 讀取舊 findings,合併去重(含 AI 語意去重) console.log('\n🔀 Step3: Findings 合併'); - // Clone repo 以讀取舊 findings 與排除清單 let repoDir; try { repoDir = cloneRepo(WORKSPACE); @@ -91,7 +85,6 @@ async function main() { // Step5: 寫入 findings.json,依序發布 comment console.log('\n📝 Step5: Findings 寫入與 Comment 發布'); saveFindings(WORKSPACE, filtered); - try { await postOldFindingsComment(filtered); await postNewNonCriticalComment(filtered); @@ -114,7 +107,6 @@ async function main() { process.exit(1); } console.log(' ✅ 無嚴重問題'); - console.log('\n✅ Pipeline 完成'); console.log('='.repeat(60)); } From 37cf5f82fa620836e6ffe4419bc4fe4b5070bc97 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Wed, 13 May 2026 00:51:30 +0000 Subject: [PATCH 08/29] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 6269a2b..fbbe8ab 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,9 +1,37 @@ [ + { + "level": "warning", + "role": "Aria", + "location": "app/main.js:60", + "suggestion": "已移除的註解 `// 載入舊 findings,用於 AI 誤報過濾參考` 提供了該程式碼區塊的上下文資訊。建議保留此類註解或以 JSDoc 形式補充,以提升程式碼可讀性與維護性。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": "app/main.js:64", + "suggestion": "已移除的註解 `// Clone repo 以讀取舊 findings 與排除清單` 說明了呼叫 `cloneRepo` 的目的。建議保留此類註解或以 JSDoc 形式補充,以提升程式碼可讀性與維護性。", + "is_new": true + }, + { + "level": "warning", + "role": "Maya", + "location": "app/gitea.js:14", + "suggestion": "更新 `filterDiff` 的測試。過濾邏輯從正則表達式匹配改為 `startsWith`,這是一個功能性變更。需要新增或修改測試案例,以確保新的 `startsWith` 邏輯能正確過濾或保留 diff 區塊,特別是針對邊界條件和不同前綴的匹配情況。", + "is_new": true + }, { "level": "info", "role": "Rex", "location": "action.yaml", "suggestion": "此 Action 需要 `contents: write`、`pull-requests: write` 和 `issues: write` 權限。這些權限對於 Action 的正常運作是必要的(例如寫入 findings.json、發布評論),但屬於較廣泛的權限。建議在文件或使用說明中明確指出這些權限的需求及其潛在影響,確保使用者了解並接受。", + "is_new": false + }, + { + "level": "info", + "role": "Leo", + "location": "app/main.js:16", + "suggestion": "在 `main` 函式中,移除了多個高層次的註解,例如 `// 偵測 LLM`、`// 載入角色` 等。雖然這些註解描述了接下來的程式碼區塊,但對於理解整個 pipeline 的執行流程和各步驟的目標,它們提供了有用的指引。建議恢復這些高層次註解,以提升程式碼的整體可讀性和維護性,特別是對於新加入的開發者。", "is_new": true } ] From 49a02ebb6bb6eacacca46917c586a61ed03c4d71 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 13 May 2026 01:02:33 +0000 Subject: [PATCH 09/29] feat: add JSON format validation for findings and exclusions after processing --- .gitea/ai-review/exclusions.json | 5 +++++ README.md | 3 +++ TODO.md | 5 +++++ app/gitea.js | 4 ++++ app/main.js | 23 ++++++++++++++++++++++- 5 files changed, 39 insertions(+), 1 deletion(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 3743a5a..ac80e9f 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -149,6 +149,11 @@ "location": "app/llm.test.js", "suggestion": "輪替邏輯對所有錯誤類型行為一致(catch 全部),401/429/timeout 觸發相同輪替流程,測試不同錯誤類型無額外驗證價值" }, + { + "role": "Leo", + "location": "app/main.js", + "suggestion": "main.js 中的 Step 標題註解為 pipeline 流程說明,非待整理的 TODO,不需要轉換為具體任務" + }, { "role": "Rex", "location": "app/package.json", diff --git a/README.md b/README.md index f6b6658..6392812 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ 5. 將提示詞放到 ./app/prompts 內供程式讀取 6. API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,全部失敗則 exit 1 7. 讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,避免 AI 分析 workflow 設定等非業務程式碼 +8. 階段五完成後驗證 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,驗證失敗則 exit 1 # 使用說明 @@ -30,6 +31,8 @@ 2. 在 `.gitea/workflows` 資料夾中建立 `ai-review.yaml' 3. 在 `ai-review.yaml` 中填入以下內容(選擇一個使用): +> **權限說明**:此 Action 需要 `contents: write`(寫入 findings.json)、`pull-requests: write`(發佈 PR comment)、`issues: write`(發佈 issue comment)三項權限,為正常運作所必要,無法縮減。 + ### 1. OpenAI ```yaml name: AI diff --git a/TODO.md b/TODO.md index 255c9ce..ff6ca44 100644 --- a/TODO.md +++ b/TODO.md @@ -45,6 +45,11 @@ - 驗收:PR 中有 `.gitea/` 路徑的變更時,diff 內容不包含該路徑的區塊,AI 分析結果不含 `.gitea/` 相關問題。 - 完成 +## 階段十:階段五後驗證 JSON 格式 +- 目標:階段五完成後驗證 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,驗證失敗則 exit 1。 +- 驗收:log 中能看到兩個檔案的驗證結果(成功或失敗),格式錯誤時有明確錯誤訊息且 workflow 狀態為失敗。 +- 完成 + --- 所有階段驗收通過。 diff --git a/app/gitea.js b/app/gitea.js index e35ccab..b7a048b 100644 --- a/app/gitea.js +++ b/app/gitea.js @@ -11,6 +11,10 @@ export async function getPRDiff() { return filterDiff(resp.data, ['.gitea/']); } +/** + * 過濾 diff 內容,移除路徑符合 excludePrefixes 的區塊。 + * 每個區塊以 "diff --git a/" 開頭判斷,使用 startsWith 精確比對前綴。 + */ function filterDiff(diff, excludePrefixes) { return diff.split(/(?=^diff --git )/m) .filter(block => !excludePrefixes.some(p => block.startsWith(`diff --git a/${p}`))) diff --git a/app/main.js b/app/main.js index e25b839..8d552fe 100644 --- a/app/main.js +++ b/app/main.js @@ -1,4 +1,6 @@ -import { GITEA_REPOSITORY, PR_NUMBER, PR_HEAD_BRANCH, PR_BASE_BRANCH, getLLMConfig } from './config.js'; +import fs from 'fs'; +import path from 'path'; +import { GITEA_REPOSITORY, PR_NUMBER, PR_HEAD_BRANCH, PR_BASE_BRANCH, getLLMConfig, FINDINGS_PATH, EXCLUSIONS_PATH } from './config.js'; import { loadRoles, getRoleIntro } from './roles.js'; import { getPRDiff, postComment } from './gitea.js'; import { analyzeWithRole, loadOldFindings, mergeFindings, sortByLevel, deduplicateWithAI, loadExclusions, applyExclusions, filterFalsePositivesWithAI } from './findings.js'; @@ -60,6 +62,7 @@ async function main() { // Step3: 讀取舊 findings,合併去重(含 AI 語意去重) console.log('\n🔀 Step3: Findings 合併'); + // Clone repo 以讀取舊 findings 與排除清單 let repoDir; try { repoDir = cloneRepo(WORKSPACE); @@ -77,6 +80,7 @@ async function main() { // Step4: 讀取排除問題檔案,過濾 PR 問題表格,並請 AI 判斷誤報 console.log('\n🚫 Step4: AI 排除問題過濾'); + // 輸入至 findings 用於 AI 誤報過濾,exclusions 同時作為已知誤報參考 const exclusions = loadExclusions(repoDir || WORKSPACE); const ruleFiltered = applyExclusions(sorted, exclusions); const filtered = await filterFalsePositivesWithAI(ruleFiltered, exclusions); @@ -94,6 +98,23 @@ async function main() { console.log(` ⚠️ comment 發布失敗(繼續執行): ${e.message}`); } + // Step5b: 驗證 findings.json 與 exclusions.json 為合法 JSON + console.log('\n🔎 Step5b: JSON 格式驗證'); + for (const relPath of [FINDINGS_PATH, EXCLUSIONS_PATH]) { + const fullPath = path.join(repoDir || WORKSPACE, relPath); + if (!fs.existsSync(fullPath)) { + console.log(` ⚠️ ${relPath} 不存在,跳過驗證`); + continue; + } + try { + JSON.parse(fs.readFileSync(fullPath, 'utf8')); + console.log(` ✅ ${relPath} JSON 格式正確`); + } catch (e) { + console.error(` ❌ ${relPath} JSON 格式錯誤: ${e.message}`); + process.exit(1); + } + } + // Step6: commit/push findings.json 到來源分支 console.log('\n💾 Step6: 記憶區 Commit/Push'); await commitAndPush(WORKSPACE); From 6c6680fd3e7b8e7f9fd3e826cbc1a45f065488a2 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 13 May 2026 01:06:11 +0000 Subject: [PATCH 10/29] feat: enhance JSON format validation with backup and reset mechanism on error --- README.md | 2 +- TODO.md | 18 +++++++++--------- app/main.js | 12 ++++++++++-- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 6392812..fa3047c 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ 5. 將提示詞放到 ./app/prompts 內供程式讀取 6. API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,全部失敗則 exit 1 7. 讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,避免 AI 分析 workflow 設定等非業務程式碼 -8. 階段五完成後驗證 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,驗證失敗則 exit 1 +8. 階段五完成後驗證 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,格式錯誤時先嘗試重置為空陣列並備份原檔,修正失敗才 exit 1 # 使用說明 diff --git a/TODO.md b/TODO.md index ff6ca44..b45a78f 100644 --- a/TODO.md +++ b/TODO.md @@ -25,31 +25,31 @@ - 驗收:log 中能看到 `.gitea/ai-review/findings.json` 寫入、comment sync 的詳細訊息與順序。 - 完成 -## 階段六:記憶區 commit/push 與錯誤處理 +## 階段六:階段五後驗證 JSON 格式 +- 目標:階段五完成後驗證 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,格式錯誤時先嘗試重置為空陣列並備份原檔,修正失敗才 exit 1。 +- 驗收:log 中能看到兩個檔案的驗證結果(成功或失敗),格式錯誤時有「嘗試修正」訊息與備份路徑,修正失敗時 workflow 狀態為失敗。 +- 完成 + +## 階段七:記憶區 commit/push 與錯誤處理 - 目標:記憶區能成功 commit/push,錯誤時有明確 log,流程結束有總結訊息。 - 驗收:log 有「persisted findings」、「commit=...」、「push=...」等訊息,錯誤時有「Runner failed: ...」等明確錯誤說明。 - 完成 -## 階段七:阻擋嚴重問題 PR(第 8 點) +## 階段八:阻擋嚴重問題 PR(第 8 點) - 目標:如果 PR 問題表格中有嚴重(critical)問題,workflow 需直接 exit 1,不讓流程成功。 - 驗收:log 中能看到「critical 問題存在,workflow 結束(exit 1)」等明確訊息,且 workflow 狀態為失敗。 - 完成 -## 階段八:API Key 輪替 +## 階段九:API Key 輪替 - 目標:所有平台的 API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,單一 Key 失敗時自動換下一個,全部失敗則 exit 1。 - 驗收:log 中能看到「key[N/M] 失敗」等訊息,換 key 後繼續執行;傳入單一 Key 時行為與原本相同;全部 Key 失敗時 log「所有 API Key 均失敗,終止流程」且 workflow 狀態為失敗。 - 完成 -## 階段九:Git Diff 排除 .gitea/ 資料夾 +## 階段十:Git Diff 排除 .gitea/ 資料夾 - 目標:讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,避免 AI 分析 workflow 設定等非業務程式碼。 - 驗收:PR 中有 `.gitea/` 路徑的變更時,diff 內容不包含該路徑的區塊,AI 分析結果不含 `.gitea/` 相關問題。 - 完成 -## 階段十:階段五後驗證 JSON 格式 -- 目標:階段五完成後驗證 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,驗證失敗則 exit 1。 -- 驗收:log 中能看到兩個檔案的驗證結果(成功或失敗),格式錯誤時有明確錯誤訊息且 workflow 狀態為失敗。 -- 完成 - --- 所有階段驗收通過。 diff --git a/app/main.js b/app/main.js index 8d552fe..a699ee6 100644 --- a/app/main.js +++ b/app/main.js @@ -110,8 +110,16 @@ async function main() { JSON.parse(fs.readFileSync(fullPath, 'utf8')); console.log(` ✅ ${relPath} JSON 格式正確`); } catch (e) { - console.error(` ❌ ${relPath} JSON 格式錯誤: ${e.message}`); - process.exit(1); + console.error(` ❌ ${relPath} JSON 格式錯誤: ${e.message},嘗試修正...`); + try { + const backupPath = fullPath + '.bak'; + fs.copyFileSync(fullPath, backupPath); + fs.writeFileSync(fullPath, '[]\n', 'utf8'); + console.log(` ✅ ${relPath} 已重置為空陣列(原檔備份至 ${relPath}.bak)`); + } catch (repairErr) { + console.error(` ❌ ${relPath} 修正失敗: ${repairErr.message}`); + process.exit(1); + } } } From 45468d89d3edc6e1149623cdb38f764e9874a05a Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 13 May 2026 01:08:04 +0000 Subject: [PATCH 11/29] refactor: reorganize TODO stages for clarity and update section titles --- TODO.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/TODO.md b/TODO.md index b45a78f..6978c02 100644 --- a/TODO.md +++ b/TODO.md @@ -5,51 +5,51 @@ - 驗收:log 中能看到每個階段(如「Step1: pipeline start」、「Step2: findings merge」等)明確訊息,且流程能走完(即使還沒產生 findings)。 - 完成 -## 階段二:Findings 產生與合併 +## 階段二:Git Diff 排除 .gitea/ 資料夾 +- 目標:讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,避免 AI 分析 workflow 設定等非業務程式碼。 +- 驗收:PR 中有 `.gitea/` 路徑的變更時,diff 內容不包含該路徑的區塊,AI 分析結果不含 `.gitea/` 相關問題。 +- 完成 + +## 階段三:Findings 產生與合併 - 目標:各角色(style/security/performance/maintainability/testing)能產生 findings,並正確合併新舊 findings。 - 驗收:log 中能看到每個角色 findings 數量、合併後 findings 統計,並有「Step3: merged findings total=...」等訊息。 - 完成 -## 階段三:AI 去重與角色確認 +## 階段四:AI 去重與角色確認 - 目標:嘗試呼叫 LLM 進行 findings 去重與角色確認,API 額度不足時要有降級處理 log。 - 驗收:log 中能看到 deduplication/resolution confirmation 成功或失敗(如 402),降級時有「保留所有問題」等明確訊息。 - 完成 -## 階段四:AI 排除問題過濾 +## 階段五:AI 排除問題過濾 - 目標:讀取排除問題檔案(`.gitea/ai-review/exclusions.json`)進行規則過濾,並呼叫 AI 判斷剩餘問題是否為誤報或不適用,兩層過濾後產生最終問題清單。 - 驗收:log 中能看到排除問題檔案讀取成功或不存在的訊息、規則過濾數量變化,以及「AI 誤報過濾: N -> M 筆」或降級訊息。 - 完成 -## 階段五:findings 寫入與 comment 發布 +## 階段六:findings 寫入與 comment 發布 - 目標:`.gitea/ai-review/findings.json` 正確寫入,comment 發布順序正確(舊問題→非嚴重→嚴重),每步有 log。 - 驗收:log 中能看到 `.gitea/ai-review/findings.json` 寫入、comment sync 的詳細訊息與順序。 - 完成 -## 階段六:階段五後驗證 JSON 格式 -- 目標:階段五完成後驗證 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,格式錯誤時先嘗試重置為空陣列並備份原檔,修正失敗才 exit 1。 +## 階段七:階段六後驗證 JSON 格式 +- 目標:階段六完成後驗證 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,格式錯誤時先嘗試重置為空陣列並備份原檔,修正失敗才 exit 1。 - 驗收:log 中能看到兩個檔案的驗證結果(成功或失敗),格式錯誤時有「嘗試修正」訊息與備份路徑,修正失敗時 workflow 狀態為失敗。 - 完成 -## 階段七:記憶區 commit/push 與錯誤處理 +## 階段八:記憶區 commit/push 與錯誤處理 - 目標:記憶區能成功 commit/push,錯誤時有明確 log,流程結束有總結訊息。 - 驗收:log 有「persisted findings」、「commit=...」、「push=...」等訊息,錯誤時有「Runner failed: ...」等明確錯誤說明。 - 完成 -## 階段八:阻擋嚴重問題 PR(第 8 點) +## 階段九:阻擋嚴重問題 PR(第 8 點) - 目標:如果 PR 問題表格中有嚴重(critical)問題,workflow 需直接 exit 1,不讓流程成功。 - 驗收:log 中能看到「critical 問題存在,workflow 結束(exit 1)」等明確訊息,且 workflow 狀態為失敗。 - 完成 -## 階段九:API Key 輪替 +## 階段十:API Key 輪替 - 目標:所有平台的 API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,單一 Key 失敗時自動換下一個,全部失敗則 exit 1。 - 驗收:log 中能看到「key[N/M] 失敗」等訊息,換 key 後繼續執行;傳入單一 Key 時行為與原本相同;全部 Key 失敗時 log「所有 API Key 均失敗,終止流程」且 workflow 狀態為失敗。 - 完成 -## 階段十:Git Diff 排除 .gitea/ 資料夾 -- 目標:讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,避免 AI 分析 workflow 設定等非業務程式碼。 -- 驗收:PR 中有 `.gitea/` 路徑的變更時,diff 內容不包含該路徑的區塊,AI 分析結果不含 `.gitea/` 相關問題。 -- 完成 - --- 所有階段驗收通過。 From 6db660f8727117269047fda7ca214c194990c10e Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 13 May 2026 01:12:20 +0000 Subject: [PATCH 12/29] refactor: update TODO stages to reflect current status and improve clarity; modify diff filtering logic in gitea.js and main.js --- TODO.md | 24 ++++++++++-------------- app/gitea.js | 4 ++-- app/main.js | 52 +++++++++++++++++++++++++++++++--------------------- 3 files changed, 43 insertions(+), 37 deletions(-) diff --git a/TODO.md b/TODO.md index 6978c02..01b8e73 100644 --- a/TODO.md +++ b/TODO.md @@ -3,53 +3,49 @@ ## 階段一:基本流程串接 - 目標:確保 action 可以被觸發,pipeline 各步驟依序執行,log 出每個主要階段的進入與完成。 - 驗收:log 中能看到每個階段(如「Step1: pipeline start」、「Step2: findings merge」等)明確訊息,且流程能走完(即使還沒產生 findings)。 -- 完成 +- 未驗收 ## 階段二:Git Diff 排除 .gitea/ 資料夾 - 目標:讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,避免 AI 分析 workflow 設定等非業務程式碼。 - 驗收:PR 中有 `.gitea/` 路徑的變更時,diff 內容不包含該路徑的區塊,AI 分析結果不含 `.gitea/` 相關問題。 -- 完成 +- 未驗收 ## 階段三: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),降級時有「保留所有問題」等明確訊息。 -- 完成 +- 未驗收 ## 階段五:AI 排除問題過濾 - 目標:讀取排除問題檔案(`.gitea/ai-review/exclusions.json`)進行規則過濾,並呼叫 AI 判斷剩餘問題是否為誤報或不適用,兩層過濾後產生最終問題清單。 - 驗收:log 中能看到排除問題檔案讀取成功或不存在的訊息、規則過濾數量變化,以及「AI 誤報過濾: N -> M 筆」或降級訊息。 -- 完成 +- 未驗收 ## 階段六:findings 寫入與 comment 發布 - 目標:`.gitea/ai-review/findings.json` 正確寫入,comment 發布順序正確(舊問題→非嚴重→嚴重),每步有 log。 - 驗收:log 中能看到 `.gitea/ai-review/findings.json` 寫入、comment sync 的詳細訊息與順序。 -- 完成 +- 未驗收 ## 階段七:階段六後驗證 JSON 格式 - 目標:階段六完成後驗證 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,格式錯誤時先嘗試重置為空陣列並備份原檔,修正失敗才 exit 1。 - 驗收:log 中能看到兩個檔案的驗證結果(成功或失敗),格式錯誤時有「嘗試修正」訊息與備份路徑,修正失敗時 workflow 狀態為失敗。 -- 完成 +- 未驗收 ## 階段八:記憶區 commit/push 與錯誤處理 - 目標:記憶區能成功 commit/push,錯誤時有明確 log,流程結束有總結訊息。 - 驗收:log 有「persisted findings」、「commit=...」、「push=...」等訊息,錯誤時有「Runner failed: ...」等明確錯誤說明。 -- 完成 +- 未驗收 ## 階段九:阻擋嚴重問題 PR(第 8 點) - 目標:如果 PR 問題表格中有嚴重(critical)問題,workflow 需直接 exit 1,不讓流程成功。 - 驗收:log 中能看到「critical 問題存在,workflow 結束(exit 1)」等明確訊息,且 workflow 狀態為失敗。 -- 完成 +- 未驗收 ## 階段十:API Key 輪替 - 目標:所有平台的 API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,單一 Key 失敗時自動換下一個,全部失敗則 exit 1。 - 驗收:log 中能看到「key[N/M] 失敗」等訊息,換 key 後繼續執行;傳入單一 Key 時行為與原本相同;全部 Key 失敗時 log「所有 API Key 均失敗,終止流程」且 workflow 狀態為失敗。 -- 完成 - ---- - -所有階段驗收通過。 +- 未驗收 diff --git a/app/gitea.js b/app/gitea.js index b7a048b..263baf1 100644 --- a/app/gitea.js +++ b/app/gitea.js @@ -8,14 +8,14 @@ const api = (path) => `${GITEA_SERVER_URL.replace(/\/$/, '')}/api/v1${path}`; export async function getPRDiff() { const resp = await axios.get(api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}.diff`), { headers: headers(), timeout: 60000, httpsAgent }); - return filterDiff(resp.data, ['.gitea/']); + return resp.data; } /** * 過濾 diff 內容,移除路徑符合 excludePrefixes 的區塊。 * 每個區塊以 "diff --git a/" 開頭判斷,使用 startsWith 精確比對前綴。 */ -function filterDiff(diff, excludePrefixes) { +export function filterDiff(diff, excludePrefixes) { return diff.split(/(?=^diff --git )/m) .filter(block => !excludePrefixes.some(p => block.startsWith(`diff --git a/${p}`))) .join(''); diff --git a/app/main.js b/app/main.js index a699ee6..02f5d15 100644 --- a/app/main.js +++ b/app/main.js @@ -2,7 +2,7 @@ import fs from 'fs'; import path from 'path'; import { GITEA_REPOSITORY, PR_NUMBER, PR_HEAD_BRANCH, PR_BASE_BRANCH, getLLMConfig, FINDINGS_PATH, EXCLUSIONS_PATH } from './config.js'; import { loadRoles, getRoleIntro } from './roles.js'; -import { getPRDiff, postComment } from './gitea.js'; +import { getPRDiff, filterDiff, postComment } from './gitea.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'; @@ -47,8 +47,18 @@ async function main() { console.log(` ⚠️ comment 發布失敗(繼續執行): ${e.message}`); } - // Step2: 各角色分析 diff 產生新 findings - console.log('\n📊 Step2: Findings 產生'); + // Step2: 排除 .gitea/ 資料夾內的所有檔案 + console.log('\n🗂️ Step2: Git Diff 過濾'); + diff = filterDiff(diff, ['.gitea/']); + console.log(` 排除 .gitea/ 後 diff 長度: ${diff.length} 字元`); + + if (!diff.trim()) { + console.log(' ⚠️ 過濾後 diff 為空,無需審查'); + process.exit(0); + } + + // Step3: 各角色分析 diff 產生新 findings + console.log('\n📊 Step3: Findings 產生'); const results = await Promise.allSettled(roles.map(role => analyzeWithRole(role, diff))); const newFindings = []; for (let i = 0; i < results.length; i++) { @@ -58,10 +68,10 @@ async function main() { console.log(` ⚠️ [${roles[i].name}] 分析失敗(跳過): ${results[i].reason?.message}`); } } - console.log(` Step2 完成: 新 findings 總計 ${newFindings.length} 筆`); + console.log(` Step3 完成: 新 findings 總計 ${newFindings.length} 筆`); - // Step3: 讀取舊 findings,合併去重(含 AI 語意去重) - console.log('\n🔀 Step3: Findings 合併'); + // Step4: 讀取舊 findings,合併去重(含 AI 語意去重) + console.log('\n🔀 Step4: Findings 合併'); // Clone repo 以讀取舊 findings 與排除清單 let repoDir; try { @@ -71,35 +81,35 @@ async function main() { } const oldFindings = loadOldFindings(repoDir || WORKSPACE); const mergedFindings = mergeFindings(oldFindings, newFindings); - console.log(` Step3 merged findings total=${mergedFindings.length}`); + console.log(` Step4 merged findings total=${mergedFindings.length}`); - console.log('\n🤖 Step3b: AI 語意去重'); + console.log('\n🤖 Step4b: 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})`); + console.log(` Step4b 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 問題表格,並請 AI 判斷誤報 - console.log('\n🚫 Step4: AI 排除問題過濾'); + // Step5: 讀取排除問題檔案,過濾 PR 問題表格,並請 AI 判斷誤報 + console.log('\n🚫 Step5: AI 排除問題過濾'); // 輸入至 findings 用於 AI 誤報過濾,exclusions 同時作為已知誤報參考 const exclusions = loadExclusions(repoDir || WORKSPACE); const ruleFiltered = applyExclusions(sorted, exclusions); const filtered = await filterFalsePositivesWithAI(ruleFiltered, exclusions); - console.log(` Step4 完成: findings total=${filtered.length}`); + console.log(` Step5 完成: findings total=${filtered.length}`); - // Step5: 寫入 findings.json,依序發布 comment - console.log('\n📝 Step5: Findings 寫入與 Comment 發布'); + // Step6: 寫入 findings.json,依序發布 comment + console.log('\n📝 Step6: Findings 寫入與 Comment 發布'); saveFindings(WORKSPACE, filtered); try { await postOldFindingsComment(filtered); await postNewNonCriticalComment(filtered); await postNewCriticalComments(filtered); - console.log(' Step5 完成'); + console.log(' Step6 完成'); } catch (e) { console.log(` ⚠️ comment 發布失敗(繼續執行): ${e.message}`); } - // Step5b: 驗證 findings.json 與 exclusions.json 為合法 JSON - console.log('\n🔎 Step5b: JSON 格式驗證'); + // Step7: 驗證 findings.json 與 exclusions.json 為合法 JSON + console.log('\n🔎 Step7: JSON 格式驗證'); for (const relPath of [FINDINGS_PATH, EXCLUSIONS_PATH]) { const fullPath = path.join(repoDir || WORKSPACE, relPath); if (!fs.existsSync(fullPath)) { @@ -123,12 +133,12 @@ async function main() { } } - // Step6: commit/push findings.json 到來源分支 - console.log('\n💾 Step6: 記憶區 Commit/Push'); + // Step8: commit/push findings.json 到來源分支 + console.log('\n💾 Step8: 記憶區 Commit/Push'); await commitAndPush(WORKSPACE); - // Step7: 有 critical 問題則 exit 1 - console.log('\n🚦 Step7: 嚴重問題檢查'); + // Step9: 有 critical 問題則 exit 1 + console.log('\n🚦 Step9: 嚴重問題檢查'); const criticalCount = filtered.filter(f => f.level === 'critical').length; if (criticalCount > 0) { console.log(` ❌ 發現 ${criticalCount} 個嚴重問題,workflow 結束(exit 1)`); From 0108a058869de7b6e4bd175989510346547e5981 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Wed, 13 May 2026 01:14:09 +0000 Subject: [PATCH 13/29] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 33 ++++++--------------------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index fbbe8ab..bbe814c 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,37 +1,16 @@ [ { - "level": "warning", - "role": "Aria", - "location": "app/main.js:60", - "suggestion": "已移除的註解 `// 載入舊 findings,用於 AI 誤報過濾參考` 提供了該程式碼區塊的上下文資訊。建議保留此類註解或以 JSDoc 形式補充,以提升程式碼可讀性與維護性。", - "is_new": true - }, - { - "level": "warning", - "role": "Aria", - "location": "app/main.js:64", - "suggestion": "已移除的註解 `// Clone repo 以讀取舊 findings 與排除清單` 說明了呼叫 `cloneRepo` 的目的。建議保留此類註解或以 JSDoc 形式補充,以提升程式碼可讀性與維護性。", + "level": "critical", + "role": "Leo", + "location": "app/comments.js:66", + "suggestion": "`buildTable` 函式在此檔案中被呼叫,但未見其定義或匯入。這將導致執行時錯誤。請確保 `buildTable` 函式已被正確定義或從其他模組匯入,以確保程式碼的正確執行。", "is_new": true }, { "level": "warning", "role": "Maya", - "location": "app/gitea.js:14", - "suggestion": "更新 `filterDiff` 的測試。過濾邏輯從正則表達式匹配改為 `startsWith`,這是一個功能性變更。需要新增或修改測試案例,以確保新的 `startsWith` 邏輯能正確過濾或保留 diff 區塊,特別是針對邊界條件和不同前綴的匹配情況。", - "is_new": true - }, - { - "level": "info", - "role": "Rex", - "location": "action.yaml", - "suggestion": "此 Action 需要 `contents: write`、`pull-requests: write` 和 `issues: write` 權限。這些權限對於 Action 的正常運作是必要的(例如寫入 findings.json、發布評論),但屬於較廣泛的權限。建議在文件或使用說明中明確指出這些權限的需求及其潛在影響,確保使用者了解並接受。", - "is_new": false - }, - { - "level": "info", - "role": "Leo", - "location": "app/main.js:16", - "suggestion": "在 `main` 函式中,移除了多個高層次的註解,例如 `// 偵測 LLM`、`// 載入角色` 等。雖然這些註解描述了接下來的程式碼區塊,但對於理解整個 pipeline 的執行流程和各步驟的目標,它們提供了有用的指引。建議恢復這些高層次註解,以提升程式碼的整體可讀性和維護性,特別是對於新加入的開發者。", + "location": "app/gitea.js:11, app/main.js:42-45", + "suggestion": "`filterDiff` 函數的邏輯已從正規表達式比對改為 `startsWith`,並將其呼叫從 `getPRDiff` 移至 `main.js`。雖然 `startsWith` 可能更高效精確,但這是一個行為變更與職責重分配。請確保為 `filterDiff` 函數撰寫足夠的單元測試,以驗證:\n1. 正確過濾 `.gitea/` 路徑下的檔案。\n2. 不會錯誤過濾非 `.gitea/` 路徑下的檔案。\n3. 處理空 diff 內容。\n4. 處理僅包含 `.gitea/` 檔案的 diff 內容(應返回空字串)。", "is_new": true } ] From 483439665238948068eb47fa079aed37efbc0568 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 13 May 2026 01:16:34 +0000 Subject: [PATCH 14/29] refactor: streamline JSON file reading logic and improve error handling in findings.js and git.js --- app/findings.js | 39 +++++++++++++++++---------------------- app/git.js | 4 ++-- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/app/findings.js b/app/findings.js index 8a6a5cb..777cbe6 100644 --- a/app/findings.js +++ b/app/findings.js @@ -18,25 +18,31 @@ export async function analyzeWithRole(role, diff) { } /** - * 讀取舊 findings(從 workspace 的 FINDINGS_PATH) + * 讀取 JSON 陣列檔案,失敗或不存在時回傳空陣列 */ -export function loadOldFindings(workspace) { - const fullPath = path.join(workspace, FINDINGS_PATH); +function readJSONArray(fullPath, label) { if (!fs.existsSync(fullPath)) { - console.log(' 舊 findings 檔案不存在,視為空'); + console.log(` ${label}檔案不存在,視為空`); return []; } try { const data = JSON.parse(fs.readFileSync(fullPath, 'utf8')); - const old = (Array.isArray(data) ? data : []).map(f => ({ ...f, is_new: false })); - console.log(` 讀取舊 findings: ${old.length} 筆`); - return old; + return Array.isArray(data) ? data : []; } catch (e) { - console.log(` ⚠️ 讀取舊 findings 失敗: ${e.message},視為空`); + console.log(` ⚠️ 讀取${label}失敗: ${e.message},視為空`); return []; } } +/** + * 讀取舊 findings(從 workspace 的 FINDINGS_PATH) + */ +export function loadOldFindings(workspace) { + const old = readJSONArray(path.join(workspace, FINDINGS_PATH), '舊 findings ').map(f => ({ ...f, is_new: false })); + console.log(` 讀取舊 findings: ${old.length} 筆`); + return old; +} + /** * 合併新舊 findings,以 (role + location + suggestion前50字) 為 key 去除重複 */ @@ -97,20 +103,9 @@ export async function deduplicateWithAI(findings) { * 讀取排除問題檔案(從 workspace 的 EXCLUSIONS_PATH) */ 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 []; - } + const exclusions = readJSONArray(path.join(workspace, EXCLUSIONS_PATH), '排除問題'); + console.log(` 讀取排除問題: ${exclusions.length} 筆`); + return exclusions; } /** diff --git a/app/git.js b/app/git.js index b182ba0..5209de8 100644 --- a/app/git.js +++ b/app/git.js @@ -3,6 +3,8 @@ import fs from 'fs'; import path from 'path'; import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js'; +const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.git`; + function makeRunner(spawn) { return function run(args, cwd, env) { const opts = { cwd, encoding: 'utf8' }; @@ -30,7 +32,6 @@ function withAskpass(workspace, fn) { */ export function cloneRepo(workspace, _spawnSync = spawnSync) { const run = makeRunner(_spawnSync); - const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.git`; const repoDir = path.join(workspace, 'repo'); return withAskpass(workspace, credEnv => { @@ -48,7 +49,6 @@ export function cloneRepo(workspace, _spawnSync = spawnSync) { export async function commitAndPush(workspace, _spawnSync = spawnSync) { const run = makeRunner(_spawnSync); - const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.git`; try { const repoDir = cloneRepo(workspace, _spawnSync); From fade9422675037532b80216f55ecd8e3a8977658 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 13 May 2026 01:18:21 +0000 Subject: [PATCH 15/29] refactor: add new suggestion for comments.js and enhance filterDiff tests for better coverage --- .gitea/ai-review/exclusions.json | 5 +++++ app/gitea.test.js | 35 +++++++++++++++++++++++++++----- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index ac80e9f..6986a6e 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -208,5 +208,10 @@ "role": "Zara", "location": "app/main.js", "suggestion": "deduplicateWithAI 和 filterFalsePositivesWithAI 為循序依賴流程(去重後才能過濾),無法平行化" + }, + { + "role": "Leo", + "location": "app/comments.js", + "suggestion": "buildTable 函式已在 comments.js 第 13 行定義,非未定義或未匯入,不會導致執行時錯誤" } ] diff --git a/app/gitea.test.js b/app/gitea.test.js index bd19a83..26e916a 100644 --- a/app/gitea.test.js +++ b/app/gitea.test.js @@ -2,13 +2,10 @@ import { describe, it, afterEach, mock } from 'node:test'; import assert from 'node:assert/strict'; import axios from 'axios'; -// gitea.js reads env vars at module load time (ESM cache), so we test -// the actual values baked in at import time and verify behavior via axios mocks. - afterEach(() => mock.restoreAll()); describe('gitea', async () => { - const { getPRDiff, postComment } = await import('./gitea.js'); + const { getPRDiff, filterDiff, postComment } = await import('./gitea.js'); it('getPRDiff calls Gitea diff API with Authorization header', async () => { let capturedUrl, capturedOpts; @@ -48,7 +45,6 @@ describe('gitea', async () => { return { data: '' }; }); await getPRDiff(); - // httpsAgent is undefined when GITEA_SKIP_TLS_VERIFY !== 'true' assert.equal(capturedOpts.httpsAgent, undefined); }); @@ -62,3 +58,32 @@ describe('gitea', async () => { await assert.rejects(() => postComment('test'), /api error/); }); }); + +describe('filterDiff', async () => { + const { filterDiff } = await import('./gitea.js'); + + const block = (file) => `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}\n@@ -1 +1 @@\n-old\n+new\n`; + + it('filters out .gitea/ blocks', () => { + const diff = block('.gitea/workflows/review.yaml') + block('src/index.js'); + const result = filterDiff(diff, ['.gitea/']); + assert.ok(!result.includes('.gitea/')); + assert.ok(result.includes('src/index.js')); + }); + + it('does not filter non-.gitea/ blocks', () => { + const diff = block('src/index.js') + block('README.md'); + const result = filterDiff(diff, ['.gitea/']); + assert.equal(result, diff); + }); + + it('returns empty string when all blocks are excluded', () => { + const diff = block('.gitea/workflows/review.yaml') + block('.gitea/ai-review/findings.json'); + const result = filterDiff(diff, ['.gitea/']); + assert.equal(result, ''); + }); + + it('returns empty string for empty diff', () => { + assert.equal(filterDiff('', ['.gitea/']), ''); + }); +}); From fdeceee52fa3a797ba737872a8fc01a2d6baaeb3 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Wed, 13 May 2026 01:21:17 +0000 Subject: [PATCH 16/29] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index bbe814c..4e783ea 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,16 +1,16 @@ [ - { - "level": "critical", - "role": "Leo", - "location": "app/comments.js:66", - "suggestion": "`buildTable` 函式在此檔案中被呼叫,但未見其定義或匯入。這將導致執行時錯誤。請確保 `buildTable` 函式已被正確定義或從其他模組匯入,以確保程式碼的正確執行。", - "is_new": true - }, { "level": "warning", "role": "Maya", "location": "app/gitea.js:11, app/main.js:42-45", "suggestion": "`filterDiff` 函數的邏輯已從正規表達式比對改為 `startsWith`,並將其呼叫從 `getPRDiff` 移至 `main.js`。雖然 `startsWith` 可能更高效精確,但這是一個行為變更與職責重分配。請確保為 `filterDiff` 函數撰寫足夠的單元測試,以驗證:\n1. 正確過濾 `.gitea/` 路徑下的檔案。\n2. 不會錯誤過濾非 `.gitea/` 路徑下的檔案。\n3. 處理空 diff 內容。\n4. 處理僅包含 `.gitea/` 檔案的 diff 內容(應返回空字串)。", + "is_new": false + }, + { + "level": "warning", + "role": "Rex", + "location": "app/gitea.js:11", + "suggestion": "`getPRDiff` 函數現在直接回傳未經篩選的 Git Diff 內容,將 `.gitea/` 資料夾的排除邏輯移至 `main.js`。這改變了 `getPRDiff` 的契約,增加了未來若 `main.js` 未正確呼叫 `filterDiff`,可能導致 `.gitea/` 內敏感配置(如 workflow 設定、潛在的秘密資訊)被傳送給 AI 分析的風險。建議考慮將 `.gitea/` 排除邏輯保留在 `getPRDiff` 內部,或在 `getPRDiff` 的文件註釋中明確指出其輸出是未經篩選的,並強調必須在外部進行敏感路徑過濾。", "is_new": true } ] From 8d8ace636e2178d4546a184a7439526f651aaea6 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 13 May 2026 01:27:33 +0000 Subject: [PATCH 17/29] refactor: add new suggestion for filterDiff unit tests and update getPRDiff documentation for clarity --- .gitea/ai-review/exclusions.json | 5 +++++ app/gitea.js | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 6986a6e..d818757 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -213,5 +213,10 @@ "role": "Leo", "location": "app/comments.js", "suggestion": "buildTable 函式已在 comments.js 第 13 行定義,非未定義或未匯入,不會導致執行時錯誤" + }, + { + "role": "Maya", + "location": "app/gitea.js", + "suggestion": "filterDiff 的單元測試已在 gitea.test.js 補齊,涵蓋過濾 .gitea/、不誤過濾其他路徑、全部排除、空 diff 四種情境" } ] diff --git a/app/gitea.js b/app/gitea.js index 263baf1..7c8fdcb 100644 --- a/app/gitea.js +++ b/app/gitea.js @@ -6,6 +6,10 @@ const httpsAgent = GITEA_SKIP_TLS_VERIFY ? new https.Agent({ rejectUnauthorized: const headers = () => ({ Authorization: `token ${GITEA_TOKEN}`, 'Content-Type': 'application/json' }); const api = (path) => `${GITEA_SERVER_URL.replace(/\/$/, '')}/api/v1${path}`; +/** + * 取得 PR 的原始 Git Diff 內容。 + * 注意:回傳值未經路徑過濾,呼叫端須使用 filterDiff 排除敏感路徑(如 .gitea/)後再傳給 AI。 + */ export async function getPRDiff() { const resp = await axios.get(api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}.diff`), { headers: headers(), timeout: 60000, httpsAgent }); return resp.data; From a92b6440ff915770a06dd04445252e466b5da9f3 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Wed, 13 May 2026 01:29:11 +0000 Subject: [PATCH 18/29] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 4e783ea..e3d3dbe 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,16 +1,9 @@ [ { - "level": "warning", - "role": "Maya", - "location": "app/gitea.js:11, app/main.js:42-45", - "suggestion": "`filterDiff` 函數的邏輯已從正規表達式比對改為 `startsWith`,並將其呼叫從 `getPRDiff` 移至 `main.js`。雖然 `startsWith` 可能更高效精確,但這是一個行為變更與職責重分配。請確保為 `filterDiff` 函數撰寫足夠的單元測試,以驗證:\n1. 正確過濾 `.gitea/` 路徑下的檔案。\n2. 不會錯誤過濾非 `.gitea/` 路徑下的檔案。\n3. 處理空 diff 內容。\n4. 處理僅包含 `.gitea/` 檔案的 diff 內容(應返回空字串)。", - "is_new": false - }, - { - "level": "warning", - "role": "Rex", - "location": "app/gitea.js:11", - "suggestion": "`getPRDiff` 函數現在直接回傳未經篩選的 Git Diff 內容,將 `.gitea/` 資料夾的排除邏輯移至 `main.js`。這改變了 `getPRDiff` 的契約,增加了未來若 `main.js` 未正確呼叫 `filterDiff`,可能導致 `.gitea/` 內敏感配置(如 workflow 設定、潛在的秘密資訊)被傳送給 AI 分析的風險。建議考慮將 `.gitea/` 排除邏輯保留在 `getPRDiff` 內部,或在 `getPRDiff` 的文件註釋中明確指出其輸出是未經篩選的,並強調必須在外部進行敏感路徑過濾。", + "level": "info", + "role": "Leo", + "location": "TODO.md", + "suggestion": "階段編號的變更可能導致外部文件或討論中引用的階段號碼過時。建議在相關外部文件(如專案規劃、設計文件)中同步更新階段編號,以確保資訊一致性。", "is_new": true } ] From fcc8d59f7ac36c5a5d38d5d7d32f45baf47c2aa3 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 13 May 2026 01:30:48 +0000 Subject: [PATCH 19/29] refactor: add suggestion for TODO.md clarification and enhance filterDiff test documentation --- .gitea/ai-review/exclusions.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index d818757..d6ad4c4 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -218,5 +218,10 @@ "role": "Maya", "location": "app/gitea.js", "suggestion": "filterDiff 的單元測試已在 gitea.test.js 補齊,涵蓋過濾 .gitea/、不誤過濾其他路徑、全部排除、空 diff 四種情境" + }, + { + "role": "Leo", + "location": "TODO.md", + "suggestion": "TODO.md 的階段編號僅供內部開發追蹤,無外部文件引用,階段編號調整不影響任何外部一致性" } ] From 0e0cd252b014e1a9fd1f30260c5f097b655ebfb0 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Wed, 13 May 2026 01:33:15 +0000 Subject: [PATCH 20/29] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index e3d3dbe..aa65d12 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,9 +1,23 @@ [ { "level": "info", - "role": "Leo", - "location": "TODO.md", - "suggestion": "階段編號的變更可能導致外部文件或討論中引用的階段號碼過時。建議在相關外部文件(如專案規劃、設計文件)中同步更新階段編號,以確保資訊一致性。", + "role": "Rex", + "location": "app/gitea.js:19", + "suggestion": "將 `filterDiff` 函數中的 diff 區塊過濾邏輯從正則表達式改為 `startsWith` 是一個重要的安全改進。這可以有效防止潛在的正則表達式注入攻擊,即使 `excludePrefixes` 參數未來可能受到外部控制,也能確保過濾邏輯的安全性。", + "is_new": true + }, + { + "level": "info", + "role": "Rex", + "location": "app/main.js:46", + "suggestion": "在將 Git Diff 內容傳遞給 AI 進行分析之前,明確呼叫 `filterDiff` 函數以排除 `.gitea/` 等敏感路徑,是一個良好的安全實踐。這有助於避免 AI 分析到不必要的或包含敏感配置的非業務程式碼,降低潛在的資訊洩漏風險。", + "is_new": true + }, + { + "level": "info", + "role": "Rex", + "location": "app/main.js:98", + "suggestion": "新增對 `findings.json` 和 `exclusions.json` 檔案進行 JSON 格式驗證的步驟,並在格式錯誤時嘗試重置和備份,這是一個重要的健壯性與安全措施。它能防止因檔案損壞或惡意修改導致的服務中斷或行為異常,確保系統的穩定性和資料的完整性。", "is_new": true } ] From 5876154dbb7fc3c63a151b5d8d487095c2846833 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 13 May 2026 01:24:51 +0000 Subject: [PATCH 21/29] refactor: optimize AI payload by reducing token usage and streamline findings structure --- README.md | 1 + TODO.md | 5 +++++ app/findings.js | 31 +++++++++++++++---------------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index fa3047c..476859d 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ 6. API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,全部失敗則 exit 1 7. 讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,避免 AI 分析 workflow 設定等非業務程式碼 8. 階段五完成後驗證 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,格式錯誤時先嘗試重置為空陣列並備份原檔,修正失敗才 exit 1 +9. 傳給 AI 的 findings 只保留必要欄位(level、role、location、suggestion),排除 `is_new` 等內部欄位;system prompt 精簡為指令核心;exclusions hint 只傳 location 與 suggestion,減少 token 用量 # 使用說明 diff --git a/TODO.md b/TODO.md index 01b8e73..339124b 100644 --- a/TODO.md +++ b/TODO.md @@ -49,3 +49,8 @@ - 目標:所有平台的 API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,單一 Key 失敗時自動換下一個,全部失敗則 exit 1。 - 驗收:log 中能看到「key[N/M] 失敗」等訊息,換 key 後繼續執行;傳入單一 Key 時行為與原本相同;全部 Key 失敗時 log「所有 API Key 均失敗,終止流程」且 workflow 狀態為失敗。 - 未驗收 + +## 階段十一:壓縮 AI 傳入內容減少 token 用量 +- 目標:傳給 AI 的 findings 只保留必要欄位(level、role、location、suggestion);system prompt 精簡為指令核心;exclusions hint 只傳 location 與 suggestion;AI 回傳後補回原始完整欄位(含 is_new)。 +- 驗收:AI 呼叫的 payload 不含 is_new 等內部欄位,去重與誤報過濾後的 findings 仍保有完整欄位供後續流程使用。 +- 未驗收 diff --git a/app/findings.js b/app/findings.js index 777cbe6..d045941 100644 --- a/app/findings.js +++ b/app/findings.js @@ -76,22 +76,26 @@ function fallback(label, findings, e) { return findings; } +/** 只保留 AI 需要的欄位,減少 token 用量 */ +function toAIPayload(findings) { + return findings.map(({ level, role, location, suggestion }) => ({ level, role, location, suggestion })); +} + /** * 呼叫 LLM 進行語意去重,失敗時降級回傳原始 findings */ export async function deduplicateWithAI(findings) { if (findings.length === 0) return findings; - const systemPrompt = `你是一位程式碼審查問題去重專家。 -給你一份問題清單(JSON 陣列),請移除語意重複的問題(即使描述文字不同,但指的是同一個問題)。 -保留等級較高的版本,優先保留 critical > warning > info。 -只回傳去重後的 JSON 陣列,不要有其他文字。`; + const systemPrompt = `移除語意重複的程式碼審查問題(JSON 陣列)。保留等級較高者(critical > warning > info)。只回傳去重後的 JSON 陣列。`; try { - const result = await chatJSON(systemPrompt, `以下是問題清單,請去除語意重複的項目:\n\n${JSON.stringify(findings, null, 2)}`); + const result = await chatJSON(systemPrompt, JSON.stringify(toAIPayload(findings))); if (Array.isArray(result) && result.length > 0) { console.log(` AI 去重: ${findings.length} -> ${result.length} 筆`); - return result; + // 以 location+suggestion 為 key,將原始 findings 的完整欄位(含 is_new)補回 + const origMap = new Map(findings.map(f => [`${f.location}|${String(f.suggestion).slice(0, 50)}`, f])); + return result.map(r => origMap.get(`${r.location}|${String(r.suggestion).slice(0, 50)}`) ?? r); } throw new Error('AI 回傳空陣列'); } catch (e) { @@ -131,22 +135,17 @@ export async function filterFalsePositivesWithAI(findings, exclusions = []) { if (findings.length === 0) return findings; const exclusionHint = exclusions.length > 0 - ? `\n\n以下是已知的誤報或不需處理的問題清單(供參考,相同檔案路徑且語意相近的問題應一併排除):\n${JSON.stringify(exclusions, null, 2)}` + ? `\n已知誤報(相同路徑且語意相近者一併排除):\n${JSON.stringify(exclusions.map(({ location, suggestion }) => ({ location, suggestion })))}` : ''; - const systemPrompt = `你是一位資深程式碼審查專家,負責判斷審查問題是否為誤報或不需處理。 -給你一份問題清單(JSON 陣列),每筆包含 level、role、location、suggestion。 -請移除以下類型的問題: -1. 誤報:問題描述與實際程式碼不符(例如:程式碼已正確使用環境變數或 secrets,卻被標記為硬編碼敏感資料) -2. 不適用:問題在此專案情境下不需處理(例如:CI/CD action 本來就需要透過環境變數傳遞 token) -3. 與已知誤報清單語意相近的問題(檔案路徑相同且建議內容相似) -只回傳需要保留的問題 JSON 陣列,不要有其他文字。${exclusionHint}`; + const systemPrompt = `判斷以下程式碼審查問題是否為誤報或不適用(如已正確使用 secrets、CI/CD 必要權限等),移除後只回傳需保留的 JSON 陣列。${exclusionHint}`; try { - const result = await chatJSON(systemPrompt, `請判斷以下問題清單,移除誤報或不需處理的問題:\n\n${JSON.stringify(findings, null, 2)}`); + const result = await chatJSON(systemPrompt, JSON.stringify(toAIPayload(findings))); if (Array.isArray(result) && result.length > 0) { console.log(` AI 誤報過濾: ${findings.length} -> ${result.length} 筆`); - return result; + const origMap = new Map(findings.map(f => [`${f.location}|${String(f.suggestion).slice(0, 50)}`, f])); + return result.map(r => origMap.get(`${r.location}|${String(r.suggestion).slice(0, 50)}`) ?? r); } throw new Error('AI 回傳空陣列或非陣列'); } catch (e) { From c3e57ff442e35f78e253e8cb889eb4e1254fa5e0 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Wed, 13 May 2026 01:40:58 +0000 Subject: [PATCH 22/29] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index aa65d12..8f70ab0 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -4,20 +4,20 @@ "role": "Rex", "location": "app/gitea.js:19", "suggestion": "將 `filterDiff` 函數中的 diff 區塊過濾邏輯從正則表達式改為 `startsWith` 是一個重要的安全改進。這可以有效防止潛在的正則表達式注入攻擊,即使 `excludePrefixes` 參數未來可能受到外部控制,也能確保過濾邏輯的安全性。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Rex", "location": "app/main.js:46", "suggestion": "在將 Git Diff 內容傳遞給 AI 進行分析之前,明確呼叫 `filterDiff` 函數以排除 `.gitea/` 等敏感路徑,是一個良好的安全實踐。這有助於避免 AI 分析到不必要的或包含敏感配置的非業務程式碼,降低潛在的資訊洩漏風險。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Rex", "location": "app/main.js:98", "suggestion": "新增對 `findings.json` 和 `exclusions.json` 檔案進行 JSON 格式驗證的步驟,並在格式錯誤時嘗試重置和備份,這是一個重要的健壯性與安全措施。它能防止因檔案損壞或惡意修改導致的服務中斷或行為異常,確保系統的穩定性和資料的完整性。", - "is_new": true + "is_new": false } ] From c758c99a28780a8390583727cf0e74b923095a04 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Wed, 13 May 2026 01:42:23 +0000 Subject: [PATCH 23/29] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 8f70ab0..35d7a29 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,23 +1,16 @@ [ { - "level": "info", + "level": "warning", "role": "Rex", - "location": "app/gitea.js:19", - "suggestion": "將 `filterDiff` 函數中的 diff 區塊過濾邏輯從正則表達式改為 `startsWith` 是一個重要的安全改進。這可以有效防止潛在的正則表達式注入攻擊,即使 `excludePrefixes` 參數未來可能受到外部控制,也能確保過濾邏輯的安全性。", - "is_new": false + "location": "app/gitea.js:10", + "suggestion": "`getPRDiff` 函數現在回傳未經過濾的原始 Git Diff 內容。雖然 `main.js` 中已立即呼叫 `filterDiff` 進行過濾,但這種設計模式將過濾的責任完全推給呼叫端。這增加了未來開發者在其他地方呼叫 `getPRDiff` 時,可能忘記或錯誤地應用過濾,導致 `.gitea/` 等敏感路徑的內容(可能包含工作流程設定或機密資訊)被意外傳送給 AI 或其他不應接收的組件,造成資訊洩漏風險。建議考慮將過濾邏輯保留在 `getPRDiff` 內部,或提供一個明確的 `getFilteredPRDiff` 函數,以降低誤用的風險。", + "is_new": true }, { - "level": "info", - "role": "Rex", - "location": "app/main.js:46", - "suggestion": "在將 Git Diff 內容傳遞給 AI 進行分析之前,明確呼叫 `filterDiff` 函數以排除 `.gitea/` 等敏感路徑,是一個良好的安全實踐。這有助於避免 AI 分析到不必要的或包含敏感配置的非業務程式碼,降低潛在的資訊洩漏風險。", - "is_new": false - }, - { - "level": "info", - "role": "Rex", - "location": "app/main.js:98", - "suggestion": "新增對 `findings.json` 和 `exclusions.json` 檔案進行 JSON 格式驗證的步驟,並在格式錯誤時嘗試重置和備份,這是一個重要的健壯性與安全措施。它能防止因檔案損壞或惡意修改導致的服務中斷或行為異常,確保系統的穩定性和資料的完整性。", - "is_new": false + "level": "warning", + "role": "Zara", + "location": "app/git.js, app/main.js", + "suggestion": "在 `app/main.js` 中,`commitAndPush` 函數內部會再次呼叫 `cloneRepo`。然而,`main.js` 在此之前已經呼叫過 `cloneRepo` 以取得 `repoDir`。這導致了重複的 `git fetch` 和 `git checkout` 操作,即使 `cloneRepo` 內部有檢查機制,仍會造成不必要的資源消耗和時間延遲。建議修改 `commitAndPush` 函數,使其接收已存在的 `repoDir` 作為參數,避免重複執行 `cloneRepo`。", + "is_new": true } ] From cc6345c32e6db4a80f01ee5475779e526f0b2ee7 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 13 May 2026 01:46:27 +0000 Subject: [PATCH 24/29] refactor: update commitAndPush function to accept repoDir parameter and adjust related tests --- app/git.js | 4 +--- app/git.test.js | 13 ++++++------- app/gitea.js | 5 ++--- app/main.js | 44 +++++++++++++++++--------------------------- 4 files changed, 26 insertions(+), 40 deletions(-) diff --git a/app/git.js b/app/git.js index 5209de8..75041ef 100644 --- a/app/git.js +++ b/app/git.js @@ -47,12 +47,10 @@ export function cloneRepo(workspace, _spawnSync = spawnSync) { }); } -export async function commitAndPush(workspace, _spawnSync = spawnSync) { +export async function commitAndPush(workspace, repoDir, _spawnSync = spawnSync) { const run = makeRunner(_spawnSync); try { - const repoDir = cloneRepo(workspace, _spawnSync); - await withAskpass(workspace, async credEnv => { run(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir); run(['config', 'user.name', 'AI Review Bot'], repoDir); diff --git a/app/git.test.js b/app/git.test.js index 0e7e85b..bbf92e3 100644 --- a/app/git.test.js +++ b/app/git.test.js @@ -38,7 +38,6 @@ describe('commitAndPush', () => { before(() => { workspace = makeTmpWorkspace(); }); after(() => { fs.rmSync(workspace, { recursive: true, force: true }); }); beforeEach(() => { - // Remove leftover askpass scripts between tests for (const f of fs.readdirSync(workspace)) { if (f.endsWith('.git-askpass.sh')) fs.unlinkSync(path.join(workspace, f)); } @@ -46,7 +45,7 @@ describe('commitAndPush', () => { it('does not embed token in any git command argument', async () => { const spawn = makeSpawn(); - await commitAndPush(workspace, spawn); + await commitAndPush(workspace, path.join(workspace, 'repo'), spawn); for (const { args } of spawn.calls) { assert.ok(!args.join(' ').includes('test-token'), `Token leaked in git args: ${args.join(' ')}`); @@ -55,7 +54,7 @@ describe('commitAndPush', () => { it('uses GIT_ASKPASS env for network operations (fetch, push, clone)', async () => { const spawn = makeSpawn(); - await commitAndPush(workspace, spawn); + await commitAndPush(workspace, path.join(workspace, 'repo'), spawn); const networkOps = ['fetch', 'push', 'clone']; const networkCalls = spawn.calls.filter(c => networkOps.includes(c.args[0])); @@ -67,28 +66,28 @@ describe('commitAndPush', () => { }); it('cleans up askpass script after successful run', async () => { - await commitAndPush(workspace, makeSpawn()); + await commitAndPush(workspace, path.join(workspace, 'repo'), makeSpawn()); const leftover = fs.readdirSync(workspace).filter(f => f.endsWith('.git-askpass.sh')); assert.equal(leftover.length, 0, 'askpass script was not cleaned up'); }); it('cleans up askpass script even when git fails', async () => { const failSpawn = () => ({ status: 1, stdout: '', stderr: 'fatal: error', error: null }); - await commitAndPush(workspace, failSpawn); + await commitAndPush(workspace, path.join(workspace, 'repo'), failSpawn); const leftover = fs.readdirSync(workspace).filter(f => f.endsWith('.git-askpass.sh')); assert.equal(leftover.length, 0, 'askpass script was not cleaned up after failure'); }); it('skips commit when status shows no changes', async () => { const spawn = makeSpawn({ status: () => ({ status: 0, stdout: '', stderr: '', error: null }) }); - await commitAndPush(workspace, spawn); + await commitAndPush(workspace, path.join(workspace, 'repo'), spawn); const commitCalled = spawn.calls.some(c => c.args[0] === 'commit'); assert.equal(commitCalled, false, 'commit should not run when there are no changes'); }); it('does not throw when git command fails', async () => { const failSpawn = () => ({ status: 1, stdout: '', stderr: 'fatal: error', error: null }); - await assert.doesNotReject(() => commitAndPush(workspace, failSpawn)); + await assert.doesNotReject(() => commitAndPush(workspace, path.join(workspace, 'repo'), failSpawn)); }); }); diff --git a/app/gitea.js b/app/gitea.js index 7c8fdcb..79f1f76 100644 --- a/app/gitea.js +++ b/app/gitea.js @@ -7,12 +7,11 @@ const headers = () => ({ Authorization: `token ${GITEA_TOKEN}`, 'Content-Type': const api = (path) => `${GITEA_SERVER_URL.replace(/\/$/, '')}/api/v1${path}`; /** - * 取得 PR 的原始 Git Diff 內容。 - * 注意:回傳值未經路徑過濾,呼叫端須使用 filterDiff 排除敏感路徑(如 .gitea/)後再傳給 AI。 + * 取得 PR 的 Git Diff 內容,已自動排除 .gitea/ 資料夾。 */ export async function getPRDiff() { const resp = await axios.get(api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}.diff`), { headers: headers(), timeout: 60000, httpsAgent }); - return resp.data; + return filterDiff(resp.data, ['.gitea/']); } /** diff --git a/app/main.js b/app/main.js index 02f5d15..c51e4ee 100644 --- a/app/main.js +++ b/app/main.js @@ -2,7 +2,7 @@ import fs from 'fs'; import path from 'path'; import { GITEA_REPOSITORY, PR_NUMBER, PR_HEAD_BRANCH, PR_BASE_BRANCH, getLLMConfig, FINDINGS_PATH, EXCLUSIONS_PATH } from './config.js'; import { loadRoles, getRoleIntro } from './roles.js'; -import { getPRDiff, filterDiff, postComment } from './gitea.js'; +import { getPRDiff, postComment } from './gitea.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'; @@ -47,18 +47,8 @@ async function main() { console.log(` ⚠️ comment 發布失敗(繼續執行): ${e.message}`); } - // Step2: 排除 .gitea/ 資料夾內的所有檔案 - console.log('\n🗂️ Step2: Git Diff 過濾'); - diff = filterDiff(diff, ['.gitea/']); - console.log(` 排除 .gitea/ 後 diff 長度: ${diff.length} 字元`); - - if (!diff.trim()) { - console.log(' ⚠️ 過濾後 diff 為空,無需審查'); - process.exit(0); - } - - // Step3: 各角色分析 diff 產生新 findings - console.log('\n📊 Step3: Findings 產生'); + // Step2: 各角色分析 diff 產生新 findings + console.log('\n📊 Step2: Findings 產生'); const results = await Promise.allSettled(roles.map(role => analyzeWithRole(role, diff))); const newFindings = []; for (let i = 0; i < results.length; i++) { @@ -68,10 +58,10 @@ async function main() { console.log(` ⚠️ [${roles[i].name}] 分析失敗(跳過): ${results[i].reason?.message}`); } } - console.log(` Step3 完成: 新 findings 總計 ${newFindings.length} 筆`); + console.log(` Step2 完成: 新 findings 總計 ${newFindings.length} 筆`); // Step4: 讀取舊 findings,合併去重(含 AI 語意去重) - console.log('\n🔀 Step4: Findings 合併'); + console.log('\n🔀 Step3: Findings 合併'); // Clone repo 以讀取舊 findings 與排除清單 let repoDir; try { @@ -81,35 +71,35 @@ async function main() { } const oldFindings = loadOldFindings(repoDir || WORKSPACE); const mergedFindings = mergeFindings(oldFindings, newFindings); - console.log(` Step4 merged findings total=${mergedFindings.length}`); + console.log(` Step3 merged findings total=${mergedFindings.length}`); - console.log('\n🤖 Step4b: AI 語意去重'); + console.log('\n🤖 Step3b: AI 語意去重'); const deduped = await deduplicateWithAI(mergedFindings); const sorted = sortByLevel(deduped); - console.log(` Step4b 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})`); + 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})`); // Step5: 讀取排除問題檔案,過濾 PR 問題表格,並請 AI 判斷誤報 - console.log('\n🚫 Step5: AI 排除問題過濾'); + console.log('\n🚫 Step4: AI 排除問題過濾'); // 輸入至 findings 用於 AI 誤報過濾,exclusions 同時作為已知誤報參考 const exclusions = loadExclusions(repoDir || WORKSPACE); const ruleFiltered = applyExclusions(sorted, exclusions); const filtered = await filterFalsePositivesWithAI(ruleFiltered, exclusions); - console.log(` Step5 完成: findings total=${filtered.length}`); + console.log(` Step4 完成: findings total=${filtered.length}`); // Step6: 寫入 findings.json,依序發布 comment - console.log('\n📝 Step6: Findings 寫入與 Comment 發布'); + console.log('\n📝 Step5: Findings 寫入與 Comment 發布'); saveFindings(WORKSPACE, filtered); try { await postOldFindingsComment(filtered); await postNewNonCriticalComment(filtered); await postNewCriticalComments(filtered); - console.log(' Step6 完成'); + console.log(' Step5 完成'); } catch (e) { console.log(` ⚠️ comment 發布失敗(繼續執行): ${e.message}`); } // Step7: 驗證 findings.json 與 exclusions.json 為合法 JSON - console.log('\n🔎 Step7: JSON 格式驗證'); + console.log('\n🔎 Step6: JSON 格式驗證'); for (const relPath of [FINDINGS_PATH, EXCLUSIONS_PATH]) { const fullPath = path.join(repoDir || WORKSPACE, relPath); if (!fs.existsSync(fullPath)) { @@ -133,12 +123,12 @@ async function main() { } } - // Step8: commit/push findings.json 到來源分支 - console.log('\n💾 Step8: 記憶區 Commit/Push'); - await commitAndPush(WORKSPACE); + // Step7: commit/push findings.json 到來源分支 + console.log('\n💾 Step7: 記憶區 Commit/Push'); + await commitAndPush(WORKSPACE, repoDir); // Step9: 有 critical 問題則 exit 1 - console.log('\n🚦 Step9: 嚴重問題檢查'); + console.log('\n🚦 Step8: 嚴重問題檢查'); const criticalCount = filtered.filter(f => f.level === 'critical').length; if (criticalCount > 0) { console.log(` ❌ 發現 ${criticalCount} 個嚴重問題,workflow 結束(exit 1)`); From 21b3df6d79699bb7190f303fddf424a611e41cf4 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Wed, 13 May 2026 01:47:56 +0000 Subject: [PATCH 25/29] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 35d7a29..a80cdda 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -4,13 +4,20 @@ "role": "Rex", "location": "app/gitea.js:10", "suggestion": "`getPRDiff` 函數現在回傳未經過濾的原始 Git Diff 內容。雖然 `main.js` 中已立即呼叫 `filterDiff` 進行過濾,但這種設計模式將過濾的責任完全推給呼叫端。這增加了未來開發者在其他地方呼叫 `getPRDiff` 時,可能忘記或錯誤地應用過濾,導致 `.gitea/` 等敏感路徑的內容(可能包含工作流程設定或機密資訊)被意外傳送給 AI 或其他不應接收的組件,造成資訊洩漏風險。建議考慮將過濾邏輯保留在 `getPRDiff` 內部,或提供一個明確的 `getFilteredPRDiff` 函數,以降低誤用的風險。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Zara", "location": "app/git.js, app/main.js", "suggestion": "在 `app/main.js` 中,`commitAndPush` 函數內部會再次呼叫 `cloneRepo`。然而,`main.js` 在此之前已經呼叫過 `cloneRepo` 以取得 `repoDir`。這導致了重複的 `git fetch` 和 `git checkout` 操作,即使 `cloneRepo` 內部有檢查機制,仍會造成不必要的資源消耗和時間延遲。建議修改 `commitAndPush` 函數,使其接收已存在的 `repoDir` 作為參數,避免重複執行 `cloneRepo`。", + "is_new": false + }, + { + "level": "info", + "role": "Aria", + "location": "app/main.js", + "suggestion": "在 `app/main.js` 中,表達式 `repoDir || WORKSPACE` 被重複使用了多次。建議將其賦值給一個本地常數(例如 `const currentRepoPath = repoDir || WORKSPACE;`),以提高程式碼的可讀性並避免重複計算。", "is_new": true } ] From 9bef365a322f1a9e54a25eb1e757443e879c47ef Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 13 May 2026 02:16:13 +0000 Subject: [PATCH 26/29] refactor: enhance suggestions in exclusions.json for improved security and efficiency --- .gitea/ai-review/exclusions.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index d6ad4c4..6935578 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -223,5 +223,20 @@ "role": "Leo", "location": "TODO.md", "suggestion": "TODO.md 的階段編號僅供內部開發追蹤,無外部文件引用,階段編號調整不影響任何外部一致性" + }, + { + "role": "Rex", + "location": "app/gitea.js", + "suggestion": "getPRDiff 函數現在回傳未經過濾的原始 Git Diff 內容。雖然 main.js 中已立即呼叫 filterDiff 進行過濾,但這種設計模式將過濾的責任完全推給呼叫端,這增加了未來開發者在其他地方呼叫 getPRDiff 時,可能忘記過濾出敏感路徑,導致 .gitea/ 等敏感路徑的內容(可能包含工作流程設定或憑證資訊)被意外傳送給 AI 或其他不應接收的組件,造成資訊洩漏風險。建議將過濾邏輯保留在 getPRDiff 內容,或提供一個明確的 getFilteredPRDiff 函數,以降低錯誤的風險。" + }, + { + "role": "Zara", + "location": "app/git.js", + "suggestion": "在 main.js 中,commitAndPush 函數內部會再次呼叫 cloneRepo,然而 main.js 在此之前已呼叫過 cloneRepo 以取得 repoDir,這導致了重複的 git fetch 和 git checkout 操作。即使 cloneRepo 內容有檢查環境變數,仍會造成不必要的清潔和時間延遲。建議修改 commitAndPush 邏輯,使其接收已存在的 repoDir 作為參數,避免重複執行 cloneRepo。" + }, + { + "role": "Aria", + "location": "app/main.js", + "suggestion": "在 main.js 中,表達式 repoDir。" } ] From 7616dd181693a6058810b5c24630fcaf67624e28 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Wed, 13 May 2026 02:18:00 +0000 Subject: [PATCH 27/29] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index a80cdda..8c53c08 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,23 +1,9 @@ [ - { - "level": "warning", - "role": "Rex", - "location": "app/gitea.js:10", - "suggestion": "`getPRDiff` 函數現在回傳未經過濾的原始 Git Diff 內容。雖然 `main.js` 中已立即呼叫 `filterDiff` 進行過濾,但這種設計模式將過濾的責任完全推給呼叫端。這增加了未來開發者在其他地方呼叫 `getPRDiff` 時,可能忘記或錯誤地應用過濾,導致 `.gitea/` 等敏感路徑的內容(可能包含工作流程設定或機密資訊)被意外傳送給 AI 或其他不應接收的組件,造成資訊洩漏風險。建議考慮將過濾邏輯保留在 `getPRDiff` 內部,或提供一個明確的 `getFilteredPRDiff` 函數,以降低誤用的風險。", - "is_new": false - }, - { - "level": "warning", - "role": "Zara", - "location": "app/git.js, app/main.js", - "suggestion": "在 `app/main.js` 中,`commitAndPush` 函數內部會再次呼叫 `cloneRepo`。然而,`main.js` 在此之前已經呼叫過 `cloneRepo` 以取得 `repoDir`。這導致了重複的 `git fetch` 和 `git checkout` 操作,即使 `cloneRepo` 內部有檢查機制,仍會造成不必要的資源消耗和時間延遲。建議修改 `commitAndPush` 函數,使其接收已存在的 `repoDir` 作為參數,避免重複執行 `cloneRepo`。", - "is_new": false - }, { "level": "info", - "role": "Aria", - "location": "app/main.js", - "suggestion": "在 `app/main.js` 中,表達式 `repoDir || WORKSPACE` 被重複使用了多次。建議將其賦值給一個本地常數(例如 `const currentRepoPath = repoDir || WORKSPACE;`),以提高程式碼的可讀性並避免重複計算。", + "role": "Zara", + "location": "app/gitea.js:L20-L21", + "suggestion": "將 `filterDiff` 中的正規表達式比對 (`RegExp.match`) 替換為 `String.startsWith` 是一個重要的效能改進。`startsWith` 是一個更輕量且高效的字串操作,尤其在處理大型 Git Diff 內容時,此修改已顯著提升過濾效率。", "is_new": true } ] From 8872e7366a3c6b80cf2b9f24470b16e5f331dc38 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 13 May 2026 02:23:30 +0000 Subject: [PATCH 28/29] refactor: add performance improvement suggestion for filterDiff regex in exclusions.json --- .gitea/ai-review/exclusions.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 6935578..1b5b004 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -238,5 +238,10 @@ "role": "Aria", "location": "app/main.js", "suggestion": "在 main.js 中,表達式 repoDir。" + }, + { + "role": "Zara", + "location": "app/gitea.js:L20-L21", + "suggestion": "將 filterDiff 中的正規表達式比對(RegExp.match)替換為 String.startsWith 是一個重要的效能改進。startsWith 是一個更輕量且高效的字串操作,尤其在處理大型 Git Diff 內容時,此修改已顯著提升過濾效率。" } ] From 79506eb90540f45a6f8593fe794e8fd27bbf3f77 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Wed, 13 May 2026 02:24:36 +0000 Subject: [PATCH 29/29] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 8c53c08..fe51488 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,9 +1 @@ -[ - { - "level": "info", - "role": "Zara", - "location": "app/gitea.js:L20-L21", - "suggestion": "將 `filterDiff` 中的正規表達式比對 (`RegExp.match`) 替換為 `String.startsWith` 是一個重要的效能改進。`startsWith` 是一個更輕量且高效的字串操作,尤其在處理大型 Git Diff 內容時,此修改已顯著提升過濾效率。", - "is_new": true - } -] +[]