建立可扩展的提供商适配器注册表,实现动态服务发现与插槽管理: 架构改进: - 采用 Map 注册表替代 switch-case 硬编码,支持热插拔适配器 - 实现 acquireSlot/releaseSlot 机制,精确追踪活跃请求与等待队列 - 新增节点评分算法,综合考量并发数、队列长度、健康状态 核心能力: - 支持并发限制与队列等待,避免单节点过载 (concurrencyLimit/queueLimit) - 实现 Fallback 链式调用,429 错误自动切换备用凭证 - 添加请求级 IP 追踪,日志格式优化为 `clientIp:requestId` 配套更新: - 管理界面新增并发/队列配置字段与 Grok 逆向提供商选项 - 用量查询服务扩展 Grok 支持,同步剩余查询次数 (固定总量 80) - 新增并发测试脚本 (tests/concurrent-test.js),支持自定义并发数与 RPM 限制 配置项: - GROK_COOKIE_TOKEN, GROK_CF_CLEARANCE, GROK_USER_AGENT, GROK_BASE_URL
347 lines
No EOL
13 KiB
JavaScript
347 lines
No EOL
13 KiB
JavaScript
import { CONFIG } from '../core/config-manager.js';
|
|
import logger from '../utils/logger.js';
|
|
import { serviceInstances, getServiceAdapter } from '../providers/adapter.js';
|
|
import { formatKiroUsage, formatGeminiUsage, formatAntigravityUsage, formatCodexUsage, formatGrokUsage } from '../services/usage-service.js';
|
|
import { readUsageCache, writeUsageCache, readProviderUsageCache, updateProviderUsageCache } from './usage-cache.js';
|
|
import path from 'path';
|
|
|
|
const supportedProviders = ['claude-kiro-oauth', 'gemini-cli-oauth', 'gemini-antigravity', 'openai-codex-oauth', 'grok-custom'];
|
|
|
|
|
|
/**
|
|
* 获取所有支持用量查询的提供商的用量信息
|
|
* @param {Object} currentConfig - 当前配置
|
|
* @param {Object} providerPoolManager - 提供商池管理器
|
|
* @returns {Promise<Object>} 所有提供商的用量信息
|
|
*/
|
|
async function getAllProvidersUsage(currentConfig, providerPoolManager) {
|
|
const results = {
|
|
timestamp: new Date().toISOString(),
|
|
providers: {}
|
|
};
|
|
|
|
// 并发获取所有提供商的用量数据
|
|
const usagePromises = supportedProviders.map(async (providerType) => {
|
|
try {
|
|
const providerUsage = await getProviderTypeUsage(providerType, currentConfig, providerPoolManager);
|
|
return { providerType, data: providerUsage, success: true };
|
|
} catch (error) {
|
|
return {
|
|
providerType,
|
|
data: {
|
|
error: error.message,
|
|
instances: []
|
|
},
|
|
success: false
|
|
};
|
|
}
|
|
});
|
|
|
|
// 等待所有并发请求完成
|
|
const usageResults = await Promise.all(usagePromises);
|
|
|
|
// 将结果整合到 results.providers 中
|
|
for (const result of usageResults) {
|
|
results.providers[result.providerType] = result.data;
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* 获取指定提供商类型的用量信息
|
|
* @param {string} providerType - 提供商类型
|
|
* @param {Object} currentConfig - 当前配置
|
|
* @param {Object} providerPoolManager - 提供商池管理器
|
|
* @returns {Promise<Object>} 提供商用量信息
|
|
*/
|
|
async function getProviderTypeUsage(providerType, currentConfig, providerPoolManager) {
|
|
const result = {
|
|
providerType,
|
|
instances: [],
|
|
totalCount: 0,
|
|
successCount: 0,
|
|
errorCount: 0
|
|
};
|
|
|
|
// 获取提供商池中的所有实例
|
|
let providers = [];
|
|
if (providerPoolManager && providerPoolManager.providerPools && providerPoolManager.providerPools[providerType]) {
|
|
providers = providerPoolManager.providerPools[providerType];
|
|
} else if (currentConfig.providerPools && currentConfig.providerPools[providerType]) {
|
|
providers = currentConfig.providerPools[providerType];
|
|
}
|
|
|
|
result.totalCount = providers.length;
|
|
|
|
// 遍历所有提供商实例获取用量
|
|
for (const provider of providers) {
|
|
const providerKey = providerType + (provider.uuid || '');
|
|
let adapter = serviceInstances[providerKey];
|
|
|
|
const instanceResult = {
|
|
uuid: provider.uuid || 'unknown',
|
|
name: getProviderDisplayName(provider, providerType),
|
|
isHealthy: provider.isHealthy !== false,
|
|
isDisabled: provider.isDisabled === true,
|
|
success: false,
|
|
usage: null,
|
|
error: null
|
|
};
|
|
|
|
// First check if disabled, skip initialization for disabled providers
|
|
if (provider.isDisabled) {
|
|
instanceResult.error = 'Provider is disabled';
|
|
result.errorCount++;
|
|
} else if (!adapter) {
|
|
// Service instance not initialized, try auto-initialization
|
|
try {
|
|
logger.info(`[Usage API] Auto-initializing service adapter for ${providerType}: ${provider.uuid}`);
|
|
// Build configuration object
|
|
const serviceConfig = {
|
|
...CONFIG,
|
|
...provider,
|
|
MODEL_PROVIDER: providerType
|
|
};
|
|
adapter = getServiceAdapter(serviceConfig);
|
|
} catch (initError) {
|
|
logger.error(`[Usage API] Failed to initialize adapter for ${providerType}: ${provider.uuid}:`, initError.message);
|
|
instanceResult.error = `Service instance initialization failed: ${initError.message}`;
|
|
result.errorCount++;
|
|
}
|
|
}
|
|
|
|
// If adapter exists (including just initialized), and no error, try to get usage
|
|
if (adapter && !instanceResult.error) {
|
|
try {
|
|
const usage = await getAdapterUsage(adapter, providerType);
|
|
instanceResult.success = true;
|
|
instanceResult.usage = usage;
|
|
result.successCount++;
|
|
} catch (error) {
|
|
instanceResult.error = error.message;
|
|
result.errorCount++;
|
|
}
|
|
}
|
|
|
|
result.instances.push(instanceResult);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* 从适配器获取用量信息
|
|
* @param {Object} adapter - 服务适配器
|
|
* @param {string} providerType - 提供商类型
|
|
* @returns {Promise<Object>} 用量信息
|
|
*/
|
|
async function getAdapterUsage(adapter, providerType) {
|
|
if (providerType === 'claude-kiro-oauth') {
|
|
if (typeof adapter.getUsageLimits === 'function') {
|
|
const rawUsage = await adapter.getUsageLimits();
|
|
return formatKiroUsage(rawUsage);
|
|
} else if (adapter.kiroApiService && typeof adapter.kiroApiService.getUsageLimits === 'function') {
|
|
const rawUsage = await adapter.kiroApiService.getUsageLimits();
|
|
return formatKiroUsage(rawUsage);
|
|
}
|
|
throw new Error('This adapter does not support usage query');
|
|
}
|
|
|
|
if (providerType === 'gemini-cli-oauth') {
|
|
if (typeof adapter.getUsageLimits === 'function') {
|
|
const rawUsage = await adapter.getUsageLimits();
|
|
return formatGeminiUsage(rawUsage);
|
|
} else if (adapter.geminiApiService && typeof adapter.geminiApiService.getUsageLimits === 'function') {
|
|
const rawUsage = await adapter.geminiApiService.getUsageLimits();
|
|
return formatGeminiUsage(rawUsage);
|
|
}
|
|
throw new Error('This adapter does not support usage query');
|
|
}
|
|
|
|
if (providerType === 'gemini-antigravity') {
|
|
if (typeof adapter.getUsageLimits === 'function') {
|
|
const rawUsage = await adapter.getUsageLimits();
|
|
return formatAntigravityUsage(rawUsage);
|
|
} else if (adapter.antigravityApiService && typeof adapter.antigravityApiService.getUsageLimits === 'function') {
|
|
const rawUsage = await adapter.antigravityApiService.getUsageLimits();
|
|
return formatAntigravityUsage(rawUsage);
|
|
}
|
|
throw new Error('This adapter does not support usage query');
|
|
}
|
|
|
|
if (providerType === 'openai-codex-oauth') {
|
|
if (typeof adapter.getUsageLimits === 'function') {
|
|
const rawUsage = await adapter.getUsageLimits();
|
|
return formatCodexUsage(rawUsage);
|
|
} else if (adapter.codexApiService && typeof adapter.codexApiService.getUsageLimits === 'function') {
|
|
const rawUsage = await adapter.codexApiService.getUsageLimits();
|
|
return formatCodexUsage(rawUsage);
|
|
}
|
|
throw new Error('This adapter does not support usage query');
|
|
}
|
|
|
|
if (providerType === 'grok-custom') {
|
|
if (typeof adapter.getUsageLimits === 'function') {
|
|
const rawUsage = await adapter.getUsageLimits();
|
|
return formatGrokUsage(rawUsage);
|
|
}
|
|
throw new Error('This adapter does not support usage query');
|
|
}
|
|
|
|
throw new Error(`Unsupported provider type: ${providerType}`);
|
|
}
|
|
|
|
/**
|
|
* 获取提供商显示名称
|
|
* @param {Object} provider - 提供商配置
|
|
* @param {string} providerType - 提供商类型
|
|
* @returns {string} 显示名称
|
|
*/
|
|
function getProviderDisplayName(provider, providerType) {
|
|
// 优先使用自定义名称
|
|
if (provider.customName) {
|
|
return provider.customName;
|
|
}
|
|
|
|
if (provider.uuid) {
|
|
return provider.uuid;
|
|
}
|
|
|
|
// 尝试从凭据文件路径提取名称
|
|
const credPathKey = {
|
|
'claude-kiro-oauth': 'KIRO_OAUTH_CREDS_FILE_PATH',
|
|
'gemini-cli-oauth': 'GEMINI_OAUTH_CREDS_FILE_PATH',
|
|
'gemini-antigravity': 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH',
|
|
'openai-codex-oauth': 'CODEX_OAUTH_CREDS_FILE_PATH',
|
|
'openai-qwen-oauth': 'QWEN_OAUTH_CREDS_FILE_PATH',
|
|
'openai-iflow': 'IFLOW_TOKEN_FILE_PATH'
|
|
}[providerType];
|
|
|
|
if (credPathKey && provider[credPathKey]) {
|
|
const filePath = provider[credPathKey];
|
|
const fileName = path.basename(filePath);
|
|
const dirName = path.basename(path.dirname(filePath));
|
|
return `${dirName}/${fileName}`;
|
|
}
|
|
|
|
return 'Unnamed';
|
|
}
|
|
|
|
/**
|
|
* 获取支持用量查询的提供商列表
|
|
*/
|
|
export async function handleGetSupportedProviders(req, res) {
|
|
try {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(supportedProviders));
|
|
return true;
|
|
} catch (error) {
|
|
logger.error('[Usage API] Failed to get supported providers:', error);
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
error: {
|
|
message: 'Failed to get supported providers: ' + error.message
|
|
}
|
|
}));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取所有提供商的用量限制
|
|
*/
|
|
export async function handleGetUsage(req, res, currentConfig, providerPoolManager) {
|
|
try {
|
|
// 解析查询参数,检查是否需要强制刷新
|
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
const refresh = url.searchParams.get('refresh') === 'true';
|
|
|
|
let usageResults;
|
|
|
|
if (!refresh) {
|
|
// 优先读取缓存
|
|
const cachedData = await readUsageCache();
|
|
if (cachedData) {
|
|
logger.info('[Usage API] Returning cached usage data');
|
|
usageResults = { ...cachedData, fromCache: true };
|
|
}
|
|
}
|
|
|
|
if (!usageResults) {
|
|
// 缓存不存在或需要刷新,重新查询
|
|
logger.info('[Usage API] Fetching fresh usage data');
|
|
usageResults = await getAllProvidersUsage(currentConfig, providerPoolManager);
|
|
// 写入缓存
|
|
await writeUsageCache(usageResults);
|
|
}
|
|
|
|
// Always include current server time
|
|
const finalResults = {
|
|
...usageResults,
|
|
serverTime: new Date().toISOString()
|
|
};
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(finalResults));
|
|
return true;
|
|
} catch (error) {
|
|
logger.error('[UI API] Failed to get usage:', error);
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
error: {
|
|
message: 'Failed to get usage info: ' + error.message
|
|
}
|
|
}));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取特定提供商类型的用量限制
|
|
*/
|
|
export async function handleGetProviderUsage(req, res, currentConfig, providerPoolManager, providerType) {
|
|
try {
|
|
// 解析查询参数,检查是否需要强制刷新
|
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
const refresh = url.searchParams.get('refresh') === 'true';
|
|
|
|
let usageResults;
|
|
|
|
if (!refresh) {
|
|
// Prefer reading from cache
|
|
const cachedData = await readProviderUsageCache(providerType);
|
|
if (cachedData) {
|
|
logger.info(`[Usage API] Returning cached usage data for ${providerType}`);
|
|
usageResults = { ...cachedData, fromCache: true };
|
|
}
|
|
}
|
|
|
|
if (!usageResults) {
|
|
// Cache does not exist or refresh required, re-query
|
|
logger.info(`[Usage API] Fetching fresh usage data for ${providerType}`);
|
|
usageResults = await getProviderTypeUsage(providerType, currentConfig, providerPoolManager);
|
|
// 更新缓存
|
|
await updateProviderUsageCache(providerType, usageResults);
|
|
}
|
|
|
|
// Always include current server time
|
|
const finalResults = {
|
|
...usageResults,
|
|
serverTime: new Date().toISOString()
|
|
};
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(finalResults));
|
|
return true;
|
|
} catch (error) {
|
|
logger.error(`[UI API] Failed to get usage for ${providerType}:`, error);
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
error: {
|
|
message: `Failed to get usage info for ${providerType}: ` + error.message
|
|
}
|
|
}));
|
|
return true;
|
|
}
|
|
} |