feat(preflight): Step2 前置驗證加入 codex 模型清單檢查
This commit is contained in:
+78
-6
@@ -1,4 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
GITEA_TOKEN,
|
||||
GITEA_COMMENT_TOKEN,
|
||||
@@ -11,6 +14,9 @@ import {
|
||||
import { verifyRemoteAccess } from './git.js';
|
||||
import { step, line, ok, error, result } from './log.js';
|
||||
|
||||
// codex 內部用來取得帳號可用模型清單的端點;auth 失效時會回 HTTP 401。
|
||||
const CODEX_MODELS_ENDPOINT = 'https://chatgpt.com/backend-api/codex/models';
|
||||
|
||||
const httpsAgent = getInsecureHttpsAgent();
|
||||
/**
|
||||
* 組出 Gitea REST API v1 的完整網址。
|
||||
@@ -95,22 +101,87 @@ export async function verifyCommentToken(token = GITEA_COMMENT_TOKEN) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 讀取本機 codex 認證檔,向模型清單端點確認帳號目前可用的模型 slug。
|
||||
*
|
||||
* 用途:preflight 期即時分辨「auth 失效(HTTP 401)」與「模型無權限(不在清單)」,
|
||||
* 不必等到 Step5 每個角色送 prompt 才神秘失敗。只讀清單、不送 prompt,不消耗生成額度。
|
||||
* 所有錯誤都被攔截並轉為回傳值,不會 throw。
|
||||
*
|
||||
* @param {object} [deps] - 可注入相依,供測試避免真的讀檔/打網路。
|
||||
* @param {typeof fetch} [deps.fetchImpl=fetch] - HTTP 取得函式。
|
||||
* @param {string} [deps.authPath=~/.codex/auth.json] - codex 認證檔路徑。
|
||||
* @param {string} [deps.clientVersion] - 帶給端點的 client_version 查詢參數。
|
||||
* @returns {Promise<{ok: true, slugs: string[]}|{ok: false, error: string}>}
|
||||
* 成功回傳可用模型 slug 陣列;失敗回傳格式化錯誤訊息。
|
||||
*/
|
||||
export async function fetchCodexModels({
|
||||
fetchImpl = fetch,
|
||||
authPath = join(os.homedir(), '.codex', 'auth.json'),
|
||||
clientVersion = '0.142.5',
|
||||
} = {}) {
|
||||
let auth;
|
||||
try {
|
||||
auth = JSON.parse(fs.readFileSync(authPath, 'utf8'));
|
||||
} catch (e) {
|
||||
return { ok: false, error: `無法讀取 codex 認證檔(${authPath}): ${e.message}` };
|
||||
}
|
||||
const tokens = auth.tokens || {};
|
||||
if (!tokens.access_token) return { ok: false, error: 'codex 認證檔缺少 tokens.access_token' };
|
||||
|
||||
const headers = { Authorization: `Bearer ${tokens.access_token}` };
|
||||
if (tokens.account_id) headers['chatgpt-account-id'] = tokens.account_id;
|
||||
|
||||
let resp;
|
||||
try {
|
||||
resp = await fetchImpl(`${CODEX_MODELS_ENDPOINT}?client_version=${clientVersion}`, { headers });
|
||||
} catch (e) {
|
||||
return { ok: false, error: `codex 模型清單查詢連線錯誤: ${e.message}` };
|
||||
}
|
||||
if (resp.status === 401) {
|
||||
return { ok: false, error: 'codex 認證失效(HTTP 401)——token 已被撤銷或過期,請重新登入 codex 並更新 LLM_OAUTH secret' };
|
||||
}
|
||||
if (!resp.ok) {
|
||||
return { ok: false, error: `codex 模型清單查詢失敗(HTTP ${resp.status})` };
|
||||
}
|
||||
let data;
|
||||
try {
|
||||
data = await resp.json();
|
||||
} catch (e) {
|
||||
return { ok: false, error: `codex 模型清單回應解析失敗: ${e.message}` };
|
||||
}
|
||||
const slugs = Array.isArray(data.models) ? data.models.map(m => m.slug).filter(Boolean) : [];
|
||||
return { ok: true, slugs };
|
||||
}
|
||||
|
||||
/**
|
||||
* 驗證 LLM(AI 助理 CLI)設定可用。
|
||||
*
|
||||
* 確認目前環境可偵測到支援的 CLI,且已解析出 model。實際模型可用性由 CLI
|
||||
* 在正式呼叫時回報;preflight 不主動送 prompt,避免額外消耗額度。
|
||||
* 確認目前環境可偵測到支援的 CLI 且已解析出 model;provider 為 codex 時,
|
||||
* 額外向模型清單端點確認 auth 有效且設定的 model 在可用清單內(不送 prompt)。
|
||||
* @param {object} [deps] - 可注入相依,供測試。
|
||||
* @param {Function} [deps.fetchCodexModelsFn=fetchCodexModels] - codex 模型清單取得函式。
|
||||
* @returns {Promise<
|
||||
* {ok: true, provider: string, command: string, model: string} |
|
||||
* {ok: false, provider?: string, error: string}
|
||||
* {ok: true, provider: string, command: string, model: string, models?: string[]} |
|
||||
* {ok: false, provider?: string, command?: string, model?: string, error: string}
|
||||
* >}
|
||||
* 通過時含 provider、command 與 model;未設定 provider 的失敗分支不含 provider 欄位。
|
||||
* 通過時含 provider、command、model(codex 另含 models 清單);未設定 provider 的失敗分支不含 provider。
|
||||
* @remarks 設定來源為 config.js 的 getLLMConfig()。
|
||||
*/
|
||||
export async function verifyLLM() {
|
||||
export async function verifyLLM({ fetchCodexModelsFn = fetchCodexModels } = {}) {
|
||||
const { provider, command, model } = getLLMConfig();
|
||||
if (!provider || !command) return { ok: false, error: '未偵測到可用 AI 助理 CLI,請安裝 codex、claude、antigravity 或 opencode' };
|
||||
if (!model) return { ok: false, provider, error: '未設定 MODEL' };
|
||||
|
||||
if (provider === 'codex') {
|
||||
const models = await fetchCodexModelsFn();
|
||||
if (!models.ok) return { ok: false, provider, command, model, error: models.error };
|
||||
if (!models.slugs.includes(model)) {
|
||||
return { ok: false, provider, command, model, error: `模型 ${model} 不在 codex 可用清單: [${models.slugs.join(', ')}]` };
|
||||
}
|
||||
return { ok: true, provider, command, model, models: models.slugs };
|
||||
}
|
||||
|
||||
return { ok: true, provider, command, model };
|
||||
}
|
||||
|
||||
@@ -174,6 +245,7 @@ export async function runPreflight(workspace = process.env.GITHUB_WORKSPACE || '
|
||||
return false;
|
||||
}
|
||||
ok(`LLM CLI 可用(command=${llm.command}, provider=${llm.provider}, model=${llm.model})`);
|
||||
if (llm.models) line(`模型已確認在可用清單內(共 ${llm.models.length} 個可用模型)`);
|
||||
|
||||
result(true, '前置驗證通過');
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user