Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b7aeedfdb | |||
| 50f422f0d3 | |||
| c3533bdfb6 | |||
| 7578bee5d3 | |||
| 8a8612b46d | |||
| e3b4c7f8d4 | |||
| fdcb9f04de | |||
| e3596eb710 | |||
| 83943b8dda | |||
| 234a8a829f | |||
| b164fe855e | |||
| 16cb1966f0 | |||
| 8d3f5e3a45 | |||
| 85ff61e98f | |||
| 4bace91d3d | |||
| e541cee83f | |||
| 8ad7ae51a4 | |||
| 3861d288fb | |||
| 990ef7c847 |
@@ -59,5 +59,39 @@
|
|||||||
"role": "Aria",
|
"role": "Aria",
|
||||||
"location": "action.yaml",
|
"location": "action.yaml",
|
||||||
"suggestion": "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": "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 解析器,無相容性問題"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": ".gitea/ai-review/findings.json",
|
||||||
|
"suggestion": "findings.json 是自動產生的問題記錄檔,不應對其內容提出審查問題"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Rex",
|
||||||
|
"location": ".gitea/workflows/review.yaml",
|
||||||
|
"suggestion": "切換 LLM 服務提供商的維護建議屬過度謹慎,不是實際程式碼問題"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+30
-100
@@ -1,107 +1,37 @@
|
|||||||
[
|
[
|
||||||
{
|
|
||||||
"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",
|
"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",
|
|
||||||
"location": ".gitea/ai-review/findings.json",
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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": "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",
|
"role": "Zara",
|
||||||
"location": ".gitea/workflows/review.yaml:35-37",
|
"location": "app/comments.js:24",
|
||||||
"suggestion": "工作流程從 OpenRouter/OpenAI 服務切換至 Google Gemini 服務。雖然這本身不是程式碼錯誤,但不同 AI 服務提供商的 API 響應時間、吞吐量和穩定性可能存在差異。建議監控切換後 AI Code Review 步驟的執行時間,確保新配置能維持或提升效能,並留意潛在的成本變化。",
|
"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
|
"is_new": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ 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:
|
||||||
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_BASE_URL: https://generativelanguage.googleapis.com/v1beta
|
||||||
GEMINI_MODEL: ${{ vars.GEMINI_MODEL }}
|
GEMINI_MODEL: ${{ vars.GEMINI_MODEL }}
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
3. Comment 加上些許 emoji 讓資訊有點活力
|
3. Comment 加上些許 emoji 讓資訊有點活力
|
||||||
4. 盡量將應用程式放在 ./app,修改 entrypoint.sh 與 Dockerfile 讓程式可以正常運行
|
4. 盡量將應用程式放在 ./app,修改 entrypoint.sh 與 Dockerfile 讓程式可以正常運行
|
||||||
5. 將提示詞放到 ./app/prompts 內供程式讀取
|
5. 將提示詞放到 ./app/prompts 內供程式讀取
|
||||||
|
6. API Key 支援逗號分隔傳入多個,依序嘗試,失敗時自動換下一個,全部失敗則 exit 1
|
||||||
|
|
||||||
# 使用說明
|
# 使用說明
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ 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:
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} # 支援逗號分隔多個 Key
|
||||||
OPENAI_BASE_URL: https://api.openai.com/v1
|
OPENAI_BASE_URL: https://api.openai.com/v1
|
||||||
OPENAI_MODEL: ${{ vars.OPENAI_MODEL }}
|
OPENAI_MODEL: ${{ vars.OPENAI_MODEL }}
|
||||||
permissions:
|
permissions:
|
||||||
@@ -65,7 +66,7 @@ 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:
|
||||||
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_BASE_URL: https://openrouter.ai/api/v1
|
||||||
OPENAI_MODEL: ${{ vars.OPENROUTER_MODEL }}
|
OPENAI_MODEL: ${{ vars.OPENROUTER_MODEL }}
|
||||||
permissions:
|
permissions:
|
||||||
@@ -88,7 +89,7 @@ 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:
|
||||||
CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }}
|
CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} # 支援逗號分隔多個 Key
|
||||||
CLAUDE_BASE_URL: https://api.anthropic.com/v1
|
CLAUDE_BASE_URL: https://api.anthropic.com/v1
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -110,7 +111,7 @@ 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:
|
||||||
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_BASE_URL: https://generativelanguage.googleapis.com/v1beta
|
||||||
GEMINI_MODEL: ${{ vars.GEMINI_MODEL }}
|
GEMINI_MODEL: ${{ vars.GEMINI_MODEL }}
|
||||||
permissions:
|
permissions:
|
||||||
@@ -133,7 +134,7 @@ 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:
|
||||||
AMAZONQ_API_KEY: ${{ secrets.AMAZONQ_API_KEY }}
|
AMAZONQ_API_KEY: ${{ secrets.AMAZONQ_API_KEY }} # 支援逗號分隔多個 Key
|
||||||
AMAZONQ_BASE_URL: https://q.api.aws
|
AMAZONQ_BASE_URL: https://q.api.aws
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@@ -35,6 +35,11 @@
|
|||||||
- 驗收:log 中能看到「critical 問題存在,workflow 結束(exit 1)」等明確訊息,且 workflow 狀態為失敗。
|
- 驗收:log 中能看到「critical 問題存在,workflow 結束(exit 1)」等明確訊息,且 workflow 狀態為失敗。
|
||||||
- 完成
|
- 完成
|
||||||
|
|
||||||
|
## 階段八:API Key 輪替
|
||||||
|
- 目標:所有平台的 API Key 支援逗號分隔傳入多個,依序嘗試,單一 Key 失敗時自動換下一個,全部失敗則 exit 1。
|
||||||
|
- 驗收:log 中能看到「key[N/M] 失敗」等訊息,換 key 後繼續執行;傳入單一 Key 時行為與原本相同;全部 Key 失敗時 log「所有 API Key 均失敗,終止流程」且 workflow 狀態為失敗。
|
||||||
|
- 完成
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
所有階段驗收通過。
|
所有階段驗收通過。
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ function buildTable(findings) {
|
|||||||
export function saveFindings(workspace, findings) {
|
export function saveFindings(workspace, findings) {
|
||||||
const fullPath = path.join(workspace, FINDINGS_PATH);
|
const fullPath = path.join(workspace, FINDINGS_PATH);
|
||||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
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} 筆)`);
|
console.log(` ✅ findings 寫入: ${fullPath} (${findings.length} 筆)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-8
@@ -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 FINDINGS_PATH = '.gitea/ai-review/findings.json';
|
||||||
export const EXCLUSIONS_PATH = '.gitea/ai-review/exclusions.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() {
|
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', splitKeys(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', 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', process.env.GEMINI_API_KEY, process.env.GEMINI_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta', process.env.GEMINI_MODEL || 'gemini-2.5-flash'],
|
['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],
|
['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'],
|
['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) {
|
for (const [provider, apiKeys, baseURL, model] of checks) {
|
||||||
if (key && baseURL) return { provider, apiKey: key, baseURL, model };
|
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 };
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -26,14 +26,14 @@ describe('getLLMConfig', () => {
|
|||||||
it('returns null provider when no env vars set', () => {
|
it('returns null provider when no env vars set', () => {
|
||||||
const cfg = getLLMConfig();
|
const cfg = getLLMConfig();
|
||||||
assert.equal(cfg.provider, null);
|
assert.equal(cfg.provider, null);
|
||||||
assert.equal(cfg.apiKey, null);
|
assert.deepEqual(cfg.apiKeys, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('detects openai with defaults', () => {
|
it('detects openai with defaults', () => {
|
||||||
process.env.OPENAI_API_KEY = 'sk-test';
|
process.env.OPENAI_API_KEY = 'sk-test';
|
||||||
const cfg = getLLMConfig();
|
const cfg = getLLMConfig();
|
||||||
assert.equal(cfg.provider, 'openai');
|
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.baseURL, 'https://api.openai.com/v1');
|
||||||
assert.equal(cfg.model, 'gpt-4o-mini');
|
assert.equal(cfg.model, 'gpt-4o-mini');
|
||||||
});
|
});
|
||||||
@@ -48,7 +48,14 @@ describe('getLLMConfig', () => {
|
|||||||
assert.equal(cfg.model, 'gpt-4o');
|
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';
|
process.env.GEMINI_API_KEY = 'gemini-key';
|
||||||
const cfg = getLLMConfig();
|
const cfg = getLLMConfig();
|
||||||
assert.equal(cfg.provider, 'gemini');
|
assert.equal(cfg.provider, 'gemini');
|
||||||
|
|||||||
+19
-11
@@ -5,23 +5,31 @@ import { getLLMConfig } from './config.js';
|
|||||||
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
||||||
|
|
||||||
export async function chat(systemPrompt, userContent) {
|
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');
|
if (!provider) throw new Error('未設定任何 LLM API Key');
|
||||||
|
|
||||||
console.log(` [LLM] provider=${provider} model=${model}`);
|
console.log(` [LLM] provider=${provider} model=${model}`);
|
||||||
|
|
||||||
const headers = {
|
const headers = { 'Content-Type': 'application/json' };
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${apiKey}`,
|
|
||||||
};
|
|
||||||
if (provider === 'claude') headers['anthropic-version'] = '2023-06-01';
|
if (provider === 'claude') headers['anthropic-version'] = '2023-06-01';
|
||||||
|
|
||||||
const resp = await axios.post(
|
let lastError;
|
||||||
`${baseURL.replace(/\/$/, '')}/chat/completions`,
|
for (let i = 0; i < apiKeys.length; i++) {
|
||||||
{ model, messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userContent }], temperature: 0.2 },
|
headers['Authorization'] = `Bearer ${apiKeys[i]}`;
|
||||||
{ headers, timeout: 120000, httpsAgent }
|
try {
|
||||||
);
|
const resp = await axios.post(
|
||||||
return resp.data.choices[0].message.content;
|
`${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) {
|
export async function chatJSON(systemPrompt, userContent) {
|
||||||
|
|||||||
Reference in New Issue
Block a user