mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
恢复并保存本地修改:仪表盘服务账户分类显示、WindowCountdown组件等功能
This commit is contained in:
@@ -63,13 +63,6 @@ npm run service:status # 查看服务状态
|
|||||||
npm run service:logs # 查看日志
|
npm run service:logs # 查看日志
|
||||||
npm run service:stop # 停止服务
|
npm run service:stop # 停止服务
|
||||||
|
|
||||||
# CLI管理工具
|
|
||||||
npm run cli admin # 管理员操作
|
|
||||||
npm run cli keys # API Key管理
|
|
||||||
npm run cli accounts # Claude账户管理
|
|
||||||
npm run cli status # 系统状态
|
|
||||||
```
|
|
||||||
|
|
||||||
### 开发环境配置
|
### 开发环境配置
|
||||||
必须配置的环境变量:
|
必须配置的环境变量:
|
||||||
- `JWT_SECRET`: JWT密钥(32字符以上随机字符串)
|
- `JWT_SECRET`: JWT密钥(32字符以上随机字符串)
|
||||||
|
|||||||
@@ -2300,6 +2300,7 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
|||||||
claudeAccounts,
|
claudeAccounts,
|
||||||
claudeConsoleAccounts,
|
claudeConsoleAccounts,
|
||||||
geminiAccounts,
|
geminiAccounts,
|
||||||
|
bedrockAccountsResult,
|
||||||
todayStats,
|
todayStats,
|
||||||
systemAverages,
|
systemAverages,
|
||||||
realtimeMetrics
|
realtimeMetrics
|
||||||
@@ -2309,11 +2310,15 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
|||||||
claudeAccountService.getAllAccounts(),
|
claudeAccountService.getAllAccounts(),
|
||||||
claudeConsoleAccountService.getAllAccounts(),
|
claudeConsoleAccountService.getAllAccounts(),
|
||||||
geminiAccountService.getAllAccounts(),
|
geminiAccountService.getAllAccounts(),
|
||||||
|
bedrockAccountService.getAllAccounts(),
|
||||||
redis.getTodayStats(),
|
redis.getTodayStats(),
|
||||||
redis.getSystemAverages(),
|
redis.getSystemAverages(),
|
||||||
redis.getRealtimeSystemMetrics()
|
redis.getRealtimeSystemMetrics()
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// 处理Bedrock账户数据
|
||||||
|
const bedrockAccounts = bedrockAccountsResult.success ? bedrockAccountsResult.data : []
|
||||||
|
|
||||||
// 计算使用统计(统一使用allTokens)
|
// 计算使用统计(统一使用allTokens)
|
||||||
const totalTokensUsed = apiKeys.reduce(
|
const totalTokensUsed = apiKeys.reduce(
|
||||||
(sum, key) => sum + (key.usage?.total?.allTokens || 0),
|
(sum, key) => sum + (key.usage?.total?.allTokens || 0),
|
||||||
@@ -2345,34 +2350,167 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const activeApiKeys = apiKeys.filter((key) => key.isActive).length
|
const activeApiKeys = apiKeys.filter((key) => key.isActive).length
|
||||||
const activeClaudeAccounts = claudeAccounts.filter(
|
|
||||||
(acc) => acc.isActive && acc.status === 'active'
|
// Claude账户统计 - 根据账户管理页面的判断逻辑
|
||||||
|
const normalClaudeAccounts = claudeAccounts.filter(
|
||||||
|
(acc) =>
|
||||||
|
acc.isActive &&
|
||||||
|
acc.status !== 'blocked' &&
|
||||||
|
acc.status !== 'unauthorized' &&
|
||||||
|
acc.schedulable !== false
|
||||||
|
).length
|
||||||
|
const abnormalClaudeAccounts = claudeAccounts.filter(
|
||||||
|
(acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized'
|
||||||
|
).length
|
||||||
|
const pausedClaudeAccounts = claudeAccounts.filter(
|
||||||
|
(acc) =>
|
||||||
|
acc.schedulable === false &&
|
||||||
|
acc.isActive &&
|
||||||
|
acc.status !== 'blocked' &&
|
||||||
|
acc.status !== 'unauthorized'
|
||||||
).length
|
).length
|
||||||
const rateLimitedClaudeAccounts = claudeAccounts.filter(
|
const rateLimitedClaudeAccounts = claudeAccounts.filter(
|
||||||
(acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited
|
(acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited
|
||||||
).length
|
).length
|
||||||
const activeClaudeConsoleAccounts = claudeConsoleAccounts.filter(
|
|
||||||
(acc) => acc.isActive && acc.status === 'active'
|
// Claude Console账户统计
|
||||||
|
const normalClaudeConsoleAccounts = claudeConsoleAccounts.filter(
|
||||||
|
(acc) =>
|
||||||
|
acc.isActive &&
|
||||||
|
acc.status !== 'blocked' &&
|
||||||
|
acc.status !== 'unauthorized' &&
|
||||||
|
acc.schedulable !== false
|
||||||
|
).length
|
||||||
|
const abnormalClaudeConsoleAccounts = claudeConsoleAccounts.filter(
|
||||||
|
(acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized'
|
||||||
|
).length
|
||||||
|
const pausedClaudeConsoleAccounts = claudeConsoleAccounts.filter(
|
||||||
|
(acc) =>
|
||||||
|
acc.schedulable === false &&
|
||||||
|
acc.isActive &&
|
||||||
|
acc.status !== 'blocked' &&
|
||||||
|
acc.status !== 'unauthorized'
|
||||||
).length
|
).length
|
||||||
const rateLimitedClaudeConsoleAccounts = claudeConsoleAccounts.filter(
|
const rateLimitedClaudeConsoleAccounts = claudeConsoleAccounts.filter(
|
||||||
(acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited
|
(acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited
|
||||||
).length
|
).length
|
||||||
const activeGeminiAccounts = geminiAccounts.filter(
|
|
||||||
(acc) => acc.isActive && acc.status === 'active'
|
// Gemini账户统计
|
||||||
|
const normalGeminiAccounts = geminiAccounts.filter(
|
||||||
|
(acc) =>
|
||||||
|
acc.isActive &&
|
||||||
|
acc.status !== 'blocked' &&
|
||||||
|
acc.status !== 'unauthorized' &&
|
||||||
|
acc.schedulable !== false
|
||||||
|
).length
|
||||||
|
const abnormalGeminiAccounts = geminiAccounts.filter(
|
||||||
|
(acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized'
|
||||||
|
).length
|
||||||
|
const pausedGeminiAccounts = geminiAccounts.filter(
|
||||||
|
(acc) =>
|
||||||
|
acc.schedulable === false &&
|
||||||
|
acc.isActive &&
|
||||||
|
acc.status !== 'blocked' &&
|
||||||
|
acc.status !== 'unauthorized'
|
||||||
).length
|
).length
|
||||||
const rateLimitedGeminiAccounts = geminiAccounts.filter(
|
const rateLimitedGeminiAccounts = geminiAccounts.filter(
|
||||||
(acc) => acc.rateLimitStatus === 'limited'
|
(acc) => acc.rateLimitStatus === 'limited'
|
||||||
).length
|
).length
|
||||||
|
|
||||||
|
// Bedrock账户统计
|
||||||
|
const normalBedrockAccounts = bedrockAccounts.filter(
|
||||||
|
(acc) =>
|
||||||
|
acc.isActive &&
|
||||||
|
acc.status !== 'blocked' &&
|
||||||
|
acc.status !== 'unauthorized' &&
|
||||||
|
acc.schedulable !== false
|
||||||
|
).length
|
||||||
|
const abnormalBedrockAccounts = bedrockAccounts.filter(
|
||||||
|
(acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized'
|
||||||
|
).length
|
||||||
|
const pausedBedrockAccounts = bedrockAccounts.filter(
|
||||||
|
(acc) =>
|
||||||
|
acc.schedulable === false &&
|
||||||
|
acc.isActive &&
|
||||||
|
acc.status !== 'blocked' &&
|
||||||
|
acc.status !== 'unauthorized'
|
||||||
|
).length
|
||||||
|
const rateLimitedBedrockAccounts = bedrockAccounts.filter(
|
||||||
|
(acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited
|
||||||
|
).length
|
||||||
|
|
||||||
const dashboard = {
|
const dashboard = {
|
||||||
overview: {
|
overview: {
|
||||||
totalApiKeys: apiKeys.length,
|
totalApiKeys: apiKeys.length,
|
||||||
activeApiKeys,
|
activeApiKeys,
|
||||||
|
// 总账户统计(所有平台)
|
||||||
|
totalAccounts:
|
||||||
|
claudeAccounts.length +
|
||||||
|
claudeConsoleAccounts.length +
|
||||||
|
geminiAccounts.length +
|
||||||
|
bedrockAccounts.length,
|
||||||
|
normalAccounts:
|
||||||
|
normalClaudeAccounts +
|
||||||
|
normalClaudeConsoleAccounts +
|
||||||
|
normalGeminiAccounts +
|
||||||
|
normalBedrockAccounts,
|
||||||
|
abnormalAccounts:
|
||||||
|
abnormalClaudeAccounts +
|
||||||
|
abnormalClaudeConsoleAccounts +
|
||||||
|
abnormalGeminiAccounts +
|
||||||
|
abnormalBedrockAccounts,
|
||||||
|
pausedAccounts:
|
||||||
|
pausedClaudeAccounts +
|
||||||
|
pausedClaudeConsoleAccounts +
|
||||||
|
pausedGeminiAccounts +
|
||||||
|
pausedBedrockAccounts,
|
||||||
|
rateLimitedAccounts:
|
||||||
|
rateLimitedClaudeAccounts +
|
||||||
|
rateLimitedClaudeConsoleAccounts +
|
||||||
|
rateLimitedGeminiAccounts +
|
||||||
|
rateLimitedBedrockAccounts,
|
||||||
|
// 各平台详细统计
|
||||||
|
accountsByPlatform: {
|
||||||
|
claude: {
|
||||||
|
total: claudeAccounts.length,
|
||||||
|
normal: normalClaudeAccounts,
|
||||||
|
abnormal: abnormalClaudeAccounts,
|
||||||
|
paused: pausedClaudeAccounts,
|
||||||
|
rateLimited: rateLimitedClaudeAccounts
|
||||||
|
},
|
||||||
|
'claude-console': {
|
||||||
|
total: claudeConsoleAccounts.length,
|
||||||
|
normal: normalClaudeConsoleAccounts,
|
||||||
|
abnormal: abnormalClaudeConsoleAccounts,
|
||||||
|
paused: pausedClaudeConsoleAccounts,
|
||||||
|
rateLimited: rateLimitedClaudeConsoleAccounts
|
||||||
|
},
|
||||||
|
gemini: {
|
||||||
|
total: geminiAccounts.length,
|
||||||
|
normal: normalGeminiAccounts,
|
||||||
|
abnormal: abnormalGeminiAccounts,
|
||||||
|
paused: pausedGeminiAccounts,
|
||||||
|
rateLimited: rateLimitedGeminiAccounts
|
||||||
|
},
|
||||||
|
bedrock: {
|
||||||
|
total: bedrockAccounts.length,
|
||||||
|
normal: normalBedrockAccounts,
|
||||||
|
abnormal: abnormalBedrockAccounts,
|
||||||
|
paused: pausedBedrockAccounts,
|
||||||
|
rateLimited: rateLimitedBedrockAccounts
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 保留旧字段以兼容
|
||||||
|
activeAccounts:
|
||||||
|
normalClaudeAccounts +
|
||||||
|
normalClaudeConsoleAccounts +
|
||||||
|
normalGeminiAccounts +
|
||||||
|
normalBedrockAccounts,
|
||||||
totalClaudeAccounts: claudeAccounts.length + claudeConsoleAccounts.length,
|
totalClaudeAccounts: claudeAccounts.length + claudeConsoleAccounts.length,
|
||||||
activeClaudeAccounts: activeClaudeAccounts + activeClaudeConsoleAccounts,
|
activeClaudeAccounts: normalClaudeAccounts + normalClaudeConsoleAccounts,
|
||||||
rateLimitedClaudeAccounts: rateLimitedClaudeAccounts + rateLimitedClaudeConsoleAccounts,
|
rateLimitedClaudeAccounts: rateLimitedClaudeAccounts + rateLimitedClaudeConsoleAccounts,
|
||||||
totalGeminiAccounts: geminiAccounts.length,
|
totalGeminiAccounts: geminiAccounts.length,
|
||||||
activeGeminiAccounts,
|
activeGeminiAccounts: normalGeminiAccounts,
|
||||||
rateLimitedGeminiAccounts,
|
rateLimitedGeminiAccounts,
|
||||||
totalTokensUsed,
|
totalTokensUsed,
|
||||||
totalRequestsUsed,
|
totalRequestsUsed,
|
||||||
@@ -2403,8 +2541,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
|||||||
},
|
},
|
||||||
systemHealth: {
|
systemHealth: {
|
||||||
redisConnected: redis.isConnected,
|
redisConnected: redis.isConnected,
|
||||||
claudeAccountsHealthy: activeClaudeAccounts + activeClaudeConsoleAccounts > 0,
|
claudeAccountsHealthy: normalClaudeAccounts + normalClaudeConsoleAccounts > 0,
|
||||||
geminiAccountsHealthy: activeGeminiAccounts > 0,
|
geminiAccountsHealthy: normalGeminiAccounts > 0,
|
||||||
uptime: process.uptime()
|
uptime: process.uptime()
|
||||||
},
|
},
|
||||||
systemTimezone: config.system.timezoneOffset || 8
|
systemTimezone: config.system.timezoneOffset || 8
|
||||||
|
|||||||
@@ -213,12 +213,45 @@ class ApiKeyService {
|
|||||||
if (key.rateLimitWindow > 0) {
|
if (key.rateLimitWindow > 0) {
|
||||||
const requestCountKey = `rate_limit:requests:${key.id}`
|
const requestCountKey = `rate_limit:requests:${key.id}`
|
||||||
const tokenCountKey = `rate_limit:tokens:${key.id}`
|
const tokenCountKey = `rate_limit:tokens:${key.id}`
|
||||||
|
const windowStartKey = `rate_limit:window_start:${key.id}`
|
||||||
|
|
||||||
key.currentWindowRequests = parseInt((await client.get(requestCountKey)) || '0')
|
key.currentWindowRequests = parseInt((await client.get(requestCountKey)) || '0')
|
||||||
key.currentWindowTokens = parseInt((await client.get(tokenCountKey)) || '0')
|
key.currentWindowTokens = parseInt((await client.get(tokenCountKey)) || '0')
|
||||||
|
|
||||||
|
// 获取窗口开始时间和计算剩余时间
|
||||||
|
const windowStart = await client.get(windowStartKey)
|
||||||
|
if (windowStart) {
|
||||||
|
const now = Date.now()
|
||||||
|
const windowStartTime = parseInt(windowStart)
|
||||||
|
const windowDuration = key.rateLimitWindow * 60 * 1000 // 转换为毫秒
|
||||||
|
const windowEndTime = windowStartTime + windowDuration
|
||||||
|
|
||||||
|
// 如果窗口还有效
|
||||||
|
if (now < windowEndTime) {
|
||||||
|
key.windowStartTime = windowStartTime
|
||||||
|
key.windowEndTime = windowEndTime
|
||||||
|
key.windowRemainingSeconds = Math.max(0, Math.floor((windowEndTime - now) / 1000))
|
||||||
|
} else {
|
||||||
|
// 窗口已过期,下次请求会重置
|
||||||
|
key.windowStartTime = null
|
||||||
|
key.windowEndTime = null
|
||||||
|
key.windowRemainingSeconds = 0
|
||||||
|
// 重置计数为0,因为窗口已过期
|
||||||
|
key.currentWindowRequests = 0
|
||||||
|
key.currentWindowTokens = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 窗口还未开始(没有任何请求)
|
||||||
|
key.windowStartTime = null
|
||||||
|
key.windowEndTime = null
|
||||||
|
key.windowRemainingSeconds = null
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
key.currentWindowRequests = 0
|
key.currentWindowRequests = 0
|
||||||
key.currentWindowTokens = 0
|
key.currentWindowTokens = 0
|
||||||
|
key.windowStartTime = null
|
||||||
|
key.windowEndTime = null
|
||||||
|
key.windowRemainingSeconds = null
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1828,15 +1828,15 @@ const handleGroupRefresh = async () => {
|
|||||||
// 监听平台变化,重置表单
|
// 监听平台变化,重置表单
|
||||||
watch(
|
watch(
|
||||||
() => form.value.platform,
|
() => form.value.platform,
|
||||||
(newPlatform, oldPlatform) => {
|
(newPlatform) => {
|
||||||
// 处理添加方式的自动切换
|
// 处理添加方式的自动切换
|
||||||
if (newPlatform === 'claude-console' || newPlatform === 'bedrock') {
|
if (newPlatform === 'claude-console' || newPlatform === 'bedrock') {
|
||||||
form.value.addType = 'manual' // Claude Console 和 Bedrock 只支持手动模式
|
form.value.addType = 'manual' // Claude Console 和 Bedrock 只支持手动模式
|
||||||
} else if (
|
} else if (newPlatform === 'claude') {
|
||||||
oldPlatform === 'claude-console' &&
|
// 切换到 Claude 时,使用 Setup Token 作为默认方式
|
||||||
(newPlatform === 'claude' || newPlatform === 'gemini')
|
form.value.addType = 'setup-token'
|
||||||
) {
|
} else if (newPlatform === 'gemini') {
|
||||||
// 从 Claude Console 切换到其他平台时,恢复为 OAuth
|
// 切换到 Gemini 时,使用 OAuth 作为默认方式
|
||||||
form.value.addType = 'oauth'
|
form.value.addType = 'oauth'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -183,47 +183,23 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="apiKey.rateLimitWindow > 0" class="space-y-2">
|
<div v-if="apiKey.rateLimitWindow > 0" class="space-y-2">
|
||||||
<div class="flex items-center justify-between text-sm">
|
<h5 class="text-sm font-medium text-gray-700">
|
||||||
<span class="text-gray-600">时间窗口</span>
|
<i class="fas fa-clock mr-1 text-blue-500" />
|
||||||
<span class="font-semibold text-indigo-600">
|
时间窗口限制
|
||||||
{{ apiKey.rateLimitWindow }} 分钟
|
</h5>
|
||||||
</span>
|
<WindowCountdown
|
||||||
</div>
|
:current-requests="apiKey.currentWindowRequests"
|
||||||
|
:current-tokens="apiKey.currentWindowTokens"
|
||||||
<!-- 请求次数限制 -->
|
label="窗口状态"
|
||||||
<div v-if="apiKey.rateLimitRequests > 0" class="space-y-1">
|
:rate-limit-window="apiKey.rateLimitWindow"
|
||||||
<div class="flex items-center justify-between text-sm">
|
:request-limit="apiKey.rateLimitRequests"
|
||||||
<span class="text-gray-600">请求限制</span>
|
:show-progress="true"
|
||||||
<span class="font-semibold text-gray-900">
|
:show-tooltip="true"
|
||||||
{{ apiKey.currentWindowRequests || 0 }} / {{ apiKey.rateLimitRequests }}
|
:token-limit="apiKey.tokenLimit"
|
||||||
</span>
|
:window-end-time="apiKey.windowEndTime"
|
||||||
</div>
|
:window-remaining-seconds="apiKey.windowRemainingSeconds"
|
||||||
<div class="h-2 w-full rounded-full bg-gray-200">
|
:window-start-time="apiKey.windowStartTime"
|
||||||
<div
|
/>
|
||||||
class="h-2 rounded-full transition-all duration-300"
|
|
||||||
:class="windowRequestProgressColor"
|
|
||||||
:style="{ width: windowRequestProgress + '%' }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Token使用量限制 -->
|
|
||||||
<div v-if="apiKey.tokenLimit > 0" class="space-y-1">
|
|
||||||
<div class="flex items-center justify-between text-sm">
|
|
||||||
<span class="text-gray-600">Token限制</span>
|
|
||||||
<span class="font-semibold text-gray-900">
|
|
||||||
{{ formatTokenCount(apiKey.currentWindowTokens || 0) }} /
|
|
||||||
{{ formatTokenCount(apiKey.tokenLimit) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="h-2 w-full rounded-full bg-gray-200">
|
|
||||||
<div
|
|
||||||
class="h-2 rounded-full transition-all duration-300"
|
|
||||||
:class="windowTokenProgressColor"
|
|
||||||
:style="{ width: windowTokenProgress + '%' }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -242,6 +218,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import WindowCountdown from './WindowCountdown.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: {
|
show: {
|
||||||
@@ -284,35 +261,6 @@ const dailyCostPercentage = computed(() => {
|
|||||||
return (dailyCost.value / props.apiKey.dailyCostLimit) * 100
|
return (dailyCost.value / props.apiKey.dailyCostLimit) * 100
|
||||||
})
|
})
|
||||||
|
|
||||||
// 窗口请求进度
|
|
||||||
const windowRequestProgress = computed(() => {
|
|
||||||
if (!props.apiKey.rateLimitRequests || props.apiKey.rateLimitRequests === 0) return 0
|
|
||||||
const percentage =
|
|
||||||
((props.apiKey.currentWindowRequests || 0) / props.apiKey.rateLimitRequests) * 100
|
|
||||||
return Math.min(percentage, 100)
|
|
||||||
})
|
|
||||||
|
|
||||||
const windowRequestProgressColor = computed(() => {
|
|
||||||
const progress = windowRequestProgress.value
|
|
||||||
if (progress >= 100) return 'bg-red-500'
|
|
||||||
if (progress >= 80) return 'bg-yellow-500'
|
|
||||||
return 'bg-blue-500'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 窗口Token进度
|
|
||||||
const windowTokenProgress = computed(() => {
|
|
||||||
if (!props.apiKey.tokenLimit || props.apiKey.tokenLimit === 0) return 0
|
|
||||||
const percentage = ((props.apiKey.currentWindowTokens || 0) / props.apiKey.tokenLimit) * 100
|
|
||||||
return Math.min(percentage, 100)
|
|
||||||
})
|
|
||||||
|
|
||||||
const windowTokenProgressColor = computed(() => {
|
|
||||||
const progress = windowTokenProgress.value
|
|
||||||
if (progress >= 100) return 'bg-red-500'
|
|
||||||
if (progress >= 80) return 'bg-yellow-500'
|
|
||||||
return 'bg-purple-500'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
const formatNumber = (num) => {
|
const formatNumber = (num) => {
|
||||||
if (!num && num !== 0) return '0'
|
if (!num && num !== 0) return '0'
|
||||||
|
|||||||
@@ -10,8 +10,17 @@ export const useDashboardStore = defineStore('dashboard', () => {
|
|||||||
totalApiKeys: 0,
|
totalApiKeys: 0,
|
||||||
activeApiKeys: 0,
|
activeApiKeys: 0,
|
||||||
totalAccounts: 0,
|
totalAccounts: 0,
|
||||||
activeAccounts: 0,
|
normalAccounts: 0,
|
||||||
|
abnormalAccounts: 0,
|
||||||
|
pausedAccounts: 0,
|
||||||
|
activeAccounts: 0, // 保留兼容
|
||||||
rateLimitedAccounts: 0,
|
rateLimitedAccounts: 0,
|
||||||
|
accountsByPlatform: {
|
||||||
|
claude: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 },
|
||||||
|
'claude-console': { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 },
|
||||||
|
gemini: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 },
|
||||||
|
bedrock: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 }
|
||||||
|
},
|
||||||
todayRequests: 0,
|
todayRequests: 0,
|
||||||
totalRequests: 0,
|
totalRequests: 0,
|
||||||
todayTokens: 0,
|
todayTokens: 0,
|
||||||
@@ -152,9 +161,21 @@ export const useDashboardStore = defineStore('dashboard', () => {
|
|||||||
dashboardData.value = {
|
dashboardData.value = {
|
||||||
totalApiKeys: overview.totalApiKeys || 0,
|
totalApiKeys: overview.totalApiKeys || 0,
|
||||||
activeApiKeys: overview.activeApiKeys || 0,
|
activeApiKeys: overview.activeApiKeys || 0,
|
||||||
totalAccounts: overview.totalClaudeAccounts || 0,
|
// 使用新的统一统计字段
|
||||||
activeAccounts: overview.activeClaudeAccounts || 0,
|
totalAccounts: overview.totalAccounts || overview.totalClaudeAccounts || 0,
|
||||||
rateLimitedAccounts: overview.rateLimitedClaudeAccounts || 0,
|
normalAccounts: overview.normalAccounts || 0,
|
||||||
|
abnormalAccounts: overview.abnormalAccounts || 0,
|
||||||
|
pausedAccounts: overview.pausedAccounts || 0,
|
||||||
|
activeAccounts: overview.activeAccounts || overview.activeClaudeAccounts || 0, // 兼容
|
||||||
|
rateLimitedAccounts:
|
||||||
|
overview.rateLimitedAccounts || overview.rateLimitedClaudeAccounts || 0,
|
||||||
|
// 各平台详细统计
|
||||||
|
accountsByPlatform: overview.accountsByPlatform || {
|
||||||
|
claude: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 },
|
||||||
|
'claude-console': { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 },
|
||||||
|
gemini: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 },
|
||||||
|
bedrock: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 }
|
||||||
|
},
|
||||||
todayRequests: recentActivity.requestsToday || 0,
|
todayRequests: recentActivity.requestsToday || 0,
|
||||||
totalRequests: overview.totalRequestsUsed || 0,
|
totalRequests: overview.totalRequestsUsed || 0,
|
||||||
todayTokens: recentActivity.tokensToday || 0,
|
todayTokens: recentActivity.tokensToday || 0,
|
||||||
|
|||||||
@@ -254,48 +254,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 时间窗口限制进度条 -->
|
<!-- 时间窗口限制进度条 -->
|
||||||
<div v-if="key.rateLimitWindow > 0" class="space-y-1">
|
<WindowCountdown
|
||||||
<div class="flex items-center justify-between text-xs">
|
v-if="key.rateLimitWindow > 0"
|
||||||
<span class="text-gray-500">窗口限制</span>
|
:current-requests="key.currentWindowRequests"
|
||||||
<span class="text-gray-700"> {{ key.rateLimitWindow }}分钟 </span>
|
:current-tokens="key.currentWindowTokens"
|
||||||
</div>
|
:rate-limit-window="key.rateLimitWindow"
|
||||||
|
:request-limit="key.rateLimitRequests"
|
||||||
<!-- 请求次数限制 -->
|
:show-progress="true"
|
||||||
<div v-if="key.rateLimitRequests > 0" class="space-y-0.5">
|
:show-tooltip="false"
|
||||||
<div class="flex items-center justify-between text-xs">
|
:token-limit="key.tokenLimit"
|
||||||
<span class="text-gray-400">请求</span>
|
:window-end-time="key.windowEndTime"
|
||||||
<span class="text-gray-600">
|
:window-remaining-seconds="key.windowRemainingSeconds"
|
||||||
{{ key.currentWindowRequests || 0 }}/{{ key.rateLimitRequests }}
|
:window-start-time="key.windowStartTime"
|
||||||
</span>
|
/>
|
||||||
</div>
|
|
||||||
<div class="h-1 w-full rounded-full bg-gray-200">
|
|
||||||
<div
|
|
||||||
class="h-1 rounded-full transition-all duration-300"
|
|
||||||
:class="getWindowRequestProgressColor(key)"
|
|
||||||
:style="{ width: getWindowRequestProgress(key) + '%' }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Token使用量限制 -->
|
|
||||||
<div v-if="key.tokenLimit > 0" class="space-y-0.5">
|
|
||||||
<div class="flex items-center justify-between text-xs">
|
|
||||||
<span class="text-gray-400">Token</span>
|
|
||||||
<span class="text-gray-600">
|
|
||||||
{{ formatTokenCount(key.currentWindowTokens || 0) }}/{{
|
|
||||||
formatTokenCount(key.tokenLimit)
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="h-1 w-full rounded-full bg-gray-200">
|
|
||||||
<div
|
|
||||||
class="h-1 rounded-full transition-all duration-300"
|
|
||||||
:class="getWindowTokenProgressColor(key)"
|
|
||||||
:style="{ width: getWindowTokenProgress(key) + '%' }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 查看详情按钮 -->
|
<!-- 查看详情按钮 -->
|
||||||
<div class="pt-1">
|
<div class="pt-1">
|
||||||
@@ -743,46 +714,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 移动端时间窗口限制 -->
|
<!-- 移动端时间窗口限制 -->
|
||||||
<div
|
<WindowCountdown
|
||||||
v-if="key.rateLimitWindow > 0 && (key.rateLimitRequests > 0 || key.tokenLimit > 0)"
|
v-if="key.rateLimitWindow > 0 && (key.rateLimitRequests > 0 || key.tokenLimit > 0)"
|
||||||
class="space-y-1"
|
:current-requests="key.currentWindowRequests"
|
||||||
>
|
:current-tokens="key.currentWindowTokens"
|
||||||
<div class="mb-1 text-xs text-gray-500">窗口限制 ({{ key.rateLimitWindow }}分钟)</div>
|
:rate-limit-window="key.rateLimitWindow"
|
||||||
|
:request-limit="key.rateLimitRequests"
|
||||||
<div v-if="key.rateLimitRequests > 0" class="flex items-center gap-2">
|
:show-progress="true"
|
||||||
<span class="w-10 text-xs text-gray-500">请求</span>
|
:show-tooltip="false"
|
||||||
<div class="flex-1">
|
:token-limit="key.tokenLimit"
|
||||||
<div class="h-1.5 w-full rounded-full bg-gray-200">
|
:window-end-time="key.windowEndTime"
|
||||||
<div
|
:window-remaining-seconds="key.windowRemainingSeconds"
|
||||||
class="h-1.5 rounded-full transition-all duration-300"
|
:window-start-time="key.windowStartTime"
|
||||||
:class="getWindowRequestProgressColor(key)"
|
/>
|
||||||
:style="{ width: getWindowRequestProgress(key) + '%' }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="w-16 text-right text-xs text-gray-600">
|
|
||||||
{{ key.currentWindowRequests || 0 }}/{{ key.rateLimitRequests }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="key.tokenLimit > 0" class="flex items-center gap-2">
|
|
||||||
<span class="w-10 text-xs text-gray-500">Token</span>
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="h-1.5 w-full rounded-full bg-gray-200">
|
|
||||||
<div
|
|
||||||
class="h-1.5 rounded-full transition-all duration-300"
|
|
||||||
:class="getWindowTokenProgressColor(key)"
|
|
||||||
:style="{ width: getWindowTokenProgress(key) + '%' }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="w-16 text-right text-xs text-gray-600">
|
|
||||||
{{ formatTokenCount(key.currentWindowTokens || 0) }}/{{
|
|
||||||
formatTokenCount(key.tokenLimit)
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 时间信息 -->
|
<!-- 时间信息 -->
|
||||||
@@ -1019,6 +963,7 @@ import NewApiKeyModal from '@/components/apikeys/NewApiKeyModal.vue'
|
|||||||
import BatchApiKeyModal from '@/components/apikeys/BatchApiKeyModal.vue'
|
import BatchApiKeyModal from '@/components/apikeys/BatchApiKeyModal.vue'
|
||||||
import ExpiryEditModal from '@/components/apikeys/ExpiryEditModal.vue'
|
import ExpiryEditModal from '@/components/apikeys/ExpiryEditModal.vue'
|
||||||
import UsageDetailModal from '@/components/apikeys/UsageDetailModal.vue'
|
import UsageDetailModal from '@/components/apikeys/UsageDetailModal.vue'
|
||||||
|
import WindowCountdown from '@/components/apikeys/WindowCountdown.vue'
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const clientsStore = useClientsStore()
|
const clientsStore = useClientsStore()
|
||||||
@@ -1662,36 +1607,6 @@ const formatTokenCount = (count) => {
|
|||||||
return count.toString()
|
return count.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取窗口请求进度
|
|
||||||
const getWindowRequestProgress = (key) => {
|
|
||||||
if (!key.rateLimitRequests || key.rateLimitRequests === 0) return 0
|
|
||||||
const percentage = ((key.currentWindowRequests || 0) / key.rateLimitRequests) * 100
|
|
||||||
return Math.min(percentage, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取窗口请求进度条颜色
|
|
||||||
const getWindowRequestProgressColor = (key) => {
|
|
||||||
const progress = getWindowRequestProgress(key)
|
|
||||||
if (progress >= 100) return 'bg-red-500'
|
|
||||||
if (progress >= 80) return 'bg-yellow-500'
|
|
||||||
return 'bg-blue-500'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取窗口Token进度
|
|
||||||
const getWindowTokenProgress = (key) => {
|
|
||||||
if (!key.tokenLimit || key.tokenLimit === 0) return 0
|
|
||||||
const percentage = ((key.currentWindowTokens || 0) / key.tokenLimit) * 100
|
|
||||||
return Math.min(percentage, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取窗口Token进度条颜色
|
|
||||||
const getWindowTokenProgressColor = (key) => {
|
|
||||||
const progress = getWindowTokenProgress(key)
|
|
||||||
if (progress >= 100) return 'bg-red-500'
|
|
||||||
if (progress >= 80) return 'bg-yellow-500'
|
|
||||||
return 'bg-purple-500'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听筛选条件变化,重置页码
|
// 监听筛选条件变化,重置页码
|
||||||
watch([selectedTagFilter, apiKeyStatsTimeRange], () => {
|
watch([selectedTagFilter, apiKeyStatsTimeRange], () => {
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
|
|||||||
@@ -21,19 +21,86 @@
|
|||||||
|
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div class="flex-1">
|
||||||
<p class="mb-1 text-xs font-semibold text-gray-600 sm:text-sm">服务账户</p>
|
<p class="mb-1 text-xs font-semibold text-gray-600 sm:text-sm">服务账户</p>
|
||||||
<p class="text-2xl font-bold text-gray-900 sm:text-3xl">
|
<div class="flex flex-wrap items-baseline gap-x-2">
|
||||||
{{ dashboardData.totalAccounts }}
|
<p class="text-2xl font-bold text-gray-900 sm:text-3xl">
|
||||||
</p>
|
{{ dashboardData.totalAccounts }}
|
||||||
|
</p>
|
||||||
|
<!-- 各平台账户数量展示 -->
|
||||||
|
<div v-if="dashboardData.accountsByPlatform" class="flex items-center gap-2">
|
||||||
|
<!-- Claude账户 -->
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
dashboardData.accountsByPlatform.claude &&
|
||||||
|
dashboardData.accountsByPlatform.claude.total > 0
|
||||||
|
"
|
||||||
|
class="inline-flex items-center gap-0.5"
|
||||||
|
:title="`Claude: ${dashboardData.accountsByPlatform.claude.total} 个 (正常: ${dashboardData.accountsByPlatform.claude.normal})`"
|
||||||
|
>
|
||||||
|
<i class="fas fa-brain text-xs text-indigo-600" />
|
||||||
|
<span class="text-xs font-medium text-gray-700">{{
|
||||||
|
dashboardData.accountsByPlatform.claude.total
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<!-- Claude Console账户 -->
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
dashboardData.accountsByPlatform['claude-console'] &&
|
||||||
|
dashboardData.accountsByPlatform['claude-console'].total > 0
|
||||||
|
"
|
||||||
|
class="inline-flex items-center gap-0.5"
|
||||||
|
:title="`Console: ${dashboardData.accountsByPlatform['claude-console'].total} 个 (正常: ${dashboardData.accountsByPlatform['claude-console'].normal})`"
|
||||||
|
>
|
||||||
|
<i class="fas fa-terminal text-xs text-purple-600" />
|
||||||
|
<span class="text-xs font-medium text-gray-700">{{
|
||||||
|
dashboardData.accountsByPlatform['claude-console'].total
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<!-- Gemini账户 -->
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
dashboardData.accountsByPlatform.gemini &&
|
||||||
|
dashboardData.accountsByPlatform.gemini.total > 0
|
||||||
|
"
|
||||||
|
class="inline-flex items-center gap-0.5"
|
||||||
|
:title="`Gemini: ${dashboardData.accountsByPlatform.gemini.total} 个 (正常: ${dashboardData.accountsByPlatform.gemini.normal})`"
|
||||||
|
>
|
||||||
|
<i class="fas fa-robot text-xs text-yellow-600" />
|
||||||
|
<span class="text-xs font-medium text-gray-700">{{
|
||||||
|
dashboardData.accountsByPlatform.gemini.total
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<!-- Bedrock账户 -->
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
dashboardData.accountsByPlatform.bedrock &&
|
||||||
|
dashboardData.accountsByPlatform.bedrock.total > 0
|
||||||
|
"
|
||||||
|
class="inline-flex items-center gap-0.5"
|
||||||
|
:title="`Bedrock: ${dashboardData.accountsByPlatform.bedrock.total} 个 (正常: ${dashboardData.accountsByPlatform.bedrock.normal})`"
|
||||||
|
>
|
||||||
|
<i class="fab fa-aws text-xs text-orange-600" />
|
||||||
|
<span class="text-xs font-medium text-gray-700">{{
|
||||||
|
dashboardData.accountsByPlatform.bedrock.total
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p class="mt-1 text-xs text-gray-500">
|
<p class="mt-1 text-xs text-gray-500">
|
||||||
活跃: {{ dashboardData.activeAccounts || 0 }}
|
正常: {{ dashboardData.normalAccounts || 0 }}
|
||||||
|
<span v-if="dashboardData.abnormalAccounts > 0" class="text-red-600">
|
||||||
|
| 异常: {{ dashboardData.abnormalAccounts }}
|
||||||
|
</span>
|
||||||
|
<span v-if="dashboardData.pausedAccounts > 0" class="text-gray-600">
|
||||||
|
| 停止调度: {{ dashboardData.pausedAccounts }}
|
||||||
|
</span>
|
||||||
<span v-if="dashboardData.rateLimitedAccounts > 0" class="text-yellow-600">
|
<span v-if="dashboardData.rateLimitedAccounts > 0" class="text-yellow-600">
|
||||||
| 限流: {{ dashboardData.rateLimitedAccounts }}
|
| 限流: {{ dashboardData.rateLimitedAccounts }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-green-500 to-green-600">
|
<div class="stat-icon ml-2 flex-shrink-0 bg-gradient-to-br from-green-500 to-green-600">
|
||||||
<i class="fas fa-user-circle" />
|
<i class="fas fa-user-circle" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user