mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 04:35:51 +00:00
ci: auto build & obfuscate [2026-04-06 00:50:28] (Jenkins #7)
This commit is contained in:
631
ui/api-config-bindings.js
Normal file
631
ui/api-config-bindings.js
Normal file
@@ -0,0 +1,631 @@
|
||||
/**
|
||||
* api-config-bindings.js — API 连接配置面板 UI 事件绑定
|
||||
*
|
||||
* 依赖:
|
||||
* ApiProfileManager(数据层)
|
||||
* ApiKeyStore(密钥存储)
|
||||
*/
|
||||
|
||||
import { apiProfileManager, PROFILE_TYPES, SLOTS } from '../utils/config/ApiProfileManager.js';
|
||||
import { apiKeyStore } from '../utils/config/api-key-store/ApiKeyStore.js';
|
||||
import { getRequestHeaders, saveSettingsDebounced } from '/script.js';
|
||||
import { extension_settings } from '/scripts/extensions.js';
|
||||
import { extensionName } from '../utils/settings.js';
|
||||
import { testApiConnection } from '../core/api.js';
|
||||
import { testJqyhApiConnection } from '../core/api/JqyhApi.js';
|
||||
import { testConcurrentApiConnection } from '../core/api/ConcurrentApi.js';
|
||||
import { testNgmsApiConnection } from '../core/api/Ngms_api.js';
|
||||
import { testNccsApiConnection } from '../core/api/NccsApi.js';
|
||||
|
||||
// 槽位 → 真实测试函数映射(发送聊天请求验证连接)
|
||||
// plotOpt 槽位同时服务剧情优化和 JQYH(互斥),根据启用状态选择测试函数
|
||||
const SLOT_TEST_FNS = {
|
||||
main: testApiConnection,
|
||||
plotOpt: () => {
|
||||
const s = extension_settings[extensionName] || {};
|
||||
return s.jqyhEnabled ? testJqyhApiConnection() : testApiConnection();
|
||||
},
|
||||
plotOptConc: testConcurrentApiConnection,
|
||||
ngms: testNgmsApiConnection,
|
||||
nccs: testNccsApiConnection,
|
||||
};
|
||||
|
||||
// 槽位 → 功能总开关映射
|
||||
// key : extension_settings[extensionName] 中的设置键
|
||||
// checkbox : 原面板中对应 checkbox 的 DOM 选择器(用于双向同步)
|
||||
const SLOT_TOGGLES = {
|
||||
plotOptConc: { key: 'plotOpt_concurrentEnabled', checkbox: '#amily2_plotOpt_concurrentEnabled' },
|
||||
ngms: { key: 'ngmsEnabled', checkbox: '#amily2_ngms_enabled' },
|
||||
nccs: { key: 'nccsEnabled', checkbox: '#nccs-api-enabled' },
|
||||
cwb: { key: 'cwb_master_enabled', checkbox: '#cwb_master_enabled-checkbox' },
|
||||
};
|
||||
|
||||
// ── 状态 ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
let _editingId = null; // 当前编辑的 Profile ID(null = 新建)
|
||||
let _currentFilter = 'all'; // 当前类型筛选
|
||||
|
||||
// ── 入口:绑定整个面板 ────────────────────────────────────────────────────────
|
||||
|
||||
export function bindApiConfigPanel(container) {
|
||||
const $c = $(container);
|
||||
|
||||
// 存储模式
|
||||
_bindStorageMode($c);
|
||||
|
||||
// 类型筛选
|
||||
$c.on('click', '.amily2_profile_type_filter', function () {
|
||||
$c.find('.amily2_profile_type_filter').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
_currentFilter = $(this).data('type');
|
||||
renderProfileList($c);
|
||||
});
|
||||
|
||||
// 新建 Profile
|
||||
$c.find('#amily2_add_profile').on('click', () => openModal($c, null));
|
||||
|
||||
// 弹窗:类型切换时显示/隐藏专有参数
|
||||
$c.find('#amily2_pf_type').on('change', function () {
|
||||
_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) {
|
||||
if (e.target === this) closeModal($c);
|
||||
});
|
||||
|
||||
// 弹窗:保存
|
||||
$c.find('#amily2_profile_modal_save').on('click', () => saveProfile($c));
|
||||
|
||||
// 初始渲染
|
||||
renderProfileList($c);
|
||||
renderSlotAssignments($c);
|
||||
}
|
||||
|
||||
// ── 存储模式 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function _bindStorageMode($c) {
|
||||
const $select = $c.find('#amily2_keystore_mode');
|
||||
const $cloud = $c.find('#amily2_cloud_key_section');
|
||||
const $note = $c.find('#amily2_keystore_mode_note');
|
||||
|
||||
const MODE_NOTES = {
|
||||
local: '本地存储:API Key 仅存于本设备浏览器,绝不上传服务端。换设备需重新填写。',
|
||||
cloud: '加密云同步:API Key 经 RSA+AES 混合加密后随设置同步。私钥仅留在本设备,服务商只能看到密文。',
|
||||
};
|
||||
|
||||
// 初始状态
|
||||
const currentMode = apiKeyStore.getMode();
|
||||
$select.val(currentMode);
|
||||
$cloud.toggle(currentMode === 'cloud');
|
||||
$note.text(MODE_NOTES[currentMode]);
|
||||
if (currentMode === 'cloud') _refreshFingerprint($c);
|
||||
|
||||
// 切换模式
|
||||
$select.on('change', async function () {
|
||||
const newMode = $(this).val();
|
||||
const confirmed = newMode === 'cloud'
|
||||
? confirm('切换到加密云同步模式:\n将自动为本设备生成 RSA 密钥对,现有 Key 会重新加密存储。\n\n确认切换?')
|
||||
: confirm('切换回本地存储模式:\n已加密的 Key 将解密迁移至本地,云端密文会被清除。\n\n确认切换?');
|
||||
|
||||
if (!confirmed) {
|
||||
$select.val(apiKeyStore.getMode());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiKeyStore.setMode(newMode);
|
||||
$cloud.toggle(newMode === 'cloud');
|
||||
$note.text(MODE_NOTES[newMode]);
|
||||
if (newMode === 'cloud') _refreshFingerprint($c);
|
||||
toastr.success(`已切换为${newMode === 'cloud' ? '加密云同步' : '本地存储'}模式。`);
|
||||
} catch (e) {
|
||||
console.error('[ApiConfig] 模式切换失败:', e);
|
||||
toastr.error('模式切换失败,请查看控制台。');
|
||||
$select.val(apiKeyStore.getMode());
|
||||
}
|
||||
});
|
||||
|
||||
// 重新生成密钥对
|
||||
$c.find('#amily2_generate_keypair').on('click', async () => {
|
||||
if (!confirm('重新生成密钥对后,所有已加密的 API Key 将失效,需要逐一重新输入。\n\n确认重新生成?')) return;
|
||||
await apiKeyStore.generateKeyPair();
|
||||
_refreshFingerprint($c);
|
||||
toastr.warning('新密钥对已生成,请重新输入各 Profile 的 API Key。');
|
||||
});
|
||||
}
|
||||
|
||||
async function _refreshFingerprint($c) {
|
||||
const fp = await apiKeyStore.getPublicKeyInfo();
|
||||
$c.find('#amily2_keypair_fingerprint').text(fp);
|
||||
}
|
||||
|
||||
// ── Profile 列表渲染 ──────────────────────────────────────────────────────────
|
||||
|
||||
export function renderProfileList($c) {
|
||||
const $list = $c.find('#amily2_profile_list');
|
||||
const profiles = apiProfileManager.getProfiles(
|
||||
_currentFilter === 'all' ? undefined : _currentFilter
|
||||
);
|
||||
|
||||
if (profiles.length === 0) {
|
||||
$list.html('<div class="amily2_profile_empty" style="color:var(--SmartThemeQuoteColor);text-align:center;padding:20px;">暂无连接配置,点击「新建配置」添加。</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
const TYPE_BADGE_COLOR = {
|
||||
chat: 'var(--SmartThemeBodyColor)',
|
||||
embedding: '#7eb8f7',
|
||||
rerank: '#f7b07e',
|
||||
};
|
||||
|
||||
const html = profiles.map(p => {
|
||||
const typeInfo = PROFILE_TYPES[p.type];
|
||||
const badgeStyle = `background:${TYPE_BADGE_COLOR[p.type]}22; color:${TYPE_BADGE_COLOR[p.type]}; border:1px solid ${TYPE_BADGE_COLOR[p.type]}55; border-radius:4px; padding:1px 6px; font-size:0.78em;`;
|
||||
return `
|
||||
<div class="amily2_profile_card" data-id="${p.id}" style="
|
||||
display:flex; align-items:center; gap:10px;
|
||||
padding:8px 12px;
|
||||
background:var(--black10a);
|
||||
border:1px solid var(--SmartThemeBorderColor);
|
||||
border-radius:6px;">
|
||||
<i class="fas ${typeInfo.icon}" style="width:16px; color:var(--SmartThemeQuoteColor);"></i>
|
||||
<div style="flex:1; min-width:0;">
|
||||
<div style="font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${_escapeHtml(p.name)}</div>
|
||||
<div style="font-size:0.82em; color:var(--SmartThemeQuoteColor); margin-top:2px;">
|
||||
<span style="${badgeStyle}"><i class="fas ${typeInfo.icon}"></i> ${typeInfo.label}</span>
|
||||
<span style="margin-left:6px;">${_escapeHtml(p.model || '(未设置模型)')}</span>
|
||||
${p.apiUrl ? `<span style="margin-left:6px; opacity:0.7;">${_escapeHtml(_truncateUrl(p.apiUrl))}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:4px; flex-shrink:0;">
|
||||
<button class="menu_button small_button interactable amily2_edit_profile" data-id="${p.id}" title="编辑">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="menu_button small_button secondary interactable amily2_delete_profile" data-id="${p.id}" title="删除">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
$list.html(html);
|
||||
|
||||
// 编辑 / 删除事件
|
||||
$list.find('.amily2_edit_profile').on('click', function () {
|
||||
openModal($c, $(this).data('id'));
|
||||
});
|
||||
$list.find('.amily2_delete_profile').on('click', function () {
|
||||
const id = $(this).data('id');
|
||||
const name = apiProfileManager.getProfile(id)?.name || id;
|
||||
if (!confirm(`确认删除连接配置「${name}」?\n此操作不可撤销,存储的 API Key 将同时清除。`)) return;
|
||||
apiProfileManager.deleteProfile(id);
|
||||
renderProfileList($c);
|
||||
renderSlotAssignments($c);
|
||||
toastr.success(`已删除配置「${name}」。`);
|
||||
});
|
||||
}
|
||||
|
||||
// ── 功能槽分配渲染 ────────────────────────────────────────────────────────────
|
||||
|
||||
export function renderSlotAssignments($c) {
|
||||
const $slots = $c.find('#amily2_slot_assignments');
|
||||
|
||||
const settings = extension_settings[extensionName] || {};
|
||||
|
||||
const rows = Object.entries(SLOTS).map(([slot, slotInfo]) => {
|
||||
const profiles = apiProfileManager.getProfiles(slotInfo.type);
|
||||
const assigned = apiProfileManager.getAssignment(slot) || '';
|
||||
const typeInfo = PROFILE_TYPES[slotInfo.type];
|
||||
const toggle = SLOT_TOGGLES[slot];
|
||||
|
||||
const options = [
|
||||
`<option value="">— 未分配 —</option>`,
|
||||
...profiles.map(p =>
|
||||
`<option value="${p.id}" ${p.id === assigned ? 'selected' : ''}>${_escapeHtml(p.name)}</option>`
|
||||
),
|
||||
].join('');
|
||||
|
||||
// 功能开关(仅有映射的槽位显示)
|
||||
const toggleHtml = toggle
|
||||
? `<label class="toggle-switch" style="flex-shrink:0;" title="启用/禁用此功能">
|
||||
<input type="checkbox" class="amily2_slot_toggle" data-slot="${slot}" ${settings[toggle.key] ? 'checked' : ''} />
|
||||
<span class="slider"></span>
|
||||
</label>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div style="display:flex; align-items:center; gap:8px; padding:4px 0;">
|
||||
${toggleHtml}
|
||||
<span style="width:140px; flex-shrink:0; font-size:0.9em;">${slotInfo.label}</span>
|
||||
<span style="color:var(--SmartThemeQuoteColor); font-size:0.78em; width:70px; flex-shrink:0;">
|
||||
<i class="fas ${typeInfo.icon}"></i> ${typeInfo.label}
|
||||
</span>
|
||||
<select class="text_pole amily2_slot_select" data-slot="${slot}" style="flex:1;">
|
||||
${options}
|
||||
</select>
|
||||
<button class="menu_button small_button interactable amily2_slot_test" data-slot="${slot}"
|
||||
title="测试此槽位的连接" style="flex-shrink:0; ${assigned ? '' : 'opacity:0.4; pointer-events:none;'}">
|
||||
<i class="fas fa-plug"></i>
|
||||
</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
$slots.html(rows);
|
||||
|
||||
$slots.find('.amily2_slot_select').on('change', function () {
|
||||
const slot = $(this).data('slot');
|
||||
const id = $(this).val() || null;
|
||||
if (!apiProfileManager.setAssignment(slot, id)) {
|
||||
toastr.error('类型不匹配,分配失败。');
|
||||
renderSlotAssignments($c);
|
||||
return;
|
||||
}
|
||||
document.dispatchEvent(new CustomEvent('amily2:slotAssigned', { detail: { slot } }));
|
||||
// 刷新行以更新测试按钮状态
|
||||
renderSlotAssignments($c);
|
||||
});
|
||||
|
||||
// 槽位快捷测试按钮(调用各模块真实测试函数,发送聊天请求验证连接)
|
||||
$slots.find('.amily2_slot_test').on('click', async function () {
|
||||
const slot = $(this).data('slot');
|
||||
const $btn = $(this).prop('disabled', true);
|
||||
$btn.html('<i class="fas fa-spinner fa-spin"></i>');
|
||||
|
||||
try {
|
||||
const testFn = SLOT_TEST_FNS[slot];
|
||||
if (!testFn) {
|
||||
toastr.warning('该槽位暂不支持快捷测试。', slot);
|
||||
return;
|
||||
}
|
||||
const profile = await apiProfileManager.getAssignedProfile(slot);
|
||||
if (!profile) {
|
||||
toastr.warning('该槽位未分配配置。', slot);
|
||||
return;
|
||||
}
|
||||
// 测试函数内部会显示 toastr 结果
|
||||
await testFn();
|
||||
} catch (e) {
|
||||
toastr.error(`测试失败:${e.message}`, slot);
|
||||
} finally {
|
||||
$btn.prop('disabled', false).html('<i class="fas fa-plug"></i>');
|
||||
}
|
||||
});
|
||||
|
||||
// 功能总开关:同步 extension_settings + 原面板 checkbox
|
||||
$slots.find('.amily2_slot_toggle').on('change', function () {
|
||||
const slot = $(this).data('slot');
|
||||
const toggle = SLOT_TOGGLES[slot];
|
||||
if (!toggle) return;
|
||||
|
||||
const checked = this.checked;
|
||||
const s = extension_settings[extensionName];
|
||||
if (s) s[toggle.key] = checked;
|
||||
|
||||
// 同步原面板的 checkbox(保持一致)
|
||||
const origCb = document.querySelector(toggle.checkbox);
|
||||
if (origCb && origCb.checked !== checked) {
|
||||
origCb.checked = checked;
|
||||
origCb.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
}
|
||||
|
||||
// ── 弹窗操作 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
async function openModal($c, id) {
|
||||
_editingId = id;
|
||||
const $modal = $c.find('#amily2_profile_modal');
|
||||
|
||||
if (id) {
|
||||
// 编辑模式
|
||||
const p = apiProfileManager.getProfile(id);
|
||||
if (!p) return;
|
||||
$c.find('#amily2_profile_modal_title').html('<i class="fas fa-edit"></i> 编辑连接配置');
|
||||
$c.find('#amily2_pf_type').val(p.type).prop('disabled', true); // 不允许修改类型
|
||||
$c.find('#amily2_pf_name').val(p.name);
|
||||
$c.find('#amily2_pf_provider').val(p.provider);
|
||||
$c.find('#amily2_pf_url').val(p.apiUrl);
|
||||
$c.find('#amily2_pf_key').val(''); // Key 不回显
|
||||
$c.find('#amily2_pf_model').val(p.model);
|
||||
|
||||
if (p.type === 'chat') {
|
||||
$c.find('#amily2_pf_max_tokens').val(p.maxTokens);
|
||||
$c.find('#amily2_pf_temperature').val(p.temperature);
|
||||
} else if (p.type === 'embedding') {
|
||||
$c.find('#amily2_pf_dimensions').val(p.dimensions ?? '');
|
||||
$c.find('#amily2_pf_encoding_format').val(p.encodingFormat);
|
||||
} else if (p.type === 'rerank') {
|
||||
$c.find('#amily2_pf_top_n').val(p.topN);
|
||||
$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('');
|
||||
$c.find('#amily2_pf_encoding_format').val('float');
|
||||
$c.find('#amily2_pf_top_n').val(5);
|
||||
$c.find('#amily2_pf_return_documents').prop('checked', false);
|
||||
_switchParamSections($c, 'chat');
|
||||
}
|
||||
|
||||
// 清空上次测试结果和模型列表缓存
|
||||
$c.find('#amily2_pf_test_result').text('');
|
||||
$c.find('#amily2_pf_model_list').empty();
|
||||
|
||||
$modal.css('display', 'flex');
|
||||
}
|
||||
|
||||
function closeModal($c) {
|
||||
$c.find('#amily2_profile_modal').hide();
|
||||
$c.find('#amily2_pf_type').prop('disabled', false);
|
||||
_editingId = null;
|
||||
}
|
||||
|
||||
async function saveProfile($c) {
|
||||
const type = $c.find('#amily2_pf_type').val();
|
||||
const name = $c.find('#amily2_pf_name').val().trim();
|
||||
const provider = $c.find('#amily2_pf_provider').val();
|
||||
const apiUrl = $c.find('#amily2_pf_url').val().trim();
|
||||
const apiKey = $c.find('#amily2_pf_key').val();
|
||||
const model = $c.find('#amily2_pf_model').val().trim();
|
||||
|
||||
if (!name) { toastr.warning('请填写配置名称。'); return; }
|
||||
|
||||
const data = { type, name, provider, apiUrl, model };
|
||||
|
||||
if (type === 'chat') {
|
||||
data.maxTokens = parseInt($c.find('#amily2_pf_max_tokens').val(), 10) || 65500;
|
||||
data.temperature = parseFloat($c.find('#amily2_pf_temperature').val()) || 1.0;
|
||||
} else if (type === 'embedding') {
|
||||
const dim = $c.find('#amily2_pf_dimensions').val();
|
||||
data.dimensions = dim ? parseInt(dim, 10) : null;
|
||||
data.encodingFormat = $c.find('#amily2_pf_encoding_format').val();
|
||||
} else if (type === 'rerank') {
|
||||
data.topN = parseInt($c.find('#amily2_pf_top_n').val(), 10) || 5;
|
||||
data.returnDocuments = $c.find('#amily2_pf_return_documents').is(':checked');
|
||||
}
|
||||
|
||||
const $btn = $c.find('#amily2_profile_modal_save').prop('disabled', true);
|
||||
|
||||
try {
|
||||
let profileId;
|
||||
if (_editingId) {
|
||||
apiProfileManager.updateProfile(_editingId, data);
|
||||
profileId = _editingId;
|
||||
} else {
|
||||
profileId = apiProfileManager.createProfile(data);
|
||||
}
|
||||
|
||||
// 保存 Key(非空才写入)
|
||||
if (apiKey) {
|
||||
await apiProfileManager.setKey(profileId, apiKey);
|
||||
}
|
||||
|
||||
closeModal($c);
|
||||
renderProfileList($c);
|
||||
renderSlotAssignments($c);
|
||||
toastr.success(`配置「${name}」已保存。`);
|
||||
} catch (e) {
|
||||
console.error('[ApiConfig] 保存 Profile 失败:', e);
|
||||
toastr.error('保存失败,请查看控制台。');
|
||||
} finally {
|
||||
$btn.prop('disabled', false);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 获取模型 / 测试连接 ───────────────────────────────────────────────────────
|
||||
|
||||
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 兼容接口 — 通过 ST 后端代理,规避 CORS
|
||||
const resp = await fetch('/api/backends/chat-completions/status', {
|
||||
method: 'POST',
|
||||
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
reverse_proxy: apiUrl,
|
||||
proxy_password: apiKey,
|
||||
chat_completion_source: 'openai',
|
||||
}),
|
||||
});
|
||||
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 rawData = await resp.json();
|
||||
// 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) {
|
||||
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 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 兼容接口 — 通过 ST 后端代理,规避 CORS
|
||||
const modelsResp = await fetch('/api/backends/chat-completions/status', {
|
||||
method: 'POST',
|
||||
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
reverse_proxy: apiUrl,
|
||||
proxy_password: apiKey,
|
||||
chat_completion_source: 'openai',
|
||||
}),
|
||||
});
|
||||
|
||||
if (modelsResp.ok) {
|
||||
const rawData = await modelsResp.json();
|
||||
const list = Array.isArray(rawData) ? rawData : (rawData.data ?? rawData.models ?? []);
|
||||
const count = list.length;
|
||||
$result.text(`连接成功${count ? `,${count} 个可用模型` : ''}`).css('color', 'var(--green)');
|
||||
toastr.success('连接测试通过!');
|
||||
return;
|
||||
}
|
||||
|
||||
const status = modelsResp.status;
|
||||
const errBody = await modelsResp.json().catch(() => ({}));
|
||||
const msg = errBody?.error?.message
|
||||
|| (status === 401 || status === 403 ? 'API Key 无效或无权限'
|
||||
: status === 404 ? '接口地址不存在'
|
||||
: `HTTP ${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) {
|
||||
$c.find('#amily2_pf_chat_params').toggle(type === 'chat');
|
||||
$c.find('#amily2_pf_embedding_params').toggle(type === 'embedding');
|
||||
$c.find('#amily2_pf_rerank_params').toggle(type === 'rerank');
|
||||
}
|
||||
|
||||
function _truncateUrl(url) {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
return u.host + (u.pathname.length > 1 ? u.pathname : '');
|
||||
} catch {
|
||||
return url.slice(0, 30);
|
||||
}
|
||||
}
|
||||
|
||||
function _escapeHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
4122
ui/bindings.js
4122
ui/bindings.js
File diff suppressed because it is too large
Load Diff
457
ui/drawer.js
457
ui/drawer.js
@@ -1,252 +1,205 @@
|
||||
import { getSlideToggleOptions } from '/script.js';
|
||||
import { slideToggle } from '/lib.js';
|
||||
import { extension_settings, renderExtensionTemplateAsync } from "/scripts/extensions.js";
|
||||
import { extensionName, defaultSettings } from "../utils/settings.js";
|
||||
import {
|
||||
checkAuthorization,
|
||||
displayExpiryInfo,
|
||||
pluginAuthStatus,
|
||||
} from "../utils/auth.js";
|
||||
import {
|
||||
updateUI,
|
||||
setAvailableModels,
|
||||
populateModelDropdown,
|
||||
applyUpdateIndicator,
|
||||
} from "./state.js";
|
||||
import { bindModalEvents } from "./bindings.js";
|
||||
import { fetchModels } from "../core/api.js";
|
||||
import { bindHistoriographyEvents } from "./historiography-bindings.js";
|
||||
import { bindHanlinyuanEvents } from "./hanlinyuan-bindings.js";
|
||||
import { bindTableEvents } from './table-bindings.js';
|
||||
import { showContentModal } from "./page-window.js";
|
||||
import { initializeRendererBindings } from "../core/tavern-helper/renderer-bindings.js";
|
||||
import { bindSuperMemoryEvents } from "../core/super-memory/bindings.js";
|
||||
const extensionFolderPath = `scripts/extensions/third-party/${extensionName}`;
|
||||
|
||||
|
||||
async function loadSettings() {
|
||||
extension_settings[extensionName] = {
|
||||
...defaultSettings,
|
||||
...(extension_settings[extensionName] || {}),
|
||||
};
|
||||
|
||||
|
||||
checkAuthorization();
|
||||
|
||||
|
||||
const autoLogin = localStorage.getItem("plugin_auto_login") === "true";
|
||||
console.log(
|
||||
`[Amily2-调试] 授权状态: ${pluginAuthStatus.authorized}, 自动登录标志: ${autoLogin}`,
|
||||
);
|
||||
if (autoLogin && pluginAuthStatus.authorized) {
|
||||
console.log("[Amily2号] 检测到有效授权,将执行自动UI更新。");
|
||||
}
|
||||
|
||||
$("#expiry_info").html(displayExpiryInfo());
|
||||
updateUI();
|
||||
|
||||
if (pluginAuthStatus.authorized && extension_settings[extensionName].apiUrl) {
|
||||
const cachedModels = localStorage.getItem("cached_models_amily2");
|
||||
if (cachedModels) {
|
||||
const models = JSON.parse(cachedModels);
|
||||
console.log(`[Amily2号] 从缓存加载模型列表 (${models.length}个)`);
|
||||
setAvailableModels(models);
|
||||
populateModelDropdown();
|
||||
} else {
|
||||
toastr.info("正在自动加载模型列表...", "Amily2号");
|
||||
setTimeout(async () => {
|
||||
const models = await fetchModels();
|
||||
if (models.length > 0) {
|
||||
setAvailableModels(models);
|
||||
localStorage.setItem("cached_models_amily2", JSON.stringify(models));
|
||||
populateModelDropdown();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function initializePanel(contentPanel, errorContainer) {
|
||||
if (contentPanel.data("initialized")) return;
|
||||
|
||||
try {
|
||||
const modalContent = await $.get(`${extensionFolderPath}/assets/amily2-modal.html`);
|
||||
contentPanel.html(modalContent);
|
||||
const mainContainer = contentPanel.find('#amily2_chat_optimiser');
|
||||
|
||||
if (mainContainer.length) {
|
||||
const additionalFeaturesContent = await $.get(`${extensionFolderPath}/assets/amily-additional-features/Amily2-AdditionalFeatures.html`);
|
||||
const additionalPanelHtml = `<div id="amily2_additional_features_panel" style="display: none;">${additionalFeaturesContent}</div>`;
|
||||
mainContainer.append(additionalPanelHtml);
|
||||
|
||||
const textOptimizationContent = await $.get(`${extensionFolderPath}/assets/Amily2-TextOptimization.html`);
|
||||
const textOptimizationPanelHtml = `<div id="amily2_text_optimization_panel" style="display: none;">${textOptimizationContent}</div>`;
|
||||
mainContainer.append(textOptimizationPanelHtml);
|
||||
|
||||
const hanlinyuanContent = await $.get(`${extensionFolderPath}/assets/amily-hanlinyuan-system/hanlinyuan.html`);
|
||||
const hanlinyuanPanelHtml = `<div id="amily2_hanlinyuan_panel" style="display: none;">${hanlinyuanContent}</div>`;
|
||||
mainContainer.append(hanlinyuanPanelHtml);
|
||||
|
||||
const memorisationFormsContent = await $.get(`${extensionFolderPath}/assets/amily-data-table/Memorisation-forms.html`);
|
||||
const memorisationFormsPanelHtml = `<div id="amily2_memorisation_forms_panel" style="display: none;">${memorisationFormsContent}</div>`;
|
||||
mainContainer.append(memorisationFormsPanelHtml);
|
||||
|
||||
const plotOptimizationContent = await $.get(`${extensionFolderPath}/assets/Amily2-optimization.html`);
|
||||
const plotOptimizationPanelHtml = `<div id="amily2_plot_optimization_panel" style="display: none;">${plotOptimizationContent}</div>`;
|
||||
mainContainer.append(plotOptimizationPanelHtml);
|
||||
|
||||
const cwbContent = await $.get(`${extensionFolderPath}/CharacterWorldBook/cwb_settings.html`);
|
||||
const cwbPanelHtml = `<div id="amily2_character_world_book_panel" style="display: none;">${cwbContent}</div>`;
|
||||
mainContainer.append(cwbPanelHtml);
|
||||
|
||||
const worldEditorContent = await $.get(`${extensionFolderPath}/WorldEditor.html`);
|
||||
const worldEditorPanelHtml = `<div id="amily2_world_editor_panel" style="display: none;">${worldEditorContent}</div>`;
|
||||
mainContainer.append(worldEditorPanelHtml);
|
||||
|
||||
const glossaryContent = await $.get(`${extensionFolderPath}/assets/amily-glossary-system/amily2-glossary.html`);
|
||||
const glossaryPanelHtml = `<div id="amily2_glossary_panel" style="display: none;">${glossaryContent}</div>`;
|
||||
mainContainer.append(glossaryPanelHtml);
|
||||
|
||||
const rendererContent = await $.get(`${extensionFolderPath}/core/tavern-helper/renderer.html`);
|
||||
const rendererPanelHtml = `<div id="amily2_renderer_panel" style="display: none;">${rendererContent}</div>`;
|
||||
mainContainer.append(rendererPanelHtml);
|
||||
|
||||
const superMemoryContent = await $.get(`${extensionFolderPath}/core/super-memory/index.html`);
|
||||
const superMemoryPanelHtml = `<div id="amily2_super_memory_panel" style="display: none;">${superMemoryContent}</div>`;
|
||||
mainContainer.append(superMemoryPanelHtml);
|
||||
|
||||
// 在面板创建后,加载世界书编辑器脚本
|
||||
const worldEditorScriptId = 'world-editor-script';
|
||||
if (!document.getElementById(worldEditorScriptId)) {
|
||||
const worldEditorScript = document.createElement("script");
|
||||
worldEditorScript.id = worldEditorScriptId;
|
||||
worldEditorScript.type = "module"; // 必须作为模块加载
|
||||
worldEditorScript.src = `${extensionFolderPath}/WorldEditor/WorldEditor.js?v=${Date.now()}`;
|
||||
document.head.appendChild(worldEditorScript);
|
||||
}
|
||||
}
|
||||
|
||||
bindModalEvents();
|
||||
bindHistoriographyEvents();
|
||||
await loadSettings();
|
||||
bindHanlinyuanEvents();
|
||||
bindTableEvents();
|
||||
initializeRendererBindings();
|
||||
bindSuperMemoryEvents();
|
||||
contentPanel.data("initialized", true);
|
||||
console.log("[Amily-重构] 宫殿模块已按蓝图竣工。");
|
||||
applyUpdateIndicator();
|
||||
} catch (error) {
|
||||
console.error("[Amily-建设部] 紧急报告:加载模块化蓝图时发生意外:", error);
|
||||
const errorMessage = errorContainer
|
||||
? '<p style="color:red; padding:10px; border:1px solid red; border-radius:5px;">紧急报告:在扩展区域建造Amily2号府邸时发生意外。</p>'
|
||||
: '<p style="color:red; padding: 20px;">紧急报告:无法加载Amily2号府邸内饰。</p>';
|
||||
|
||||
if (errorContainer) {
|
||||
errorContainer.append(errorMessage);
|
||||
} else {
|
||||
contentPanel.html(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDrawerFallback() {
|
||||
const drawerIcon = $('#amily2_drawer_icon');
|
||||
const contentPanel = $('#amily2_drawer_content');
|
||||
if (drawerIcon.hasClass('openIcon') && !contentPanel.is(':visible')) {
|
||||
drawerIcon.removeClass('openIcon').addClass('closedIcon');
|
||||
}
|
||||
if (drawerIcon.hasClass('closedIcon')) {
|
||||
$('.openDrawer').not(contentPanel).not('.pinnedOpen').addClass('resizing').each((_, el) => {
|
||||
slideToggle(el, {
|
||||
...getSlideToggleOptions(),
|
||||
onAnimationEnd: function (el) {
|
||||
el.closest('.drawer-content').classList.remove('resizing');
|
||||
},
|
||||
});
|
||||
});
|
||||
$('.openIcon').not(drawerIcon).not('.drawerPinnedOpen').toggleClass('closedIcon openIcon');
|
||||
$('.openDrawer').not(contentPanel).not('.pinnedOpen').toggleClass('closedDrawer openDrawer');
|
||||
|
||||
drawerIcon.toggleClass('closedIcon openIcon');
|
||||
contentPanel.toggleClass('closedDrawer openDrawer');
|
||||
|
||||
contentPanel.addClass('resizing').each((_, el) => {
|
||||
slideToggle(el, {
|
||||
...getSlideToggleOptions(),
|
||||
onAnimationEnd: function (el) {
|
||||
el.closest('.drawer-content').classList.remove('resizing');
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
drawerIcon.toggleClass('openIcon closedIcon');
|
||||
contentPanel.toggleClass('openDrawer closedDrawer');
|
||||
|
||||
contentPanel.addClass('resizing').each((_, el) => {
|
||||
slideToggle(el, {
|
||||
...getSlideToggleOptions(),
|
||||
onAnimationEnd: function (el) {
|
||||
el.closest('.drawer-content').classList.remove('resizing');
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function createDrawer() {
|
||||
const settings = extension_settings[extensionName];
|
||||
const location = settings.iconLocation || 'topbar';
|
||||
|
||||
if (location === 'topbar') {
|
||||
if ($("#amily2_main_drawer").length > 0) return;
|
||||
|
||||
const amily2DrawerHtml = `
|
||||
<div id="amily2_main_drawer" class="drawer">
|
||||
<div class="drawer-toggle" data-drawer="amily2_drawer_content">
|
||||
<div id="amily2_drawer_icon" class="drawer-icon fa-solid fa-magic fa-fw closedIcon interactable" title="Amily2号优化助手" tabindex="0"></div>
|
||||
</div>
|
||||
<div id="amily2_drawer_content" class="drawer-content closedDrawer">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
$("#sys-settings-button").after(amily2DrawerHtml);
|
||||
|
||||
const contentPanel = $("#amily2_drawer_content");
|
||||
await initializePanel(contentPanel);
|
||||
|
||||
try {
|
||||
const { doNavbarIconClick } = await import('/script.js');
|
||||
if (typeof doNavbarIconClick === 'function') {
|
||||
$('#amily2_main_drawer .drawer-toggle').on('click', doNavbarIconClick);
|
||||
console.log('[Amily2-兼容性] 检测到新版环境,已绑定官方点击事件。');
|
||||
} else {
|
||||
throw new Error('doNavbarIconClick is not a function');
|
||||
}
|
||||
} catch (error) {
|
||||
$('#amily2_main_drawer .drawer-toggle').on('click', toggleDrawerFallback);
|
||||
console.log('[Amily2-兼容性] 检测到旧版环境 (无法导入 doNavbarIconClick),已绑定后备点击事件。');
|
||||
}
|
||||
|
||||
} else if (location === 'extensions') {
|
||||
if ($("#extensions_settings2 #amily2_chat_optimiser").length > 0) return;
|
||||
const amilyFrameHtml = `
|
||||
<div id="amily2_extension_frame">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b><i class="fas fa-crown" style="color: #ffc107;"></i> Amily2号 优化中枢</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const frame = $(amilyFrameHtml);
|
||||
$('#extensions_settings2').append(frame);
|
||||
const contentPanel = frame.find('.inline-drawer-content');
|
||||
initializePanel(contentPanel, frame);
|
||||
}
|
||||
}
|
||||
import { getSlideToggleOptions } from '/script.js';
|
||||
import { slideToggle } from '/lib.js';
|
||||
import { extension_settings, renderExtensionTemplateAsync } from "/scripts/extensions.js";
|
||||
import { extensionName, extensionBasePath, defaultSettings } from "../utils/settings.js";
|
||||
import {
|
||||
checkAuthorization,
|
||||
displayExpiryInfo,
|
||||
pluginAuthStatus,
|
||||
} from "../utils/auth.js";
|
||||
import {
|
||||
updateUI,
|
||||
setAvailableModels,
|
||||
populateModelDropdown,
|
||||
applyUpdateIndicator,
|
||||
} from "./state.js";
|
||||
import { bindModalEvents } from "./bindings.js";
|
||||
import { fetchModels } from "../core/api.js";
|
||||
import registry from '../SL/module/ModuleRegistry.js';
|
||||
import { registerAllModules } from '../SL/module/register-all.js';
|
||||
const extensionFolderPath = `scripts/extensions/third-party/${extensionName}`;
|
||||
|
||||
|
||||
async function loadSettings() {
|
||||
extension_settings[extensionName] = {
|
||||
...defaultSettings,
|
||||
...(extension_settings[extensionName] || {}),
|
||||
};
|
||||
|
||||
|
||||
checkAuthorization();
|
||||
|
||||
|
||||
const autoLogin = localStorage.getItem("plugin_auto_login") === "true";
|
||||
console.log(
|
||||
`[Amily2-调试] 授权状态: ${pluginAuthStatus.authorized}, 自动登录标志: ${autoLogin}`,
|
||||
);
|
||||
if (autoLogin && pluginAuthStatus.authorized) {
|
||||
console.log("[Amily2号] 检测到有效授权,将执行自动UI更新。");
|
||||
}
|
||||
|
||||
$("#expiry_info").html(displayExpiryInfo());
|
||||
updateUI();
|
||||
|
||||
if (pluginAuthStatus.authorized && extension_settings[extensionName].apiUrl) {
|
||||
const cachedModels = localStorage.getItem("cached_models_amily2");
|
||||
if (cachedModels) {
|
||||
const models = JSON.parse(cachedModels);
|
||||
console.log(`[Amily2号] 从缓存加载模型列表 (${models.length}个)`);
|
||||
setAvailableModels(models);
|
||||
populateModelDropdown();
|
||||
} else {
|
||||
toastr.info("正在自动加载模型列表...", "Amily2号");
|
||||
setTimeout(async () => {
|
||||
const models = await fetchModels();
|
||||
if (models.length > 0) {
|
||||
setAvailableModels(models);
|
||||
localStorage.setItem("cached_models_amily2", JSON.stringify(models));
|
||||
populateModelDropdown();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function initializePanel(contentPanel, errorContainer) {
|
||||
if (contentPanel.data("initialized")) return;
|
||||
|
||||
try {
|
||||
// 1. 加载主面板外壳
|
||||
const modalContent = await $.get(`${extensionFolderPath}/assets/amily2-modal.html`);
|
||||
contentPanel.html(modalContent);
|
||||
const mainContainer = contentPanel.find('#amily2_chat_optimiser');
|
||||
|
||||
if (mainContainer.length) {
|
||||
// 2. 注册所有模块 → 统一 init + mount
|
||||
registerAllModules();
|
||||
await registry.mountAll({
|
||||
baseUrl: extensionFolderPath + '/',
|
||||
root: mainContainer[0], // 所有模块挂载到此 DOM 元素下
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 主面板跨模块绑定(导航、授权、API provider 切换等)
|
||||
bindModalEvents();
|
||||
|
||||
// 4. 加载设置(模型列表等)
|
||||
await loadSettings();
|
||||
|
||||
contentPanel.data("initialized", true);
|
||||
console.log("[Amily-重构] 模块注册式架构已就绪,已挂载模块:", registry.names().join(', '));
|
||||
applyUpdateIndicator();
|
||||
} catch (error) {
|
||||
console.error("[Amily-建设部] 紧急报告:加载模块化蓝图时发生意外:", error);
|
||||
const errorMessage = errorContainer
|
||||
? '<p style="color:red; padding:10px; border:1px solid red; border-radius:5px;">紧急报告:在扩展区域建造Amily2号府邸时发生意外。</p>'
|
||||
: '<p style="color:red; padding: 20px;">紧急报告:无法加载Amily2号府邸内饰。</p>';
|
||||
|
||||
if (errorContainer) {
|
||||
errorContainer.append(errorMessage);
|
||||
} else {
|
||||
contentPanel.html(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDrawerFallback() {
|
||||
const drawerIcon = $('#amily2_drawer_icon');
|
||||
const contentPanel = $('#amily2_drawer_content');
|
||||
if (drawerIcon.hasClass('openIcon') && !contentPanel.is(':visible')) {
|
||||
drawerIcon.removeClass('openIcon').addClass('closedIcon');
|
||||
}
|
||||
if (drawerIcon.hasClass('closedIcon')) {
|
||||
$('.openDrawer').not(contentPanel).not('.pinnedOpen').addClass('resizing').each((_, el) => {
|
||||
slideToggle(el, {
|
||||
...getSlideToggleOptions(),
|
||||
onAnimationEnd: function (el) {
|
||||
el.closest('.drawer-content').classList.remove('resizing');
|
||||
},
|
||||
});
|
||||
});
|
||||
$('.openIcon').not(drawerIcon).not('.drawerPinnedOpen').toggleClass('closedIcon openIcon');
|
||||
$('.openDrawer').not(contentPanel).not('.pinnedOpen').toggleClass('closedDrawer openDrawer');
|
||||
|
||||
drawerIcon.toggleClass('closedIcon openIcon');
|
||||
contentPanel.toggleClass('closedDrawer openDrawer');
|
||||
|
||||
contentPanel.addClass('resizing').each((_, el) => {
|
||||
slideToggle(el, {
|
||||
...getSlideToggleOptions(),
|
||||
onAnimationEnd: function (el) {
|
||||
el.closest('.drawer-content').classList.remove('resizing');
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
drawerIcon.toggleClass('openIcon closedIcon');
|
||||
contentPanel.toggleClass('openDrawer closedDrawer');
|
||||
|
||||
contentPanel.addClass('resizing').each((_, el) => {
|
||||
slideToggle(el, {
|
||||
...getSlideToggleOptions(),
|
||||
onAnimationEnd: function (el) {
|
||||
el.closest('.drawer-content').classList.remove('resizing');
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function createDrawer() {
|
||||
const settings = extension_settings[extensionName];
|
||||
const location = settings.iconLocation || 'topbar';
|
||||
|
||||
if (location === 'topbar') {
|
||||
if ($("#amily2_main_drawer").length > 0) return;
|
||||
|
||||
const amily2DrawerHtml = `
|
||||
<div id="amily2_main_drawer" class="drawer">
|
||||
<div class="drawer-toggle" data-drawer="amily2_drawer_content">
|
||||
<div id="amily2_drawer_icon" class="drawer-icon fa-solid fa-magic fa-fw closedIcon interactable" title="Amily2号优化助手" tabindex="0"></div>
|
||||
</div>
|
||||
<div id="amily2_drawer_content" class="drawer-content closedDrawer">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
$("#sys-settings-button").after(amily2DrawerHtml);
|
||||
|
||||
const contentPanel = $("#amily2_drawer_content");
|
||||
await initializePanel(contentPanel);
|
||||
|
||||
try {
|
||||
const { doNavbarIconClick } = await import('/script.js');
|
||||
if (typeof doNavbarIconClick === 'function') {
|
||||
$('#amily2_main_drawer .drawer-toggle').on('click', doNavbarIconClick);
|
||||
console.log('[Amily2-兼容性] 检测到新版环境,已绑定官方点击事件。');
|
||||
} else {
|
||||
throw new Error('doNavbarIconClick is not a function');
|
||||
}
|
||||
} catch (error) {
|
||||
$('#amily2_main_drawer .drawer-toggle').on('click', toggleDrawerFallback);
|
||||
console.log('[Amily2-兼容性] 检测到旧版环境 (无法导入 doNavbarIconClick),已绑定后备点击事件。');
|
||||
}
|
||||
|
||||
} else if (location === 'extensions') {
|
||||
if ($("#extensions_settings2 #amily2_chat_optimiser").length > 0) return;
|
||||
const amilyFrameHtml = `
|
||||
<div id="amily2_extension_frame">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b><i class="fas fa-crown" style="color: #ffc107;"></i> Amily2号 优化中枢</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const frame = $(amilyFrameHtml);
|
||||
$('#extensions_settings2').append(frame);
|
||||
const contentPanel = frame.find('.inline-drawer-content');
|
||||
initializePanel(contentPanel, frame);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
1543
ui/plot-opt-bindings.js
Normal file
1543
ui/plot-opt-bindings.js
Normal file
File diff suppressed because it is too large
Load Diff
402
ui/profile-sync.js
Normal file
402
ui/profile-sync.js
Normal file
@@ -0,0 +1,402 @@
|
||||
/**
|
||||
* ui/profile-sync.js — API Profile → 子面板 UI 同步
|
||||
*
|
||||
* 当某功能槽分配了 Profile 时:
|
||||
* 1. 隐藏对应功能区的 API 连接配置字段(保留温度/Token 等生成参数)
|
||||
* 2. 注入一张状态卡,显示 Profile 信息 + 测试连接 / 获取模型按钮
|
||||
*
|
||||
* 当槽位未分配时:恢复旧字段显示,移除状态卡。
|
||||
*
|
||||
* 用法:
|
||||
* import { syncAllSlots, syncSlot } from './profile-sync.js';
|
||||
* await syncAllSlots(); // 面板初始化时全量同步
|
||||
* await syncSlot('main'); // 单个槽位分配变更时调用
|
||||
*
|
||||
* 外部事件:
|
||||
* document 上监听 'amily2:slotAssigned',detail = { slot }
|
||||
* 由 api-config-bindings.js 在分配变更后 dispatch。
|
||||
*/
|
||||
|
||||
import { apiProfileManager } from '../utils/config/ApiProfileManager.js';
|
||||
import { getRequestHeaders } from '/script.js';
|
||||
import { testApiConnection } from '../core/api.js';
|
||||
import { testConcurrentApiConnection } from '../core/api/ConcurrentApi.js';
|
||||
import { testNgmsApiConnection } from '../core/api/Ngms_api.js';
|
||||
import { testNccsApiConnection } from '../core/api/NccsApi.js';
|
||||
|
||||
// ── 常量 ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
// 用于通过子元素定位父 block 的选择器
|
||||
const BLOCK_SEL = '.amily2_settings_block, .control-group, .amily2_opt_settings_block';
|
||||
|
||||
const CARD_CLASS = 'amily2_profile_status_card';
|
||||
const CARD_SLOT_ATTR = 'data-card-slot';
|
||||
const HIDDEN_ATTR = 'data-profile-hidden';
|
||||
|
||||
// ── 槽位 → DOM 映射 ───────────────────────────────────────────────────────────
|
||||
//
|
||||
// container : 状态卡注入的父容器(CSS 选择器或 'closest-fieldset:xxx')
|
||||
// hideParentBlock: 通过子元素选择器找到其最近的 BLOCK_SEL 父元素并隐藏
|
||||
// hideDirectly : 直接隐藏的元素选择器
|
||||
// hideWithLabel : 隐藏元素(上溯到容器直接子元素)+ 前一个兄弟 <label>(inline-grid 布局用)
|
||||
// hideInContainer: 在容器内 querySelector 查找并隐藏
|
||||
// fields : { profileKey: domSelector } — 用于回填值(向下兼容 fallback 读取)
|
||||
// keyField : API Key 输入框(回填遮蔽值)
|
||||
// testFn : 测试连接函数(发送真实聊天请求)
|
||||
|
||||
const SLOT_CONFIGS = {
|
||||
main: {
|
||||
container: 'closest-fieldset:#amily2_api_provider',
|
||||
hideParentBlock: ['#amily2_api_provider', '#amily2_model_selector'],
|
||||
hideDirectly: ['#amily2_api_url_wrapper', '#amily2_api_key_wrapper', '#amily2_preset_wrapper'],
|
||||
hideWithLabel: [],
|
||||
hideInContainer: [],
|
||||
fields: { provider: '#amily2_api_provider', apiUrl: '#amily2_api_url', model: '#amily2_manual_model_input' },
|
||||
keyField: '#amily2_api_key',
|
||||
testFn: testApiConnection,
|
||||
},
|
||||
plotOpt: {
|
||||
container: '#amily2_opt_custom_api_settings_block',
|
||||
hideParentBlock: [],
|
||||
hideDirectly: [],
|
||||
hideWithLabel: [],
|
||||
hideInContainer: [],
|
||||
fields: { apiUrl: '#amily2_opt_api_url', model: '#amily2_opt_model' },
|
||||
keyField: '#amily2_opt_api_key',
|
||||
testFn: null,
|
||||
},
|
||||
plotOptConc: {
|
||||
container: '#amily2_concurrent_content',
|
||||
hideParentBlock: [],
|
||||
hideDirectly: [],
|
||||
hideWithLabel: [
|
||||
'#amily2_plotOpt_concurrentApiProvider',
|
||||
'#amily2_plotOpt_concurrentApiUrl',
|
||||
'#amily2_plotOpt_concurrentApiKey',
|
||||
'#amily2_plotOpt_concurrentModel',
|
||||
],
|
||||
hideInContainer: ['.jqyh-button-row'],
|
||||
fields: { provider: '#amily2_plotOpt_concurrentApiProvider', apiUrl: '#amily2_plotOpt_concurrentApiUrl', model: '#amily2_plotOpt_concurrentModel' },
|
||||
keyField: '#amily2_plotOpt_concurrentApiKey',
|
||||
testFn: testConcurrentApiConnection,
|
||||
},
|
||||
nccs: {
|
||||
container: '#nccs-api-config',
|
||||
hideParentBlock: ['#nccs-api-mode', '#nccs-api-url', '#nccs-api-key', '#nccs-api-model', '#nccs-api-fakestream-enabled', '#nccs-sillytavern-preset'],
|
||||
hideDirectly: [],
|
||||
hideWithLabel: [],
|
||||
hideInContainer: ['.nccs-button-row'],
|
||||
fields: { apiUrl: '#nccs-api-url', model: '#nccs-api-model' },
|
||||
keyField: '#nccs-api-key',
|
||||
testFn: testNccsApiConnection,
|
||||
},
|
||||
ngms: {
|
||||
container: '#amily2_ngms_content',
|
||||
hideParentBlock: ['#amily2_ngms_api_mode', '#amily2_ngms_fakestream_enabled'],
|
||||
hideDirectly: ['#amily2_ngms_compatible_config', '#amily2_ngms_preset_config'],
|
||||
hideWithLabel: [],
|
||||
hideInContainer: ['.ngms-button-row'],
|
||||
fields: { apiUrl: '#amily2_ngms_api_url', model: '#amily2_ngms_model' },
|
||||
keyField: '#amily2_ngms_api_key',
|
||||
testFn: testNgmsApiConnection,
|
||||
},
|
||||
};
|
||||
|
||||
// ── 公开 API ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/** 同步单个槽位到对应 DOM 区域。 */
|
||||
export async function syncSlot(slot) {
|
||||
const config = SLOT_CONFIGS[slot];
|
||||
if (!config) return;
|
||||
|
||||
const profile = await apiProfileManager.getAssignedProfile(slot);
|
||||
|
||||
// 先清理:移除旧卡片、恢复被隐藏的元素
|
||||
_removeCard(slot);
|
||||
_restoreHidden(slot);
|
||||
|
||||
if (!profile) return;
|
||||
|
||||
const container = _resolveContainer(config.container);
|
||||
if (!container) return;
|
||||
|
||||
// 回填值(向下兼容:部分代码仍从 DOM 读取 fallback)
|
||||
for (const [key, sel] of Object.entries(config.fields || {})) {
|
||||
const el = document.querySelector(sel);
|
||||
if (el) el.value = profile[key] ?? '';
|
||||
}
|
||||
if (config.keyField) {
|
||||
const keyEl = document.querySelector(config.keyField);
|
||||
if (keyEl) keyEl.value = profile.apiKey ? '••••••••' : '';
|
||||
}
|
||||
|
||||
// 隐藏 API 连接字段(保留温度 / 最大 Token 等生成参数)
|
||||
_hideApiFields(config, container, slot);
|
||||
|
||||
// 注入状态卡
|
||||
_injectCard(slot, profile, config, container);
|
||||
}
|
||||
|
||||
/** 同步所有槽位(面板初始化时调用)。 */
|
||||
export async function syncAllSlots() {
|
||||
await Promise.all(Object.keys(SLOT_CONFIGS).map(syncSlot));
|
||||
}
|
||||
|
||||
// ── 事件监听:响应 api-config-bindings 的 slotAssigned 事件 ──────────────────
|
||||
|
||||
document.addEventListener('amily2:slotAssigned', (e) => {
|
||||
const slot = e.detail?.slot;
|
||||
if (slot) syncSlot(slot);
|
||||
});
|
||||
|
||||
// ── 内部:容器定位 ──────────────────────────────────────────────────────────────
|
||||
|
||||
function _resolveContainer(spec) {
|
||||
if (!spec) return null;
|
||||
|
||||
// 'closest-fieldset:#amily2_api_provider' → 从该元素向上找 fieldset
|
||||
if (spec.startsWith('closest-fieldset:')) {
|
||||
const anchorSel = spec.slice('closest-fieldset:'.length);
|
||||
const anchor = document.querySelector(anchorSel);
|
||||
return anchor?.closest('fieldset') ?? null;
|
||||
}
|
||||
|
||||
return document.querySelector(spec);
|
||||
}
|
||||
|
||||
// ── 内部:隐藏 / 恢复 API 字段 ──────────────────────────────────────────────────
|
||||
|
||||
function _hideEl(el, slot) {
|
||||
if (!el || el.hasAttribute(HIDDEN_ATTR)) return;
|
||||
el.setAttribute(HIDDEN_ATTR, slot);
|
||||
el.setAttribute('data-prev-display', el.style.display || '');
|
||||
el.style.display = 'none';
|
||||
}
|
||||
|
||||
function _restoreHidden(slot) {
|
||||
document.querySelectorAll(`[${HIDDEN_ATTR}="${slot}"]`).forEach(el => {
|
||||
el.style.display = el.getAttribute('data-prev-display') || '';
|
||||
el.removeAttribute(HIDDEN_ATTR);
|
||||
el.removeAttribute('data-prev-display');
|
||||
});
|
||||
}
|
||||
|
||||
function _hideApiFields(config, container, slot) {
|
||||
// 1. 通过子元素找到其父 block 并隐藏
|
||||
(config.hideParentBlock || []).forEach(sel => {
|
||||
const el = document.querySelector(sel);
|
||||
if (!el) return;
|
||||
const block = el.closest(BLOCK_SEL);
|
||||
if (block && block !== container) _hideEl(block, slot);
|
||||
});
|
||||
|
||||
// 2. 直接隐藏指定元素
|
||||
(config.hideDirectly || []).forEach(sel => {
|
||||
const el = document.querySelector(sel);
|
||||
if (el) _hideEl(el, slot);
|
||||
});
|
||||
|
||||
// 3. 隐藏元素(上溯到容器直接子元素)+ 前一个兄弟 label(inline-grid 布局)
|
||||
(config.hideWithLabel || []).forEach(sel => {
|
||||
const el = document.querySelector(sel);
|
||||
if (!el) return;
|
||||
// 沿 DOM 树上溯到容器的直接子元素
|
||||
let target = el;
|
||||
while (target.parentElement && target.parentElement !== container) {
|
||||
target = target.parentElement;
|
||||
}
|
||||
_hideEl(target, slot);
|
||||
const prev = target.previousElementSibling;
|
||||
if (prev && prev.tagName === 'LABEL') _hideEl(prev, slot);
|
||||
});
|
||||
|
||||
// 4. 在容器内查找并隐藏
|
||||
(config.hideInContainer || []).forEach(sel => {
|
||||
const el = container.querySelector(sel);
|
||||
if (el) _hideEl(el, slot);
|
||||
});
|
||||
}
|
||||
|
||||
// ── 内部:状态卡 ──────────────────────────────────────────────────────────────
|
||||
|
||||
function _removeCard(slot) {
|
||||
document.querySelectorAll(`.${CARD_CLASS}[${CARD_SLOT_ATTR}="${slot}"]`)
|
||||
.forEach(el => el.remove());
|
||||
}
|
||||
|
||||
function _injectCard(slot, profile, _config, container) {
|
||||
const card = document.createElement('div');
|
||||
card.className = CARD_CLASS;
|
||||
card.setAttribute(CARD_SLOT_ATTR, slot);
|
||||
card.style.cssText = [
|
||||
'padding:10px 14px', 'margin:6px 0 10px',
|
||||
'background:var(--black10a)',
|
||||
'border:1px solid var(--SmartThemeBorderColor)',
|
||||
'border-radius:6px', 'font-size:0.88em',
|
||||
].join(';');
|
||||
|
||||
const providerLabel = {
|
||||
openai: 'OpenAI 兼容',
|
||||
openai_test: '全兼容',
|
||||
google: 'Google Gemini',
|
||||
sillytavern_backend: 'ST 后端',
|
||||
sillytavern_preset: 'ST 预设',
|
||||
}[profile.provider] || profile.provider || '';
|
||||
|
||||
card.innerHTML = `
|
||||
<div style="display:flex; align-items:center; gap:8px; margin-bottom:8px;">
|
||||
<i class="fas fa-link" style="color:var(--green,#4caf50);"></i>
|
||||
<span style="font-weight:600;">${_esc(profile.name)}</span>
|
||||
<span style="color:var(--SmartThemeQuoteColor); font-size:0.85em;">
|
||||
${providerLabel ? `<i class="fas fa-cloud"></i> ${_esc(providerLabel)}` : ''}
|
||||
${profile.model ? ` · <i class="fas fa-robot"></i> ${_esc(profile.model)}` : ''}
|
||||
</span>
|
||||
<span class="amily2_psc_goto" style="margin-left:auto; opacity:0.6; font-size:0.85em; cursor:pointer;"
|
||||
title="前往 API 配置页面">
|
||||
<i class="fas fa-cog"></i> 管理
|
||||
</span>
|
||||
</div>
|
||||
<div style="display:flex; gap:6px; flex-wrap:wrap;">
|
||||
<button class="menu_button small_button interactable amily2_psc_test" type="button">
|
||||
<i class="fas fa-plug"></i> 测试连接
|
||||
</button>
|
||||
<button class="menu_button small_button interactable amily2_psc_fetch" type="button">
|
||||
<i class="fas fa-list"></i> 获取模型
|
||||
</button>
|
||||
<span class="amily2_psc_result" style="font-size:0.85em; display:flex; align-items:center; margin-left:4px;"></span>
|
||||
</div>`;
|
||||
|
||||
// 绑定按钮事件
|
||||
card.querySelector('.amily2_psc_goto').addEventListener('click', () => {
|
||||
document.getElementById('amily2_open_api_config')?.click();
|
||||
});
|
||||
card.querySelector('.amily2_psc_test').addEventListener('click', () => _testSlot(slot, card));
|
||||
card.querySelector('.amily2_psc_fetch').addEventListener('click', () => _fetchSlotModels(slot, card));
|
||||
|
||||
// 插入到 legend 之后(fieldset)或容器开头
|
||||
const legend = container.querySelector(':scope > legend');
|
||||
if (legend) {
|
||||
legend.insertAdjacentElement('afterend', card);
|
||||
} else {
|
||||
container.prepend(card);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 内部:测试连接(调用各模块的真实测试函数,发送聊天请求)──────────────────────
|
||||
|
||||
async function _testSlot(slot, card) {
|
||||
const $btn = $(card.querySelector('.amily2_psc_test')).prop('disabled', true);
|
||||
const $result = $(card.querySelector('.amily2_psc_result'));
|
||||
$btn.html('<i class="fas fa-spinner fa-spin"></i> 测试中...');
|
||||
$result.text('').css('color', '');
|
||||
|
||||
try {
|
||||
const testFn = SLOT_CONFIGS[slot]?.testFn;
|
||||
if (!testFn) {
|
||||
$result.text('该槽位不支持测试').css('color', 'var(--warning-color)');
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用模块原生测试函数(发送 "你好!" 聊天请求验证连接)
|
||||
const success = await testFn();
|
||||
|
||||
if (success === true) {
|
||||
$result.text('测试通过').css('color', 'var(--green)');
|
||||
} else if (success === false) {
|
||||
$result.text('测试失败(详见弹窗)').css('color', 'var(--warning-color)');
|
||||
}
|
||||
// undefined = 函数未执行(如 DOM 依赖缺失),不更新卡片
|
||||
} catch (e) {
|
||||
$result.text(`错误:${e.message}`).css('color', 'var(--warning-color)');
|
||||
} finally {
|
||||
$btn.prop('disabled', false).html('<i class="fas fa-plug"></i> 测试连接');
|
||||
}
|
||||
}
|
||||
|
||||
// ── 内部:获取模型列表 ──────────────────────────────────────────────────────────
|
||||
|
||||
async function _fetchSlotModels(slot, card) {
|
||||
const $btn = $(card.querySelector('.amily2_psc_fetch')).prop('disabled', true);
|
||||
const $result = $(card.querySelector('.amily2_psc_result'));
|
||||
$btn.html('<i class="fas fa-spinner fa-spin"></i> 获取中...');
|
||||
$result.text('').css('color', '');
|
||||
|
||||
try {
|
||||
const profile = await apiProfileManager.getAssignedProfile(slot);
|
||||
if (!profile) {
|
||||
$result.text('槽位未分配').css('color', 'var(--warning-color)');
|
||||
return;
|
||||
}
|
||||
|
||||
// ST 预设由酒馆管理,无法获取模型列表
|
||||
if (profile.provider === 'sillytavern_preset' || profile.provider === 'sillytavern_backend') {
|
||||
$result.text('ST 预设/后端管理,无需获取').css('color', 'var(--SmartThemeQuoteColor)');
|
||||
return;
|
||||
}
|
||||
|
||||
let models = [];
|
||||
|
||||
if (profile.provider === 'google') {
|
||||
if (!profile.apiKey) {
|
||||
$result.text('API Key 为空').css('color', 'var(--warning-color)');
|
||||
return;
|
||||
}
|
||||
const resp = await fetch(
|
||||
`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(profile.apiKey)}`
|
||||
);
|
||||
if (!resp.ok) {
|
||||
$result.text(`失败:HTTP ${resp.status}`).css('color', 'var(--warning-color)');
|
||||
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 兼容 — 通过 ST 后端代理获取模型列表
|
||||
const resp = await fetch('/api/backends/chat-completions/status', {
|
||||
method: 'POST',
|
||||
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
reverse_proxy: profile.apiUrl,
|
||||
proxy_password: profile.apiKey,
|
||||
chat_completion_source: 'openai',
|
||||
}),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
$result.text(`失败:HTTP ${resp.status}`).css('color', 'var(--warning-color)');
|
||||
return;
|
||||
}
|
||||
const rawData = await resp.json();
|
||||
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) {
|
||||
$result.text('未获取到模型').css('color', 'var(--warning-color)');
|
||||
return;
|
||||
}
|
||||
|
||||
const current = profile.model;
|
||||
const inList = current && models.includes(current);
|
||||
$result.html(
|
||||
`<span style="color:var(--green);">${models.length} 个模型</span>` +
|
||||
(current ? ` · 当前: <b>${_esc(current)}</b> ${inList ? '✓' : '<span style="color:var(--warning-color);">(不在列表中)</span>'}` : '')
|
||||
);
|
||||
toastr.success(`已获取 ${models.length} 个模型。`, `槽位:${slot}`);
|
||||
} catch (e) {
|
||||
$result.text(`错误:${e.message}`).css('color', 'var(--warning-color)');
|
||||
} finally {
|
||||
$btn.prop('disabled', false).html('<i class="fas fa-list"></i> 获取模型');
|
||||
}
|
||||
}
|
||||
|
||||
// ── 工具 ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
function _esc(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&').replace(/</g, '<')
|
||||
.replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
4595
ui/table-bindings.js
4595
ui/table-bindings.js
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user