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==",
|
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
|
|
@ -2960,7 +2959,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001726",
|
"caniuse-lite": "^1.0.30001726",
|
||||||
"electron-to-chromium": "^1.5.173",
|
"electron-to-chromium": "^1.5.173",
|
||||||
|
|
|
||||||
|
|
@ -665,7 +665,7 @@ export async function handleQwenOAuth(currentConfig, options = {}) {
|
||||||
* @returns {Promise<Object>} 返回授权URL和相关信息
|
* @returns {Promise<Object>} 返回授权URL和相关信息
|
||||||
*/
|
*/
|
||||||
export async function handleKiroOAuth(currentConfig, options = {}) {
|
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}`);
|
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 客户端
|
// 1. 注册 OIDC 客户端
|
||||||
const regResponse = await fetchWithProxy(`${KIRO_OAUTH_CONFIG.ssoOIDCEndpoint}/client/register`, {
|
const regResponse = await fetchWithProxy(`${KIRO_OAUTH_CONFIG.ssoOIDCEndpoint}/client/register`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -771,7 +775,7 @@ async function handleKiroBuilderIDDeviceCode(currentConfig, options = {}) {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
clientId: regData.clientId,
|
clientId: regData.clientId,
|
||||||
clientSecret: regData.clientSecret,
|
clientSecret: regData.clientSecret,
|
||||||
startUrl: KIRO_OAUTH_CONFIG.builderIDStartURL
|
startUrl: builderIDStartURL
|
||||||
})
|
})
|
||||||
}, 'claude-kiro-oauth');
|
}, 'claude-kiro-oauth');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,57 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
|
||||||
return await providerApi.handleAddProvider(req, res, currentConfig, providerPoolManager);
|
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
|
// 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\/([^\/]+)\/([^\/]+)$/);
|
const updateProviderMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/([^\/]+)$/);
|
||||||
if (method === 'PUT' && updateProviderMatch) {
|
if (method === 'PUT' && updateProviderMatch) {
|
||||||
const providerType = decodeURIComponent(updateProviderMatch[1]);
|
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);
|
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
|
// Generate OAuth authorization URL for providers
|
||||||
const generateAuthUrlMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/generate-auth-url$/);
|
const generateAuthUrlMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/generate-auth-url$/);
|
||||||
if (method === 'POST' && generateAuthUrlMatch) {
|
if (method === 'POST' && generateAuthUrlMatch) {
|
||||||
|
|
@ -216,6 +243,11 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
|
||||||
return await uploadConfigApi.handleDownloadAllConfigs(req, res);
|
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
|
// Quick link config to corresponding provider based on directory
|
||||||
if (method === 'POST' && pathParam === '/api/quick-link-provider') {
|
if (method === 'POST' && pathParam === '/api/quick-link-provider') {
|
||||||
return await providerApi.handleQuickLinkProvider(req, res, currentConfig, providerPoolManager);
|
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;
|
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 配置)
|
// 执行健康检测(强制检查,忽略 checkHealth 配置)
|
||||||
const results = [];
|
const results = [];
|
||||||
for (const providerStatus of providers) {
|
for (const providerStatus of unhealthyProviders) {
|
||||||
const providerConfig = providerStatus.config;
|
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 successCount = results.filter(r => r.success === true).length;
|
||||||
const failCount = results.filter(r => r.success === false).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', {
|
broadcastEvent('config_update', {
|
||||||
|
|
@ -729,3 +923,76 @@ export async function handleQuickLinkProvider(req, res, currentConfig, providerP
|
||||||
return true;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -198,3 +198,113 @@ export async function handleDownloadAllConfigs(req, res) {
|
||||||
return true;
|
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.dashboard': '仪表盘',
|
||||||
'nav.config': '配置管理',
|
'nav.config': '配置管理',
|
||||||
'nav.providers': '提供商池管理',
|
'nav.providers': '提供商池管理',
|
||||||
'nav.upload': '配置管理',
|
'nav.upload': '凭据文件管理',
|
||||||
'nav.usage': '用量查询',
|
'nav.usage': '用量查询',
|
||||||
'nav.logs': '实时日志',
|
'nav.logs': '实时日志',
|
||||||
'nav.plugins': '插件管理',
|
'nav.plugins': '插件管理',
|
||||||
|
|
@ -177,6 +177,8 @@ const translations = {
|
||||||
'oauth.kiro.importError': '导入出错',
|
'oauth.kiro.importError': '导入出错',
|
||||||
'oauth.kiro.duplicateToken': '重复凭据 - 此 refreshToken 已存在',
|
'oauth.kiro.duplicateToken': '重复凭据 - 此 refreshToken 已存在',
|
||||||
'oauth.kiro.duplicateCredentials': '该凭据已存在,请勿重复导入',
|
'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.step1': '点击下方按钮在浏览器中打开 iFlow 授权页面',
|
||||||
'oauth.iflow.step2': '使用您的 iFlow 账号登录并授权',
|
'oauth.iflow.step2': '使用您的 iFlow 账号登录并授权',
|
||||||
'oauth.iflow.step3': '授权完成后,系统会自动获取 API Key',
|
'oauth.iflow.step3': '授权完成后,系统会自动获取 API Key',
|
||||||
|
|
@ -283,6 +285,8 @@ const translations = {
|
||||||
'config.advanced.maxErrorCount': '提供商最大错误次数',
|
'config.advanced.maxErrorCount': '提供商最大错误次数',
|
||||||
'config.advanced.maxErrorCountPlaceholder': '默认: 3',
|
'config.advanced.maxErrorCountPlaceholder': '默认: 3',
|
||||||
'config.advanced.maxErrorCountNote': '提供商连续错误达到此次数后将被标记为不健康,默认为 3 次',
|
'config.advanced.maxErrorCountNote': '提供商连续错误达到此次数后将被标记为不健康,默认为 3 次',
|
||||||
|
'config.advanced.credentialSwitchMaxRetries': '坏凭证切换最大重试次数',
|
||||||
|
'config.advanced.credentialSwitchMaxRetriesNote': '认证错误(401/403)后切换凭证的最大重试次数,默认 5 次',
|
||||||
'config.advanced.fallbackChain': '跨类型 Fallback 链配置',
|
'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.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 类型数组(按优先级排序)',
|
'config.advanced.fallbackChainNote': '当某一 Provider Type 所有账号都不健康时,自动切换到配置的 Fallback 类型。JSON 格式,键为主类型,值为 Fallback 类型数组(按优先级排序)',
|
||||||
|
|
@ -308,9 +312,17 @@ const translations = {
|
||||||
'config.placeholder.model': '例如: gpt-3.5-turbo',
|
'config.placeholder.model': '例如: gpt-3.5-turbo',
|
||||||
|
|
||||||
// Upload Config
|
// Upload Config
|
||||||
'upload.title': '配置管理',
|
'upload.title': '凭据文件管理',
|
||||||
'upload.search': '搜索配置',
|
'upload.search': '搜索配置',
|
||||||
'upload.searchPlaceholder': '输入文件名',
|
'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': '关联状态',
|
||||||
'upload.statusFilter.all': '全部状态',
|
'upload.statusFilter.all': '全部状态',
|
||||||
'upload.statusFilter.used': '已关联',
|
'upload.statusFilter.used': '已关联',
|
||||||
|
|
@ -362,6 +374,13 @@ const translations = {
|
||||||
'upload.batchLink.processing': '正在批量关联 {count} 个配置...',
|
'upload.batchLink.processing': '正在批量关联 {count} 个配置...',
|
||||||
'upload.batchLink.success': '成功关联 {count} 个配置',
|
'upload.batchLink.success': '成功关联 {count} 个配置',
|
||||||
'upload.batchLink.partial': '关联完成: 成功 {success} 个, 失败 {fail} 个',
|
'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
|
||||||
'providers.title': '提供商池管理',
|
'providers.title': '提供商池管理',
|
||||||
|
|
@ -384,9 +403,9 @@ const translations = {
|
||||||
'modal.provider.healthyAccounts': '健康账户:',
|
'modal.provider.healthyAccounts': '健康账户:',
|
||||||
'modal.provider.add': '添加新提供商',
|
'modal.provider.add': '添加新提供商',
|
||||||
'modal.provider.resetHealth': '重置为健康',
|
'modal.provider.resetHealth': '重置为健康',
|
||||||
'modal.provider.healthCheck': '健康检测',
|
'modal.provider.healthCheck': '检测不健康',
|
||||||
'modal.provider.resetHealthConfirm': '确定要将 {type} 的所有节点重置为健康状态吗?\n\n这将清除所有节点的错误计数和错误时间。',
|
'modal.provider.resetHealthConfirm': '确定要将 {type} 的所有节点重置为健康状态吗?\n\n这将清除所有节点的错误计数和错误时间。',
|
||||||
'modal.provider.healthCheckConfirm': '确定要对 {type} 的所有节点执行健康检测吗?\n\n这将向每个节点发送测试请求来验证其可用性。',
|
'modal.provider.healthCheckConfirm': '确定要对 {type} 的不健康节点执行健康检测吗?\n\n这将向不健康节点发送测试请求来验证其可用性。',
|
||||||
'modal.provider.deleteConfirm': '确定要删除这个提供商配置吗?此操作不可恢复。',
|
'modal.provider.deleteConfirm': '确定要删除这个提供商配置吗?此操作不可恢复。',
|
||||||
'modal.provider.disableConfirm': '确定要禁用这个提供商配置吗?禁用后该提供商将不会被选中使用。',
|
'modal.provider.disableConfirm': '确定要禁用这个提供商配置吗?禁用后该提供商将不会被选中使用。',
|
||||||
'modal.provider.enableConfirm': '确定要启用这个提供商配置吗?',
|
'modal.provider.enableConfirm': '确定要启用这个提供商配置吗?',
|
||||||
|
|
@ -417,6 +436,10 @@ const translations = {
|
||||||
'modal.provider.enabled': '启用',
|
'modal.provider.enabled': '启用',
|
||||||
'modal.provider.disabled': '禁用',
|
'modal.provider.disabled': '禁用',
|
||||||
'modal.provider.noProviderType': '不支持的提供商类型',
|
'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.load.failed': '加载提供商详情失败',
|
||||||
'modal.provider.auth.initializing': '正在初始化凭据生成...',
|
'modal.provider.auth.initializing': '正在初始化凭据生成...',
|
||||||
|
|
@ -431,6 +454,20 @@ const translations = {
|
||||||
'modal.provider.add.failed': '添加失败',
|
'modal.provider.add.failed': '添加失败',
|
||||||
'modal.provider.resetHealth.success': '成功重置 {count} 个节点的健康状态',
|
'modal.provider.resetHealth.success': '成功重置 {count} 个节点的健康状态',
|
||||||
'modal.provider.resetHealth.failed': '重置健康状态失败',
|
'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 文件中获取',
|
'modal.provider.kiroAuthHint': '使用 AWS Builder ID 登录方式时,需要 <code>clientId</code> 和 <code>clientSecret</code> 字段,可在同文件夹下的另一个 JSON 文件中获取',
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
|
|
@ -496,6 +533,7 @@ const translations = {
|
||||||
'common.loading': '加载中...',
|
'common.loading': '加载中...',
|
||||||
'common.upload': '上传',
|
'common.upload': '上传',
|
||||||
'common.generate': '生成',
|
'common.generate': '生成',
|
||||||
|
'common.optional': '可选',
|
||||||
'common.found': '已找到',
|
'common.found': '已找到',
|
||||||
'common.missing': '缺失',
|
'common.missing': '缺失',
|
||||||
'common.search': '搜索',
|
'common.search': '搜索',
|
||||||
|
|
@ -552,7 +590,7 @@ const translations = {
|
||||||
'nav.dashboard': 'Dashboard',
|
'nav.dashboard': 'Dashboard',
|
||||||
'nav.config': 'Configuration',
|
'nav.config': 'Configuration',
|
||||||
'nav.providers': 'Provider Pools',
|
'nav.providers': 'Provider Pools',
|
||||||
'nav.upload': 'Config Management',
|
'nav.upload': 'Credential Files',
|
||||||
'nav.usage': 'Usage Query',
|
'nav.usage': 'Usage Query',
|
||||||
'nav.logs': 'Real-time Logs',
|
'nav.logs': 'Real-time Logs',
|
||||||
'nav.plugins': 'Plugin Management',
|
'nav.plugins': 'Plugin Management',
|
||||||
|
|
@ -701,6 +739,8 @@ const translations = {
|
||||||
'oauth.kiro.importError': 'Import error',
|
'oauth.kiro.importError': 'Import error',
|
||||||
'oauth.kiro.duplicateToken': 'Duplicate - this refreshToken already exists',
|
'oauth.kiro.duplicateToken': 'Duplicate - this refreshToken already exists',
|
||||||
'oauth.kiro.duplicateCredentials': 'This credential already exists, please do not import duplicates',
|
'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.step1': 'Click the button below to open the iFlow authorization page',
|
||||||
'oauth.iflow.step2': 'Log in with your iFlow account and authorize',
|
'oauth.iflow.step2': 'Log in with your iFlow account and authorize',
|
||||||
'oauth.iflow.step3': 'After authorization, the system will automatically fetch the API Key',
|
'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.maxErrorCount': 'Provider Max Error Count',
|
||||||
'config.advanced.maxErrorCountPlaceholder': 'Default: 3',
|
'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.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.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.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)',
|
'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',
|
'config.placeholder.model': 'e.g.: gpt-3.5-turbo',
|
||||||
|
|
||||||
// Upload Config
|
// Upload Config
|
||||||
'upload.title': 'Config Management',
|
'upload.title': 'Credential Files Management',
|
||||||
'upload.search': 'Search Config',
|
'upload.search': 'Search Config',
|
||||||
'upload.searchPlaceholder': 'Enter filename',
|
'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': 'Association Status',
|
||||||
'upload.statusFilter.all': 'All Status',
|
'upload.statusFilter.all': 'All Status',
|
||||||
'upload.statusFilter.used': 'Associated',
|
'upload.statusFilter.used': 'Associated',
|
||||||
|
|
@ -886,6 +936,13 @@ const translations = {
|
||||||
'upload.batchLink.processing': 'Batch linking {count} configurations...',
|
'upload.batchLink.processing': 'Batch linking {count} configurations...',
|
||||||
'upload.batchLink.success': 'Successfully linked {count} configurations',
|
'upload.batchLink.success': 'Successfully linked {count} configurations',
|
||||||
'upload.batchLink.partial': 'Linking completed: {success} succeeded, {fail} failed',
|
'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
|
||||||
'providers.title': 'Provider Pool Management',
|
'providers.title': 'Provider Pool Management',
|
||||||
|
|
@ -908,9 +965,9 @@ const translations = {
|
||||||
'modal.provider.healthyAccounts': 'Healthy Accounts:',
|
'modal.provider.healthyAccounts': 'Healthy Accounts:',
|
||||||
'modal.provider.add': 'Add Provider',
|
'modal.provider.add': 'Add Provider',
|
||||||
'modal.provider.resetHealth': 'Reset Health',
|
'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.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.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.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?',
|
'modal.provider.enableConfirm': 'Are you sure you want to enable this provider?',
|
||||||
|
|
@ -941,6 +998,10 @@ const translations = {
|
||||||
'modal.provider.enabled': 'Enabled',
|
'modal.provider.enabled': 'Enabled',
|
||||||
'modal.provider.disabled': 'Disabled',
|
'modal.provider.disabled': 'Disabled',
|
||||||
'modal.provider.noProviderType': 'Unsupported provider type',
|
'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.load.failed': 'Failed to load provider details',
|
||||||
'modal.provider.auth.initializing': 'Initializing credential generation...',
|
'modal.provider.auth.initializing': 'Initializing credential generation...',
|
||||||
|
|
@ -955,6 +1016,20 @@ const translations = {
|
||||||
'modal.provider.add.failed': 'Add failed',
|
'modal.provider.add.failed': 'Add failed',
|
||||||
'modal.provider.resetHealth.success': 'Successfully reset health status for {count} nodes',
|
'modal.provider.resetHealth.success': 'Successfully reset health status for {count} nodes',
|
||||||
'modal.provider.resetHealth.failed': 'Failed to reset health status',
|
'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',
|
'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
|
// Pagination
|
||||||
|
|
@ -1023,6 +1098,7 @@ const translations = {
|
||||||
'common.loading': 'Loading...',
|
'common.loading': 'Loading...',
|
||||||
'common.upload': 'Upload',
|
'common.upload': 'Upload',
|
||||||
'common.generate': 'Generate',
|
'common.generate': 'Generate',
|
||||||
|
'common.optional': 'Optional',
|
||||||
'common.found': 'Found',
|
'common.found': 'Found',
|
||||||
'common.missing': 'Missing',
|
'common.missing': 'Missing',
|
||||||
'common.search': 'Search',
|
'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="将所有节点的健康状态重置为健康">
|
<button class="btn btn-warning" onclick="window.resetAllProvidersHealth('${providerType}')" data-i18n="modal.provider.resetHealth" title="将所有节点的健康状态重置为健康">
|
||||||
<i class="fas fa-heartbeat"></i> 重置为健康
|
<i class="fas fa-heartbeat"></i> 重置为健康
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-info" onclick="window.performHealthCheck('${providerType}')" data-i18n="modal.provider.healthCheck" title="对所有节点执行健康检测">
|
<button class="btn btn-info" onclick="window.performHealthCheck('${providerType}')" data-i18n="modal.provider.healthCheck" title="对不健康节点执行健康检测">
|
||||||
<i class="fas fa-stethoscope"></i> 健康检测
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -418,6 +424,9 @@ function renderProviderList(providers) {
|
||||||
<button class="btn-small btn-delete" onclick="window.deleteProvider('${provider.uuid}', event)">
|
<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>
|
<i class="fas fa-trash"></i> <span data-i18n="modal.provider.delete">删除</span>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
<div class="provider-item-content" id="content-${provider.uuid}">
|
<div class="provider-item-content" id="content-${provider.uuid}">
|
||||||
|
|
@ -784,19 +793,10 @@ function editProvider(uuid, event) {
|
||||||
// 添加编辑状态类
|
// 添加编辑状态类
|
||||||
providerDetail.classList.add('editing');
|
providerDetail.classList.add('editing');
|
||||||
|
|
||||||
// 替换编辑按钮为保存和取消按钮,但保留禁用/启用按钮
|
// 替换编辑按钮为保存和取消按钮,不显示禁用/启用按钮
|
||||||
const actionsGroup = providerDetail.querySelector('.provider-actions-group');
|
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 = `
|
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)">
|
<button class="btn-small btn-save" onclick="window.saveProvider('${uuid}', event)">
|
||||||
<i class="fas fa-save"></i> <span data-i18n="modal.provider.save">保存</span>
|
<i class="fas fa-save"></i> <span data-i18n="modal.provider.save">保存</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -852,11 +852,11 @@ function cancelEdit(uuid, event) {
|
||||||
select.value = originalValue || '';
|
select.value = originalValue || '';
|
||||||
});
|
});
|
||||||
|
|
||||||
// 恢复原来的编辑和删除按钮,但保留禁用/启用按钮
|
// 恢复原来的按钮布局
|
||||||
const actionsGroup = providerDetail.querySelector('.provider-actions-group');
|
const actionsGroup = providerDetail.querySelector('.provider-actions-group');
|
||||||
const currentProvider = providerDetail.closest('.provider-modal').querySelector(`[data-uuid="${uuid}"]`);
|
const currentProvider = providerDetail.closest('.provider-modal').querySelector(`[data-uuid="${uuid}"]`);
|
||||||
const isCurrentlyDisabled = currentProvider.classList.contains('disabled');
|
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 toggleButtonIcon = isCurrentlyDisabled ? 'fas fa-play' : 'fas fa-ban';
|
||||||
const toggleButtonClass = isCurrentlyDisabled ? 'btn-success' : 'btn-warning';
|
const toggleButtonClass = isCurrentlyDisabled ? 'btn-success' : 'btn-warning';
|
||||||
|
|
||||||
|
|
@ -865,10 +865,13 @@ function cancelEdit(uuid, event) {
|
||||||
<i class="${toggleButtonIcon}"></i> ${toggleButtonText}
|
<i class="${toggleButtonIcon}"></i> ${toggleButtonText}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-small btn-edit" onclick="window.editProvider('${uuid}', event)">
|
<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>
|
||||||
<button class="btn-small btn-delete" onclick="window.deleteProvider('${uuid}', event)">
|
<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>
|
</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,直接使用传入的模型列表)
|
* 渲染不支持的模型选择器(不调用API,直接使用传入的模型列表)
|
||||||
* @param {string} uuid - 提供商UUID
|
* @param {string} uuid - 提供商UUID
|
||||||
|
|
@ -1430,9 +1561,12 @@ export {
|
||||||
toggleProviderStatus,
|
toggleProviderStatus,
|
||||||
resetAllProvidersHealth,
|
resetAllProvidersHealth,
|
||||||
performHealthCheck,
|
performHealthCheck,
|
||||||
|
deleteUnhealthyProviders,
|
||||||
|
refreshUnhealthyUuids,
|
||||||
loadModelsForProviderType,
|
loadModelsForProviderType,
|
||||||
renderNotSupportedModelsSelector,
|
renderNotSupportedModelsSelector,
|
||||||
goToProviderPage
|
goToProviderPage,
|
||||||
|
refreshProviderUuid
|
||||||
};
|
};
|
||||||
|
|
||||||
// 将函数挂载到window对象
|
// 将函数挂载到window对象
|
||||||
|
|
@ -1447,4 +1581,7 @@ window.addProvider = addProvider;
|
||||||
window.toggleProviderStatus = toggleProviderStatus;
|
window.toggleProviderStatus = toggleProviderStatus;
|
||||||
window.resetAllProvidersHealth = resetAllProvidersHealth;
|
window.resetAllProvidersHealth = resetAllProvidersHealth;
|
||||||
window.performHealthCheck = performHealthCheck;
|
window.performHealthCheck = performHealthCheck;
|
||||||
|
window.deleteUnhealthyProviders = deleteUnhealthyProviders;
|
||||||
|
window.refreshUnhealthyUuids = refreshUnhealthyUuids;
|
||||||
window.goToProviderPage = goToProviderPage;
|
window.goToProviderPage = goToProviderPage;
|
||||||
|
window.refreshProviderUuid = refreshProviderUuid;
|
||||||
|
|
@ -680,7 +680,7 @@ function showKiroAuthMethodSelector(providerType) {
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
|
|
||||||
modal.innerHTML = `
|
modal.innerHTML = `
|
||||||
<div class="modal-content" style="max-width: 500px;">
|
<div class="modal-content" style="max-width: 550px;">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3><i class="fas fa-key"></i> <span data-i18n="oauth.kiro.selectMethod">${t('oauth.kiro.selectMethod')}</span></h3>
|
<h3><i class="fas fa-key"></i> <span data-i18n="oauth.kiro.selectMethod">${t('oauth.kiro.selectMethod')}</span></h3>
|
||||||
<button class="modal-close">×</button>
|
<button class="modal-close">×</button>
|
||||||
|
|
@ -1727,6 +1727,29 @@ function showAuthModal(authUrl, authInfo) {
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<p style="margin: 8px 0 0 0; font-size: 0.85rem; color: #92400e;" data-i18n="oauth.modal.portNote">${t('oauth.modal.portNote')}</p>
|
<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>
|
</div>
|
||||||
${instructionsHtml}
|
${instructionsHtml}
|
||||||
<div class="auth-url-section">
|
<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');
|
const copyBtn = modal.querySelector('.copy-btn');
|
||||||
copyBtn.addEventListener('click', () => {
|
copyBtn.addEventListener('click', () => {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ let isLoadingConfigs = false; // 防止重复加载配置
|
||||||
* @param {string} searchTerm - 搜索关键词
|
* @param {string} searchTerm - 搜索关键词
|
||||||
* @param {string} statusFilter - 状态过滤
|
* @param {string} statusFilter - 状态过滤
|
||||||
*/
|
*/
|
||||||
function searchConfigs(searchTerm = '', statusFilter = '') {
|
function searchConfigs(searchTerm = '', statusFilter = '', providerFilter = '') {
|
||||||
if (!allConfigs.length) {
|
if (!allConfigs.length) {
|
||||||
console.log('没有配置数据可搜索');
|
console.log('没有配置数据可搜索');
|
||||||
return;
|
return;
|
||||||
|
|
@ -29,7 +29,20 @@ function searchConfigs(searchTerm = '', statusFilter = '') {
|
||||||
const configStatus = config.isUsed ? 'used' : 'unused';
|
const configStatus = config.isUsed ? 'used' : 'unused';
|
||||||
const matchesStatus = !statusFilter || configStatus === statusFilter;
|
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();
|
renderConfigList();
|
||||||
|
|
@ -705,6 +718,7 @@ function initUploadConfigManager() {
|
||||||
const searchInput = document.getElementById('configSearch');
|
const searchInput = document.getElementById('configSearch');
|
||||||
const searchBtn = document.getElementById('searchConfigBtn');
|
const searchBtn = document.getElementById('searchConfigBtn');
|
||||||
const statusFilter = document.getElementById('configStatusFilter');
|
const statusFilter = document.getElementById('configStatusFilter');
|
||||||
|
const providerFilter = document.getElementById('configProviderFilter');
|
||||||
const refreshBtn = document.getElementById('refreshConfigList');
|
const refreshBtn = document.getElementById('refreshConfigList');
|
||||||
const downloadAllBtn = document.getElementById('downloadAllConfigs');
|
const downloadAllBtn = document.getElementById('downloadAllConfigs');
|
||||||
|
|
||||||
|
|
@ -712,7 +726,8 @@ function initUploadConfigManager() {
|
||||||
searchInput.addEventListener('input', debounce(() => {
|
searchInput.addEventListener('input', debounce(() => {
|
||||||
const searchTerm = searchInput.value.trim();
|
const searchTerm = searchInput.value.trim();
|
||||||
const currentStatusFilter = statusFilter?.value || '';
|
const currentStatusFilter = statusFilter?.value || '';
|
||||||
searchConfigs(searchTerm, currentStatusFilter);
|
const currentProviderFilter = providerFilter?.value || '';
|
||||||
|
searchConfigs(searchTerm, currentStatusFilter, currentProviderFilter);
|
||||||
}, 300));
|
}, 300));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -720,7 +735,8 @@ function initUploadConfigManager() {
|
||||||
searchBtn.addEventListener('click', () => {
|
searchBtn.addEventListener('click', () => {
|
||||||
const searchTerm = searchInput?.value.trim() || '';
|
const searchTerm = searchInput?.value.trim() || '';
|
||||||
const currentStatusFilter = statusFilter?.value || '';
|
const currentStatusFilter = statusFilter?.value || '';
|
||||||
searchConfigs(searchTerm, currentStatusFilter);
|
const currentProviderFilter = providerFilter?.value || '';
|
||||||
|
searchConfigs(searchTerm, currentStatusFilter, currentProviderFilter);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -728,7 +744,17 @@ function initUploadConfigManager() {
|
||||||
statusFilter.addEventListener('change', () => {
|
statusFilter.addEventListener('change', () => {
|
||||||
const searchTerm = searchInput?.value.trim() || '';
|
const searchTerm = searchInput?.value.trim() || '';
|
||||||
const currentStatusFilter = statusFilter.value;
|
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);
|
batchLinkBtn.addEventListener('click', batchLinkProviderConfigs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 删除未绑定配置按钮
|
||||||
|
const deleteUnboundBtn = document.getElementById('deleteUnboundBtn');
|
||||||
|
if (deleteUnboundBtn) {
|
||||||
|
deleteUnboundBtn.addEventListener('click', deleteUnboundConfigs);
|
||||||
|
}
|
||||||
|
|
||||||
// 初始加载配置列表
|
// 初始加载配置列表
|
||||||
loadConfigList();
|
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 - 要防抖的函数
|
* @param {Function} func - 要防抖的函数
|
||||||
|
|
@ -1002,5 +1095,6 @@ export {
|
||||||
deleteConfig,
|
deleteConfig,
|
||||||
closeConfigModal,
|
closeConfigModal,
|
||||||
copyConfigContent,
|
copyConfigContent,
|
||||||
reloadConfig
|
reloadConfig,
|
||||||
|
deleteUnboundConfigs
|
||||||
};
|
};
|
||||||
|
|
@ -163,7 +163,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="config-row">
|
<div class="config-row">
|
||||||
<div class="form-group">
|
<div class="form-group pool-section">
|
||||||
<label for="credentialSwitchMaxRetries" data-i18n="config.advanced.credentialSwitchMaxRetries">坏凭证切换最大重试次数</label>
|
<label for="credentialSwitchMaxRetries" data-i18n="config.advanced.credentialSwitchMaxRetries">坏凭证切换最大重试次数</label>
|
||||||
<input type="number" id="credentialSwitchMaxRetries" class="form-control" min="1" max="50" value="5">
|
<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>
|
<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);
|
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 {
|
.not-supported-models-section {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,40 @@
|
||||||
overflow-y: auto;
|
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 {
|
.config-item-manager {
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
|
@ -396,6 +430,96 @@
|
||||||
.delete-modal-footer { flex-direction: column; }
|
.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-used { background: var(--success-bg); color: var(--success-text); }
|
||||||
[data-theme="dark"] .status-unused { background: var(--warning-bg); color: var(--warning-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">
|
<link rel="stylesheet" href="components/section-upload-config.css">
|
||||||
<!-- Upload Configuration Section -->
|
<!-- Upload Configuration Section -->
|
||||||
<section id="upload-config" class="section" aria-labelledby="upload-config-title">
|
<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="upload-config-panel">
|
||||||
<!-- 搜索和过滤区域 -->
|
<!-- 搜索和过滤区域 -->
|
||||||
<div class="config-search-panel">
|
<div class="config-search-panel">
|
||||||
|
|
@ -15,6 +15,18 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="form-group">
|
||||||
<label for="configStatusFilter" data-i18n="upload.statusFilter">关联状态</label>
|
<label for="configStatusFilter" data-i18n="upload.statusFilter">关联状态</label>
|
||||||
<select id="configStatusFilter" class="form-control">
|
<select id="configStatusFilter" class="form-control">
|
||||||
|
|
@ -23,17 +35,6 @@
|
||||||
<option value="unused" data-i18n="upload.statusFilter.unused">未关联</option>
|
<option value="unused" data-i18n="upload.statusFilter.unused">未关联</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -48,6 +49,15 @@
|
||||||
<button id="batchLinkKiroBtn" class="btn-batch-link" data-i18n-title="upload.batchLink" title="批量关联 configs/ 下的未关联配置">
|
<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>
|
<i class="fas fa-link"></i> <span data-i18n="upload.batchLink">自动关联oauth</span>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
<div id="configList" class="config-list">
|
<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">
|
<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>
|
<i class="fas fa-network-wired" aria-hidden="true"></i> <span data-i18n="nav.providers">提供商池管理</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#upload-config" class="nav-item" data-section="upload-config" aria-label="Upload Config Management" data-i18n-aria-label="nav.upload">
|
<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>
|
<i class="fas fa-upload" aria-hidden="true"></i> <span data-i18n="nav.upload">凭据文件管理</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#usage" class="nav-item" data-section="usage" aria-label="Usage Query" data-i18n-aria-label="nav.usage">
|
<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>
|
<i class="fas fa-chart-bar" aria-hidden="true"></i> <span data-i18n="nav.usage">用量查询</span>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue