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:
shaw
2025-08-05 17:06:52 +08:00
parent f6b7342286
commit 7fa75df1fd
7 changed files with 243 additions and 68 deletions

View File

@@ -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流式请求