// [核心适配] 创建一个双向映射表,连接前端ID与后端字段 // 这是我们所有“翻译”工作的“密码本”。 const FIELD_MAP = { // ========================================================== // ========= I. 核心运行时配置 (映射到现有字段) ========= // ========================================================== 'MAX_FAILURES': 'blacklist_threshold', 'TIME_OUT': 'connect_timeout_seconds', 'MAX_RETRIES': 'max_retries', 'CHECK_INTERVAL_HOURS': 'health_check_interval_seconds', 'TEST_MODEL': 'key_check_endpoint', 'URL_NORMALIZATION_ENABLED': 'enable_smart_gateway', // [逻辑收束] 这两个独立的UI开关,都控制同一个后端数据 'AUTO_DELETE_ERROR_LOGS_DAYS': 'request_log_retention_days', 'AUTO_DELETE_REQUEST_LOGS_DAYS': 'request_log_retention_days', // ========================================================== // ========= II.新增字段的精确映射 ========= // ========================================================== // --- 全局API基础URL --- 'BASE_URL': 'default_upstream_url', // --- 登录安全配置 --- 'ENABLE_IP_BANNING': 'enable_ip_banning', 'MAX_LOGIN_ATTEMPTS': 'max_login_attempts', 'IP_BAN_DURATION_MINUTES': 'ip_ban_duration_minutes', // --- API配置 --- 'CUSTOM_HEADERS': 'custom_headers', 'PROXIES_USE_CONSISTENCY_HASH_BY_API_KEY': 'use_proxy_hash', // --- 模型配置 --- 'IMAGE_MODELS': 'image_models', 'SEARCH_MODELS': 'search_models', 'FILTERED_MODELS': 'filtered_models', 'TOOLS_CODE_EXECUTION_ENABLED': 'enable_code_executor', 'URL_CONTEXT_ENABLED': 'enable_url_context', 'URL_CONTEXT_MODELS': 'url_context_models', 'SHOW_SEARCH_LINK': 'show_search_link', 'SHOW_THINKING_PROCESS': 'show_thinking', 'THINKING_MODELS': 'thinking_models', 'THINKING_BUDGET_MAP': 'thinking_budget_map', 'SAFETY_SETTINGS': 'safety_settings', // --- 代理检查配置 --- 'ENABLE_PROXY_CHECK': 'enable_proxy_check', 'PROXY_CHECK_TIMEOUT_SECONDS': 'proxy_check_timeout_seconds', 'PROXY_CHECK_CONCURRENCY': 'proxy_check_concurrency', 'USE_PROXY_HASH': 'use_proxy_hash', // --- TTS配置 --- 'TTS_MODEL': 'tts_model', 'TTS_VOICE_NAME': 'tts_voice_name', 'TTS_SPEED': 'tts_speed', // --- 图像生成配置 --- 'PAID_KEY': 'paid_key', 'CREATE_IMAGE_MODEL': 'create_image_model', 'UPLOAD_PROVIDER': 'upload_provider', 'SMMS_SECRET_TOKEN': 'smms_secret_token', 'PICGO_API_KEY': 'picgo_api_key', 'CLOUDFLARE_IMGBED_URL': 'cloudflare_imgbed_url', 'CLOUDFLARE_IMGBED_AUTH_CODE': 'cloudflare_imgbed_auth_code', 'CLOUDFLARE_IMGBED_UPLOAD_FOLDER': 'cloudflare_imgbed_upload_folder', // --- 流式输出配置 --- 'STREAM_OPTIMIZER_ENABLED': 'enable_stream_optimizer', 'STREAM_MIN_DELAY': 'stream_min_delay', 'STREAM_MAX_DELAY': 'stream_max_delay', 'STREAM_SHORT_TEXT_THRESHOLD': 'stream_short_text_thresh', 'STREAM_LONG_TEXT_THRESHOLD': 'stream_long_text_thresh', 'STREAM_CHUNK_SIZE': 'stream_chunk_size', 'FAKE_STREAM_ENABLED': 'enable_fake_stream', 'FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS': 'fake_stream_interval', // --- 日志配置 --- 'LOG_LEVEL': 'log_level', 'AUTO_DELETE_ERROR_LOGS_ENABLED': 'auto_delete_error_logs_enabled', 'AUTO_DELETE_REQUEST_LOGS_ENABLED': 'auto_delete_request_logs_enabled', // --- 定时任务配置 --- 'TIMEZONE': 'timezone', // ========================================================== // ========= III. 幻影映射 (由独立API资源提供) ========= // ========================================================== // 备注: 以下字段的数据源不是/admin/settings, // 在未来的重构中,将由独立的API端点提供。 // 当前保留在此,是为了让JS的填充和收集逻辑暂时“兼容”。 //'PROXIES': 'phantom_proxies' }; // 全局变量,用于缓存从 /admin/tokens 响应中提取的所有可用分组 let ALL_AVAILABLE_GROUPS = []; // [核心适配] 创建一个反向映射表,用于数据提交 const REVERSE_FIELD_MAP = Object.fromEntries( Object.entries(FIELD_MAP).map(([key, value]) => [value, key]) ); // 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 CUSTOM_HEADER_ITEM_CLASS = "custom-header-item"; const CUSTOM_HEADER_KEY_INPUT_CLASS = "custom-header-key-input"; const CUSTOM_HEADER_VALUE_INPUT_CLASS = "custom-header-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 Express 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 // [核心新增] 令牌配置模态框的元素 const tokenSettingsModal = document.getElementById("tokenSettingsModal"); const closeTokenSettingsModalBtn = document.getElementById("closeTokenSettingsModalBtn"); const cancelTokenSettingsBtn = document.getElementById("cancelTokenSettingsBtn"); const confirmTokenSettingsBtn = document.getElementById("confirmTokenSettingsBtn"); // 模态框内部的交互元素 const tokenSettingsGenerateBtn = document.getElementById("tokenSettingsGenerateBtn"); const tokenSettingsStatusToggle = document.getElementById("tokenSettingsStatusToggle"); const tokenSettingsStatusText = document.getElementById("tokenSettingsStatusText"); const tokenSettingsSelectAllGroupsBtn = document.getElementById("tokenSettingsSelectAllGroupsBtn"); const tokenSettingsDeselectAllGroupsBtn = document.getElementById("tokenSettingsDeselectAllGroupsBtn"); // [核心新增] IP 封禁配置模态框的元素 const ipBanSettingsModal = document.getElementById('ipBanSettingsModal'); const closeIpBanSettingsModalBtn = document.getElementById('closeIpBanSettingsModalBtn'); const cancelIpBanSettingsBtn = document.getElementById('cancelIpBanSettingsBtn'); const confirmIpBanSettingsBtn = document.getElementById('confirmIpBanSettingsBtn'); const ipBanMaxAttemptsInput = document.getElementById('ipBanMaxAttemptsInput'); const ipBanDurationInput = document.getElementById('ipBanDurationInput'); // [核心新增] 代理高级配置模态框的元素 const proxySettingsModal = document.getElementById('proxySettingsModal'); const proxySettingsBtn = document.getElementById('proxySettingsBtn'); const closeProxySettingsModalBtn = document.getElementById('closeProxySettingsModalBtn'); const cancelProxySettingsBtn = document.getElementById('cancelProxySettingsBtn'); const confirmProxySettingsBtn = document.getElementById('confirmProxySettingsBtn'); // 模态框内的输入元素 const enableProxyCheckInput = document.getElementById('enableProxyCheckInput'); const useProxyHashInput = document.getElementById('useProxyHashInput'); const proxyCheckTimeoutInput = document.getElementById('proxyCheckTimeoutInput'); const proxyCheckConcurrencyInput = document.getElementById('proxyCheckConcurrencyInput'); // Vertex Express 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(); // ============================================================= // 启动独立的代理数据加载流程 // ============================================================= loadAndPopulateProxies(); // 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 ); // [核心新增] 为令牌配置模态框,绑定所有的事件监听器 if (closeTokenSettingsModalBtn) { closeTokenSettingsModalBtn.addEventListener("click", () => closeModal(tokenSettingsModal)); } if (cancelTokenSettingsBtn) { cancelTokenSettingsBtn.addEventListener("click", () => closeModal(tokenSettingsModal)); } if (confirmTokenSettingsBtn) { confirmTokenSettingsBtn.addEventListener("click", handleConfirmTokenSettings); } if (tokenSettingsGenerateBtn) { tokenSettingsGenerateBtn.addEventListener("click", () => { const input = document.getElementById('tokenSettingsTokenInput'); if (input) input.value = generateRandomToken(); }); } if (tokenSettingsStatusToggle) { tokenSettingsStatusToggle.addEventListener('change', () => { if (tokenSettingsStatusText) { tokenSettingsStatusText.textContent = tokenSettingsStatusToggle.checked ? 'Active' : 'Inactive'; } }); } if (tokenSettingsSelectAllGroupsBtn) { tokenSettingsSelectAllGroupsBtn.addEventListener('click', () => toggleAllGroups(true)); } if (tokenSettingsDeselectAllGroupsBtn) { tokenSettingsDeselectAllGroupsBtn.addEventListener('click', () => toggleAllGroups(false)); } // [核心新增] 为IP封禁配置模态框,绑定所有事件监听器 const ipBanSettingsBtn = document.getElementById('ipBanSettingsBtn'); if (ipBanSettingsBtn) { ipBanSettingsBtn.addEventListener('click', openIpBanSettings); } if (closeIpBanSettingsModalBtn) { closeIpBanSettingsModalBtn.addEventListener('click', () => closeModal(ipBanSettingsModal)); } if (cancelIpBanSettingsBtn) { cancelIpBanSettingsBtn.addEventListener('click', () => closeModal(ipBanSettingsModal)); } if (confirmIpBanSettingsBtn) { confirmIpBanSettingsBtn.addEventListener('click', handleConfirmIpBanSettings); } // [核心新增] 为代理高级配置模态框,绑定所有事件监听器 if (proxySettingsBtn) { proxySettingsBtn.addEventListener('click', openProxySettingsModal); } if (closeProxySettingsModalBtn) { closeProxySettingsModalBtn.addEventListener('click', () => closeModal(proxySettingsModal)); } if (cancelProxySettingsBtn) { cancelProxySettingsBtn.addEventListener('click', () => closeModal(proxySettingsModal)); } if (confirmProxySettingsBtn) { confirmProxySettingsBtn.addEventListener('click', handleConfirmProxySettings); } // 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"); // Proxy Check Elements and Events const checkAllProxiesBtn = document.getElementById("checkAllProxiesBtn"); const proxyCheckModal = document.getElementById("proxyCheckModal"); const closeProxyCheckModalBtn = document.getElementById("closeProxyCheckModalBtn"); const closeProxyCheckBtn = document.getElementById("closeProxyCheckBtn"); const retryFailedProxiesBtn = document.getElementById("retryFailedProxiesBtn"); if (addProxyBtn) { addProxyBtn.addEventListener("click", () => { openModal(proxyModal); if (proxyBulkInput) proxyBulkInput.value = ""; }); } if (checkAllProxiesBtn) { checkAllProxiesBtn.addEventListener("click", checkAllProxies); } if (closeProxyCheckModalBtn) { closeProxyCheckModalBtn.addEventListener("click", () => closeModal(proxyCheckModal)); } if (closeProxyCheckBtn) { closeProxyCheckBtn.addEventListener("click", () => closeModal(proxyCheckModal)); } if (retryFailedProxiesBtn) { retryFailedProxiesBtn.addEventListener("click", () => { // 重试失败的代理检测 checkAllProxies(); }); } 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, tokenSettingsModal, // 新增tokenSettingsModal ipBanSettingsModal, // 新增IP封禁配置模态框 proxySettingsModal, // 新增代理高级配置模态框 ]; 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"); const settingsButton = target.closest(".settings-btn"); // 新增settingsButton 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"); } } else if (settingsButton && settingsButton.closest(`.${ARRAY_ITEM_CLASS}`)) { // 如果识别到“设置”指令,就命令 openTokenSettings 函数,立即执行! openTokenSettings(settingsButton); // 新增openTokenSettings调用 } }); } // Add Safety Setting button const addSafetySettingBtn = document.getElementById("addSafetySettingBtn"); if (addSafetySettingBtn) { addSafetySettingBtn.addEventListener("click", () => addSafetySettingItem()); } // Add Custom Header button const addCustomHeaderBtn = document.getElementById("addCustomHeaderBtn"); if (addCustomHeaderBtn) { addCustomHeaderBtn.addEventListener("click", () => addCustomHeaderItem()); } initializeSensitiveFields(); // Initialize sensitive field handling // Vertex Express 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); }); } /** * [定稿] 页面初始化函数 * 作为页面加载的唯一入口,负责: * 1. 获取并填充系统配置。 * 2. 获取令牌数据,并调用 addArrayItemWithValue 进行渲染。 * 3. [新增] 从令牌数据中提取所有唯一的分组,并缓存到全局变量中。 */ async function initConfig() { // --- 第一部分: 加载系统运行时配置 --- try { showNotification("正在加载系统配置...", "info"); const settingsResponse = await apiFetch("/admin/settings"); const settingsResult = await settingsResponse.json(); populateForm(settingsResult.data); showNotification("系统配置加载成功", "success"); } catch (error) { console.error("加载系统配置失败:", error); showNotification("加载系统配置失败: " + error.message, "error"); } // --- 第二部分: 加载令牌,并执行分拣、渲染、缓存 --- try { const tokensResponse = await apiFetch("/admin/tokens"); const result = await tokensResponse.json(); const allTokensData = result.data; let adminToken = null; let userTokens = []; // [核心逻辑] 分拣管理员令牌和用户令牌 if (allTokensData && Array.isArray(allTokensData)) { allTokensData.forEach(token => { if (token.IsAdmin) { adminToken = token; } else { userTokens.push(token); } }); } // [任务 1] 填充独立的管理员 AUTH_TOKEN 输入框 if (adminToken) { const authTokenInput = document.getElementById("AUTH_TOKEN"); if (authTokenInput) { authTokenInput.value = adminToken.Token; // 触发敏感字段的遮蔽逻辑,保持UI一致性 authTokenInput.dispatchEvent(new Event('focusout', { bubbles: true, cancelable: true })); } } // [任务 2] 渲染过滤后的用户令牌列表 const container = document.getElementById('ALLOWED_TOKENS_container'); if (container) { container.innerHTML = ''; } if (userTokens.length > 0) { userTokens.forEach(token => { addArrayItemWithValue('ALLOWED_TOKENS', token); }); } else { if (container) { container.innerHTML = '
系统中暂无用户级认证令牌。
'; } } // [附加任务] 从所有令牌数据中提取、去重并缓存所有可用分组 if (allTokensData && Array.isArray(allTokensData)) { const groupMap = new Map(); allTokensData.forEach(token => { if (token.AllowedGroups && Array.isArray(token.AllowedGroups)) { token.AllowedGroups.forEach(group => { if (!groupMap.has(group.ID)) { groupMap.set(group.ID, group); } }); } }); ALL_AVAILABLE_GROUPS = Array.from(groupMap.values()); } } catch (error) { console.error("加载认证令牌失败:", error); showNotification("加载认证令牌失败: " + error.message, "error"); } } /** * [核心修正] 此函数现在是“总调度员” * 它负责获取数据,然后命令 addArrayItemWithValue 函数进行渲染。 * @param {Array} tokens - 从 /admin/tokens 获取的令牌对象数组 */ function populateAuthTokens(tokens) { const container = document.getElementById('ALLOWED_TOKENS_container'); if (!container) return; container.innerHTML = ''; // 清空“正在加载”的提示 if (!tokens || tokens.length === 0) { container.innerHTML = '
系统中暂无认证令牌。
'; return; } // [核心] 遍历所有token数据 tokens.forEach(token => { // 命令我们最终版的 addArrayItemWithValue 函数,使用完整的token对象去创建DOM addArrayItemWithValue('ALLOWED_TOKENS', token); }); } /** * Populates the configuration form with data. * @param {object} config - The configuration object. * 它将使用 FIELD_MAP 来查找正确的HTML元素ID,并完整复刻了原版的所有复杂逻辑。 * @param {object} config - 从后端 /admin/settings 获取的、使用 Go Struct 字段名的配置对象。 */ function populateForm(config) { if (!config) { console.error("PopulateForm called with null or undefined config."); return; } const modelIdMap = {}; // 用于关联 Thinking Model 和 Budget Map // 1. [适配] 清空所有动态内容容器 const arrayContainers = document.querySelectorAll(".array-container"); arrayContainers.forEach(container => { container.innerHTML = ""; }); // 单独处理特殊容器 ['THINKING_BUDGET_MAP_container', 'CUSTOM_HEADERS_container', 'SAFETY_SETTINGS_container'].forEach(id => { const el = document.getElementById(id); if (el) el.innerHTML = ""; }); // 2. [适配] 特殊处理:首先填充 THINKING_MODELS,并构建ID映射表 const thinkingModelsData = config.ThinkingModels || []; if (Array.isArray(thinkingModelsData)) { thinkingModelsData.forEach(modelName => { if (modelName && typeof modelName === 'string' && modelName.trim()) { const trimmedModelName = modelName.trim(); // 使用前端ID 'THINKING_MODELS' 来调用辅助函数 const modelId = addArrayItemWithValue('THINKING_MODELS', trimmedModelName); if (modelId) { modelIdMap[trimmedModelName] = modelId; } } }); } // 3. [适配] 特殊处理:使用映射表填充 THINKING_BUDGET_MAP const budgetMapContainer = document.getElementById('THINKING_BUDGET_MAP_container'); let budgetItemsAdded = false; const budgetMapData = config.ThinkingBudgetMap || {}; if (budgetMapContainer && typeof budgetMapData === 'object') { for (const [modelName, budgetValue] of Object.entries(budgetMapData)) { const modelId = modelIdMap[modelName.trim()]; // 使用映射表查找ID if (modelId) { createAndAppendBudgetMapItem(modelName.trim(), budgetValue, modelId); budgetItemsAdded = true; } } } if (budgetMapContainer && !budgetItemsAdded) { budgetMapContainer.innerHTML = '
请在上方添加思考模型,预算将自动关联。
'; } // 4. [适配] 特殊处理:填充 CUSTOM_HEADERS const customHeadersContainer = document.getElementById('CUSTOM_HEADERS_container'); let customHeadersAdded = false; const customHeadersData = config.CustomHeaders || {}; if (customHeadersContainer && typeof customHeadersData === 'object') { for (const [key, value] of Object.entries(customHeadersData)) { createAndAppendCustomHeaderItem(key, value); customHeadersAdded = true; } } if (customHeadersContainer && !customHeadersAdded) { customHeadersContainer.innerHTML = '
添加自定义请求头,例如 X-Api-Key: your-key
'; } // 5. [适配] 特殊处理:填充 SAFETY_SETTINGS const safetySettingsContainer = document.getElementById('SAFETY_SETTINGS_container'); let safetyItemsAdded = false; const safetySettingsData = config.SafetySettings || []; if (safetySettingsContainer && Array.isArray(safetySettingsData)) { safetySettingsData.forEach(setting => { if (setting && setting.category && setting.threshold) { addSafetySettingItem(setting.category, setting.threshold); safetyItemsAdded = true; } }); } if (safetySettingsContainer && !safetyItemsAdded) { safetySettingsContainer.innerHTML = '
定义模型的安全过滤阈值。
'; } // 6. [适配] 遍历后端数据,填充所有其余的字段 for (const backendKey in config) { if (Object.prototype.hasOwnProperty.call(config, backendKey)) { const value = config[backendKey]; const frontendId = REVERSE_FIELD_MAP[backendKey]; if (!frontendId) continue; // 如果在映射表中找不到,则跳过 // 跳过我们已经手动处理过的特殊字段 if (['ThinkingModels', 'ThinkingBudgetMap', 'CustomHeaders', 'SafetySettings'].includes(backendKey)) { continue; } const element = document.getElementById(frontendId); if (element) { // 处理简单字段 (input, select, checkbox) if (element.type === 'checkbox') { element.checked = !!value; } else if (backendKey === 'HealthCheckIntervalSeconds') { // [单位转换] 从秒转换到小时 element.value = value > 0 ? Math.round(value / 3600) : 1; } else { element.value = value !== null && value !== undefined ? value : ""; } } else if (Array.isArray(value)) { // 处理其余的数组字段 const container = document.getElementById(`${frontendId}_container`); if (container) { value.forEach(itemValue => { if (typeof itemValue === 'string') { addArrayItemWithValue(frontendId, itemValue); } }); } } } } // 7. [适配] 处理依赖UI逻辑(例如,根据checkbox禁用/启用其他控件) // 自动删除错误日志 const autoDeleteErrorEnabled = document.getElementById('AUTO_DELETE_ERROR_LOGS_ENABLED'); const autoDeleteErrorDays = document.getElementById('AUTO_DELETE_ERROR_LOGS_DAYS'); if (autoDeleteErrorEnabled && autoDeleteErrorDays) { autoDeleteErrorDays.disabled = !autoDeleteErrorEnabled.checked; autoDeleteErrorEnabled.addEventListener('change', () => { autoDeleteErrorDays.disabled = !autoDeleteErrorEnabled.checked; }); } // 自动删除请求日志 const autoDeleteRequestEnabled = document.getElementById('AUTO_DELETE_REQUEST_LOGS_ENABLED'); const autoDeleteRequestDays = document.getElementById('AUTO_DELETE_REQUEST_LOGS_DAYS'); if (autoDeleteRequestEnabled && autoDeleteRequestDays) { autoDeleteRequestDays.disabled = !autoDeleteRequestEnabled.checked; autoDeleteRequestEnabled.addEventListener('change', () => { autoDeleteRequestDays.disabled = !autoDeleteRequestEnabled.checked; }); } // 8. [适配] 初始化上传提供商和敏感字段的UI状态 const uploadProviderSelect = document.getElementById("UPLOAD_PROVIDER"); if (uploadProviderSelect) { toggleProviderConfig(uploadProviderSelect.value); } initializeSensitiveFields(); } /** * 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 Express 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 Express 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 Express API 密钥", "warning"); return; } const keysToDelete = new Set(bulkText.match(VERTEX_API_KEY_REGEX) || []); if (keysToDelete.size === 0) { showNotification( "未在输入内容中提取到有效的 Vertex Express 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; } /** * 打开令牌配置窗口,并用现有数据和可用分组填充它 * @param {HTMLElement} buttonElement - 被点击的“齿轮”按钮 */ let currentEditingTokenItem = null; /** */ async function openTokenSettings(buttonElement) { // 1. [定位] currentEditingTokenItem = buttonElement.closest('.array-item'); if (!currentEditingTokenItem) return; // 2. [数据恢复] const tokenInput = currentEditingTokenItem.querySelector('.array-input'); const descriptionInput = currentEditingTokenItem.querySelector('.description-input'); const currentToken = tokenInput.getAttribute('data-real-value') || tokenInput.value; const currentDescription = descriptionInput ? descriptionInput.value : currentEditingTokenItem.dataset.description || ''; const currentTag = currentEditingTokenItem.dataset.tag || ''; const currentStatus = currentEditingTokenItem.dataset.status !== 'inactive'; const currentGroupIds = JSON.parse(currentEditingTokenItem.dataset.groups || '[]'); // 3. [表单填充] document.getElementById('tokenSettingsTokenInput').value = currentToken; document.getElementById('tokenSettingsDescriptionInput').value = currentDescription; document.getElementById('tokenSettingsTagInput').value = currentTag; const statusToggle = document.getElementById('tokenSettingsStatusToggle'); statusToggle.checked = currentStatus; statusToggle.dispatchEvent(new Event('change')); // 4. [打开模态框] openModal(tokenSettingsModal); // 5. [异步加载分组]使用全局缓存的分组数据,同步渲染分组列表 const groupsContainer = document.getElementById('tokenSettingsGroupsContainer'); groupsContainer.innerHTML = ''; // 清空容器 if (ALL_AVAILABLE_GROUPS && ALL_AVAILABLE_GROUPS.length > 0) { ALL_AVAILABLE_GROUPS.forEach(group => { const isChecked = currentGroupIds.includes(group.ID); // 注意:此处group.id应为group.ID const wrapper = document.createElement('div'); const checkboxId = `group-checkbox-${group.ID}`; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = checkboxId; checkbox.className = 'hidden group-checkbox'; checkbox.value = group.ID; checkbox.checked = isChecked; const label = document.createElement('label'); label.htmlFor = checkboxId; label.textContent = group.DisplayName || group.Name; // 同样适配大小写 label.className = `cursor-pointer text-sm px-3 py-1 rounded-full transition-all ${isChecked ? 'bg-green-500 text-white' : 'bg-gray-200 text-gray-700'}`; if (isChecked) { label.innerHTML = ` ${label.textContent}`; } checkbox.addEventListener('change', () => { label.classList.toggle('bg-green-500', checkbox.checked); label.classList.toggle('text-white', checkbox.checked); label.classList.toggle('bg-gray-200', !checkbox.checked); label.classList.toggle('text-gray-700', !checkbox.checked); label.innerHTML = checkbox.checked ? ` ${group.DisplayName || group.Name}` : `${group.DisplayName || group.Name}`; }); wrapper.appendChild(checkbox); wrapper.appendChild(label); groupsContainer.appendChild(wrapper); }); } else { groupsContainer.innerHTML = '系统中暂无可用分组。'; } } /** * 当用户点击“确认”时 */ function handleConfirmTokenSettings() { if (!currentEditingTokenItem) return; // 1. [数据收集] const newToken = document.getElementById('tokenSettingsTokenInput').value; const newDescription = document.getElementById('tokenSettingsDescriptionInput').value; const newTag = document.getElementById('tokenSettingsTagInput').value; const newStatus = document.getElementById('tokenSettingsStatusToggle').checked ? 'active' : 'inactive'; const selectedGroups = []; document.querySelectorAll('#tokenSettingsGroupsContainer .group-checkbox:checked').forEach(cb => { selectedGroups.push(parseInt(cb.value, 10)); }); // 2. [数据持久化到DOM] const tokenInput = currentEditingTokenItem.querySelector('.array-input'); tokenInput.value = maskToken(newToken); tokenInput.setAttribute('data-real-value', newToken); currentEditingTokenItem.dataset.description = newDescription; currentEditingTokenItem.dataset.tag = newTag; currentEditingTokenItem.dataset.status = newStatus; currentEditingTokenItem.dataset.groups = JSON.stringify(selectedGroups); const descriptionInput = currentEditingTokenItem.querySelector('.description-input'); if (descriptionInput) { descriptionInput.value = newDescription; } // 3. [收尾] closeModal(tokenSettingsModal); showNotification("令牌配置已在本地更新,请在页面底部保存以生效。", "success"); currentEditingTokenItem = null; } /** * [灵魂-辅助] 处理“全选/全不选”按钮的逻辑 */ function toggleAllGroups(select) { document.querySelectorAll('#tokenSettingsGroupsContainer .group-checkbox').forEach(checkbox => { if (checkbox.checked !== select) { checkbox.checked = select; checkbox.dispatchEvent(new Event('change')); } }); } /** * [新增] 创建一个“配置”按钮 (齿轮图标) * @returns {HTMLButtonElement} */ function createSettingsButton() { const settingsBtn = document.createElement('button'); settingsBtn.type = 'button'; settingsBtn.className = 'settings-btn px-2 py-2 text-gray-400 hover:text-blue-500 focus:outline-none rounded-md hover:bg-gray-100 transition-colors'; settingsBtn.innerHTML = ''; settingsBtn.title = '配置此令牌'; return settingsBtn; } /** * Creates a proxy status icon for displaying proxy check status. * @returns {HTMLSpanElement} The status icon element. */ function createProxyStatusIcon() { const statusIcon = document.createElement("span"); statusIcon.className = "proxy-status-icon px-2 py-2 text-gray-400"; statusIcon.innerHTML = ''; statusIcon.setAttribute("data-status", "unknown"); return statusIcon; } /** * Creates a proxy check button for individual proxy checking. * @returns {HTMLButtonElement} The check button element. */ function createProxyCheckButton() { const checkBtn = document.createElement("button"); checkBtn.type = "button"; checkBtn.className = "proxy-check-btn px-2 py-2 text-blue-500 hover:text-blue-700 focus:outline-none transition-colors duration-150 rounded-r-md"; checkBtn.innerHTML = ''; checkBtn.title = "检测此代理"; // 添加点击事件监听器 checkBtn.addEventListener("click", function(e) { e.preventDefault(); e.stopPropagation(); const inputElement = this.closest('.flex').querySelector('.array-input'); if (inputElement && inputElement.value.trim()) { checkSingleProxy(inputElement.value.trim(), this); } else { showNotification("请先输入代理地址", "warning"); } }); return checkBtn; } // [辅助函数] function maskToken(token) { if (typeof token !== 'string' || token.length <= 4) return token; return `${token.substring(0, 2)}...${token.substring(token.length - 2)}`; } /** * [核心新增] 打开IP封禁配置模态框 * 从主表单读取当前值,并填充到模态框中 */ function openIpBanSettings() { // 从主表单的原始输入框中读取数据 const maxAttemptsEl = document.getElementById('MAX_LOGIN_ATTEMPTS'); const banDurationEl = document.getElementById('IP_BAN_DURATION_MINUTES'); if (ipBanMaxAttemptsInput && maxAttemptsEl) { ipBanMaxAttemptsInput.value = maxAttemptsEl.value; } if (ipBanDurationInput && banDurationEl) { ipBanDurationInput.value = banDurationEl.value; } openModal(ipBanSettingsModal); } /** * [核心新增] 处理IP封禁配置的确认操作 * 从模态框读取新值,并写回到主表单中 */ function handleConfirmIpBanSettings() { // 将模态框中的值,写回到主表单的原始输入框 const maxAttemptsEl = document.getElementById('MAX_LOGIN_ATTEMPTS'); const banDurationEl = document.getElementById('IP_BAN_DURATION_MINUTES'); if (maxAttemptsEl && ipBanMaxAttemptsInput) { maxAttemptsEl.value = ipBanMaxAttemptsInput.value; } if (banDurationEl && ipBanDurationInput) { banDurationEl.value = ipBanDurationInput.value; } showNotification("IP封禁规则已更新。", "info"); closeModal(ipBanSettingsModal); } /** * 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, -1, modelId); // Default budget -1 } } /** * @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 isProxy = key === "PROXIES"; const isSensitive = key === "API_KEYS" || isAllowedToken || isVertexApiKey; const modelId = isThinkingModel ? generateUUID() : null; // [数据对接] 当是令牌时,将传入的对象分解为具体的值 let tokenValue = value; let descriptionValue = ""; if (isAllowedToken && typeof value === 'object' && value !== null) { tokenValue = value.Token || ""; descriptionValue = value.Description || ""; } 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); } // [数据对接] 将令牌的完整数据附加到父元素上,这是后续编辑功能正常工作所必需的 if (isAllowedToken && typeof value === 'object' && value !== null) { arrayItem.dataset.id = value.ID || ''; arrayItem.dataset.description = value.Description || ''; arrayItem.dataset.tag = value.Tag || ''; arrayItem.dataset.status = value.Status || 'active'; arrayItem.dataset.groups = JSON.stringify((value.AllowedGroups || []).map(g => g.ID)); } const inputWrapper = document.createElement("div"); if (isAllowedToken) { // 设置宽度为 3/4 inputWrapper.className = "flex items-center w-3/4 flex-grow rounded-md focus-within:border-blue-500 focus-within:ring focus-within:ring-blue-500 focus-within:ring-opacity-50"; } else { // 保持所有其他数组类型的原始样式不变 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"; } inputWrapper.style.border = "1px solid rgba(0, 0, 0, 0.12)"; inputWrapper.style.backgroundColor = "transparent"; // createArrayInput 接收分解后的 tokenValue const input = createArrayInput( key, tokenValue, isSensitive, isThinkingModel ? modelId : null ); inputWrapper.appendChild(input); // --- 内部组件的装配逻辑,100%保持不变 --- if (isAllowedToken) { const generateBtn = createGenerateTokenButton(); inputWrapper.appendChild(generateBtn); } else if (isProxy) { const proxyStatusIcon = createProxyStatusIcon(); inputWrapper.appendChild(proxyStatusIcon); const proxyCheckBtn = createProxyCheckButton(); inputWrapper.appendChild(proxyCheckBtn); } else { input.classList.add("rounded-r-md"); } // --- 外部组件的创建,100%保持不变 --- const removeBtn = createRemoveButton(); let settingsBtn = null; if (isAllowedToken) { settingsBtn = createSettingsButton(); } // --- 最终的“总装配”流程 --- arrayItem.appendChild(inputWrapper); // 在令牌输入框后,插入1/4宽度的描述输入框 if (isAllowedToken) { const descriptionInput = document.createElement("input"); descriptionInput.type = "text"; // 添加 .description-input 类,以便 handleConfirmTokenSettings 可以找到并更新它 descriptionInput.className = "w-1/4 px-3 py-2 flex-grow rounded-md focus-within:border-blue-500 focus-within:ring focus-within:ring-blue-500 focus-within:ring-opacity-50 description-input"; descriptionInput.value = descriptionValue; // [添加事件监听器,实时更新dataset descriptionInput.addEventListener('input', (event) => { const parentArrayItem = event.target.closest('.array-item'); if (parentArrayItem) { parentArrayItem.dataset.description = event.target.value; } }); descriptionInput.placeholder = "令牌描述..."; //descriptionInput.readOnly = true; arrayItem.appendChild(descriptionInput); } if (settingsBtn) { arrayItem.appendChild(settingsBtn); } arrayItem.appendChild(removeBtn); container.appendChild(arrayItem); // --- 所有收尾逻辑,100%保持不变 --- 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) ? -1 : 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); } /** * Adds a new custom header item to the DOM. */ function addCustomHeaderItem() { createAndAppendCustomHeaderItem("", ""); } /** * Creates and appends a DOM element for a custom header. * @param {string} key - The header key. * @param {string} value - The header value. */ function createAndAppendCustomHeaderItem(key, value) { const container = document.getElementById("CUSTOM_HEADERS_container"); if (!container) { console.error( "Cannot add custom header: CUSTOM_HEADERS_container not found!" ); return; } const placeholder = container.querySelector(".text-gray-500.italic"); if ( placeholder && container.children.length === 1 && container.firstChild === placeholder ) { container.innerHTML = ""; } const headerItem = document.createElement("div"); headerItem.className = `${CUSTOM_HEADER_ITEM_CLASS} flex items-center mb-2 gap-2`; const keyInput = document.createElement("input"); keyInput.type = "text"; keyInput.value = key; keyInput.placeholder = "Header Name"; keyInput.className = `${CUSTOM_HEADER_KEY_INPUT_CLASS} flex-grow px-3 py-2 border border-gray-300 rounded-md focus:outline-none bg-gray-100 text-gray-500`; const valueInput = document.createElement("input"); valueInput.type = "text"; valueInput.value = value; valueInput.placeholder = "Header Value"; valueInput.className = `${CUSTOM_HEADER_VALUE_INPUT_CLASS} 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`; const removeBtn = createRemoveButton(); removeBtn.addEventListener("click", () => { headerItem.remove(); if (container.children.length === 0) { container.innerHTML = '
添加自定义请求头,例如 X-Api-Key: your-key
'; } }); headerItem.appendChild(keyInput); headerItem.appendChild(valueInput); headerItem.appendChild(removeBtn); container.appendChild(headerItem); } /** * [架构对齐版] 收集所有认证令牌的完整数据 (包括管理员令牌) * @returns {Array} 一个包含所有令牌完整信息的对象数组 */ function collectTokenData() { const tokens = []; const container = document.getElementById('ALLOWED_TOKENS_container'); // 1. [核心增强] 首先,收集并添加“管理员令牌” const authTokenInput = document.getElementById('AUTH_TOKEN'); if (authTokenInput) { tokens.push({ Token: authTokenInput.getAttribute('data-real-value') || authTokenInput.value, IsAdmin: true // [关键] 打上“admin”的标签 // 其他属性(描述、分组等)对于管理员令牌是固定的,前端无需提交 }); } // 2. 收集所有“用户级令牌” if (container) { const tokenItems = container.querySelectorAll('.array-item'); tokenItems.forEach(item => { const tokenInput = item.querySelector('.array-input'); const descriptionInput = item.querySelector('.description-input'); if (!tokenInput || !descriptionInput) return; // 为后端API组装一个干净的用户令牌对象 const tokenObject = { ID: parseInt(item.dataset.id, 10) || 0, Token: tokenInput.getAttribute('data-real-value') || tokenInput.value, Description: descriptionInput.value, Tag: item.dataset.tag || '', Status: item.dataset.status || 'active', IsAdmin: false, // 明确标记为用户级令牌 AllowedGroupIDs: JSON.parse(item.dataset.groups || '[]') }; tokens.push(tokenObject); }); } return tokens; } /** * [核心对接] 创建一个新的函数,专门用于根据后端数据对象渲染一个代理项UI * @param {object} proxy - 从 /admin/proxies API 获取的单个代理对象 */ function addProxyItem(proxy) { const container = document.getElementById('PROXIES_container'); if (!container || typeof proxy !== 'object' || !proxy.Protocol || !proxy.Address) return; const displayValue = `${proxy.Protocol}://${proxy.Address}`; const arrayItem = document.createElement("div"); arrayItem.className = `${ARRAY_ITEM_CLASS} flex items-center mb-2 gap-2`; // [数据对接] 将后端的 ID 和 Status 存储在 dataset 中 arrayItem.dataset.id = proxy.ID; arrayItem.dataset.status = proxy.Status; const inputWrapper = document.createElement("div"); inputWrapper.className = "flex items-center flex-grow rounded-md focus-within:border-blue-500"; inputWrapper.style.border = "1px solid rgba(0, 0, 0, 0.12)"; inputWrapper.style.backgroundColor = "transparent"; // 使用现有的辅助函数创建基础 input 元素 const input = createArrayInput('PROXIES', displayValue, false, null); inputWrapper.appendChild(input); // [数据对接] 创建图标,并根据后端数据设置初始状态 const proxyStatusIcon = createProxyStatusIcon(); updateProxyStatus(proxyStatusIcon, { is_available: proxy.Status === 'active', response_time: 'N/A', error_message: `后台状态: ${proxy.Status}` }); inputWrapper.appendChild(proxyStatusIcon); inputWrapper.appendChild(createProxyCheckButton()); arrayItem.appendChild(inputWrapper); arrayItem.appendChild(createRemoveButton()); container.appendChild(arrayItem); } /** * [核心对接 & 已校准] 异步从后端加载并填充代理服务器列表 */ async function loadAndPopulateProxies() { try { const response = await apiFetch('/admin/proxies'); if (!response.ok) { throw new Error(`Failed to fetch proxies: ${response.statusText}`); } const responseData = await response.json(); // [核心修正] 从这里开始,我们不再假设 responseData 就是数组本身 // 而是从 'data' 字段中,安全地提取我们需要的代理列表 const proxies = responseData.data || []; const proxyContainer = document.getElementById('PROXIES_container'); if (proxyContainer) { proxyContainer.innerHTML = ''; if (Array.isArray(proxies) && proxies.length > 0) { proxies.sort((a, b) => a.ID - b.ID); proxies.forEach(proxy => { addProxyItem(proxy); }); } else { } } } catch (error) { showNotification("加载代理列表时发生严重错误,请检查控制台。", "error"); } } /** * [核心对接] 从UI的代理容器中,收集所有代理的URL字符串 * @returns {Array} 一个包含所有代理URL的字符串数组 */ function collectProxyData() { const proxies = []; const container = document.getElementById('PROXIES_container'); if (container) { const proxyItems = container.querySelectorAll('.array-item .array-input'); proxyItems.forEach(input => { const proxyValue = input.value.trim(); if (proxyValue) { proxies.push(proxyValue); } }); } return proxies; } /** * [核心新增] 打开代理高级配置模态框 * 从主表单的隐藏“数据仓库”中读取当前值,并填充到模态框中 */ function openProxySettingsModal() { // 从隐藏输入框中读取数据 const enableCheckEl = document.getElementById('ENABLE_PROXY_CHECK'); const useHashEl = document.getElementById('USE_PROXY_HASH'); const timeoutEl = document.getElementById('PROXY_CHECK_TIMEOUT_SECONDS'); const concurrencyEl = document.getElementById('PROXY_CHECK_CONCURRENCY'); if (enableProxyCheckInput && enableCheckEl) { enableProxyCheckInput.checked = enableCheckEl.value === 'true'; } if (useProxyHashInput && useHashEl) { useProxyHashInput.checked = useHashEl.value === 'true'; } if (proxyCheckTimeoutInput && timeoutEl) { proxyCheckTimeoutInput.value = timeoutEl.value; } if (proxyCheckConcurrencyInput && concurrencyEl) { proxyCheckConcurrencyInput.value = concurrencyEl.value; } openModal(proxySettingsModal); } /** * [核心新增] 处理代理高级配置的确认操作 * 从模态框读取新值,并写回到主表单的隐藏“数据仓库”中 */ function handleConfirmProxySettings() { const enableCheckEl = document.getElementById('ENABLE_PROXY_CHECK'); const useHashEl = document.getElementById('USE_PROXY_HASH'); const timeoutEl = document.getElementById('PROXY_CHECK_TIMEOUT_SECONDS'); const concurrencyEl = document.getElementById('PROXY_CHECK_CONCURRENCY'); if (enableCheckEl && enableProxyCheckInput) { enableCheckEl.value = enableProxyCheckInput.checked; } if (useHashEl && useProxyHashInput) { useHashEl.value = useProxyHashInput.checked; } if (timeoutEl && proxyCheckTimeoutInput) { timeoutEl.value = proxyCheckTimeoutInput.value; } // 从模态框的输入框 (proxyCheckConcurrencyInput) 读取新值, if (concurrencyEl && proxyCheckConcurrencyInput) { concurrencyEl.value = proxyCheckConcurrencyInput.value; } showNotification("代理高级配置已更新。", "info"); closeModal(proxySettingsModal); } /** * [核心改造] 收集表单数据函数 (完整生产版) * 它严格遵循“后端优先”原则,使用 FIELD_MAP 进行翻译, * 并完整复刻了原版函数中所有处理复杂数据结构的精密逻辑。 * @returns {object} 一个键名为后端 Go Struct 字段名的配置对象。 */ /** * [最终架构版] 收集表单数据函数 * 该函数不再包含任何硬编码的字段名,而是通过读取元素的'data-type'属性, * 来智能地对数据进行类型转换,实现了真正的可扩展性。 * @returns {object} 一个类型完美的、发往后端的配置对象。 */ function collectFormData() { const formData = {}; for (const frontendId in FIELD_MAP) { if (!Object.prototype.hasOwnProperty.call(FIELD_MAP, frontendId)) continue; const backendKey = FIELD_MAP[frontendId]; const element = document.getElementById(frontendId); if (element) { let value; // ==================================================================== // [核心架构升级] // 我们不再硬编码任何字段名,而是建立一套通用的类型处理规则。 // ==================================================================== const dataType = element.dataset.type; // 读取 data-type 属性 if (element.type === 'checkbox') { // 规则1:可见的 checkbox,其值是 .checked value = element.checked; } else if (dataType === 'boolean') { // 规则2:任何带有 data-type="boolean" 的元素,其值从字符串转换 value = (element.value === 'true'); } else if (dataType === 'number' || element.type === 'number') { // 规则3:任何带有 data-type="number" 或 type="number" 的元素 const numValue = parseInt(element.value, 10); value = isNaN(numValue) ? 0 : numValue; } else { // 规则4:其他所有情况,作为字符串处理 if (element.classList.contains('sensitive-input') && element.hasAttribute('data-real-value')) { value = element.getAttribute('data-real-value'); } else { value = element.value; } } // 特殊单位转换(如果需要,可以保留) if (backendKey === 'health_check_interval_seconds') { value = (value || 1) * 3600; } formData[backendKey] = value; } } // B. [适配 & 移植] 单独处理复杂的、基于容器的字段 (数组、对象映射、对象数组) // 我们将严格复刻原版函数的逻辑,但使用FIELD_MAP中的键来进行赋值。 // B.1 处理所有常规数组 (e.g., ALLOWED_TOKENS, PROXIES, etc.) const arrayContainers = document.querySelectorAll(".array-container"); arrayContainers.forEach((container) => { const frontendId = container.id.replace("_container", ""); if (frontendId === 'ALLOWED_TOKENS') { return; // 'continue' to the next iteration } const backendKey = FIELD_MAP[frontendId]; if (backendKey) { const arrayInputs = container.querySelectorAll(`.${ARRAY_INPUT_CLASS}`); formData[backendKey] = 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); // [逻辑移植] 严格过滤空值和掩码 } }); // B.2 处理 THINKING_BUDGET_MAP (对象映射) const budgetMapContainer = document.getElementById("THINKING_BUDGET_MAP_container"); const budgetMapBackendKey = FIELD_MAP['THINKING_BUDGET_MAP']; if (budgetMapContainer && budgetMapBackendKey) { formData[budgetMapBackendKey] = {}; 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); // [逻辑移植] 对无效数字提供默认值 -1 formData[budgetMapBackendKey][keyInput.value.trim()] = isNaN(budgetValue) ? -1 : budgetValue; } }); } // B.3 处理 CUSTOM_HEADERS (对象映射) const customHeadersContainer = document.getElementById("CUSTOM_HEADERS_container"); const customHeadersBackendKey = FIELD_MAP['CUSTOM_HEADERS']; if (customHeadersContainer && customHeadersBackendKey) { formData[customHeadersBackendKey] = {}; const headerItems = customHeadersContainer.querySelectorAll(`.${CUSTOM_HEADER_ITEM_CLASS}`); headerItems.forEach((item) => { const keyInput = item.querySelector(`.${CUSTOM_HEADER_KEY_INPUT_CLASS}`); const valueInput = item.querySelector(`.${CUSTOM_HEADER_VALUE_INPUT_CLASS}`); if (keyInput && valueInput && keyInput.value.trim() !== "") { formData[customHeadersBackendKey][keyInput.value.trim()] = valueInput.value.trim(); } }); } // B.4 处理 SAFETY_SETTINGS (对象数组) const safetySettingsContainer = document.getElementById("SAFETY_SETTINGS_container"); const safetySettingsBackendKey = FIELD_MAP['SAFETY_SETTINGS']; if (safetySettingsContainer && safetySettingsBackendKey) { formData[safetySettingsBackendKey] = []; 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[safetySettingsBackendKey].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); } } /** * [核心重构] 保存配置总指挥函数 * 它将按顺序分别保存“系统配置”和“认证令牌” */ async function saveConfig() { console.log("[FINAL] Save process initiated with cache bypass."); showNotification("正在保存配置...", "info"); try { await stopScheduler(); const settingsData = collectFormData(); // =================================================================== // [核心调试代码] 在此处安装我们的“数据探针” // =================================================================== console.log("=============== 🔴 DEBUG START: Settings Payload 🔴 ==============="); console.log("整个即将发送的 settingsData 对象:"); // 使用 JSON.stringify 美化输出,方便查看对象结构 console.log(JSON.stringify(settingsData, null, 2)); console.log("\n--- 单独检查关键字段 ---"); // 检查失效的布尔值 console.log(`[布尔/失效] enable_proxy_check:`, settingsData['enable_proxy_check'], `(类型: ${typeof settingsData['enable_proxy_check']})`); console.log(`[布尔/失效] use_proxy_hash:`, settingsData['use_proxy_hash'], `(类型: ${typeof settingsData['use_proxy_hash']})`); // 检查有效的布尔值作为“对照组” console.log(`[布尔/有效] enable_ip_banning:`, settingsData['enable_ip_banning'], `(类型: ${typeof settingsData['enable_ip_banning']})`); // 检查失效的数字 console.log(`[数字/失效] proxy_check_concurrency:`, settingsData['proxy_check_concurrency'], `(类型: ${typeof settingsData['proxy_check_concurrency']})`); // 检查有效的数字作为“对照组” console.log(`[数字/有效] proxy_check_timeout_seconds:`, settingsData['proxy_check_timeout_seconds'], `(类型: ${typeof settingsData['proxy_check_timeout_seconds']})`); console.log("======================= 🔴 DEBUG END 🔴 ======================="); // =================================================================== // [调试结束] // =================================================================== const settingsResponse = await apiFetch("/admin/settings", { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(settingsData), noCache: true // [指令] 禁用缓存! }); if (!settingsResponse.ok) { const errorText = await settingsResponse.text(); throw new Error(`保存系统设置失败: ${errorText}`); } const tokenData = collectTokenData(); const tokensResponse = await apiFetch("/admin/tokens", { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(tokenData), noCache: true // [指令] 禁用缓存! }); if (!tokensResponse.ok) { const errorText = await tokensResponse.text(); throw new Error(`保存认证令牌失败: ${errorText}`); } // ============================================================= // [核心对接] 3. 同步代理服务器列表 // ============================================================= console.log("[SYNC] Collecting proxy data..."); const proxyData = collectProxyData(); console.log(`[SYNC] Found ${proxyData.length} proxies to sync.`); const proxySyncResponse = await apiFetch("/admin/proxies/sync", { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ proxies: proxyData }), // [数据对接] 按照后端API要求,封装成 { proxies: [...] } noCache: true }); if (!proxySyncResponse.ok) { const errorText = await proxySyncResponse.text(); throw new Error(`同步代理服务器失败: ${errorText}`); } const syncResult = await proxySyncResponse.json(); console.log("[SYNC] Proxy sync task started successfully.", syncResult); showNotification("配置保存成功!页面即将刷新...", "success", 2000); setTimeout(() => window.location.reload(), 2000); } catch (error) { console.error("CRITICAL FAILURE in saveConfig:", error); showNotification("保存配置失败: " + error.message, "error"); await startScheduler().catch(e => console.warn("Scheduler restart failed.", e)); } } /** * 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(); } } } } async function executeReset() { try { showNotification("正在重置配置...", "info"); // [核心移植] 1. 在写入操作前,停止定时任务。 await stopScheduler(); // [路由适配] 2. 向我们新的 /admin/settings/reset 接口发送 POST 请求。 const response = await apiFetch("/admin/settings/reset", { method: 'POST' }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || `HTTP error! status: ${response.status}`); } // [逻辑移植] 3. 重置成功后,用后端返回的默认配置,重新填充表单。 const result = await response.json(); populateForm(result.data); // 假设您的成功响应是 { success: true, data: {...} } showNotification("配置已重置为默认值", "success"); } catch (error) { console.error("重置配置失败:", error); showNotification("重置配置失败: " + error.message, "error"); } finally { // [核心移植] 4. 无论成功失败,都重启定时任务。 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, -1, modelId); } } if (modelHelperModal) closeModal(modelHelperModal); currentModelHelperTarget = null; // Reset target } // -- End Model Helper Functions -- // -- Proxy Check Functions -- /** * [最终对齐版] 检测单个代理是否可用 * 严格遵循 apiFetch 返回原始 Response 对象的合同 * @param {string} proxy - 代理地址 * @param {HTMLElement} buttonElement - 触发检测的按钮元素 */ async function checkSingleProxy(proxy, buttonElement) { const statusIcon = buttonElement.parentElement.querySelector('.proxy-status-icon'); const originalButtonContent = buttonElement.innerHTML; try { // 1. 进入“检测中”状态 buttonElement.innerHTML = ``; buttonElement.disabled = true; if (statusIcon) { statusIcon.className = "proxy-status-icon px-2 py-2 text-blue-500"; statusIcon.innerHTML = ``; statusIcon.setAttribute("data-status", "checking"); } // 2. `apiFetch` 返回原始的 Response 对象 const response = await apiFetch('/admin/proxies/check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ proxy: proxy }), noCache: true }); // 3. 动调用 .json() 来解析响应体 const responseData = await response.json(); // 4. 从解析后的对象中提取 .data 字段 const result = responseData.data; // ======================================================================= // 5. 最终验证 if (!result || typeof result.is_available === 'undefined') { throw new Error("API响应的数据结构不符合预期,缺少'data'字段或'is_available'属性。"); } // 6. 更新UI和通知 updateProxyStatus(statusIcon, result); if (result.is_available) { showNotification(`代理可用 (${result.response_time.toFixed(3)}s)`, "success"); } else { showNotification(`代理不可用: ${result.error_message}`, "error"); } } catch (error) { console.error('代理检测失败:', error); if (statusIcon) { statusIcon.className = "proxy-status-icon px-2 py-2 text-red-500"; statusIcon.innerHTML = ``; statusIcon.setAttribute("data-status", "error"); } const errorMessage = error.message || "未知错误"; showNotification(`检测失败: ${errorMessage}`, "error"); } finally { // 7. 恢复按钮状态 buttonElement.innerHTML = originalButtonContent; buttonElement.disabled = false; } } /** * 更新代理状态图标 * @param {HTMLElement} statusIcon - 状态图标元素 * @param {Object} result - 检测结果 */ function updateProxyStatus(statusIcon, result) { if (!statusIcon) return; if (result.is_available) { statusIcon.className = "proxy-status-icon px-2 py-2 text-green-500"; statusIcon.innerHTML = ``; statusIcon.setAttribute("data-status", "available"); } else { statusIcon.className = "proxy-status-icon px-2 py-2 text-red-500"; statusIcon.innerHTML = ``; statusIcon.setAttribute("data-status", "unavailable"); } } /** * 检测所有代理 */ async function checkAllProxies() { const proxyContainer = document.getElementById("PROXIES_container"); if (!proxyContainer) return; const proxyInputs = proxyContainer.querySelectorAll('.array-input'); const proxies = Array.from(proxyInputs) .map(input => input.value.trim()) .filter(proxy => proxy.length > 0); if (proxies.length === 0) { showNotification("没有代理需要检测", "warning"); return; } // 打开检测结果模态框 const proxyCheckModal = document.getElementById("proxyCheckModal"); if (proxyCheckModal) { openModal(proxyCheckModal); // 显示进度 const progressContainer = document.getElementById("proxyCheckProgress"); const summaryContainer = document.getElementById("proxyCheckSummary"); const resultsContainer = document.getElementById("proxyCheckResults"); if (progressContainer) progressContainer.classList.remove("hidden"); if (summaryContainer) summaryContainer.classList.add("hidden"); if (resultsContainer) resultsContainer.innerHTML = ""; // 更新总数 const totalCountElement = document.getElementById("totalCount"); if (totalCountElement) totalCountElement.textContent = proxies.length; try { const response = await apiFetch('/admin/proxies/check-all', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ proxies: proxies, noCache: true , max_concurrent: 5 }) }); if (!response.ok) { throw new Error(`批量检测请求失败: ${response.status}`); } const responseData = await response.json(); const results = responseData.data; // 直接使用返回的data字段作为结果数组 displayProxyCheckResults(results); updateProxyStatusInList(results); } catch (error) { console.error('批量代理检测失败:', error); showNotification(`批量检测失败: ${error.message}`, "error"); if (resultsContainer) { resultsContainer.innerHTML = `
检测失败: ${error.message}
`; } } finally { // 隐藏进度 if (progressContainer) progressContainer.classList.add("hidden"); } } } /** * 显示代理检测结果 * @param {Array} results - 检测结果数组 */ function displayProxyCheckResults(results) { const summaryContainer = document.getElementById("proxyCheckSummary"); const resultsContainer = document.getElementById("proxyCheckResults"); const availableCountElement = document.getElementById("availableCount"); const unavailableCountElement = document.getElementById("unavailableCount"); const retryButton = document.getElementById("retryFailedProxiesBtn"); if (!resultsContainer) return; // 统计结果 const availableCount = results.filter(r => r.is_available).length; const unavailableCount = results.length - availableCount; // 更新概览 if (availableCountElement) availableCountElement.textContent = availableCount; if (unavailableCountElement) unavailableCountElement.textContent = unavailableCount; if (summaryContainer) summaryContainer.classList.remove("hidden"); // 显示重试按钮(如果有失败的代理) if (retryButton) { if (unavailableCount > 0) { retryButton.classList.remove("hidden"); } else { retryButton.classList.add("hidden"); } } // 清空并填充结果 resultsContainer.innerHTML = ""; results.forEach(result => { const resultItem = document.createElement("div"); resultItem.className = `flex items-center justify-between p-3 border rounded-lg ${ result.is_available ? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50' }`; const statusIcon = result.is_available ? '' : ''; const responseTimeText = result.response_time ? ` (${result.response_time}s)` : ''; const errorText = result.error_message ? `${result.error_message}` : ''; resultItem.innerHTML = `
${statusIcon} ${result.proxy} ${responseTimeText}
${result.is_available ? '可用' : '不可用'} ${errorText}
`; resultsContainer.appendChild(resultItem); }); } /** * 根据检测结果更新代理列表中的状态图标 * @param {Array} results - 检测结果数组 */ function updateProxyStatusInList(results) { const proxyContainer = document.getElementById("PROXIES_container"); if (!proxyContainer) return; results.forEach(result => { const proxyInputs = proxyContainer.querySelectorAll('.array-input'); proxyInputs.forEach(input => { if (input.value.trim() === result.proxy) { const statusIcon = input.parentElement.querySelector('.proxy-status-icon'); updateProxyStatus(statusIcon, result); } }); }); } // -- End Proxy Check Functions --