mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-05-11 18:09:55 +08:00
本次提交引入了一项新功能,允许模型在对话中理解所提供URL的上下文。 主要变更: - **配置**:新增了 `URL_CONTEXT_ENABLED` 和 `URL_CONTEXT_MODELS` 两个配置项,用于控制此功能的开关和支持的模型列表。 - **后端服务**:在 `gemini_chat_service`、`openai_chat_service` 和 `vertex_express_chat_service` 中,为支持的模型动态添加 `urlContext` 工具。 - **前端界面**:在配置编辑器页面增加了相应的UI控件,方便用户启用/禁用该功能并管理支持的模型列表。 - **文档**:更新了 `.env.example`、`README.md` 和 `README_ZH.md`,包含了新配置项的说明。
2302 lines
77 KiB
JavaScript
2302 lines
77 KiB
JavaScript
// 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
|
|
|
|
// 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();
|
|
|
|
// 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());
|
|
}
|
|
|
|
// 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);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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 = {}; // 默认为空对象
|
|
}
|
|
// --- 新增:处理 CUSTOM_HEADERS 默认值 ---
|
|
if (
|
|
!config.CUSTOM_HEADERS ||
|
|
typeof config.CUSTOM_HEADERS !== "object" ||
|
|
config.CUSTOM_HEADERS === null
|
|
) {
|
|
config.CUSTOM_HEADERS = {}; // 默认为空对象
|
|
}
|
|
// --- 新增:处理 SAFETY_SETTINGS 默认值 ---
|
|
if (!config.SAFETY_SETTINGS || !Array.isArray(config.SAFETY_SETTINGS)) {
|
|
config.SAFETY_SETTINGS = []; // 默认为空数组
|
|
}
|
|
// --- 结束:处理 SAFETY_SETTINGS 默认值 ---
|
|
if (typeof config.URL_CONTEXT_ENABLED === "undefined") {
|
|
config.URL_CONTEXT_ENABLED = true;
|
|
}
|
|
if (!config.URL_CONTEXT_MODELS || !Array.isArray(config.URL_CONTEXT_MODELS)) {
|
|
config.URL_CONTEXT_MODELS = [];
|
|
}
|
|
|
|
// --- 新增:处理自动删除错误日志配置的默认值 ---
|
|
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: {},
|
|
CUSTOM_HEADERS: {},
|
|
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>';
|
|
}
|
|
|
|
// Populate CUSTOM_HEADERS
|
|
const customHeadersContainer = document.getElementById(
|
|
"CUSTOM_HEADERS_container"
|
|
);
|
|
let customHeadersAdded = false;
|
|
if (
|
|
customHeadersContainer &&
|
|
config.CUSTOM_HEADERS &&
|
|
typeof config.CUSTOM_HEADERS === "object"
|
|
) {
|
|
for (const [key, value] of Object.entries(config.CUSTOM_HEADERS)) {
|
|
createAndAppendCustomHeaderItem(key, value);
|
|
customHeadersAdded = true;
|
|
}
|
|
}
|
|
if (!customHeadersAdded && customHeadersContainer) {
|
|
customHeadersContainer.innerHTML =
|
|
'<div class="text-gray-500 text-sm italic">添加自定义请求头,例如 X-Api-Key: your-key</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 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;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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) ? -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);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
)
|
|
? -1
|
|
: budgetValue;
|
|
}
|
|
});
|
|
}
|
|
|
|
const customHeadersContainer = document.getElementById(
|
|
"CUSTOM_HEADERS_container"
|
|
);
|
|
if (customHeadersContainer) {
|
|
formData["CUSTOM_HEADERS"] = {};
|
|
const customHeaderItems = customHeadersContainer.querySelectorAll(
|
|
`.${CUSTOM_HEADER_ITEM_CLASS}`
|
|
);
|
|
customHeaderItems.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["CUSTOM_HEADERS"][keyInput.value.trim()] =
|
|
valueInput.value.trim();
|
|
}
|
|
});
|
|
}
|
|
|
|
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, -1, modelId);
|
|
}
|
|
}
|
|
|
|
if (modelHelperModal) closeModal(modelHelperModal);
|
|
currentModelHelperTarget = null; // Reset target
|
|
}
|
|
|
|
// -- End Model Helper Functions --
|