This commit is contained in:
leonai 2026-01-13 19:56:10 +08:00
commit a97f8b4b5a
15 changed files with 1002 additions and 79 deletions

View file

@ -1 +1 @@
2.6.7
2.6.8

2
package-lock.json generated
View file

@ -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",

View file

@ -665,7 +665,7 @@ export async function handleQwenOAuth(currentConfig, options = {}) {
* @returns {Promise<Object>} 返回授权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');

View file

@ -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);

View file

@ -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;
// 跳过已禁用的节点
@ -580,7 +774,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', {
@ -728,4 +922,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;
}
}

View file

@ -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;
}
}

View file

@ -28,7 +28,7 @@ const translations = {
'nav.dashboard': '仪表盘',
'nav.config': '配置管理',
'nav.providers': '提供商池管理',
'nav.upload': '配置管理',
'nav.upload': '凭据文件管理',
'nav.usage': '用量查询',
'nav.logs': '实时日志',
'nav.plugins': '插件管理',
@ -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',
@ -283,6 +285,8 @@ const translations = {
'config.advanced.maxErrorCount': '提供商最大错误次数',
'config.advanced.maxErrorCountPlaceholder': '默认: 3',
'config.advanced.maxErrorCountNote': '提供商连续错误达到此次数后将被标记为不健康,默认为 3 次',
'config.advanced.credentialSwitchMaxRetries': '坏凭证切换最大重试次数',
'config.advanced.credentialSwitchMaxRetriesNote': '认证错误(401/403)后切换凭证的最大重试次数,默认 5 次',
'config.advanced.fallbackChain': '跨类型 Fallback 链配置',
'config.advanced.fallbackChainPlaceholder': '例如:\n{\n "gemini-cli-oauth": ["gemini-antigravity"],\n "gemini-antigravity": ["gemini-cli-oauth"],\n "claude-kiro-oauth": ["claude-custom"]\n}',
'config.advanced.fallbackChainNote': '当某一 Provider Type 所有账号都不健康时,自动切换到配置的 Fallback 类型。JSON 格式,键为主类型,值为 Fallback 类型数组(按优先级排序)',
@ -308,9 +312,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': '已关联',
@ -362,6 +374,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': '提供商池管理',
@ -384,9 +403,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': '确定要启用这个提供商配置吗?',
@ -417,6 +436,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': '正在初始化凭据生成...',
@ -431,6 +454,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 登录方式时,需要 <code>clientId</code> 和 <code>clientSecret</code> 字段,可在同文件夹下的另一个 JSON 文件中获取',
// Pagination
@ -496,6 +533,7 @@ const translations = {
'common.loading': '加载中...',
'common.upload': '上传',
'common.generate': '生成',
'common.optional': '可选',
'common.found': '已找到',
'common.missing': '缺失',
'common.search': '搜索',
@ -552,7 +590,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',
@ -701,6 +739,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',
@ -807,6 +847,8 @@ const translations = {
'config.advanced.maxErrorCount': 'Provider Max Error Count',
'config.advanced.maxErrorCountPlaceholder': 'Default: 3',
'config.advanced.maxErrorCountNote': 'Provider will be marked as unhealthy after consecutive errors reach this count, default is 3',
'config.advanced.credentialSwitchMaxRetries': 'Credential Switch Max Retries',
'config.advanced.credentialSwitchMaxRetriesNote': 'Maximum retries for switching credentials after authentication errors (401/403), default is 5',
'config.advanced.fallbackChain': 'Cross-Type Fallback Chain Config',
'config.advanced.fallbackChainPlaceholder': 'Example:\n{\n "gemini-cli-oauth": ["gemini-antigravity"],\n "gemini-antigravity": ["gemini-cli-oauth"],\n "claude-kiro-oauth": ["claude-custom"]\n}',
'config.advanced.fallbackChainNote': 'When all accounts of a Provider Type are unhealthy, automatically switch to configured Fallback types. JSON format, key is primary type, value is Fallback type array (sorted by priority)',
@ -832,9 +874,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',
@ -886,6 +936,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',
@ -908,9 +965,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?',
@ -941,6 +998,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...',
@ -955,6 +1016,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, <code>clientId</code> and <code>clientSecret</code> fields are required, which can be found in another JSON file in the same folder',
// Pagination
@ -1023,6 +1098,7 @@ const translations = {
'common.loading': 'Loading...',
'common.upload': 'Upload',
'common.generate': 'Generate',
'common.optional': 'Optional',
'common.found': 'Found',
'common.missing': 'Missing',
'common.search': 'Search',

View file

@ -65,8 +65,14 @@ function showProviderManagerModal(data) {
<button class="btn btn-warning" onclick="window.resetAllProvidersHealth('${providerType}')" data-i18n="modal.provider.resetHealth" title="将所有节点的健康状态重置为健康">
<i class="fas fa-heartbeat"></i>
</button>
<button class="btn btn-info" onclick="window.performHealthCheck('${providerType}')" data-i18n="modal.provider.healthCheck" title="对所有节点执行健康检测">
<i class="fas fa-stethoscope"></i>
<button class="btn btn-info" onclick="window.performHealthCheck('${providerType}')" data-i18n="modal.provider.healthCheck" title="对不健康节点执行健康检测">
<i class="fas fa-stethoscope"></i>
</button>
<button class="btn btn-secondary" onclick="window.refreshUnhealthyUuids('${providerType}')" data-i18n="modal.provider.refreshUnhealthyUuids" title="刷新不健康节点的UUID">
<i class="fas fa-sync-alt"></i> <span data-i18n="modal.provider.refreshUnhealthyUuidsBtn">UUID</span>
</button>
<button class="btn btn-danger" onclick="window.deleteUnhealthyProviders('${providerType}')" data-i18n="modal.provider.deleteUnhealthy" title="删除不健康节点">
<i class="fas fa-trash-alt"></i> <span data-i18n="modal.provider.deleteUnhealthyBtn"></span>
</button>
</div>
</div>
@ -418,6 +424,9 @@ function renderProviderList(providers) {
<button class="btn-small btn-delete" onclick="window.deleteProvider('${provider.uuid}', event)">
<i class="fas fa-trash"></i> <span data-i18n="modal.provider.delete"></span>
</button>
<button class="btn-small btn-refresh-uuid" onclick="window.refreshProviderUuid('${provider.uuid}', event)" title="${t('modal.provider.refreshUuid')}">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>
<div class="provider-item-content" id="content-${provider.uuid}">
@ -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 = `
<button class="btn-small ${toggleButtonClass}" onclick="window.toggleProviderStatus('${uuid}', event)" title="${toggleButtonText}此提供商">
<i class="${toggleButtonIcon}"></i> ${toggleButtonText}
</button>
<button class="btn-small btn-save" onclick="window.saveProvider('${uuid}', event)">
<i class="fas fa-save"></i> <span data-i18n="modal.provider.save"></span>
</button>
@ -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) {
<i class="${toggleButtonIcon}"></i> ${toggleButtonText}
</button>
<button class="btn-small btn-edit" onclick="window.editProvider('${uuid}', event)">
<i class="fas fa-edit"></i> <span data-i18n="modal.provider.edit"></span>
<i class="fas fa-edit"></i> <span data-i18n="modal.provider.edit">${t('modal.provider.edit')}</span>
</button>
<button class="btn-small btn-delete" onclick="window.deleteProvider('${uuid}', event)">
<i class="fas fa-trash"></i> <span data-i18n="modal.provider.delete"></span>
<i class="fas fa-trash"></i> <span data-i18n="modal.provider.delete">${t('modal.provider.delete')}</span>
</button>
<button class="btn-small btn-refresh-uuid" onclick="window.refreshProviderUuid('${uuid}', event)" title="${t('modal.provider.refreshUuid')}">
<i class="fas fa-sync-alt"></i>
</button>
`;
}
@ -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;
window.deleteUnhealthyProviders = deleteUnhealthyProviders;
window.refreshUnhealthyUuids = refreshUnhealthyUuids;
window.goToProviderPage = goToProviderPage;
window.refreshProviderUuid = refreshProviderUuid;

View file

@ -680,7 +680,7 @@ function showKiroAuthMethodSelector(providerType) {
modal.style.display = 'flex';
modal.innerHTML = `
<div class="modal-content" style="max-width: 500px;">
<div class="modal-content" style="max-width: 550px;">
<div class="modal-header">
<h3><i class="fas fa-key"></i> <span data-i18n="oauth.kiro.selectMethod">${t('oauth.kiro.selectMethod')}</span></h3>
<button class="modal-close">&times;</button>
@ -1727,6 +1727,29 @@ function showAuthModal(authUrl, authInfo) {
}
</div>
<p style="margin: 8px 0 0 0; font-size: 0.85rem; color: #92400e;" data-i18n="oauth.modal.portNote">${t('oauth.modal.portNote')}</p>
${(authInfo.provider === 'claude-kiro-oauth' && authInfo.authMethod === 'builder-id') ? `
<div class="builder-id-url-section" style="margin-top: 12px; padding-top: 12px; border-top: 1px dashed #fcd34d;">
<label style="display: flex; align-items: center; gap: 6px; margin-bottom: 6px; font-size: 13px; font-weight: 600; color: #92400e;">
<i class="fas fa-link"></i>
<span data-i18n="oauth.kiro.builderIDStartURL">${t('oauth.kiro.builderIDStartURL') || 'Builder ID Start URL'}</span>
<span style="font-weight: normal; color: #b45309;">(${t('common.optional') || '可选'})</span>
</label>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="text" class="builder-id-start-url-input"
value="${authInfo.builderIDStartURL || 'https://view.awsapps.com/start'}"
placeholder="https://view.awsapps.com/start"
style="flex: 1; padding: 6px 10px; border: 1px solid #fcd34d; border-radius: 4px; font-size: 13px; color: #92400e; background: white;"
/>
<button class="regenerate-builder-id-btn" title="${t('common.generate')}" style="background: none; border: 1px solid #d97706; border-radius: 4px; cursor: pointer; color: #d97706; padding: 4px 8px;">
<i class="fas fa-sync-alt"></i>
</button>
</div>
<p style="margin: 6px 0 0 0; font-size: 0.75rem; color: #b45309;">
<i class="fas fa-info-circle"></i>
<span data-i18n="oauth.kiro.builderIDStartURLHint">${t('oauth.kiro.builderIDStartURLHint') || '如果您使用 AWS IAM Identity Center请输入您的 Start URL'}</span>
</p>
</div>
` : ''}
</div>
${instructionsHtml}
<div class="auth-url-section">
@ -1780,6 +1803,26 @@ function showAuthModal(authUrl, authInfo) {
};
}
// Builder ID Start URL 重新生成按钮事件
const regenerateBuilderIdBtn = modal.querySelector('.regenerate-builder-id-btn');
if (regenerateBuilderIdBtn) {
regenerateBuilderIdBtn.onclick = async () => {
const builderIdStartUrl = modal.querySelector('.builder-id-start-url-input').value.trim();
modal.remove();
// 构造重新请求的参数
const options = {
...authInfo,
builderIDStartURL: builderIdStartUrl || 'https://view.awsapps.com/start'
};
// 移除不需要传递回后端的字段
delete options.provider;
delete options.redirectUri;
delete options.callbackPort;
await executeGenerateAuthUrl(authInfo.provider, options);
};
}
// 复制链接按钮
const copyBtn = modal.querySelector('.copy-btn');
copyBtn.addEventListener('click', () => {

View file

@ -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
};

View file

@ -163,7 +163,7 @@
</div>
<div class="config-row">
<div class="form-group">
<div class="form-group pool-section">
<label for="credentialSwitchMaxRetries" data-i18n="config.advanced.credentialSwitchMaxRetries">坏凭证切换最大重试次数</label>
<input type="number" id="credentialSwitchMaxRetries" class="form-control" min="1" max="50" value="5">
<small class="form-text" data-i18n="config.advanced.credentialSwitchMaxRetriesNote">认证错误(401/403)后切换凭证的最大重试次数,默认 5 次</small>

View file

@ -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;

View file

@ -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); }

View file

@ -1,7 +1,7 @@
<link rel="stylesheet" href="components/section-upload-config.css">
<!-- Upload Configuration Section -->
<section id="upload-config" class="section" aria-labelledby="upload-config-title">
<h2 id="upload-config-title" data-i18n="upload.title">配置管理</h2>
<h2 id="upload-config-title" data-i18n="upload.title">凭据文件管理</h2>
<div class="upload-config-panel">
<!-- 搜索和过滤区域 -->
<div class="config-search-panel">
@ -15,6 +15,18 @@
</button>
</div>
</div>
<div class="form-group">
<label for="configProviderFilter" data-i18n="upload.providerFilter">提供商类型</label>
<select id="configProviderFilter" class="form-control">
<option value="" data-i18n="upload.providerFilter.all">全部提供商</option>
<option value="claude-kiro-oauth" data-i18n="upload.providerFilter.kiro">Kiro OAuth</option>
<option value="gemini-cli-oauth" data-i18n="upload.providerFilter.gemini">Gemini OAuth</option>
<option value="openai-qwen-oauth" data-i18n="upload.providerFilter.qwen">Qwen OAuth</option>
<option value="gemini-antigravity" data-i18n="upload.providerFilter.antigravity">Antigravity</option>
<option value="claude-orchids-oauth" data-i18n="upload.providerFilter.orchids">Orchids OAuth</option>
<option value="other" data-i18n="upload.providerFilter.other">其他/未识别</option>
</select>
</div>
<div class="form-group">
<label for="configStatusFilter" data-i18n="upload.statusFilter">关联状态</label>
<select id="configStatusFilter" class="form-control">
@ -23,17 +35,6 @@
<option value="unused" data-i18n="upload.statusFilter.unused">未关联</option>
</select>
</div>
<div class="form-group">
<label>&nbsp;</label>
<div class="config-actions">
<button class="btn btn-outline" id="refreshConfigList" aria-label="Refresh Config List" data-i18n-aria-label="upload.refresh">
<i class="fas fa-sync-alt"></i> <span data-i18n="upload.refresh">刷新</span>
</button>
<button class="btn btn-outline" id="downloadAllConfigs" aria-label="Download All Configs" data-i18n-aria-label="upload.downloadAll">
<i class="fas fa-file-archive"></i> <span data-i18n="upload.downloadAll">打包下载</span>
</button>
</div>
</div>
</div>
</div>
@ -48,6 +49,15 @@
<button id="batchLinkKiroBtn" class="btn-batch-link" data-i18n-title="upload.batchLink" title="批量关联 configs/ 下的未关联配置">
<i class="fas fa-link"></i> <span data-i18n="upload.batchLink">自动关联oauth</span>
</button>
<button id="deleteUnboundBtn" class="btn-delete-unbound" data-i18n-title="upload.deleteUnbound" title="删除所有未关联的配置文件">
<i class="fas fa-trash-alt"></i> <span data-i18n="upload.deleteUnbound">删除未关联</span>
</button>
<button class="btn-refresh" id="refreshConfigList" aria-label="Refresh Config List" data-i18n-aria-label="upload.refresh">
<i class="fas fa-sync-alt"></i> <span data-i18n="upload.refresh">刷新</span>
</button>
<button class="btn-download" id="downloadAllConfigs" aria-label="Download All Configs" data-i18n-aria-label="upload.downloadAll">
<i class="fas fa-file-archive"></i> <span data-i18n="upload.downloadAll">打包下载</span>
</button>
</div>
</div>
<div id="configList" class="config-list">

View file

@ -11,8 +11,8 @@
<a href="#providers" class="nav-item" data-section="providers" aria-label="Provider Pool Management" data-i18n-aria-label="nav.providers">
<i class="fas fa-network-wired" aria-hidden="true"></i> <span data-i18n="nav.providers">提供商池管理</span>
</a>
<a href="#upload-config" class="nav-item" data-section="upload-config" aria-label="Upload Config Management" data-i18n-aria-label="nav.upload">
<i class="fas fa-upload" aria-hidden="true"></i> <span data-i18n="nav.upload">配置管理</span>
<a href="#upload-config" class="nav-item" data-section="upload-config" aria-label="Credential Files Management" data-i18n-aria-label="nav.upload">
<i class="fas fa-upload" aria-hidden="true"></i> <span data-i18n="nav.upload">凭据文件管理</span>
</a>
<a href="#usage" class="nav-item" data-section="usage" aria-label="Usage Query" data-i18n-aria-label="nav.usage">
<i class="fas fa-chart-bar" aria-hidden="true"></i> <span data-i18n="nav.usage">用量查询</span>