This commit is contained in:
SunSeekerX
2026-01-04 12:05:53 +08:00
parent 90023d1551
commit f5e982632d
28 changed files with 481 additions and 213 deletions

View File

@@ -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
@@ -84,10 +98,13 @@ const getDecryptCacheStats = defaultEncryptor.getStats
// ============================================
// 转换为布尔值(宽松模式)
const toBoolean = (value) => value === true || value === 'true' || (typeof value === 'string' && value.toLowerCase() === 'true')
const toBoolean = (value) =>
value === true ||
value === 'true' ||
(typeof value === 'string' && value.toLowerCase() === 'true')
// 检查是否为真值null/undefined 返回 false
const isTruthy = (value) => value != null && toBoolean(value)
const isTruthy = (value) => value !== null && value !== undefined && toBoolean(value)
// 检查是否可调度(默认 true只有明确 false 才返回 false
const isSchedulable = (value) => value !== false && value !== 'false'
@@ -97,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)
}
@@ -109,8 +130,14 @@ const isAccountHealthy = (account) => {
// 安全解析 JSON
const safeParseJson = (value, fallback = null) => {
if (!value || typeof value !== 'string') return fallback
try { return JSON.parse(value) } catch { return fallback }
if (!value || typeof value !== 'string') {
return fallback
}
try {
return JSON.parse(value)
} catch {
return fallback
}
}
// 安全解析 JSON 为对象
@@ -131,36 +158,53 @@ 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.replace(/^[a-z0-9-]+\./, '').replace('anthropic.', '').replace(/-v\d+:\d+$/, '')
return model
.replace(/^[a-z0-9-]+\./, '')
.replace('anthropic.', '')
.replace(/-v\d+:\d+$/, '')
}
return model.replace(/-v\d+:\d+$|:latest$/, '')
}
// 规范化端点类型
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)
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
}
@@ -170,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))
// ============================================
// 字符串处理
@@ -201,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)
}
@@ -236,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()) => {
@@ -248,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))
}