101 lines
3.5 KiB
JavaScript
101 lines
3.5 KiB
JavaScript
import https from 'https';
|
||
import { execFileSync } from 'child_process';
|
||
|
||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||
|
||
export const GITEA_TOKEN = process.env.GITEA_TOKEN || '';
|
||
export const GITEA_COMMENT_TOKEN = process.env.GITEA_COMMENT_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_SHA = process.env.PR_HEAD_SHA || '';
|
||
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';
|
||
|
||
/**
|
||
* 建立一個停用 TLS 憑證驗證(`rejectUnauthorized: false`)的 HTTPS Agent,
|
||
* 供連接使用自簽或無效憑證的內部服務時使用。
|
||
*
|
||
* @remarks 首次呼叫時建立,之後快取為模組層級單例(singleton)重複使用,
|
||
* 避免每次都新建 Agent 與連線池、浪費 TCP 三次握手。
|
||
* 停用憑證驗證有中間人攻擊風險,僅限受信任的內部環境使用。
|
||
* @returns {import('https').Agent} 已關閉憑證驗證的 HTTPS Agent 單例。
|
||
*/
|
||
let _insecureHttpsAgent = null;
|
||
export function getInsecureHttpsAgent() {
|
||
return (_insecureHttpsAgent ??= new https.Agent({ rejectUnauthorized: false }));
|
||
}
|
||
|
||
export const getOpenCodeHttpsAgent = getInsecureHttpsAgent;
|
||
|
||
const CLI_CANDIDATES = [
|
||
{
|
||
provider: 'codex',
|
||
command: 'codex',
|
||
defaultModel: 'gpt-5',
|
||
},
|
||
{
|
||
provider: 'claude',
|
||
command: 'claude',
|
||
defaultModel: 'sonnet',
|
||
},
|
||
{
|
||
provider: 'antigravity',
|
||
command: 'agy',
|
||
defaultModel: 'gemini-2.5-flash',
|
||
},
|
||
{
|
||
provider: 'antigravity',
|
||
command: 'antigravity',
|
||
defaultModel: 'gemini-2.5-flash',
|
||
},
|
||
{
|
||
provider: 'opencode',
|
||
command: 'opencode',
|
||
defaultModel: 'google/gemini-2.5-flash',
|
||
},
|
||
];
|
||
|
||
export function getLLMCLICommands() {
|
||
return CLI_CANDIDATES.map(c => c.command);
|
||
}
|
||
|
||
function commandExists(command) {
|
||
try {
|
||
execFileSync('/bin/sh', ['-lc', `command -v ${command}`], { stdio: 'ignore' });
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 依環境變數解析並回傳 LLM 提供者設定。
|
||
*
|
||
* 優先使用 `AI_ASSISTANT_CLI` 指定的 CLI;未指定時依序偵測 codex、claude、antigravity、opencode。
|
||
* model 優先取 `MODEL`,再相容舊的 `OPENCODE_MODEL`,最後使用各 CLI 預設值。
|
||
*
|
||
* @param {{ commandExistsFn?: (command: string) => boolean }} [deps] - 可注入的 CLI 偵測函式,供測試使用。
|
||
* @returns {{ provider: ('codex'|'claude'|'antigravity'|'opencode'|null), apiKeys: string[], baseURL: null, model: (string|null), command: (string|null) }}
|
||
* LLM 設定物件;`provider` 為 `null` 表示沒有可用的提供者。
|
||
*/
|
||
export function getLLMConfig({ commandExistsFn = commandExists } = {}) {
|
||
const requested = process.env.AI_ASSISTANT_CLI;
|
||
const candidates = requested
|
||
? CLI_CANDIDATES.filter(c => c.provider === requested || c.command === requested)
|
||
: CLI_CANDIDATES;
|
||
const cli = candidates.find(c => commandExistsFn(c.command));
|
||
if (!cli) return { provider: null, apiKeys: [], baseURL: null, model: null, command: null };
|
||
|
||
return {
|
||
provider: cli.provider,
|
||
apiKeys: [cli.provider],
|
||
baseURL: null,
|
||
model: process.env.MODEL || process.env.OPENCODE_MODEL || cli.defaultModel,
|
||
command: cli.command,
|
||
};
|
||
}
|