From 817c25267bf2d5c3727276d68381356905be753d Mon Sep 17 00:00:00 2001 From: hex2077 Date: Tue, 13 Jan 2026 18:32:27 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(=E4=B8=8A=E4=BC=A0=E9=85=8D=E7=BD=AE):?= =?UTF-8?q?=20=E6=B7=BB=E5=8A=A0=E6=89=B9=E9=87=8F=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E6=9C=AA=E5=85=B3=E8=81=94=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(提供商管理): 重构API路由顺序并添加健康节点管理功能 style(侧边栏): 更新配置管理为凭据文件管理以更准确描述功能 perf(提供商池): 优化健康检查仅检测不健康节点提升性能 fix(UI): 修复提供商编辑状态按钮显示问题 docs(i18n): 更新翻译文件以匹配新功能 --- package-lock.json | 2 - src/services/ui-manager.js | 78 ++++-- src/ui-modules/provider-api.js | 273 ++++++++++++++++++- src/ui-modules/upload-config-api.js | 110 ++++++++ static/app/i18n.js | 82 +++++- static/app/modal.js | 173 ++++++++++-- static/app/upload-config-manager.js | 106 ++++++- static/components/section-providers.css | 28 ++ static/components/section-upload-config.css | 124 +++++++++ static/components/section-upload-config.html | 34 ++- static/components/sidebar.html | 4 +- 11 files changed, 940 insertions(+), 74 deletions(-) diff --git a/package-lock.json b/package-lock.json index bfecc3e..8e81846 100644 --- a/package-lock.json +++ b/package-lock.json @@ -101,7 +101,6 @@ "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2960,7 +2959,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", diff --git a/src/services/ui-manager.js b/src/services/ui-manager.js index 496d9a5..aa3989d 100644 --- a/src/services/ui-manager.js +++ b/src/services/ui-manager.js @@ -137,7 +137,57 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo return await providerApi.handleAddProvider(req, res, currentConfig, providerPoolManager); } + // Reset all providers health status for a specific provider type + // NOTE: This must be before the generic /{providerType}/{uuid} route to avoid matching 'reset-health' as UUID + const resetHealthMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/reset-health$/); + if (method === 'POST' && resetHealthMatch) { + const providerType = decodeURIComponent(resetHealthMatch[1]); + return await providerApi.handleResetProviderHealth(req, res, currentConfig, providerPoolManager, providerType); + } + + // Perform health check for all providers of a specific type + // NOTE: This must be before the generic /{providerType}/{uuid} route to avoid matching 'health-check' as UUID + const healthCheckMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/health-check$/); + if (method === 'POST' && healthCheckMatch) { + const providerType = decodeURIComponent(healthCheckMatch[1]); + return await providerApi.handleHealthCheck(req, res, currentConfig, providerPoolManager, providerType); + } + + // Delete all unhealthy providers for a specific type + // NOTE: This must be before the generic /{providerType}/{uuid} route to avoid matching 'delete-unhealthy' as UUID + const deleteUnhealthyMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/delete-unhealthy$/); + if (method === 'DELETE' && deleteUnhealthyMatch) { + const providerType = decodeURIComponent(deleteUnhealthyMatch[1]); + return await providerApi.handleDeleteUnhealthyProviders(req, res, currentConfig, providerPoolManager, providerType); + } + + // Refresh UUIDs for all unhealthy providers of a specific type + // NOTE: This must be before the generic /{providerType}/{uuid} route to avoid matching 'refresh-unhealthy-uuids' as UUID + const refreshUnhealthyUuidsMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/refresh-unhealthy-uuids$/); + if (method === 'POST' && refreshUnhealthyUuidsMatch) { + const providerType = decodeURIComponent(refreshUnhealthyUuidsMatch[1]); + return await providerApi.handleRefreshUnhealthyUuids(req, res, currentConfig, providerPoolManager, providerType); + } + + // Disable/Enable specific provider configuration + const disableEnableProviderMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/([^\/]+)\/(disable|enable)$/); + if (disableEnableProviderMatch) { + const providerType = decodeURIComponent(disableEnableProviderMatch[1]); + const providerUuid = disableEnableProviderMatch[2]; + const action = disableEnableProviderMatch[3]; + return await providerApi.handleDisableEnableProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid, action); + } + + // Refresh UUID for specific provider configuration + const refreshUuidMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/([^\/]+)\/refresh-uuid$/); + if (method === 'POST' && refreshUuidMatch) { + const providerType = decodeURIComponent(refreshUuidMatch[1]); + const providerUuid = refreshUuidMatch[2]; + return await providerApi.handleRefreshProviderUuid(req, res, currentConfig, providerPoolManager, providerType, providerUuid); + } + // Update specific provider configuration + // NOTE: This generic route must be after all specific routes like /reset-health, /health-check, /delete-unhealthy const updateProviderMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/([^\/]+)$/); if (method === 'PUT' && updateProviderMatch) { const providerType = decodeURIComponent(updateProviderMatch[1]); @@ -152,29 +202,6 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo return await providerApi.handleDeleteProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid); } - // Disable/Enable specific provider configuration - const disableEnableProviderMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/([^\/]+)\/(disable|enable)$/); - if (disableEnableProviderMatch) { - const providerType = decodeURIComponent(disableEnableProviderMatch[1]); - const providerUuid = disableEnableProviderMatch[2]; - const action = disableEnableProviderMatch[3]; - return await providerApi.handleDisableEnableProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid, action); - } - - // Reset all providers health status for a specific provider type - const resetHealthMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/reset-health$/); - if (method === 'POST' && resetHealthMatch) { - const providerType = decodeURIComponent(resetHealthMatch[1]); - return await providerApi.handleResetProviderHealth(req, res, currentConfig, providerPoolManager, providerType); - } - - // Perform health check for all providers of a specific type - const healthCheckMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/health-check$/); - if (method === 'POST' && healthCheckMatch) { - const providerType = decodeURIComponent(healthCheckMatch[1]); - return await providerApi.handleHealthCheck(req, res, currentConfig, providerPoolManager, providerType); - } - // Generate OAuth authorization URL for providers const generateAuthUrlMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/generate-auth-url$/); if (method === 'POST' && generateAuthUrlMatch) { @@ -216,6 +243,11 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo return await uploadConfigApi.handleDownloadAllConfigs(req, res); } + // Delete all unbound config files + if (method === 'DELETE' && pathParam === '/api/upload-configs/delete-unbound') { + return await uploadConfigApi.handleDeleteUnboundConfigs(req, res, currentConfig, providerPoolManager); + } + // Quick link config to corresponding provider based on directory if (method === 'POST' && pathParam === '/api/quick-link-provider') { return await providerApi.handleQuickLinkProvider(req, res, currentConfig, providerPoolManager); diff --git a/src/ui-modules/provider-api.js b/src/ui-modules/provider-api.js index 1da20b4..4eef5ad 100644 --- a/src/ui-modules/provider-api.js +++ b/src/ui-modules/provider-api.js @@ -467,6 +467,184 @@ export async function handleResetProviderHealth(req, res, currentConfig, provide } } +/** + * 删除特定提供商类型的所有不健康节点 + */ +export async function handleDeleteUnhealthyProviders(req, res, currentConfig, providerPoolManager, providerType) { + try { + const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json'; + let providerPools = {}; + + // Load existing pools + if (existsSync(filePath)) { + try { + const fileContent = readFileSync(filePath, 'utf-8'); + providerPools = JSON.parse(fileContent); + } catch (readError) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: { message: 'Provider pools file not found' } })); + return true; + } + } + + // Find and remove unhealthy providers + const providers = providerPools[providerType] || []; + + if (providers.length === 0) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: { message: 'No providers found for this type' } })); + return true; + } + + // Filter out unhealthy providers (keep only healthy ones) + const unhealthyProviders = providers.filter(p => !p.isHealthy); + const healthyProviders = providers.filter(p => p.isHealthy); + + if (unhealthyProviders.length === 0) { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: true, + message: 'No unhealthy providers to delete', + deletedCount: 0, + remainingCount: providers.length + })); + return true; + } + + // Update the provider pool with only healthy providers + if (healthyProviders.length === 0) { + delete providerPools[providerType]; + } else { + providerPools[providerType] = healthyProviders; + } + + // Save to file + writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); + console.log(`[UI API] Deleted ${unhealthyProviders.length} unhealthy providers from ${providerType}`); + + // Update provider pool manager if available + if (providerPoolManager) { + providerPoolManager.providerPools = providerPools; + providerPoolManager.initializeProviderStatus(); + } + + // 广播更新事件 + broadcastEvent('config_update', { + action: 'delete_unhealthy', + filePath: filePath, + providerType, + deletedCount: unhealthyProviders.length, + deletedProviders: unhealthyProviders.map(p => ({ uuid: p.uuid, customName: p.customName })), + timestamp: new Date().toISOString() + }); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: true, + message: `Successfully deleted ${unhealthyProviders.length} unhealthy providers`, + deletedCount: unhealthyProviders.length, + remainingCount: healthyProviders.length, + deletedProviders: unhealthyProviders.map(p => ({ uuid: p.uuid, customName: p.customName })) + })); + return true; + } catch (error) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: { message: error.message } })); + return true; + } +} + +/** + * 批量刷新特定提供商类型的所有不健康节点的 UUID + */ +export async function handleRefreshUnhealthyUuids(req, res, currentConfig, providerPoolManager, providerType) { + try { + const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json'; + let providerPools = {}; + + // Load existing pools + if (existsSync(filePath)) { + try { + const fileContent = readFileSync(filePath, 'utf-8'); + providerPools = JSON.parse(fileContent); + } catch (readError) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: { message: 'Provider pools file not found' } })); + return true; + } + } + + // Find unhealthy providers + const providers = providerPools[providerType] || []; + + if (providers.length === 0) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: { message: 'No providers found for this type' } })); + return true; + } + + // Filter unhealthy providers and refresh their UUIDs + const refreshedProviders = []; + for (const provider of providers) { + if (!provider.isHealthy) { + const oldUuid = provider.uuid; + const newUuid = generateUUID(); + provider.uuid = newUuid; + refreshedProviders.push({ + oldUuid, + newUuid, + customName: provider.customName + }); + } + } + + if (refreshedProviders.length === 0) { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: true, + message: 'No unhealthy providers to refresh', + refreshedCount: 0, + totalCount: providers.length + })); + return true; + } + + // Save to file + writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); + console.log(`[UI API] Refreshed UUIDs for ${refreshedProviders.length} unhealthy providers in ${providerType}`); + + // Update provider pool manager if available + if (providerPoolManager) { + providerPoolManager.providerPools = providerPools; + providerPoolManager.initializeProviderStatus(); + } + + // 广播更新事件 + broadcastEvent('config_update', { + action: 'refresh_unhealthy_uuids', + filePath: filePath, + providerType, + refreshedCount: refreshedProviders.length, + refreshedProviders, + timestamp: new Date().toISOString() + }); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: true, + message: `Successfully refreshed UUIDs for ${refreshedProviders.length} unhealthy providers`, + refreshedCount: refreshedProviders.length, + totalCount: providers.length, + refreshedProviders + })); + return true; + } catch (error) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: { message: error.message } })); + return true; + } +} + /** * 对特定提供商类型的所有提供商执行健康检查 */ @@ -486,11 +664,27 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan return true; } - console.log(`[UI API] Starting health check for ${providers.length} providers in ${providerType}`); + // 只检测不健康的节点 + const unhealthyProviders = providers.filter(ps => !ps.config.isHealthy); + + if (unhealthyProviders.length === 0) { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: true, + message: 'No unhealthy providers to check', + successCount: 0, + failCount: 0, + totalCount: providers.length, + results: [] + })); + return true; + } + + console.log(`[UI API] Starting health check for ${unhealthyProviders.length} unhealthy providers in ${providerType} (total: ${providers.length})`); // 执行健康检测(强制检查,忽略 checkHealth 配置) const results = []; - for (const providerStatus of providers) { + for (const providerStatus of unhealthyProviders) { const providerConfig = providerStatus.config; // 跳过已禁用的节点 @@ -556,7 +750,7 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan const successCount = results.filter(r => r.success === true).length; const failCount = results.filter(r => r.success === false).length; - console.log(`[UI API] Health check completed for ${providerType}: ${successCount} healthy, ${failCount} unhealthy`); + console.log(`[UI API] Health check completed for ${providerType}: ${successCount} recovered, ${failCount} still unhealthy (checked ${unhealthyProviders.length} unhealthy nodes)`); // 广播更新事件 broadcastEvent('config_update', { @@ -704,4 +898,77 @@ export async function handleQuickLinkProvider(req, res, currentConfig, providerP })); return true; } +} + +/** + * 刷新特定提供商的UUID + */ +export async function handleRefreshProviderUuid(req, res, currentConfig, providerPoolManager, providerType, providerUuid) { + try { + const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json'; + let providerPools = {}; + + // Load existing pools + if (existsSync(filePath)) { + try { + const fileContent = readFileSync(filePath, 'utf-8'); + providerPools = JSON.parse(fileContent); + } catch (readError) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: { message: 'Provider pools file not found' } })); + return true; + } + } + + // Find the provider + const providers = providerPools[providerType] || []; + const providerIndex = providers.findIndex(p => p.uuid === providerUuid); + + if (providerIndex === -1) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: { message: 'Provider not found' } })); + return true; + } + + // Generate new UUID + const oldUuid = providerUuid; + const newUuid = generateUUID(); + + // Update provider UUID + providerPools[providerType][providerIndex].uuid = newUuid; + + // Save to file + writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); + console.log(`[UI API] Refreshed UUID for provider in ${providerType}: ${oldUuid} -> ${newUuid}`); + + // Update provider pool manager if available + if (providerPoolManager) { + providerPoolManager.providerPools = providerPools; + providerPoolManager.initializeProviderStatus(); + } + + // 广播更新事件 + broadcastEvent('config_update', { + action: 'refresh_uuid', + filePath: filePath, + providerType, + oldUuid, + newUuid, + timestamp: new Date().toISOString() + }); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: true, + message: 'UUID refreshed successfully', + oldUuid, + newUuid, + provider: providerPools[providerType][providerIndex] + })); + return true; + } catch (error) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: { message: error.message } })); + return true; + } } \ No newline at end of file diff --git a/src/ui-modules/upload-config-api.js b/src/ui-modules/upload-config-api.js index aa673a4..426c323 100644 --- a/src/ui-modules/upload-config-api.js +++ b/src/ui-modules/upload-config-api.js @@ -197,4 +197,114 @@ export async function handleDownloadAllConfigs(req, res) { })); return true; } +} + +/** + * 批量删除未绑定的配置文件 + * 只删除 configs/xxx/ 子目录下的未绑定配置文件 + */ +export async function handleDeleteUnboundConfigs(req, res, currentConfig, providerPoolManager) { + try { + // 首先获取所有配置文件及其绑定状态 + const configFiles = await scanConfigFiles(currentConfig, providerPoolManager); + + // 筛选出未绑定的配置文件,并且必须在 configs/xxx/ 子目录下 + // 即路径格式为 configs/子目录名/文件名,而不是直接在 configs/ 根目录下 + const unboundConfigs = configFiles.filter(config => { + if (config.isUsed) return false; + + // 检查路径是否在 configs/xxx/ 子目录下 + // 路径格式应该是 configs/子目录/... + const normalizedPath = config.path.replace(/\\/g, '/'); + const pathParts = normalizedPath.split('/'); + + // 路径至少需要3部分:configs/子目录/文件名 + // 例如:configs/kiro/xxx.json 或 configs/gemini/xxx.json + if (pathParts.length >= 3 && pathParts[0] === 'configs') { + // 确保第二部分是子目录名(不是文件名) + return true; + } + + return false; + }); + + if (unboundConfigs.length === 0) { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: true, + message: 'No unbound config files to delete', + deletedCount: 0, + deletedFiles: [] + })); + return true; + } + + const deletedFiles = []; + const failedFiles = []; + + for (const config of unboundConfigs) { + try { + const fullPath = path.join(process.cwd(), config.path); + + // 安全检查:确保文件路径在允许的目录内 + const allowedDirs = ['configs']; + const relativePath = path.relative(process.cwd(), fullPath); + const isAllowed = allowedDirs.some(dir => relativePath.startsWith(dir + path.sep) || relativePath === dir); + + if (!isAllowed) { + failedFiles.push({ + path: config.path, + error: 'Access denied: can only delete files in configs directory' + }); + continue; + } + + if (!existsSync(fullPath)) { + failedFiles.push({ + path: config.path, + error: 'File does not exist' + }); + continue; + } + + await fs.unlink(fullPath); + deletedFiles.push(config.path); + + } catch (error) { + failedFiles.push({ + path: config.path, + error: error.message + }); + } + } + + // 广播更新事件 + if (deletedFiles.length > 0) { + broadcastEvent('config_update', { + action: 'batch_delete', + deletedFiles: deletedFiles, + timestamp: new Date().toISOString() + }); + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: true, + message: `Deleted ${deletedFiles.length} unbound config files`, + deletedCount: deletedFiles.length, + deletedFiles: deletedFiles, + failedCount: failedFiles.length, + failedFiles: failedFiles + })); + return true; + } catch (error) { + console.error('[UI API] Failed to delete unbound configs:', error); + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + error: { + message: 'Failed to delete unbound configs: ' + error.message + } + })); + return true; + } } \ No newline at end of file diff --git a/static/app/i18n.js b/static/app/i18n.js index c940cf1..063134c 100644 --- a/static/app/i18n.js +++ b/static/app/i18n.js @@ -28,7 +28,7 @@ const translations = { 'nav.dashboard': '仪表盘', 'nav.config': '配置管理', 'nav.providers': '提供商池管理', - 'nav.upload': '配置管理', + 'nav.upload': '凭据文件管理', 'nav.usage': '用量查询', 'nav.logs': '实时日志', 'nav.plugins': '插件管理', @@ -306,9 +306,17 @@ const translations = { 'config.placeholder.model': '例如: gpt-3.5-turbo', // Upload Config - 'upload.title': '配置管理', + 'upload.title': '凭据文件管理', 'upload.search': '搜索配置', 'upload.searchPlaceholder': '输入文件名', + 'upload.providerFilter': '提供商类型', + 'upload.providerFilter.all': '全部提供商', + 'upload.providerFilter.kiro': 'Kiro OAuth', + 'upload.providerFilter.gemini': 'Gemini OAuth', + 'upload.providerFilter.qwen': 'Qwen OAuth', + 'upload.providerFilter.antigravity': 'Antigravity', + 'upload.providerFilter.orchids': 'Orchids OAuth', + 'upload.providerFilter.other': '其他/未识别', 'upload.statusFilter': '关联状态', 'upload.statusFilter.all': '全部状态', 'upload.statusFilter.used': '已关联', @@ -360,6 +368,13 @@ const translations = { 'upload.batchLink.processing': '正在批量关联 {count} 个配置...', 'upload.batchLink.success': '成功关联 {count} 个配置', 'upload.batchLink.partial': '关联完成: 成功 {success} 个, 失败 {fail} 个', + 'upload.deleteUnbound': '删除未关联', + 'upload.deleteUnbound.none': '没有可删除的未关联配置文件(仅删除 configs/子目录/ 下的文件)', + 'upload.deleteUnbound.confirm': '确定要删除 {count} 个未关联的配置文件吗?\n\n注意:仅删除 configs/子目录/ 下的未关联文件,configs/ 根目录下的文件不会被删除。\n\n此操作不可撤销!', + 'upload.deleteUnbound.processing': '正在删除未关联的配置文件...', + 'upload.deleteUnbound.success': '成功删除 {count} 个未关联的配置文件', + 'upload.deleteUnbound.partial': '删除完成: 成功 {success} 个, 失败 {fail} 个', + 'upload.deleteUnbound.failed': '删除未关联配置失败', // Providers 'providers.title': '提供商池管理', @@ -382,9 +397,9 @@ const translations = { 'modal.provider.healthyAccounts': '健康账户:', 'modal.provider.add': '添加新提供商', 'modal.provider.resetHealth': '重置为健康', - 'modal.provider.healthCheck': '健康检测', + 'modal.provider.healthCheck': '检测不健康', 'modal.provider.resetHealthConfirm': '确定要将 {type} 的所有节点重置为健康状态吗?\n\n这将清除所有节点的错误计数和错误时间。', - 'modal.provider.healthCheckConfirm': '确定要对 {type} 的所有节点执行健康检测吗?\n\n这将向每个节点发送测试请求来验证其可用性。', + 'modal.provider.healthCheckConfirm': '确定要对 {type} 的不健康节点执行健康检测吗?\n\n这将向不健康节点发送测试请求来验证其可用性。', 'modal.provider.deleteConfirm': '确定要删除这个提供商配置吗?此操作不可恢复。', 'modal.provider.disableConfirm': '确定要禁用这个提供商配置吗?禁用后该提供商将不会被选中使用。', 'modal.provider.enableConfirm': '确定要启用这个提供商配置吗?', @@ -415,6 +430,10 @@ const translations = { 'modal.provider.enabled': '启用', 'modal.provider.disabled': '禁用', 'modal.provider.noProviderType': '不支持的提供商类型', + 'modal.provider.refreshUuid': '刷新uuid', + 'modal.provider.refreshUuidConfirm': '确定要刷新此提供商的uuid吗?\n\n旧uuid: {oldUuid}\n\n刷新后将生成新的uuid,请确保没有其他系统依赖此uuid。', + 'modal.provider.refreshUuid.success': 'uuid刷新成功\n\n旧uuid: {oldUuid}\n新uuid: {newUuid}', + 'modal.provider.refreshUuid.failed': 'uuid刷新失败', 'modal.provider.load.failed': '加载提供商详情失败', 'modal.provider.auth.initializing': '正在初始化凭据生成...', @@ -429,6 +448,20 @@ const translations = { 'modal.provider.add.failed': '添加失败', 'modal.provider.resetHealth.success': '成功重置 {count} 个节点的健康状态', 'modal.provider.resetHealth.failed': '重置健康状态失败', + 'modal.provider.deleteUnhealthy': '删除不健康节点', + 'modal.provider.deleteUnhealthyBtn': '删除不健康', + 'modal.provider.deleteUnhealthyConfirm': '确定要删除 {count} 个不健康节点吗?此操作不可恢复。', + 'modal.provider.deleteUnhealthy.noUnhealthy': '没有不健康节点', + 'modal.provider.deleteUnhealthy.deleting': '正在删除...', + 'modal.provider.deleteUnhealthy.success': '已删除 {count} 个节点', + 'modal.provider.deleteUnhealthy.failed': '删除失败', + 'modal.provider.refreshUnhealthyUuids': '刷新不健康UUID', + 'modal.provider.refreshUnhealthyUuidsBtn': '刷新UUID', + 'modal.provider.refreshUnhealthyUuidsConfirm': '确定要刷新 {count} 个不健康节点的UUID吗?', + 'modal.provider.refreshUnhealthyUuids.noUnhealthy': '没有不健康节点', + 'modal.provider.refreshUnhealthyUuids.refreshing': '正在刷新...', + 'modal.provider.refreshUnhealthyUuids.success': '已刷新 {count} 个节点的UUID', + 'modal.provider.refreshUnhealthyUuids.failed': '刷新失败', 'modal.provider.kiroAuthHint': '使用 AWS Builder ID 登录方式时,需要 clientIdclientSecret 字段,可在同文件夹下的另一个 JSON 文件中获取', // Pagination @@ -550,7 +583,7 @@ const translations = { 'nav.dashboard': 'Dashboard', 'nav.config': 'Configuration', 'nav.providers': 'Provider Pools', - 'nav.upload': 'Config Management', + 'nav.upload': 'Credential Files', 'nav.usage': 'Usage Query', 'nav.logs': 'Real-time Logs', 'nav.plugins': 'Plugin Management', @@ -828,9 +861,17 @@ const translations = { 'config.placeholder.model': 'e.g.: gpt-3.5-turbo', // Upload Config - 'upload.title': 'Config Management', + 'upload.title': 'Credential Files Management', 'upload.search': 'Search Config', 'upload.searchPlaceholder': 'Enter filename', + 'upload.providerFilter': 'Provider Type', + 'upload.providerFilter.all': 'All Providers', + 'upload.providerFilter.kiro': 'Kiro OAuth', + 'upload.providerFilter.gemini': 'Gemini OAuth', + 'upload.providerFilter.qwen': 'Qwen OAuth', + 'upload.providerFilter.antigravity': 'Antigravity', + 'upload.providerFilter.orchids': 'Orchids OAuth', + 'upload.providerFilter.other': 'Other/Unknown', 'upload.statusFilter': 'Association Status', 'upload.statusFilter.all': 'All Status', 'upload.statusFilter.used': 'Associated', @@ -882,6 +923,13 @@ const translations = { 'upload.batchLink.processing': 'Batch linking {count} configurations...', 'upload.batchLink.success': 'Successfully linked {count} configurations', 'upload.batchLink.partial': 'Linking completed: {success} succeeded, {fail} failed', + 'upload.deleteUnbound': 'Delete Unbound', + 'upload.deleteUnbound.none': 'No unbound config files to delete (only files in configs/subdirectory/ are deleted)', + 'upload.deleteUnbound.confirm': 'Are you sure you want to delete {count} unbound config files?\n\nNote: Only unbound files in configs/subdirectory/ will be deleted. Files directly in configs/ root will not be deleted.\n\nThis action cannot be undone!', + 'upload.deleteUnbound.processing': 'Deleting unbound config files...', + 'upload.deleteUnbound.success': 'Successfully deleted {count} unbound config files', + 'upload.deleteUnbound.partial': 'Deletion completed: {success} succeeded, {fail} failed', + 'upload.deleteUnbound.failed': 'Failed to delete unbound configs', // Providers 'providers.title': 'Provider Pool Management', @@ -904,9 +952,9 @@ const translations = { 'modal.provider.healthyAccounts': 'Healthy Accounts:', 'modal.provider.add': 'Add Provider', 'modal.provider.resetHealth': 'Reset Health', - 'modal.provider.healthCheck': 'Health Check', + 'modal.provider.healthCheck': 'Check Unhealthy', 'modal.provider.resetHealthConfirm': 'Are you sure you want to reset all {type} nodes to healthy status?\n\nThis will clear error counts and timestamps for all nodes.', - 'modal.provider.healthCheckConfirm': 'Are you sure you want to perform a health check on all {type} nodes?\n\nThis will send test requests to each node to verify availability.', + 'modal.provider.healthCheckConfirm': 'Are you sure you want to perform a health check on unhealthy {type} nodes?\n\nThis will send test requests to unhealthy nodes to verify availability.', 'modal.provider.deleteConfirm': 'Are you sure you want to delete this provider config? This cannot be undone.', 'modal.provider.disableConfirm': 'Are you sure you want to disable this provider? It will no longer be selected for use.', 'modal.provider.enableConfirm': 'Are you sure you want to enable this provider?', @@ -937,6 +985,10 @@ const translations = { 'modal.provider.enabled': 'Enabled', 'modal.provider.disabled': 'Disabled', 'modal.provider.noProviderType': 'Unsupported provider type', + 'modal.provider.refreshUuid': 'Refresh uuid', + 'modal.provider.refreshUuidConfirm': 'Are you sure you want to refresh the uuid for this provider?\n\nOld uuid: {oldUuid}\n\nA new uuid will be generated. Make sure no other systems depend on this uuid.', + 'modal.provider.refreshUuid.success': 'uuid refreshed successfully\n\nOld uuid: {oldUuid}\nNew uuid: {newUuid}', + 'modal.provider.refreshUuid.failed': 'Failed to refresh uuid', 'modal.provider.load.failed': 'Failed to load provider details', 'modal.provider.auth.initializing': 'Initializing credential generation...', @@ -951,6 +1003,20 @@ const translations = { 'modal.provider.add.failed': 'Add failed', 'modal.provider.resetHealth.success': 'Successfully reset health status for {count} nodes', 'modal.provider.resetHealth.failed': 'Failed to reset health status', + 'modal.provider.deleteUnhealthy': 'Delete unhealthy nodes', + 'modal.provider.deleteUnhealthyBtn': 'Delete Unhealthy', + 'modal.provider.deleteUnhealthyConfirm': 'Delete {count} unhealthy node(s)? This cannot be undone.', + 'modal.provider.deleteUnhealthy.noUnhealthy': 'No unhealthy nodes', + 'modal.provider.deleteUnhealthy.deleting': 'Deleting...', + 'modal.provider.deleteUnhealthy.success': 'Deleted {count} node(s)', + 'modal.provider.deleteUnhealthy.failed': 'Delete failed', + 'modal.provider.refreshUnhealthyUuids': 'Refresh unhealthy UUIDs', + 'modal.provider.refreshUnhealthyUuidsBtn': 'Refresh UUIDs', + 'modal.provider.refreshUnhealthyUuidsConfirm': 'Refresh UUIDs for {count} unhealthy node(s)?', + 'modal.provider.refreshUnhealthyUuids.noUnhealthy': 'No unhealthy nodes', + 'modal.provider.refreshUnhealthyUuids.refreshing': 'Refreshing...', + 'modal.provider.refreshUnhealthyUuids.success': 'Refreshed {count} UUID(s)', + 'modal.provider.refreshUnhealthyUuids.failed': 'Refresh failed', 'modal.provider.kiroAuthHint': 'When using AWS Builder ID login, clientId and clientSecret fields are required, which can be found in another JSON file in the same folder', // Pagination diff --git a/static/app/modal.js b/static/app/modal.js index 0def12c..c30540c 100644 --- a/static/app/modal.js +++ b/static/app/modal.js @@ -65,8 +65,14 @@ function showProviderManagerModal(data) { - + + @@ -418,6 +424,9 @@ function renderProviderList(providers) { +
@@ -784,19 +793,10 @@ function editProvider(uuid, event) { // 添加编辑状态类 providerDetail.classList.add('editing'); - // 替换编辑按钮为保存和取消按钮,但保留禁用/启用按钮 + // 替换编辑按钮为保存和取消按钮,不显示禁用/启用按钮 const actionsGroup = providerDetail.querySelector('.provider-actions-group'); - const toggleButton = actionsGroup.querySelector('[onclick*="toggleProviderStatus"]'); - const currentProvider = providerDetail.closest('.provider-modal').querySelector(`[data-uuid="${uuid}"]`); - const isCurrentlyDisabled = currentProvider.classList.contains('disabled'); - const toggleButtonText = isCurrentlyDisabled ? '启用' : '禁用'; - const toggleButtonIcon = isCurrentlyDisabled ? 'fas fa-play' : 'fas fa-ban'; - const toggleButtonClass = isCurrentlyDisabled ? 'btn-success' : 'btn-warning'; actionsGroup.innerHTML = ` - @@ -852,11 +852,11 @@ function cancelEdit(uuid, event) { select.value = originalValue || ''; }); - // 恢复原来的编辑和删除按钮,但保留禁用/启用按钮 + // 恢复原来的按钮布局 const actionsGroup = providerDetail.querySelector('.provider-actions-group'); const currentProvider = providerDetail.closest('.provider-modal').querySelector(`[data-uuid="${uuid}"]`); const isCurrentlyDisabled = currentProvider.classList.contains('disabled'); - const toggleButtonText = isCurrentlyDisabled ? '启用' : '禁用'; + const toggleButtonText = isCurrentlyDisabled ? t('modal.provider.enabled') : t('modal.provider.disabled'); const toggleButtonIcon = isCurrentlyDisabled ? 'fas fa-play' : 'fas fa-ban'; const toggleButtonClass = isCurrentlyDisabled ? 'btn-success' : 'btn-warning'; @@ -865,10 +865,13 @@ function cancelEdit(uuid, event) { ${toggleButtonText} + `; } @@ -1379,6 +1382,134 @@ async function performHealthCheck(providerType) { } } +/** + * 刷新提供商UUID + * @param {string} uuid - 提供商UUID + * @param {Event} event - 事件对象 + */ +async function refreshProviderUuid(uuid, event) { + event.stopPropagation(); + + if (!confirm(t('modal.provider.refreshUuidConfirm', { oldUuid: uuid }))) { + return; + } + + const providerDetail = event.target.closest('.provider-item-detail'); + const providerType = providerDetail.closest('.provider-modal').getAttribute('data-provider-type'); + + try { + const response = await window.apiClient.post( + `/providers/${encodeURIComponent(providerType)}/${uuid}/refresh-uuid`, + {} + ); + + if (response.success) { + showToast(t('common.success'), t('modal.provider.refreshUuid.success', { oldUuid: response.oldUuid, newUuid: response.newUuid }), 'success'); + + // 重新加载配置 + await window.apiClient.post('/reload-config'); + + // 刷新提供商配置显示 + await refreshProviderConfig(providerType); + } else { + showToast(t('common.error'), t('modal.provider.refreshUuid.failed'), 'error'); + } + } catch (error) { + console.error('刷新uuid失败:', error); + showToast(t('common.error'), t('modal.provider.refreshUuid.failed') + ': ' + error.message, 'error'); + } +} + +/** + * 删除所有不健康的提供商节点 + * @param {string} providerType - 提供商类型 + */ +async function deleteUnhealthyProviders(providerType) { + // 先获取不健康节点数量 + const unhealthyCount = currentProviders.filter(p => !p.isHealthy).length; + + if (unhealthyCount === 0) { + showToast(t('common.info'), t('modal.provider.deleteUnhealthy.noUnhealthy'), 'info'); + return; + } + + if (!confirm(t('modal.provider.deleteUnhealthyConfirm', { type: providerType, count: unhealthyCount }))) { + return; + } + + try { + showToast(t('common.info'), t('modal.provider.deleteUnhealthy.deleting'), 'info'); + + const response = await window.apiClient.delete( + `/providers/${encodeURIComponent(providerType)}/delete-unhealthy` + ); + + if (response.success) { + showToast( + t('common.success'), + t('modal.provider.deleteUnhealthy.success', { count: response.deletedCount }), + 'success' + ); + + // 重新加载配置 + await window.apiClient.post('/reload-config'); + + // 刷新提供商配置显示 + await refreshProviderConfig(providerType); + } else { + showToast(t('common.error'), t('modal.provider.deleteUnhealthy.failed'), 'error'); + } + } catch (error) { + console.error('删除不健康节点失败:', error); + showToast(t('common.error'), t('modal.provider.deleteUnhealthy.failed') + ': ' + error.message, 'error'); + } +} + +/** + * 批量刷新不健康节点的UUID + * @param {string} providerType - 提供商类型 + */ +async function refreshUnhealthyUuids(providerType) { + // 先获取不健康节点数量 + const unhealthyCount = currentProviders.filter(p => !p.isHealthy).length; + + if (unhealthyCount === 0) { + showToast(t('common.info'), t('modal.provider.refreshUnhealthyUuids.noUnhealthy'), 'info'); + return; + } + + if (!confirm(t('modal.provider.refreshUnhealthyUuidsConfirm', { type: providerType, count: unhealthyCount }))) { + return; + } + + try { + showToast(t('common.info'), t('modal.provider.refreshUnhealthyUuids.refreshing'), 'info'); + + const response = await window.apiClient.post( + `/providers/${encodeURIComponent(providerType)}/refresh-unhealthy-uuids` + ); + + if (response.success) { + showToast( + t('common.success'), + t('modal.provider.refreshUnhealthyUuids.success', { count: response.refreshedCount }), + 'success' + ); + + // 重新加载配置 + await window.apiClient.post('/reload-config'); + + // 刷新提供商配置显示 + await refreshProviderConfig(providerType); + } else { + showToast(t('common.error'), t('modal.provider.refreshUnhealthyUuids.failed'), 'error'); + } + } catch (error) { + console.error('刷新不健康节点UUID失败:', error); + showToast(t('common.error'), t('modal.provider.refreshUnhealthyUuids.failed') + ': ' + error.message, 'error'); + } +} + /** * 渲染不支持的模型选择器(不调用API,直接使用传入的模型列表) * @param {string} uuid - 提供商UUID @@ -1430,9 +1561,12 @@ export { toggleProviderStatus, resetAllProvidersHealth, performHealthCheck, + deleteUnhealthyProviders, + refreshUnhealthyUuids, loadModelsForProviderType, renderNotSupportedModelsSelector, - goToProviderPage + goToProviderPage, + refreshProviderUuid }; // 将函数挂载到window对象 @@ -1447,4 +1581,7 @@ window.addProvider = addProvider; window.toggleProviderStatus = toggleProviderStatus; window.resetAllProvidersHealth = resetAllProvidersHealth; window.performHealthCheck = performHealthCheck; -window.goToProviderPage = goToProviderPage; \ No newline at end of file +window.deleteUnhealthyProviders = deleteUnhealthyProviders; +window.refreshUnhealthyUuids = refreshUnhealthyUuids; +window.goToProviderPage = goToProviderPage; +window.refreshProviderUuid = refreshProviderUuid; \ No newline at end of file diff --git a/static/app/upload-config-manager.js b/static/app/upload-config-manager.js index f802112..3fcf96d 100644 --- a/static/app/upload-config-manager.js +++ b/static/app/upload-config-manager.js @@ -12,7 +12,7 @@ let isLoadingConfigs = false; // 防止重复加载配置 * @param {string} searchTerm - 搜索关键词 * @param {string} statusFilter - 状态过滤 */ -function searchConfigs(searchTerm = '', statusFilter = '') { +function searchConfigs(searchTerm = '', statusFilter = '', providerFilter = '') { if (!allConfigs.length) { console.log('没有配置数据可搜索'); return; @@ -29,7 +29,20 @@ function searchConfigs(searchTerm = '', statusFilter = '') { const configStatus = config.isUsed ? 'used' : 'unused'; const matchesStatus = !statusFilter || configStatus === statusFilter; - return matchesSearch && matchesStatus; + // 提供商类型过滤 + let matchesProvider = true; + if (providerFilter) { + const providerInfo = detectProviderFromPath(config.path); + if (providerFilter === 'other') { + // "其他/未识别" 选项:匹配没有识别到提供商的配置 + matchesProvider = providerInfo === null; + } else { + // 匹配特定提供商类型 + matchesProvider = providerInfo !== null && providerInfo.providerType === providerFilter; + } + } + + return matchesSearch && matchesStatus && matchesProvider; }); renderConfigList(); @@ -705,6 +718,7 @@ function initUploadConfigManager() { const searchInput = document.getElementById('configSearch'); const searchBtn = document.getElementById('searchConfigBtn'); const statusFilter = document.getElementById('configStatusFilter'); + const providerFilter = document.getElementById('configProviderFilter'); const refreshBtn = document.getElementById('refreshConfigList'); const downloadAllBtn = document.getElementById('downloadAllConfigs'); @@ -712,7 +726,8 @@ function initUploadConfigManager() { searchInput.addEventListener('input', debounce(() => { const searchTerm = searchInput.value.trim(); const currentStatusFilter = statusFilter?.value || ''; - searchConfigs(searchTerm, currentStatusFilter); + const currentProviderFilter = providerFilter?.value || ''; + searchConfigs(searchTerm, currentStatusFilter, currentProviderFilter); }, 300)); } @@ -720,7 +735,8 @@ function initUploadConfigManager() { searchBtn.addEventListener('click', () => { const searchTerm = searchInput?.value.trim() || ''; const currentStatusFilter = statusFilter?.value || ''; - searchConfigs(searchTerm, currentStatusFilter); + const currentProviderFilter = providerFilter?.value || ''; + searchConfigs(searchTerm, currentStatusFilter, currentProviderFilter); }); } @@ -728,7 +744,17 @@ function initUploadConfigManager() { statusFilter.addEventListener('change', () => { const searchTerm = searchInput?.value.trim() || ''; const currentStatusFilter = statusFilter.value; - searchConfigs(searchTerm, currentStatusFilter); + const currentProviderFilter = providerFilter?.value || ''; + searchConfigs(searchTerm, currentStatusFilter, currentProviderFilter); + }); + } + + if (providerFilter) { + providerFilter.addEventListener('change', () => { + const searchTerm = searchInput?.value.trim() || ''; + const currentStatusFilter = statusFilter?.value || ''; + const currentProviderFilter = providerFilter.value; + searchConfigs(searchTerm, currentStatusFilter, currentProviderFilter); }); } @@ -746,6 +772,12 @@ function initUploadConfigManager() { batchLinkBtn.addEventListener('click', batchLinkProviderConfigs); } + // 删除未绑定配置按钮 + const deleteUnboundBtn = document.getElementById('deleteUnboundBtn'); + if (deleteUnboundBtn) { + deleteUnboundBtn.addEventListener('click', deleteUnboundConfigs); + } + // 初始加载配置列表 loadConfigList(); } @@ -928,6 +960,67 @@ async function batchLinkProviderConfigs() { } } +/** + * 删除所有未绑定的配置文件 + * 只删除 configs/xxx/ 子目录下的未绑定配置文件 + */ +async function deleteUnboundConfigs() { + // 统计未绑定的配置数量,并且必须在 configs/xxx/ 子目录下 + const unboundConfigs = allConfigs.filter(config => { + if (config.isUsed) return false; + + // 检查路径是否在 configs/xxx/ 子目录下 + const normalizedPath = config.path.replace(/\\/g, '/'); + const pathParts = normalizedPath.split('/'); + + // 路径至少需要3部分:configs/子目录/文件名 + // 例如:configs/kiro/xxx.json 或 configs/gemini/xxx.json + if (pathParts.length >= 3 && pathParts[0] === 'configs') { + return true; + } + + return false; + }); + + if (unboundConfigs.length === 0) { + showToast(t('common.info'), t('upload.deleteUnbound.none'), 'info'); + return; + } + + // 显示确认对话框 + const confirmMsg = t('upload.deleteUnbound.confirm', { count: unboundConfigs.length }); + if (!confirm(confirmMsg)) { + return; + } + + try { + showToast(t('common.info'), t('upload.deleteUnbound.processing'), 'info'); + + const result = await window.apiClient.delete('/upload-configs/delete-unbound'); + + if (result.deletedCount > 0) { + showToast(t('common.success'), t('upload.deleteUnbound.success', { count: result.deletedCount }), 'success'); + + // 刷新配置列表 + await loadConfigList(); + } else { + showToast(t('common.info'), t('upload.deleteUnbound.none'), 'info'); + } + + // 如果有失败的文件,显示警告 + if (result.failedCount > 0) { + console.warn('部分文件删除失败:', result.failedFiles); + showToast(t('common.warning'), t('upload.deleteUnbound.partial', { + success: result.deletedCount, + fail: result.failedCount + }), 'warning'); + } + } catch (error) { + console.error('删除未绑定配置失败:', error); + showToast(t('common.error'), t('upload.deleteUnbound.failed') + ': ' + error.message, 'error'); + } +} + /** * 防抖函数 * @param {Function} func - 要防抖的函数 @@ -1002,5 +1095,6 @@ export { deleteConfig, closeConfigModal, copyConfigContent, - reloadConfig + reloadConfig, + deleteUnboundConfigs }; \ No newline at end of file diff --git a/static/components/section-providers.css b/static/components/section-providers.css index 2e133ae..3375bbb 100644 --- a/static/components/section-providers.css +++ b/static/components/section-providers.css @@ -1004,6 +1004,34 @@ box-shadow: 0 4px 12px var(--info-hover); } +/* 删除不健康节点按钮样式 */ +.provider-summary-actions .btn-danger { + background: linear-gradient(135deg, var(--danger-alt) 0%, var(--danger-secondary) 100%); + color: var(--white); + border: none; + box-shadow: 0 2px 8px var(--danger-30); +} + +.provider-summary-actions .btn-danger:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px var(--danger-40); + background: linear-gradient(135deg, var(--danger-color) 0%, var(--danger-alt) 100%); +} + +/* 刷新不健康UUID按钮样式 */ +.provider-summary-actions .btn-secondary { + background: linear-gradient(135deg, var(--neutral-500) 0%, var(--neutral-600) 100%); + color: var(--white); + border: none; + box-shadow: 0 2px 8px var(--neutral-shadow-lg); +} + +.provider-summary-actions .btn-secondary:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px var(--neutral-shadow-lg); + background: linear-gradient(135deg, var(--neutral-600) 0%, var(--neutral-700) 100%); +} + /* 不支持的模型选择器样式 */ .not-supported-models-section { grid-column: 1 / -1; diff --git a/static/components/section-upload-config.css b/static/components/section-upload-config.css index 86da830..ee0c877 100644 --- a/static/components/section-upload-config.css +++ b/static/components/section-upload-config.css @@ -93,6 +93,40 @@ overflow-y: auto; } +/* 无配置文件提示样式 */ +.no-configs { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + text-align: center; + background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-primary) 100%); + border-radius: 0.5rem; + margin: 1rem; +} + +.no-configs p { + font-size: 1rem; + color: var(--text-secondary); + margin: 0; + padding: 1rem 2rem; + background: var(--bg-tertiary); + border: 1px dashed var(--border-color); + border-radius: 0.5rem; + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +.no-configs p::before { + content: '\f07c'; + font-family: 'Font Awesome 6 Free'; + font-weight: 400; + font-size: 1.25rem; + color: var(--text-tertiary); +} + .config-item-manager { padding: 1.5rem; border-bottom: 1px solid var(--border-color); @@ -396,6 +430,96 @@ .delete-modal-footer { flex-direction: column; } } +/* 删除未绑定按钮样式 */ +.btn-delete-unbound { + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); + color: white; + border: none; + border-radius: 6px; + padding: 6px 12px; + font-size: 12px; + cursor: pointer; + margin-left: 8px; + display: inline-flex; + align-items: center; + gap: 6px; + transition: all 0.2s ease; + box-shadow: 0 2px 8px var(--danger-30); + font-weight: 500; +} + +.btn-delete-unbound:hover { + transform: translateY(-1px); + box-shadow: var(--shadow-md); + background: linear-gradient(135deg, #dc2626 0%, #ef4444 100%); +} + +.btn-delete-unbound:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +/* 配置列表头部的刷新按钮样式 */ +.btn-refresh { + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); + color: white; + border: none; + border-radius: 6px; + padding: 6px 12px; + font-size: 12px; + cursor: pointer; + margin-left: 8px; + display: inline-flex; + align-items: center; + gap: 6px; + transition: all 0.2s ease; + box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); + font-weight: 500; +} + +.btn-refresh:hover { + transform: translateY(-1px); + box-shadow: var(--shadow-md); + background: linear-gradient(135deg, #2563eb 0%, #3b82f6 100%); +} + +.btn-refresh:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +/* 配置列表头部的打包下载按钮样式 */ +.btn-download { + background: linear-gradient(135deg, #10b981 0%, #059669 100%); + color: white; + border: none; + border-radius: 6px; + padding: 6px 12px; + font-size: 12px; + cursor: pointer; + margin-left: 8px; + display: inline-flex; + align-items: center; + gap: 6px; + transition: all 0.2s ease; + box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3); + font-weight: 500; +} + +.btn-download:hover { + transform: translateY(-1px); + box-shadow: var(--shadow-md); + background: linear-gradient(135deg, #059669 0%, #10b981 100%); +} + +.btn-download:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + /* 暗黑主题适配 */ [data-theme="dark"] .status-used { background: var(--success-bg); color: var(--success-text); } [data-theme="dark"] .status-unused { background: var(--warning-bg); color: var(--warning-text); } diff --git a/static/components/section-upload-config.html b/static/components/section-upload-config.html index faea4b3..29e7ebb 100644 --- a/static/components/section-upload-config.html +++ b/static/components/section-upload-config.html @@ -1,7 +1,7 @@
-

配置管理

+

凭据文件管理

@@ -15,6 +15,18 @@
+
+ + +
-
- -
- - -
-
@@ -48,6 +49,15 @@ + + +
diff --git a/static/components/sidebar.html b/static/components/sidebar.html index 16276fb..e9beb2c 100644 --- a/static/components/sidebar.html +++ b/static/components/sidebar.html @@ -11,8 +11,8 @@ 提供商池管理 - - 配置管理 + + 凭据文件管理 用量查询 From 281d242466021cd25e63a895c5798c3effb70dd2 Mon Sep 17 00:00:00 2001 From: hex2077 Date: Tue, 13 Jan 2026 19:09:17 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat(auth):=20=E6=94=AF=E6=8C=81=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=20Builder=20ID=20Start=20URL=20=E5=B9=B6?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=9B=B8=E5=85=B3=E5=9B=BD=E9=99=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 Kiro OAuth 添加 Builder ID Start URL 的可配置选项,优先使用前端传入的值 添加相关国际化文本和 UI 控件,允许用户自定义或重新生成 Start URL 同时支持通过 options.authMethod 参数指定认证方法 --- src/auth/oauth-handlers.js | 8 ++++-- static/app/i18n.js | 6 +++++ static/app/provider-manager.js | 45 +++++++++++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/auth/oauth-handlers.js b/src/auth/oauth-handlers.js index c4a241f..c2e578a 100644 --- a/src/auth/oauth-handlers.js +++ b/src/auth/oauth-handlers.js @@ -665,7 +665,7 @@ export async function handleQwenOAuth(currentConfig, options = {}) { * @returns {Promise} 返回授权URL和相关信息 */ export async function handleKiroOAuth(currentConfig, options = {}) { - const method = options.method || 'google'; // 默认使用 Google + const method = options.method || options.authMethod || 'google'; // 默认使用 Google,同时支持 authMethod 参数 console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Starting OAuth with method: ${method}`); @@ -740,6 +740,10 @@ async function handleKiroBuilderIDDeviceCode(currentConfig, options = {}) { } } + // 获取 Builder ID Start URL(优先使用前端传入的值,否则使用默认值) + const builderIDStartURL = options.builderIDStartURL || KIRO_OAUTH_CONFIG.builderIDStartURL; + console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Using Builder ID Start URL: ${builderIDStartURL}`); + // 1. 注册 OIDC 客户端 const regResponse = await fetchWithProxy(`${KIRO_OAUTH_CONFIG.ssoOIDCEndpoint}/client/register`, { method: 'POST', @@ -771,7 +775,7 @@ async function handleKiroBuilderIDDeviceCode(currentConfig, options = {}) { body: JSON.stringify({ clientId: regData.clientId, clientSecret: regData.clientSecret, - startUrl: KIRO_OAUTH_CONFIG.builderIDStartURL + startUrl: builderIDStartURL }) }, 'claude-kiro-oauth'); diff --git a/static/app/i18n.js b/static/app/i18n.js index 063134c..c19517d 100644 --- a/static/app/i18n.js +++ b/static/app/i18n.js @@ -177,6 +177,8 @@ const translations = { 'oauth.kiro.importError': '导入出错', 'oauth.kiro.duplicateToken': '重复凭据 - 此 refreshToken 已存在', 'oauth.kiro.duplicateCredentials': '该凭据已存在,请勿重复导入', + 'oauth.kiro.builderIDStartURL': 'Builder ID Start URL', + 'oauth.kiro.builderIDStartURLHint': '如果您使用 AWS IAM Identity Center,请输入您的 Start URL', 'oauth.iflow.step1': '点击下方按钮在浏览器中打开 iFlow 授权页面', 'oauth.iflow.step2': '使用您的 iFlow 账号登录并授权', 'oauth.iflow.step3': '授权完成后,系统会自动获取 API Key', @@ -527,6 +529,7 @@ const translations = { 'common.loading': '加载中...', 'common.upload': '上传', 'common.generate': '生成', + 'common.optional': '可选', 'common.found': '已找到', 'common.missing': '缺失', 'common.search': '搜索', @@ -732,6 +735,8 @@ const translations = { 'oauth.kiro.importError': 'Import error', 'oauth.kiro.duplicateToken': 'Duplicate - this refreshToken already exists', 'oauth.kiro.duplicateCredentials': 'This credential already exists, please do not import duplicates', + 'oauth.kiro.builderIDStartURL': 'Builder ID Start URL', + 'oauth.kiro.builderIDStartURLHint': 'If you use AWS IAM Identity Center, enter your Start URL', 'oauth.iflow.step1': 'Click the button below to open the iFlow authorization page', 'oauth.iflow.step2': 'Log in with your iFlow account and authorize', 'oauth.iflow.step3': 'After authorization, the system will automatically fetch the API Key', @@ -1085,6 +1090,7 @@ const translations = { 'common.loading': 'Loading...', 'common.upload': 'Upload', 'common.generate': 'Generate', + 'common.optional': 'Optional', 'common.found': 'Found', 'common.missing': 'Missing', 'common.search': 'Search', diff --git a/static/app/provider-manager.js b/static/app/provider-manager.js index f27662e..779d56e 100644 --- a/static/app/provider-manager.js +++ b/static/app/provider-manager.js @@ -680,7 +680,7 @@ function showKiroAuthMethodSelector(providerType) { modal.style.display = 'flex'; modal.innerHTML = ` -