import axios from 'axios'; import https from 'https'; import { GITEA_TOKEN, GITEA_COMMENT_TOKEN, GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_SKIP_TLS_VERIFY, PR_NUMBER, getLLMConfig, } from './config.js'; import { verifyRemoteAccess } from './git.js'; import { step, line, ok, error } from './log.js'; const httpsAgent = GITEA_SKIP_TLS_VERIFY ? new https.Agent({ rejectUnauthorized: false }) : undefined; const api = (path) => `${GITEA_SERVER_URL.replace(/\/$/, '')}/api/v1${path}`; const giteaHeaders = (token) => ({ Authorization: `token ${token}`, 'Content-Type': 'application/json' }); function giteaErr(e) { const status = e.response?.status; return status ? `HTTP ${status} ${e.message}` : e.message; } /** 檢查必要環境變數是否齊全;可傳入覆寫值供測試使用 */ export function checkRequiredEnv({ token = GITEA_TOKEN, repo = GITEA_REPOSITORY, pr = PR_NUMBER } = {}) { const missing = []; if (!token) missing.push('GITEA_TOKEN'); if (!repo) missing.push('GITEA_REPOSITORY'); if (!pr) missing.push('PR_NUMBER'); return { ok: missing.length === 0, missing }; } /** 用 GITEA_TOKEN 讀取此 repo,同時驗證 token 有效與有讀取權限 */ export async function verifyGiteaToken(token = GITEA_TOKEN, repo = GITEA_REPOSITORY) { try { await axios.get(api(`/repos/${repo}`), { headers: giteaHeaders(token), timeout: 30000, httpsAgent }); return { ok: true }; } catch (e) { return { ok: false, error: giteaErr(e) }; } } /** 若有提供 comment token,用它呼叫 /user 驗證可用;沒提供則略過 */ export async function verifyCommentToken(token = GITEA_COMMENT_TOKEN) { if (!token) return { ok: true, skipped: true }; try { await axios.get(api('/user'), { headers: giteaHeaders(token), timeout: 30000, httpsAgent }); return { ok: true }; } catch (e) { return { ok: false, error: giteaErr(e) }; } } /** * 驗證 LLM 設定可用: * - 須已選定一個 provider * - Ollama 檢查 base URL 是否可連線 * - 其餘 provider 以最小請求驗證認證,多把 Key 只要一把成功即可 */ export async function verifyLLM() { const { provider, apiKeys, baseURL, model } = getLLMConfig(); if (!provider) return { ok: false, error: '未設定任何 LLM provider 或 API Key' }; if (!baseURL) return { ok: false, provider, error: `${provider} 缺少 base URL` }; const base = baseURL.replace(/\/$/, ''); if (provider === 'ollama') { try { await axios.get(`${base}/models`, { timeout: 30000 }); return { ok: true, provider }; } catch (e) { return { ok: false, provider, error: `Ollama base URL 無法連線: ${e.message}` }; } } const headers = { 'Content-Type': 'application/json' }; if (provider === 'claude') headers['anthropic-version'] = '2023-06-01'; const payload = { model, messages: [{ role: 'user', content: 'ping' }], max_tokens: 1, temperature: 0 }; for (let i = 0; i < apiKeys.length; i++) { headers['Authorization'] = `Bearer ${apiKeys[i]}`; try { await axios.post(`${base}/chat/completions`, payload, { headers, timeout: 30000 }); return { ok: true, provider, keyIndex: i + 1, total: apiKeys.length }; } catch (e) { line(`[preflight] LLM key[${i + 1}/${apiKeys.length}] 驗證失敗: ${e.message}`); } } return { ok: false, provider, error: `所有 ${apiKeys.length} 把 ${provider} API Key 驗證失敗` }; } /** * 集中執行所有驗證相關設定的前置檢查;全部通過回傳 true,任一失敗回傳 false。 * 僅做唯讀的認證/連線確認,不發布任何 comment。 */ export async function runPreflight(workspace = process.env.GITHUB_WORKSPACE || '/workspace') { step('Step1.5', '前置驗證(驗證相關設定)'); const env = checkRequiredEnv(); if (!env.ok) { error(`缺少必要環境變數: ${env.missing.join(', ')}`); return false; } ok('必要環境變數齊全 (GITEA_TOKEN, GITEA_REPOSITORY, PR_NUMBER)'); const gitea = await verifyGiteaToken(); if (!gitea.ok) { error(`GITEA_TOKEN 驗證失敗(無法讀取 repo ${GITEA_REPOSITORY}): ${gitea.error}`); return false; } ok(`GITEA_TOKEN 可讀取 repo ${GITEA_REPOSITORY}`); const comment = await verifyCommentToken(); if (!comment.ok) { error(`GITEA_COMMENT_TOKEN 驗證失敗: ${comment.error}`); return false; } if (comment.skipped) line('未提供 GITEA_COMMENT_TOKEN,comment 將沿用 GITEA_TOKEN'); else ok('GITEA_COMMENT_TOKEN 可用'); const remote = verifyRemoteAccess(workspace); if (!remote.ok) { error(`git push 認證/連線驗證失敗(ls-remote): ${remote.error}`); return false; } ok('git remote 認證可用(ls-remote 成功)'); const llm = await verifyLLM(); if (!llm.ok) { error(`LLM 驗證失敗: ${llm.error}`); return false; } if (llm.keyIndex) ok(`LLM provider=${llm.provider} 驗證通過(key ${llm.keyIndex}/${llm.total})`); else ok(`LLM provider=${llm.provider} 連線正常`); ok('前置驗證通過'); return true; }