mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
Merge branch 'main' into dev
This commit is contained in:
@@ -269,6 +269,28 @@ router.post('/api/user-stats', async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前使用量
|
||||
let currentWindowRequests = 0;
|
||||
let currentWindowTokens = 0;
|
||||
let currentDailyCost = 0;
|
||||
|
||||
try {
|
||||
// 获取当前时间窗口的请求次数和Token使用量
|
||||
if (fullKeyData.rateLimitWindow > 0) {
|
||||
const client = redis.getClientSafe();
|
||||
const requestCountKey = `rate_limit:requests:${keyId}`;
|
||||
const tokenCountKey = `rate_limit:tokens:${keyId}`;
|
||||
|
||||
currentWindowRequests = parseInt(await client.get(requestCountKey) || '0');
|
||||
currentWindowTokens = parseInt(await client.get(tokenCountKey) || '0');
|
||||
}
|
||||
|
||||
// 获取当日费用
|
||||
currentDailyCost = await redis.getDailyCost(keyId) || 0;
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to get current usage for key ${keyId}:`, error);
|
||||
}
|
||||
|
||||
// 构建响应数据(只返回该API Key自己的信息,确保不泄露其他信息)
|
||||
const responseData = {
|
||||
id: keyId,
|
||||
@@ -296,13 +318,17 @@ router.post('/api/user-stats', async (req, res) => {
|
||||
}
|
||||
},
|
||||
|
||||
// 限制信息(只显示配置,不显示当前使用量)
|
||||
// 限制信息(显示配置和当前使用量)
|
||||
limits: {
|
||||
tokenLimit: fullKeyData.tokenLimit || 0,
|
||||
concurrencyLimit: fullKeyData.concurrencyLimit || 0,
|
||||
rateLimitWindow: fullKeyData.rateLimitWindow || 0,
|
||||
rateLimitRequests: fullKeyData.rateLimitRequests || 0,
|
||||
dailyCostLimit: fullKeyData.dailyCostLimit || 0
|
||||
dailyCostLimit: fullKeyData.dailyCostLimit || 0,
|
||||
// 当前使用量
|
||||
currentWindowRequests: currentWindowRequests,
|
||||
currentWindowTokens: currentWindowTokens,
|
||||
currentDailyCost: currentDailyCost
|
||||
},
|
||||
|
||||
// 绑定的账户信息(只显示ID,不显示敏感信息)
|
||||
|
||||
@@ -6,64 +6,131 @@
|
||||
<i class="fas fa-shield-alt mr-2 md:mr-3 text-red-500 text-sm md:text-base" />
|
||||
限制配置
|
||||
</h3>
|
||||
<div class="space-y-2 md:space-y-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600 text-sm md:text-base">Token 限制</span>
|
||||
<span class="font-medium text-gray-900 text-sm md:text-base">{{ statsData.limits.tokenLimit > 0 ? formatNumber(statsData.limits.tokenLimit) : '无限制' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600 text-sm md:text-base">并发限制</span>
|
||||
<span class="font-medium text-gray-900 text-sm md:text-base">{{ statsData.limits.concurrencyLimit > 0 ? statsData.limits.concurrencyLimit : '无限制' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600 text-sm md:text-base">速率限制</span>
|
||||
<span class="font-medium text-gray-900 text-sm md:text-base">
|
||||
{{ statsData.limits.rateLimitRequests > 0 && statsData.limits.rateLimitWindow > 0
|
||||
? `${statsData.limits.rateLimitRequests}次/${statsData.limits.rateLimitWindow}分钟`
|
||||
: '无限制' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600 text-sm md:text-base">每日费用限制</span>
|
||||
<span class="font-medium text-gray-900 text-sm md:text-base">{{ statsData.limits.dailyCostLimit > 0 ? '$' + statsData.limits.dailyCostLimit : '无限制' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600 text-sm md:text-base">模型限制</span>
|
||||
<span class="font-medium text-gray-900 text-sm md:text-base">
|
||||
<span
|
||||
v-if="statsData.restrictions.enableModelRestriction && statsData.restrictions.restrictedModels.length > 0"
|
||||
class="text-orange-600"
|
||||
>
|
||||
<i class="fas fa-exclamation-triangle mr-1 text-xs md:text-sm" />
|
||||
限制 {{ statsData.restrictions.restrictedModels.length }} 个模型
|
||||
<div class="space-y-4 md:space-y-5">
|
||||
<!-- 每日费用限制 -->
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-gray-600 text-sm md:text-base font-medium">每日费用限制</span>
|
||||
<span class="text-xs md:text-sm text-gray-500">
|
||||
<span v-if="statsData.limits.dailyCostLimit > 0">
|
||||
${{ statsData.limits.currentDailyCost.toFixed(4) }} / ${{ statsData.limits.dailyCostLimit.toFixed(2) }}
|
||||
</span>
|
||||
<span v-else class="flex items-center gap-1">
|
||||
${{ statsData.limits.currentDailyCost.toFixed(4) }} / <i class="fas fa-infinity" />
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-green-600"
|
||||
>
|
||||
<i class="fas fa-check-circle mr-1 text-xs md:text-sm" />
|
||||
允许所有模型
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="statsData.limits.dailyCostLimit > 0" class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
:class="getDailyCostProgressColor()"
|
||||
class="h-2 rounded-full transition-all duration-300"
|
||||
:style="{ width: getDailyCostProgress() + '%' }"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-green-500 h-2 rounded-full" style="width: 0%" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600 text-sm md:text-base">客户端限制</span>
|
||||
<span class="font-medium text-gray-900 text-sm md:text-base">
|
||||
<span
|
||||
v-if="statsData.restrictions.enableClientRestriction && statsData.restrictions.allowedClients.length > 0"
|
||||
class="text-orange-600"
|
||||
>
|
||||
<i class="fas fa-exclamation-triangle mr-1 text-xs md:text-sm" />
|
||||
限制 {{ statsData.restrictions.allowedClients.length }} 个客户端
|
||||
|
||||
<!-- 时间窗口限制 -->
|
||||
<div v-if="statsData.limits.rateLimitWindow > 0 && (statsData.limits.rateLimitRequests > 0 || statsData.limits.tokenLimit > 0)">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-gray-600 text-sm md:text-base font-medium">
|
||||
时间窗口限制 ({{ statsData.limits.rateLimitWindow }}分钟)
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-green-600"
|
||||
>
|
||||
<i class="fas fa-check-circle mr-1 text-xs md:text-sm" />
|
||||
允许所有客户端
|
||||
</div>
|
||||
|
||||
<!-- 请求次数限制 -->
|
||||
<div v-if="statsData.limits.rateLimitRequests > 0" class="space-y-1.5 mb-3">
|
||||
<div class="flex justify-between items-center text-xs md:text-sm">
|
||||
<span class="text-gray-500">请求次数</span>
|
||||
<span class="text-gray-700">
|
||||
{{ formatNumber(statsData.limits.currentWindowRequests) }} / {{ formatNumber(statsData.limits.rateLimitRequests) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-1.5">
|
||||
<div
|
||||
:class="getWindowRequestProgressColor()"
|
||||
class="h-1.5 rounded-full transition-all duration-300"
|
||||
:style="{ width: getWindowRequestProgress() + '%' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token使用量限制 -->
|
||||
<div v-if="statsData.limits.tokenLimit > 0" class="space-y-1.5">
|
||||
<div class="flex justify-between items-center text-xs md:text-sm">
|
||||
<span class="text-gray-500">Token 使用量</span>
|
||||
<span class="text-gray-700">
|
||||
{{ formatNumber(statsData.limits.currentWindowTokens) }} / {{ formatNumber(statsData.limits.tokenLimit) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-1.5">
|
||||
<div
|
||||
:class="getWindowTokenProgressColor()"
|
||||
class="h-1.5 rounded-full transition-all duration-300"
|
||||
:style="{ width: getWindowTokenProgress() + '%' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-500">
|
||||
<i class="fas fa-info-circle mr-1" />
|
||||
请求次数和Token使用量为"或"的关系,任一达到限制即触发限流
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 其他限制信息 -->
|
||||
<div class="pt-2 border-t border-gray-100 space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600 text-sm md:text-base">并发限制</span>
|
||||
<span class="font-medium text-gray-900 text-sm md:text-base">
|
||||
<span v-if="statsData.limits.concurrencyLimit > 0">
|
||||
{{ statsData.limits.concurrencyLimit }}
|
||||
</span>
|
||||
<span v-else class="flex items-center gap-1">
|
||||
<i class="fas fa-infinity text-gray-400" />
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600 text-sm md:text-base">模型限制</span>
|
||||
<span class="font-medium text-gray-900 text-sm md:text-base">
|
||||
<span
|
||||
v-if="statsData.restrictions.enableModelRestriction && statsData.restrictions.restrictedModels.length > 0"
|
||||
class="text-orange-600"
|
||||
>
|
||||
<i class="fas fa-exclamation-triangle mr-1 text-xs md:text-sm" />
|
||||
限制 {{ statsData.restrictions.restrictedModels.length }} 个模型
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-green-600"
|
||||
>
|
||||
<i class="fas fa-check-circle mr-1 text-xs md:text-sm" />
|
||||
允许所有模型
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600 text-sm md:text-base">客户端限制</span>
|
||||
<span class="font-medium text-gray-900 text-sm md:text-base">
|
||||
<span
|
||||
v-if="statsData.restrictions.enableClientRestriction && statsData.restrictions.allowedClients.length > 0"
|
||||
class="text-orange-600"
|
||||
>
|
||||
<i class="fas fa-exclamation-triangle mr-1 text-xs md:text-sm" />
|
||||
限制 {{ statsData.restrictions.allowedClients.length }} 个客户端
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-green-600"
|
||||
>
|
||||
<i class="fas fa-check-circle mr-1 text-xs md:text-sm" />
|
||||
允许所有客户端
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -158,6 +225,51 @@ const formatNumber = (num) => {
|
||||
return num.toLocaleString()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取每日费用进度
|
||||
const getDailyCostProgress = () => {
|
||||
if (!statsData.value.limits.dailyCostLimit || statsData.value.limits.dailyCostLimit === 0) return 0
|
||||
const percentage = (statsData.value.limits.currentDailyCost / statsData.value.limits.dailyCostLimit) * 100
|
||||
return Math.min(percentage, 100)
|
||||
}
|
||||
|
||||
// 获取每日费用进度条颜色
|
||||
const getDailyCostProgressColor = () => {
|
||||
const progress = getDailyCostProgress()
|
||||
if (progress >= 100) return 'bg-red-500'
|
||||
if (progress >= 80) return 'bg-yellow-500'
|
||||
return 'bg-green-500'
|
||||
}
|
||||
|
||||
// 获取窗口请求进度
|
||||
const getWindowRequestProgress = () => {
|
||||
if (!statsData.value.limits.rateLimitRequests || statsData.value.limits.rateLimitRequests === 0) return 0
|
||||
const percentage = (statsData.value.limits.currentWindowRequests / statsData.value.limits.rateLimitRequests) * 100
|
||||
return Math.min(percentage, 100)
|
||||
}
|
||||
|
||||
// 获取窗口请求进度条颜色
|
||||
const getWindowRequestProgressColor = () => {
|
||||
const progress = getWindowRequestProgress()
|
||||
if (progress >= 100) return 'bg-red-500'
|
||||
if (progress >= 80) return 'bg-yellow-500'
|
||||
return 'bg-blue-500'
|
||||
}
|
||||
|
||||
// 获取窗口Token进度
|
||||
const getWindowTokenProgress = () => {
|
||||
if (!statsData.value.limits.tokenLimit || statsData.value.limits.tokenLimit === 0) return 0
|
||||
const percentage = (statsData.value.limits.currentWindowTokens / statsData.value.limits.tokenLimit) * 100
|
||||
return Math.min(percentage, 100)
|
||||
}
|
||||
|
||||
// 获取窗口Token进度条颜色
|
||||
const getWindowTokenProgressColor = () => {
|
||||
const progress = getWindowTokenProgress()
|
||||
if (progress >= 100) return 'bg-red-500'
|
||||
if (progress >= 80) return 'bg-yellow-500'
|
||||
return 'bg-purple-500'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -311,6 +311,28 @@ export const useDashboardStore = defineStore('dashboard', () => {
|
||||
url += `&startDate=${encodeURIComponent(startTime.toISOString())}`
|
||||
url += `&endDate=${encodeURIComponent(endTime.toISOString())}`
|
||||
}
|
||||
} else if (dateFilter.value.type === 'preset' && trendGranularity.value === 'day') {
|
||||
// 天粒度的预设时间范围,需要传递startDate和endDate参数
|
||||
const now = new Date()
|
||||
let startDate, endDate
|
||||
|
||||
const option = dateFilter.value.presetOptions.find(opt => opt.value === dateFilter.value.preset)
|
||||
if (option) {
|
||||
if (dateFilter.value.preset === 'today') {
|
||||
// 今日:从系统时区的今天0点到23:59
|
||||
startDate = getSystemTimezoneDay(now, true)
|
||||
endDate = getSystemTimezoneDay(now, false)
|
||||
} else {
|
||||
// 7天或30天:从N天前的0点到今天的23:59
|
||||
const daysAgo = new Date()
|
||||
daysAgo.setDate(daysAgo.getDate() - (option.days - 1))
|
||||
startDate = getSystemTimezoneDay(daysAgo, true)
|
||||
endDate = getSystemTimezoneDay(now, false)
|
||||
}
|
||||
|
||||
url += `&startDate=${encodeURIComponent(startDate.toISOString())}`
|
||||
url += `&endDate=${encodeURIComponent(endDate.toISOString())}`
|
||||
}
|
||||
}
|
||||
|
||||
const response = await apiClient.get(url)
|
||||
|
||||
@@ -1533,7 +1533,7 @@ const deleteApiKey = async (keyId) => {
|
||||
const copyApiStatsLink = (apiKey) => {
|
||||
// 构建统计页面的完整URL
|
||||
const baseUrl = window.location.origin
|
||||
const statsUrl = `${baseUrl}/admin/api-stats?apiId=${apiKey.id}`
|
||||
const statsUrl = `${baseUrl}/admin-next/api-stats?apiId=${apiKey.id}`
|
||||
|
||||
// 使用传统的textarea方法复制到剪贴板
|
||||
const textarea = document.createElement('textarea')
|
||||
|
||||
@@ -1132,7 +1132,42 @@ const tutorialSystems = [
|
||||
|
||||
// 当前基础URL
|
||||
const currentBaseUrl = computed(() => {
|
||||
return window.location.origin
|
||||
// 更健壮的获取 origin 的方法,兼容旧版浏览器和特殊环境
|
||||
let origin = ''
|
||||
|
||||
if (window.location.origin) {
|
||||
// 现代浏览器直接支持 origin
|
||||
origin = window.location.origin
|
||||
} else {
|
||||
// 旧版浏览器或特殊环境的兼容处理
|
||||
const protocol = window.location.protocol
|
||||
const hostname = window.location.hostname
|
||||
const port = window.location.port
|
||||
|
||||
origin = protocol + '//' + hostname
|
||||
|
||||
// 只有在非默认端口时才添加端口号
|
||||
if (port &&
|
||||
((protocol === 'http:' && port !== '80') ||
|
||||
(protocol === 'https:' && port !== '443'))) {
|
||||
origin += ':' + port
|
||||
}
|
||||
}
|
||||
|
||||
// 如果还是获取不到,使用当前页面的 URL 推导
|
||||
if (!origin) {
|
||||
const currentUrl = window.location.href
|
||||
const pathStart = currentUrl.indexOf('/', 8) // 跳过 http:// 或 https://
|
||||
if (pathStart !== -1) {
|
||||
origin = currentUrl.substring(0, pathStart)
|
||||
} else {
|
||||
// 最后的降级方案,使用相对路径
|
||||
console.warn('无法获取完整的 origin,将使用相对路径')
|
||||
return '/api'
|
||||
}
|
||||
}
|
||||
|
||||
return origin + '/api'
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user