fix(ai-review model validation): 驗證 model 並支援自動選模
This commit is contained in:
+1
-1
@@ -9,7 +9,7 @@ inputs:
|
||||
description: '操作 Gitea Commit API 的 Token'
|
||||
required: false
|
||||
model:
|
||||
description: '使用的 AI 模型'
|
||||
description: '使用的 AI 模型,僅允許英數字、點、底線與連字號'
|
||||
required: false
|
||||
runs:
|
||||
using: 'docker'
|
||||
|
||||
+18
-6
@@ -42,6 +42,16 @@ export const LLM_PROVIDER = 'cliproxyapi';
|
||||
|
||||
export const FINDINGS_PATH = '.gitea/ai-review/findings.json';
|
||||
export const EXCLUSIONS_PATH = '.gitea/ai-review/exclusions.json';
|
||||
const MODEL_NAME_RE = /^[A-Za-z0-9._-]+$/;
|
||||
|
||||
function normalizeModelName(raw) {
|
||||
const model = String(raw || '').trim();
|
||||
if (!model) return { model: null, modelError: null };
|
||||
if (!MODEL_NAME_RE.test(model)) {
|
||||
return { model: null, modelError: '無效的 model 參數,僅允許英數字、點、底線與連字號' };
|
||||
}
|
||||
return { model, modelError: null };
|
||||
}
|
||||
|
||||
let _insecureHttpsAgent = null;
|
||||
/**
|
||||
@@ -67,26 +77,28 @@ export const getOpenCodeHttpsAgent = getInsecureHttpsAgent;
|
||||
* 優先讀取 `INPUT_CLI_PROXY_API` / `CLI_PROXY_API` 作為 base URL(會 trim 並移除結尾斜線),
|
||||
* `INPUT_MODEL` / `CLI_PROXY_API_MODEL` / `MODEL` / `OPENCODE_MODEL`(依序 fallback,
|
||||
* 相容 action input、舊 OpenCode 設定與環境變數)作為可選模型名稱;若未提供,
|
||||
* 則交由 CLIProxyAPI 自動選擇模型,`INPUT_CLI_PROXY_API_KEY` / `CLI_PROXY_API_KEY`
|
||||
* 作為存取金鑰(會 trim)。
|
||||
* 則交由 CLIProxyAPI 自動選擇模型。若提供的名稱含非法字元,會被視為無效並於
|
||||
* `modelError` 回報,`INPUT_CLI_PROXY_API_KEY` / `CLI_PROXY_API_KEY` 作為存取金鑰(會 trim)。
|
||||
*
|
||||
* 若 base URL 無法解析出任何值,視為沒有可用的 proxy 設定:`provider`/`baseURL` 回傳 `null`、
|
||||
* `apiKeys` 回傳空陣列,但 `model`(若有解析到)仍會回傳,不會被清空。
|
||||
*
|
||||
* @returns {{ provider: ('cliproxyapi'|null), apiKeys: string[], baseURL: (string|null), model: (string|null), command: null }}
|
||||
* @returns {{ provider: ('cliproxyapi'|null), apiKeys: string[], baseURL: (string|null), model: (string|null), modelError: (string|null), command: null }}
|
||||
* 設定物件;`provider` 為 `null` 表示沒有可用的 proxy 設定。
|
||||
*/
|
||||
export function getLLMConfig() {
|
||||
const baseURL = String(process.env.INPUT_CLI_PROXY_API || process.env.CLI_PROXY_API || '').trim().replace(/\/$/, '');
|
||||
const model = process.env.INPUT_MODEL || process.env.CLI_PROXY_API_MODEL || process.env.MODEL || process.env.OPENCODE_MODEL || '';
|
||||
const rawModel = process.env.INPUT_MODEL || process.env.CLI_PROXY_API_MODEL || process.env.MODEL || process.env.OPENCODE_MODEL || '';
|
||||
const { model, modelError } = normalizeModelName(rawModel);
|
||||
const apiKey = String(process.env.INPUT_CLI_PROXY_API_KEY || process.env.CLI_PROXY_API_KEY || '').trim();
|
||||
if (!baseURL) return { provider: null, apiKeys: [], baseURL: null, model: model || null, command: null };
|
||||
if (!baseURL) return { provider: null, apiKeys: [], baseURL: null, model, modelError, command: null };
|
||||
|
||||
return {
|
||||
provider: LLM_PROVIDER,
|
||||
apiKeys: apiKey ? [apiKey] : [],
|
||||
baseURL,
|
||||
model: model || null,
|
||||
model,
|
||||
modelError,
|
||||
command: null,
|
||||
};
|
||||
}
|
||||
|
||||
+2
-1
@@ -209,8 +209,9 @@ async function runProxyAPI({ provider, baseURL, apiKeys, model }, prompt) {
|
||||
*/
|
||||
export async function chat(systemPrompt, userContent) {
|
||||
const cfg = getLLMConfig();
|
||||
const { provider, baseURL, model } = cfg;
|
||||
const { provider, baseURL, model, modelError } = cfg;
|
||||
if (!provider || !baseURL) throw new Error('未偵測到可用的 CLIProxyAPI 設定,請確認 CLI_PROXY_API');
|
||||
if (modelError) throw new Error(modelError);
|
||||
|
||||
line(`[LLM] provider=${provider} baseURL=${baseURL} model=${model || 'auto'}`);
|
||||
|
||||
|
||||
+2
-1
@@ -177,8 +177,9 @@ export async function fetchLLMModels({
|
||||
* @remarks 設定來源為 config.js 的 getLLMConfig()。
|
||||
*/
|
||||
export async function verifyLLM({ fetchLLMModelsFn = fetchLLMModels } = {}) {
|
||||
const { provider, command, model } = getLLMConfig();
|
||||
const { provider, command, model, modelError } = getLLMConfig();
|
||||
if (!provider) return { ok: false, error: '未偵測到可用的 CLIProxyAPI 設定,請確認 CLI_PROXY_API' };
|
||||
if (modelError) return { ok: false, provider, command, model, error: modelError };
|
||||
|
||||
if (provider === 'cliproxyapi') {
|
||||
const models = await fetchLLMModelsFn();
|
||||
|
||||
@@ -62,6 +62,16 @@ describe('getLLMConfig', () => {
|
||||
assert.equal(cfg.model, 'gpt-5.4-mini');
|
||||
});
|
||||
|
||||
it('rejects invalid model names', () => {
|
||||
process.env.CLI_PROXY_API = 'https://proxy.example';
|
||||
process.env.MODEL = 'gpt-5.5; rm -rf /';
|
||||
|
||||
const cfg = getLLMConfig();
|
||||
|
||||
assert.equal(cfg.model, null);
|
||||
assert.match(cfg.modelError, /無效的 model 參數/);
|
||||
});
|
||||
|
||||
it('returns null provider when CLI_PROXY_API is missing', () => {
|
||||
process.env.MODEL = 'gpt-5.5';
|
||||
const cfg = getLLMConfig();
|
||||
|
||||
@@ -88,6 +88,13 @@ describe('chat - CLIProxyAPI', async () => {
|
||||
await assert.rejects(() => chat('sys', 'user'), /401/);
|
||||
await assert.rejects(() => chat('sys', 'user'), /access token revoked/);
|
||||
});
|
||||
|
||||
it('throws when the configured model name is invalid', async () => {
|
||||
process.env.CLI_PROXY_API = 'https://proxy.example';
|
||||
process.env.MODEL = 'gpt-5.5; rm -rf /';
|
||||
|
||||
await assert.rejects(() => chat('sys', 'user'), /無效的 model 參數/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('chatJSON', async () => {
|
||||
|
||||
@@ -208,6 +208,20 @@ describe('verifyLLM', () => {
|
||||
assert.match(result.error, /不在 CLIProxyAPI 可用清單/);
|
||||
assert.match(result.error, /gpt-9-imaginary/);
|
||||
});
|
||||
|
||||
it('fails when the configured model name is invalid', async () => {
|
||||
clearLLMEnv();
|
||||
process.env.CLI_PROXY_API = 'https://proxy.example';
|
||||
process.env.MODEL = 'gpt-5.5; rm -rf /';
|
||||
|
||||
const result = await verifyLLM({
|
||||
fetchLLMModelsFn: async () => ({ ok: true, slugs: ['gpt-5.5'] }),
|
||||
});
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.provider, 'cliproxyapi');
|
||||
assert.match(result.error, /無效的 model 參數/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('runPreflight', () => {
|
||||
|
||||
Reference in New Issue
Block a user