fix(api-config): route model fetch/test through ST backend proxy to bypass CORS

Replace direct browser fetch with /api/backends/chat-completions/status for
OpenAI-compatible providers; Google keeps direct fetch (supports CORS natively).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 23:58:20 +08:00
parent 153c0616d2
commit 5a6a8b205c

View File

@@ -8,6 +8,7 @@
import { apiProfileManager, PROFILE_TYPES, SLOTS } from '../utils/config/ApiProfileManager.js'; import { apiProfileManager, PROFILE_TYPES, SLOTS } from '../utils/config/ApiProfileManager.js';
import { apiKeyStore } from '../utils/config/api-key-store/ApiKeyStore.js'; import { apiKeyStore } from '../utils/config/api-key-store/ApiKeyStore.js';
import { getRequestHeaders } from '/script.js';
// ── 状态 ───────────────────────────────────────────────────────────────────── // ── 状态 ─────────────────────────────────────────────────────────────────────
@@ -375,12 +376,16 @@ async function _fetchModels($c) {
)) ))
.map(m => m.name.replace(/^models\//, '')); .map(m => m.name.replace(/^models\//, ''));
} else { } else {
// OpenAI 兼容接口 // OpenAI 兼容接口 — 通过 ST 后端代理,规避 CORS
const baseUrl = apiUrl.replace(/\/+$/, ''); const resp = await fetch('/api/backends/chat-completions/status', {
const headers = {}; method: 'POST',
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`; headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify({
const resp = await fetch(`${baseUrl}/models`, { headers }); reverse_proxy: apiUrl,
proxy_password: apiKey,
chat_completion_source: 'openai',
}),
});
if (!resp.ok) { if (!resp.ok) {
const status = resp.status; const status = resp.status;
if (status === 401 || status === 403) { if (status === 401 || status === 403) {
@@ -392,8 +397,10 @@ async function _fetchModels($c) {
} }
return; return;
} }
const data = await resp.json(); const rawData = await resp.json();
models = (data.data ?? []).map(m => m.id).filter(Boolean); // ST 返回原始数组或包含 data/models 字段的对象
const list = Array.isArray(rawData) ? rawData : (rawData.data ?? rawData.models ?? []);
models = list.map(m => m.id ?? m.name ?? m).filter(m => typeof m === 'string' && m);
} }
if (models.length === 0) { if (models.length === 0) {
@@ -418,8 +425,6 @@ async function _fetchModels($c) {
async function _testConnection($c) { async function _testConnection($c) {
const apiUrl = $c.find('#amily2_pf_url').val().trim(); const apiUrl = $c.find('#amily2_pf_url').val().trim();
const apiKey = $c.find('#amily2_pf_key').val().trim(); const apiKey = $c.find('#amily2_pf_key').val().trim();
const model = $c.find('#amily2_pf_model').val().trim();
const type = $c.find('#amily2_pf_type').val();
const provider = $c.find('#amily2_pf_provider').val(); const provider = $c.find('#amily2_pf_provider').val();
if (!apiUrl) { toastr.warning('请先填写 API 地址。'); return; } if (!apiUrl) { toastr.warning('请先填写 API 地址。'); return; }
@@ -454,51 +459,34 @@ async function _testConnection($c) {
return; return;
} }
// OpenAI 兼容接口 // OpenAI 兼容接口 — 通过 ST 后端代理,规避 CORS
const headers = { 'Content-Type': 'application/json' }; const modelsResp = await fetch('/api/backends/chat-completions/status', {
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`; method: 'POST',
const baseUrl = apiUrl.replace(/\/+$/, ''); headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify({
// 先尝试 GET /models通用、免费、无副作用 reverse_proxy: apiUrl,
const modelsResp = await fetch(`${baseUrl}/models`, { headers }); proxy_password: apiKey,
chat_completion_source: 'openai',
}),
});
if (modelsResp.ok) { if (modelsResp.ok) {
const data = await modelsResp.json(); const rawData = await modelsResp.json();
const count = (data.data ?? []).length; const list = Array.isArray(rawData) ? rawData : (rawData.data ?? rawData.models ?? []);
const count = list.length;
$result.text(`连接成功${count ? `${count} 个可用模型` : ''}`).css('color', 'var(--green)'); $result.text(`连接成功${count ? `${count} 个可用模型` : ''}`).css('color', 'var(--green)');
toastr.success('连接测试通过!'); toastr.success('连接测试通过!');
return; return;
} }
if (modelsResp.status === 401 || modelsResp.status === 403) { const status = modelsResp.status;
$result.text('认证失败,请检查 API Key').css('color', 'var(--warning-color)'); const errBody = await modelsResp.json().catch(() => ({}));
toastr.error('认证失败API Key 无效或无权限。'); const msg = errBody?.error?.message
return; || (status === 401 || status === 403 ? 'API Key 无效或无权限'
} : status === 404 ? '接口地址不存在'
: `HTTP ${status}`);
// /models 不支持时,发一个最小请求验证连通性
let endpoint, body;
if (type === 'embedding') {
endpoint = `${baseUrl}/embeddings`;
body = { model: model || 'text-embedding-ada-002', input: 'test' };
} else if (type === 'rerank') {
endpoint = `${baseUrl}/rerank`;
body = { model, query: 'test', documents: ['a'] };
} else {
endpoint = `${baseUrl}/chat/completions`;
body = { model: model || 'gpt-3.5-turbo', messages: [{ role: 'user', content: 'hi' }], max_tokens: 1 };
}
const fallbackResp = await fetch(endpoint, { method: 'POST', headers, body: JSON.stringify(body) });
if (fallbackResp.ok || fallbackResp.status === 200) {
$result.text('连接成功').css('color', 'var(--green)');
toastr.success('连接测试通过!');
} else {
const err = await fallbackResp.json().catch(() => ({}));
const msg = err?.error?.message || `HTTP ${fallbackResp.status}`;
$result.text(`失败:${msg}`).css('color', 'var(--warning-color)'); $result.text(`失败:${msg}`).css('color', 'var(--warning-color)');
toastr.error(`测试失败:${msg}`); toastr.error(`测试失败:${msg}`);
}
} catch (e) { } catch (e) {
$result.text(`无法连接:${e.message}`).css('color', 'var(--warning-color)'); $result.text(`无法连接:${e.message}`).css('color', 'var(--warning-color)');
toastr.error(`连接失败:${e.message}`); toastr.error(`连接失败:${e.message}`);