|
|
|
|
|
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"; |
|
|
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; |
|
|
const MASKED_VALUE = "••••••••"; |
|
|
|
|
|
|
|
|
const API_KEYS_PER_PAGE = 20; |
|
|
let currentApiKeyPage = 1; |
|
|
let totalApiKeyPages = 1; |
|
|
let allApiKeys = []; |
|
|
let filteredApiKeys = []; |
|
|
|
|
|
|
|
|
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"); |
|
|
|
|
|
|
|
|
const vertexApiKeyModal = document.getElementById("vertexApiKeyModal"); |
|
|
const vertexApiKeyBulkInput = document.getElementById("vertexApiKeyBulkInput"); |
|
|
const bulkDeleteVertexApiKeyModal = document.getElementById( |
|
|
"bulkDeleteVertexApiKeyModal" |
|
|
); |
|
|
const bulkDeleteVertexApiKeyInput = document.getElementById( |
|
|
"bulkDeleteVertexApiKeyInput" |
|
|
); |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
function openModal(modalElement) { |
|
|
if (modalElement) { |
|
|
modalElement.classList.add(SHOW_CLASS); |
|
|
} |
|
|
} |
|
|
|
|
|
function closeModal(modalElement) { |
|
|
if (modalElement) { |
|
|
modalElement.classList.remove(SHOW_CLASS); |
|
|
} |
|
|
} |
|
|
|
|
|
document.addEventListener("DOMContentLoaded", function () { |
|
|
|
|
|
initConfig(); |
|
|
|
|
|
|
|
|
const tabButtons = document.querySelectorAll(".tab-btn"); |
|
|
tabButtons.forEach((button) => { |
|
|
button.addEventListener("click", function (e) { |
|
|
e.stopPropagation(); |
|
|
const tabId = this.getAttribute("data-tab"); |
|
|
switchTab(tabId); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
const uploadProviderSelect = document.getElementById("UPLOAD_PROVIDER"); |
|
|
if (uploadProviderSelect) { |
|
|
uploadProviderSelect.addEventListener("change", function () { |
|
|
toggleProviderConfig(this.value); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const checkIntervalInput = document.getElementById("CHECK_INTERVAL_HOURS"); |
|
|
if (checkIntervalInput) { |
|
|
checkIntervalInput.addEventListener("input", function () { |
|
|
let value = parseFloat(this.value); |
|
|
if (isNaN(value) || value < 0) { |
|
|
this.value = 0; |
|
|
} |
|
|
}); |
|
|
|
|
|
checkIntervalInput.addEventListener("change", function () { |
|
|
let value = parseFloat(this.value); |
|
|
if (isNaN(value) || value < 0) { |
|
|
this.value = 0; |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
const saveBtn = document.getElementById("saveBtn"); |
|
|
if (saveBtn) { |
|
|
saveBtn.addEventListener("click", saveConfig); |
|
|
} |
|
|
|
|
|
|
|
|
const resetBtn = document.getElementById("resetBtn"); |
|
|
if (resetBtn) { |
|
|
resetBtn.addEventListener("click", resetConfig); |
|
|
} |
|
|
|
|
|
|
|
|
window.addEventListener("scroll", toggleScrollButtons); |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
const apiKeyPrevBtn = document.getElementById("apiKeyPrevBtn"); |
|
|
const apiKeyNextBtn = document.getElementById("apiKeyNextBtn"); |
|
|
|
|
|
if (apiKeyPrevBtn) { |
|
|
apiKeyPrevBtn.addEventListener("click", prevApiKeyPage); |
|
|
} |
|
|
if (apiKeyNextBtn) { |
|
|
apiKeyNextBtn.addEventListener("click", nextApiKeyPage); |
|
|
} |
|
|
|
|
|
|
|
|
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 |
|
|
); |
|
|
|
|
|
|
|
|
const addProxyBtn = document.getElementById("addProxyBtn"); |
|
|
const closeProxyModalBtn = document.getElementById("closeProxyModalBtn"); |
|
|
const cancelAddProxyBtn = document.getElementById("cancelAddProxyBtn"); |
|
|
const confirmAddProxyBtn = document.getElementById("confirmAddProxyBtn"); |
|
|
|
|
|
|
|
|
const checkAllProxiesBtn = document.getElementById("checkAllProxiesBtn"); |
|
|
const proxyCheckModal = document.getElementById("proxyCheckModal"); |
|
|
const closeProxyCheckModalBtn = document.getElementById("closeProxyCheckModalBtn"); |
|
|
const closeProxyCheckBtn = document.getElementById("closeProxyCheckBtn"); |
|
|
const retryFailedProxiesBtn = document.getElementById("retryFailedProxiesBtn"); |
|
|
|
|
|
if (addProxyBtn) { |
|
|
addProxyBtn.addEventListener("click", () => { |
|
|
openModal(proxyModal); |
|
|
if (proxyBulkInput) proxyBulkInput.value = ""; |
|
|
}); |
|
|
} |
|
|
|
|
|
if (checkAllProxiesBtn) { |
|
|
checkAllProxiesBtn.addEventListener("click", checkAllProxies); |
|
|
} |
|
|
|
|
|
if (closeProxyCheckModalBtn) { |
|
|
closeProxyCheckModalBtn.addEventListener("click", () => closeModal(proxyCheckModal)); |
|
|
} |
|
|
|
|
|
if (closeProxyCheckBtn) { |
|
|
closeProxyCheckBtn.addEventListener("click", () => closeModal(proxyCheckModal)); |
|
|
} |
|
|
|
|
|
if (retryFailedProxiesBtn) { |
|
|
retryFailedProxiesBtn.addEventListener("click", () => { |
|
|
|
|
|
checkAllProxies(); |
|
|
}); |
|
|
} |
|
|
if (closeProxyModalBtn) |
|
|
closeProxyModalBtn.addEventListener("click", () => closeModal(proxyModal)); |
|
|
if (cancelAddProxyBtn) |
|
|
cancelAddProxyBtn.addEventListener("click", () => closeModal(proxyModal)); |
|
|
if (confirmAddProxyBtn) |
|
|
confirmAddProxyBtn.addEventListener("click", handleBulkAddProxies); |
|
|
|
|
|
|
|
|
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 |
|
|
); |
|
|
|
|
|
|
|
|
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(); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
window.addEventListener("click", (event) => { |
|
|
const modals = [ |
|
|
apiKeyModal, |
|
|
resetConfirmModal, |
|
|
bulkDeleteApiKeyModal, |
|
|
proxyModal, |
|
|
bulkDeleteProxyModal, |
|
|
vertexApiKeyModal, |
|
|
bulkDeleteVertexApiKeyModal, |
|
|
modelHelperModal, |
|
|
]; |
|
|
modals.forEach((modal) => { |
|
|
if (event.target === modal) { |
|
|
closeModal(modal); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const generateAuthTokenBtn = document.getElementById("generateAuthTokenBtn"); |
|
|
const authTokenInput = document.getElementById("AUTH_TOKEN"); |
|
|
if (generateAuthTokenBtn && authTokenInput) { |
|
|
generateAuthTokenBtn.addEventListener("click", function () { |
|
|
const newToken = generateRandomToken(); |
|
|
authTokenInput.value = newToken; |
|
|
if (authTokenInput.classList.contains(SENSITIVE_INPUT_CLASS)) { |
|
|
const event = new Event("focusout", { |
|
|
bubbles: true, |
|
|
cancelable: true, |
|
|
}); |
|
|
authTokenInput.dispatchEvent(event); |
|
|
} |
|
|
showNotification("已生成新认证令牌", "success"); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (configForm) { |
|
|
|
|
|
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(); |
|
|
} |
|
|
|
|
|
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(); |
|
|
|
|
|
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"); |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const addSafetySettingBtn = document.getElementById("addSafetySettingBtn"); |
|
|
if (addSafetySettingBtn) { |
|
|
addSafetySettingBtn.addEventListener("click", () => addSafetySettingItem()); |
|
|
} |
|
|
|
|
|
|
|
|
const addCustomHeaderBtn = document.getElementById("addCustomHeaderBtn"); |
|
|
if (addCustomHeaderBtn) { |
|
|
addCustomHeaderBtn.addEventListener("click", () => addCustomHeaderItem()); |
|
|
} |
|
|
|
|
|
initializeSensitiveFields(); |
|
|
|
|
|
|
|
|
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 |
|
|
); |
|
|
|
|
|
|
|
|
if (closeModelHelperModalBtn) { |
|
|
closeModelHelperModalBtn.addEventListener("click", () => |
|
|
closeModal(modelHelperModal) |
|
|
); |
|
|
} |
|
|
if (cancelModelHelperBtn) { |
|
|
cancelModelHelperBtn.addEventListener("click", () => |
|
|
closeModal(modelHelperModal) |
|
|
); |
|
|
} |
|
|
if (modelHelperSearchInput) { |
|
|
modelHelperSearchInput.addEventListener("input", () => |
|
|
renderModelsInModal() |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
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(); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function initializeSensitiveFields() { |
|
|
if (!configForm) return; |
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
field.removeAttribute("data-real-value"); |
|
|
|
|
|
if (field.value === MASKED_VALUE) field.value = ""; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function unmaskField(field) { |
|
|
if (field.hasAttribute("data-real-value")) { |
|
|
field.value = field.getAttribute("data-real-value"); |
|
|
} |
|
|
|
|
|
else if ( |
|
|
field.value === MASKED_VALUE && |
|
|
!field.hasAttribute("data-real-value") |
|
|
) { |
|
|
field.value = ""; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function initialMaskAllExisting() { |
|
|
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" |
|
|
) { |
|
|
maskField(field); |
|
|
} |
|
|
}); |
|
|
} |
|
|
initialMaskAllExisting(); |
|
|
|
|
|
|
|
|
configForm.addEventListener("focusin", function (event) { |
|
|
const target = event.target; |
|
|
if (target.classList.contains(SENSITIVE_INPUT_CLASS)) { |
|
|
if (target.type === "password") { |
|
|
|
|
|
if (!target.hasAttribute("data-original-type")) { |
|
|
target.setAttribute("data-original-type", "password"); |
|
|
} |
|
|
target.type = "text"; |
|
|
|
|
|
if (target.hasAttribute("data-real-value")) { |
|
|
target.value = target.getAttribute("data-real-value"); |
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
unmaskField(target); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
configForm.addEventListener("focusout", function (event) { |
|
|
const target = event.target; |
|
|
if (target.classList.contains(SENSITIVE_INPUT_CLASS)) { |
|
|
|
|
|
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) { |
|
|
|
|
|
target.removeAttribute("data-real-value"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if ( |
|
|
target.getAttribute("data-original-type") === "password" && |
|
|
target.type === "text" |
|
|
) { |
|
|
target.type = "password"; |
|
|
|
|
|
|
|
|
} else if ( |
|
|
target.type === "text" || |
|
|
target.tagName.toLowerCase() === "textarea" |
|
|
) { |
|
|
|
|
|
maskField(target); |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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"]; |
|
|
} |
|
|
|
|
|
if (!config.VERTEX_API_KEYS || !Array.isArray(config.VERTEX_API_KEYS)) { |
|
|
config.VERTEX_API_KEYS = []; |
|
|
} |
|
|
|
|
|
if (typeof config.VERTEX_EXPRESS_BASE_URL === "undefined") { |
|
|
config.VERTEX_EXPRESS_BASE_URL = ""; |
|
|
} |
|
|
|
|
|
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 = {}; |
|
|
} |
|
|
|
|
|
if ( |
|
|
!config.CUSTOM_HEADERS || |
|
|
typeof config.CUSTOM_HEADERS !== "object" || |
|
|
config.CUSTOM_HEADERS === null |
|
|
) { |
|
|
config.CUSTOM_HEADERS = {}; |
|
|
} |
|
|
|
|
|
if (!config.SAFETY_SETTINGS || !Array.isArray(config.SAFETY_SETTINGS)) { |
|
|
config.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.ERROR_LOG_RECORD_REQUEST_BODY === "undefined") { |
|
|
config.ERROR_LOG_RECORD_REQUEST_BODY = false; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
if (configForm) { |
|
|
|
|
|
initializeSensitiveFields(); |
|
|
} |
|
|
|
|
|
|
|
|
const uploadProvider = document.getElementById("UPLOAD_PROVIDER"); |
|
|
if (uploadProvider && !uploadProvider.value) { |
|
|
uploadProvider.value = "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) { |
|
|
|
|
|
initializeSensitiveFields(); |
|
|
} |
|
|
toggleProviderConfig("smms"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function populateForm(config) { |
|
|
const modelIdMap = {}; |
|
|
|
|
|
|
|
|
const arrayContainers = document.querySelectorAll(".array-container"); |
|
|
arrayContainers.forEach((container) => { |
|
|
container.innerHTML = ""; |
|
|
}); |
|
|
const budgetMapContainer = document.getElementById( |
|
|
"THINKING_BUDGET_MAP_container" |
|
|
); |
|
|
if (budgetMapContainer) { |
|
|
budgetMapContainer.innerHTML = ""; |
|
|
} else { |
|
|
console.error("Critical: THINKING_BUDGET_MAP_container not found!"); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
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!"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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]; |
|
|
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>'; |
|
|
} |
|
|
|
|
|
|
|
|
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>'; |
|
|
} |
|
|
|
|
|
|
|
|
for (const [key, value] of Object.entries(config)) { |
|
|
if (Array.isArray(value) && key !== "THINKING_MODELS" && key !== "API_KEYS") { |
|
|
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); |
|
|
} |
|
|
}); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (Array.isArray(config.API_KEYS)) { |
|
|
allApiKeys = config.API_KEYS.filter(key => |
|
|
typeof key === "string" && key.trim() !== "" |
|
|
); |
|
|
filteredApiKeys = [...allApiKeys]; |
|
|
currentApiKeyPage = 1; |
|
|
renderApiKeyPage(); |
|
|
updateApiKeyPagination(); |
|
|
} |
|
|
|
|
|
|
|
|
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 : ""; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const uploadProvider = document.getElementById("UPLOAD_PROVIDER"); |
|
|
if (uploadProvider) { |
|
|
toggleProviderConfig(uploadProvider.value); |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleBulkAddApiKeys() { |
|
|
if (!apiKeyBulkInput || !apiKeyModal) return; |
|
|
|
|
|
const bulkText = apiKeyBulkInput.value; |
|
|
const extractedKeys = bulkText.match(API_KEY_REGEX) || []; |
|
|
|
|
|
|
|
|
const combinedKeys = new Set([...allApiKeys, ...extractedKeys]); |
|
|
const uniqueKeys = Array.from(combinedKeys); |
|
|
|
|
|
|
|
|
allApiKeys = uniqueKeys; |
|
|
|
|
|
|
|
|
const searchTerm = apiKeySearchInput ? apiKeySearchInput.value.toLowerCase() : ""; |
|
|
if (!searchTerm) { |
|
|
filteredApiKeys = [...allApiKeys]; |
|
|
} else { |
|
|
filteredApiKeys = allApiKeys.filter(key => |
|
|
key.toLowerCase().includes(searchTerm) |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
renderApiKeyPage(); |
|
|
updateApiKeyPagination(); |
|
|
|
|
|
closeModal(apiKeyModal); |
|
|
showNotification(`添加/更新了 ${uniqueKeys.length} 个唯一密钥`, "success"); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleApiKeySearch() { |
|
|
if (!apiKeySearchInput) return; |
|
|
|
|
|
const searchTerm = apiKeySearchInput.value.toLowerCase(); |
|
|
|
|
|
|
|
|
if (!searchTerm) { |
|
|
filteredApiKeys = [...allApiKeys]; |
|
|
} else { |
|
|
filteredApiKeys = allApiKeys.filter(key => |
|
|
key.toLowerCase().includes(searchTerm) |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
currentApiKeyPage = 1; |
|
|
|
|
|
|
|
|
renderApiKeyPage(); |
|
|
updateApiKeyPagination(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function renderApiKeyPage() { |
|
|
const apiKeyContainer = document.getElementById("API_KEYS_container"); |
|
|
if (!apiKeyContainer) return; |
|
|
|
|
|
|
|
|
apiKeyContainer.innerHTML = ""; |
|
|
|
|
|
|
|
|
const startIndex = (currentApiKeyPage - 1) * API_KEYS_PER_PAGE; |
|
|
const endIndex = Math.min(startIndex + API_KEYS_PER_PAGE, filteredApiKeys.length); |
|
|
const pageKeys = filteredApiKeys.slice(startIndex, endIndex); |
|
|
|
|
|
|
|
|
pageKeys.forEach((key) => { |
|
|
addArrayItemWithValue("API_KEYS", key); |
|
|
}); |
|
|
|
|
|
|
|
|
if (pageKeys.length === 0) { |
|
|
const emptyMessage = document.createElement("div"); |
|
|
emptyMessage.className = "text-gray-500 text-sm italic text-center py-4"; |
|
|
emptyMessage.textContent = filteredApiKeys.length === 0 ? |
|
|
(allApiKeys.length === 0 ? "暂无API密钥" : "未找到匹配的密钥") : |
|
|
"当前页无数据"; |
|
|
apiKeyContainer.appendChild(emptyMessage); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function updateApiKeyPagination() { |
|
|
totalApiKeyPages = Math.max(1, Math.ceil(filteredApiKeys.length / API_KEYS_PER_PAGE)); |
|
|
|
|
|
|
|
|
if (currentApiKeyPage > totalApiKeyPages) { |
|
|
currentApiKeyPage = totalApiKeyPages; |
|
|
} |
|
|
|
|
|
const paginationContainer = document.getElementById("apiKeyPagination"); |
|
|
if (!paginationContainer) return; |
|
|
|
|
|
|
|
|
if (totalApiKeyPages <= 1) { |
|
|
paginationContainer.style.display = "none"; |
|
|
return; |
|
|
} |
|
|
|
|
|
paginationContainer.style.display = "flex"; |
|
|
|
|
|
|
|
|
const pageInfo = document.getElementById("apiKeyPageInfo"); |
|
|
if (pageInfo) { |
|
|
pageInfo.textContent = `第 ${currentApiKeyPage} 页,共 ${totalApiKeyPages} 页 (${filteredApiKeys.length} 个密钥)`; |
|
|
} |
|
|
|
|
|
|
|
|
const prevBtn = document.getElementById("apiKeyPrevBtn"); |
|
|
const nextBtn = document.getElementById("apiKeyNextBtn"); |
|
|
|
|
|
if (prevBtn) { |
|
|
prevBtn.disabled = currentApiKeyPage <= 1; |
|
|
prevBtn.className = currentApiKeyPage <= 1 ? |
|
|
"px-3 py-1 rounded bg-gray-300 text-gray-500 cursor-not-allowed" : |
|
|
"px-3 py-1 rounded bg-blue-500 text-white hover:bg-blue-600 cursor-pointer"; |
|
|
} |
|
|
|
|
|
if (nextBtn) { |
|
|
nextBtn.disabled = currentApiKeyPage >= totalApiKeyPages; |
|
|
nextBtn.className = currentApiKeyPage >= totalApiKeyPages ? |
|
|
"px-3 py-1 rounded bg-gray-300 text-gray-500 cursor-not-allowed" : |
|
|
"px-3 py-1 rounded bg-blue-500 text-white hover:bg-blue-600 cursor-pointer"; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function goToApiKeyPage(page) { |
|
|
if (page < 1 || page > totalApiKeyPages) return; |
|
|
|
|
|
currentApiKeyPage = page; |
|
|
renderApiKeyPage(); |
|
|
updateApiKeyPagination(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function prevApiKeyPage() { |
|
|
if (currentApiKeyPage > 1) { |
|
|
goToApiKeyPage(currentApiKeyPage - 1); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function nextApiKeyPage() { |
|
|
if (currentApiKeyPage < totalApiKeyPages) { |
|
|
goToApiKeyPage(currentApiKeyPage + 1); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleBulkDeleteApiKeys() { |
|
|
if (!bulkDeleteApiKeyInput || !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; |
|
|
} |
|
|
|
|
|
|
|
|
let deleteCount = 0; |
|
|
allApiKeys = allApiKeys.filter(key => { |
|
|
if (keysToDelete.has(key)) { |
|
|
deleteCount++; |
|
|
return false; |
|
|
} |
|
|
return true; |
|
|
}); |
|
|
|
|
|
|
|
|
const searchTerm = apiKeySearchInput ? apiKeySearchInput.value.toLowerCase() : ""; |
|
|
if (!searchTerm) { |
|
|
filteredApiKeys = [...allApiKeys]; |
|
|
} else { |
|
|
filteredApiKeys = allApiKeys.filter(key => |
|
|
key.toLowerCase().includes(searchTerm) |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
renderApiKeyPage(); |
|
|
updateApiKeyPagination(); |
|
|
|
|
|
closeModal(bulkDeleteApiKeyModal); |
|
|
|
|
|
if (deleteCount > 0) { |
|
|
showNotification(`成功删除了 ${deleteCount} 个匹配的密钥`, "success"); |
|
|
} else { |
|
|
showNotification("列表中未找到您输入的任何密钥进行删除", "info"); |
|
|
} |
|
|
bulkDeleteApiKeyInput.value = ""; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 = ""; |
|
|
|
|
|
uniqueProxies.forEach((proxy) => { |
|
|
addArrayItemWithValue("PROXIES", proxy); |
|
|
}); |
|
|
|
|
|
closeModal(proxyModal); |
|
|
showNotification(`添加/更新了 ${uniqueProxies.length} 个唯一代理`, "success"); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 = ""; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 = ""; |
|
|
|
|
|
uniqueKeys.forEach((key) => { |
|
|
addArrayItemWithValue("VERTEX_API_KEYS", key); |
|
|
}); |
|
|
|
|
|
|
|
|
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 = ""; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 = ""; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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"); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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"); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createArrayInput(key, value, isSensitive, modelId = null) { |
|
|
const input = document.createElement("input"); |
|
|
input.type = "text"; |
|
|
input.name = `${key}[]`; |
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 = "生成随机令牌"; |
|
|
|
|
|
return generateBtn; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 = "删除"; |
|
|
|
|
|
return removeBtn; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createProxyStatusIcon() { |
|
|
const statusIcon = document.createElement("span"); |
|
|
statusIcon.className = "proxy-status-icon px-2 py-2 text-gray-400"; |
|
|
statusIcon.innerHTML = '<i class="fas fa-question-circle" title="未检测"></i>'; |
|
|
statusIcon.setAttribute("data-status", "unknown"); |
|
|
return statusIcon; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createProxyCheckButton() { |
|
|
const checkBtn = document.createElement("button"); |
|
|
checkBtn.type = "button"; |
|
|
checkBtn.className = |
|
|
"proxy-check-btn px-2 py-2 text-blue-500 hover:text-blue-700 focus:outline-none transition-colors duration-150 rounded-r-md"; |
|
|
checkBtn.innerHTML = '<i class="fas fa-globe"></i>'; |
|
|
checkBtn.title = "检测此代理"; |
|
|
|
|
|
|
|
|
checkBtn.addEventListener("click", function(e) { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
const inputElement = this.closest('.flex').querySelector('.array-input'); |
|
|
if (inputElement && inputElement.value.trim()) { |
|
|
checkSingleProxy(inputElement.value.trim(), this); |
|
|
} else { |
|
|
showNotification("请先输入代理地址", "warning"); |
|
|
} |
|
|
}); |
|
|
|
|
|
return checkBtn; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function addArrayItem(key) { |
|
|
const container = document.getElementById(`${key}_container`); |
|
|
if (!container) return; |
|
|
|
|
|
const newItemValue = ""; |
|
|
const modelId = addArrayItemWithValue(key, newItemValue); |
|
|
|
|
|
if (key === "THINKING_MODELS" && modelId) { |
|
|
createAndAppendBudgetMapItem(newItemValue, -1, modelId); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function addArrayItemWithValue(key, value) { |
|
|
const container = document.getElementById(`${key}_container`); |
|
|
if (!container) return null; |
|
|
|
|
|
const isThinkingModel = key === "THINKING_MODELS"; |
|
|
const isAllowedToken = key === "ALLOWED_TOKENS"; |
|
|
const isVertexApiKey = key === "VERTEX_API_KEYS"; |
|
|
const isProxy = key === "PROXIES"; |
|
|
const isSensitive = key === "API_KEYS" || isAllowedToken || isVertexApiKey; |
|
|
const modelId = isThinkingModel ? generateUUID() : null; |
|
|
|
|
|
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"; |
|
|
|
|
|
inputWrapper.style.border = "1px solid rgba(0, 0, 0, 0.12)"; |
|
|
inputWrapper.style.backgroundColor = "transparent"; |
|
|
|
|
|
const input = createArrayInput( |
|
|
key, |
|
|
value, |
|
|
isSensitive, |
|
|
isThinkingModel ? modelId : null |
|
|
); |
|
|
inputWrapper.appendChild(input); |
|
|
|
|
|
if (isAllowedToken) { |
|
|
const generateBtn = createGenerateTokenButton(); |
|
|
inputWrapper.appendChild(generateBtn); |
|
|
} else if (isProxy) { |
|
|
|
|
|
const proxyStatusIcon = createProxyStatusIcon(); |
|
|
inputWrapper.appendChild(proxyStatusIcon); |
|
|
|
|
|
const proxyCheckBtn = createProxyCheckButton(); |
|
|
inputWrapper.appendChild(proxyCheckBtn); |
|
|
} else { |
|
|
|
|
|
input.classList.add("rounded-r-md"); |
|
|
} |
|
|
|
|
|
const removeBtn = createRemoveButton(); |
|
|
|
|
|
arrayItem.appendChild(inputWrapper); |
|
|
arrayItem.appendChild(removeBtn); |
|
|
container.appendChild(arrayItem); |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
const placeholder = container.querySelector(".text-gray-500.italic"); |
|
|
|
|
|
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; |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mapItem.appendChild(keyInput); |
|
|
mapItem.appendChild(valueInput); |
|
|
|
|
|
|
|
|
container.appendChild(mapItem); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function addCustomHeaderItem() { |
|
|
createAndAppendCustomHeaderItem("", ""); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function collectFormData() { |
|
|
const formData = {}; |
|
|
|
|
|
|
|
|
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", ""); |
|
|
|
|
|
|
|
|
if (key === "API_KEYS") { |
|
|
formData[key] = allApiKeys.filter( |
|
|
(value) => value && value.trim() !== "" && value !== MASKED_VALUE |
|
|
); |
|
|
return; |
|
|
} |
|
|
|
|
|
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 |
|
|
); |
|
|
}); |
|
|
|
|
|
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) { |
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function startScheduler() { |
|
|
try { |
|
|
const response = await fetch("/api/scheduler/start", { method: "POST" }); |
|
|
if (!response.ok) { |
|
|
console.warn(`启动定时任务失败: ${response.status}`); |
|
|
} else { |
|
|
console.log("定时任务已启动"); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error("调用启动定时任务API时出错:", error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function saveConfig() { |
|
|
try { |
|
|
const formData = collectFormData(); |
|
|
|
|
|
showNotification("正在保存配置...", "info"); |
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
|
|
|
showNotification("配置保存成功", "success"); |
|
|
|
|
|
|
|
|
await startScheduler(); |
|
|
} catch (error) { |
|
|
console.error("保存配置失败:", error); |
|
|
|
|
|
await startScheduler(); |
|
|
|
|
|
|
|
|
showNotification("保存配置失败: " + error.message, "error"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function resetConfig(event) { |
|
|
|
|
|
if (event) { |
|
|
event.preventDefault(); |
|
|
event.stopPropagation(); |
|
|
} |
|
|
|
|
|
console.log( |
|
|
"resetConfig called. Event target:", |
|
|
event ? event.target.id : "No event" |
|
|
); |
|
|
|
|
|
|
|
|
if ( |
|
|
!event || |
|
|
event.target.id === "resetBtn" || |
|
|
(event.currentTarget && event.currentTarget.id === "resetBtn") |
|
|
) { |
|
|
if (resetConfirmModal) { |
|
|
openModal(resetConfirmModal); |
|
|
} else { |
|
|
console.error( |
|
|
"Reset confirmation modal not found! Falling back to default confirm." |
|
|
); |
|
|
if (confirm("确定要重置所有配置吗?这将恢复到默认值。")) { |
|
|
executeReset(); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function executeReset() { |
|
|
try { |
|
|
showNotification("正在重置配置...", "info"); |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
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"); |
|
|
|
|
|
|
|
|
await startScheduler(); |
|
|
} catch (error) { |
|
|
console.error("重置配置失败:", error); |
|
|
showNotification("重置配置失败: " + error.message, "error"); |
|
|
|
|
|
await startScheduler(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function showNotification(message, type = "info") { |
|
|
const notification = document.getElementById("notification"); |
|
|
notification.textContent = message; |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function refreshPage(button) { |
|
|
if (button) button.classList.add("loading"); |
|
|
location.reload(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function scrollToTop() { |
|
|
window.scrollTo({ top: 0, behavior: "smooth" }); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function scrollToBottom() { |
|
|
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function toggleScrollButtons() { |
|
|
const scrollButtons = document.querySelector(".scroll-buttons"); |
|
|
if (scrollButtons) { |
|
|
scrollButtons.style.display = window.scrollY > 200 ? "flex" : "none"; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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", |
|
|
]; |
|
|
|
|
|
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 = "删除此设置"; |
|
|
|
|
|
|
|
|
settingItem.appendChild(categorySelect); |
|
|
settingItem.appendChild(thresholdSelect); |
|
|
settingItem.appendChild(removeBtn); |
|
|
|
|
|
container.appendChild(settingItem); |
|
|
} |
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
if ( |
|
|
responseData && |
|
|
responseData.success && |
|
|
Array.isArray(responseData.data) |
|
|
) { |
|
|
cachedModelsList = 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 = []; |
|
|
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 = ""; |
|
|
|
|
|
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"; |
|
|
|
|
|
|
|
|
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(); |
|
|
renderModelsInModal(); |
|
|
|
|
|
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 = ""; |
|
|
if (modelHelperModal) openModal(modelHelperModal); |
|
|
} |
|
|
|
|
|
function handleModelSelection(selectedModelId) { |
|
|
if (!currentModelHelperTarget) return; |
|
|
|
|
|
if ( |
|
|
currentModelHelperTarget.type === "input" && |
|
|
currentModelHelperTarget.target |
|
|
) { |
|
|
const inputElement = currentModelHelperTarget.target; |
|
|
inputElement.value = selectedModelId; |
|
|
|
|
|
if (inputElement.classList.contains(SENSITIVE_INPUT_CLASS)) { |
|
|
const event = new Event("focusout", { bubbles: true, cancelable: true }); |
|
|
inputElement.dispatchEvent(event); |
|
|
} |
|
|
|
|
|
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) { |
|
|
|
|
|
createAndAppendBudgetMapItem(selectedModelId, -1, modelId); |
|
|
} |
|
|
} |
|
|
|
|
|
if (modelHelperModal) closeModal(modelHelperModal); |
|
|
currentModelHelperTarget = null; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function checkSingleProxy(proxy, buttonElement) { |
|
|
const statusIcon = buttonElement.parentElement.querySelector('.proxy-status-icon'); |
|
|
const originalButtonContent = buttonElement.innerHTML; |
|
|
|
|
|
try { |
|
|
|
|
|
buttonElement.innerHTML = '<i class="fas fa-spinner fa-spin"></i>'; |
|
|
buttonElement.disabled = true; |
|
|
if (statusIcon) { |
|
|
statusIcon.className = "proxy-status-icon px-2 py-2 text-blue-500"; |
|
|
statusIcon.innerHTML = '<i class="fas fa-spinner fa-spin" title="检测中..."></i>'; |
|
|
statusIcon.setAttribute("data-status", "checking"); |
|
|
} |
|
|
|
|
|
const response = await fetch('/api/config/proxy/check', { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
'Content-Type': 'application/json', |
|
|
}, |
|
|
body: JSON.stringify({ |
|
|
proxy: proxy, |
|
|
use_cache: true |
|
|
}) |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(`检测请求失败: ${response.status}`); |
|
|
} |
|
|
|
|
|
const result = await response.json(); |
|
|
updateProxyStatus(statusIcon, result); |
|
|
|
|
|
|
|
|
if (result.is_available) { |
|
|
showNotification(`代理可用 (${result.response_time}s)`, "success"); |
|
|
} else { |
|
|
showNotification(`代理不可用: ${result.error_message}`, "error"); |
|
|
} |
|
|
|
|
|
} catch (error) { |
|
|
console.error('代理检测失败:', error); |
|
|
if (statusIcon) { |
|
|
statusIcon.className = "proxy-status-icon px-2 py-2 text-red-500"; |
|
|
statusIcon.innerHTML = '<i class="fas fa-times-circle" title="检测失败"></i>'; |
|
|
statusIcon.setAttribute("data-status", "error"); |
|
|
} |
|
|
showNotification(`检测失败: ${error.message}`, "error"); |
|
|
} finally { |
|
|
|
|
|
buttonElement.innerHTML = originalButtonContent; |
|
|
buttonElement.disabled = false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function updateProxyStatus(statusIcon, result) { |
|
|
if (!statusIcon) return; |
|
|
|
|
|
if (result.is_available) { |
|
|
statusIcon.className = "proxy-status-icon px-2 py-2 text-green-500"; |
|
|
statusIcon.innerHTML = `<i class="fas fa-check-circle" title="可用 (${result.response_time}s)"></i>`; |
|
|
statusIcon.setAttribute("data-status", "available"); |
|
|
} else { |
|
|
statusIcon.className = "proxy-status-icon px-2 py-2 text-red-500"; |
|
|
statusIcon.innerHTML = `<i class="fas fa-times-circle" title="不可用: ${result.error_message}"></i>`; |
|
|
statusIcon.setAttribute("data-status", "unavailable"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function checkAllProxies() { |
|
|
const proxyContainer = document.getElementById("PROXIES_container"); |
|
|
if (!proxyContainer) return; |
|
|
|
|
|
const proxyInputs = proxyContainer.querySelectorAll('.array-input'); |
|
|
const proxies = Array.from(proxyInputs) |
|
|
.map(input => input.value.trim()) |
|
|
.filter(proxy => proxy.length > 0); |
|
|
|
|
|
if (proxies.length === 0) { |
|
|
showNotification("没有代理需要检测", "warning"); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const proxyCheckModal = document.getElementById("proxyCheckModal"); |
|
|
if (proxyCheckModal) { |
|
|
openModal(proxyCheckModal); |
|
|
|
|
|
|
|
|
const progressContainer = document.getElementById("proxyCheckProgress"); |
|
|
const summaryContainer = document.getElementById("proxyCheckSummary"); |
|
|
const resultsContainer = document.getElementById("proxyCheckResults"); |
|
|
|
|
|
if (progressContainer) progressContainer.classList.remove("hidden"); |
|
|
if (summaryContainer) summaryContainer.classList.add("hidden"); |
|
|
if (resultsContainer) resultsContainer.innerHTML = ""; |
|
|
|
|
|
|
|
|
const totalCountElement = document.getElementById("totalCount"); |
|
|
if (totalCountElement) totalCountElement.textContent = proxies.length; |
|
|
|
|
|
try { |
|
|
const response = await fetch('/api/config/proxy/check-all', { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
'Content-Type': 'application/json', |
|
|
}, |
|
|
body: JSON.stringify({ |
|
|
proxies: proxies, |
|
|
use_cache: true, |
|
|
max_concurrent: 5 |
|
|
}) |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(`批量检测请求失败: ${response.status}`); |
|
|
} |
|
|
|
|
|
const results = await response.json(); |
|
|
displayProxyCheckResults(results); |
|
|
updateProxyStatusInList(results); |
|
|
|
|
|
} catch (error) { |
|
|
console.error('批量代理检测失败:', error); |
|
|
showNotification(`批量检测失败: ${error.message}`, "error"); |
|
|
if (resultsContainer) { |
|
|
resultsContainer.innerHTML = `<div class="text-red-500 text-center py-4">检测失败: ${error.message}</div>`; |
|
|
} |
|
|
} finally { |
|
|
|
|
|
if (progressContainer) progressContainer.classList.add("hidden"); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function displayProxyCheckResults(results) { |
|
|
const summaryContainer = document.getElementById("proxyCheckSummary"); |
|
|
const resultsContainer = document.getElementById("proxyCheckResults"); |
|
|
const availableCountElement = document.getElementById("availableCount"); |
|
|
const unavailableCountElement = document.getElementById("unavailableCount"); |
|
|
const retryButton = document.getElementById("retryFailedProxiesBtn"); |
|
|
|
|
|
if (!resultsContainer) return; |
|
|
|
|
|
|
|
|
const availableCount = results.filter(r => r.is_available).length; |
|
|
const unavailableCount = results.length - availableCount; |
|
|
|
|
|
|
|
|
if (availableCountElement) availableCountElement.textContent = availableCount; |
|
|
if (unavailableCountElement) unavailableCountElement.textContent = unavailableCount; |
|
|
if (summaryContainer) summaryContainer.classList.remove("hidden"); |
|
|
|
|
|
|
|
|
if (retryButton) { |
|
|
if (unavailableCount > 0) { |
|
|
retryButton.classList.remove("hidden"); |
|
|
} else { |
|
|
retryButton.classList.add("hidden"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
resultsContainer.innerHTML = ""; |
|
|
|
|
|
results.forEach(result => { |
|
|
const resultItem = document.createElement("div"); |
|
|
resultItem.className = `flex items-center justify-between p-3 border rounded-lg ${ |
|
|
result.is_available ? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50' |
|
|
}`; |
|
|
|
|
|
const statusIcon = result.is_available ? |
|
|
'<i class="fas fa-check-circle text-green-500"></i>' : |
|
|
'<i class="fas fa-times-circle text-red-500"></i>'; |
|
|
|
|
|
const responseTimeText = result.response_time ? |
|
|
` (${result.response_time}s)` : ''; |
|
|
|
|
|
const errorText = result.error_message ? |
|
|
`<span class="text-red-600 text-sm ml-2">${result.error_message}</span>` : ''; |
|
|
|
|
|
resultItem.innerHTML = ` |
|
|
<div class="flex items-center gap-3"> |
|
|
${statusIcon} |
|
|
<span class="font-mono text-sm">${result.proxy}</span> |
|
|
${responseTimeText} |
|
|
</div> |
|
|
<div class="flex items-center"> |
|
|
<span class="text-sm ${result.is_available ? 'text-green-700' : 'text-red-700'}"> |
|
|
${result.is_available ? '可用' : '不可用'} |
|
|
</span> |
|
|
${errorText} |
|
|
</div> |
|
|
`; |
|
|
|
|
|
resultsContainer.appendChild(resultItem); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function updateProxyStatusInList(results) { |
|
|
const proxyContainer = document.getElementById("PROXIES_container"); |
|
|
if (!proxyContainer) return; |
|
|
|
|
|
results.forEach(result => { |
|
|
const proxyInputs = proxyContainer.querySelectorAll('.array-input'); |
|
|
proxyInputs.forEach(input => { |
|
|
if (input.value.trim() === result.proxy) { |
|
|
const statusIcon = input.parentElement.querySelector('.proxy-status-icon'); |
|
|
updateProxyStatus(statusIcon, result); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|