40ebfe99a8
git push 走 askpass + HTTP 認證,與 Gitea REST API 是兩套機制,API token 有效不代表 push 能用(曾出現 askpass 無法執行、could not read Username 而 push 失敗)。新增 git.js verifyRemoteAccess() 以相同 askpass + remote URL 跑唯讀 git ls-remote,preflight 呼叫並在失敗時 exit 1,提前攔下設定問題。 新增 git.test.js 對 verifyRemoteAccess 的測試(成功、失敗不丟例外、token 不外洩、askpass 清理)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
139 lines
5.0 KiB
JavaScript
139 lines
5.0 KiB
JavaScript
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;
|
||
}
|