Files
gemini-balance/app/static/js/config_editor.js
snaily 8ec1d16e9d refactor: 优化 JS 结构、API 调用和密钥管理
此次提交引入了重要的重构和改进:

- 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 密钥状态管理的用户界面。
2025-05-07 13:58:05 +08:00

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);
}