From 1a45c53aa23bfe27ab3a19a5ad7a0172a47025f6 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 06:22:59 +0000 Subject: [PATCH 001/174] Add README.md for AI Code Review Action documentation --- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..a354bcd --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# 簡介 + +這是一個 AI Code Review Action。Gitea Workflow 可以使用此 Action 讓 AI 助理根據不同面向分析 Push Request 中變更的內容後,將問題分級 Commnet 到 Push Request 中。 + +# 流程 + +1. 服務名稱、模型名稱、角色資訊(個性、符合個性的英文名稱、工作內容),Comment 到 Push Request +2. 每個角色個別分析 Git Diff 的內容產生新問題表格(問題等級、角色名稱、問題位置或行數、修改建議) +3. 讀取所有未解決的舊問題(問題檔案存在於使用此 Action 的專案固定位置)加上新問題後,去除重複產生本次 Push Request 的問題表格(PR問題表格)覆蓋問題檔案 +4. 從PR問題表格中取出所有舊問題,依照等級排序後 Comment 到 Push Request +5. 從PR問題表格中取出所有新問題,排除嚴重等級的問題後 Comment 到 Push Request +6. 從PR問題表格中取出所有新問題,將每個嚴重等級的問題 Comment 到 Push Request + +# 使用說明 + +1. 在 Gitea 專案中建立 `.gitea/workflows` 資料夾 +2. 在 `.gitea/workflows` 資料夾中建立 `ai-review.yaml' +3. 在 `ai-review.yaml` 中填入以下內容: + +如果服務提供者是 OpenRouter + +```yaml +name: AI +on: + pull_request: + types: [opened, synchronize] +jobs: + code-review: + name: 'Code Review' + runs-on: ubuntu + steps: + - name: AI Code Review + uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} + with: + OPENAI_API_KEY: ${{ secrets.OPEN_ROUTER_TOKEN }} + permissions: + contents: write + pull-requests: write + issues: write +``` + +如果服務提供者是 Ollama + +```yaml +name: AI +on: + pull_request: + types: [opened, synchronize] +jobs: + code-review: + name: 'Code Review' + runs-on: ubuntu + steps: + - name: AI Code Review + uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} + with: + OLLAMA_BASE_URL: ${{ secrets.OLLAMA_BASE_URL }} + OLLAMA_MODEL: ${{ secrets.OLLAMA_MODEL }} + permissions: + contents: write + pull-requests: write + issues: write +``` \ No newline at end of file From f158182229714e69dce4151719ac28020293ee59 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 06:40:55 +0000 Subject: [PATCH 002/174] Enhance README.md with detailed configuration examples for various AI Code Review services Co-authored-by: Copilot --- README.md | 207 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 203 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a354bcd..b538ac5 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,7 @@ 2. 在 `.gitea/workflows` 資料夾中建立 `ai-review.yaml' 3. 在 `ai-review.yaml` 中填入以下內容: -如果服務提供者是 OpenRouter - +### 1. OpenAI(OpenRouter) ```yaml name: AI on: @@ -32,14 +31,213 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} with: - OPENAI_API_KEY: ${{ secrets.OPEN_ROUTER_TOKEN }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENAI_BASE_URL: https://api.openai.com/v1 permissions: contents: write pull-requests: write issues: write ``` -如果服務提供者是 Ollama +### 2. Anthropic Claude +```yaml +name: AI +on: + pull_request: + types: [opened, synchronize] +jobs: + code-review: + name: 'Code Review' + runs-on: ubuntu + steps: + - name: AI Code Review + uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} + with: + CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} + CLAUDE_BASE_URL: https://api.anthropic.com/v1 + permissions: + contents: write + pull-requests: write + issues: write +``` + +### 3. Google Gemini +```yaml +name: AI +on: + pull_request: + types: [opened, synchronize] +jobs: + code-review: + name: 'Code Review' + runs-on: ubuntu + steps: + - name: AI Code Review + uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} + with: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta + permissions: + contents: write + pull-requests: write + issues: write +``` + +### 4. Amazon Q +```yaml +name: AI +on: + pull_request: + types: [opened, synchronize] +jobs: + code-review: + name: 'Code Review' + runs-on: ubuntu + steps: + - name: AI Code Review + uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} + with: + AMAZONQ_API_KEY: ${{ secrets.AMAZONQ_API_KEY }} + AMAZONQ_BASE_URL: https://q.api.aws + permissions: + contents: write + pull-requests: write + issues: write +``` + +### 5. SonarQube +```yaml +name: AI +on: + pull_request: + types: [opened, synchronize] +jobs: + code-review: + name: 'Code Review' + runs-on: ubuntu + steps: + - name: AI Code Review + uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} + with: + SONARQUBE_TOKEN: ${{ secrets.SONARQUBE_TOKEN }} + SONARQUBE_URL: https://sonarqube.example.com + permissions: + contents: write + pull-requests: write + issues: write +``` + +### 6. Kilo Code +```yaml +name: AI +on: + pull_request: + types: [opened, synchronize] +jobs: + code-review: + name: 'Code Review' + runs-on: ubuntu + steps: + - name: AI Code Review + uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} + with: + KILO_API_KEY: ${{ secrets.KILO_API_KEY }} + KILO_BASE_URL: https://api.kilocode.com/v1 + permissions: + contents: write + pull-requests: write + issues: write +``` + +### 7. Roo Code +```yaml +name: AI +on: + pull_request: + types: [opened, synchronize] +jobs: + code-review: + name: 'Code Review' + runs-on: ubuntu + steps: + - name: AI Code Review + uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} + with: + ROO_API_KEY: ${{ secrets.ROO_API_KEY }} + ROO_BASE_URL: https://api.roocode.com/v1 + permissions: + contents: write + pull-requests: write + issues: write +``` + +### 8. Cline +```yaml +name: AI +on: + pull_request: + types: [opened, synchronize] +jobs: + code-review: + name: 'Code Review' + runs-on: ubuntu + steps: + - name: AI Code Review + uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} + with: + CLINE_API_KEY: ${{ secrets.CLINE_API_KEY }} + CLINE_BASE_URL: https://api.cline.dev/v1 + permissions: + contents: write + pull-requests: write + issues: write +``` + +### 9. Continue +```yaml +name: AI +on: + pull_request: + types: [opened, synchronize] +jobs: + code-review: + name: 'Code Review' + runs-on: ubuntu + steps: + - name: AI Code Review + uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} + with: + CONTINUE_API_KEY: ${{ secrets.CONTINUE_API_KEY }} + CONTINUE_BASE_URL: https://api.continue.dev/v1 + permissions: + contents: write + pull-requests: write + issues: write +``` + +### 10. Kade +```yaml +name: AI +on: + pull_request: + types: [opened, synchronize] +jobs: + code-review: + name: 'Code Review' + runs-on: ubuntu + steps: + - name: AI Code Review + uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} + with: + KADE_API_KEY: ${{ secrets.KADE_API_KEY }} + KADE_BASE_URL: https://api.kade.dev/v1 + permissions: + contents: write + pull-requests: write + issues: write +``` + +### - Ollama ```yaml name: AI @@ -59,5 +257,6 @@ jobs: permissions: contents: write pull-requests: write + issues: write ``` \ No newline at end of file From 02247899a38011a6b299779c587747dfa371b4c7 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 06:42:30 +0000 Subject: [PATCH 003/174] Update README.md to clarify instructions for ai-review.yaml configuration --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b538ac5..9cc0f9c 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ 1. 在 Gitea 專案中建立 `.gitea/workflows` 資料夾 2. 在 `.gitea/workflows` 資料夾中建立 `ai-review.yaml' -3. 在 `ai-review.yaml` 中填入以下內容: +3. 在 `ai-review.yaml` 中填入以下內容(選擇一個使用): ### 1. OpenAI(OpenRouter) ```yaml From 86f30f3158b9374d070fdd61d3e1066ca484b4ab Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 06:50:33 +0000 Subject: [PATCH 004/174] Refactor README.md to clarify workflow process and additional notes --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9cc0f9c..a62fb07 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 這是一個 AI Code Review Action。Gitea Workflow 可以使用此 Action 讓 AI 助理根據不同面向分析 Push Request 中變更的內容後,將問題分級 Commnet 到 Push Request 中。 -# 流程 +# 流程(新 Push Request、新 Commit (排除 AI 助理的 Commit) 觸發) 1. 服務名稱、模型名稱、角色資訊(個性、符合個性的英文名稱、工作內容),Comment 到 Push Request 2. 每個角色個別分析 Git Diff 的內容產生新問題表格(問題等級、角色名稱、問題位置或行數、修改建議) @@ -10,6 +10,13 @@ 4. 從PR問題表格中取出所有舊問題,依照等級排序後 Comment 到 Push Request 5. 從PR問題表格中取出所有新問題,排除嚴重等級的問題後 Comment 到 Push Request 6. 從PR問題表格中取出所有新問題,將每個嚴重等級的問題 Comment 到 Push Request +7. Commit 問題檔案 + +# 額外說明 + +1. Gitea 的相關參數如果 inputs 沒有定義,則從 ${{ gitea.* }} 取得 +2. BASE_URL 如果 inputs 沒有定義,則使用預設值 +3. Comment 加上些許 emoji 讓資訊有點活力 # 使用說明 From d7ef864458e6c6eeb59e5c12420229daa94e8f60 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 06:52:51 +0000 Subject: [PATCH 005/174] Refactor README.md to update section headers and enhance application setup instructions --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a62fb07..a351e33 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,12 @@ 6. 從PR問題表格中取出所有新問題,將每個嚴重等級的問題 Comment 到 Push Request 7. Commit 問題檔案 -# 額外說明 +# 設計 1. Gitea 的相關參數如果 inputs 沒有定義,則從 ${{ gitea.* }} 取得 2. BASE_URL 如果 inputs 沒有定義,則使用預設值 3. Comment 加上些許 emoji 讓資訊有點活力 +4. 盡量將應用程式放在 ./app,修改 entrypoint.sh 與 Dockerfile 讓程式可以正常運行 # 使用說明 From 713289737a6dba9597f7ac75fe22ad1f7402b3b4 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 07:02:48 +0000 Subject: [PATCH 006/174] Update README.md to include instruction for placing prompts in ./app/prompts --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a351e33..f39e3f9 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ 2. BASE_URL 如果 inputs 沒有定義,則使用預設值 3. Comment 加上些許 emoji 讓資訊有點活力 4. 盡量將應用程式放在 ./app,修改 entrypoint.sh 與 Dockerfile 讓程式可以正常運行 +5. 將提示詞放到 ./app/prompts 內供程式讀取 # 使用說明 From 2ec18843a3149fc57d09a6aed58259e610750b56 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 07:08:59 +0000 Subject: [PATCH 007/174] Update README.md to clarify workflow behavior when severe issues are present --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f39e3f9..532c85c 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ 5. 從PR問題表格中取出所有新問題,排除嚴重等級的問題後 Comment 到 Push Request 6. 從PR問題表格中取出所有新問題,將每個嚴重等級的問題 Comment 到 Push Request 7. Commit 問題檔案 +8. 如果PR問題表格中有嚴重問題,則不要讓 workflow 執行成功(exit 1) # 設計 From 6e8b6492da7ce5b68f177b8c1a27dce0d7823b73 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 07:09:37 +0000 Subject: [PATCH 008/174] Add initial TODO documentation for development phases and workflow processes Co-authored-by: Copilot --- TODO.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..791a6a7 --- /dev/null +++ b/TODO.md @@ -0,0 +1,31 @@ +# 開發階段 TODO + +## 階段一:基本流程串接 +- 目標:確保 action 可以被觸發,pipeline 各步驟依序執行,log 出每個主要階段的進入與完成。 +- 驗收:log 中能看到每個階段(如「Step1: pipeline start」、「Step2: findings merge」等)明確訊息,且流程能走完(即使還沒產生 findings)。 + +## 階段二: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),降級時有「保留所有問題」等明確訊息。 + +## 階段四:findings 寫入與 comment 發布 +- 目標:findings.jsonl 正確寫入,comment 發布順序正確(舊問題→非嚴重→嚴重),每步有 log。 +- 驗收:log 中能看到 findings 寫入、comment sync 的詳細訊息與順序。 + +## 階段五:記憶區 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 狀態為失敗。 + +--- + +每個階段都會加上明確的 log,並確保即使部分功能未完成也能降級執行、不會中斷 pipeline。 + +每次執行後請貼 log,我會協助 debug。 \ No newline at end of file From 1324f1575d2bad2b02134996a84788def04b6991 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 07:23:06 +0000 Subject: [PATCH 009/174] =?UTF-8?q?feat:=20=E9=9A=8E=E6=AE=B5=E4=B8=80=20-?= =?UTF-8?q?=20=E5=9F=BA=E6=9C=AC=E6=B5=81=E7=A8=8B=E4=B8=B2=E6=8E=A5?= =?UTF-8?q?=E9=AA=A8=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重寫 action.yaml:支援所有 LLM providers 的 inputs - 重寫 Dockerfile:python:3.11-slim + git - 重寫 entrypoint.sh:啟動 app/main.py - app/config.py:環境變數與 LLM 自動偵測 - app/llm.py:OpenAI-compatible 統一介面 - app/gitea.py:PR diff 取得與 comment 發布 - app/roles.py:從 prompts/roles/*.yaml 載入角色 - app/main.py:pipeline 骨架,log 每個主要階段 - app/prompts/roles/:五個角色定義(Aria/Rex/Zara/Leo/Maya) --- Dockerfile | 17 ++- action.yaml | 166 ++++++++++++++++++++++--- app/config.py | 31 +++++ app/gitea.py | 26 ++++ app/llm.py | 50 ++++++++ app/main.py | 75 +++++++++++ app/prompts/roles/maintainability.yaml | 23 ++++ app/prompts/roles/performance.yaml | 23 ++++ app/prompts/roles/security.yaml | 23 ++++ app/prompts/roles/style.yaml | 23 ++++ app/prompts/roles/testing.yaml | 23 ++++ app/requirements.txt | 3 + app/roles.py | 22 ++++ entrypoint.sh | 13 +- 14 files changed, 490 insertions(+), 28 deletions(-) create mode 100644 app/config.py create mode 100644 app/gitea.py create mode 100644 app/llm.py create mode 100644 app/main.py create mode 100644 app/prompts/roles/maintainability.yaml create mode 100644 app/prompts/roles/performance.yaml create mode 100644 app/prompts/roles/security.yaml create mode 100644 app/prompts/roles/style.yaml create mode 100644 app/prompts/roles/testing.yaml create mode 100644 app/requirements.txt create mode 100644 app/roles.py diff --git a/Dockerfile b/Dockerfile index af3dacb..1ef0bb9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,15 @@ -FROM alpine:latest +FROM python:3.11-slim -# 安裝必要的工具 -RUN apk add --no-cache --no-check-certificate bash - +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /action + +COPY app/ /action/app/ COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh +RUN pip install --no-cache-dir -r /action/app/requirements.txt && \ + chmod +x /entrypoint.sh -ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["/entrypoint.sh"] diff --git a/action.yaml b/action.yaml index f8849d0..4e28a5c 100644 --- a/action.yaml +++ b/action.yaml @@ -1,20 +1,158 @@ -name: 'Docker Action Template' -description: 'Docker Action 範本' +name: 'AI Code Review' +description: 'AI 多角色 Code Review Action,自動分析 PR 並發布問題 Comment' author: 'Jeffery' inputs: - runner_token: - description: 'Gitea Runner Token' - required: true - text: - description: '輸入的文字' - default: "Hello, World!" -outputs: - text: - description: '輸出的文字' + # Gitea 相關(可從 gitea context 自動取得) + GITEA_TOKEN: + description: 'Gitea API Token' + required: false + GITEA_SERVER_URL: + description: 'Gitea Server URL' + required: false + GITEA_REPOSITORY: + description: 'Gitea Repository (owner/repo)' + required: false + PR_NUMBER: + description: 'Pull Request Number' + required: false + PR_HEAD_BRANCH: + description: 'PR 來源分支' + required: false + PR_BASE_BRANCH: + description: 'PR 目標分支' + required: false + + # OpenAI-compatible + OPENAI_API_KEY: + description: 'OpenAI / OpenRouter API Key' + required: false + OPENAI_BASE_URL: + description: 'OpenAI-compatible Base URL' + required: false + OPENAI_MODEL: + description: 'OpenAI-compatible Model Name' + required: false + + # Anthropic Claude + CLAUDE_API_KEY: + description: 'Anthropic Claude API Key' + required: false + CLAUDE_BASE_URL: + description: 'Claude Base URL' + required: false + CLAUDE_MODEL: + description: 'Claude Model Name' + required: false + + # Google Gemini + GEMINI_API_KEY: + description: 'Google Gemini API Key' + required: false + GEMINI_BASE_URL: + description: 'Gemini Base URL' + required: false + GEMINI_MODEL: + description: 'Gemini Model Name' + required: false + + # Ollama + OLLAMA_BASE_URL: + description: 'Ollama Base URL' + required: false + OLLAMA_MODEL: + description: 'Ollama Model Name' + required: false + + # Amazon Q + AMAZONQ_API_KEY: + description: 'Amazon Q API Key' + required: false + AMAZONQ_BASE_URL: + description: 'Amazon Q Base URL' + required: false + + # SonarQube + SONARQUBE_TOKEN: + description: 'SonarQube Token' + required: false + SONARQUBE_URL: + description: 'SonarQube URL' + required: false + + # Kilo Code + KILO_API_KEY: + description: 'Kilo Code API Key' + required: false + KILO_BASE_URL: + description: 'Kilo Code Base URL' + required: false + + # Roo Code + ROO_API_KEY: + description: 'Roo Code API Key' + required: false + ROO_BASE_URL: + description: 'Roo Code Base URL' + required: false + + # Cline + CLINE_API_KEY: + description: 'Cline API Key' + required: false + CLINE_BASE_URL: + description: 'Cline Base URL' + required: false + + # Continue + CONTINUE_API_KEY: + description: 'Continue API Key' + required: false + CONTINUE_BASE_URL: + description: 'Continue Base URL' + required: false + + # Kade + KADE_API_KEY: + description: 'Kade API Key' + required: false + KADE_BASE_URL: + description: 'Kade Base URL' + required: false + runs: using: 'docker' image: 'Dockerfile' env: - GITEA_SERVER_URL: ${{ gitea.server_url }} - GITEA_REPOSITORY: ${{ gitea.repository }} - RUNNER_TOKEN: ${{ inputs.runner_token || secrets.GITEA_TOKEN || secrets.RUNNER_TOKEN }} \ No newline at end of file + # Gitea context(優先用 inputs,否則從 gitea context 取) + GITEA_TOKEN: ${{ inputs.GITEA_TOKEN || secrets.GITEA_TOKEN }} + GITEA_SERVER_URL: ${{ inputs.GITEA_SERVER_URL || gitea.server_url }} + GITEA_REPOSITORY: ${{ inputs.GITEA_REPOSITORY || gitea.repository }} + PR_NUMBER: ${{ inputs.PR_NUMBER || gitea.event.pull_request.number }} + PR_HEAD_BRANCH: ${{ inputs.PR_HEAD_BRANCH || gitea.event.pull_request.head.ref }} + PR_BASE_BRANCH: ${{ inputs.PR_BASE_BRANCH || gitea.event.pull_request.base.ref }} + # LLM + OPENAI_API_KEY: ${{ inputs.OPENAI_API_KEY }} + OPENAI_BASE_URL: ${{ inputs.OPENAI_BASE_URL }} + OPENAI_MODEL: ${{ inputs.OPENAI_MODEL }} + CLAUDE_API_KEY: ${{ inputs.CLAUDE_API_KEY }} + CLAUDE_BASE_URL: ${{ inputs.CLAUDE_BASE_URL }} + CLAUDE_MODEL: ${{ inputs.CLAUDE_MODEL }} + GEMINI_API_KEY: ${{ inputs.GEMINI_API_KEY }} + GEMINI_BASE_URL: ${{ inputs.GEMINI_BASE_URL }} + GEMINI_MODEL: ${{ inputs.GEMINI_MODEL }} + OLLAMA_BASE_URL: ${{ inputs.OLLAMA_BASE_URL }} + OLLAMA_MODEL: ${{ inputs.OLLAMA_MODEL }} + AMAZONQ_API_KEY: ${{ inputs.AMAZONQ_API_KEY }} + AMAZONQ_BASE_URL: ${{ inputs.AMAZONQ_BASE_URL }} + SONARQUBE_TOKEN: ${{ inputs.SONARQUBE_TOKEN }} + SONARQUBE_URL: ${{ inputs.SONARQUBE_URL }} + KILO_API_KEY: ${{ inputs.KILO_API_KEY }} + KILO_BASE_URL: ${{ inputs.KILO_BASE_URL }} + ROO_API_KEY: ${{ inputs.ROO_API_KEY }} + ROO_BASE_URL: ${{ inputs.ROO_BASE_URL }} + CLINE_API_KEY: ${{ inputs.CLINE_API_KEY }} + CLINE_BASE_URL: ${{ inputs.CLINE_BASE_URL }} + CONTINUE_API_KEY: ${{ inputs.CONTINUE_API_KEY }} + CONTINUE_BASE_URL: ${{ inputs.CONTINUE_BASE_URL }} + KADE_API_KEY: ${{ inputs.KADE_API_KEY }} + KADE_BASE_URL: ${{ inputs.KADE_BASE_URL }} diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..d0f6c04 --- /dev/null +++ b/app/config.py @@ -0,0 +1,31 @@ +import os + +# Gitea +GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "") +GITEA_SERVER_URL = os.environ.get("GITEA_SERVER_URL", "https://gitea.com") +GITEA_REPOSITORY = os.environ.get("GITEA_REPOSITORY", "") +PR_NUMBER = os.environ.get("PR_NUMBER", "") +PR_HEAD_BRANCH = os.environ.get("PR_HEAD_BRANCH", "") +PR_BASE_BRANCH = os.environ.get("PR_BASE_BRANCH", "") + +FINDINGS_PATH = ".gitea/ai-review/findings.json" + + +def get_llm_config(): + """依優先順序偵測可用的 LLM,回傳 (provider, api_key, base_url, model)""" + checks = [ + ("openai", os.environ.get("OPENAI_API_KEY"), os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"), os.environ.get("OPENAI_MODEL", "gpt-4o-mini")), + ("claude", os.environ.get("CLAUDE_API_KEY"), os.environ.get("CLAUDE_BASE_URL", "https://api.anthropic.com/v1"), os.environ.get("CLAUDE_MODEL", "claude-3-haiku-20240307")), + ("gemini", os.environ.get("GEMINI_API_KEY"), os.environ.get("GEMINI_BASE_URL", "https://generativelanguage.googleapis.com/v1beta"), os.environ.get("GEMINI_MODEL", "gemini-1.5-flash")), + ("ollama", "ollama", os.environ.get("OLLAMA_BASE_URL", ""), os.environ.get("OLLAMA_MODEL", "")), + ("amazonq", os.environ.get("AMAZONQ_API_KEY"), os.environ.get("AMAZONQ_BASE_URL", "https://q.api.aws"), os.environ.get("OPENAI_MODEL", "amazon-q")), + ("kilo", os.environ.get("KILO_API_KEY"), os.environ.get("KILO_BASE_URL", "https://api.kilocode.com/v1"), os.environ.get("OPENAI_MODEL", "kilo-default")), + ("roo", os.environ.get("ROO_API_KEY"), os.environ.get("ROO_BASE_URL", "https://api.roocode.com/v1"), os.environ.get("OPENAI_MODEL", "roo-default")), + ("cline", os.environ.get("CLINE_API_KEY"), os.environ.get("CLINE_BASE_URL", "https://api.cline.dev/v1"), os.environ.get("OPENAI_MODEL", "cline-default")), + ("continue", os.environ.get("CONTINUE_API_KEY"), os.environ.get("CONTINUE_BASE_URL", "https://api.continue.dev/v1"), os.environ.get("OPENAI_MODEL", "continue-default")), + ("kade", os.environ.get("KADE_API_KEY"), os.environ.get("KADE_BASE_URL", "https://api.kade.dev/v1"), os.environ.get("OPENAI_MODEL", "kade-default")), + ] + for provider, key, base_url, model in checks: + if key and base_url: + return provider, key, base_url, model + return None, None, None, None diff --git a/app/gitea.py b/app/gitea.py new file mode 100644 index 0000000..4a1cd4c --- /dev/null +++ b/app/gitea.py @@ -0,0 +1,26 @@ +import requests +from config import GITEA_TOKEN, GITEA_SERVER_URL, GITEA_REPOSITORY, PR_NUMBER + + +def _headers(): + return {"Authorization": f"token {GITEA_TOKEN}", "Content-Type": "application/json"} + + +def _api(path: str) -> str: + return f"{GITEA_SERVER_URL.rstrip('/')}/api/v1{path}" + + +def get_pr_diff() -> str: + """取得 PR 的 git diff 內容""" + url = _api(f"/repos/{GITEA_REPOSITORY}/pulls/{PR_NUMBER}.diff") + resp = requests.get(url, headers=_headers(), timeout=60) + resp.raise_for_status() + return resp.text + + +def post_comment(body: str) -> dict: + """在 PR 發布 comment""" + url = _api(f"/repos/{GITEA_REPOSITORY}/issues/{PR_NUMBER}/comments") + resp = requests.post(url, headers=_headers(), json={"body": body}, timeout=30) + resp.raise_for_status() + return resp.json() diff --git a/app/llm.py b/app/llm.py new file mode 100644 index 0000000..b15a8ae --- /dev/null +++ b/app/llm.py @@ -0,0 +1,50 @@ +import json +import requests +from config import get_llm_config + + +def chat(system_prompt: str, user_content: str) -> str: + """呼叫 LLM,回傳回應文字。失敗時拋出例外。""" + provider, api_key, base_url, model = get_llm_config() + if not provider: + raise RuntimeError("未設定任何 LLM API Key") + + print(f" [LLM] provider={provider} model={model}") + + # 所有服務統一用 OpenAI-compatible chat completions 介面 + url = f"{base_url.rstrip('/')}/chat/completions" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}", + } + # Claude 額外 header + if provider == "claude": + headers["anthropic-version"] = "2023-06-01" + + payload = { + "model": model, + "messages": [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_content}, + ], + "temperature": 0.2, + } + + resp = requests.post(url, headers=headers, json=payload, timeout=120) + resp.raise_for_status() + return resp.json()["choices"][0]["message"]["content"] + + +def chat_json(system_prompt: str, user_content: str) -> list: + """呼叫 LLM 並解析 JSON 陣列回應。失敗時回傳空陣列。""" + try: + text = chat(system_prompt, user_content) + # 去除可能的 markdown code block + text = text.strip() + if text.startswith("```"): + text = text.split("\n", 1)[1] + text = text.rsplit("```", 1)[0] + return json.loads(text) + except Exception as e: + print(f" [LLM] 解析失敗: {e}") + return [] diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..2c7c725 --- /dev/null +++ b/app/main.py @@ -0,0 +1,75 @@ +import sys +import traceback +from config import GITEA_REPOSITORY, PR_NUMBER, PR_HEAD_BRANCH, PR_BASE_BRANCH, get_llm_config +from roles import load_roles, get_role_intro +from gitea import get_pr_diff, post_comment + + +def main(): + print("=" * 60) + print("🚀 Step1: Pipeline 啟動") + print(f" repo={GITEA_REPOSITORY} PR=#{PR_NUMBER}") + print(f" {PR_HEAD_BRANCH} -> {PR_BASE_BRANCH}") + + # 偵測 LLM + provider, _, base_url, model = get_llm_config() + if not provider: + print("❌ 未設定任何 LLM API Key,請檢查 action inputs") + sys.exit(1) + print(f" LLM: provider={provider} model={model} base_url={base_url}") + + # 載入角色 + roles = load_roles() + print(f" 已載入 {len(roles)} 個角色: {[r['name'] for r in roles]}") + + # 取得 PR diff + print("\n📋 Step1: 取得 PR Diff") + try: + diff = get_pr_diff() + print(f" diff 長度: {len(diff)} 字元") + except Exception as e: + print(f" ❌ 取得 diff 失敗: {e}") + sys.exit(1) + + if not diff.strip(): + print(" ⚠️ diff 為空,無需審查") + sys.exit(0) + + # 發布角色介紹 comment + print("\n💬 Step1: 發布角色介紹 Comment") + try: + intro = get_role_intro(roles) + intro += f"\n\n> 🔍 服務:{provider} 模型:{model}" + post_comment(intro) + print(" ✅ 角色介紹 comment 發布成功") + except Exception as e: + print(f" ⚠️ comment 發布失敗(繼續執行): {e}") + + print("\n📊 Step2: Findings 產生(待實作)") + print(" [stub] 各角色分析 diff...") + + print("\n🔀 Step3: Findings 合併與去重(待實作)") + print(" [stub] 合併新舊 findings...") + + print("\n📝 Step4: Findings 寫入與 Comment 發布(待實作)") + print(" [stub] 寫入 findings.json,發布 comment...") + + print("\n💾 Step5: 記憶區 Commit/Push(待實作)") + print(" [stub] commit & push findings.json...") + + print("\n🚦 Step6: 嚴重問題檢查(待實作)") + print(" [stub] 檢查 critical findings...") + + print("\n✅ Pipeline 完成") + print("=" * 60) + + +if __name__ == "__main__": + try: + main() + except SystemExit: + raise + except Exception: + print("❌ Runner failed:") + traceback.print_exc() + sys.exit(1) diff --git a/app/prompts/roles/maintainability.yaml b/app/prompts/roles/maintainability.yaml new file mode 100644 index 0000000..23f22d3 --- /dev/null +++ b/app/prompts/roles/maintainability.yaml @@ -0,0 +1,23 @@ +name: "Leo" +role: "可維護性審查員" +personality: "有遠見、重視長期維護成本,常常思考「六個月後的自己能看懂嗎?」" +focus: "程式碼複雜度、模組化、重複程式碼、文件完整性、錯誤處理、可測試性" +system_prompt: | + 你是 Leo,一位重視長期維護成本的審查員。你的工作是審查程式碼的可維護性,包含複雜度、模組化、重複程式碼、文件完整性、錯誤處理。 + + 請分析以下 Git Diff,找出所有可維護性相關問題。 + + 回傳 JSON 陣列,每個問題格式如下: + { + "level": "critical|warning|info", + "role": "Leo", + "location": "檔案路徑:行號 或 檔案路徑", + "suggestion": "繁體中文的具體修改建議" + } + + 等級定義: + - critical:嚴重影響可維護性,會造成技術債(如超長函式、完全無文件的公開 API) + - warning:建議改善的可維護性問題 + - info:可選的改善建議 + + 只回傳 JSON 陣列,不要有其他文字。如果沒有問題,回傳空陣列 []。 diff --git a/app/prompts/roles/performance.yaml b/app/prompts/roles/performance.yaml new file mode 100644 index 0000000..51f6249 --- /dev/null +++ b/app/prompts/roles/performance.yaml @@ -0,0 +1,23 @@ +name: "Zara" +role: "效能優化專家" +personality: "追求極致效能,對任何不必要的資源消耗都感到不舒服,喜歡用數據說話" +focus: "時間複雜度、空間複雜度、資料庫查詢效率、快取策略、不必要的重複運算" +system_prompt: | + 你是 Zara,一位追求極致效能的優化專家。你的工作是審查程式碼的效能問題,包含時間複雜度、空間複雜度、資料庫查詢效率、快取策略。 + + 請分析以下 Git Diff,找出所有效能相關問題。 + + 回傳 JSON 陣列,每個問題格式如下: + { + "level": "critical|warning|info", + "role": "Zara", + "location": "檔案路徑:行號 或 檔案路徑", + "suggestion": "繁體中文的具體修改建議" + } + + 等級定義: + - critical:會造成明顯效能瓶頸或系統崩潰的問題(如 N+1 query、無限迴圈風險) + - warning:值得優化的效能問題 + - info:效能最佳實踐建議 + + 只回傳 JSON 陣列,不要有其他文字。如果沒有問題,回傳空陣列 []。 diff --git a/app/prompts/roles/security.yaml b/app/prompts/roles/security.yaml new file mode 100644 index 0000000..3bc5d31 --- /dev/null +++ b/app/prompts/roles/security.yaml @@ -0,0 +1,23 @@ +name: "Rex" +role: "資安審查員" +personality: "謹慎、多疑、對任何潛在風險都保持高度警覺,寧可誤報也不放過漏洞" +focus: "安全漏洞、注入攻擊、敏感資料洩漏、認證授權問題、依賴套件風險" +system_prompt: | + 你是 Rex,一位謹慎的資安審查員。你的工作是審查程式碼中的安全漏洞、注入攻擊風險、敏感資料洩漏、認證授權問題。 + + 請分析以下 Git Diff,找出所有安全相關問題。 + + 回傳 JSON 陣列,每個問題格式如下: + { + "level": "critical|warning|info", + "role": "Rex", + "location": "檔案路徑:行號 或 檔案路徑", + "suggestion": "繁體中文的具體修改建議" + } + + 等級定義: + - critical:可被直接利用的安全漏洞(如 SQL injection、hardcoded secret、RCE) + - warning:潛在安全風險,需要關注 + - info:安全最佳實踐建議 + + 只回傳 JSON 陣列,不要有其他文字。如果沒有問題,回傳空陣列 []。 diff --git a/app/prompts/roles/style.yaml b/app/prompts/roles/style.yaml new file mode 100644 index 0000000..75955a4 --- /dev/null +++ b/app/prompts/roles/style.yaml @@ -0,0 +1,23 @@ +name: "Aria" +role: "程式碼風格審查員" +personality: "嚴謹、注重細節、對程式碼整潔度有高度要求,說話直接但不失禮貌" +focus: "程式碼風格、命名規範、格式一致性、可讀性" +system_prompt: | + 你是 Aria,一位嚴謹的程式碼風格審查員。你的工作是審查程式碼的風格、命名規範、格式一致性與可讀性。 + + 請分析以下 Git Diff,找出所有風格相關問題。 + + 回傳 JSON 陣列,每個問題格式如下: + { + "level": "critical|warning|info", + "role": "Aria", + "location": "檔案路徑:行號 或 檔案路徑", + "suggestion": "繁體中文的具體修改建議" + } + + 等級定義: + - critical:嚴重違反規範,會影響團隊協作或工具運作 + - warning:建議修正的風格問題 + - info:可選的改善建議 + + 只回傳 JSON 陣列,不要有其他文字。如果沒有問題,回傳空陣列 []。 diff --git a/app/prompts/roles/testing.yaml b/app/prompts/roles/testing.yaml new file mode 100644 index 0000000..e83cf05 --- /dev/null +++ b/app/prompts/roles/testing.yaml @@ -0,0 +1,23 @@ +name: "Maya" +role: "測試品質審查員" +personality: "對測試覆蓋率有執念,相信沒有測試的程式碼等於沒有完成,溫和但堅持" +focus: "測試覆蓋率、測試品質、邊界條件、錯誤情境測試、測試可讀性" +system_prompt: | + 你是 Maya,一位對測試品質有高度要求的審查員。你的工作是審查程式碼的測試覆蓋率、測試品質、邊界條件處理。 + + 請分析以下 Git Diff,找出所有測試相關問題。 + + 回傳 JSON 陣列,每個問題格式如下: + { + "level": "critical|warning|info", + "role": "Maya", + "location": "檔案路徑:行號 或 檔案路徑", + "suggestion": "繁體中文的具體修改建議" + } + + 等級定義: + - critical:完全缺少測試的核心功能,或測試邏輯有嚴重錯誤 + - warning:測試覆蓋不足或測試品質有待改善 + - info:測試最佳實踐建議 + + 只回傳 JSON 陣列,不要有其他文字。如果沒有問題,回傳空陣列 []。 diff --git a/app/requirements.txt b/app/requirements.txt new file mode 100644 index 0000000..1ebdae8 --- /dev/null +++ b/app/requirements.txt @@ -0,0 +1,3 @@ +requests==2.31.0 +pyyaml==6.0.1 +openai==1.12.0 diff --git a/app/roles.py b/app/roles.py new file mode 100644 index 0000000..07d123c --- /dev/null +++ b/app/roles.py @@ -0,0 +1,22 @@ +import os +import yaml + +ROLES_DIR = "/action/app/prompts/roles" + + +def load_roles() -> list[dict]: + """載入所有角色定義""" + roles = [] + for fname in sorted(os.listdir(ROLES_DIR)): + if fname.endswith(".yaml"): + with open(os.path.join(ROLES_DIR, fname), "r", encoding="utf-8") as f: + roles.append(yaml.safe_load(f)) + return roles + + +def get_role_intro(roles: list[dict]) -> str: + """產生角色介紹文字(用於 comment)""" + lines = ["## 🤖 AI Code Review 團隊", ""] + for r in roles: + lines.append(f"- **{r['name']}** ({r['role']}): {r['personality']}") + return "\n".join(lines) diff --git a/entrypoint.sh b/entrypoint.sh index 378b552..ec0b7b7 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,11 +1,8 @@ #!/bin/bash +set -e -echo "Gitea Server Url: $GITEA_SERVER_URL" +echo "🚀 AI Code Review Action 啟動" +echo "Repository: $GITEA_REPOSITORY" +echo "PR: #$PR_NUMBER ($PR_HEAD_BRANCH -> $PR_BASE_BRANCH)" -echo "Gitea Repository: $GITEA_REPOSITORY" - -echo "Gitea Runner Token: $RUNNER_TOKEN" - -echo "Input Text: $INPUT_TEXT" - -echo "text=$INPUT_TEXT" >> "$GITHUB_OUTPUT" \ No newline at end of file +exec python /action/app/main.py From ec1f6c96e7650c68517088259f4599dfd20fa21a Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 07:24:47 +0000 Subject: [PATCH 010/174] =?UTF-8?q?feat:=20=E9=9A=8E=E6=AE=B5=E4=B8=80=20-?= =?UTF-8?q?=20=E6=94=B9=E7=94=A8=20Node.js=20=E5=AF=A6=E4=BD=9C=E5=9F=BA?= =?UTF-8?q?=E6=9C=AC=E6=B5=81=E7=A8=8B=E9=AA=A8=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dockerfile: 改用 node:20-slim - entrypoint.sh: 執行 app/main.js - app/package.json: axios + js-yaml + openai - app/config.js: 環境變數與 LLM 自動偵測(10 種服務) - app/llm.js: OpenAI-compatible 統一介面 - app/gitea.js: PR diff 取得與 comment 發布 - app/roles.js: 從 prompts/roles/*.yaml 載入角色 - app/main.js: pipeline 骨架,log 每個主要階段 --- Dockerfile | 4 +-- app/config.js | 27 ++++++++++++++++ app/config.py | 31 ------------------ app/gitea.js | 15 +++++++++ app/gitea.py | 26 --------------- app/llm.js | 33 +++++++++++++++++++ app/llm.py | 50 ----------------------------- app/main.js | 71 +++++++++++++++++++++++++++++++++++++++++ app/main.py | 75 -------------------------------------------- app/package.json | 10 ++++++ app/requirements.txt | 3 -- app/roles.js | 20 ++++++++++++ app/roles.py | 22 ------------- entrypoint.sh | 2 +- 14 files changed, 179 insertions(+), 210 deletions(-) create mode 100644 app/config.js delete mode 100644 app/config.py create mode 100644 app/gitea.js delete mode 100644 app/gitea.py create mode 100644 app/llm.js delete mode 100644 app/llm.py create mode 100644 app/main.js delete mode 100644 app/main.py create mode 100644 app/package.json delete mode 100644 app/requirements.txt create mode 100644 app/roles.js delete mode 100644 app/roles.py diff --git a/Dockerfile b/Dockerfile index 1ef0bb9..484bf92 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim +FROM node:20-slim RUN apt-get update && apt-get install -y --no-install-recommends \ git \ @@ -9,7 +9,7 @@ WORKDIR /action COPY app/ /action/app/ COPY entrypoint.sh /entrypoint.sh -RUN pip install --no-cache-dir -r /action/app/requirements.txt && \ +RUN cd /action/app && npm install && \ chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] diff --git a/app/config.js b/app/config.js new file mode 100644 index 0000000..ea20e2c --- /dev/null +++ b/app/config.js @@ -0,0 +1,27 @@ +export const GITEA_TOKEN = process.env.GITEA_TOKEN || ''; +export const GITEA_SERVER_URL = process.env.GITEA_SERVER_URL || 'https://gitea.com'; +export const GITEA_REPOSITORY = process.env.GITEA_REPOSITORY || ''; +export const PR_NUMBER = process.env.PR_NUMBER || ''; +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 function getLLMConfig() { + const checks = [ + ['openai', process.env.OPENAI_API_KEY, process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1', process.env.OPENAI_MODEL || 'gpt-4o-mini'], + ['claude', process.env.CLAUDE_API_KEY, process.env.CLAUDE_BASE_URL || 'https://api.anthropic.com/v1', process.env.CLAUDE_MODEL || 'claude-3-haiku-20240307'], + ['gemini', process.env.GEMINI_API_KEY, process.env.GEMINI_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta', process.env.GEMINI_MODEL || 'gemini-1.5-flash'], + ['ollama', 'ollama', process.env.OLLAMA_BASE_URL, process.env.OLLAMA_MODEL], + ['amazonq', process.env.AMAZONQ_API_KEY, process.env.AMAZONQ_BASE_URL || 'https://q.api.aws', process.env.OPENAI_MODEL || 'amazon-q'], + ['kilo', process.env.KILO_API_KEY, process.env.KILO_BASE_URL || 'https://api.kilocode.com/v1', process.env.OPENAI_MODEL || 'kilo-default'], + ['roo', process.env.ROO_API_KEY, process.env.ROO_BASE_URL || 'https://api.roocode.com/v1', process.env.OPENAI_MODEL || 'roo-default'], + ['cline', process.env.CLINE_API_KEY, process.env.CLINE_BASE_URL || 'https://api.cline.dev/v1', process.env.OPENAI_MODEL || 'cline-default'], + ['continue', process.env.CONTINUE_API_KEY, process.env.CONTINUE_BASE_URL || 'https://api.continue.dev/v1', process.env.OPENAI_MODEL || 'continue-default'], + ['kade', process.env.KADE_API_KEY, process.env.KADE_BASE_URL || 'https://api.kade.dev/v1', process.env.OPENAI_MODEL || 'kade-default'], + ]; + for (const [provider, key, baseURL, model] of checks) { + if (key && baseURL) return { provider, apiKey: key, baseURL, model }; + } + return { provider: null, apiKey: null, baseURL: null, model: null }; +} diff --git a/app/config.py b/app/config.py deleted file mode 100644 index d0f6c04..0000000 --- a/app/config.py +++ /dev/null @@ -1,31 +0,0 @@ -import os - -# Gitea -GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "") -GITEA_SERVER_URL = os.environ.get("GITEA_SERVER_URL", "https://gitea.com") -GITEA_REPOSITORY = os.environ.get("GITEA_REPOSITORY", "") -PR_NUMBER = os.environ.get("PR_NUMBER", "") -PR_HEAD_BRANCH = os.environ.get("PR_HEAD_BRANCH", "") -PR_BASE_BRANCH = os.environ.get("PR_BASE_BRANCH", "") - -FINDINGS_PATH = ".gitea/ai-review/findings.json" - - -def get_llm_config(): - """依優先順序偵測可用的 LLM,回傳 (provider, api_key, base_url, model)""" - checks = [ - ("openai", os.environ.get("OPENAI_API_KEY"), os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"), os.environ.get("OPENAI_MODEL", "gpt-4o-mini")), - ("claude", os.environ.get("CLAUDE_API_KEY"), os.environ.get("CLAUDE_BASE_URL", "https://api.anthropic.com/v1"), os.environ.get("CLAUDE_MODEL", "claude-3-haiku-20240307")), - ("gemini", os.environ.get("GEMINI_API_KEY"), os.environ.get("GEMINI_BASE_URL", "https://generativelanguage.googleapis.com/v1beta"), os.environ.get("GEMINI_MODEL", "gemini-1.5-flash")), - ("ollama", "ollama", os.environ.get("OLLAMA_BASE_URL", ""), os.environ.get("OLLAMA_MODEL", "")), - ("amazonq", os.environ.get("AMAZONQ_API_KEY"), os.environ.get("AMAZONQ_BASE_URL", "https://q.api.aws"), os.environ.get("OPENAI_MODEL", "amazon-q")), - ("kilo", os.environ.get("KILO_API_KEY"), os.environ.get("KILO_BASE_URL", "https://api.kilocode.com/v1"), os.environ.get("OPENAI_MODEL", "kilo-default")), - ("roo", os.environ.get("ROO_API_KEY"), os.environ.get("ROO_BASE_URL", "https://api.roocode.com/v1"), os.environ.get("OPENAI_MODEL", "roo-default")), - ("cline", os.environ.get("CLINE_API_KEY"), os.environ.get("CLINE_BASE_URL", "https://api.cline.dev/v1"), os.environ.get("OPENAI_MODEL", "cline-default")), - ("continue", os.environ.get("CONTINUE_API_KEY"), os.environ.get("CONTINUE_BASE_URL", "https://api.continue.dev/v1"), os.environ.get("OPENAI_MODEL", "continue-default")), - ("kade", os.environ.get("KADE_API_KEY"), os.environ.get("KADE_BASE_URL", "https://api.kade.dev/v1"), os.environ.get("OPENAI_MODEL", "kade-default")), - ] - for provider, key, base_url, model in checks: - if key and base_url: - return provider, key, base_url, model - return None, None, None, None diff --git a/app/gitea.js b/app/gitea.js new file mode 100644 index 0000000..787f801 --- /dev/null +++ b/app/gitea.js @@ -0,0 +1,15 @@ +import axios from 'axios'; +import { GITEA_TOKEN, GITEA_SERVER_URL, GITEA_REPOSITORY, PR_NUMBER } from './config.js'; + +const headers = () => ({ Authorization: `token ${GITEA_TOKEN}`, 'Content-Type': 'application/json' }); +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 }); + return resp.data; +} + +export async function postComment(body) { + const resp = await axios.post(api(`/repos/${GITEA_REPOSITORY}/issues/${PR_NUMBER}/comments`), { body }, { headers: headers(), timeout: 30000 }); + return resp.data; +} diff --git a/app/gitea.py b/app/gitea.py deleted file mode 100644 index 4a1cd4c..0000000 --- a/app/gitea.py +++ /dev/null @@ -1,26 +0,0 @@ -import requests -from config import GITEA_TOKEN, GITEA_SERVER_URL, GITEA_REPOSITORY, PR_NUMBER - - -def _headers(): - return {"Authorization": f"token {GITEA_TOKEN}", "Content-Type": "application/json"} - - -def _api(path: str) -> str: - return f"{GITEA_SERVER_URL.rstrip('/')}/api/v1{path}" - - -def get_pr_diff() -> str: - """取得 PR 的 git diff 內容""" - url = _api(f"/repos/{GITEA_REPOSITORY}/pulls/{PR_NUMBER}.diff") - resp = requests.get(url, headers=_headers(), timeout=60) - resp.raise_for_status() - return resp.text - - -def post_comment(body: str) -> dict: - """在 PR 發布 comment""" - url = _api(f"/repos/{GITEA_REPOSITORY}/issues/{PR_NUMBER}/comments") - resp = requests.post(url, headers=_headers(), json={"body": body}, timeout=30) - resp.raise_for_status() - return resp.json() diff --git a/app/llm.js b/app/llm.js new file mode 100644 index 0000000..f2521d3 --- /dev/null +++ b/app/llm.js @@ -0,0 +1,33 @@ +import axios from 'axios'; +import { getLLMConfig } from './config.js'; + +export async function chat(systemPrompt, userContent) { + const { provider, apiKey, baseURL, model } = getLLMConfig(); + if (!provider) throw new Error('未設定任何 LLM API Key'); + + console.log(` [LLM] provider=${provider} model=${model}`); + + const headers = { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + }; + if (provider === 'claude') headers['anthropic-version'] = '2023-06-01'; + + const resp = await axios.post( + `${baseURL.replace(/\/$/, '')}/chat/completions`, + { model, messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userContent }], temperature: 0.2 }, + { headers, timeout: 120000 } + ); + return resp.data.choices[0].message.content; +} + +export async function chatJSON(systemPrompt, userContent) { + try { + let text = await chat(systemPrompt, userContent); + text = text.trim().replace(/^```[^\n]*\n?/, '').replace(/```$/, '').trim(); + return JSON.parse(text); + } catch (e) { + console.log(` [LLM] 解析失敗: ${e.message}`); + return []; + } +} diff --git a/app/llm.py b/app/llm.py deleted file mode 100644 index b15a8ae..0000000 --- a/app/llm.py +++ /dev/null @@ -1,50 +0,0 @@ -import json -import requests -from config import get_llm_config - - -def chat(system_prompt: str, user_content: str) -> str: - """呼叫 LLM,回傳回應文字。失敗時拋出例外。""" - provider, api_key, base_url, model = get_llm_config() - if not provider: - raise RuntimeError("未設定任何 LLM API Key") - - print(f" [LLM] provider={provider} model={model}") - - # 所有服務統一用 OpenAI-compatible chat completions 介面 - url = f"{base_url.rstrip('/')}/chat/completions" - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {api_key}", - } - # Claude 額外 header - if provider == "claude": - headers["anthropic-version"] = "2023-06-01" - - payload = { - "model": model, - "messages": [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_content}, - ], - "temperature": 0.2, - } - - resp = requests.post(url, headers=headers, json=payload, timeout=120) - resp.raise_for_status() - return resp.json()["choices"][0]["message"]["content"] - - -def chat_json(system_prompt: str, user_content: str) -> list: - """呼叫 LLM 並解析 JSON 陣列回應。失敗時回傳空陣列。""" - try: - text = chat(system_prompt, user_content) - # 去除可能的 markdown code block - text = text.strip() - if text.startswith("```"): - text = text.split("\n", 1)[1] - text = text.rsplit("```", 1)[0] - return json.loads(text) - except Exception as e: - print(f" [LLM] 解析失敗: {e}") - return [] diff --git a/app/main.js b/app/main.js new file mode 100644 index 0000000..16981ba --- /dev/null +++ b/app/main.js @@ -0,0 +1,71 @@ +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'; + +async function main() { + console.log('='.repeat(60)); + console.log('🚀 Step1: Pipeline 啟動'); + 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'); + process.exit(1); + } + 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 + console.log('\n📋 Step1: 取得 PR Diff'); + let diff; + try { + diff = await getPRDiff(); + console.log(` diff 長度: ${diff.length} 字元`); + } catch (e) { + console.error(` ❌ 取得 diff 失敗: ${e.message}`); + process.exit(1); + } + + if (!diff.trim()) { + console.log(' ⚠️ diff 為空,無需審查'); + process.exit(0); + } + + // 發布角色介紹 comment + console.log('\n💬 Step1: 發布角色介紹 Comment'); + try { + const intro = getRoleIntro(roles) + `\n\n> 🔍 服務:${provider} 模型:${model}`; + await postComment(intro); + console.log(' ✅ 角色介紹 comment 發布成功'); + } catch (e) { + console.log(` ⚠️ comment 發布失敗(繼續執行): ${e.message}`); + } + + console.log('\n📊 Step2: Findings 產生(待實作)'); + console.log(' [stub] 各角色分析 diff...'); + + console.log('\n🔀 Step3: Findings 合併與去重(待實作)'); + console.log(' [stub] 合併新舊 findings...'); + + console.log('\n📝 Step4: Findings 寫入與 Comment 發布(待實作)'); + console.log(' [stub] 寫入 findings.json,發布 comment...'); + + console.log('\n💾 Step5: 記憶區 Commit/Push(待實作)'); + console.log(' [stub] commit & push findings.json...'); + + console.log('\n🚦 Step6: 嚴重問題檢查(待實作)'); + console.log(' [stub] 檢查 critical findings...'); + + console.log('\n✅ Pipeline 完成'); + console.log('='.repeat(60)); +} + +main().catch(e => { + console.error('❌ Runner failed:', e.message); + process.exit(1); +}); diff --git a/app/main.py b/app/main.py deleted file mode 100644 index 2c7c725..0000000 --- a/app/main.py +++ /dev/null @@ -1,75 +0,0 @@ -import sys -import traceback -from config import GITEA_REPOSITORY, PR_NUMBER, PR_HEAD_BRANCH, PR_BASE_BRANCH, get_llm_config -from roles import load_roles, get_role_intro -from gitea import get_pr_diff, post_comment - - -def main(): - print("=" * 60) - print("🚀 Step1: Pipeline 啟動") - print(f" repo={GITEA_REPOSITORY} PR=#{PR_NUMBER}") - print(f" {PR_HEAD_BRANCH} -> {PR_BASE_BRANCH}") - - # 偵測 LLM - provider, _, base_url, model = get_llm_config() - if not provider: - print("❌ 未設定任何 LLM API Key,請檢查 action inputs") - sys.exit(1) - print(f" LLM: provider={provider} model={model} base_url={base_url}") - - # 載入角色 - roles = load_roles() - print(f" 已載入 {len(roles)} 個角色: {[r['name'] for r in roles]}") - - # 取得 PR diff - print("\n📋 Step1: 取得 PR Diff") - try: - diff = get_pr_diff() - print(f" diff 長度: {len(diff)} 字元") - except Exception as e: - print(f" ❌ 取得 diff 失敗: {e}") - sys.exit(1) - - if not diff.strip(): - print(" ⚠️ diff 為空,無需審查") - sys.exit(0) - - # 發布角色介紹 comment - print("\n💬 Step1: 發布角色介紹 Comment") - try: - intro = get_role_intro(roles) - intro += f"\n\n> 🔍 服務:{provider} 模型:{model}" - post_comment(intro) - print(" ✅ 角色介紹 comment 發布成功") - except Exception as e: - print(f" ⚠️ comment 發布失敗(繼續執行): {e}") - - print("\n📊 Step2: Findings 產生(待實作)") - print(" [stub] 各角色分析 diff...") - - print("\n🔀 Step3: Findings 合併與去重(待實作)") - print(" [stub] 合併新舊 findings...") - - print("\n📝 Step4: Findings 寫入與 Comment 發布(待實作)") - print(" [stub] 寫入 findings.json,發布 comment...") - - print("\n💾 Step5: 記憶區 Commit/Push(待實作)") - print(" [stub] commit & push findings.json...") - - print("\n🚦 Step6: 嚴重問題檢查(待實作)") - print(" [stub] 檢查 critical findings...") - - print("\n✅ Pipeline 完成") - print("=" * 60) - - -if __name__ == "__main__": - try: - main() - except SystemExit: - raise - except Exception: - print("❌ Runner failed:") - traceback.print_exc() - sys.exit(1) diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..b5e3877 --- /dev/null +++ b/app/package.json @@ -0,0 +1,10 @@ +{ + "name": "ai-code-review", + "version": "1.0.0", + "type": "module", + "dependencies": { + "axios": "^1.6.7", + "js-yaml": "^4.1.0", + "openai": "^4.28.0" + } +} diff --git a/app/requirements.txt b/app/requirements.txt deleted file mode 100644 index 1ebdae8..0000000 --- a/app/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -requests==2.31.0 -pyyaml==6.0.1 -openai==1.12.0 diff --git a/app/roles.js b/app/roles.js new file mode 100644 index 0000000..f36e2d4 --- /dev/null +++ b/app/roles.js @@ -0,0 +1,20 @@ +import fs from 'fs'; +import path from 'path'; +import yaml from 'js-yaml'; + +const ROLES_DIR = '/action/app/prompts/roles'; + +export function loadRoles() { + return fs.readdirSync(ROLES_DIR) + .filter(f => f.endsWith('.yaml')) + .sort() + .map(f => yaml.load(fs.readFileSync(path.join(ROLES_DIR, f), 'utf8'))); +} + +export function getRoleIntro(roles) { + const lines = ['## 🤖 AI Code Review 團隊', '']; + for (const r of roles) { + lines.push(`- **${r.name}** (${r.role}):${r.personality}`); + } + return lines.join('\n'); +} diff --git a/app/roles.py b/app/roles.py deleted file mode 100644 index 07d123c..0000000 --- a/app/roles.py +++ /dev/null @@ -1,22 +0,0 @@ -import os -import yaml - -ROLES_DIR = "/action/app/prompts/roles" - - -def load_roles() -> list[dict]: - """載入所有角色定義""" - roles = [] - for fname in sorted(os.listdir(ROLES_DIR)): - if fname.endswith(".yaml"): - with open(os.path.join(ROLES_DIR, fname), "r", encoding="utf-8") as f: - roles.append(yaml.safe_load(f)) - return roles - - -def get_role_intro(roles: list[dict]) -> str: - """產生角色介紹文字(用於 comment)""" - lines = ["## 🤖 AI Code Review 團隊", ""] - for r in roles: - lines.append(f"- **{r['name']}** ({r['role']}): {r['personality']}") - return "\n".join(lines) diff --git a/entrypoint.sh b/entrypoint.sh index ec0b7b7..64c4ccb 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -5,4 +5,4 @@ echo "🚀 AI Code Review Action 啟動" echo "Repository: $GITEA_REPOSITORY" echo "PR: #$PR_NUMBER ($PR_HEAD_BRANCH -> $PR_BASE_BRANCH)" -exec python /action/app/main.py +exec node /action/app/main.js From bccf0e5b0b7e74b134f3d0f656c31601e81b1452 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 07:29:52 +0000 Subject: [PATCH 011/174] feat: add AI code review workflow configuration Co-authored-by: Copilot --- .gitea/workflows/review.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .gitea/workflows/review.yaml diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml new file mode 100644 index 0000000..6612336 --- /dev/null +++ b/.gitea/workflows/review.yaml @@ -0,0 +1,18 @@ +name: AI +on: + pull_request: + types: [opened, synchronize] +jobs: + code-review: + name: 'Code Review' + runs-on: ubuntu + steps: + - name: AI Code Review + uses: ../../action.yaml + with: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENAI_BASE_URL: https://api.openai.com/v1 + permissions: + contents: write + pull-requests: write + issues: write \ No newline at end of file From 43e21d07cdb231f1116cbd18870b152262fdef2f Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 07:37:04 +0000 Subject: [PATCH 012/174] feat: update AI code review workflow to use specific action URL and add tagging step Co-authored-by: Copilot --- .gitea/workflows/review.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 6612336..0d8f2c1 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -7,8 +7,15 @@ jobs: name: 'Code Review' runs-on: ubuntu steps: + - name: Tag develop on source branch + uses: akkuman/gitea-release-action@${{ vars.ACTION_RELEASE_VERSION }} + with: + tag: develop + target: ${{ github.head_ref }} + generate_release_notes: false + prerelease: false - name: AI Code Review - uses: ../../action.yaml + uses: https://gitea.jsc.idv.tw/jiantw83/code-review@develop with: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENAI_BASE_URL: https://api.openai.com/v1 From fa5a734166a33238519676eddf289d5e7e76e48f Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 07:39:29 +0000 Subject: [PATCH 013/174] fix: ensure 'name' parameter is included in the tag step of the code review workflow Co-authored-by: Copilot --- .gitea/workflows/review.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 0d8f2c1..e07f83c 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -10,6 +10,7 @@ jobs: - name: Tag develop on source branch uses: akkuman/gitea-release-action@${{ vars.ACTION_RELEASE_VERSION }} with: + name: develop tag: develop target: ${{ github.head_ref }} generate_release_notes: false From cdac64e22400f7c783bf218de859ad0ef164063a Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 07:46:59 +0000 Subject: [PATCH 014/174] feat: refactor code review workflow to remove tagging step and use dynamic branch reference --- .gitea/workflows/review.yaml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index e07f83c..18e356e 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -7,16 +7,8 @@ jobs: name: 'Code Review' runs-on: ubuntu steps: - - name: Tag develop on source branch - uses: akkuman/gitea-release-action@${{ vars.ACTION_RELEASE_VERSION }} - with: - name: develop - tag: develop - target: ${{ github.head_ref }} - generate_release_notes: false - prerelease: false - name: AI Code Review - uses: https://gitea.jsc.idv.tw/jiantw83/code-review@develop + uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ github.head_ref }} with: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENAI_BASE_URL: https://api.openai.com/v1 From 46dd8320d143524309d8bdc284463bd5739cc991 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 07:52:21 +0000 Subject: [PATCH 015/174] =?UTF-8?q?feat:=20=E9=9A=8E=E6=AE=B5=E4=BA=8C=20-?= =?UTF-8?q?=20Findings=20=E7=94=A2=E7=94=9F=E8=88=87=E5=90=88=E4=BD=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app/findings.js: 各角色分析 diff、讀取舊 findings、合併去重、等級排序 - app/main.js: 實作 Step2/Step3,log findings 統計 --- app/findings.js | 63 +++++++++++++++++++++++++++++++++++++++++++++++++ app/main.js | 25 ++++++++++++++++---- 2 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 app/findings.js diff --git a/app/findings.js b/app/findings.js new file mode 100644 index 0000000..3975a80 --- /dev/null +++ b/app/findings.js @@ -0,0 +1,63 @@ +import fs from 'fs'; +import path from 'path'; +import { chatJSON } from './llm.js'; +import { FINDINGS_PATH } from './config.js'; + +const LEVELS = ['critical', 'warning', 'info']; + +/** + * 用單一角色分析 diff,回傳 findings 陣列 + */ +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} 個問題`); + return valid; +} + +/** + * 讀取舊 findings(從 workspace 的 FINDINGS_PATH) + */ +export function loadOldFindings(workspace) { + const fullPath = path.join(workspace, FINDINGS_PATH); + if (!fs.existsSync(fullPath)) { + console.log(' 舊 findings 檔案不存在,視為空'); + 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; + } catch (e) { + console.log(` ⚠️ 讀取舊 findings 失敗: ${e.message},視為空`); + return []; + } +} + +/** + * 合併新舊 findings,以 (role + location + suggestion前50字) 為 key 去除重複 + * 舊問題保留,新問題若與舊問題重複則捨棄 + */ +export function mergeFindings(oldFindings, newFindings) { + const key = f => `${f.role}|${f.location}|${String(f.suggestion).slice(0, 50)}`; + const seen = new Set(oldFindings.map(key)); + const deduped = newFindings.filter(f => { + if (seen.has(key(f))) return false; + seen.add(key(f)); + return true; + }); + const merged = [...oldFindings, ...deduped]; + console.log(` 合併結果: 舊=${oldFindings.length} 新(去重後)=${deduped.length} 總計=${merged.length}`); + return merged; +} + +/** + * 依等級排序(critical > warning > info) + */ +export function sortByLevel(findings) { + return [...findings].sort((a, b) => LEVELS.indexOf(a.level) - LEVELS.indexOf(b.level)); +} diff --git a/app/main.js b/app/main.js index 16981ba..e317d61 100644 --- a/app/main.js +++ b/app/main.js @@ -1,6 +1,9 @@ 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 } from './findings.js'; + +const WORKSPACE = process.env.GITHUB_WORKSPACE || '/workspace'; async function main() { console.log('='.repeat(60)); @@ -46,11 +49,25 @@ async function main() { console.log(` ⚠️ comment 發布失敗(繼續執行): ${e.message}`); } - console.log('\n📊 Step2: Findings 產生(待實作)'); - console.log(' [stub] 各角色分析 diff...'); + // Step2: 各角色分析 diff 產生新 findings + console.log('\n📊 Step2: Findings 產生'); + const newFindings = []; + for (const role of roles) { + try { + const found = await analyzeWithRole(role, diff); + newFindings.push(...found); + } catch (e) { + console.log(` ⚠️ [${role.name}] 分析失敗(跳過): ${e.message}`); + } + } + console.log(` Step2 完成: 新 findings 總計 ${newFindings.length} 筆`); - console.log('\n🔀 Step3: Findings 合併與去重(待實作)'); - console.log(' [stub] 合併新舊 findings...'); + // Step3: 讀取舊 findings,合併去重 + console.log('\n🔀 Step3: Findings 合併'); + const oldFindings = loadOldFindings(WORKSPACE); + const mergedFindings = mergeFindings(oldFindings, newFindings); + const sorted = sortByLevel(mergedFindings); + console.log(` Step3 merged 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('\n📝 Step4: Findings 寫入與 Comment 發布(待實作)'); console.log(' [stub] 寫入 findings.json,發布 comment...'); From 9ff521955f540cfa31213b1f2f6d7a84331002a1 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 07:59:30 +0000 Subject: [PATCH 016/174] feat: add completion note to stage one in TODO --- TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.md b/TODO.md index 791a6a7..d0a4b23 100644 --- a/TODO.md +++ b/TODO.md @@ -3,6 +3,7 @@ ## 階段一:基本流程串接 - 目標:確保 action 可以被觸發,pipeline 各步驟依序執行,log 出每個主要階段的進入與完成。 - 驗收:log 中能看到每個階段(如「Step1: pipeline start」、「Step2: findings merge」等)明確訊息,且流程能走完(即使還沒產生 findings)。 +- 完成 ## 階段二:Findings 產生與合併 - 目標:各角色(style/security/performance/maintainability/testing)能產生 findings,並正確合併新舊 findings。 From 0fae1f383c7271aaded5473a3bf0be3a6e315621 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 08:03:19 +0000 Subject: [PATCH 017/174] feat: add cache cleaning step to code review workflow --- .gitea/workflows/review.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 18e356e..d27cc63 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -7,6 +7,8 @@ jobs: name: 'Code Review' runs-on: ubuntu steps: + - name: Clean Action Cache + run: rm -rf ~/.local/share/act/_actions/* - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ github.head_ref }} with: From 597fcf1f73f8776d199e845215234e352a83d681 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 08:09:30 +0000 Subject: [PATCH 018/174] feat: refactor code review workflow to include version calculation step --- .gitea/workflows/review.yaml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index d27cc63..6e8b509 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -3,12 +3,26 @@ on: pull_request: types: [opened, synchronize] jobs: + version: + name: 計算版本號 + runs-on: ubuntu + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - name: 計算版本號 + id: version + uses: https://gitea.jsc.idv.tw/actions/calculate-version@${{ vars.ACTION_CALCULATE_VERSION }} + with: + IS_BETA: "true" code-review: - name: 'Code Review' + name: Code Review runs-on: ubuntu steps: - - name: Clean Action Cache - run: rm -rf ~/.local/share/act/_actions/* + - name: 標註版本號 + uses: akkuman/gitea-release-action@${{ vars.ACTION_RELEASE_VERSION }} + with: + name: code-review v${{ steps.version.outputs.version }} + tag_name: v${{ steps.version.outputs.version }} - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ github.head_ref }} with: From 88b326ba3c4e454a43dbd4eb1f89fc34e7ac05e4 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 08:12:12 +0000 Subject: [PATCH 019/174] feat: refactor version handling in code review workflow for consistency --- .gitea/workflows/review.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 6e8b509..fb4b769 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -13,18 +13,19 @@ jobs: id: version uses: https://gitea.jsc.idv.tw/actions/calculate-version@${{ vars.ACTION_CALCULATE_VERSION }} with: - IS_BETA: "true" + IS_BETA: true code-review: name: Code Review runs-on: ubuntu + needs: [version] steps: - name: 標註版本號 uses: akkuman/gitea-release-action@${{ vars.ACTION_RELEASE_VERSION }} with: - name: code-review v${{ steps.version.outputs.version }} - tag_name: v${{ steps.version.outputs.version }} + name: code-review v${{ needs.version.outputs.version }} + tag_name: v${{ needs.version.outputs.version }} - name: AI Code Review - uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ github.head_ref }} + uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} with: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENAI_BASE_URL: https://api.openai.com/v1 From 3d9700ade70fe6fe1a7848ddc2db75adfd661c9f Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 08:13:13 +0000 Subject: [PATCH 020/174] feat: refactor version tagging step in review workflow for improved clarity --- .gitea/workflows/review.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index fb4b769..c590e7b 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -14,16 +14,16 @@ jobs: uses: https://gitea.jsc.idv.tw/actions/calculate-version@${{ vars.ACTION_CALCULATE_VERSION }} with: IS_BETA: true + - name: 標註版本號 + uses: akkuman/gitea-release-action@${{ vars.ACTION_RELEASE_VERSION }} + with: + name: code-review v${{ steps.version.outputs.version }} + tag_name: v${{ steps.version.outputs.version }} code-review: name: Code Review runs-on: ubuntu needs: [version] steps: - - name: 標註版本號 - uses: akkuman/gitea-release-action@${{ vars.ACTION_RELEASE_VERSION }} - with: - name: code-review v${{ needs.version.outputs.version }} - tag_name: v${{ needs.version.outputs.version }} - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} with: From 8fbdaadca3e7c47f3aca2fa9e2d36fb479af30f5 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 08:14:41 +0000 Subject: [PATCH 021/174] feat: add target_commitish to version tagging step for improved accuracy --- .gitea/workflows/review.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index c590e7b..7f83b41 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -19,6 +19,7 @@ jobs: with: name: code-review v${{ steps.version.outputs.version }} tag_name: v${{ steps.version.outputs.version }} + target_commitish: ${{ github.head_ref }} code-review: name: Code Review runs-on: ubuntu From 06303f784a4d09e39b785f6fb66c2f2d63956c76 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 08:20:57 +0000 Subject: [PATCH 022/174] feat: update workflow configurations for version calculation and API integration --- .gitea/workflows/master.yaml | 7 ++++--- .gitea/workflows/review.yaml | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/master.yaml b/.gitea/workflows/master.yaml index b54c903..b37b091 100644 --- a/.gitea/workflows/master.yaml +++ b/.gitea/workflows/master.yaml @@ -1,10 +1,11 @@ +name: CD on: push: branches: - master jobs: version: - name: "CD > 計算版本號" + name: 計算版本號 runs-on: ubuntu outputs: version: ${{ steps.version.outputs.version }} @@ -13,14 +14,14 @@ jobs: id: version uses: https://gitea.jsc.idv.tw/actions/calculate-version@${{ vars.ACTION_CALCULATE_VERSION }} release: - name: "CD > 發布專案" + name: 發布專案 runs-on: ubuntu needs: version steps: - name: 發布專案 uses: akkuman/gitea-release-action@${{ vars.ACTION_RELEASE_VERSION }} with: - tag_name: "v${{ needs.version.outputs.version }}" + tag_name: v${{ needs.version.outputs.version }} - name: 清理成品 uses: https://gitea.jsc.idv.tw/actions/cleanup-release@${{ vars.ACTION_CLEANUP_RELEASE_VERSION }} with: diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 7f83b41..040c8c4 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -28,8 +28,9 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} with: + # 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 OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - OPENAI_BASE_URL: https://api.openai.com/v1 + OPENAI_BASE_URL: https://openrouter.ai/api/v1 permissions: contents: write pull-requests: write From a9163cdfda3e5bf79e71fd9a4b875eb0a8d33658 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 08:24:47 +0000 Subject: [PATCH 023/174] feat: update AI Code Review action inputs to use OLLAMA configuration and set default OpenAI base URL --- .gitea/workflows/review.yaml | 11 +++++------ action.yaml | 1 + 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 040c8c4..1b2e722 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -21,16 +21,15 @@ 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: - name: AI Code Review - uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} - with: - # 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 - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - OPENAI_BASE_URL: https://openrouter.ai/api/v1 + uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} + with: + OLLAMA_BASE_URL: ${{ secrets.OLLAMA_BASE_URL }} + OLLAMA_MODEL: ${{ secrets.OLLAMA_MODEL }} permissions: contents: write pull-requests: write diff --git a/action.yaml b/action.yaml index 4e28a5c..ce0e7d2 100644 --- a/action.yaml +++ b/action.yaml @@ -29,6 +29,7 @@ inputs: OPENAI_BASE_URL: description: 'OpenAI-compatible Base URL' required: false + default: 'https://openrouter.ai/api/v1' OPENAI_MODEL: description: 'OpenAI-compatible Model Name' required: false From d20300eec7fa38fee11851a0c86eb2a73d9274ae Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 08:27:51 +0000 Subject: [PATCH 024/174] feat: format AI Code Review step for improved readability --- .gitea/workflows/review.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 1b2e722..1c43912 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -26,10 +26,10 @@ jobs: needs: [version] steps: - name: AI Code Review - uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} - with: - OLLAMA_BASE_URL: ${{ secrets.OLLAMA_BASE_URL }} - OLLAMA_MODEL: ${{ secrets.OLLAMA_MODEL }} + uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} + with: + OLLAMA_BASE_URL: ${{ secrets.OLLAMA_BASE_URL }} + OLLAMA_MODEL: ${{ secrets.OLLAMA_MODEL }} permissions: contents: write pull-requests: write From 0609e7fe7f511bf759b82b455dad4e0629386e80 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 08:29:59 +0000 Subject: [PATCH 025/174] feat: update OLLAMA configuration to use vars instead of secrets for improved flexibility --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 532c85c..e50cebb 100644 --- a/README.md +++ b/README.md @@ -262,8 +262,8 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} with: - OLLAMA_BASE_URL: ${{ secrets.OLLAMA_BASE_URL }} - OLLAMA_MODEL: ${{ secrets.OLLAMA_MODEL }} + OLLAMA_BASE_URL: ${{ vars.OLLAMA_BASE_URL }} + OLLAMA_MODEL: ${{ vars.OLLAMA_MODEL }} permissions: contents: write pull-requests: write From 7b5decf46acafcdf0084c9e64c285a0e30426734 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 08:31:02 +0000 Subject: [PATCH 026/174] feat: update AI Code Review step to use vars instead of secrets for improved flexibility --- .gitea/workflows/review.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 1c43912..3a93fa5 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -28,8 +28,8 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} with: - OLLAMA_BASE_URL: ${{ secrets.OLLAMA_BASE_URL }} - OLLAMA_MODEL: ${{ secrets.OLLAMA_MODEL }} + OLLAMA_BASE_URL: ${{ vars.OLLAMA_BASE_URL }} + OLLAMA_MODEL: ${{ vars.OLLAMA_MODEL }} permissions: contents: write pull-requests: write From e183e31ce0a552a7f36a3ae0d1bbee6acb8f723c Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 08:34:04 +0000 Subject: [PATCH 027/174] =?UTF-8?q?fix:=20=E5=BF=BD=E7=95=A5=20SSL=20?= =?UTF-8?q?=E6=86=91=E8=AD=89=E9=A9=97=E8=AD=89=EF=BC=88=E6=94=AF=E6=8F=B4?= =?UTF-8?q?=E8=87=AA=E7=B0=BD=E6=86=91=E8=AD=89=E7=9A=84=20Ollama/Gitea?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/gitea.js | 6 ++++-- app/llm.js | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/gitea.js b/app/gitea.js index 787f801..f8b4216 100644 --- a/app/gitea.js +++ b/app/gitea.js @@ -1,15 +1,17 @@ import axios from 'axios'; +import https from 'https'; import { GITEA_TOKEN, GITEA_SERVER_URL, GITEA_REPOSITORY, PR_NUMBER } from './config.js'; +const httpsAgent = new https.Agent({ rejectUnauthorized: false }); const headers = () => ({ Authorization: `token ${GITEA_TOKEN}`, 'Content-Type': 'application/json' }); 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 }); + const resp = await axios.get(api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}.diff`), { headers: headers(), timeout: 60000, httpsAgent }); return resp.data; } export async function postComment(body) { - const resp = await axios.post(api(`/repos/${GITEA_REPOSITORY}/issues/${PR_NUMBER}/comments`), { body }, { headers: headers(), timeout: 30000 }); + const resp = await axios.post(api(`/repos/${GITEA_REPOSITORY}/issues/${PR_NUMBER}/comments`), { body }, { headers: headers(), timeout: 30000, httpsAgent }); return resp.data; } diff --git a/app/llm.js b/app/llm.js index f2521d3..db7217b 100644 --- a/app/llm.js +++ b/app/llm.js @@ -1,6 +1,9 @@ import axios from 'axios'; +import https from 'https'; import { getLLMConfig } from './config.js'; +const httpsAgent = new https.Agent({ rejectUnauthorized: false }); + export async function chat(systemPrompt, userContent) { const { provider, apiKey, baseURL, model } = getLLMConfig(); if (!provider) throw new Error('未設定任何 LLM API Key'); @@ -16,7 +19,7 @@ export async function chat(systemPrompt, userContent) { const resp = await axios.post( `${baseURL.replace(/\/$/, '')}/chat/completions`, { model, messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userContent }], temperature: 0.2 }, - { headers, timeout: 120000 } + { headers, timeout: 120000, httpsAgent } ); return resp.data.choices[0].message.content; } From bdf8d8a79712afaa50acaf4b152141a82271bc47 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 08:47:34 +0000 Subject: [PATCH 028/174] feat: update AI Code Review step to use OpenAI API key and base URL --- .gitea/workflows/review.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 3a93fa5..98a2d6b 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -28,8 +28,8 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} with: - OLLAMA_BASE_URL: ${{ vars.OLLAMA_BASE_URL }} - OLLAMA_MODEL: ${{ vars.OLLAMA_MODEL }} + OPENAI_API_KEY: ${{ secrets.HF_API_KEY }} + OPENAI_BASE_URL: https://api-inference.huggingface.co/v1 permissions: contents: write pull-requests: write From aca76f23afabbb42abc3dcb9233fb4f547c15359 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 08:53:03 +0000 Subject: [PATCH 029/174] feat: add OPENAI_MODEL parameter to AI Code Review step --- .gitea/workflows/review.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 98a2d6b..19960f2 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -30,6 +30,7 @@ jobs: with: OPENAI_API_KEY: ${{ secrets.HF_API_KEY }} OPENAI_BASE_URL: https://api-inference.huggingface.co/v1 + OPENAI_MODEL: deepseek-ai/DeepSeek-R1 permissions: contents: write pull-requests: write From 7ba2af338471f30c57de31184307f20d35b7a1e8 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 08:54:13 +0000 Subject: [PATCH 030/174] feat: update OPENAI_MODEL to use DeepSeek-R1-Distill-Qwen-32B for improved performance --- .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 19960f2..5cf890b 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -30,7 +30,7 @@ jobs: with: OPENAI_API_KEY: ${{ secrets.HF_API_KEY }} OPENAI_BASE_URL: https://api-inference.huggingface.co/v1 - OPENAI_MODEL: deepseek-ai/DeepSeek-R1 + OPENAI_MODEL: deepseek-ai/DeepSeek-R1-Distill-Qwen-32B permissions: contents: write pull-requests: write From 73c11129ab773aa404add94186fe01ca6650b8d7 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 08:55:53 +0000 Subject: [PATCH 031/174] feat: update AI Code Review step to use new OpenAI base URL and model --- .gitea/workflows/review.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 5cf890b..4d3a52a 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -29,8 +29,8 @@ jobs: uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} with: OPENAI_API_KEY: ${{ secrets.HF_API_KEY }} - OPENAI_BASE_URL: https://api-inference.huggingface.co/v1 - OPENAI_MODEL: deepseek-ai/DeepSeek-R1-Distill-Qwen-32B + OPENAI_BASE_URL: https://router.huggingface.co/novita/v1 + OPENAI_MODEL: deepseek-ai/DeepSeek-R1 permissions: contents: write pull-requests: write From 64b904dd07d8fa5cd872607c5ba0c54bc12168d9 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 08:56:48 +0000 Subject: [PATCH 032/174] =?UTF-8?q?fix:=20=E6=94=AF=E6=8F=B4=E4=B8=8D?= =?UTF-8?q?=E6=8E=A5=E5=8F=97=20system=20role=20=E7=9A=84=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=EF=BC=88DeepSeek-R1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/llm.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/llm.js b/app/llm.js index db7217b..5dcd129 100644 --- a/app/llm.js +++ b/app/llm.js @@ -16,9 +16,16 @@ export async function chat(systemPrompt, userContent) { }; if (provider === 'claude') headers['anthropic-version'] = '2023-06-01'; + // 部分模型(如 DeepSeek-R1)不支援 system role,改合併到 user message + const NO_SYSTEM_ROLE_MODELS = ['deepseek-ai/deepseek-r1', 'deepseek-r1']; + const isNoSystemRole = NO_SYSTEM_ROLE_MODELS.some(m => model.toLowerCase().includes(m.toLowerCase())); + const messages = isNoSystemRole + ? [{ role: 'user', content: `${systemPrompt}\n\n${userContent}` }] + : [{ role: 'system', content: systemPrompt }, { role: 'user', content: userContent }]; + const resp = await axios.post( `${baseURL.replace(/\/$/, '')}/chat/completions`, - { model, messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userContent }], temperature: 0.2 }, + { model, messages, temperature: 0.2 }, { headers, timeout: 120000, httpsAgent } ); return resp.data.choices[0].message.content; From 9a11d25c0027d6c44e44aa60691cfbd573dd9a22 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 08:58:59 +0000 Subject: [PATCH 033/174] =?UTF-8?q?revert:=20=E7=A7=BB=E9=99=A4=20DeepSeek?= =?UTF-8?q?-R1=20=E7=89=B9=E5=88=A5=E8=99=95=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/llm.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/app/llm.js b/app/llm.js index 5dcd129..db7217b 100644 --- a/app/llm.js +++ b/app/llm.js @@ -16,16 +16,9 @@ export async function chat(systemPrompt, userContent) { }; if (provider === 'claude') headers['anthropic-version'] = '2023-06-01'; - // 部分模型(如 DeepSeek-R1)不支援 system role,改合併到 user message - const NO_SYSTEM_ROLE_MODELS = ['deepseek-ai/deepseek-r1', 'deepseek-r1']; - const isNoSystemRole = NO_SYSTEM_ROLE_MODELS.some(m => model.toLowerCase().includes(m.toLowerCase())); - const messages = isNoSystemRole - ? [{ role: 'user', content: `${systemPrompt}\n\n${userContent}` }] - : [{ role: 'system', content: systemPrompt }, { role: 'user', content: userContent }]; - const resp = await axios.post( `${baseURL.replace(/\/$/, '')}/chat/completions`, - { model, messages, temperature: 0.2 }, + { model, messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userContent }], temperature: 0.2 }, { headers, timeout: 120000, httpsAgent } ); return resp.data.choices[0].message.content; From ed1f2bea15f23f20b6297ec81834fd308f26796a Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 09:03:03 +0000 Subject: [PATCH 034/174] feat: update AI Code Review step to use DeepSeek API and correct API key Co-authored-by: Copilot --- .gitea/workflows/review.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 4d3a52a..c839a4f 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -28,9 +28,8 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} with: - OPENAI_API_KEY: ${{ secrets.HF_API_KEY }} - OPENAI_BASE_URL: https://router.huggingface.co/novita/v1 - OPENAI_MODEL: deepseek-ai/DeepSeek-R1 + OPENAI_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} + OPENAI_BASE_URL: https://api.deepseek.com/v1 permissions: contents: write pull-requests: write From 6eae6eb0ce31c80840db382d9440b99b738b0583 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 09:07:21 +0000 Subject: [PATCH 035/174] feat: add OPENAI_MODEL parameter to AI Code Review step --- .gitea/workflows/review.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index c839a4f..56507d8 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -30,6 +30,7 @@ jobs: with: OPENAI_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} OPENAI_BASE_URL: https://api.deepseek.com/v1 + OPENAI_MODEL: deepseek-chat permissions: contents: write pull-requests: write From fb1254aa32bfc0f91fdcf27e254d6d9d028450db Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 09:10:20 +0000 Subject: [PATCH 036/174] feat: refactor AI Code Review step to use OLLAMA_BASE_URL and OLLAMA_MODEL Co-authored-by: Copilot --- .gitea/workflows/review.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 56507d8..3a93fa5 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -28,9 +28,8 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} with: - OPENAI_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} - OPENAI_BASE_URL: https://api.deepseek.com/v1 - OPENAI_MODEL: deepseek-chat + OLLAMA_BASE_URL: ${{ vars.OLLAMA_BASE_URL }} + OLLAMA_MODEL: ${{ vars.OLLAMA_MODEL }} permissions: contents: write pull-requests: write From 624a71836c34321ebee9ead821f9bcf9f5d386b7 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 09:31:01 +0000 Subject: [PATCH 037/174] feat: update AI Code Review step to use OpenAI API key and base URL --- .gitea/workflows/review.yaml | 4 ++-- README.md | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 3a93fa5..deec2ca 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -28,8 +28,8 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} with: - OLLAMA_BASE_URL: ${{ vars.OLLAMA_BASE_URL }} - OLLAMA_MODEL: ${{ vars.OLLAMA_MODEL }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENAI_BASE_URL: https://api.openai.com/v1 permissions: contents: write pull-requests: write diff --git a/README.md b/README.md index e50cebb..87c6cb1 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} with: + # Github (h3285@evertrust.com.tw) + # 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 OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENAI_BASE_URL: https://api.openai.com/v1 permissions: From 02529a4ec9f0215f0cc9d1949e98c9511958f025 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 09:34:43 +0000 Subject: [PATCH 038/174] feat: update OpenAI API key in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 87c6cb1..8094684 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ jobs: uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} with: # Github (h3285@evertrust.com.tw) - # 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 + # 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 OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENAI_BASE_URL: https://api.openai.com/v1 permissions: From 6ecb018ef43c9ae2879f75cc336d2ce52b4c1fc9 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 09:37:01 +0000 Subject: [PATCH 039/174] feat: update OpenAI base URL to use OpenRouter API --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8094684..e1c2026 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ jobs: # Github (h3285@evertrust.com.tw) # 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 OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - OPENAI_BASE_URL: https://api.openai.com/v1 + OPENAI_BASE_URL: https://openrouter.ai/api/v1 permissions: contents: write pull-requests: write From 5c5660a34b8a2a685cd965a049641b5e5a6737fa Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 09:37:53 +0000 Subject: [PATCH 040/174] feat: update OpenAI base URL to use OpenRouter API --- .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 deec2ca..3a49860 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -29,7 +29,7 @@ jobs: uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} with: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - OPENAI_BASE_URL: https://api.openai.com/v1 + OPENAI_BASE_URL: https://openrouter.ai/api/v1 permissions: contents: write pull-requests: write From 4a67dec32a98decb9650349bba5254b5c5f5faa2 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 09:40:38 +0000 Subject: [PATCH 041/174] =?UTF-8?q?feat:=20=E9=9A=8E=E6=AE=B5=E4=B8=89=20-?= =?UTF-8?q?=20AI=20=E8=AA=9E=E6=84=8F=E5=8E=BB=E9=87=8D=EF=BC=8C=E5=A4=B1?= =?UTF-8?q?=E6=95=97=E6=99=82=E9=99=8D=E7=B4=9A=E4=BF=9D=E7=95=99=E6=89=80?= =?UTF-8?q?=E6=9C=89=E5=95=8F=E9=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/findings.js | 32 ++++++++++++++++++++++++++++++++ app/main.js | 11 ++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/app/findings.js b/app/findings.js index 3975a80..7c38515 100644 --- a/app/findings.js +++ b/app/findings.js @@ -61,3 +61,35 @@ export function mergeFindings(oldFindings, newFindings) { export function sortByLevel(findings) { return [...findings].sort((a, b) => LEVELS.indexOf(a.level) - LEVELS.indexOf(b.level)); } + +/** + * 呼叫 LLM 進行語意去重,回傳去重後的 findings + * 失敗時降級回傳原始 findings + */ +export async function deduplicateWithAI(findings) { + if (findings.length === 0) return findings; + + const systemPrompt = `你是一位程式碼審查問題去重專家。 +給你一份問題清單(JSON 陣列),請移除語意重複的問題(即使描述文字不同,但指的是同一個問題)。 +保留等級較高的版本,優先保留 critical > warning > info。 +只回傳去重後的 JSON 陣列,不要有其他文字。`; + + const userContent = `以下是問題清單,請去除語意重複的項目:\n\n${JSON.stringify(findings, null, 2)}`; + + try { + const result = await chatJSON(systemPrompt, userContent); + 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; + } +} diff --git a/app/main.js b/app/main.js index e317d61..d9db8c9 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 } from './findings.js'; +import { analyzeWithRole, loadOldFindings, mergeFindings, sortByLevel, deduplicateWithAI } from './findings.js'; const WORKSPACE = process.env.GITHUB_WORKSPACE || '/workspace'; @@ -66,8 +66,13 @@ async function main() { console.log('\n🔀 Step3: Findings 合併'); const oldFindings = loadOldFindings(WORKSPACE); const mergedFindings = mergeFindings(oldFindings, newFindings); - const sorted = sortByLevel(mergedFindings); - console.log(` Step3 merged 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(` 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})`); console.log('\n📝 Step4: Findings 寫入與 Comment 發布(待實作)'); console.log(' [stub] 寫入 findings.json,發布 comment...'); From 81e38de6493480682b459561d966ce5b20d2d4c0 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 09:44:45 +0000 Subject: [PATCH 042/174] =?UTF-8?q?feat:=20=E9=9A=8E=E6=AE=B5=E5=9B=9B=20-?= =?UTF-8?q?=20findings=20=E5=AF=AB=E5=85=A5=E8=88=87=20comment=20=E4=BE=9D?= =?UTF-8?q?=E5=BA=8F=E7=99=BC=E5=B8=83=EF=BC=88=E8=88=8A=E5=95=8F=E9=A1=8C?= =?UTF-8?q?=E2=86=92=E9=9D=9E=E5=9A=B4=E9=87=8D=E2=86=92=E5=9A=B4=E9=87=8D?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comments.js | 70 +++++++++++++++++++++++++++++++++++++++++++++++++ app/main.js | 15 +++++++++-- 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 app/comments.js diff --git a/app/comments.js b/app/comments.js new file mode 100644 index 0000000..7a349f4 --- /dev/null +++ b/app/comments.js @@ -0,0 +1,70 @@ +import fs from 'fs'; +import path from 'path'; +import { postComment } from './gitea.js'; +import { FINDINGS_PATH } from './config.js'; + +const LEVEL_EMOJI = { critical: '🔴', warning: '🟡', info: '🔵' }; +const LEVEL_LABEL = { critical: '嚴重', warning: '警告', info: '建議' }; + +function findingRow(f) { + return `| ${LEVEL_EMOJI[f.level] || ''} ${LEVEL_LABEL[f.level] || f.level} | ${f.role} | ${f.location} | ${f.suggestion} |`; +} + +function buildTable(findings) { + const rows = findings.map(findingRow).join('\n'); + return `| 等級 | 審查員 | 位置 | 建議 |\n|------|--------|------|------|\n${rows}`; +} + +/** + * 寫入 findings.json 到 workspace + */ +export function saveFindings(workspace, findings) { + const fullPath = path.join(workspace, FINDINGS_PATH); + fs.mkdirSync(path.dirname(fullPath), { recursive: true }); + fs.writeFileSync(fullPath, JSON.stringify(findings, null, 2), 'utf8'); + console.log(` ✅ findings 寫入: ${fullPath} (${findings.length} 筆)`); +} + +/** + * 發布所有舊問題 comment(一次發布,依等級排序) + */ +export async function postOldFindingsComment(findings) { + const old = findings.filter(f => !f.is_new); + if (old.length === 0) { + console.log(' 無舊問題,跳過'); + return; + } + const body = `## 📋 舊有未解決問題(${old.length} 筆)\n\n${buildTable(old)}`; + await postComment(body); + console.log(` ✅ 舊問題 comment 發布 (${old.length} 筆)`); +} + +/** + * 發布新問題中非 critical 的 comment(一次發布) + */ +export async function postNewNonCriticalComment(findings) { + const items = findings.filter(f => f.is_new && f.level !== 'critical'); + if (items.length === 0) { + console.log(' 無新的非嚴重問題,跳過'); + return; + } + const body = `## 🔍 新發現問題(${items.length} 筆)\n\n${buildTable(items)}`; + await postComment(body); + console.log(` ✅ 新問題(非嚴重)comment 發布 (${items.length} 筆)`); +} + +/** + * 每個新 critical 問題各發一個 comment + */ +export async function postNewCriticalComments(findings) { + const criticals = findings.filter(f => f.is_new && f.level === 'critical'); + if (criticals.length === 0) { + console.log(' 無新的嚴重問題,跳過'); + return; + } + for (const f of criticals) { + const body = `## 🚨 嚴重問題\n\n| 審查員 | 位置 | 建議 |\n|--------|------|------|\n| ${f.role} | ${f.location} | ${f.suggestion} |`; + await postComment(body); + console.log(` ✅ 嚴重問題 comment 發布: [${f.role}] ${f.location}`); + } +} diff --git a/app/main.js b/app/main.js index d9db8c9..de466f7 100644 --- a/app/main.js +++ b/app/main.js @@ -2,6 +2,7 @@ import { GITEA_REPOSITORY, PR_NUMBER, PR_HEAD_BRANCH, PR_BASE_BRANCH, getLLMConf import { loadRoles, getRoleIntro } from './roles.js'; import { getPRDiff, postComment } from './gitea.js'; import { analyzeWithRole, loadOldFindings, mergeFindings, sortByLevel, deduplicateWithAI } from './findings.js'; +import { saveFindings, postOldFindingsComment, postNewNonCriticalComment, postNewCriticalComments } from './comments.js'; const WORKSPACE = process.env.GITHUB_WORKSPACE || '/workspace'; @@ -74,8 +75,18 @@ 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})`); - console.log('\n📝 Step4: Findings 寫入與 Comment 發布(待實作)'); - console.log(' [stub] 寫入 findings.json,發布 comment...'); + // Step4: 寫入 findings.json,依序發布 comment + console.log('\n📝 Step4: Findings 寫入與 Comment 發布'); + saveFindings(WORKSPACE, sorted); + + try { + await postOldFindingsComment(sorted); + await postNewNonCriticalComment(sorted); + await postNewCriticalComments(sorted); + console.log(' Step4 完成'); + } catch (e) { + console.log(` ⚠️ comment 發布失敗(繼續執行): ${e.message}`); + } console.log('\n💾 Step5: 記憶區 Commit/Push(待實作)'); console.log(' [stub] commit & push findings.json...'); From 5ae0549453f66ff4cc4cdfea719381cdc9118295 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 09:49:07 +0000 Subject: [PATCH 043/174] =?UTF-8?q?feat:=20=E9=9A=8E=E6=AE=B5=E4=BA=94?= =?UTF-8?q?=E5=85=AD=20-=20findings=20commit/push=20=E5=88=B0=E4=BE=86?= =?UTF-8?q?=E6=BA=90=E5=88=86=E6=94=AF=EF=BC=8Ccritical=20=E5=95=8F?= =?UTF-8?q?=E9=A1=8C=20exit=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/git.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ app/main.js | 17 +++++++++++++---- 2 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 app/git.js diff --git a/app/git.js b/app/git.js new file mode 100644 index 0000000..7634ffb --- /dev/null +++ b/app/git.js @@ -0,0 +1,44 @@ +import { execSync } from 'child_process'; +import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js'; + +function exec(cmd, cwd) { + return execSync(cmd, { cwd, stdio: 'pipe' }).toString().trim(); +} + +/** + * Commit findings.json 並 push 到 PR 來源分支 + */ +export function commitAndPush(workspace) { + const repoDir = `${workspace}/${GITEA_REPOSITORY}`; + const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.git` + .replace('https://', `https://${GITEA_TOKEN}@`); + + try { + // 設定 git 身份 + exec('git config user.email "ai-review[bot]@gitea"', repoDir); + exec('git config user.name "AI Review Bot"', repoDir); + + // 切換到來源分支 + exec(`git fetch origin ${PR_HEAD_BRANCH}`, repoDir); + exec(`git checkout ${PR_HEAD_BRANCH}`, repoDir); + + // 確認 findings.json 存在 + exec(`git add ${FINDINGS_PATH}`, repoDir); + + // 檢查是否有變更 + const status = exec('git status --porcelain', repoDir); + if (!status) { + console.log(' findings.json 無變更,跳過 commit'); + return; + } + + const commitMsg = 'chore: update ai-review findings [skip ci]'; + const commitHash = exec(`git commit -m "${commitMsg}"`, repoDir) + .match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown'; + + exec(`git push ${remoteUrl} ${PR_HEAD_BRANCH}`, repoDir); + console.log(` ✅ persisted findings commit=${commitHash} push=${PR_HEAD_BRANCH}`); + } catch (e) { + console.log(` ⚠️ Runner failed: commit/push 失敗: ${e.message}`); + } +} diff --git a/app/main.js b/app/main.js index de466f7..4e6f88c 100644 --- a/app/main.js +++ b/app/main.js @@ -3,6 +3,7 @@ import { loadRoles, getRoleIntro } from './roles.js'; import { getPRDiff, postComment } from './gitea.js'; import { analyzeWithRole, loadOldFindings, mergeFindings, sortByLevel, deduplicateWithAI } from './findings.js'; import { saveFindings, postOldFindingsComment, postNewNonCriticalComment, postNewCriticalComments } from './comments.js'; +import { commitAndPush } from './git.js'; const WORKSPACE = process.env.GITHUB_WORKSPACE || '/workspace'; @@ -88,11 +89,19 @@ async function main() { console.log(` ⚠️ comment 發布失敗(繼續執行): ${e.message}`); } - console.log('\n💾 Step5: 記憶區 Commit/Push(待實作)'); - console.log(' [stub] commit & push findings.json...'); + // Step5: commit/push findings.json 到來源分支 + console.log('\n💾 Step5: 記憶區 Commit/Push'); + commitAndPush(WORKSPACE); - console.log('\n🚦 Step6: 嚴重問題檢查(待實作)'); - console.log(' [stub] 檢查 critical findings...'); + // Step6: 有 critical 問題則 exit 1 + console.log('\n🚦 Step6: 嚴重問題檢查'); + const criticalCount = sorted.filter(f => f.level === 'critical').length; + if (criticalCount > 0) { + console.log(` ❌ 發現 ${criticalCount} 個嚴重問題,workflow 結束(exit 1)`); + console.log('='.repeat(60)); + process.exit(1); + } + console.log(' ✅ 無嚴重問題'); console.log('\n✅ Pipeline 完成'); console.log('='.repeat(60)); From 519e04691d2ce0f888b3592d5c21f41869fc3e05 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 09:51:28 +0000 Subject: [PATCH 044/174] =?UTF-8?q?fix:=20git.js=20=E6=94=B9=E7=94=A8=20sp?= =?UTF-8?q?awnSync=20=E7=9B=B4=E6=8E=A5=E5=91=BC=E5=8F=AB=20git=20binary?= =?UTF-8?q?=EF=BC=88=E9=81=BF=E5=85=8D=20/bin/sh=20ENOENT=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/git.js | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/app/git.js b/app/git.js index 7634ffb..ed80d20 100644 --- a/app/git.js +++ b/app/git.js @@ -1,8 +1,11 @@ -import { execSync } from 'child_process'; +import { spawnSync } from 'child_process'; import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js'; -function exec(cmd, cwd) { - return execSync(cmd, { cwd, stdio: 'pipe' }).toString().trim(); +function git(args, cwd) { + const result = spawnSync('git', args, { cwd, encoding: 'utf8' }); + if (result.error) throw result.error; + if (result.status !== 0) throw new Error(result.stderr || result.stdout); + return (result.stdout || '').trim(); } /** @@ -10,33 +13,24 @@ function exec(cmd, cwd) { */ export function commitAndPush(workspace) { const repoDir = `${workspace}/${GITEA_REPOSITORY}`; - const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.git` - .replace('https://', `https://${GITEA_TOKEN}@`); + const remoteUrl = GITEA_SERVER_URL.replace(/\/$/, '').replace('https://', `https://${GITEA_TOKEN}@`) + `/${GITEA_REPOSITORY}.git`; try { - // 設定 git 身份 - exec('git config user.email "ai-review[bot]@gitea"', repoDir); - exec('git config user.name "AI Review Bot"', repoDir); + git(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir); + git(['config', 'user.name', 'AI Review Bot'], repoDir); + git(['fetch', 'origin', PR_HEAD_BRANCH], repoDir); + git(['checkout', PR_HEAD_BRANCH], repoDir); + git(['add', FINDINGS_PATH], repoDir); - // 切換到來源分支 - exec(`git fetch origin ${PR_HEAD_BRANCH}`, repoDir); - exec(`git checkout ${PR_HEAD_BRANCH}`, repoDir); - - // 確認 findings.json 存在 - exec(`git add ${FINDINGS_PATH}`, repoDir); - - // 檢查是否有變更 - const status = exec('git status --porcelain', repoDir); + const status = git(['status', '--porcelain'], repoDir); if (!status) { console.log(' findings.json 無變更,跳過 commit'); return; } - const commitMsg = 'chore: update ai-review findings [skip ci]'; - const commitHash = exec(`git commit -m "${commitMsg}"`, repoDir) - .match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown'; - - exec(`git push ${remoteUrl} ${PR_HEAD_BRANCH}`, repoDir); + const out = git(['commit', '-m', 'chore: update ai-review findings [skip ci]'], repoDir); + const commitHash = out.match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown'; + git(['push', remoteUrl, PR_HEAD_BRANCH], repoDir); console.log(` ✅ persisted findings commit=${commitHash} push=${PR_HEAD_BRANCH}`); } catch (e) { console.log(` ⚠️ Runner failed: commit/push 失敗: ${e.message}`); From 59978c6fb5ae15298413fc1a1b23af35107f65e0 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 09:56:10 +0000 Subject: [PATCH 045/174] fix: add newline at end of TODO.md for proper formatting --- TODO.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index d0a4b23..e2545b8 100644 --- a/TODO.md +++ b/TODO.md @@ -29,4 +29,5 @@ 每個階段都會加上明確的 log,並確保即使部分功能未完成也能降級執行、不會中斷 pipeline。 -每次執行後請貼 log,我會協助 debug。 \ No newline at end of file +每次執行後請貼 log,我會協助 debug。 + From 710cd7308e1e07edb86d68f3afe089f60873fca8 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 09:57:49 +0000 Subject: [PATCH 046/174] =?UTF-8?q?fix:=20=E6=94=B9=E7=94=A8=20Gitea=20API?= =?UTF-8?q?=20commit=20findings.json=EF=BC=8C=E4=B8=8D=E4=BE=9D=E8=B3=B4?= =?UTF-8?q?=20git=20binary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/git.js | 45 +++++++++++++++------------------------------ app/gitea.js | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 31 deletions(-) diff --git a/app/git.js b/app/git.js index ed80d20..7727d93 100644 --- a/app/git.js +++ b/app/git.js @@ -1,37 +1,22 @@ -import { spawnSync } from 'child_process'; -import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js'; - -function git(args, cwd) { - const result = spawnSync('git', args, { cwd, encoding: 'utf8' }); - if (result.error) throw result.error; - if (result.status !== 0) throw new Error(result.stderr || result.stdout); - return (result.stdout || '').trim(); -} +import fs from 'fs'; +import path from 'path'; +import { commitFile } from './gitea.js'; +import { FINDINGS_PATH } from './config.js'; /** - * Commit findings.json 並 push 到 PR 來源分支 + * 透過 Gitea API 將 findings.json push 到來源分支(不需要 git binary) */ -export function commitAndPush(workspace) { - const repoDir = `${workspace}/${GITEA_REPOSITORY}`; - const remoteUrl = GITEA_SERVER_URL.replace(/\/$/, '').replace('https://', `https://${GITEA_TOKEN}@`) + `/${GITEA_REPOSITORY}.git`; - +export async function commitAndPush(workspace) { try { - git(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir); - git(['config', 'user.name', 'AI Review Bot'], repoDir); - git(['fetch', 'origin', PR_HEAD_BRANCH], repoDir); - git(['checkout', PR_HEAD_BRANCH], repoDir); - git(['add', FINDINGS_PATH], repoDir); - - const status = git(['status', '--porcelain'], repoDir); - if (!status) { - console.log(' findings.json 無變更,跳過 commit'); - return; - } - - const out = git(['commit', '-m', 'chore: update ai-review findings [skip ci]'], repoDir); - const commitHash = out.match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown'; - git(['push', remoteUrl, PR_HEAD_BRANCH], repoDir); - console.log(` ✅ persisted findings commit=${commitHash} push=${PR_HEAD_BRANCH}`); + const fullPath = path.join(workspace, FINDINGS_PATH); + const content = fs.readFileSync(fullPath, 'utf8'); + const result = await commitFile( + FINDINGS_PATH, + content, + 'chore: update ai-review findings [skip ci]' + ); + const commitHash = result.commit?.sha?.slice(0, 7) || 'unknown'; + console.log(` ✅ persisted findings commit=${commitHash} push=${process.env.PR_HEAD_BRANCH}`); } catch (e) { console.log(` ⚠️ Runner failed: commit/push 失敗: ${e.message}`); } diff --git a/app/gitea.js b/app/gitea.js index f8b4216..e30b1b1 100644 --- a/app/gitea.js +++ b/app/gitea.js @@ -1,6 +1,6 @@ import axios from 'axios'; import https from 'https'; -import { GITEA_TOKEN, GITEA_SERVER_URL, GITEA_REPOSITORY, PR_NUMBER } from './config.js'; +import { GITEA_TOKEN, GITEA_SERVER_URL, GITEA_REPOSITORY, PR_NUMBER, PR_HEAD_BRANCH } from './config.js'; const httpsAgent = new https.Agent({ rejectUnauthorized: false }); const headers = () => ({ Authorization: `token ${GITEA_TOKEN}`, 'Content-Type': 'application/json' }); @@ -15,3 +15,37 @@ export async function postComment(body) { const resp = await axios.post(api(`/repos/${GITEA_REPOSITORY}/issues/${PR_NUMBER}/comments`), { body }, { headers: headers(), timeout: 30000, httpsAgent }); return resp.data; } + +/** + * 透過 Gitea API 建立或更新檔案(不需要 git binary) + */ +export async function commitFile(filePath, content, message) { + const encoded = Buffer.from(content).toString('base64'); + const url = api(`/repos/${GITEA_REPOSITORY}/contents/${filePath}`); + + // 先嘗試取得現有檔案的 SHA + let sha; + try { + const existing = await axios.get(`${url}?ref=${PR_HEAD_BRANCH}`, { headers: headers(), httpsAgent, timeout: 15000 }); + sha = existing.data.sha; + } catch { + sha = undefined; + } + + const payload = { + message, + content: encoded, + branch: PR_HEAD_BRANCH, + ...(sha ? { sha } : {}), + }; + + const resp = await axios.request({ + method: sha ? 'put' : 'post', + url, + headers: headers(), + httpsAgent, + timeout: 30000, + data: payload, + }); + return resp.data; +} From 1c321b7ba2e7db934783d01bcb014ae3c40c3a77 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:00:08 +0000 Subject: [PATCH 047/174] =?UTF-8?q?fix:=20commitAndPush=20=E5=8A=A0?= =?UTF-8?q?=E4=B8=8A=20await?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.js b/app/main.js index 4e6f88c..5e13dc8 100644 --- a/app/main.js +++ b/app/main.js @@ -91,7 +91,7 @@ async function main() { // Step5: commit/push findings.json 到來源分支 console.log('\n💾 Step5: 記憶區 Commit/Push'); - commitAndPush(WORKSPACE); + await commitAndPush(WORKSPACE); // Step6: 有 critical 問題則 exit 1 console.log('\n🚦 Step6: 嚴重問題檢查'); From fc537958ca614dabb6e738551da2ec799858ad2f Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:01:40 +0000 Subject: [PATCH 048/174] =?UTF-8?q?debug:=20commit/push=20=E5=A4=B1?= =?UTF-8?q?=E6=95=97=E6=99=82=E9=A1=AF=E7=A4=BA=E8=A9=B3=E7=B4=B0=E9=8C=AF?= =?UTF-8?q?=E8=AA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/git.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/git.js b/app/git.js index 7727d93..07a4d54 100644 --- a/app/git.js +++ b/app/git.js @@ -18,6 +18,7 @@ export async function commitAndPush(workspace) { const commitHash = result.commit?.sha?.slice(0, 7) || 'unknown'; console.log(` ✅ persisted findings commit=${commitHash} push=${process.env.PR_HEAD_BRANCH}`); } catch (e) { - console.log(` ⚠️ Runner failed: commit/push 失敗: ${e.message}`); + const detail = e.response?.data ? JSON.stringify(e.response.data) : e.message; + console.log(` ⚠️ Runner failed: commit/push 失敗: ${e.response?.status || ''} ${detail}`); } } From 07df3ef4a5a519114bf35dd22835fbbd0ff340b7 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:05:57 +0000 Subject: [PATCH 049/174] test --- .gitea/ai-review/findings.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitea/ai-review/findings.json diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/.gitea/ai-review/findings.json @@ -0,0 +1 @@ +[] \ No newline at end of file From faa808bb5f2a112ea1ea6d1406a91e95573dd9c8 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:06:05 +0000 Subject: [PATCH 050/174] test update From 121f66b0b3448fece78294fff9c2215663155388 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:06:22 +0000 Subject: [PATCH 051/174] =?UTF-8?q?debug:=20git.js=20=E5=8A=A0=E4=B8=8A?= =?UTF-8?q?=E5=8F=83=E6=95=B8=20log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/git.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/git.js b/app/git.js index 07a4d54..1b89d22 100644 --- a/app/git.js +++ b/app/git.js @@ -10,6 +10,7 @@ export async function commitAndPush(workspace) { try { const fullPath = path.join(workspace, FINDINGS_PATH); const content = fs.readFileSync(fullPath, 'utf8'); + console.log(` [debug] FINDINGS_PATH=${FINDINGS_PATH} branch=${process.env.PR_HEAD_BRANCH} token=${process.env.GITEA_TOKEN ? '***' : 'EMPTY'}`); const result = await commitFile( FINDINGS_PATH, content, From 577a9304383bee9ace3a85cdcbacb5471ca1b951 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Mon, 11 May 2026 10:08:26 +0000 Subject: [PATCH 052/174] chore: update ai-review findings [skip ci] From da43cb02b0c1064aa4ec04646d6b075e96d36a61 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:08:57 +0000 Subject: [PATCH 053/174] chore: update ai-review findings [skip ci] From 5f77b83a0f2635fe2b07c537b92b83f584788948 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:09:16 +0000 Subject: [PATCH 054/174] =?UTF-8?q?debug:=20commitFile=20=E5=8A=A0?= =?UTF-8?q?=E4=B8=8A=E8=A9=B3=E7=B4=B0=20log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/gitea.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/gitea.js b/app/gitea.js index e30b1b1..cbd59d4 100644 --- a/app/gitea.js +++ b/app/gitea.js @@ -28,7 +28,9 @@ export async function commitFile(filePath, content, message) { try { const existing = await axios.get(`${url}?ref=${PR_HEAD_BRANCH}`, { headers: headers(), httpsAgent, timeout: 15000 }); sha = existing.data.sha; - } catch { + console.log(` [debug] 取得現有檔案 SHA=${sha}`); + } catch (e) { + console.log(` [debug] 檔案不存在,將建立新檔案: ${e.response?.status || e.message}`); sha = undefined; } @@ -39,6 +41,7 @@ export async function commitFile(filePath, content, message) { ...(sha ? { sha } : {}), }; + console.log(` [debug] ${sha ? 'PUT' : 'POST'} ${url} branch=${PR_HEAD_BRANCH}`); const resp = await axios.request({ method: sha ? 'put' : 'post', url, From e017705c640e88e1566873b5b2c63ec1e762ba5f Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:10:27 +0000 Subject: [PATCH 055/174] chore: remove test findings --- .gitea/ai-review/findings.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .gitea/ai-review/findings.json diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json deleted file mode 100644 index 0637a08..0000000 --- a/.gitea/ai-review/findings.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file From 1576e783fbdcadf6316dbe3815e7daa585de2ba1 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:11:40 +0000 Subject: [PATCH 056/174] test --- .gitea/ai-review/findings.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitea/ai-review/findings.json diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/.gitea/ai-review/findings.json @@ -0,0 +1 @@ +[] \ No newline at end of file From 953951145fbf85a5543e3440584c43440660dd1c Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:11:56 +0000 Subject: [PATCH 057/174] chore: remove test findings --- .gitea/ai-review/findings.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .gitea/ai-review/findings.json diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json deleted file mode 100644 index 0637a08..0000000 --- a/.gitea/ai-review/findings.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file From 8acea007e795706f197fcb8c528af082447ababc Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:12:47 +0000 Subject: [PATCH 058/174] chore: remove duplicate log assistance note in TODO.md --- TODO.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index e2545b8..d0a4b23 100644 --- a/TODO.md +++ b/TODO.md @@ -29,5 +29,4 @@ 每個階段都會加上明確的 log,並確保即使部分功能未完成也能降級執行、不會中斷 pipeline。 -每次執行後請貼 log,我會協助 debug。 - +每次執行後請貼 log,我會協助 debug。 \ No newline at end of file From 607c9b82ea01daac2356db7f54c5cd79629b4aee Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:13:57 +0000 Subject: [PATCH 059/174] =?UTF-8?q?debug:=20log=20content=5Flen=EF=BC=8C?= =?UTF-8?q?=E6=94=B9=E7=94=A8=20JSON.stringify?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/gitea.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/gitea.js b/app/gitea.js index cbd59d4..6219f5a 100644 --- a/app/gitea.js +++ b/app/gitea.js @@ -41,14 +41,15 @@ export async function commitFile(filePath, content, message) { ...(sha ? { sha } : {}), }; - console.log(` [debug] ${sha ? 'PUT' : 'POST'} ${url} branch=${PR_HEAD_BRANCH}`); + console.log(` [debug] ${sha ? 'PUT' : 'POST'} ${url} branch=${PR_HEAD_BRANCH} content_len=${encoded.length}`); const resp = await axios.request({ method: sha ? 'put' : 'post', url, - headers: headers(), + headers: { ...headers(), 'Content-Type': 'application/json' }, httpsAgent, timeout: 30000, - data: payload, + data: JSON.stringify(payload), }); + console.log(` [debug] response status=${resp.status}`); return resp.data; } From 21fb9c1d940f68e41575e0f60c13fcb2a8133c7a Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:15:36 +0000 Subject: [PATCH 060/174] =?UTF-8?q?fix:=20=E6=94=B9=E5=9B=9E=20git=20commi?= =?UTF-8?q?t/push=EF=BC=8C=E5=BC=B7=E5=88=B6=20Dockerfile=20rebuild=20?= =?UTF-8?q?=E7=A2=BA=E4=BF=9D=20git=20binary=20=E5=AD=98=E5=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 1 + app/git.js | 47 +++++++++++++++++++++++++++++------------------ app/gitea.js | 40 +--------------------------------------- 3 files changed, 31 insertions(+), 57 deletions(-) diff --git a/Dockerfile b/Dockerfile index 484bf92..c55c659 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,6 @@ FROM node:20-slim +# force rebuild to ensure git is installed RUN apt-get update && apt-get install -y --no-install-recommends \ git \ && rm -rf /var/lib/apt/lists/* diff --git a/app/git.js b/app/git.js index 1b89d22..36b01a9 100644 --- a/app/git.js +++ b/app/git.js @@ -1,25 +1,36 @@ -import fs from 'fs'; +import { spawnSync } from 'child_process'; import path from 'path'; -import { commitFile } from './gitea.js'; -import { FINDINGS_PATH } from './config.js'; +import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js'; + +function git(args, cwd) { + const result = spawnSync('git', args, { cwd, encoding: 'utf8' }); + if (result.error) throw result.error; + if (result.status !== 0) throw new Error((result.stderr || result.stdout || '').trim()); + return (result.stdout || '').trim(); +} -/** - * 透過 Gitea API 將 findings.json push 到來源分支(不需要 git binary) - */ export async function commitAndPush(workspace) { + const repoDir = path.join(workspace, GITEA_REPOSITORY); + const remoteUrl = GITEA_SERVER_URL.replace(/\/$/, '').replace('https://', `https://${GITEA_TOKEN}@`).replace('http://', `http://${GITEA_TOKEN}@`) + `/${GITEA_REPOSITORY}.git`; + try { - const fullPath = path.join(workspace, FINDINGS_PATH); - const content = fs.readFileSync(fullPath, 'utf8'); - console.log(` [debug] FINDINGS_PATH=${FINDINGS_PATH} branch=${process.env.PR_HEAD_BRANCH} token=${process.env.GITEA_TOKEN ? '***' : 'EMPTY'}`); - const result = await commitFile( - FINDINGS_PATH, - content, - 'chore: update ai-review findings [skip ci]' - ); - const commitHash = result.commit?.sha?.slice(0, 7) || 'unknown'; - console.log(` ✅ persisted findings commit=${commitHash} push=${process.env.PR_HEAD_BRANCH}`); + git(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir); + git(['config', 'user.name', 'AI Review Bot'], repoDir); + git(['fetch', 'origin', PR_HEAD_BRANCH], repoDir); + git(['checkout', PR_HEAD_BRANCH], repoDir); + git(['add', FINDINGS_PATH], repoDir); + + const status = git(['status', '--porcelain'], repoDir); + if (!status) { + console.log(' findings.json 無變更,跳過 commit'); + return; + } + + const out = git(['commit', '-m', 'chore: update ai-review findings [skip ci]'], repoDir); + const commitHash = out.match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown'; + git(['push', remoteUrl, PR_HEAD_BRANCH], repoDir); + console.log(` ✅ persisted findings commit=${commitHash} push=${PR_HEAD_BRANCH}`); } catch (e) { - const detail = e.response?.data ? JSON.stringify(e.response.data) : e.message; - console.log(` ⚠️ Runner failed: commit/push 失敗: ${e.response?.status || ''} ${detail}`); + console.log(` ⚠️ Runner failed: commit/push 失敗: ${e.message}`); } } diff --git a/app/gitea.js b/app/gitea.js index 6219f5a..f8b4216 100644 --- a/app/gitea.js +++ b/app/gitea.js @@ -1,6 +1,6 @@ import axios from 'axios'; import https from 'https'; -import { GITEA_TOKEN, GITEA_SERVER_URL, GITEA_REPOSITORY, PR_NUMBER, PR_HEAD_BRANCH } from './config.js'; +import { GITEA_TOKEN, GITEA_SERVER_URL, GITEA_REPOSITORY, PR_NUMBER } from './config.js'; const httpsAgent = new https.Agent({ rejectUnauthorized: false }); const headers = () => ({ Authorization: `token ${GITEA_TOKEN}`, 'Content-Type': 'application/json' }); @@ -15,41 +15,3 @@ export async function postComment(body) { const resp = await axios.post(api(`/repos/${GITEA_REPOSITORY}/issues/${PR_NUMBER}/comments`), { body }, { headers: headers(), timeout: 30000, httpsAgent }); return resp.data; } - -/** - * 透過 Gitea API 建立或更新檔案(不需要 git binary) - */ -export async function commitFile(filePath, content, message) { - const encoded = Buffer.from(content).toString('base64'); - const url = api(`/repos/${GITEA_REPOSITORY}/contents/${filePath}`); - - // 先嘗試取得現有檔案的 SHA - let sha; - try { - const existing = await axios.get(`${url}?ref=${PR_HEAD_BRANCH}`, { headers: headers(), httpsAgent, timeout: 15000 }); - sha = existing.data.sha; - console.log(` [debug] 取得現有檔案 SHA=${sha}`); - } catch (e) { - console.log(` [debug] 檔案不存在,將建立新檔案: ${e.response?.status || e.message}`); - sha = undefined; - } - - const payload = { - message, - content: encoded, - branch: PR_HEAD_BRANCH, - ...(sha ? { sha } : {}), - }; - - console.log(` [debug] ${sha ? 'PUT' : 'POST'} ${url} branch=${PR_HEAD_BRANCH} content_len=${encoded.length}`); - const resp = await axios.request({ - method: sha ? 'put' : 'post', - url, - headers: { ...headers(), 'Content-Type': 'application/json' }, - httpsAgent, - timeout: 30000, - data: JSON.stringify(payload), - }); - console.log(` [debug] response status=${resp.status}`); - return resp.data; -} From d9c55ca347ee9116947c554ccbd6d34c85d0beba Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:18:34 +0000 Subject: [PATCH 061/174] chore: add newline at end of TODO.md for better formatting --- TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.md b/TODO.md index d0a4b23..9fe2599 100644 --- a/TODO.md +++ b/TODO.md @@ -27,6 +27,7 @@ --- + 每個階段都會加上明確的 log,並確保即使部分功能未完成也能降級執行、不會中斷 pipeline。 每次執行後請貼 log,我會協助 debug。 \ No newline at end of file From af51ffacee0baa92b523562706cefc978e8afc57 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:24:24 +0000 Subject: [PATCH 062/174] =?UTF-8?q?fix:=20=E6=8F=9B=E7=94=A8=20node:20=20?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E7=89=88=EF=BC=88=E5=85=A7=E5=BB=BA=20git?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E9=9C=80=E8=A6=81=20apt-get=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index c55c659..a071e53 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,4 @@ -FROM node:20-slim - -# force rebuild to ensure git is installed -RUN apt-get update && apt-get install -y --no-install-recommends \ - git \ - && rm -rf /var/lib/apt/lists/* +FROM node:20 WORKDIR /action From 2193bdd4d6547772ea4c99f1c0d6054a16ece9eb Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:30:07 +0000 Subject: [PATCH 063/174] =?UTF-8?q?fix:=20=E6=94=B9=E5=9B=9E=20Gitea=20API?= =?UTF-8?q?=20commit=EF=BC=8C=E4=BF=AE=E6=AD=A3=20URL=20encode=20=E8=88=87?= =?UTF-8?q?=20JSON.stringify?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/git.js | 61 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/app/git.js b/app/git.js index 36b01a9..a183851 100644 --- a/app/git.js +++ b/app/git.js @@ -1,36 +1,49 @@ -import { spawnSync } from 'child_process'; +import fs from 'fs'; import path from 'path'; -import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js'; +import axios from 'axios'; +import https from 'https'; +import { GITEA_TOKEN, GITEA_SERVER_URL, GITEA_REPOSITORY, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js'; -function git(args, cwd) { - const result = spawnSync('git', args, { cwd, encoding: 'utf8' }); - if (result.error) throw result.error; - if (result.status !== 0) throw new Error((result.stderr || result.stdout || '').trim()); - return (result.stdout || '').trim(); -} +const httpsAgent = new https.Agent({ rejectUnauthorized: false }); +const headers = () => ({ Authorization: `token ${GITEA_TOKEN}`, 'Content-Type': 'application/json' }); +const api = (p) => `${GITEA_SERVER_URL.replace(/\/$/, '')}/api/v1${p}`; export async function commitAndPush(workspace) { - const repoDir = path.join(workspace, GITEA_REPOSITORY); - const remoteUrl = GITEA_SERVER_URL.replace(/\/$/, '').replace('https://', `https://${GITEA_TOKEN}@`).replace('http://', `http://${GITEA_TOKEN}@`) + `/${GITEA_REPOSITORY}.git`; - try { - git(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir); - git(['config', 'user.name', 'AI Review Bot'], repoDir); - git(['fetch', 'origin', PR_HEAD_BRANCH], repoDir); - git(['checkout', PR_HEAD_BRANCH], repoDir); - git(['add', FINDINGS_PATH], repoDir); + const fullPath = path.join(workspace, GITEA_REPOSITORY, FINDINGS_PATH); + const content = fs.readFileSync(fullPath, 'utf8'); + const encoded = Buffer.from(content).toString('base64'); + const url = api(`/repos/${GITEA_REPOSITORY}/contents/${FINDINGS_PATH}`); - const status = git(['status', '--porcelain'], repoDir); - if (!status) { - console.log(' findings.json 無變更,跳過 commit'); - return; + // 取得現有檔案 SHA(若存在) + let sha; + try { + const res = await axios.get(`${url}?ref=${encodeURIComponent(PR_HEAD_BRANCH)}`, { headers: headers(), httpsAgent, timeout: 15000 }); + sha = res.data.sha; + } catch { + sha = undefined; } - const out = git(['commit', '-m', 'chore: update ai-review findings [skip ci]'], repoDir); - const commitHash = out.match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown'; - git(['push', remoteUrl, PR_HEAD_BRANCH], repoDir); + const payload = JSON.stringify({ + message: 'chore: update ai-review findings [skip ci]', + content: encoded, + branch: PR_HEAD_BRANCH, + ...(sha ? { sha } : {}), + }); + + const resp = await axios.request({ + method: sha ? 'put' : 'post', + url, + headers: { ...headers(), 'Content-Type': 'application/json' }, + httpsAgent, + timeout: 30000, + data: payload, + }); + + const commitHash = resp.data.commit?.sha?.slice(0, 7) || 'unknown'; console.log(` ✅ persisted findings commit=${commitHash} push=${PR_HEAD_BRANCH}`); } catch (e) { - console.log(` ⚠️ Runner failed: commit/push 失敗: ${e.message}`); + const detail = e.response?.data ? JSON.stringify(e.response.data) : e.message; + console.log(` ⚠️ Runner failed: commit/push 失敗: ${e.response?.status || ''} ${detail}`); } } From fe2a513fbb4db013c05a1c23e067aeface403f51 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:31:43 +0000 Subject: [PATCH 064/174] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=20findings.js?= =?UTF-8?q?on=20=E8=B7=AF=E5=BE=91=E9=87=8D=E8=A4=87=E5=95=8F=E9=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/git.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/git.js b/app/git.js index a183851..2d9ed29 100644 --- a/app/git.js +++ b/app/git.js @@ -10,7 +10,7 @@ const api = (p) => `${GITEA_SERVER_URL.replace(/\/$/, '')}/api/v1${p}`; export async function commitAndPush(workspace) { try { - const fullPath = path.join(workspace, GITEA_REPOSITORY, FINDINGS_PATH); + const fullPath = path.join(workspace, FINDINGS_PATH); const content = fs.readFileSync(fullPath, 'utf8'); const encoded = Buffer.from(content).toString('base64'); const url = api(`/repos/${GITEA_REPOSITORY}/contents/${FINDINGS_PATH}`); From 894ece033b8cd507e05c642516104950e521d9c1 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:34:23 +0000 Subject: [PATCH 065/174] =?UTF-8?q?fix:=20Dockerfile=20=E5=8A=A0=E5=85=A5?= =?UTF-8?q?=20git=20=E5=AE=89=E8=A3=9D=E9=A9=97=E8=AD=89=EF=BC=8Cgit.js=20?= =?UTF-8?q?=E6=94=B9=E5=9B=9E=20git=20binary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 7 +++++- app/git.js | 65 +++++++++++++++++++++++------------------------------- 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/Dockerfile b/Dockerfile index a071e53..78951d0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,9 @@ -FROM node:20 +FROM node:20-slim + +RUN apt-get update && \ + apt-get install -y --no-install-recommends git && \ + rm -rf /var/lib/apt/lists/* && \ + git --version WORKDIR /action diff --git a/app/git.js b/app/git.js index 2d9ed29..87ec0c9 100644 --- a/app/git.js +++ b/app/git.js @@ -1,49 +1,38 @@ -import fs from 'fs'; +import { spawnSync } from 'child_process'; import path from 'path'; -import axios from 'axios'; -import https from 'https'; -import { GITEA_TOKEN, GITEA_SERVER_URL, GITEA_REPOSITORY, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js'; +import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js'; -const httpsAgent = new https.Agent({ rejectUnauthorized: false }); -const headers = () => ({ Authorization: `token ${GITEA_TOKEN}`, 'Content-Type': 'application/json' }); -const api = (p) => `${GITEA_SERVER_URL.replace(/\/$/, '')}/api/v1${p}`; +function git(args, cwd) { + const result = spawnSync('git', args, { cwd, encoding: 'utf8' }); + if (result.error) throw result.error; + if (result.status !== 0) throw new Error((result.stderr || result.stdout || '').trim()); + return (result.stdout || '').trim(); +} export async function commitAndPush(workspace) { - try { - const fullPath = path.join(workspace, FINDINGS_PATH); - const content = fs.readFileSync(fullPath, 'utf8'); - const encoded = Buffer.from(content).toString('base64'); - const url = api(`/repos/${GITEA_REPOSITORY}/contents/${FINDINGS_PATH}`); + const repoDir = path.join(workspace, GITEA_REPOSITORY); + const remoteUrl = GITEA_SERVER_URL.replace(/\/$/, '') + .replace('https://', `https://${GITEA_TOKEN}@`) + .replace('http://', `http://${GITEA_TOKEN}@`) + `/${GITEA_REPOSITORY}.git`; - // 取得現有檔案 SHA(若存在) - let sha; - try { - const res = await axios.get(`${url}?ref=${encodeURIComponent(PR_HEAD_BRANCH)}`, { headers: headers(), httpsAgent, timeout: 15000 }); - sha = res.data.sha; - } catch { - sha = undefined; + try { + git(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir); + git(['config', 'user.name', 'AI Review Bot'], repoDir); + git(['fetch', 'origin', PR_HEAD_BRANCH], repoDir); + git(['checkout', PR_HEAD_BRANCH], repoDir); + git(['add', FINDINGS_PATH], repoDir); + + const status = git(['status', '--porcelain'], repoDir); + if (!status) { + console.log(' findings.json 無變更,跳過 commit'); + return; } - const payload = JSON.stringify({ - message: 'chore: update ai-review findings [skip ci]', - content: encoded, - branch: PR_HEAD_BRANCH, - ...(sha ? { sha } : {}), - }); - - const resp = await axios.request({ - method: sha ? 'put' : 'post', - url, - headers: { ...headers(), 'Content-Type': 'application/json' }, - httpsAgent, - timeout: 30000, - data: payload, - }); - - const commitHash = resp.data.commit?.sha?.slice(0, 7) || 'unknown'; + const out = git(['commit', '-m', 'chore: update ai-review findings [skip ci]'], repoDir); + const commitHash = out.match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown'; + git(['push', remoteUrl, PR_HEAD_BRANCH], repoDir); console.log(` ✅ persisted findings commit=${commitHash} push=${PR_HEAD_BRANCH}`); } catch (e) { - const detail = e.response?.data ? JSON.stringify(e.response.data) : e.message; - console.log(` ⚠️ Runner failed: commit/push 失敗: ${e.response?.status || ''} ${detail}`); + console.log(` ⚠️ Runner failed: commit/push 失敗: ${e.message}`); } } From 00458d4eb2922b4d1a0b0f312b35676655954234 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:43:58 +0000 Subject: [PATCH 066/174] fix: switch Dockerfile base image to alpine and install dependencies using apk --- Dockerfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 78951d0..747ba86 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ -FROM node:20-slim +FROM alpine -RUN apt-get update && \ - apt-get install -y --no-install-recommends git && \ - rm -rf /var/lib/apt/lists/* && \ - git --version +RUN apk add --no-cache nodejs npm git \ + && node --version \ + && npm --version \ + && git --version WORKDIR /action From 8aa273b8bdbce7e7d27b1f47ed9570b3cf625c6b Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:46:58 +0000 Subject: [PATCH 067/174] fix: add bash to Dockerfile dependencies --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 747ba86..3a5481d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM alpine -RUN apk add --no-cache nodejs npm git \ +RUN apk add --no-cache bash nodejs npm git \ && node --version \ && npm --version \ && git --version From 69624a542e37ca1df37a27203da581d262c67d63 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:50:16 +0000 Subject: [PATCH 068/174] fix: refactor commitAndPush function to improve clarity and maintainability --- app/git.js | 51 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/app/git.js b/app/git.js index 87ec0c9..e3e0a2c 100644 --- a/app/git.js +++ b/app/git.js @@ -15,24 +15,41 @@ export async function commitAndPush(workspace) { .replace('https://', `https://${GITEA_TOKEN}@`) .replace('http://', `http://${GITEA_TOKEN}@`) + `/${GITEA_REPOSITORY}.git`; - try { - git(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir); - git(['config', 'user.name', 'AI Review Bot'], repoDir); - git(['fetch', 'origin', PR_HEAD_BRANCH], repoDir); - git(['checkout', PR_HEAD_BRANCH], repoDir); - git(['add', FINDINGS_PATH], repoDir); + // try { + // git(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir); + // git(['config', 'user.name', 'AI Review Bot'], repoDir); + // git(['fetch', 'origin', PR_HEAD_BRANCH], repoDir); + // git(['checkout', PR_HEAD_BRANCH], repoDir); + // git(['add', FINDINGS_PATH], repoDir); - const status = git(['status', '--porcelain'], repoDir); - if (!status) { - console.log(' findings.json 無變更,跳過 commit'); - return; - } + // const status = git(['status', '--porcelain'], repoDir); + // if (!status) { + // console.log(' findings.json 無變更,跳過 commit'); + // return; + // } - const out = git(['commit', '-m', 'chore: update ai-review findings [skip ci]'], repoDir); - const commitHash = out.match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown'; - git(['push', remoteUrl, PR_HEAD_BRANCH], repoDir); - console.log(` ✅ persisted findings commit=${commitHash} push=${PR_HEAD_BRANCH}`); - } catch (e) { - console.log(` ⚠️ Runner failed: commit/push 失敗: ${e.message}`); + // const out = git(['commit', '-m', 'chore: update ai-review findings [skip ci]'], repoDir); + // const commitHash = out.match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown'; + // git(['push', remoteUrl, PR_HEAD_BRANCH], repoDir); + // console.log(` ✅ persisted findings commit=${commitHash} push=${PR_HEAD_BRANCH}`); + // } catch (e) { + // console.log(` ⚠️ Runner failed: commit/push 失敗: ${e.message}`); + // } + + git(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir); + git(['config', 'user.name', 'AI Review Bot'], repoDir); + git(['fetch', 'origin', PR_HEAD_BRANCH], repoDir); + git(['checkout', PR_HEAD_BRANCH], repoDir); + git(['add', FINDINGS_PATH], repoDir); + + const status = git(['status', '--porcelain'], repoDir); + if (!status) { + console.log(' findings.json 無變更,跳過 commit'); + return; } + + const out = git(['commit', '-m', 'chore: update ai-review findings [skip ci]'], repoDir); + const commitHash = out.match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown'; + git(['push', remoteUrl, PR_HEAD_BRANCH], repoDir); + console.log(` ✅ persisted findings commit=${commitHash} push=${PR_HEAD_BRANCH}`); } From eae73092ad0791166fb57676513cab1e1b225998 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 10:53:59 +0000 Subject: [PATCH 069/174] fix: streamline commitAndPush function by removing redundant code and improving error handling --- app/git.js | 51 +++++++++++++++++---------------------------------- 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/app/git.js b/app/git.js index e3e0a2c..87ec0c9 100644 --- a/app/git.js +++ b/app/git.js @@ -15,41 +15,24 @@ export async function commitAndPush(workspace) { .replace('https://', `https://${GITEA_TOKEN}@`) .replace('http://', `http://${GITEA_TOKEN}@`) + `/${GITEA_REPOSITORY}.git`; - // try { - // git(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir); - // git(['config', 'user.name', 'AI Review Bot'], repoDir); - // git(['fetch', 'origin', PR_HEAD_BRANCH], repoDir); - // git(['checkout', PR_HEAD_BRANCH], repoDir); - // git(['add', FINDINGS_PATH], repoDir); + try { + git(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir); + git(['config', 'user.name', 'AI Review Bot'], repoDir); + git(['fetch', 'origin', PR_HEAD_BRANCH], repoDir); + git(['checkout', PR_HEAD_BRANCH], repoDir); + git(['add', FINDINGS_PATH], repoDir); - // const status = git(['status', '--porcelain'], repoDir); - // if (!status) { - // console.log(' findings.json 無變更,跳過 commit'); - // return; - // } + const status = git(['status', '--porcelain'], repoDir); + if (!status) { + console.log(' findings.json 無變更,跳過 commit'); + return; + } - // const out = git(['commit', '-m', 'chore: update ai-review findings [skip ci]'], repoDir); - // const commitHash = out.match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown'; - // git(['push', remoteUrl, PR_HEAD_BRANCH], repoDir); - // console.log(` ✅ persisted findings commit=${commitHash} push=${PR_HEAD_BRANCH}`); - // } catch (e) { - // console.log(` ⚠️ Runner failed: commit/push 失敗: ${e.message}`); - // } - - git(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir); - git(['config', 'user.name', 'AI Review Bot'], repoDir); - git(['fetch', 'origin', PR_HEAD_BRANCH], repoDir); - git(['checkout', PR_HEAD_BRANCH], repoDir); - git(['add', FINDINGS_PATH], repoDir); - - const status = git(['status', '--porcelain'], repoDir); - if (!status) { - console.log(' findings.json 無變更,跳過 commit'); - return; + const out = git(['commit', '-m', 'chore: update ai-review findings [skip ci]'], repoDir); + const commitHash = out.match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown'; + git(['push', remoteUrl, PR_HEAD_BRANCH], repoDir); + console.log(` ✅ persisted findings commit=${commitHash} push=${PR_HEAD_BRANCH}`); + } catch (e) { + console.log(` ⚠️ Runner failed: commit/push 失敗: ${e.message}`); } - - const out = git(['commit', '-m', 'chore: update ai-review findings [skip ci]'], repoDir); - const commitHash = out.match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown'; - git(['push', remoteUrl, PR_HEAD_BRANCH], repoDir); - console.log(` ✅ persisted findings commit=${commitHash} push=${PR_HEAD_BRANCH}`); } From f5cf5950bd51709b7f9597649ee641677f47506e Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 13:29:31 +0000 Subject: [PATCH 070/174] =?UTF-8?q?refactor:=20=E6=94=B9=E7=94=A8=20execSy?= =?UTF-8?q?nc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/git.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/git.js b/app/git.js index 87ec0c9..b67814a 100644 --- a/app/git.js +++ b/app/git.js @@ -1,12 +1,9 @@ -import { spawnSync } from 'child_process'; +import { execSync } from 'child_process'; import path from 'path'; import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js'; function git(args, cwd) { - const result = spawnSync('git', args, { cwd, encoding: 'utf8' }); - if (result.error) throw result.error; - if (result.status !== 0) throw new Error((result.stderr || result.stdout || '').trim()); - return (result.stdout || '').trim(); + return execSync(`git ${args.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ')}`, { cwd, encoding: 'utf8' }).trim(); } export async function commitAndPush(workspace) { From d04f4dd2bbf7800baa9697bf1ca696583ed74380 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 13:47:40 +0000 Subject: [PATCH 071/174] =?UTF-8?q?feat:=20=E5=9B=9E=E5=BE=A9=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20spawnSync=20=E5=9F=B7=E8=A1=8C=E6=8C=87=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/git.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/git.js b/app/git.js index b67814a..87ec0c9 100644 --- a/app/git.js +++ b/app/git.js @@ -1,9 +1,12 @@ -import { execSync } from 'child_process'; +import { spawnSync } from 'child_process'; import path from 'path'; import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js'; function git(args, cwd) { - return execSync(`git ${args.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ')}`, { cwd, encoding: 'utf8' }).trim(); + const result = spawnSync('git', args, { cwd, encoding: 'utf8' }); + if (result.error) throw result.error; + if (result.status !== 0) throw new Error((result.stderr || result.stdout || '').trim()); + return (result.stdout || '').trim(); } export async function commitAndPush(workspace) { From 8a28d1f1ef2dfe236008bb842de2ff67779cfd33 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 13:50:54 +0000 Subject: [PATCH 072/174] =?UTF-8?q?test:=20=E4=BF=AE=E6=AD=A3=E8=B7=AF?= =?UTF-8?q?=E5=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/git.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/git.js b/app/git.js index 87ec0c9..123178d 100644 --- a/app/git.js +++ b/app/git.js @@ -1,5 +1,4 @@ import { spawnSync } from 'child_process'; -import path from 'path'; import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js'; function git(args, cwd) { @@ -10,7 +9,7 @@ function git(args, cwd) { } export async function commitAndPush(workspace) { - const repoDir = path.join(workspace, GITEA_REPOSITORY); + const repoDir = workspace; const remoteUrl = GITEA_SERVER_URL.replace(/\/$/, '') .replace('https://', `https://${GITEA_TOKEN}@`) .replace('http://', `http://${GITEA_TOKEN}@`) + `/${GITEA_REPOSITORY}.git`; From c1f8aa3c72eb659763ab72d05aad6337b6aa0c3e Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 13:54:44 +0000 Subject: [PATCH 073/174] =?UTF-8?q?feat:=20git=20=E6=93=8D=E4=BD=9C=20clon?= =?UTF-8?q?e=20=E7=9A=84=20repo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/git.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/git.js b/app/git.js index 123178d..c9bc837 100644 --- a/app/git.js +++ b/app/git.js @@ -1,4 +1,6 @@ import { spawnSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js'; function git(args, cwd) { @@ -9,16 +11,28 @@ function git(args, cwd) { } export async function commitAndPush(workspace) { - const repoDir = workspace; const remoteUrl = GITEA_SERVER_URL.replace(/\/$/, '') .replace('https://', `https://${GITEA_TOKEN}@`) .replace('http://', `http://${GITEA_TOKEN}@`) + `/${GITEA_REPOSITORY}.git`; + const repoDir = path.join(workspace, 'repo'); + try { + if (!fs.existsSync(repoDir)) { + git(['clone', '--depth=1', '--branch', PR_HEAD_BRANCH, remoteUrl, repoDir], workspace); + } + git(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir); git(['config', 'user.name', 'AI Review Bot'], repoDir); git(['fetch', 'origin', PR_HEAD_BRANCH], repoDir); git(['checkout', PR_HEAD_BRANCH], 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); + git(['add', FINDINGS_PATH], repoDir); const status = git(['status', '--porcelain'], repoDir); From d7336dbe6c657f57641916655664766f291c5631 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Mon, 11 May 2026 13:55:46 +0000 Subject: [PATCH 074/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 114 +++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 .gitea/ai-review/findings.json diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json new file mode 100644 index 0000000..d38cfc7 --- /dev/null +++ b/.gitea/ai-review/findings.json @@ -0,0 +1,114 @@ +[ + { + "level": "critical", + "role": "Rex", + "location": "app/git.js:11", + "suggestion": "GITEA_TOKEN 直接嵌入 URL 中,可能導致憑證洩漏。建議使用環境變數或安全的憑證管理方式來處理敏感資訊。", + "is_new": true + }, + { + "level": "critical", + "role": "Maya", + "location": "app/git.js:1", + "suggestion": "缺少對 commitAndPush 函數的單元測試,應該為其添加測試以確保其正確性。", + "is_new": true + }, + { + "level": "warning", + "role": "Leo", + "location": "app/git.js:11", + "suggestion": "建議將 git 函式的實作細節封裝到一個獨立的模組中,以提高模組化和可重用性。", + "is_new": true + }, + { + "level": "warning", + "role": "Leo", + "location": "app/git.js:41", + "suggestion": "建議在 try-catch 區塊中增加更詳細的錯誤處理,以便於未來的除錯和維護。", + "is_new": true + }, + { + "level": "warning", + "role": "Zara", + "location": "app/git.js:25", + "suggestion": "在使用 fs.existsSync 檢查目錄是否存在時,應考慮使用非同步方法以避免阻塞事件循環。", + "is_new": true + }, + { + "level": "warning", + "role": "Zara", + "location": "app/git.js:29", + "suggestion": "在 git clone 時使用 --depth=1 可能會導致未來需要完整歷史紀錄時的性能問題,建議根據實際需求調整。", + "is_new": true + }, + { + "level": "warning", + "role": "Rex", + "location": "app/git.js:11", + "suggestion": "在使用 fs.copyFileSync 時,未檢查目標檔案是否存在,可能會覆蓋重要資料。建議在複製之前檢查檔案是否存在。", + "is_new": true + }, + { + "level": "warning", + "role": "Maya", + "location": "app/git.js:11", + "suggestion": "在 commitAndPush 函數中,對於 git 操作的錯誤處理不夠完善,應該添加更多的測試來驗證不同情況下的行為。", + "is_new": true + }, + { + "level": "info", + "role": "Leo", + "location": "app/git.js:5", + "suggestion": "建議為函式添加 JSDoc 註解,以提高文件完整性和可讀性。", + "is_new": true + }, + { + "level": "info", + "role": "Leo", + "location": "app/git.js:10", + "suggestion": "建議將常數如 'repo' 提取為變數,以提高可讀性和可維護性。", + "is_new": true + }, + { + "level": "info", + "role": "Zara", + "location": "app/git.js:45", + "suggestion": "考慮使用 async/await 來處理 fs.copyFileSync,以提高可讀性和錯誤處理能力。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "app/git.js:12", + "suggestion": "考慮將常量 GITEA_SERVER_URL、GITEA_REPOSITORY、GITEA_TOKEN、PR_HEAD_BRANCH 和 FINDINGS_PATH 的引入放在一起,以保持一致性。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "app/git.js:41", + "suggestion": "在 console.log 的訊息中,考慮使用更具描述性的文字來提高可讀性。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "app/git.js:43", + "suggestion": "考慮將錯誤處理的 console.log 訊息翻譯成英文,以保持一致性。", + "is_new": true + }, + { + "level": "info", + "role": "Maya", + "location": "app/git.js:11", + "suggestion": "建議為 git 函數添加測試,以確保其在不同參數下的行為正確。", + "is_new": true + }, + { + "level": "info", + "role": "Maya", + "location": "app/git.js:11", + "suggestion": "建議對 repoDir 的存在性檢查進行單元測試,以確保在不存在時能正確執行 clone 操作。", + "is_new": true + } +] \ No newline at end of file From 323be94a7280952a034b4be842b4b641b590fa22 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 14:04:32 +0000 Subject: [PATCH 075/174] =?UTF-8?q?feat:=20master=20=E4=B8=8D=E6=9C=83?= =?UTF-8?q?=E8=A7=B8=E7=99=BC=20review.yaml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/review.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 3a49860..776e323 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -2,6 +2,8 @@ name: AI on: pull_request: types: [opened, synchronize] + branches-ignore: + - master jobs: version: name: 計算版本號 From ec05ce7869573c12177752088acc35406da45823 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 14:06:09 +0000 Subject: [PATCH 076/174] =?UTF-8?q?feat:=20=E8=AA=BF=E6=95=B4=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A=E7=9A=84=E9=A0=86=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/review.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 776e323..19f0d12 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -1,9 +1,9 @@ name: AI on: pull_request: - types: [opened, synchronize] branches-ignore: - - master + - master + types: [opened, synchronize] jobs: version: name: 計算版本號 From 09c78835e73d1afc98369e2defdd7a6ab11530e7 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Mon, 11 May 2026 14:06:17 +0000 Subject: [PATCH 077/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 109 +-------------------------------- 1 file changed, 2 insertions(+), 107 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index d38cfc7..0b0115d 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,114 +1,9 @@ [ - { - "level": "critical", - "role": "Rex", - "location": "app/git.js:11", - "suggestion": "GITEA_TOKEN 直接嵌入 URL 中,可能導致憑證洩漏。建議使用環境變數或安全的憑證管理方式來處理敏感資訊。", - "is_new": true - }, - { - "level": "critical", - "role": "Maya", - "location": "app/git.js:1", - "suggestion": "缺少對 commitAndPush 函數的單元測試,應該為其添加測試以確保其正確性。", - "is_new": true - }, - { - "level": "warning", - "role": "Leo", - "location": "app/git.js:11", - "suggestion": "建議將 git 函式的實作細節封裝到一個獨立的模組中,以提高模組化和可重用性。", - "is_new": true - }, - { - "level": "warning", - "role": "Leo", - "location": "app/git.js:41", - "suggestion": "建議在 try-catch 區塊中增加更詳細的錯誤處理,以便於未來的除錯和維護。", - "is_new": true - }, - { - "level": "warning", - "role": "Zara", - "location": "app/git.js:25", - "suggestion": "在使用 fs.existsSync 檢查目錄是否存在時,應考慮使用非同步方法以避免阻塞事件循環。", - "is_new": true - }, - { - "level": "warning", - "role": "Zara", - "location": "app/git.js:29", - "suggestion": "在 git clone 時使用 --depth=1 可能會導致未來需要完整歷史紀錄時的性能問題,建議根據實際需求調整。", - "is_new": true - }, - { - "level": "warning", - "role": "Rex", - "location": "app/git.js:11", - "suggestion": "在使用 fs.copyFileSync 時,未檢查目標檔案是否存在,可能會覆蓋重要資料。建議在複製之前檢查檔案是否存在。", - "is_new": true - }, - { - "level": "warning", - "role": "Maya", - "location": "app/git.js:11", - "suggestion": "在 commitAndPush 函數中,對於 git 操作的錯誤處理不夠完善,應該添加更多的測試來驗證不同情況下的行為。", - "is_new": true - }, - { - "level": "info", - "role": "Leo", - "location": "app/git.js:5", - "suggestion": "建議為函式添加 JSDoc 註解,以提高文件完整性和可讀性。", - "is_new": true - }, - { - "level": "info", - "role": "Leo", - "location": "app/git.js:10", - "suggestion": "建議將常數如 'repo' 提取為變數,以提高可讀性和可維護性。", - "is_new": true - }, - { - "level": "info", - "role": "Zara", - "location": "app/git.js:45", - "suggestion": "考慮使用 async/await 來處理 fs.copyFileSync,以提高可讀性和錯誤處理能力。", - "is_new": true - }, { "level": "info", "role": "Aria", - "location": "app/git.js:12", - "suggestion": "考慮將常量 GITEA_SERVER_URL、GITEA_REPOSITORY、GITEA_TOKEN、PR_HEAD_BRANCH 和 FINDINGS_PATH 的引入放在一起,以保持一致性。", - "is_new": true - }, - { - "level": "info", - "role": "Aria", - "location": "app/git.js:41", - "suggestion": "在 console.log 的訊息中,考慮使用更具描述性的文字來提高可讀性。", - "is_new": true - }, - { - "level": "info", - "role": "Aria", - "location": "app/git.js:43", - "suggestion": "考慮將錯誤處理的 console.log 訊息翻譯成英文,以保持一致性。", - "is_new": true - }, - { - "level": "info", - "role": "Maya", - "location": "app/git.js:11", - "suggestion": "建議為 git 函數添加測試,以確保其在不同參數下的行為正確。", - "is_new": true - }, - { - "level": "info", - "role": "Maya", - "location": "app/git.js:11", - "suggestion": "建議對 repoDir 的存在性檢查進行單元測試,以確保在不存在時能正確執行 clone 操作。", + "location": ".gitea/workflows/review.yaml:5", + "suggestion": "建議在 'branches-ignore' 前加上空行,以提高可讀性。", "is_new": true } ] \ No newline at end of file From 65cf45c5589bad0994609a49a654c514cac7051d Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Mon, 11 May 2026 14:06:55 +0000 Subject: [PATCH 078/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 93 +++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 0b0115d..01635c8 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,9 +1,100 @@ [ + { + "level": "critical", + "role": "Leo", + "location": "app/git.js:11", + "suggestion": "GITEA_TOKEN 直接嵌入 URL 中,可能導致憑證洩漏。建議使用環境變數或安全的憑證管理方式來處理敏感資訊。", + "is_new": true + }, + { + "level": "critical", + "role": "Leo", + "location": "app/git.js:1", + "suggestion": "缺少對 commitAndPush 函數的單元測試,應該為其添加測試以確保其正確性。", + "is_new": true + }, + { + "level": "warning", + "role": "Leo", + "location": "app/git.js:11", + "suggestion": "建議將 git 函式的實作細節封裝到一個獨立的模組中,以提高模組化和可重用性。", + "is_new": true + }, + { + "level": "warning", + "role": "Leo", + "location": "app/git.js:41", + "suggestion": "建議在 try-catch 區塊中增加更詳細的錯誤處理,以便於未來的除錯和維護。", + "is_new": true + }, + { + "level": "warning", + "role": "Leo", + "location": "app/git.js:25", + "suggestion": "在使用 fs.existsSync 檢查目錄是否存在時,應考慮使用非同步方法以避免阻塞事件循環。", + "is_new": true + }, + { + "level": "warning", + "role": "Leo", + "location": "app/git.js:29", + "suggestion": "在 git clone 時使用 --depth=1 可能會導致未來需要完整歷史紀錄時的性能問題,建議根據實際需求調整。", + "is_new": true + }, + { + "level": "warning", + "role": "Leo", + "location": "app/git.js:11", + "suggestion": "在使用 fs.copyFileSync 時,未檢查目標檔案是否存在,可能會覆蓋重要資料。建議在複製之前檢查檔案是否存在。", + "is_new": true + }, + { + "level": "warning", + "role": "Leo", + "location": "app/git.js:11", + "suggestion": "在 commitAndPush 函數中,對於 git 操作的錯誤處理不夠完善,應該添加更多的測試來驗證不同情況下的行為。", + "is_new": true + }, { "level": "info", - "role": "Aria", + "role": "Leo", + "location": "app/git.js:5", + "suggestion": "建議為函式添加 JSDoc 註解,以提高文件完整性和可讀性。", + "is_new": true + }, + { + "level": "info", + "role": "Leo", + "location": "app/git.js:10", + "suggestion": "建議將常數如 'repo' 提取為變數,以提高可讀性和可維護性。", + "is_new": true + }, + { + "level": "info", + "role": "Leo", + "location": "app/git.js:45", + "suggestion": "考慮使用 async/await 來處理 fs.copyFileSync,以提高可讀性和錯誤處理能力。", + "is_new": true + }, + { + "level": "info", + "role": "Leo", "location": ".gitea/workflows/review.yaml:5", "suggestion": "建議在 'branches-ignore' 前加上空行,以提高可讀性。", "is_new": true + }, + { + "level": "info", + "role": "Maya", + "location": "app/git.js:11", + "suggestion": "建議為 git 函數添加測試,以確保其在不同參數下的行為正確。", + "is_new": true + }, + { + "level": "info", + "role": "Maya", + "location": "app/git.js:11", + "suggestion": "建議對 repoDir 的存在性檢查進行單元測試,以確保在不存在時能正確執行 clone 操作。", + "is_new": true } ] \ No newline at end of file From cd0ced1b7ff105e114bc4e01075ba52b92060445 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 14:08:40 +0000 Subject: [PATCH 079/174] =?UTF-8?q?feat:=20=E5=90=8C=E6=99=82=E5=8F=AA?= =?UTF-8?q?=E8=83=BD=E6=9C=89=E4=B8=80=E5=80=8B=20review.yaml=20=E5=9F=B7?= =?UTF-8?q?=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/review.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 19f0d12..71795f2 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -1,4 +1,7 @@ name: AI +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true on: pull_request: branches-ignore: From e217b18c6268fbf574596a6dc3ff9ff6066e7e13 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Mon, 11 May 2026 14:09:25 +0000 Subject: [PATCH 080/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 68 ++++++++++++++-------------------- 1 file changed, 27 insertions(+), 41 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 01635c8..82839c2 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -3,98 +3,84 @@ "level": "critical", "role": "Leo", "location": "app/git.js:11", - "suggestion": "GITEA_TOKEN 直接嵌入 URL 中,可能導致憑證洩漏。建議使用環境變數或安全的憑證管理方式來處理敏感資訊。", + "suggestion": "GITEA_TOKEN 直接嵌入 URL 中,可能導致憑證洩漏。建議使用環境變數或安全的憑證管理方式來處理敏感資訊.", "is_new": true }, { "level": "critical", - "role": "Leo", + "role": "Maya", "location": "app/git.js:1", - "suggestion": "缺少對 commitAndPush 函數的單元測試,應該為其添加測試以確保其正確性。", - "is_new": true - }, - { - "level": "warning", - "role": "Leo", - "location": "app/git.js:11", - "suggestion": "建議將 git 函式的實作細節封裝到一個獨立的模組中,以提高模組化和可重用性。", - "is_new": true - }, - { - "level": "warning", - "role": "Leo", - "location": "app/git.js:41", - "suggestion": "建議在 try-catch 區塊中增加更詳細的錯誤處理,以便於未來的除錯和維護。", + "suggestion": "缺少對 commitAndPush 函數的單元測試,應該為其添加測試以確保其正確性.", "is_new": true }, { "level": "warning", "role": "Leo", "location": "app/git.js:25", - "suggestion": "在使用 fs.existsSync 檢查目錄是否存在時,應考慮使用非同步方法以避免阻塞事件循環。", + "suggestion": "在使用 fs.existsSync 檢查目錄是否存在時,應考慮使用非同步方法以避免阻塞事件循環.", "is_new": true }, { "level": "warning", "role": "Leo", "location": "app/git.js:29", - "suggestion": "在 git clone 時使用 --depth=1 可能會導致未來需要完整歷史紀錄時的性能問題,建議根據實際需求調整。", + "suggestion": "在 git clone 時使用 --depth=1 可能會導致未來需要完整歷史紀錄時的性能問題,建議根據實際需求調整.", "is_new": true }, { "level": "warning", "role": "Leo", "location": "app/git.js:11", - "suggestion": "在使用 fs.copyFileSync 時,未檢查目標檔案是否存在,可能會覆蓋重要資料。建議在複製之前檢查檔案是否存在。", + "suggestion": "在使用 fs.copyFileSync 時,未檢查目標檔案是否存在,可能會覆蓋重要資料。建議在複製之前檢查檔案是否存在.", "is_new": true }, { "level": "warning", "role": "Leo", "location": "app/git.js:11", - "suggestion": "在 commitAndPush 函數中,對於 git 操作的錯誤處理不夠完善,應該添加更多的測試來驗證不同情況下的行為。", + "suggestion": "在 commitAndPush 函數中,對於 git 操作的錯誤處理不夠完善,應該添加更多的測試來驗證不同情況下的行為.", "is_new": true }, { - "level": "info", - "role": "Leo", - "location": "app/git.js:5", - "suggestion": "建議為函式添加 JSDoc 註解,以提高文件完整性和可讀性。", + "level": "warning", + "role": "Rex", + "location": "app/git.js:11", + "suggestion": "在使用 fs.copyFileSync 時,未檢查目標檔案是否存在,可能會覆蓋重要資料。建議在複製之前檢查檔案是否存在.", "is_new": true }, { - "level": "info", - "role": "Leo", - "location": "app/git.js:10", - "suggestion": "建議將常數如 'repo' 提取為變數,以提高可讀性和可維護性。", + "level": "warning", + "role": "Rex", + "location": "app/git.js:29", + "suggestion": "在 git clone 時使用 --depth=1 可能會導致未來需要完整歷史紀錄時的性能問題,建議根據實際需求調整.", + "is_new": true + }, + { + "level": "warning", + "role": "Rex", + "location": "app/git.js:11", + "suggestion": "在 commitAndPush 函數中,對於 git 操作的錯誤處理不夠完善,應該添加更多的測試來驗證不同情況下的行為.", "is_new": true }, { "level": "info", "role": "Leo", "location": "app/git.js:45", - "suggestion": "考慮使用 async/await 來處理 fs.copyFileSync,以提高可讀性和錯誤處理能力。", + "suggestion": "考慮使用 async/await 來處理 fs.copyFileSync,以提高可讀性和錯誤處理能力.", "is_new": true }, { "level": "info", "role": "Leo", "location": ".gitea/workflows/review.yaml:5", - "suggestion": "建議在 'branches-ignore' 前加上空行,以提高可讀性。", + "suggestion": "建議在 'branches-ignore' 前加上空行,以提高可讀性.", "is_new": true }, { "level": "info", - "role": "Maya", - "location": "app/git.js:11", - "suggestion": "建議為 git 函數添加測試,以確保其在不同參數下的行為正確。", - "is_new": true - }, - { - "level": "info", - "role": "Maya", - "location": "app/git.js:11", - "suggestion": "建議對 repoDir 的存在性檢查進行單元測試,以確保在不存在時能正確執行 clone 操作。", + "role": "Aria", + "location": ".gitea/workflows/review.yaml:5", + "suggestion": "建議在 'branches-ignore' 前加上空行,以提高可讀性.", "is_new": true } ] \ No newline at end of file From 09533ff74188a195cc614d0d9c1ba039969c777c Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 11 May 2026 14:10:57 +0000 Subject: [PATCH 081/174] =?UTF-8?q?feat:=20=E6=94=B9=E7=94=A8=E5=88=86?= =?UTF-8?q?=E6=94=AF=E5=90=8D=E7=A8=B1=E9=8E=96=E5=AE=9A=20review=20?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81?= 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 71795f2..dc785d9 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -1,6 +1,6 @@ name: AI concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.head_ref }} cancel-in-progress: true on: pull_request: From a4b87f9108e5bc5d36e8c771a1532f04c06f3e3a Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Mon, 11 May 2026 14:11:43 +0000 Subject: [PATCH 082/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 50 ++++++++-------------------------- 1 file changed, 11 insertions(+), 39 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 82839c2..7dbcb7b 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -3,84 +3,56 @@ "level": "critical", "role": "Leo", "location": "app/git.js:11", - "suggestion": "GITEA_TOKEN 直接嵌入 URL 中,可能導致憑證洩漏。建議使用環境變數或安全的憑證管理方式來處理敏感資訊.", + "suggestion": "GITEA_TOKEN 直接嵌入 URL 中,可能導致憑證洩漏。建議使用環境變數或安全的憑證管理方式來處理敏感資訊。", "is_new": true }, { "level": "critical", "role": "Maya", "location": "app/git.js:1", - "suggestion": "缺少對 commitAndPush 函數的單元測試,應該為其添加測試以確保其正確性.", + "suggestion": "缺少對 commitAndPush 函數的單元測試,應該為其添加測試以確保其正確性。", "is_new": true }, { "level": "warning", "role": "Leo", "location": "app/git.js:25", - "suggestion": "在使用 fs.existsSync 檢查目錄是否存在時,應考慮使用非同步方法以避免阻塞事件循環.", + "suggestion": "在使用 fs.existsSync 檢查目錄是否存在時,應考慮使用非同步方法以避免阻塞事件循環。", "is_new": true }, { "level": "warning", "role": "Leo", "location": "app/git.js:29", - "suggestion": "在 git clone 時使用 --depth=1 可能會導致未來需要完整歷史紀錄時的性能問題,建議根據實際需求調整.", + "suggestion": "在 git clone 時使用 --depth=1 可能會導致未來需要完整歷史紀錄時的性能問題,建議根據實際需求調整。", "is_new": true }, { "level": "warning", "role": "Leo", "location": "app/git.js:11", - "suggestion": "在使用 fs.copyFileSync 時,未檢查目標檔案是否存在,可能會覆蓋重要資料。建議在複製之前檢查檔案是否存在.", + "suggestion": "在使用 fs.copyFileSync 時,未檢查目標檔案是否存在,可能會覆蓋重要資料。建議在複製之前檢查檔案是否存在。", "is_new": true }, { "level": "warning", "role": "Leo", "location": "app/git.js:11", - "suggestion": "在 commitAndPush 函數中,對於 git 操作的錯誤處理不夠完善,應該添加更多的測試來驗證不同情況下的行為.", + "suggestion": "在 commitAndPush 函數中,對於 git 操作的錯誤處理不夠完善,應該添加更多的測試來驗證不同情況下的行為。", "is_new": true }, { - "level": "warning", - "role": "Rex", - "location": "app/git.js:11", - "suggestion": "在使用 fs.copyFileSync 時,未檢查目標檔案是否存在,可能會覆蓋重要資料。建議在複製之前檢查檔案是否存在.", - "is_new": true - }, - { - "level": "warning", - "role": "Rex", - "location": "app/git.js:29", - "suggestion": "在 git clone 時使用 --depth=1 可能會導致未來需要完整歷史紀錄時的性能問題,建議根據實際需求調整.", - "is_new": true - }, - { - "level": "warning", - "role": "Rex", - "location": "app/git.js:11", - "suggestion": "在 commitAndPush 函數中,對於 git 操作的錯誤處理不夠完善,應該添加更多的測試來驗證不同情況下的行為.", + "level": "info", + "role": "Leo", + "location": ".gitea/workflows/review.yaml:5", + "suggestion": "建議在 'branches-ignore' 前加上空行,以提高可讀性。", "is_new": true }, { "level": "info", "role": "Leo", "location": "app/git.js:45", - "suggestion": "考慮使用 async/await 來處理 fs.copyFileSync,以提高可讀性和錯誤處理能力.", - "is_new": true - }, - { - "level": "info", - "role": "Leo", - "location": ".gitea/workflows/review.yaml:5", - "suggestion": "建議在 'branches-ignore' 前加上空行,以提高可讀性.", - "is_new": true - }, - { - "level": "info", - "role": "Aria", - "location": ".gitea/workflows/review.yaml:5", - "suggestion": "建議在 'branches-ignore' 前加上空行,以提高可讀性.", + "suggestion": "考慮使用 async/await 來處理 fs.copyFileSync,以提高可讀性和錯誤處理能力。", "is_new": true } ] \ No newline at end of file From 59509ae96356de64e8d9613327dc18b4c1a1477a Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 01:08:39 +0000 Subject: [PATCH 083/174] feat: refactor commitAndPush to use a runner function and improve token security; add tests for git operations --- app/git.js | 48 +++++++++++++++---------- app/git.test.js | 93 ++++++++++++++++++++++++++++++++++++++++++++++++ app/package.json | 3 ++ 3 files changed, 126 insertions(+), 18 deletions(-) create mode 100644 app/git.test.js diff --git a/app/git.js b/app/git.js index c9bc837..143e848 100644 --- a/app/git.js +++ b/app/git.js @@ -3,29 +3,39 @@ import fs from 'fs'; import path from 'path'; import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js'; -function git(args, cwd) { - const result = spawnSync('git', args, { cwd, encoding: 'utf8' }); - if (result.error) throw result.error; - if (result.status !== 0) throw new Error((result.stderr || result.stdout || '').trim()); - return (result.stdout || '').trim(); +function makeRunner(spawn) { + return function run(args, cwd, env) { + const opts = { cwd, encoding: 'utf8' }; + if (env) opts.env = env; + const result = spawn('git', args, opts); + if (result.error) throw result.error; + if (result.status !== 0) throw new Error((result.stderr || result.stdout || '').trim()); + return (result.stdout || '').trim(); + }; } -export async function commitAndPush(workspace) { - const remoteUrl = GITEA_SERVER_URL.replace(/\/$/, '') - .replace('https://', `https://${GITEA_TOKEN}@`) - .replace('http://', `http://${GITEA_TOKEN}@`) + `/${GITEA_REPOSITORY}.git`; +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 so the token never appears in the URL or process list + const askpassScript = path.join(workspace, '.git-askpass.sh'); + fs.writeFileSync(askpassScript, `#!/bin/sh\necho "${GITEA_TOKEN}"\n`, { mode: 0o700 }); + + const credEnv = { ...process.env, GIT_ASKPASS: askpassScript, GIT_USERNAME: 'x-token' }; + try { if (!fs.existsSync(repoDir)) { - git(['clone', '--depth=1', '--branch', PR_HEAD_BRANCH, remoteUrl, repoDir], workspace); + run(['clone', '--depth=1', '--branch', PR_HEAD_BRANCH, remoteUrl, repoDir], workspace, credEnv); } - git(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir); - git(['config', 'user.name', 'AI Review Bot'], repoDir); - git(['fetch', 'origin', PR_HEAD_BRANCH], repoDir); - git(['checkout', PR_HEAD_BRANCH], repoDir); + 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); // 將 findings.json 從 workspace 複製到 clone 的 repo const srcFindings = path.join(workspace, FINDINGS_PATH); @@ -33,19 +43,21 @@ export async function commitAndPush(workspace) { fs.mkdirSync(path.dirname(destFindings), { recursive: true }); fs.copyFileSync(srcFindings, destFindings); - git(['add', FINDINGS_PATH], repoDir); + run(['add', FINDINGS_PATH], repoDir); - const status = git(['status', '--porcelain'], repoDir); + const status = run(['status', '--porcelain'], repoDir); if (!status) { console.log(' findings.json 無變更,跳過 commit'); return; } - const out = git(['commit', '-m', 'chore: update ai-review findings [skip ci]'], repoDir); + const out = run(['commit', '-m', 'chore: update ai-review findings [skip ci]'], repoDir); const commitHash = out.match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown'; - git(['push', remoteUrl, PR_HEAD_BRANCH], repoDir); + 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/git.test.js b/app/git.test.js new file mode 100644 index 0000000..d96efce --- /dev/null +++ b/app/git.test.js @@ -0,0 +1,93 @@ +import { describe, it, before, after, beforeEach } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { commitAndPush } from './git.js'; + +// --- helpers --- +function makeTmpWorkspace() { + const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'git-test-')); + // Pre-create repo dir so clone branch is skipped + fs.mkdirSync(path.join(ws, 'repo'), { recursive: true }); + // Create a findings.json to copy + const findingsDir = path.join(ws, '.gitea/ai-review'); + fs.mkdirSync(findingsDir, { recursive: true }); + fs.writeFileSync(path.join(findingsDir, 'findings.json'), '[]'); + return ws; +} + +// Default stub: all commands succeed, status returns changes +function makeSpawn(overrides = {}) { + const calls = []; + const spawn = (cmd, args, opts) => { + const key = args[0]; + calls.push({ cmd, args, opts }); + if (overrides[key]) return overrides[key](args, opts); + if (key === 'status') return { status: 0, stdout: 'M .gitea/ai-review/findings.json', stderr: '', error: null }; + if (key === 'commit') return { status: 0, stdout: '[feature-branch abc1234] chore', stderr: '', error: null }; + return { status: 0, stdout: '', stderr: '', error: null }; + }; + spawn.calls = calls; + return spawn; +} + +describe('commitAndPush', () => { + let workspace; + + 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)); + } + }); + + it('does not embed token in any git command argument', async () => { + const spawn = makeSpawn(); + await commitAndPush(workspace, spawn); + + for (const { args } of spawn.calls) { + assert.ok(!args.join(' ').includes('test-token'), `Token leaked in git args: ${args.join(' ')}`); + } + }); + + it('uses GIT_ASKPASS env for network operations (fetch, push, clone)', async () => { + const spawn = makeSpawn(); + await commitAndPush(workspace, spawn); + + const networkOps = ['fetch', 'push', 'clone']; + const networkCalls = spawn.calls.filter(c => networkOps.includes(c.args[0])); + assert.ok(networkCalls.length > 0, 'expected at least one network git call'); + + for (const { args, opts } of networkCalls) { + assert.ok(opts?.env?.GIT_ASKPASS, `GIT_ASKPASS missing for git ${args[0]}`); + } + }); + + it('cleans up askpass script after successful run', async () => { + await commitAndPush(workspace, 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); + 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); + 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)); + }); +}); diff --git a/app/package.json b/app/package.json index b5e3877..466e7c7 100644 --- a/app/package.json +++ b/app/package.json @@ -2,6 +2,9 @@ "name": "ai-code-review", "version": "1.0.0", "type": "module", + "scripts": { + "test": "node --test app/git.test.js" + }, "dependencies": { "axios": "^1.6.7", "js-yaml": "^4.1.0", From d282779f688e27d8d4dab6d15ac26d4a7663c96b Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 01:09:42 +0000 Subject: [PATCH 084/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 153 +++++++++++++++++++++++++-------- 1 file changed, 115 insertions(+), 38 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 7dbcb7b..e4a78e8 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,58 +1,135 @@ [ { "level": "critical", - "role": "Leo", - "location": "app/git.js:11", - "suggestion": "GITEA_TOKEN 直接嵌入 URL 中,可能導致憑證洩漏。建議使用環境變數或安全的憑證管理方式來處理敏感資訊。", + "role": "Rex", + "location": "app/git.js:20", + "suggestion": "請避免在程式碼中硬編碼敏感資料,如 GITEA_TOKEN。應使用環境變數或安全的秘密管理工具來管理這些敏感資料。", "is_new": true }, { - "level": "critical", + "level": "warning", + "role": "Leo", + "location": "app/git.js:39", + "suggestion": "建議在函式 `makeRunner` 中增加對於 `spawn` 函式的參數檢查,以確保其為有效的函式,避免未來可能的錯誤。", + "is_new": true + }, + { + "level": "warning", + "role": "Zara", + "location": "app/git.js:39", + "suggestion": "在使用 git clone 時,建議使用 --single-branch 參數來避免下載不必要的分支,這樣可以節省時間和空間。", + "is_new": true + }, + { + "level": "warning", + "role": "Rex", + "location": "app/git.js:39", + "suggestion": "在使用 git 命令時,請確保適當處理錯誤,避免潛在的資訊洩漏。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": "app/git.js:3", + "suggestion": "檔案開頭應該有一行空白行,以提高可讀性。", + "is_new": true + }, + { + "level": "warning", "role": "Maya", - "location": "app/git.js:1", - "suggestion": "缺少對 commitAndPush 函數的單元測試,應該為其添加測試以確保其正確性。", - "is_new": true - }, - { - "level": "warning", - "role": "Leo", - "location": "app/git.js:25", - "suggestion": "在使用 fs.existsSync 檢查目錄是否存在時,應考慮使用非同步方法以避免阻塞事件循環。", - "is_new": true - }, - { - "level": "warning", - "role": "Leo", - "location": "app/git.js:29", - "suggestion": "在 git clone 時使用 --depth=1 可能會導致未來需要完整歷史紀錄時的性能問題,建議根據實際需求調整。", - "is_new": true - }, - { - "level": "warning", - "role": "Leo", - "location": "app/git.js:11", - "suggestion": "在使用 fs.copyFileSync 時,未檢查目標檔案是否存在,可能會覆蓋重要資料。建議在複製之前檢查檔案是否存在。", - "is_new": true - }, - { - "level": "warning", - "role": "Leo", - "location": "app/git.js:11", - "suggestion": "在 commitAndPush 函數中,對於 git 操作的錯誤處理不夠完善,應該添加更多的測試來驗證不同情況下的行為。", + "location": "app/git.test.js:1", + "suggestion": "建議在測試檔案中加入更多的測試案例,以涵蓋不同的邊界條件和異常情況。", "is_new": true }, { "level": "info", "role": "Leo", - "location": ".gitea/workflows/review.yaml:5", - "suggestion": "建議在 'branches-ignore' 前加上空行,以提高可讀性。", + "location": "app/git.js:43", + "suggestion": "考慮將 `askpassScript` 的寫入過程封裝成一個獨立的函式,以提高程式碼的模組化和可讀性。", "is_new": true }, { "level": "info", "role": "Leo", - "location": "app/git.js:45", - "suggestion": "考慮使用 async/await 來處理 fs.copyFileSync,以提高可讀性和錯誤處理能力。", + "location": "app/git.js:53", + "suggestion": "在 `catch` 區塊中,建議記錄更詳細的錯誤資訊,以便於未來的除錯和維護。", + "is_new": true + }, + { + "level": "info", + "role": "Leo", + "location": "app/git.js:58", + "suggestion": "在 `finally` 區塊中,建議增加對於 `fs.unlinkSync` 的錯誤處理,以避免在刪除檔案時發生未捕獲的錯誤。", + "is_new": true + }, + { + "level": "info", + "role": "Zara", + "location": "app/git.js:43", + "suggestion": "在寫入 askpass 腳本時,考慮使用 fs.promises.writeFile 來避免阻塞事件循環,提升效能。", + "is_new": true + }, + { + "level": "info", + "role": "Zara", + "location": "app/git.js:53", + "suggestion": "在使用 fs.mkdirSync 時,建議使用 fs.promises.mkdir 來避免阻塞,提升效能。", + "is_new": true + }, + { + "level": "info", + "role": "Zara", + "location": "app/git.js:65", + "suggestion": "在 git push 時,考慮使用 --quiet 參數來減少不必要的輸出,這樣可以提升效能。", + "is_new": true + }, + { + "level": "info", + "role": "Rex", + "location": "app/git.js:43", + "suggestion": "建議在使用完 askpass 腳本後,確保其被刪除,以減少潛在的安全風險。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "app/git.js:39", + "suggestion": "考慮將 'run' 函數的命名改為更具描述性的名稱,例如 'executeGitCommand',以提高可讀性。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "app/git.js:43", + "suggestion": "在 'try' 區塊的結尾添加註解,說明 'finally' 區塊的目的,以提高可讀性。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "app/git.js:51", + "suggestion": "在 'catch' 區塊中,考慮使用更具描述性的錯誤訊息,以便於除錯。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "app/git.test.js:1", + "suggestion": "考慮在檔案開頭添加檔案描述註解,以提高可讀性。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "app/git.test.js:93", + "suggestion": "考慮在測試結束後添加註解,說明測試的目的,以提高可讀性。", + "is_new": true + }, + { + "level": "info", + "role": "Maya", + "location": "app/git.test.js:1", + "suggestion": "建議使用更具描述性的測試名稱,以提高測試的可讀性和可維護性。", "is_new": true } ] \ No newline at end of file From 6c7278e996f49440c9c8cc90e23a32c2d2b71b82 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 01:12:32 +0000 Subject: [PATCH 085/174] fix: update askpass script to securely read token from env var --- app/git.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/git.js b/app/git.js index 143e848..fe11ac5 100644 --- a/app/git.js +++ b/app/git.js @@ -21,11 +21,12 @@ export async function commitAndPush(workspace, _spawnSync = spawnSync) { const remoteUrl = `${baseUrl}/${GITEA_REPOSITORY}.git`; const repoDir = path.join(workspace, 'repo'); - // Write a temporary askpass script so the token never appears in the URL or process list + // 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 "${GITEA_TOKEN}"\n`, { mode: 0o700 }); + fs.writeFileSync(askpassScript, '#!/bin/sh\necho "$GIT_TOKEN"\n', { mode: 0o700 }); - const credEnv = { ...process.env, GIT_ASKPASS: askpassScript, GIT_USERNAME: 'x-token' }; + const credEnv = { ...process.env, GIT_ASKPASS: askpassScript, GIT_USERNAME: 'x-token', GIT_TOKEN: GITEA_TOKEN }; try { if (!fs.existsSync(repoDir)) { From d327cf40d483540001c0f79a25dcee63a8e91fe7 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 01:13:53 +0000 Subject: [PATCH 086/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 121 ++++++--------------------------- 1 file changed, 22 insertions(+), 99 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index e4a78e8..399b9a7 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -2,134 +2,57 @@ { "level": "critical", "role": "Rex", - "location": "app/git.js:20", - "suggestion": "請避免在程式碼中硬編碼敏感資料,如 GITEA_TOKEN。應使用環境變數或安全的秘密管理工具來管理這些敏感資料。", + "location": "app/git.js:12", + "suggestion": "請避免將敏感資料(如 GITEA_TOKEN)直接寫入環境變數,應使用安全的秘密管理工具來管理這些敏感資訊。", "is_new": true }, { "level": "warning", "role": "Leo", - "location": "app/git.js:39", - "suggestion": "建議在函式 `makeRunner` 中增加對於 `spawn` 函式的參數檢查,以確保其為有效的函式,避免未來可能的錯誤。", + "location": "app/git.js:21", + "suggestion": "建議在函式開頭添加文件註解,說明函式的用途、參數及回傳值,以增強可讀性和可維護性。", "is_new": true }, { "level": "warning", - "role": "Zara", - "location": "app/git.js:39", - "suggestion": "在使用 git clone 時,建議使用 --single-branch 參數來避免下載不必要的分支,這樣可以節省時間和空間。", - "is_new": true - }, - { - "level": "warning", - "role": "Rex", - "location": "app/git.js:39", - "suggestion": "在使用 git 命令時,請確保適當處理錯誤,避免潛在的資訊洩漏。", + "role": "Leo", + "location": "app/git.js:21", + "suggestion": "建議將硬編碼的 'x-token' 和 'GIT_TOKEN' 提取為常數,並在程式碼中使用這些常數,以提高可維護性。", "is_new": true }, { "level": "warning", "role": "Aria", - "location": "app/git.js:3", - "suggestion": "檔案開頭應該有一行空白行,以提高可讀性。", + "location": "app/git.js:12", + "suggestion": "建議將註解中的「that reads the token from an env var」改為「從環境變數讀取令牌」,以提高可讀性。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": "app/git.js:14", + "suggestion": "建議將註解中的「the token value never appears in the script file itself」改為「令牌值不會出現在腳本文件中」,以提高可讀性。", "is_new": true }, { "level": "warning", "role": "Maya", - "location": "app/git.test.js:1", - "suggestion": "建議在測試檔案中加入更多的測試案例,以涵蓋不同的邊界條件和異常情況。", - "is_new": true - }, - { - "level": "info", - "role": "Leo", - "location": "app/git.js:43", - "suggestion": "考慮將 `askpassScript` 的寫入過程封裝成一個獨立的函式,以提高程式碼的模組化和可讀性。", - "is_new": true - }, - { - "level": "info", - "role": "Leo", - "location": "app/git.js:53", - "suggestion": "在 `catch` 區塊中,建議記錄更詳細的錯誤資訊,以便於未來的除錯和維護。", - "is_new": true - }, - { - "level": "info", - "role": "Leo", - "location": "app/git.js:58", - "suggestion": "在 `finally` 區塊中,建議增加對於 `fs.unlinkSync` 的錯誤處理,以避免在刪除檔案時發生未捕獲的錯誤。", - "is_new": true - }, - { - "level": "info", - "role": "Zara", - "location": "app/git.js:43", - "suggestion": "在寫入 askpass 腳本時,考慮使用 fs.promises.writeFile 來避免阻塞事件循環,提升效能。", - "is_new": true - }, - { - "level": "info", - "role": "Zara", - "location": "app/git.js:53", - "suggestion": "在使用 fs.mkdirSync 時,建議使用 fs.promises.mkdir 來避免阻塞,提升效能。", - "is_new": true - }, - { - "level": "info", - "role": "Zara", - "location": "app/git.js:65", - "suggestion": "在 git push 時,考慮使用 --quiet 參數來減少不必要的輸出,這樣可以提升效能。", - "is_new": true - }, - { - "level": "info", - "role": "Rex", - "location": "app/git.js:43", - "suggestion": "建議在使用完 askpass 腳本後,確保其被刪除,以減少潛在的安全風險。", + "location": "app/git.js:21", + "suggestion": "應該為 commitAndPush 函數撰寫單元測試,以確保其功能正確性和邊界條件處理。", "is_new": true }, { "level": "info", "role": "Aria", - "location": "app/git.js:39", - "suggestion": "考慮將 'run' 函數的命名改為更具描述性的名稱,例如 'executeGitCommand',以提高可讀性。", - "is_new": true - }, - { - "level": "info", - "role": "Aria", - "location": "app/git.js:43", - "suggestion": "在 'try' 區塊的結尾添加註解,說明 'finally' 區塊的目的,以提高可讀性。", - "is_new": true - }, - { - "level": "info", - "role": "Aria", - "location": "app/git.js:51", - "suggestion": "在 'catch' 區塊中,考慮使用更具描述性的錯誤訊息,以便於除錯。", - "is_new": true - }, - { - "level": "info", - "role": "Aria", - "location": "app/git.test.js:1", - "suggestion": "考慮在檔案開頭添加檔案描述註解,以提高可讀性。", - "is_new": true - }, - { - "level": "info", - "role": "Aria", - "location": "app/git.test.js:93", - "suggestion": "考慮在測試結束後添加註解,說明測試的目的,以提高可讀性。", + "location": "app/git.js:15", + "suggestion": "考慮將 GIT_TOKEN 的命名改為 GITEA_TOKEN,以保持一致性。", "is_new": true }, { "level": "info", "role": "Maya", - "location": "app/git.test.js:1", - "suggestion": "建議使用更具描述性的測試名稱,以提高測試的可讀性和可維護性。", + "location": "app/git.js:21", + "suggestion": "建議在測試中模擬環境變數,以避免在測試過程中暴露敏感資訊。", "is_new": true } ] \ No newline at end of file From 8ee9239edb03f0c3ccb2535c1d3da699c69d70f0 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 01:15:12 +0000 Subject: [PATCH 087/174] refactor: remove outdated AI Code configurations for Kilo, Roo, Cline, Continue, and Kade --- README.md | 110 ------------------------------------------------------ 1 file changed, 110 deletions(-) diff --git a/README.md b/README.md index e1c2026..86f8ab6 100644 --- a/README.md +++ b/README.md @@ -139,116 +139,6 @@ jobs: issues: write ``` -### 6. Kilo Code -```yaml -name: AI -on: - pull_request: - types: [opened, synchronize] -jobs: - code-review: - name: 'Code Review' - runs-on: ubuntu - steps: - - name: AI Code Review - uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} - with: - KILO_API_KEY: ${{ secrets.KILO_API_KEY }} - KILO_BASE_URL: https://api.kilocode.com/v1 - permissions: - contents: write - pull-requests: write - issues: write -``` - -### 7. Roo Code -```yaml -name: AI -on: - pull_request: - types: [opened, synchronize] -jobs: - code-review: - name: 'Code Review' - runs-on: ubuntu - steps: - - name: AI Code Review - uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} - with: - ROO_API_KEY: ${{ secrets.ROO_API_KEY }} - ROO_BASE_URL: https://api.roocode.com/v1 - permissions: - contents: write - pull-requests: write - issues: write -``` - -### 8. Cline -```yaml -name: AI -on: - pull_request: - types: [opened, synchronize] -jobs: - code-review: - name: 'Code Review' - runs-on: ubuntu - steps: - - name: AI Code Review - uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} - with: - CLINE_API_KEY: ${{ secrets.CLINE_API_KEY }} - CLINE_BASE_URL: https://api.cline.dev/v1 - permissions: - contents: write - pull-requests: write - issues: write -``` - -### 9. Continue -```yaml -name: AI -on: - pull_request: - types: [opened, synchronize] -jobs: - code-review: - name: 'Code Review' - runs-on: ubuntu - steps: - - name: AI Code Review - uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} - with: - CONTINUE_API_KEY: ${{ secrets.CONTINUE_API_KEY }} - CONTINUE_BASE_URL: https://api.continue.dev/v1 - permissions: - contents: write - pull-requests: write - issues: write -``` - -### 10. Kade -```yaml -name: AI -on: - pull_request: - types: [opened, synchronize] -jobs: - code-review: - name: 'Code Review' - runs-on: ubuntu - steps: - - name: AI Code Review - uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} - with: - KADE_API_KEY: ${{ secrets.KADE_API_KEY }} - KADE_BASE_URL: https://api.kade.dev/v1 - permissions: - contents: write - pull-requests: write - issues: write -``` - ### - Ollama ```yaml From 6a526294b9d6b71b1e745d06cadbdcbd1e86c175 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 01:21:01 +0000 Subject: [PATCH 088/174] refactor: update processing steps in README for clarity and accuracy --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 86f8ab6..11002fc 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,12 @@ 1. 服務名稱、模型名稱、角色資訊(個性、符合個性的英文名稱、工作內容),Comment 到 Push Request 2. 每個角色個別分析 Git Diff 的內容產生新問題表格(問題等級、角色名稱、問題位置或行數、修改建議) 3. 讀取所有未解決的舊問題(問題檔案存在於使用此 Action 的專案固定位置)加上新問題後,去除重複產生本次 Push Request 的問題表格(PR問題表格)覆蓋問題檔案 -4. 從PR問題表格中取出所有舊問題,依照等級排序後 Comment 到 Push Request -5. 從PR問題表格中取出所有新問題,排除嚴重等級的問題後 Comment 到 Push Request -6. 從PR問題表格中取出所有新問題,將每個嚴重等級的問題 Comment 到 Push Request -7. Commit 問題檔案 -8. 如果PR問題表格中有嚴重問題,則不要讓 workflow 執行成功(exit 1) +4. 讀取排除問題檔案,用來過濾PR問題表格中不需要處理的問題 +5. 從PR問題表格中取出所有舊問題,依照等級排序後 Comment 到 Push Request +6. 從PR問題表格中取出所有新問題,排除嚴重等級的問題後 Comment 到 Push Request +7. 從PR問題表格中取出所有新問題,將每個嚴重等級的問題 Comment 到 Push Request +8. Commit 問題檔案 +9. 如果PR問題表格中有嚴重問題,則不要讓 workflow 執行成功(exit 1) # 設計 From 2460652b49536f2e0425d528f98bf3344a4b81a9 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 01:27:48 +0000 Subject: [PATCH 089/174] refactor: reorganize TODO stages for clarity and accuracy in workflow steps Co-authored-by: Copilot --- TODO.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/TODO.md b/TODO.md index 9fe2599..bb6edcd 100644 --- a/TODO.md +++ b/TODO.md @@ -13,15 +13,19 @@ - 目標:嘗試呼叫 LLM 進行 findings 去重與角色確認,API 額度不足時要有降級處理 log。 - 驗收:log 中能看到 deduplication/resolution confirmation 成功或失敗(如 402),降級時有「保留所有問題」等明確訊息。 -## 階段四:findings 寫入與 comment 發布 +## 階段四:排除問題過濾 +- 目標:讀取排除問題檔案,過濾 PR 問題表格中不需要處理的問題。 +- 驗收:log 中能看到排除問題檔案讀取成功或不存在的訊息,以及過濾後 findings 數量變化。 + +## 階段五:findings 寫入與 comment 發布 - 目標:findings.jsonl 正確寫入,comment 發布順序正確(舊問題→非嚴重→嚴重),每步有 log。 - 驗收:log 中能看到 findings 寫入、comment sync 的詳細訊息與順序。 -## 階段五:記憶區 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 狀態為失敗。 From bb7fa425db9512daf9a4f9e3371eff2914b79c99 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 01:35:57 +0000 Subject: [PATCH 090/174] 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)); From eba21ade277711e679009ca27ba60314027e3f6f Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 01:39:43 +0000 Subject: [PATCH 091/174] docs: mark all TODO stages complete --- TODO.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/TODO.md b/TODO.md index a6fd2e7..2da04ea 100644 --- a/TODO.md +++ b/TODO.md @@ -23,17 +23,18 @@ ## 階段五:findings 寫入與 comment 發布 - 目標:findings.jsonl 正確寫入,comment 發布順序正確(舊問題→非嚴重→嚴重),每步有 log。 - 驗收:log 中能看到 findings 寫入、comment sync 的詳細訊息與順序。 +- 完成 ## 階段六:記憶區 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 狀態為失敗。 +- 完成 --- -每個階段都會加上明確的 log,並確保即使部分功能未完成也能降級執行、不會中斷 pipeline。 - -每次執行後請貼 log,我會協助 debug。 +所有階段驗收通過。 From 58bea7951dddecb0eb85335c7ce2a88fe105b1be Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 01:41:14 +0000 Subject: [PATCH 092/174] chore: add exclusions for Rex false positive on git.js token handling --- .gitea/ai-review/exclusions.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .gitea/ai-review/exclusions.json diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json new file mode 100644 index 0000000..3ae3cda --- /dev/null +++ b/.gitea/ai-review/exclusions.json @@ -0,0 +1,7 @@ +[ + { + "role": "Rex", + "location": "app/git.js", + "suggestion": "請避免將敏感資料(如 GITEA_TOKEN)直接寫入環境變數" + } +] From b9a6bebbe4b7c694b8082aaaab84434f37a47aec Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 01:49:18 +0000 Subject: [PATCH 093/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 148 ++++++++++++++++++++++++++------- 1 file changed, 116 insertions(+), 32 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 399b9a7..705c310 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,58 +1,142 @@ [ { - "level": "critical", + "level": "warning", + "role": "Leo", + "location": "app/findings.js:93", + "suggestion": "建議在 loadExclusions 函式中增加對於 JSON 格式的驗證,確保讀取的資料符合預期格式,避免潛在的錯誤。", + "is_new": true + }, + { + "level": "warning", + "role": "Leo", + "location": "app/findings.js:40", + "suggestion": "在 applyExclusions 函式中,建議增加對於 findings 和 exclusions 參數的有效性檢查,以提高程式的健壯性。", + "is_new": true + }, + { + "level": "warning", + "role": "Zara", + "location": "app/findings.js:40", + "suggestion": "在 applyExclusions 函數中,使用 Array.prototype.some 進行過濾時,可能會導致性能問題,特別是當 findings 和 exclusions 的數量都很大時。建議使用更高效的資料結構(如 HashSet)來加速查詢。", + "is_new": true + }, + { + "level": "warning", "role": "Rex", - "location": "app/git.js:12", - "suggestion": "請避免將敏感資料(如 GITEA_TOKEN)直接寫入環境變數,應使用安全的秘密管理工具來管理這些敏感資訊。", - "is_new": true - }, - { - "level": "warning", - "role": "Leo", - "location": "app/git.js:21", - "suggestion": "建議在函式開頭添加文件註解,說明函式的用途、參數及回傳值,以增強可讀性和可維護性。", - "is_new": true - }, - { - "level": "warning", - "role": "Leo", - "location": "app/git.js:21", - "suggestion": "建議將硬編碼的 'x-token' 和 'GIT_TOKEN' 提取為常數,並在程式碼中使用這些常數,以提高可維護性。", + "location": ".gitea/ai-review/exclusions.json", + "suggestion": "請避免將敏感資料(如 GITEA_TOKEN)直接寫入環境變數", "is_new": true }, { "level": "warning", "role": "Aria", - "location": "app/git.js:12", - "suggestion": "建議將註解中的「that reads the token from an env var」改為「從環境變數讀取令牌」,以提高可讀性。", - "is_new": true - }, - { - "level": "warning", - "role": "Aria", - "location": "app/git.js:14", - "suggestion": "建議將註解中的「the token value never appears in the script file itself」改為「令牌值不會出現在腳本文件中」,以提高可讀性。", + "location": "README.md:4", + "suggestion": "建議將「讀取排除問題檔案,用來過濾PR問題表格中不需要處理的問題」的描述改為「讀取排除問題檔案,過濾 PR 問題表格中不需要處理的問題」,以保持一致性。", "is_new": true }, { "level": "warning", "role": "Maya", - "location": "app/git.js:21", - "suggestion": "應該為 commitAndPush 函數撰寫單元測試,以確保其功能正確性和邊界條件處理。", + "location": "app/findings.js:40", + "suggestion": "建議在 applyExclusions 函數中增加對 findings 內容的驗證,確保其格式正確,以提高測試的穩定性和可靠性。", + "is_new": true + }, + { + "level": "info", + "role": "Leo", + "location": "README.md", + "suggestion": "建議在 README 中增加對於新功能(如排除問題過濾)的詳細說明,以便未來的維護者能快速了解其功能。", + "is_new": true + }, + { + "level": "info", + "role": "Leo", + "location": "app/main.js", + "suggestion": "建議在 main 函式中增加對於每個步驟的詳細註解,讓未來的維護者能更容易理解程式邏輯。", + "is_new": true + }, + { + "level": "info", + "role": "Zara", + "location": "app/findings.js:39", + "suggestion": "在過濾 findings 時,建議將過濾條件的邏輯提取為獨立函數,以提高可讀性和可維護性。", + "is_new": true + }, + { + "level": "info", + "role": "Zara", + "location": "app/main.js:64", + "suggestion": "在讀取排除問題檔案時,建議考慮使用非同步方法(如 fs.promises.readFile)來避免阻塞事件循環,提升效能。", "is_new": true }, { "level": "info", "role": "Aria", - "location": "app/git.js:15", - "suggestion": "考慮將 GIT_TOKEN 的命名改為 GITEA_TOKEN,以保持一致性。", + "location": "README.md:8", + "suggestion": "建議將「如果PR問題表格中有嚴重問題,則不要讓 workflow 執行成功(exit 1)」的描述改為「如果 PR 問題表格中有嚴重問題,則不要讓 workflow 執行成功(exit 1)」以提高可讀性。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "TODO.md:4", + "suggestion": "建議將「階段四:findings 寫入與 comment 發布」的標題改為「階段四:排除問題過濾」,以更清楚地反映內容。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "TODO.md:6", + "suggestion": "建議將「階段五:findings 寫入與 comment 發布」的標題改為「階段五:findings 寫入與 comment 發布」,以保持一致性。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "TODO.md:8", + "suggestion": "建議將「階段六:記憶區 commit/push 與錯誤處理」的標題改為「階段六:記憶區 commit/push 與錯誤處理」,以保持一致性。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "TODO.md:10", + "suggestion": "建議將「階段七:阻擋嚴重問題 PR(第 8 點)」的標題改為「階段七:阻擋嚴重問題 PR(第 8 點)」以保持一致性。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "app/config.js", + "suggestion": "建議在 EXCLUSIONS_PATH 的定義上方添加註解,說明該常數的用途,以提高可讀性。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "app/findings.js", + "suggestion": "建議在 loadExclusions 函數的開頭添加註解,說明該函數的用途,以提高可讀性。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "app/findings.js", + "suggestion": "建議在 applyExclusions 函數的開頭添加註解,說明該函數的用途,以提高可讀性。", "is_new": true }, { "level": "info", "role": "Maya", - "location": "app/git.js:21", - "suggestion": "建議在測試中模擬環境變數,以避免在測試過程中暴露敏感資訊。", + "location": "app/findings.js:7", + "suggestion": "建議為 loadExclusions 和 applyExclusions 函數撰寫單元測試,以確保其功能正確並能處理邊界條件。", + "is_new": true + }, + { + "level": "info", + "role": "Maya", + "location": "app/main.js:48", + "suggestion": "建議在每個主要步驟之後增加測試用例,以驗證每個步驟的輸出是否符合預期。", "is_new": true } ] \ No newline at end of file From 80f56b74e5f0cf7f2ff50419cee48457010b4097 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 02:06:24 +0000 Subject: [PATCH 094/174] fix: clone repo before Step3/4 to read findings and exclusions from head branch --- app/git.js | 28 ++++++++++++++++++++++++++++ app/main.js | 13 ++++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/app/git.js b/app/git.js index fe11ac5..5006d88 100644 --- a/app/git.js +++ b/app/git.js @@ -14,6 +14,34 @@ function makeRunner(spawn) { }; } +/** + * 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 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 { + if (!fs.existsSync(repoDir)) { + run(['clone', '--depth=1', '--branch', PR_HEAD_BRANCH, remoteUrl, repoDir], workspace, credEnv); + console.log(` ✅ repo cloned to ${repoDir}`); + } else { + run(['fetch', 'origin', PR_HEAD_BRANCH], repoDir, credEnv); + run(['checkout', PR_HEAD_BRANCH], repoDir); + console.log(` ✅ repo already exists, fetched latest`); + } + } finally { + try { fs.unlinkSync(askpassScript); } catch {} + } + return repoDir; +} + export async function commitAndPush(workspace, _spawnSync = spawnSync) { const run = makeRunner(_spawnSync); diff --git a/app/main.js b/app/main.js index ef0b173..b3bd670 100644 --- a/app/main.js +++ b/app/main.js @@ -3,7 +3,7 @@ import { loadRoles, getRoleIntro } from './roles.js'; import { getPRDiff, postComment } from './gitea.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'; +import { cloneRepo, commitAndPush } from './git.js'; const WORKSPACE = process.env.GITHUB_WORKSPACE || '/workspace'; @@ -65,7 +65,14 @@ async function main() { // Step3: 讀取舊 findings,合併去重(含 AI 語意去重) console.log('\n🔀 Step3: Findings 合併'); - const oldFindings = loadOldFindings(WORKSPACE); + // Clone repo 以讀取舊 findings 與排除清單 + let repoDir; + try { + repoDir = cloneRepo(WORKSPACE); + } catch (e) { + console.log(` ⚠️ clone repo 失敗(繼續執行): ${e.message}`); + } + const oldFindings = loadOldFindings(repoDir || WORKSPACE); const mergedFindings = mergeFindings(oldFindings, newFindings); console.log(` Step3 merged findings total=${mergedFindings.length}`); @@ -76,7 +83,7 @@ async function main() { // Step4: 讀取排除問題檔案,過濾 PR 問題表格 console.log('\n🚫 Step4: 排除問題過濾'); - const exclusions = loadExclusions(WORKSPACE); + const exclusions = loadExclusions(repoDir || WORKSPACE); const filtered = applyExclusions(sorted, exclusions); console.log(` Step4 完成: findings total=${filtered.length}`); From 433b59516577e267b74cadcafc1687c195c8fedc Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 02:07:34 +0000 Subject: [PATCH 095/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 59 +++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 705c310..a48caee 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,142 +1,149 @@ [ + { + "level": "critical", + "role": "Rex", + "location": "app/git.js:14", + "suggestion": "請避免將敏感資料(如 GITEA_TOKEN)直接寫入程式碼,應使用安全的秘密管理工具來管理這些敏感資訊。", + "is_new": true + }, + { + "level": "warning", + "role": "Rex", + "location": "app/git.js:14", + "suggestion": "在 cloneRepo 函數中,請確保 GIT_TOKEN 不會被寫入到檔案系統中,避免敏感資訊洩漏。", + "is_new": true + }, { "level": "warning", "role": "Leo", "location": "app/findings.js:93", "suggestion": "建議在 loadExclusions 函式中增加對於 JSON 格式的驗證,確保讀取的資料符合預期格式,避免潛在的錯誤。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Leo", "location": "app/findings.js:40", "suggestion": "在 applyExclusions 函式中,建議增加對於 findings 和 exclusions 參數的有效性檢查,以提高程式的健壯性。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Zara", "location": "app/findings.js:40", "suggestion": "在 applyExclusions 函數中,使用 Array.prototype.some 進行過濾時,可能會導致性能問題,特別是當 findings 和 exclusions 的數量都很大時。建議使用更高效的資料結構(如 HashSet)來加速查詢。", - "is_new": true - }, - { - "level": "warning", - "role": "Rex", - "location": ".gitea/ai-review/exclusions.json", - "suggestion": "請避免將敏感資料(如 GITEA_TOKEN)直接寫入環境變數", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Aria", "location": "README.md:4", "suggestion": "建議將「讀取排除問題檔案,用來過濾PR問題表格中不需要處理的問題」的描述改為「讀取排除問題檔案,過濾 PR 問題表格中不需要處理的問題」,以保持一致性。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Maya", "location": "app/findings.js:40", "suggestion": "建議在 applyExclusions 函數中增加對 findings 內容的驗證,確保其格式正確,以提高測試的穩定性和可靠性。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Leo", "location": "README.md", "suggestion": "建議在 README 中增加對於新功能(如排除問題過濾)的詳細說明,以便未來的維護者能快速了解其功能。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Leo", "location": "app/main.js", "suggestion": "建議在 main 函式中增加對於每個步驟的詳細註解,讓未來的維護者能更容易理解程式邏輯。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Zara", "location": "app/findings.js:39", "suggestion": "在過濾 findings 時,建議將過濾條件的邏輯提取為獨立函數,以提高可讀性和可維護性。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Zara", "location": "app/main.js:64", "suggestion": "在讀取排除問題檔案時,建議考慮使用非同步方法(如 fs.promises.readFile)來避免阻塞事件循環,提升效能。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Aria", "location": "README.md:8", "suggestion": "建議將「如果PR問題表格中有嚴重問題,則不要讓 workflow 執行成功(exit 1)」的描述改為「如果 PR 問題表格中有嚴重問題,則不要讓 workflow 執行成功(exit 1)」以提高可讀性。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Aria", "location": "TODO.md:4", "suggestion": "建議將「階段四:findings 寫入與 comment 發布」的標題改為「階段四:排除問題過濾」,以更清楚地反映內容。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Aria", "location": "TODO.md:6", "suggestion": "建議將「階段五:findings 寫入與 comment 發布」的標題改為「階段五:findings 寫入與 comment 發布」,以保持一致性。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Aria", "location": "TODO.md:8", "suggestion": "建議將「階段六:記憶區 commit/push 與錯誤處理」的標題改為「階段六:記憶區 commit/push 與錯誤處理」,以保持一致性。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Aria", "location": "TODO.md:10", "suggestion": "建議將「階段七:阻擋嚴重問題 PR(第 8 點)」的標題改為「階段七:阻擋嚴重問題 PR(第 8 點)」以保持一致性。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Aria", "location": "app/config.js", "suggestion": "建議在 EXCLUSIONS_PATH 的定義上方添加註解,說明該常數的用途,以提高可讀性。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Aria", "location": "app/findings.js", "suggestion": "建議在 loadExclusions 函數的開頭添加註解,說明該函數的用途,以提高可讀性。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Aria", "location": "app/findings.js", "suggestion": "建議在 applyExclusions 函數的開頭添加註解,說明該函數的用途,以提高可讀性。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Maya", "location": "app/findings.js:7", "suggestion": "建議為 loadExclusions 和 applyExclusions 函數撰寫單元測試,以確保其功能正確並能處理邊界條件。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Maya", "location": "app/main.js:48", "suggestion": "建議在每個主要步驟之後增加測試用例,以驗證每個步驟的輸出是否符合預期。", - "is_new": true + "is_new": false } ] \ No newline at end of file From 78c085414576e278dea332556e04bbabe4537b9a Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 02:10:47 +0000 Subject: [PATCH 096/174] fix: use includes matching for exclusions location and suggestion --- app/findings.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/findings.js b/app/findings.js index cb65dbd..118b9e4 100644 --- a/app/findings.js +++ b/app/findings.js @@ -124,8 +124,8 @@ export function applyExclusions(findings, exclusions) { 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))) + (!ex.location || String(f.location).includes(ex.location)) && + (!ex.suggestion || String(f.suggestion).includes(String(ex.suggestion).slice(0, 20))) )); console.log(` 排除過濾: ${before} -> ${filtered.length} 筆(排除 ${before - filtered.length} 筆)`); return filtered; From a0e69b4e826e9327020ca7bbedd3cae9f958d20b Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 02:12:26 +0000 Subject: [PATCH 097/174] 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 From 7b8d71cf87aa68b02045ca8fd0b46a948aa80a73 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 02:13:38 +0000 Subject: [PATCH 098/174] docs: update TODO stage4 description and fix findings filename typo --- TODO.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TODO.md b/TODO.md index 2da04ea..285fdb4 100644 --- a/TODO.md +++ b/TODO.md @@ -16,13 +16,13 @@ - 完成 ## 階段四:排除問題過濾 -- 目標:讀取排除問題檔案,過濾 PR 問題表格中不需要處理的問題。 -- 驗收:log 中能看到排除問題檔案讀取成功或不存在的訊息,以及過濾後 findings 數量變化。 +- 目標:讀取排除問題檔案(exclusions.json)進行規則過濾,並呼叫 AI 判斷剩餘問題是否為誤報或不適用,兩層過濾後產生最終問題清單。 +- 驗收:log 中能看到排除問題檔案讀取成功或不存在的訊息、規則過濾數量變化,以及「AI 誤報過濾: N -> M 筆」或降級訊息。 - 完成 ## 階段五:findings 寫入與 comment 發布 - 目標:findings.jsonl 正確寫入,comment 發布順序正確(舊問題→非嚴重→嚴重),每步有 log。 -- 驗收:log 中能看到 findings 寫入、comment sync 的詳細訊息與順序。 +- 驗收:log 中能看到 findings.json 寫入、comment sync 的詳細訊息與順序。 - 完成 ## 階段六:記憶區 commit/push 與錯誤處理 From 93aa6864f597fe990ee436bb79b5b11ce25e4ac8 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 02:14:49 +0000 Subject: [PATCH 099/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 37 +--------------------------------- 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index a48caee..3fad226 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,17 +1,10 @@ [ - { - "level": "critical", - "role": "Rex", - "location": "app/git.js:14", - "suggestion": "請避免將敏感資料(如 GITEA_TOKEN)直接寫入程式碼,應使用安全的秘密管理工具來管理這些敏感資訊。", - "is_new": true - }, { "level": "warning", "role": "Rex", "location": "app/git.js:14", "suggestion": "在 cloneRepo 函數中,請確保 GIT_TOKEN 不會被寫入到檔案系統中,避免敏感資訊洩漏。", - "is_new": true + "is_new": false }, { "level": "warning", @@ -34,13 +27,6 @@ "suggestion": "在 applyExclusions 函數中,使用 Array.prototype.some 進行過濾時,可能會導致性能問題,特別是當 findings 和 exclusions 的數量都很大時。建議使用更高效的資料結構(如 HashSet)來加速查詢。", "is_new": false }, - { - "level": "warning", - "role": "Aria", - "location": "README.md:4", - "suggestion": "建議將「讀取排除問題檔案,用來過濾PR問題表格中不需要處理的問題」的描述改為「讀取排除問題檔案,過濾 PR 問題表格中不需要處理的問題」,以保持一致性。", - "is_new": false - }, { "level": "warning", "role": "Maya", @@ -90,27 +76,6 @@ "suggestion": "建議將「階段四:findings 寫入與 comment 發布」的標題改為「階段四:排除問題過濾」,以更清楚地反映內容。", "is_new": false }, - { - "level": "info", - "role": "Aria", - "location": "TODO.md:6", - "suggestion": "建議將「階段五:findings 寫入與 comment 發布」的標題改為「階段五:findings 寫入與 comment 發布」,以保持一致性。", - "is_new": false - }, - { - "level": "info", - "role": "Aria", - "location": "TODO.md:8", - "suggestion": "建議將「階段六:記憶區 commit/push 與錯誤處理」的標題改為「階段六:記憶區 commit/push 與錯誤處理」,以保持一致性。", - "is_new": false - }, - { - "level": "info", - "role": "Aria", - "location": "TODO.md:10", - "suggestion": "建議將「階段七:阻擋嚴重問題 PR(第 8 點)」的標題改為「階段七:阻擋嚴重問題 PR(第 8 點)」以保持一致性。", - "is_new": false - }, { "level": "info", "role": "Aria", From 0825f8ebbeca150d5ce1545504470f23c45052b9 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 02:15:47 +0000 Subject: [PATCH 100/174] =?UTF-8?q?refactor:=20rename=20Step4=20to=20AI=20?= =?UTF-8?q?=E6=8E=92=E9=99=A4=E5=95=8F=E9=A1=8C=E9=81=8E=E6=BF=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 2 +- app/main.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index 285fdb4..e5115dc 100644 --- a/TODO.md +++ b/TODO.md @@ -15,7 +15,7 @@ - 驗收:log 中能看到 deduplication/resolution confirmation 成功或失敗(如 402),降級時有「保留所有問題」等明確訊息。 - 完成 -## 階段四:排除問題過濾 +## 階段四:AI 排除問題過濾 - 目標:讀取排除問題檔案(exclusions.json)進行規則過濾,並呼叫 AI 判斷剩餘問題是否為誤報或不適用,兩層過濾後產生最終問題清單。 - 驗收:log 中能看到排除問題檔案讀取成功或不存在的訊息、規則過濾數量變化,以及「AI 誤報過濾: N -> M 筆」或降級訊息。 - 完成 diff --git a/app/main.js b/app/main.js index de40363..7d0eef9 100644 --- a/app/main.js +++ b/app/main.js @@ -82,7 +82,7 @@ async function main() { 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 問題表格,並請 AI 判斷誤報 - console.log('\n🚫 Step4: 排除問題過濾'); + console.log('\n🚫 Step4: AI 排除問題過濾'); const exclusions = loadExclusions(repoDir || WORKSPACE); const ruleFiltered = applyExclusions(sorted, exclusions); const filtered = await filterFalsePositivesWithAI(ruleFiltered); From d631c25f376e01990b7353983ff556a6850e12f8 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 02:16:42 +0000 Subject: [PATCH 101/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 105 +++++---------------------------- 1 file changed, 14 insertions(+), 91 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 3fad226..0f41675 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,114 +1,37 @@ [ { - "level": "warning", + "level": "critical", "role": "Rex", "location": "app/git.js:14", - "suggestion": "在 cloneRepo 函數中,請確保 GIT_TOKEN 不會被寫入到檔案系統中,避免敏感資訊洩漏。", - "is_new": false + "suggestion": "請避免將 GIT_TOKEN 直接寫入腳本中,應使用安全的秘密管理工具來管理這些敏感資訊.", + "is_new": true + }, + { + "level": "warning", + "role": "Leo", + "location": "app/git.js:14", + "suggestion": "建議在 cloneRepo 函數中增加對於 GIT_TOKEN 的安全性處理,避免敏感資訊洩漏.", + "is_new": true }, { "level": "warning", "role": "Leo", "location": "app/findings.js:93", - "suggestion": "建議在 loadExclusions 函式中增加對於 JSON 格式的驗證,確保讀取的資料符合預期格式,避免潛在的錯誤。", + "suggestion": "建議在 loadExclusions 函式中增加對於 JSON 格式的驗證,確保讀取的資料符合預期格式,避免潛在的錯誤.", "is_new": false }, { "level": "warning", "role": "Leo", "location": "app/findings.js:40", - "suggestion": "在 applyExclusions 函式中,建議增加對於 findings 和 exclusions 參數的有效性檢查,以提高程式的健壯性。", - "is_new": false - }, - { - "level": "warning", - "role": "Zara", - "location": "app/findings.js:40", - "suggestion": "在 applyExclusions 函數中,使用 Array.prototype.some 進行過濾時,可能會導致性能問題,特別是當 findings 和 exclusions 的數量都很大時。建議使用更高效的資料結構(如 HashSet)來加速查詢。", - "is_new": false - }, - { - "level": "warning", - "role": "Maya", - "location": "app/findings.js:40", - "suggestion": "建議在 applyExclusions 函數中增加對 findings 內容的驗證,確保其格式正確,以提高測試的穩定性和可靠性。", + "suggestion": "在 applyExclusions 函式中,建議增加對於 findings 和 exclusions 參數的有效性檢查,以提高程式的健壯性.", "is_new": false }, { "level": "info", "role": "Leo", "location": "README.md", - "suggestion": "建議在 README 中增加對於新功能(如排除問題過濾)的詳細說明,以便未來的維護者能快速了解其功能。", - "is_new": false - }, - { - "level": "info", - "role": "Leo", - "location": "app/main.js", - "suggestion": "建議在 main 函式中增加對於每個步驟的詳細註解,讓未來的維護者能更容易理解程式邏輯。", - "is_new": false - }, - { - "level": "info", - "role": "Zara", - "location": "app/findings.js:39", - "suggestion": "在過濾 findings 時,建議將過濾條件的邏輯提取為獨立函數,以提高可讀性和可維護性。", - "is_new": false - }, - { - "level": "info", - "role": "Zara", - "location": "app/main.js:64", - "suggestion": "在讀取排除問題檔案時,建議考慮使用非同步方法(如 fs.promises.readFile)來避免阻塞事件循環,提升效能。", - "is_new": false - }, - { - "level": "info", - "role": "Aria", - "location": "README.md:8", - "suggestion": "建議將「如果PR問題表格中有嚴重問題,則不要讓 workflow 執行成功(exit 1)」的描述改為「如果 PR 問題表格中有嚴重問題,則不要讓 workflow 執行成功(exit 1)」以提高可讀性。", - "is_new": false - }, - { - "level": "info", - "role": "Aria", - "location": "TODO.md:4", - "suggestion": "建議將「階段四:findings 寫入與 comment 發布」的標題改為「階段四:排除問題過濾」,以更清楚地反映內容。", - "is_new": false - }, - { - "level": "info", - "role": "Aria", - "location": "app/config.js", - "suggestion": "建議在 EXCLUSIONS_PATH 的定義上方添加註解,說明該常數的用途,以提高可讀性。", - "is_new": false - }, - { - "level": "info", - "role": "Aria", - "location": "app/findings.js", - "suggestion": "建議在 loadExclusions 函數的開頭添加註解,說明該函數的用途,以提高可讀性。", - "is_new": false - }, - { - "level": "info", - "role": "Aria", - "location": "app/findings.js", - "suggestion": "建議在 applyExclusions 函數的開頭添加註解,說明該函數的用途,以提高可讀性。", - "is_new": false - }, - { - "level": "info", - "role": "Maya", - "location": "app/findings.js:7", - "suggestion": "建議為 loadExclusions 和 applyExclusions 函數撰寫單元測試,以確保其功能正確並能處理邊界條件。", - "is_new": false - }, - { - "level": "info", - "role": "Maya", - "location": "app/main.js:48", - "suggestion": "建議在每個主要步驟之後增加測試用例,以驗證每個步驟的輸出是否符合預期。", - "is_new": false + "suggestion": "建議在 README 中增加對於新功能(如排除問題過濾)的詳細說明,以便未來的維護者能快速了解其功能.", + "is_new": true } ] \ No newline at end of file From dafadcd6b253548880edc6f19bac3323c3cdb813 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 02:18:22 +0000 Subject: [PATCH 102/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 87 +++++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 0f41675..d99a124 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,37 +1,100 @@ [ { "level": "critical", - "role": "Rex", + "role": "Leo", "location": "app/git.js:14", - "suggestion": "請避免將 GIT_TOKEN 直接寫入腳本中,應使用安全的秘密管理工具來管理這些敏感資訊.", + "suggestion": "GITEA_TOKEN 直接嵌入 URL 中,可能導致憑證洩漏。建議使用環境變數或安全的憑證管理方式來處理敏感資訊。", + "is_new": true + }, + { + "level": "critical", + "role": "Zara", + "location": "app/git.js:14", + "suggestion": "GITEA_TOKEN 直接嵌入 URL 中,可能導致憑證洩漏。建議使用環境變數或安全的憑證管理方式來處理敏感資訊。", + "is_new": true + }, + { + "level": "critical", + "role": "Maya", + "location": "app/git.js:1", + "suggestion": "缺少對 cloneRepo 函數的單元測試,應該為其添加測試以確保其正確性。", + "is_new": true + }, + { + "level": "critical", + "role": "Maya", + "location": "app/git.js:1", + "suggestion": "缺少對 commitAndPush 函數的單元測試,應該為其添加測試以確保其正確性。", "is_new": true }, { "level": "warning", "role": "Leo", - "location": "app/git.js:14", - "suggestion": "建議在 cloneRepo 函數中增加對於 GIT_TOKEN 的安全性處理,避免敏感資訊洩漏.", + "location": "app/git.js:25", + "suggestion": "在使用 fs.existsSync 檢查目錄是否存在時,應考慮使用非同步方法以避免阻塞事件循環。", "is_new": true }, { "level": "warning", "role": "Leo", - "location": "app/findings.js:93", - "suggestion": "建議在 loadExclusions 函式中增加對於 JSON 格式的驗證,確保讀取的資料符合預期格式,避免潛在的錯誤.", - "is_new": false + "location": "app/git.js:29", + "suggestion": "在 git clone 時使用 --depth=1 可能會導致未來需要完整歷史紀錄時的性能問題,建議根據實際需求調整。", + "is_new": true }, { "level": "warning", - "role": "Leo", - "location": "app/findings.js:40", - "suggestion": "在 applyExclusions 函式中,建議增加對於 findings 和 exclusions 參數的有效性檢查,以提高程式的健壯性.", - "is_new": false + "role": "Maya", + "location": "app/findings.js:1", + "suggestion": "loadExclusions 函數中對於 JSON 格式的驗證不足,建議增加對於資料結構的檢查,以避免潛在的錯誤。", + "is_new": true + }, + { + "level": "warning", + "role": "Maya", + "location": "app/findings.js:1", + "suggestion": "applyExclusions 函數中對於 findings 和 exclusions 參數的有效性檢查不足,建議增加檢查以提高程式的健壯性。", + "is_new": true }, { "level": "info", - "role": "Leo", + "role": "Aria", "location": "README.md", "suggestion": "建議在 README 中增加對於新功能(如排除問題過濾)的詳細說明,以便未來的維護者能快速了解其功能.", "is_new": true + }, + { + "level": "info", + "role": "Rex", + "location": "app/findings.js:93", + "suggestion": "建議在 loadExclusions 函式中增加對於 JSON 格式的驗證,確保讀取的資料符合預期格式,避免潛在的錯誤。", + "is_new": true + }, + { + "level": "info", + "role": "Rex", + "location": "app/findings.js:40", + "suggestion": "在 applyExclusions 函式中,建議增加對於 findings 和 exclusions 參數的有效性檢查,以提高程式的健壯性。", + "is_new": true + }, + { + "level": "info", + "role": "Zara", + "location": "app/findings.js:93", + "suggestion": "建議在 loadExclusions 函式中增加對於 JSON 格式的驗證,確保讀取的資料符合預期格式,避免潛在的錯誤。", + "is_new": true + }, + { + "level": "info", + "role": "Zara", + "location": "app/findings.js:40", + "suggestion": "在 applyExclusions 函式中,建議增加對於 findings 和 exclusions 參數的有效性檢查,以提高程式的健壯性。", + "is_new": true + }, + { + "level": "info", + "role": "Maya", + "location": "app/main.js:1", + "suggestion": "建議為主要流程中的每個步驟添加詳細的單元測試,以確保整體功能的正確性和穩定性。", + "is_new": true } ] \ No newline at end of file From d230b5f4458f3ae6c6e0a7e3a6755f49dc74b0b1 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 02:21:25 +0000 Subject: [PATCH 103/174] fix: add Leo/Zara false positive exclusion; add cloneRepo unit tests --- .gitea/ai-review/exclusions.json | 4 +++ app/git.test.js | 58 +++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 3ae3cda..2ca1f18 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -3,5 +3,9 @@ "role": "Rex", "location": "app/git.js", "suggestion": "請避免將敏感資料(如 GITEA_TOKEN)直接寫入環境變數" + }, + { + "location": "app/git.js", + "suggestion": "GITEA_TOKEN 直接嵌入 URL 中" } ] diff --git a/app/git.test.js b/app/git.test.js index d96efce..0e7e85b 100644 --- a/app/git.test.js +++ b/app/git.test.js @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import fs from 'fs'; import os from 'os'; import path from 'path'; -import { commitAndPush } from './git.js'; +import { commitAndPush, cloneRepo } from './git.js'; // --- helpers --- function makeTmpWorkspace() { @@ -91,3 +91,59 @@ describe('commitAndPush', () => { await assert.doesNotReject(() => commitAndPush(workspace, failSpawn)); }); }); + +describe('cloneRepo', () => { + let workspace; + + before(() => { workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'clone-test-')); }); + after(() => { fs.rmSync(workspace, { recursive: true, force: true }); }); + + it('clones repo when repoDir does not exist', () => { + const spawn = makeSpawn(); + cloneRepo(workspace, spawn); + const cloneCalled = spawn.calls.some(c => c.args[0] === 'clone'); + assert.ok(cloneCalled, 'expected git clone to be called'); + }); + + it('fetches and checks out when repoDir already exists', () => { + const repoDir = path.join(workspace, 'repo'); + fs.mkdirSync(repoDir, { recursive: true }); + const spawn = makeSpawn(); + cloneRepo(workspace, spawn); + const cloneCalled = spawn.calls.some(c => c.args[0] === 'clone'); + const fetchCalled = spawn.calls.some(c => c.args[0] === 'fetch'); + assert.ok(!cloneCalled, 'clone should not run when repoDir exists'); + assert.ok(fetchCalled, 'fetch should run when repoDir exists'); + }); + + it('does not embed token in any git command argument', () => { + const spawn = makeSpawn(); + cloneRepo(workspace, spawn); + for (const { args } of spawn.calls) { + assert.ok(!args.join(' ').includes('test-token'), `Token leaked in git args: ${args.join(' ')}`); + } + }); + + it('uses GIT_ASKPASS for network operations', () => { + const spawn = makeSpawn(); + cloneRepo(workspace, spawn); + const networkCalls = spawn.calls.filter(c => ['clone', 'fetch'].includes(c.args[0])); + assert.ok(networkCalls.length > 0, 'expected at least one network git call'); + for (const { args, opts } of networkCalls) { + assert.ok(opts?.env?.GIT_ASKPASS, `GIT_ASKPASS missing for git ${args[0]}`); + } + }); + + it('cleans up askpass script after run', () => { + const spawn = makeSpawn(); + cloneRepo(workspace, spawn); + const leftover = fs.readdirSync(workspace).filter(f => f.endsWith('.git-askpass.sh')); + assert.equal(leftover.length, 0, 'askpass script was not cleaned up'); + }); + + it('returns repoDir path', () => { + const spawn = makeSpawn(); + const result = cloneRepo(workspace, spawn); + assert.equal(result, path.join(workspace, 'repo')); + }); +}); From 8a4932bbd47c917e759eaed1832e8ef5b79e47a9 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 02:21:39 +0000 Subject: [PATCH 104/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 101 +-------------------------------- 1 file changed, 1 insertion(+), 100 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index d99a124..0637a08 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,100 +1 @@ -[ - { - "level": "critical", - "role": "Leo", - "location": "app/git.js:14", - "suggestion": "GITEA_TOKEN 直接嵌入 URL 中,可能導致憑證洩漏。建議使用環境變數或安全的憑證管理方式來處理敏感資訊。", - "is_new": true - }, - { - "level": "critical", - "role": "Zara", - "location": "app/git.js:14", - "suggestion": "GITEA_TOKEN 直接嵌入 URL 中,可能導致憑證洩漏。建議使用環境變數或安全的憑證管理方式來處理敏感資訊。", - "is_new": true - }, - { - "level": "critical", - "role": "Maya", - "location": "app/git.js:1", - "suggestion": "缺少對 cloneRepo 函數的單元測試,應該為其添加測試以確保其正確性。", - "is_new": true - }, - { - "level": "critical", - "role": "Maya", - "location": "app/git.js:1", - "suggestion": "缺少對 commitAndPush 函數的單元測試,應該為其添加測試以確保其正確性。", - "is_new": true - }, - { - "level": "warning", - "role": "Leo", - "location": "app/git.js:25", - "suggestion": "在使用 fs.existsSync 檢查目錄是否存在時,應考慮使用非同步方法以避免阻塞事件循環。", - "is_new": true - }, - { - "level": "warning", - "role": "Leo", - "location": "app/git.js:29", - "suggestion": "在 git clone 時使用 --depth=1 可能會導致未來需要完整歷史紀錄時的性能問題,建議根據實際需求調整。", - "is_new": true - }, - { - "level": "warning", - "role": "Maya", - "location": "app/findings.js:1", - "suggestion": "loadExclusions 函數中對於 JSON 格式的驗證不足,建議增加對於資料結構的檢查,以避免潛在的錯誤。", - "is_new": true - }, - { - "level": "warning", - "role": "Maya", - "location": "app/findings.js:1", - "suggestion": "applyExclusions 函數中對於 findings 和 exclusions 參數的有效性檢查不足,建議增加檢查以提高程式的健壯性。", - "is_new": true - }, - { - "level": "info", - "role": "Aria", - "location": "README.md", - "suggestion": "建議在 README 中增加對於新功能(如排除問題過濾)的詳細說明,以便未來的維護者能快速了解其功能.", - "is_new": true - }, - { - "level": "info", - "role": "Rex", - "location": "app/findings.js:93", - "suggestion": "建議在 loadExclusions 函式中增加對於 JSON 格式的驗證,確保讀取的資料符合預期格式,避免潛在的錯誤。", - "is_new": true - }, - { - "level": "info", - "role": "Rex", - "location": "app/findings.js:40", - "suggestion": "在 applyExclusions 函式中,建議增加對於 findings 和 exclusions 參數的有效性檢查,以提高程式的健壯性。", - "is_new": true - }, - { - "level": "info", - "role": "Zara", - "location": "app/findings.js:93", - "suggestion": "建議在 loadExclusions 函式中增加對於 JSON 格式的驗證,確保讀取的資料符合預期格式,避免潛在的錯誤。", - "is_new": true - }, - { - "level": "info", - "role": "Zara", - "location": "app/findings.js:40", - "suggestion": "在 applyExclusions 函式中,建議增加對於 findings 和 exclusions 參數的有效性檢查,以提高程式的健壯性。", - "is_new": true - }, - { - "level": "info", - "role": "Maya", - "location": "app/main.js:1", - "suggestion": "建議為主要流程中的每個步驟添加詳細的單元測試,以確保整體功能的正確性和穩定性。", - "is_new": true - } -] \ No newline at end of file +[] \ No newline at end of file From 181a0ccf685783f4fc4a9e8d74c2ca55ed30e3ac Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 02:43:21 +0000 Subject: [PATCH 105/174] docs: update README and TODO for clarity on file paths --- README.md | 4 ++-- TODO.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 11002fc..0dacb5e 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ 1. 服務名稱、模型名稱、角色資訊(個性、符合個性的英文名稱、工作內容),Comment 到 Push Request 2. 每個角色個別分析 Git Diff 的內容產生新問題表格(問題等級、角色名稱、問題位置或行數、修改建議) -3. 讀取所有未解決的舊問題(問題檔案存在於使用此 Action 的專案固定位置)加上新問題後,去除重複產生本次 Push Request 的問題表格(PR問題表格)覆蓋問題檔案 -4. 讀取排除問題檔案,用來過濾PR問題表格中不需要處理的問題 +3. 讀取所有未解決的舊問題(問題檔案 `.gitea/ai-review/findings.json` 存在於使用此 Action 的專案固定位置)加上新問題後,去除重複產生本次 Push Request 的問題表格(PR問題表格)覆蓋問題檔案 +4. 讀取排除問題檔案(`.gitea/ai-review/exclusions.json` 存在於使用此 Action 的專案固定位置),用來過濾PR問題表格中不需要處理的問題 5. 從PR問題表格中取出所有舊問題,依照等級排序後 Comment 到 Push Request 6. 從PR問題表格中取出所有新問題,排除嚴重等級的問題後 Comment 到 Push Request 7. 從PR問題表格中取出所有新問題,將每個嚴重等級的問題 Comment 到 Push Request diff --git a/TODO.md b/TODO.md index e5115dc..540a5c0 100644 --- a/TODO.md +++ b/TODO.md @@ -16,13 +16,13 @@ - 完成 ## 階段四:AI 排除問題過濾 -- 目標:讀取排除問題檔案(exclusions.json)進行規則過濾,並呼叫 AI 判斷剩餘問題是否為誤報或不適用,兩層過濾後產生最終問題清單。 +- 目標:讀取排除問題檔案(`.gitea/ai-review/exclusions.json`)進行規則過濾,並呼叫 AI 判斷剩餘問題是否為誤報或不適用,兩層過濾後產生最終問題清單。 - 驗收:log 中能看到排除問題檔案讀取成功或不存在的訊息、規則過濾數量變化,以及「AI 誤報過濾: N -> M 筆」或降級訊息。 - 完成 ## 階段五:findings 寫入與 comment 發布 -- 目標:findings.jsonl 正確寫入,comment 發布順序正確(舊問題→非嚴重→嚴重),每步有 log。 -- 驗收:log 中能看到 findings.json 寫入、comment sync 的詳細訊息與順序。 +- 目標:`.gitea/ai-review/findings.json` 正確寫入,comment 發布順序正確(舊問題→非嚴重→嚴重),每步有 log。 +- 驗收:log 中能看到 `.gitea/ai-review/findings.json` 寫入、comment sync 的詳細訊息與順序。 - 完成 ## 階段六:記憶區 commit/push 與錯誤處理 From 6bf805b4539e175d13cdd298812a0e5e60be89ea Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 02:47:48 +0000 Subject: [PATCH 106/174] fix: update role introduction formatting to use table layout --- app/roles.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/roles.js b/app/roles.js index f36e2d4..da58bf0 100644 --- a/app/roles.js +++ b/app/roles.js @@ -12,9 +12,13 @@ export function loadRoles() { } export function getRoleIntro(roles) { - const lines = ['## 🤖 AI Code Review 團隊', '']; + const lines = [ + '## 🤖 AI Code Review 團隊', '', + '| 👤 名稱 | 🎯 職責 | 🧠 個性 |', + '|--------|--------|--------|', + ]; for (const r of roles) { - lines.push(`- **${r.name}** (${r.role}):${r.personality}`); + lines.push(`| **${r.name}** | ${r.role} | ${r.personality} |`); } return lines.join('\n'); } From 43e990cb30e1cbffd24d53fdc9fc8c30fa05c80a Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 02:53:19 +0000 Subject: [PATCH 107/174] fix: update workflow section in README for clarity and correct numbering --- README.md | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0dacb5e..cab6fc2 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ 2. 在 `.gitea/workflows` 資料夾中建立 `ai-review.yaml' 3. 在 `ai-review.yaml` 中填入以下內容(選擇一個使用): -### 1. OpenAI(OpenRouter) +### 1. OpenAI ```yaml name: AI on: @@ -42,9 +42,29 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} with: - # Github (h3285@evertrust.com.tw) - # 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 OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENAI_BASE_URL: https://api.openai.com/v1 + permissions: + contents: write + pull-requests: write + issues: write +``` + +### 2. OpenRouter +```yaml +name: AI +on: + pull_request: + types: [opened, synchronize] +jobs: + code-review: + name: 'Code Review' + runs-on: ubuntu + steps: + - name: AI Code Review + uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} + with: + OPENAI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} OPENAI_BASE_URL: https://openrouter.ai/api/v1 permissions: contents: write @@ -52,7 +72,7 @@ jobs: issues: write ``` -### 2. Anthropic Claude +### 3. Anthropic Claude ```yaml name: AI on: @@ -74,7 +94,7 @@ jobs: issues: write ``` -### 3. Google Gemini +### 4. Google Gemini ```yaml name: AI on: @@ -96,7 +116,7 @@ jobs: issues: write ``` -### 4. Amazon Q +### 5. Amazon Q ```yaml name: AI on: @@ -118,7 +138,7 @@ jobs: issues: write ``` -### 5. SonarQube +### 6. SonarQube ```yaml name: AI on: From fd91ed4e5a2ba5a0f9814d0f41e526cc1921828e Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 02:54:27 +0000 Subject: [PATCH 108/174] fix: update OPENAI_BASE_URL to use the correct API endpoint --- .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 dc785d9..7d4b90c 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -34,7 +34,7 @@ jobs: uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} with: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - OPENAI_BASE_URL: https://openrouter.ai/api/v1 + OPENAI_BASE_URL: https://api.openai.com/v1 permissions: contents: write pull-requests: write From 774b78d84e00964f8fca92993546f9fc340c64d0 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 02:56:34 +0000 Subject: [PATCH 109/174] fix: update AI Code Review to use GEMINI API and base URL --- .gitea/workflows/review.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 7d4b90c..cd59e98 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -33,8 +33,8 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} with: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - OPENAI_BASE_URL: https://api.openai.com/v1 + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta permissions: contents: write pull-requests: write From c2e56e4bb24b0f4ccf7267cd572e97c9c3d83ca0 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 02:59:22 +0000 Subject: [PATCH 110/174] fix: update GEMINI model version in configuration and workflows --- .gitea/workflows/review.yaml | 1 + README.md | 1 + app/config.js | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index cd59e98..408bd6c 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -35,6 +35,7 @@ jobs: with: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta + GEMINI_MODEL: ${{ vars.GEMINI_MODEL }} permissions: contents: write pull-requests: write diff --git a/README.md b/README.md index cab6fc2..9574270 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ jobs: with: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta + GEMINI_MODEL: ${{ vars.GEMINI_MODEL }} permissions: contents: write pull-requests: write diff --git a/app/config.js b/app/config.js index c5e7caa..4a5fc7b 100644 --- a/app/config.js +++ b/app/config.js @@ -12,7 +12,7 @@ export function getLLMConfig() { const checks = [ ['openai', process.env.OPENAI_API_KEY, process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1', process.env.OPENAI_MODEL || 'gpt-4o-mini'], ['claude', process.env.CLAUDE_API_KEY, process.env.CLAUDE_BASE_URL || 'https://api.anthropic.com/v1', process.env.CLAUDE_MODEL || 'claude-3-haiku-20240307'], - ['gemini', process.env.GEMINI_API_KEY, process.env.GEMINI_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta', process.env.GEMINI_MODEL || 'gemini-1.5-flash'], + ['gemini', process.env.GEMINI_API_KEY, process.env.GEMINI_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta', process.env.GEMINI_MODEL || 'gemini-2.5-flash'], ['ollama', 'ollama', process.env.OLLAMA_BASE_URL, process.env.OLLAMA_MODEL], ['amazonq', process.env.AMAZONQ_API_KEY, process.env.AMAZONQ_BASE_URL || 'https://q.api.aws', process.env.OPENAI_MODEL || 'amazon-q'], ['kilo', process.env.KILO_API_KEY, process.env.KILO_BASE_URL || 'https://api.kilocode.com/v1', process.env.OPENAI_MODEL || 'kilo-default'], From f8e24844e8b88386221f6975f324ccaf04de1833 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 03:01:32 +0000 Subject: [PATCH 111/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 52 +++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 0637a08..2174917 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1 +1,51 @@ -[] \ No newline at end of file +[ + { + "level": "warning", + "role": "Leo", + "location": "README.md:50", + "suggestion": "在 `2. OpenRouter` 的範例中,`with:` 區塊使用 `OPENAI_API_KEY` 參數來傳遞 `OPENROUTER_API_KEY` secret。雖然這可能是 `code-review` action 的設計,但 `OPENAI_API_KEY` 這個名稱可能會讓使用者誤解為只能用於 OpenAI。建議考慮在 `code-review` action 中提供更通用的 API key 參數(例如 `API_KEY` 或 `PROVIDER_API_KEY`),或針對 OpenRouter 提供專屬的參數(例如 `OPENROUTER_API_KEY`),以提高清晰度並減少使用者設定時的困惑。如果 action 無法修改,目前的說明已盡力澄清,但仍是一個潛在的混淆點。", + "is_new": true + }, + { + "level": "warning", + "role": "Zara", + "location": "app/config.js:15", + "suggestion": "將預設的 Gemini 模型從 `gemini-1.5-flash` 更新為 `gemini-2.5-flash`,這可能影響應用程式與 LLM 互動的效能和成本。建議在部署前,對 `gemini-2.5-flash` 模型進行詳細的效能基準測試,評估其在回應時間、處理速度、準確性及成本效益方面的表現,確保其符合應用程式的特定需求,並避免潛在的效能退化或不必要的成本增加。", + "is_new": true + }, + { + "level": "warning", + "role": "Rex", + "location": ".gitea/workflows/review.yaml:33-40", + "suggestion": "工作流程中授予了 `contents: write`, `pull-requests: write`, `issues: write` 等廣泛權限。特別是 `contents: write` 權限,若工作流程或其使用的 Action 存在漏洞,可能導致程式碼庫被惡意修改。建議審查這些權限是否都絕對必要,並遵循最小權限原則,僅授予工作流程執行所需的最少權限。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": "app/config.js:15", + "suggestion": "在 `app/config.js` 的 `checks` 陣列中,使用多個空格進行欄位對齊可能導致格式不一致且難以維護。建議改用單一空格分隔元素,或考慮將每個配置項重構為物件形式,以提升程式碼的可讀性與可維護性。", + "is_new": true + }, + { + "level": "warning", + "role": "Maya", + "location": ".gitea/workflows/review.yaml", + "suggestion": "工作流程已從使用 OpenAI 轉換為 Gemini。雖然這是一個配置變更,但應確保新的 LLM 整合能正常運作。建議在 CI/CD 中增加一個整合測試步驟,以驗證使用 Gemini 模型時,AI Code Review 功能是否能成功生成評論,例如檢查 PR 評論是否存在或特定輸出訊息。", + "is_new": true + }, + { + "level": "warning", + "role": "Maya", + "location": "app/config.js:15", + "suggestion": "預設的 `GEMINI_MODEL` 已從 `gemini-1.5-flash` 變更為 `gemini-2.5-flash`。請確保有對應的單元測試來驗證當 `process.env.GEMINI_MODEL` 未設定時,`getLLMConfig` 函數能正確回傳新的預設模型 `gemini-2.5-flash`。", + "is_new": true + }, + { + "level": "info", + "role": "Maya", + "location": "app/config.js", + "suggestion": "`getLLMConfig` 函數依賴於環境變數來配置 LLM。建議為此函數增加更全面的邊界條件測試,例如:\n1. 當只有部分 LLM 相關的環境變數被設定時(例如,只有 `GEMINI_API_KEY` 而沒有 `GEMINI_BASE_URL`)。\n2. 當沒有任何 LLM 相關的環境變數被設定時,確保函數能優雅地處理(例如,回傳 `null`、空物件或拋出特定錯誤)。\n3. 測試 API 金鑰為空字串的情況,確保其行為符合預期。", + "is_new": true + } +] \ No newline at end of file From 4b382b4183d5f15c527f65e606630c1040463436 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 03:04:12 +0000 Subject: [PATCH 112/174] fix: update README for OpenRouter API compatibility and add tests for LLM configuration --- README.md | 2 +- app/config.js | 12 +++--- app/config.test.js | 101 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 app/config.test.js diff --git a/README.md b/README.md index 9574270..9af31e9 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} with: - OPENAI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} # OpenRouter 使用 OpenAI 相容介面,以 OPENAI_API_KEY 傳入 OPENAI_BASE_URL: https://openrouter.ai/api/v1 permissions: contents: write diff --git a/app/config.js b/app/config.js index 4a5fc7b..d852576 100644 --- a/app/config.js +++ b/app/config.js @@ -14,12 +14,12 @@ export function getLLMConfig() { ['claude', process.env.CLAUDE_API_KEY, process.env.CLAUDE_BASE_URL || 'https://api.anthropic.com/v1', process.env.CLAUDE_MODEL || 'claude-3-haiku-20240307'], ['gemini', process.env.GEMINI_API_KEY, process.env.GEMINI_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta', process.env.GEMINI_MODEL || 'gemini-2.5-flash'], ['ollama', 'ollama', process.env.OLLAMA_BASE_URL, process.env.OLLAMA_MODEL], - ['amazonq', process.env.AMAZONQ_API_KEY, process.env.AMAZONQ_BASE_URL || 'https://q.api.aws', process.env.OPENAI_MODEL || 'amazon-q'], - ['kilo', process.env.KILO_API_KEY, process.env.KILO_BASE_URL || 'https://api.kilocode.com/v1', process.env.OPENAI_MODEL || 'kilo-default'], - ['roo', process.env.ROO_API_KEY, process.env.ROO_BASE_URL || 'https://api.roocode.com/v1', process.env.OPENAI_MODEL || 'roo-default'], - ['cline', process.env.CLINE_API_KEY, process.env.CLINE_BASE_URL || 'https://api.cline.dev/v1', process.env.OPENAI_MODEL || 'cline-default'], - ['continue', process.env.CONTINUE_API_KEY, process.env.CONTINUE_BASE_URL || 'https://api.continue.dev/v1', process.env.OPENAI_MODEL || 'continue-default'], - ['kade', process.env.KADE_API_KEY, process.env.KADE_BASE_URL || 'https://api.kade.dev/v1', process.env.OPENAI_MODEL || 'kade-default'], + ['amazonq', process.env.AMAZONQ_API_KEY, process.env.AMAZONQ_BASE_URL || 'https://q.api.aws', process.env.AMAZONQ_MODEL || 'amazon-q'], + ['kilo', process.env.KILO_API_KEY, process.env.KILO_BASE_URL || 'https://api.kilocode.com/v1', process.env.KILO_MODEL || 'kilo-default'], + ['roo', process.env.ROO_API_KEY, process.env.ROO_BASE_URL || 'https://api.roocode.com/v1', process.env.ROO_MODEL || 'roo-default'], + ['cline', process.env.CLINE_API_KEY, process.env.CLINE_BASE_URL || 'https://api.cline.dev/v1', process.env.CLINE_MODEL || 'cline-default'], + ['continue', process.env.CONTINUE_API_KEY, process.env.CONTINUE_BASE_URL || 'https://api.continue.dev/v1', process.env.CONTINUE_MODEL || 'continue-default'], + ['kade', process.env.KADE_API_KEY, process.env.KADE_BASE_URL || 'https://api.kade.dev/v1', process.env.KADE_MODEL || 'kade-default'], ]; for (const [provider, key, baseURL, model] of checks) { if (key && baseURL) return { provider, apiKey: key, baseURL, model }; diff --git a/app/config.test.js b/app/config.test.js new file mode 100644 index 0000000..b757cec --- /dev/null +++ b/app/config.test.js @@ -0,0 +1,101 @@ +import { describe, it, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; +import { getLLMConfig } from './config.js'; + +const ENV_KEYS = [ + 'OPENAI_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_MODEL', + 'CLAUDE_API_KEY', 'CLAUDE_BASE_URL', 'CLAUDE_MODEL', + 'GEMINI_API_KEY', 'GEMINI_BASE_URL', 'GEMINI_MODEL', + 'OLLAMA_BASE_URL', 'OLLAMA_MODEL', + 'AMAZONQ_API_KEY', 'AMAZONQ_BASE_URL', 'AMAZONQ_MODEL', +]; + +let saved = {}; +beforeEach(() => { + saved = {}; + for (const k of ENV_KEYS) { saved[k] = process.env[k]; delete process.env[k]; } +}); +afterEach(() => { + for (const k of ENV_KEYS) { + if (saved[k] === undefined) delete process.env[k]; + else process.env[k] = saved[k]; + } +}); + +describe('getLLMConfig', () => { + it('returns null provider when no env vars set', () => { + const cfg = getLLMConfig(); + assert.equal(cfg.provider, null); + assert.equal(cfg.apiKey, null); + }); + + it('detects openai with defaults', () => { + process.env.OPENAI_API_KEY = 'sk-test'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'openai'); + assert.equal(cfg.apiKey, 'sk-test'); + assert.equal(cfg.baseURL, 'https://api.openai.com/v1'); + assert.equal(cfg.model, 'gpt-4o-mini'); + }); + + it('detects openai with custom base url and model', () => { + process.env.OPENAI_API_KEY = 'sk-test'; + process.env.OPENAI_BASE_URL = 'https://openrouter.ai/api/v1'; + process.env.OPENAI_MODEL = 'gpt-4o'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'openai'); + assert.equal(cfg.baseURL, 'https://openrouter.ai/api/v1'); + assert.equal(cfg.model, 'gpt-4o'); + }); + + it('detects gemini with defaults', () => { + process.env.GEMINI_API_KEY = 'gemini-key'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'gemini'); + assert.equal(cfg.model, 'gemini-2.5-flash'); + }); + + it('detects gemini with custom model', () => { + process.env.GEMINI_API_KEY = 'gemini-key'; + process.env.GEMINI_MODEL = 'gemini-2.0-flash'; + const cfg = getLLMConfig(); + assert.equal(cfg.model, 'gemini-2.0-flash'); + }); + + it('detects claude with defaults', () => { + process.env.CLAUDE_API_KEY = 'claude-key'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'claude'); + assert.equal(cfg.model, 'claude-3-haiku-20240307'); + }); + + it('detects amazonq with its own model env', () => { + process.env.AMAZONQ_API_KEY = 'aq-key'; + process.env.AMAZONQ_MODEL = 'my-amazon-model'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'amazonq'); + assert.equal(cfg.model, 'my-amazon-model'); + }); + + it('openai takes priority over gemini when both set', () => { + process.env.OPENAI_API_KEY = 'sk-test'; + process.env.GEMINI_API_KEY = 'gemini-key'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'openai'); + }); + + it('empty string api key is treated as not set', () => { + process.env.OPENAI_API_KEY = ''; + process.env.GEMINI_API_KEY = 'gemini-key'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'gemini'); + }); + + it('detects ollama without api key', () => { + process.env.OLLAMA_BASE_URL = 'http://localhost:11434'; + process.env.OLLAMA_MODEL = 'llama3'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'ollama'); + assert.equal(cfg.model, 'llama3'); + }); +}); From d73d360051aeb5d19a827c6b5d256728154fc5b6 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 03:07:13 +0000 Subject: [PATCH 113/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 59 +++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 2174917..7f518a5 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,51 +1,58 @@ [ + { + "level": "critical", + "role": "Rex", + "location": ".gitea/workflows/review.yaml:41-44", + "suggestion": "工作流程 `AI Code Review` 被授予了 `contents: write`, `pull-requests: write`, `issues: write` 等廣泛權限。特別是 `contents: write` 權限,若工作流程所使用的 Action (`code-review`) 存在漏洞,可能導致程式碼庫被惡意修改,構成嚴重的安全風險。建議遵循最小權限原則,審查並僅授予工作流程執行所需的最少權限。例如,若僅需讀取程式碼和發布評論,則 `contents: read` 和 `pull-requests: write` 可能已足夠,而 `issues: write` 則可能完全不需要。", + "is_new": true + }, + { + "level": "critical", + "role": "Rex", + "location": "README.md", + "suggestion": "`README.md` 中的 Gitea Actions 工作流程範例(特別是 OpenRouter 和 Google Gemini 部分)建議使用者配置 `contents: write`, `pull-requests: write`, `issues: write` 等廣泛權限。這會引導使用者建立具有過高權限的工作流程,若所使用的 Action 存在漏洞,可能導致程式碼庫被惡意修改。建議更新所有範例,遵循最小權限原則,僅建議授予工作流程執行所需的最少權限,例如 `contents: read` 和 `pull-requests: write`。", + "is_new": true + }, + { + "level": "critical", + "role": "Maya", + "location": "app/config.test.js", + "suggestion": "在 `app/config.js` 中,`amazonq`, `kilo`, `roo`, `cline`, `continue`, `kade` 等 LLM 供應商的模型環境變數已從 `OPENAI_MODEL` 變更為各自專屬的 `PROVIDER_MODEL` (例如 `AMAZONQ_MODEL`)。然而,`app/config.test.js` 中僅針對 `amazonq` 進行了部分測試,而 `kilo`, `roo`, `cline`, `continue`, `kade` 這些供應商完全沒有任何測試案例。這導致這些供應商的配置邏輯(包括新的模型環境變數和預設值)完全未經驗證。請為這些未測試的供應商新增完整的單元測試,確保它們的 API 金鑰、基礎 URL 和模型配置都能正確解析,並驗證當對應的環境變數未設定時,能正確使用預設模型。", + "is_new": true + }, { "level": "warning", "role": "Leo", "location": "README.md:50", "suggestion": "在 `2. OpenRouter` 的範例中,`with:` 區塊使用 `OPENAI_API_KEY` 參數來傳遞 `OPENROUTER_API_KEY` secret。雖然這可能是 `code-review` action 的設計,但 `OPENAI_API_KEY` 這個名稱可能會讓使用者誤解為只能用於 OpenAI。建議考慮在 `code-review` action 中提供更通用的 API key 參數(例如 `API_KEY` 或 `PROVIDER_API_KEY`),或針對 OpenRouter 提供專屬的參數(例如 `OPENROUTER_API_KEY`),以提高清晰度並減少使用者設定時的困惑。如果 action 無法修改,目前的說明已盡力澄清,但仍是一個潛在的混淆點。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Zara", "location": "app/config.js:15", "suggestion": "將預設的 Gemini 模型從 `gemini-1.5-flash` 更新為 `gemini-2.5-flash`,這可能影響應用程式與 LLM 互動的效能和成本。建議在部署前,對 `gemini-2.5-flash` 模型進行詳細的效能基準測試,評估其在回應時間、處理速度、準確性及成本效益方面的表現,確保其符合應用程式的特定需求,並避免潛在的效能退化或不必要的成本增加。", - "is_new": true - }, - { - "level": "warning", - "role": "Rex", - "location": ".gitea/workflows/review.yaml:33-40", - "suggestion": "工作流程中授予了 `contents: write`, `pull-requests: write`, `issues: write` 等廣泛權限。特別是 `contents: write` 權限,若工作流程或其使用的 Action 存在漏洞,可能導致程式碼庫被惡意修改。建議審查這些權限是否都絕對必要,並遵循最小權限原則,僅授予工作流程執行所需的最少權限。", - "is_new": true - }, - { - "level": "warning", - "role": "Aria", - "location": "app/config.js:15", - "suggestion": "在 `app/config.js` 的 `checks` 陣列中,使用多個空格進行欄位對齊可能導致格式不一致且難以維護。建議改用單一空格分隔元素,或考慮將每個配置項重構為物件形式,以提升程式碼的可讀性與可維護性。", - "is_new": true - }, - { - "level": "warning", - "role": "Maya", - "location": ".gitea/workflows/review.yaml", - "suggestion": "工作流程已從使用 OpenAI 轉換為 Gemini。雖然這是一個配置變更,但應確保新的 LLM 整合能正常運作。建議在 CI/CD 中增加一個整合測試步驟,以驗證使用 Gemini 模型時,AI Code Review 功能是否能成功生成評論,例如檢查 PR 評論是否存在或特定輸出訊息。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Maya", "location": "app/config.js:15", "suggestion": "預設的 `GEMINI_MODEL` 已從 `gemini-1.5-flash` 變更為 `gemini-2.5-flash`。請確保有對應的單元測試來驗證當 `process.env.GEMINI_MODEL` 未設定時,`getLLMConfig` 函數能正確回傳新的預設模型 `gemini-2.5-flash`。", + "is_new": false + }, + { + "level": "warning", + "role": "Leo", + "location": "app/config.js:15", + "suggestion": "目前 `checks` 陣列使用多個空格進行欄位對齊,這是一種脆弱的格式化方式,當配置項的內容長度改變時,容易導致對齊混亂,增加維護成本。建議將 `checks` 陣列中的每個 LLM 配置項重構為物件形式(例如 `{ provider: 'openai', apiKeyEnv: 'OPENAI_API_KEY', baseURL: '...', modelEnv: 'OPENAI_MODEL', defaultModel: '...' }`)。這樣可以提高程式碼的可讀性、可維護性及擴展性,並使新增或修改配置項更加清晰。", "is_new": true }, { - "level": "info", + "level": "warning", "role": "Maya", - "location": "app/config.js", - "suggestion": "`getLLMConfig` 函數依賴於環境變數來配置 LLM。建議為此函數增加更全面的邊界條件測試,例如:\n1. 當只有部分 LLM 相關的環境變數被設定時(例如,只有 `GEMINI_API_KEY` 而沒有 `GEMINI_BASE_URL`)。\n2. 當沒有任何 LLM 相關的環境變數被設定時,確保函數能優雅地處理(例如,回傳 `null`、空物件或拋出特定錯誤)。\n3. 測試 API 金鑰為空字串的情況,確保其行為符合預期。", + "location": ".gitea/workflows/review.yaml", + "suggestion": "工作流程已從使用 OpenAI 轉換為 Gemini。雖然 `app/config.test.js` 增加了 `getLLMConfig` 的單元測試,但這僅驗證了配置的解析。為了確保 AI Code Review 功能在實際使用 Gemini 模型時能正常運作,建議在 CI/CD 中增加一個整合測試步驟。此測試應能驗證使用 Gemini 模型時,AI Code Review Action 是否能成功生成 PR 評論,例如檢查 PR 評論是否存在或其內容是否符合預期,以確保端到端的整合是成功的。", "is_new": true } ] \ No newline at end of file From 66a75f135f483c3d925e1caf50a54f554e794605 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 03:07:09 +0000 Subject: [PATCH 114/174] refactor: remove unused API keys and configurations from action and config files --- README.md | 22 ------------------- action.yaml | 58 --------------------------------------------------- app/config.js | 5 ----- 3 files changed, 85 deletions(-) diff --git a/README.md b/README.md index 9af31e9..f388ab7 100644 --- a/README.md +++ b/README.md @@ -139,28 +139,6 @@ jobs: issues: write ``` -### 6. SonarQube -```yaml -name: AI -on: - pull_request: - types: [opened, synchronize] -jobs: - code-review: - name: 'Code Review' - runs-on: ubuntu - steps: - - name: AI Code Review - uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} - with: - SONARQUBE_TOKEN: ${{ secrets.SONARQUBE_TOKEN }} - SONARQUBE_URL: https://sonarqube.example.com - permissions: - contents: write - pull-requests: write - issues: write -``` - ### - Ollama ```yaml diff --git a/action.yaml b/action.yaml index ce0e7d2..52ed03a 100644 --- a/action.yaml +++ b/action.yaml @@ -72,53 +72,7 @@ inputs: description: 'Amazon Q Base URL' required: false - # SonarQube - SONARQUBE_TOKEN: - description: 'SonarQube Token' - required: false - SONARQUBE_URL: - description: 'SonarQube URL' - required: false - # Kilo Code - KILO_API_KEY: - description: 'Kilo Code API Key' - required: false - KILO_BASE_URL: - description: 'Kilo Code Base URL' - required: false - - # Roo Code - ROO_API_KEY: - description: 'Roo Code API Key' - required: false - ROO_BASE_URL: - description: 'Roo Code Base URL' - required: false - - # Cline - CLINE_API_KEY: - description: 'Cline API Key' - required: false - CLINE_BASE_URL: - description: 'Cline Base URL' - required: false - - # Continue - CONTINUE_API_KEY: - description: 'Continue API Key' - required: false - CONTINUE_BASE_URL: - description: 'Continue Base URL' - required: false - - # Kade - KADE_API_KEY: - description: 'Kade API Key' - required: false - KADE_BASE_URL: - description: 'Kade Base URL' - required: false runs: using: 'docker' @@ -145,15 +99,3 @@ runs: OLLAMA_MODEL: ${{ inputs.OLLAMA_MODEL }} AMAZONQ_API_KEY: ${{ inputs.AMAZONQ_API_KEY }} AMAZONQ_BASE_URL: ${{ inputs.AMAZONQ_BASE_URL }} - SONARQUBE_TOKEN: ${{ inputs.SONARQUBE_TOKEN }} - SONARQUBE_URL: ${{ inputs.SONARQUBE_URL }} - KILO_API_KEY: ${{ inputs.KILO_API_KEY }} - KILO_BASE_URL: ${{ inputs.KILO_BASE_URL }} - ROO_API_KEY: ${{ inputs.ROO_API_KEY }} - ROO_BASE_URL: ${{ inputs.ROO_BASE_URL }} - CLINE_API_KEY: ${{ inputs.CLINE_API_KEY }} - CLINE_BASE_URL: ${{ inputs.CLINE_BASE_URL }} - CONTINUE_API_KEY: ${{ inputs.CONTINUE_API_KEY }} - CONTINUE_BASE_URL: ${{ inputs.CONTINUE_BASE_URL }} - KADE_API_KEY: ${{ inputs.KADE_API_KEY }} - KADE_BASE_URL: ${{ inputs.KADE_BASE_URL }} diff --git a/app/config.js b/app/config.js index d852576..3dc350e 100644 --- a/app/config.js +++ b/app/config.js @@ -15,11 +15,6 @@ export function getLLMConfig() { ['gemini', process.env.GEMINI_API_KEY, process.env.GEMINI_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta', process.env.GEMINI_MODEL || 'gemini-2.5-flash'], ['ollama', 'ollama', process.env.OLLAMA_BASE_URL, process.env.OLLAMA_MODEL], ['amazonq', process.env.AMAZONQ_API_KEY, process.env.AMAZONQ_BASE_URL || 'https://q.api.aws', process.env.AMAZONQ_MODEL || 'amazon-q'], - ['kilo', process.env.KILO_API_KEY, process.env.KILO_BASE_URL || 'https://api.kilocode.com/v1', process.env.KILO_MODEL || 'kilo-default'], - ['roo', process.env.ROO_API_KEY, process.env.ROO_BASE_URL || 'https://api.roocode.com/v1', process.env.ROO_MODEL || 'roo-default'], - ['cline', process.env.CLINE_API_KEY, process.env.CLINE_BASE_URL || 'https://api.cline.dev/v1', process.env.CLINE_MODEL || 'cline-default'], - ['continue', process.env.CONTINUE_API_KEY, process.env.CONTINUE_BASE_URL || 'https://api.continue.dev/v1', process.env.CONTINUE_MODEL || 'continue-default'], - ['kade', process.env.KADE_API_KEY, process.env.KADE_BASE_URL || 'https://api.kade.dev/v1', process.env.KADE_MODEL || 'kade-default'], ]; for (const [provider, key, baseURL, model] of checks) { if (key && baseURL) return { provider, apiKey: key, baseURL, model }; From c16e07bddd3cb0a8ce04778d5e0f8009d49a860f Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 03:11:29 +0000 Subject: [PATCH 115/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 7f518a5..11c84d8 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -4,13 +4,13 @@ "role": "Rex", "location": ".gitea/workflows/review.yaml:41-44", "suggestion": "工作流程 `AI Code Review` 被授予了 `contents: write`, `pull-requests: write`, `issues: write` 等廣泛權限。特別是 `contents: write` 權限,若工作流程所使用的 Action (`code-review`) 存在漏洞,可能導致程式碼庫被惡意修改,構成嚴重的安全風險。建議遵循最小權限原則,審查並僅授予工作流程執行所需的最少權限。例如,若僅需讀取程式碼和發布評論,則 `contents: read` 和 `pull-requests: write` 可能已足夠,而 `issues: write` 則可能完全不需要。", - "is_new": true + "is_new": false }, { "level": "critical", "role": "Rex", "location": "README.md", - "suggestion": "`README.md` 中的 Gitea Actions 工作流程範例(特別是 OpenRouter 和 Google Gemini 部分)建議使用者配置 `contents: write`, `pull-requests: write`, `issues: write` 等廣泛權限。這會引導使用者建立具有過高權限的工作流程,若所使用的 Action 存在漏洞,可能導致程式碼庫被惡意修改。建議更新所有範例,遵循最小權限原則,僅建議授予工作流程執行所需的最少權限,例如 `contents: read` 和 `pull-requests: write`。", + "suggestion": "`README.md` 中的 Gitea Actions 工作流程範例(特別是 OpenAI, OpenRouter, Anthropic Claude, Google Gemini, Amazon Q 部分)建議使用者配置 `contents: write`, `pull-requests: write`, `issues: write` 等廣泛權限。這會引導使用者建立具有過高權限的工作流程,若所使用的 Action 存在漏洞,可能導致程式碼庫被惡意修改。建議更新所有範例,遵循最小權限原則,僅建議授予工作流程執行所需的最少權限,例如 `contents: read` 和 `pull-requests: write`。", "is_new": true }, { @@ -18,7 +18,7 @@ "role": "Maya", "location": "app/config.test.js", "suggestion": "在 `app/config.js` 中,`amazonq`, `kilo`, `roo`, `cline`, `continue`, `kade` 等 LLM 供應商的模型環境變數已從 `OPENAI_MODEL` 變更為各自專屬的 `PROVIDER_MODEL` (例如 `AMAZONQ_MODEL`)。然而,`app/config.test.js` 中僅針對 `amazonq` 進行了部分測試,而 `kilo`, `roo`, `cline`, `continue`, `kade` 這些供應商完全沒有任何測試案例。這導致這些供應商的配置邏輯(包括新的模型環境變數和預設值)完全未經驗證。請為這些未測試的供應商新增完整的單元測試,確保它們的 API 金鑰、基礎 URL 和模型配置都能正確解析,並驗證當對應的環境變數未設定時,能正確使用預設模型。", - "is_new": true + "is_new": false }, { "level": "warning", @@ -29,10 +29,10 @@ }, { "level": "warning", - "role": "Zara", + "role": "Leo", "location": "app/config.js:15", - "suggestion": "將預設的 Gemini 模型從 `gemini-1.5-flash` 更新為 `gemini-2.5-flash`,這可能影響應用程式與 LLM 互動的效能和成本。建議在部署前,對 `gemini-2.5-flash` 模型進行詳細的效能基準測試,評估其在回應時間、處理速度、準確性及成本效益方面的表現,確保其符合應用程式的特定需求,並避免潛在的效能退化或不必要的成本增加。", - "is_new": false + "suggestion": "將預設的 Gemini 模型從 `gemini-1.5-flash` 更新為 `gemini-2.5-flash`,這可能影響應用程式與 LLM 互動的效能和成本。從長期維護成本的角度來看,建議在部署前,對 `gemini-2.5-flash` 模型進行詳細的效能基準測試,評估其在回應時間、處理速度、準確性及成本效益方面的表現,確保其符合應用程式的特定需求,並避免潛在的效能退化或不必要的成本增加。", + "is_new": true }, { "level": "warning", @@ -46,13 +46,20 @@ "role": "Leo", "location": "app/config.js:15", "suggestion": "目前 `checks` 陣列使用多個空格進行欄位對齊,這是一種脆弱的格式化方式,當配置項的內容長度改變時,容易導致對齊混亂,增加維護成本。建議將 `checks` 陣列中的每個 LLM 配置項重構為物件形式(例如 `{ provider: 'openai', apiKeyEnv: 'OPENAI_API_KEY', baseURL: '...', modelEnv: 'OPENAI_MODEL', defaultModel: '...' }`)。這樣可以提高程式碼的可讀性、可維護性及擴展性,並使新增或修改配置項更加清晰。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Maya", "location": ".gitea/workflows/review.yaml", "suggestion": "工作流程已從使用 OpenAI 轉換為 Gemini。雖然 `app/config.test.js` 增加了 `getLLMConfig` 的單元測試,但這僅驗證了配置的解析。為了確保 AI Code Review 功能在實際使用 Gemini 模型時能正常運作,建議在 CI/CD 中增加一個整合測試步驟。此測試應能驗證使用 Gemini 模型時,AI Code Review Action 是否能成功生成 PR 評論,例如檢查 PR 評論是否存在或其內容是否符合預期,以確保端到端的整合是成功的。", + "is_new": false + }, + { + "level": "warning", + "role": "Aria", + "location": ".gitea/ai-review/findings.json", + "suggestion": "檔案結尾應包含一個換行符號 (newline character),以符合常見的檔案格式規範,避免在某些工具或版本控制系統中產生問題。", "is_new": true } ] \ No newline at end of file From 23ceb84073627039e2140637070f60eda667291c Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 03:17:06 +0000 Subject: [PATCH 116/174] fix: add OPENAI_MODEL variable to OpenAI and OpenRouter job configurations --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f388ab7..fd8c719 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ jobs: with: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENAI_BASE_URL: https://api.openai.com/v1 + OPENAI_MODEL: ${{ vars.OPENAI_MODEL }} permissions: contents: write pull-requests: write @@ -66,6 +67,7 @@ jobs: with: OPENAI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} # OpenRouter 使用 OpenAI 相容介面,以 OPENAI_API_KEY 傳入 OPENAI_BASE_URL: https://openrouter.ai/api/v1 + OPENAI_MODEL: ${{ vars.OPENROUTER_MODEL }} permissions: contents: write pull-requests: write From 940d03bc6e5d09e4746a213dcd2a2e605c0905cd Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 03:18:04 +0000 Subject: [PATCH 117/174] fix: add suggestion for necessary permissions in README.md for Action functionality --- .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 2ca1f18..9e47c69 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -7,5 +7,10 @@ { "location": "app/git.js", "suggestion": "GITEA_TOKEN 直接嵌入 URL 中" + }, + { + "role": "Rex", + "location": "README.md", + "suggestion": "contents: write、pull-requests: write、issues: write 為此 Action 正常運作所必要的權限" } ] From e9874e61fe847dc0e76d949431a6b0ad2b9fa3bb Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 03:19:47 +0000 Subject: [PATCH 118/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 66 +--------------------------------- 1 file changed, 1 insertion(+), 65 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 11c84d8..0637a08 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,65 +1 @@ -[ - { - "level": "critical", - "role": "Rex", - "location": ".gitea/workflows/review.yaml:41-44", - "suggestion": "工作流程 `AI Code Review` 被授予了 `contents: write`, `pull-requests: write`, `issues: write` 等廣泛權限。特別是 `contents: write` 權限,若工作流程所使用的 Action (`code-review`) 存在漏洞,可能導致程式碼庫被惡意修改,構成嚴重的安全風險。建議遵循最小權限原則,審查並僅授予工作流程執行所需的最少權限。例如,若僅需讀取程式碼和發布評論,則 `contents: read` 和 `pull-requests: write` 可能已足夠,而 `issues: write` 則可能完全不需要。", - "is_new": false - }, - { - "level": "critical", - "role": "Rex", - "location": "README.md", - "suggestion": "`README.md` 中的 Gitea Actions 工作流程範例(特別是 OpenAI, OpenRouter, Anthropic Claude, Google Gemini, Amazon Q 部分)建議使用者配置 `contents: write`, `pull-requests: write`, `issues: write` 等廣泛權限。這會引導使用者建立具有過高權限的工作流程,若所使用的 Action 存在漏洞,可能導致程式碼庫被惡意修改。建議更新所有範例,遵循最小權限原則,僅建議授予工作流程執行所需的最少權限,例如 `contents: read` 和 `pull-requests: write`。", - "is_new": true - }, - { - "level": "critical", - "role": "Maya", - "location": "app/config.test.js", - "suggestion": "在 `app/config.js` 中,`amazonq`, `kilo`, `roo`, `cline`, `continue`, `kade` 等 LLM 供應商的模型環境變數已從 `OPENAI_MODEL` 變更為各自專屬的 `PROVIDER_MODEL` (例如 `AMAZONQ_MODEL`)。然而,`app/config.test.js` 中僅針對 `amazonq` 進行了部分測試,而 `kilo`, `roo`, `cline`, `continue`, `kade` 這些供應商完全沒有任何測試案例。這導致這些供應商的配置邏輯(包括新的模型環境變數和預設值)完全未經驗證。請為這些未測試的供應商新增完整的單元測試,確保它們的 API 金鑰、基礎 URL 和模型配置都能正確解析,並驗證當對應的環境變數未設定時,能正確使用預設模型。", - "is_new": false - }, - { - "level": "warning", - "role": "Leo", - "location": "README.md:50", - "suggestion": "在 `2. OpenRouter` 的範例中,`with:` 區塊使用 `OPENAI_API_KEY` 參數來傳遞 `OPENROUTER_API_KEY` secret。雖然這可能是 `code-review` action 的設計,但 `OPENAI_API_KEY` 這個名稱可能會讓使用者誤解為只能用於 OpenAI。建議考慮在 `code-review` action 中提供更通用的 API key 參數(例如 `API_KEY` 或 `PROVIDER_API_KEY`),或針對 OpenRouter 提供專屬的參數(例如 `OPENROUTER_API_KEY`),以提高清晰度並減少使用者設定時的困惑。如果 action 無法修改,目前的說明已盡力澄清,但仍是一個潛在的混淆點。", - "is_new": false - }, - { - "level": "warning", - "role": "Leo", - "location": "app/config.js:15", - "suggestion": "將預設的 Gemini 模型從 `gemini-1.5-flash` 更新為 `gemini-2.5-flash`,這可能影響應用程式與 LLM 互動的效能和成本。從長期維護成本的角度來看,建議在部署前,對 `gemini-2.5-flash` 模型進行詳細的效能基準測試,評估其在回應時間、處理速度、準確性及成本效益方面的表現,確保其符合應用程式的特定需求,並避免潛在的效能退化或不必要的成本增加。", - "is_new": true - }, - { - "level": "warning", - "role": "Maya", - "location": "app/config.js:15", - "suggestion": "預設的 `GEMINI_MODEL` 已從 `gemini-1.5-flash` 變更為 `gemini-2.5-flash`。請確保有對應的單元測試來驗證當 `process.env.GEMINI_MODEL` 未設定時,`getLLMConfig` 函數能正確回傳新的預設模型 `gemini-2.5-flash`。", - "is_new": false - }, - { - "level": "warning", - "role": "Leo", - "location": "app/config.js:15", - "suggestion": "目前 `checks` 陣列使用多個空格進行欄位對齊,這是一種脆弱的格式化方式,當配置項的內容長度改變時,容易導致對齊混亂,增加維護成本。建議將 `checks` 陣列中的每個 LLM 配置項重構為物件形式(例如 `{ provider: 'openai', apiKeyEnv: 'OPENAI_API_KEY', baseURL: '...', modelEnv: 'OPENAI_MODEL', defaultModel: '...' }`)。這樣可以提高程式碼的可讀性、可維護性及擴展性,並使新增或修改配置項更加清晰。", - "is_new": false - }, - { - "level": "warning", - "role": "Maya", - "location": ".gitea/workflows/review.yaml", - "suggestion": "工作流程已從使用 OpenAI 轉換為 Gemini。雖然 `app/config.test.js` 增加了 `getLLMConfig` 的單元測試,但這僅驗證了配置的解析。為了確保 AI Code Review 功能在實際使用 Gemini 模型時能正常運作,建議在 CI/CD 中增加一個整合測試步驟。此測試應能驗證使用 Gemini 模型時,AI Code Review Action 是否能成功生成 PR 評論,例如檢查 PR 評論是否存在或其內容是否符合預期,以確保端到端的整合是成功的。", - "is_new": false - }, - { - "level": "warning", - "role": "Aria", - "location": ".gitea/ai-review/findings.json", - "suggestion": "檔案結尾應包含一個換行符號 (newline character),以符合常見的檔案格式規範,避免在某些工具或版本控制系統中產生問題。", - "is_new": true - } -] \ No newline at end of file +[] \ No newline at end of file From 878d8a5bb46ac414cf450630d05434fef7897c70 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 03:20:22 +0000 Subject: [PATCH 119/174] fix: update OLLAMA_BASE_URL to a specific endpoint in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fd8c719..dd5d383 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} with: - OLLAMA_BASE_URL: ${{ vars.OLLAMA_BASE_URL }} + OLLAMA_BASE_URL: https://ollama.jsc.idv.me/v1 OLLAMA_MODEL: ${{ vars.OLLAMA_MODEL }} permissions: contents: write From 2c59ce1bc1e3739a42a126285dfee2044041c2f9 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 03:22:50 +0000 Subject: [PATCH 120/174] fix: enhance error handling in filterFalsePositivesWithAI to check for empty array response --- app/findings.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/findings.js b/app/findings.js index 7dcf060..ad4f245 100644 --- a/app/findings.js +++ b/app/findings.js @@ -149,11 +149,11 @@ export async function filterFalsePositivesWithAI(findings) { try { const result = await chatJSON(systemPrompt, userContent); - if (Array.isArray(result)) { + if (Array.isArray(result) && result.length > 0) { console.log(` AI 誤報過濾: ${findings.length} -> ${result.length} 筆`); return result; } - throw new Error('AI 回傳非陣列'); + throw new Error('AI 回傳空陣列或非陣列'); } catch (e) { const status = e.response?.status; if (status === 402 || status === 429) { From cf8dd629b2966957e924055c73c2fb393f023fc8 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 03:25:39 +0000 Subject: [PATCH 121/174] fix: update AI Code Review step to use OpenAI API instead of Gemini --- .gitea/workflows/review.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 408bd6c..289c154 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -33,9 +33,9 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} with: - GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} - GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta - GEMINI_MODEL: ${{ vars.GEMINI_MODEL }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENAI_BASE_URL: https://api.openai.com/v1 + OPENAI_MODEL: ${{ vars.OPENAI_MODEL }} permissions: contents: write pull-requests: write From c7a2a3cfc718e07abc1608f177a82cdc0e8d0337 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 03:28:17 +0000 Subject: [PATCH 122/174] fix: update AI Code Review step to use OpenRouter API instead of OpenAI --- .gitea/workflows/review.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 289c154..932f11f 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -33,9 +33,9 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} with: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - OPENAI_BASE_URL: https://api.openai.com/v1 - OPENAI_MODEL: ${{ vars.OPENAI_MODEL }} + OPENAI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} # OpenRouter 使用 OpenAI 相容介面,以 OPENAI_API_KEY 傳入 + OPENAI_BASE_URL: https://openrouter.ai/api/v1 + OPENAI_MODEL: ${{ vars.OPENROUTER_MODEL }} permissions: contents: write pull-requests: write From a479ccdd54d2976414ee77d2b36db6856fe2eb3e Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 03:37:41 +0000 Subject: [PATCH 123/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 143 ++++++++++++++++++++++++++++++++- 1 file changed, 142 insertions(+), 1 deletion(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 0637a08..8b2e9d9 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1 +1,142 @@ -[] \ No newline at end of file +[ + { + "level": "critical", + "role": "Leo", + "location": "app/config.js:1", + "suggestion": "`getLLMConfig` 在找不到任何符合條件的 provider 時會回傳 `undefined`,而呼叫端(例如測試)假設會得到一個包含 `provider`、`apiKey`、`baseURL`、`model` 欄位的物件,導致執行時拋出 `TypeError`。請在函式結尾加入預設回傳值,例如 `return { provider: null, apiKey: null, baseURL: null, model: null };`,並在文件中說明此行為。", + "is_new": true + }, + { + "level": "critical", + "role": "Rex", + "location": ".gitea/ai-review/exclusions.json:7", + "suggestion": "請移除 GITEA_TOKEN 直接嵌入 URL 的做法,改以環境變數或 Gitea Secrets 注入,避免在程式碼或設定檔中硬編碼機密資訊。", + "is_new": true + }, + { + "level": "critical", + "role": "Aria", + "location": ".gitea/ai-review/exclusions.json", + "suggestion": "新增的條目包含 `role` 欄位,但目前的 JSON schema 只接受 `location` 與 `suggestion`,此欄位會導致驗證失敗,請移除或更新 schema。", + "is_new": true + }, + { + "level": "warning", + "role": "Leo", + "location": "app/config.js:5", + "suggestion": "目前使用硬編碼的 `checks` 陣列來管理所有 LLM provider,未來若要新增或移除 provider 必須直接修改程式碼,易產生重複與維護負擔。建議將 provider 設定抽離至外部 JSON/YAML 檔或使用可擴充的資料結構,並在程式中載入,提升模組化與可維護性。", + "is_new": true + }, + { + "level": "warning", + "role": "Leo", + "location": "app/config.js:3", + "suggestion": "為 `getLLMConfig` 加上 JSDoc 或 TypeScript 型別註解,說明回傳物件的結構與每個欄位的意義,方便其他開發者快速了解 API,降低錯誤使用的風險。", + "is_new": true + }, + { + "level": "warning", + "role": "Zara", + "location": "app/config.js", + "suggestion": "將 getLLMConfig 的結果快取(例如使用單例或 memoization),避免在程式執行期間多次重複遍歷 checks 陣列與讀取 process.env,減少不必要的 I/O 開銷。", + "is_new": true + }, + { + "level": "warning", + "role": "Rex", + "location": ".gitea/workflows/review.yaml:33", + "suggestion": "工作流程目前授予 contents、pull‑requests、issues 三項 write 權限,過於寬鬆。建議依實際需求僅授予 read 或最小必要的 write 權限,以降低被濫用的風險。", + "is_new": true + }, + { + "level": "warning", + "role": "Rex", + "location": ".gitea/workflows/review.yaml:35", + "suggestion": "將 OPENAI_API_KEY 參數改為使用正確的 secret 名稱(如 OPENROUTER_API_KEY)時,請確保工作流程文件中不會同時暴露兩個不同的 secret 名稱,以免因名稱錯誤導致金鑰未傳入或意外洩漏。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": ".gitea/workflows/review.yaml:33", + "suggestion": "在 `OPENAI_API_KEY` 後的註解前應保留一個空格,以符合常見的 YAML 註解風格:`... ${{ secrets.OPENROUTER_API_KEY }} # OpenRouter 使用 OpenAI 相容介面,以 OPENAI_API_KEY 傳入`。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": "README.md", + "suggestion": "文件中章節編號不連續(例如 `### 2. OpenRouter` 後直接跳到 `### 3. Anthropic Claude`),建議重新編號或使用一致的標題層級,以提升可讀性與維護性。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": "app/config.js", + "suggestion": "`checks` 陣列的每一行過長,超過 120 個字元,建議拆成多行並對齊欄位,以符合程式碼可讀性與行長限制的慣例。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": "app/config.js", + "suggestion": "`amazonq` 那一行的空格對齊與其他項目不一致,請統一使用單一個空格分隔欄位,或使用對齊工具保持列的垂直對齊。", + "is_new": true + }, + { + "level": "info", + "role": "Leo", + "location": "app/findings.js:150", + "suggestion": "在 `filterFalsePositivesWithAI` 中,當 AI 回傳空陣列時拋出錯誤,可能導致上層流程中斷。建議改為回傳原始 `findings` 或提供更具體的錯誤類別,並在文件中說明此行為,以提升錯誤處理的可預測性。", + "is_new": true + }, + { + "level": "info", + "role": "Zara", + "location": "app/findings.js", + "suggestion": "在 filterFalsePositivesWithAI 中,若 AI 回傳的陣列非常大,建議在回傳前加入分頁或限制筆數,避免一次性載入過多資料導致記憶體使用量激增。", + "is_new": true + }, + { + "level": "info", + "role": "Zara", + "location": "app/config.js", + "suggestion": "對於未設定 API 金鑰的 provider(如 ollama),可提前返回快取的預設設定,避免每次呼叫都進行條件檢查。", + "is_new": true + }, + { + "level": "info", + "role": "Rex", + "location": "README.md:28", + "suggestion": "文件中列出的權限說明應與實際 workflow 中的 permissions 保持一致,並提醒使用者僅在必要時授予 write 權限,避免在 CI/CD 環境中過度授權。", + "is_new": true + }, + { + "level": "info", + "role": "Rex", + "location": "app/config.js:12", + "suggestion": "在取得 LLM 設定時,若 key 為空字串仍會被視為有效。建議在判斷前先檢查 key 是否為非空字串,以防止意外使用空的 API 金鑰發送請求。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "app/config.test.js", + "suggestion": "匯入語句過長,建議改寫為多行匯入,例如:\n```js\nimport {\n describe,\n it,\n beforeEach,\n afterEach\n} from 'node:test';\n```", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "app/config.test.js", + "suggestion": "`ENV_KEYS` 陣列過長,建議分行列舉,每行放置一個環境變數,以提升可讀性。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "app/findings.js", + "suggestion": "錯誤訊息 `AI 回傳空陣列或非陣列` 可簡化為 `AI 回傳的結果不是有效的非空陣列`,讓訊息更清晰。", + "is_new": true + } +] \ No newline at end of file From 76c5160915274f66b16d6a1b322b0994243407f7 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 03:42:57 +0000 Subject: [PATCH 124/174] fix: add additional suggestions for AI review exclusions in exclusions.json --- .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 9e47c69..21dc2dc 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -12,5 +12,20 @@ "role": "Rex", "location": "README.md", "suggestion": "contents: write、pull-requests: write、issues: write 為此 Action 正常運作所必要的權限" + }, + { + "role": "Leo", + "location": "app/config.js", + "suggestion": "getLLMConfig 在找不到任何符合條件的 provider 時會回傳 undefined" + }, + { + "role": "Rex", + "location": ".gitea/ai-review/exclusions.json", + "suggestion": "GITEA_TOKEN 直接嵌入 URL 的做法" + }, + { + "role": "Aria", + "location": ".gitea/ai-review/exclusions.json", + "suggestion": "role 欄位會導致驗證失敗" } ] From 4f631ef9b87f5ea045d6a22ad5a303dc38d3613b Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 03:48:43 +0000 Subject: [PATCH 125/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 242 +++++++++++++++++++++++++++++---- 1 file changed, 219 insertions(+), 23 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 8b2e9d9..85bbe32 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -4,20 +4,48 @@ "role": "Leo", "location": "app/config.js:1", "suggestion": "`getLLMConfig` 在找不到任何符合條件的 provider 時會回傳 `undefined`,而呼叫端(例如測試)假設會得到一個包含 `provider`、`apiKey`、`baseURL`、`model` 欄位的物件,導致執行時拋出 `TypeError`。請在函式結尾加入預設回傳值,例如 `return { provider: null, apiKey: null, baseURL: null, model: null };`,並在文件中說明此行為。", - "is_new": true - }, - { - "level": "critical", - "role": "Rex", - "location": ".gitea/ai-review/exclusions.json:7", - "suggestion": "請移除 GITEA_TOKEN 直接嵌入 URL 的做法,改以環境變數或 Gitea Secrets 注入,避免在程式碼或設定檔中硬編碼機密資訊。", - "is_new": true + "is_new": false }, { "level": "critical", "role": "Aria", "location": ".gitea/ai-review/exclusions.json", "suggestion": "新增的條目包含 `role` 欄位,但目前的 JSON schema 只接受 `location` 與 `suggestion`,此欄位會導致驗證失敗,請移除或更新 schema。", + "is_new": false + }, + { + "level": "critical", + "role": "Leo", + "location": "app/findings.js:149", + "suggestion": "`filterFalsePositivesWithAI` 在 AI 回傳空陣列或非陣列時拋出一般 `Error`,會導致上層流程中斷。建議定義專屬的錯誤類別(例如 `AIResultError`),在空結果時回傳原始 `findings` 或提供可恢復的結果,並於文件說明此行為,以提升錯誤處理的可預測性。", + "is_new": true + }, + { + "level": "critical", + "role": "Rex", + "location": ".gitea/ai-review/exclusions.json:7", + "suggestion": "移除在 exclusions.json 中直接寫入的 GITEA_TOKEN,改以環境變數或 Gitea Secrets 注入方式取得,避免機密資訊硬編碼於檔案中。", + "is_new": true + }, + { + "level": "critical", + "role": "Rex", + "location": "app/config.js:1", + "suggestion": "當找不到符合條件的 provider 時,getLLMConfig 會回傳 undefined,導致呼叫端產生 TypeError。請在函式結尾加入預設回傳值(例如 { provider: null, apiKey: null, baseURL: null, model: null })或拋出明確的錯誤訊息,以避免未處理的例外。", + "is_new": true + }, + { + "level": "critical", + "role": "Aria", + "location": ".gitea/ai-review/exclusions.json:12", + "suggestion": "移除 `role` 欄位,JSON schema 只接受 `location` 與 `suggestion`,保留這兩個欄位即可,避免驗證失敗。", + "is_new": true + }, + { + "level": "critical", + "role": "Aria", + "location": "app/config.js:1", + "suggestion": "`getLLMConfig` 在找不到任何符合條件的 provider 時會回傳 `undefined`,請在迴圈結束後加入預設回傳值,例如 `return { provider: null, apiKey: null, baseURL: null, model: null };`。", "is_new": true }, { @@ -25,62 +53,174 @@ "role": "Leo", "location": "app/config.js:5", "suggestion": "目前使用硬編碼的 `checks` 陣列來管理所有 LLM provider,未來若要新增或移除 provider 必須直接修改程式碼,易產生重複與維護負擔。建議將 provider 設定抽離至外部 JSON/YAML 檔或使用可擴充的資料結構,並在程式中載入,提升模組化與可維護性。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Leo", "location": "app/config.js:3", "suggestion": "為 `getLLMConfig` 加上 JSDoc 或 TypeScript 型別註解,說明回傳物件的結構與每個欄位的意義,方便其他開發者快速了解 API,降低錯誤使用的風險。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Zara", "location": "app/config.js", "suggestion": "將 getLLMConfig 的結果快取(例如使用單例或 memoization),避免在程式執行期間多次重複遍歷 checks 陣列與讀取 process.env,減少不必要的 I/O 開銷。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Rex", "location": ".gitea/workflows/review.yaml:33", "suggestion": "工作流程目前授予 contents、pull‑requests、issues 三項 write 權限,過於寬鬆。建議依實際需求僅授予 read 或最小必要的 write 權限,以降低被濫用的風險。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Rex", "location": ".gitea/workflows/review.yaml:35", "suggestion": "將 OPENAI_API_KEY 參數改為使用正確的 secret 名稱(如 OPENROUTER_API_KEY)時,請確保工作流程文件中不會同時暴露兩個不同的 secret 名稱,以免因名稱錯誤導致金鑰未傳入或意外洩漏。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Aria", "location": ".gitea/workflows/review.yaml:33", "suggestion": "在 `OPENAI_API_KEY` 後的註解前應保留一個空格,以符合常見的 YAML 註解風格:`... ${{ secrets.OPENROUTER_API_KEY }} # OpenRouter 使用 OpenAI 相容介面,以 OPENAI_API_KEY 傳入`。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Aria", "location": "README.md", "suggestion": "文件中章節編號不連續(例如 `### 2. OpenRouter` 後直接跳到 `### 3. Anthropic Claude`),建議重新編號或使用一致的標題層級,以提升可讀性與維護性。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Aria", "location": "app/config.js", "suggestion": "`checks` 陣列的每一行過長,超過 120 個字元,建議拆成多行並對齊欄位,以符合程式碼可讀性與行長限制的慣例。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Aria", "location": "app/config.js", "suggestion": "`amazonq` 那一行的空格對齊與其他項目不一致,請統一使用單一個空格分隔欄位,或使用對齊工具保持列的垂直對齊。", + "is_new": false + }, + { + "level": "warning", + "role": "Leo", + "location": "app/config.js:12", + "suggestion": "目前使用硬編碼的 `checks` 陣列管理所有 LLM provider,未來若要新增或移除 provider 必須直接修改程式碼,易產生重複與維護負擔。建議將 provider 設定抽離至外部 JSON/YAML 檔或使用可擴充的資料結構,於程式中載入,以提升模組化與可維護性。", + "is_new": true + }, + { + "level": "warning", + "role": "Zara", + "location": "app/config.js", + "suggestion": "將 `getLLMConfig` 的結果快取(例如使用單例或 memoization),避免在程式執行期間多次遍歷 `checks` 陣列與讀取 `process.env`,可大幅降低 I/O 與 CPU 開銷。", + "is_new": true + }, + { + "level": "warning", + "role": "Zara", + "location": "app/config.js:12", + "suggestion": "改用映射表(Map)或物件儲存 provider 設定,並以 O(1) 方式查找符合條件的項目,減少每次呼叫時的線性掃描成本。", + "is_new": true + }, + { + "level": "warning", + "role": "Rex", + "location": ".gitea/workflows/review.yaml:33-35", + "suggestion": "工作流程目前授予 contents、pull‑requests、issues 三項 write 權限過於寬鬆。建議依實際需求僅授予 read 或最小必要的 write 權限,降低被濫用的風險。", + "is_new": true + }, + { + "level": "warning", + "role": "Rex", + "location": ".gitea/workflows/review.yaml:33", + "suggestion": "將 OPENAI_API_KEY 參數改為使用正確的 secret 名稱(如 OPENROUTER_API_KEY)時,請確保工作流程檔案中不會同時暴露兩個不同的 secret 名稱,以免因名稱錯誤導致金鑰未傳入或意外洩漏。", + "is_new": true + }, + { + "level": "warning", + "role": "Rex", + "location": ".gitea/ai-review/exclusions.json", + "suggestion": "JSON 中加入了 role 欄位,但目前的 schema 只接受 location 與 suggestion,會導致驗證失敗。請移除不必要的 role 欄位或更新 schema,以避免程式在載入時拋出錯誤。", + "is_new": true + }, + { + "level": "warning", + "role": "Rex", + "location": "app/config.js:12", + "suggestion": "在判斷 API 金鑰是否有效時,應先檢查 key 是否為非空字串,避免空字串被誤認為有效金鑰而發送請求。可改為 `if (key && key.trim() && baseURL) …`。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": ".gitea/ai-review/exclusions.json:7", + "suggestion": "檔案中出現兩筆相同的 GITEA_TOKEN 建議移除重複項目,保持唯一性。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": ".gitea/ai-review/exclusions.json", + "suggestion": "檔案最後缺少換行符號,請在檔案結尾加入一個空白換行,以符合 POSIX 標準。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": ".gitea/ai-review/findings.json", + "suggestion": "檔案最後缺少換行符號,請在檔案結尾加入一個空白換行。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": "app/config.js:12-13", + "suggestion": "`checks` 陣列每一行超過 120 個字元,請拆成多行並對齊欄位,以符合常見的行長限制。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": "app/config.js:13", + "suggestion": "`amazonq` 那一行的空格對齊與其他項目不一致,請統一使用單一個空格分隔欄位或使用對齊工具保持列的垂直對齊。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": "app/config.test.js", + "suggestion": "檔案最後缺少換行符號,請在檔案結尾加入空白換行。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": "README.md:28", + "suggestion": "文件中章節編號不連續(例如 `### 2. OpenRouter` 後直接跳到 `### 3. Anthropic Claude`),請重新編號或使用自動編號方式,保持編號連續。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": "README.md:42", + "suggestion": "標題層級使用不一致,部分章節使用 `### 1.`、`### 2.`,而後面的章節直接跳到 `### 3.`,建議統一使用相同層級的 Markdown 標題,並在每個標題後留一個空行以提升可讀性。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": "action.yaml:72-99", + "suggestion": "大量移除的輸入欄位留下多行空白與註解,請整理檔案結構,移除不必要的空行與註解,保持檔案整潔。", "is_new": true }, { @@ -88,55 +228,111 @@ "role": "Leo", "location": "app/findings.js:150", "suggestion": "在 `filterFalsePositivesWithAI` 中,當 AI 回傳空陣列時拋出錯誤,可能導致上層流程中斷。建議改為回傳原始 `findings` 或提供更具體的錯誤類別,並在文件中說明此行為,以提升錯誤處理的可預測性。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Zara", "location": "app/findings.js", "suggestion": "在 filterFalsePositivesWithAI 中,若 AI 回傳的陣列非常大,建議在回傳前加入分頁或限制筆數,避免一次性載入過多資料導致記憶體使用量激增。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Zara", "location": "app/config.js", "suggestion": "對於未設定 API 金鑰的 provider(如 ollama),可提前返回快取的預設設定,避免每次呼叫都進行條件檢查。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Rex", "location": "README.md:28", "suggestion": "文件中列出的權限說明應與實際 workflow 中的 permissions 保持一致,並提醒使用者僅在必要時授予 write 權限,避免在 CI/CD 環境中過度授權。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Rex", "location": "app/config.js:12", "suggestion": "在取得 LLM 設定時,若 key 為空字串仍會被視為有效。建議在判斷前先檢查 key 是否為非空字串,以防止意外使用空的 API 金鑰發送請求。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Aria", "location": "app/config.test.js", "suggestion": "匯入語句過長,建議改寫為多行匯入,例如:\n```js\nimport {\n describe,\n it,\n beforeEach,\n afterEach\n} from 'node:test';\n```", - "is_new": true + "is_new": false }, { "level": "info", "role": "Aria", "location": "app/config.test.js", "suggestion": "`ENV_KEYS` 陣列過長,建議分行列舉,每行放置一個環境變數,以提升可讀性。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Aria", "location": "app/findings.js", "suggestion": "錯誤訊息 `AI 回傳空陣列或非陣列` 可簡化為 `AI 回傳的結果不是有效的非空陣列`,讓訊息更清晰。", + "is_new": false + }, + { + "level": "info", + "role": "Leo", + "location": "app/config.js:12", + "suggestion": "`checks` 陣列的每一行過長,超過 120 個字元,建議拆成多行並對齊欄位,以符合程式碼可讀性與行長限制的慣例。", + "is_new": true + }, + { + "level": "info", + "role": "Leo", + "location": "app/findings.js:151", + "suggestion": "函式內使用 `console.log` 輸出過濾結果,可能在正式環境產生過多日誌。建議改以 logger 並提供可設定的 log level,或在非除錯模式下移除此輸出。", + "is_new": true + }, + { + "level": "info", + "role": "Leo", + "location": "app/config.test.js:1", + "suggestion": "匯入語句過長,建議改寫為多行匯入,以提升可讀性,例如:\n```js\nimport {\n describe,\n it,\n beforeEach,\n afterEach\n} from 'node:test';\n```", + "is_new": true + }, + { + "level": "info", + "role": "Leo", + "location": "app/config.test.js:9", + "suggestion": "`ENV_KEYS` 陣列過長,建議分行列舉,每行放置一個環境變數,以提升可讀性與維護性。", + "is_new": true + }, + { + "level": "info", + "role": "Zara", + "location": "app/findings.js:149", + "suggestion": "在 `filterFalsePositivesWithAI` 中,若 AI 回傳的陣列可能非常大,建議加入分頁或筆數上限(例如一次只處理前 500 筆),避免一次性載入過多資料導致記憶體使用激增。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "app/config.test.js:1", + "suggestion": "匯入語句過長,建議改寫為多行匯入,例如:\n```js\nimport {\n describe,\n it,\n beforeEach,\n afterEach\n} from 'node:test';\n```", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "app/config.test.js:7", + "suggestion": "`ENV_KEYS` 陣列過長,建議每行放置一個環境變數,分行列舉以提升可讀性。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": ".gitea/workflows/review.yaml:33", + "suggestion": "在 `OPENAI_API_KEY` 後的註解前保留一個空格,以符合常見的 YAML 註解風格:`... ${{ secrets.OPENROUTER_API_KEY }} # OpenRouter 使用 OpenAI 相容介面,以 OPENAI_API_KEY 傳入`。", "is_new": true } ] \ No newline at end of file From 33c0357165c22e8d455ae2ca4bfbbef91618015e Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 04:05:49 +0000 Subject: [PATCH 126/174] fix: update suggestions in exclusions.json for clarity and accuracy --- .gitea/ai-review/exclusions.json | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 21dc2dc..7d8545c 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -14,18 +14,19 @@ "suggestion": "contents: write、pull-requests: write、issues: write 為此 Action 正常運作所必要的權限" }, { - "role": "Leo", "location": "app/config.js", - "suggestion": "getLLMConfig 在找不到任何符合條件的 provider 時會回傳 undefined" + "suggestion": "getLLMConfig" }, { - "role": "Rex", "location": ".gitea/ai-review/exclusions.json", - "suggestion": "GITEA_TOKEN 直接嵌入 URL 的做法" + "suggestion": "GITEA_TOKEN" }, { - "role": "Aria", "location": ".gitea/ai-review/exclusions.json", - "suggestion": "role 欄位會導致驗證失敗" + "suggestion": "role 欄位" + }, + { + "location": "app/findings.js", + "suggestion": "filterFalsePositivesWithAI" } ] From 24ae565e38240322b2573700a7e804c7e9c53cf6 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 04:06:08 +0000 Subject: [PATCH 127/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 131 +++++---------------------------- 1 file changed, 20 insertions(+), 111 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 85bbe32..5c5d93f 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,11 +1,4 @@ [ - { - "level": "critical", - "role": "Leo", - "location": "app/config.js:1", - "suggestion": "`getLLMConfig` 在找不到任何符合條件的 provider 時會回傳 `undefined`,而呼叫端(例如測試)假設會得到一個包含 `provider`、`apiKey`、`baseURL`、`model` 欄位的物件,導致執行時拋出 `TypeError`。請在函式結尾加入預設回傳值,例如 `return { provider: null, apiKey: null, baseURL: null, model: null };`,並在文件中說明此行為。", - "is_new": false - }, { "level": "critical", "role": "Aria", @@ -13,40 +6,12 @@ "suggestion": "新增的條目包含 `role` 欄位,但目前的 JSON schema 只接受 `location` 與 `suggestion`,此欄位會導致驗證失敗,請移除或更新 schema。", "is_new": false }, - { - "level": "critical", - "role": "Leo", - "location": "app/findings.js:149", - "suggestion": "`filterFalsePositivesWithAI` 在 AI 回傳空陣列或非陣列時拋出一般 `Error`,會導致上層流程中斷。建議定義專屬的錯誤類別(例如 `AIResultError`),在空結果時回傳原始 `findings` 或提供可恢復的結果,並於文件說明此行為,以提升錯誤處理的可預測性。", - "is_new": true - }, - { - "level": "critical", - "role": "Rex", - "location": ".gitea/ai-review/exclusions.json:7", - "suggestion": "移除在 exclusions.json 中直接寫入的 GITEA_TOKEN,改以環境變數或 Gitea Secrets 注入方式取得,避免機密資訊硬編碼於檔案中。", - "is_new": true - }, - { - "level": "critical", - "role": "Rex", - "location": "app/config.js:1", - "suggestion": "當找不到符合條件的 provider 時,getLLMConfig 會回傳 undefined,導致呼叫端產生 TypeError。請在函式結尾加入預設回傳值(例如 { provider: null, apiKey: null, baseURL: null, model: null })或拋出明確的錯誤訊息,以避免未處理的例外。", - "is_new": true - }, { "level": "critical", "role": "Aria", "location": ".gitea/ai-review/exclusions.json:12", "suggestion": "移除 `role` 欄位,JSON schema 只接受 `location` 與 `suggestion`,保留這兩個欄位即可,避免驗證失敗。", - "is_new": true - }, - { - "level": "critical", - "role": "Aria", - "location": "app/config.js:1", - "suggestion": "`getLLMConfig` 在找不到任何符合條件的 provider 時會回傳 `undefined`,請在迴圈結束後加入預設回傳值,例如 `return { provider: null, apiKey: null, baseURL: null, model: null };`。", - "is_new": true + "is_new": false }, { "level": "warning", @@ -55,20 +20,6 @@ "suggestion": "目前使用硬編碼的 `checks` 陣列來管理所有 LLM provider,未來若要新增或移除 provider 必須直接修改程式碼,易產生重複與維護負擔。建議將 provider 設定抽離至外部 JSON/YAML 檔或使用可擴充的資料結構,並在程式中載入,提升模組化與可維護性。", "is_new": false }, - { - "level": "warning", - "role": "Leo", - "location": "app/config.js:3", - "suggestion": "為 `getLLMConfig` 加上 JSDoc 或 TypeScript 型別註解,說明回傳物件的結構與每個欄位的意義,方便其他開發者快速了解 API,降低錯誤使用的風險。", - "is_new": false - }, - { - "level": "warning", - "role": "Zara", - "location": "app/config.js", - "suggestion": "將 getLLMConfig 的結果快取(例如使用單例或 memoization),避免在程式執行期間多次重複遍歷 checks 陣列與讀取 process.env,減少不必要的 I/O 開銷。", - "is_new": false - }, { "level": "warning", "role": "Rex", @@ -116,125 +67,90 @@ "role": "Leo", "location": "app/config.js:12", "suggestion": "目前使用硬編碼的 `checks` 陣列管理所有 LLM provider,未來若要新增或移除 provider 必須直接修改程式碼,易產生重複與維護負擔。建議將 provider 設定抽離至外部 JSON/YAML 檔或使用可擴充的資料結構,於程式中載入,以提升模組化與可維護性。", - "is_new": true - }, - { - "level": "warning", - "role": "Zara", - "location": "app/config.js", - "suggestion": "將 `getLLMConfig` 的結果快取(例如使用單例或 memoization),避免在程式執行期間多次遍歷 `checks` 陣列與讀取 `process.env`,可大幅降低 I/O 與 CPU 開銷。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Zara", "location": "app/config.js:12", "suggestion": "改用映射表(Map)或物件儲存 provider 設定,並以 O(1) 方式查找符合條件的項目,減少每次呼叫時的線性掃描成本。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Rex", "location": ".gitea/workflows/review.yaml:33-35", "suggestion": "工作流程目前授予 contents、pull‑requests、issues 三項 write 權限過於寬鬆。建議依實際需求僅授予 read 或最小必要的 write 權限,降低被濫用的風險。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Rex", "location": ".gitea/workflows/review.yaml:33", "suggestion": "將 OPENAI_API_KEY 參數改為使用正確的 secret 名稱(如 OPENROUTER_API_KEY)時,請確保工作流程檔案中不會同時暴露兩個不同的 secret 名稱,以免因名稱錯誤導致金鑰未傳入或意外洩漏。", - "is_new": true - }, - { - "level": "warning", - "role": "Rex", - "location": ".gitea/ai-review/exclusions.json", - "suggestion": "JSON 中加入了 role 欄位,但目前的 schema 只接受 location 與 suggestion,會導致驗證失敗。請移除不必要的 role 欄位或更新 schema,以避免程式在載入時拋出錯誤。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Rex", "location": "app/config.js:12", "suggestion": "在判斷 API 金鑰是否有效時,應先檢查 key 是否為非空字串,避免空字串被誤認為有效金鑰而發送請求。可改為 `if (key && key.trim() && baseURL) …`。", - "is_new": true - }, - { - "level": "warning", - "role": "Aria", - "location": ".gitea/ai-review/exclusions.json:7", - "suggestion": "檔案中出現兩筆相同的 GITEA_TOKEN 建議移除重複項目,保持唯一性。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Aria", "location": ".gitea/ai-review/exclusions.json", "suggestion": "檔案最後缺少換行符號,請在檔案結尾加入一個空白換行,以符合 POSIX 標準。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Aria", "location": ".gitea/ai-review/findings.json", "suggestion": "檔案最後缺少換行符號,請在檔案結尾加入一個空白換行。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Aria", "location": "app/config.js:12-13", "suggestion": "`checks` 陣列每一行超過 120 個字元,請拆成多行並對齊欄位,以符合常見的行長限制。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Aria", "location": "app/config.js:13", "suggestion": "`amazonq` 那一行的空格對齊與其他項目不一致,請統一使用單一個空格分隔欄位或使用對齊工具保持列的垂直對齊。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Aria", "location": "app/config.test.js", "suggestion": "檔案最後缺少換行符號,請在檔案結尾加入空白換行。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Aria", "location": "README.md:28", "suggestion": "文件中章節編號不連續(例如 `### 2. OpenRouter` 後直接跳到 `### 3. Anthropic Claude`),請重新編號或使用自動編號方式,保持編號連續。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Aria", "location": "README.md:42", "suggestion": "標題層級使用不一致,部分章節使用 `### 1.`、`### 2.`,而後面的章節直接跳到 `### 3.`,建議統一使用相同層級的 Markdown 標題,並在每個標題後留一個空行以提升可讀性。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Aria", "location": "action.yaml:72-99", "suggestion": "大量移除的輸入欄位留下多行空白與註解,請整理檔案結構,移除不必要的空行與註解,保持檔案整潔。", - "is_new": true - }, - { - "level": "info", - "role": "Leo", - "location": "app/findings.js:150", - "suggestion": "在 `filterFalsePositivesWithAI` 中,當 AI 回傳空陣列時拋出錯誤,可能導致上層流程中斷。建議改為回傳原始 `findings` 或提供更具體的錯誤類別,並在文件中說明此行為,以提升錯誤處理的可預測性。", - "is_new": false - }, - { - "level": "info", - "role": "Zara", - "location": "app/findings.js", - "suggestion": "在 filterFalsePositivesWithAI 中,若 AI 回傳的陣列非常大,建議在回傳前加入分頁或限制筆數,避免一次性載入過多資料導致記憶體使用量激增。", "is_new": false }, { @@ -284,55 +200,48 @@ "role": "Leo", "location": "app/config.js:12", "suggestion": "`checks` 陣列的每一行過長,超過 120 個字元,建議拆成多行並對齊欄位,以符合程式碼可讀性與行長限制的慣例。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Leo", "location": "app/findings.js:151", "suggestion": "函式內使用 `console.log` 輸出過濾結果,可能在正式環境產生過多日誌。建議改以 logger 並提供可設定的 log level,或在非除錯模式下移除此輸出。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Leo", "location": "app/config.test.js:1", "suggestion": "匯入語句過長,建議改寫為多行匯入,以提升可讀性,例如:\n```js\nimport {\n describe,\n it,\n beforeEach,\n afterEach\n} from 'node:test';\n```", - "is_new": true + "is_new": false }, { "level": "info", "role": "Leo", "location": "app/config.test.js:9", "suggestion": "`ENV_KEYS` 陣列過長,建議分行列舉,每行放置一個環境變數,以提升可讀性與維護性。", - "is_new": true - }, - { - "level": "info", - "role": "Zara", - "location": "app/findings.js:149", - "suggestion": "在 `filterFalsePositivesWithAI` 中,若 AI 回傳的陣列可能非常大,建議加入分頁或筆數上限(例如一次只處理前 500 筆),避免一次性載入過多資料導致記憶體使用激增。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Aria", "location": "app/config.test.js:1", "suggestion": "匯入語句過長,建議改寫為多行匯入,例如:\n```js\nimport {\n describe,\n it,\n beforeEach,\n afterEach\n} from 'node:test';\n```", - "is_new": true + "is_new": false }, { "level": "info", "role": "Aria", "location": "app/config.test.js:7", "suggestion": "`ENV_KEYS` 陣列過長,建議每行放置一個環境變數,分行列舉以提升可讀性。", - "is_new": true + "is_new": false }, { "level": "info", "role": "Aria", "location": ".gitea/workflows/review.yaml:33", "suggestion": "在 `OPENAI_API_KEY` 後的註解前保留一個空格,以符合常見的 YAML 註解風格:`... ${{ secrets.OPENROUTER_API_KEY }} # OpenRouter 使用 OpenAI 相容介面,以 OPENAI_API_KEY 傳入`。", - "is_new": true + "is_new": false } ] \ No newline at end of file From 5ef9ab81fff1096357215189ede4df6f180c3ea2 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 04:20:35 +0000 Subject: [PATCH 128/174] fix: enhance suggestions in exclusions.json for clarity and accuracy; update filterFalsePositivesWithAI to accept exclusions --- .gitea/ai-review/exclusions.json | 14 +++++--------- app/findings.js | 23 +++++++++++++++-------- app/main.js | 2 +- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 7d8545c..67fdaaa 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -6,27 +6,23 @@ }, { "location": "app/git.js", - "suggestion": "GITEA_TOKEN 直接嵌入 URL 中" + "suggestion": "GITEA_TOKEN 直接嵌入 URL 中,建議改以環境變數或 Gitea Secrets 注入" }, { "role": "Rex", "location": "README.md", - "suggestion": "contents: write、pull-requests: write、issues: write 為此 Action 正常運作所必要的權限" + "suggestion": "contents: write、pull-requests: write、issues: write 為此 Action 正常運作所必要的權限,無法縮減" }, { "location": "app/config.js", - "suggestion": "getLLMConfig" + "suggestion": "getLLMConfig 在找不到任何符合條件的 provider 時已有預設回傳值 { provider: null, apiKey: null, baseURL: null, model: null },非誤報" }, { "location": ".gitea/ai-review/exclusions.json", - "suggestion": "GITEA_TOKEN" - }, - { - "location": ".gitea/ai-review/exclusions.json", - "suggestion": "role 欄位" + "suggestion": "exclusions.json 是排除規則檔,內容為問題描述字串,不是實際程式碼或 token,role 欄位為有效欄位" }, { "location": "app/findings.js", - "suggestion": "filterFalsePositivesWithAI" + "suggestion": "filterFalsePositivesWithAI 拋出的 Error 會被 catch 攔截並降級回傳原始 findings,不會中斷流程" } ] diff --git a/app/findings.js b/app/findings.js index ad4f245..f381cfa 100644 --- a/app/findings.js +++ b/app/findings.js @@ -117,33 +117,40 @@ export function loadExclusions(workspace) { /** * 套用排除規則,過濾掉符合排除條件的 findings - * 排除條件:role/location/suggestion 皆符合(省略的欄位視為萬用) + * 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 || String(f.location).includes(ex.location)) && - (!ex.suggestion || String(f.suggestion).includes(String(ex.suggestion).slice(0, 20))) - )); + 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); + })); console.log(` 排除過濾: ${before} -> ${filtered.length} 筆(排除 ${before - filtered.length} 筆)`); return filtered; } /** * 呼叫 AI 判斷哪些問題是誤報或不需處理,回傳需保留的 findings + * exclusions 為已知誤報清單,供 AI 參考判斷 * 失敗時降級回傳原始 findings */ -export async function filterFalsePositivesWithAI(findings) { +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)}` + : ''; + const systemPrompt = `你是一位資深程式碼審查專家,負責判斷審查問題是否為誤報或不需處理。 給你一份問題清單(JSON 陣列),每筆包含 level、role、location、suggestion。 請移除以下類型的問題: 1. 誤報:問題描述與實際程式碼不符(例如:程式碼已正確使用環境變數或 secrets,卻被標記為硬編碼敏感資料) 2. 不適用:問題在此專案情境下不需處理(例如:CI/CD action 本來就需要透過環境變數傳遞 token) -只回傳需要保留的問題 JSON 陣列,不要有其他文字。`; +3. 與已知誤報清單語意相近的問題(檔案路徑相同且建議內容相似) +只回傳需要保留的問題 JSON 陣列,不要有其他文字。${exclusionHint}`; const userContent = `請判斷以下問題清單,移除誤報或不需處理的問題:\n\n${JSON.stringify(findings, null, 2)}`; diff --git a/app/main.js b/app/main.js index 7d0eef9..6e58f60 100644 --- a/app/main.js +++ b/app/main.js @@ -85,7 +85,7 @@ async function main() { console.log('\n🚫 Step4: AI 排除問題過濾'); const exclusions = loadExclusions(repoDir || WORKSPACE); const ruleFiltered = applyExclusions(sorted, exclusions); - const filtered = await filterFalsePositivesWithAI(ruleFiltered); + const filtered = await filterFalsePositivesWithAI(ruleFiltered, exclusions); console.log(` Step4 完成: findings total=${filtered.length}`); // Step5: 寫入 findings.json,依序發布 comment From 19efa8a8de1ef010a003a7324e3b84383722b4ca Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 04:20:55 +0000 Subject: [PATCH 129/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 119 --------------------------------- 1 file changed, 119 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 5c5d93f..749743a 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,25 +1,4 @@ [ - { - "level": "critical", - "role": "Aria", - "location": ".gitea/ai-review/exclusions.json", - "suggestion": "新增的條目包含 `role` 欄位,但目前的 JSON schema 只接受 `location` 與 `suggestion`,此欄位會導致驗證失敗,請移除或更新 schema。", - "is_new": false - }, - { - "level": "critical", - "role": "Aria", - "location": ".gitea/ai-review/exclusions.json:12", - "suggestion": "移除 `role` 欄位,JSON schema 只接受 `location` 與 `suggestion`,保留這兩個欄位即可,避免驗證失敗。", - "is_new": false - }, - { - "level": "warning", - "role": "Leo", - "location": "app/config.js:5", - "suggestion": "目前使用硬編碼的 `checks` 陣列來管理所有 LLM provider,未來若要新增或移除 provider 必須直接修改程式碼,易產生重複與維護負擔。建議將 provider 設定抽離至外部 JSON/YAML 檔或使用可擴充的資料結構,並在程式中載入,提升模組化與可維護性。", - "is_new": false - }, { "level": "warning", "role": "Rex", @@ -48,34 +27,6 @@ "suggestion": "文件中章節編號不連續(例如 `### 2. OpenRouter` 後直接跳到 `### 3. Anthropic Claude`),建議重新編號或使用一致的標題層級,以提升可讀性與維護性。", "is_new": false }, - { - "level": "warning", - "role": "Aria", - "location": "app/config.js", - "suggestion": "`checks` 陣列的每一行過長,超過 120 個字元,建議拆成多行並對齊欄位,以符合程式碼可讀性與行長限制的慣例。", - "is_new": false - }, - { - "level": "warning", - "role": "Aria", - "location": "app/config.js", - "suggestion": "`amazonq` 那一行的空格對齊與其他項目不一致,請統一使用單一個空格分隔欄位,或使用對齊工具保持列的垂直對齊。", - "is_new": false - }, - { - "level": "warning", - "role": "Leo", - "location": "app/config.js:12", - "suggestion": "目前使用硬編碼的 `checks` 陣列管理所有 LLM provider,未來若要新增或移除 provider 必須直接修改程式碼,易產生重複與維護負擔。建議將 provider 設定抽離至外部 JSON/YAML 檔或使用可擴充的資料結構,於程式中載入,以提升模組化與可維護性。", - "is_new": false - }, - { - "level": "warning", - "role": "Zara", - "location": "app/config.js:12", - "suggestion": "改用映射表(Map)或物件儲存 provider 設定,並以 O(1) 方式查找符合條件的項目,減少每次呼叫時的線性掃描成本。", - "is_new": false - }, { "level": "warning", "role": "Rex", @@ -90,20 +41,6 @@ "suggestion": "將 OPENAI_API_KEY 參數改為使用正確的 secret 名稱(如 OPENROUTER_API_KEY)時,請確保工作流程檔案中不會同時暴露兩個不同的 secret 名稱,以免因名稱錯誤導致金鑰未傳入或意外洩漏。", "is_new": false }, - { - "level": "warning", - "role": "Rex", - "location": "app/config.js:12", - "suggestion": "在判斷 API 金鑰是否有效時,應先檢查 key 是否為非空字串,避免空字串被誤認為有效金鑰而發送請求。可改為 `if (key && key.trim() && baseURL) …`。", - "is_new": false - }, - { - "level": "warning", - "role": "Aria", - "location": ".gitea/ai-review/exclusions.json", - "suggestion": "檔案最後缺少換行符號,請在檔案結尾加入一個空白換行,以符合 POSIX 標準。", - "is_new": false - }, { "level": "warning", "role": "Aria", @@ -111,20 +48,6 @@ "suggestion": "檔案最後缺少換行符號,請在檔案結尾加入一個空白換行。", "is_new": false }, - { - "level": "warning", - "role": "Aria", - "location": "app/config.js:12-13", - "suggestion": "`checks` 陣列每一行超過 120 個字元,請拆成多行並對齊欄位,以符合常見的行長限制。", - "is_new": false - }, - { - "level": "warning", - "role": "Aria", - "location": "app/config.js:13", - "suggestion": "`amazonq` 那一行的空格對齊與其他項目不一致,請統一使用單一個空格分隔欄位或使用對齊工具保持列的垂直對齊。", - "is_new": false - }, { "level": "warning", "role": "Aria", @@ -153,27 +76,6 @@ "suggestion": "大量移除的輸入欄位留下多行空白與註解,請整理檔案結構,移除不必要的空行與註解,保持檔案整潔。", "is_new": false }, - { - "level": "info", - "role": "Zara", - "location": "app/config.js", - "suggestion": "對於未設定 API 金鑰的 provider(如 ollama),可提前返回快取的預設設定,避免每次呼叫都進行條件檢查。", - "is_new": false - }, - { - "level": "info", - "role": "Rex", - "location": "README.md:28", - "suggestion": "文件中列出的權限說明應與實際 workflow 中的 permissions 保持一致,並提醒使用者僅在必要時授予 write 權限,避免在 CI/CD 環境中過度授權。", - "is_new": false - }, - { - "level": "info", - "role": "Rex", - "location": "app/config.js:12", - "suggestion": "在取得 LLM 設定時,若 key 為空字串仍會被視為有效。建議在判斷前先檢查 key 是否為非空字串,以防止意外使用空的 API 金鑰發送請求。", - "is_new": false - }, { "level": "info", "role": "Aria", @@ -188,27 +90,6 @@ "suggestion": "`ENV_KEYS` 陣列過長,建議分行列舉,每行放置一個環境變數,以提升可讀性。", "is_new": false }, - { - "level": "info", - "role": "Aria", - "location": "app/findings.js", - "suggestion": "錯誤訊息 `AI 回傳空陣列或非陣列` 可簡化為 `AI 回傳的結果不是有效的非空陣列`,讓訊息更清晰。", - "is_new": false - }, - { - "level": "info", - "role": "Leo", - "location": "app/config.js:12", - "suggestion": "`checks` 陣列的每一行過長,超過 120 個字元,建議拆成多行並對齊欄位,以符合程式碼可讀性與行長限制的慣例。", - "is_new": false - }, - { - "level": "info", - "role": "Leo", - "location": "app/findings.js:151", - "suggestion": "函式內使用 `console.log` 輸出過濾結果,可能在正式環境產生過多日誌。建議改以 logger 並提供可設定的 log level,或在非除錯模式下移除此輸出。", - "is_new": false - }, { "level": "info", "role": "Leo", From 58fdbdd96572752c17f53b4a5596d1e478011eea Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 05:37:07 +0000 Subject: [PATCH 130/174] fix: enhance exclusions.json suggestions for clarity; remove unnecessary lines in action.yaml --- .gitea/ai-review/exclusions.json | 15 +++++++++++++++ .gitea/ai-review/findings.json | 2 +- action.yaml | 2 -- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 67fdaaa..7ed4cb0 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -24,5 +24,20 @@ { "location": "app/findings.js", "suggestion": "filterFalsePositivesWithAI 拋出的 Error 會被 catch 攔截並降級回傳原始 findings,不會中斷流程" + }, + { + "role": "Rex", + "location": ".gitea/workflows/review.yaml", + "suggestion": "contents: write、pull-requests: write、issues: write 為此 Action 正常運作所必要的權限,無法縮減" + }, + { + "role": "Rex", + "location": ".gitea/workflows/review.yaml", + "suggestion": "OPENAI_API_KEY 參數傳入的是 OPENROUTER_API_KEY secret,為 OpenRouter 使用 OpenAI 相容介面的正確做法" + }, + { + "role": "Aria", + "location": "README.md", + "suggestion": "章節編號連續且正確,無需調整" } ] diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 749743a..2f08d59 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -125,4 +125,4 @@ "suggestion": "在 `OPENAI_API_KEY` 後的註解前保留一個空格,以符合常見的 YAML 註解風格:`... ${{ secrets.OPENROUTER_API_KEY }} # OpenRouter 使用 OpenAI 相容介面,以 OPENAI_API_KEY 傳入`。", "is_new": false } -] \ No newline at end of file +] diff --git a/action.yaml b/action.yaml index 52ed03a..a1b6c68 100644 --- a/action.yaml +++ b/action.yaml @@ -72,8 +72,6 @@ inputs: description: 'Amazon Q Base URL' required: false - - runs: using: 'docker' image: 'Dockerfile' From 8046708f00acc610379248a170ddf87d822e2c1b Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 05:37:32 +0000 Subject: [PATCH 131/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 51 +--------------------------------- 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 2f08d59..aebeda8 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,18 +1,4 @@ [ - { - "level": "warning", - "role": "Rex", - "location": ".gitea/workflows/review.yaml:33", - "suggestion": "工作流程目前授予 contents、pull‑requests、issues 三項 write 權限,過於寬鬆。建議依實際需求僅授予 read 或最小必要的 write 權限,以降低被濫用的風險。", - "is_new": false - }, - { - "level": "warning", - "role": "Rex", - "location": ".gitea/workflows/review.yaml:35", - "suggestion": "將 OPENAI_API_KEY 參數改為使用正確的 secret 名稱(如 OPENROUTER_API_KEY)時,請確保工作流程文件中不會同時暴露兩個不同的 secret 名稱,以免因名稱錯誤導致金鑰未傳入或意外洩漏。", - "is_new": false - }, { "level": "warning", "role": "Aria", @@ -20,27 +6,6 @@ "suggestion": "在 `OPENAI_API_KEY` 後的註解前應保留一個空格,以符合常見的 YAML 註解風格:`... ${{ secrets.OPENROUTER_API_KEY }} # OpenRouter 使用 OpenAI 相容介面,以 OPENAI_API_KEY 傳入`。", "is_new": false }, - { - "level": "warning", - "role": "Aria", - "location": "README.md", - "suggestion": "文件中章節編號不連續(例如 `### 2. OpenRouter` 後直接跳到 `### 3. Anthropic Claude`),建議重新編號或使用一致的標題層級,以提升可讀性與維護性。", - "is_new": false - }, - { - "level": "warning", - "role": "Rex", - "location": ".gitea/workflows/review.yaml:33-35", - "suggestion": "工作流程目前授予 contents、pull‑requests、issues 三項 write 權限過於寬鬆。建議依實際需求僅授予 read 或最小必要的 write 權限,降低被濫用的風險。", - "is_new": false - }, - { - "level": "warning", - "role": "Rex", - "location": ".gitea/workflows/review.yaml:33", - "suggestion": "將 OPENAI_API_KEY 參數改為使用正確的 secret 名稱(如 OPENROUTER_API_KEY)時,請確保工作流程檔案中不會同時暴露兩個不同的 secret 名稱,以免因名稱錯誤導致金鑰未傳入或意外洩漏。", - "is_new": false - }, { "level": "warning", "role": "Aria", @@ -55,20 +20,6 @@ "suggestion": "檔案最後缺少換行符號,請在檔案結尾加入空白換行。", "is_new": false }, - { - "level": "warning", - "role": "Aria", - "location": "README.md:28", - "suggestion": "文件中章節編號不連續(例如 `### 2. OpenRouter` 後直接跳到 `### 3. Anthropic Claude`),請重新編號或使用自動編號方式,保持編號連續。", - "is_new": false - }, - { - "level": "warning", - "role": "Aria", - "location": "README.md:42", - "suggestion": "標題層級使用不一致,部分章節使用 `### 1.`、`### 2.`,而後面的章節直接跳到 `### 3.`,建議統一使用相同層級的 Markdown 標題,並在每個標題後留一個空行以提升可讀性。", - "is_new": false - }, { "level": "warning", "role": "Aria", @@ -125,4 +76,4 @@ "suggestion": "在 `OPENAI_API_KEY` 後的註解前保留一個空格,以符合常見的 YAML 註解風格:`... ${{ secrets.OPENROUTER_API_KEY }} # OpenRouter 使用 OpenAI 相容介面,以 OPENAI_API_KEY 傳入`。", "is_new": false } -] +] \ No newline at end of file From 6921ca05ec68d2cd330fc050ca5e52292a39194f Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 05:47:09 +0000 Subject: [PATCH 132/174] fix: update AI Code Review step to use GEMINI API instead of OpenRouter --- .gitea/workflows/review.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 932f11f..b2ca93a 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -33,9 +33,9 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} with: - OPENAI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} # OpenRouter 使用 OpenAI 相容介面,以 OPENAI_API_KEY 傳入 - OPENAI_BASE_URL: https://openrouter.ai/api/v1 - OPENAI_MODEL: ${{ vars.OPENROUTER_MODEL }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY_1 }} + GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta + GEMINI_MODEL: ${{ vars.GEMINI_MODEL }} permissions: contents: write pull-requests: write From e88e586ac6072625d2a53416bd8504479380c8ec Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 05:48:20 +0000 Subject: [PATCH 133/174] 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 aebeda8..56387ce 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,4 +1,11 @@ [ + { + "level": "critical", + "role": "Maya", + "location": ".gitea/workflows/review.yaml:35-37 與 action.yaml", + "suggestion": "在 `review.yaml` 工作流程中,您嘗試傳遞 `GEMINI_API_KEY`、`GEMINI_BASE_URL` 和 `GEMINI_MODEL` 參數。然而,根據 `action.yaml` 的定義,此 Action 預期接收的是 `GOOGLE_API_KEY`、`GOOGLE_BASE_URL` 和 `GOOGLE_MODEL`。這導致參數名稱不匹配,Action 將無法正確取得 Gemini 的設定,進而導致功能失效。請修正 `review.yaml`,將參數名稱改為 `GOOGLE_API_KEY`、`GOOGLE_BASE_URL` 和 `GOOGLE_MODEL`,以符合 `action.yaml` 中已定義的 Google 相關輸入。", + "is_new": true + }, { "level": "warning", "role": "Aria", @@ -27,6 +34,20 @@ "suggestion": "大量移除的輸入欄位留下多行空白與註解,請整理檔案結構,移除不必要的空行與註解,保持檔案整潔。", "is_new": false }, + { + "level": "warning", + "role": "Aria", + "location": ".gitea/workflows/review.yaml:36", + "suggestion": "`secrets.GEMINI_API_KEY_1` 的命名方式建議審視。如果這是唯一的 Gemini API 金鑰,可考慮簡化為 `GEMINI_API_KEY`。若有多個金鑰,建議使用更具描述性的後綴(例如 `GEMINI_API_KEY_PRIMARY` 或 `GEMINI_API_KEY_SERVICE_A`),以提升命名清晰度與一致性。", + "is_new": true + }, + { + "level": "warning", + "role": "Maya", + "location": "app/", + "suggestion": "此次變更將 AI 審查服務從 OpenRouter 切換至 Gemini。儘管 `action.yaml` 中已存在 `GOOGLE_*` 相關的輸入,但此 Git Diff 並未包含任何針對 Gemini API 整合的單元測試或整合測試。請確認現有的測試是否足以涵蓋 Gemini API 的特定行為、回應格式以及錯誤處理。若無,建議為 Gemini 整合新增專屬的整合測試,以確保其在實際運作中的穩定性與正確性,特別是針對 `https://generativelanguage.googleapis.com/v1beta` 這個 Base URL 和所選的 `GEMINI_MODEL`。", + "is_new": true + }, { "level": "info", "role": "Aria", @@ -75,5 +96,12 @@ "location": ".gitea/workflows/review.yaml:33", "suggestion": "在 `OPENAI_API_KEY` 後的註解前保留一個空格,以符合常見的 YAML 註解風格:`... ${{ secrets.OPENROUTER_API_KEY }} # OpenRouter 使用 OpenAI 相容介面,以 OPENAI_API_KEY 傳入`。", "is_new": false + }, + { + "level": "info", + "role": "Zara", + "location": ".gitea/workflows/review.yaml:35-37", + "suggestion": "工作流程從 OpenRouter/OpenAI 服務切換至 Google Gemini 服務。雖然這本身不是程式碼錯誤,但不同 AI 服務提供商的 API 響應時間、吞吐量和穩定性可能存在差異。建議監控切換後 AI Code Review 步驟的執行時間,確保新配置能維持或提升效能,並留意潛在的成本變化。", + "is_new": true } ] \ No newline at end of file From bf7b8f843bc0472fb1f9c2cf37267d9bbc957b79 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 05:50:46 +0000 Subject: [PATCH 134/174] fix: add suggestion for GEMINI API parameter consistency in review.yaml --- .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 7ed4cb0..001034e 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -40,4 +40,9 @@ "location": "README.md", "suggestion": "章節編號連續且正確,無需調整" } + { + "role": "Maya", + "location": ".gitea/workflows/review.yaml", + "suggestion": "action.yaml 定義的參數名稱為 GEMINI_API_KEY、GEMINI_BASE_URL、GEMINI_MODEL,與 review.yaml 完全一致,無不匹配問題" + } ] From 95802c422becfc8cbab665a2c832961904a16daf Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 05:54:25 +0000 Subject: [PATCH 135/174] fix: update suggestions in exclusions.json and findings.json for clarity and consistency with GEMINI API --- .gitea/ai-review/exclusions.json | 17 ++++++++++++++++- .gitea/ai-review/findings.json | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 001034e..6d63dac 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -39,10 +39,25 @@ "role": "Aria", "location": "README.md", "suggestion": "章節編號連續且正確,無需調整" - } + }, { "role": "Maya", "location": ".gitea/workflows/review.yaml", "suggestion": "action.yaml 定義的參數名稱為 GEMINI_API_KEY、GEMINI_BASE_URL、GEMINI_MODEL,與 review.yaml 完全一致,無不匹配問題" + }, + { + "role": "Aria", + "location": ".gitea/workflows/review.yaml", + "suggestion": "review.yaml 已改用 Gemini,不再有 OPENAI_API_KEY 行,註解空格問題不存在" + }, + { + "role": "Aria", + "location": "app/config.test.js", + "suggestion": "檔案結尾已有換行符號,import 行長度合理,無需修改" + }, + { + "role": "Aria", + "location": "action.yaml", + "suggestion": "action.yaml 已整理,多餘空行已移除,結構整潔" } ] diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 56387ce..cdcc7e9 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -104,4 +104,4 @@ "suggestion": "工作流程從 OpenRouter/OpenAI 服務切換至 Google Gemini 服務。雖然這本身不是程式碼錯誤,但不同 AI 服務提供商的 API 響應時間、吞吐量和穩定性可能存在差異。建議監控切換後 AI Code Review 步驟的執行時間,確保新配置能維持或提升效能,並留意潛在的成本變化。", "is_new": true } -] \ No newline at end of file +] From e541cee83f59f6917c84a7d7090a72fcca9d8931 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 05:54:43 +0000 Subject: [PATCH 136/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 74 +--------------------------------- 1 file changed, 2 insertions(+), 72 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index cdcc7e9..4bf1a22 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,18 +1,4 @@ [ - { - "level": "critical", - "role": "Maya", - "location": ".gitea/workflows/review.yaml:35-37 與 action.yaml", - "suggestion": "在 `review.yaml` 工作流程中,您嘗試傳遞 `GEMINI_API_KEY`、`GEMINI_BASE_URL` 和 `GEMINI_MODEL` 參數。然而,根據 `action.yaml` 的定義,此 Action 預期接收的是 `GOOGLE_API_KEY`、`GOOGLE_BASE_URL` 和 `GOOGLE_MODEL`。這導致參數名稱不匹配,Action 將無法正確取得 Gemini 的設定,進而導致功能失效。請修正 `review.yaml`,將參數名稱改為 `GOOGLE_API_KEY`、`GOOGLE_BASE_URL` 和 `GOOGLE_MODEL`,以符合 `action.yaml` 中已定義的 Google 相關輸入。", - "is_new": true - }, - { - "level": "warning", - "role": "Aria", - "location": ".gitea/workflows/review.yaml:33", - "suggestion": "在 `OPENAI_API_KEY` 後的註解前應保留一個空格,以符合常見的 YAML 註解風格:`... ${{ secrets.OPENROUTER_API_KEY }} # OpenRouter 使用 OpenAI 相容介面,以 OPENAI_API_KEY 傳入`。", - "is_new": false - }, { "level": "warning", "role": "Aria", @@ -20,46 +6,11 @@ "suggestion": "檔案最後缺少換行符號,請在檔案結尾加入一個空白換行。", "is_new": false }, - { - "level": "warning", - "role": "Aria", - "location": "app/config.test.js", - "suggestion": "檔案最後缺少換行符號,請在檔案結尾加入空白換行。", - "is_new": false - }, - { - "level": "warning", - "role": "Aria", - "location": "action.yaml:72-99", - "suggestion": "大量移除的輸入欄位留下多行空白與註解,請整理檔案結構,移除不必要的空行與註解,保持檔案整潔。", - "is_new": false - }, - { - "level": "warning", - "role": "Aria", - "location": ".gitea/workflows/review.yaml:36", - "suggestion": "`secrets.GEMINI_API_KEY_1` 的命名方式建議審視。如果這是唯一的 Gemini API 金鑰,可考慮簡化為 `GEMINI_API_KEY`。若有多個金鑰,建議使用更具描述性的後綴(例如 `GEMINI_API_KEY_PRIMARY` 或 `GEMINI_API_KEY_SERVICE_A`),以提升命名清晰度與一致性。", - "is_new": true - }, { "level": "warning", "role": "Maya", "location": "app/", "suggestion": "此次變更將 AI 審查服務從 OpenRouter 切換至 Gemini。儘管 `action.yaml` 中已存在 `GOOGLE_*` 相關的輸入,但此 Git Diff 並未包含任何針對 Gemini API 整合的單元測試或整合測試。請確認現有的測試是否足以涵蓋 Gemini API 的特定行為、回應格式以及錯誤處理。若無,建議為 Gemini 整合新增專屬的整合測試,以確保其在實際運作中的穩定性與正確性,特別是針對 `https://generativelanguage.googleapis.com/v1beta` 這個 Base URL 和所選的 `GEMINI_MODEL`。", - "is_new": true - }, - { - "level": "info", - "role": "Aria", - "location": "app/config.test.js", - "suggestion": "匯入語句過長,建議改寫為多行匯入,例如:\n```js\nimport {\n describe,\n it,\n beforeEach,\n afterEach\n} from 'node:test';\n```", - "is_new": false - }, - { - "level": "info", - "role": "Aria", - "location": "app/config.test.js", - "suggestion": "`ENV_KEYS` 陣列過長,建議分行列舉,每行放置一個環境變數,以提升可讀性。", "is_new": false }, { @@ -76,32 +27,11 @@ "suggestion": "`ENV_KEYS` 陣列過長,建議分行列舉,每行放置一個環境變數,以提升可讀性與維護性。", "is_new": false }, - { - "level": "info", - "role": "Aria", - "location": "app/config.test.js:1", - "suggestion": "匯入語句過長,建議改寫為多行匯入,例如:\n```js\nimport {\n describe,\n it,\n beforeEach,\n afterEach\n} from 'node:test';\n```", - "is_new": false - }, - { - "level": "info", - "role": "Aria", - "location": "app/config.test.js:7", - "suggestion": "`ENV_KEYS` 陣列過長,建議每行放置一個環境變數,分行列舉以提升可讀性。", - "is_new": false - }, - { - "level": "info", - "role": "Aria", - "location": ".gitea/workflows/review.yaml:33", - "suggestion": "在 `OPENAI_API_KEY` 後的註解前保留一個空格,以符合常見的 YAML 註解風格:`... ${{ secrets.OPENROUTER_API_KEY }} # OpenRouter 使用 OpenAI 相容介面,以 OPENAI_API_KEY 傳入`。", - "is_new": false - }, { "level": "info", "role": "Zara", "location": ".gitea/workflows/review.yaml:35-37", "suggestion": "工作流程從 OpenRouter/OpenAI 服務切換至 Google Gemini 服務。雖然這本身不是程式碼錯誤,但不同 AI 服務提供商的 API 響應時間、吞吐量和穩定性可能存在差異。建議監控切換後 AI Code Review 步驟的執行時間,確保新配置能維持或提升效能,並留意潛在的成本變化。", - "is_new": true + "is_new": false } -] +] \ No newline at end of file From 4bace91d3d9565d5ba73bcc6db842a3ca0d9bbd7 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 06:03:30 +0000 Subject: [PATCH 137/174] fix: add suggestions for LLM integration tests and code structure improvements in exclusions.json; ensure findings.json is saved with a newline --- .gitea/ai-review/exclusions.json | 10 ++++++++++ app/comments.js | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 6d63dac..4d7d1b6 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -60,4 +60,14 @@ "location": "action.yaml", "suggestion": "action.yaml 已整理,多餘空行已移除,結構整潔" } + { + "role": "Maya", + "location": "app/", + "suggestion": "LLM 整合測試需要真實 API key,不適合加入單元測試,現有測試已涵蓋 config/findings/git 邏輯" + }, + { + "role": "Leo", + "location": "app/config.test.js", + "suggestion": "import 語句長度合理,無需拆分為多行" + } ] diff --git a/app/comments.js b/app/comments.js index 7a349f4..779a177 100644 --- a/app/comments.js +++ b/app/comments.js @@ -21,7 +21,7 @@ function buildTable(findings) { export function saveFindings(workspace, findings) { const fullPath = path.join(workspace, FINDINGS_PATH); fs.mkdirSync(path.dirname(fullPath), { recursive: true }); - fs.writeFileSync(fullPath, JSON.stringify(findings, null, 2), 'utf8'); + fs.writeFileSync(fullPath, JSON.stringify(findings, null, 2) + '\n', 'utf8'); console.log(` ✅ findings 寫入: ${fullPath} (${findings.length} 筆)`); } From 85ff61e98f4e8ac2453cd60bdd10e090fc3e59b9 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 06:03:48 +0000 Subject: [PATCH 138/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 4bf1a22..a218a68 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -34,4 +34,4 @@ "suggestion": "工作流程從 OpenRouter/OpenAI 服務切換至 Google Gemini 服務。雖然這本身不是程式碼錯誤,但不同 AI 服務提供商的 API 響應時間、吞吐量和穩定性可能存在差異。建議監控切換後 AI Code Review 步驟的執行時間,確保新配置能維持或提升效能,並留意潛在的成本變化。", "is_new": false } -] \ No newline at end of file +] From 8d3f5e3a45c456bc5e2ebdb52483346d6367d081 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 06:06:30 +0000 Subject: [PATCH 139/174] fix: update GEMINI_API_KEY in review.yaml to use the correct secret --- .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 b2ca93a..10a3e97 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -33,7 +33,7 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} with: - GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY_1 }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY_2 }} GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta GEMINI_MODEL: ${{ vars.GEMINI_MODEL }} permissions: From 16cb1966f0d5e53beef0bd767f6bdcdc86f47c53 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 06:11:12 +0000 Subject: [PATCH 140/174] fix: update GEMINI_API_KEY in review.yaml to use the correct secret --- .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 10a3e97..b2ca93a 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -33,7 +33,7 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} with: - GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY_2 }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY_1 }} GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta GEMINI_MODEL: ${{ vars.GEMINI_MODEL }} permissions: From b164fe855e7ee4c4d1577cbb35d261940f09e9a1 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 06:15:35 +0000 Subject: [PATCH 141/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 40 +++++++++++++++++----------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index a218a68..4c13c3d 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,4 +1,11 @@ [ + { + "level": "critical", + "role": "Maya", + "location": "app/", + "suggestion": "此次變更將 AI 審查服務從 OpenRouter 切換至 Gemini,涉及不同的 API 端點、請求/回應格式及錯誤處理機制。儘管 `action.yaml` 已配置 Gemini 相關參數,但 Git Diff 中未見針對 Gemini API 整合的單元測試或整合測試。雖然 LLM 整合測試可能不適合傳統的單元測試(因需真實 API key),但這不應排除建立專屬的整合測試。現有測試可能僅涵蓋 `config/findings/git` 的通用邏輯,不足以全面驗證與 Gemini 服務的特定互動,例如請求建構、回應解析、特定錯誤碼處理及邊界條件。強烈建議為 Gemini 整合新增專屬的整合測試,以確保其在實際運作中的穩定性與正確性。", + "is_new": true + }, { "level": "warning", "role": "Aria", @@ -6,32 +13,25 @@ "suggestion": "檔案最後缺少換行符號,請在檔案結尾加入一個空白換行。", "is_new": false }, + { + "level": "warning", + "role": "Leo", + "location": ".gitea/ai-review/findings.json", + "suggestion": "`findings.json` 中重新出現關於 `app/config.test.js` 檔案結尾換行符號及匯入語句長度的警告/資訊,但 `exclusions.json` 卻明確指出這些問題已解決或無需修改。這種矛盾會導致審查流程混亂,降低 AI 審查系統的可靠性。建議釐清並統一 AI 審查的設定或排除規則,確保其一致性,避免重複提出已處理或被排除的問題。", + "is_new": true + }, { "level": "warning", "role": "Maya", - "location": "app/", - "suggestion": "此次變更將 AI 審查服務從 OpenRouter 切換至 Gemini。儘管 `action.yaml` 中已存在 `GOOGLE_*` 相關的輸入,但此 Git Diff 並未包含任何針對 Gemini API 整合的單元測試或整合測試。請確認現有的測試是否足以涵蓋 Gemini API 的特定行為、回應格式以及錯誤處理。若無,建議為 Gemini 整合新增專屬的整合測試,以確保其在實際運作中的穩定性與正確性,特別是針對 `https://generativelanguage.googleapis.com/v1beta` 這個 Base URL 和所選的 `GEMINI_MODEL`。", - "is_new": false + "location": "app/comments.js:24", + "suggestion": "`saveFindings` 函數在寫入 `findings.json` 時新增了換行符號。請確認現有的測試是否已涵蓋此輸出格式的變更,確保測試能夠精確驗證檔案內容,包括結尾的換行符號。若無,建議更新或新增測試案例以確保此行為被正確驗證。", + "is_new": true }, { "level": "info", "role": "Leo", - "location": "app/config.test.js:1", - "suggestion": "匯入語句過長,建議改寫為多行匯入,以提升可讀性,例如:\n```js\nimport {\n describe,\n it,\n beforeEach,\n afterEach\n} from 'node:test';\n```", - "is_new": false - }, - { - "level": "info", - "role": "Leo", - "location": "app/config.test.js:9", - "suggestion": "`ENV_KEYS` 陣列過長,建議分行列舉,每行放置一個環境變數,以提升可讀性與維護性。", - "is_new": false - }, - { - "level": "info", - "role": "Zara", - "location": ".gitea/workflows/review.yaml:35-37", - "suggestion": "工作流程從 OpenRouter/OpenAI 服務切換至 Google Gemini 服務。雖然這本身不是程式碼錯誤,但不同 AI 服務提供商的 API 響應時間、吞吐量和穩定性可能存在差異。建議監控切換後 AI Code Review 步驟的執行時間,確保新配置能維持或提升效能,並留意潛在的成本變化。", - "is_new": false + "location": ".gitea/workflows/review.yaml", + "suggestion": "工作流程已從 OpenRouter/OpenAI 服務切換至 Google Gemini 服務。不同 AI 服務提供商的 API 響應時間、吞吐量和穩定性可能存在差異。建議在切換後監控 AI Code Review 步驟的執行時間,確保新配置能維持或提升效能,並留意潛在的成本變化,以利長期維護與優化。", + "is_new": true } ] From 234a8a829f943d9a93b53d63c8bff725970ce059 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 06:21:24 +0000 Subject: [PATCH 142/174] fix: update suggestion in exclusions.json for LLM integration tests to clarify API key requirements --- .gitea/ai-review/exclusions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 4d7d1b6..215acee 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -63,7 +63,7 @@ { "role": "Maya", "location": "app/", - "suggestion": "LLM 整合測試需要真實 API key,不適合加入單元測試,現有測試已涵蓋 config/findings/git 邏輯" + "suggestion": "LLM 整合測試需要真實 API key 與網路,不適合加入單元測試。llm.js 使用統一 OpenAI 相容介面,Gemini 透過相同介面呼叫,無特殊格式差異,現有測試已涵蓋 config/findings/git 邏輯" }, { "role": "Leo", From 83943b8ddaf0ba52305490c50d22116fb05cfa0d Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 06:24:11 +0000 Subject: [PATCH 143/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 4c13c3d..54d2fb4 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,37 +1,37 @@ [ { "level": "critical", - "role": "Maya", + "role": "Rex", "location": "app/", - "suggestion": "此次變更將 AI 審查服務從 OpenRouter 切換至 Gemini,涉及不同的 API 端點、請求/回應格式及錯誤處理機制。儘管 `action.yaml` 已配置 Gemini 相關參數,但 Git Diff 中未見針對 Gemini API 整合的單元測試或整合測試。雖然 LLM 整合測試可能不適合傳統的單元測試(因需真實 API key),但這不應排除建立專屬的整合測試。現有測試可能僅涵蓋 `config/findings/git` 的通用邏輯,不足以全面驗證與 Gemini 服務的特定互動,例如請求建構、回應解析、特定錯誤碼處理及邊界條件。強烈建議為 Gemini 整合新增專屬的整合測試,以確保其在實際運作中的穩定性與正確性。", + "suggestion": "此次變更將 AI 審查服務從 OpenRouter 切換至 Google Gemini,涉及不同的 API 端點、請求/回應格式及錯誤處理機制。儘管 `action.yaml` 已配置 Gemini 相關參數,但 Git Diff 中未見針對 Gemini API 整合的單元測試或整合測試。缺乏專屬測試可能導致:1. 錯誤解析 Gemini 回應,影響安全審查結果的準確性。2. 未能正確消毒 LLM 輸出,引入潛在的注入風險。3. 錯誤處理 API 異常,導致服務中斷或審查流程失敗。強烈建議為 Gemini 整合新增專屬的整合測試,以確保其在實際運作中的穩定性、正確性及安全性。", "is_new": true }, { - "level": "warning", + "level": "critical", "role": "Aria", - "location": ".gitea/ai-review/findings.json", - "suggestion": "檔案最後缺少換行符號,請在檔案結尾加入一個空白換行。", - "is_new": false - }, - { - "level": "warning", - "role": "Leo", - "location": ".gitea/ai-review/findings.json", - "suggestion": "`findings.json` 中重新出現關於 `app/config.test.js` 檔案結尾換行符號及匯入語句長度的警告/資訊,但 `exclusions.json` 卻明確指出這些問題已解決或無需修改。這種矛盾會導致審查流程混亂,降低 AI 審查系統的可靠性。建議釐清並統一 AI 審查的設定或排除規則,確保其一致性,避免重複提出已處理或被排除的問題。", + "location": ".gitea/ai-review/exclusions.json:47", + "suggestion": "JSON 陣列中的物件之間缺少逗號。在 `action.yaml` 物件的結束大括號 `}` 後面,以及 `app/` 物件的開始大括號 `{` 前面,應補上逗號 `,` 以符合 JSON 語法。", "is_new": true }, { "level": "warning", - "role": "Maya", + "role": "Rex", + "location": ".gitea/ai-review/findings.json", + "suggestion": "`findings.json` 中重新出現關於 `app/config.test.js` 檔案結尾換行符號及匯入語句長度的警告/資訊,但 `exclusions.json` 卻明確指出這些問題已解決或無需修改。這種矛盾會導致 AI 審查系統的可靠性降低,可能導致開發者對審查結果產生混淆或不信任。建議釐清並統一 AI 審查的設定與排除規則,確保 `findings.json` 和 `exclusions.json` 之間的一致性,避免重複提出已處理或被排除的問題。", + "is_new": true + }, + { + "level": "warning", + "role": "Rex", "location": "app/comments.js:24", - "suggestion": "`saveFindings` 函數在寫入 `findings.json` 時新增了換行符號。請確認現有的測試是否已涵蓋此輸出格式的變更,確保測試能夠精確驗證檔案內容,包括結尾的換行符號。若無,建議更新或新增測試案例以確保此行為被正確驗證。", + "suggestion": "`saveFindings` 函數在寫入 `findings.json` 時新增了換行符號。此輸出格式的變更可能影響依賴此檔案的下游系統(例如解析器、報告工具)。若下游系統未預期此額外換行符號,可能導致解析錯誤、資料處理異常或安全審查結果無法正確呈現。請確認現有的測試是否已涵蓋此輸出格式的變更,並驗證所有依賴此檔案的下游系統都能正確解析,以確保資料完整性及流程穩定性。", "is_new": true }, { "level": "info", - "role": "Leo", + "role": "Rex", "location": ".gitea/workflows/review.yaml", - "suggestion": "工作流程已從 OpenRouter/OpenAI 服務切換至 Google Gemini 服務。不同 AI 服務提供商的 API 響應時間、吞吐量和穩定性可能存在差異。建議在切換後監控 AI Code Review 步驟的執行時間,確保新配置能維持或提升效能,並留意潛在的成本變化,以利長期維護與優化。", + "suggestion": "工作流程已從 OpenRouter/OpenAI 服務切換至 Google Gemini 服務。不同 AI 服務提供商的 API 響應時間、吞吐量和穩定性可能存在差異。建議在切換後監控 AI Code Review 步驟的執行時間、API 穩定性及相關成本,確保新配置能維持或提升效能,並留意潛在的成本變化,以利長期維護與優化,這也是確保服務持續可用性與資源合理利用的重要環節。", "is_new": true } ] From e3596eb710bfeae2f892b4a36f1db416e969a2f9 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 06:28:34 +0000 Subject: [PATCH 144/174] fix: add duplicate suggestion for LLM integration tests in exclusions.json --- .gitea/ai-review/exclusions.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 215acee..bf1909c 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -59,12 +59,17 @@ "role": "Aria", "location": "action.yaml", "suggestion": "action.yaml 已整理,多餘空行已移除,結構整潔" - } + }, { "role": "Maya", "location": "app/", "suggestion": "LLM 整合測試需要真實 API key 與網路,不適合加入單元測試。llm.js 使用統一 OpenAI 相容介面,Gemini 透過相同介面呼叫,無特殊格式差異,現有測試已涵蓋 config/findings/git 邏輯" }, + { + "role": "Rex", + "location": "app/", + "suggestion": "LLM 整合測試需要真實 API key 與網路,不適合加入單元測試。llm.js 使用統一 OpenAI 相容介面,Gemini 透過相同介面呼叫,無特殊格式差異" + }, { "role": "Leo", "location": "app/config.test.js", From fdcb9f04deb888984d50b22654ecf48486974ad9 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 06:29:43 +0000 Subject: [PATCH 145/174] fix: update suggestions in exclusions.json to clarify handling of findings and LLM service provider maintenance --- .gitea/ai-review/exclusions.json | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index bf1909c..6415095 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -71,8 +71,23 @@ "suggestion": "LLM 整合測試需要真實 API key 與網路,不適合加入單元測試。llm.js 使用統一 OpenAI 相容介面,Gemini 透過相同介面呼叫,無特殊格式差異" }, { - "role": "Leo", + "role": "Rex", "location": "app/config.test.js", "suggestion": "import 語句長度合理,無需拆分為多行" + }, + { + "role": "Rex", + "location": ".gitea/ai-review/findings.json", + "suggestion": "findings.json 重複問題由 AI 去重與排除機制處理,不是程式碼問題" + }, + { + "role": "Rex", + "location": "app/comments.js", + "suggestion": "JSON 結尾換行符號為標準做法,不影響任何 JSON 解析器,無相容性問題" + }, + { + "role": "Rex", + "location": ".gitea/workflows/review.yaml", + "suggestion": "切換 LLM 服務提供商的維護建議屬過度謹慎,不是實際程式碼問題" } ] From e3b4c7f8d4c2dc30801f2de5c51b3bacc093ed09 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 06:32:21 +0000 Subject: [PATCH 146/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 39 ++++++++++++++-------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 54d2fb4..e978562 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,37 +1,30 @@ [ - { - "level": "critical", - "role": "Rex", - "location": "app/", - "suggestion": "此次變更將 AI 審查服務從 OpenRouter 切換至 Google Gemini,涉及不同的 API 端點、請求/回應格式及錯誤處理機制。儘管 `action.yaml` 已配置 Gemini 相關參數,但 Git Diff 中未見針對 Gemini API 整合的單元測試或整合測試。缺乏專屬測試可能導致:1. 錯誤解析 Gemini 回應,影響安全審查結果的準確性。2. 未能正確消毒 LLM 輸出,引入潛在的注入風險。3. 錯誤處理 API 異常,導致服務中斷或審查流程失敗。強烈建議為 Gemini 整合新增專屬的整合測試,以確保其在實際運作中的穩定性、正確性及安全性。", - "is_new": true - }, - { - "level": "critical", - "role": "Aria", - "location": ".gitea/ai-review/exclusions.json:47", - "suggestion": "JSON 陣列中的物件之間缺少逗號。在 `action.yaml` 物件的結束大括號 `}` 後面,以及 `app/` 物件的開始大括號 `{` 前面,應補上逗號 `,` 以符合 JSON 語法。", - "is_new": true - }, { "level": "warning", - "role": "Rex", + "role": "Leo", "location": ".gitea/ai-review/findings.json", - "suggestion": "`findings.json` 中重新出現關於 `app/config.test.js` 檔案結尾換行符號及匯入語句長度的警告/資訊,但 `exclusions.json` 卻明確指出這些問題已解決或無需修改。這種矛盾會導致 AI 審查系統的可靠性降低,可能導致開發者對審查結果產生混淆或不信任。建議釐清並統一 AI 審查的設定與排除規則,確保 `findings.json` 和 `exclusions.json` 之間的一致性,避免重複提出已處理或被排除的問題。", + "suggestion": "`findings.json` 中重新出現關於 `app/config.test.js` 檔案結尾換行符號及匯入語句長度的警告/資訊,但 `exclusions.json` 卻明確指出這些問題已解決或無需修改。這種矛盾會導致 AI 審查系統的可靠性降低,可能導致開發者對審查結果產生混淆或不信任。建議釐清並統一 AI 審查的設定與排除規則,確保 `findings.json` 和 `exclusions.json` 之間的一致性,避免重複提出已處理或被排除的問題,以提升審查系統的長期可維護性。", "is_new": true }, { "level": "warning", - "role": "Rex", - "location": "app/comments.js:24", - "suggestion": "`saveFindings` 函數在寫入 `findings.json` 時新增了換行符號。此輸出格式的變更可能影響依賴此檔案的下游系統(例如解析器、報告工具)。若下游系統未預期此額外換行符號,可能導致解析錯誤、資料處理異常或安全審查結果無法正確呈現。請確認現有的測試是否已涵蓋此輸出格式的變更,並驗證所有依賴此檔案的下游系統都能正確解析,以確保資料完整性及流程穩定性。", + "role": "Zara", + "location": ".gitea/ai-review/findings.json", + "suggestion": "`findings.json` 中重新出現關於 `app/config.test.js` 檔案結尾換行符號及匯入語句長度的警告/資訊,但 `exclusions.json` 卻明確指出這些問題已解決或無需修改。這種矛盾會導致 AI 審查系統的可靠性降低,可能導致開發者對審查結果產生混淆或不信任。建議釐清並統一 AI 審查的設定與排除規則,確保 `findings.json` 和 `exclusions.json` 之間的一致性,避免重複提出已處理或被排除的問題,以提升審查流程的效率與開發者體驗。", "is_new": true }, { - "level": "info", - "role": "Rex", - "location": ".gitea/workflows/review.yaml", - "suggestion": "工作流程已從 OpenRouter/OpenAI 服務切換至 Google Gemini 服務。不同 AI 服務提供商的 API 響應時間、吞吐量和穩定性可能存在差異。建議在切換後監控 AI Code Review 步驟的執行時間、API 穩定性及相關成本,確保新配置能維持或提升效能,並留意潛在的成本變化,以利長期維護與優化,這也是確保服務持續可用性與資源合理利用的重要環節。", + "level": "warning", + "role": "Aria", + "location": ".gitea/ai-review/findings.json", + "suggestion": "在 `.gitea/ai-review/findings.json` 中,關於 `app/config.test.js` 檔案結尾換行符號及匯入語句長度的警告/資訊,與 `.gitea/ai-review/exclusions.json` 中明確的排除規則相矛盾。這種不一致性會降低 AI 審查系統的可靠性與可讀性,並可能導致開發者混淆。建議審查並統一這兩個檔案的內容,確保排除規則被正確應用,避免重複報告已處理或已排除的問題。", + "is_new": true + }, + { + "level": "warning", + "role": "Maya", + "location": ".gitea/ai-review/findings.json", + "suggestion": "`findings.json` 中重新出現關於 `app/config.test.js` 檔案結尾換行符號及匯入語句長度的警告/資訊,但 `exclusions.json` 卻明確指出這些問題已解決或無需修改。這種矛盾會導致 AI 審查系統的可靠性降低,可能導致開發者對審查結果產生混淆或不信任。建議釐清並統一 AI 審查的設定與排除規則,確保 `findings.json` 和 `exclusions.json` 之間的一致性,避免重複提出已處理或被排除的問題,以提升審查品質與開發者體驗。", "is_new": true } ] From 8a8612b46d8f05e9c246523feef5e8585ee7a87e Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 06:46:51 +0000 Subject: [PATCH 147/174] fix: update findings.json to remove redundant warnings and ensure consistency with exclusions.json --- .gitea/ai-review/exclusions.json | 4 ++++ .gitea/ai-review/findings.json | 28 ---------------------------- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 6415095..e081e36 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -85,6 +85,10 @@ "location": "app/comments.js", "suggestion": "JSON 結尾換行符號為標準做法,不影響任何 JSON 解析器,無相容性問題" }, + { + "location": ".gitea/ai-review/findings.json", + "suggestion": "findings.json 是自動產生的問題記錄檔,不應對其內容提出審查問題" + }, { "role": "Rex", "location": ".gitea/workflows/review.yaml", diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index e978562..0d4f101 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,30 +1,2 @@ [ - { - "level": "warning", - "role": "Leo", - "location": ".gitea/ai-review/findings.json", - "suggestion": "`findings.json` 中重新出現關於 `app/config.test.js` 檔案結尾換行符號及匯入語句長度的警告/資訊,但 `exclusions.json` 卻明確指出這些問題已解決或無需修改。這種矛盾會導致 AI 審查系統的可靠性降低,可能導致開發者對審查結果產生混淆或不信任。建議釐清並統一 AI 審查的設定與排除規則,確保 `findings.json` 和 `exclusions.json` 之間的一致性,避免重複提出已處理或被排除的問題,以提升審查系統的長期可維護性。", - "is_new": true - }, - { - "level": "warning", - "role": "Zara", - "location": ".gitea/ai-review/findings.json", - "suggestion": "`findings.json` 中重新出現關於 `app/config.test.js` 檔案結尾換行符號及匯入語句長度的警告/資訊,但 `exclusions.json` 卻明確指出這些問題已解決或無需修改。這種矛盾會導致 AI 審查系統的可靠性降低,可能導致開發者對審查結果產生混淆或不信任。建議釐清並統一 AI 審查的設定與排除規則,確保 `findings.json` 和 `exclusions.json` 之間的一致性,避免重複提出已處理或被排除的問題,以提升審查流程的效率與開發者體驗。", - "is_new": true - }, - { - "level": "warning", - "role": "Aria", - "location": ".gitea/ai-review/findings.json", - "suggestion": "在 `.gitea/ai-review/findings.json` 中,關於 `app/config.test.js` 檔案結尾換行符號及匯入語句長度的警告/資訊,與 `.gitea/ai-review/exclusions.json` 中明確的排除規則相矛盾。這種不一致性會降低 AI 審查系統的可靠性與可讀性,並可能導致開發者混淆。建議審查並統一這兩個檔案的內容,確保排除規則被正確應用,避免重複報告已處理或已排除的問題。", - "is_new": true - }, - { - "level": "warning", - "role": "Maya", - "location": ".gitea/ai-review/findings.json", - "suggestion": "`findings.json` 中重新出現關於 `app/config.test.js` 檔案結尾換行符號及匯入語句長度的警告/資訊,但 `exclusions.json` 卻明確指出這些問題已解決或無需修改。這種矛盾會導致 AI 審查系統的可靠性降低,可能導致開發者對審查結果產生混淆或不信任。建議釐清並統一 AI 審查的設定與排除規則,確保 `findings.json` 和 `exclusions.json` 之間的一致性,避免重複提出已處理或被排除的問題,以提升審查品質與開發者體驗。", - "is_new": true - } ] From 7578bee5d32da547d0603a1d8c636ae5370596ff Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 06:47:39 +0000 Subject: [PATCH 148/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 0d4f101..64cba33 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,2 +1,9 @@ [ + { + "level": "warning", + "role": "Zara", + "location": "app/comments.js:24", + "suggestion": "在 `saveFindings` 函數中,`fs.writeFileSync` 是一個同步操作。如果 `findings` 陣列可能非常大,或者此函數會被頻繁呼叫,同步寫入檔案可能會阻塞 Node.js 事件迴圈,導致應用程式響應變慢。建議改用 `fs.writeFile` (非同步) 以避免阻塞主執行緒,提升應用程式的響應能力。", + "is_new": true + } ] From 50f422f0d39de034f6610130972caea95167eb8d Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 06:49:07 +0000 Subject: [PATCH 149/174] 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 64cba33..bbf0185 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -4,6 +4,34 @@ "role": "Zara", "location": "app/comments.js:24", "suggestion": "在 `saveFindings` 函數中,`fs.writeFileSync` 是一個同步操作。如果 `findings` 陣列可能非常大,或者此函數會被頻繁呼叫,同步寫入檔案可能會阻塞 Node.js 事件迴圈,導致應用程式響應變慢。建議改用 `fs.writeFile` (非同步) 以避免阻塞主執行緒,提升應用程式的響應能力。", + "is_new": false + }, + { + "level": "warning", + "role": "Aria", + "location": ".gitea/workflows/master.yaml", + "suggestion": "檔案結尾應包含一個換行符號 (newline at EOF),這是 POSIX 系統的慣例,有助於版本控制系統的正確處理。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": "Dockerfile", + "suggestion": "檔案結尾應包含一個換行符號 (newline at EOF),這是 POSIX 系統的慣例,有助於版本控制系統的正確處理。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": "TODO.md", + "suggestion": "檔案結尾應包含一個換行符號 (newline at EOF),這是 POSIX 系統的慣例,有助於版本控制系統的正確處理。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": "entrypoint.sh", + "suggestion": "檔案結尾應包含一個換行符號 (newline at EOF),這是 POSIX 系統的慣例,有助於版本控制系統的正確處理。", "is_new": true } ] From 1b7aeedfdb3c827360cc08e55e4c43a1f4b3dd04 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 07:09:20 +0000 Subject: [PATCH 150/174] feat: support multiple API keys for LLM providers, allowing automatic key rotation on failure --- .gitea/workflows/review.yaml | 2 +- README.md | 11 ++++++----- TODO.md | 5 +++++ app/config.js | 22 ++++++++++++++-------- app/config.test.js | 13 ++++++++++--- app/llm.js | 30 +++++++++++++++++++----------- 6 files changed, 55 insertions(+), 28 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index b2ca93a..6fbcb0b 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -33,7 +33,7 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} with: - GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY_1 }} + 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 }} GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta GEMINI_MODEL: ${{ vars.GEMINI_MODEL }} permissions: diff --git a/README.md b/README.md index dd5d383..745001c 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ 3. Comment 加上些許 emoji 讓資訊有點活力 4. 盡量將應用程式放在 ./app,修改 entrypoint.sh 與 Dockerfile 讓程式可以正常運行 5. 將提示詞放到 ./app/prompts 內供程式讀取 +6. API Key 支援逗號分隔傳入多個,依序嘗試,失敗時自動換下一個,全部失敗則 exit 1 # 使用說明 @@ -42,7 +43,7 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} with: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} # 支援逗號分隔多個 Key OPENAI_BASE_URL: https://api.openai.com/v1 OPENAI_MODEL: ${{ vars.OPENAI_MODEL }} permissions: @@ -65,7 +66,7 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} with: - OPENAI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} # OpenRouter 使用 OpenAI 相容介面,以 OPENAI_API_KEY 傳入 + OPENAI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} # OpenRouter 使用 OpenAI 相容介面,以 OPENAI_API_KEY 傳入,支援逗號分隔多個 Key OPENAI_BASE_URL: https://openrouter.ai/api/v1 OPENAI_MODEL: ${{ vars.OPENROUTER_MODEL }} permissions: @@ -88,7 +89,7 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} with: - CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} + CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} # 支援逗號分隔多個 Key CLAUDE_BASE_URL: https://api.anthropic.com/v1 permissions: contents: write @@ -110,7 +111,7 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} with: - GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + 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 }} GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta GEMINI_MODEL: ${{ vars.GEMINI_MODEL }} permissions: @@ -133,7 +134,7 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} with: - AMAZONQ_API_KEY: ${{ secrets.AMAZONQ_API_KEY }} + AMAZONQ_API_KEY: ${{ secrets.AMAZONQ_API_KEY }} # 支援逗號分隔多個 Key AMAZONQ_BASE_URL: https://q.api.aws permissions: contents: write diff --git a/TODO.md b/TODO.md index 540a5c0..c605eac 100644 --- a/TODO.md +++ b/TODO.md @@ -35,6 +35,11 @@ - 驗收: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/config.js b/app/config.js index 3dc350e..c8212e3 100644 --- a/app/config.js +++ b/app/config.js @@ -8,16 +8,22 @@ 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'; +/** 將逗號分隔的 API key 字串拆成陣列 */ +function splitKeys(value) { + if (!value) return []; + return value.split(',').map(k => k.trim()).filter(Boolean); +} + export function getLLMConfig() { const checks = [ - ['openai', process.env.OPENAI_API_KEY, process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1', process.env.OPENAI_MODEL || 'gpt-4o-mini'], - ['claude', process.env.CLAUDE_API_KEY, process.env.CLAUDE_BASE_URL || 'https://api.anthropic.com/v1', process.env.CLAUDE_MODEL || 'claude-3-haiku-20240307'], - ['gemini', process.env.GEMINI_API_KEY, process.env.GEMINI_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta', process.env.GEMINI_MODEL || 'gemini-2.5-flash'], - ['ollama', 'ollama', process.env.OLLAMA_BASE_URL, process.env.OLLAMA_MODEL], - ['amazonq', process.env.AMAZONQ_API_KEY, process.env.AMAZONQ_BASE_URL || 'https://q.api.aws', process.env.AMAZONQ_MODEL || 'amazon-q'], + ['openai', splitKeys(process.env.OPENAI_API_KEY), process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1', process.env.OPENAI_MODEL || 'gpt-4o-mini'], + ['claude', splitKeys(process.env.CLAUDE_API_KEY), process.env.CLAUDE_BASE_URL || 'https://api.anthropic.com/v1', process.env.CLAUDE_MODEL || 'claude-3-haiku-20240307'], + ['gemini', splitKeys(process.env.GEMINI_API_KEY), process.env.GEMINI_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta', process.env.GEMINI_MODEL || 'gemini-2.5-flash'], + ['ollama', ['ollama'], process.env.OLLAMA_BASE_URL, process.env.OLLAMA_MODEL], + ['amazonq', splitKeys(process.env.AMAZONQ_API_KEY), process.env.AMAZONQ_BASE_URL || 'https://q.api.aws', process.env.AMAZONQ_MODEL || 'amazon-q'], ]; - for (const [provider, key, baseURL, model] of checks) { - if (key && baseURL) return { provider, apiKey: key, baseURL, model }; + for (const [provider, apiKeys, baseURL, model] of checks) { + if (apiKeys.length > 0 && baseURL) return { provider, apiKeys, baseURL, model }; } - return { provider: null, apiKey: null, baseURL: null, model: null }; + return { provider: null, apiKeys: [], baseURL: null, model: null }; } diff --git a/app/config.test.js b/app/config.test.js index b757cec..03cf9ac 100644 --- a/app/config.test.js +++ b/app/config.test.js @@ -26,14 +26,14 @@ describe('getLLMConfig', () => { it('returns null provider when no env vars set', () => { const cfg = getLLMConfig(); assert.equal(cfg.provider, null); - assert.equal(cfg.apiKey, null); + assert.deepEqual(cfg.apiKeys, []); }); it('detects openai with defaults', () => { process.env.OPENAI_API_KEY = 'sk-test'; const cfg = getLLMConfig(); assert.equal(cfg.provider, 'openai'); - assert.equal(cfg.apiKey, 'sk-test'); + assert.deepEqual(cfg.apiKeys, ['sk-test']); assert.equal(cfg.baseURL, 'https://api.openai.com/v1'); assert.equal(cfg.model, 'gpt-4o-mini'); }); @@ -48,7 +48,14 @@ describe('getLLMConfig', () => { assert.equal(cfg.model, 'gpt-4o'); }); - it('detects gemini with defaults', () => { + it('detects gemini with comma-separated keys, picks one', () => { + process.env.GEMINI_API_KEY = 'key1,key2,key3'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'gemini'); + assert.deepEqual(cfg.apiKeys, ['key1', 'key2', 'key3']); + }); + + it('detects gemini with single key (no comma)', () => { process.env.GEMINI_API_KEY = 'gemini-key'; const cfg = getLLMConfig(); assert.equal(cfg.provider, 'gemini'); diff --git a/app/llm.js b/app/llm.js index db7217b..dddce5f 100644 --- a/app/llm.js +++ b/app/llm.js @@ -5,23 +5,31 @@ import { getLLMConfig } from './config.js'; const httpsAgent = new https.Agent({ rejectUnauthorized: false }); export async function chat(systemPrompt, userContent) { - const { provider, apiKey, baseURL, model } = getLLMConfig(); + const { provider, apiKeys, baseURL, model } = getLLMConfig(); if (!provider) throw new Error('未設定任何 LLM API Key'); console.log(` [LLM] provider=${provider} model=${model}`); - const headers = { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}`, - }; + const headers = { 'Content-Type': 'application/json' }; if (provider === 'claude') headers['anthropic-version'] = '2023-06-01'; - const resp = await axios.post( - `${baseURL.replace(/\/$/, '')}/chat/completions`, - { model, messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userContent }], temperature: 0.2 }, - { headers, timeout: 120000, httpsAgent } - ); - return resp.data.choices[0].message.content; + let lastError; + for (let i = 0; i < apiKeys.length; i++) { + headers['Authorization'] = `Bearer ${apiKeys[i]}`; + try { + const resp = await axios.post( + `${baseURL.replace(/\/$/, '')}/chat/completions`, + { model, messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userContent }], temperature: 0.2 }, + { headers, timeout: 120000, httpsAgent } + ); + return resp.data.choices[0].message.content; + } catch (e) { + lastError = e; + console.log(` [LLM] key[${i + 1}/${apiKeys.length}] 失敗: ${e.message}`); + } + } + console.error(' [LLM] 所有 API Key 均失敗,終止流程'); + process.exit(1); } export async function chatJSON(systemPrompt, userContent) { From 328d6b2100e1c288b341fc50c8861b2d1b6c8f50 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 07:18:00 +0000 Subject: [PATCH 151/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 55 +++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index bbf0185..856ea5a 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,4 +1,32 @@ [ + { + "level": "critical", + "role": "Leo", + "location": "app/llm.js:22", + "suggestion": "在 `chat` 函式中,`Authorization` 標頭是無條件地被加入到所有 LLM 請求中。這對於不需要 API Key 的服務(如 Ollama)是不必要的,且可能導致錯誤。建議在設定 `Authorization` 標頭之前,先判斷當前的 `provider` 是否需要 API Key,例如 `if (provider !== 'ollama' && apiKeys[i]) { headers['Authorization'] = `Bearer ${apiKeys[i]}`; }`,以提高程式碼的健壯性和正確性。", + "is_new": true + }, + { + "level": "critical", + "role": "Zara", + "location": "app/llm.js:20", + "suggestion": "當啟用 API Key 輪替機制時,單一 API 請求的 `timeout` 設定為 120 秒過長。若有多個 Key 且每個 Key 都因逾時而失敗,可能導致整個流程耗時過久(例如 10 個 Key 可能耗時 20 分鐘)。建議將單次請求的逾時時間縮短(例如 10-30 秒),以加速 Key 的輪替,避免 CI/CD 流程長時間阻塞。", + "is_new": true + }, + { + "level": "critical", + "role": "Rex", + "location": "app/llm.js:6", + "suggestion": "程式碼中 `https.Agent({ rejectUnauthorized: false })` 停用了 SSL/TLS 憑證驗證。這會使所有 HTTPS 連線容易受到中間人 (Man-in-the-Middle, MITM) 攻擊,攻擊者可以攔截並修改與 LLM 服務提供者的通訊,導致資料洩漏、未經授權的存取或 AI 回應被操縱。請移除 `const httpsAgent = new https.Agent({ rejectUnauthorized: false });` 這一行,並確保 `axios.post` 呼叫中不再使用 `httpsAgent` 選項。預設情況下,Node.js 和 Axios 會執行嚴格的 SSL 憑證驗證,這是確保通訊安全的最佳實踐。如果遇到憑證問題,應調查並解決底層的憑證信任鏈問題,而非禁用驗證。", + "is_new": true + }, + { + "level": "critical", + "role": "Maya", + "location": "app/llm.js:10-31", + "suggestion": "`app/llm.js` 中實現的 API Key 輪替功能是本次改動的核心,但目前缺少對應的單元測試。請務必在 `app/llm.test.js` 中新增全面的測試案例,以驗證 `TODO.md` 中「階段八:API Key 輪替」的所有驗收標準:\n* **單一 Key 成功**:傳入單一有效 Key 時,確保行為與原本相同。\n* **多個 Key 輪替成功**:驗證當前 N-1 個 Key 失敗,第 N 個 Key 成功時,系統能依序嘗試並最終成功。\n* **所有 Key 失敗**:驗證當所有傳入的 Key 都失敗時,系統能正確記錄每次失敗,並最終呼叫 `process.exit(1)` 終止流程(測試時需模擬 `process.exit` 以捕獲其調用)。\n* **日誌訊息**:驗證在 Key 失敗時,能正確輸出「key[N/M] 失敗」的日誌;所有 Key 失敗時,能輸出「所有 API Key 均失敗,終止流程」。\n* **錯誤處理**:模擬不同類型的 API 錯誤(例如 401 Unauthorized, 429 Too Many Requests, 網路超時等),確保 Key 輪替機制能穩健處理。", + "is_new": true + }, { "level": "warning", "role": "Zara", @@ -11,27 +39,20 @@ "role": "Aria", "location": ".gitea/workflows/master.yaml", "suggestion": "檔案結尾應包含一個換行符號 (newline at EOF),這是 POSIX 系統的慣例,有助於版本控制系統的正確處理。", + "is_new": false + }, + { + "level": "warning", + "role": "Leo", + "location": ".gitea/workflows/review.yaml:36", + "suggestion": "GEMINI_API_KEY 的值過長,影響可讀性。雖然這是為了傳遞多個 Secret,但建議考慮是否有其他方式可以讓設定檔更簡潔,例如將多個 Secret 組合為一個,或在 Action 內部處理多個獨立的 Secret 變數(如果 Action 支援)。如果沒有其他方式,請考慮將其分行以提高可讀性(雖然 YAML 可能會將其視為單行)。", "is_new": true }, { "level": "warning", - "role": "Aria", - "location": "Dockerfile", - "suggestion": "檔案結尾應包含一個換行符號 (newline at EOF),這是 POSIX 系統的慣例,有助於版本控制系統的正確處理。", - "is_new": true - }, - { - "level": "warning", - "role": "Aria", - "location": "TODO.md", - "suggestion": "檔案結尾應包含一個換行符號 (newline at EOF),這是 POSIX 系統的慣例,有助於版本控制系統的正確處理。", - "is_new": true - }, - { - "level": "warning", - "role": "Aria", - "location": "entrypoint.sh", - "suggestion": "檔案結尾應包含一個換行符號 (newline at EOF),這是 POSIX 系統的慣例,有助於版本控制系統的正確處理。", + "role": "Maya", + "location": "app/llm.js", + "suggestion": "`chatJSON` 函數依賴於 `chat` 函數的 API Key 輪替邏輯。為確保在處理 JSON 格式回應時,API Key 輪替機制也能正常運作,建議在 `app/llm.test.js` 中為 `chatJSON` 函數新增至少一個測試案例,特別是針對 Key 輪替失敗或成功後的行為進行驗證。", "is_new": true } ] From 2ced37f54f8ffc108674923c6c190f9a441fa22e Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 07:38:38 +0000 Subject: [PATCH 152/174] feat: refactor LLM API handling, add tests for key rotation and update package files --- .gitignore | 1 + app/llm.js | 7 +- app/llm.test.js | 128 ++++++++++++ app/package-lock.json | 468 ++++++++++++++++++++++++++++++++++++++++++ app/package.json | 2 +- 5 files changed, 600 insertions(+), 6 deletions(-) create mode 100644 .gitignore create mode 100644 app/llm.test.js create mode 100644 app/package-lock.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84376b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +app/node_modules/ diff --git a/app/llm.js b/app/llm.js index dddce5f..e2c2db1 100644 --- a/app/llm.js +++ b/app/llm.js @@ -1,9 +1,6 @@ import axios from 'axios'; -import https from 'https'; import { getLLMConfig } from './config.js'; -const httpsAgent = new https.Agent({ rejectUnauthorized: false }); - export async function chat(systemPrompt, userContent) { const { provider, apiKeys, baseURL, model } = getLLMConfig(); if (!provider) throw new Error('未設定任何 LLM API Key'); @@ -15,12 +12,12 @@ export async function chat(systemPrompt, userContent) { let lastError; for (let i = 0; i < apiKeys.length; i++) { - headers['Authorization'] = `Bearer ${apiKeys[i]}`; + if (provider !== 'ollama') headers['Authorization'] = `Bearer ${apiKeys[i]}`; try { const resp = await axios.post( `${baseURL.replace(/\/$/, '')}/chat/completions`, { model, messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userContent }], temperature: 0.2 }, - { headers, timeout: 120000, httpsAgent } + { headers, timeout: 30000 } ); return resp.data.choices[0].message.content; } catch (e) { diff --git a/app/llm.test.js b/app/llm.test.js new file mode 100644 index 0000000..74f41f6 --- /dev/null +++ b/app/llm.test.js @@ -0,0 +1,128 @@ +import { describe, it, beforeEach, afterEach, mock } from 'node:test'; +import assert from 'node:assert/strict'; + +// Mock axios before importing llm.js +import axios from 'axios'; + +const ENV_KEYS = [ + 'OPENAI_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_MODEL', + 'GEMINI_API_KEY', 'GEMINI_BASE_URL', 'GEMINI_MODEL', + 'CLAUDE_API_KEY', 'CLAUDE_BASE_URL', 'CLAUDE_MODEL', + 'OLLAMA_BASE_URL', 'OLLAMA_MODEL', + 'AMAZONQ_API_KEY', 'AMAZONQ_BASE_URL', 'AMAZONQ_MODEL', +]; + +let saved = {}; +beforeEach(() => { + saved = {}; + for (const k of ENV_KEYS) { saved[k] = process.env[k]; delete process.env[k]; } +}); +afterEach(() => { + for (const k of ENV_KEYS) { + if (saved[k] === undefined) delete process.env[k]; + else process.env[k] = saved[k]; + } + mock.restoreAll(); +}); + +function mockAxiosPost(responses) { + let call = 0; + mock.method(axios, 'post', async () => { + const r = responses[call++] ?? responses[responses.length - 1]; + if (r instanceof Error) throw r; + return r; + }); +} + +function makeOkResponse(content = 'ok') { + return { data: { choices: [{ message: { content } }] } }; +} + +describe('chat - key rotation', async () => { + const { chat } = await import('./llm.js'); + + it('succeeds on first key', async () => { + process.env.OPENAI_API_KEY = 'key1'; + mockAxiosPost([makeOkResponse('hello')]); + const result = await chat('sys', 'user'); + assert.equal(result, 'hello'); + }); + + it('rotates to second key when first fails', async () => { + process.env.OPENAI_API_KEY = 'key1,key2'; + mockAxiosPost([new Error('rate limit'), makeOkResponse('from key2')]); + const result = await chat('sys', 'user'); + assert.equal(result, 'from key2'); + }); + + it('rotates through all keys and succeeds on last', async () => { + process.env.OPENAI_API_KEY = 'k1,k2,k3'; + mockAxiosPost([new Error('fail'), new Error('fail'), makeOkResponse('from k3')]); + const result = await chat('sys', 'user'); + assert.equal(result, 'from k3'); + }); + + it('calls process.exit(1) when all keys fail', async () => { + process.env.OPENAI_API_KEY = 'k1,k2'; + mockAxiosPost([new Error('fail'), new Error('fail')]); + const exitMock = mock.method(process, 'exit', () => { throw new Error('exit:1'); }); + await assert.rejects(() => chat('sys', 'user'), /exit:1/); + assert.equal(exitMock.mock.calls[0].arguments[0], 1); + }); + + it('does not set Authorization header for ollama', async () => { + process.env.OLLAMA_BASE_URL = 'http://localhost:11434/v1'; + process.env.OLLAMA_MODEL = 'llama3'; + let capturedHeaders; + mock.method(axios, 'post', async (_url, _body, opts) => { + capturedHeaders = opts.headers; + return makeOkResponse('ollama response'); + }); + await chat('sys', 'user'); + assert.equal(capturedHeaders['Authorization'], undefined); + }); + + it('sets Authorization header for openai', async () => { + process.env.OPENAI_API_KEY = 'sk-test'; + let capturedHeaders; + mock.method(axios, 'post', async (_url, _body, opts) => { + capturedHeaders = opts.headers; + return makeOkResponse(); + }); + await chat('sys', 'user'); + assert.equal(capturedHeaders['Authorization'], 'Bearer sk-test'); + }); + + it('uses 30s timeout', async () => { + process.env.OPENAI_API_KEY = 'sk-test'; + let capturedOpts; + mock.method(axios, 'post', async (_url, _body, opts) => { + capturedOpts = opts; + return makeOkResponse(); + }); + await chat('sys', 'user'); + assert.equal(capturedOpts.timeout, 30000); + }); + + it('does not pass httpsAgent to axios', async () => { + process.env.OPENAI_API_KEY = 'sk-test'; + let capturedOpts; + mock.method(axios, 'post', async (_url, _body, opts) => { + capturedOpts = opts; + return makeOkResponse(); + }); + await chat('sys', 'user'); + assert.equal(capturedOpts.httpsAgent, undefined); + }); + + it('sets anthropic-version header for claude', async () => { + process.env.CLAUDE_API_KEY = 'claude-key'; + let capturedHeaders; + mock.method(axios, 'post', async (_url, _body, opts) => { + capturedHeaders = opts.headers; + return makeOkResponse(); + }); + await chat('sys', 'user'); + assert.equal(capturedHeaders['anthropic-version'], '2023-06-01'); + }); +}); diff --git a/app/package-lock.json b/app/package-lock.json new file mode 100644 index 0000000..6aaee51 --- /dev/null +++ b/app/package-lock.json @@ -0,0 +1,468 @@ +{ + "name": "ai-code-review", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ai-code-review", + "version": "1.0.0", + "dependencies": { + "axios": "^1.6.7", + "js-yaml": "^4.1.0", + "openai": "^4.28.0" + } + }, + "node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/openai": { + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } +} diff --git a/app/package.json b/app/package.json index 466e7c7..642c211 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "type": "module", "scripts": { - "test": "node --test app/git.test.js" + "test": "node --test git.test.js config.test.js llm.test.js" }, "dependencies": { "axios": "^1.6.7", From 710b9a1bb57e82ebb1310262454163d9a8e730b3 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 07:47:28 +0000 Subject: [PATCH 153/174] feat: update AI Code Review workflow to use OpenAI API instead of Gemini API --- .gitea/workflows/review.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 6fbcb0b..787e9e9 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -33,9 +33,9 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/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 }} - GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta - GEMINI_MODEL: ${{ vars.GEMINI_MODEL }} + OPENAI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + OPENAI_BASE_URL: https://openrouter.ai/api/v1 + OPENAI_MODEL: ${{ vars.OPENROUTER_MODEL }} permissions: contents: write pull-requests: write From 4a631bc62ae27b6d0b1d87453dbafe4399c17942 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 08:04:44 +0000 Subject: [PATCH 154/174] feat: support multiple OpenAI API keys in AI Code Review workflow --- .gitea/workflows/review.yaml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 787e9e9..f341996 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -33,7 +33,7 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} with: - OPENAI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }},${{ secrets.OPENROUTER_API_KEY_1 }} OPENAI_BASE_URL: https://openrouter.ai/api/v1 OPENAI_MODEL: ${{ vars.OPENROUTER_MODEL }} permissions: diff --git a/README.md b/README.md index 745001c..2b2be5a 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} with: - OPENAI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} # OpenRouter 使用 OpenAI 相容介面,以 OPENAI_API_KEY 傳入,支援逗號分隔多個 Key + OPENAI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }},${{ secrets.OPENROUTER_API_KEY_1 }} OPENAI_BASE_URL: https://openrouter.ai/api/v1 OPENAI_MODEL: ${{ vars.OPENROUTER_MODEL }} permissions: From e28c6ea5a30db713683011b07a41418a0e14bd7c Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 08:05:37 +0000 Subject: [PATCH 155/174] feat: update AI Code Review workflow to use Gemini API keys and configuration --- .gitea/workflows/review.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index f341996..6fbcb0b 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -33,9 +33,9 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} with: - OPENAI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }},${{ secrets.OPENROUTER_API_KEY_1 }} - OPENAI_BASE_URL: https://openrouter.ai/api/v1 - OPENAI_MODEL: ${{ vars.OPENROUTER_MODEL }} + 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 }} + GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta + GEMINI_MODEL: ${{ vars.GEMINI_MODEL }} permissions: contents: write pull-requests: write From a6df5c4f431eecb9fd3202f1f52e0e4a3666f335 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 08:17:49 +0000 Subject: [PATCH 156/174] feat: refactor API key handling to shuffle keys and attempt each once --- README.md | 2 +- TODO.md | 2 +- app/llm.js | 9 +++++---- app/llm.test.js | 28 ++++++++++++++-------------- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 2b2be5a..e3cd44a 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ 3. Comment 加上些許 emoji 讓資訊有點活力 4. 盡量將應用程式放在 ./app,修改 entrypoint.sh 與 Dockerfile 讓程式可以正常運行 5. 將提示詞放到 ./app/prompts 內供程式讀取 -6. API Key 支援逗號分隔傳入多個,依序嘗試,失敗時自動換下一個,全部失敗則 exit 1 +6. API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,全部失敗則 exit 1 # 使用說明 diff --git a/TODO.md b/TODO.md index c605eac..225c458 100644 --- a/TODO.md +++ b/TODO.md @@ -36,7 +36,7 @@ - 完成 ## 階段八:API Key 輪替 -- 目標:所有平台的 API Key 支援逗號分隔傳入多個,依序嘗試,單一 Key 失敗時自動換下一個,全部失敗則 exit 1。 +- 目標:所有平台的 API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,單一 Key 失敗時自動換下一個,全部失敗則 exit 1。 - 驗收:log 中能看到「key[N/M] 失敗」等訊息,換 key 後繼續執行;傳入單一 Key 時行為與原本相同;全部 Key 失敗時 log「所有 API Key 均失敗,終止流程」且 workflow 狀態為失敗。 - 完成 diff --git a/app/llm.js b/app/llm.js index e2c2db1..1b009e0 100644 --- a/app/llm.js +++ b/app/llm.js @@ -10,19 +10,20 @@ export async function chat(systemPrompt, userContent) { const headers = { 'Content-Type': 'application/json' }; if (provider === 'claude') headers['anthropic-version'] = '2023-06-01'; + const shuffled = [...apiKeys].sort(() => Math.random() - 0.5); let lastError; - for (let i = 0; i < apiKeys.length; i++) { - if (provider !== 'ollama') headers['Authorization'] = `Bearer ${apiKeys[i]}`; + for (let i = 0; i < shuffled.length; i++) { + if (provider !== 'ollama') headers['Authorization'] = `Bearer ${shuffled[i]}`; try { const resp = await axios.post( `${baseURL.replace(/\/$/, '')}/chat/completions`, { model, messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userContent }], temperature: 0.2 }, - { headers, timeout: 30000 } + { headers } ); return resp.data.choices[0].message.content; } catch (e) { lastError = e; - console.log(` [LLM] key[${i + 1}/${apiKeys.length}] 失敗: ${e.message}`); + console.log(` [LLM] key[${i + 1}/${shuffled.length}] 失敗: ${e.message}`); } } console.error(' [LLM] 所有 API Key 均失敗,終止流程'); diff --git a/app/llm.test.js b/app/llm.test.js index 74f41f6..c94b893 100644 --- a/app/llm.test.js +++ b/app/llm.test.js @@ -48,18 +48,18 @@ describe('chat - key rotation', async () => { assert.equal(result, 'hello'); }); - it('rotates to second key when first fails', async () => { - process.env.OPENAI_API_KEY = 'key1,key2'; - mockAxiosPost([new Error('rate limit'), makeOkResponse('from key2')]); - const result = await chat('sys', 'user'); - assert.equal(result, 'from key2'); - }); - - it('rotates through all keys and succeeds on last', async () => { - process.env.OPENAI_API_KEY = 'k1,k2,k3'; - mockAxiosPost([new Error('fail'), new Error('fail'), makeOkResponse('from k3')]); - const result = await chat('sys', 'user'); - assert.equal(result, 'from k3'); + it('shuffles keys and tries each exactly once', async () => { + process.env.OPENAI_API_KEY = 'key1,key2,key3'; + const usedKeys = []; + mock.method(axios, 'post', async (_url, _body, opts) => { + usedKeys.push(opts.headers['Authorization'].replace('Bearer ', '')); + throw new Error('fail'); + }); + const exitMock = mock.method(process, 'exit', () => { throw new Error('exit:1'); }); + await assert.rejects(() => chat('sys', 'user'), /exit:1/); + assert.equal(exitMock.mock.calls[0].arguments[0], 1); + assert.equal(usedKeys.length, 3); + assert.deepEqual([...usedKeys].sort(), ['key1', 'key2', 'key3']); }); it('calls process.exit(1) when all keys fail', async () => { @@ -93,7 +93,7 @@ describe('chat - key rotation', async () => { assert.equal(capturedHeaders['Authorization'], 'Bearer sk-test'); }); - it('uses 30s timeout', async () => { + it('does not set timeout', async () => { process.env.OPENAI_API_KEY = 'sk-test'; let capturedOpts; mock.method(axios, 'post', async (_url, _body, opts) => { @@ -101,7 +101,7 @@ describe('chat - key rotation', async () => { return makeOkResponse(); }); await chat('sys', 'user'); - assert.equal(capturedOpts.timeout, 30000); + assert.equal(capturedOpts.timeout, undefined); }); it('does not pass httpsAgent to axios', async () => { From b3c868ceec03a79bc3e9cdb7055dc8828d01761f Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 08:23:00 +0000 Subject: [PATCH 157/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 856ea5a..dacae23 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -4,28 +4,28 @@ "role": "Leo", "location": "app/llm.js:22", "suggestion": "在 `chat` 函式中,`Authorization` 標頭是無條件地被加入到所有 LLM 請求中。這對於不需要 API Key 的服務(如 Ollama)是不必要的,且可能導致錯誤。建議在設定 `Authorization` 標頭之前,先判斷當前的 `provider` 是否需要 API Key,例如 `if (provider !== 'ollama' && apiKeys[i]) { headers['Authorization'] = `Bearer ${apiKeys[i]}`; }`,以提高程式碼的健壯性和正確性。", - "is_new": true + "is_new": false }, { "level": "critical", "role": "Zara", "location": "app/llm.js:20", "suggestion": "當啟用 API Key 輪替機制時,單一 API 請求的 `timeout` 設定為 120 秒過長。若有多個 Key 且每個 Key 都因逾時而失敗,可能導致整個流程耗時過久(例如 10 個 Key 可能耗時 20 分鐘)。建議將單次請求的逾時時間縮短(例如 10-30 秒),以加速 Key 的輪替,避免 CI/CD 流程長時間阻塞。", - "is_new": true + "is_new": false }, { "level": "critical", "role": "Rex", "location": "app/llm.js:6", "suggestion": "程式碼中 `https.Agent({ rejectUnauthorized: false })` 停用了 SSL/TLS 憑證驗證。這會使所有 HTTPS 連線容易受到中間人 (Man-in-the-Middle, MITM) 攻擊,攻擊者可以攔截並修改與 LLM 服務提供者的通訊,導致資料洩漏、未經授權的存取或 AI 回應被操縱。請移除 `const httpsAgent = new https.Agent({ rejectUnauthorized: false });` 這一行,並確保 `axios.post` 呼叫中不再使用 `httpsAgent` 選項。預設情況下,Node.js 和 Axios 會執行嚴格的 SSL 憑證驗證,這是確保通訊安全的最佳實踐。如果遇到憑證問題,應調查並解決底層的憑證信任鏈問題,而非禁用驗證。", - "is_new": true + "is_new": false }, { "level": "critical", "role": "Maya", "location": "app/llm.js:10-31", "suggestion": "`app/llm.js` 中實現的 API Key 輪替功能是本次改動的核心,但目前缺少對應的單元測試。請務必在 `app/llm.test.js` 中新增全面的測試案例,以驗證 `TODO.md` 中「階段八:API Key 輪替」的所有驗收標準:\n* **單一 Key 成功**:傳入單一有效 Key 時,確保行為與原本相同。\n* **多個 Key 輪替成功**:驗證當前 N-1 個 Key 失敗,第 N 個 Key 成功時,系統能依序嘗試並最終成功。\n* **所有 Key 失敗**:驗證當所有傳入的 Key 都失敗時,系統能正確記錄每次失敗,並最終呼叫 `process.exit(1)` 終止流程(測試時需模擬 `process.exit` 以捕獲其調用)。\n* **日誌訊息**:驗證在 Key 失敗時,能正確輸出「key[N/M] 失敗」的日誌;所有 Key 失敗時,能輸出「所有 API Key 均失敗,終止流程」。\n* **錯誤處理**:模擬不同類型的 API 錯誤(例如 401 Unauthorized, 429 Too Many Requests, 網路超時等),確保 Key 輪替機制能穩健處理。", - "is_new": true + "is_new": false }, { "level": "warning", @@ -46,13 +46,27 @@ "role": "Leo", "location": ".gitea/workflows/review.yaml:36", "suggestion": "GEMINI_API_KEY 的值過長,影響可讀性。雖然這是為了傳遞多個 Secret,但建議考慮是否有其他方式可以讓設定檔更簡潔,例如將多個 Secret 組合為一個,或在 Action 內部處理多個獨立的 Secret 變數(如果 Action 支援)。如果沒有其他方式,請考慮將其分行以提高可讀性(雖然 YAML 可能會將其視為單行)。", + "is_new": false + }, + { + "level": "warning", + "role": "Maya", + "location": "app/llm.test.js", + "suggestion": "在 `llm.test.js` 中,`TODO.md` 驗收標準明確要求驗證「key[N/M] 失敗」和「所有 API Key 均失敗,終止流程」的日誌訊息。目前的測試雖然驗證了 `process.exit(1)` 的調用,但並未對 `console.log` 和 `console.error` 的輸出進行模擬和斷言。建議使用 `mock.method(console, 'log', ...)` 和 `mock.method(console, 'error', ...)` 來捕獲並驗證這些重要的日誌訊息,確保系統在 API Key 輪替失敗時能提供清晰的診斷資訊。", + "is_new": true + }, + { + "level": "warning", + "role": "Maya", + "location": "app/llm.test.js", + "suggestion": "針對 API Key 輪替的錯誤處理,`TODO.md` 驗收標準中提到「模擬不同類型的 API 錯誤(例如 401 Unauthorized, 429 Too Many Requests, 網路超時等)」。目前的測試僅使用 `new Error('fail')` 進行通用錯誤模擬。建議擴展測試案例,模擬 `axios` 拋出帶有特定 HTTP 狀態碼(如 401, 429)的錯誤,以及模擬網路超時(例如 `axios.isAxiosError` 且 `e.code === 'ECONNABORTED'`),以確保 API Key 輪替機制在面對不同類型的 API 錯誤時都能穩健運作。", "is_new": true }, { "level": "warning", "role": "Maya", "location": "app/llm.js", - "suggestion": "`chatJSON` 函數依賴於 `chat` 函數的 API Key 輪替邏輯。為確保在處理 JSON 格式回應時,API Key 輪替機制也能正常運作,建議在 `app/llm.test.js` 中為 `chatJSON` 函數新增至少一個測試案例,特別是針對 Key 輪替失敗或成功後的行為進行驗證。", + "suggestion": "`app/llm.js` 中的 `chatJSON` 函數依賴於 `chat` 函數的 API Key 輪替邏輯。雖然 `chat` 函數已新增測試,但 `chatJSON` 函數本身並未有專屬的測試案例。建議在 `app/llm.test.js` 中新增針對 `chatJSON` 的測試,特別是驗證在 API Key 輪替成功和失敗時,`chatJSON` 能正確處理 JSON 格式的回應,並在所有 Key 失敗時也能正確終止流程。", "is_new": true } ] From bb18147cab91badf964fee2d0287675ec005626d Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 08:26:58 +0000 Subject: [PATCH 158/174] feat: update exclusions.json with additional suggestions and refine master.yaml for cleaner token handling --- .gitea/ai-review/exclusions.json | 30 ++++++++++++++++++++++++++++++ .gitea/workflows/master.yaml | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index e081e36..0bcf829 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -93,5 +93,35 @@ "role": "Rex", "location": ".gitea/workflows/review.yaml", "suggestion": "切換 LLM 服務提供商的維護建議屬過度謹慎,不是實際程式碼問題" + }, + { + "role": "Leo", + "location": "app/llm.js", + "suggestion": "Authorization 標頭已有 provider !== 'ollama' 判斷,不會無條件加入,已正確處理" + }, + { + "role": "Zara", + "location": "app/llm.js", + "suggestion": "timeout 已移除,每個 key 等待完整回應,避免浪費免費額度" + }, + { + "role": "Rex", + "location": "app/llm.js", + "suggestion": "httpsAgent (rejectUnauthorized: false) 已移除,SSL/TLS 驗證已恢復正常" + }, + { + "role": "Maya", + "location": "app/llm.js", + "suggestion": "llm.test.js 已存在並涵蓋 API Key 輪替的所有異常狀況,包含單 Key、多 Key 輪替、所有 Key 失敗等測試案例" + }, + { + "role": "Zara", + "location": "app/comments.js", + "suggestion": "comments.js:24 的 saveFindings 函式為正常寫入邏輯,不涉及異常訊息格式或重複寫入問題" + }, + { + "role": "Leo", + "location": ".gitea/workflows/review.yaml", + "suggestion": "Gitea Actions 不支援在 workflow 內合併 secrets 再拆解,多個 secret 逗號串接是唯一可行做法,非設計缺陷" } ] diff --git a/.gitea/workflows/master.yaml b/.gitea/workflows/master.yaml index b37b091..bfe2ef2 100644 --- a/.gitea/workflows/master.yaml +++ b/.gitea/workflows/master.yaml @@ -25,4 +25,4 @@ jobs: - name: 清理成品 uses: https://gitea.jsc.idv.tw/actions/cleanup-release@${{ vars.ACTION_CLEANUP_RELEASE_VERSION }} with: - RUNNER_TOKEN: ${{ secrets.RUNNER_TOKEN }} \ No newline at end of file + RUNNER_TOKEN: ${{ secrets.RUNNER_TOKEN }} From b149508dab37fcc18fc5b8a04ed33c43dd65cae3 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 08:28:37 +0000 Subject: [PATCH 159/174] feat: enhance exclusions.json with additional suggestions and add chatJSON tests for JSON response handling --- .gitea/ai-review/exclusions.json | 10 ++++++++++ app/llm.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 0bcf829..d9360c8 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -123,5 +123,15 @@ "role": "Leo", "location": ".gitea/workflows/review.yaml", "suggestion": "Gitea Actions 不支援在 workflow 內合併 secrets 再拆解,多個 secret 逗號串接是唯一可行做法,非設計缺陷" + }, + { + "role": "Maya", + "location": "app/llm.test.js", + "suggestion": "console.log/error 為診斷用途,不是業務邏輯,TODO.md 驗收標準為人工驗收描述,不需要在單元測試中斷言 console 輸出" + }, + { + "role": "Maya", + "location": "app/llm.test.js", + "suggestion": "輪替邏輯對所有錯誤類型行為一致(catch 全部),401/429/timeout 觸發相同輪替流程,測試不同錯誤類型無額外驗證價值" } ] diff --git a/app/llm.test.js b/app/llm.test.js index c94b893..c4654ab 100644 --- a/app/llm.test.js +++ b/app/llm.test.js @@ -126,3 +126,28 @@ describe('chat - key rotation', async () => { assert.equal(capturedHeaders['anthropic-version'], '2023-06-01'); }); }); + +describe('chatJSON', async () => { + const { chatJSON } = await import('./llm.js'); + + it('parses plain JSON response', async () => { + process.env.OPENAI_API_KEY = 'sk-test'; + mockAxiosPost([makeOkResponse('[{"level":"critical"}]')]); + const result = await chatJSON('sys', 'user'); + assert.deepEqual(result, [{ level: 'critical' }]); + }); + + it('strips markdown code block before parsing', async () => { + process.env.OPENAI_API_KEY = 'sk-test'; + mockAxiosPost([makeOkResponse('```json\n[{"level":"info"}]\n```')]); + const result = await chatJSON('sys', 'user'); + assert.deepEqual(result, [{ level: 'info' }]); + }); + + it('returns [] when JSON is invalid', async () => { + process.env.OPENAI_API_KEY = 'sk-test'; + mockAxiosPost([makeOkResponse('not json')]); + const result = await chatJSON('sys', 'user'); + assert.deepEqual(result, []); + }); +}); From af195b9c3b6ad663a7e0dfcf3b637c139e173c01 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 08:36:23 +0000 Subject: [PATCH 160/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 55 ++-------------------------------- 1 file changed, 3 insertions(+), 52 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index dacae23..3c3e056 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,39 +1,4 @@ [ - { - "level": "critical", - "role": "Leo", - "location": "app/llm.js:22", - "suggestion": "在 `chat` 函式中,`Authorization` 標頭是無條件地被加入到所有 LLM 請求中。這對於不需要 API Key 的服務(如 Ollama)是不必要的,且可能導致錯誤。建議在設定 `Authorization` 標頭之前,先判斷當前的 `provider` 是否需要 API Key,例如 `if (provider !== 'ollama' && apiKeys[i]) { headers['Authorization'] = `Bearer ${apiKeys[i]}`; }`,以提高程式碼的健壯性和正確性。", - "is_new": false - }, - { - "level": "critical", - "role": "Zara", - "location": "app/llm.js:20", - "suggestion": "當啟用 API Key 輪替機制時,單一 API 請求的 `timeout` 設定為 120 秒過長。若有多個 Key 且每個 Key 都因逾時而失敗,可能導致整個流程耗時過久(例如 10 個 Key 可能耗時 20 分鐘)。建議將單次請求的逾時時間縮短(例如 10-30 秒),以加速 Key 的輪替,避免 CI/CD 流程長時間阻塞。", - "is_new": false - }, - { - "level": "critical", - "role": "Rex", - "location": "app/llm.js:6", - "suggestion": "程式碼中 `https.Agent({ rejectUnauthorized: false })` 停用了 SSL/TLS 憑證驗證。這會使所有 HTTPS 連線容易受到中間人 (Man-in-the-Middle, MITM) 攻擊,攻擊者可以攔截並修改與 LLM 服務提供者的通訊,導致資料洩漏、未經授權的存取或 AI 回應被操縱。請移除 `const httpsAgent = new https.Agent({ rejectUnauthorized: false });` 這一行,並確保 `axios.post` 呼叫中不再使用 `httpsAgent` 選項。預設情況下,Node.js 和 Axios 會執行嚴格的 SSL 憑證驗證,這是確保通訊安全的最佳實踐。如果遇到憑證問題,應調查並解決底層的憑證信任鏈問題,而非禁用驗證。", - "is_new": false - }, - { - "level": "critical", - "role": "Maya", - "location": "app/llm.js:10-31", - "suggestion": "`app/llm.js` 中實現的 API Key 輪替功能是本次改動的核心,但目前缺少對應的單元測試。請務必在 `app/llm.test.js` 中新增全面的測試案例,以驗證 `TODO.md` 中「階段八:API Key 輪替」的所有驗收標準:\n* **單一 Key 成功**:傳入單一有效 Key 時,確保行為與原本相同。\n* **多個 Key 輪替成功**:驗證當前 N-1 個 Key 失敗,第 N 個 Key 成功時,系統能依序嘗試並最終成功。\n* **所有 Key 失敗**:驗證當所有傳入的 Key 都失敗時,系統能正確記錄每次失敗,並最終呼叫 `process.exit(1)` 終止流程(測試時需模擬 `process.exit` 以捕獲其調用)。\n* **日誌訊息**:驗證在 Key 失敗時,能正確輸出「key[N/M] 失敗」的日誌;所有 Key 失敗時,能輸出「所有 API Key 均失敗,終止流程」。\n* **錯誤處理**:模擬不同類型的 API 錯誤(例如 401 Unauthorized, 429 Too Many Requests, 網路超時等),確保 Key 輪替機制能穩健處理。", - "is_new": false - }, - { - "level": "warning", - "role": "Zara", - "location": "app/comments.js:24", - "suggestion": "在 `saveFindings` 函數中,`fs.writeFileSync` 是一個同步操作。如果 `findings` 陣列可能非常大,或者此函數會被頻繁呼叫,同步寫入檔案可能會阻塞 Node.js 事件迴圈,導致應用程式響應變慢。建議改用 `fs.writeFile` (非同步) 以避免阻塞主執行緒,提升應用程式的響應能力。", - "is_new": false - }, { "level": "warning", "role": "Aria", @@ -44,29 +9,15 @@ { "level": "warning", "role": "Leo", - "location": ".gitea/workflows/review.yaml:36", - "suggestion": "GEMINI_API_KEY 的值過長,影響可讀性。雖然這是為了傳遞多個 Secret,但建議考慮是否有其他方式可以讓設定檔更簡潔,例如將多個 Secret 組合為一個,或在 Action 內部處理多個獨立的 Secret 變數(如果 Action 支援)。如果沒有其他方式,請考慮將其分行以提高可讀性(雖然 YAML 可能會將其視為單行)。", - "is_new": false - }, - { - "level": "warning", - "role": "Maya", "location": "app/llm.test.js", - "suggestion": "在 `llm.test.js` 中,`TODO.md` 驗收標準明確要求驗證「key[N/M] 失敗」和「所有 API Key 均失敗,終止流程」的日誌訊息。目前的測試雖然驗證了 `process.exit(1)` 的調用,但並未對 `console.log` 和 `console.error` 的輸出進行模擬和斷言。建議使用 `mock.method(console, 'log', ...)` 和 `mock.method(console, 'error', ...)` 來捕獲並驗證這些重要的日誌訊息,確保系統在 API Key 輪替失敗時能提供清晰的診斷資訊。", + "suggestion": "根據 `TODO.md` 的驗收標準,API Key 輪替失敗時應輸出特定的日誌訊息。目前的單元測試雖然驗證了 `process.exit(1)` 的調用,但並未對 `console.log` 和 `console.error` 的輸出進行模擬和斷言。建議使用 `mock.method(console, 'log', ...)` 和 `mock.method(console, 'error', ...)` 來捕獲並驗證這些重要的日誌訊息,以確保系統在 API Key 輪替失敗時能提供清晰的診斷資訊,這對長期維護和問題排查至關重要。", "is_new": true }, { "level": "warning", - "role": "Maya", + "role": "Leo", "location": "app/llm.test.js", - "suggestion": "針對 API Key 輪替的錯誤處理,`TODO.md` 驗收標準中提到「模擬不同類型的 API 錯誤(例如 401 Unauthorized, 429 Too Many Requests, 網路超時等)」。目前的測試僅使用 `new Error('fail')` 進行通用錯誤模擬。建議擴展測試案例,模擬 `axios` 拋出帶有特定 HTTP 狀態碼(如 401, 429)的錯誤,以及模擬網路超時(例如 `axios.isAxiosError` 且 `e.code === 'ECONNABORTED'`),以確保 API Key 輪替機制在面對不同類型的 API 錯誤時都能穩健運作。", - "is_new": true - }, - { - "level": "warning", - "role": "Maya", - "location": "app/llm.js", - "suggestion": "`app/llm.js` 中的 `chatJSON` 函數依賴於 `chat` 函數的 API Key 輪替邏輯。雖然 `chat` 函數已新增測試,但 `chatJSON` 函數本身並未有專屬的測試案例。建議在 `app/llm.test.js` 中新增針對 `chatJSON` 的測試,特別是驗證在 API Key 輪替成功和失敗時,`chatJSON` 能正確處理 JSON 格式的回應,並在所有 Key 失敗時也能正確終止流程。", + "suggestion": "針對 API Key 輪替的錯誤處理,`TODO.md` 驗收標準中明確提到「模擬不同類型的 API 錯誤(例如 401 Unauthorized, 429 Too Many Requests, 網路超時等)」。目前的測試僅使用 `new Error('fail')` 進行通用錯誤模擬。建議擴展測試案例,模擬 `axios` 拋出帶有特定 HTTP 狀態碼(如 401, 429)的錯誤,以及模擬網路超時(例如 `axios.isAxiosError` 且 `e.code === 'ECONNABORTED'`),以確保 API Key 輪替機制在面對各種實際的 API 錯誤時都能穩健運作,這有助於提高程式碼的健壯性和可維護性。", "is_new": true } ] From 95929fdced28d202050d50122a2d93cc555fc774 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 08:41:23 +0000 Subject: [PATCH 161/174] feat: add suggestions for master.yaml and llm.test.js to improve code quality and testing practices --- .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 d9360c8..27703d2 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -133,5 +133,20 @@ "role": "Maya", "location": "app/llm.test.js", "suggestion": "輪替邏輯對所有錯誤類型行為一致(catch 全部),401/429/timeout 觸發相同輪替流程,測試不同錯誤類型無額外驗證價值" + }, + { + "role": "Aria", + "location": ".gitea/workflows/master.yaml", + "suggestion": "master.yaml 檔案結尾已有換行符號(0x0a),符合 POSIX 慣例,無需修改" + }, + { + "role": "Leo", + "location": "app/llm.test.js", + "suggestion": "console.log/error 為診斷用途,不是業務邏輯,TODO.md 驗收標準為人工驗收描述,不需要在單元測試中斷言 console 輸出" + }, + { + "role": "Leo", + "location": "app/llm.test.js", + "suggestion": "輪替邏輯對所有錯誤類型行為一致(catch 全部),401/429/timeout 觸發相同輪替流程,測試不同錯誤類型無額外驗證價值" } ] From a296c594d3bcfb2a3fb400a2e81401730e75dc80 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 08:45:24 +0000 Subject: [PATCH 162/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 3c3e056..3e4cf15 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,23 +1,9 @@ [ { - "level": "warning", - "role": "Aria", - "location": ".gitea/workflows/master.yaml", - "suggestion": "檔案結尾應包含一個換行符號 (newline at EOF),這是 POSIX 系統的慣例,有助於版本控制系統的正確處理。", - "is_new": false - }, - { - "level": "warning", - "role": "Leo", - "location": "app/llm.test.js", - "suggestion": "根據 `TODO.md` 的驗收標準,API Key 輪替失敗時應輸出特定的日誌訊息。目前的單元測試雖然驗證了 `process.exit(1)` 的調用,但並未對 `console.log` 和 `console.error` 的輸出進行模擬和斷言。建議使用 `mock.method(console, 'log', ...)` 和 `mock.method(console, 'error', ...)` 來捕獲並驗證這些重要的日誌訊息,以確保系統在 API Key 輪替失敗時能提供清晰的診斷資訊,這對長期維護和問題排查至關重要。", - "is_new": true - }, - { - "level": "warning", - "role": "Leo", - "location": "app/llm.test.js", - "suggestion": "針對 API Key 輪替的錯誤處理,`TODO.md` 驗收標準中明確提到「模擬不同類型的 API 錯誤(例如 401 Unauthorized, 429 Too Many Requests, 網路超時等)」。目前的測試僅使用 `new Error('fail')` 進行通用錯誤模擬。建議擴展測試案例,模擬 `axios` 拋出帶有特定 HTTP 狀態碼(如 401, 429)的錯誤,以及模擬網路超時(例如 `axios.isAxiosError` 且 `e.code === 'ECONNABORTED'`),以確保 API Key 輪替機制在面對各種實際的 API 錯誤時都能穩健運作,這有助於提高程式碼的健壯性和可維護性。", + "level": "info", + "role": "Maya", + "location": "app/config.test.js", + "suggestion": "在 `app/config.js` 中,`splitKeys` 函式會過濾掉空字串,這表示如果環境變數只包含逗號(例如 `OPENAI_API_KEY = ','` 或 `OPENAI_API_KEY = ' '`),`apiKeys` 陣列將會是空的。雖然目前的 `getLLMConfig` 邏輯能正確處理空的 `apiKeys` 陣列,並最終導致 `provider` 為 `null`,但建議在 `app/config.test.js` 中增加一個明確的測試案例,以驗證這種邊界條件下 `getLLMConfig` 的行為是否符合預期,確保其在無效或空字串輸入時的穩定性。", "is_new": true } ] From b6aa37201ad11d773a11a4ca42fcc82188ead119 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 09:01:54 +0000 Subject: [PATCH 163/174] feat: add test for handling comma-only API key in getLLMConfig --- app/config.test.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/config.test.js b/app/config.test.js index 03cf9ac..c648520 100644 --- a/app/config.test.js +++ b/app/config.test.js @@ -105,4 +105,11 @@ describe('getLLMConfig', () => { assert.equal(cfg.provider, 'ollama'); assert.equal(cfg.model, 'llama3'); }); + + it('comma-only api key is treated as not set', () => { + process.env.OPENAI_API_KEY = ',,,'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, null); + assert.deepEqual(cfg.apiKeys, []); + }); }); From 3fa5504e9ae0b3c5d4eeea9a0659617ea5ccf2e3 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 09:13:49 +0000 Subject: [PATCH 164/174] 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 3e4cf15..80c1dad 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,9 +1,23 @@ [ + { + "level": "critical", + "role": "Aria", + "location": "app/llm.js:39", + "suggestion": "在 `chat` 函式中直接呼叫 `process.exit(1)` 會導致應用程式立即終止,降低了模組的重用性和測試彈性。建議改為拋出一個自訂錯誤(例如 `AllApiKeysFailedError`),讓呼叫端(例如應用程式的入口點)來決定如何處理此錯誤,例如在頂層捕獲後再呼叫 `process.exit(1)`。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": "app/llm.js:26", + "suggestion": "變數 `lastError` 在迴圈結束後並未使用。請考慮移除此變數,或在所有 API Key 均失敗時,將其包含在拋出的錯誤訊息中,以提供更詳細的失敗原因。", + "is_new": true + }, { "level": "info", - "role": "Maya", - "location": "app/config.test.js", - "suggestion": "在 `app/config.js` 中,`splitKeys` 函式會過濾掉空字串,這表示如果環境變數只包含逗號(例如 `OPENAI_API_KEY = ','` 或 `OPENAI_API_KEY = ' '`),`apiKeys` 陣列將會是空的。雖然目前的 `getLLMConfig` 邏輯能正確處理空的 `apiKeys` 陣列,並最終導致 `provider` 為 `null`,但建議在 `app/config.test.js` 中增加一個明確的測試案例,以驗證這種邊界條件下 `getLLMConfig` 的行為是否符合預期,確保其在無效或空字串輸入時的穩定性。", + "role": "Rex", + "location": "app/package.json", + "suggestion": "此次變更包含 `axios` 和 `openai` 等重要函式庫的版本更新,特別是 `openai` 從 `4.28.0` 升級到 `4.104.0`。建議審查這些函式庫的發行說明(changelog),以了解是否有任何安全修補、已知漏洞或行為變更,確保更新不會引入新的安全風險或不預期的行為。", "is_new": true } ] From 9650162a67268beb9872ec93187b0f8b2856877d Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 09:21:32 +0000 Subject: [PATCH 165/174] feat: extend GEMINI_API_KEY support to include additional keys and update suggestions in exclusions.json --- .gitea/ai-review/exclusions.json | 10 ++++++++++ .gitea/workflows/review.yaml | 2 +- README.md | 2 +- app/llm.js | 2 -- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 27703d2..8d3f313 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -148,5 +148,15 @@ "role": "Leo", "location": "app/llm.test.js", "suggestion": "輪替邏輯對所有錯誤類型行為一致(catch 全部),401/429/timeout 觸發相同輪替流程,測試不同錯誤類型無額外驗證價值" + }, + { + "role": "Rex", + "location": "app/package.json", + "suggestion": "審查 changelog 是人工作業,不是程式碼問題,不適合作為 code review 問題" + }, + { + "role": "Aria", + "location": "app/llm.js", + "suggestion": "此 action 為 CLI 工具,process.exit(1) 是設計意圖讓 CI/CD workflow 失敗。改拋錯會被 chatJSON 的 catch 吞掉回傳 [],破壞現有行為" } ] diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 6fbcb0b..5c36cc4 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -33,7 +33,7 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/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 }} + 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 GEMINI_MODEL: ${{ vars.GEMINI_MODEL }} permissions: diff --git a/README.md b/README.md index e3cd44a..e1717ef 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/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 }} + 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 GEMINI_MODEL: ${{ vars.GEMINI_MODEL }} permissions: diff --git a/app/llm.js b/app/llm.js index 1b009e0..4bf932f 100644 --- a/app/llm.js +++ b/app/llm.js @@ -11,7 +11,6 @@ export async function chat(systemPrompt, userContent) { if (provider === 'claude') headers['anthropic-version'] = '2023-06-01'; const shuffled = [...apiKeys].sort(() => Math.random() - 0.5); - let lastError; for (let i = 0; i < shuffled.length; i++) { if (provider !== 'ollama') headers['Authorization'] = `Bearer ${shuffled[i]}`; try { @@ -22,7 +21,6 @@ export async function chat(systemPrompt, userContent) { ); return resp.data.choices[0].message.content; } catch (e) { - lastError = e; console.log(` [LLM] key[${i + 1}/${shuffled.length}] 失敗: ${e.message}`); } } From d9acf3b0b794dc2368c258ed3970e7c2a262655c Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 09:25:14 +0000 Subject: [PATCH 166/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 80c1dad..6a44d72 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,23 +1,9 @@ [ - { - "level": "critical", - "role": "Aria", - "location": "app/llm.js:39", - "suggestion": "在 `chat` 函式中直接呼叫 `process.exit(1)` 會導致應用程式立即終止,降低了模組的重用性和測試彈性。建議改為拋出一個自訂錯誤(例如 `AllApiKeysFailedError`),讓呼叫端(例如應用程式的入口點)來決定如何處理此錯誤,例如在頂層捕獲後再呼叫 `process.exit(1)`。", - "is_new": true - }, - { - "level": "warning", - "role": "Aria", - "location": "app/llm.js:26", - "suggestion": "變數 `lastError` 在迴圈結束後並未使用。請考慮移除此變數,或在所有 API Key 均失敗時,將其包含在拋出的錯誤訊息中,以提供更詳細的失敗原因。", - "is_new": true - }, { "level": "info", - "role": "Rex", + "role": "Leo", "location": "app/package.json", - "suggestion": "此次變更包含 `axios` 和 `openai` 等重要函式庫的版本更新,特別是 `openai` 從 `4.28.0` 升級到 `4.104.0`。建議審查這些函式庫的發行說明(changelog),以了解是否有任何安全修補、已知漏洞或行為變更,確保更新不會引入新的安全風險或不預期的行為。", + "suggestion": "在 `app/package.json` 中,`axios` 和 `openai` 等函式庫進行了版本更新,特別是 `openai` 從 `4.28.0` 升級到 `4.104.0`。為了確保長期維護的穩定性和安全性,建議審查這些函式庫的發行說明(changelog),以了解是否有任何重大變更、安全修補或已知漏洞,並確認這些更新不會引入不預期的行為或技術債。", "is_new": true } ] From c815c300888afe6c648e694125c0df218c336c58 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 09:31:01 +0000 Subject: [PATCH 167/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 60 ++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 6a44d72..171fde4 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,9 +1,65 @@ [ { - "level": "info", + "level": "critical", "role": "Leo", + "location": "app/gitea.js:10", + "suggestion": "在 `app/gitea.js` 中,`https.Agent` 設定了 `rejectUnauthorized: false`,這會禁用 SSL/TLS 憑證驗證。這是一個嚴重的安全漏洞,會使應用程式容易受到中間人攻擊,並嚴重影響系統的安全性與可維護性。強烈建議移除此設定,或在必要時配置正確的憑證信任鏈。", + "is_new": true + }, + { + "level": "critical", + "role": "Aria", + "location": "Dockerfile", + "suggestion": "檔案結尾缺少換行符號(newline character),這不符合 POSIX 慣例,建議在檔案末尾新增一個空行。", + "is_new": true + }, + { + "level": "critical", + "role": "Aria", + "location": "entrypoint.sh", + "suggestion": "檔案結尾缺少換行符號(newline character),這不符合 POSIX 慣例,建議在檔案末尾新增一個空行。", + "is_new": true + }, + { + "level": "warning", + "role": "Leo", + "location": "Dockerfile:2", + "suggestion": "在 `Dockerfile` 中,基礎映像檔使用了 `FROM alpine` 而沒有指定具體的版本標籤(例如 `alpine:3.18`)。這可能導致未來 `alpine:latest` 更新時,建置環境發生非預期的變更,影響建置的可重現性和穩定性。為了長期維護性,建議明確指定基礎映像檔的版本。", + "is_new": true + }, + { + "level": "warning", + "role": "Zara", + "location": "Dockerfile", + "suggestion": "建議調整 Dockerfile 的層次,將 `COPY app/package.json app/package-lock.json /action/app/` 放在 `npm install` 之前,然後再 `COPY app/ /action/app/`。這樣可以利用 Docker 的層次快取,當 `package.json` 或 `package-lock.json` 未變更時,`npm install` 步驟就不會重複執行,顯著加快 Docker 映像檔的建置速度。", + "is_new": true + }, + { + "level": "warning", + "role": "Maya", + "location": "app/main.js", + "suggestion": "儘管各單元模組(`config`, `git`, `llm`)已有良好的單元測試,但 `app/main.js` 作為整個 Action 的協調者,其整合邏輯和端到端流程缺乏測試。特別是 `TODO.md` 中提到的「阻擋嚴重問題 PR(exit 1)」等關鍵行為,應透過整合測試或端到端測試來驗證,確保各模組協同運作正確,且整體流程符合預期。", + "is_new": true + }, + { + "level": "warning", + "role": "Maya", + "location": "app/comments.js", + "suggestion": "`app/comments.js` 模組負責生成評論內容和儲存 findings 檔案。其中 `buildTable` 等函式負責 Markdown 表格的生成,以及 `postOldFindingsComment`, `postNewNonCriticalComment`, `postNewCriticalComments` 中的過濾邏輯,應補齊單元測試,以確保評論內容的格式正確性及問題分類的準確性。", + "is_new": true + }, + { + "level": "warning", + "role": "Maya", + "location": "app/roles.js", + "suggestion": "`app/roles.js` 模組負責載入和處理角色定義。`loadRoles` 函式應增加單元測試,以驗證其能正確解析 YAML 檔案,並處理檔案不存在或格式錯誤等異常情況。`getRoleIntro` 函式也應測試其生成的 Markdown 介紹內容是否符合預期格式。", + "is_new": true + }, + { + "level": "info", + "role": "Maya", "location": "app/package.json", - "suggestion": "在 `app/package.json` 中,`axios` 和 `openai` 等函式庫進行了版本更新,特別是 `openai` 從 `4.28.0` 升級到 `4.104.0`。為了確保長期維護的穩定性和安全性,建議審查這些函式庫的發行說明(changelog),以了解是否有任何重大變更、安全修補或已知漏洞,並確認這些更新不會引入不預期的行為或技術債。", + "suggestion": "目前的 `test` 腳本 (`node --test git.test.js config.test.js llm.test.js`) 僅涵蓋了部分單元測試。建議更新此腳本,使其能自動發現並執行所有 `*.test.js` 檔案,以確保所有新增的測試都能被納入 CI 流程中執行。", "is_new": true } ] From 1ccc2cd5600da6125a27ba30680de6175eedbd65 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 09:37:51 +0000 Subject: [PATCH 168/174] feat: add GITEA_SKIP_TLS_VERIFY support for skipping SSL/TLS verification in Gitea integration --- .gitea/ai-review/exclusions.json | 10 ++++++++++ action.yaml | 5 +++++ app/config.js | 1 + app/gitea.js | 4 ++-- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 8d3f313..6dbc176 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -158,5 +158,15 @@ "role": "Aria", "location": "app/llm.js", "suggestion": "此 action 為 CLI 工具,process.exit(1) 是設計意圖讓 CI/CD workflow 失敗。改拋錯會被 chatJSON 的 catch 吞掉回傳 [],破壞現有行為" + }, + { + "role": "Aria", + "location": "Dockerfile", + "suggestion": "Dockerfile 檔案結尾已有換行符號(0x0a),符合 POSIX 慣例" + }, + { + "role": "Aria", + "location": "entrypoint.sh", + "suggestion": "entrypoint.sh 檔案結尾已有換行符號(0x0a),符合 POSIX 慣例" } ] diff --git a/action.yaml b/action.yaml index a1b6c68..7dce0bc 100644 --- a/action.yaml +++ b/action.yaml @@ -12,6 +12,10 @@ inputs: GITEA_REPOSITORY: description: 'Gitea Repository (owner/repo)' required: false + GITEA_SKIP_TLS_VERIFY: + description: '跳過 Gitea SSL/TLS 憑證驗證(自簽憑證時使用)' + required: false + default: 'false' PR_NUMBER: description: 'Pull Request Number' required: false @@ -80,6 +84,7 @@ runs: GITEA_TOKEN: ${{ inputs.GITEA_TOKEN || secrets.GITEA_TOKEN }} GITEA_SERVER_URL: ${{ inputs.GITEA_SERVER_URL || gitea.server_url }} GITEA_REPOSITORY: ${{ inputs.GITEA_REPOSITORY || gitea.repository }} + GITEA_SKIP_TLS_VERIFY: ${{ inputs.GITEA_SKIP_TLS_VERIFY }} PR_NUMBER: ${{ inputs.PR_NUMBER || gitea.event.pull_request.number }} PR_HEAD_BRANCH: ${{ inputs.PR_HEAD_BRANCH || gitea.event.pull_request.head.ref }} PR_BASE_BRANCH: ${{ inputs.PR_BASE_BRANCH || gitea.event.pull_request.base.ref }} diff --git a/app/config.js b/app/config.js index c8212e3..83921d5 100644 --- a/app/config.js +++ b/app/config.js @@ -1,6 +1,7 @@ export const GITEA_TOKEN = process.env.GITEA_TOKEN || ''; export const GITEA_SERVER_URL = process.env.GITEA_SERVER_URL || 'https://gitea.com'; export const GITEA_REPOSITORY = process.env.GITEA_REPOSITORY || ''; +export const GITEA_SKIP_TLS_VERIFY = process.env.GITEA_SKIP_TLS_VERIFY === 'true'; export const PR_NUMBER = process.env.PR_NUMBER || ''; export const PR_HEAD_BRANCH = process.env.PR_HEAD_BRANCH || ''; export const PR_BASE_BRANCH = process.env.PR_BASE_BRANCH || ''; diff --git a/app/gitea.js b/app/gitea.js index f8b4216..904b456 100644 --- a/app/gitea.js +++ b/app/gitea.js @@ -1,8 +1,8 @@ import axios from 'axios'; import https from 'https'; -import { GITEA_TOKEN, GITEA_SERVER_URL, GITEA_REPOSITORY, PR_NUMBER } from './config.js'; +import { GITEA_TOKEN, GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_SKIP_TLS_VERIFY, PR_NUMBER } from './config.js'; -const httpsAgent = new https.Agent({ rejectUnauthorized: false }); +const httpsAgent = GITEA_SKIP_TLS_VERIFY ? new https.Agent({ rejectUnauthorized: false }) : undefined; const headers = () => ({ Authorization: `token ${GITEA_TOKEN}`, 'Content-Type': 'application/json' }); const api = (path) => `${GITEA_SERVER_URL.replace(/\/$/, '')}/api/v1${path}`; From 81d5e3ff13f76142b08f56e14a4a56e0c15f65b9 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 09:40:56 +0000 Subject: [PATCH 169/174] feat: update exclusions.json suggestions and refactor Dockerfile for improved npm installation --- .gitea/ai-review/exclusions.json | 15 +++++++++++++++ Dockerfile | 9 +++++---- app/package.json | 2 +- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 6dbc176..10800b7 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -168,5 +168,20 @@ "role": "Aria", "location": "entrypoint.sh", "suggestion": "entrypoint.sh 檔案結尾已有換行符號(0x0a),符合 POSIX 慣例" + }, + { + "role": "Maya", + "location": "app/main.js", + "suggestion": "main.js 整合測試需要真實 Gitea API、LLM API、git 操作,不適合單元測試。各模組已有獨立單元測試覆蓋" + }, + { + "role": "Maya", + "location": "app/comments.js", + "suggestion": "comments.js 的 buildTable 為簡單字串拼接,postComment 已透過 gitea.js mock 間接測試,補測試效益低" + }, + { + "role": "Maya", + "location": "app/roles.js", + "suggestion": "roles.js 依賴容器內固定路徑 /action/app/prompts/roles,單元測試環境無法存取,且邏輯為簡單 YAML 讀取與字串拼接" } ] diff --git a/Dockerfile b/Dockerfile index 3a5481d..ee322d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine +FROM alpine:3.20 RUN apk add --no-cache bash nodejs npm git \ && node --version \ @@ -7,10 +7,11 @@ RUN apk add --no-cache bash nodejs npm git \ WORKDIR /action +COPY app/package.json /action/app/ +RUN cd /action/app && npm install + COPY app/ /action/app/ COPY entrypoint.sh /entrypoint.sh - -RUN cd /action/app && npm install && \ - chmod +x /entrypoint.sh +RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] diff --git a/app/package.json b/app/package.json index 642c211..b010617 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "type": "module", "scripts": { - "test": "node --test git.test.js config.test.js llm.test.js" + "test": "node --test *.test.js" }, "dependencies": { "axios": "^1.6.7", From d565b79feb10f7ebe3528e255c1e3500f7ffb67f Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 09:50:51 +0000 Subject: [PATCH 170/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 62 +++++++++++++--------------------- 1 file changed, 24 insertions(+), 38 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 171fde4..fe5e661 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,30 +1,30 @@ [ + { + "level": "critical", + "role": "Maya", + "location": "app/gitea.js", + "suggestion": "`app/gitea.js` 模組負責與 Gitea API 進行所有互動,包括獲取 PR Diff 和發布評論。這些網路操作是 Action 運作的基礎,但目前缺乏單元測試。應補齊測試以驗證 API 請求的正確性(URL、Header、Body)、錯誤處理機制(例如網路中斷、API 錯誤回應)以及 `GITEA_SKIP_TLS_VERIFY` 的行為。", + "is_new": true + }, { "level": "critical", "role": "Leo", "location": "app/gitea.js:10", "suggestion": "在 `app/gitea.js` 中,`https.Agent` 設定了 `rejectUnauthorized: false`,這會禁用 SSL/TLS 憑證驗證。這是一個嚴重的安全漏洞,會使應用程式容易受到中間人攻擊,並嚴重影響系統的安全性與可維護性。強烈建議移除此設定,或在必要時配置正確的憑證信任鏈。", - "is_new": true - }, - { - "level": "critical", - "role": "Aria", - "location": "Dockerfile", - "suggestion": "檔案結尾缺少換行符號(newline character),這不符合 POSIX 慣例,建議在檔案末尾新增一個空行。", - "is_new": true - }, - { - "level": "critical", - "role": "Aria", - "location": "entrypoint.sh", - "suggestion": "檔案結尾缺少換行符號(newline character),這不符合 POSIX 慣例,建議在檔案末尾新增一個空行。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Leo", - "location": "Dockerfile:2", - "suggestion": "在 `Dockerfile` 中,基礎映像檔使用了 `FROM alpine` 而沒有指定具體的版本標籤(例如 `alpine:3.18`)。這可能導致未來 `alpine:latest` 更新時,建置環境發生非預期的變更,影響建置的可重現性和穩定性。為了長期維護性,建議明確指定基礎映像檔的版本。", + "location": "app/roles.js:4", + "suggestion": "在 `ROLES_DIR` 常數中,使用了硬編碼的路徑 `/action/app/prompts/roles`。這使得在 Docker 容器外部進行本地開發或測試時,需要額外配置才能正確載入角色定義。建議將此路徑設定為可配置的環境變數,或使用相對路徑並在 `main.js` 中傳入基礎路徑,以提高模組的彈性和可測試性。", + "is_new": true + }, + { + "level": "warning", + "role": "Zara", + "location": "app/main.js:60", + "suggestion": "`analyzeWithRole` 函式在迴圈中為每個角色依序呼叫 LLM 進行分析。由於 LLM 呼叫是高延遲操作,這種序列化執行會顯著增加整個 Code Review Action 的總執行時間。考慮使用 `Promise.all` 等方式平行化這些 LLM 呼叫,以縮短總等待時間。", "is_new": true }, { @@ -32,34 +32,20 @@ "role": "Zara", "location": "Dockerfile", "suggestion": "建議調整 Dockerfile 的層次,將 `COPY app/package.json app/package-lock.json /action/app/` 放在 `npm install` 之前,然後再 `COPY app/ /action/app/`。這樣可以利用 Docker 的層次快取,當 `package.json` 或 `package-lock.json` 未變更時,`npm install` 步驟就不會重複執行,顯著加快 Docker 映像檔的建置速度。", - "is_new": true + "is_new": false }, { "level": "warning", - "role": "Maya", - "location": "app/main.js", - "suggestion": "儘管各單元模組(`config`, `git`, `llm`)已有良好的單元測試,但 `app/main.js` 作為整個 Action 的協調者,其整合邏輯和端到端流程缺乏測試。特別是 `TODO.md` 中提到的「阻擋嚴重問題 PR(exit 1)」等關鍵行為,應透過整合測試或端到端測試來驗證,確保各模組協同運作正確,且整體流程符合預期。", - "is_new": true - }, - { - "level": "warning", - "role": "Maya", - "location": "app/comments.js", - "suggestion": "`app/comments.js` 模組負責生成評論內容和儲存 findings 檔案。其中 `buildTable` 等函式負責 Markdown 表格的生成,以及 `postOldFindingsComment`, `postNewNonCriticalComment`, `postNewCriticalComments` 中的過濾邏輯,應補齊單元測試,以確保評論內容的格式正確性及問題分類的準確性。", - "is_new": true - }, - { - "level": "warning", - "role": "Maya", - "location": "app/roles.js", - "suggestion": "`app/roles.js` 模組負責載入和處理角色定義。`loadRoles` 函式應增加單元測試,以驗證其能正確解析 YAML 檔案,並處理檔案不存在或格式錯誤等異常情況。`getRoleIntro` 函式也應測試其生成的 Markdown 介紹內容是否符合預期格式。", + "role": "Aria", + "location": "app/package.json:5", + "suggestion": "`package.json` 中的 `test` 腳本 (`node --test *.test.js`) 僅會執行 `app/` 目錄下的測試檔案。建議修改為 `node --test app/**/*.test.js` 或使用更完善的測試框架(如 `mocha` 或 `jest`),以確保所有測試檔案都能被自動發現並執行。", "is_new": true }, { "level": "info", - "role": "Maya", - "location": "app/package.json", - "suggestion": "目前的 `test` 腳本 (`node --test git.test.js config.test.js llm.test.js`) 僅涵蓋了部分單元測試。建議更新此腳本,使其能自動發現並執行所有 `*.test.js` 檔案,以確保所有新增的測試都能被納入 CI 流程中執行。", + "role": "Zara", + "location": "app/main.js:72, app/main.js:78", + "suggestion": "`deduplicateWithAI` 和 `filterFalsePositivesWithAI` 函式也涉及 LLM 呼叫,它們在主流程中依序執行。雖然這些是單次呼叫,但其延遲仍會累積。在設計上,可以考慮是否有可能將這些步驟與其他非依賴的任務平行化,或或評估其對整體效能的影響。", "is_new": true } ] From 2aba414d36a52b832add1969c9614614cb18d5f3 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 09:52:03 +0000 Subject: [PATCH 171/174] feat: update GEMINI_BASE_URL to use v1 endpoint in workflow and documentation --- .gitea/workflows/review.yaml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 5c36cc4..4fca04a 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -34,7 +34,7 @@ jobs: uses: https://gitea.jsc.idv.tw/jiantw83/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 + GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1 GEMINI_MODEL: ${{ vars.GEMINI_MODEL }} permissions: contents: write diff --git a/README.md b/README.md index e1717ef..1262f37 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ jobs: uses: https://gitea.jsc.idv.tw/jiantw83/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 + GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1 GEMINI_MODEL: ${{ vars.GEMINI_MODEL }} permissions: contents: write From c751a53d432ae76c667c309f3f7d347e44c1f15c Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 09:57:33 +0000 Subject: [PATCH 172/174] feat: enhance exclusions.json with new suggestions and refactor roles.js for dynamic path resolution --- .gitea/ai-review/exclusions.json | 20 ++++++++++ app/gitea.test.js | 64 ++++++++++++++++++++++++++++++++ app/main.js | 12 +++--- app/roles.js | 3 +- 4 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 app/gitea.test.js diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 10800b7..3743a5a 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -183,5 +183,25 @@ "role": "Maya", "location": "app/roles.js", "suggestion": "roles.js 依賴容器內固定路徑 /action/app/prompts/roles,單元測試環境無法存取,且邏輯為簡單 YAML 讀取與字串拼接" + }, + { + "role": "Leo", + "location": "app/gitea.js", + "suggestion": "gitea.js 的 SSL 驗證已改為由 GITEA_SKIP_TLS_VERIFY 環境變數控制,預設啟用驗證,非安全漏洞" + }, + { + "role": "Zara", + "location": "Dockerfile", + "suggestion": "Dockerfile 已優化層次快取:先 COPY package.json 再 npm install,最後才 COPY 其餘檔案" + }, + { + "role": "Aria", + "location": "app/package.json", + "suggestion": "test 腳本已改為 node --test *.test.js,在 app/ 目錄下執行可自動發現所有測試檔案" + }, + { + "role": "Zara", + "location": "app/main.js", + "suggestion": "deduplicateWithAI 和 filterFalsePositivesWithAI 為循序依賴流程(去重後才能過濾),無法平行化" } ] diff --git a/app/gitea.test.js b/app/gitea.test.js new file mode 100644 index 0000000..bd19a83 --- /dev/null +++ b/app/gitea.test.js @@ -0,0 +1,64 @@ +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'); + + it('getPRDiff calls Gitea diff API with Authorization header', async () => { + let capturedUrl, capturedOpts; + mock.method(axios, 'get', async (url, opts) => { + capturedUrl = url; + capturedOpts = opts; + return { data: 'diff content' }; + }); + const result = await getPRDiff(); + assert.equal(result, 'diff content'); + assert.ok(capturedUrl.includes('/api/v1/repos/')); + assert.ok(capturedUrl.endsWith('.diff')); + assert.ok(capturedOpts.headers['Authorization'].startsWith('token ')); + assert.equal(capturedOpts.headers['Content-Type'], 'application/json'); + }); + + it('postComment calls Gitea issues comments API with body', async () => { + let capturedUrl, capturedBody, capturedOpts; + mock.method(axios, 'post', async (url, body, opts) => { + capturedUrl = url; + capturedBody = body; + capturedOpts = opts; + return { data: { id: 1 } }; + }); + const result = await postComment('hello world'); + assert.deepEqual(result, { id: 1 }); + assert.ok(capturedUrl.includes('/api/v1/repos/')); + assert.ok(capturedUrl.endsWith('/comments')); + assert.equal(capturedBody.body, 'hello world'); + assert.ok(capturedOpts.headers['Authorization'].startsWith('token ')); + }); + + it('does not set httpsAgent by default (GITEA_SKIP_TLS_VERIFY not true)', async () => { + let capturedOpts; + mock.method(axios, 'get', async (_url, opts) => { + capturedOpts = opts; + return { data: '' }; + }); + await getPRDiff(); + // httpsAgent is undefined when GITEA_SKIP_TLS_VERIFY !== 'true' + assert.equal(capturedOpts.httpsAgent, undefined); + }); + + it('getPRDiff propagates axios errors', async () => { + mock.method(axios, 'get', async () => { throw new Error('network error'); }); + await assert.rejects(() => getPRDiff(), /network error/); + }); + + it('postComment propagates axios errors', async () => { + mock.method(axios, 'post', async () => { throw new Error('api error'); }); + await assert.rejects(() => postComment('test'), /api error/); + }); +}); diff --git a/app/main.js b/app/main.js index 6e58f60..67a797d 100644 --- a/app/main.js +++ b/app/main.js @@ -52,13 +52,13 @@ async function main() { // Step2: 各角色分析 diff 產生新 findings console.log('\n📊 Step2: Findings 產生'); + const results = await Promise.allSettled(roles.map(role => analyzeWithRole(role, diff))); const newFindings = []; - for (const role of roles) { - try { - const found = await analyzeWithRole(role, diff); - newFindings.push(...found); - } catch (e) { - console.log(` ⚠️ [${role.name}] 分析失敗(跳過): ${e.message}`); + for (let i = 0; i < results.length; i++) { + if (results[i].status === 'fulfilled') { + newFindings.push(...results[i].value); + } else { + console.log(` ⚠️ [${roles[i].name}] 分析失敗(跳過): ${results[i].reason?.message}`); } } console.log(` Step2 完成: 新 findings 總計 ${newFindings.length} 筆`); diff --git a/app/roles.js b/app/roles.js index da58bf0..d4e6a7c 100644 --- a/app/roles.js +++ b/app/roles.js @@ -1,8 +1,9 @@ import fs from 'fs'; import path from 'path'; +import { fileURLToPath } from 'url'; import yaml from 'js-yaml'; -const ROLES_DIR = '/action/app/prompts/roles'; +const ROLES_DIR = path.join(fileURLToPath(import.meta.url), '..', 'prompts', 'roles'); export function loadRoles() { return fs.readdirSync(ROLES_DIR) From 52fa3acf187e0c692af1f6e812fd17952cacc274 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 09:58:43 +0000 Subject: [PATCH 173/174] feat: update GEMINI_BASE_URL to v1beta for improved API compatibility --- .gitea/workflows/review.yaml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index 4fca04a..5c36cc4 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -34,7 +34,7 @@ jobs: uses: https://gitea.jsc.idv.tw/jiantw83/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/v1 + GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta GEMINI_MODEL: ${{ vars.GEMINI_MODEL }} permissions: contents: write diff --git a/README.md b/README.md index 1262f37..e1717ef 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ jobs: uses: https://gitea.jsc.idv.tw/jiantw83/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/v1 + GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta GEMINI_MODEL: ${{ vars.GEMINI_MODEL }} permissions: contents: write From 931481179ad014aaef2aa6459674e5dceb1a1a0c Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Tue, 12 May 2026 10:02:45 +0000 Subject: [PATCH 174/174] chore: update ai-review findings [skip ci] --- .gitea/ai-review/findings.json | 48 +++------------------------------- 1 file changed, 3 insertions(+), 45 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index fe5e661..6269a2b 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,51 +1,9 @@ [ - { - "level": "critical", - "role": "Maya", - "location": "app/gitea.js", - "suggestion": "`app/gitea.js` 模組負責與 Gitea API 進行所有互動,包括獲取 PR Diff 和發布評論。這些網路操作是 Action 運作的基礎,但目前缺乏單元測試。應補齊測試以驗證 API 請求的正確性(URL、Header、Body)、錯誤處理機制(例如網路中斷、API 錯誤回應)以及 `GITEA_SKIP_TLS_VERIFY` 的行為。", - "is_new": true - }, - { - "level": "critical", - "role": "Leo", - "location": "app/gitea.js:10", - "suggestion": "在 `app/gitea.js` 中,`https.Agent` 設定了 `rejectUnauthorized: false`,這會禁用 SSL/TLS 憑證驗證。這是一個嚴重的安全漏洞,會使應用程式容易受到中間人攻擊,並嚴重影響系統的安全性與可維護性。強烈建議移除此設定,或在必要時配置正確的憑證信任鏈。", - "is_new": false - }, - { - "level": "warning", - "role": "Leo", - "location": "app/roles.js:4", - "suggestion": "在 `ROLES_DIR` 常數中,使用了硬編碼的路徑 `/action/app/prompts/roles`。這使得在 Docker 容器外部進行本地開發或測試時,需要額外配置才能正確載入角色定義。建議將此路徑設定為可配置的環境變數,或使用相對路徑並在 `main.js` 中傳入基礎路徑,以提高模組的彈性和可測試性。", - "is_new": true - }, - { - "level": "warning", - "role": "Zara", - "location": "app/main.js:60", - "suggestion": "`analyzeWithRole` 函式在迴圈中為每個角色依序呼叫 LLM 進行分析。由於 LLM 呼叫是高延遲操作,這種序列化執行會顯著增加整個 Code Review Action 的總執行時間。考慮使用 `Promise.all` 等方式平行化這些 LLM 呼叫,以縮短總等待時間。", - "is_new": true - }, - { - "level": "warning", - "role": "Zara", - "location": "Dockerfile", - "suggestion": "建議調整 Dockerfile 的層次,將 `COPY app/package.json app/package-lock.json /action/app/` 放在 `npm install` 之前,然後再 `COPY app/ /action/app/`。這樣可以利用 Docker 的層次快取,當 `package.json` 或 `package-lock.json` 未變更時,`npm install` 步驟就不會重複執行,顯著加快 Docker 映像檔的建置速度。", - "is_new": false - }, - { - "level": "warning", - "role": "Aria", - "location": "app/package.json:5", - "suggestion": "`package.json` 中的 `test` 腳本 (`node --test *.test.js`) 僅會執行 `app/` 目錄下的測試檔案。建議修改為 `node --test app/**/*.test.js` 或使用更完善的測試框架(如 `mocha` 或 `jest`),以確保所有測試檔案都能被自動發現並執行。", - "is_new": true - }, { "level": "info", - "role": "Zara", - "location": "app/main.js:72, app/main.js:78", - "suggestion": "`deduplicateWithAI` 和 `filterFalsePositivesWithAI` 函式也涉及 LLM 呼叫,它們在主流程中依序執行。雖然這些是單次呼叫,但其延遲仍會累積。在設計上,可以考慮是否有可能將這些步驟與其他非依賴的任務平行化,或或評估其對整體效能的影響。", + "role": "Rex", + "location": "action.yaml", + "suggestion": "此 Action 需要 `contents: write`、`pull-requests: write` 和 `issues: write` 權限。這些權限對於 Action 的正常運作是必要的(例如寫入 findings.json、發布評論),但屬於較廣泛的權限。建議在文件或使用說明中明確指出這些權限的需求及其潛在影響,確保使用者了解並接受。", "is_new": true } ]