mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
fix: 修复分组调度功能和API Keys统计弹窗UI问题
1. 分组调度功能修复: - 统一使用 unifiedClaudeScheduler 和 unifiedGeminiScheduler - 修复 schedulable 字段数据类型不一致问题(布尔值/字符串) - 添加 _isSchedulable() 辅助方法确保兼容性 - 修复所有路由文件中的调度器调用 2. API Keys 统计弹窗UI优化: - 统一弹窗样式与系统UI风格 - 添加右上角关闭按钮 - 修复移动端宽度问题(设置为95%屏幕宽度) - 使用 Teleport 组件和项目通用样式 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ const path = require('path');
|
||||
const { SocksProxyAgent } = require('socks-proxy-agent');
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const claudeAccountService = require('./claudeAccountService');
|
||||
const unifiedClaudeScheduler = require('./unifiedClaudeScheduler');
|
||||
const sessionHelper = require('../utils/sessionHelper');
|
||||
const logger = require('../utils/logger');
|
||||
const config = require('../../config/config');
|
||||
@@ -91,9 +92,11 @@ class ClaudeRelayService {
|
||||
const sessionHash = sessionHelper.generateSessionHash(requestBody);
|
||||
|
||||
// 选择可用的Claude账户(支持专属绑定和sticky会话)
|
||||
const accountId = await claudeAccountService.selectAccountForApiKey(apiKeyData, sessionHash);
|
||||
const accountSelection = await unifiedClaudeScheduler.selectAccountForApiKey(apiKeyData, sessionHash, requestBody.model);
|
||||
const accountId = accountSelection.accountId;
|
||||
const accountType = accountSelection.accountType;
|
||||
|
||||
logger.info(`📤 Processing API request for key: ${apiKeyData.name || apiKeyData.id}, account: ${accountId}${sessionHash ? `, session: ${sessionHash}` : ''}`);
|
||||
logger.info(`📤 Processing API request for key: ${apiKeyData.name || apiKeyData.id}, account: ${accountId} (${accountType})${sessionHash ? `, session: ${sessionHash}` : ''}`);
|
||||
|
||||
// 获取有效的访问token
|
||||
const accessToken = await claudeAccountService.getValidAccessToken(accountId);
|
||||
@@ -172,13 +175,13 @@ class ClaudeRelayService {
|
||||
if (isRateLimited) {
|
||||
logger.warn(`🚫 Rate limit detected for account ${accountId}, status: ${response.statusCode}`);
|
||||
// 标记账号为限流状态并删除粘性会话映射,传递准确的重置时间戳
|
||||
await claudeAccountService.markAccountRateLimited(accountId, sessionHash, rateLimitResetTimestamp);
|
||||
await unifiedClaudeScheduler.markAccountRateLimited(accountId, accountType, sessionHash, rateLimitResetTimestamp);
|
||||
}
|
||||
} else if (response.statusCode === 200 || response.statusCode === 201) {
|
||||
// 如果请求成功,检查并移除限流状态
|
||||
const isRateLimited = await claudeAccountService.isAccountRateLimited(accountId);
|
||||
const isRateLimited = await unifiedClaudeScheduler.isAccountRateLimited(accountId, accountType);
|
||||
if (isRateLimited) {
|
||||
await claudeAccountService.removeAccountRateLimit(accountId);
|
||||
await unifiedClaudeScheduler.removeAccountRateLimit(accountId, accountType);
|
||||
}
|
||||
|
||||
// 只有真实的 Claude Code 请求才更新 headers
|
||||
@@ -621,9 +624,11 @@ class ClaudeRelayService {
|
||||
const sessionHash = sessionHelper.generateSessionHash(requestBody);
|
||||
|
||||
// 选择可用的Claude账户(支持专属绑定和sticky会话)
|
||||
const accountId = await claudeAccountService.selectAccountForApiKey(apiKeyData, sessionHash);
|
||||
const accountSelection = await unifiedClaudeScheduler.selectAccountForApiKey(apiKeyData, sessionHash, requestBody.model);
|
||||
const accountId = accountSelection.accountId;
|
||||
const accountType = accountSelection.accountType;
|
||||
|
||||
logger.info(`📡 Processing streaming API request with usage capture for key: ${apiKeyData.name || apiKeyData.id}, account: ${accountId}${sessionHash ? `, session: ${sessionHash}` : ''}`);
|
||||
logger.info(`📡 Processing streaming API request with usage capture for key: ${apiKeyData.name || apiKeyData.id}, account: ${accountId} (${accountType})${sessionHash ? `, session: ${sessionHash}` : ''}`);
|
||||
|
||||
// 获取有效的访问token
|
||||
const accessToken = await claudeAccountService.getValidAccessToken(accountId);
|
||||
@@ -638,7 +643,7 @@ class ClaudeRelayService {
|
||||
return await this._makeClaudeStreamRequestWithUsageCapture(processedBody, accessToken, proxyAgent, clientHeaders, responseStream, (usageData) => {
|
||||
// 在usageCallback中添加accountId
|
||||
usageCallback({ ...usageData, accountId });
|
||||
}, accountId, sessionHash, streamTransformer, options);
|
||||
}, accountId, accountType, sessionHash, streamTransformer, options);
|
||||
} catch (error) {
|
||||
logger.error('❌ Claude stream relay with usage capture failed:', error);
|
||||
throw error;
|
||||
@@ -646,7 +651,7 @@ class ClaudeRelayService {
|
||||
}
|
||||
|
||||
// 🌊 发送流式请求到Claude API(带usage数据捕获)
|
||||
async _makeClaudeStreamRequestWithUsageCapture(body, accessToken, proxyAgent, clientHeaders, responseStream, usageCallback, accountId, sessionHash, streamTransformer = null, requestOptions = {}) {
|
||||
async _makeClaudeStreamRequestWithUsageCapture(body, accessToken, proxyAgent, clientHeaders, responseStream, usageCallback, accountId, accountType, sessionHash, streamTransformer = null, requestOptions = {}) {
|
||||
// 获取过滤后的客户端 headers
|
||||
const filteredHeaders = this._filterClientHeaders(clientHeaders);
|
||||
|
||||
@@ -854,12 +859,12 @@ class ClaudeRelayService {
|
||||
}
|
||||
|
||||
// 标记账号为限流状态并删除粘性会话映射
|
||||
await claudeAccountService.markAccountRateLimited(accountId, sessionHash, rateLimitResetTimestamp);
|
||||
await unifiedClaudeScheduler.markAccountRateLimited(accountId, accountType, sessionHash, rateLimitResetTimestamp);
|
||||
} else if (res.statusCode === 200) {
|
||||
// 如果请求成功,检查并移除限流状态
|
||||
const isRateLimited = await claudeAccountService.isAccountRateLimited(accountId);
|
||||
const isRateLimited = await unifiedClaudeScheduler.isAccountRateLimited(accountId, accountType);
|
||||
if (isRateLimited) {
|
||||
await claudeAccountService.removeAccountRateLimit(accountId);
|
||||
await unifiedClaudeScheduler.removeAccountRateLimit(accountId, accountType);
|
||||
}
|
||||
|
||||
// 只有真实的 Claude Code 请求才更新 headers(流式请求)
|
||||
|
||||
@@ -9,6 +9,16 @@ class UnifiedClaudeScheduler {
|
||||
this.SESSION_MAPPING_PREFIX = 'unified_claude_session_mapping:';
|
||||
}
|
||||
|
||||
// 🔧 辅助方法:检查账户是否可调度(兼容字符串和布尔值)
|
||||
_isSchedulable(schedulable) {
|
||||
// 如果是 undefined 或 null,默认为可调度
|
||||
if (schedulable === undefined || schedulable === null) {
|
||||
return true;
|
||||
}
|
||||
// 明确设置为 false(布尔值)或 'false'(字符串)时不可调度
|
||||
return schedulable !== false && schedulable !== 'false';
|
||||
}
|
||||
|
||||
// 🎯 统一调度Claude账号(官方和Console)
|
||||
async selectAccountForApiKey(apiKeyData, sessionHash = null, requestedModel = null) {
|
||||
try {
|
||||
@@ -152,7 +162,7 @@ class UnifiedClaudeScheduler {
|
||||
account.status !== 'error' &&
|
||||
account.status !== 'blocked' &&
|
||||
(account.accountType === 'shared' || !account.accountType) && // 兼容旧数据
|
||||
account.schedulable !== 'false') { // 检查是否可调度
|
||||
this._isSchedulable(account.schedulable)) { // 检查是否可调度
|
||||
|
||||
// 检查是否被限流
|
||||
const isRateLimited = await claudeAccountService.isAccountRateLimited(account.id);
|
||||
@@ -179,7 +189,7 @@ class UnifiedClaudeScheduler {
|
||||
if (account.isActive === true &&
|
||||
account.status === 'active' &&
|
||||
account.accountType === 'shared' &&
|
||||
account.schedulable !== false) { // 检查是否可调度
|
||||
this._isSchedulable(account.schedulable)) { // 检查是否可调度
|
||||
|
||||
// 检查模型支持(如果有请求的模型)
|
||||
if (requestedModel && account.supportedModels) {
|
||||
@@ -246,7 +256,7 @@ class UnifiedClaudeScheduler {
|
||||
return false;
|
||||
}
|
||||
// 检查是否可调度
|
||||
if (account.schedulable === 'false') {
|
||||
if (!this._isSchedulable(account.schedulable)) {
|
||||
logger.info(`🚫 Account ${accountId} is not schedulable`);
|
||||
return false;
|
||||
}
|
||||
@@ -257,7 +267,7 @@ class UnifiedClaudeScheduler {
|
||||
return false;
|
||||
}
|
||||
// 检查是否可调度
|
||||
if (account.schedulable === false) {
|
||||
if (!this._isSchedulable(account.schedulable)) {
|
||||
logger.info(`🚫 Claude Console account ${accountId} is not schedulable`);
|
||||
return false;
|
||||
}
|
||||
@@ -444,7 +454,7 @@ class UnifiedClaudeScheduler {
|
||||
? account.status !== 'error' && account.status !== 'blocked'
|
||||
: account.status === 'active';
|
||||
|
||||
if (isActive && status && account.schedulable !== false) {
|
||||
if (isActive && status && this._isSchedulable(account.schedulable)) {
|
||||
// 检查模型支持(Console账户)
|
||||
if (accountType === 'claude-console' && requestedModel && account.supportedModels && account.supportedModels.length > 0) {
|
||||
if (!account.supportedModels.includes(requestedModel)) {
|
||||
|
||||
@@ -8,6 +8,16 @@ class UnifiedGeminiScheduler {
|
||||
this.SESSION_MAPPING_PREFIX = 'unified_gemini_session_mapping:';
|
||||
}
|
||||
|
||||
// 🔧 辅助方法:检查账户是否可调度(兼容字符串和布尔值)
|
||||
_isSchedulable(schedulable) {
|
||||
// 如果是 undefined 或 null,默认为可调度
|
||||
if (schedulable === undefined || schedulable === null) {
|
||||
return true;
|
||||
}
|
||||
// 明确设置为 false(布尔值)或 'false'(字符串)时不可调度
|
||||
return schedulable !== false && schedulable !== 'false';
|
||||
}
|
||||
|
||||
// 🎯 统一调度Gemini账号
|
||||
async selectAccountForApiKey(apiKeyData, sessionHash = null, requestedModel = null) {
|
||||
try {
|
||||
@@ -128,7 +138,7 @@ class UnifiedGeminiScheduler {
|
||||
if (account.isActive === 'true' &&
|
||||
account.status !== 'error' &&
|
||||
(account.accountType === 'shared' || !account.accountType) && // 兼容旧数据
|
||||
account.schedulable !== 'false') { // 检查是否可调度
|
||||
this._isSchedulable(account.schedulable)) { // 检查是否可调度
|
||||
|
||||
// 检查token是否过期
|
||||
const isExpired = geminiAccountService.isTokenExpired(account);
|
||||
@@ -192,7 +202,7 @@ class UnifiedGeminiScheduler {
|
||||
return false;
|
||||
}
|
||||
// 检查是否可调度
|
||||
if (account.schedulable === 'false') {
|
||||
if (!this._isSchedulable(account.schedulable)) {
|
||||
logger.info(`🚫 Gemini account ${accountId} is not schedulable`);
|
||||
return false;
|
||||
}
|
||||
@@ -347,7 +357,7 @@ class UnifiedGeminiScheduler {
|
||||
// 检查账户是否可用
|
||||
if (account.isActive === 'true' &&
|
||||
account.status !== 'error' &&
|
||||
account.schedulable !== 'false') {
|
||||
this._isSchedulable(account.schedulable)) {
|
||||
|
||||
// 检查token是否过期
|
||||
const isExpired = geminiAccountService.isTokenExpired(account);
|
||||
|
||||
Reference in New Issue
Block a user