mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 03:25:51 +00:00
feat(api-config): add model fetch, connection test, and Google AI Studio support
- Add "获取模型" button: fetches /v1/models and populates datalist autocomplete - Add "测试连接" button: tries GET /models first (free), falls back to minimal type-specific request if endpoint unsupported; shows inline result - Google AI Studio: auto-fill fixed endpoint URL, hide URL field, fetch models via native v1beta/models?key= API, test via same endpoint - Collapse Chat/Embedding/Rerank advanced params into <details> sections - Simplify provider options: remove ambiguous "实验性全兼容" option - Clear test result and model datalist cache on each modal open Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,6 @@
|
||||
<fieldset class="settings-group">
|
||||
<legend><i class="fas fa-server"></i> 连接配置列表</legend>
|
||||
|
||||
<!-- 类型筛选标签 -->
|
||||
<div style="display:flex; gap:6px; margin-bottom:10px; flex-wrap:wrap;">
|
||||
<button class="menu_button small_button amily2_profile_type_filter active" data-type="all">全部</button>
|
||||
<button class="menu_button small_button amily2_profile_type_filter" data-type="chat">
|
||||
@@ -58,9 +57,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Profile 卡片列表 -->
|
||||
<div id="amily2_profile_list" style="display:flex; flex-direction:column; gap:8px;">
|
||||
<!-- 由 JS 动态渲染 -->
|
||||
<div class="amily2_profile_empty" style="color:var(--SmartThemeQuoteColor); text-align:center; padding:20px;">
|
||||
暂无连接配置,点击「新建配置」添加。
|
||||
</div>
|
||||
@@ -74,13 +71,12 @@
|
||||
为每个系统功能指定使用的连接配置。选单只会显示类型匹配的配置。
|
||||
</small>
|
||||
<div id="amily2_slot_assignments" style="display:flex; flex-direction:column; gap:6px;">
|
||||
<!-- 由 JS 动态渲染 -->
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- 新建/编辑 Profile 弹窗 -->
|
||||
<div id="amily2_profile_modal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:9999; align-items:center; justify-content:center;">
|
||||
<div style="background:var(--SmartThemeBlurTintColor); border:1px solid var(--SmartThemeBorderColor); border-radius:8px; padding:20px; width:min(480px,92vw); max-height:85vh; overflow-y:auto;">
|
||||
<div style="background:var(--SmartThemeBlurTintColor); border:1px solid var(--SmartThemeBorderColor); border-radius:8px; padding:20px; width:min(500px,94vw); max-height:88vh; overflow-y:auto;">
|
||||
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:14px;">
|
||||
<strong id="amily2_profile_modal_title"><i class="fas fa-key"></i> 新建连接配置</strong>
|
||||
@@ -103,66 +99,113 @@
|
||||
<input id="amily2_pf_name" type="text" class="text_pole" placeholder="例如:我的 DeepSeek" />
|
||||
</div>
|
||||
<div class="amily2_settings_block">
|
||||
<label for="amily2_pf_provider">API 提供商</label>
|
||||
<label for="amily2_pf_provider">接口类型</label>
|
||||
<select id="amily2_pf_provider" class="text_pole">
|
||||
<option value="openai">OpenAI 兼容</option>
|
||||
<option value="openai_test">实验性全兼容</option>
|
||||
<option value="google">Google 直连</option>
|
||||
<option value="sillytavern_backend">SillyTavern 后端</option>
|
||||
<option value="sillytavern_preset">SillyTavern 预设</option>
|
||||
<option value="openai">OpenAI / 兼容接口(推荐)</option>
|
||||
<option value="google">Google Gemini 直连</option>
|
||||
<option value="sillytavern_backend">SillyTavern 后端代理</option>
|
||||
<option value="sillytavern_preset">SillyTavern 预设转发</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="amily2_settings_block">
|
||||
<label for="amily2_pf_url">API URL</label>
|
||||
<div class="amily2_settings_block" id="amily2_pf_url_row">
|
||||
<label for="amily2_pf_url">API 地址</label>
|
||||
<input id="amily2_pf_url" type="text" class="text_pole" placeholder="https://api.example.com/v1" />
|
||||
</div>
|
||||
<!-- Google 专属提示(选 Google 时显示) -->
|
||||
<div id="amily2_pf_google_note" style="display:none; margin-bottom:8px;">
|
||||
<small class="notes" style="display:block; padding:6px 10px; background:var(--black10a); border-radius:4px; border-left:3px solid #4285f4;">
|
||||
<i class="fas fa-info-circle" style="color:#4285f4;"></i>
|
||||
Google AI Studio — 接口地址已自动配置,只需填写 API Key 即可。
|
||||
在 <a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener" style="color:#4285f4;">aistudio.google.com</a> 生成密钥。
|
||||
</small>
|
||||
</div>
|
||||
<div class="amily2_settings_block">
|
||||
<label for="amily2_pf_key">API Key <span style="color:var(--SmartThemeQuoteColor); font-size:0.85em;">(加密存储)</span></label>
|
||||
<input id="amily2_pf_key" type="password" class="text_pole" placeholder="sk-..." autocomplete="off" />
|
||||
<small class="notes">留空则不修改现有 Key。</small>
|
||||
</div>
|
||||
|
||||
<!-- 模型选择(带获取按钮) -->
|
||||
<div class="amily2_settings_block">
|
||||
<label for="amily2_pf_model">模型 ID</label>
|
||||
<input id="amily2_pf_model" type="text" class="text_pole" placeholder="例如:deepseek-r1-250528" />
|
||||
<label for="amily2_pf_model">模型</label>
|
||||
<div style="display:flex; gap:6px; align-items:stretch;">
|
||||
<input id="amily2_pf_model" type="text" class="text_pole"
|
||||
list="amily2_pf_model_list"
|
||||
placeholder="手动填写或点击「获取」"
|
||||
style="flex:1;" />
|
||||
<datalist id="amily2_pf_model_list"></datalist>
|
||||
<button id="amily2_pf_fetch_models" class="menu_button small_button interactable" type="button" title="从 API 获取可用模型列表(需先填写地址和 Key)">
|
||||
<i class="fas fa-list"></i> 获取
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat 专有参数 -->
|
||||
<!-- 测试连接 -->
|
||||
<div style="display:flex; align-items:center; gap:10px; margin-bottom:10px;">
|
||||
<button id="amily2_pf_test_conn" class="menu_button small_button interactable" type="button">
|
||||
<i class="fas fa-plug"></i> 测试连接
|
||||
</button>
|
||||
<span id="amily2_pf_test_result" style="font-size:0.85em;"></span>
|
||||
</div>
|
||||
|
||||
<!-- Chat 高级参数(折叠) -->
|
||||
<div id="amily2_pf_chat_params">
|
||||
<div class="amily2_settings_block">
|
||||
<label for="amily2_pf_max_tokens">最大 Token 数</label>
|
||||
<input id="amily2_pf_max_tokens" type="number" class="text_pole" min="100" max="200000" value="65500" />
|
||||
</div>
|
||||
<div class="amily2_settings_block">
|
||||
<label for="amily2_pf_temperature">温度(Temperature)</label>
|
||||
<input id="amily2_pf_temperature" type="number" class="text_pole" min="0" max="2" step="0.1" value="1.0" />
|
||||
</div>
|
||||
<details class="amily2_advanced_section" style="margin-top:4px;">
|
||||
<summary style="cursor:pointer; font-size:0.88em; color:var(--SmartThemeQuoteColor); user-select:none; padding:4px 0;">
|
||||
<i class="fas fa-sliders-h"></i> 高级参数
|
||||
</summary>
|
||||
<div style="padding-top:8px;">
|
||||
<div class="amily2_settings_block">
|
||||
<label for="amily2_pf_max_tokens">最大 Token 数</label>
|
||||
<input id="amily2_pf_max_tokens" type="number" class="text_pole" min="100" max="200000" value="65500" />
|
||||
</div>
|
||||
<div class="amily2_settings_block">
|
||||
<label for="amily2_pf_temperature">温度(Temperature)</label>
|
||||
<input id="amily2_pf_temperature" type="number" class="text_pole" min="0" max="2" step="0.1" value="1.0" />
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- Embedding 专有参数 -->
|
||||
<!-- Embedding 高级参数(折叠) -->
|
||||
<div id="amily2_pf_embedding_params" style="display:none;">
|
||||
<div class="amily2_settings_block">
|
||||
<label for="amily2_pf_dimensions">输出维度 <span style="color:var(--SmartThemeQuoteColor); font-size:0.85em;">(留空=模型默认)</span></label>
|
||||
<input id="amily2_pf_dimensions" type="number" class="text_pole" min="1" placeholder="例如:1536" />
|
||||
</div>
|
||||
<div class="amily2_settings_block">
|
||||
<label for="amily2_pf_encoding_format">编码格式</label>
|
||||
<select id="amily2_pf_encoding_format" class="text_pole">
|
||||
<option value="float">float(默认)</option>
|
||||
<option value="base64">base64</option>
|
||||
</select>
|
||||
</div>
|
||||
<details class="amily2_advanced_section" style="margin-top:4px;">
|
||||
<summary style="cursor:pointer; font-size:0.88em; color:var(--SmartThemeQuoteColor); user-select:none; padding:4px 0;">
|
||||
<i class="fas fa-sliders-h"></i> 高级参数
|
||||
</summary>
|
||||
<div style="padding-top:8px;">
|
||||
<div class="amily2_settings_block">
|
||||
<label for="amily2_pf_dimensions">输出维度 <span style="color:var(--SmartThemeQuoteColor); font-size:0.85em;">(留空 = 模型默认)</span></label>
|
||||
<input id="amily2_pf_dimensions" type="number" class="text_pole" min="1" placeholder="例如:1536" />
|
||||
</div>
|
||||
<div class="amily2_settings_block">
|
||||
<label for="amily2_pf_encoding_format">编码格式</label>
|
||||
<select id="amily2_pf_encoding_format" class="text_pole">
|
||||
<option value="float">float(默认)</option>
|
||||
<option value="base64">base64</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- Rerank 专有参数 -->
|
||||
<!-- Rerank 参数 -->
|
||||
<div id="amily2_pf_rerank_params" style="display:none;">
|
||||
<div class="amily2_settings_block">
|
||||
<label for="amily2_pf_top_n">返回结果数量(Top N)</label>
|
||||
<input id="amily2_pf_top_n" type="number" class="text_pole" min="1" max="100" value="5" />
|
||||
</div>
|
||||
<div class="amily2_settings_block" style="flex-direction:row; align-items:center; gap:8px;">
|
||||
<input id="amily2_pf_return_documents" type="checkbox" />
|
||||
<label for="amily2_pf_return_documents">返回原始文档内容</label>
|
||||
</div>
|
||||
<details class="amily2_advanced_section" style="margin-top:4px;">
|
||||
<summary style="cursor:pointer; font-size:0.88em; color:var(--SmartThemeQuoteColor); user-select:none; padding:4px 0;">
|
||||
<i class="fas fa-sliders-h"></i> 高级参数
|
||||
</summary>
|
||||
<div style="padding-top:8px;">
|
||||
<div class="amily2_settings_block" style="flex-direction:row; align-items:center; gap:8px;">
|
||||
<input id="amily2_pf_return_documents" type="checkbox" />
|
||||
<label for="amily2_pf_return_documents">返回原始文档内容</label>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
|
||||
@@ -38,6 +38,17 @@ export function bindApiConfigPanel(container) {
|
||||
_switchParamSections($c, $(this).val());
|
||||
});
|
||||
|
||||
// 弹窗:接口类型切换(Google 自动填 URL)
|
||||
$c.find('#amily2_pf_provider').on('change', function () {
|
||||
_handleProviderChange($c, $(this).val());
|
||||
});
|
||||
|
||||
// 弹窗:获取模型列表
|
||||
$c.find('#amily2_pf_fetch_models').on('click', () => _fetchModels($c));
|
||||
|
||||
// 弹窗:测试连接
|
||||
$c.find('#amily2_pf_test_conn').on('click', () => _testConnection($c));
|
||||
|
||||
// 弹窗:关闭
|
||||
$c.find('#amily2_profile_modal_close, #amily2_profile_modal_cancel').on('click', () => closeModal($c));
|
||||
$c.find('#amily2_profile_modal').on('click', function (e) {
|
||||
@@ -246,12 +257,14 @@ async function openModal($c, id) {
|
||||
$c.find('#amily2_pf_return_documents').prop('checked', p.returnDocuments);
|
||||
}
|
||||
_switchParamSections($c, p.type);
|
||||
_handleProviderChange($c, p.provider);
|
||||
} else {
|
||||
// 新建模式
|
||||
$c.find('#amily2_profile_modal_title').html('<i class="fas fa-plus"></i> 新建连接配置');
|
||||
$c.find('#amily2_pf_type').val('chat').prop('disabled', false);
|
||||
$c.find('#amily2_pf_name, #amily2_pf_url, #amily2_pf_key, #amily2_pf_model').val('');
|
||||
$c.find('#amily2_pf_provider').val('openai');
|
||||
_handleProviderChange($c, 'openai');
|
||||
$c.find('#amily2_pf_max_tokens').val(65500);
|
||||
$c.find('#amily2_pf_temperature').val(1.0);
|
||||
$c.find('#amily2_pf_dimensions').val('');
|
||||
@@ -261,6 +274,10 @@ async function openModal($c, id) {
|
||||
_switchParamSections($c, 'chat');
|
||||
}
|
||||
|
||||
// 清空上次测试结果和模型列表缓存
|
||||
$c.find('#amily2_pf_test_result').text('');
|
||||
$c.find('#amily2_pf_model_list').empty();
|
||||
|
||||
$modal.css('display', 'flex');
|
||||
}
|
||||
|
||||
@@ -322,6 +339,188 @@ async function saveProfile($c) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 获取模型 / 测试连接 ───────────────────────────────────────────────────────
|
||||
|
||||
async function _fetchModels($c) {
|
||||
const apiUrl = $c.find('#amily2_pf_url').val().trim();
|
||||
const apiKey = $c.find('#amily2_pf_key').val().trim();
|
||||
const provider = $c.find('#amily2_pf_provider').val();
|
||||
|
||||
if (!apiUrl) { toastr.warning('请先填写 API 地址。'); return; }
|
||||
|
||||
const $btn = $c.find('#amily2_pf_fetch_models').prop('disabled', true);
|
||||
$btn.html('<i class="fas fa-spinner fa-spin"></i> 获取中...');
|
||||
|
||||
try {
|
||||
let models;
|
||||
|
||||
if (provider === 'google') {
|
||||
// Google 用原生 API,以 ?key= 传参,返回 models[] 而非 data[]
|
||||
if (!apiKey) { toastr.warning('请先填写 Google API Key。'); return; }
|
||||
const resp = await fetch(
|
||||
`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}`
|
||||
);
|
||||
if (!resp.ok) {
|
||||
const status = resp.status;
|
||||
toastr.error(status === 400 ? '获取失败:API Key 格式错误。'
|
||||
: status === 403 ? '获取失败:API Key 无效或无权限。'
|
||||
: `获取失败:HTTP ${status}`);
|
||||
return;
|
||||
}
|
||||
const data = await resp.json();
|
||||
// 只保留支持文本生成的模型
|
||||
models = (data.models ?? [])
|
||||
.filter(m => m.supportedGenerationMethods?.some(
|
||||
method => ['generateContent', 'embedContent'].includes(method)
|
||||
))
|
||||
.map(m => m.name.replace(/^models\//, ''));
|
||||
} else {
|
||||
// OpenAI 兼容接口
|
||||
const baseUrl = apiUrl.replace(/\/+$/, '');
|
||||
const headers = {};
|
||||
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
||||
|
||||
const resp = await fetch(`${baseUrl}/models`, { headers });
|
||||
if (!resp.ok) {
|
||||
const status = resp.status;
|
||||
if (status === 401 || status === 403) {
|
||||
toastr.error('获取失败:API Key 无效或无权限。');
|
||||
} else if (status === 404) {
|
||||
toastr.warning('该接口不支持模型列表查询,请手动填写模型 ID。');
|
||||
} else {
|
||||
toastr.error(`获取失败:HTTP ${status}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const data = await resp.json();
|
||||
models = (data.data ?? []).map(m => m.id).filter(Boolean);
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
toastr.warning('未获取到模型列表,请手动填写。');
|
||||
return;
|
||||
}
|
||||
|
||||
const $dl = $c.find('#amily2_pf_model_list');
|
||||
$dl.html(models.map(m => `<option value="${_escapeHtml(m)}">`).join(''));
|
||||
|
||||
const $modelInput = $c.find('#amily2_pf_model');
|
||||
if (!$modelInput.val()) $modelInput.val(models[0]);
|
||||
|
||||
toastr.success(`已获取 ${models.length} 个可用模型。`);
|
||||
} catch (e) {
|
||||
toastr.error(`获取失败:${e.message}`);
|
||||
} finally {
|
||||
$btn.prop('disabled', false).html('<i class="fas fa-list"></i> 获取');
|
||||
}
|
||||
}
|
||||
|
||||
async function _testConnection($c) {
|
||||
const apiUrl = $c.find('#amily2_pf_url').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();
|
||||
|
||||
if (!apiUrl) { toastr.warning('请先填写 API 地址。'); return; }
|
||||
|
||||
const $btn = $c.find('#amily2_pf_test_conn').prop('disabled', true);
|
||||
const $result = $c.find('#amily2_pf_test_result').text('测试中…').css('color', 'var(--SmartThemeQuoteColor)');
|
||||
$btn.html('<i class="fas fa-spinner fa-spin"></i> 测试中...');
|
||||
|
||||
try {
|
||||
if (provider === 'google') {
|
||||
// Google 用原生 models 端点测试
|
||||
if (!apiKey) {
|
||||
$result.text('请填写 API Key').css('color', 'var(--warning-color)');
|
||||
return;
|
||||
}
|
||||
const resp = await fetch(
|
||||
`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}`
|
||||
);
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
const count = (data.models ?? []).length;
|
||||
$result.text(`连接成功${count ? `,${count} 个可用模型` : ''}`).css('color', 'var(--green)');
|
||||
toastr.success('Google AI Studio 连接测试通过!');
|
||||
} else {
|
||||
const status = resp.status;
|
||||
const msg = status === 400 ? 'API Key 格式错误'
|
||||
: status === 403 ? 'API Key 无效或无权限'
|
||||
: `HTTP ${status}`;
|
||||
$result.text(`失败:${msg}`).css('color', 'var(--warning-color)');
|
||||
toastr.error(`测试失败:${msg}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// OpenAI 兼容接口
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
||||
const baseUrl = apiUrl.replace(/\/+$/, '');
|
||||
|
||||
// 先尝试 GET /models(通用、免费、无副作用)
|
||||
const modelsResp = await fetch(`${baseUrl}/models`, { headers });
|
||||
|
||||
if (modelsResp.ok) {
|
||||
const data = await modelsResp.json();
|
||||
const count = (data.data ?? []).length;
|
||||
$result.text(`连接成功${count ? `,${count} 个可用模型` : ''}`).css('color', 'var(--green)');
|
||||
toastr.success('连接测试通过!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (modelsResp.status === 401 || modelsResp.status === 403) {
|
||||
$result.text('认证失败,请检查 API Key').css('color', 'var(--warning-color)');
|
||||
toastr.error('认证失败:API Key 无效或无权限。');
|
||||
return;
|
||||
}
|
||||
|
||||
// /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)');
|
||||
toastr.error(`测试失败:${msg}`);
|
||||
}
|
||||
} catch (e) {
|
||||
$result.text(`无法连接:${e.message}`).css('color', 'var(--warning-color)');
|
||||
toastr.error(`连接失败:${e.message}`);
|
||||
} finally {
|
||||
$btn.prop('disabled', false).html('<i class="fas fa-plug"></i> 测试连接');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Provider 切换 ─────────────────────────────────────────────────────────────
|
||||
|
||||
const GOOGLE_API_BASE = 'https://generativelanguage.googleapis.com/v1beta/openai';
|
||||
|
||||
function _handleProviderChange($c, provider) {
|
||||
const isGoogle = provider === 'google';
|
||||
$c.find('#amily2_pf_url_row').toggle(!isGoogle);
|
||||
$c.find('#amily2_pf_google_note').toggle(isGoogle);
|
||||
|
||||
if (isGoogle) {
|
||||
$c.find('#amily2_pf_url').val(GOOGLE_API_BASE);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 内部工具 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function _switchParamSections($c, type) {
|
||||
|
||||
Reference in New Issue
Block a user