// [核心适配] 创建一个双向映射表,连接前端ID与后端字段
// 这是我们所有“翻译”工作的“密码本”。
const FIELD_MAP = {
// ==========================================================
// ========= I. 核心运行时配置 (映射到现有字段) =========
// ==========================================================
'MAX_FAILURES': 'blacklist_threshold',
'TIME_OUT': 'connect_timeout_seconds',
'MAX_RETRIES': 'max_retries',
'CHECK_INTERVAL_HOURS': 'health_check_interval_seconds',
'TEST_MODEL': 'key_check_endpoint',
'URL_NORMALIZATION_ENABLED': 'enable_smart_gateway',
// [逻辑收束] 这两个独立的UI开关,都控制同一个后端数据
'AUTO_DELETE_ERROR_LOGS_DAYS': 'request_log_retention_days',
'AUTO_DELETE_REQUEST_LOGS_DAYS': 'request_log_retention_days',
// ==========================================================
// ========= II.新增字段的精确映射 =========
// ==========================================================
// --- 全局API基础URL ---
'BASE_URL': 'default_upstream_url',
// --- 登录安全配置 ---
'ENABLE_IP_BANNING': 'enable_ip_banning',
'MAX_LOGIN_ATTEMPTS': 'max_login_attempts',
'IP_BAN_DURATION_MINUTES': 'ip_ban_duration_minutes',
// --- API配置 ---
'CUSTOM_HEADERS': 'custom_headers',
'PROXIES_USE_CONSISTENCY_HASH_BY_API_KEY': 'use_proxy_hash',
// --- 模型配置 ---
'IMAGE_MODELS': 'image_models',
'SEARCH_MODELS': 'search_models',
'FILTERED_MODELS': 'filtered_models',
'TOOLS_CODE_EXECUTION_ENABLED': 'enable_code_executor',
'URL_CONTEXT_ENABLED': 'enable_url_context',
'URL_CONTEXT_MODELS': 'url_context_models',
'SHOW_SEARCH_LINK': 'show_search_link',
'SHOW_THINKING_PROCESS': 'show_thinking',
'THINKING_MODELS': 'thinking_models',
'THINKING_BUDGET_MAP': 'thinking_budget_map',
'SAFETY_SETTINGS': 'safety_settings',
// --- 代理检查配置 ---
'ENABLE_PROXY_CHECK': 'enable_proxy_check',
'PROXY_CHECK_TIMEOUT_SECONDS': 'proxy_check_timeout_seconds',
'PROXY_CHECK_CONCURRENCY': 'proxy_check_concurrency',
'USE_PROXY_HASH': 'use_proxy_hash',
// --- TTS配置 ---
'TTS_MODEL': 'tts_model',
'TTS_VOICE_NAME': 'tts_voice_name',
'TTS_SPEED': 'tts_speed',
// --- 图像生成配置 ---
'PAID_KEY': 'paid_key',
'CREATE_IMAGE_MODEL': 'create_image_model',
'UPLOAD_PROVIDER': 'upload_provider',
'SMMS_SECRET_TOKEN': 'smms_secret_token',
'PICGO_API_KEY': 'picgo_api_key',
'CLOUDFLARE_IMGBED_URL': 'cloudflare_imgbed_url',
'CLOUDFLARE_IMGBED_AUTH_CODE': 'cloudflare_imgbed_auth_code',
'CLOUDFLARE_IMGBED_UPLOAD_FOLDER': 'cloudflare_imgbed_upload_folder',
// --- 流式输出配置 ---
'STREAM_OPTIMIZER_ENABLED': 'enable_stream_optimizer',
'STREAM_MIN_DELAY': 'stream_min_delay',
'STREAM_MAX_DELAY': 'stream_max_delay',
'STREAM_SHORT_TEXT_THRESHOLD': 'stream_short_text_thresh',
'STREAM_LONG_TEXT_THRESHOLD': 'stream_long_text_thresh',
'STREAM_CHUNK_SIZE': 'stream_chunk_size',
'FAKE_STREAM_ENABLED': 'enable_fake_stream',
'FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS': 'fake_stream_interval',
// --- 日志配置 ---
'LOG_LEVEL': 'log_level',
'AUTO_DELETE_ERROR_LOGS_ENABLED': 'auto_delete_error_logs_enabled',
'AUTO_DELETE_REQUEST_LOGS_ENABLED': 'auto_delete_request_logs_enabled',
// --- 定时任务配置 ---
'TIMEZONE': 'timezone',
// ==========================================================
// ========= III. 幻影映射 (由独立API资源提供) =========
// ==========================================================
// 备注: 以下字段的数据源不是/admin/settings,
// 在未来的重构中,将由独立的API端点提供。
// 当前保留在此,是为了让JS的填充和收集逻辑暂时“兼容”。
//'PROXIES': 'phantom_proxies'
};
// 全局变量,用于缓存从 /admin/tokens 响应中提取的所有可用分组
let ALL_AVAILABLE_GROUPS = [];
// [核心适配] 创建一个反向映射表,用于数据提交
const REVERSE_FIELD_MAP = Object.fromEntries(
Object.entries(FIELD_MAP).map(([key, value]) => [value, key])
);
// Constants
const SENSITIVE_INPUT_CLASS = "sensitive-input";
const ARRAY_ITEM_CLASS = "array-item";
const ARRAY_INPUT_CLASS = "array-input";
const MAP_ITEM_CLASS = "map-item";
const MAP_KEY_INPUT_CLASS = "map-key-input";
const MAP_VALUE_INPUT_CLASS = "map-value-input";
const CUSTOM_HEADER_ITEM_CLASS = "custom-header-item";
const CUSTOM_HEADER_KEY_INPUT_CLASS = "custom-header-key-input";
const CUSTOM_HEADER_VALUE_INPUT_CLASS = "custom-header-value-input";
const SAFETY_SETTING_ITEM_CLASS = "safety-setting-item";
const SHOW_CLASS = "show"; // For modals
const API_KEY_REGEX = /AIzaSy\S{33}/g;
const PROXY_REGEX =
/(?:https?|socks5):\/\/(?:[^:@\/]+(?::[^@\/]+)?@)?(?:[^:\/\s]+)(?::\d+)?/g;
const VERTEX_API_KEY_REGEX = /AQ\.[a-zA-Z0-9_\-]{50}/g; // 新增 Vertex Express API Key 正则
const MASKED_VALUE = "••••••••";
// DOM Elements - Global Scope for frequently accessed elements
const safetySettingsContainer = document.getElementById(
"SAFETY_SETTINGS_container"
);
const thinkingModelsContainer = document.getElementById(
"THINKING_MODELS_container"
);
const apiKeyModal = document.getElementById("apiKeyModal");
const apiKeyBulkInput = document.getElementById("apiKeyBulkInput");
const apiKeySearchInput = document.getElementById("apiKeySearchInput");
const bulkDeleteApiKeyModal = document.getElementById("bulkDeleteApiKeyModal");
const bulkDeleteApiKeyInput = document.getElementById("bulkDeleteApiKeyInput");
const proxyModal = document.getElementById("proxyModal");
const proxyBulkInput = document.getElementById("proxyBulkInput");
const bulkDeleteProxyModal = document.getElementById("bulkDeleteProxyModal");
const bulkDeleteProxyInput = document.getElementById("bulkDeleteProxyInput");
const resetConfirmModal = document.getElementById("resetConfirmModal");
const configForm = document.getElementById("configForm"); // Added for frequent use
// [核心新增] 令牌配置模态框的元素
const tokenSettingsModal = document.getElementById("tokenSettingsModal");
const closeTokenSettingsModalBtn = document.getElementById("closeTokenSettingsModalBtn");
const cancelTokenSettingsBtn = document.getElementById("cancelTokenSettingsBtn");
const confirmTokenSettingsBtn = document.getElementById("confirmTokenSettingsBtn");
// 模态框内部的交互元素
const tokenSettingsGenerateBtn = document.getElementById("tokenSettingsGenerateBtn");
const tokenSettingsStatusToggle = document.getElementById("tokenSettingsStatusToggle");
const tokenSettingsStatusText = document.getElementById("tokenSettingsStatusText");
const tokenSettingsSelectAllGroupsBtn = document.getElementById("tokenSettingsSelectAllGroupsBtn");
const tokenSettingsDeselectAllGroupsBtn = document.getElementById("tokenSettingsDeselectAllGroupsBtn");
// [核心新增] IP 封禁配置模态框的元素
const ipBanSettingsModal = document.getElementById('ipBanSettingsModal');
const closeIpBanSettingsModalBtn = document.getElementById('closeIpBanSettingsModalBtn');
const cancelIpBanSettingsBtn = document.getElementById('cancelIpBanSettingsBtn');
const confirmIpBanSettingsBtn = document.getElementById('confirmIpBanSettingsBtn');
const ipBanMaxAttemptsInput = document.getElementById('ipBanMaxAttemptsInput');
const ipBanDurationInput = document.getElementById('ipBanDurationInput');
// [核心新增] 代理高级配置模态框的元素
const proxySettingsModal = document.getElementById('proxySettingsModal');
const proxySettingsBtn = document.getElementById('proxySettingsBtn');
const closeProxySettingsModalBtn = document.getElementById('closeProxySettingsModalBtn');
const cancelProxySettingsBtn = document.getElementById('cancelProxySettingsBtn');
const confirmProxySettingsBtn = document.getElementById('confirmProxySettingsBtn');
// 模态框内的输入元素
const enableProxyCheckInput = document.getElementById('enableProxyCheckInput');
const useProxyHashInput = document.getElementById('useProxyHashInput');
const proxyCheckTimeoutInput = document.getElementById('proxyCheckTimeoutInput');
const proxyCheckConcurrencyInput = document.getElementById('proxyCheckConcurrencyInput');
// Vertex Express API Key Modal Elements
const vertexApiKeyModal = document.getElementById("vertexApiKeyModal");
const vertexApiKeyBulkInput = document.getElementById("vertexApiKeyBulkInput");
const bulkDeleteVertexApiKeyModal = document.getElementById(
"bulkDeleteVertexApiKeyModal"
);
const bulkDeleteVertexApiKeyInput = document.getElementById(
"bulkDeleteVertexApiKeyInput"
);
// Model Helper Modal Elements
const modelHelperModal = document.getElementById("modelHelperModal");
const modelHelperTitleElement = document.getElementById("modelHelperTitle");
const modelHelperSearchInput = document.getElementById(
"modelHelperSearchInput"
);
const modelHelperListContainer = document.getElementById(
"modelHelperListContainer"
);
const closeModelHelperModalBtn = document.getElementById(
"closeModelHelperModalBtn"
);
const cancelModelHelperBtn = document.getElementById("cancelModelHelperBtn");
let cachedModelsList = null;
let currentModelHelperTarget = null; // { type: 'input'/'array', target: elementOrIdOrKey }
// Modal Control Functions
function openModal(modalElement) {
if (modalElement) {
modalElement.classList.add(SHOW_CLASS);
}
}
function closeModal(modalElement) {
if (modalElement) {
modalElement.classList.remove(SHOW_CLASS);
}
}
document.addEventListener("DOMContentLoaded", function () {
// Initialize configuration
initConfig();
// =============================================================
// 启动独立的代理数据加载流程
// =============================================================
loadAndPopulateProxies();
// Tab switching
const tabButtons = document.querySelectorAll(".tab-btn");
tabButtons.forEach((button) => {
button.addEventListener("click", function (e) {
e.stopPropagation();
const tabId = this.getAttribute("data-tab");
switchTab(tabId);
});
});
// Upload provider switching
const uploadProviderSelect = document.getElementById("UPLOAD_PROVIDER");
if (uploadProviderSelect) {
uploadProviderSelect.addEventListener("change", function () {
toggleProviderConfig(this.value);
});
}
// Toggle switch events
const toggleSwitches = document.querySelectorAll(".toggle-switch");
toggleSwitches.forEach((toggleSwitch) => {
toggleSwitch.addEventListener("click", function (e) {
e.stopPropagation();
const checkbox = this.querySelector('input[type="checkbox"]');
if (checkbox) {
checkbox.checked = !checkbox.checked;
}
});
});
// Save button
const saveBtn = document.getElementById("saveBtn");
if (saveBtn) {
saveBtn.addEventListener("click", saveConfig);
}
// Reset button
const resetBtn = document.getElementById("resetBtn");
if (resetBtn) {
resetBtn.addEventListener("click", resetConfig); // resetConfig will open the modal
}
// Scroll buttons
window.addEventListener("scroll", toggleScrollButtons);
// API Key Modal Elements and Events
const addApiKeyBtn = document.getElementById("addApiKeyBtn");
const closeApiKeyModalBtn = document.getElementById("closeApiKeyModalBtn");
const cancelAddApiKeyBtn = document.getElementById("cancelAddApiKeyBtn");
const confirmAddApiKeyBtn = document.getElementById("confirmAddApiKeyBtn");
if (addApiKeyBtn) {
addApiKeyBtn.addEventListener("click", () => {
openModal(apiKeyModal);
if (apiKeyBulkInput) apiKeyBulkInput.value = "";
});
}
if (closeApiKeyModalBtn)
closeApiKeyModalBtn.addEventListener("click", () =>
closeModal(apiKeyModal)
);
if (cancelAddApiKeyBtn)
cancelAddApiKeyBtn.addEventListener("click", () => closeModal(apiKeyModal));
if (confirmAddApiKeyBtn)
confirmAddApiKeyBtn.addEventListener("click", handleBulkAddApiKeys);
if (apiKeySearchInput)
apiKeySearchInput.addEventListener("input", handleApiKeySearch);
// Bulk Delete API Key Modal Elements and Events
const bulkDeleteApiKeyBtn = document.getElementById("bulkDeleteApiKeyBtn");
const closeBulkDeleteModalBtn = document.getElementById(
"closeBulkDeleteModalBtn"
);
const cancelBulkDeleteApiKeyBtn = document.getElementById(
"cancelBulkDeleteApiKeyBtn"
);
const confirmBulkDeleteApiKeyBtn = document.getElementById(
"confirmBulkDeleteApiKeyBtn"
);
if (bulkDeleteApiKeyBtn) {
bulkDeleteApiKeyBtn.addEventListener("click", () => {
openModal(bulkDeleteApiKeyModal);
if (bulkDeleteApiKeyInput) bulkDeleteApiKeyInput.value = "";
});
}
if (closeBulkDeleteModalBtn)
closeBulkDeleteModalBtn.addEventListener("click", () =>
closeModal(bulkDeleteApiKeyModal)
);
if (cancelBulkDeleteApiKeyBtn)
cancelBulkDeleteApiKeyBtn.addEventListener("click", () =>
closeModal(bulkDeleteApiKeyModal)
);
if (confirmBulkDeleteApiKeyBtn)
confirmBulkDeleteApiKeyBtn.addEventListener(
"click",
handleBulkDeleteApiKeys
);
// [核心新增] 为令牌配置模态框,绑定所有的事件监听器
if (closeTokenSettingsModalBtn) {
closeTokenSettingsModalBtn.addEventListener("click", () => closeModal(tokenSettingsModal));
}
if (cancelTokenSettingsBtn) {
cancelTokenSettingsBtn.addEventListener("click", () => closeModal(tokenSettingsModal));
}
if (confirmTokenSettingsBtn) {
confirmTokenSettingsBtn.addEventListener("click", handleConfirmTokenSettings);
}
if (tokenSettingsGenerateBtn) {
tokenSettingsGenerateBtn.addEventListener("click", () => {
const input = document.getElementById('tokenSettingsTokenInput');
if (input) input.value = generateRandomToken();
});
}
if (tokenSettingsStatusToggle) {
tokenSettingsStatusToggle.addEventListener('change', () => {
if (tokenSettingsStatusText) {
tokenSettingsStatusText.textContent = tokenSettingsStatusToggle.checked ? 'Active' : 'Inactive';
}
});
}
if (tokenSettingsSelectAllGroupsBtn) {
tokenSettingsSelectAllGroupsBtn.addEventListener('click', () => toggleAllGroups(true));
}
if (tokenSettingsDeselectAllGroupsBtn) {
tokenSettingsDeselectAllGroupsBtn.addEventListener('click', () => toggleAllGroups(false));
}
// [核心新增] 为IP封禁配置模态框,绑定所有事件监听器
const ipBanSettingsBtn = document.getElementById('ipBanSettingsBtn');
if (ipBanSettingsBtn) {
ipBanSettingsBtn.addEventListener('click', openIpBanSettings);
}
if (closeIpBanSettingsModalBtn) {
closeIpBanSettingsModalBtn.addEventListener('click', () => closeModal(ipBanSettingsModal));
}
if (cancelIpBanSettingsBtn) {
cancelIpBanSettingsBtn.addEventListener('click', () => closeModal(ipBanSettingsModal));
}
if (confirmIpBanSettingsBtn) {
confirmIpBanSettingsBtn.addEventListener('click', handleConfirmIpBanSettings);
}
// [核心新增] 为代理高级配置模态框,绑定所有事件监听器
if (proxySettingsBtn) {
proxySettingsBtn.addEventListener('click', openProxySettingsModal);
}
if (closeProxySettingsModalBtn) {
closeProxySettingsModalBtn.addEventListener('click', () => closeModal(proxySettingsModal));
}
if (cancelProxySettingsBtn) {
cancelProxySettingsBtn.addEventListener('click', () => closeModal(proxySettingsModal));
}
if (confirmProxySettingsBtn) {
confirmProxySettingsBtn.addEventListener('click', handleConfirmProxySettings);
}
// Proxy Modal Elements and Events
const addProxyBtn = document.getElementById("addProxyBtn");
const closeProxyModalBtn = document.getElementById("closeProxyModalBtn");
const cancelAddProxyBtn = document.getElementById("cancelAddProxyBtn");
const confirmAddProxyBtn = document.getElementById("confirmAddProxyBtn");
// Proxy Check Elements and Events
const checkAllProxiesBtn = document.getElementById("checkAllProxiesBtn");
const proxyCheckModal = document.getElementById("proxyCheckModal");
const closeProxyCheckModalBtn = document.getElementById("closeProxyCheckModalBtn");
const closeProxyCheckBtn = document.getElementById("closeProxyCheckBtn");
const retryFailedProxiesBtn = document.getElementById("retryFailedProxiesBtn");
if (addProxyBtn) {
addProxyBtn.addEventListener("click", () => {
openModal(proxyModal);
if (proxyBulkInput) proxyBulkInput.value = "";
});
}
if (checkAllProxiesBtn) {
checkAllProxiesBtn.addEventListener("click", checkAllProxies);
}
if (closeProxyCheckModalBtn) {
closeProxyCheckModalBtn.addEventListener("click", () => closeModal(proxyCheckModal));
}
if (closeProxyCheckBtn) {
closeProxyCheckBtn.addEventListener("click", () => closeModal(proxyCheckModal));
}
if (retryFailedProxiesBtn) {
retryFailedProxiesBtn.addEventListener("click", () => {
// 重试失败的代理检测
checkAllProxies();
});
}
if (closeProxyModalBtn)
closeProxyModalBtn.addEventListener("click", () => closeModal(proxyModal));
if (cancelAddProxyBtn)
cancelAddProxyBtn.addEventListener("click", () => closeModal(proxyModal));
if (confirmAddProxyBtn)
confirmAddProxyBtn.addEventListener("click", handleBulkAddProxies);
// Bulk Delete Proxy Modal Elements and Events
const bulkDeleteProxyBtn = document.getElementById("bulkDeleteProxyBtn");
const closeBulkDeleteProxyModalBtn = document.getElementById(
"closeBulkDeleteProxyModalBtn"
);
const cancelBulkDeleteProxyBtn = document.getElementById(
"cancelBulkDeleteProxyBtn"
);
const confirmBulkDeleteProxyBtn = document.getElementById(
"confirmBulkDeleteProxyBtn"
);
if (bulkDeleteProxyBtn) {
bulkDeleteProxyBtn.addEventListener("click", () => {
openModal(bulkDeleteProxyModal);
if (bulkDeleteProxyInput) bulkDeleteProxyInput.value = "";
});
}
if (closeBulkDeleteProxyModalBtn)
closeBulkDeleteProxyModalBtn.addEventListener("click", () =>
closeModal(bulkDeleteProxyModal)
);
if (cancelBulkDeleteProxyBtn)
cancelBulkDeleteProxyBtn.addEventListener("click", () =>
closeModal(bulkDeleteProxyModal)
);
if (confirmBulkDeleteProxyBtn)
confirmBulkDeleteProxyBtn.addEventListener(
"click",
handleBulkDeleteProxies
);
// Reset Confirmation Modal Elements and Events
const closeResetModalBtn = document.getElementById("closeResetModalBtn");
const cancelResetBtn = document.getElementById("cancelResetBtn");
const confirmResetBtn = document.getElementById("confirmResetBtn");
if (closeResetModalBtn)
closeResetModalBtn.addEventListener("click", () =>
closeModal(resetConfirmModal)
);
if (cancelResetBtn)
cancelResetBtn.addEventListener("click", () =>
closeModal(resetConfirmModal)
);
if (confirmResetBtn) {
confirmResetBtn.addEventListener("click", () => {
closeModal(resetConfirmModal);
executeReset();
});
}
// Click outside modal to close
window.addEventListener("click", (event) => {
const modals = [
apiKeyModal,
resetConfirmModal,
bulkDeleteApiKeyModal,
proxyModal,
bulkDeleteProxyModal,
vertexApiKeyModal, // 新增
bulkDeleteVertexApiKeyModal, // 新增
modelHelperModal,
tokenSettingsModal, // 新增tokenSettingsModal
ipBanSettingsModal, // 新增IP封禁配置模态框
proxySettingsModal, // 新增代理高级配置模态框
];
modals.forEach((modal) => {
if (event.target === modal) {
closeModal(modal);
}
});
});
// Removed static token generation button event listener, now handled dynamically if needed or by specific buttons.
// Authentication token generation button
const generateAuthTokenBtn = document.getElementById("generateAuthTokenBtn");
const authTokenInput = document.getElementById("AUTH_TOKEN");
if (generateAuthTokenBtn && authTokenInput) {
generateAuthTokenBtn.addEventListener("click", function () {
const newToken = generateRandomToken(); // Assuming generateRandomToken is defined elsewhere
authTokenInput.value = newToken;
if (authTokenInput.classList.contains(SENSITIVE_INPUT_CLASS)) {
const event = new Event("focusout", {
bubbles: true,
cancelable: true,
});
authTokenInput.dispatchEvent(event);
}
showNotification("已生成新认证令牌", "success");
});
}
// Event delegation for THINKING_MODELS input changes to update budget map keys
if (thinkingModelsContainer) {
thinkingModelsContainer.addEventListener("input", function (event) {
const target = event.target;
if (
target &&
target.classList.contains(ARRAY_INPUT_CLASS) &&
target.closest(`.${ARRAY_ITEM_CLASS}[data-model-id]`)
) {
const modelInput = target;
const modelItem = modelInput.closest(`.${ARRAY_ITEM_CLASS}`);
const modelId = modelItem.getAttribute("data-model-id");
const budgetKeyInput = document.querySelector(
`.${MAP_KEY_INPUT_CLASS}[data-model-id="${modelId}"]`
);
if (budgetKeyInput) {
budgetKeyInput.value = modelInput.value;
}
}
});
}
// Event delegation for dynamically added remove buttons and generate token buttons within array items
if (configForm) {
// Ensure configForm exists before adding event listener
configForm.addEventListener("click", function (event) {
const target = event.target;
const removeButton = target.closest(".remove-btn");
const generateButton = target.closest(".generate-btn");
const settingsButton = target.closest(".settings-btn"); // 新增settingsButton
if (removeButton && removeButton.closest(`.${ARRAY_ITEM_CLASS}`)) {
const arrayItem = removeButton.closest(`.${ARRAY_ITEM_CLASS}`);
const parentContainer = arrayItem.parentElement;
const isThinkingModelItem =
arrayItem.hasAttribute("data-model-id") &&
parentContainer &&
parentContainer.id === "THINKING_MODELS_container";
const isSafetySettingItem = arrayItem.classList.contains(
SAFETY_SETTING_ITEM_CLASS
);
if (isThinkingModelItem) {
const modelId = arrayItem.getAttribute("data-model-id");
const budgetMapItem = document.querySelector(
`.${MAP_ITEM_CLASS}[data-model-id="${modelId}"]`
);
if (budgetMapItem) {
budgetMapItem.remove();
}
// Check and add placeholder for budget map if empty
const budgetContainer = document.getElementById(
"THINKING_BUDGET_MAP_container"
);
if (budgetContainer && budgetContainer.children.length === 0) {
budgetContainer.innerHTML =
'
请在上方添加思考模型,预算将自动关联。
';
}
}
arrayItem.remove();
// Check and add placeholder for safety settings if empty
if (
isSafetySettingItem &&
parentContainer &&
parentContainer.children.length === 0
) {
parentContainer.innerHTML =
'定义模型的安全过滤阈值。
';
}
} else if (
generateButton &&
generateButton.closest(`.${ARRAY_ITEM_CLASS}`)
) {
const inputField = generateButton
.closest(`.${ARRAY_ITEM_CLASS}`)
.querySelector(`.${ARRAY_INPUT_CLASS}`);
if (inputField) {
const newToken = generateRandomToken();
inputField.value = newToken;
if (inputField.classList.contains(SENSITIVE_INPUT_CLASS)) {
const event = new Event("focusout", {
bubbles: true,
cancelable: true,
});
inputField.dispatchEvent(event);
}
showNotification("已生成新令牌", "success");
}
}
else if (settingsButton && settingsButton.closest(`.${ARRAY_ITEM_CLASS}`)) {
// 如果识别到“设置”指令,就命令 openTokenSettings 函数,立即执行!
openTokenSettings(settingsButton); // 新增openTokenSettings调用
}
});
}
// Add Safety Setting button
const addSafetySettingBtn = document.getElementById("addSafetySettingBtn");
if (addSafetySettingBtn) {
addSafetySettingBtn.addEventListener("click", () => addSafetySettingItem());
}
// Add Custom Header button
const addCustomHeaderBtn = document.getElementById("addCustomHeaderBtn");
if (addCustomHeaderBtn) {
addCustomHeaderBtn.addEventListener("click", () => addCustomHeaderItem());
}
initializeSensitiveFields(); // Initialize sensitive field handling
// Vertex Express API Key Modal Elements and Events
const addVertexApiKeyBtn = document.getElementById("addVertexApiKeyBtn");
const closeVertexApiKeyModalBtn = document.getElementById(
"closeVertexApiKeyModalBtn"
);
const cancelAddVertexApiKeyBtn = document.getElementById(
"cancelAddVertexApiKeyBtn"
);
const confirmAddVertexApiKeyBtn = document.getElementById(
"confirmAddVertexApiKeyBtn"
);
const bulkDeleteVertexApiKeyBtn = document.getElementById(
"bulkDeleteVertexApiKeyBtn"
);
const closeBulkDeleteVertexModalBtn = document.getElementById(
"closeBulkDeleteVertexModalBtn"
);
const cancelBulkDeleteVertexApiKeyBtn = document.getElementById(
"cancelBulkDeleteVertexApiKeyBtn"
);
const confirmBulkDeleteVertexApiKeyBtn = document.getElementById(
"confirmBulkDeleteVertexApiKeyBtn"
);
if (addVertexApiKeyBtn) {
addVertexApiKeyBtn.addEventListener("click", () => {
openModal(vertexApiKeyModal);
if (vertexApiKeyBulkInput) vertexApiKeyBulkInput.value = "";
});
}
if (closeVertexApiKeyModalBtn)
closeVertexApiKeyModalBtn.addEventListener("click", () =>
closeModal(vertexApiKeyModal)
);
if (cancelAddVertexApiKeyBtn)
cancelAddVertexApiKeyBtn.addEventListener("click", () =>
closeModal(vertexApiKeyModal)
);
if (confirmAddVertexApiKeyBtn)
confirmAddVertexApiKeyBtn.addEventListener(
"click",
handleBulkAddVertexApiKeys
);
if (bulkDeleteVertexApiKeyBtn) {
bulkDeleteVertexApiKeyBtn.addEventListener("click", () => {
openModal(bulkDeleteVertexApiKeyModal);
if (bulkDeleteVertexApiKeyInput) bulkDeleteVertexApiKeyInput.value = "";
});
}
if (closeBulkDeleteVertexModalBtn)
closeBulkDeleteVertexModalBtn.addEventListener("click", () =>
closeModal(bulkDeleteVertexApiKeyModal)
);
if (cancelBulkDeleteVertexApiKeyBtn)
cancelBulkDeleteVertexApiKeyBtn.addEventListener("click", () =>
closeModal(bulkDeleteVertexApiKeyModal)
);
if (confirmBulkDeleteVertexApiKeyBtn)
confirmBulkDeleteVertexApiKeyBtn.addEventListener(
"click",
handleBulkDeleteVertexApiKeys
);
// Model Helper Modal Event Listeners
if (closeModelHelperModalBtn) {
closeModelHelperModalBtn.addEventListener("click", () =>
closeModal(modelHelperModal)
);
}
if (cancelModelHelperBtn) {
cancelModelHelperBtn.addEventListener("click", () =>
closeModal(modelHelperModal)
);
}
if (modelHelperSearchInput) {
modelHelperSearchInput.addEventListener("input", () =>
renderModelsInModal()
);
}
// Add event listeners to all model helper trigger buttons
const modelHelperTriggerBtns = document.querySelectorAll(
".model-helper-trigger-btn"
);
modelHelperTriggerBtns.forEach((btn) => {
btn.addEventListener("click", () => {
const targetInputId = btn.dataset.targetInputId;
const targetArrayKey = btn.dataset.targetArrayKey;
if (targetInputId) {
currentModelHelperTarget = {
type: "input",
target: document.getElementById(targetInputId),
};
} else if (targetArrayKey) {
currentModelHelperTarget = { type: "array", targetKey: targetArrayKey };
}
openModelHelperModal();
});
});
}); // <-- DOMContentLoaded end
/**
* Initializes sensitive input field behavior (masking/unmasking).
*/
function initializeSensitiveFields() {
if (!configForm) return;
// Helper function: Mask field
function maskField(field) {
if (field.value && field.value !== MASKED_VALUE) {
field.setAttribute("data-real-value", field.value);
field.value = MASKED_VALUE;
} else if (!field.value) {
// If field value is empty string
field.removeAttribute("data-real-value");
// Ensure empty value doesn't show as asterisks
if (field.value === MASKED_VALUE) field.value = "";
}
}
// Helper function: Unmask field
function unmaskField(field) {
if (field.hasAttribute("data-real-value")) {
field.value = field.getAttribute("data-real-value");
}
// If no data-real-value and value is MASKED_VALUE, it might be an initial empty sensitive field, clear it
else if (
field.value === MASKED_VALUE &&
!field.hasAttribute("data-real-value")
) {
field.value = "";
}
}
// Initial masking for existing sensitive fields on page load
// This function is called after populateForm and after dynamic element additions (via event delegation)
function initialMaskAllExisting() {
const sensitiveFields = configForm.querySelectorAll(
`.${SENSITIVE_INPUT_CLASS}`
);
sensitiveFields.forEach((field) => {
if (field.type === "password") {
// For password fields, browser handles it. We just ensure data-original-type is set
// and if it has a value, we also store data-real-value so it can be shown when switched to text
if (field.value) {
field.setAttribute("data-real-value", field.value);
}
// No need to set to MASKED_VALUE as browser handles it.
} else if (
field.type === "text" ||
field.tagName.toLowerCase() === "textarea"
) {
maskField(field);
}
});
}
initialMaskAllExisting();
// Event delegation for dynamic and static fields
configForm.addEventListener("focusin", function (event) {
const target = event.target;
if (target.classList.contains(SENSITIVE_INPUT_CLASS)) {
if (target.type === "password") {
// Record original type to switch back on blur
if (!target.hasAttribute("data-original-type")) {
target.setAttribute("data-original-type", "password");
}
target.type = "text"; // Switch to text type to show content
// If data-real-value exists (e.g., set during populateForm), use it
if (target.hasAttribute("data-real-value")) {
target.value = target.getAttribute("data-real-value");
}
// Otherwise, the browser's existing password value will be shown directly
} else {
// For type="text" or textarea
unmaskField(target);
}
}
});
configForm.addEventListener("focusout", function (event) {
const target = event.target;
if (target.classList.contains(SENSITIVE_INPUT_CLASS)) {
// First, if the field is currently text and has a value, update data-real-value
if (
target.type === "text" ||
target.tagName.toLowerCase() === "textarea"
) {
if (target.value && target.value !== MASKED_VALUE) {
target.setAttribute("data-real-value", target.value);
} else if (!target.value) {
// If value is empty, remove data-real-value
target.removeAttribute("data-real-value");
}
}
// Then handle type switching and masking
if (
target.getAttribute("data-original-type") === "password" &&
target.type === "text"
) {
target.type = "password"; // Switch back to password type
// For password type, browser handles masking automatically, no need to set MASKED_VALUE manually
// data-real-value has already been updated by the logic above
} else if (
target.type === "text" ||
target.tagName.toLowerCase() === "textarea"
) {
// For text or textarea sensitive fields, perform masking
maskField(target);
}
}
});
}
/**
* Generates a UUID.
* @returns {string} A new UUID.
*/
function generateUUID() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
var r = (Math.random() * 16) | 0,
v = c == "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
/**
* [定稿] 页面初始化函数
* 作为页面加载的唯一入口,负责:
* 1. 获取并填充系统配置。
* 2. 获取令牌数据,并调用 addArrayItemWithValue 进行渲染。
* 3. [新增] 从令牌数据中提取所有唯一的分组,并缓存到全局变量中。
*/
async function initConfig() {
// --- 第一部分: 加载系统运行时配置 ---
try {
showNotification("正在加载系统配置...", "info");
const settingsResponse = await apiFetch("/admin/settings");
const settingsResult = await settingsResponse.json();
populateForm(settingsResult.data);
showNotification("系统配置加载成功", "success");
} catch (error) {
console.error("加载系统配置失败:", error);
showNotification("加载系统配置失败: " + error.message, "error");
}
// --- 第二部分: 加载令牌,并执行分拣、渲染、缓存 ---
try {
const tokensResponse = await apiFetch("/admin/tokens");
const result = await tokensResponse.json();
const allTokensData = result.data;
let adminToken = null;
let userTokens = [];
// [核心逻辑] 分拣管理员令牌和用户令牌
if (allTokensData && Array.isArray(allTokensData)) {
allTokensData.forEach(token => {
if (token.IsAdmin) {
adminToken = token;
} else {
userTokens.push(token);
}
});
}
// [任务 1] 填充独立的管理员 AUTH_TOKEN 输入框
if (adminToken) {
const authTokenInput = document.getElementById("AUTH_TOKEN");
if (authTokenInput) {
authTokenInput.value = adminToken.Token;
// 触发敏感字段的遮蔽逻辑,保持UI一致性
authTokenInput.dispatchEvent(new Event('focusout', { bubbles: true, cancelable: true }));
}
}
// [任务 2] 渲染过滤后的用户令牌列表
const container = document.getElementById('ALLOWED_TOKENS_container');
if (container) {
container.innerHTML = '';
}
if (userTokens.length > 0) {
userTokens.forEach(token => {
addArrayItemWithValue('ALLOWED_TOKENS', token);
});
} else {
if (container) {
container.innerHTML = '系统中暂无用户级认证令牌。
';
}
}
// [附加任务] 从所有令牌数据中提取、去重并缓存所有可用分组
if (allTokensData && Array.isArray(allTokensData)) {
const groupMap = new Map();
allTokensData.forEach(token => {
if (token.AllowedGroups && Array.isArray(token.AllowedGroups)) {
token.AllowedGroups.forEach(group => {
if (!groupMap.has(group.ID)) {
groupMap.set(group.ID, group);
}
});
}
});
ALL_AVAILABLE_GROUPS = Array.from(groupMap.values());
}
} catch (error) {
console.error("加载认证令牌失败:", error);
showNotification("加载认证令牌失败: " + error.message, "error");
}
}
/**
* [核心修正] 此函数现在是“总调度员”
* 它负责获取数据,然后命令 addArrayItemWithValue 函数进行渲染。
* @param {Array