From dcd80750bafd7d5afc598be9e6f7afc3994c7a31 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 7 Aug 2026 08:48:50 +0000 Subject: [PATCH] =?UTF-8?q?fix(ai-review=20model=20validation):=20?= =?UTF-8?q?=E9=A9=97=E8=AD=89=20model=20=E4=B8=A6=E6=94=AF=E6=8F=B4?= =?UTF-8?q?=E8=87=AA=E5=8B=95=E9=81=B8=E6=A8=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- action.yml | 4 ++-- src/config.js | 24 ++++++++++++++++++------ src/llm.js | 3 ++- src/preflight.js | 3 ++- src/test/config.test.js | 10 ++++++++++ src/test/llm.test.js | 7 +++++++ src/test/preflight.test.js | 14 ++++++++++++++ 7 files changed, 55 insertions(+), 10 deletions(-) diff --git a/action.yml b/action.yml index f1badc1..1bba83c 100644 --- a/action.yml +++ b/action.yml @@ -9,7 +9,7 @@ inputs: description: '操作 Gitea Commit API 的 Token' required: false model: - description: '使用的 AI 模型' + description: '使用的 AI 模型,僅允許英數字、點、底線與連字號' required: false runs: using: 'docker' @@ -19,4 +19,4 @@ runs: GITEA_COMMENT_TOKEN: ${{ inputs.comment_token || inputs.token || secrets.TOKEN || gitea.token }} CLI_PROXY_API: ${{ vars.CLI_PROXY_API }} CLI_PROXY_API_KEY: ${{ secrets.CLI_PROXY_API_KEY }} - CLI_PROXY_API_MODEL: ${{ inputs.model || vars.CLI_PROXY_API_MODEL }} \ No newline at end of file + CLI_PROXY_API_MODEL: ${{ inputs.model || vars.CLI_PROXY_API_MODEL }} diff --git a/src/config.js b/src/config.js index bcfde41..c1f2526 100644 --- a/src/config.js +++ b/src/config.js @@ -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, }; } diff --git a/src/llm.js b/src/llm.js index 677669a..120b9c5 100644 --- a/src/llm.js +++ b/src/llm.js @@ -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'}`); diff --git a/src/preflight.js b/src/preflight.js index 3b2343c..068bdda 100644 --- a/src/preflight.js +++ b/src/preflight.js @@ -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(); diff --git a/src/test/config.test.js b/src/test/config.test.js index 949cf1c..5286a23 100644 --- a/src/test/config.test.js +++ b/src/test/config.test.js @@ -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(); diff --git a/src/test/llm.test.js b/src/test/llm.test.js index 3251240..5af38c1 100644 --- a/src/test/llm.test.js +++ b/src/test/llm.test.js @@ -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 () => { diff --git a/src/test/preflight.test.js b/src/test/preflight.test.js index 53102f0..fcdf63e 100644 --- a/src/test/preflight.test.js +++ b/src/test/preflight.test.js @@ -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', () => {