mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
chore
This commit is contained in:
@@ -93,7 +93,13 @@ async function migrate() {
|
||||
console.log('\n4. 迁移 usage:model:hourly 索引...')
|
||||
cursor = '0'
|
||||
do {
|
||||
const [newCursor, keys] = await redis.scan(cursor, 'MATCH', 'usage:model:hourly:*', 'COUNT', 500)
|
||||
const [newCursor, keys] = await redis.scan(
|
||||
cursor,
|
||||
'MATCH',
|
||||
'usage:model:hourly:*',
|
||||
'COUNT',
|
||||
500
|
||||
)
|
||||
cursor = newCursor
|
||||
|
||||
const pipeline = redis.pipeline()
|
||||
|
||||
@@ -289,17 +289,13 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
}
|
||||
|
||||
// 为每个API Key添加owner的displayName(批量获取优化)
|
||||
const userIdsToFetch = [
|
||||
...new Set(result.items.filter((k) => k.userId).map((k) => k.userId))
|
||||
]
|
||||
const userIdsToFetch = [...new Set(result.items.filter((k) => k.userId).map((k) => k.userId))]
|
||||
const userMap = new Map()
|
||||
|
||||
if (userIdsToFetch.length > 0) {
|
||||
// 批量获取用户信息
|
||||
const users = await Promise.all(
|
||||
userIdsToFetch.map((id) =>
|
||||
userService.getUserById(id, false).catch(() => null)
|
||||
)
|
||||
userIdsToFetch.map((id) => userService.getUserById(id, false).catch(() => null))
|
||||
)
|
||||
userIdsToFetch.forEach((id, i) => {
|
||||
if (users[i]) userMap.set(id, users[i])
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
|
||||
const redis = require('../models/redis')
|
||||
const logger = require('../utils/logger')
|
||||
const { getCachedConfig, setCachedConfig, deleteCachedConfig } = require('../utils/performanceOptimizer')
|
||||
const {
|
||||
getCachedConfig,
|
||||
setCachedConfig,
|
||||
deleteCachedConfig
|
||||
} = require('../utils/performanceOptimizer')
|
||||
|
||||
class ClaudeCodeHeadersService {
|
||||
constructor() {
|
||||
|
||||
@@ -24,9 +24,7 @@ const {
|
||||
|
||||
// structuredClone polyfill for Node < 17
|
||||
const safeClone =
|
||||
typeof structuredClone === 'function'
|
||||
? structuredClone
|
||||
: (obj) => JSON.parse(JSON.stringify(obj))
|
||||
typeof structuredClone === 'function' ? structuredClone : (obj) => JSON.parse(JSON.stringify(obj))
|
||||
|
||||
class ClaudeRelayService {
|
||||
constructor() {
|
||||
|
||||
@@ -94,7 +94,9 @@ class CostInitService {
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`💰 Found ${apiKeyIds.length} active API Keys to process (filtered ${allKeyIds.length - apiKeyIds.length} deleted)`)
|
||||
logger.info(
|
||||
`💰 Found ${apiKeyIds.length} active API Keys to process (filtered ${allKeyIds.length - apiKeyIds.length} deleted)`
|
||||
)
|
||||
|
||||
let processedCount = 0
|
||||
let errorCount = 0
|
||||
|
||||
@@ -30,10 +30,13 @@ class DroidAccountService {
|
||||
this._encryptor = createEncryptor('droid-account-salt')
|
||||
|
||||
// 🧹 定期清理缓存(每10分钟)
|
||||
setInterval(() => {
|
||||
this._encryptor.clearCache()
|
||||
logger.info('🧹 Droid decrypt cache cleanup completed', this._encryptor.getStats())
|
||||
}, 10 * 60 * 1000)
|
||||
setInterval(
|
||||
() => {
|
||||
this._encryptor.clearCache()
|
||||
logger.info('🧹 Droid decrypt cache cleanup completed', this._encryptor.getStats())
|
||||
},
|
||||
10 * 60 * 1000
|
||||
)
|
||||
|
||||
this.supportedEndpointTypes = new Set(['anthropic', 'openai', 'comm'])
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ const droidAccountService = require('./droidAccountService')
|
||||
const accountGroupService = require('./accountGroupService')
|
||||
const redis = require('../models/redis')
|
||||
const logger = require('../utils/logger')
|
||||
const { isTruthy, isAccountHealthy, sortAccountsByPriority, normalizeEndpointType } = require('../utils/commonHelper')
|
||||
const {
|
||||
isTruthy,
|
||||
isAccountHealthy,
|
||||
sortAccountsByPriority,
|
||||
normalizeEndpointType
|
||||
} = require('../utils/commonHelper')
|
||||
|
||||
class DroidScheduler {
|
||||
constructor() {
|
||||
|
||||
@@ -26,11 +26,7 @@ class UnifiedGeminiScheduler {
|
||||
if (apiKeyData.geminiAccountId.startsWith('api:')) {
|
||||
const accountId = apiKeyData.geminiAccountId.replace('api:', '')
|
||||
const boundAccount = await geminiApiAccountService.getAccount(accountId)
|
||||
if (
|
||||
boundAccount &&
|
||||
isActive(boundAccount.isActive) &&
|
||||
boundAccount.status !== 'error'
|
||||
) {
|
||||
if (boundAccount && isActive(boundAccount.isActive) && boundAccount.status !== 'error') {
|
||||
logger.info(
|
||||
`🎯 Using bound Gemini-API account: ${boundAccount.name} (${accountId}) for API key ${apiKeyData.name}`
|
||||
)
|
||||
@@ -63,11 +59,7 @@ class UnifiedGeminiScheduler {
|
||||
// 普通 Gemini OAuth 专属账户
|
||||
else {
|
||||
const boundAccount = await geminiAccountService.getAccount(apiKeyData.geminiAccountId)
|
||||
if (
|
||||
boundAccount &&
|
||||
isActive(boundAccount.isActive) &&
|
||||
boundAccount.status !== 'error'
|
||||
) {
|
||||
if (boundAccount && isActive(boundAccount.isActive) && boundAccount.status !== 'error') {
|
||||
logger.info(
|
||||
`🎯 Using bound dedicated Gemini account: ${boundAccount.name} (${apiKeyData.geminiAccountId}) for API key ${apiKeyData.name}`
|
||||
)
|
||||
@@ -183,11 +175,7 @@ class UnifiedGeminiScheduler {
|
||||
if (apiKeyData.geminiAccountId.startsWith('api:')) {
|
||||
const accountId = apiKeyData.geminiAccountId.replace('api:', '')
|
||||
const boundAccount = await geminiApiAccountService.getAccount(accountId)
|
||||
if (
|
||||
boundAccount &&
|
||||
isActive(boundAccount.isActive) &&
|
||||
boundAccount.status !== 'error'
|
||||
) {
|
||||
if (boundAccount && isActive(boundAccount.isActive) && boundAccount.status !== 'error') {
|
||||
const isRateLimited = await this.isAccountRateLimited(accountId)
|
||||
if (!isRateLimited) {
|
||||
// 检查模型支持
|
||||
@@ -234,11 +222,7 @@ class UnifiedGeminiScheduler {
|
||||
// 普通 Gemini OAuth 账户
|
||||
else if (!apiKeyData.geminiAccountId.startsWith('group:')) {
|
||||
const boundAccount = await geminiAccountService.getAccount(apiKeyData.geminiAccountId)
|
||||
if (
|
||||
boundAccount &&
|
||||
isActive(boundAccount.isActive) &&
|
||||
boundAccount.status !== 'error'
|
||||
) {
|
||||
if (boundAccount && isActive(boundAccount.isActive) && boundAccount.status !== 'error') {
|
||||
const isRateLimited = await this.isAccountRateLimited(boundAccount.id)
|
||||
if (!isRateLimited) {
|
||||
// 检查模型支持
|
||||
|
||||
@@ -84,7 +84,10 @@ const getDecryptCacheStats = defaultEncryptor.getStats
|
||||
// ============================================
|
||||
|
||||
// 转换为布尔值(宽松模式)
|
||||
const toBoolean = (value) => value === true || value === 'true' || (typeof value === 'string' && value.toLowerCase() === 'true')
|
||||
const toBoolean = (value) =>
|
||||
value === true ||
|
||||
value === 'true' ||
|
||||
(typeof value === 'string' && value.toLowerCase() === 'true')
|
||||
|
||||
// 检查是否为真值(null/undefined 返回 false)
|
||||
const isTruthy = (value) => value != null && toBoolean(value)
|
||||
@@ -110,7 +113,11 @@ const isAccountHealthy = (account) => {
|
||||
// 安全解析 JSON
|
||||
const safeParseJson = (value, fallback = null) => {
|
||||
if (!value || typeof value !== 'string') return fallback
|
||||
try { return JSON.parse(value) } catch { return fallback }
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
// 安全解析 JSON 为对象
|
||||
@@ -134,7 +141,10 @@ const normalizeModelName = (model) => {
|
||||
if (!model || model === 'unknown') return model
|
||||
// Bedrock 模型: us-east-1.anthropic.claude-3-5-sonnet-v1:0
|
||||
if (model.includes('.anthropic.') || model.includes('.claude')) {
|
||||
return model.replace(/^[a-z0-9-]+\./, '').replace('anthropic.', '').replace(/-v\d+:\d+$/, '')
|
||||
return model
|
||||
.replace(/^[a-z0-9-]+\./, '')
|
||||
.replace('anthropic.', '')
|
||||
.replace(/-v\d+:\d+$/, '')
|
||||
}
|
||||
return model.replace(/-v\d+:\d+$|:latest$/, '')
|
||||
}
|
||||
@@ -151,7 +161,7 @@ const isModelInMapping = (modelMapping, requestedModel) => {
|
||||
if (!modelMapping || Object.keys(modelMapping).length === 0) return true
|
||||
if (Object.prototype.hasOwnProperty.call(modelMapping, requestedModel)) return true
|
||||
const lower = requestedModel.toLowerCase()
|
||||
return Object.keys(modelMapping).some(k => k.toLowerCase() === lower)
|
||||
return Object.keys(modelMapping).some((k) => k.toLowerCase() === lower)
|
||||
}
|
||||
|
||||
// 获取映射后的模型名称
|
||||
@@ -192,7 +202,7 @@ const composeStickySessionKey = (prefix, sessionHash, apiKeyId = null) => {
|
||||
|
||||
// 过滤可用账户(激活 + 健康 + 可调度)
|
||||
const filterAvailableAccounts = (accounts) => {
|
||||
return accounts.filter(acc => acc && isAccountHealthy(acc) && isSchedulable(acc.schedulable))
|
||||
return accounts.filter((acc) => acc && isAccountHealthy(acc) && isSchedulable(acc.schedulable))
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
||||
Reference in New Issue
Block a user