GeminiBalance / app /static /js /config_editor.js
CatPtain's picture
Upload 77 files
76b9762 verified
// Constants
const SENSITIVE_INPUT_CLASS = "sensitive-input";
const ARRAY_ITEM_CLASS = "array-item";
const ARRAY_INPUT_CLASS = "array-input";
const MAP_ITEM_CLASS = "map-item";
const MAP_KEY_INPUT_CLASS = "map-key-input";
const MAP_VALUE_INPUT_CLASS = "map-value-input";
const SAFETY_SETTING_ITEM_CLASS = "safety-setting-item";
const SHOW_CLASS = "show"; // For modals
const API_KEY_REGEX = /AIzaSy\S{33}/g;
const PROXY_REGEX =
/(?:https?|socks5):\/\/(?:[^:@\/]+(?::[^@\/]+)?@)?(?:[^:\/\s]+)(?::\d+)?/g;
const VERTEX_API_KEY_REGEX = /AQ\.[a-zA-Z0-9_]{50}/g; // 新增 Vertex API Key 正则
const MASKED_VALUE = "••••••••";
// DOM Elements - Global Scope for frequently accessed elements
const safetySettingsContainer = document.getElementById(
"SAFETY_SETTINGS_container"
);
const thinkingModelsContainer = document.getElementById(
"THINKING_MODELS_container"
);
const apiKeyModal = document.getElementById("apiKeyModal");
const apiKeyBulkInput = document.getElementById("apiKeyBulkInput");
const apiKeySearchInput = document.getElementById("apiKeySearchInput");
const bulkDeleteApiKeyModal = document.getElementById("bulkDeleteApiKeyModal");
const bulkDeleteApiKeyInput = document.getElementById("bulkDeleteApiKeyInput");
const proxyModal = document.getElementById("proxyModal");
const proxyBulkInput = document.getElementById("proxyBulkInput");
const bulkDeleteProxyModal = document.getElementById("bulkDeleteProxyModal");
const bulkDeleteProxyInput = document.getElementById("bulkDeleteProxyInput");
const resetConfirmModal = document.getElementById("resetConfirmModal");
const configForm = document.getElementById("configForm"); // Added for frequent use
// Vertex API Key Modal Elements
const vertexApiKeyModal = document.getElementById("vertexApiKeyModal");
const vertexApiKeyBulkInput = document.getElementById("vertexApiKeyBulkInput");
const bulkDeleteVertexApiKeyModal = document.getElementById(
"bulkDeleteVertexApiKeyModal"
);
const bulkDeleteVertexApiKeyInput = document.getElementById(
"bulkDeleteVertexApiKeyInput"
);
// Model Helper Modal Elements
const modelHelperModal = document.getElementById("modelHelperModal");
const modelHelperTitleElement = document.getElementById("modelHelperTitle");
const modelHelperSearchInput = document.getElementById(
"modelHelperSearchInput"
);
const modelHelperListContainer = document.getElementById(
"modelHelperListContainer"
);
const closeModelHelperModalBtn = document.getElementById(
"closeModelHelperModalBtn"
);
const cancelModelHelperBtn = document.getElementById("cancelModelHelperBtn");
let cachedModelsList = null;
let currentModelHelperTarget = null; // { type: 'input'/'array', target: elementOrIdOrKey }
// Modal Control Functions
function openModal(modalElement) {
if (modalElement) {
modalElement.classList.add(SHOW_CLASS);
}
}
function closeModal(modalElement) {
if (modalElement) {
modalElement.classList.remove(SHOW_CLASS);
}
}
document.addEventListener("DOMContentLoaded", function () {
// Initialize configuration
initConfig();
// Tab switching
const tabButtons = document.querySelectorAll(".tab-btn");
tabButtons.forEach((button) => {
button.addEventListener("click", function (e) {
e.stopPropagation();
const tabId = this.getAttribute("data-tab");
switchTab(tabId);
});
});
// Upload provider switching
const uploadProviderSelect = document.getElementById("UPLOAD_PROVIDER");
if (uploadProviderSelect) {
uploadProviderSelect.addEventListener("change", function () {
toggleProviderConfig(this.value);
});
}
// Toggle switch events
const toggleSwitches = document.querySelectorAll(".toggle-switch");
toggleSwitches.forEach((toggleSwitch) => {
toggleSwitch.addEventListener("click", function (e) {
e.stopPropagation();
const checkbox = this.querySelector('input[type="checkbox"]');
if (checkbox) {
checkbox.checked = !checkbox.checked;
}
});
});
// Save button
const saveBtn = document.getElementById("saveBtn");
if (saveBtn) {
saveBtn.addEventListener("click", saveConfig);
}
// Reset button
const resetBtn = document.getElementById("resetBtn");
if (resetBtn) {
resetBtn.addEventListener("click", resetConfig); // resetConfig will open the modal
}
// Scroll buttons
window.addEventListener("scroll", toggleScrollButtons);
// API Key Modal Elements and Events
const addApiKeyBtn = document.getElementById("addApiKeyBtn");
const closeApiKeyModalBtn = document.getElementById("closeApiKeyModalBtn");
const cancelAddApiKeyBtn = document.getElementById("cancelAddApiKeyBtn");
const confirmAddApiKeyBtn = document.getElementById("confirmAddApiKeyBtn");
if (addApiKeyBtn) {
addApiKeyBtn.addEventListener("click", () => {
openModal(apiKeyModal);
if (apiKeyBulkInput) apiKeyBulkInput.value = "";
});
}
if (closeApiKeyModalBtn)
closeApiKeyModalBtn.addEventListener("click", () =>
closeModal(apiKeyModal)
);
if (cancelAddApiKeyBtn)
cancelAddApiKeyBtn.addEventListener("click", () => closeModal(apiKeyModal));
if (confirmAddApiKeyBtn)
confirmAddApiKeyBtn.addEventListener("click", handleBulkAddApiKeys);
if (apiKeySearchInput)
apiKeySearchInput.addEventListener("input", handleApiKeySearch);
// Bulk Delete API Key Modal Elements and Events
const bulkDeleteApiKeyBtn = document.getElementById("bulkDeleteApiKeyBtn");
const closeBulkDeleteModalBtn = document.getElementById(
"closeBulkDeleteModalBtn"
);
const cancelBulkDeleteApiKeyBtn = document.getElementById(
"cancelBulkDeleteApiKeyBtn"
);
const confirmBulkDeleteApiKeyBtn = document.getElementById(
"confirmBulkDeleteApiKeyBtn"
);
if (bulkDeleteApiKeyBtn) {
bulkDeleteApiKeyBtn.addEventListener("click", () => {
openModal(bulkDeleteApiKeyModal);
if (bulkDeleteApiKeyInput) bulkDeleteApiKeyInput.value = "";
});
}
if (closeBulkDeleteModalBtn)
closeBulkDeleteModalBtn.addEventListener("click", () =>
closeModal(bulkDeleteApiKeyModal)
);
if (cancelBulkDeleteApiKeyBtn)
cancelBulkDeleteApiKeyBtn.addEventListener("click", () =>
closeModal(bulkDeleteApiKeyModal)
);
if (confirmBulkDeleteApiKeyBtn)
confirmBulkDeleteApiKeyBtn.addEventListener(
"click",
handleBulkDeleteApiKeys
);
// Proxy Modal Elements and Events
const addProxyBtn = document.getElementById("addProxyBtn");
const closeProxyModalBtn = document.getElementById("closeProxyModalBtn");
const cancelAddProxyBtn = document.getElementById("cancelAddProxyBtn");
const confirmAddProxyBtn = document.getElementById("confirmAddProxyBtn");
if (addProxyBtn) {
addProxyBtn.addEventListener("click", () => {
openModal(proxyModal);
if (proxyBulkInput) proxyBulkInput.value = "";
});
}
if (closeProxyModalBtn)
closeProxyModalBtn.addEventListener("click", () => closeModal(proxyModal));
if (cancelAddProxyBtn)
cancelAddProxyBtn.addEventListener("click", () => closeModal(proxyModal));
if (confirmAddProxyBtn)
confirmAddProxyBtn.addEventListener("click", handleBulkAddProxies);
// Bulk Delete Proxy Modal Elements and Events
const bulkDeleteProxyBtn = document.getElementById("bulkDeleteProxyBtn");
const closeBulkDeleteProxyModalBtn = document.getElementById(
"closeBulkDeleteProxyModalBtn"
);
const cancelBulkDeleteProxyBtn = document.getElementById(
"cancelBulkDeleteProxyBtn"
);
const confirmBulkDeleteProxyBtn = document.getElementById(
"confirmBulkDeleteProxyBtn"
);
if (bulkDeleteProxyBtn) {
bulkDeleteProxyBtn.addEventListener("click", () => {
openModal(bulkDeleteProxyModal);
if (bulkDeleteProxyInput) bulkDeleteProxyInput.value = "";
});
}
if (closeBulkDeleteProxyModalBtn)
closeBulkDeleteProxyModalBtn.addEventListener("click", () =>
closeModal(bulkDeleteProxyModal)
);
if (cancelBulkDeleteProxyBtn)
cancelBulkDeleteProxyBtn.addEventListener("click", () =>
closeModal(bulkDeleteProxyModal)
);
if (confirmBulkDeleteProxyBtn)
confirmBulkDeleteProxyBtn.addEventListener(
"click",
handleBulkDeleteProxies
);
// Reset Confirmation Modal Elements and Events
const closeResetModalBtn = document.getElementById("closeResetModalBtn");
const cancelResetBtn = document.getElementById("cancelResetBtn");
const confirmResetBtn = document.getElementById("confirmResetBtn");
if (closeResetModalBtn)
closeResetModalBtn.addEventListener("click", () =>
closeModal(resetConfirmModal)
);
if (cancelResetBtn)
cancelResetBtn.addEventListener("click", () =>
closeModal(resetConfirmModal)
);
if (confirmResetBtn) {
confirmResetBtn.addEventListener("click", () => {
closeModal(resetConfirmModal);
executeReset();
});
}
// Click outside modal to close
window.addEventListener("click", (event) => {
const modals = [
apiKeyModal,
resetConfirmModal,
bulkDeleteApiKeyModal,
proxyModal,
bulkDeleteProxyModal,
vertexApiKeyModal, // 新增
bulkDeleteVertexApiKeyModal, // 新增
modelHelperModal,
];
modals.forEach((modal) => {
if (event.target === modal) {
closeModal(modal);
}
});
});
// Removed static token generation button event listener, now handled dynamically if needed or by specific buttons.
// Authentication token generation button
const generateAuthTokenBtn = document.getElementById("generateAuthTokenBtn");
const authTokenInput = document.getElementById("AUTH_TOKEN");
if (generateAuthTokenBtn && authTokenInput) {
generateAuthTokenBtn.addEventListener("click", function () {
const newToken = generateRandomToken(); // Assuming generateRandomToken is defined elsewhere
authTokenInput.value = newToken;
if (authTokenInput.classList.contains(SENSITIVE_INPUT_CLASS)) {
const event = new Event("focusout", {
bubbles: true,
cancelable: true,
});
authTokenInput.dispatchEvent(event);
}
showNotification("已生成新认证令牌", "success");
});
}
// Event delegation for THINKING_MODELS input changes to update budget map keys
if (thinkingModelsContainer) {
thinkingModelsContainer.addEventListener("input", function (event) {
const target = event.target;
if (
target &&
target.classList.contains(ARRAY_INPUT_CLASS) &&
target.closest(`.${ARRAY_ITEM_CLASS}[data-model-id]`)
) {
const modelInput = target;
const modelItem = modelInput.closest(`.${ARRAY_ITEM_CLASS}`);
const modelId = modelItem.getAttribute("data-model-id");
const budgetKeyInput = document.querySelector(
`.${MAP_KEY_INPUT_CLASS}[data-model-id="${modelId}"]`
);
if (budgetKeyInput) {
budgetKeyInput.value = modelInput.value;
}
}
});
}
// Event delegation for dynamically added remove buttons and generate token buttons within array items
if (configForm) {
// Ensure configForm exists before adding event listener
configForm.addEventListener("click", function (event) {
const target = event.target;
const removeButton = target.closest(".remove-btn");
const generateButton = target.closest(".generate-btn");
if (removeButton && removeButton.closest(`.${ARRAY_ITEM_CLASS}`)) {
const arrayItem = removeButton.closest(`.${ARRAY_ITEM_CLASS}`);
const parentContainer = arrayItem.parentElement;
const isThinkingModelItem =
arrayItem.hasAttribute("data-model-id") &&
parentContainer &&
parentContainer.id === "THINKING_MODELS_container";
const isSafetySettingItem = arrayItem.classList.contains(
SAFETY_SETTING_ITEM_CLASS
);
if (isThinkingModelItem) {
const modelId = arrayItem.getAttribute("data-model-id");
const budgetMapItem = document.querySelector(
`.${MAP_ITEM_CLASS}[data-model-id="${modelId}"]`
);
if (budgetMapItem) {
budgetMapItem.remove();
}
// Check and add placeholder for budget map if empty
const budgetContainer = document.getElementById(
"THINKING_BUDGET_MAP_container"
);
if (budgetContainer && budgetContainer.children.length === 0) {
budgetContainer.innerHTML =
'<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");
}
}
});
}
// Add Safety Setting button
const addSafetySettingBtn = document.getElementById("addSafetySettingBtn");
if (addSafetySettingBtn) {
addSafetySettingBtn.addEventListener("click", () => addSafetySettingItem());
}
initializeSensitiveFields(); // Initialize sensitive field handling
// Vertex API Key Modal Elements and Events
const addVertexApiKeyBtn = document.getElementById("addVertexApiKeyBtn");
const closeVertexApiKeyModalBtn = document.getElementById(
"closeVertexApiKeyModalBtn"
);
const cancelAddVertexApiKeyBtn = document.getElementById(
"cancelAddVertexApiKeyBtn"
);
const confirmAddVertexApiKeyBtn = document.getElementById(
"confirmAddVertexApiKeyBtn"
);
const bulkDeleteVertexApiKeyBtn = document.getElementById(
"bulkDeleteVertexApiKeyBtn"
);
const closeBulkDeleteVertexModalBtn = document.getElementById(
"closeBulkDeleteVertexModalBtn"
);
const cancelBulkDeleteVertexApiKeyBtn = document.getElementById(
"cancelBulkDeleteVertexApiKeyBtn"
);
const confirmBulkDeleteVertexApiKeyBtn = document.getElementById(
"confirmBulkDeleteVertexApiKeyBtn"
);
if (addVertexApiKeyBtn) {
addVertexApiKeyBtn.addEventListener("click", () => {
openModal(vertexApiKeyModal);
if (vertexApiKeyBulkInput) vertexApiKeyBulkInput.value = "";
});
}
if (closeVertexApiKeyModalBtn)
closeVertexApiKeyModalBtn.addEventListener("click", () =>
closeModal(vertexApiKeyModal)
);
if (cancelAddVertexApiKeyBtn)
cancelAddVertexApiKeyBtn.addEventListener("click", () =>
closeModal(vertexApiKeyModal)
);
if (confirmAddVertexApiKeyBtn)
confirmAddVertexApiKeyBtn.addEventListener(
"click",
handleBulkAddVertexApiKeys
);
if (bulkDeleteVertexApiKeyBtn) {
bulkDeleteVertexApiKeyBtn.addEventListener("click", () => {
openModal(bulkDeleteVertexApiKeyModal);
if (bulkDeleteVertexApiKeyInput) bulkDeleteVertexApiKeyInput.value = "";
});
}
if (closeBulkDeleteVertexModalBtn)
closeBulkDeleteVertexModalBtn.addEventListener("click", () =>
closeModal(bulkDeleteVertexApiKeyModal)
);
if (cancelBulkDeleteVertexApiKeyBtn)
cancelBulkDeleteVertexApiKeyBtn.addEventListener("click", () =>
closeModal(bulkDeleteVertexApiKeyModal)
);
if (confirmBulkDeleteVertexApiKeyBtn)
confirmBulkDeleteVertexApiKeyBtn.addEventListener(
"click",
handleBulkDeleteVertexApiKeys
);
// Model Helper Modal Event Listeners
if (closeModelHelperModalBtn) {
closeModelHelperModalBtn.addEventListener("click", () =>
closeModal(modelHelperModal)
);
}
if (cancelModelHelperBtn) {
cancelModelHelperBtn.addEventListener("click", () =>
closeModal(modelHelperModal)
);
}
if (modelHelperSearchInput) {
modelHelperSearchInput.addEventListener("input", () =>
renderModelsInModal()
);
}
// Add event listeners to all model helper trigger buttons
const modelHelperTriggerBtns = document.querySelectorAll(
".model-helper-trigger-btn"
);
modelHelperTriggerBtns.forEach((btn) => {
btn.addEventListener("click", () => {
const targetInputId = btn.dataset.targetInputId;
const targetArrayKey = btn.dataset.targetArrayKey;
if (targetInputId) {
currentModelHelperTarget = {
type: "input",
target: document.getElementById(targetInputId),
};
} else if (targetArrayKey) {
currentModelHelperTarget = { type: "array", targetKey: targetArrayKey };
}
openModelHelperModal();
});
});
}); // <-- DOMContentLoaded end
/**
* Initializes sensitive input field behavior (masking/unmasking).
*/
function initializeSensitiveFields() {
if (!configForm) return;
// Helper function: Mask field
function maskField(field) {
if (field.value && field.value !== MASKED_VALUE) {
field.setAttribute("data-real-value", field.value);
field.value = MASKED_VALUE;
} else if (!field.value) {
// If field value is empty string
field.removeAttribute("data-real-value");
// Ensure empty value doesn't show as asterisks
if (field.value === MASKED_VALUE) field.value = "";
}
}
// Helper function: Unmask field
function unmaskField(field) {
if (field.hasAttribute("data-real-value")) {
field.value = field.getAttribute("data-real-value");
}
// If no data-real-value and value is MASKED_VALUE, it might be an initial empty sensitive field, clear it
else if (
field.value === MASKED_VALUE &&
!field.hasAttribute("data-real-value")
) {
field.value = "";
}
}
// Initial masking for existing sensitive fields on page load
// This function is called after populateForm and after dynamic element additions (via event delegation)
function initialMaskAllExisting() {
const sensitiveFields = configForm.querySelectorAll(
`.${SENSITIVE_INPUT_CLASS}`
);
sensitiveFields.forEach((field) => {
if (field.type === "password") {
// For password fields, browser handles it. We just ensure data-original-type is set
// and if it has a value, we also store data-real-value so it can be shown when switched to text
if (field.value) {
field.setAttribute("data-real-value", field.value);
}
// No need to set to MASKED_VALUE as browser handles it.
} else if (
field.type === "text" ||
field.tagName.toLowerCase() === "textarea"
) {
maskField(field);
}
});
}
initialMaskAllExisting();
// Event delegation for dynamic and static fields
configForm.addEventListener("focusin", function (event) {
const target = event.target;
if (target.classList.contains(SENSITIVE_INPUT_CLASS)) {
if (target.type === "password") {
// Record original type to switch back on blur
if (!target.hasAttribute("data-original-type")) {
target.setAttribute("data-original-type", "password");
}
target.type = "text"; // Switch to text type to show content
// If data-real-value exists (e.g., set during populateForm), use it
if (target.hasAttribute("data-real-value")) {
target.value = target.getAttribute("data-real-value");
}
// Otherwise, the browser's existing password value will be shown directly
} else {
// For type="text" or textarea
unmaskField(target);
}
}
});
configForm.addEventListener("focusout", function (event) {
const target = event.target;
if (target.classList.contains(SENSITIVE_INPUT_CLASS)) {
// First, if the field is currently text and has a value, update data-real-value
if (
target.type === "text" ||
target.tagName.toLowerCase() === "textarea"
) {
if (target.value && target.value !== MASKED_VALUE) {
target.setAttribute("data-real-value", target.value);
} else if (!target.value) {
// If value is empty, remove data-real-value
target.removeAttribute("data-real-value");
}
}
// Then handle type switching and masking
if (
target.getAttribute("data-original-type") === "password" &&
target.type === "text"
) {
target.type = "password"; // Switch back to password type
// For password type, browser handles masking automatically, no need to set MASKED_VALUE manually
// data-real-value has already been updated by the logic above
} else if (
target.type === "text" ||
target.tagName.toLowerCase() === "textarea"
) {
// For text or textarea sensitive fields, perform masking
maskField(target);
}
}
});
}
/**
* Generates a UUID.
* @returns {string} A new UUID.
*/
function generateUUID() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
var r = (Math.random() * 16) | 0,
v = c == "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
/**
* Initializes the configuration by fetching it from the server and populating the form.
*/
async function initConfig() {
try {
showNotification("正在加载配置...", "info");
const response = await fetch("/api/config");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const config = await response.json();
// 确保数组字段有默认值
if (
!config.API_KEYS ||
!Array.isArray(config.API_KEYS) ||
config.API_KEYS.length === 0
) {
config.API_KEYS = ["请在此处输入 API 密钥"];
}
if (
!config.ALLOWED_TOKENS ||
!Array.isArray(config.ALLOWED_TOKENS) ||
config.ALLOWED_TOKENS.length === 0
) {
config.ALLOWED_TOKENS = [""];
}
if (
!config.IMAGE_MODELS ||
!Array.isArray(config.IMAGE_MODELS) ||
config.IMAGE_MODELS.length === 0
) {
config.IMAGE_MODELS = ["gemini-1.5-pro-latest"];
}
if (
!config.SEARCH_MODELS ||
!Array.isArray(config.SEARCH_MODELS) ||
config.SEARCH_MODELS.length === 0
) {
config.SEARCH_MODELS = ["gemini-1.5-flash-latest"];
}
if (
!config.FILTERED_MODELS ||
!Array.isArray(config.FILTERED_MODELS) ||
config.FILTERED_MODELS.length === 0
) {
config.FILTERED_MODELS = ["gemini-1.0-pro-latest"];
}
// --- 新增:处理 VERTEX_API_KEYS 默认值 ---
if (!config.VERTEX_API_KEYS || !Array.isArray(config.VERTEX_API_KEYS)) {
config.VERTEX_API_KEYS = [];
}
// --- 新增:处理 VERTEX_EXPRESS_BASE_URL 默认值 ---
if (typeof config.VERTEX_EXPRESS_BASE_URL === "undefined") {
config.VERTEX_EXPRESS_BASE_URL = "";
}
// --- 新增:处理 PROXIES 默认值 ---
if (!config.PROXIES || !Array.isArray(config.PROXIES)) {
config.PROXIES = []; // 默认为空数组
}
// --- 新增:处理新字段的默认值 ---
if (!config.THINKING_MODELS || !Array.isArray(config.THINKING_MODELS)) {
config.THINKING_MODELS = []; // 默认为空数组
}
if (
!config.THINKING_BUDGET_MAP ||
typeof config.THINKING_BUDGET_MAP !== "object" ||
config.THINKING_BUDGET_MAP === null
) {
config.THINKING_BUDGET_MAP = {}; // 默认为空对象
}
// --- 新增:处理 SAFETY_SETTINGS 默认值 ---
if (!config.SAFETY_SETTINGS || !Array.isArray(config.SAFETY_SETTINGS)) {
config.SAFETY_SETTINGS = []; // 默认为空数组
}
// --- 结束:处理 SAFETY_SETTINGS 默认值 ---
// --- 新增:处理自动删除错误日志配置的默认值 ---
if (typeof config.AUTO_DELETE_ERROR_LOGS_ENABLED === "undefined") {
config.AUTO_DELETE_ERROR_LOGS_ENABLED = false;
}
if (typeof config.AUTO_DELETE_ERROR_LOGS_DAYS === "undefined") {
config.AUTO_DELETE_ERROR_LOGS_DAYS = 7;
}
// --- 结束:处理自动删除错误日志配置的默认值 ---
// --- 新增:处理自动删除请求日志配置的默认值 ---
if (typeof config.AUTO_DELETE_REQUEST_LOGS_ENABLED === "undefined") {
config.AUTO_DELETE_REQUEST_LOGS_ENABLED = false;
}
if (typeof config.AUTO_DELETE_REQUEST_LOGS_DAYS === "undefined") {
config.AUTO_DELETE_REQUEST_LOGS_DAYS = 30;
}
// --- 结束:处理自动删除请求日志配置的默认值 ---
// --- 新增:处理假流式配置的默认值 ---
if (typeof config.FAKE_STREAM_ENABLED === "undefined") {
config.FAKE_STREAM_ENABLED = false;
}
if (typeof config.FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS === "undefined") {
config.FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS = 5;
}
// --- 结束:处理假流式配置的默认值 ---
populateForm(config);
// After populateForm, initialize masking for all populated sensitive fields
if (configForm) {
// Ensure form exists
initializeSensitiveFields(); // Call initializeSensitiveFields to handle initial masking
}
// Ensure upload provider has a default value
const uploadProvider = document.getElementById("UPLOAD_PROVIDER");
if (uploadProvider && !uploadProvider.value) {
uploadProvider.value = "smms"; // 设置默认值为 smms
toggleProviderConfig("smms");
}
showNotification("配置加载成功", "success");
} catch (error) {
console.error("加载配置失败:", error);
showNotification("加载配置失败: " + error.message, "error");
// 加载失败时,使用默认配置
const defaultConfig = {
API_KEYS: [""],
ALLOWED_TOKENS: [""],
IMAGE_MODELS: ["gemini-1.5-pro-latest"],
SEARCH_MODELS: ["gemini-1.5-flash-latest"],
FILTERED_MODELS: ["gemini-1.0-pro-latest"],
UPLOAD_PROVIDER: "smms",
PROXIES: [],
VERTEX_API_KEYS: [], // 确保默认值存在
VERTEX_EXPRESS_BASE_URL: "", // 确保默认值存在
THINKING_MODELS: [],
THINKING_BUDGET_MAP: {},
AUTO_DELETE_ERROR_LOGS_ENABLED: false,
AUTO_DELETE_ERROR_LOGS_DAYS: 7, // 新增默认值
AUTO_DELETE_REQUEST_LOGS_ENABLED: false, // 新增默认值
AUTO_DELETE_REQUEST_LOGS_DAYS: 30, // 新增默认值
// --- 新增:处理假流式配置的默认值 ---
FAKE_STREAM_ENABLED: false,
FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS: 5,
// --- 结束:处理假流式配置的默认值 ---
};
populateForm(defaultConfig);
if (configForm) {
// Ensure form exists
initializeSensitiveFields(); // Call initializeSensitiveFields to handle initial masking
}
toggleProviderConfig("smms");
}
}
/**
* Populates the configuration form with data.
* @param {object} config - The configuration object.
*/
function populateForm(config) {
const modelIdMap = {}; // modelName -> modelId
// 1. Clear existing dynamic content first
const arrayContainers = document.querySelectorAll(".array-container");
arrayContainers.forEach((container) => {
container.innerHTML = ""; // Clear all array containers
});
const budgetMapContainer = document.getElementById(
"THINKING_BUDGET_MAP_container"
);
if (budgetMapContainer) {
budgetMapContainer.innerHTML = ""; // Clear budget map container
} else {
console.error("Critical: THINKING_BUDGET_MAP_container not found!");
return; // Cannot proceed
}
// 2. Populate THINKING_MODELS and build the map
if (Array.isArray(config.THINKING_MODELS)) {
const container = document.getElementById("THINKING_MODELS_container");
if (container) {
config.THINKING_MODELS.forEach((modelName) => {
if (modelName && typeof modelName === "string" && modelName.trim()) {
const trimmedModelName = modelName.trim();
const modelId = addArrayItemWithValue(
"THINKING_MODELS",
trimmedModelName
);
if (modelId) {
modelIdMap[trimmedModelName] = modelId;
} else {
console.warn(
`Failed to get modelId for THINKING_MODEL: '${trimmedModelName}'`
);
}
} else {
console.warn(`Invalid THINKING_MODEL entry found:`, modelName);
}
});
} else {
console.error("Critical: THINKING_MODELS_container not found!");
}
}
// 3. Populate THINKING_BUDGET_MAP using the map
let budgetItemsAdded = false;
if (
config.THINKING_BUDGET_MAP &&
typeof config.THINKING_BUDGET_MAP === "object"
) {
for (const [modelName, budgetValue] of Object.entries(
config.THINKING_BUDGET_MAP
)) {
if (modelName && typeof modelName === "string") {
const trimmedModelName = modelName.trim();
const modelId = modelIdMap[trimmedModelName]; // Look up the ID
if (modelId) {
createAndAppendBudgetMapItem(trimmedModelName, budgetValue, modelId);
budgetItemsAdded = true;
} else {
console.warn(
`Budget map: Could not find model ID for '${trimmedModelName}'. Skipping budget item.`
);
}
} else {
console.warn(`Invalid key found in THINKING_BUDGET_MAP:`, modelName);
}
}
}
if (!budgetItemsAdded && budgetMapContainer) {
budgetMapContainer.innerHTML =
'<div class="text-gray-500 text-sm italic">请在上方添加思考模型,预算将自动关联。</div>';
}
// 4. Populate other array fields (excluding THINKING_MODELS)
for (const [key, value] of Object.entries(config)) {
if (Array.isArray(value) && key !== "THINKING_MODELS") {
const container = document.getElementById(`${key}_container`);
if (container) {
value.forEach((itemValue) => {
if (typeof itemValue === "string") {
addArrayItemWithValue(key, itemValue);
} else {
console.warn(`Invalid item found in array '${key}':`, itemValue);
}
});
}
}
}
// 5. Populate non-array/non-budget fields
for (const [key, value] of Object.entries(config)) {
if (
!Array.isArray(value) &&
!(
typeof value === "object" &&
value !== null &&
key === "THINKING_BUDGET_MAP"
)
) {
const element = document.getElementById(key);
if (element) {
if (element.type === "checkbox" && typeof value === "boolean") {
element.checked = value;
} else if (element.type !== "checkbox") {
if (key === "LOG_LEVEL" && typeof value === "string") {
element.value = value.toUpperCase();
} else {
element.value = value !== null && value !== undefined ? value : "";
}
}
}
}
}
// 6. Initialize upload provider
const uploadProvider = document.getElementById("UPLOAD_PROVIDER");
if (uploadProvider) {
toggleProviderConfig(uploadProvider.value);
}
// Populate SAFETY_SETTINGS
let safetyItemsAdded = false;
if (safetySettingsContainer && Array.isArray(config.SAFETY_SETTINGS)) {
config.SAFETY_SETTINGS.forEach((setting) => {
if (
setting &&
typeof setting === "object" &&
setting.category &&
setting.threshold
) {
addSafetySettingItem(setting.category, setting.threshold);
safetyItemsAdded = true;
} else {
console.warn("Invalid safety setting item found:", setting);
}
});
}
if (safetySettingsContainer && !safetyItemsAdded) {
safetySettingsContainer.innerHTML =
'<div class="text-gray-500 text-sm italic">定义模型的安全过滤阈值。</div>';
}
// --- 新增:处理自动删除错误日志的字段 ---
const autoDeleteEnabledCheckbox = document.getElementById(
"AUTO_DELETE_ERROR_LOGS_ENABLED"
);
const autoDeleteDaysSelect = document.getElementById(
"AUTO_DELETE_ERROR_LOGS_DAYS"
);
if (autoDeleteEnabledCheckbox && autoDeleteDaysSelect) {
autoDeleteEnabledCheckbox.checked = !!config.AUTO_DELETE_ERROR_LOGS_ENABLED; // 确保是布尔值
autoDeleteDaysSelect.value = config.AUTO_DELETE_ERROR_LOGS_DAYS || 7; // 默认7天
// 根据复选框状态设置下拉框的禁用状态
autoDeleteDaysSelect.disabled = !autoDeleteEnabledCheckbox.checked;
// 添加事件监听器
autoDeleteEnabledCheckbox.addEventListener("change", function () {
autoDeleteDaysSelect.disabled = !this.checked;
});
}
// --- 结束:处理自动删除错误日志的字段 ---
// --- 新增:处理自动删除请求日志的字段 ---
const autoDeleteRequestEnabledCheckbox = document.getElementById(
"AUTO_DELETE_REQUEST_LOGS_ENABLED"
);
const autoDeleteRequestDaysSelect = document.getElementById(
"AUTO_DELETE_REQUEST_LOGS_DAYS"
);
if (autoDeleteRequestEnabledCheckbox && autoDeleteRequestDaysSelect) {
autoDeleteRequestEnabledCheckbox.checked =
!!config.AUTO_DELETE_REQUEST_LOGS_ENABLED;
autoDeleteRequestDaysSelect.value =
config.AUTO_DELETE_REQUEST_LOGS_DAYS || 30;
autoDeleteRequestDaysSelect.disabled =
!autoDeleteRequestEnabledCheckbox.checked;
autoDeleteRequestEnabledCheckbox.addEventListener("change", function () {
autoDeleteRequestDaysSelect.disabled = !this.checked;
});
}
// --- 结束:处理自动删除请求日志的字段 ---
// --- 新增:处理假流式配置的字段 ---
const fakeStreamEnabledCheckbox = document.getElementById(
"FAKE_STREAM_ENABLED"
);
const fakeStreamIntervalInput = document.getElementById(
"FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS"
);
if (fakeStreamEnabledCheckbox && fakeStreamIntervalInput) {
fakeStreamEnabledCheckbox.checked = !!config.FAKE_STREAM_ENABLED;
fakeStreamIntervalInput.value =
config.FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS || 5;
// 根据复选框状态设置输入框的禁用状态 (如果需要)
// fakeStreamIntervalInput.disabled = !fakeStreamEnabledCheckbox.checked;
// fakeStreamEnabledCheckbox.addEventListener("change", function () {
// fakeStreamIntervalInput.disabled = !this.checked;
// });
}
// --- 结束:处理假流式配置的字段 ---
}
/**
* Handles the bulk addition of API keys from the modal input.
*/
function handleBulkAddApiKeys() {
const apiKeyContainer = document.getElementById("API_KEYS_container");
if (!apiKeyBulkInput || !apiKeyContainer || !apiKeyModal) return;
const bulkText = apiKeyBulkInput.value;
const extractedKeys = bulkText.match(API_KEY_REGEX) || [];
const currentKeyInputs = apiKeyContainer.querySelectorAll(
`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`
);
let currentKeys = Array.from(currentKeyInputs)
.map((input) => {
return input.hasAttribute("data-real-value")
? input.getAttribute("data-real-value")
: input.value;
})
.filter((key) => key && key.trim() !== "" && key !== MASKED_VALUE);
const combinedKeys = new Set([...currentKeys, ...extractedKeys]);
const uniqueKeys = Array.from(combinedKeys);
apiKeyContainer.innerHTML = ""; // Clear existing items more directly
uniqueKeys.forEach((key) => {
addArrayItemWithValue("API_KEYS", key);
});
const newKeyInputs = apiKeyContainer.querySelectorAll(
`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`
);
newKeyInputs.forEach((input) => {
if (configForm && typeof initializeSensitiveFields === "function") {
const focusoutEvent = new Event("focusout", {
bubbles: true,
cancelable: true,
});
input.dispatchEvent(focusoutEvent);
}
});
closeModal(apiKeyModal);
showNotification(`添加/更新了 ${uniqueKeys.length} 个唯一密钥`, "success");
}
/**
* Handles searching/filtering of API keys in the list.
*/
function handleApiKeySearch() {
const apiKeyContainer = document.getElementById("API_KEYS_container");
if (!apiKeySearchInput || !apiKeyContainer) return;
const searchTerm = apiKeySearchInput.value.toLowerCase();
const keyItems = apiKeyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`);
keyItems.forEach((item) => {
const input = item.querySelector(
`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`
);
if (input) {
const realValue = input.hasAttribute("data-real-value")
? input.getAttribute("data-real-value").toLowerCase()
: input.value.toLowerCase();
item.style.display = realValue.includes(searchTerm) ? "flex" : "none";
}
});
}
/**
* Handles the bulk deletion of API keys based on input from the modal.
*/
function handleBulkDeleteApiKeys() {
const apiKeyContainer = document.getElementById("API_KEYS_container");
if (!bulkDeleteApiKeyInput || !apiKeyContainer || !bulkDeleteApiKeyModal)
return;
const bulkText = bulkDeleteApiKeyInput.value;
if (!bulkText.trim()) {
showNotification("请粘贴需要删除的 API 密钥", "warning");
return;
}
const keysToDelete = new Set(bulkText.match(API_KEY_REGEX) || []);
if (keysToDelete.size === 0) {
showNotification("未在输入内容中提取到有效的 API 密钥格式", "warning");
return;
}
const keyItems = apiKeyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`);
let deleteCount = 0;
keyItems.forEach((item) => {
const input = item.querySelector(
`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`
);
const realValue =
input &&
(input.hasAttribute("data-real-value")
? input.getAttribute("data-real-value")
: input.value);
if (realValue && keysToDelete.has(realValue)) {
item.remove();
deleteCount++;
}
});
closeModal(bulkDeleteApiKeyModal);
if (deleteCount > 0) {
showNotification(`成功删除了 ${deleteCount} 个匹配的密钥`, "success");
} else {
showNotification("列表中未找到您输入的任何密钥进行删除", "info");
}
bulkDeleteApiKeyInput.value = "";
}
/**
* Handles the bulk addition of proxies from the modal input.
*/
function handleBulkAddProxies() {
const proxyContainer = document.getElementById("PROXIES_container");
if (!proxyBulkInput || !proxyContainer || !proxyModal) return;
const bulkText = proxyBulkInput.value;
const extractedProxies = bulkText.match(PROXY_REGEX) || [];
const currentProxyInputs = proxyContainer.querySelectorAll(
`.${ARRAY_INPUT_CLASS}`
);
const currentProxies = Array.from(currentProxyInputs)
.map((input) => input.value)
.filter((proxy) => proxy.trim() !== "");
const combinedProxies = new Set([...currentProxies, ...extractedProxies]);
const uniqueProxies = Array.from(combinedProxies);
proxyContainer.innerHTML = ""; // Clear existing items
uniqueProxies.forEach((proxy) => {
addArrayItemWithValue("PROXIES", proxy);
});
closeModal(proxyModal);
showNotification(`添加/更新了 ${uniqueProxies.length} 个唯一代理`, "success");
}
/**
* Handles the bulk deletion of proxies based on input from the modal.
*/
function handleBulkDeleteProxies() {
const proxyContainer = document.getElementById("PROXIES_container");
if (!bulkDeleteProxyInput || !proxyContainer || !bulkDeleteProxyModal) return;
const bulkText = bulkDeleteProxyInput.value;
if (!bulkText.trim()) {
showNotification("请粘贴需要删除的代理地址", "warning");
return;
}
const proxiesToDelete = new Set(bulkText.match(PROXY_REGEX) || []);
if (proxiesToDelete.size === 0) {
showNotification("未在输入内容中提取到有效的代理地址格式", "warning");
return;
}
const proxyItems = proxyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`);
let deleteCount = 0;
proxyItems.forEach((item) => {
const input = item.querySelector(`.${ARRAY_INPUT_CLASS}`);
if (input && proxiesToDelete.has(input.value)) {
item.remove();
deleteCount++;
}
});
closeModal(bulkDeleteProxyModal);
if (deleteCount > 0) {
showNotification(`成功删除了 ${deleteCount} 个匹配的代理`, "success");
} else {
showNotification("列表中未找到您输入的任何代理进行删除", "info");
}
bulkDeleteProxyInput.value = "";
}
/**
* Handles the bulk addition of Vertex API keys from the modal input.
*/
function handleBulkAddVertexApiKeys() {
const vertexApiKeyContainer = document.getElementById(
"VERTEX_API_KEYS_container"
);
if (
!vertexApiKeyBulkInput ||
!vertexApiKeyContainer ||
!vertexApiKeyModal
) {
return;
}
const bulkText = vertexApiKeyBulkInput.value;
const extractedKeys = bulkText.match(VERTEX_API_KEY_REGEX) || [];
const currentKeyInputs = vertexApiKeyContainer.querySelectorAll(
`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`
);
let currentKeys = Array.from(currentKeyInputs)
.map((input) => {
return input.hasAttribute("data-real-value")
? input.getAttribute("data-real-value")
: input.value;
})
.filter((key) => key && key.trim() !== "" && key !== MASKED_VALUE);
const combinedKeys = new Set([...currentKeys, ...extractedKeys]);
const uniqueKeys = Array.from(combinedKeys);
vertexApiKeyContainer.innerHTML = ""; // Clear existing items
uniqueKeys.forEach((key) => {
addArrayItemWithValue("VERTEX_API_KEYS", key); // VERTEX_API_KEYS are sensitive
});
// Ensure new sensitive inputs are masked
const newKeyInputs = vertexApiKeyContainer.querySelectorAll(
`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`
);
newKeyInputs.forEach((input) => {
if (configForm && typeof initializeSensitiveFields === "function") {
const focusoutEvent = new Event("focusout", {
bubbles: true,
cancelable: true,
});
input.dispatchEvent(focusoutEvent);
}
});
closeModal(vertexApiKeyModal);
showNotification(
`添加/更新了 ${uniqueKeys.length} 个唯一 Vertex 密钥`,
"success"
);
vertexApiKeyBulkInput.value = "";
}
/**
* Handles the bulk deletion of Vertex API keys based on input from the modal.
*/
function handleBulkDeleteVertexApiKeys() {
const vertexApiKeyContainer = document.getElementById(
"VERTEX_API_KEYS_container"
);
if (
!bulkDeleteVertexApiKeyInput ||
!vertexApiKeyContainer ||
!bulkDeleteVertexApiKeyModal
) {
return;
}
const bulkText = bulkDeleteVertexApiKeyInput.value;
if (!bulkText.trim()) {
showNotification("请粘贴需要删除的 Vertex API 密钥", "warning");
return;
}
const keysToDelete = new Set(bulkText.match(VERTEX_API_KEY_REGEX) || []);
if (keysToDelete.size === 0) {
showNotification(
"未在输入内容中提取到有效的 Vertex API 密钥格式",
"warning"
);
return;
}
const keyItems = vertexApiKeyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`);
let deleteCount = 0;
keyItems.forEach((item) => {
const input = item.querySelector(
`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`
);
const realValue =
input &&
(input.hasAttribute("data-real-value")
? input.getAttribute("data-real-value")
: input.value);
if (realValue && keysToDelete.has(realValue)) {
item.remove();
deleteCount++;
}
});
closeModal(bulkDeleteVertexApiKeyModal);
if (deleteCount > 0) {
showNotification(`成功删除了 ${deleteCount} 个匹配的 Vertex 密钥`, "success");
} else {
showNotification("列表中未找到您输入的任何 Vertex 密钥进行删除", "info");
}
bulkDeleteVertexApiKeyInput.value = "";
}
/**
* Switches the active configuration tab.
* @param {string} tabId - The ID of the tab to switch to.
*/
function switchTab(tabId) {
console.log(`Switching to tab: ${tabId}`);
// 定义选中态和未选中态的样式
const activeStyle = "background-color: #3b82f6 !important; color: #ffffff !important; border: 2px solid #2563eb !important; box-shadow: 0 4px 12px -2px rgba(59, 130, 246, 0.4), 0 2px 6px -1px rgba(59, 130, 246, 0.2) !important; transform: translateY(-2px) !important; font-weight: 600 !important;";
const inactiveStyle = "background-color: #f8fafc !important; color: #64748b !important; border: 2px solid #e2e8f0 !important; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1) !important; font-weight: 500 !important; transform: none !important;";
// 更新标签按钮状态
const tabButtons = document.querySelectorAll(".tab-btn");
console.log(`Found ${tabButtons.length} tab buttons`);
tabButtons.forEach((button) => {
const buttonTabId = button.getAttribute("data-tab");
if (buttonTabId === tabId) {
// 激活状态:直接设置内联样式
button.classList.add("active");
button.setAttribute("style", activeStyle);
console.log(`Applied active style to button: ${buttonTabId}`);
} else {
// 非激活状态:直接设置内联样式
button.classList.remove("active");
button.setAttribute("style", inactiveStyle);
console.log(`Applied inactive style to button: ${buttonTabId}`);
}
});
// 更新内容区域
const sections = document.querySelectorAll(".config-section");
sections.forEach((section) => {
if (section.id === `${tabId}-section`) {
section.classList.add("active");
} else {
section.classList.remove("active");
}
});
}
/**
* Toggles the visibility of configuration sections for different upload providers.
* @param {string} provider - The selected upload provider.
*/
function toggleProviderConfig(provider) {
const providerConfigs = document.querySelectorAll(".provider-config");
providerConfigs.forEach((config) => {
if (config.getAttribute("data-provider") === provider) {
config.classList.add("active");
} else {
config.classList.remove("active");
}
});
}
/**
* Creates and appends an input field for an array item.
* @param {string} key - The configuration key for the array.
* @param {string} value - The initial value for the input field.
* @param {boolean} isSensitive - Whether the input is for sensitive data.
* @param {string|null} modelId - Optional model ID for thinking models.
* @returns {HTMLInputElement} The created input element.
*/
function createArrayInput(key, value, isSensitive, modelId = null) {
const input = document.createElement("input");
input.type = "text";
input.name = `${key}[]`; // Used for form submission if not handled by JS
input.value = value;
let inputClasses = `${ARRAY_INPUT_CLASS} flex-grow px-3 py-2 border-none rounded-l-md focus:outline-none form-input-themed`;
if (isSensitive) {
inputClasses += ` ${SENSITIVE_INPUT_CLASS}`;
}
input.className = inputClasses;
if (modelId) {
input.setAttribute("data-model-id", modelId);
input.placeholder = "思考模型名称";
}
return input;
}
/**
* Creates a generate token button for allowed tokens.
* @returns {HTMLButtonElement} The created button element.
*/
function createGenerateTokenButton() {
const generateBtn = document.createElement("button");
generateBtn.type = "button";
generateBtn.className =
"generate-btn px-2 py-2 text-gray-500 hover:text-primary-600 focus:outline-none rounded-r-md bg-gray-100 hover:bg-gray-200 transition-colors";
generateBtn.innerHTML = '<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;
}
/**
* Adds a new item to an array configuration section (e.g., API_KEYS, ALLOWED_TOKENS).
* This function is typically called by a "+" button.
* @param {string} key - The configuration key for the array (e.g., 'API_KEYS').
*/
function addArrayItem(key) {
const container = document.getElementById(`${key}_container`);
if (!container) return;
const newItemValue = ""; // New items start empty
const modelId = addArrayItemWithValue(key, newItemValue); // This adds the DOM element
if (key === "THINKING_MODELS" && modelId) {
createAndAppendBudgetMapItem(newItemValue, 0, modelId); // Default budget 0
}
}
/**
* Adds an array item with a specific value to the DOM.
* This is used both for initially populating the form and for adding new items.
* @param {string} key - The configuration key (e.g., 'API_KEYS', 'THINKING_MODELS').
* @param {string} value - The value for the array item.
* @returns {string|null} The generated modelId if it's a thinking model, otherwise null.
*/
function addArrayItemWithValue(key, value) {
const container = document.getElementById(`${key}_container`);
if (!container) return null;
const isThinkingModel = key === "THINKING_MODELS";
const isAllowedToken = key === "ALLOWED_TOKENS";
const isVertexApiKey = key === "VERTEX_API_KEYS"; // 新增判断
const isSensitive =
key === "API_KEYS" || isAllowedToken || isVertexApiKey; // 更新敏感判断
const modelId = isThinkingModel ? generateUUID() : null;
const arrayItem = document.createElement("div");
arrayItem.className = `${ARRAY_ITEM_CLASS} flex items-center mb-2 gap-2`;
if (isThinkingModel) {
arrayItem.setAttribute("data-model-id", modelId);
}
const inputWrapper = document.createElement("div");
inputWrapper.className =
"flex items-center flex-grow rounded-md focus-within:border-blue-500 focus-within:ring focus-within:ring-blue-500 focus-within:ring-opacity-50";
// Apply light theme border directly via style
inputWrapper.style.border = "1px solid rgba(0, 0, 0, 0.12)";
inputWrapper.style.backgroundColor = "transparent"; // Ensure wrapper is transparent
const input = createArrayInput(
key,
value,
isSensitive,
isThinkingModel ? modelId : null
);
inputWrapper.appendChild(input);
if (isAllowedToken) {
const generateBtn = createGenerateTokenButton();
inputWrapper.appendChild(generateBtn);
} else {
// Ensure right-side rounding if no button is present
input.classList.add("rounded-r-md");
}
const removeBtn = createRemoveButton();
arrayItem.appendChild(inputWrapper);
arrayItem.appendChild(removeBtn);
container.appendChild(arrayItem);
// Initialize sensitive field if applicable
if (isSensitive && input.value) {
if (configForm && typeof initializeSensitiveFields === "function") {
const focusoutEvent = new Event("focusout", {
bubbles: true,
cancelable: true,
});
input.dispatchEvent(focusoutEvent);
}
}
return isThinkingModel ? modelId : null;
}
/**
* Creates and appends a DOM element for a thinking model's budget mapping.
* @param {string} mapKey - The model name (key for the map).
* @param {number|string} mapValue - The budget value.
* @param {string} modelId - The unique ID of the corresponding thinking model.
*/
function createAndAppendBudgetMapItem(mapKey, mapValue, modelId) {
const container = document.getElementById("THINKING_BUDGET_MAP_container");
if (!container) {
console.error(
"Cannot add budget item: THINKING_BUDGET_MAP_container not found!"
);
return;
}
// If container currently only has the placeholder, clear it
const placeholder = container.querySelector(".text-gray-500.italic");
// Check if the only child is the placeholder before clearing
if (
placeholder &&
container.children.length === 1 &&
container.firstChild === placeholder
) {
container.innerHTML = "";
}
const mapItem = document.createElement("div");
mapItem.className = `${MAP_ITEM_CLASS} flex items-center mb-2 gap-2`;
mapItem.setAttribute("data-model-id", modelId);
const keyInput = document.createElement("input");
keyInput.type = "text";
keyInput.value = mapKey;
keyInput.placeholder = "模型名称 (自动关联)";
keyInput.readOnly = true;
keyInput.className = `${MAP_KEY_INPUT_CLASS} flex-grow px-3 py-2 border border-gray-300 rounded-md focus:outline-none bg-gray-100 text-gray-500`;
keyInput.setAttribute("data-model-id", modelId);
const valueInput = document.createElement("input");
valueInput.type = "number";
const intValue = parseInt(mapValue, 10);
valueInput.value = isNaN(intValue) ? 0 : intValue;
valueInput.placeholder = "预算 (整数)";
valueInput.className = `${MAP_VALUE_INPUT_CLASS} w-24 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50`;
valueInput.min = -1;
valueInput.max = 32767;
valueInput.addEventListener("input", function () {
let val = this.value.replace(/[^0-9-]/g, "");
if (val !== "") {
val = parseInt(val, 10);
if (val < -1) val = -1;
if (val > 32767) val = 32767;
}
this.value = val; // Corrected variable name
});
// Remove Button - Removed for budget map items
// const removeBtn = document.createElement('button');
// removeBtn.type = 'button';
// removeBtn.className = 'remove-btn text-gray-300 cursor-not-allowed focus:outline-none'; // Kept original class for reference
// removeBtn.innerHTML = '<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);
}
/**
* Collects all data from the configuration form.
* @returns {object} An object containing all configuration data.
*/
function collectFormData() {
const formData = {};
// 处理普通输入和 select
const inputsAndSelects = document.querySelectorAll(
'input[type="text"], input[type="number"], input[type="password"], select, textarea'
);
inputsAndSelects.forEach((element) => {
if (
element.name &&
!element.name.includes("[]") &&
!element.closest(".array-container") &&
!element.closest(`.${MAP_ITEM_CLASS}`) &&
!element.closest(`.${SAFETY_SETTING_ITEM_CLASS}`)
) {
if (element.type === "number") {
formData[element.name] = parseFloat(element.value);
} else if (
element.classList.contains(SENSITIVE_INPUT_CLASS) &&
element.hasAttribute("data-real-value")
) {
formData[element.name] = element.getAttribute("data-real-value");
} else {
formData[element.name] = element.value;
}
}
});
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach((checkbox) => {
formData[checkbox.name] = checkbox.checked;
});
const arrayContainers = document.querySelectorAll(".array-container");
arrayContainers.forEach((container) => {
const key = container.id.replace("_container", "");
const arrayInputs = container.querySelectorAll(`.${ARRAY_INPUT_CLASS}`);
formData[key] = Array.from(arrayInputs)
.map((input) => {
if (
input.classList.contains(SENSITIVE_INPUT_CLASS) &&
input.hasAttribute("data-real-value")
) {
return input.getAttribute("data-real-value");
}
return input.value;
})
.filter(
(value) => value && value.trim() !== "" && value !== MASKED_VALUE
); // Ensure MASKED_VALUE is also filtered if not handled
});
const budgetMapContainer = document.getElementById(
"THINKING_BUDGET_MAP_container"
);
if (budgetMapContainer) {
formData["THINKING_BUDGET_MAP"] = {};
const mapItems = budgetMapContainer.querySelectorAll(`.${MAP_ITEM_CLASS}`);
mapItems.forEach((item) => {
const keyInput = item.querySelector(`.${MAP_KEY_INPUT_CLASS}`);
const valueInput = item.querySelector(`.${MAP_VALUE_INPUT_CLASS}`);
if (keyInput && valueInput && keyInput.value.trim() !== "") {
const budgetValue = parseInt(valueInput.value, 10);
formData["THINKING_BUDGET_MAP"][keyInput.value.trim()] = isNaN(
budgetValue
)
? 0
: budgetValue;
}
});
}
if (safetySettingsContainer) {
formData["SAFETY_SETTINGS"] = [];
const settingItems = safetySettingsContainer.querySelectorAll(
`.${SAFETY_SETTING_ITEM_CLASS}`
);
settingItems.forEach((item) => {
const categorySelect = item.querySelector(".safety-category-select");
const thresholdSelect = item.querySelector(".safety-threshold-select");
if (
categorySelect &&
thresholdSelect &&
categorySelect.value &&
thresholdSelect.value
) {
formData["SAFETY_SETTINGS"].push({
category: categorySelect.value,
threshold: thresholdSelect.value,
});
}
});
}
// --- 新增:收集自动删除错误日志的配置 ---
const autoDeleteEnabledCheckbox = document.getElementById(
"AUTO_DELETE_ERROR_LOGS_ENABLED"
);
if (autoDeleteEnabledCheckbox) {
formData["AUTO_DELETE_ERROR_LOGS_ENABLED"] =
autoDeleteEnabledCheckbox.checked;
}
const autoDeleteDaysSelect = document.getElementById(
"AUTO_DELETE_ERROR_LOGS_DAYS"
);
if (autoDeleteDaysSelect) {
// 如果复选框未选中,则不应提交天数,或者可以提交一个默认/无效值,
// 但后端应该只在 ENABLED 为 true 时才关心 DAYS。
// 这里我们总是收集它,后端逻辑会处理。
formData["AUTO_DELETE_ERROR_LOGS_DAYS"] = parseInt(
autoDeleteDaysSelect.value,
10
);
}
// --- 结束:收集自动删除错误日志的配置 ---
// --- 新增:收集自动删除请求日志的配置 ---
const autoDeleteRequestEnabledCheckbox = document.getElementById(
"AUTO_DELETE_REQUEST_LOGS_ENABLED"
);
if (autoDeleteRequestEnabledCheckbox) {
formData["AUTO_DELETE_REQUEST_LOGS_ENABLED"] =
autoDeleteRequestEnabledCheckbox.checked;
}
const autoDeleteRequestDaysSelect = document.getElementById(
"AUTO_DELETE_REQUEST_LOGS_DAYS"
);
if (autoDeleteRequestDaysSelect) {
formData["AUTO_DELETE_REQUEST_LOGS_DAYS"] = parseInt(
autoDeleteRequestDaysSelect.value,
10
);
}
// --- 结束:收集自动删除请求日志的配置 ---
// --- 新增:收集假流式配置 ---
const fakeStreamEnabledCheckbox = document.getElementById(
"FAKE_STREAM_ENABLED"
);
if (fakeStreamEnabledCheckbox) {
formData["FAKE_STREAM_ENABLED"] = fakeStreamEnabledCheckbox.checked;
}
const fakeStreamIntervalInput = document.getElementById(
"FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS"
);
if (fakeStreamIntervalInput) {
formData["FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS"] = parseInt(
fakeStreamIntervalInput.value,
10
);
}
// --- 结束:收集假流式配置 ---
return formData;
}
/**
* Stops the scheduler task on the server.
*/
async function stopScheduler() {
try {
const response = await fetch("/api/scheduler/stop", { method: "POST" });
if (!response.ok) {
console.warn(`停止定时任务失败: ${response.status}`);
} else {
console.log("定时任务已停止");
}
} catch (error) {
console.error("调用停止定时任务API时出错:", error);
}
}
/**
* Starts the scheduler task on the server.
*/
async function startScheduler() {
try {
const response = await fetch("/api/scheduler/start", { method: "POST" });
if (!response.ok) {
console.warn(`启动定时任务失败: ${response.status}`);
} else {
console.log("定时任务已启动");
}
} catch (error) {
console.error("调用启动定时任务API时出错:", error);
}
}
/**
* Saves the current configuration to the server.
*/
async function saveConfig() {
try {
const formData = collectFormData();
showNotification("正在保存配置...", "info");
// 1. 停止定时任务
await stopScheduler();
const response = await fetch("/api/config", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(
errorData.detail || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
// 移除居中的 saveStatus 提示
showNotification("配置保存成功", "success");
// 3. 启动新的定时任务
await startScheduler();
} catch (error) {
console.error("保存配置失败:", error);
// 保存失败时,也尝试重启定时任务,以防万一
await startScheduler();
// 移除居中的 saveStatus 提示
showNotification("保存配置失败: " + error.message, "error");
}
}
/**
* Initiates the configuration reset process by showing a confirmation modal.
* @param {Event} [event] - The click event, if triggered by a button.
*/
function resetConfig(event) {
// 阻止事件冒泡和默认行为
if (event) {
event.preventDefault();
event.stopPropagation();
}
console.log(
"resetConfig called. Event target:",
event ? event.target.id : "No event"
);
// Ensure modal is shown only if the event comes from the reset button
if (
!event ||
event.target.id === "resetBtn" ||
(event.currentTarget && event.currentTarget.id === "resetBtn")
) {
if (resetConfirmModal) {
openModal(resetConfirmModal);
} else {
console.error(
"Reset confirmation modal not found! Falling back to default confirm."
);
if (confirm("确定要重置所有配置吗?这将恢复到默认值。")) {
executeReset();
}
}
}
}
/**
* Executes the actual configuration reset after confirmation.
*/
async function executeReset() {
try {
showNotification("正在重置配置...", "info");
// 1. 停止定时任务
await stopScheduler();
const response = await fetch("/api/config/reset", { method: "POST" });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const config = await response.json();
populateForm(config);
// Re-initialize masking for sensitive fields after reset
if (configForm && typeof initializeSensitiveFields === "function") {
const sensitiveFields = configForm.querySelectorAll(
`.${SENSITIVE_INPUT_CLASS}`
);
sensitiveFields.forEach((field) => {
if (field.type === "password") {
if (field.value) field.setAttribute("data-real-value", field.value);
} else if (
field.type === "text" ||
field.tagName.toLowerCase() === "textarea"
) {
const focusoutEvent = new Event("focusout", {
bubbles: true,
cancelable: true,
});
field.dispatchEvent(focusoutEvent);
}
});
}
showNotification("配置已重置为默认值", "success");
// 3. 启动新的定时任务
await startScheduler();
} catch (error) {
console.error("重置配置失败:", error);
showNotification("重置配置失败: " + error.message, "error");
// 重置失败时,也尝试重启定时任务
await startScheduler();
}
}
/**
* Displays a notification message to the user.
* @param {string} message - The message to display.
* @param {string} [type='info'] - The type of notification ('info', 'success', 'error', 'warning').
*/
function showNotification(message, type = "info") {
const notification = document.getElementById("notification");
notification.textContent = message;
// 统一样式为黑色半透明,与 keys_status.js 保持一致
notification.classList.remove("bg-danger-500");
notification.classList.add("bg-black");
notification.style.backgroundColor = "rgba(0,0,0,0.8)";
notification.style.color = "#fff";
// 应用过渡效果
notification.style.opacity = "1";
notification.style.transform = "translate(-50%, 0)";
// 设置自动消失
setTimeout(() => {
notification.style.opacity = "0";
notification.style.transform = "translate(-50%, 10px)";
}, 3000);
}
/**
* Refreshes the current page.
* @param {HTMLButtonElement} [button] - The button that triggered the refresh (to show loading state).
*/
function refreshPage(button) {
if (button) button.classList.add("loading");
location.reload();
}
/**
* Scrolls the page to the top.
*/
function scrollToTop() {
window.scrollTo({ top: 0, behavior: "smooth" });
}
/**
* Scrolls the page to the bottom.
*/
function scrollToBottom() {
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
}
/**
* Toggles the visibility of scroll-to-top/bottom buttons based on scroll position.
*/
function toggleScrollButtons() {
const scrollButtons = document.querySelector(".scroll-buttons");
if (scrollButtons) {
scrollButtons.style.display = window.scrollY > 200 ? "flex" : "none";
}
}
/**
* Generates a random token string.
* @returns {string} A randomly generated token.
*/
function generateRandomToken() {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_";
const length = 48;
let result = "sk-";
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
}
/**
* Adds a new safety setting item to the DOM.
* @param {string} [category=''] - The initial category for the setting.
* @param {string} [threshold=''] - The initial threshold for the setting.
*/
function addSafetySettingItem(category = "", threshold = "") {
const container = document.getElementById("SAFETY_SETTINGS_container");
if (!container) {
console.error(
"Cannot add safety setting: SAFETY_SETTINGS_container not found!"
);
return;
}
// 如果容器当前只有占位符,则清除它
const placeholder = container.querySelector(".text-gray-500.italic");
if (
placeholder &&
container.children.length === 1 &&
container.firstChild === placeholder
) {
container.innerHTML = "";
}
const harmCategories = [
"HARM_CATEGORY_HARASSMENT",
"HARM_CATEGORY_HATE_SPEECH",
"HARM_CATEGORY_SEXUALLY_EXPLICIT",
"HARM_CATEGORY_DANGEROUS_CONTENT",
"HARM_CATEGORY_CIVIC_INTEGRITY", // 根据需要添加或移除
];
const harmThresholds = [
"BLOCK_NONE",
"BLOCK_LOW_AND_ABOVE",
"BLOCK_MEDIUM_AND_ABOVE",
"BLOCK_ONLY_HIGH",
"OFF", // 根据 Google API 文档添加或移除
];
const settingItem = document.createElement("div");
settingItem.className = `${SAFETY_SETTING_ITEM_CLASS} flex items-center mb-2 gap-2`;
const categorySelect = document.createElement("select");
categorySelect.className =
"safety-category-select flex-grow px-3 py-2 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 form-select-themed";
harmCategories.forEach((cat) => {
const option = document.createElement("option");
option.value = cat;
option.textContent = cat.replace("HARM_CATEGORY_", "");
if (cat === category) option.selected = true;
categorySelect.appendChild(option);
});
const thresholdSelect = document.createElement("select");
thresholdSelect.className =
"safety-threshold-select w-48 px-3 py-2 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 form-select-themed";
harmThresholds.forEach((thr) => {
const option = document.createElement("option");
option.value = thr;
option.textContent = thr.replace("BLOCK_", "").replace("_AND_ABOVE", "+");
if (thr === threshold) option.selected = true;
thresholdSelect.appendChild(option);
});
const removeBtn = document.createElement("button");
removeBtn.type = "button";
removeBtn.className =
"remove-btn text-gray-400 hover:text-red-500 focus:outline-none transition-colors duration-150";
removeBtn.innerHTML = '<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, 0, modelId);
}
}
if (modelHelperModal) closeModal(modelHelperModal);
currentModelHelperTarget = null; // Reset target
}
// -- End Model Helper Functions --