mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
chore
This commit is contained in:
@@ -93,7 +93,13 @@ async function migrate() {
|
|||||||
console.log('\n4. 迁移 usage:model:hourly 索引...')
|
console.log('\n4. 迁移 usage:model:hourly 索引...')
|
||||||
cursor = '0'
|
cursor = '0'
|
||||||
do {
|
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
|
cursor = newCursor
|
||||||
|
|
||||||
const pipeline = redis.pipeline()
|
const pipeline = redis.pipeline()
|
||||||
|
|||||||
@@ -289,17 +289,13 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 为每个API Key添加owner的displayName(批量获取优化)
|
// 为每个API Key添加owner的displayName(批量获取优化)
|
||||||
const userIdsToFetch = [
|
const userIdsToFetch = [...new Set(result.items.filter((k) => k.userId).map((k) => k.userId))]
|
||||||
...new Set(result.items.filter((k) => k.userId).map((k) => k.userId))
|
|
||||||
]
|
|
||||||
const userMap = new Map()
|
const userMap = new Map()
|
||||||
|
|
||||||
if (userIdsToFetch.length > 0) {
|
if (userIdsToFetch.length > 0) {
|
||||||
// 批量获取用户信息
|
// 批量获取用户信息
|
||||||
const users = await Promise.all(
|
const users = await Promise.all(
|
||||||
userIdsToFetch.map((id) =>
|
userIdsToFetch.map((id) => userService.getUserById(id, false).catch(() => null))
|
||||||
userService.getUserById(id, false).catch(() => null)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
userIdsToFetch.forEach((id, i) => {
|
userIdsToFetch.forEach((id, i) => {
|
||||||
if (users[i]) userMap.set(id, users[i])
|
if (users[i]) userMap.set(id, users[i])
|
||||||
|
|||||||
@@ -5,7 +5,11 @@
|
|||||||
|
|
||||||
const redis = require('../models/redis')
|
const redis = require('../models/redis')
|
||||||
const logger = require('../utils/logger')
|
const logger = require('../utils/logger')
|
||||||
const { getCachedConfig, setCachedConfig, deleteCachedConfig } = require('../utils/performanceOptimizer')
|
const {
|
||||||
|
getCachedConfig,
|
||||||
|
setCachedConfig,
|
||||||
|
deleteCachedConfig
|
||||||
|
} = require('../utils/performanceOptimizer')
|
||||||
|
|
||||||
class ClaudeCodeHeadersService {
|
class ClaudeCodeHeadersService {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ const {
|
|||||||
|
|
||||||
// structuredClone polyfill for Node < 17
|
// structuredClone polyfill for Node < 17
|
||||||
const safeClone =
|
const safeClone =
|
||||||
typeof structuredClone === 'function'
|
typeof structuredClone === 'function' ? structuredClone : (obj) => JSON.parse(JSON.stringify(obj))
|
||||||
? structuredClone
|
|
||||||
: (obj) => JSON.parse(JSON.stringify(obj))
|
|
||||||
|
|
||||||
class ClaudeRelayService {
|
class ClaudeRelayService {
|
||||||
constructor() {
|
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 processedCount = 0
|
||||||
let errorCount = 0
|
let errorCount = 0
|
||||||
|
|||||||
@@ -30,10 +30,13 @@ class DroidAccountService {
|
|||||||
this._encryptor = createEncryptor('droid-account-salt')
|
this._encryptor = createEncryptor('droid-account-salt')
|
||||||
|
|
||||||
// 🧹 定期清理缓存(每10分钟)
|
// 🧹 定期清理缓存(每10分钟)
|
||||||
setInterval(() => {
|
setInterval(
|
||||||
this._encryptor.clearCache()
|
() => {
|
||||||
logger.info('🧹 Droid decrypt cache cleanup completed', this._encryptor.getStats())
|
this._encryptor.clearCache()
|
||||||
}, 10 * 60 * 1000)
|
logger.info('🧹 Droid decrypt cache cleanup completed', this._encryptor.getStats())
|
||||||
|
},
|
||||||
|
10 * 60 * 1000
|
||||||
|
)
|
||||||
|
|
||||||
this.supportedEndpointTypes = new Set(['anthropic', 'openai', 'comm'])
|
this.supportedEndpointTypes = new Set(['anthropic', 'openai', 'comm'])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ const droidAccountService = require('./droidAccountService')
|
|||||||
const accountGroupService = require('./accountGroupService')
|
const accountGroupService = require('./accountGroupService')
|
||||||
const redis = require('../models/redis')
|
const redis = require('../models/redis')
|
||||||
const logger = require('../utils/logger')
|
const logger = require('../utils/logger')
|
||||||
const { isTruthy, isAccountHealthy, sortAccountsByPriority, normalizeEndpointType } = require('../utils/commonHelper')
|
const {
|
||||||
|
isTruthy,
|
||||||
|
isAccountHealthy,
|
||||||
|
sortAccountsByPriority,
|
||||||
|
normalizeEndpointType
|
||||||
|
} = require('../utils/commonHelper')
|
||||||
|
|
||||||
class DroidScheduler {
|
class DroidScheduler {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@@ -26,11 +26,7 @@ class UnifiedGeminiScheduler {
|
|||||||
if (apiKeyData.geminiAccountId.startsWith('api:')) {
|
if (apiKeyData.geminiAccountId.startsWith('api:')) {
|
||||||
const accountId = apiKeyData.geminiAccountId.replace('api:', '')
|
const accountId = apiKeyData.geminiAccountId.replace('api:', '')
|
||||||
const boundAccount = await geminiApiAccountService.getAccount(accountId)
|
const boundAccount = await geminiApiAccountService.getAccount(accountId)
|
||||||
if (
|
if (boundAccount && isActive(boundAccount.isActive) && boundAccount.status !== 'error') {
|
||||||
boundAccount &&
|
|
||||||
isActive(boundAccount.isActive) &&
|
|
||||||
boundAccount.status !== 'error'
|
|
||||||
) {
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Using bound Gemini-API account: ${boundAccount.name} (${accountId}) for API key ${apiKeyData.name}`
|
`🎯 Using bound Gemini-API account: ${boundAccount.name} (${accountId}) for API key ${apiKeyData.name}`
|
||||||
)
|
)
|
||||||
@@ -63,11 +59,7 @@ class UnifiedGeminiScheduler {
|
|||||||
// 普通 Gemini OAuth 专属账户
|
// 普通 Gemini OAuth 专属账户
|
||||||
else {
|
else {
|
||||||
const boundAccount = await geminiAccountService.getAccount(apiKeyData.geminiAccountId)
|
const boundAccount = await geminiAccountService.getAccount(apiKeyData.geminiAccountId)
|
||||||
if (
|
if (boundAccount && isActive(boundAccount.isActive) && boundAccount.status !== 'error') {
|
||||||
boundAccount &&
|
|
||||||
isActive(boundAccount.isActive) &&
|
|
||||||
boundAccount.status !== 'error'
|
|
||||||
) {
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Using bound dedicated Gemini account: ${boundAccount.name} (${apiKeyData.geminiAccountId}) for API key ${apiKeyData.name}`
|
`🎯 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:')) {
|
if (apiKeyData.geminiAccountId.startsWith('api:')) {
|
||||||
const accountId = apiKeyData.geminiAccountId.replace('api:', '')
|
const accountId = apiKeyData.geminiAccountId.replace('api:', '')
|
||||||
const boundAccount = await geminiApiAccountService.getAccount(accountId)
|
const boundAccount = await geminiApiAccountService.getAccount(accountId)
|
||||||
if (
|
if (boundAccount && isActive(boundAccount.isActive) && boundAccount.status !== 'error') {
|
||||||
boundAccount &&
|
|
||||||
isActive(boundAccount.isActive) &&
|
|
||||||
boundAccount.status !== 'error'
|
|
||||||
) {
|
|
||||||
const isRateLimited = await this.isAccountRateLimited(accountId)
|
const isRateLimited = await this.isAccountRateLimited(accountId)
|
||||||
if (!isRateLimited) {
|
if (!isRateLimited) {
|
||||||
// 检查模型支持
|
// 检查模型支持
|
||||||
@@ -234,11 +222,7 @@ class UnifiedGeminiScheduler {
|
|||||||
// 普通 Gemini OAuth 账户
|
// 普通 Gemini OAuth 账户
|
||||||
else if (!apiKeyData.geminiAccountId.startsWith('group:')) {
|
else if (!apiKeyData.geminiAccountId.startsWith('group:')) {
|
||||||
const boundAccount = await geminiAccountService.getAccount(apiKeyData.geminiAccountId)
|
const boundAccount = await geminiAccountService.getAccount(apiKeyData.geminiAccountId)
|
||||||
if (
|
if (boundAccount && isActive(boundAccount.isActive) && boundAccount.status !== 'error') {
|
||||||
boundAccount &&
|
|
||||||
isActive(boundAccount.isActive) &&
|
|
||||||
boundAccount.status !== 'error'
|
|
||||||
) {
|
|
||||||
const isRateLimited = await this.isAccountRateLimited(boundAccount.id)
|
const isRateLimited = await this.isAccountRateLimited(boundAccount.id)
|
||||||
if (!isRateLimited) {
|
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)
|
// 检查是否为真值(null/undefined 返回 false)
|
||||||
const isTruthy = (value) => value != null && toBoolean(value)
|
const isTruthy = (value) => value != null && toBoolean(value)
|
||||||
@@ -110,7 +113,11 @@ const isAccountHealthy = (account) => {
|
|||||||
// 安全解析 JSON
|
// 安全解析 JSON
|
||||||
const safeParseJson = (value, fallback = null) => {
|
const safeParseJson = (value, fallback = null) => {
|
||||||
if (!value || typeof value !== 'string') return fallback
|
if (!value || typeof value !== 'string') return fallback
|
||||||
try { return JSON.parse(value) } catch { return fallback }
|
try {
|
||||||
|
return JSON.parse(value)
|
||||||
|
} catch {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 安全解析 JSON 为对象
|
// 安全解析 JSON 为对象
|
||||||
@@ -134,7 +141,10 @@ const normalizeModelName = (model) => {
|
|||||||
if (!model || model === 'unknown') return model
|
if (!model || model === 'unknown') return model
|
||||||
// Bedrock 模型: us-east-1.anthropic.claude-3-5-sonnet-v1:0
|
// Bedrock 模型: us-east-1.anthropic.claude-3-5-sonnet-v1:0
|
||||||
if (model.includes('.anthropic.') || model.includes('.claude')) {
|
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$/, '')
|
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 (!modelMapping || Object.keys(modelMapping).length === 0) return true
|
||||||
if (Object.prototype.hasOwnProperty.call(modelMapping, requestedModel)) return true
|
if (Object.prototype.hasOwnProperty.call(modelMapping, requestedModel)) return true
|
||||||
const lower = requestedModel.toLowerCase()
|
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) => {
|
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