|
|
import fs from 'fs'; |
|
|
import path from 'path'; |
|
|
import { log } from '../utils/logger.js'; |
|
|
import memoryManager, { MemoryPressure } from '../utils/memoryManager.js'; |
|
|
import { getDataDir } from '../utils/paths.js'; |
|
|
import { QUOTA_CACHE_TTL, QUOTA_CLEANUP_INTERVAL } from '../constants/index.js'; |
|
|
|
|
|
class QuotaManager { |
|
|
|
|
|
|
|
|
|
|
|
constructor(filePath = path.join(getDataDir(), 'quotas.json')) { |
|
|
this.filePath = filePath; |
|
|
|
|
|
this.cache = new Map(); |
|
|
this.CACHE_TTL = QUOTA_CACHE_TTL; |
|
|
this.CLEANUP_INTERVAL = QUOTA_CLEANUP_INTERVAL; |
|
|
this.cleanupTimer = null; |
|
|
this.ensureFileExists(); |
|
|
this.loadFromFile(); |
|
|
this.startCleanupTimer(); |
|
|
this.registerMemoryCleanup(); |
|
|
} |
|
|
|
|
|
ensureFileExists() { |
|
|
const dir = path.dirname(this.filePath); |
|
|
if (!fs.existsSync(dir)) { |
|
|
fs.mkdirSync(dir, { recursive: true }); |
|
|
} |
|
|
if (!fs.existsSync(this.filePath)) { |
|
|
fs.writeFileSync(this.filePath, JSON.stringify({ meta: { lastCleanup: Date.now(), ttl: this.CLEANUP_INTERVAL }, quotas: {} }, null, 2), 'utf8'); |
|
|
} |
|
|
} |
|
|
|
|
|
loadFromFile() { |
|
|
try { |
|
|
const data = fs.readFileSync(this.filePath, 'utf8'); |
|
|
const parsed = JSON.parse(data); |
|
|
Object.entries(parsed.quotas || {}).forEach(([key, value]) => { |
|
|
this.cache.set(key, value); |
|
|
}); |
|
|
} catch (error) { |
|
|
log.error('加载额度文件失败:', error.message); |
|
|
} |
|
|
} |
|
|
|
|
|
saveToFile() { |
|
|
try { |
|
|
const quotas = {}; |
|
|
this.cache.forEach((value, key) => { |
|
|
quotas[key] = value; |
|
|
}); |
|
|
const data = { |
|
|
meta: { lastCleanup: Date.now(), ttl: this.CLEANUP_INTERVAL }, |
|
|
quotas |
|
|
}; |
|
|
fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2), 'utf8'); |
|
|
} catch (error) { |
|
|
log.error('保存额度文件失败:', error.message); |
|
|
} |
|
|
} |
|
|
|
|
|
updateQuota(refreshToken, quotas) { |
|
|
this.cache.set(refreshToken, { |
|
|
lastUpdated: Date.now(), |
|
|
models: quotas |
|
|
}); |
|
|
this.saveToFile(); |
|
|
} |
|
|
|
|
|
getQuota(refreshToken) { |
|
|
const data = this.cache.get(refreshToken); |
|
|
if (!data) return null; |
|
|
|
|
|
|
|
|
if (Date.now() - data.lastUpdated > this.CACHE_TTL) { |
|
|
return null; |
|
|
} |
|
|
|
|
|
return data; |
|
|
} |
|
|
|
|
|
cleanup() { |
|
|
const now = Date.now(); |
|
|
let cleaned = 0; |
|
|
|
|
|
this.cache.forEach((value, key) => { |
|
|
if (now - value.lastUpdated > this.CLEANUP_INTERVAL) { |
|
|
this.cache.delete(key); |
|
|
cleaned++; |
|
|
} |
|
|
}); |
|
|
|
|
|
if (cleaned > 0) { |
|
|
log.info(`清理了 ${cleaned} 个过期的额度记录`); |
|
|
this.saveToFile(); |
|
|
} |
|
|
} |
|
|
|
|
|
startCleanupTimer() { |
|
|
if (this.cleanupTimer) { |
|
|
clearInterval(this.cleanupTimer); |
|
|
} |
|
|
this.cleanupTimer = setInterval(() => this.cleanup(), this.CLEANUP_INTERVAL); |
|
|
} |
|
|
|
|
|
stopCleanupTimer() { |
|
|
if (this.cleanupTimer) { |
|
|
clearInterval(this.cleanupTimer); |
|
|
this.cleanupTimer = null; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
registerMemoryCleanup() { |
|
|
memoryManager.registerCleanup((pressure) => { |
|
|
|
|
|
if (pressure === MemoryPressure.CRITICAL) { |
|
|
|
|
|
const size = this.cache.size; |
|
|
if (size > 0) { |
|
|
this.cache.clear(); |
|
|
log.info(`紧急清理 ${size} 个额度缓存`); |
|
|
} |
|
|
} else if (pressure === MemoryPressure.HIGH) { |
|
|
|
|
|
this.cleanup(); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
convertToBeijingTime(utcTimeStr) { |
|
|
if (!utcTimeStr) return 'N/A'; |
|
|
try { |
|
|
const utcDate = new Date(utcTimeStr); |
|
|
return utcDate.toLocaleString('zh-CN', { |
|
|
month: '2-digit', |
|
|
day: '2-digit', |
|
|
hour: '2-digit', |
|
|
minute: '2-digit', |
|
|
timeZone: 'Asia/Shanghai' |
|
|
}); |
|
|
} catch (error) { |
|
|
return 'N/A'; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
const quotaManager = new QuotaManager(); |
|
|
export default quotaManager; |
|
|
|