mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 19:09:15 +00:00
Revert: 撤销 584fa8c 之后的所有提交
This commit is contained in:
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
|||||||
* text=auto eol=lf
|
|
||||||
@@ -41,9 +41,7 @@ async function migrate() {
|
|||||||
stats.dailyIndex++
|
stats.dailyIndex++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (keys.length > 0) {
|
if (keys.length > 0) await pipeline.exec()
|
||||||
await pipeline.exec()
|
|
||||||
}
|
|
||||||
} while (cursor !== '0')
|
} while (cursor !== '0')
|
||||||
console.log(` 已处理 ${stats.dailyIndex} 条`)
|
console.log(` 已处理 ${stats.dailyIndex} 条`)
|
||||||
|
|
||||||
@@ -65,9 +63,7 @@ async function migrate() {
|
|||||||
stats.hourlyIndex++
|
stats.hourlyIndex++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (keys.length > 0) {
|
if (keys.length > 0) await pipeline.exec()
|
||||||
await pipeline.exec()
|
|
||||||
}
|
|
||||||
} while (cursor !== '0')
|
} while (cursor !== '0')
|
||||||
console.log(` 已处理 ${stats.hourlyIndex} 条`)
|
console.log(` 已处理 ${stats.hourlyIndex} 条`)
|
||||||
|
|
||||||
@@ -89,9 +85,7 @@ async function migrate() {
|
|||||||
stats.modelDailyIndex++
|
stats.modelDailyIndex++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (keys.length > 0) {
|
if (keys.length > 0) await pipeline.exec()
|
||||||
await pipeline.exec()
|
|
||||||
}
|
|
||||||
} while (cursor !== '0')
|
} while (cursor !== '0')
|
||||||
console.log(` 已处理 ${stats.modelDailyIndex} 条`)
|
console.log(` 已处理 ${stats.modelDailyIndex} 条`)
|
||||||
|
|
||||||
@@ -99,13 +93,7 @@ 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(
|
const [newCursor, keys] = await redis.scan(cursor, 'MATCH', 'usage:model:hourly:*', 'COUNT', 500)
|
||||||
cursor,
|
|
||||||
'MATCH',
|
|
||||||
'usage:model:hourly:*',
|
|
||||||
'COUNT',
|
|
||||||
500
|
|
||||||
)
|
|
||||||
cursor = newCursor
|
cursor = newCursor
|
||||||
|
|
||||||
const pipeline = redis.pipeline()
|
const pipeline = redis.pipeline()
|
||||||
@@ -119,9 +107,7 @@ async function migrate() {
|
|||||||
stats.modelHourlyIndex++
|
stats.modelHourlyIndex++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (keys.length > 0) {
|
if (keys.length > 0) await pipeline.exec()
|
||||||
await pipeline.exec()
|
|
||||||
}
|
|
||||||
} while (cursor !== '0')
|
} while (cursor !== '0')
|
||||||
console.log(` 已处理 ${stats.modelHourlyIndex} 条`)
|
console.log(` 已处理 ${stats.modelHourlyIndex} 条`)
|
||||||
|
|
||||||
|
|||||||
@@ -155,9 +155,7 @@ class RedisClient {
|
|||||||
stats.daily++
|
stats.daily++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (keys.length > 0) {
|
if (keys.length > 0) await pipeline.exec()
|
||||||
await pipeline.exec()
|
|
||||||
}
|
|
||||||
} while (cursor !== '0')
|
} while (cursor !== '0')
|
||||||
|
|
||||||
// 迁移 usage:hourly
|
// 迁移 usage:hourly
|
||||||
@@ -180,9 +178,7 @@ class RedisClient {
|
|||||||
stats.hourly++
|
stats.hourly++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (keys.length > 0) {
|
if (keys.length > 0) await pipeline.exec()
|
||||||
await pipeline.exec()
|
|
||||||
}
|
|
||||||
} while (cursor !== '0')
|
} while (cursor !== '0')
|
||||||
|
|
||||||
// 迁移 usage:model:daily
|
// 迁移 usage:model:daily
|
||||||
@@ -205,9 +201,7 @@ class RedisClient {
|
|||||||
stats.modelDaily++
|
stats.modelDaily++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (keys.length > 0) {
|
if (keys.length > 0) await pipeline.exec()
|
||||||
await pipeline.exec()
|
|
||||||
}
|
|
||||||
} while (cursor !== '0')
|
} while (cursor !== '0')
|
||||||
|
|
||||||
// 迁移 usage:model:hourly
|
// 迁移 usage:model:hourly
|
||||||
@@ -230,9 +224,7 @@ class RedisClient {
|
|||||||
stats.modelHourly++
|
stats.modelHourly++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (keys.length > 0) {
|
if (keys.length > 0) await pipeline.exec()
|
||||||
await pipeline.exec()
|
|
||||||
}
|
|
||||||
} while (cursor !== '0')
|
} while (cursor !== '0')
|
||||||
|
|
||||||
// 迁移 usage:keymodel:daily (usage:{keyId}:model:daily:{model}:{date})
|
// 迁移 usage:keymodel:daily (usage:{keyId}:model:daily:{model}:{date})
|
||||||
@@ -257,9 +249,7 @@ class RedisClient {
|
|||||||
stats.keymodelDaily = (stats.keymodelDaily || 0) + 1
|
stats.keymodelDaily = (stats.keymodelDaily || 0) + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (keys.length > 0) {
|
if (keys.length > 0) await pipeline.exec()
|
||||||
await pipeline.exec()
|
|
||||||
}
|
|
||||||
} while (cursor !== '0')
|
} while (cursor !== '0')
|
||||||
|
|
||||||
// 迁移 usage:keymodel:hourly (usage:{keyId}:model:hourly:{model}:{hour})
|
// 迁移 usage:keymodel:hourly (usage:{keyId}:model:hourly:{model}:{hour})
|
||||||
@@ -284,9 +274,7 @@ class RedisClient {
|
|||||||
stats.keymodelHourly = (stats.keymodelHourly || 0) + 1
|
stats.keymodelHourly = (stats.keymodelHourly || 0) + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (keys.length > 0) {
|
if (keys.length > 0) await pipeline.exec()
|
||||||
await pipeline.exec()
|
|
||||||
}
|
|
||||||
} while (cursor !== '0')
|
} while (cursor !== '0')
|
||||||
|
|
||||||
// 标记迁移完成
|
// 标记迁移完成
|
||||||
@@ -398,13 +386,9 @@ class RedisClient {
|
|||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
// 只接受 apikey:<uuid> 形态,排除索引 key
|
// 只接受 apikey:<uuid> 形态,排除索引 key
|
||||||
if (excludePrefixes.some((prefix) => key.startsWith(prefix))) {
|
if (excludePrefixes.some((prefix) => key.startsWith(prefix))) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
// 确保是 apikey:<id> 格式(只有一个冒号)
|
// 确保是 apikey:<id> 格式(只有一个冒号)
|
||||||
if (key.split(':').length !== 2) {
|
if (key.split(':').length !== 2) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
keyIds.add(key.replace('apikey:', ''))
|
keyIds.add(key.replace('apikey:', ''))
|
||||||
}
|
}
|
||||||
} while (cursor !== '0')
|
} while (cursor !== '0')
|
||||||
@@ -464,22 +448,14 @@ class RedisClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const results = await pipeline.exec()
|
const results = await pipeline.exec()
|
||||||
if (!results) {
|
if (!results) return []
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
if (!result) {
|
if (!result) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
const [err, values] = result
|
const [err, values] = result
|
||||||
if (err || !values) {
|
if (err || !values) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
const [tags, isDeleted] = values
|
const [tags, isDeleted] = values
|
||||||
if (isDeleted === 'true' || !tags) {
|
if (isDeleted === 'true' || !tags) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(tags)
|
const parsed = JSON.parse(tags)
|
||||||
@@ -506,9 +482,7 @@ class RedisClient {
|
|||||||
cursor = newCursor
|
cursor = newCursor
|
||||||
|
|
||||||
const validKeys = keys.filter((k) => k !== 'apikey:hash_map' && k.split(':').length === 2)
|
const validKeys = keys.filter((k) => k !== 'apikey:hash_map' && k.split(':').length === 2)
|
||||||
if (validKeys.length === 0) {
|
if (validKeys.length === 0) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const pipeline = this.client.pipeline()
|
const pipeline = this.client.pipeline()
|
||||||
for (const key of validKeys) {
|
for (const key of validKeys) {
|
||||||
@@ -516,22 +490,14 @@ class RedisClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const results = await pipeline.exec()
|
const results = await pipeline.exec()
|
||||||
if (!results) {
|
if (!results) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
if (!result) {
|
if (!result) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
const [err, values] = result
|
const [err, values] = result
|
||||||
if (err || !values) {
|
if (err || !values) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
const [tags, isDeleted] = values
|
const [tags, isDeleted] = values
|
||||||
if (isDeleted === 'true' || !tags) {
|
if (isDeleted === 'true' || !tags) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(tags)
|
const parsed = JSON.parse(tags)
|
||||||
@@ -1606,15 +1572,11 @@ class RedisClient {
|
|||||||
const entriesByAccount = new Map()
|
const entriesByAccount = new Map()
|
||||||
for (const entry of allEntries) {
|
for (const entry of allEntries) {
|
||||||
const colonIndex = entry.indexOf(':')
|
const colonIndex = entry.indexOf(':')
|
||||||
if (colonIndex === -1) {
|
if (colonIndex === -1) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
const accountId = entry.substring(0, colonIndex)
|
const accountId = entry.substring(0, colonIndex)
|
||||||
const model = entry.substring(colonIndex + 1)
|
const model = entry.substring(colonIndex + 1)
|
||||||
if (accountIdSet.has(accountId)) {
|
if (accountIdSet.has(accountId)) {
|
||||||
if (!entriesByAccount.has(accountId)) {
|
if (!entriesByAccount.has(accountId)) entriesByAccount.set(accountId, [])
|
||||||
entriesByAccount.set(accountId, [])
|
|
||||||
}
|
|
||||||
entriesByAccount.get(accountId).push(model)
|
entriesByAccount.get(accountId).push(model)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1690,9 +1652,7 @@ class RedisClient {
|
|||||||
for (let i = 0; i < modelKeys.length; i++) {
|
for (let i = 0; i < modelKeys.length; i++) {
|
||||||
const key = modelKeys[i]
|
const key = modelKeys[i]
|
||||||
const [err, modelUsage] = results[i]
|
const [err, modelUsage] = results[i]
|
||||||
if (err || !modelUsage) {
|
if (err || !modelUsage) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = key.split(':')
|
const parts = key.split(':')
|
||||||
const model = parts[4]
|
const model = parts[4]
|
||||||
@@ -1937,9 +1897,7 @@ class RedisClient {
|
|||||||
'claude:account:*',
|
'claude:account:*',
|
||||||
/^claude:account:(.+)$/
|
/^claude:account:(.+)$/
|
||||||
)
|
)
|
||||||
if (accountIds.length === 0) {
|
if (accountIds.length === 0) return []
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = accountIds.map((id) => `claude:account:${id}`)
|
const keys = accountIds.map((id) => `claude:account:${id}`)
|
||||||
const pipeline = this.client.pipeline()
|
const pipeline = this.client.pipeline()
|
||||||
@@ -1980,9 +1938,7 @@ class RedisClient {
|
|||||||
'droid:account:*',
|
'droid:account:*',
|
||||||
/^droid:account:(.+)$/
|
/^droid:account:(.+)$/
|
||||||
)
|
)
|
||||||
if (accountIds.length === 0) {
|
if (accountIds.length === 0) return []
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = accountIds.map((id) => `droid:account:${id}`)
|
const keys = accountIds.map((id) => `droid:account:${id}`)
|
||||||
const pipeline = this.client.pipeline()
|
const pipeline = this.client.pipeline()
|
||||||
@@ -2027,9 +1983,7 @@ class RedisClient {
|
|||||||
'openai:account:*',
|
'openai:account:*',
|
||||||
/^openai:account:(.+)$/
|
/^openai:account:(.+)$/
|
||||||
)
|
)
|
||||||
if (accountIds.length === 0) {
|
if (accountIds.length === 0) return []
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = accountIds.map((id) => `openai:account:${id}`)
|
const keys = accountIds.map((id) => `openai:account:${id}`)
|
||||||
const pipeline = this.client.pipeline()
|
const pipeline = this.client.pipeline()
|
||||||
@@ -2148,18 +2102,14 @@ class RedisClient {
|
|||||||
// 🔍 通过索引获取 key 列表(替代 SCAN)
|
// 🔍 通过索引获取 key 列表(替代 SCAN)
|
||||||
async getKeysByIndex(indexKey, keyPattern) {
|
async getKeysByIndex(indexKey, keyPattern) {
|
||||||
const members = await this.client.smembers(indexKey)
|
const members = await this.client.smembers(indexKey)
|
||||||
if (!members || members.length === 0) {
|
if (!members || members.length === 0) return []
|
||||||
return []
|
|
||||||
}
|
|
||||||
return members.map((id) => keyPattern.replace('{id}', id))
|
return members.map((id) => keyPattern.replace('{id}', id))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔍 批量通过索引获取数据
|
// 🔍 批量通过索引获取数据
|
||||||
async getDataByIndex(indexKey, keyPattern) {
|
async getDataByIndex(indexKey, keyPattern) {
|
||||||
const keys = await this.getKeysByIndex(indexKey, keyPattern)
|
const keys = await this.getKeysByIndex(indexKey, keyPattern)
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) return []
|
||||||
return []
|
|
||||||
}
|
|
||||||
return await this.batchHgetallChunked(keys)
|
return await this.batchHgetallChunked(keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4308,12 +4258,8 @@ redisClient.batchGetApiKeyStats = async function (keyIds) {
|
|||||||
* @returns {Promise<Object[]>} 每个 key 对应的数据,失败的返回 null
|
* @returns {Promise<Object[]>} 每个 key 对应的数据,失败的返回 null
|
||||||
*/
|
*/
|
||||||
redisClient.batchHgetallChunked = async function (keys, chunkSize = 500) {
|
redisClient.batchHgetallChunked = async function (keys, chunkSize = 500) {
|
||||||
if (!keys || keys.length === 0) {
|
if (!keys || keys.length === 0) return []
|
||||||
return []
|
if (keys.length <= chunkSize) return this.batchHgetall(keys)
|
||||||
}
|
|
||||||
if (keys.length <= chunkSize) {
|
|
||||||
return this.batchHgetall(keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = []
|
const results = []
|
||||||
for (let i = 0; i < keys.length; i += chunkSize) {
|
for (let i = 0; i < keys.length; i += chunkSize) {
|
||||||
@@ -4331,9 +4277,7 @@ redisClient.batchHgetallChunked = async function (keys, chunkSize = 500) {
|
|||||||
* @returns {Promise<(string|null)[]>} 每个 key 对应的值
|
* @returns {Promise<(string|null)[]>} 每个 key 对应的值
|
||||||
*/
|
*/
|
||||||
redisClient.batchGetChunked = async function (keys, chunkSize = 500) {
|
redisClient.batchGetChunked = async function (keys, chunkSize = 500) {
|
||||||
if (!keys || keys.length === 0) {
|
if (!keys || keys.length === 0) return []
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = this.getClientSafe()
|
const client = this.getClientSafe()
|
||||||
if (keys.length <= chunkSize) {
|
if (keys.length <= chunkSize) {
|
||||||
@@ -4372,15 +4316,11 @@ redisClient.scanAndProcess = async function (pattern, processor, options = {}) {
|
|||||||
const processedKeys = new Set() // 全程去重
|
const processedKeys = new Set() // 全程去重
|
||||||
|
|
||||||
const processBatch = async (keys) => {
|
const processBatch = async (keys) => {
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 过滤已处理的 key
|
// 过滤已处理的 key
|
||||||
const uniqueKeys = keys.filter((k) => !processedKeys.has(k))
|
const uniqueKeys = keys.filter((k) => !processedKeys.has(k))
|
||||||
if (uniqueKeys.length === 0) {
|
if (uniqueKeys.length === 0) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
uniqueKeys.forEach((k) => processedKeys.add(k))
|
uniqueKeys.forEach((k) => processedKeys.add(k))
|
||||||
|
|
||||||
@@ -4447,9 +4387,7 @@ redisClient.scanAndGetAllChunked = async function (pattern, options = {}) {
|
|||||||
* @returns {Promise<number>} 删除的 key 数量
|
* @returns {Promise<number>} 删除的 key 数量
|
||||||
*/
|
*/
|
||||||
redisClient.batchDelChunked = async function (keys, chunkSize = 500) {
|
redisClient.batchDelChunked = async function (keys, chunkSize = 500) {
|
||||||
if (!keys || keys.length === 0) {
|
if (!keys || keys.length === 0) return 0
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = this.getClientSafe()
|
const client = this.getClientSafe()
|
||||||
let deleted = 0
|
let deleted = 0
|
||||||
|
|||||||
@@ -79,8 +79,7 @@ router.get('/api-keys/:keyId/cost-debug', authenticateAdmin, async (req, res) =>
|
|||||||
const costStats = await redis.getCostStats(keyId)
|
const costStats = await redis.getCostStats(keyId)
|
||||||
const dailyCost = await redis.getDailyCost(keyId)
|
const dailyCost = await redis.getDailyCost(keyId)
|
||||||
const today = redis.getDateStringInTimezone()
|
const today = redis.getDateStringInTimezone()
|
||||||
// eslint-disable-next-line no-unused-vars
|
const client = redis.getClientSafe()
|
||||||
const _client = redis.getClientSafe()
|
|
||||||
|
|
||||||
// 获取所有相关的Redis键
|
// 获取所有相关的Redis键
|
||||||
const costKeys = await redis.scanKeys(`usage:cost:*:${keyId}:*`)
|
const costKeys = await redis.scanKeys(`usage:cost:*:${keyId}:*`)
|
||||||
@@ -290,18 +289,20 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 为每个API Key添加owner的displayName(批量获取优化)
|
// 为每个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()
|
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) => userService.getUserById(id, false).catch(() => null))
|
userIdsToFetch.map((id) =>
|
||||||
|
userService.getUserById(id, false).catch(() => null)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
userIdsToFetch.forEach((id, i) => {
|
userIdsToFetch.forEach((id, i) => {
|
||||||
if (users[i]) {
|
if (users[i]) userMap.set(id, users[i])
|
||||||
userMap.set(id, users[i])
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ const bedrockAccountService = require('../../services/bedrockAccountService')
|
|||||||
const ccrAccountService = require('../../services/ccrAccountService')
|
const ccrAccountService = require('../../services/ccrAccountService')
|
||||||
const geminiAccountService = require('../../services/geminiAccountService')
|
const geminiAccountService = require('../../services/geminiAccountService')
|
||||||
const droidAccountService = require('../../services/droidAccountService')
|
const droidAccountService = require('../../services/droidAccountService')
|
||||||
// const openaiAccountService = require('../../services/openaiAccountService') // TODO: 未来用于OpenAI账户统计
|
const openaiAccountService = require('../../services/openaiAccountService')
|
||||||
const openaiResponsesAccountService = require('../../services/openaiResponsesAccountService')
|
const openaiResponsesAccountService = require('../../services/openaiResponsesAccountService')
|
||||||
const redis = require('../../models/redis')
|
const redis = require('../../models/redis')
|
||||||
const { authenticateAdmin } = require('../../middleware/auth')
|
const { authenticateAdmin } = require('../../middleware/auth')
|
||||||
const logger = require('../../utils/logger')
|
const logger = require('../../utils/logger')
|
||||||
const CostCalculator = require('../../utils/costCalculator')
|
const CostCalculator = require('../../utils/costCalculator')
|
||||||
// const pricingService = require('../../services/pricingService') // TODO: 未来用于成本计算
|
const pricingService = require('../../services/pricingService')
|
||||||
const config = require('../../../config/config')
|
const config = require('../../../config/config')
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
@@ -144,9 +144,7 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
|||||||
totalCacheReadTokensUsed += usage.cacheReadTokens || 0
|
totalCacheReadTokensUsed += usage.cacheReadTokens || 0
|
||||||
totalAllTokensUsed += usage.allTokens || 0
|
totalAllTokensUsed += usage.allTokens || 0
|
||||||
}
|
}
|
||||||
if (key.isActive) {
|
if (key.isActive) activeApiKeys++
|
||||||
activeApiKeys++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 各平台账户统计(单次遍历)
|
// 各平台账户统计(单次遍历)
|
||||||
|
|||||||
@@ -157,9 +157,7 @@ router.get('/droid-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
const groupToAccountIds = new Map()
|
const groupToAccountIds = new Map()
|
||||||
for (const [accountId, groups] of allGroupInfosMap) {
|
for (const [accountId, groups] of allGroupInfosMap) {
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
if (!groupToAccountIds.has(group.id)) {
|
if (!groupToAccountIds.has(group.id)) groupToAccountIds.set(group.id, [])
|
||||||
groupToAccountIds.set(group.id, [])
|
|
||||||
}
|
|
||||||
groupToAccountIds.get(group.id).push(accountId)
|
groupToAccountIds.get(group.id).push(accountId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,9 +167,7 @@ router.get('/droid-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
const groupBindingCount = new Map()
|
const groupBindingCount = new Map()
|
||||||
for (const key of allApiKeys) {
|
for (const key of allApiKeys) {
|
||||||
const binding = key.droidAccountId
|
const binding = key.droidAccountId
|
||||||
if (!binding) {
|
if (!binding) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (binding.startsWith('group:')) {
|
if (binding.startsWith('group:')) {
|
||||||
const groupId = binding.substring('group:'.length)
|
const groupId = binding.substring('group:'.length)
|
||||||
groupBindingCount.set(groupId, (groupBindingCount.get(groupId) || 0) + 1)
|
groupBindingCount.set(groupId, (groupBindingCount.get(groupId) || 0) + 1)
|
||||||
|
|||||||
@@ -46,9 +46,7 @@ router.get('/gemini-api-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
const bindingCountMap = new Map()
|
const bindingCountMap = new Map()
|
||||||
for (const key of allApiKeys) {
|
for (const key of allApiKeys) {
|
||||||
const binding = key.geminiAccountId
|
const binding = key.geminiAccountId
|
||||||
if (!binding) {
|
if (!binding) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
// 处理 api: 前缀
|
// 处理 api: 前缀
|
||||||
const accountId = binding.startsWith('api:') ? binding.substring(4) : binding
|
const accountId = binding.startsWith('api:') ? binding.substring(4) : binding
|
||||||
bindingCountMap.set(accountId, (bindingCountMap.get(accountId) || 0) + 1)
|
bindingCountMap.set(accountId, (bindingCountMap.get(accountId) || 0) + 1)
|
||||||
|
|||||||
@@ -54,9 +54,7 @@ router.get('/openai-responses-accounts', authenticateAdmin, async (req, res) =>
|
|||||||
const bindingCountMap = new Map()
|
const bindingCountMap = new Map()
|
||||||
for (const key of allApiKeys) {
|
for (const key of allApiKeys) {
|
||||||
const binding = key.openaiAccountId
|
const binding = key.openaiAccountId
|
||||||
if (!binding) {
|
if (!binding) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
// 处理 responses: 前缀
|
// 处理 responses: 前缀
|
||||||
const accountId = binding.startsWith('responses:') ? binding.substring(10) : binding
|
const accountId = binding.startsWith('responses:') ? binding.substring(10) : binding
|
||||||
bindingCountMap.set(accountId, (bindingCountMap.get(accountId) || 0) + 1)
|
bindingCountMap.set(accountId, (bindingCountMap.get(accountId) || 0) + 1)
|
||||||
|
|||||||
@@ -64,18 +64,14 @@ async function getUsageDataByIndex(indexKey, keyPattern, scanPattern) {
|
|||||||
const match =
|
const match =
|
||||||
k.match(/usage:([^:]+):model:daily:(.+):\d{4}-\d{2}-\d{2}$/) ||
|
k.match(/usage:([^:]+):model:daily:(.+):\d{4}-\d{2}-\d{2}$/) ||
|
||||||
k.match(/usage:([^:]+):model:hourly:(.+):\d{4}-\d{2}-\d{2}:\d{2}$/)
|
k.match(/usage:([^:]+):model:hourly:(.+):\d{4}-\d{2}-\d{2}:\d{2}$/)
|
||||||
if (match) {
|
if (match) return `${match[1]}:${match[2]}`
|
||||||
return `${match[1]}:${match[2]}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (keyPattern.includes('{accountId}') && keyPattern.includes('{model}')) {
|
if (keyPattern.includes('{accountId}') && keyPattern.includes('{model}')) {
|
||||||
// account_usage:model:daily 或 hourly
|
// account_usage:model:daily 或 hourly
|
||||||
const match =
|
const match =
|
||||||
k.match(/account_usage:model:daily:([^:]+):(.+):\d{4}-\d{2}-\d{2}$/) ||
|
k.match(/account_usage:model:daily:([^:]+):(.+):\d{4}-\d{2}-\d{2}$/) ||
|
||||||
k.match(/account_usage:model:hourly:([^:]+):(.+):\d{4}-\d{2}-\d{2}:\d{2}$/)
|
k.match(/account_usage:model:hourly:([^:]+):(.+):\d{4}-\d{2}-\d{2}:\d{2}$/)
|
||||||
if (match) {
|
if (match) return `${match[1]}:${match[2]}`
|
||||||
return `${match[1]}:${match[2]}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 通用格式:提取最后一个 : 前的 id
|
// 通用格式:提取最后一个 : 前的 id
|
||||||
const parts = k.split(':')
|
const parts = k.split(':')
|
||||||
@@ -541,7 +537,7 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用索引获取数据,按小时批量查询
|
// 使用索引获取数据,按小时批量查询
|
||||||
const _dates = [...dateSet]
|
const dates = [...dateSet]
|
||||||
const modelDataMap = new Map()
|
const modelDataMap = new Map()
|
||||||
const usageDataMap = new Map()
|
const usageDataMap = new Map()
|
||||||
|
|
||||||
@@ -575,9 +571,7 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const match = key.match(/usage:model:hourly:.+?:(\d{4}-\d{2}-\d{2}:\d{2})/)
|
const match = key.match(/usage:model:hourly:.+?:(\d{4}-\d{2}-\d{2}:\d{2})/)
|
||||||
if (match) {
|
if (match) {
|
||||||
const hourKey = match[1]
|
const hourKey = match[1]
|
||||||
if (!modelKeysByHour.has(hourKey)) {
|
if (!modelKeysByHour.has(hourKey)) modelKeysByHour.set(hourKey, [])
|
||||||
modelKeysByHour.set(hourKey, [])
|
|
||||||
}
|
|
||||||
modelKeysByHour.get(hourKey).push(key)
|
modelKeysByHour.get(hourKey).push(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -585,9 +579,7 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const match = key.match(/usage:hourly:.+?:(\d{4}-\d{2}-\d{2}:\d{2})/)
|
const match = key.match(/usage:hourly:.+?:(\d{4}-\d{2}-\d{2}:\d{2})/)
|
||||||
if (match) {
|
if (match) {
|
||||||
const hourKey = match[1]
|
const hourKey = match[1]
|
||||||
if (!usageKeysByHour.has(hourKey)) {
|
if (!usageKeysByHour.has(hourKey)) usageKeysByHour.set(hourKey, [])
|
||||||
usageKeysByHour.set(hourKey, [])
|
|
||||||
}
|
|
||||||
usageKeysByHour.get(hourKey).push(key)
|
usageKeysByHour.get(hourKey).push(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -607,15 +599,11 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
// 处理模型级别数据
|
// 处理模型级别数据
|
||||||
for (const modelKey of modelKeys) {
|
for (const modelKey of modelKeys) {
|
||||||
const modelMatch = modelKey.match(/usage:model:hourly:(.+?):\d{4}-\d{2}-\d{2}:\d{2}/)
|
const modelMatch = modelKey.match(/usage:model:hourly:(.+?):\d{4}-\d{2}-\d{2}:\d{2}/)
|
||||||
if (!modelMatch) {
|
if (!modelMatch) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const model = modelMatch[1]
|
const model = modelMatch[1]
|
||||||
const data = modelDataMap.get(modelKey)
|
const data = modelDataMap.get(modelKey)
|
||||||
if (!data || Object.keys(data).length === 0) {
|
if (!data || Object.keys(data).length === 0) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const modelInputTokens = parseInt(data.inputTokens) || 0
|
const modelInputTokens = parseInt(data.inputTokens) || 0
|
||||||
const modelOutputTokens = parseInt(data.outputTokens) || 0
|
const modelOutputTokens = parseInt(data.outputTokens) || 0
|
||||||
@@ -722,9 +710,7 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const match = key.match(/usage:model:daily:.+?:(\d{4}-\d{2}-\d{2})/)
|
const match = key.match(/usage:model:daily:.+?:(\d{4}-\d{2}-\d{2})/)
|
||||||
if (match) {
|
if (match) {
|
||||||
const dateStr = match[1]
|
const dateStr = match[1]
|
||||||
if (!modelKeysByDate.has(dateStr)) {
|
if (!modelKeysByDate.has(dateStr)) modelKeysByDate.set(dateStr, [])
|
||||||
modelKeysByDate.set(dateStr, [])
|
|
||||||
}
|
|
||||||
modelKeysByDate.get(dateStr).push(key)
|
modelKeysByDate.get(dateStr).push(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -732,9 +718,7 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const match = key.match(/usage:daily:.+?:(\d{4}-\d{2}-\d{2})/)
|
const match = key.match(/usage:daily:.+?:(\d{4}-\d{2}-\d{2})/)
|
||||||
if (match) {
|
if (match) {
|
||||||
const dateStr = match[1]
|
const dateStr = match[1]
|
||||||
if (!usageKeysByDate.has(dateStr)) {
|
if (!usageKeysByDate.has(dateStr)) usageKeysByDate.set(dateStr, [])
|
||||||
usageKeysByDate.set(dateStr, [])
|
|
||||||
}
|
|
||||||
usageKeysByDate.get(dateStr).push(key)
|
usageKeysByDate.get(dateStr).push(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -754,15 +738,11 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
// 处理模型级别数据
|
// 处理模型级别数据
|
||||||
for (const modelKey of modelKeys) {
|
for (const modelKey of modelKeys) {
|
||||||
const modelMatch = modelKey.match(/usage:model:daily:(.+?):\d{4}-\d{2}-\d{2}/)
|
const modelMatch = modelKey.match(/usage:model:daily:(.+?):\d{4}-\d{2}-\d{2}/)
|
||||||
if (!modelMatch) {
|
if (!modelMatch) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const model = modelMatch[1]
|
const model = modelMatch[1]
|
||||||
const data = modelDataMap.get(modelKey)
|
const data = modelDataMap.get(modelKey)
|
||||||
if (!data || Object.keys(data).length === 0) {
|
if (!data || Object.keys(data).length === 0) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const modelInputTokens = parseInt(data.inputTokens) || 0
|
const modelInputTokens = parseInt(data.inputTokens) || 0
|
||||||
const modelOutputTokens = parseInt(data.outputTokens) || 0
|
const modelOutputTokens = parseInt(data.outputTokens) || 0
|
||||||
@@ -847,7 +827,7 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) =
|
|||||||
`📊 Getting model stats for API key: ${keyId}, period: ${period}, startDate: ${startDate}, endDate: ${endDate}`
|
`📊 Getting model stats for API key: ${keyId}, period: ${period}, startDate: ${startDate}, endDate: ${endDate}`
|
||||||
)
|
)
|
||||||
|
|
||||||
const _client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
const today = redis.getDateStringInTimezone()
|
const today = redis.getDateStringInTimezone()
|
||||||
const tzDate = redis.getDateInTimezone()
|
const tzDate = redis.getDateInTimezone()
|
||||||
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(
|
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(
|
||||||
@@ -915,13 +895,9 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) =
|
|||||||
for (const results of allResults) {
|
for (const results of allResults) {
|
||||||
for (const { key, data } of results) {
|
for (const { key, data } of results) {
|
||||||
// 过滤出属于该 keyId 的记录
|
// 过滤出属于该 keyId 的记录
|
||||||
if (!key.startsWith(`usage:${keyId}:model:`)) {
|
if (!key.startsWith(`usage:${keyId}:model:`)) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
const match = key.match(/usage:.+:model:daily:(.+):\d{4}-\d{2}-\d{2}$/)
|
const match = key.match(/usage:.+:model:daily:(.+):\d{4}-\d{2}-\d{2}$/)
|
||||||
if (!match) {
|
if (!match) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
const model = match[1]
|
const model = match[1]
|
||||||
if (!modelStatsMap.has(model)) {
|
if (!modelStatsMap.has(model)) {
|
||||||
modelStatsMap.set(model, {
|
modelStatsMap.set(model, {
|
||||||
@@ -957,15 +933,11 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) =
|
|||||||
results = await redis.scanAndGetAllChunked(pattern)
|
results = await redis.scanAndGetAllChunked(pattern)
|
||||||
}
|
}
|
||||||
for (const { key, data } of results) {
|
for (const { key, data } of results) {
|
||||||
if (!key.startsWith(`usage:${keyId}:model:`)) {
|
if (!key.startsWith(`usage:${keyId}:model:`)) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
const match =
|
const match =
|
||||||
key.match(/usage:.+:model:daily:(.+):\d{4}-\d{2}-\d{2}$/) ||
|
key.match(/usage:.+:model:daily:(.+):\d{4}-\d{2}-\d{2}$/) ||
|
||||||
key.match(/usage:.+:model:monthly:(.+):\d{4}-\d{2}$/)
|
key.match(/usage:.+:model:monthly:(.+):\d{4}-\d{2}$/)
|
||||||
if (!match) {
|
if (!match) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
const model = match[1]
|
const model = match[1]
|
||||||
if (!modelStatsMap.has(model)) {
|
if (!modelStatsMap.has(model)) {
|
||||||
modelStatsMap.set(model, {
|
modelStatsMap.set(model, {
|
||||||
@@ -1283,7 +1255,7 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 按小时获取 account_usage 数据(避免全库扫描)
|
// 按小时获取 account_usage 数据(避免全库扫描)
|
||||||
const _dates = [...dateSet]
|
const dates = [...dateSet]
|
||||||
const usageDataMap = new Map()
|
const usageDataMap = new Map()
|
||||||
const modelDataMap = new Map()
|
const modelDataMap = new Map()
|
||||||
|
|
||||||
@@ -1317,9 +1289,7 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const match = key.match(/account_usage:hourly:.+?:(\d{4}-\d{2}-\d{2}:\d{2})/)
|
const match = key.match(/account_usage:hourly:.+?:(\d{4}-\d{2}-\d{2}:\d{2})/)
|
||||||
if (match) {
|
if (match) {
|
||||||
const hourKey = match[1]
|
const hourKey = match[1]
|
||||||
if (!usageKeysByHour.has(hourKey)) {
|
if (!usageKeysByHour.has(hourKey)) usageKeysByHour.set(hourKey, [])
|
||||||
usageKeysByHour.set(hourKey, [])
|
|
||||||
}
|
|
||||||
usageKeysByHour.get(hourKey).push(key)
|
usageKeysByHour.get(hourKey).push(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1329,9 +1299,7 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const accountId = match[1]
|
const accountId = match[1]
|
||||||
const hourKey = match[2]
|
const hourKey = match[2]
|
||||||
const mapKey = `${accountId}:${hourKey}`
|
const mapKey = `${accountId}:${hourKey}`
|
||||||
if (!modelKeysByHour.has(mapKey)) {
|
if (!modelKeysByHour.has(mapKey)) modelKeysByHour.set(mapKey, [])
|
||||||
modelKeysByHour.set(mapKey, [])
|
|
||||||
}
|
|
||||||
modelKeysByHour.get(mapKey).push(key)
|
modelKeysByHour.get(mapKey).push(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1348,19 +1316,13 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
|
|
||||||
for (const key of usageKeys) {
|
for (const key of usageKeys) {
|
||||||
const match = key.match(/account_usage:hourly:(.+?):\d{4}-\d{2}-\d{2}:\d{2}/)
|
const match = key.match(/account_usage:hourly:(.+?):\d{4}-\d{2}-\d{2}:\d{2}/)
|
||||||
if (!match) {
|
if (!match) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const accountId = match[1]
|
const accountId = match[1]
|
||||||
if (!accountIdSet.has(accountId)) {
|
if (!accountIdSet.has(accountId)) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = usageDataMap.get(key)
|
const data = usageDataMap.get(key)
|
||||||
if (!data) {
|
if (!data) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputTokens = parseInt(data.inputTokens) || 0
|
const inputTokens = parseInt(data.inputTokens) || 0
|
||||||
const outputTokens = parseInt(data.outputTokens) || 0
|
const outputTokens = parseInt(data.outputTokens) || 0
|
||||||
@@ -1376,14 +1338,10 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const modelKeys = modelKeysByHour.get(`${accountId}:${hourInfo.hourKey}`) || []
|
const modelKeys = modelKeysByHour.get(`${accountId}:${hourInfo.hourKey}`) || []
|
||||||
for (const modelKey of modelKeys) {
|
for (const modelKey of modelKeys) {
|
||||||
const modelData = modelDataMap.get(modelKey)
|
const modelData = modelDataMap.get(modelKey)
|
||||||
if (!modelData) {
|
if (!modelData) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = modelKey.split(':')
|
const parts = modelKey.split(':')
|
||||||
if (parts.length < 5) {
|
if (parts.length < 5) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const modelName = parts[4]
|
const modelName = parts[4]
|
||||||
const usage = {
|
const usage = {
|
||||||
@@ -1476,9 +1434,7 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const match = key.match(/account_usage:daily:.+?:(\d{4}-\d{2}-\d{2})/)
|
const match = key.match(/account_usage:daily:.+?:(\d{4}-\d{2}-\d{2})/)
|
||||||
if (match) {
|
if (match) {
|
||||||
const dateStr = match[1]
|
const dateStr = match[1]
|
||||||
if (!usageKeysByDate.has(dateStr)) {
|
if (!usageKeysByDate.has(dateStr)) usageKeysByDate.set(dateStr, [])
|
||||||
usageKeysByDate.set(dateStr, [])
|
|
||||||
}
|
|
||||||
usageKeysByDate.get(dateStr).push(key)
|
usageKeysByDate.get(dateStr).push(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1488,9 +1444,7 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const accountId = match[1]
|
const accountId = match[1]
|
||||||
const dateStr = match[2]
|
const dateStr = match[2]
|
||||||
const mapKey = `${accountId}:${dateStr}`
|
const mapKey = `${accountId}:${dateStr}`
|
||||||
if (!modelKeysByDate.has(mapKey)) {
|
if (!modelKeysByDate.has(mapKey)) modelKeysByDate.set(mapKey, [])
|
||||||
modelKeysByDate.set(mapKey, [])
|
|
||||||
}
|
|
||||||
modelKeysByDate.get(mapKey).push(key)
|
modelKeysByDate.get(mapKey).push(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1506,19 +1460,13 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
|
|
||||||
for (const key of usageKeys) {
|
for (const key of usageKeys) {
|
||||||
const match = key.match(/account_usage:daily:(.+?):\d{4}-\d{2}-\d{2}/)
|
const match = key.match(/account_usage:daily:(.+?):\d{4}-\d{2}-\d{2}/)
|
||||||
if (!match) {
|
if (!match) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const accountId = match[1]
|
const accountId = match[1]
|
||||||
if (!accountIdSet.has(accountId)) {
|
if (!accountIdSet.has(accountId)) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = usageDataMap.get(key)
|
const data = usageDataMap.get(key)
|
||||||
if (!data) {
|
if (!data) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputTokens = parseInt(data.inputTokens) || 0
|
const inputTokens = parseInt(data.inputTokens) || 0
|
||||||
const outputTokens = parseInt(data.outputTokens) || 0
|
const outputTokens = parseInt(data.outputTokens) || 0
|
||||||
@@ -1534,14 +1482,10 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const modelKeys = modelKeysByDate.get(`${accountId}:${dayInfo.dateStr}`) || []
|
const modelKeys = modelKeysByDate.get(`${accountId}:${dayInfo.dateStr}`) || []
|
||||||
for (const modelKey of modelKeys) {
|
for (const modelKey of modelKeys) {
|
||||||
const modelData = modelDataMap.get(modelKey)
|
const modelData = modelDataMap.get(modelKey)
|
||||||
if (!modelData) {
|
if (!modelData) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = modelKey.split(':')
|
const parts = modelKey.split(':')
|
||||||
if (parts.length < 5) {
|
if (parts.length < 5) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const modelName = parts[4]
|
const modelName = parts[4]
|
||||||
const usage = {
|
const usage = {
|
||||||
@@ -1669,7 +1613,7 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用索引获取数据,按小时批量查询
|
// 使用索引获取数据,按小时批量查询
|
||||||
const _dates = [...dateSet]
|
const dates = [...dateSet]
|
||||||
const usageDataMap = new Map()
|
const usageDataMap = new Map()
|
||||||
const modelDataMap = new Map()
|
const modelDataMap = new Map()
|
||||||
|
|
||||||
@@ -1702,9 +1646,7 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const match = key.match(/usage:hourly:.+?:(\d{4}-\d{2}-\d{2}:\d{2})/)
|
const match = key.match(/usage:hourly:.+?:(\d{4}-\d{2}-\d{2}:\d{2})/)
|
||||||
if (match) {
|
if (match) {
|
||||||
const hourKey = match[1]
|
const hourKey = match[1]
|
||||||
if (!usageKeysByHour.has(hourKey)) {
|
if (!usageKeysByHour.has(hourKey)) usageKeysByHour.set(hourKey, [])
|
||||||
usageKeysByHour.set(hourKey, [])
|
|
||||||
}
|
|
||||||
usageKeysByHour.get(hourKey).push(key)
|
usageKeysByHour.get(hourKey).push(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1712,9 +1654,7 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const match = key.match(/usage:.+?:model:hourly:.+?:(\d{4}-\d{2}-\d{2}:\d{2})/)
|
const match = key.match(/usage:.+?:model:hourly:.+?:(\d{4}-\d{2}-\d{2}:\d{2})/)
|
||||||
if (match) {
|
if (match) {
|
||||||
const hourKey = match[1]
|
const hourKey = match[1]
|
||||||
if (!modelKeysByHour.has(hourKey)) {
|
if (!modelKeysByHour.has(hourKey)) modelKeysByHour.set(hourKey, [])
|
||||||
modelKeysByHour.set(hourKey, [])
|
|
||||||
}
|
|
||||||
modelKeysByHour.get(hourKey).push(key)
|
modelKeysByHour.get(hourKey).push(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1734,15 +1674,11 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const apiKeyDataMap = new Map()
|
const apiKeyDataMap = new Map()
|
||||||
for (const key of hourUsageKeys) {
|
for (const key of hourUsageKeys) {
|
||||||
const match = key.match(/usage:hourly:(.+?):\d{4}-\d{2}-\d{2}:\d{2}/)
|
const match = key.match(/usage:hourly:(.+?):\d{4}-\d{2}-\d{2}:\d{2}/)
|
||||||
if (!match) {
|
if (!match) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiKeyId = match[1]
|
const apiKeyId = match[1]
|
||||||
const data = usageDataMap.get(key)
|
const data = usageDataMap.get(key)
|
||||||
if (!data || !apiKeyMap.has(apiKeyId)) {
|
if (!data || !apiKeyMap.has(apiKeyId)) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputTokens = parseInt(data.inputTokens) || 0
|
const inputTokens = parseInt(data.inputTokens) || 0
|
||||||
const outputTokens = parseInt(data.outputTokens) || 0
|
const outputTokens = parseInt(data.outputTokens) || 0
|
||||||
@@ -1764,16 +1700,12 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const apiKeyCostMap = new Map()
|
const apiKeyCostMap = new Map()
|
||||||
for (const modelKey of hourModelKeys) {
|
for (const modelKey of hourModelKeys) {
|
||||||
const match = modelKey.match(/usage:(.+?):model:hourly:(.+?):\d{4}-\d{2}-\d{2}:\d{2}/)
|
const match = modelKey.match(/usage:(.+?):model:hourly:(.+?):\d{4}-\d{2}-\d{2}:\d{2}/)
|
||||||
if (!match) {
|
if (!match) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiKeyId = match[1]
|
const apiKeyId = match[1]
|
||||||
const model = match[2]
|
const model = match[2]
|
||||||
const modelData = modelDataMap.get(modelKey)
|
const modelData = modelDataMap.get(modelKey)
|
||||||
if (!modelData || !apiKeyDataMap.has(apiKeyId)) {
|
if (!modelData || !apiKeyDataMap.has(apiKeyId)) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const usage = {
|
const usage = {
|
||||||
input_tokens: parseInt(modelData.inputTokens) || 0,
|
input_tokens: parseInt(modelData.inputTokens) || 0,
|
||||||
@@ -1863,9 +1795,7 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const match = key.match(/usage:daily:.+?:(\d{4}-\d{2}-\d{2})/)
|
const match = key.match(/usage:daily:.+?:(\d{4}-\d{2}-\d{2})/)
|
||||||
if (match) {
|
if (match) {
|
||||||
const dateStr = match[1]
|
const dateStr = match[1]
|
||||||
if (!usageKeysByDate.has(dateStr)) {
|
if (!usageKeysByDate.has(dateStr)) usageKeysByDate.set(dateStr, [])
|
||||||
usageKeysByDate.set(dateStr, [])
|
|
||||||
}
|
|
||||||
usageKeysByDate.get(dateStr).push(key)
|
usageKeysByDate.get(dateStr).push(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1873,9 +1803,7 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const match = key.match(/usage:.+?:model:daily:.+?:(\d{4}-\d{2}-\d{2})/)
|
const match = key.match(/usage:.+?:model:daily:.+?:(\d{4}-\d{2}-\d{2})/)
|
||||||
if (match) {
|
if (match) {
|
||||||
const dateStr = match[1]
|
const dateStr = match[1]
|
||||||
if (!modelKeysByDate.has(dateStr)) {
|
if (!modelKeysByDate.has(dateStr)) modelKeysByDate.set(dateStr, [])
|
||||||
modelKeysByDate.set(dateStr, [])
|
|
||||||
}
|
|
||||||
modelKeysByDate.get(dateStr).push(key)
|
modelKeysByDate.get(dateStr).push(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1894,15 +1822,11 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const apiKeyDataMap = new Map()
|
const apiKeyDataMap = new Map()
|
||||||
for (const key of dayUsageKeys) {
|
for (const key of dayUsageKeys) {
|
||||||
const match = key.match(/usage:daily:(.+?):\d{4}-\d{2}-\d{2}/)
|
const match = key.match(/usage:daily:(.+?):\d{4}-\d{2}-\d{2}/)
|
||||||
if (!match) {
|
if (!match) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiKeyId = match[1]
|
const apiKeyId = match[1]
|
||||||
const data = usageDataMap.get(key)
|
const data = usageDataMap.get(key)
|
||||||
if (!data || !apiKeyMap.has(apiKeyId)) {
|
if (!data || !apiKeyMap.has(apiKeyId)) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputTokens = parseInt(data.inputTokens) || 0
|
const inputTokens = parseInt(data.inputTokens) || 0
|
||||||
const outputTokens = parseInt(data.outputTokens) || 0
|
const outputTokens = parseInt(data.outputTokens) || 0
|
||||||
@@ -1924,16 +1848,12 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const apiKeyCostMap = new Map()
|
const apiKeyCostMap = new Map()
|
||||||
for (const modelKey of dayModelKeys) {
|
for (const modelKey of dayModelKeys) {
|
||||||
const match = modelKey.match(/usage:(.+?):model:daily:(.+?):\d{4}-\d{2}-\d{2}/)
|
const match = modelKey.match(/usage:(.+?):model:daily:(.+?):\d{4}-\d{2}-\d{2}/)
|
||||||
if (!match) {
|
if (!match) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiKeyId = match[1]
|
const apiKeyId = match[1]
|
||||||
const model = match[2]
|
const model = match[2]
|
||||||
const modelData = modelDataMap.get(modelKey)
|
const modelData = modelDataMap.get(modelKey)
|
||||||
if (!modelData || !apiKeyDataMap.has(apiKeyId)) {
|
if (!modelData || !apiKeyDataMap.has(apiKeyId)) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const usage = {
|
const usage = {
|
||||||
input_tokens: parseInt(modelData.inputTokens) || 0,
|
input_tokens: parseInt(modelData.inputTokens) || 0,
|
||||||
@@ -2052,7 +1972,7 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
|
|||||||
const modelCosts = {}
|
const modelCosts = {}
|
||||||
|
|
||||||
// 按模型统计费用
|
// 按模型统计费用
|
||||||
const _client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
const today = redis.getDateStringInTimezone()
|
const today = redis.getDateStringInTimezone()
|
||||||
const tzDate = redis.getDateInTimezone()
|
const tzDate = redis.getDateInTimezone()
|
||||||
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(
|
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(
|
||||||
@@ -2060,11 +1980,11 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
|
|||||||
'0'
|
'0'
|
||||||
)}`
|
)}`
|
||||||
|
|
||||||
let _pattern
|
let pattern
|
||||||
if (period === 'today') {
|
if (period === 'today') {
|
||||||
_pattern = `usage:model:daily:*:${today}`
|
pattern = `usage:model:daily:*:${today}`
|
||||||
} else if (period === 'monthly') {
|
} else if (period === 'monthly') {
|
||||||
_pattern = `usage:model:monthly:*:${currentMonth}`
|
pattern = `usage:model:monthly:*:${currentMonth}`
|
||||||
} else if (period === '7days') {
|
} else if (period === '7days') {
|
||||||
// 最近7天:汇总daily数据(使用 SCAN + Pipeline 优化)
|
// 最近7天:汇总daily数据(使用 SCAN + Pipeline 优化)
|
||||||
const modelUsageMap = new Map()
|
const modelUsageMap = new Map()
|
||||||
@@ -2094,14 +2014,10 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
|
|||||||
|
|
||||||
// 处理数据
|
// 处理数据
|
||||||
for (const { key, data } of allData) {
|
for (const { key, data } of allData) {
|
||||||
if (!data) {
|
if (!data) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const modelMatch = key.match(/usage:model:daily:(.+):\d{4}-\d{2}-\d{2}$/)
|
const modelMatch = key.match(/usage:model:daily:(.+):\d{4}-\d{2}-\d{2}$/)
|
||||||
if (!modelMatch) {
|
if (!modelMatch) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawModel = modelMatch[1]
|
const rawModel = modelMatch[1]
|
||||||
const normalizedModel = normalizeModelName(rawModel)
|
const normalizedModel = normalizeModelName(rawModel)
|
||||||
@@ -2196,14 +2112,10 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
|
|||||||
const modelUsageMap = new Map()
|
const modelUsageMap = new Map()
|
||||||
|
|
||||||
for (const { key, data } of allData) {
|
for (const { key, data } of allData) {
|
||||||
if (!data) {
|
if (!data) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const modelMatch = key.match(/usage:model:monthly:(.+):(\d{4}-\d{2})$/)
|
const modelMatch = key.match(/usage:model:monthly:(.+):(\d{4}-\d{2})$/)
|
||||||
if (!modelMatch) {
|
if (!modelMatch) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const model = modelMatch[1]
|
const model = modelMatch[1]
|
||||||
|
|
||||||
@@ -2329,14 +2241,10 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
|
|||||||
: /usage:model:monthly:(.+):\d{4}-\d{2}$/
|
: /usage:model:monthly:(.+):\d{4}-\d{2}$/
|
||||||
|
|
||||||
for (const { key, data } of allData) {
|
for (const { key, data } of allData) {
|
||||||
if (!data) {
|
if (!data) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = key.match(regex)
|
const match = key.match(regex)
|
||||||
if (!match) {
|
if (!match) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const model = match[1]
|
const model = match[1]
|
||||||
const usage = {
|
const usage = {
|
||||||
|
|||||||
@@ -701,7 +701,7 @@ router.post('/api/batch-model-stats', async (req, res) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const _client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
const tzDate = redis.getDateInTimezone()
|
const tzDate = redis.getDateInTimezone()
|
||||||
const today = redis.getDateStringInTimezone()
|
const today = redis.getDateStringInTimezone()
|
||||||
const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`
|
const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`
|
||||||
@@ -940,7 +940,7 @@ router.post('/api/user-model-stats', async (req, res) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 重用管理后台的模型统计逻辑,但只返回该API Key的数据
|
// 重用管理后台的模型统计逻辑,但只返回该API Key的数据
|
||||||
const _client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
// 使用与管理页面相同的时区处理逻辑
|
// 使用与管理页面相同的时区处理逻辑
|
||||||
const tzDate = redis.getDateInTimezone()
|
const tzDate = redis.getDateInTimezone()
|
||||||
const today = redis.getDateStringInTimezone()
|
const today = redis.getDateStringInTimezone()
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ class AccountGroupService {
|
|||||||
async ensureReverseIndexes() {
|
async ensureReverseIndexes() {
|
||||||
try {
|
try {
|
||||||
const client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
if (!client) {
|
if (!client) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否已迁移
|
// 检查是否已迁移
|
||||||
const migrated = await client.get(this.REVERSE_INDEX_MIGRATED_KEY)
|
const migrated = await client.get(this.REVERSE_INDEX_MIGRATED_KEY)
|
||||||
@@ -41,14 +39,10 @@ class AccountGroupService {
|
|||||||
|
|
||||||
for (const groupId of allGroupIds) {
|
for (const groupId of allGroupIds) {
|
||||||
const group = await client.hgetall(`${this.GROUP_PREFIX}${groupId}`)
|
const group = await client.hgetall(`${this.GROUP_PREFIX}${groupId}`)
|
||||||
if (!group || !group.platform) {
|
if (!group || !group.platform) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const members = await client.smembers(`${this.GROUP_MEMBERS_PREFIX}${groupId}`)
|
const members = await client.smembers(`${this.GROUP_MEMBERS_PREFIX}${groupId}`)
|
||||||
if (members.length === 0) {
|
if (members.length === 0) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const pipeline = client.pipeline()
|
const pipeline = client.pipeline()
|
||||||
for (const accountId of members) {
|
for (const accountId of members) {
|
||||||
|
|||||||
@@ -71,9 +71,7 @@ class ApiKeyIndexService {
|
|||||||
* 扫描所有 API Key,确保 hash -> keyId 映射存在
|
* 扫描所有 API Key,确保 hash -> keyId 映射存在
|
||||||
*/
|
*/
|
||||||
async rebuildHashMap() {
|
async rebuildHashMap() {
|
||||||
if (!this.redis) {
|
if (!this.redis) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = this.redis.getClientSafe()
|
const client = this.redis.getClientSafe()
|
||||||
@@ -189,9 +187,7 @@ class ApiKeyIndexService {
|
|||||||
const pipeline = client.pipeline()
|
const pipeline = client.pipeline()
|
||||||
|
|
||||||
for (const apiKey of apiKeys) {
|
for (const apiKey of apiKeys) {
|
||||||
if (!apiKey || !apiKey.id) {
|
if (!apiKey || !apiKey.id) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyId = apiKey.id
|
const keyId = apiKey.id
|
||||||
const createdAt = apiKey.createdAt ? new Date(apiKey.createdAt).getTime() : 0
|
const createdAt = apiKey.createdAt ? new Date(apiKey.createdAt).getTime() : 0
|
||||||
@@ -253,9 +249,7 @@ class ApiKeyIndexService {
|
|||||||
* 添加单个 API Key 到索引
|
* 添加单个 API Key 到索引
|
||||||
*/
|
*/
|
||||||
async addToIndex(apiKey) {
|
async addToIndex(apiKey) {
|
||||||
if (!this.redis || !apiKey || !apiKey.id) {
|
if (!this.redis || !apiKey || !apiKey.id) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = this.redis.getClientSafe()
|
const client = this.redis.getClientSafe()
|
||||||
@@ -303,9 +297,7 @@ class ApiKeyIndexService {
|
|||||||
* 更新索引(状态、名称、标签变化时调用)
|
* 更新索引(状态、名称、标签变化时调用)
|
||||||
*/
|
*/
|
||||||
async updateIndex(keyId, updates, oldData = {}) {
|
async updateIndex(keyId, updates, oldData = {}) {
|
||||||
if (!this.redis || !keyId) {
|
if (!this.redis || !keyId) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = this.redis.getClientSafe()
|
const client = this.redis.getClientSafe()
|
||||||
@@ -384,9 +376,7 @@ class ApiKeyIndexService {
|
|||||||
* 从索引中移除 API Key
|
* 从索引中移除 API Key
|
||||||
*/
|
*/
|
||||||
async removeFromIndex(keyId, oldData = {}) {
|
async removeFromIndex(keyId, oldData = {}) {
|
||||||
if (!this.redis || !keyId) {
|
if (!this.redis || !keyId) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = this.redis.getClientSafe()
|
const client = this.redis.getClientSafe()
|
||||||
@@ -608,9 +598,7 @@ class ApiKeyIndexService {
|
|||||||
* 更新 lastUsedAt 索引(供 recordUsage 调用)
|
* 更新 lastUsedAt 索引(供 recordUsage 调用)
|
||||||
*/
|
*/
|
||||||
async updateLastUsedAt(keyId, lastUsedAt) {
|
async updateLastUsedAt(keyId, lastUsedAt) {
|
||||||
if (!this.redis || !keyId) {
|
if (!this.redis || !keyId) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = this.redis.getClientSafe()
|
const client = this.redis.getClientSafe()
|
||||||
|
|||||||
@@ -921,9 +921,7 @@ class ApiKeyService {
|
|||||||
return keyIds
|
return keyIds
|
||||||
.map((id, i) => {
|
.map((id, i) => {
|
||||||
const [err, fields] = results[i]
|
const [err, fields] = results[i]
|
||||||
if (err) {
|
if (err) return null
|
||||||
return null
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
claudeAccountId: fields[0] || null,
|
claudeAccountId: fields[0] || null,
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ class BedrockAccountService {
|
|||||||
// 📋 获取所有账户列表
|
// 📋 获取所有账户列表
|
||||||
async getAllAccounts() {
|
async getAllAccounts() {
|
||||||
try {
|
try {
|
||||||
const _client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
const accountIds = await redis.getAllIdsByIndex(
|
const accountIds = await redis.getAllIdsByIndex(
|
||||||
'bedrock_account:index',
|
'bedrock_account:index',
|
||||||
'bedrock_account:*',
|
'bedrock_account:*',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const { v4: uuidv4 } = require('uuid')
|
|||||||
const ProxyHelper = require('../utils/proxyHelper')
|
const ProxyHelper = require('../utils/proxyHelper')
|
||||||
const redis = require('../models/redis')
|
const redis = require('../models/redis')
|
||||||
const logger = require('../utils/logger')
|
const logger = require('../utils/logger')
|
||||||
// const config = require('../../config/config')
|
const config = require('../../config/config')
|
||||||
const { createEncryptor } = require('../utils/commonHelper')
|
const { createEncryptor } = require('../utils/commonHelper')
|
||||||
|
|
||||||
class CcrAccountService {
|
class CcrAccountService {
|
||||||
|
|||||||
@@ -5,11 +5,7 @@
|
|||||||
|
|
||||||
const redis = require('../models/redis')
|
const redis = require('../models/redis')
|
||||||
const logger = require('../utils/logger')
|
const logger = require('../utils/logger')
|
||||||
const {
|
const { getCachedConfig, setCachedConfig, deleteCachedConfig } = require('../utils/performanceOptimizer')
|
||||||
getCachedConfig,
|
|
||||||
setCachedConfig,
|
|
||||||
deleteCachedConfig
|
|
||||||
} = require('../utils/performanceOptimizer')
|
|
||||||
|
|
||||||
class ClaudeCodeHeadersService {
|
class ClaudeCodeHeadersService {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ const {
|
|||||||
|
|
||||||
// structuredClone polyfill for Node < 17
|
// structuredClone polyfill for Node < 17
|
||||||
const safeClone =
|
const safeClone =
|
||||||
typeof structuredClone === 'function' ? structuredClone : (obj) => JSON.parse(JSON.stringify(obj))
|
typeof structuredClone === 'function'
|
||||||
|
? structuredClone
|
||||||
|
: (obj) => JSON.parse(JSON.stringify(obj))
|
||||||
|
|
||||||
class ClaudeRelayService {
|
class ClaudeRelayService {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@@ -94,9 +94,7 @@ class CostInitService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(`💰 Found ${apiKeyIds.length} active API Keys to process (filtered ${allKeyIds.length - apiKeyIds.length} deleted)`)
|
||||||
`💰 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
|
||||||
@@ -157,9 +155,7 @@ class CostInitService {
|
|||||||
|
|
||||||
for (let j = 0; j < results.length; j++) {
|
for (let j = 0; j < results.length; j++) {
|
||||||
const [err, values] = results[j]
|
const [err, values] = results[j]
|
||||||
if (err) {
|
if (err) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将数组转换为对象
|
// 将数组转换为对象
|
||||||
const data = {}
|
const data = {}
|
||||||
@@ -186,9 +182,7 @@ class CostInitService {
|
|||||||
const match = key.match(
|
const match = key.match(
|
||||||
/usage:(.+):model:(daily|monthly|hourly):(.+):(\d{4}-\d{2}(?:-\d{2})?(?::\d{2})?)$/
|
/usage:(.+):model:(daily|monthly|hourly):(.+):(\d{4}-\d{2}(?:-\d{2})?(?::\d{2})?)$/
|
||||||
)
|
)
|
||||||
if (!match) {
|
if (!match) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, , period, model, dateStr] = match
|
const [, , period, model, dateStr] = match
|
||||||
|
|
||||||
@@ -307,9 +301,7 @@ class CostInitService {
|
|||||||
cursor = newCursor
|
cursor = newCursor
|
||||||
|
|
||||||
for (const usageKey of usageKeys) {
|
for (const usageKey of usageKeys) {
|
||||||
if (samplesChecked >= maxSamples) {
|
if (samplesChecked >= maxSamples) break
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = usageKey.match(/usage:(.+):model:daily:(.+):(\d{4}-\d{2}-\d{2})$/)
|
const match = usageKey.match(/usage:(.+):model:daily:(.+):(\d{4}-\d{2}-\d{2})$/)
|
||||||
if (match) {
|
if (match) {
|
||||||
@@ -327,9 +319,7 @@ class CostInitService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (samplesChecked >= maxSamples) {
|
if (samplesChecked >= maxSamples) break
|
||||||
break
|
|
||||||
}
|
|
||||||
} while (cursor !== '0')
|
} while (cursor !== '0')
|
||||||
|
|
||||||
logger.info('💰 Cost data appears to be up to date')
|
logger.info('💰 Cost data appears to be up to date')
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const { v4: uuidv4 } = require('uuid')
|
|||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const redis = require('../models/redis')
|
const redis = require('../models/redis')
|
||||||
// const config = require('../../config/config')
|
const config = require('../../config/config')
|
||||||
const logger = require('../utils/logger')
|
const logger = require('../utils/logger')
|
||||||
const { maskToken } = require('../utils/tokenMask')
|
const { maskToken } = require('../utils/tokenMask')
|
||||||
const ProxyHelper = require('../utils/proxyHelper')
|
const ProxyHelper = require('../utils/proxyHelper')
|
||||||
@@ -30,13 +30,10 @@ class DroidAccountService {
|
|||||||
this._encryptor = createEncryptor('droid-account-salt')
|
this._encryptor = createEncryptor('droid-account-salt')
|
||||||
|
|
||||||
// 🧹 定期清理缓存(每10分钟)
|
// 🧹 定期清理缓存(每10分钟)
|
||||||
setInterval(
|
setInterval(() => {
|
||||||
() => {
|
this._encryptor.clearCache()
|
||||||
this._encryptor.clearCache()
|
logger.info('🧹 Droid decrypt cache cleanup completed', this._encryptor.getStats())
|
||||||
logger.info('🧹 Droid decrypt cache cleanup completed', this._encryptor.getStats())
|
}, 10 * 60 * 1000)
|
||||||
},
|
|
||||||
10 * 60 * 1000
|
|
||||||
)
|
|
||||||
|
|
||||||
this.supportedEndpointTypes = new Set(['anthropic', 'openai', 'comm'])
|
this.supportedEndpointTypes = new Set(['anthropic', 'openai', 'comm'])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,7 @@ 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 {
|
const { isTruthy, isAccountHealthy, sortAccountsByPriority, normalizeEndpointType } = require('../utils/commonHelper')
|
||||||
isTruthy,
|
|
||||||
isAccountHealthy,
|
|
||||||
sortAccountsByPriority,
|
|
||||||
normalizeEndpointType
|
|
||||||
} = require('../utils/commonHelper')
|
|
||||||
|
|
||||||
class DroidScheduler {
|
class DroidScheduler {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -21,12 +16,8 @@ class DroidScheduler {
|
|||||||
_matchesEndpoint(account, endpointType) {
|
_matchesEndpoint(account, endpointType) {
|
||||||
const normalizedEndpoint = normalizeEndpointType(endpointType)
|
const normalizedEndpoint = normalizeEndpointType(endpointType)
|
||||||
const accountEndpoint = normalizeEndpointType(account?.endpointType)
|
const accountEndpoint = normalizeEndpointType(account?.endpointType)
|
||||||
if (normalizedEndpoint === accountEndpoint) {
|
if (normalizedEndpoint === accountEndpoint) return true
|
||||||
return true
|
if (normalizedEndpoint === 'comm') return true
|
||||||
}
|
|
||||||
if (normalizedEndpoint === 'comm') {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
const sharedEndpoints = new Set(['anthropic', 'openai'])
|
const sharedEndpoints = new Set(['anthropic', 'openai'])
|
||||||
return sharedEndpoints.has(normalizedEndpoint) && sharedEndpoints.has(accountEndpoint)
|
return sharedEndpoints.has(normalizedEndpoint) && sharedEndpoints.has(accountEndpoint)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const redisClient = require('../models/redis')
|
const redisClient = require('../models/redis')
|
||||||
const { v4: uuidv4 } = require('uuid')
|
const { v4: uuidv4 } = require('uuid')
|
||||||
const https = require('https')
|
const https = require('https')
|
||||||
// const config = require('../../config/config')
|
const config = require('../../config/config')
|
||||||
const logger = require('../utils/logger')
|
const logger = require('../utils/logger')
|
||||||
const { OAuth2Client } = require('google-auth-library')
|
const { OAuth2Client } = require('google-auth-library')
|
||||||
const { maskToken } = require('../utils/tokenMask')
|
const { maskToken } = require('../utils/tokenMask')
|
||||||
@@ -19,8 +19,6 @@ const { createEncryptor } = require('../utils/commonHelper')
|
|||||||
// Gemini 账户键前缀
|
// Gemini 账户键前缀
|
||||||
const GEMINI_ACCOUNT_KEY_PREFIX = 'gemini_account:'
|
const GEMINI_ACCOUNT_KEY_PREFIX = 'gemini_account:'
|
||||||
|
|
||||||
const SHARED_GEMINI_ACCOUNTS_KEY = 'shared_gemini_accounts'
|
|
||||||
const ACCOUNT_SESSION_MAPPING_PREFIX = 'gemini_session_account_mapping:'
|
|
||||||
// Gemini CLI OAuth 配置 - 这些是公开的 Gemini CLI 凭据
|
// Gemini CLI OAuth 配置 - 这些是公开的 Gemini CLI 凭据
|
||||||
const OAUTH_CLIENT_ID = '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com'
|
const OAUTH_CLIENT_ID = '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com'
|
||||||
const OAUTH_CLIENT_SECRET = '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'
|
const OAUTH_CLIENT_SECRET = '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'
|
||||||
@@ -574,7 +572,7 @@ async function deleteAccount(accountId) {
|
|||||||
|
|
||||||
// 获取所有账户
|
// 获取所有账户
|
||||||
async function getAllAccounts() {
|
async function getAllAccounts() {
|
||||||
const _client = redisClient.getClientSafe()
|
const client = redisClient.getClientSafe()
|
||||||
const accountIds = await redisClient.getAllIdsByIndex(
|
const accountIds = await redisClient.getAllIdsByIndex(
|
||||||
'gemini_account:index',
|
'gemini_account:index',
|
||||||
`${GEMINI_ACCOUNT_KEY_PREFIX}*`,
|
`${GEMINI_ACCOUNT_KEY_PREFIX}*`,
|
||||||
|
|||||||
@@ -666,7 +666,7 @@ async function deleteAccount(accountId) {
|
|||||||
|
|
||||||
// 获取所有账户
|
// 获取所有账户
|
||||||
async function getAllAccounts() {
|
async function getAllAccounts() {
|
||||||
const _client = redisClient.getClientSafe()
|
const client = redisClient.getClientSafe()
|
||||||
const accountIds = await redisClient.getAllIdsByIndex(
|
const accountIds = await redisClient.getAllIdsByIndex(
|
||||||
'openai:account:index',
|
'openai:account:index',
|
||||||
`${OPENAI_ACCOUNT_KEY_PREFIX}*`,
|
`${OPENAI_ACCOUNT_KEY_PREFIX}*`,
|
||||||
|
|||||||
@@ -201,9 +201,7 @@ class OpenAIResponsesAccountService {
|
|||||||
`${this.ACCOUNT_KEY_PREFIX}*`,
|
`${this.ACCOUNT_KEY_PREFIX}*`,
|
||||||
/^openai_responses_account:(.+)$/
|
/^openai_responses_account:(.+)$/
|
||||||
)
|
)
|
||||||
if (accountIds.length === 0) {
|
if (accountIds.length === 0) return []
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = accountIds.map((id) => `${this.ACCOUNT_KEY_PREFIX}${id}`)
|
const keys = accountIds.map((id) => `${this.ACCOUNT_KEY_PREFIX}${id}`)
|
||||||
// Pipeline 批量查询所有账户数据
|
// Pipeline 批量查询所有账户数据
|
||||||
@@ -212,15 +210,11 @@ class OpenAIResponsesAccountService {
|
|||||||
const results = await pipeline.exec()
|
const results = await pipeline.exec()
|
||||||
|
|
||||||
const accounts = []
|
const accounts = []
|
||||||
results.forEach(([err, accountData], _index) => {
|
results.forEach(([err, accountData], index) => {
|
||||||
if (err || !accountData || !accountData.id) {
|
if (err || !accountData || !accountData.id) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 过滤非活跃账户
|
// 过滤非活跃账户
|
||||||
if (!includeInactive && accountData.isActive !== 'true') {
|
if (!includeInactive && accountData.isActive !== 'true') return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏敏感信息
|
// 隐藏敏感信息
|
||||||
accountData.apiKey = '***'
|
accountData.apiKey = '***'
|
||||||
|
|||||||
@@ -26,7 +26,11 @@ 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 (boundAccount && isActive(boundAccount.isActive) && boundAccount.status !== 'error') {
|
if (
|
||||||
|
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}`
|
||||||
)
|
)
|
||||||
@@ -59,7 +63,11 @@ class UnifiedGeminiScheduler {
|
|||||||
// 普通 Gemini OAuth 专属账户
|
// 普通 Gemini OAuth 专属账户
|
||||||
else {
|
else {
|
||||||
const boundAccount = await geminiAccountService.getAccount(apiKeyData.geminiAccountId)
|
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(
|
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}`
|
||||||
)
|
)
|
||||||
@@ -175,7 +183,11 @@ 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 (boundAccount && isActive(boundAccount.isActive) && boundAccount.status !== 'error') {
|
if (
|
||||||
|
boundAccount &&
|
||||||
|
isActive(boundAccount.isActive) &&
|
||||||
|
boundAccount.status !== 'error'
|
||||||
|
) {
|
||||||
const isRateLimited = await this.isAccountRateLimited(accountId)
|
const isRateLimited = await this.isAccountRateLimited(accountId)
|
||||||
if (!isRateLimited) {
|
if (!isRateLimited) {
|
||||||
// 检查模型支持
|
// 检查模型支持
|
||||||
@@ -222,7 +234,11 @@ 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 (boundAccount && isActive(boundAccount.isActive) && boundAccount.status !== 'error') {
|
if (
|
||||||
|
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) {
|
||||||
// 检查模型支持
|
// 检查模型支持
|
||||||
|
|||||||
@@ -56,9 +56,9 @@ class UnifiedOpenAIScheduler {
|
|||||||
let rateLimitChecked = false
|
let rateLimitChecked = false
|
||||||
let stillLimited = false
|
let stillLimited = false
|
||||||
|
|
||||||
let _isSchedulable = isSchedulable(account.schedulable)
|
let isSchedulable = isSchedulable(account.schedulable)
|
||||||
|
|
||||||
if (!_isSchedulable) {
|
if (!isSchedulable) {
|
||||||
if (!hasRateLimitFlag) {
|
if (!hasRateLimitFlag) {
|
||||||
return { canUse: false, reason: 'not_schedulable' }
|
return { canUse: false, reason: 'not_schedulable' }
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ class UnifiedOpenAIScheduler {
|
|||||||
} else {
|
} else {
|
||||||
account.schedulable = 'true'
|
account.schedulable = 'true'
|
||||||
}
|
}
|
||||||
_isSchedulable = true
|
isSchedulable = true
|
||||||
logger.info(`✅ OpenAI账号 ${account.name || accountId} 已解除限流,恢复调度权限`)
|
logger.info(`✅ OpenAI账号 ${account.name || accountId} 已解除限流,恢复调度权限`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,45 +17,33 @@ const _encryptorCache = new Map()
|
|||||||
|
|
||||||
// 创建加密器实例(每个 salt 独立缓存)
|
// 创建加密器实例(每个 salt 独立缓存)
|
||||||
const createEncryptor = (salt) => {
|
const createEncryptor = (salt) => {
|
||||||
if (_encryptorCache.has(salt)) {
|
if (_encryptorCache.has(salt)) return _encryptorCache.get(salt)
|
||||||
return _encryptorCache.get(salt)
|
|
||||||
}
|
|
||||||
|
|
||||||
let keyCache = null
|
let keyCache = null
|
||||||
const decryptCache = new LRUCache(500)
|
const decryptCache = new LRUCache(500)
|
||||||
|
|
||||||
const getKey = () => {
|
const getKey = () => {
|
||||||
if (!keyCache) {
|
if (!keyCache) keyCache = crypto.scryptSync(config.security.encryptionKey, salt, 32)
|
||||||
keyCache = crypto.scryptSync(config.security.encryptionKey, salt, 32)
|
|
||||||
}
|
|
||||||
return keyCache
|
return keyCache
|
||||||
}
|
}
|
||||||
|
|
||||||
const encrypt = (text) => {
|
const encrypt = (text) => {
|
||||||
if (!text) {
|
if (!text) return ''
|
||||||
return ''
|
|
||||||
}
|
|
||||||
const key = getKey()
|
const key = getKey()
|
||||||
const iv = crypto.randomBytes(IV_LENGTH)
|
const iv = crypto.randomBytes(IV_LENGTH)
|
||||||
const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
|
const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
|
||||||
let encrypted = cipher.update(text, 'utf8', 'hex')
|
let encrypted = cipher.update(text, 'utf8', 'hex')
|
||||||
encrypted += cipher.final('hex')
|
encrypted += cipher.final('hex')
|
||||||
return `${iv.toString('hex')}:${encrypted}`
|
return iv.toString('hex') + ':' + encrypted
|
||||||
}
|
}
|
||||||
|
|
||||||
const decrypt = (text, useCache = true) => {
|
const decrypt = (text, useCache = true) => {
|
||||||
if (!text) {
|
if (!text) return ''
|
||||||
return ''
|
if (!text.includes(':')) return text
|
||||||
}
|
|
||||||
if (!text.includes(':')) {
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
const cacheKey = crypto.createHash('sha256').update(text).digest('hex')
|
const cacheKey = crypto.createHash('sha256').update(text).digest('hex')
|
||||||
if (useCache) {
|
if (useCache) {
|
||||||
const cached = decryptCache.get(cacheKey)
|
const cached = decryptCache.get(cacheKey)
|
||||||
if (cached !== undefined) {
|
if (cached !== undefined) return cached
|
||||||
return cached
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const key = getKey()
|
const key = getKey()
|
||||||
@@ -64,9 +52,7 @@ const createEncryptor = (salt) => {
|
|||||||
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)
|
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)
|
||||||
let decrypted = decipher.update(encrypted, 'hex', 'utf8')
|
let decrypted = decipher.update(encrypted, 'hex', 'utf8')
|
||||||
decrypted += decipher.final('utf8')
|
decrypted += decipher.final('utf8')
|
||||||
if (useCache) {
|
if (useCache) decryptCache.set(cacheKey, decrypted, 5 * 60 * 1000)
|
||||||
decryptCache.set(cacheKey, decrypted, 5 * 60 * 1000)
|
|
||||||
}
|
|
||||||
return decrypted
|
return decrypted
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return text
|
return text
|
||||||
@@ -87,8 +73,8 @@ const createEncryptor = (salt) => {
|
|||||||
|
|
||||||
// 默认加密器(向后兼容)
|
// 默认加密器(向后兼容)
|
||||||
const defaultEncryptor = createEncryptor('claude-relay-salt')
|
const defaultEncryptor = createEncryptor('claude-relay-salt')
|
||||||
const { encrypt } = defaultEncryptor
|
const encrypt = defaultEncryptor.encrypt
|
||||||
const { decrypt } = defaultEncryptor
|
const decrypt = defaultEncryptor.decrypt
|
||||||
const getEncryptionKey = defaultEncryptor.getKey
|
const getEncryptionKey = defaultEncryptor.getKey
|
||||||
const clearDecryptCache = defaultEncryptor.clearCache
|
const clearDecryptCache = defaultEncryptor.clearCache
|
||||||
const getDecryptCacheStats = defaultEncryptor.getStats
|
const getDecryptCacheStats = defaultEncryptor.getStats
|
||||||
@@ -98,13 +84,10 @@ const getDecryptCacheStats = defaultEncryptor.getStats
|
|||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
// 转换为布尔值(宽松模式)
|
// 转换为布尔值(宽松模式)
|
||||||
const toBoolean = (value) =>
|
const toBoolean = (value) => value === true || value === 'true' || (typeof value === 'string' && value.toLowerCase() === 'true')
|
||||||
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)
|
||||||
|
|
||||||
// 检查是否可调度(默认 true,只有明确 false 才返回 false)
|
// 检查是否可调度(默认 true,只有明确 false 才返回 false)
|
||||||
const isSchedulable = (value) => value !== false && value !== 'false'
|
const isSchedulable = (value) => value !== false && value !== 'false'
|
||||||
@@ -114,12 +97,8 @@ const isActive = (value) => value === true || value === 'true'
|
|||||||
|
|
||||||
// 检查账户是否健康(激活且状态正常)
|
// 检查账户是否健康(激活且状态正常)
|
||||||
const isAccountHealthy = (account) => {
|
const isAccountHealthy = (account) => {
|
||||||
if (!account) {
|
if (!account) return false
|
||||||
return false
|
if (!isTruthy(account.isActive)) return false
|
||||||
}
|
|
||||||
if (!isTruthy(account.isActive)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const status = (account.status || 'active').toLowerCase()
|
const status = (account.status || 'active').toLowerCase()
|
||||||
return !['error', 'unauthorized', 'blocked', 'temp_error'].includes(status)
|
return !['error', 'unauthorized', 'blocked', 'temp_error'].includes(status)
|
||||||
}
|
}
|
||||||
@@ -130,14 +109,8 @@ const isAccountHealthy = (account) => {
|
|||||||
|
|
||||||
// 安全解析 JSON
|
// 安全解析 JSON
|
||||||
const safeParseJson = (value, fallback = null) => {
|
const safeParseJson = (value, fallback = null) => {
|
||||||
if (!value || typeof value !== 'string') {
|
if (!value || typeof value !== 'string') return fallback
|
||||||
return fallback
|
try { return JSON.parse(value) } catch { return fallback }
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.parse(value)
|
|
||||||
} catch {
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 安全解析 JSON 为对象
|
// 安全解析 JSON 为对象
|
||||||
@@ -158,53 +131,36 @@ const safeParseJsonArray = (value, fallback = []) => {
|
|||||||
|
|
||||||
// 规范化模型名称(用于统计聚合)
|
// 规范化模型名称(用于统计聚合)
|
||||||
const normalizeModelName = (model) => {
|
const normalizeModelName = (model) => {
|
||||||
if (!model || model === 'unknown') {
|
if (!model || model === 'unknown') return model
|
||||||
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
|
return model.replace(/^[a-z0-9-]+\./, '').replace('anthropic.', '').replace(/-v\d+:\d+$/, '')
|
||||||
.replace(/^[a-z0-9-]+\./, '')
|
|
||||||
.replace('anthropic.', '')
|
|
||||||
.replace(/-v\d+:\d+$/, '')
|
|
||||||
}
|
}
|
||||||
return model.replace(/-v\d+:\d+$|:latest$/, '')
|
return model.replace(/-v\d+:\d+$|:latest$/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 规范化端点类型
|
// 规范化端点类型
|
||||||
const normalizeEndpointType = (endpointType) => {
|
const normalizeEndpointType = (endpointType) => {
|
||||||
if (!endpointType) {
|
if (!endpointType) return 'anthropic'
|
||||||
return 'anthropic'
|
|
||||||
}
|
|
||||||
const normalized = String(endpointType).toLowerCase()
|
const normalized = String(endpointType).toLowerCase()
|
||||||
return ['openai', 'comm', 'anthropic'].includes(normalized) ? normalized : 'anthropic'
|
return ['openai', 'comm', 'anthropic'].includes(normalized) ? normalized : 'anthropic'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查模型是否在映射表中
|
// 检查模型是否在映射表中
|
||||||
const isModelInMapping = (modelMapping, requestedModel) => {
|
const isModelInMapping = (modelMapping, requestedModel) => {
|
||||||
if (!modelMapping || Object.keys(modelMapping).length === 0) {
|
if (!modelMapping || Object.keys(modelMapping).length === 0) return true
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取映射后的模型名称
|
// 获取映射后的模型名称
|
||||||
const getMappedModelName = (modelMapping, requestedModel) => {
|
const getMappedModelName = (modelMapping, requestedModel) => {
|
||||||
if (!modelMapping || Object.keys(modelMapping).length === 0) {
|
if (!modelMapping || Object.keys(modelMapping).length === 0) return requestedModel
|
||||||
return requestedModel
|
if (modelMapping[requestedModel]) return modelMapping[requestedModel]
|
||||||
}
|
|
||||||
if (modelMapping[requestedModel]) {
|
|
||||||
return modelMapping[requestedModel]
|
|
||||||
}
|
|
||||||
const lower = requestedModel.toLowerCase()
|
const lower = requestedModel.toLowerCase()
|
||||||
for (const [key, value] of Object.entries(modelMapping)) {
|
for (const [key, value] of Object.entries(modelMapping)) {
|
||||||
if (key.toLowerCase() === lower) {
|
if (key.toLowerCase() === lower) return value
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return requestedModel
|
return requestedModel
|
||||||
}
|
}
|
||||||
@@ -214,34 +170,30 @@ const getMappedModelName = (modelMapping, requestedModel) => {
|
|||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
// 按优先级和最后使用时间排序账户
|
// 按优先级和最后使用时间排序账户
|
||||||
const sortAccountsByPriority = (accounts) =>
|
const sortAccountsByPriority = (accounts) => {
|
||||||
[...accounts].sort((a, b) => {
|
return [...accounts].sort((a, b) => {
|
||||||
const priorityA = parseInt(a.priority, 10) || 50
|
const priorityA = parseInt(a.priority, 10) || 50
|
||||||
const priorityB = parseInt(b.priority, 10) || 50
|
const priorityB = parseInt(b.priority, 10) || 50
|
||||||
if (priorityA !== priorityB) {
|
if (priorityA !== priorityB) return priorityA - priorityB
|
||||||
return priorityA - priorityB
|
|
||||||
}
|
|
||||||
const lastUsedA = a.lastUsedAt ? new Date(a.lastUsedAt).getTime() : 0
|
const lastUsedA = a.lastUsedAt ? new Date(a.lastUsedAt).getTime() : 0
|
||||||
const lastUsedB = b.lastUsedAt ? new Date(b.lastUsedAt).getTime() : 0
|
const lastUsedB = b.lastUsedAt ? new Date(b.lastUsedAt).getTime() : 0
|
||||||
if (lastUsedA !== lastUsedB) {
|
if (lastUsedA !== lastUsedB) return lastUsedA - lastUsedB
|
||||||
return lastUsedA - lastUsedB
|
|
||||||
}
|
|
||||||
const createdA = a.createdAt ? new Date(a.createdAt).getTime() : 0
|
const createdA = a.createdAt ? new Date(a.createdAt).getTime() : 0
|
||||||
const createdB = b.createdAt ? new Date(b.createdAt).getTime() : 0
|
const createdB = b.createdAt ? new Date(b.createdAt).getTime() : 0
|
||||||
return createdA - createdB
|
return createdA - createdB
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 生成粘性会话 Key
|
// 生成粘性会话 Key
|
||||||
const composeStickySessionKey = (prefix, sessionHash, apiKeyId = null) => {
|
const composeStickySessionKey = (prefix, sessionHash, apiKeyId = null) => {
|
||||||
if (!sessionHash) {
|
if (!sessionHash) return null
|
||||||
return null
|
|
||||||
}
|
|
||||||
return `sticky:${prefix}:${apiKeyId || 'default'}:${sessionHash}`
|
return `sticky:${prefix}:${apiKeyId || 'default'}:${sessionHash}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤可用账户(激活 + 健康 + 可调度)
|
// 过滤可用账户(激活 + 健康 + 可调度)
|
||||||
const filterAvailableAccounts = (accounts) =>
|
const filterAvailableAccounts = (accounts) => {
|
||||||
accounts.filter((acc) => acc && isAccountHealthy(acc) && isSchedulable(acc.schedulable))
|
return accounts.filter(acc => acc && isAccountHealthy(acc) && isSchedulable(acc.schedulable))
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 字符串处理
|
// 字符串处理
|
||||||
@@ -249,17 +201,13 @@ const filterAvailableAccounts = (accounts) =>
|
|||||||
|
|
||||||
// 截断字符串
|
// 截断字符串
|
||||||
const truncate = (str, maxLen = 100, suffix = '...') => {
|
const truncate = (str, maxLen = 100, suffix = '...') => {
|
||||||
if (!str || str.length <= maxLen) {
|
if (!str || str.length <= maxLen) return str
|
||||||
return str
|
|
||||||
}
|
|
||||||
return str.slice(0, maxLen - suffix.length) + suffix
|
return str.slice(0, maxLen - suffix.length) + suffix
|
||||||
}
|
}
|
||||||
|
|
||||||
// 掩码敏感信息(保留前后几位)
|
// 掩码敏感信息(保留前后几位)
|
||||||
const maskSensitive = (str, keepStart = 4, keepEnd = 4, maskChar = '*') => {
|
const maskSensitive = (str, keepStart = 4, keepEnd = 4, maskChar = '*') => {
|
||||||
if (!str || str.length <= keepStart + keepEnd) {
|
if (!str || str.length <= keepStart + keepEnd) return str
|
||||||
return str
|
|
||||||
}
|
|
||||||
const maskLen = Math.min(str.length - keepStart - keepEnd, 8)
|
const maskLen = Math.min(str.length - keepStart - keepEnd, 8)
|
||||||
return str.slice(0, keepStart) + maskChar.repeat(maskLen) + str.slice(-keepEnd)
|
return str.slice(0, keepStart) + maskChar.repeat(maskLen) + str.slice(-keepEnd)
|
||||||
}
|
}
|
||||||
@@ -288,8 +236,9 @@ const clamp = (value, min, max) => Math.min(Math.max(value, min), max)
|
|||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
// 获取时区偏移后的日期
|
// 获取时区偏移后的日期
|
||||||
const getDateInTimezone = (date = new Date(), offset = config.system?.timezoneOffset || 8) =>
|
const getDateInTimezone = (date = new Date(), offset = config.system?.timezoneOffset || 8) => {
|
||||||
new Date(date.getTime() + offset * 3600000)
|
return new Date(date.getTime() + offset * 3600000)
|
||||||
|
}
|
||||||
|
|
||||||
// 获取时区日期字符串 YYYY-MM-DD
|
// 获取时区日期字符串 YYYY-MM-DD
|
||||||
const getDateStringInTimezone = (date = new Date()) => {
|
const getDateStringInTimezone = (date = new Date()) => {
|
||||||
@@ -299,17 +248,13 @@ const getDateStringInTimezone = (date = new Date()) => {
|
|||||||
|
|
||||||
// 检查是否过期
|
// 检查是否过期
|
||||||
const isExpired = (expiresAt) => {
|
const isExpired = (expiresAt) => {
|
||||||
if (!expiresAt) {
|
if (!expiresAt) return false
|
||||||
return false
|
|
||||||
}
|
|
||||||
return new Date(expiresAt).getTime() < Date.now()
|
return new Date(expiresAt).getTime() < Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算剩余时间(秒)
|
// 计算剩余时间(秒)
|
||||||
const getTimeRemaining = (expiresAt) => {
|
const getTimeRemaining = (expiresAt) => {
|
||||||
if (!expiresAt) {
|
if (!expiresAt) return Infinity
|
||||||
return Infinity
|
|
||||||
}
|
|
||||||
return Math.max(0, Math.floor((new Date(expiresAt).getTime() - Date.now()) / 1000))
|
return Math.max(0, Math.floor((new Date(expiresAt).getTime() - Date.now()) / 1000))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user