import https from 'https'; import fs from 'fs'; import { execFileSync } from 'child_process'; // 本 action 會連接自架 Gitea / OpenCode,部署環境可能使用內部 CA 或自簽憑證。 // 對外部服務請優先使用預設 TLS 驗證;需要內部服務相容時才使用 getInsecureHttpsAgent()。 process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; /** * 讀取 runner 寫入的事件 payload JSON(`GITHUB_EVENT_PATH` / `GITEA_EVENT_PATH`)。 * 讀不到或解析失敗時回傳空物件,讓後續取值一律走 fallback,不讓 import 期噴錯。 * * @returns {Record} 事件 payload 物件,失敗時為 `{}`。 */ function readEventPayload() { const eventPath = process.env.GITHUB_EVENT_PATH || process.env.GITEA_EVENT_PATH; if (!eventPath) return {}; try { return JSON.parse(fs.readFileSync(eventPath, 'utf8')); } catch { return {}; } } const EVENT = readEventPayload(); const PR = EVENT.pull_request || {}; // 取值優先序:有對應 `with:` 輸入的欄位一律 INPUT_* 優先(使用端明確傳入的值最權威, // 蓋過環境中剛好存在的 ambient env)→ 專用 env(相容舊 Docker 版與測試)→ 內建預設。 // 無對應輸入的欄位(server url / repository / PR_*)則走專用 env → runner 內建 / 事件 payload。 // 使用端 workflow 只需傳 `with: token`。 export const GITEA_TOKEN = process.env.INPUT_TOKEN || process.env.GITEA_TOKEN || ''; export const GITEA_COMMENT_TOKEN = process.env.INPUT_COMMENT_TOKEN || process.env.GITEA_COMMENT_TOKEN || process.env.INPUT_TOKEN || process.env.GITEA_TOKEN || ''; export const GITEA_SERVER_URL = process.env.GITEA_SERVER_URL || process.env.GITHUB_SERVER_URL || 'https://gitea.com'; export const GITEA_REPOSITORY = process.env.GITEA_REPOSITORY || process.env.GITHUB_REPOSITORY || ''; export const PR_NUMBER = process.env.PR_NUMBER || (PR.number != null ? String(PR.number) : ''); export const PR_HEAD_SHA = process.env.PR_HEAD_SHA || PR.head?.sha || process.env.GITHUB_SHA || ''; export const PR_HEAD_BRANCH = process.env.PR_HEAD_BRANCH || PR.head?.ref || process.env.GITHUB_HEAD_REF || ''; export const PR_BASE_BRANCH = process.env.PR_BASE_BRANCH || PR.base?.ref || process.env.GITHUB_BASE_REF || ''; 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 })); } // 過渡別名:既有呼叫端仍可用 OpenCode 語意名稱;新程式碼請直接使用 getInsecureHttpsAgent。 export const getOpenCodeHttpsAgent = getInsecureHttpsAgent; const CLI_CANDIDATES = [ { provider: 'codex', command: 'codex', defaultModel: 'gpt-5.4-mini', }, { 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 依序取 `with: model`(`INPUT_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.INPUT_MODEL || process.env.MODEL || process.env.OPENCODE_MODEL || cli.defaultModel, command: cli.command, }; }