Merge branch 'main' of https://github.com/leonaii/AIClient-2-API
This commit is contained in:
commit
a97f8b4b5a
15 changed files with 1002 additions and 79 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
2.6.7
|
||||
2.6.8
|
||||
|
|
|
|||
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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">×</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', () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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); }
|
||||
|
|
|
|||
|
|
@ -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> </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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue