// 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 VERTEX_API_KEY_REGEX = /AQ\.[a-zA-Z0-9_]{50}/g; // 新增 Vertex API Key 正则 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 // Vertex API Key Modal Elements const vertexApiKeyModal = document.getElementById("vertexApiKeyModal"); const vertexApiKeyBulkInput = document.getElementById("vertexApiKeyBulkInput"); const bulkDeleteVertexApiKeyModal = document.getElementById( "bulkDeleteVertexApiKeyModal" ); const bulkDeleteVertexApiKeyInput = document.getElementById( "bulkDeleteVertexApiKeyInput" ); // Model Helper Modal Elements const modelHelperModal = document.getElementById("modelHelperModal"); const modelHelperTitleElement = document.getElementById("modelHelperTitle"); const modelHelperSearchInput = document.getElementById( "modelHelperSearchInput" ); const modelHelperListContainer = document.getElementById( "modelHelperListContainer" ); const closeModelHelperModalBtn = document.getElementById( "closeModelHelperModalBtn" ); const cancelModelHelperBtn = document.getElementById("cancelModelHelperBtn"); let cachedModelsList = null; let currentModelHelperTarget = null; // { type: 'input'/'array', target: elementOrIdOrKey } // 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, vertexApiKeyModal, // 新增 bulkDeleteVertexApiKeyModal, // 新增 modelHelperModal, ]; 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 = '
请在上方添加思考模型,预算将自动关联。
'; } } arrayItem.remove(); // Check and add placeholder for safety settings if empty if ( isSafetySettingItem && parentContainer && parentContainer.children.length === 0 ) { parentContainer.innerHTML = '
定义模型的安全过滤阈值。
'; } } 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 // Vertex API Key Modal Elements and Events const addVertexApiKeyBtn = document.getElementById("addVertexApiKeyBtn"); const closeVertexApiKeyModalBtn = document.getElementById( "closeVertexApiKeyModalBtn" ); const cancelAddVertexApiKeyBtn = document.getElementById( "cancelAddVertexApiKeyBtn" ); const confirmAddVertexApiKeyBtn = document.getElementById( "confirmAddVertexApiKeyBtn" ); const bulkDeleteVertexApiKeyBtn = document.getElementById( "bulkDeleteVertexApiKeyBtn" ); const closeBulkDeleteVertexModalBtn = document.getElementById( "closeBulkDeleteVertexModalBtn" ); const cancelBulkDeleteVertexApiKeyBtn = document.getElementById( "cancelBulkDeleteVertexApiKeyBtn" ); const confirmBulkDeleteVertexApiKeyBtn = document.getElementById( "confirmBulkDeleteVertexApiKeyBtn" ); if (addVertexApiKeyBtn) { addVertexApiKeyBtn.addEventListener("click", () => { openModal(vertexApiKeyModal); if (vertexApiKeyBulkInput) vertexApiKeyBulkInput.value = ""; }); } if (closeVertexApiKeyModalBtn) closeVertexApiKeyModalBtn.addEventListener("click", () => closeModal(vertexApiKeyModal) ); if (cancelAddVertexApiKeyBtn) cancelAddVertexApiKeyBtn.addEventListener("click", () => closeModal(vertexApiKeyModal) ); if (confirmAddVertexApiKeyBtn) confirmAddVertexApiKeyBtn.addEventListener( "click", handleBulkAddVertexApiKeys ); if (bulkDeleteVertexApiKeyBtn) { bulkDeleteVertexApiKeyBtn.addEventListener("click", () => { openModal(bulkDeleteVertexApiKeyModal); if (bulkDeleteVertexApiKeyInput) bulkDeleteVertexApiKeyInput.value = ""; }); } if (closeBulkDeleteVertexModalBtn) closeBulkDeleteVertexModalBtn.addEventListener("click", () => closeModal(bulkDeleteVertexApiKeyModal) ); if (cancelBulkDeleteVertexApiKeyBtn) cancelBulkDeleteVertexApiKeyBtn.addEventListener("click", () => closeModal(bulkDeleteVertexApiKeyModal) ); if (confirmBulkDeleteVertexApiKeyBtn) confirmBulkDeleteVertexApiKeyBtn.addEventListener( "click", handleBulkDeleteVertexApiKeys ); // Model Helper Modal Event Listeners if (closeModelHelperModalBtn) { closeModelHelperModalBtn.addEventListener("click", () => closeModal(modelHelperModal) ); } if (cancelModelHelperBtn) { cancelModelHelperBtn.addEventListener("click", () => closeModal(modelHelperModal) ); } if (modelHelperSearchInput) { modelHelperSearchInput.addEventListener("input", () => renderModelsInModal() ); } // Add event listeners to all model helper trigger buttons const modelHelperTriggerBtns = document.querySelectorAll( ".model-helper-trigger-btn" ); modelHelperTriggerBtns.forEach((btn) => { btn.addEventListener("click", () => { const targetInputId = btn.dataset.targetInputId; const targetArrayKey = btn.dataset.targetArrayKey; if (targetInputId) { currentModelHelperTarget = { type: "input", target: document.getElementById(targetInputId), }; } else if (targetArrayKey) { currentModelHelperTarget = { type: "array", targetKey: targetArrayKey }; } openModelHelperModal(); }); }); }); // <-- 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"]; } // --- 新增:处理 VERTEX_API_KEYS 默认值 --- if (!config.VERTEX_API_KEYS || !Array.isArray(config.VERTEX_API_KEYS)) { config.VERTEX_API_KEYS = []; } // --- 新增:处理 VERTEX_EXPRESS_BASE_URL 默认值 --- if (typeof config.VERTEX_EXPRESS_BASE_URL === "undefined") { config.VERTEX_EXPRESS_BASE_URL = ""; } // --- 新增:处理 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 默认值 --- // --- 新增:处理自动删除错误日志配置的默认值 --- if (typeof config.AUTO_DELETE_ERROR_LOGS_ENABLED === "undefined") { config.AUTO_DELETE_ERROR_LOGS_ENABLED = false; } if (typeof config.AUTO_DELETE_ERROR_LOGS_DAYS === "undefined") { config.AUTO_DELETE_ERROR_LOGS_DAYS = 7; } // --- 结束:处理自动删除错误日志配置的默认值 --- // --- 新增:处理自动删除请求日志配置的默认值 --- if (typeof config.AUTO_DELETE_REQUEST_LOGS_ENABLED === "undefined") { config.AUTO_DELETE_REQUEST_LOGS_ENABLED = false; } if (typeof config.AUTO_DELETE_REQUEST_LOGS_DAYS === "undefined") { config.AUTO_DELETE_REQUEST_LOGS_DAYS = 30; } // --- 结束:处理自动删除请求日志配置的默认值 --- // --- 新增:处理假流式配置的默认值 --- if (typeof config.FAKE_STREAM_ENABLED === "undefined") { config.FAKE_STREAM_ENABLED = false; } if (typeof config.FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS === "undefined") { config.FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS = 5; } // --- 结束:处理假流式配置的默认值 --- 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: [], VERTEX_API_KEYS: [], // 确保默认值存在 VERTEX_EXPRESS_BASE_URL: "", // 确保默认值存在 THINKING_MODELS: [], THINKING_BUDGET_MAP: {}, AUTO_DELETE_ERROR_LOGS_ENABLED: false, AUTO_DELETE_ERROR_LOGS_DAYS: 7, // 新增默认值 AUTO_DELETE_REQUEST_LOGS_ENABLED: false, // 新增默认值 AUTO_DELETE_REQUEST_LOGS_DAYS: 30, // 新增默认值 // --- 新增:处理假流式配置的默认值 --- FAKE_STREAM_ENABLED: false, FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS: 5, // --- 结束:处理假流式配置的默认值 --- }; 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 = '
请在上方添加思考模型,预算将自动关联。
'; } // 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 = '
定义模型的安全过滤阈值。
'; } // --- 新增:处理自动删除错误日志的字段 --- const autoDeleteEnabledCheckbox = document.getElementById( "AUTO_DELETE_ERROR_LOGS_ENABLED" ); const autoDeleteDaysSelect = document.getElementById( "AUTO_DELETE_ERROR_LOGS_DAYS" ); if (autoDeleteEnabledCheckbox && autoDeleteDaysSelect) { autoDeleteEnabledCheckbox.checked = !!config.AUTO_DELETE_ERROR_LOGS_ENABLED; // 确保是布尔值 autoDeleteDaysSelect.value = config.AUTO_DELETE_ERROR_LOGS_DAYS || 7; // 默认7天 // 根据复选框状态设置下拉框的禁用状态 autoDeleteDaysSelect.disabled = !autoDeleteEnabledCheckbox.checked; // 添加事件监听器 autoDeleteEnabledCheckbox.addEventListener("change", function () { autoDeleteDaysSelect.disabled = !this.checked; }); } // --- 结束:处理自动删除错误日志的字段 --- // --- 新增:处理自动删除请求日志的字段 --- const autoDeleteRequestEnabledCheckbox = document.getElementById( "AUTO_DELETE_REQUEST_LOGS_ENABLED" ); const autoDeleteRequestDaysSelect = document.getElementById( "AUTO_DELETE_REQUEST_LOGS_DAYS" ); if (autoDeleteRequestEnabledCheckbox && autoDeleteRequestDaysSelect) { autoDeleteRequestEnabledCheckbox.checked = !!config.AUTO_DELETE_REQUEST_LOGS_ENABLED; autoDeleteRequestDaysSelect.value = config.AUTO_DELETE_REQUEST_LOGS_DAYS || 30; autoDeleteRequestDaysSelect.disabled = !autoDeleteRequestEnabledCheckbox.checked; autoDeleteRequestEnabledCheckbox.addEventListener("change", function () { autoDeleteRequestDaysSelect.disabled = !this.checked; }); } // --- 结束:处理自动删除请求日志的字段 --- // --- 新增:处理假流式配置的字段 --- const fakeStreamEnabledCheckbox = document.getElementById( "FAKE_STREAM_ENABLED" ); const fakeStreamIntervalInput = document.getElementById( "FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS" ); if (fakeStreamEnabledCheckbox && fakeStreamIntervalInput) { fakeStreamEnabledCheckbox.checked = !!config.FAKE_STREAM_ENABLED; fakeStreamIntervalInput.value = config.FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS || 5; // 根据复选框状态设置输入框的禁用状态 (如果需要) // fakeStreamIntervalInput.disabled = !fakeStreamEnabledCheckbox.checked; // fakeStreamEnabledCheckbox.addEventListener("change", function () { // fakeStreamIntervalInput.disabled = !this.checked; // }); } // --- 结束:处理假流式配置的字段 --- } /** * 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 = ""; } /** * Handles the bulk addition of Vertex API keys from the modal input. */ function handleBulkAddVertexApiKeys() { const vertexApiKeyContainer = document.getElementById( "VERTEX_API_KEYS_container" ); if ( !vertexApiKeyBulkInput || !vertexApiKeyContainer || !vertexApiKeyModal ) { return; } const bulkText = vertexApiKeyBulkInput.value; const extractedKeys = bulkText.match(VERTEX_API_KEY_REGEX) || []; const currentKeyInputs = vertexApiKeyContainer.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); vertexApiKeyContainer.innerHTML = ""; // Clear existing items uniqueKeys.forEach((key) => { addArrayItemWithValue("VERTEX_API_KEYS", key); // VERTEX_API_KEYS are sensitive }); // Ensure new sensitive inputs are masked const newKeyInputs = vertexApiKeyContainer.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(vertexApiKeyModal); showNotification( `添加/更新了 ${uniqueKeys.length} 个唯一 Vertex 密钥`, "success" ); vertexApiKeyBulkInput.value = ""; } /** * Handles the bulk deletion of Vertex API keys based on input from the modal. */ function handleBulkDeleteVertexApiKeys() { const vertexApiKeyContainer = document.getElementById( "VERTEX_API_KEYS_container" ); if ( !bulkDeleteVertexApiKeyInput || !vertexApiKeyContainer || !bulkDeleteVertexApiKeyModal ) { return; } const bulkText = bulkDeleteVertexApiKeyInput.value; if (!bulkText.trim()) { showNotification("请粘贴需要删除的 Vertex API 密钥", "warning"); return; } const keysToDelete = new Set(bulkText.match(VERTEX_API_KEY_REGEX) || []); if (keysToDelete.size === 0) { showNotification( "未在输入内容中提取到有效的 Vertex API 密钥格式", "warning" ); return; } const keyItems = vertexApiKeyContainer.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(bulkDeleteVertexApiKeyModal); if (deleteCount > 0) { showNotification(`成功删除了 ${deleteCount} 个匹配的 Vertex 密钥`, "success"); } else { showNotification("列表中未找到您输入的任何 Vertex 密钥进行删除", "info"); } bulkDeleteVertexApiKeyInput.value = ""; } /** * Switches the active configuration tab. * @param {string} tabId - The ID of the tab to switch to. */ function switchTab(tabId) { console.log(`Switching to tab: ${tabId}`); // 定义选中态和未选中态的样式 const activeStyle = "background-color: #3b82f6 !important; color: #ffffff !important; border: 2px solid #2563eb !important; box-shadow: 0 4px 12px -2px rgba(59, 130, 246, 0.4), 0 2px 6px -1px rgba(59, 130, 246, 0.2) !important; transform: translateY(-2px) !important; font-weight: 600 !important;"; const inactiveStyle = "background-color: #f8fafc !important; color: #64748b !important; border: 2px solid #e2e8f0 !important; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1) !important; font-weight: 500 !important; transform: none !important;"; // 更新标签按钮状态 const tabButtons = document.querySelectorAll(".tab-btn"); console.log(`Found ${tabButtons.length} tab buttons`); tabButtons.forEach((button) => { const buttonTabId = button.getAttribute("data-tab"); if (buttonTabId === tabId) { // 激活状态:直接设置内联样式 button.classList.add("active"); button.setAttribute("style", activeStyle); console.log(`Applied active style to button: ${buttonTabId}`); } else { // 非激活状态:直接设置内联样式 button.classList.remove("active"); button.setAttribute("style", inactiveStyle); console.log(`Applied inactive style to button: ${buttonTabId}`); } }); // 更新内容区域 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 form-input-themed`; 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 = ''; 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 = ''; 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 isVertexApiKey = key === "VERTEX_API_KEYS"; // 新增判断 const isSensitive = key === "API_KEYS" || isAllowedToken || isVertexApiKey; // 更新敏感判断 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 rounded-md focus-within:border-blue-500 focus-within:ring focus-within:ring-blue-500 focus-within:ring-opacity-50"; // Apply light theme border directly via style inputWrapper.style.border = "1px solid rgba(0, 0, 0, 0.12)"; inputWrapper.style.backgroundColor = "transparent"; // Ensure wrapper is transparent 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 = -1; valueInput.max = 32767; valueInput.addEventListener("input", function () { let val = this.value.replace(/[^0-9-]/g, ""); if (val !== "") { val = parseInt(val, 10); if (val < -1) val = -1; if (val > 32767) val = 32767; } 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 = ''; // 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, }); } }); } // --- 新增:收集自动删除错误日志的配置 --- const autoDeleteEnabledCheckbox = document.getElementById( "AUTO_DELETE_ERROR_LOGS_ENABLED" ); if (autoDeleteEnabledCheckbox) { formData["AUTO_DELETE_ERROR_LOGS_ENABLED"] = autoDeleteEnabledCheckbox.checked; } const autoDeleteDaysSelect = document.getElementById( "AUTO_DELETE_ERROR_LOGS_DAYS" ); if (autoDeleteDaysSelect) { // 如果复选框未选中,则不应提交天数,或者可以提交一个默认/无效值, // 但后端应该只在 ENABLED 为 true 时才关心 DAYS。 // 这里我们总是收集它,后端逻辑会处理。 formData["AUTO_DELETE_ERROR_LOGS_DAYS"] = parseInt( autoDeleteDaysSelect.value, 10 ); } // --- 结束:收集自动删除错误日志的配置 --- // --- 新增:收集自动删除请求日志的配置 --- const autoDeleteRequestEnabledCheckbox = document.getElementById( "AUTO_DELETE_REQUEST_LOGS_ENABLED" ); if (autoDeleteRequestEnabledCheckbox) { formData["AUTO_DELETE_REQUEST_LOGS_ENABLED"] = autoDeleteRequestEnabledCheckbox.checked; } const autoDeleteRequestDaysSelect = document.getElementById( "AUTO_DELETE_REQUEST_LOGS_DAYS" ); if (autoDeleteRequestDaysSelect) { formData["AUTO_DELETE_REQUEST_LOGS_DAYS"] = parseInt( autoDeleteRequestDaysSelect.value, 10 ); } // --- 结束:收集自动删除请求日志的配置 --- // --- 新增:收集假流式配置 --- const fakeStreamEnabledCheckbox = document.getElementById( "FAKE_STREAM_ENABLED" ); if (fakeStreamEnabledCheckbox) { formData["FAKE_STREAM_ENABLED"] = fakeStreamEnabledCheckbox.checked; } const fakeStreamIntervalInput = document.getElementById( "FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS" ); if (fakeStreamIntervalInput) { formData["FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS"] = parseInt( fakeStreamIntervalInput.value, 10 ); } // --- 结束:收集假流式配置 --- 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 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 form-select-themed"; 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 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 form-select-themed"; 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 = ''; 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); } // --- Model Helper Functions --- async function fetchModels() { if (cachedModelsList) { return cachedModelsList; } try { showNotification("正在从 /api/config/ui/models 加载模型列表...", "info"); const response = await fetch("/api/config/ui/models"); if (!response.ok) { const errorData = await response.text(); throw new Error(`HTTP error ${response.status}: ${errorData}`); } const responseData = await response.json(); // Changed variable name to responseData // The backend returns an object like: { object: "list", data: [{id: "m1"}, {id: "m2"}], success: true } if ( responseData && responseData.success && Array.isArray(responseData.data) ) { cachedModelsList = responseData.data; // Use responseData.data showNotification("模型列表加载成功", "success"); return cachedModelsList; } else { console.error("Invalid model list format received:", responseData); throw new Error("模型列表格式无效或请求未成功"); } } catch (error) { console.error("加载模型列表失败:", error); showNotification(`加载模型列表失败: ${error.message}`, "error"); cachedModelsList = []; // Avoid repeated fetches on error for this session, or set to null to retry return []; } } function renderModelsInModal() { if (!modelHelperListContainer) return; if (!cachedModelsList) { modelHelperListContainer.innerHTML = '

模型列表尚未加载。

'; return; } const searchTerm = modelHelperSearchInput.value.toLowerCase(); const filteredModels = cachedModelsList.filter((model) => model.id.toLowerCase().includes(searchTerm) ); modelHelperListContainer.innerHTML = ""; // Clear previous items if (filteredModels.length === 0) { modelHelperListContainer.innerHTML = '

未找到匹配的模型。

'; return; } filteredModels.forEach((model) => { const modelItemElement = document.createElement("button"); modelItemElement.type = "button"; modelItemElement.textContent = model.id; modelItemElement.className = "block w-full text-left px-4 py-2 rounded-md hover:bg-blue-100 focus:bg-blue-100 focus:outline-none transition-colors text-gray-700 hover:text-gray-800"; // Add any other classes for styling, e.g., from existing modals or array items modelItemElement.addEventListener("click", () => handleModelSelection(model.id) ); modelHelperListContainer.appendChild(modelItemElement); }); } async function openModelHelperModal() { if (!currentModelHelperTarget) { console.error("Model helper target not set."); showNotification("无法打开模型助手:目标未设置", "error"); return; } await fetchModels(); // Ensure models are loaded renderModelsInModal(); // Render them (handles empty/error cases internally) if (modelHelperTitleElement) { if ( currentModelHelperTarget.type === "input" && currentModelHelperTarget.target ) { const label = document.querySelector( `label[for="${currentModelHelperTarget.target.id}"]` ); modelHelperTitleElement.textContent = label ? `为 "${label.textContent.trim()}" 选择模型` : "选择模型"; } else if (currentModelHelperTarget.type === "array") { modelHelperTitleElement.textContent = `为 ${currentModelHelperTarget.targetKey} 添加模型`; } else { modelHelperTitleElement.textContent = "选择模型"; } } if (modelHelperSearchInput) modelHelperSearchInput.value = ""; // Clear search on open if (modelHelperModal) openModal(modelHelperModal); } function handleModelSelection(selectedModelId) { if (!currentModelHelperTarget) return; if ( currentModelHelperTarget.type === "input" && currentModelHelperTarget.target ) { const inputElement = currentModelHelperTarget.target; inputElement.value = selectedModelId; // If the input is a sensitive field, dispatch focusout to trigger masking behavior if needed if (inputElement.classList.contains(SENSITIVE_INPUT_CLASS)) { const event = new Event("focusout", { bubbles: true, cancelable: true }); inputElement.dispatchEvent(event); } // Dispatch input event for any other listeners inputElement.dispatchEvent(new Event("input", { bubbles: true })); } else if ( currentModelHelperTarget.type === "array" && currentModelHelperTarget.targetKey ) { const modelId = addArrayItemWithValue( currentModelHelperTarget.targetKey, selectedModelId ); if (currentModelHelperTarget.targetKey === "THINKING_MODELS" && modelId) { // Automatically add corresponding budget map item with default budget 0 createAndAppendBudgetMapItem(selectedModelId, 0, modelId); } } if (modelHelperModal) closeModal(modelHelperModal); currentModelHelperTarget = null; // Reset target } // -- End Model Helper Functions --