mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-05-17 00:07:37 +08:00
此次提交引入了重要的重构和改进: - JavaScript ([`app/static/js/config_editor.js`](app/static/js/config_editor.js:1), [`app/static/js/keys_status.js`](app/static/js/keys_status.js:1), [`app/static/js/error_logs.js`](app/static/js/error_logs.js:1)): - 通过初始化函数(例如 [`initializeKeyPaginationAndSearch()`](app/static/js/config_editor.js:985),[`initializeAutoRefreshControls()`](app/static/js/config_editor.js:936))实现代码模块化,以实现更好的组织。 - 通过采用 `fetchAPI` 辅助函数(在 [`showApiCallDetails()`](app/static/js/config_editor.js:1097),[`fetchAndDisplayLogs()`](app/static/js/error_logs.js:68),[`fetchKeyStatus()`](app/static/js/keys_status.js:283) 中可见其用法)标准化 API 交互。 - 改进了分页、搜索和 DOM 元素管理,尤其是在 [`config_editor.js`](app/static/js/config_editor.js:1) 和 [`keys_status.js`](app/static/js/keys_status.js:1) 中。 - 在 [`config_editor.js`](app/static/js/config_editor.js:1029) 中通过 [`registerServiceWorker()`](app/static/js/config_editor.js:1018) 添加了 service worker 注册。 - Gemini API ([`app/router/gemini_routes.py`](app/router/gemini_routes.py:1)): - 在 [`verify_selected_keys()`](app/router/gemini_routes.py:328) 端点内的 `GeminiRequest` 中添加了 `generation_config`(包含 `temperature`、`top_p`、`max_output_tokens`),以实现更可控和一致的 API 密钥验证调用。 - 配置用户界面 ([`app/templates/config_editor.html`](app/templates/config_editor.html:1)): - 将 `sensitive-input` 类应用于各种 API 密钥和令牌字段(例如 [`AUTH_TOKEN`](app/templates/config_editor.html:149),[`PAID_KEY`](app/templates/config_editor.html:339),[`SMMS_SECRET_TOKEN`](app/templates/config_editor.html:364)),以启用特定的客户端处理(例如屏蔽或特殊验证)。 这些更改旨在提高代码的可维护性,标准化前端后端通信,增强 API 交互的稳健性,并优化用于应用程序配置和 API 密钥状态管理的用户界面。
1342 lines
54 KiB
JavaScript
1342 lines
54 KiB
JavaScript
// Constants
|
|
const SENSITIVE_INPUT_CLASS = 'sensitive-input';
|
|
const ARRAY_ITEM_CLASS = 'array-item';
|
|
const ARRAY_INPUT_CLASS = 'array-input';
|
|
const MAP_ITEM_CLASS = 'map-item';
|
|
const MAP_KEY_INPUT_CLASS = 'map-key-input';
|
|
const MAP_VALUE_INPUT_CLASS = 'map-value-input';
|
|
const SAFETY_SETTING_ITEM_CLASS = 'safety-setting-item';
|
|
const SHOW_CLASS = 'show'; // For modals
|
|
const API_KEY_REGEX = /AIzaSy\S{33}/g;
|
|
const PROXY_REGEX = /(?:https?|socks5):\/\/(?:[^:@\/]+(?::[^@\/]+)?@)?(?:[^:\/\s]+)(?::\d+)?/g;
|
|
const MASKED_VALUE = '••••••••';
|
|
|
|
// DOM Elements - Global Scope for frequently accessed elements
|
|
const safetySettingsContainer = document.getElementById('SAFETY_SETTINGS_container');
|
|
const thinkingModelsContainer = document.getElementById('THINKING_MODELS_container');
|
|
const apiKeyModal = document.getElementById('apiKeyModal');
|
|
const apiKeyBulkInput = document.getElementById('apiKeyBulkInput');
|
|
const apiKeySearchInput = document.getElementById('apiKeySearchInput');
|
|
const bulkDeleteApiKeyModal = document.getElementById('bulkDeleteApiKeyModal');
|
|
const bulkDeleteApiKeyInput = document.getElementById('bulkDeleteApiKeyInput');
|
|
const proxyModal = document.getElementById('proxyModal');
|
|
const proxyBulkInput = document.getElementById('proxyBulkInput');
|
|
const bulkDeleteProxyModal = document.getElementById('bulkDeleteProxyModal');
|
|
const bulkDeleteProxyInput = document.getElementById('bulkDeleteProxyInput');
|
|
const resetConfirmModal = document.getElementById('resetConfirmModal');
|
|
const configForm = document.getElementById('configForm'); // Added for frequent use
|
|
|
|
// Modal Control Functions
|
|
function openModal(modalElement) {
|
|
if (modalElement) {
|
|
modalElement.classList.add(SHOW_CLASS);
|
|
}
|
|
}
|
|
|
|
function closeModal(modalElement) {
|
|
if (modalElement) {
|
|
modalElement.classList.remove(SHOW_CLASS);
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialize configuration
|
|
initConfig();
|
|
|
|
// Tab switching
|
|
const tabButtons = document.querySelectorAll('.tab-btn');
|
|
tabButtons.forEach(button => {
|
|
button.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
const tabId = this.getAttribute('data-tab');
|
|
switchTab(tabId);
|
|
});
|
|
});
|
|
|
|
// Upload provider switching
|
|
const uploadProviderSelect = document.getElementById('UPLOAD_PROVIDER');
|
|
if (uploadProviderSelect) {
|
|
uploadProviderSelect.addEventListener('change', function() {
|
|
toggleProviderConfig(this.value);
|
|
});
|
|
}
|
|
|
|
// Toggle switch events
|
|
const toggleSwitches = document.querySelectorAll('.toggle-switch');
|
|
toggleSwitches.forEach(toggleSwitch => {
|
|
toggleSwitch.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
const checkbox = this.querySelector('input[type="checkbox"]');
|
|
if (checkbox) {
|
|
checkbox.checked = !checkbox.checked;
|
|
}
|
|
});
|
|
});
|
|
|
|
// Save button
|
|
const saveBtn = document.getElementById('saveBtn');
|
|
if (saveBtn) {
|
|
saveBtn.addEventListener('click', saveConfig);
|
|
}
|
|
|
|
// Reset button
|
|
const resetBtn = document.getElementById('resetBtn');
|
|
if (resetBtn) {
|
|
resetBtn.addEventListener('click', resetConfig); // resetConfig will open the modal
|
|
}
|
|
|
|
// Scroll buttons
|
|
window.addEventListener('scroll', toggleScrollButtons);
|
|
|
|
// API Key Modal Elements and Events
|
|
const addApiKeyBtn = document.getElementById('addApiKeyBtn');
|
|
const closeApiKeyModalBtn = document.getElementById('closeApiKeyModalBtn');
|
|
const cancelAddApiKeyBtn = document.getElementById('cancelAddApiKeyBtn');
|
|
const confirmAddApiKeyBtn = document.getElementById('confirmAddApiKeyBtn');
|
|
|
|
if (addApiKeyBtn) {
|
|
addApiKeyBtn.addEventListener('click', () => {
|
|
openModal(apiKeyModal);
|
|
if (apiKeyBulkInput) apiKeyBulkInput.value = '';
|
|
});
|
|
}
|
|
if (closeApiKeyModalBtn) closeApiKeyModalBtn.addEventListener('click', () => closeModal(apiKeyModal));
|
|
if (cancelAddApiKeyBtn) cancelAddApiKeyBtn.addEventListener('click', () => closeModal(apiKeyModal));
|
|
if (confirmAddApiKeyBtn) confirmAddApiKeyBtn.addEventListener('click', handleBulkAddApiKeys);
|
|
if (apiKeySearchInput) apiKeySearchInput.addEventListener('input', handleApiKeySearch);
|
|
|
|
|
|
// Bulk Delete API Key Modal Elements and Events
|
|
const bulkDeleteApiKeyBtn = document.getElementById('bulkDeleteApiKeyBtn');
|
|
const closeBulkDeleteModalBtn = document.getElementById('closeBulkDeleteModalBtn');
|
|
const cancelBulkDeleteApiKeyBtn = document.getElementById('cancelBulkDeleteApiKeyBtn');
|
|
const confirmBulkDeleteApiKeyBtn = document.getElementById('confirmBulkDeleteApiKeyBtn');
|
|
|
|
if (bulkDeleteApiKeyBtn) {
|
|
bulkDeleteApiKeyBtn.addEventListener('click', () => {
|
|
openModal(bulkDeleteApiKeyModal);
|
|
if (bulkDeleteApiKeyInput) bulkDeleteApiKeyInput.value = '';
|
|
});
|
|
}
|
|
if (closeBulkDeleteModalBtn) closeBulkDeleteModalBtn.addEventListener('click', () => closeModal(bulkDeleteApiKeyModal));
|
|
if (cancelBulkDeleteApiKeyBtn) cancelBulkDeleteApiKeyBtn.addEventListener('click', () => closeModal(bulkDeleteApiKeyModal));
|
|
if (confirmBulkDeleteApiKeyBtn) confirmBulkDeleteApiKeyBtn.addEventListener('click', handleBulkDeleteApiKeys);
|
|
|
|
|
|
// Proxy Modal Elements and Events
|
|
const addProxyBtn = document.getElementById('addProxyBtn');
|
|
const closeProxyModalBtn = document.getElementById('closeProxyModalBtn');
|
|
const cancelAddProxyBtn = document.getElementById('cancelAddProxyBtn');
|
|
const confirmAddProxyBtn = document.getElementById('confirmAddProxyBtn');
|
|
|
|
if (addProxyBtn) {
|
|
addProxyBtn.addEventListener('click', () => {
|
|
openModal(proxyModal);
|
|
if (proxyBulkInput) proxyBulkInput.value = '';
|
|
});
|
|
}
|
|
if (closeProxyModalBtn) closeProxyModalBtn.addEventListener('click', () => closeModal(proxyModal));
|
|
if (cancelAddProxyBtn) cancelAddProxyBtn.addEventListener('click', () => closeModal(proxyModal));
|
|
if (confirmAddProxyBtn) confirmAddProxyBtn.addEventListener('click', handleBulkAddProxies);
|
|
|
|
|
|
// Bulk Delete Proxy Modal Elements and Events
|
|
const bulkDeleteProxyBtn = document.getElementById('bulkDeleteProxyBtn');
|
|
const closeBulkDeleteProxyModalBtn = document.getElementById('closeBulkDeleteProxyModalBtn');
|
|
const cancelBulkDeleteProxyBtn = document.getElementById('cancelBulkDeleteProxyBtn');
|
|
const confirmBulkDeleteProxyBtn = document.getElementById('confirmBulkDeleteProxyBtn');
|
|
|
|
if (bulkDeleteProxyBtn) {
|
|
bulkDeleteProxyBtn.addEventListener('click', () => {
|
|
openModal(bulkDeleteProxyModal);
|
|
if (bulkDeleteProxyInput) bulkDeleteProxyInput.value = '';
|
|
});
|
|
}
|
|
if (closeBulkDeleteProxyModalBtn) closeBulkDeleteProxyModalBtn.addEventListener('click', () => closeModal(bulkDeleteProxyModal));
|
|
if (cancelBulkDeleteProxyBtn) cancelBulkDeleteProxyBtn.addEventListener('click', () => closeModal(bulkDeleteProxyModal));
|
|
if (confirmBulkDeleteProxyBtn) confirmBulkDeleteProxyBtn.addEventListener('click', handleBulkDeleteProxies);
|
|
|
|
|
|
// Reset Confirmation Modal Elements and Events
|
|
const closeResetModalBtn = document.getElementById('closeResetModalBtn');
|
|
const cancelResetBtn = document.getElementById('cancelResetBtn');
|
|
const confirmResetBtn = document.getElementById('confirmResetBtn');
|
|
|
|
if (closeResetModalBtn) closeResetModalBtn.addEventListener('click', () => closeModal(resetConfirmModal));
|
|
if (cancelResetBtn) cancelResetBtn.addEventListener('click', () => closeModal(resetConfirmModal));
|
|
if (confirmResetBtn) {
|
|
confirmResetBtn.addEventListener('click', () => {
|
|
closeModal(resetConfirmModal);
|
|
executeReset();
|
|
});
|
|
}
|
|
|
|
// Click outside modal to close
|
|
window.addEventListener('click', (event) => {
|
|
const modals = [apiKeyModal, resetConfirmModal, bulkDeleteApiKeyModal, proxyModal, bulkDeleteProxyModal];
|
|
modals.forEach(modal => {
|
|
if (event.target === modal) {
|
|
closeModal(modal);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Removed static token generation button event listener, now handled dynamically if needed or by specific buttons.
|
|
|
|
// Authentication token generation button
|
|
const generateAuthTokenBtn = document.getElementById('generateAuthTokenBtn');
|
|
const authTokenInput = document.getElementById('AUTH_TOKEN');
|
|
if (generateAuthTokenBtn && authTokenInput) {
|
|
generateAuthTokenBtn.addEventListener('click', function() {
|
|
const newToken = generateRandomToken(); // Assuming generateRandomToken is defined elsewhere
|
|
authTokenInput.value = newToken;
|
|
if (authTokenInput.classList.contains(SENSITIVE_INPUT_CLASS)) {
|
|
const event = new Event('focusout', { bubbles: true, cancelable: true });
|
|
authTokenInput.dispatchEvent(event);
|
|
}
|
|
showNotification('已生成新认证令牌', 'success');
|
|
});
|
|
}
|
|
|
|
// Event delegation for THINKING_MODELS input changes to update budget map keys
|
|
if (thinkingModelsContainer) {
|
|
thinkingModelsContainer.addEventListener('input', function(event) {
|
|
const target = event.target;
|
|
if (target && target.classList.contains(ARRAY_INPUT_CLASS) && target.closest(`.${ARRAY_ITEM_CLASS}[data-model-id]`)) {
|
|
const modelInput = target;
|
|
const modelItem = modelInput.closest(`.${ARRAY_ITEM_CLASS}`);
|
|
const modelId = modelItem.getAttribute('data-model-id');
|
|
const budgetKeyInput = document.querySelector(`.${MAP_KEY_INPUT_CLASS}[data-model-id="${modelId}"]`);
|
|
if (budgetKeyInput) {
|
|
budgetKeyInput.value = modelInput.value;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Event delegation for dynamically added remove buttons and generate token buttons within array items
|
|
if(configForm) { // Ensure configForm exists before adding event listener
|
|
configForm.addEventListener('click', function(event) {
|
|
const target = event.target;
|
|
const removeButton = target.closest('.remove-btn');
|
|
const generateButton = target.closest('.generate-btn');
|
|
|
|
if (removeButton && removeButton.closest(`.${ARRAY_ITEM_CLASS}`)) {
|
|
const arrayItem = removeButton.closest(`.${ARRAY_ITEM_CLASS}`);
|
|
const parentContainer = arrayItem.parentElement;
|
|
const isThinkingModelItem = arrayItem.hasAttribute('data-model-id') && parentContainer && parentContainer.id === 'THINKING_MODELS_container';
|
|
const isSafetySettingItem = arrayItem.classList.contains(SAFETY_SETTING_ITEM_CLASS);
|
|
|
|
if (isThinkingModelItem) {
|
|
const modelId = arrayItem.getAttribute('data-model-id');
|
|
const budgetMapItem = document.querySelector(`.${MAP_ITEM_CLASS}[data-model-id="${modelId}"]`);
|
|
if (budgetMapItem) {
|
|
budgetMapItem.remove();
|
|
}
|
|
// Check and add placeholder for budget map if empty
|
|
const budgetContainer = document.getElementById('THINKING_BUDGET_MAP_container');
|
|
if (budgetContainer && budgetContainer.children.length === 0) {
|
|
budgetContainer.innerHTML = '<div class="text-gray-500 text-sm italic">请在上方添加思考模型,预算将自动关联。</div>';
|
|
}
|
|
}
|
|
arrayItem.remove();
|
|
// Check and add placeholder for safety settings if empty
|
|
if (isSafetySettingItem && parentContainer && parentContainer.children.length === 0) {
|
|
parentContainer.innerHTML = '<div class="text-gray-500 text-sm italic">定义模型的安全过滤阈值。</div>';
|
|
}
|
|
} else if (generateButton && generateButton.closest(`.${ARRAY_ITEM_CLASS}`)) {
|
|
const inputField = generateButton.closest(`.${ARRAY_ITEM_CLASS}`).querySelector(`.${ARRAY_INPUT_CLASS}`);
|
|
if (inputField) {
|
|
const newToken = generateRandomToken();
|
|
inputField.value = newToken;
|
|
if (inputField.classList.contains(SENSITIVE_INPUT_CLASS)) {
|
|
const event = new Event('focusout', { bubbles: true, cancelable: true });
|
|
inputField.dispatchEvent(event);
|
|
}
|
|
showNotification('已生成新令牌', 'success');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// Add Safety Setting button
|
|
const addSafetySettingBtn = document.getElementById('addSafetySettingBtn');
|
|
if (addSafetySettingBtn) {
|
|
addSafetySettingBtn.addEventListener('click', () => addSafetySettingItem());
|
|
}
|
|
|
|
initializeSensitiveFields(); // Initialize sensitive field handling
|
|
}); // <-- DOMContentLoaded end
|
|
|
|
/**
|
|
* Initializes sensitive input field behavior (masking/unmasking).
|
|
*/
|
|
function initializeSensitiveFields() {
|
|
if (!configForm) return;
|
|
|
|
// Helper function: Mask field
|
|
function maskField(field) {
|
|
if (field.value && field.value !== MASKED_VALUE) {
|
|
field.setAttribute('data-real-value', field.value);
|
|
field.value = MASKED_VALUE;
|
|
} else if (!field.value) { // If field value is empty string
|
|
field.removeAttribute('data-real-value');
|
|
// Ensure empty value doesn't show as asterisks
|
|
if (field.value === MASKED_VALUE) field.value = '';
|
|
}
|
|
}
|
|
|
|
// Helper function: Unmask field
|
|
function unmaskField(field) {
|
|
if (field.hasAttribute('data-real-value')) {
|
|
field.value = field.getAttribute('data-real-value');
|
|
}
|
|
// If no data-real-value and value is MASKED_VALUE, it might be an initial empty sensitive field, clear it
|
|
else if (field.value === MASKED_VALUE && !field.hasAttribute('data-real-value')) {
|
|
field.value = '';
|
|
}
|
|
}
|
|
|
|
// Initial masking for existing sensitive fields on page load
|
|
// This function is called after populateForm and after dynamic element additions (via event delegation)
|
|
function initialMaskAllExisting() {
|
|
const sensitiveFields = configForm.querySelectorAll(`.${SENSITIVE_INPUT_CLASS}`);
|
|
sensitiveFields.forEach(field => {
|
|
if (field.type === 'password') {
|
|
// For password fields, browser handles it. We just ensure data-original-type is set
|
|
// and if it has a value, we also store data-real-value so it can be shown when switched to text
|
|
if (field.value) {
|
|
field.setAttribute('data-real-value', field.value);
|
|
}
|
|
// No need to set to MASKED_VALUE as browser handles it.
|
|
} else if (field.type === 'text' || field.tagName.toLowerCase() === 'textarea') {
|
|
maskField(field);
|
|
}
|
|
});
|
|
}
|
|
initialMaskAllExisting();
|
|
|
|
|
|
// Event delegation for dynamic and static fields
|
|
configForm.addEventListener('focusin', function(event) {
|
|
const target = event.target;
|
|
if (target.classList.contains(SENSITIVE_INPUT_CLASS)) {
|
|
if (target.type === 'password') {
|
|
// Record original type to switch back on blur
|
|
if (!target.hasAttribute('data-original-type')) {
|
|
target.setAttribute('data-original-type', 'password');
|
|
}
|
|
target.type = 'text'; // Switch to text type to show content
|
|
// If data-real-value exists (e.g., set during populateForm), use it
|
|
if (target.hasAttribute('data-real-value')) {
|
|
target.value = target.getAttribute('data-real-value');
|
|
}
|
|
// Otherwise, the browser's existing password value will be shown directly
|
|
} else { // For type="text" or textarea
|
|
unmaskField(target);
|
|
}
|
|
}
|
|
});
|
|
|
|
configForm.addEventListener('focusout', function(event) {
|
|
const target = event.target;
|
|
if (target.classList.contains(SENSITIVE_INPUT_CLASS)) {
|
|
// First, if the field is currently text and has a value, update data-real-value
|
|
if (target.type === 'text' || target.tagName.toLowerCase() === 'textarea') {
|
|
if (target.value && target.value !== MASKED_VALUE) {
|
|
target.setAttribute('data-real-value', target.value);
|
|
} else if (!target.value) { // If value is empty, remove data-real-value
|
|
target.removeAttribute('data-real-value');
|
|
}
|
|
}
|
|
|
|
// Then handle type switching and masking
|
|
if (target.getAttribute('data-original-type') === 'password' && target.type === 'text') {
|
|
target.type = 'password'; // Switch back to password type
|
|
// For password type, browser handles masking automatically, no need to set MASKED_VALUE manually
|
|
// data-real-value has already been updated by the logic above
|
|
} else if (target.type === 'text' || target.tagName.toLowerCase() === 'textarea') {
|
|
// For text or textarea sensitive fields, perform masking
|
|
maskField(target);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generates a UUID.
|
|
* @returns {string} A new UUID.
|
|
*/
|
|
function generateUUID() {
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
return v.toString(16);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initializes the configuration by fetching it from the server and populating the form.
|
|
*/
|
|
async function initConfig() {
|
|
try {
|
|
showNotification('正在加载配置...', 'info');
|
|
const response = await fetch('/api/config');
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const config = await response.json();
|
|
|
|
// 确保数组字段有默认值
|
|
if (!config.API_KEYS || !Array.isArray(config.API_KEYS) || config.API_KEYS.length === 0) {
|
|
config.API_KEYS = ['请在此处输入 API 密钥'];
|
|
}
|
|
|
|
if (!config.ALLOWED_TOKENS || !Array.isArray(config.ALLOWED_TOKENS) || config.ALLOWED_TOKENS.length === 0) {
|
|
config.ALLOWED_TOKENS = [''];
|
|
}
|
|
|
|
if (!config.IMAGE_MODELS || !Array.isArray(config.IMAGE_MODELS) || config.IMAGE_MODELS.length === 0) {
|
|
config.IMAGE_MODELS = ['gemini-1.5-pro-latest'];
|
|
}
|
|
|
|
if (!config.SEARCH_MODELS || !Array.isArray(config.SEARCH_MODELS) || config.SEARCH_MODELS.length === 0) {
|
|
config.SEARCH_MODELS = ['gemini-1.5-flash-latest'];
|
|
}
|
|
|
|
if (!config.FILTERED_MODELS || !Array.isArray(config.FILTERED_MODELS) || config.FILTERED_MODELS.length === 0) {
|
|
config.FILTERED_MODELS = ['gemini-1.0-pro-latest'];
|
|
}
|
|
// --- 新增:处理 PROXIES 默认值 ---
|
|
if (!config.PROXIES || !Array.isArray(config.PROXIES)) {
|
|
config.PROXIES = []; // 默认为空数组
|
|
}
|
|
// --- 新增:处理新字段的默认值 ---
|
|
if (!config.THINKING_MODELS || !Array.isArray(config.THINKING_MODELS)) {
|
|
config.THINKING_MODELS = []; // 默认为空数组
|
|
}
|
|
if (!config.THINKING_BUDGET_MAP || typeof config.THINKING_BUDGET_MAP !== 'object' || config.THINKING_BUDGET_MAP === null) {
|
|
config.THINKING_BUDGET_MAP = {}; // 默认为空对象
|
|
}
|
|
// --- 新增:处理 SAFETY_SETTINGS 默认值 ---
|
|
if (!config.SAFETY_SETTINGS || !Array.isArray(config.SAFETY_SETTINGS)) {
|
|
config.SAFETY_SETTINGS = []; // 默认为空数组
|
|
}
|
|
// --- 结束:处理 SAFETY_SETTINGS 默认值 ---
|
|
|
|
populateForm(config);
|
|
// After populateForm, initialize masking for all populated sensitive fields
|
|
if (configForm) { // Ensure form exists
|
|
initializeSensitiveFields(); // Call initializeSensitiveFields to handle initial masking
|
|
}
|
|
|
|
// Ensure upload provider has a default value
|
|
const uploadProvider = document.getElementById('UPLOAD_PROVIDER');
|
|
if (uploadProvider && !uploadProvider.value) {
|
|
uploadProvider.value = 'smms'; // 设置默认值为 smms
|
|
toggleProviderConfig('smms');
|
|
}
|
|
|
|
showNotification('配置加载成功', 'success');
|
|
} catch (error) {
|
|
console.error('加载配置失败:', error);
|
|
showNotification('加载配置失败: ' + error.message, 'error');
|
|
|
|
// 加载失败时,使用默认配置
|
|
const defaultConfig = {
|
|
API_KEYS: [''],
|
|
ALLOWED_TOKENS: [''],
|
|
IMAGE_MODELS: ['gemini-1.5-pro-latest'],
|
|
SEARCH_MODELS: ['gemini-1.5-flash-latest'],
|
|
FILTERED_MODELS: ['gemini-1.0-pro-latest'],
|
|
UPLOAD_PROVIDER: 'smms',
|
|
PROXIES: [], // 添加默认值
|
|
THINKING_MODELS: [],
|
|
THINKING_BUDGET_MAP: {}
|
|
};
|
|
|
|
populateForm(defaultConfig);
|
|
if (configForm) { // Ensure form exists
|
|
initializeSensitiveFields(); // Call initializeSensitiveFields to handle initial masking
|
|
}
|
|
toggleProviderConfig('smms');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Populates the configuration form with data.
|
|
* @param {object} config - The configuration object.
|
|
*/
|
|
function populateForm(config) {
|
|
const modelIdMap = {}; // modelName -> modelId
|
|
|
|
// 1. Clear existing dynamic content first
|
|
const arrayContainers = document.querySelectorAll('.array-container');
|
|
arrayContainers.forEach(container => {
|
|
container.innerHTML = ''; // Clear all array containers
|
|
});
|
|
const budgetMapContainer = document.getElementById('THINKING_BUDGET_MAP_container');
|
|
if (budgetMapContainer) {
|
|
budgetMapContainer.innerHTML = ''; // Clear budget map container
|
|
} else {
|
|
console.error("Critical: THINKING_BUDGET_MAP_container not found!");
|
|
return; // Cannot proceed
|
|
}
|
|
|
|
// 2. Populate THINKING_MODELS and build the map
|
|
if (Array.isArray(config.THINKING_MODELS)) {
|
|
const container = document.getElementById('THINKING_MODELS_container');
|
|
if (container) {
|
|
config.THINKING_MODELS.forEach(modelName => {
|
|
if (modelName && typeof modelName === 'string' && modelName.trim()) {
|
|
const trimmedModelName = modelName.trim();
|
|
const modelId = addArrayItemWithValue('THINKING_MODELS', trimmedModelName);
|
|
if (modelId) {
|
|
modelIdMap[trimmedModelName] = modelId;
|
|
} else {
|
|
console.warn(`Failed to get modelId for THINKING_MODEL: '${trimmedModelName}'`);
|
|
}
|
|
} else {
|
|
console.warn(`Invalid THINKING_MODEL entry found:`, modelName);
|
|
}
|
|
});
|
|
} else {
|
|
console.error("Critical: THINKING_MODELS_container not found!");
|
|
}
|
|
}
|
|
|
|
// 3. Populate THINKING_BUDGET_MAP using the map
|
|
let budgetItemsAdded = false;
|
|
if (config.THINKING_BUDGET_MAP && typeof config.THINKING_BUDGET_MAP === 'object') {
|
|
for (const [modelName, budgetValue] of Object.entries(config.THINKING_BUDGET_MAP)) {
|
|
if (modelName && typeof modelName === 'string') {
|
|
const trimmedModelName = modelName.trim();
|
|
const modelId = modelIdMap[trimmedModelName]; // Look up the ID
|
|
if (modelId) {
|
|
createAndAppendBudgetMapItem(trimmedModelName, budgetValue, modelId);
|
|
budgetItemsAdded = true;
|
|
} else {
|
|
console.warn(`Budget map: Could not find model ID for '${trimmedModelName}'. Skipping budget item.`);
|
|
}
|
|
} else {
|
|
console.warn(`Invalid key found in THINKING_BUDGET_MAP:`, modelName);
|
|
}
|
|
}
|
|
}
|
|
if (!budgetItemsAdded && budgetMapContainer) {
|
|
budgetMapContainer.innerHTML = '<div class="text-gray-500 text-sm italic">请在上方添加思考模型,预算将自动关联。</div>';
|
|
}
|
|
|
|
// 4. Populate other array fields (excluding THINKING_MODELS)
|
|
for (const [key, value] of Object.entries(config)) {
|
|
if (Array.isArray(value) && key !== 'THINKING_MODELS') {
|
|
const container = document.getElementById(`${key}_container`);
|
|
if (container) {
|
|
value.forEach(itemValue => {
|
|
if (typeof itemValue === 'string') {
|
|
addArrayItemWithValue(key, itemValue);
|
|
} else {
|
|
console.warn(`Invalid item found in array '${key}':`, itemValue);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// 5. Populate non-array/non-budget fields
|
|
for (const [key, value] of Object.entries(config)) {
|
|
if (!Array.isArray(value) && !(typeof value === 'object' && value !== null && key === 'THINKING_BUDGET_MAP')) {
|
|
const element = document.getElementById(key);
|
|
if (element) {
|
|
if (element.type === 'checkbox' && typeof value === 'boolean') {
|
|
element.checked = value;
|
|
} else if (element.type !== 'checkbox') {
|
|
if (key === 'LOG_LEVEL' && typeof value === 'string') {
|
|
element.value = value.toUpperCase();
|
|
} else {
|
|
element.value = (value !== null && value !== undefined) ? value : '';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 6. Initialize upload provider
|
|
const uploadProvider = document.getElementById('UPLOAD_PROVIDER');
|
|
if (uploadProvider) {
|
|
toggleProviderConfig(uploadProvider.value);
|
|
}
|
|
|
|
// Populate SAFETY_SETTINGS
|
|
let safetyItemsAdded = false;
|
|
if (safetySettingsContainer && Array.isArray(config.SAFETY_SETTINGS)) {
|
|
config.SAFETY_SETTINGS.forEach(setting => {
|
|
if (setting && typeof setting === 'object' && setting.category && setting.threshold) {
|
|
addSafetySettingItem(setting.category, setting.threshold);
|
|
safetyItemsAdded = true;
|
|
} else {
|
|
console.warn("Invalid safety setting item found:", setting);
|
|
}
|
|
});
|
|
}
|
|
if (safetySettingsContainer && !safetyItemsAdded) {
|
|
safetySettingsContainer.innerHTML = '<div class="text-gray-500 text-sm italic">定义模型的安全过滤阈值。</div>';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles the bulk addition of API keys from the modal input.
|
|
*/
|
|
function handleBulkAddApiKeys() {
|
|
const apiKeyContainer = document.getElementById('API_KEYS_container');
|
|
if (!apiKeyBulkInput || !apiKeyContainer || !apiKeyModal) return;
|
|
|
|
const bulkText = apiKeyBulkInput.value;
|
|
const extractedKeys = bulkText.match(API_KEY_REGEX) || [];
|
|
|
|
const currentKeyInputs = apiKeyContainer.querySelectorAll(`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`);
|
|
let currentKeys = Array.from(currentKeyInputs).map(input => {
|
|
return input.hasAttribute('data-real-value') ? input.getAttribute('data-real-value') : input.value;
|
|
}).filter(key => key && key.trim() !== '' && key !== MASKED_VALUE);
|
|
|
|
const combinedKeys = new Set([...currentKeys, ...extractedKeys]);
|
|
const uniqueKeys = Array.from(combinedKeys);
|
|
|
|
apiKeyContainer.innerHTML = ''; // Clear existing items more directly
|
|
|
|
uniqueKeys.forEach(key => {
|
|
addArrayItemWithValue('API_KEYS', key);
|
|
});
|
|
|
|
const newKeyInputs = apiKeyContainer.querySelectorAll(`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`);
|
|
newKeyInputs.forEach(input => {
|
|
if (configForm && typeof initializeSensitiveFields === 'function') {
|
|
const focusoutEvent = new Event('focusout', { bubbles: true, cancelable: true });
|
|
input.dispatchEvent(focusoutEvent);
|
|
}
|
|
});
|
|
|
|
closeModal(apiKeyModal);
|
|
showNotification(`添加/更新了 ${uniqueKeys.length} 个唯一密钥`, 'success');
|
|
}
|
|
|
|
/**
|
|
* Handles searching/filtering of API keys in the list.
|
|
*/
|
|
function handleApiKeySearch() {
|
|
const apiKeyContainer = document.getElementById('API_KEYS_container');
|
|
if (!apiKeySearchInput || !apiKeyContainer) return;
|
|
|
|
const searchTerm = apiKeySearchInput.value.toLowerCase();
|
|
const keyItems = apiKeyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`);
|
|
|
|
keyItems.forEach(item => {
|
|
const input = item.querySelector(`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`);
|
|
if (input) {
|
|
const realValue = input.hasAttribute('data-real-value') ? input.getAttribute('data-real-value').toLowerCase() : input.value.toLowerCase();
|
|
item.style.display = realValue.includes(searchTerm) ? 'flex' : 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handles the bulk deletion of API keys based on input from the modal.
|
|
*/
|
|
function handleBulkDeleteApiKeys() {
|
|
const apiKeyContainer = document.getElementById('API_KEYS_container');
|
|
if (!bulkDeleteApiKeyInput || !apiKeyContainer || !bulkDeleteApiKeyModal) return;
|
|
|
|
const bulkText = bulkDeleteApiKeyInput.value;
|
|
if (!bulkText.trim()) {
|
|
showNotification('请粘贴需要删除的 API 密钥', 'warning');
|
|
return;
|
|
}
|
|
|
|
const keysToDelete = new Set(bulkText.match(API_KEY_REGEX) || []);
|
|
|
|
if (keysToDelete.size === 0) {
|
|
showNotification('未在输入内容中提取到有效的 API 密钥格式', 'warning');
|
|
return;
|
|
}
|
|
|
|
const keyItems = apiKeyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`);
|
|
let deleteCount = 0;
|
|
|
|
keyItems.forEach(item => {
|
|
const input = item.querySelector(`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`);
|
|
const realValue = input && (input.hasAttribute('data-real-value') ? input.getAttribute('data-real-value') : input.value);
|
|
if (realValue && keysToDelete.has(realValue)) {
|
|
item.remove();
|
|
deleteCount++;
|
|
}
|
|
});
|
|
|
|
closeModal(bulkDeleteApiKeyModal);
|
|
|
|
if (deleteCount > 0) {
|
|
showNotification(`成功删除了 ${deleteCount} 个匹配的密钥`, 'success');
|
|
} else {
|
|
showNotification('列表中未找到您输入的任何密钥进行删除', 'info');
|
|
}
|
|
bulkDeleteApiKeyInput.value = '';
|
|
}
|
|
|
|
/**
|
|
* Handles the bulk addition of proxies from the modal input.
|
|
*/
|
|
function handleBulkAddProxies() {
|
|
const proxyContainer = document.getElementById('PROXIES_container');
|
|
if (!proxyBulkInput || !proxyContainer || !proxyModal) return;
|
|
|
|
const bulkText = proxyBulkInput.value;
|
|
const extractedProxies = bulkText.match(PROXY_REGEX) || [];
|
|
|
|
const currentProxyInputs = proxyContainer.querySelectorAll(`.${ARRAY_INPUT_CLASS}`);
|
|
const currentProxies = Array.from(currentProxyInputs).map(input => input.value).filter(proxy => proxy.trim() !== '');
|
|
|
|
const combinedProxies = new Set([...currentProxies, ...extractedProxies]);
|
|
const uniqueProxies = Array.from(combinedProxies);
|
|
|
|
proxyContainer.innerHTML = ''; // Clear existing items
|
|
|
|
uniqueProxies.forEach(proxy => {
|
|
addArrayItemWithValue('PROXIES', proxy);
|
|
});
|
|
|
|
closeModal(proxyModal);
|
|
showNotification(`添加/更新了 ${uniqueProxies.length} 个唯一代理`, 'success');
|
|
}
|
|
|
|
/**
|
|
* Handles the bulk deletion of proxies based on input from the modal.
|
|
*/
|
|
function handleBulkDeleteProxies() {
|
|
const proxyContainer = document.getElementById('PROXIES_container');
|
|
if (!bulkDeleteProxyInput || !proxyContainer || !bulkDeleteProxyModal) return;
|
|
|
|
const bulkText = bulkDeleteProxyInput.value;
|
|
if (!bulkText.trim()) {
|
|
showNotification('请粘贴需要删除的代理地址', 'warning');
|
|
return;
|
|
}
|
|
|
|
const proxiesToDelete = new Set(bulkText.match(PROXY_REGEX) || []);
|
|
|
|
if (proxiesToDelete.size === 0) {
|
|
showNotification('未在输入内容中提取到有效的代理地址格式', 'warning');
|
|
return;
|
|
}
|
|
|
|
const proxyItems = proxyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`);
|
|
let deleteCount = 0;
|
|
|
|
proxyItems.forEach(item => {
|
|
const input = item.querySelector(`.${ARRAY_INPUT_CLASS}`);
|
|
if (input && proxiesToDelete.has(input.value)) {
|
|
item.remove();
|
|
deleteCount++;
|
|
}
|
|
});
|
|
|
|
closeModal(bulkDeleteProxyModal);
|
|
|
|
if (deleteCount > 0) {
|
|
showNotification(`成功删除了 ${deleteCount} 个匹配的代理`, 'success');
|
|
} else {
|
|
showNotification('列表中未找到您输入的任何代理进行删除', 'info');
|
|
}
|
|
bulkDeleteProxyInput.value = '';
|
|
}
|
|
|
|
/**
|
|
* Switches the active configuration tab.
|
|
* @param {string} tabId - The ID of the tab to switch to.
|
|
*/
|
|
function switchTab(tabId) {
|
|
// 更新标签按钮状态
|
|
const tabButtons = document.querySelectorAll('.tab-btn');
|
|
tabButtons.forEach(button => {
|
|
if (button.getAttribute('data-tab') === tabId) {
|
|
// 激活状态:主色背景,白色文字,添加阴影
|
|
button.classList.remove('bg-white', 'bg-opacity-50', 'text-gray-700', 'hover:bg-opacity-70');
|
|
button.classList.add('bg-primary-600', 'text-white', 'shadow-md');
|
|
} else {
|
|
// 非激活状态:白色背景,灰色文字,无阴影
|
|
button.classList.remove('bg-primary-600', 'text-white', 'shadow-md');
|
|
button.classList.add('bg-white', 'bg-opacity-50', 'text-gray-700', 'hover:bg-opacity-70');
|
|
}
|
|
});
|
|
|
|
// 更新内容区域
|
|
const sections = document.querySelectorAll('.config-section');
|
|
sections.forEach(section => {
|
|
if (section.id === `${tabId}-section`) {
|
|
section.classList.add('active');
|
|
} else {
|
|
section.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Toggles the visibility of configuration sections for different upload providers.
|
|
* @param {string} provider - The selected upload provider.
|
|
*/
|
|
function toggleProviderConfig(provider) {
|
|
const providerConfigs = document.querySelectorAll('.provider-config');
|
|
providerConfigs.forEach(config => {
|
|
if (config.getAttribute('data-provider') === provider) {
|
|
config.classList.add('active');
|
|
} else {
|
|
config.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Creates and appends an input field for an array item.
|
|
* @param {string} key - The configuration key for the array.
|
|
* @param {string} value - The initial value for the input field.
|
|
* @param {boolean} isSensitive - Whether the input is for sensitive data.
|
|
* @param {string|null} modelId - Optional model ID for thinking models.
|
|
* @returns {HTMLInputElement} The created input element.
|
|
*/
|
|
function createArrayInput(key, value, isSensitive, modelId = null) {
|
|
const input = document.createElement('input');
|
|
input.type = 'text';
|
|
input.name = `${key}[]`; // Used for form submission if not handled by JS
|
|
input.value = value;
|
|
let inputClasses = `${ARRAY_INPUT_CLASS} flex-grow px-3 py-2 border-none rounded-l-md focus:outline-none`;
|
|
if (isSensitive) {
|
|
inputClasses += ` ${SENSITIVE_INPUT_CLASS}`;
|
|
}
|
|
input.className = inputClasses;
|
|
if (modelId) {
|
|
input.setAttribute('data-model-id', modelId);
|
|
input.placeholder = '思考模型名称';
|
|
}
|
|
return input;
|
|
}
|
|
|
|
/**
|
|
* Creates a generate token button for allowed tokens.
|
|
* @returns {HTMLButtonElement} The created button element.
|
|
*/
|
|
function createGenerateTokenButton() {
|
|
const generateBtn = document.createElement('button');
|
|
generateBtn.type = 'button';
|
|
generateBtn.className = 'generate-btn px-2 py-2 text-gray-500 hover:text-primary-600 focus:outline-none rounded-r-md bg-gray-100 hover:bg-gray-200 transition-colors';
|
|
generateBtn.innerHTML = '<i class="fas fa-dice"></i>';
|
|
generateBtn.title = '生成随机令牌';
|
|
// Event listener will be added via delegation in DOMContentLoaded
|
|
return generateBtn;
|
|
}
|
|
|
|
/**
|
|
* Creates a remove button for an array item.
|
|
* @returns {HTMLButtonElement} The created button element.
|
|
*/
|
|
function createRemoveButton() {
|
|
const removeBtn = document.createElement('button');
|
|
removeBtn.type = 'button';
|
|
removeBtn.className = 'remove-btn text-gray-400 hover:text-red-500 focus:outline-none transition-colors duration-150';
|
|
removeBtn.innerHTML = '<i class="fas fa-trash-alt"></i>';
|
|
removeBtn.title = '删除';
|
|
// Event listener will be added via delegation in DOMContentLoaded
|
|
return removeBtn;
|
|
}
|
|
|
|
|
|
/**
|
|
* Adds a new item to an array configuration section (e.g., API_KEYS, ALLOWED_TOKENS).
|
|
* This function is typically called by a "+" button.
|
|
* @param {string} key - The configuration key for the array (e.g., 'API_KEYS').
|
|
*/
|
|
function addArrayItem(key) {
|
|
const container = document.getElementById(`${key}_container`);
|
|
if (!container) return;
|
|
|
|
const newItemValue = ''; // New items start empty
|
|
const modelId = addArrayItemWithValue(key, newItemValue); // This adds the DOM element
|
|
|
|
if (key === 'THINKING_MODELS' && modelId) {
|
|
createAndAppendBudgetMapItem(newItemValue, 0, modelId); // Default budget 0
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Adds an array item with a specific value to the DOM.
|
|
* This is used both for initially populating the form and for adding new items.
|
|
* @param {string} key - The configuration key (e.g., 'API_KEYS', 'THINKING_MODELS').
|
|
* @param {string} value - The value for the array item.
|
|
* @returns {string|null} The generated modelId if it's a thinking model, otherwise null.
|
|
*/
|
|
function addArrayItemWithValue(key, value) {
|
|
const container = document.getElementById(`${key}_container`);
|
|
if (!container) return null;
|
|
|
|
const isThinkingModel = key === 'THINKING_MODELS';
|
|
const isAllowedToken = key === 'ALLOWED_TOKENS';
|
|
const isSensitive = key === 'API_KEYS' || isAllowedToken;
|
|
const modelId = isThinkingModel ? generateUUID() : null;
|
|
|
|
const arrayItem = document.createElement('div');
|
|
arrayItem.className = `${ARRAY_ITEM_CLASS} flex items-center mb-2 gap-2`;
|
|
if (isThinkingModel) {
|
|
arrayItem.setAttribute('data-model-id', modelId);
|
|
}
|
|
|
|
const inputWrapper = document.createElement('div');
|
|
inputWrapper.className = 'flex items-center flex-grow border border-gray-300 rounded-md focus-within:border-primary-500 focus-within:ring focus-within:ring-primary-200 focus-within:ring-opacity-50';
|
|
|
|
const input = createArrayInput(key, value, isSensitive, isThinkingModel ? modelId : null);
|
|
inputWrapper.appendChild(input);
|
|
|
|
if (isAllowedToken) {
|
|
const generateBtn = createGenerateTokenButton();
|
|
inputWrapper.appendChild(generateBtn);
|
|
} else {
|
|
// Ensure right-side rounding if no button is present
|
|
input.classList.add('rounded-r-md');
|
|
}
|
|
|
|
const removeBtn = createRemoveButton();
|
|
|
|
arrayItem.appendChild(inputWrapper);
|
|
arrayItem.appendChild(removeBtn);
|
|
container.appendChild(arrayItem);
|
|
|
|
// Initialize sensitive field if applicable
|
|
if (isSensitive && input.value) {
|
|
if (configForm && typeof initializeSensitiveFields === 'function') {
|
|
const focusoutEvent = new Event('focusout', { bubbles: true, cancelable: true });
|
|
input.dispatchEvent(focusoutEvent);
|
|
}
|
|
}
|
|
return isThinkingModel ? modelId : null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates and appends a DOM element for a thinking model's budget mapping.
|
|
* @param {string} mapKey - The model name (key for the map).
|
|
* @param {number|string} mapValue - The budget value.
|
|
* @param {string} modelId - The unique ID of the corresponding thinking model.
|
|
*/
|
|
function createAndAppendBudgetMapItem(mapKey, mapValue, modelId) {
|
|
const container = document.getElementById('THINKING_BUDGET_MAP_container');
|
|
if (!container) {
|
|
console.error("Cannot add budget item: THINKING_BUDGET_MAP_container not found!");
|
|
return;
|
|
}
|
|
|
|
// If container currently only has the placeholder, clear it
|
|
const placeholder = container.querySelector('.text-gray-500.italic');
|
|
// Check if the only child is the placeholder before clearing
|
|
if (placeholder && container.children.length === 1 && container.firstChild === placeholder) {
|
|
container.innerHTML = '';
|
|
}
|
|
|
|
const mapItem = document.createElement('div');
|
|
mapItem.className = `${MAP_ITEM_CLASS} flex items-center mb-2 gap-2`;
|
|
mapItem.setAttribute('data-model-id', modelId);
|
|
|
|
const keyInput = document.createElement('input');
|
|
keyInput.type = 'text';
|
|
keyInput.value = mapKey;
|
|
keyInput.placeholder = '模型名称 (自动关联)';
|
|
keyInput.readOnly = true;
|
|
keyInput.className = `${MAP_KEY_INPUT_CLASS} flex-grow px-3 py-2 border border-gray-300 rounded-md focus:outline-none bg-gray-100 text-gray-500`;
|
|
keyInput.setAttribute('data-model-id', modelId);
|
|
|
|
const valueInput = document.createElement('input');
|
|
valueInput.type = 'number';
|
|
const intValue = parseInt(mapValue, 10);
|
|
valueInput.value = isNaN(intValue) ? 0 : intValue;
|
|
valueInput.placeholder = '预算 (整数)';
|
|
valueInput.className = `${MAP_VALUE_INPUT_CLASS} w-24 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50`;
|
|
valueInput.min = 0;
|
|
valueInput.max = 24576;
|
|
valueInput.addEventListener('input', function() {
|
|
let val = this.value.replace(/[^0-9]/g, '');
|
|
if (val !== '') {
|
|
val = parseInt(val, 10);
|
|
if (val < 0) val = 0;
|
|
if (val > 24576) val = 24576;
|
|
}
|
|
this.value = val; // Corrected variable name
|
|
});
|
|
|
|
// Remove Button - Removed for budget map items
|
|
// const removeBtn = document.createElement('button');
|
|
// removeBtn.type = 'button';
|
|
// removeBtn.className = 'remove-btn text-gray-300 cursor-not-allowed focus:outline-none'; // Kept original class for reference
|
|
// removeBtn.innerHTML = '<i class="fas fa-trash-alt"></i>';
|
|
// removeBtn.title = '请从上方模型列表删除';
|
|
// removeBtn.disabled = true;
|
|
|
|
mapItem.appendChild(keyInput);
|
|
mapItem.appendChild(valueInput);
|
|
// mapItem.appendChild(removeBtn); // Do not append the remove button
|
|
|
|
container.appendChild(mapItem);
|
|
}
|
|
|
|
/**
|
|
* Collects all data from the configuration form.
|
|
* @returns {object} An object containing all configuration data.
|
|
*/
|
|
function collectFormData() {
|
|
const formData = {};
|
|
|
|
// 处理普通输入和 select
|
|
const inputsAndSelects = document.querySelectorAll('input[type="text"], input[type="number"], input[type="password"], select, textarea');
|
|
inputsAndSelects.forEach(element => {
|
|
if (element.name && !element.name.includes('[]') && !element.closest('.array-container') && !element.closest(`.${MAP_ITEM_CLASS}`) && !element.closest(`.${SAFETY_SETTING_ITEM_CLASS}`)) {
|
|
if (element.type === 'number') {
|
|
formData[element.name] = parseFloat(element.value);
|
|
} else if (element.classList.contains(SENSITIVE_INPUT_CLASS) && element.hasAttribute('data-real-value')) {
|
|
formData[element.name] = element.getAttribute('data-real-value');
|
|
} else {
|
|
formData[element.name] = element.value;
|
|
}
|
|
}
|
|
});
|
|
|
|
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
|
|
checkboxes.forEach(checkbox => {
|
|
formData[checkbox.name] = checkbox.checked;
|
|
});
|
|
|
|
const arrayContainers = document.querySelectorAll('.array-container');
|
|
arrayContainers.forEach(container => {
|
|
const key = container.id.replace('_container', '');
|
|
const arrayInputs = container.querySelectorAll(`.${ARRAY_INPUT_CLASS}`);
|
|
formData[key] = Array.from(arrayInputs).map(input => {
|
|
if (input.classList.contains(SENSITIVE_INPUT_CLASS) && input.hasAttribute('data-real-value')) {
|
|
return input.getAttribute('data-real-value');
|
|
}
|
|
return input.value;
|
|
}).filter(value => value && value.trim() !== '' && value !== MASKED_VALUE); // Ensure MASKED_VALUE is also filtered if not handled
|
|
});
|
|
|
|
const budgetMapContainer = document.getElementById('THINKING_BUDGET_MAP_container');
|
|
if (budgetMapContainer) {
|
|
formData['THINKING_BUDGET_MAP'] = {};
|
|
const mapItems = budgetMapContainer.querySelectorAll(`.${MAP_ITEM_CLASS}`);
|
|
mapItems.forEach(item => {
|
|
const keyInput = item.querySelector(`.${MAP_KEY_INPUT_CLASS}`);
|
|
const valueInput = item.querySelector(`.${MAP_VALUE_INPUT_CLASS}`);
|
|
if (keyInput && valueInput && keyInput.value.trim() !== '') {
|
|
const budgetValue = parseInt(valueInput.value, 10);
|
|
formData['THINKING_BUDGET_MAP'][keyInput.value.trim()] = isNaN(budgetValue) ? 0 : budgetValue;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (safetySettingsContainer) {
|
|
formData['SAFETY_SETTINGS'] = [];
|
|
const settingItems = safetySettingsContainer.querySelectorAll(`.${SAFETY_SETTING_ITEM_CLASS}`);
|
|
settingItems.forEach(item => {
|
|
const categorySelect = item.querySelector('.safety-category-select');
|
|
const thresholdSelect = item.querySelector('.safety-threshold-select');
|
|
if (categorySelect && thresholdSelect && categorySelect.value && thresholdSelect.value) {
|
|
formData['SAFETY_SETTINGS'].push({
|
|
category: categorySelect.value,
|
|
threshold: thresholdSelect.value
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
return formData;
|
|
}
|
|
|
|
/**
|
|
* Stops the scheduler task on the server.
|
|
*/
|
|
async function stopScheduler() {
|
|
try {
|
|
const response = await fetch('/api/scheduler/stop', { method: 'POST' });
|
|
if (!response.ok) {
|
|
console.warn(`停止定时任务失败: ${response.status}`);
|
|
} else {
|
|
console.log('定时任务已停止');
|
|
}
|
|
} catch (error) {
|
|
console.error('调用停止定时任务API时出错:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts the scheduler task on the server.
|
|
*/
|
|
async function startScheduler() {
|
|
try {
|
|
const response = await fetch('/api/scheduler/start', { method: 'POST' });
|
|
if (!response.ok) {
|
|
console.warn(`启动定时任务失败: ${response.status}`);
|
|
} else {
|
|
console.log('定时任务已启动');
|
|
}
|
|
} catch (error) {
|
|
console.error('调用启动定时任务API时出错:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Saves the current configuration to the server.
|
|
*/
|
|
async function saveConfig() {
|
|
try {
|
|
const formData = collectFormData();
|
|
|
|
showNotification('正在保存配置...', 'info');
|
|
|
|
// 1. 停止定时任务
|
|
await stopScheduler();
|
|
|
|
const response = await fetch('/api/config', {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(formData)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
// 移除居中的 saveStatus 提示
|
|
|
|
showNotification('配置保存成功', 'success');
|
|
|
|
// 3. 启动新的定时任务
|
|
await startScheduler();
|
|
|
|
} catch (error) {
|
|
console.error('保存配置失败:', error);
|
|
// 保存失败时,也尝试重启定时任务,以防万一
|
|
await startScheduler();
|
|
// 移除居中的 saveStatus 提示
|
|
|
|
showNotification('保存配置失败: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initiates the configuration reset process by showing a confirmation modal.
|
|
* @param {Event} [event] - The click event, if triggered by a button.
|
|
*/
|
|
function resetConfig(event) {
|
|
// 阻止事件冒泡和默认行为
|
|
if (event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
|
|
console.log('resetConfig called. Event target:', event ? event.target.id : 'No event');
|
|
|
|
// Ensure modal is shown only if the event comes from the reset button
|
|
if (!event || event.target.id === 'resetBtn' || (event.currentTarget && event.currentTarget.id === 'resetBtn')) {
|
|
if (resetConfirmModal) {
|
|
openModal(resetConfirmModal);
|
|
} else {
|
|
console.error("Reset confirmation modal not found! Falling back to default confirm.");
|
|
if (confirm('确定要重置所有配置吗?这将恢复到默认值。')) {
|
|
executeReset();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executes the actual configuration reset after confirmation.
|
|
*/
|
|
async function executeReset() {
|
|
try {
|
|
showNotification('正在重置配置...', 'info');
|
|
|
|
// 1. 停止定时任务
|
|
await stopScheduler();
|
|
const response = await fetch('/api/config/reset', { method: 'POST' });
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const config = await response.json();
|
|
populateForm(config);
|
|
// Re-initialize masking for sensitive fields after reset
|
|
if (configForm && typeof initializeSensitiveFields === 'function') {
|
|
const sensitiveFields = configForm.querySelectorAll(`.${SENSITIVE_INPUT_CLASS}`);
|
|
sensitiveFields.forEach(field => {
|
|
if (field.type === 'password') {
|
|
if (field.value) field.setAttribute('data-real-value', field.value);
|
|
} else if (field.type === 'text' || field.tagName.toLowerCase() === 'textarea') {
|
|
const focusoutEvent = new Event('focusout', { bubbles: true, cancelable: true });
|
|
field.dispatchEvent(focusoutEvent);
|
|
}
|
|
});
|
|
}
|
|
showNotification('配置已重置为默认值', 'success');
|
|
|
|
// 3. 启动新的定时任务
|
|
await startScheduler();
|
|
|
|
} catch (error) {
|
|
console.error('重置配置失败:', error);
|
|
showNotification('重置配置失败: ' + error.message, 'error');
|
|
// 重置失败时,也尝试重启定时任务
|
|
await startScheduler();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Displays a notification message to the user.
|
|
* @param {string} message - The message to display.
|
|
* @param {string} [type='info'] - The type of notification ('info', 'success', 'error', 'warning').
|
|
*/
|
|
function showNotification(message, type = 'info') {
|
|
const notification = document.getElementById('notification');
|
|
notification.textContent = message;
|
|
|
|
// 统一样式为黑色半透明,与 keys_status.js 保持一致
|
|
notification.classList.remove('bg-danger-500');
|
|
notification.classList.add('bg-black');
|
|
notification.style.backgroundColor = 'rgba(0,0,0,0.8)';
|
|
notification.style.color = '#fff';
|
|
|
|
// 应用过渡效果
|
|
notification.style.opacity = "1";
|
|
notification.style.transform = "translate(-50%, 0)";
|
|
|
|
// 设置自动消失
|
|
setTimeout(() => {
|
|
notification.style.opacity = "0";
|
|
notification.style.transform = "translate(-50%, 10px)";
|
|
}, 3000);
|
|
}
|
|
|
|
/**
|
|
* Refreshes the current page.
|
|
* @param {HTMLButtonElement} [button] - The button that triggered the refresh (to show loading state).
|
|
*/
|
|
function refreshPage(button) {
|
|
if (button) button.classList.add('loading');
|
|
location.reload();
|
|
}
|
|
|
|
/**
|
|
* Scrolls the page to the top.
|
|
*/
|
|
function scrollToTop() {
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}
|
|
|
|
/**
|
|
* Scrolls the page to the bottom.
|
|
*/
|
|
function scrollToBottom() {
|
|
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
|
}
|
|
|
|
/**
|
|
* Toggles the visibility of scroll-to-top/bottom buttons based on scroll position.
|
|
*/
|
|
function toggleScrollButtons() {
|
|
const scrollButtons = document.querySelector('.scroll-buttons');
|
|
if (scrollButtons) {
|
|
scrollButtons.style.display = (window.scrollY > 200) ? 'flex' : 'none';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates a random token string.
|
|
* @returns {string} A randomly generated token.
|
|
*/
|
|
function generateRandomToken() {
|
|
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_';
|
|
const length = 48;
|
|
let result = 'sk-';
|
|
for (let i = 0; i < length; i++) {
|
|
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Adds a new safety setting item to the DOM.
|
|
* @param {string} [category=''] - The initial category for the setting.
|
|
* @param {string} [threshold=''] - The initial threshold for the setting.
|
|
*/
|
|
function addSafetySettingItem(category = '', threshold = '') {
|
|
const container = document.getElementById('SAFETY_SETTINGS_container');
|
|
if (!container) {
|
|
console.error("Cannot add safety setting: SAFETY_SETTINGS_container not found!");
|
|
return;
|
|
}
|
|
|
|
// 如果容器当前只有占位符,则清除它
|
|
const placeholder = container.querySelector('.text-gray-500.italic');
|
|
if (placeholder && container.children.length === 1 && container.firstChild === placeholder) {
|
|
container.innerHTML = '';
|
|
}
|
|
|
|
const harmCategories = [
|
|
"HARM_CATEGORY_HARASSMENT",
|
|
"HARM_CATEGORY_HATE_SPEECH",
|
|
"HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
|
"HARM_CATEGORY_DANGEROUS_CONTENT",
|
|
"HARM_CATEGORY_CIVIC_INTEGRITY" // 根据需要添加或移除
|
|
];
|
|
const harmThresholds = [
|
|
"BLOCK_NONE",
|
|
"BLOCK_LOW_AND_ABOVE",
|
|
"BLOCK_MEDIUM_AND_ABOVE",
|
|
"BLOCK_ONLY_HIGH",
|
|
"OFF" // 根据 Google API 文档添加或移除
|
|
];
|
|
|
|
const settingItem = document.createElement('div');
|
|
settingItem.className = `${SAFETY_SETTING_ITEM_CLASS} flex items-center mb-2 gap-2`;
|
|
|
|
const categorySelect = document.createElement('select');
|
|
categorySelect.className = 'safety-category-select flex-grow px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 bg-white';
|
|
harmCategories.forEach(cat => {
|
|
const option = document.createElement('option');
|
|
option.value = cat;
|
|
option.textContent = cat.replace('HARM_CATEGORY_', '');
|
|
if (cat === category) option.selected = true;
|
|
categorySelect.appendChild(option);
|
|
});
|
|
|
|
const thresholdSelect = document.createElement('select');
|
|
thresholdSelect.className = 'safety-threshold-select w-48 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 bg-white';
|
|
harmThresholds.forEach(thr => {
|
|
const option = document.createElement('option');
|
|
option.value = thr;
|
|
option.textContent = thr.replace('BLOCK_', '').replace('_AND_ABOVE', '+');
|
|
if (thr === threshold) option.selected = true;
|
|
thresholdSelect.appendChild(option);
|
|
});
|
|
|
|
const removeBtn = document.createElement('button');
|
|
removeBtn.type = 'button';
|
|
removeBtn.className = 'remove-btn text-gray-400 hover:text-red-500 focus:outline-none transition-colors duration-150';
|
|
removeBtn.innerHTML = '<i class="fas fa-trash-alt"></i>';
|
|
removeBtn.title = '删除此设置';
|
|
// Event listener for removeBtn is now handled by event delegation in DOMContentLoaded
|
|
|
|
settingItem.appendChild(categorySelect);
|
|
settingItem.appendChild(thresholdSelect);
|
|
settingItem.appendChild(removeBtn);
|
|
|
|
container.appendChild(settingItem);
|
|
}
|