From 983efeb13f342de86cf463c31d2f720b68a5c9b5 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 23 Jun 2026 13:07:27 +0800 Subject: [PATCH] =?UTF-8?q?fix(ai-review=20=E4=BD=BF=E7=94=A8=E9=87=8F):?= =?UTF-8?q?=20=E4=BF=AE=E6=AD=A3=E9=A1=8D=E5=BA=A6=E6=9F=A5=E8=A9=A2=20hos?= =?UTF-8?q?tname=20=E7=B2=BE=E7=A2=BA=E6=AF=94=E5=B0=8D=E8=88=87=20total?= =?UTF-8?q?=20token=20=E7=82=BA=200=20=E7=9A=84=E9=82=8A=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/usage.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/app/usage.js b/app/usage.js index 3907acb..8829358 100644 --- a/app/usage.js +++ b/app/usage.js @@ -23,7 +23,7 @@ export function extractUsage(data) { if (u && typeof u === 'object') { const prompt = num(u.prompt_tokens ?? u.input_tokens); const completion = num(u.completion_tokens ?? u.output_tokens); - const total = num(u.total_tokens) || prompt + completion; + const total = u.total_tokens != null ? num(u.total_tokens) : prompt + completion; if (prompt || completion || total) return { promptTokens: prompt, completionTokens: completion, totalTokens: total }; } @@ -32,7 +32,7 @@ export function extractUsage(data) { if (g && typeof g === 'object') { const prompt = num(g.promptTokenCount); const completion = num(g.candidatesTokenCount); - const total = num(g.totalTokenCount) || prompt + completion; + const total = g.totalTokenCount != null ? num(g.totalTokenCount) : prompt + completion; if (prompt || completion || total) return { promptTokens: prompt, completionTokens: completion, totalTokens: total }; } @@ -48,7 +48,7 @@ export function extractUsage(data) { if (t && typeof t === 'object') { const prompt = num(t.input ?? t.prompt); const completion = num(t.output ?? t.completion); - const total = num(t.total) || prompt + completion; + const total = t.total != null ? num(t.total) : prompt + completion; if (prompt || completion || total) return { promptTokens: prompt, completionTokens: completion, totalTokens: total }; } @@ -124,6 +124,19 @@ export function resetRateLimit() { const stripSlash = (s) => String(s || '').replace(/\/$/, ''); +/** + * 以實際 hostname 精確比對是否為 OpenRouter,避免被偽造的 baseURL + * (如 `openrouter.ai.evil.com` 或 `evil.com/openrouter.ai`)矇騙而把 API key 送往惡意主機。 + */ +function isOpenRouterBaseURL(baseURL) { + try { + const host = new URL(baseURL).hostname.toLowerCase(); + return host === 'openrouter.ai' || host.endsWith('.openrouter.ai'); + } catch { + return false; + } +} + /** * OpenRouter:以 API key 呼叫 GET /auth/key 取得額度(可靠)。 * 回傳金額單位為 USD credits。 @@ -147,7 +160,7 @@ async function fetchOpenRouterQuota({ apiKey, baseURL }, get) { */ const QUOTA_STRATEGIES = { openai: async (cfg, get) => { - if (/openrouter\.ai/i.test(cfg.baseURL || '')) return fetchOpenRouterQuota(cfg, get); + if (isOpenRouterBaseURL(cfg.baseURL)) return fetchOpenRouterQuota(cfg, get); return { available: false, reason: 'OpenAI 帳號額度需 dashboard session 權限,API key 無法取得' }; }, claude: async () => ({ available: false, reason: 'Anthropic 額度需 Admin API 權限,一般 API key 無法取得' }),