mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
chore
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"keep": {
|
||||
"days": false,
|
||||
"amount": 5
|
||||
},
|
||||
"auditLog": "D:\\code\\github\\claude-relay-service\\logs\\.claude-relay-audit.log.json",
|
||||
"files": [
|
||||
{
|
||||
"date": 1766937830728,
|
||||
"name": "D:\\code\\github\\claude-relay-service\\logs\\claude-relay-2025-12-29.log",
|
||||
"hash": "d82c1d686e66788fe8083e7e0905e214a400c033e83cb6d2d30bfca72b079a5c"
|
||||
},
|
||||
{
|
||||
"date": 1767024423693,
|
||||
"name": "D:\\code\\github\\claude-relay-service\\logs\\claude-relay-2025-12-30.log",
|
||||
"hash": "22671b37716c6044051919421a80eedbb42fff28937b467ff9a58eefe70a084c"
|
||||
},
|
||||
{
|
||||
"date": 1767098022995,
|
||||
"name": "/mnt/d/code/github/claude-relay-service/logs/claude-relay-2025-12-30.log",
|
||||
"hash": "17b4d378f134d2d9d8a4e929b085d7e2121bcfced44b4ffe2860455b027bd2a7"
|
||||
},
|
||||
{
|
||||
"date": 1767110489197,
|
||||
"name": "/mnt/d/code/github/claude-relay-service/logs/claude-relay-2025-12-31.log",
|
||||
"hash": "29305b4cd76e3e64212c41de27ee5c44d1525e090e3a6d157a18d3f4a14cb633"
|
||||
},
|
||||
{
|
||||
"date": 1767197382349,
|
||||
"name": "/mnt/d/code/github/claude-relay-service/logs/claude-relay-2026-01-01.log",
|
||||
"hash": "57dea51b19fd2115b1b20ee5ecf1908ded5f709feb102c7210501b20786c1e35"
|
||||
}
|
||||
],
|
||||
"hashType": "sha256"
|
||||
}
|
||||
@@ -79,7 +79,8 @@ router.get('/api-keys/:keyId/cost-debug', authenticateAdmin, async (req, res) =>
|
||||
const costStats = await redis.getCostStats(keyId)
|
||||
const dailyCost = await redis.getDailyCost(keyId)
|
||||
const today = redis.getDateStringInTimezone()
|
||||
const client = redis.getClientSafe()
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const _client = redis.getClientSafe()
|
||||
|
||||
// 获取所有相关的Redis键
|
||||
const costKeys = await redis.scanKeys(`usage:cost:*:${keyId}:*`)
|
||||
@@ -298,7 +299,9 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
userIdsToFetch.map((id) => userService.getUserById(id, false).catch(() => null))
|
||||
)
|
||||
userIdsToFetch.forEach((id, i) => {
|
||||
if (users[i]) userMap.set(id, users[i])
|
||||
if (users[i]) {
|
||||
userMap.set(id, users[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ const bedrockAccountService = require('../../services/bedrockAccountService')
|
||||
const ccrAccountService = require('../../services/ccrAccountService')
|
||||
const geminiAccountService = require('../../services/geminiAccountService')
|
||||
const droidAccountService = require('../../services/droidAccountService')
|
||||
const openaiAccountService = require('../../services/openaiAccountService')
|
||||
// const openaiAccountService = require('../../services/openaiAccountService') // TODO: 未来用于OpenAI账户统计
|
||||
const openaiResponsesAccountService = require('../../services/openaiResponsesAccountService')
|
||||
const redis = require('../../models/redis')
|
||||
const { authenticateAdmin } = require('../../middleware/auth')
|
||||
const logger = require('../../utils/logger')
|
||||
const CostCalculator = require('../../utils/costCalculator')
|
||||
const pricingService = require('../../services/pricingService')
|
||||
// const pricingService = require('../../services/pricingService') // TODO: 未来用于成本计算
|
||||
const config = require('../../../config/config')
|
||||
|
||||
const router = express.Router()
|
||||
@@ -144,7 +144,9 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
||||
totalCacheReadTokensUsed += usage.cacheReadTokens || 0
|
||||
totalAllTokensUsed += usage.allTokens || 0
|
||||
}
|
||||
if (key.isActive) activeApiKeys++
|
||||
if (key.isActive) {
|
||||
activeApiKeys++
|
||||
}
|
||||
}
|
||||
|
||||
// 各平台账户统计(单次遍历)
|
||||
|
||||
@@ -157,7 +157,9 @@ router.get('/droid-accounts', authenticateAdmin, async (req, res) => {
|
||||
const groupToAccountIds = new Map()
|
||||
for (const [accountId, groups] of allGroupInfosMap) {
|
||||
for (const group of groups) {
|
||||
if (!groupToAccountIds.has(group.id)) groupToAccountIds.set(group.id, [])
|
||||
if (!groupToAccountIds.has(group.id)) {
|
||||
groupToAccountIds.set(group.id, [])
|
||||
}
|
||||
groupToAccountIds.get(group.id).push(accountId)
|
||||
}
|
||||
}
|
||||
@@ -167,7 +169,9 @@ router.get('/droid-accounts', authenticateAdmin, async (req, res) => {
|
||||
const groupBindingCount = new Map()
|
||||
for (const key of allApiKeys) {
|
||||
const binding = key.droidAccountId
|
||||
if (!binding) continue
|
||||
if (!binding) {
|
||||
continue
|
||||
}
|
||||
if (binding.startsWith('group:')) {
|
||||
const groupId = binding.substring('group:'.length)
|
||||
groupBindingCount.set(groupId, (groupBindingCount.get(groupId) || 0) + 1)
|
||||
|
||||
@@ -46,7 +46,9 @@ router.get('/gemini-api-accounts', authenticateAdmin, async (req, res) => {
|
||||
const bindingCountMap = new Map()
|
||||
for (const key of allApiKeys) {
|
||||
const binding = key.geminiAccountId
|
||||
if (!binding) continue
|
||||
if (!binding) {
|
||||
continue
|
||||
}
|
||||
// 处理 api: 前缀
|
||||
const accountId = binding.startsWith('api:') ? binding.substring(4) : binding
|
||||
bindingCountMap.set(accountId, (bindingCountMap.get(accountId) || 0) + 1)
|
||||
|
||||
@@ -54,7 +54,9 @@ router.get('/openai-responses-accounts', authenticateAdmin, async (req, res) =>
|
||||
const bindingCountMap = new Map()
|
||||
for (const key of allApiKeys) {
|
||||
const binding = key.openaiAccountId
|
||||
if (!binding) continue
|
||||
if (!binding) {
|
||||
continue
|
||||
}
|
||||
// 处理 responses: 前缀
|
||||
const accountId = binding.startsWith('responses:') ? binding.substring(10) : binding
|
||||
bindingCountMap.set(accountId, (bindingCountMap.get(accountId) || 0) + 1)
|
||||
|
||||
@@ -64,14 +64,18 @@ async function getUsageDataByIndex(indexKey, keyPattern, scanPattern) {
|
||||
const match =
|
||||
k.match(/usage:([^:]+):model:daily:(.+):\d{4}-\d{2}-\d{2}$/) ||
|
||||
k.match(/usage:([^:]+):model:hourly:(.+):\d{4}-\d{2}-\d{2}:\d{2}$/)
|
||||
if (match) return `${match[1]}:${match[2]}`
|
||||
if (match) {
|
||||
return `${match[1]}:${match[2]}`
|
||||
}
|
||||
}
|
||||
if (keyPattern.includes('{accountId}') && keyPattern.includes('{model}')) {
|
||||
// account_usage:model:daily 或 hourly
|
||||
const match =
|
||||
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}$/)
|
||||
if (match) return `${match[1]}:${match[2]}`
|
||||
if (match) {
|
||||
return `${match[1]}:${match[2]}`
|
||||
}
|
||||
}
|
||||
// 通用格式:提取最后一个 : 前的 id
|
||||
const parts = k.split(':')
|
||||
@@ -537,7 +541,7 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
|
||||
}
|
||||
|
||||
// 使用索引获取数据,按小时批量查询
|
||||
const dates = [...dateSet]
|
||||
const _dates = [...dateSet]
|
||||
const modelDataMap = new Map()
|
||||
const usageDataMap = new Map()
|
||||
|
||||
@@ -571,7 +575,9 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
|
||||
const match = key.match(/usage:model:hourly:.+?:(\d{4}-\d{2}-\d{2}:\d{2})/)
|
||||
if (match) {
|
||||
const hourKey = match[1]
|
||||
if (!modelKeysByHour.has(hourKey)) modelKeysByHour.set(hourKey, [])
|
||||
if (!modelKeysByHour.has(hourKey)) {
|
||||
modelKeysByHour.set(hourKey, [])
|
||||
}
|
||||
modelKeysByHour.get(hourKey).push(key)
|
||||
}
|
||||
}
|
||||
@@ -579,7 +585,9 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
|
||||
const match = key.match(/usage:hourly:.+?:(\d{4}-\d{2}-\d{2}:\d{2})/)
|
||||
if (match) {
|
||||
const hourKey = match[1]
|
||||
if (!usageKeysByHour.has(hourKey)) usageKeysByHour.set(hourKey, [])
|
||||
if (!usageKeysByHour.has(hourKey)) {
|
||||
usageKeysByHour.set(hourKey, [])
|
||||
}
|
||||
usageKeysByHour.get(hourKey).push(key)
|
||||
}
|
||||
}
|
||||
@@ -599,11 +607,15 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
|
||||
// 处理模型级别数据
|
||||
for (const modelKey of modelKeys) {
|
||||
const modelMatch = modelKey.match(/usage:model:hourly:(.+?):\d{4}-\d{2}-\d{2}:\d{2}/)
|
||||
if (!modelMatch) continue
|
||||
if (!modelMatch) {
|
||||
continue
|
||||
}
|
||||
|
||||
const model = modelMatch[1]
|
||||
const data = modelDataMap.get(modelKey)
|
||||
if (!data || Object.keys(data).length === 0) continue
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const modelInputTokens = parseInt(data.inputTokens) || 0
|
||||
const modelOutputTokens = parseInt(data.outputTokens) || 0
|
||||
@@ -710,7 +722,9 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
|
||||
const match = key.match(/usage:model:daily:.+?:(\d{4}-\d{2}-\d{2})/)
|
||||
if (match) {
|
||||
const dateStr = match[1]
|
||||
if (!modelKeysByDate.has(dateStr)) modelKeysByDate.set(dateStr, [])
|
||||
if (!modelKeysByDate.has(dateStr)) {
|
||||
modelKeysByDate.set(dateStr, [])
|
||||
}
|
||||
modelKeysByDate.get(dateStr).push(key)
|
||||
}
|
||||
}
|
||||
@@ -718,7 +732,9 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
|
||||
const match = key.match(/usage:daily:.+?:(\d{4}-\d{2}-\d{2})/)
|
||||
if (match) {
|
||||
const dateStr = match[1]
|
||||
if (!usageKeysByDate.has(dateStr)) usageKeysByDate.set(dateStr, [])
|
||||
if (!usageKeysByDate.has(dateStr)) {
|
||||
usageKeysByDate.set(dateStr, [])
|
||||
}
|
||||
usageKeysByDate.get(dateStr).push(key)
|
||||
}
|
||||
}
|
||||
@@ -738,11 +754,15 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
|
||||
// 处理模型级别数据
|
||||
for (const modelKey of modelKeys) {
|
||||
const modelMatch = modelKey.match(/usage:model:daily:(.+?):\d{4}-\d{2}-\d{2}/)
|
||||
if (!modelMatch) continue
|
||||
if (!modelMatch) {
|
||||
continue
|
||||
}
|
||||
|
||||
const model = modelMatch[1]
|
||||
const data = modelDataMap.get(modelKey)
|
||||
if (!data || Object.keys(data).length === 0) continue
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const modelInputTokens = parseInt(data.inputTokens) || 0
|
||||
const modelOutputTokens = parseInt(data.outputTokens) || 0
|
||||
@@ -827,7 +847,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}`
|
||||
)
|
||||
|
||||
const client = redis.getClientSafe()
|
||||
const _client = redis.getClientSafe()
|
||||
const today = redis.getDateStringInTimezone()
|
||||
const tzDate = redis.getDateInTimezone()
|
||||
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(
|
||||
@@ -895,9 +915,13 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) =
|
||||
for (const results of allResults) {
|
||||
for (const { key, data } of results) {
|
||||
// 过滤出属于该 keyId 的记录
|
||||
if (!key.startsWith(`usage:${keyId}:model:`)) continue
|
||||
if (!key.startsWith(`usage:${keyId}:model:`)) {
|
||||
continue
|
||||
}
|
||||
const match = key.match(/usage:.+:model:daily:(.+):\d{4}-\d{2}-\d{2}$/)
|
||||
if (!match) continue
|
||||
if (!match) {
|
||||
continue
|
||||
}
|
||||
const model = match[1]
|
||||
if (!modelStatsMap.has(model)) {
|
||||
modelStatsMap.set(model, {
|
||||
@@ -933,11 +957,15 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) =
|
||||
results = await redis.scanAndGetAllChunked(pattern)
|
||||
}
|
||||
for (const { key, data } of results) {
|
||||
if (!key.startsWith(`usage:${keyId}:model:`)) continue
|
||||
if (!key.startsWith(`usage:${keyId}:model:`)) {
|
||||
continue
|
||||
}
|
||||
const match =
|
||||
key.match(/usage:.+:model:daily:(.+):\d{4}-\d{2}-\d{2}$/) ||
|
||||
key.match(/usage:.+:model:monthly:(.+):\d{4}-\d{2}$/)
|
||||
if (!match) continue
|
||||
if (!match) {
|
||||
continue
|
||||
}
|
||||
const model = match[1]
|
||||
if (!modelStatsMap.has(model)) {
|
||||
modelStatsMap.set(model, {
|
||||
@@ -1255,7 +1283,7 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
}
|
||||
|
||||
// 按小时获取 account_usage 数据(避免全库扫描)
|
||||
const dates = [...dateSet]
|
||||
const _dates = [...dateSet]
|
||||
const usageDataMap = new Map()
|
||||
const modelDataMap = new Map()
|
||||
|
||||
@@ -1289,7 +1317,9 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
const match = key.match(/account_usage:hourly:.+?:(\d{4}-\d{2}-\d{2}:\d{2})/)
|
||||
if (match) {
|
||||
const hourKey = match[1]
|
||||
if (!usageKeysByHour.has(hourKey)) usageKeysByHour.set(hourKey, [])
|
||||
if (!usageKeysByHour.has(hourKey)) {
|
||||
usageKeysByHour.set(hourKey, [])
|
||||
}
|
||||
usageKeysByHour.get(hourKey).push(key)
|
||||
}
|
||||
}
|
||||
@@ -1299,7 +1329,9 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
const accountId = match[1]
|
||||
const hourKey = match[2]
|
||||
const mapKey = `${accountId}:${hourKey}`
|
||||
if (!modelKeysByHour.has(mapKey)) modelKeysByHour.set(mapKey, [])
|
||||
if (!modelKeysByHour.has(mapKey)) {
|
||||
modelKeysByHour.set(mapKey, [])
|
||||
}
|
||||
modelKeysByHour.get(mapKey).push(key)
|
||||
}
|
||||
}
|
||||
@@ -1316,13 +1348,19 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
|
||||
for (const key of usageKeys) {
|
||||
const match = key.match(/account_usage:hourly:(.+?):\d{4}-\d{2}-\d{2}:\d{2}/)
|
||||
if (!match) continue
|
||||
if (!match) {
|
||||
continue
|
||||
}
|
||||
|
||||
const accountId = match[1]
|
||||
if (!accountIdSet.has(accountId)) continue
|
||||
if (!accountIdSet.has(accountId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const data = usageDataMap.get(key)
|
||||
if (!data) continue
|
||||
if (!data) {
|
||||
continue
|
||||
}
|
||||
|
||||
const inputTokens = parseInt(data.inputTokens) || 0
|
||||
const outputTokens = parseInt(data.outputTokens) || 0
|
||||
@@ -1338,10 +1376,14 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
const modelKeys = modelKeysByHour.get(`${accountId}:${hourInfo.hourKey}`) || []
|
||||
for (const modelKey of modelKeys) {
|
||||
const modelData = modelDataMap.get(modelKey)
|
||||
if (!modelData) continue
|
||||
if (!modelData) {
|
||||
continue
|
||||
}
|
||||
|
||||
const parts = modelKey.split(':')
|
||||
if (parts.length < 5) continue
|
||||
if (parts.length < 5) {
|
||||
continue
|
||||
}
|
||||
|
||||
const modelName = parts[4]
|
||||
const usage = {
|
||||
@@ -1434,7 +1476,9 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
const match = key.match(/account_usage:daily:.+?:(\d{4}-\d{2}-\d{2})/)
|
||||
if (match) {
|
||||
const dateStr = match[1]
|
||||
if (!usageKeysByDate.has(dateStr)) usageKeysByDate.set(dateStr, [])
|
||||
if (!usageKeysByDate.has(dateStr)) {
|
||||
usageKeysByDate.set(dateStr, [])
|
||||
}
|
||||
usageKeysByDate.get(dateStr).push(key)
|
||||
}
|
||||
}
|
||||
@@ -1444,7 +1488,9 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
const accountId = match[1]
|
||||
const dateStr = match[2]
|
||||
const mapKey = `${accountId}:${dateStr}`
|
||||
if (!modelKeysByDate.has(mapKey)) modelKeysByDate.set(mapKey, [])
|
||||
if (!modelKeysByDate.has(mapKey)) {
|
||||
modelKeysByDate.set(mapKey, [])
|
||||
}
|
||||
modelKeysByDate.get(mapKey).push(key)
|
||||
}
|
||||
}
|
||||
@@ -1460,13 +1506,19 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
|
||||
for (const key of usageKeys) {
|
||||
const match = key.match(/account_usage:daily:(.+?):\d{4}-\d{2}-\d{2}/)
|
||||
if (!match) continue
|
||||
if (!match) {
|
||||
continue
|
||||
}
|
||||
|
||||
const accountId = match[1]
|
||||
if (!accountIdSet.has(accountId)) continue
|
||||
if (!accountIdSet.has(accountId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const data = usageDataMap.get(key)
|
||||
if (!data) continue
|
||||
if (!data) {
|
||||
continue
|
||||
}
|
||||
|
||||
const inputTokens = parseInt(data.inputTokens) || 0
|
||||
const outputTokens = parseInt(data.outputTokens) || 0
|
||||
@@ -1482,10 +1534,14 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
const modelKeys = modelKeysByDate.get(`${accountId}:${dayInfo.dateStr}`) || []
|
||||
for (const modelKey of modelKeys) {
|
||||
const modelData = modelDataMap.get(modelKey)
|
||||
if (!modelData) continue
|
||||
if (!modelData) {
|
||||
continue
|
||||
}
|
||||
|
||||
const parts = modelKey.split(':')
|
||||
if (parts.length < 5) continue
|
||||
if (parts.length < 5) {
|
||||
continue
|
||||
}
|
||||
|
||||
const modelName = parts[4]
|
||||
const usage = {
|
||||
@@ -1613,7 +1669,7 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
}
|
||||
|
||||
// 使用索引获取数据,按小时批量查询
|
||||
const dates = [...dateSet]
|
||||
const _dates = [...dateSet]
|
||||
const usageDataMap = new Map()
|
||||
const modelDataMap = new Map()
|
||||
|
||||
@@ -1646,7 +1702,9 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
const match = key.match(/usage:hourly:.+?:(\d{4}-\d{2}-\d{2}:\d{2})/)
|
||||
if (match) {
|
||||
const hourKey = match[1]
|
||||
if (!usageKeysByHour.has(hourKey)) usageKeysByHour.set(hourKey, [])
|
||||
if (!usageKeysByHour.has(hourKey)) {
|
||||
usageKeysByHour.set(hourKey, [])
|
||||
}
|
||||
usageKeysByHour.get(hourKey).push(key)
|
||||
}
|
||||
}
|
||||
@@ -1654,7 +1712,9 @@ 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})/)
|
||||
if (match) {
|
||||
const hourKey = match[1]
|
||||
if (!modelKeysByHour.has(hourKey)) modelKeysByHour.set(hourKey, [])
|
||||
if (!modelKeysByHour.has(hourKey)) {
|
||||
modelKeysByHour.set(hourKey, [])
|
||||
}
|
||||
modelKeysByHour.get(hourKey).push(key)
|
||||
}
|
||||
}
|
||||
@@ -1674,11 +1734,15 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
const apiKeyDataMap = new Map()
|
||||
for (const key of hourUsageKeys) {
|
||||
const match = key.match(/usage:hourly:(.+?):\d{4}-\d{2}-\d{2}:\d{2}/)
|
||||
if (!match) continue
|
||||
if (!match) {
|
||||
continue
|
||||
}
|
||||
|
||||
const apiKeyId = match[1]
|
||||
const data = usageDataMap.get(key)
|
||||
if (!data || !apiKeyMap.has(apiKeyId)) continue
|
||||
if (!data || !apiKeyMap.has(apiKeyId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const inputTokens = parseInt(data.inputTokens) || 0
|
||||
const outputTokens = parseInt(data.outputTokens) || 0
|
||||
@@ -1700,12 +1764,16 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
const apiKeyCostMap = new Map()
|
||||
for (const modelKey of hourModelKeys) {
|
||||
const match = modelKey.match(/usage:(.+?):model:hourly:(.+?):\d{4}-\d{2}-\d{2}:\d{2}/)
|
||||
if (!match) continue
|
||||
if (!match) {
|
||||
continue
|
||||
}
|
||||
|
||||
const apiKeyId = match[1]
|
||||
const model = match[2]
|
||||
const modelData = modelDataMap.get(modelKey)
|
||||
if (!modelData || !apiKeyDataMap.has(apiKeyId)) continue
|
||||
if (!modelData || !apiKeyDataMap.has(apiKeyId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const usage = {
|
||||
input_tokens: parseInt(modelData.inputTokens) || 0,
|
||||
@@ -1795,7 +1863,9 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
const match = key.match(/usage:daily:.+?:(\d{4}-\d{2}-\d{2})/)
|
||||
if (match) {
|
||||
const dateStr = match[1]
|
||||
if (!usageKeysByDate.has(dateStr)) usageKeysByDate.set(dateStr, [])
|
||||
if (!usageKeysByDate.has(dateStr)) {
|
||||
usageKeysByDate.set(dateStr, [])
|
||||
}
|
||||
usageKeysByDate.get(dateStr).push(key)
|
||||
}
|
||||
}
|
||||
@@ -1803,7 +1873,9 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
const match = key.match(/usage:.+?:model:daily:.+?:(\d{4}-\d{2}-\d{2})/)
|
||||
if (match) {
|
||||
const dateStr = match[1]
|
||||
if (!modelKeysByDate.has(dateStr)) modelKeysByDate.set(dateStr, [])
|
||||
if (!modelKeysByDate.has(dateStr)) {
|
||||
modelKeysByDate.set(dateStr, [])
|
||||
}
|
||||
modelKeysByDate.get(dateStr).push(key)
|
||||
}
|
||||
}
|
||||
@@ -1822,11 +1894,15 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
const apiKeyDataMap = new Map()
|
||||
for (const key of dayUsageKeys) {
|
||||
const match = key.match(/usage:daily:(.+?):\d{4}-\d{2}-\d{2}/)
|
||||
if (!match) continue
|
||||
if (!match) {
|
||||
continue
|
||||
}
|
||||
|
||||
const apiKeyId = match[1]
|
||||
const data = usageDataMap.get(key)
|
||||
if (!data || !apiKeyMap.has(apiKeyId)) continue
|
||||
if (!data || !apiKeyMap.has(apiKeyId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const inputTokens = parseInt(data.inputTokens) || 0
|
||||
const outputTokens = parseInt(data.outputTokens) || 0
|
||||
@@ -1848,12 +1924,16 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
const apiKeyCostMap = new Map()
|
||||
for (const modelKey of dayModelKeys) {
|
||||
const match = modelKey.match(/usage:(.+?):model:daily:(.+?):\d{4}-\d{2}-\d{2}/)
|
||||
if (!match) continue
|
||||
if (!match) {
|
||||
continue
|
||||
}
|
||||
|
||||
const apiKeyId = match[1]
|
||||
const model = match[2]
|
||||
const modelData = modelDataMap.get(modelKey)
|
||||
if (!modelData || !apiKeyDataMap.has(apiKeyId)) continue
|
||||
if (!modelData || !apiKeyDataMap.has(apiKeyId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const usage = {
|
||||
input_tokens: parseInt(modelData.inputTokens) || 0,
|
||||
@@ -1972,7 +2052,7 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
|
||||
const modelCosts = {}
|
||||
|
||||
// 按模型统计费用
|
||||
const client = redis.getClientSafe()
|
||||
const _client = redis.getClientSafe()
|
||||
const today = redis.getDateStringInTimezone()
|
||||
const tzDate = redis.getDateInTimezone()
|
||||
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(
|
||||
@@ -1980,11 +2060,11 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
|
||||
'0'
|
||||
)}`
|
||||
|
||||
let pattern
|
||||
let _pattern
|
||||
if (period === 'today') {
|
||||
pattern = `usage:model:daily:*:${today}`
|
||||
_pattern = `usage:model:daily:*:${today}`
|
||||
} else if (period === 'monthly') {
|
||||
pattern = `usage:model:monthly:*:${currentMonth}`
|
||||
_pattern = `usage:model:monthly:*:${currentMonth}`
|
||||
} else if (period === '7days') {
|
||||
// 最近7天:汇总daily数据(使用 SCAN + Pipeline 优化)
|
||||
const modelUsageMap = new Map()
|
||||
@@ -2014,10 +2094,14 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
|
||||
|
||||
// 处理数据
|
||||
for (const { key, data } of allData) {
|
||||
if (!data) continue
|
||||
if (!data) {
|
||||
continue
|
||||
}
|
||||
|
||||
const modelMatch = key.match(/usage:model:daily:(.+):\d{4}-\d{2}-\d{2}$/)
|
||||
if (!modelMatch) continue
|
||||
if (!modelMatch) {
|
||||
continue
|
||||
}
|
||||
|
||||
const rawModel = modelMatch[1]
|
||||
const normalizedModel = normalizeModelName(rawModel)
|
||||
@@ -2112,10 +2196,14 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
|
||||
const modelUsageMap = new Map()
|
||||
|
||||
for (const { key, data } of allData) {
|
||||
if (!data) continue
|
||||
if (!data) {
|
||||
continue
|
||||
}
|
||||
|
||||
const modelMatch = key.match(/usage:model:monthly:(.+):(\d{4}-\d{2})$/)
|
||||
if (!modelMatch) continue
|
||||
if (!modelMatch) {
|
||||
continue
|
||||
}
|
||||
|
||||
const model = modelMatch[1]
|
||||
|
||||
@@ -2241,10 +2329,14 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
|
||||
: /usage:model:monthly:(.+):\d{4}-\d{2}$/
|
||||
|
||||
for (const { key, data } of allData) {
|
||||
if (!data) continue
|
||||
if (!data) {
|
||||
continue
|
||||
}
|
||||
|
||||
const match = key.match(regex)
|
||||
if (!match) continue
|
||||
if (!match) {
|
||||
continue
|
||||
}
|
||||
|
||||
const model = match[1]
|
||||
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 today = redis.getDateStringInTimezone()
|
||||
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的数据
|
||||
const client = redis.getClientSafe()
|
||||
const _client = redis.getClientSafe()
|
||||
// 使用与管理页面相同的时区处理逻辑
|
||||
const tzDate = redis.getDateInTimezone()
|
||||
const today = redis.getDateStringInTimezone()
|
||||
|
||||
@@ -18,7 +18,9 @@ class AccountGroupService {
|
||||
async ensureReverseIndexes() {
|
||||
try {
|
||||
const client = redis.getClientSafe()
|
||||
if (!client) return
|
||||
if (!client) {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否已迁移
|
||||
const migrated = await client.get(this.REVERSE_INDEX_MIGRATED_KEY)
|
||||
@@ -39,10 +41,14 @@ class AccountGroupService {
|
||||
|
||||
for (const groupId of allGroupIds) {
|
||||
const group = await client.hgetall(`${this.GROUP_PREFIX}${groupId}`)
|
||||
if (!group || !group.platform) continue
|
||||
if (!group || !group.platform) {
|
||||
continue
|
||||
}
|
||||
|
||||
const members = await client.smembers(`${this.GROUP_MEMBERS_PREFIX}${groupId}`)
|
||||
if (members.length === 0) continue
|
||||
if (members.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const pipeline = client.pipeline()
|
||||
for (const accountId of members) {
|
||||
|
||||
@@ -71,7 +71,9 @@ class ApiKeyIndexService {
|
||||
* 扫描所有 API Key,确保 hash -> keyId 映射存在
|
||||
*/
|
||||
async rebuildHashMap() {
|
||||
if (!this.redis) return
|
||||
if (!this.redis) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const client = this.redis.getClientSafe()
|
||||
@@ -187,7 +189,9 @@ class ApiKeyIndexService {
|
||||
const pipeline = client.pipeline()
|
||||
|
||||
for (const apiKey of apiKeys) {
|
||||
if (!apiKey || !apiKey.id) continue
|
||||
if (!apiKey || !apiKey.id) {
|
||||
continue
|
||||
}
|
||||
|
||||
const keyId = apiKey.id
|
||||
const createdAt = apiKey.createdAt ? new Date(apiKey.createdAt).getTime() : 0
|
||||
@@ -249,7 +253,9 @@ class ApiKeyIndexService {
|
||||
* 添加单个 API Key 到索引
|
||||
*/
|
||||
async addToIndex(apiKey) {
|
||||
if (!this.redis || !apiKey || !apiKey.id) return
|
||||
if (!this.redis || !apiKey || !apiKey.id) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const client = this.redis.getClientSafe()
|
||||
@@ -297,7 +303,9 @@ class ApiKeyIndexService {
|
||||
* 更新索引(状态、名称、标签变化时调用)
|
||||
*/
|
||||
async updateIndex(keyId, updates, oldData = {}) {
|
||||
if (!this.redis || !keyId) return
|
||||
if (!this.redis || !keyId) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const client = this.redis.getClientSafe()
|
||||
@@ -376,7 +384,9 @@ class ApiKeyIndexService {
|
||||
* 从索引中移除 API Key
|
||||
*/
|
||||
async removeFromIndex(keyId, oldData = {}) {
|
||||
if (!this.redis || !keyId) return
|
||||
if (!this.redis || !keyId) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const client = this.redis.getClientSafe()
|
||||
@@ -598,7 +608,9 @@ class ApiKeyIndexService {
|
||||
* 更新 lastUsedAt 索引(供 recordUsage 调用)
|
||||
*/
|
||||
async updateLastUsedAt(keyId, lastUsedAt) {
|
||||
if (!this.redis || !keyId) return
|
||||
if (!this.redis || !keyId) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const client = this.redis.getClientSafe()
|
||||
|
||||
@@ -921,7 +921,9 @@ class ApiKeyService {
|
||||
return keyIds
|
||||
.map((id, i) => {
|
||||
const [err, fields] = results[i]
|
||||
if (err) return null
|
||||
if (err) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
id,
|
||||
claudeAccountId: fields[0] || null,
|
||||
|
||||
@@ -127,7 +127,7 @@ class BedrockAccountService {
|
||||
// 📋 获取所有账户列表
|
||||
async getAllAccounts() {
|
||||
try {
|
||||
const client = redis.getClientSafe()
|
||||
const _client = redis.getClientSafe()
|
||||
const accountIds = await redis.getAllIdsByIndex(
|
||||
'bedrock_account:index',
|
||||
'bedrock_account:*',
|
||||
|
||||
@@ -2,7 +2,7 @@ const { v4: uuidv4 } = require('uuid')
|
||||
const ProxyHelper = require('../utils/proxyHelper')
|
||||
const redis = require('../models/redis')
|
||||
const logger = require('../utils/logger')
|
||||
const config = require('../../config/config')
|
||||
// const config = require('../../config/config')
|
||||
const { createEncryptor } = require('../utils/commonHelper')
|
||||
|
||||
class CcrAccountService {
|
||||
|
||||
@@ -2,7 +2,7 @@ const { v4: uuidv4 } = require('uuid')
|
||||
const crypto = require('crypto')
|
||||
const axios = require('axios')
|
||||
const redis = require('../models/redis')
|
||||
const config = require('../../config/config')
|
||||
// const config = require('../../config/config')
|
||||
const logger = require('../utils/logger')
|
||||
const { maskToken } = require('../utils/tokenMask')
|
||||
const ProxyHelper = require('../utils/proxyHelper')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const redisClient = require('../models/redis')
|
||||
const { v4: uuidv4 } = require('uuid')
|
||||
const https = require('https')
|
||||
const config = require('../../config/config')
|
||||
// const config = require('../../config/config')
|
||||
const logger = require('../utils/logger')
|
||||
const { OAuth2Client } = require('google-auth-library')
|
||||
const { maskToken } = require('../utils/tokenMask')
|
||||
@@ -19,6 +19,8 @@ const { createEncryptor } = require('../utils/commonHelper')
|
||||
// Gemini 账户键前缀
|
||||
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 凭据
|
||||
const OAUTH_CLIENT_ID = '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com'
|
||||
const OAUTH_CLIENT_SECRET = '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'
|
||||
@@ -572,7 +574,7 @@ async function deleteAccount(accountId) {
|
||||
|
||||
// 获取所有账户
|
||||
async function getAllAccounts() {
|
||||
const client = redisClient.getClientSafe()
|
||||
const _client = redisClient.getClientSafe()
|
||||
const accountIds = await redisClient.getAllIdsByIndex(
|
||||
'gemini_account:index',
|
||||
`${GEMINI_ACCOUNT_KEY_PREFIX}*`,
|
||||
|
||||
@@ -666,7 +666,7 @@ async function deleteAccount(accountId) {
|
||||
|
||||
// 获取所有账户
|
||||
async function getAllAccounts() {
|
||||
const client = redisClient.getClientSafe()
|
||||
const _client = redisClient.getClientSafe()
|
||||
const accountIds = await redisClient.getAllIdsByIndex(
|
||||
'openai:account:index',
|
||||
`${OPENAI_ACCOUNT_KEY_PREFIX}*`,
|
||||
|
||||
@@ -201,7 +201,9 @@ class OpenAIResponsesAccountService {
|
||||
`${this.ACCOUNT_KEY_PREFIX}*`,
|
||||
/^openai_responses_account:(.+)$/
|
||||
)
|
||||
if (accountIds.length === 0) return []
|
||||
if (accountIds.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const keys = accountIds.map((id) => `${this.ACCOUNT_KEY_PREFIX}${id}`)
|
||||
// Pipeline 批量查询所有账户数据
|
||||
@@ -210,11 +212,15 @@ class OpenAIResponsesAccountService {
|
||||
const results = await pipeline.exec()
|
||||
|
||||
const accounts = []
|
||||
results.forEach(([err, accountData], index) => {
|
||||
if (err || !accountData || !accountData.id) return
|
||||
results.forEach(([err, accountData], _index) => {
|
||||
if (err || !accountData || !accountData.id) {
|
||||
return
|
||||
}
|
||||
|
||||
// 过滤非活跃账户
|
||||
if (!includeInactive && accountData.isActive !== 'true') return
|
||||
if (!includeInactive && accountData.isActive !== 'true') {
|
||||
return
|
||||
}
|
||||
|
||||
// 隐藏敏感信息
|
||||
accountData.apiKey = '***'
|
||||
|
||||
@@ -17,33 +17,45 @@ const _encryptorCache = new Map()
|
||||
|
||||
// 创建加密器实例(每个 salt 独立缓存)
|
||||
const createEncryptor = (salt) => {
|
||||
if (_encryptorCache.has(salt)) return _encryptorCache.get(salt)
|
||||
if (_encryptorCache.has(salt)) {
|
||||
return _encryptorCache.get(salt)
|
||||
}
|
||||
|
||||
let keyCache = null
|
||||
const decryptCache = new LRUCache(500)
|
||||
|
||||
const getKey = () => {
|
||||
if (!keyCache) keyCache = crypto.scryptSync(config.security.encryptionKey, salt, 32)
|
||||
if (!keyCache) {
|
||||
keyCache = crypto.scryptSync(config.security.encryptionKey, salt, 32)
|
||||
}
|
||||
return keyCache
|
||||
}
|
||||
|
||||
const encrypt = (text) => {
|
||||
if (!text) return ''
|
||||
if (!text) {
|
||||
return ''
|
||||
}
|
||||
const key = getKey()
|
||||
const iv = crypto.randomBytes(IV_LENGTH)
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
|
||||
let encrypted = cipher.update(text, 'utf8', 'hex')
|
||||
encrypted += cipher.final('hex')
|
||||
return iv.toString('hex') + ':' + encrypted
|
||||
return `${iv.toString('hex')}:${encrypted}`
|
||||
}
|
||||
|
||||
const decrypt = (text, useCache = true) => {
|
||||
if (!text) return ''
|
||||
if (!text.includes(':')) return text
|
||||
if (!text) {
|
||||
return ''
|
||||
}
|
||||
if (!text.includes(':')) {
|
||||
return text
|
||||
}
|
||||
const cacheKey = crypto.createHash('sha256').update(text).digest('hex')
|
||||
if (useCache) {
|
||||
const cached = decryptCache.get(cacheKey)
|
||||
if (cached !== undefined) return cached
|
||||
if (cached !== undefined) {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
try {
|
||||
const key = getKey()
|
||||
@@ -52,7 +64,9 @@ const createEncryptor = (salt) => {
|
||||
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)
|
||||
let decrypted = decipher.update(encrypted, 'hex', 'utf8')
|
||||
decrypted += decipher.final('utf8')
|
||||
if (useCache) decryptCache.set(cacheKey, decrypted, 5 * 60 * 1000)
|
||||
if (useCache) {
|
||||
decryptCache.set(cacheKey, decrypted, 5 * 60 * 1000)
|
||||
}
|
||||
return decrypted
|
||||
} catch (e) {
|
||||
return text
|
||||
@@ -73,8 +87,8 @@ const createEncryptor = (salt) => {
|
||||
|
||||
// 默认加密器(向后兼容)
|
||||
const defaultEncryptor = createEncryptor('claude-relay-salt')
|
||||
const encrypt = defaultEncryptor.encrypt
|
||||
const decrypt = defaultEncryptor.decrypt
|
||||
const { encrypt } = defaultEncryptor
|
||||
const { decrypt } = defaultEncryptor
|
||||
const getEncryptionKey = defaultEncryptor.getKey
|
||||
const clearDecryptCache = defaultEncryptor.clearCache
|
||||
const getDecryptCacheStats = defaultEncryptor.getStats
|
||||
@@ -90,7 +104,7 @@ const toBoolean = (value) =>
|
||||
(typeof value === 'string' && value.toLowerCase() === 'true')
|
||||
|
||||
// 检查是否为真值(null/undefined 返回 false)
|
||||
const isTruthy = (value) => value != null && toBoolean(value)
|
||||
const isTruthy = (value) => value !== null && toBoolean(value)
|
||||
|
||||
// 检查是否可调度(默认 true,只有明确 false 才返回 false)
|
||||
const isSchedulable = (value) => value !== false && value !== 'false'
|
||||
@@ -100,8 +114,12 @@ const isActive = (value) => value === true || value === 'true'
|
||||
|
||||
// 检查账户是否健康(激活且状态正常)
|
||||
const isAccountHealthy = (account) => {
|
||||
if (!account) return false
|
||||
if (!isTruthy(account.isActive)) return false
|
||||
if (!account) {
|
||||
return false
|
||||
}
|
||||
if (!isTruthy(account.isActive)) {
|
||||
return false
|
||||
}
|
||||
const status = (account.status || 'active').toLowerCase()
|
||||
return !['error', 'unauthorized', 'blocked', 'temp_error'].includes(status)
|
||||
}
|
||||
@@ -112,7 +130,9 @@ const isAccountHealthy = (account) => {
|
||||
|
||||
// 安全解析 JSON
|
||||
const safeParseJson = (value, fallback = null) => {
|
||||
if (!value || typeof value !== 'string') return fallback
|
||||
if (!value || typeof value !== 'string') {
|
||||
return fallback
|
||||
}
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch {
|
||||
@@ -138,7 +158,9 @@ const safeParseJsonArray = (value, fallback = []) => {
|
||||
|
||||
// 规范化模型名称(用于统计聚合)
|
||||
const normalizeModelName = (model) => {
|
||||
if (!model || model === 'unknown') return model
|
||||
if (!model || model === 'unknown') {
|
||||
return model
|
||||
}
|
||||
// Bedrock 模型: us-east-1.anthropic.claude-3-5-sonnet-v1:0
|
||||
if (model.includes('.anthropic.') || model.includes('.claude')) {
|
||||
return model
|
||||
@@ -151,26 +173,38 @@ const normalizeModelName = (model) => {
|
||||
|
||||
// 规范化端点类型
|
||||
const normalizeEndpointType = (endpointType) => {
|
||||
if (!endpointType) return 'anthropic'
|
||||
if (!endpointType) {
|
||||
return 'anthropic'
|
||||
}
|
||||
const normalized = String(endpointType).toLowerCase()
|
||||
return ['openai', 'comm', 'anthropic'].includes(normalized) ? normalized : 'anthropic'
|
||||
}
|
||||
|
||||
// 检查模型是否在映射表中
|
||||
const isModelInMapping = (modelMapping, requestedModel) => {
|
||||
if (!modelMapping || Object.keys(modelMapping).length === 0) return true
|
||||
if (Object.prototype.hasOwnProperty.call(modelMapping, requestedModel)) return true
|
||||
if (!modelMapping || Object.keys(modelMapping).length === 0) {
|
||||
return true
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(modelMapping, requestedModel)) {
|
||||
return true
|
||||
}
|
||||
const lower = requestedModel.toLowerCase()
|
||||
return Object.keys(modelMapping).some((k) => k.toLowerCase() === lower)
|
||||
}
|
||||
|
||||
// 获取映射后的模型名称
|
||||
const getMappedModelName = (modelMapping, requestedModel) => {
|
||||
if (!modelMapping || Object.keys(modelMapping).length === 0) return requestedModel
|
||||
if (modelMapping[requestedModel]) return modelMapping[requestedModel]
|
||||
if (!modelMapping || Object.keys(modelMapping).length === 0) {
|
||||
return requestedModel
|
||||
}
|
||||
if (modelMapping[requestedModel]) {
|
||||
return modelMapping[requestedModel]
|
||||
}
|
||||
const lower = requestedModel.toLowerCase()
|
||||
for (const [key, value] of Object.entries(modelMapping)) {
|
||||
if (key.toLowerCase() === lower) return value
|
||||
if (key.toLowerCase() === lower) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return requestedModel
|
||||
}
|
||||
@@ -180,30 +214,34 @@ const getMappedModelName = (modelMapping, requestedModel) => {
|
||||
// ============================================
|
||||
|
||||
// 按优先级和最后使用时间排序账户
|
||||
const sortAccountsByPriority = (accounts) => {
|
||||
return [...accounts].sort((a, b) => {
|
||||
const sortAccountsByPriority = (accounts) =>
|
||||
[...accounts].sort((a, b) => {
|
||||
const priorityA = parseInt(a.priority, 10) || 50
|
||||
const priorityB = parseInt(b.priority, 10) || 50
|
||||
if (priorityA !== priorityB) return priorityA - priorityB
|
||||
if (priorityA !== priorityB) {
|
||||
return priorityA - priorityB
|
||||
}
|
||||
const lastUsedA = a.lastUsedAt ? new Date(a.lastUsedAt).getTime() : 0
|
||||
const lastUsedB = b.lastUsedAt ? new Date(b.lastUsedAt).getTime() : 0
|
||||
if (lastUsedA !== lastUsedB) return lastUsedA - lastUsedB
|
||||
if (lastUsedA !== lastUsedB) {
|
||||
return lastUsedA - lastUsedB
|
||||
}
|
||||
const createdA = a.createdAt ? new Date(a.createdAt).getTime() : 0
|
||||
const createdB = b.createdAt ? new Date(b.createdAt).getTime() : 0
|
||||
return createdA - createdB
|
||||
})
|
||||
}
|
||||
|
||||
// 生成粘性会话 Key
|
||||
const composeStickySessionKey = (prefix, sessionHash, apiKeyId = null) => {
|
||||
if (!sessionHash) return null
|
||||
if (!sessionHash) {
|
||||
return null
|
||||
}
|
||||
return `sticky:${prefix}:${apiKeyId || 'default'}:${sessionHash}`
|
||||
}
|
||||
|
||||
// 过滤可用账户(激活 + 健康 + 可调度)
|
||||
const filterAvailableAccounts = (accounts) => {
|
||||
return accounts.filter((acc) => acc && isAccountHealthy(acc) && isSchedulable(acc.schedulable))
|
||||
}
|
||||
const filterAvailableAccounts = (accounts) =>
|
||||
accounts.filter((acc) => acc && isAccountHealthy(acc) && isSchedulable(acc.schedulable))
|
||||
|
||||
// ============================================
|
||||
// 字符串处理
|
||||
@@ -211,13 +249,17 @@ const filterAvailableAccounts = (accounts) => {
|
||||
|
||||
// 截断字符串
|
||||
const truncate = (str, maxLen = 100, suffix = '...') => {
|
||||
if (!str || str.length <= maxLen) return str
|
||||
if (!str || str.length <= maxLen) {
|
||||
return str
|
||||
}
|
||||
return str.slice(0, maxLen - suffix.length) + suffix
|
||||
}
|
||||
|
||||
// 掩码敏感信息(保留前后几位)
|
||||
const maskSensitive = (str, keepStart = 4, keepEnd = 4, maskChar = '*') => {
|
||||
if (!str || str.length <= keepStart + keepEnd) return str
|
||||
if (!str || str.length <= keepStart + keepEnd) {
|
||||
return str
|
||||
}
|
||||
const maskLen = Math.min(str.length - keepStart - keepEnd, 8)
|
||||
return str.slice(0, keepStart) + maskChar.repeat(maskLen) + str.slice(-keepEnd)
|
||||
}
|
||||
@@ -246,9 +288,8 @@ const clamp = (value, min, max) => Math.min(Math.max(value, min), max)
|
||||
// ============================================
|
||||
|
||||
// 获取时区偏移后的日期
|
||||
const getDateInTimezone = (date = new Date(), offset = config.system?.timezoneOffset || 8) => {
|
||||
return new Date(date.getTime() + offset * 3600000)
|
||||
}
|
||||
const getDateInTimezone = (date = new Date(), offset = config.system?.timezoneOffset || 8) =>
|
||||
new Date(date.getTime() + offset * 3600000)
|
||||
|
||||
// 获取时区日期字符串 YYYY-MM-DD
|
||||
const getDateStringInTimezone = (date = new Date()) => {
|
||||
@@ -258,13 +299,17 @@ const getDateStringInTimezone = (date = new Date()) => {
|
||||
|
||||
// 检查是否过期
|
||||
const isExpired = (expiresAt) => {
|
||||
if (!expiresAt) return false
|
||||
if (!expiresAt) {
|
||||
return false
|
||||
}
|
||||
return new Date(expiresAt).getTime() < Date.now()
|
||||
}
|
||||
|
||||
// 计算剩余时间(秒)
|
||||
const getTimeRemaining = (expiresAt) => {
|
||||
if (!expiresAt) return Infinity
|
||||
if (!expiresAt) {
|
||||
return Infinity
|
||||
}
|
||||
return Math.max(0, Math.floor((new Date(expiresAt).getTime() - Date.now()) / 1000))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user