diff --git a/cmd/desktop/frontend/src/i18n/en.js b/cmd/desktop/frontend/src/i18n/en.js index 2a581c1a..fc925bec 100644 --- a/cmd/desktop/frontend/src/i18n/en.js +++ b/cmd/desktop/frontend/src/i18n/en.js @@ -28,6 +28,13 @@ export default { noEndpoints: 'No endpoints configured. Click "Add Endpoint" to get started.', copy: 'Copy', copied: 'Copied', + cloneSuffix: '(Copy)', + cloned: 'Endpoint cloned successfully', + cloneFailed: 'Failed to clone endpoint', + noEndpointAtIdx: 'No endpoint found', + noEndpointAtIdxWithIndex: 'No endpoint found at index', + invalidIndex: 'Invalid index', + functionUnavailable: 'Function not available', current: 'Current', switchTo: 'Switch', switchFailed: 'Switch Failed', @@ -94,6 +101,7 @@ export default { remarkHelp: 'Optional: Add a remark for this endpoint', cancel: 'Cancel', save: 'Save', + invalidFormat: 'Invalid format', manageTokenPool: 'Manage Token Pool', close: 'Close', changePort: 'Change Port', @@ -561,4 +569,4 @@ export default { 'Tip: Test endpoints before saving to ensure they work correctly', 'Tip: Your API keys and data are stored locally, safe and secure' ] -}; +}; \ No newline at end of file diff --git a/cmd/desktop/frontend/src/i18n/zh-CN.js b/cmd/desktop/frontend/src/i18n/zh-CN.js index ce1dbea2..d6cdae53 100644 --- a/cmd/desktop/frontend/src/i18n/zh-CN.js +++ b/cmd/desktop/frontend/src/i18n/zh-CN.js @@ -28,6 +28,13 @@ export default { noEndpoints: '未配置端点。点击"添加端点"开始使用。', copy: '复制', copied: '已复制', + cloneSuffix: '(副本)', + cloned: '端点克隆成功', + cloneFailed: '端点克隆失败', + noEndpointAtIdx: '未找到端点', + noEndpointAtIdxWithIndex: '在索引处未找到端点', + invalidIndex: '无效索引', + functionUnavailable: '功能不可用', current: '当前使用', switchTo: '切换', switchFailed: '切换失败', @@ -94,6 +101,7 @@ export default { remarkHelp: '可选:为此端点添加备注说明', cancel: '取消', save: '保存', + invalidFormat: '格式无效', manageTokenPool: '管理 Token Pool', close: '关闭', changePort: '修改端口', @@ -562,4 +570,4 @@ export default { '小贴士:保存端点前先点击"测试"可以确保它们能正常工作', '小贴士:您的 API 密钥和数据都存储在本地,安全可靠', ] -}; +}; \ No newline at end of file diff --git a/cmd/desktop/frontend/src/main.js b/cmd/desktop/frontend/src/main.js index 00077bb2..39e5c652 100644 --- a/cmd/desktop/frontend/src/main.js +++ b/cmd/desktop/frontend/src/main.js @@ -19,6 +19,7 @@ import { initFilterDropdowns, clearAllFilters } from './modules/filters.js' import { formatTokens } from './utils/format.js' import { showAddEndpointModal, + showAddEndpointModalWithPreset, editEndpoint, saveEndpoint, openEndpointTokenPoolFromModal, @@ -223,6 +224,7 @@ async function loadConfigAndRender() { // Expose functions to window for onclick handlers window.loadConfig = loadConfigAndRender; window.showAddEndpointModal = showAddEndpointModal; +window.showAddEndpointModalWithPreset = showAddEndpointModalWithPreset; window.editEndpoint = editEndpoint; window.saveEndpoint = saveEndpoint; window.openEndpointTokenPoolFromModal = openEndpointTokenPoolFromModal; @@ -279,4 +281,4 @@ window.closeHistoryModal = async () => { window.deleteHistoryArchive = async () => { const { deleteHistoryArchive } = await import('./modules/history.js'); deleteHistoryArchive(); -}; +}; \ No newline at end of file diff --git a/cmd/desktop/frontend/src/modules/config.js b/cmd/desktop/frontend/src/modules/config.js index 67e5864c..e1e13132 100644 --- a/cmd/desktop/frontend/src/modules/config.js +++ b/cmd/desktop/frontend/src/modules/config.js @@ -15,6 +15,9 @@ export async function loadConfig() { const configStr = await window.go.main.App.GetConfig(); const config = JSON.parse(configStr); + // 保存到全局变量,供克隆等功能使用 + window.config = config; + document.getElementById('proxyPort').textContent = config.port; document.getElementById('totalEndpoints').textContent = config.endpoints.length; diff --git a/cmd/desktop/frontend/src/modules/endpoints.js b/cmd/desktop/frontend/src/modules/endpoints.js index 2f76d433..403f1a3b 100644 --- a/cmd/desktop/frontend/src/modules/endpoints.js +++ b/cmd/desktop/frontend/src/modules/endpoints.js @@ -4,6 +4,19 @@ import { getEndpointStats } from './stats.js'; import { toggleEndpoint, testAllEndpointsZeroCost } from './config.js'; import { filterEndpoints, isFilterActive, updateFilterStats } from './filters.js'; +// 提取基础名称,移除副本后缀 +function extractBaseName(name) { + // 移除类似 "(Copy)", "(副本)", "(Copy) 1", "(副本) 1" 等后缀 + // 使用固定的模式匹配,避免在函数内部调用 t() + const copyPattern = /\(Copy\)(?:\s+\d+)?$/; + const chineseCopyPattern = /\(副本\)(?:\s+\d+)?$/; + + let baseName = name.replace(copyPattern, '').trim(); + baseName = baseName.replace(chineseCopyPattern, '').trim(); + + return baseName; +} + const ENDPOINT_TEST_STATUS_KEY = 'ccNexus_endpointTestStatus'; const ENDPOINT_VIEW_MODE_KEY = 'ccNexus_endpointViewMode'; @@ -260,6 +273,7 @@ export async function renderEndpoints(endpoints) { + @@ -281,6 +295,11 @@ export async function renderEndpoints(endpoints) { const idx = parseInt(testBtn.getAttribute('data-index')); window.testEndpoint(idx, testBtn); }); + const copyBtn = item.querySelector('[data-action="copy"]'); + copyBtn.addEventListener('click', () => { + const idx = parseInt(copyBtn.getAttribute('data-index')); + copyEndpointConfig(idx, copyBtn); + }); editBtn.addEventListener('click', () => { const idx = parseInt(editBtn.getAttribute('data-index')); window.editEndpoint(idx); @@ -1308,6 +1327,81 @@ export async function openTokenPoolModal(index, endpointName = '') { } } +// 克隆端点配置(创建副本) +function copyEndpointConfig(index, button) { + const allEndpoints = window.config?.endpoints || []; + + if (index < 0 || index >= allEndpoints.length) { + const errorMsg = `Invalid index ${index} for cloning endpoint. Total endpoints: ${allEndpoints.length} at ${new Date().toISOString()}`; + console.error(errorMsg); + if (typeof window.logError === 'function') { + window.logError(errorMsg); + } + showNotification(t('endpoints.cloneFailed') + ': ' + (t('endpoints.invalidIndex') || `Invalid index ${index}`), 'error'); + return; + } + + const endpoint = allEndpoints[index]; + + if (endpoint) { + const clonedEndpoint = { ...endpoint }; + + const baseName = extractBaseName(endpoint.name); + const copySuffix = '(Copy)'; + + let newName = `${baseName}${copySuffix}`; + let counter = 1; + while (allEndpoints.some(ep => ep.name === newName)) { + newName = `${baseName}${copySuffix} ${counter}`; + counter++; + } + clonedEndpoint.name = newName; + + if (clonedEndpoint.authMode === "token_pool") { + delete clonedEndpoint.apiKey; + } + + const originalHTML = button.innerHTML; + button.innerHTML = ''; + setTimeout(() => { button.innerHTML = originalHTML; }, 1000); + + showNotification(t('endpoints.cloned') || 'Endpoint cloned successfully', 'success'); + + window.clonedEndpointData = clonedEndpoint; + + if (typeof window.showAddEndpointModalWithPreset === 'function') { + try { + window.showAddEndpointModalWithPreset(clonedEndpoint); + } catch (error) { + const errorMsg = `Error calling showAddEndpointModalWithPreset at ${new Date().toISOString()}: ${error.message}\nStack: ${error.stack}`; + console.error(errorMsg); + try { + if (typeof window.logError === 'function') { + window.logError(errorMsg); + } + } catch (logErr) { + console.error('Failed to call logError:', logErr); + } + showNotification(t('endpoints.cloneFailed') + ': ' + error.message || `Failed to clone endpoint: ${error.message}`, 'error'); + } + } else { + const errorMsg = `showAddEndpointModalWithPreset function is not available at ${new Date().toISOString()}`; + console.error(errorMsg); + if (typeof window.logError === 'function') { + window.logError(errorMsg); + } + showNotification(t('endpoints.cloneFailed') + ': ' + t('endpoints.functionUnavailable') || 'Failed to clone endpoint: Function not available', 'error'); + } + } else { + const errorMsg = `Failed to clone endpoint: endpoint data not found at index ${index} at ${new Date().toISOString()}`; + console.error(errorMsg); + if (typeof window.logError === 'function') { + window.logError(errorMsg); + } + showNotification(t('endpoints.cloneFailed') + ': ' + (t('endpoints.noEndpointAtIdxWithIndex') || `No endpoint found at index ${index}`), 'error'); + } +} + export function toggleEndpointPanel() { const panel = document.getElementById('endpointPanel'); const icon = document.getElementById('endpointToggleIcon'); @@ -1569,6 +1663,7 @@ function renderCompactView(sortedEndpoints, container, currentEndpointName, isFi
@@ -1658,6 +1753,14 @@ function bindCompactItemEvents(item, index, enabled) { window.testEndpoint(idx, testBtn); }); + // 复制按钮 + const copyBtn = item.querySelector('[data-action="copy"]'); + copyBtn.addEventListener('click', () => { + closeAllDropdowns(); + const idx = parseInt(copyBtn.getAttribute('data-index')); + copyEndpointConfig(idx, copyBtn); +}); + // 编辑按钮 editBtn.addEventListener('click', () => { closeAllDropdowns(); @@ -1945,4 +2048,4 @@ export function updateEndpointStatsIncremental(endpointName, data) { const tooltip = `${t('endpoints.requests')}: ${data.requests} | ${t('endpoints.errors')}: ${data.errors}\n${t('statistics.in')}: ${formatTokens(data.inputTokens)} | ${t('statistics.out')}: ${formatTokens(data.outputTokens)}`; compactStats.title = tooltip; } -} +} \ No newline at end of file diff --git a/cmd/desktop/frontend/src/modules/modal.js b/cmd/desktop/frontend/src/modules/modal.js index 4c9017a3..d94e6f2d 100644 --- a/cmd/desktop/frontend/src/modules/modal.js +++ b/cmd/desktop/frontend/src/modules/modal.js @@ -198,6 +198,25 @@ export function showAddEndpointModal() { document.getElementById('endpointModal').classList.add('active'); } +// 使用预设数据打开添加端点模态框 +export function showAddEndpointModalWithPreset(presetData) { + currentEditIndex = -1; + document.getElementById('modalTitle').textContent = '➕ ' + t('modal.addEndpoint'); + document.getElementById('endpointName').value = presetData.name || ''; + document.getElementById('endpointUrl').value = presetData.apiUrl || ''; + document.getElementById('endpointKey').value = presetData.apiKey || ''; + document.getElementById('endpointKey').type = 'password'; + document.getElementById('eyeIcon').innerHTML = '