Compare commits

..

9 Commits

7 changed files with 197 additions and 98 deletions
+58 -1
View File
@@ -1 +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": 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": 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": "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": true
}
]
+3 -2
View File
@@ -33,8 +33,9 @@ jobs:
- name: AI Code Review - name: AI Code Review
uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }} uses: https://gitea.jsc.idv.tw/jiantw83/code-review@v${{ needs.version.outputs.version }}
with: with:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
OPENAI_BASE_URL: https://openrouter.ai/api/v1 GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta
GEMINI_MODEL: ${{ vars.GEMINI_MODEL }}
permissions: permissions:
contents: write contents: write
pull-requests: write pull-requests: write
+27 -28
View File
@@ -28,7 +28,7 @@
2.`.gitea/workflows` 資料夾中建立 `ai-review.yaml' 2.`.gitea/workflows` 資料夾中建立 `ai-review.yaml'
3.`ai-review.yaml` 中填入以下內容(選擇一個使用) 3.`ai-review.yaml` 中填入以下內容(選擇一個使用)
### 1. OpenAIOpenRouter ### 1. OpenAI
```yaml ```yaml
name: AI name: AI
on: on:
@@ -42,9 +42,29 @@ jobs:
- name: AI Code Review - name: AI Code Review
uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }}
with: with:
# Github (h3285@evertrust.com.tw)
# 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
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 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 }} # OpenRouter 使用 OpenAI 相容介面,以 OPENAI_API_KEY 傳入
OPENAI_BASE_URL: https://openrouter.ai/api/v1 OPENAI_BASE_URL: https://openrouter.ai/api/v1
permissions: permissions:
contents: write contents: write
@@ -52,7 +72,7 @@ jobs:
issues: write issues: write
``` ```
### 2. Anthropic Claude ### 3. Anthropic Claude
```yaml ```yaml
name: AI name: AI
on: on:
@@ -74,7 +94,7 @@ jobs:
issues: write issues: write
``` ```
### 3. Google Gemini ### 4. Google Gemini
```yaml ```yaml
name: AI name: AI
on: on:
@@ -90,13 +110,14 @@ jobs:
with: with:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta
GEMINI_MODEL: ${{ vars.GEMINI_MODEL }}
permissions: permissions:
contents: write contents: write
pull-requests: write pull-requests: write
issues: write issues: write
``` ```
### 4. Amazon Q ### 5. Amazon Q
```yaml ```yaml
name: AI name: AI
on: on:
@@ -118,28 +139,6 @@ jobs:
issues: 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
```
### - Ollama ### - Ollama
```yaml ```yaml
-58
View File
@@ -72,53 +72,7 @@ inputs:
description: 'Amazon Q Base URL' description: 'Amazon Q Base URL'
required: false 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: runs:
using: 'docker' using: 'docker'
@@ -145,15 +99,3 @@ runs:
OLLAMA_MODEL: ${{ inputs.OLLAMA_MODEL }} OLLAMA_MODEL: ${{ inputs.OLLAMA_MODEL }}
AMAZONQ_API_KEY: ${{ inputs.AMAZONQ_API_KEY }} AMAZONQ_API_KEY: ${{ inputs.AMAZONQ_API_KEY }}
AMAZONQ_BASE_URL: ${{ inputs.AMAZONQ_BASE_URL }} 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 }}
+2 -7
View File
@@ -12,14 +12,9 @@ export function getLLMConfig() {
const checks = [ 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'], ['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'], ['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], ['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'], ['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.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) { for (const [provider, key, baseURL, model] of checks) {
if (key && baseURL) return { provider, apiKey: key, baseURL, model }; if (key && baseURL) return { provider, apiKey: key, baseURL, model };
+101
View File
@@ -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');
});
});
+6 -2
View File
@@ -12,9 +12,13 @@ export function loadRoles() {
} }
export function getRoleIntro(roles) { export function getRoleIntro(roles) {
const lines = ['## 🤖 AI Code Review 團隊', '']; const lines = [
'## 🤖 AI Code Review 團隊', '',
'| 👤 名稱 | 🎯 職責 | 🧠 個性 |',
'|--------|--------|--------|',
];
for (const r of roles) { 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'); return lines.join('\n');
} }