3056 lines
115 KiB
JavaScript
3056 lines
115 KiB
JavaScript
|
||
// [核心适配] 创建一个双向映射表,连接前端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 --
|