mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 模型使用分布支持自定义时间范围
- 后端:getPublicModelStats 支持 today/24h/7d/30d/all 五种时间范围 - 后端:新增 publicStatsModelDistributionPeriod 设置项 - 前端:设置页面添加横向选项卡式时间范围选择器 - 前端:公开统计组件显示当前数据时间范围标签 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -267,6 +267,7 @@ const defaultOemSettings = {
|
|||||||
publicStatsEnabled: false, // 是否在首页显示公开统计概览
|
publicStatsEnabled: false, // 是否在首页显示公开统计概览
|
||||||
// 公开统计显示选项
|
// 公开统计显示选项
|
||||||
publicStatsShowModelDistribution: true, // 显示模型使用分布
|
publicStatsShowModelDistribution: true, // 显示模型使用分布
|
||||||
|
publicStatsModelDistributionPeriod: 'today', // 模型使用分布时间范围: today, 24h, 7d, 30d, all
|
||||||
publicStatsShowTokenTrends: false, // 显示Token使用趋势
|
publicStatsShowTokenTrends: false, // 显示Token使用趋势
|
||||||
publicStatsShowApiKeysTrends: false, // 显示API Keys使用趋势
|
publicStatsShowApiKeysTrends: false, // 显示API Keys使用趋势
|
||||||
publicStatsShowAccountTrends: false, // 显示账号使用趋势
|
publicStatsShowAccountTrends: false, // 显示账号使用趋势
|
||||||
@@ -319,6 +320,7 @@ router.put('/oem-settings', authenticateAdmin, async (req, res) => {
|
|||||||
showAdminButton,
|
showAdminButton,
|
||||||
publicStatsEnabled,
|
publicStatsEnabled,
|
||||||
publicStatsShowModelDistribution,
|
publicStatsShowModelDistribution,
|
||||||
|
publicStatsModelDistributionPeriod,
|
||||||
publicStatsShowTokenTrends,
|
publicStatsShowTokenTrends,
|
||||||
publicStatsShowApiKeysTrends,
|
publicStatsShowApiKeysTrends,
|
||||||
publicStatsShowAccountTrends
|
publicStatsShowAccountTrends
|
||||||
@@ -349,6 +351,12 @@ router.put('/oem-settings', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证时间范围值
|
||||||
|
const validPeriods = ['today', '24h', '7d', '30d', 'all']
|
||||||
|
const periodValue = validPeriods.includes(publicStatsModelDistributionPeriod)
|
||||||
|
? publicStatsModelDistributionPeriod
|
||||||
|
: 'today'
|
||||||
|
|
||||||
const settings = {
|
const settings = {
|
||||||
siteName: siteName.trim(),
|
siteName: siteName.trim(),
|
||||||
siteIcon: (siteIcon || '').trim(),
|
siteIcon: (siteIcon || '').trim(),
|
||||||
@@ -357,6 +365,7 @@ router.put('/oem-settings', authenticateAdmin, async (req, res) => {
|
|||||||
publicStatsEnabled: publicStatsEnabled === true, // 默认为false
|
publicStatsEnabled: publicStatsEnabled === true, // 默认为false
|
||||||
// 公开统计显示选项
|
// 公开统计显示选项
|
||||||
publicStatsShowModelDistribution: publicStatsShowModelDistribution !== false, // 默认为true
|
publicStatsShowModelDistribution: publicStatsShowModelDistribution !== false, // 默认为true
|
||||||
|
publicStatsModelDistributionPeriod: periodValue, // 时间范围
|
||||||
publicStatsShowTokenTrends: publicStatsShowTokenTrends === true, // 默认为false
|
publicStatsShowTokenTrends: publicStatsShowTokenTrends === true, // 默认为false
|
||||||
publicStatsShowApiKeysTrends: publicStatsShowApiKeysTrends === true, // 默认为false
|
publicStatsShowApiKeysTrends: publicStatsShowApiKeysTrends === true, // 默认为false
|
||||||
publicStatsShowAccountTrends: publicStatsShowAccountTrends === true, // 默认为false
|
publicStatsShowAccountTrends: publicStatsShowAccountTrends === true, // 默认为false
|
||||||
@@ -449,9 +458,15 @@ router.get('/public-stats', async (req, res) => {
|
|||||||
// 辅助函数:规范化布尔值
|
// 辅助函数:规范化布尔值
|
||||||
const normalizeBoolean = (value) => value === true || value === 'true'
|
const normalizeBoolean = (value) => value === true || value === 'true'
|
||||||
const isRateLimitedFlag = (status) => {
|
const isRateLimitedFlag = (status) => {
|
||||||
if (!status) return false
|
if (!status) {
|
||||||
if (typeof status === 'string') return status === 'limited'
|
return false
|
||||||
if (typeof status === 'object') return status.isRateLimited === true
|
}
|
||||||
|
if (typeof status === 'string') {
|
||||||
|
return status === 'limited'
|
||||||
|
}
|
||||||
|
if (typeof status === 'object') {
|
||||||
|
return status.isRateLimited === true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,7 +486,7 @@ router.get('/public-stats', async (req, res) => {
|
|||||||
bedrockAccountService.getAllAccounts(),
|
bedrockAccountService.getAllAccounts(),
|
||||||
droidAccountService.getAllAccounts(),
|
droidAccountService.getAllAccounts(),
|
||||||
redis.getTodayStats(),
|
redis.getTodayStats(),
|
||||||
getPublicModelStats()
|
getPublicModelStats(settings.publicStatsModelDistributionPeriod || 'today')
|
||||||
])
|
])
|
||||||
|
|
||||||
const bedrockAccounts = bedrockAccountsResult.success ? bedrockAccountsResult.data : []
|
const bedrockAccounts = bedrockAccountsResult.success ? bedrockAccountsResult.data : []
|
||||||
@@ -568,7 +583,9 @@ router.get('/public-stats', async (req, res) => {
|
|||||||
|
|
||||||
// 根据设置添加可选数据
|
// 根据设置添加可选数据
|
||||||
if (settings.publicStatsShowModelDistribution !== false) {
|
if (settings.publicStatsShowModelDistribution !== false) {
|
||||||
publicStats.modelDistribution = modelStats
|
// modelStats 现在返回 { stats: [], period }
|
||||||
|
publicStats.modelDistribution = modelStats.stats
|
||||||
|
publicStats.modelDistributionPeriod = modelStats.period
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取趋势数据(最近7天)
|
// 获取趋势数据(最近7天)
|
||||||
@@ -605,20 +622,70 @@ router.get('/public-stats', async (req, res) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 获取公开模型统计的辅助函数
|
// 获取公开模型统计的辅助函数
|
||||||
async function getPublicModelStats() {
|
// period: 'today' | '24h' | '7d' | '30d' | 'all'
|
||||||
|
async function getPublicModelStats(period = 'today') {
|
||||||
try {
|
try {
|
||||||
const client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
const today = redis.getDateStringInTimezone()
|
const today = redis.getDateStringInTimezone()
|
||||||
const pattern = `usage:model:daily:*:${today}`
|
const tzDate = redis.getDateInTimezone()
|
||||||
const keys = await client.keys(pattern)
|
|
||||||
|
|
||||||
if (keys.length === 0) {
|
// 根据period生成日期范围
|
||||||
return []
|
const getDatePatterns = () => {
|
||||||
|
const patterns = []
|
||||||
|
|
||||||
|
if (period === 'today') {
|
||||||
|
patterns.push(`usage:model:daily:*:${today}`)
|
||||||
|
} else if (period === '24h') {
|
||||||
|
// 过去24小时 = 今天 + 昨天
|
||||||
|
patterns.push(`usage:model:daily:*:${today}`)
|
||||||
|
const yesterday = new Date(tzDate)
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1)
|
||||||
|
patterns.push(`usage:model:daily:*:${redis.getDateStringInTimezone(yesterday)}`)
|
||||||
|
} else if (period === '7d') {
|
||||||
|
// 过去7天
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
const date = new Date(tzDate)
|
||||||
|
date.setDate(date.getDate() - i)
|
||||||
|
patterns.push(`usage:model:daily:*:${redis.getDateStringInTimezone(date)}`)
|
||||||
|
}
|
||||||
|
} else if (period === '30d') {
|
||||||
|
// 过去30天
|
||||||
|
for (let i = 0; i < 30; i++) {
|
||||||
|
const date = new Date(tzDate)
|
||||||
|
date.setDate(date.getDate() - i)
|
||||||
|
patterns.push(`usage:model:daily:*:${redis.getDateStringInTimezone(date)}`)
|
||||||
|
}
|
||||||
|
} else if (period === 'all') {
|
||||||
|
// 所有数据
|
||||||
|
patterns.push('usage:model:daily:*')
|
||||||
|
} else {
|
||||||
|
// 默认今天
|
||||||
|
patterns.push(`usage:model:daily:*:${today}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return patterns
|
||||||
|
}
|
||||||
|
|
||||||
|
const patterns = getDatePatterns()
|
||||||
|
let allKeys = []
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const keys = await client.keys(pattern)
|
||||||
|
allKeys.push(...keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去重
|
||||||
|
allKeys = [...new Set(allKeys)]
|
||||||
|
|
||||||
|
if (allKeys.length === 0) {
|
||||||
|
return { stats: [], period }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模型名标准化
|
// 模型名标准化
|
||||||
const normalizeModelName = (model) => {
|
const normalizeModelName = (model) => {
|
||||||
if (!model || model === 'unknown') return model
|
if (!model || model === 'unknown') {
|
||||||
|
return model
|
||||||
|
}
|
||||||
if (model.includes('.anthropic.') || model.includes('.claude')) {
|
if (model.includes('.anthropic.') || model.includes('.claude')) {
|
||||||
let normalized = model.replace(/^[a-z0-9-]+\./, '')
|
let normalized = model.replace(/^[a-z0-9-]+\./, '')
|
||||||
normalized = normalized.replace('anthropic.', '')
|
normalized = normalized.replace('anthropic.', '')
|
||||||
@@ -632,9 +699,11 @@ async function getPublicModelStats() {
|
|||||||
const modelStatsMap = new Map()
|
const modelStatsMap = new Map()
|
||||||
let totalRequests = 0
|
let totalRequests = 0
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of allKeys) {
|
||||||
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) continue
|
if (!match) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const rawModel = match[1]
|
const rawModel = match[1]
|
||||||
const normalizedModel = normalizeModelName(rawModel)
|
const normalizedModel = normalizeModelName(rawModel)
|
||||||
@@ -661,10 +730,10 @@ async function getPublicModelStats() {
|
|||||||
|
|
||||||
// 按占比排序,取前5个
|
// 按占比排序,取前5个
|
||||||
modelStats.sort((a, b) => b.percentage - a.percentage)
|
modelStats.sort((a, b) => b.percentage - a.percentage)
|
||||||
return modelStats.slice(0, 5)
|
return { stats: modelStats.slice(0, 5), period }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn('⚠️ Failed to get public model stats:', error.message)
|
logger.warn('⚠️ Failed to get public model stats:', error.message)
|
||||||
return []
|
return { stats: [], period }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,12 @@
|
|||||||
"
|
"
|
||||||
class="mt-4"
|
class="mt-4"
|
||||||
>
|
>
|
||||||
<div class="section-title">模型使用分布</div>
|
<div class="section-title">
|
||||||
|
模型使用分布
|
||||||
|
<span class="period-label">{{
|
||||||
|
formatPeriodLabel(authStore.publicStats.modelDistributionPeriod)
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
<div class="model-distribution">
|
<div class="model-distribution">
|
||||||
<div
|
<div
|
||||||
v-for="model in authStore.publicStats.modelDistribution"
|
v-for="model in authStore.publicStats.modelDistribution"
|
||||||
@@ -384,6 +389,18 @@ function formatTokensShort(tokens) {
|
|||||||
return tokens.toString()
|
return tokens.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 格式化时间范围标签
|
||||||
|
function formatPeriodLabel(period) {
|
||||||
|
const labels = {
|
||||||
|
today: '今天',
|
||||||
|
'24h': '过去24小时',
|
||||||
|
'7d': '过去7天',
|
||||||
|
'30d': '过去30天',
|
||||||
|
all: '全部'
|
||||||
|
}
|
||||||
|
return labels[period] || labels['today']
|
||||||
|
}
|
||||||
|
|
||||||
// 获取平台图标
|
// 获取平台图标
|
||||||
function getPlatformIcon(platform) {
|
function getPlatformIcon(platform) {
|
||||||
const icons = {
|
const icons = {
|
||||||
@@ -450,6 +467,11 @@ function formatDateShort(dateStr) {
|
|||||||
@apply mb-2 text-center text-xs text-gray-600 dark:text-gray-400;
|
@apply mb-2 text-center text-xs text-gray-600 dark:text-gray-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 时间范围标签 */
|
||||||
|
.period-label {
|
||||||
|
@apply ml-1 rounded bg-gray-100 px-1.5 py-0.5 text-[10px] text-gray-500 dark:bg-gray-700 dark:text-gray-400;
|
||||||
|
}
|
||||||
|
|
||||||
/* 状态徽章 */
|
/* 状态徽章 */
|
||||||
.status-badge {
|
.status-badge {
|
||||||
@apply inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium;
|
@apply inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium;
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
siteIconData: '',
|
siteIconData: '',
|
||||||
showAdminButton: true, // 控制管理后台按钮的显示
|
showAdminButton: true, // 控制管理后台按钮的显示
|
||||||
publicStatsEnabled: false, // 是否在首页显示公开统计概览
|
publicStatsEnabled: false, // 是否在首页显示公开统计概览
|
||||||
|
publicStatsShowModelDistribution: true,
|
||||||
|
publicStatsModelDistributionPeriod: 'today', // 时间范围: today, 24h, 7d, 30d, all
|
||||||
|
publicStatsShowTokenTrends: false,
|
||||||
|
publicStatsShowApiKeysTrends: false,
|
||||||
|
publicStatsShowAccountTrends: false,
|
||||||
updatedAt: null
|
updatedAt: null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1082,9 +1082,10 @@
|
|||||||
选择要公开显示的数据:
|
选择要公开显示的数据:
|
||||||
</p>
|
</p>
|
||||||
<div class="grid gap-3 sm:grid-cols-2">
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
<label
|
<div
|
||||||
class="flex cursor-pointer items-center gap-3 rounded-lg border border-gray-200 bg-white p-3 transition-colors hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:hover:bg-gray-700"
|
class="rounded-lg border border-gray-200 bg-white p-3 transition-colors dark:border-gray-600 dark:bg-gray-800"
|
||||||
>
|
>
|
||||||
|
<label class="flex cursor-pointer items-center gap-3">
|
||||||
<input
|
<input
|
||||||
v-model="oemSettings.publicStatsShowModelDistribution"
|
v-model="oemSettings.publicStatsShowModelDistribution"
|
||||||
class="h-4 w-4 rounded border-gray-300 text-green-600 focus:ring-green-500 dark:border-gray-600 dark:bg-gray-700"
|
class="h-4 w-4 rounded border-gray-300 text-green-600 focus:ring-green-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
@@ -1097,6 +1098,26 @@
|
|||||||
<p class="text-xs text-gray-500 dark:text-gray-400">显示各模型的使用占比</p>
|
<p class="text-xs text-gray-500 dark:text-gray-400">显示各模型的使用占比</p>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
<div v-if="oemSettings.publicStatsShowModelDistribution" class="mt-3 pl-7">
|
||||||
|
<div class="mb-1.5 text-xs text-gray-500 dark:text-gray-400">时间范围</div>
|
||||||
|
<div class="inline-flex rounded-lg bg-gray-100 p-0.5 dark:bg-gray-700/50">
|
||||||
|
<button
|
||||||
|
v-for="option in modelDistributionPeriodOptions"
|
||||||
|
:key="option.value"
|
||||||
|
class="rounded-md px-2.5 py-1 text-xs font-medium transition-all"
|
||||||
|
:class="
|
||||||
|
oemSettings.publicStatsModelDistributionPeriod === option.value
|
||||||
|
? 'bg-white text-green-600 shadow-sm dark:bg-gray-600 dark:text-green-400'
|
||||||
|
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
|
||||||
|
"
|
||||||
|
type="button"
|
||||||
|
@click="oemSettings.publicStatsModelDistributionPeriod = option.value"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<label
|
<label
|
||||||
class="flex cursor-pointer items-center gap-3 rounded-lg border border-gray-200 bg-white p-3 transition-colors hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:hover:bg-gray-700"
|
class="flex cursor-pointer items-center gap-3 rounded-lg border border-gray-200 bg-white p-3 transition-colors hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:hover:bg-gray-700"
|
||||||
>
|
>
|
||||||
@@ -1765,6 +1786,15 @@ defineOptions({
|
|||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
const { loading, saving, oemSettings } = storeToRefs(settingsStore)
|
const { loading, saving, oemSettings } = storeToRefs(settingsStore)
|
||||||
|
|
||||||
|
// 模型使用分布时间范围选项
|
||||||
|
const modelDistributionPeriodOptions = [
|
||||||
|
{ value: 'today', label: '今天' },
|
||||||
|
{ value: '24h', label: '24小时' },
|
||||||
|
{ value: '7d', label: '7天' },
|
||||||
|
{ value: '30d', label: '30天' },
|
||||||
|
{ value: 'all', label: '全部' }
|
||||||
|
]
|
||||||
|
|
||||||
// 组件refs
|
// 组件refs
|
||||||
const iconFileInput = ref()
|
const iconFileInput = ref()
|
||||||
|
|
||||||
@@ -2613,6 +2643,8 @@ const saveOemSettings = async () => {
|
|||||||
showAdminButton: oemSettings.value.showAdminButton,
|
showAdminButton: oemSettings.value.showAdminButton,
|
||||||
publicStatsEnabled: oemSettings.value.publicStatsEnabled,
|
publicStatsEnabled: oemSettings.value.publicStatsEnabled,
|
||||||
publicStatsShowModelDistribution: oemSettings.value.publicStatsShowModelDistribution,
|
publicStatsShowModelDistribution: oemSettings.value.publicStatsShowModelDistribution,
|
||||||
|
publicStatsModelDistributionPeriod:
|
||||||
|
oemSettings.value.publicStatsModelDistributionPeriod || 'today',
|
||||||
publicStatsShowTokenTrends: oemSettings.value.publicStatsShowTokenTrends,
|
publicStatsShowTokenTrends: oemSettings.value.publicStatsShowTokenTrends,
|
||||||
publicStatsShowApiKeysTrends: oemSettings.value.publicStatsShowApiKeysTrends,
|
publicStatsShowApiKeysTrends: oemSettings.value.publicStatsShowApiKeysTrends,
|
||||||
publicStatsShowAccountTrends: oemSettings.value.publicStatsShowAccountTrends
|
publicStatsShowAccountTrends: oemSettings.value.publicStatsShowAccountTrends
|
||||||
|
|||||||
Reference in New Issue
Block a user