Files
gemini-banlancer/web/static/js/settings.js
2025-11-20 12:24:05 +08:00

3056 lines
115 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// [核心适配] 创建一个双向映射表连接前端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 =
'<div class="text-gray-500 text-sm italic">请在上方添加思考模型,预算将自动关联。</div>';
}
}
arrayItem.remove();
// Check and add placeholder for safety settings if empty
if (
isSafetySettingItem &&
parentContainer &&
parentContainer.children.length === 0
) {
parentContainer.innerHTML =
'<div class="text-gray-500 text-sm italic">定义模型的安全过滤阈值。</div>';
}
} else if (
generateButton &&
generateButton.closest(`.${ARRAY_ITEM_CLASS}`)
) {
const inputField = generateButton
.closest(`.${ARRAY_ITEM_CLASS}`)
.querySelector(`.${ARRAY_INPUT_CLASS}`);
if (inputField) {
const newToken = generateRandomToken();
inputField.value = newToken;
if (inputField.classList.contains(SENSITIVE_INPUT_CLASS)) {
const event = new Event("focusout", {
bubbles: true,
cancelable: true,
});
inputField.dispatchEvent(event);
}
showNotification("已生成新令牌", "success");
}
}
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 = '<div class="text-gray-500 p-2 text-center italic">系统中暂无用户级认证令牌。</div>';
}
}
// [附加任务] 从所有令牌数据中提取、去重并缓存所有可用分组
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<object>} 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 = '<div class="text-gray-500 p-2">系统中暂无认证令牌。</div>';
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 = '<div class="text-gray-500 text-sm italic">请在上方添加思考模型,预算将自动关联。</div>';
}
// 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 = '<div class="text-gray-500 text-sm italic">添加自定义请求头,例如 X-Api-Key: your-key</div>';
}
// 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 = '<div class="text-gray-500 text-sm italic">定义模型的安全过滤阈值。</div>';
}
// 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 = '<i class="fas fa-dice"></i>';
generateBtn.title = "生成随机令牌";
// Event listener will be added via delegation in DOMContentLoaded
return generateBtn;
}
/**
* Creates a remove button for an array item.
* @returns {HTMLButtonElement} The created button element.
*/
function createRemoveButton() {
const removeBtn = document.createElement("button");
removeBtn.type = "button";
removeBtn.className =
"remove-btn text-gray-400 hover:text-red-500 focus:outline-none transition-colors duration-150";
removeBtn.innerHTML = '<i class="fas fa-trash-alt"></i>';
removeBtn.title = "删除";
// Event listener will be added via delegation in DOMContentLoaded
return removeBtn;
}
/**
* 打开令牌配置窗口,并用现有数据和可用分组填充它
* @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 = `<i class="fas fa-check-circle mr-1"></i> ${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 ? `<i class="fas fa-check-circle mr-1"></i> ${group.DisplayName || group.Name}` : `${group.DisplayName || group.Name}`;
});
wrapper.appendChild(checkbox);
wrapper.appendChild(label);
groupsContainer.appendChild(wrapper);
});
} else {
groupsContainer.innerHTML = '<span class="text-gray-500 text-sm">系统中暂无可用分组。</span>';
}
}
/**
* 当用户点击“确认”时
*/
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 = '<i class="fas fa-cog"></i>';
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 = '<i class="fas fa-question-circle" title="未检测"></i>';
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 = '<i class="fas fa-globe"></i>';
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 = '<i class="fas fa-trash-alt"></i>';
// removeBtn.title = '请从上方模型列表删除';
// removeBtn.disabled = true;
mapItem.appendChild(keyInput);
mapItem.appendChild(valueInput);
// mapItem.appendChild(removeBtn); // Do not append the remove button
container.appendChild(mapItem);
}
/**
* 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 =
'<div class="text-gray-500 text-sm italic">添加自定义请求头,例如 X-Api-Key: your-key</div>';
}
});
headerItem.appendChild(keyInput);
headerItem.appendChild(valueInput);
headerItem.appendChild(removeBtn);
container.appendChild(headerItem);
}
/**
* [架构对齐版] 收集所有认证令牌的完整数据 (包括管理员令牌)
* @returns {Array<object>} 一个包含所有令牌完整信息的对象数组
*/
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<string>} 一个包含所有代理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 = '<i class="fas fa-trash-alt"></i>';
removeBtn.title = "删除此设置";
// Event listener for removeBtn is now handled by event delegation in DOMContentLoaded
settingItem.appendChild(categorySelect);
settingItem.appendChild(thresholdSelect);
settingItem.appendChild(removeBtn);
container.appendChild(settingItem);
}
// --- 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 =
'<p class="text-gray-400 text-sm italic">模型列表尚未加载。</p>';
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 =
'<p class="text-gray-400 text-sm italic">未找到匹配的模型。</p>';
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 = `<i class="fa fa-spinner fa-spin"></i>`;
buttonElement.disabled = true;
if (statusIcon) {
statusIcon.className = "proxy-status-icon px-2 py-2 text-blue-500";
statusIcon.innerHTML = `<i class="fa fa-spinner fa-spin" title="检测中..."></i>`;
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 = `<i class="fa fa-times-circle" title="检测失败: ${error.message}"></i>`;
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 = `<i class="fas fa-check-circle" title="可用 (${result.response_time}s)"></i>`;
statusIcon.setAttribute("data-status", "available");
} else {
statusIcon.className = "proxy-status-icon px-2 py-2 text-red-500";
statusIcon.innerHTML = `<i class="fas fa-times-circle" title="不可用: ${result.error_message}"></i>`;
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 = `<div class="text-red-500 text-center py-4">检测失败: ${error.message}</div>`;
}
} 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 ?
'<i class="fas fa-check-circle text-green-500"></i>' :
'<i class="fas fa-times-circle text-red-500"></i>';
const responseTimeText = result.response_time ?
` (${result.response_time}s)` : '';
const errorText = result.error_message ?
`<span class="text-red-600 text-sm ml-2">${result.error_message}</span>` : '';
resultItem.innerHTML = `
<div class="flex items-center gap-3">
${statusIcon}
<span class="font-mono text-sm">${result.proxy}</span>
${responseTimeText}
</div>
<div class="flex items-center">
<span class="text-sm ${result.is_available ? 'text-green-700' : 'text-red-700'}">
${result.is_available ? '可用' : '不可用'}
</span>
${errorText}
</div>
`;
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 --