Qwen2API-A / src /utils /usage-stats.js
github-actions[bot]
Sync from GitHub Viciy2023/Qwen2API-A@c518c81ff1356e6883c7ddbdb7371d0ff3ee3de4
7dba9e6
const fs = require('fs').promises
const path = require('path')
const config = require('../config')
const { logger } = require('./logger')
class UsageStats {
constructor() {
this.filePath = config.usageStatsFilePath
this.writeQueue = Promise.resolve()
}
createEmptyStats() {
return {
summary: {
requests: 0,
success: 0,
failed: 0,
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
updatedAt: null,
today: {
date: new Date().toISOString().slice(0, 10),
requests: 0,
success: 0,
failed: 0,
totalTokens: 0,
hourly: []
}
},
models: {}
}
}
async ensureFileExists() {
try {
await fs.access(this.filePath)
} catch {
await fs.mkdir(path.dirname(this.filePath), { recursive: true })
await fs.writeFile(this.filePath, JSON.stringify(this.createEmptyStats(), null, 2), 'utf-8')
}
}
async readStats() {
await this.ensureFileExists()
try {
const content = await fs.readFile(this.filePath, 'utf-8')
if (!content.trim()) {
return this.createEmptyStats()
}
const parsed = JSON.parse(content)
if (!parsed.summary || !parsed.models) {
return this.createEmptyStats()
}
return parsed
} catch (error) {
logger.warn(`使用统计文件读取失败,已回退为空统计: ${error.message}`, 'USAGE')
return this.createEmptyStats()
}
}
async writeStats(stats) {
await fs.writeFile(this.filePath, JSON.stringify(stats, null, 2), 'utf-8')
}
async queueWrite(task) {
this.writeQueue = this.writeQueue.then(task, task)
return this.writeQueue
}
async track({ model, success, usage = {} }) {
return this.queueWrite(async () => {
try {
const stats = await this.readStats()
const modelId = model || 'unknown-model'
const promptTokens = Math.max(0, usage.prompt_tokens || 0)
const completionTokens = Math.max(0, usage.completion_tokens || 0)
const totalTokens = Math.max(0, usage.total_tokens || (promptTokens + completionTokens))
const now = new Date()
const today = now.toISOString().slice(0, 10)
if (!stats.summary.today || stats.summary.today.date !== today) {
stats.summary.today = {
date: today,
requests: 0,
success: 0,
failed: 0,
totalTokens: 0,
hourly: []
}
}
if (!stats.models[modelId]) {
stats.models[modelId] = {
model: modelId,
requests: 0,
success: 0,
failed: 0,
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
successRate: 0,
lastUsedAt: null,
today: {
date: today,
requests: 0,
success: 0,
failed: 0,
totalTokens: 0
}
}
}
const modelStats = stats.models[modelId]
if (!modelStats.today || modelStats.today.date !== today) {
modelStats.today = {
date: today,
requests: 0,
success: 0,
failed: 0,
totalTokens: 0
}
}
modelStats.requests += 1
modelStats.success += success ? 1 : 0
modelStats.failed += success ? 0 : 1
modelStats.promptTokens += promptTokens
modelStats.completionTokens += completionTokens
modelStats.totalTokens += totalTokens
modelStats.successRate = modelStats.requests > 0 ? Number(((modelStats.success / modelStats.requests) * 100).toFixed(2)) : 0
modelStats.lastUsedAt = now.toISOString()
modelStats.today.requests += 1
modelStats.today.success += success ? 1 : 0
modelStats.today.failed += success ? 0 : 1
modelStats.today.totalTokens += totalTokens
stats.summary.requests += 1
stats.summary.success += success ? 1 : 0
stats.summary.failed += success ? 0 : 1
stats.summary.promptTokens += promptTokens
stats.summary.completionTokens += completionTokens
stats.summary.totalTokens += totalTokens
stats.summary.updatedAt = now.toISOString()
stats.summary.today.requests += 1
stats.summary.today.success += success ? 1 : 0
stats.summary.today.failed += success ? 0 : 1
stats.summary.today.totalTokens += totalTokens
const hour = now.getHours()
if (!Array.isArray(stats.summary.today.hourly)) {
stats.summary.today.hourly = []
}
let hourlyItem = stats.summary.today.hourly.find(item => item.hour === hour)
if (!hourlyItem) {
hourlyItem = { hour, requests: 0, totalTokens: 0 }
stats.summary.today.hourly.push(hourlyItem)
}
hourlyItem.requests += 1
hourlyItem.totalTokens += totalTokens
await this.writeStats(stats)
logger.info(`使用统计已记录: model=${modelId}, success=${success}, totalTokens=${totalTokens}, file=${this.filePath}`, 'USAGE')
} catch (error) {
logger.error(`记录使用统计失败: model=${model || 'unknown-model'} file=${this.filePath}`, 'USAGE', '', error)
}
})
}
async getStats() {
const stats = await this.readStats()
const today = new Date().toISOString().slice(0, 10)
if (!stats.summary.today || stats.summary.today.date !== today) {
stats.summary.today = {
date: today,
requests: 0,
success: 0,
failed: 0,
totalTokens: 0,
hourly: []
}
}
const models = Object.values(stats.models).sort((a, b) => b.totalTokens - a.totalTokens)
const successRate = stats.summary.requests > 0 ? Number(((stats.summary.success / stats.summary.requests) * 100).toFixed(2)) : 0
return {
filePath: this.filePath,
summary: {
...stats.summary,
successRate
},
models
}
}
async reset() {
await this.writeStats(this.createEmptyStats())
}
}
module.exports = new UsageStats()