cc / src /services /apiKeyService.js
hequ's picture
Upload 224 files
6c6056a verified
const crypto = require('crypto')
const { v4: uuidv4 } = require('uuid')
const config = require('../../config/config')
const redis = require('../models/redis')
const logger = require('../utils/logger')
const ACCOUNT_TYPE_CONFIG = {
claude: { prefix: 'claude:account:' },
'claude-console': { prefix: 'claude_console_account:' },
openai: { prefix: 'openai:account:' },
'openai-responses': { prefix: 'openai_responses_account:' },
'azure-openai': { prefix: 'azure_openai:account:' },
gemini: { prefix: 'gemini_account:' },
droid: { prefix: 'droid:account:' }
}
const ACCOUNT_TYPE_PRIORITY = [
'openai',
'openai-responses',
'azure-openai',
'claude',
'claude-console',
'gemini',
'droid'
]
const ACCOUNT_CATEGORY_MAP = {
claude: 'claude',
'claude-console': 'claude',
openai: 'openai',
'openai-responses': 'openai',
'azure-openai': 'openai',
gemini: 'gemini',
droid: 'droid'
}
function normalizeAccountTypeKey(type) {
if (!type) {
return null
}
const lower = String(type).toLowerCase()
if (lower === 'claude_console') {
return 'claude-console'
}
if (lower === 'openai_responses' || lower === 'openai-response' || lower === 'openai-responses') {
return 'openai-responses'
}
if (lower === 'azure_openai' || lower === 'azureopenai' || lower === 'azure-openai') {
return 'azure-openai'
}
return lower
}
function sanitizeAccountIdForType(accountId, accountType) {
if (!accountId || typeof accountId !== 'string') {
return accountId
}
if (accountType === 'openai-responses') {
return accountId.replace(/^responses:/, '')
}
return accountId
}
class ApiKeyService {
constructor() {
this.prefix = config.security.apiKeyPrefix
}
// 🔑 生成新的API Key
async generateApiKey(options = {}) {
const {
name = 'Unnamed Key',
description = '',
tokenLimit = 0, // 默认为0,不再使用token限制
expiresAt = null,
claudeAccountId = null,
claudeConsoleAccountId = null,
geminiAccountId = null,
openaiAccountId = null,
azureOpenaiAccountId = null,
bedrockAccountId = null, // 添加 Bedrock 账号ID支持
droidAccountId = null,
permissions = 'all', // 可选值:'claude'、'gemini'、'openai'、'droid' 或 'all'
isActive = true,
concurrencyLimit = 0,
rateLimitWindow = null,
rateLimitRequests = null,
rateLimitCost = null, // 新增:速率限制费用字段
enableModelRestriction = false,
restrictedModels = [],
enableClientRestriction = false,
allowedClients = [],
dailyCostLimit = 0,
totalCostLimit = 0,
weeklyOpusCostLimit = 0,
tags = [],
activationDays = 0, // 新增:激活后有效天数(0表示不使用此功能)
activationUnit = 'days', // 新增:激活时间单位 'hours' 或 'days'
expirationMode = 'fixed', // 新增:过期模式 'fixed'(固定时间) 或 'activation'(首次使用后激活)
icon = '' // 新增:图标(base64编码)
} = options
// 生成简单的API Key (64字符十六进制)
const apiKey = `${this.prefix}${this._generateSecretKey()}`
const keyId = uuidv4()
const hashedKey = this._hashApiKey(apiKey)
const keyData = {
id: keyId,
name,
description,
apiKey: hashedKey,
tokenLimit: String(tokenLimit ?? 0),
concurrencyLimit: String(concurrencyLimit ?? 0),
rateLimitWindow: String(rateLimitWindow ?? 0),
rateLimitRequests: String(rateLimitRequests ?? 0),
rateLimitCost: String(rateLimitCost ?? 0), // 新增:速率限制费用字段
isActive: String(isActive),
claudeAccountId: claudeAccountId || '',
claudeConsoleAccountId: claudeConsoleAccountId || '',
geminiAccountId: geminiAccountId || '',
openaiAccountId: openaiAccountId || '',
azureOpenaiAccountId: azureOpenaiAccountId || '',
bedrockAccountId: bedrockAccountId || '', // 添加 Bedrock 账号ID
droidAccountId: droidAccountId || '',
permissions: permissions || 'all',
enableModelRestriction: String(enableModelRestriction),
restrictedModels: JSON.stringify(restrictedModels || []),
enableClientRestriction: String(enableClientRestriction || false),
allowedClients: JSON.stringify(allowedClients || []),
dailyCostLimit: String(dailyCostLimit || 0),
totalCostLimit: String(totalCostLimit || 0),
weeklyOpusCostLimit: String(weeklyOpusCostLimit || 0),
tags: JSON.stringify(tags || []),
activationDays: String(activationDays || 0), // 新增:激活后有效天数
activationUnit: activationUnit || 'days', // 新增:激活时间单位
expirationMode: expirationMode || 'fixed', // 新增:过期模式
isActivated: expirationMode === 'fixed' ? 'true' : 'false', // 根据模式决定激活状态
activatedAt: expirationMode === 'fixed' ? new Date().toISOString() : '', // 激活时间
createdAt: new Date().toISOString(),
lastUsedAt: '',
expiresAt: expirationMode === 'fixed' ? expiresAt || '' : '', // 固定模式才设置过期时间
createdBy: options.createdBy || 'admin',
userId: options.userId || '',
userUsername: options.userUsername || '',
icon: icon || '' // 新增:图标(base64编码)
}
// 保存API Key数据并建立哈希映射
await redis.setApiKey(keyId, keyData, hashedKey)
logger.success(`🔑 Generated new API key: ${name} (${keyId})`)
return {
id: keyId,
apiKey, // 只在创建时返回完整的key
name: keyData.name,
description: keyData.description,
tokenLimit: parseInt(keyData.tokenLimit),
concurrencyLimit: parseInt(keyData.concurrencyLimit),
rateLimitWindow: parseInt(keyData.rateLimitWindow || 0),
rateLimitRequests: parseInt(keyData.rateLimitRequests || 0),
rateLimitCost: parseFloat(keyData.rateLimitCost || 0), // 新增:速率限制费用字段
isActive: keyData.isActive === 'true',
claudeAccountId: keyData.claudeAccountId,
claudeConsoleAccountId: keyData.claudeConsoleAccountId,
geminiAccountId: keyData.geminiAccountId,
openaiAccountId: keyData.openaiAccountId,
azureOpenaiAccountId: keyData.azureOpenaiAccountId,
bedrockAccountId: keyData.bedrockAccountId, // 添加 Bedrock 账号ID
droidAccountId: keyData.droidAccountId,
permissions: keyData.permissions,
enableModelRestriction: keyData.enableModelRestriction === 'true',
restrictedModels: JSON.parse(keyData.restrictedModels),
enableClientRestriction: keyData.enableClientRestriction === 'true',
allowedClients: JSON.parse(keyData.allowedClients || '[]'),
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
totalCostLimit: parseFloat(keyData.totalCostLimit || 0),
weeklyOpusCostLimit: parseFloat(keyData.weeklyOpusCostLimit || 0),
tags: JSON.parse(keyData.tags || '[]'),
activationDays: parseInt(keyData.activationDays || 0),
activationUnit: keyData.activationUnit || 'days',
expirationMode: keyData.expirationMode || 'fixed',
isActivated: keyData.isActivated === 'true',
activatedAt: keyData.activatedAt,
createdAt: keyData.createdAt,
expiresAt: keyData.expiresAt,
createdBy: keyData.createdBy
}
}
// 🔍 验证API Key
async validateApiKey(apiKey) {
try {
if (!apiKey || !apiKey.startsWith(this.prefix)) {
return { valid: false, error: 'Invalid API key format' }
}
// 计算API Key的哈希值
const hashedKey = this._hashApiKey(apiKey)
// 通过哈希值直接查找API Key(性能优化)
const keyData = await redis.findApiKeyByHash(hashedKey)
if (!keyData) {
return { valid: false, error: 'API key not found' }
}
// 检查是否激活
if (keyData.isActive !== 'true') {
return { valid: false, error: 'API key is disabled' }
}
// 处理激活逻辑(仅在 activation 模式下)
if (keyData.expirationMode === 'activation' && keyData.isActivated !== 'true') {
// 首次使用,需要激活
const now = new Date()
const activationPeriod = parseInt(keyData.activationDays || 30) // 默认30
const activationUnit = keyData.activationUnit || 'days' // 默认天
// 根据单位计算过期时间
let milliseconds
if (activationUnit === 'hours') {
milliseconds = activationPeriod * 60 * 60 * 1000 // 小时转毫秒
} else {
milliseconds = activationPeriod * 24 * 60 * 60 * 1000 // 天转毫秒
}
const expiresAt = new Date(now.getTime() + milliseconds)
// 更新激活状态和过期时间
keyData.isActivated = 'true'
keyData.activatedAt = now.toISOString()
keyData.expiresAt = expiresAt.toISOString()
keyData.lastUsedAt = now.toISOString()
// 保存到Redis
await redis.setApiKey(keyData.id, keyData)
logger.success(
`🔓 API key activated: ${keyData.id} (${
keyData.name
}), will expire in ${activationPeriod} ${activationUnit} at ${expiresAt.toISOString()}`
)
}
// 检查是否过期
if (keyData.expiresAt && new Date() > new Date(keyData.expiresAt)) {
return { valid: false, error: 'API key has expired' }
}
// 如果API Key属于某个用户,检查用户是否被禁用
if (keyData.userId) {
try {
const userService = require('./userService')
const user = await userService.getUserById(keyData.userId, false)
if (!user || !user.isActive) {
return { valid: false, error: 'User account is disabled' }
}
} catch (error) {
logger.error('❌ Error checking user status during API key validation:', error)
return { valid: false, error: 'Unable to validate user status' }
}
}
// 获取使用统计(供返回数据使用)
const usage = await redis.getUsageStats(keyData.id)
// 获取费用统计
const [dailyCost, costStats] = await Promise.all([
redis.getDailyCost(keyData.id),
redis.getCostStats(keyData.id)
])
const totalCost = costStats?.total || 0
// 更新最后使用时间(优化:只在实际API调用时更新,而不是验证时)
// 注意:lastUsedAt的更新已移至recordUsage方法中
logger.api(`🔓 API key validated successfully: ${keyData.id}`)
// 解析限制模型数据
let restrictedModels = []
try {
restrictedModels = keyData.restrictedModels ? JSON.parse(keyData.restrictedModels) : []
} catch (e) {
restrictedModels = []
}
// 解析允许的客户端
let allowedClients = []
try {
allowedClients = keyData.allowedClients ? JSON.parse(keyData.allowedClients) : []
} catch (e) {
allowedClients = []
}
// 解析标签
let tags = []
try {
tags = keyData.tags ? JSON.parse(keyData.tags) : []
} catch (e) {
tags = []
}
return {
valid: true,
keyData: {
id: keyData.id,
name: keyData.name,
description: keyData.description,
createdAt: keyData.createdAt,
expiresAt: keyData.expiresAt,
claudeAccountId: keyData.claudeAccountId,
claudeConsoleAccountId: keyData.claudeConsoleAccountId,
geminiAccountId: keyData.geminiAccountId,
openaiAccountId: keyData.openaiAccountId,
azureOpenaiAccountId: keyData.azureOpenaiAccountId,
bedrockAccountId: keyData.bedrockAccountId, // 添加 Bedrock 账号ID
droidAccountId: keyData.droidAccountId,
permissions: keyData.permissions || 'all',
tokenLimit: parseInt(keyData.tokenLimit),
concurrencyLimit: parseInt(keyData.concurrencyLimit || 0),
rateLimitWindow: parseInt(keyData.rateLimitWindow || 0),
rateLimitRequests: parseInt(keyData.rateLimitRequests || 0),
rateLimitCost: parseFloat(keyData.rateLimitCost || 0), // 新增:速率限制费用字段
enableModelRestriction: keyData.enableModelRestriction === 'true',
restrictedModels,
enableClientRestriction: keyData.enableClientRestriction === 'true',
allowedClients,
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
totalCostLimit: parseFloat(keyData.totalCostLimit || 0),
weeklyOpusCostLimit: parseFloat(keyData.weeklyOpusCostLimit || 0),
dailyCost: dailyCost || 0,
totalCost,
weeklyOpusCost: (await redis.getWeeklyOpusCost(keyData.id)) || 0,
tags,
usage
}
}
} catch (error) {
logger.error('❌ API key validation error:', error)
return { valid: false, error: 'Internal validation error' }
}
}
// 🔍 验证API Key(仅用于统计查询,不触发激活)
async validateApiKeyForStats(apiKey) {
try {
if (!apiKey || !apiKey.startsWith(this.prefix)) {
return { valid: false, error: 'Invalid API key format' }
}
// 计算API Key的哈希值
const hashedKey = this._hashApiKey(apiKey)
// 通过哈希值直接查找API Key(性能优化)
const keyData = await redis.findApiKeyByHash(hashedKey)
if (!keyData) {
return { valid: false, error: 'API key not found' }
}
// 检查是否激活
if (keyData.isActive !== 'true') {
return { valid: false, error: 'API key is disabled' }
}
// 注意:这里不处理激活逻辑,保持 API Key 的未激活状态
// 检查是否过期(仅对已激活的 Key 检查)
if (
keyData.isActivated === 'true' &&
keyData.expiresAt &&
new Date() > new Date(keyData.expiresAt)
) {
return { valid: false, error: 'API key has expired' }
}
// 如果API Key属于某个用户,检查用户是否被禁用
if (keyData.userId) {
try {
const userService = require('./userService')
const user = await userService.getUserById(keyData.userId, false)
if (!user || !user.isActive) {
return { valid: false, error: 'User account is disabled' }
}
} catch (userError) {
// 如果用户服务出错,记录但不影响API Key验证
logger.warn(`Failed to check user status for API key ${keyData.id}:`, userError)
}
}
// 获取当日费用
const [dailyCost, costStats] = await Promise.all([
redis.getDailyCost(keyData.id),
redis.getCostStats(keyData.id)
])
// 获取使用统计
const usage = await redis.getUsageStats(keyData.id)
// 解析限制模型数据
let restrictedModels = []
try {
restrictedModels = keyData.restrictedModels ? JSON.parse(keyData.restrictedModels) : []
} catch (e) {
restrictedModels = []
}
// 解析允许的客户端
let allowedClients = []
try {
allowedClients = keyData.allowedClients ? JSON.parse(keyData.allowedClients) : []
} catch (e) {
allowedClients = []
}
// 解析标签
let tags = []
try {
tags = keyData.tags ? JSON.parse(keyData.tags) : []
} catch (e) {
tags = []
}
return {
valid: true,
keyData: {
id: keyData.id,
name: keyData.name,
description: keyData.description,
createdAt: keyData.createdAt,
expiresAt: keyData.expiresAt,
// 添加激活相关字段
expirationMode: keyData.expirationMode || 'fixed',
isActivated: keyData.isActivated === 'true',
activationDays: parseInt(keyData.activationDays || 0),
activationUnit: keyData.activationUnit || 'days',
activatedAt: keyData.activatedAt || null,
claudeAccountId: keyData.claudeAccountId,
claudeConsoleAccountId: keyData.claudeConsoleAccountId,
geminiAccountId: keyData.geminiAccountId,
openaiAccountId: keyData.openaiAccountId,
azureOpenaiAccountId: keyData.azureOpenaiAccountId,
bedrockAccountId: keyData.bedrockAccountId,
droidAccountId: keyData.droidAccountId,
permissions: keyData.permissions || 'all',
tokenLimit: parseInt(keyData.tokenLimit),
concurrencyLimit: parseInt(keyData.concurrencyLimit || 0),
rateLimitWindow: parseInt(keyData.rateLimitWindow || 0),
rateLimitRequests: parseInt(keyData.rateLimitRequests || 0),
rateLimitCost: parseFloat(keyData.rateLimitCost || 0),
enableModelRestriction: keyData.enableModelRestriction === 'true',
restrictedModels,
enableClientRestriction: keyData.enableClientRestriction === 'true',
allowedClients,
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
totalCostLimit: parseFloat(keyData.totalCostLimit || 0),
weeklyOpusCostLimit: parseFloat(keyData.weeklyOpusCostLimit || 0),
dailyCost: dailyCost || 0,
totalCost: costStats?.total || 0,
weeklyOpusCost: (await redis.getWeeklyOpusCost(keyData.id)) || 0,
tags,
usage
}
}
} catch (error) {
logger.error('❌ API key validation error (stats):', error)
return { valid: false, error: 'Internal validation error' }
}
}
// 📋 获取所有API Keys
async getAllApiKeys(includeDeleted = false) {
try {
let apiKeys = await redis.getAllApiKeys()
const client = redis.getClientSafe()
const accountInfoCache = new Map()
// 默认过滤掉已删除的API Keys
if (!includeDeleted) {
apiKeys = apiKeys.filter((key) => key.isDeleted !== 'true')
}
// 为每个key添加使用统计和当前并发数
for (const key of apiKeys) {
key.usage = await redis.getUsageStats(key.id)
const costStats = await redis.getCostStats(key.id)
// Add cost information to usage object for frontend compatibility
if (key.usage && costStats) {
key.usage.total = key.usage.total || {}
key.usage.total.cost = costStats.total
key.usage.totalCost = costStats.total
}
key.totalCost = costStats ? costStats.total : 0
key.tokenLimit = parseInt(key.tokenLimit)
key.concurrencyLimit = parseInt(key.concurrencyLimit || 0)
key.rateLimitWindow = parseInt(key.rateLimitWindow || 0)
key.rateLimitRequests = parseInt(key.rateLimitRequests || 0)
key.rateLimitCost = parseFloat(key.rateLimitCost || 0) // 新增:速率限制费用字段
key.currentConcurrency = await redis.getConcurrency(key.id)
key.isActive = key.isActive === 'true'
key.enableModelRestriction = key.enableModelRestriction === 'true'
key.enableClientRestriction = key.enableClientRestriction === 'true'
key.permissions = key.permissions || 'all' // 兼容旧数据
key.dailyCostLimit = parseFloat(key.dailyCostLimit || 0)
key.totalCostLimit = parseFloat(key.totalCostLimit || 0)
key.weeklyOpusCostLimit = parseFloat(key.weeklyOpusCostLimit || 0)
key.dailyCost = (await redis.getDailyCost(key.id)) || 0
key.weeklyOpusCost = (await redis.getWeeklyOpusCost(key.id)) || 0
key.activationDays = parseInt(key.activationDays || 0)
key.activationUnit = key.activationUnit || 'days'
key.expirationMode = key.expirationMode || 'fixed'
key.isActivated = key.isActivated === 'true'
key.activatedAt = key.activatedAt || null
// 获取当前时间窗口的请求次数、Token使用量和费用
if (key.rateLimitWindow > 0) {
const requestCountKey = `rate_limit:requests:${key.id}`
const tokenCountKey = `rate_limit:tokens:${key.id}`
const costCountKey = `rate_limit:cost:${key.id}` // 新增:费用计数器
const windowStartKey = `rate_limit:window_start:${key.id}`
key.currentWindowRequests = parseInt((await client.get(requestCountKey)) || '0')
key.currentWindowTokens = parseInt((await client.get(tokenCountKey)) || '0')
key.currentWindowCost = parseFloat((await client.get(costCountKey)) || '0') // 新增:当前窗口费用
// 获取窗口开始时间和计算剩余时间
const windowStart = await client.get(windowStartKey)
if (windowStart) {
const now = Date.now()
const windowStartTime = parseInt(windowStart)
const windowDuration = key.rateLimitWindow * 60 * 1000 // 转换为毫秒
const windowEndTime = windowStartTime + windowDuration
// 如果窗口还有效
if (now < windowEndTime) {
key.windowStartTime = windowStartTime
key.windowEndTime = windowEndTime
key.windowRemainingSeconds = Math.max(0, Math.floor((windowEndTime - now) / 1000))
} else {
// 窗口已过期,下次请求会重置
key.windowStartTime = null
key.windowEndTime = null
key.windowRemainingSeconds = 0
// 重置计数为0,因为窗口已过期
key.currentWindowRequests = 0
key.currentWindowTokens = 0
key.currentWindowCost = 0 // 新增:重置费用
}
} else {
// 窗口还未开始(没有任何请求)
key.windowStartTime = null
key.windowEndTime = null
key.windowRemainingSeconds = null
}
} else {
key.currentWindowRequests = 0
key.currentWindowTokens = 0
key.currentWindowCost = 0 // 新增:重置费用
key.windowStartTime = null
key.windowEndTime = null
key.windowRemainingSeconds = null
}
try {
key.restrictedModels = key.restrictedModels ? JSON.parse(key.restrictedModels) : []
} catch (e) {
key.restrictedModels = []
}
try {
key.allowedClients = key.allowedClients ? JSON.parse(key.allowedClients) : []
} catch (e) {
key.allowedClients = []
}
try {
key.tags = key.tags ? JSON.parse(key.tags) : []
} catch (e) {
key.tags = []
}
// 不暴露已弃用字段
if (Object.prototype.hasOwnProperty.call(key, 'ccrAccountId')) {
delete key.ccrAccountId
}
let lastUsageRecord = null
try {
const usageRecords = await redis.getUsageRecords(key.id, 1)
if (Array.isArray(usageRecords) && usageRecords.length > 0) {
lastUsageRecord = usageRecords[0]
}
} catch (error) {
logger.debug(`加载 API Key ${key.id} 的使用记录失败:`, error)
}
if (lastUsageRecord && (lastUsageRecord.accountId || lastUsageRecord.accountType)) {
const resolvedAccount = await this._resolveLastUsageAccount(
key,
lastUsageRecord,
accountInfoCache,
client
)
if (resolvedAccount) {
key.lastUsage = {
accountId: resolvedAccount.accountId,
rawAccountId: lastUsageRecord.accountId || resolvedAccount.accountId,
accountType: resolvedAccount.accountType,
accountCategory: resolvedAccount.accountCategory,
accountName: resolvedAccount.accountName,
recordedAt: lastUsageRecord.timestamp || key.lastUsedAt || null
}
} else {
key.lastUsage = {
accountId: null,
rawAccountId: lastUsageRecord.accountId || null,
accountType: 'deleted',
accountCategory: 'deleted',
accountName: '已删除',
recordedAt: lastUsageRecord.timestamp || key.lastUsedAt || null
}
}
} else {
key.lastUsage = null
}
delete key.apiKey // 不返回哈希后的key
}
return apiKeys
} catch (error) {
logger.error('❌ Failed to get API keys:', error)
throw error
}
}
// 📝 更新API Key
async updateApiKey(keyId, updates) {
try {
const keyData = await redis.getApiKey(keyId)
if (!keyData || Object.keys(keyData).length === 0) {
throw new Error('API key not found')
}
// 允许更新的字段
const allowedUpdates = [
'name',
'description',
'tokenLimit',
'concurrencyLimit',
'rateLimitWindow',
'rateLimitRequests',
'rateLimitCost', // 新增:速率限制费用字段
'isActive',
'claudeAccountId',
'claudeConsoleAccountId',
'geminiAccountId',
'openaiAccountId',
'azureOpenaiAccountId',
'bedrockAccountId', // 添加 Bedrock 账号ID
'droidAccountId',
'permissions',
'expiresAt',
'activationDays', // 新增:激活后有效天数
'activationUnit', // 新增:激活时间单位
'expirationMode', // 新增:过期模式
'isActivated', // 新增:是否已激活
'activatedAt', // 新增:激活时间
'enableModelRestriction',
'restrictedModels',
'enableClientRestriction',
'allowedClients',
'dailyCostLimit',
'totalCostLimit',
'weeklyOpusCostLimit',
'tags',
'userId', // 新增:用户ID(所有者变更)
'userUsername', // 新增:用户名(所有者变更)
'createdBy' // 新增:创建者(所有者变更)
]
const updatedData = { ...keyData }
for (const [field, value] of Object.entries(updates)) {
if (allowedUpdates.includes(field)) {
if (field === 'restrictedModels' || field === 'allowedClients' || field === 'tags') {
// 特殊处理数组字段
updatedData[field] = JSON.stringify(value || [])
} else if (
field === 'enableModelRestriction' ||
field === 'enableClientRestriction' ||
field === 'isActivated'
) {
// 布尔值转字符串
updatedData[field] = String(value)
} else if (field === 'expiresAt' || field === 'activatedAt') {
// 日期字段保持原样,不要toString()
updatedData[field] = value || ''
} else {
updatedData[field] = (value !== null && value !== undefined ? value : '').toString()
}
}
}
updatedData.updatedAt = new Date().toISOString()
// 更新时不需要重新建立哈希映射,因为API Key本身没有变化
await redis.setApiKey(keyId, updatedData)
logger.success(`📝 Updated API key: ${keyId}`)
return { success: true }
} catch (error) {
logger.error('❌ Failed to update API key:', error)
throw error
}
}
// 🗑️ 软删除API Key (保留使用统计)
async deleteApiKey(keyId, deletedBy = 'system', deletedByType = 'system') {
try {
const keyData = await redis.getApiKey(keyId)
if (!keyData || Object.keys(keyData).length === 0) {
throw new Error('API key not found')
}
// 标记为已删除,保留所有数据和统计信息
const updatedData = {
...keyData,
isDeleted: 'true',
deletedAt: new Date().toISOString(),
deletedBy,
deletedByType, // 'user', 'admin', 'system'
isActive: 'false' // 同时禁用
}
await redis.setApiKey(keyId, updatedData)
// 从哈希映射中移除(这样就不能再使用这个key进行API调用)
if (keyData.apiKey) {
await redis.deleteApiKeyHash(keyData.apiKey)
}
logger.success(`🗑️ Soft deleted API key: ${keyId} by ${deletedBy} (${deletedByType})`)
return { success: true }
} catch (error) {
logger.error('❌ Failed to delete API key:', error)
throw error
}
}
// 🔄 恢复已删除的API Key
async restoreApiKey(keyId, restoredBy = 'system', restoredByType = 'system') {
try {
const keyData = await redis.getApiKey(keyId)
if (!keyData || Object.keys(keyData).length === 0) {
throw new Error('API key not found')
}
// 检查是否确实是已删除的key
if (keyData.isDeleted !== 'true') {
throw new Error('API key is not deleted')
}
// 准备更新的数据
const updatedData = { ...keyData }
updatedData.isActive = 'true'
updatedData.restoredAt = new Date().toISOString()
updatedData.restoredBy = restoredBy
updatedData.restoredByType = restoredByType
// 从更新的数据中移除删除相关的字段
delete updatedData.isDeleted
delete updatedData.deletedAt
delete updatedData.deletedBy
delete updatedData.deletedByType
// 保存更新后的数据
await redis.setApiKey(keyId, updatedData)
// 使用Redis的hdel命令删除不需要的字段
const keyName = `apikey:${keyId}`
await redis.client.hdel(keyName, 'isDeleted', 'deletedAt', 'deletedBy', 'deletedByType')
// 重新建立哈希映射(恢复API Key的使用能力)
if (keyData.apiKey) {
await redis.setApiKeyHash(keyData.apiKey, {
id: keyId,
name: keyData.name,
isActive: 'true'
})
}
logger.success(`✅ Restored API key: ${keyId} by ${restoredBy} (${restoredByType})`)
return { success: true, apiKey: updatedData }
} catch (error) {
logger.error('❌ Failed to restore API key:', error)
throw error
}
}
// 🗑️ 彻底删除API Key(物理删除)
async permanentDeleteApiKey(keyId) {
try {
const keyData = await redis.getApiKey(keyId)
if (!keyData || Object.keys(keyData).length === 0) {
throw new Error('API key not found')
}
// 确保只能彻底删除已经软删除的key
if (keyData.isDeleted !== 'true') {
throw new Error('只能彻底删除已经删除的API Key')
}
// 删除所有相关的使用统计数据
const today = new Date().toISOString().split('T')[0]
const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0]
// 删除每日统计
await redis.client.del(`usage:daily:${today}:${keyId}`)
await redis.client.del(`usage:daily:${yesterday}:${keyId}`)
// 删除月度统计
const currentMonth = today.substring(0, 7)
await redis.client.del(`usage:monthly:${currentMonth}:${keyId}`)
// 删除所有相关的统计键(通过模式匹配)
const usageKeys = await redis.client.keys(`usage:*:${keyId}*`)
if (usageKeys.length > 0) {
await redis.client.del(...usageKeys)
}
// 删除API Key本身
await redis.deleteApiKey(keyId)
logger.success(`🗑️ Permanently deleted API key: ${keyId}`)
return { success: true }
} catch (error) {
logger.error('❌ Failed to permanently delete API key:', error)
throw error
}
}
// 🧹 清空所有已删除的API Keys
async clearAllDeletedApiKeys() {
try {
const allKeys = await this.getAllApiKeys(true)
const deletedKeys = allKeys.filter((key) => key.isDeleted === 'true')
let successCount = 0
let failedCount = 0
const errors = []
for (const key of deletedKeys) {
try {
await this.permanentDeleteApiKey(key.id)
successCount++
} catch (error) {
failedCount++
errors.push({
keyId: key.id,
keyName: key.name,
error: error.message
})
}
}
logger.success(`🧹 Cleared deleted API keys: ${successCount} success, ${failedCount} failed`)
return {
success: true,
total: deletedKeys.length,
successCount,
failedCount,
errors
}
} catch (error) {
logger.error('❌ Failed to clear all deleted API keys:', error)
throw error
}
}
// 📊 记录使用情况(支持缓存token和账户级别统计)
async recordUsage(
keyId,
inputTokens = 0,
outputTokens = 0,
cacheCreateTokens = 0,
cacheReadTokens = 0,
model = 'unknown',
accountId = null
) {
try {
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens
// 计算费用
const CostCalculator = require('../utils/costCalculator')
const costInfo = CostCalculator.calculateCost(
{
input_tokens: inputTokens,
output_tokens: outputTokens,
cache_creation_input_tokens: cacheCreateTokens,
cache_read_input_tokens: cacheReadTokens
},
model
)
// 检查是否为 1M 上下文请求
let isLongContextRequest = false
if (model && model.includes('[1m]')) {
const totalInputTokens = inputTokens + cacheCreateTokens + cacheReadTokens
isLongContextRequest = totalInputTokens > 200000
}
// 记录API Key级别的使用统计
await redis.incrementTokenUsage(
keyId,
totalTokens,
inputTokens,
outputTokens,
cacheCreateTokens,
cacheReadTokens,
model,
0, // ephemeral5mTokens - 暂时为0,后续处理
0, // ephemeral1hTokens - 暂时为0,后续处理
isLongContextRequest
)
// 记录费用统计
if (costInfo.costs.total > 0) {
await redis.incrementDailyCost(keyId, costInfo.costs.total)
logger.database(
`💰 Recorded cost for ${keyId}: $${costInfo.costs.total.toFixed(6)}, model: ${model}`
)
} else {
logger.debug(`💰 No cost recorded for ${keyId} - zero cost for model: ${model}`)
}
// 获取API Key数据以确定关联的账户
const keyData = await redis.getApiKey(keyId)
if (keyData && Object.keys(keyData).length > 0) {
// 更新最后使用时间
keyData.lastUsedAt = new Date().toISOString()
await redis.setApiKey(keyId, keyData)
// 记录账户级别的使用统计(只统计实际处理请求的账户)
if (accountId) {
await redis.incrementAccountUsage(
accountId,
totalTokens,
inputTokens,
outputTokens,
cacheCreateTokens,
cacheReadTokens,
model,
isLongContextRequest
)
logger.database(
`📊 Recorded account usage: ${accountId} - ${totalTokens} tokens (API Key: ${keyId})`
)
} else {
logger.debug(
'⚠️ No accountId provided for usage recording, skipping account-level statistics'
)
}
}
// 记录单次请求的使用详情
const usageCost = costInfo && costInfo.costs ? costInfo.costs.total || 0 : 0
await redis.addUsageRecord(keyId, {
timestamp: new Date().toISOString(),
model,
accountId: accountId || null,
inputTokens,
outputTokens,
cacheCreateTokens,
cacheReadTokens,
totalTokens,
cost: Number(usageCost.toFixed(6)),
costBreakdown: costInfo && costInfo.costs ? costInfo.costs : undefined
})
const logParts = [`Model: ${model}`, `Input: ${inputTokens}`, `Output: ${outputTokens}`]
if (cacheCreateTokens > 0) {
logParts.push(`Cache Create: ${cacheCreateTokens}`)
}
if (cacheReadTokens > 0) {
logParts.push(`Cache Read: ${cacheReadTokens}`)
}
logParts.push(`Total: ${totalTokens} tokens`)
logger.database(`📊 Recorded usage: ${keyId} - ${logParts.join(', ')}`)
} catch (error) {
logger.error('❌ Failed to record usage:', error)
}
}
// 📊 记录 Opus 模型费用(仅限 claude 和 claude-console 账户)
async recordOpusCost(keyId, cost, model, accountType) {
try {
// 判断是否为 Opus 模型
if (!model || !model.toLowerCase().includes('claude-opus')) {
return // 不是 Opus 模型,直接返回
}
// 判断是否为 claude、claude-console 或 ccr 账户
if (
!accountType ||
(accountType !== 'claude' && accountType !== 'claude-console' && accountType !== 'ccr')
) {
logger.debug(`⚠️ Skipping Opus cost recording for non-Claude account type: ${accountType}`)
return // 不是 claude 账户,直接返回
}
// 记录 Opus 周费用
await redis.incrementWeeklyOpusCost(keyId, cost)
logger.database(
`💰 Recorded Opus weekly cost for ${keyId}: $${cost.toFixed(
6
)}, model: ${model}, account type: ${accountType}`
)
} catch (error) {
logger.error('❌ Failed to record Opus cost:', error)
}
}
// 📊 记录使用情况(新版本,支持详细的缓存类型)
async recordUsageWithDetails(
keyId,
usageObject,
model = 'unknown',
accountId = null,
accountType = null
) {
try {
// 提取 token 数量
const inputTokens = usageObject.input_tokens || 0
const outputTokens = usageObject.output_tokens || 0
const cacheCreateTokens = usageObject.cache_creation_input_tokens || 0
const cacheReadTokens = usageObject.cache_read_input_tokens || 0
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens
// 计算费用(支持详细的缓存类型)- 添加错误处理
let costInfo = { totalCost: 0, ephemeral5mCost: 0, ephemeral1hCost: 0 }
try {
const pricingService = require('./pricingService')
// 确保 pricingService 已初始化
if (!pricingService.pricingData) {
logger.warn('⚠️ PricingService not initialized, initializing now...')
await pricingService.initialize()
}
costInfo = pricingService.calculateCost(usageObject, model)
// 验证计算结果
if (!costInfo || typeof costInfo.totalCost !== 'number') {
logger.error(`❌ Invalid cost calculation result for model ${model}:`, costInfo)
// 使用 CostCalculator 作为后备
const CostCalculator = require('../utils/costCalculator')
const fallbackCost = CostCalculator.calculateCost(usageObject, model)
if (fallbackCost && fallbackCost.costs && fallbackCost.costs.total > 0) {
logger.warn(
`⚠️ Using fallback cost calculation for ${model}: $${fallbackCost.costs.total}`
)
costInfo = {
totalCost: fallbackCost.costs.total,
ephemeral5mCost: 0,
ephemeral1hCost: 0
}
} else {
costInfo = { totalCost: 0, ephemeral5mCost: 0, ephemeral1hCost: 0 }
}
}
} catch (pricingError) {
logger.error(`❌ Failed to calculate cost for model ${model}:`, pricingError)
logger.error(` Usage object:`, JSON.stringify(usageObject))
// 使用 CostCalculator 作为后备
try {
const CostCalculator = require('../utils/costCalculator')
const fallbackCost = CostCalculator.calculateCost(usageObject, model)
if (fallbackCost && fallbackCost.costs && fallbackCost.costs.total > 0) {
logger.warn(
`⚠️ Using fallback cost calculation for ${model}: $${fallbackCost.costs.total}`
)
costInfo = {
totalCost: fallbackCost.costs.total,
ephemeral5mCost: 0,
ephemeral1hCost: 0
}
}
} catch (fallbackError) {
logger.error(`❌ Fallback cost calculation also failed:`, fallbackError)
}
}
// 提取详细的缓存创建数据
let ephemeral5mTokens = 0
let ephemeral1hTokens = 0
if (usageObject.cache_creation && typeof usageObject.cache_creation === 'object') {
ephemeral5mTokens = usageObject.cache_creation.ephemeral_5m_input_tokens || 0
ephemeral1hTokens = usageObject.cache_creation.ephemeral_1h_input_tokens || 0
}
// 记录API Key级别的使用统计 - 这个必须执行
await redis.incrementTokenUsage(
keyId,
totalTokens,
inputTokens,
outputTokens,
cacheCreateTokens,
cacheReadTokens,
model,
ephemeral5mTokens, // 传递5分钟缓存 tokens
ephemeral1hTokens, // 传递1小时缓存 tokens
costInfo.isLongContextRequest || false // 传递 1M 上下文请求标记
)
// 记录费用统计
if (costInfo.totalCost > 0) {
await redis.incrementDailyCost(keyId, costInfo.totalCost)
logger.database(
`💰 Recorded cost for ${keyId}: $${costInfo.totalCost.toFixed(6)}, model: ${model}`
)
// 记录 Opus 周费用(如果适用)
await this.recordOpusCost(keyId, costInfo.totalCost, model, accountType)
// 记录详细的缓存费用(如果有)
if (costInfo.ephemeral5mCost > 0 || costInfo.ephemeral1hCost > 0) {
logger.database(
`💰 Cache costs - 5m: $${costInfo.ephemeral5mCost.toFixed(
6
)}, 1h: $${costInfo.ephemeral1hCost.toFixed(6)}`
)
}
} else {
// 如果有 token 使用但费用为 0,记录警告
if (totalTokens > 0) {
logger.warn(
`⚠️ No cost recorded for ${keyId} - zero cost for model: ${model} (tokens: ${totalTokens})`
)
logger.warn(` This may indicate a pricing issue or model not found in pricing data`)
} else {
logger.debug(`💰 No cost recorded for ${keyId} - zero tokens for model: ${model}`)
}
}
// 获取API Key数据以确定关联的账户
const keyData = await redis.getApiKey(keyId)
if (keyData && Object.keys(keyData).length > 0) {
// 更新最后使用时间
keyData.lastUsedAt = new Date().toISOString()
await redis.setApiKey(keyId, keyData)
// 记录账户级别的使用统计(只统计实际处理请求的账户)
if (accountId) {
await redis.incrementAccountUsage(
accountId,
totalTokens,
inputTokens,
outputTokens,
cacheCreateTokens,
cacheReadTokens,
model,
costInfo.isLongContextRequest || false
)
logger.database(
`📊 Recorded account usage: ${accountId} - ${totalTokens} tokens (API Key: ${keyId})`
)
} else {
logger.debug(
'⚠️ No accountId provided for usage recording, skipping account-level statistics'
)
}
}
const usageRecord = {
timestamp: new Date().toISOString(),
model,
accountId: accountId || null,
accountType: accountType || null,
inputTokens,
outputTokens,
cacheCreateTokens,
cacheReadTokens,
ephemeral5mTokens,
ephemeral1hTokens,
totalTokens,
cost: Number((costInfo.totalCost || 0).toFixed(6)),
costBreakdown: {
input: costInfo.inputCost || 0,
output: costInfo.outputCost || 0,
cacheCreate: costInfo.cacheCreateCost || 0,
cacheRead: costInfo.cacheReadCost || 0,
ephemeral5m: costInfo.ephemeral5mCost || 0,
ephemeral1h: costInfo.ephemeral1hCost || 0
},
isLongContext: costInfo.isLongContextRequest || false
}
await redis.addUsageRecord(keyId, usageRecord)
const logParts = [`Model: ${model}`, `Input: ${inputTokens}`, `Output: ${outputTokens}`]
if (cacheCreateTokens > 0) {
logParts.push(`Cache Create: ${cacheCreateTokens}`)
// 如果有详细的缓存创建数据,也记录它们
if (usageObject.cache_creation) {
const { ephemeral_5m_input_tokens, ephemeral_1h_input_tokens } =
usageObject.cache_creation
if (ephemeral_5m_input_tokens > 0) {
logParts.push(`5m: ${ephemeral_5m_input_tokens}`)
}
if (ephemeral_1h_input_tokens > 0) {
logParts.push(`1h: ${ephemeral_1h_input_tokens}`)
}
}
}
if (cacheReadTokens > 0) {
logParts.push(`Cache Read: ${cacheReadTokens}`)
}
logParts.push(`Total: ${totalTokens} tokens`)
logger.database(`📊 Recorded usage: ${keyId} - ${logParts.join(', ')}`)
// 🔔 发布计费事件到消息队列(异步非阻塞)
this._publishBillingEvent({
keyId,
keyName: keyData?.name,
userId: keyData?.userId,
model,
inputTokens,
outputTokens,
cacheCreateTokens,
cacheReadTokens,
ephemeral5mTokens,
ephemeral1hTokens,
totalTokens,
cost: costInfo.totalCost || 0,
costBreakdown: {
input: costInfo.inputCost || 0,
output: costInfo.outputCost || 0,
cacheCreate: costInfo.cacheCreateCost || 0,
cacheRead: costInfo.cacheReadCost || 0,
ephemeral5m: costInfo.ephemeral5mCost || 0,
ephemeral1h: costInfo.ephemeral1hCost || 0
},
accountId,
accountType,
isLongContext: costInfo.isLongContextRequest || false,
requestTimestamp: usageRecord.timestamp
}).catch((err) => {
// 发布失败不影响主流程,只记录错误
logger.warn('⚠️ Failed to publish billing event:', err.message)
})
} catch (error) {
logger.error('❌ Failed to record usage:', error)
}
}
async _fetchAccountInfo(accountId, accountType, cache, client) {
if (!client || !accountId || !accountType) {
return null
}
const cacheKey = `${accountType}:${accountId}`
if (cache.has(cacheKey)) {
return cache.get(cacheKey)
}
const accountConfig = ACCOUNT_TYPE_CONFIG[accountType]
if (!accountConfig) {
cache.set(cacheKey, null)
return null
}
const redisKey = `${accountConfig.prefix}${accountId}`
let accountData = null
try {
accountData = await client.hgetall(redisKey)
} catch (error) {
logger.debug(`加载账号信息失败 ${redisKey}:`, error)
}
if (accountData && Object.keys(accountData).length > 0) {
const displayName =
accountData.name ||
accountData.displayName ||
accountData.email ||
accountData.username ||
accountData.description ||
accountId
const info = { id: accountId, name: displayName }
cache.set(cacheKey, info)
return info
}
cache.set(cacheKey, null)
return null
}
async _resolveAccountByUsageRecord(usageRecord, cache, client) {
if (!usageRecord || !client) {
return null
}
const rawAccountId = usageRecord.accountId || null
const rawAccountType = normalizeAccountTypeKey(usageRecord.accountType)
const modelName = usageRecord.model || usageRecord.actualModel || usageRecord.service || null
if (!rawAccountId && !rawAccountType) {
return null
}
const candidateIds = new Set()
if (rawAccountId) {
candidateIds.add(rawAccountId)
if (typeof rawAccountId === 'string' && rawAccountId.startsWith('responses:')) {
candidateIds.add(rawAccountId.replace(/^responses:/, ''))
}
}
if (candidateIds.size === 0) {
return null
}
const typeCandidates = []
const pushType = (type) => {
const normalized = normalizeAccountTypeKey(type)
if (normalized && ACCOUNT_TYPE_CONFIG[normalized] && !typeCandidates.includes(normalized)) {
typeCandidates.push(normalized)
}
}
pushType(rawAccountType)
if (modelName) {
const lowerModel = modelName.toLowerCase()
if (lowerModel.includes('gpt') || lowerModel.includes('openai')) {
pushType('openai')
pushType('openai-responses')
pushType('azure-openai')
} else if (lowerModel.includes('gemini')) {
pushType('gemini')
} else if (lowerModel.includes('claude') || lowerModel.includes('anthropic')) {
pushType('claude')
pushType('claude-console')
} else if (lowerModel.includes('droid')) {
pushType('droid')
}
}
ACCOUNT_TYPE_PRIORITY.forEach(pushType)
for (const type of typeCandidates) {
const accountConfig = ACCOUNT_TYPE_CONFIG[type]
if (!accountConfig) {
continue
}
for (const candidateId of candidateIds) {
const normalizedId = sanitizeAccountIdForType(candidateId, type)
const accountInfo = await this._fetchAccountInfo(normalizedId, type, cache, client)
if (accountInfo) {
return {
accountId: normalizedId,
accountName: accountInfo.name,
accountType: type,
accountCategory: ACCOUNT_CATEGORY_MAP[type] || 'other',
rawAccountId: rawAccountId || normalizedId
}
}
}
}
return null
}
async _resolveLastUsageAccount(apiKey, usageRecord, cache, client) {
return await this._resolveAccountByUsageRecord(usageRecord, cache, client)
}
// 🔔 发布计费事件(内部方法)
async _publishBillingEvent(eventData) {
try {
const billingEventPublisher = require('./billingEventPublisher')
await billingEventPublisher.publishBillingEvent(eventData)
} catch (error) {
// 静默失败,不影响主流程
logger.debug('Failed to publish billing event:', error.message)
}
}
// 🔐 生成密钥
_generateSecretKey() {
return crypto.randomBytes(32).toString('hex')
}
// 🔒 哈希API Key
_hashApiKey(apiKey) {
return crypto
.createHash('sha256')
.update(apiKey + config.security.encryptionKey)
.digest('hex')
}
// 📈 获取使用统计
async getUsageStats(keyId, options = {}) {
const usageStats = await redis.getUsageStats(keyId)
// options 可能是字符串(兼容旧接口),仅当为对象时才解析
const optionObject =
options && typeof options === 'object' && !Array.isArray(options) ? options : {}
if (optionObject.includeRecords === false) {
return usageStats
}
const recordLimit = optionObject.recordLimit || 20
const recentRecords = await redis.getUsageRecords(keyId, recordLimit)
return {
...usageStats,
recentRecords
}
}
// 📊 获取账户使用统计
async getAccountUsageStats(accountId) {
return await redis.getAccountUsageStats(accountId)
}
// 📈 获取所有账户使用统计
async getAllAccountsUsageStats() {
return await redis.getAllAccountsUsageStats()
}
// === 用户相关方法 ===
// 🔑 创建API Key(支持用户)
async createApiKey(options = {}) {
return await this.generateApiKey(options)
}
// 👤 获取用户的API Keys
async getUserApiKeys(userId, includeDeleted = false) {
try {
const allKeys = await redis.getAllApiKeys()
let userKeys = allKeys.filter((key) => key.userId === userId)
// 默认过滤掉已删除的API Keys
if (!includeDeleted) {
userKeys = userKeys.filter((key) => key.isDeleted !== 'true')
}
// Populate usage stats for each user's API key (same as getAllApiKeys does)
const userKeysWithUsage = []
for (const key of userKeys) {
const usage = await redis.getUsageStats(key.id)
const dailyCost = (await redis.getDailyCost(key.id)) || 0
const costStats = await redis.getCostStats(key.id)
userKeysWithUsage.push({
id: key.id,
name: key.name,
description: key.description,
key: key.apiKey ? `${this.prefix}****${key.apiKey.slice(-4)}` : null, // 只显示前缀和后4位
tokenLimit: parseInt(key.tokenLimit || 0),
isActive: key.isActive === 'true',
createdAt: key.createdAt,
lastUsedAt: key.lastUsedAt,
expiresAt: key.expiresAt,
usage,
dailyCost,
totalCost: costStats.total,
dailyCostLimit: parseFloat(key.dailyCostLimit || 0),
totalCostLimit: parseFloat(key.totalCostLimit || 0),
userId: key.userId,
userUsername: key.userUsername,
createdBy: key.createdBy,
droidAccountId: key.droidAccountId,
// Include deletion fields for deleted keys
isDeleted: key.isDeleted,
deletedAt: key.deletedAt,
deletedBy: key.deletedBy,
deletedByType: key.deletedByType
})
}
return userKeysWithUsage
} catch (error) {
logger.error('❌ Failed to get user API keys:', error)
return []
}
}
// 🔍 通过ID获取API Key(检查权限)
async getApiKeyById(keyId, userId = null) {
try {
const keyData = await redis.getApiKey(keyId)
if (!keyData) {
return null
}
// 如果指定了用户ID,检查权限
if (userId && keyData.userId !== userId) {
return null
}
return {
id: keyData.id,
name: keyData.name,
description: keyData.description,
key: keyData.apiKey,
tokenLimit: parseInt(keyData.tokenLimit || 0),
isActive: keyData.isActive === 'true',
createdAt: keyData.createdAt,
lastUsedAt: keyData.lastUsedAt,
expiresAt: keyData.expiresAt,
userId: keyData.userId,
userUsername: keyData.userUsername,
createdBy: keyData.createdBy,
permissions: keyData.permissions,
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
totalCostLimit: parseFloat(keyData.totalCostLimit || 0),
droidAccountId: keyData.droidAccountId
}
} catch (error) {
logger.error('❌ Failed to get API key by ID:', error)
return null
}
}
// 🔄 重新生成API Key
async regenerateApiKey(keyId) {
try {
const existingKey = await redis.getApiKey(keyId)
if (!existingKey) {
throw new Error('API key not found')
}
// 生成新的key
const newApiKey = `${this.prefix}${this._generateSecretKey()}`
const newHashedKey = this._hashApiKey(newApiKey)
// 删除旧的哈希映射
const oldHashedKey = existingKey.apiKey
await redis.deleteApiKeyHash(oldHashedKey)
// 更新key数据
const updatedKeyData = {
...existingKey,
apiKey: newHashedKey,
updatedAt: new Date().toISOString()
}
// 保存新数据并建立新的哈希映射
await redis.setApiKey(keyId, updatedKeyData, newHashedKey)
logger.info(`🔄 Regenerated API key: ${existingKey.name} (${keyId})`)
return {
id: keyId,
name: existingKey.name,
key: newApiKey, // 返回完整的新key
updatedAt: updatedKeyData.updatedAt
}
} catch (error) {
logger.error('❌ Failed to regenerate API key:', error)
throw error
}
}
// 🗑️ 硬删除API Key (完全移除)
async hardDeleteApiKey(keyId) {
try {
const keyData = await redis.getApiKey(keyId)
if (!keyData) {
throw new Error('API key not found')
}
// 删除key数据和哈希映射
await redis.deleteApiKey(keyId)
await redis.deleteApiKeyHash(keyData.apiKey)
logger.info(`🗑️ Deleted API key: ${keyData.name} (${keyId})`)
return true
} catch (error) {
logger.error('❌ Failed to delete API key:', error)
throw error
}
}
// 🚫 禁用用户的所有API Keys
async disableUserApiKeys(userId) {
try {
const userKeys = await this.getUserApiKeys(userId)
let disabledCount = 0
for (const key of userKeys) {
if (key.isActive) {
await this.updateApiKey(key.id, { isActive: false })
disabledCount++
}
}
logger.info(`🚫 Disabled ${disabledCount} API keys for user: ${userId}`)
return { count: disabledCount }
} catch (error) {
logger.error('❌ Failed to disable user API keys:', error)
throw error
}
}
// 📊 获取聚合使用统计(支持多个API Key)
async getAggregatedUsageStats(keyIds, options = {}) {
try {
if (!Array.isArray(keyIds)) {
keyIds = [keyIds]
}
const { period: _period = 'week', model: _model } = options
const stats = {
totalRequests: 0,
totalInputTokens: 0,
totalOutputTokens: 0,
totalCost: 0,
dailyStats: [],
modelStats: []
}
// 汇总所有API Key的统计数据
for (const keyId of keyIds) {
const keyStats = await redis.getUsageStats(keyId)
const costStats = await redis.getCostStats(keyId)
if (keyStats && keyStats.total) {
stats.totalRequests += keyStats.total.requests || 0
stats.totalInputTokens += keyStats.total.inputTokens || 0
stats.totalOutputTokens += keyStats.total.outputTokens || 0
stats.totalCost += costStats?.total || 0
}
}
// TODO: 实现日期范围和模型统计
// 这里可以根据需要添加更详细的统计逻辑
return stats
} catch (error) {
logger.error('❌ Failed to get usage stats:', error)
return {
totalRequests: 0,
totalInputTokens: 0,
totalOutputTokens: 0,
totalCost: 0,
dailyStats: [],
modelStats: []
}
}
}
// 🔓 解绑账号从所有API Keys
async unbindAccountFromAllKeys(accountId, accountType) {
try {
// 账号类型与字段的映射关系
const fieldMap = {
claude: 'claudeAccountId',
'claude-console': 'claudeConsoleAccountId',
gemini: 'geminiAccountId',
openai: 'openaiAccountId',
'openai-responses': 'openaiAccountId', // 特殊处理,带 responses: 前缀
azure_openai: 'azureOpenaiAccountId',
bedrock: 'bedrockAccountId',
droid: 'droidAccountId',
ccr: null // CCR 账号没有对应的 API Key 字段
}
const field = fieldMap[accountType]
if (!field) {
logger.info(`账号类型 ${accountType} 不需要解绑 API Key`)
return 0
}
// 获取所有API Keys
const allKeys = await this.getAllApiKeys()
// 筛选绑定到此账号的 API Keys
let boundKeys = []
if (accountType === 'openai-responses') {
// OpenAI-Responses 特殊处理:查找 openaiAccountId 字段中带 responses: 前缀的
boundKeys = allKeys.filter((key) => key.openaiAccountId === `responses:${accountId}`)
} else {
// 其他账号类型正常匹配
boundKeys = allKeys.filter((key) => key[field] === accountId)
}
// 批量解绑
for (const key of boundKeys) {
const updates = {}
if (accountType === 'openai-responses') {
updates.openaiAccountId = null
} else if (accountType === 'claude-console') {
updates.claudeConsoleAccountId = null
} else {
updates[field] = null
}
await this.updateApiKey(key.id, updates)
logger.info(
`✅ 自动解绑 API Key ${key.id} (${key.name}) 从 ${accountType} 账号 ${accountId}`
)
}
if (boundKeys.length > 0) {
logger.success(
`🔓 成功解绑 ${boundKeys.length} 个 API Key 从 ${accountType} 账号 ${accountId}`
)
}
return boundKeys.length
} catch (error) {
logger.error(`❌ 解绑 API Keys 失败 (${accountType} 账号 ${accountId}):`, error)
return 0
}
}
// 🧹 清理过期的API Keys
async cleanupExpiredKeys() {
try {
const apiKeys = await redis.getAllApiKeys()
const now = new Date()
let cleanedCount = 0
for (const key of apiKeys) {
// 检查是否已过期且仍处于激活状态
if (key.expiresAt && new Date(key.expiresAt) < now && key.isActive === 'true') {
// 将过期的 API Key 标记为禁用状态,而不是直接删除
await this.updateApiKey(key.id, { isActive: false })
logger.info(`🔒 API Key ${key.id} (${key.name}) has expired and been disabled`)
cleanedCount++
}
}
if (cleanedCount > 0) {
logger.success(`🧹 Disabled ${cleanedCount} expired API keys`)
}
return cleanedCount
} catch (error) {
logger.error('❌ Failed to cleanup expired keys:', error)
return 0
}
}
}
// 导出实例和单独的方法
const apiKeyService = new ApiKeyService()
// 为了方便其他服务调用,导出 recordUsage 方法
apiKeyService.recordUsageMetrics = apiKeyService.recordUsage.bind(apiKeyService)
module.exports = apiKeyService