mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-05-11 02:24:31 +00:00
Merge remote-tracking branch 'f3n9/main' into um-5
This commit is contained in:
@@ -191,7 +191,39 @@
|
||||
<th
|
||||
class="w-[10%] min-w-[100px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
会话窗口
|
||||
<div class="flex items-center gap-2">
|
||||
<span>会话窗口</span>
|
||||
<el-tooltip placement="top">
|
||||
<template #content>
|
||||
<div class="space-y-2">
|
||||
<div>会话窗口进度表示5小时窗口的时间进度</div>
|
||||
<div class="space-y-1 text-xs">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="h-2 w-16 rounded bg-gradient-to-r from-blue-500 to-indigo-600"
|
||||
></div>
|
||||
<span>正常:请求正常处理</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="h-2 w-16 rounded bg-gradient-to-r from-yellow-500 to-orange-500"
|
||||
></div>
|
||||
<span>警告:接近限制</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="h-2 w-16 rounded bg-gradient-to-r from-red-500 to-red-600"
|
||||
></div>
|
||||
<span>拒绝:达到速率限制</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<i
|
||||
class="fas fa-question-circle cursor-help text-xs text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-400"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
class="w-[8%] min-w-[80px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||
@@ -240,12 +272,14 @@
|
||||
>
|
||||
<i class="fas fa-share-alt mr-1" />共享
|
||||
</span>
|
||||
<!-- 显示所有分组 -->
|
||||
<span
|
||||
v-if="account.groupInfo"
|
||||
v-for="group in account.groupInfos"
|
||||
:key="group.id"
|
||||
class="ml-1 inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-400"
|
||||
:title="`所属分组: ${account.groupInfo.name}`"
|
||||
:title="`所属分组: ${group.name}`"
|
||||
>
|
||||
<i class="fas fa-folder mr-1" />{{ account.groupInfo.name }}
|
||||
<i class="fas fa-folder mr-1" />{{ group.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -393,6 +427,14 @@
|
||||
>
|
||||
<i class="fas fa-pause-circle mr-1" />
|
||||
不可调度
|
||||
<el-tooltip
|
||||
v-if="getSchedulableReason(account)"
|
||||
:content="getSchedulableReason(account)"
|
||||
effect="dark"
|
||||
placement="top"
|
||||
>
|
||||
<i class="fas fa-question-circle ml-1 cursor-help text-gray-500" />
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<span
|
||||
v-if="account.status === 'blocked' && account.errorMessage"
|
||||
@@ -447,15 +489,21 @@
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm">
|
||||
<div v-if="account.usage && account.usage.daily" class="space-y-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-2 w-2 rounded-full bg-green-500" />
|
||||
<div class="h-2 w-2 rounded-full bg-blue-500" />
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
>{{ account.usage.daily.requests || 0 }} 次</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-2 w-2 rounded-full bg-blue-500" />
|
||||
<div class="h-2 w-2 rounded-full bg-purple-500" />
|
||||
<span class="text-xs text-gray-600 dark:text-gray-300"
|
||||
>{{ formatNumber(account.usage.daily.allTokens || 0) }} tokens</span
|
||||
>{{ formatNumber(account.usage.daily.allTokens || 0) }}M</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-2 w-2 rounded-full bg-green-500" />
|
||||
<span class="text-xs text-gray-600 dark:text-gray-300"
|
||||
>${{ calculateDailyCost(account) }}</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
@@ -476,10 +524,33 @@
|
||||
"
|
||||
class="space-y-2"
|
||||
>
|
||||
<!-- 使用统计在顶部 -->
|
||||
<div
|
||||
v-if="account.usage && account.usage.sessionWindow"
|
||||
class="flex items-center gap-3 text-xs"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="h-1.5 w-1.5 rounded-full bg-purple-500" />
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ formatNumber(account.usage.sessionWindow.totalTokens) }}M
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="h-1.5 w-1.5 rounded-full bg-green-500" />
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">
|
||||
${{ formatCost(account.usage.sessionWindow.totalCost) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-2 w-24 rounded-full bg-gray-200">
|
||||
<div class="h-2 w-24 rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div
|
||||
class="h-2 rounded-full bg-gradient-to-r from-blue-500 to-indigo-600 transition-all duration-300"
|
||||
:class="[
|
||||
'h-2 rounded-full transition-all duration-300',
|
||||
getSessionProgressBarClass(account.sessionWindow.sessionWindowStatus)
|
||||
]"
|
||||
:style="{ width: account.sessionWindow.progress + '%' }"
|
||||
/>
|
||||
</div>
|
||||
@@ -487,7 +558,9 @@
|
||||
{{ account.sessionWindow.progress }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-300">
|
||||
|
||||
<!-- 时间信息 -->
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
<div>
|
||||
{{
|
||||
formatSessionWindow(
|
||||
@@ -498,7 +571,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="account.sessionWindow.remainingTime > 0"
|
||||
class="font-medium text-indigo-600"
|
||||
class="font-medium text-indigo-600 dark:text-indigo-400"
|
||||
>
|
||||
剩余 {{ formatRemainingTime(account.sessionWindow.remainingTime) }}
|
||||
</div>
|
||||
@@ -646,21 +719,44 @@
|
||||
<div class="mb-3 grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">今日使用</p>
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ formatNumber(account.usage?.daily?.requests || 0) }} 次
|
||||
</p>
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ formatNumber(account.usage?.daily?.allTokens || 0) }} tokens
|
||||
</p>
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="h-1.5 w-1.5 rounded-full bg-blue-500" />
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ account.usage?.daily?.requests || 0 }} 次
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="h-1.5 w-1.5 rounded-full bg-purple-500" />
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{{ formatNumber(account.usage?.daily?.allTokens || 0) }}M
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="h-1.5 w-1.5 rounded-full bg-green-500" />
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
${{ calculateDailyCost(account) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">总使用量</p>
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ formatNumber(account.usage?.total?.requests || 0) }} 次
|
||||
</p>
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ formatNumber(account.usage?.total?.allTokens || 0) }} tokens
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">会话窗口</p>
|
||||
<div v-if="account.usage && account.usage.sessionWindow" class="space-y-1">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="h-1.5 w-1.5 rounded-full bg-purple-500" />
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ formatNumber(account.usage.sessionWindow.totalTokens) }}M
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="h-1.5 w-1.5 rounded-full bg-green-500" />
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
${{ formatCost(account.usage.sessionWindow.totalCost) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-sm font-semibold text-gray-400">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -676,14 +772,27 @@
|
||||
class="space-y-1.5 rounded-lg bg-gray-50 p-2 dark:bg-gray-700"
|
||||
>
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="font-medium text-gray-600 dark:text-gray-300">会话窗口</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="font-medium text-gray-600 dark:text-gray-300">会话窗口</span>
|
||||
<el-tooltip
|
||||
content="会话窗口进度不代表使用量,仅表示距离下一个5小时窗口的剩余时间"
|
||||
placement="top"
|
||||
>
|
||||
<i
|
||||
class="fas fa-question-circle cursor-help text-xs text-gray-400 hover:text-gray-600"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-200">
|
||||
{{ account.sessionWindow.progress }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="h-2 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-600">
|
||||
<div
|
||||
class="h-full bg-gradient-to-r from-blue-500 to-indigo-600 transition-all duration-300"
|
||||
:class="[
|
||||
'h-full transition-all duration-300',
|
||||
getSessionProgressBarClass(account.sessionWindow.sessionWindowStatus)
|
||||
]"
|
||||
:style="{ width: account.sessionWindow.progress + '%' }"
|
||||
/>
|
||||
</div>
|
||||
@@ -824,7 +933,7 @@ const platformFilter = ref('all')
|
||||
const apiKeysLoaded = ref(false)
|
||||
const groupsLoaded = ref(false)
|
||||
const groupMembersLoaded = ref(false)
|
||||
const accountGroupMap = ref(new Map())
|
||||
const accountGroupMap = ref(new Map()) // Map<accountId, Array<groupInfo>>
|
||||
|
||||
// 下拉选项数据
|
||||
const sortOptions = ref([
|
||||
@@ -945,7 +1054,9 @@ const loadAccounts = async (forceReload = false) => {
|
||||
apiClient.get('/admin/claude-accounts', { params }),
|
||||
Promise.resolve({ success: true, data: [] }), // claude-console 占位
|
||||
Promise.resolve({ success: true, data: [] }), // bedrock 占位
|
||||
Promise.resolve({ success: true, data: [] }) // gemini 占位
|
||||
Promise.resolve({ success: true, data: [] }), // gemini 占位
|
||||
Promise.resolve({ success: true, data: [] }), // openai 占位
|
||||
Promise.resolve({ success: true, data: [] }) // azure-openai 占位
|
||||
)
|
||||
break
|
||||
case 'claude-console':
|
||||
@@ -953,7 +1064,9 @@ const loadAccounts = async (forceReload = false) => {
|
||||
Promise.resolve({ success: true, data: [] }), // claude 占位
|
||||
apiClient.get('/admin/claude-console-accounts', { params }),
|
||||
Promise.resolve({ success: true, data: [] }), // bedrock 占位
|
||||
Promise.resolve({ success: true, data: [] }) // gemini 占位
|
||||
Promise.resolve({ success: true, data: [] }), // gemini 占位
|
||||
Promise.resolve({ success: true, data: [] }), // openai 占位
|
||||
Promise.resolve({ success: true, data: [] }) // azure-openai 占位
|
||||
)
|
||||
break
|
||||
case 'bedrock':
|
||||
@@ -961,7 +1074,9 @@ const loadAccounts = async (forceReload = false) => {
|
||||
Promise.resolve({ success: true, data: [] }), // claude 占位
|
||||
Promise.resolve({ success: true, data: [] }), // claude-console 占位
|
||||
apiClient.get('/admin/bedrock-accounts', { params }),
|
||||
Promise.resolve({ success: true, data: [] }) // gemini 占位
|
||||
Promise.resolve({ success: true, data: [] }), // gemini 占位
|
||||
Promise.resolve({ success: true, data: [] }), // openai 占位
|
||||
Promise.resolve({ success: true, data: [] }) // azure-openai 占位
|
||||
)
|
||||
break
|
||||
case 'gemini':
|
||||
@@ -969,7 +1084,29 @@ const loadAccounts = async (forceReload = false) => {
|
||||
Promise.resolve({ success: true, data: [] }), // claude 占位
|
||||
Promise.resolve({ success: true, data: [] }), // claude-console 占位
|
||||
Promise.resolve({ success: true, data: [] }), // bedrock 占位
|
||||
apiClient.get('/admin/gemini-accounts', { params })
|
||||
apiClient.get('/admin/gemini-accounts', { params }),
|
||||
Promise.resolve({ success: true, data: [] }), // openai 占位
|
||||
Promise.resolve({ success: true, data: [] }) // azure-openai 占位
|
||||
)
|
||||
break
|
||||
case 'openai':
|
||||
requests.push(
|
||||
Promise.resolve({ success: true, data: [] }), // claude 占位
|
||||
Promise.resolve({ success: true, data: [] }), // claude-console 占位
|
||||
Promise.resolve({ success: true, data: [] }), // bedrock 占位
|
||||
Promise.resolve({ success: true, data: [] }), // gemini 占位
|
||||
apiClient.get('/admin/openai-accounts', { params }),
|
||||
Promise.resolve({ success: true, data: [] }) // azure-openai 占位
|
||||
)
|
||||
break
|
||||
case 'azure_openai':
|
||||
requests.push(
|
||||
Promise.resolve({ success: true, data: [] }), // claude 占位
|
||||
Promise.resolve({ success: true, data: [] }), // claude-console 占位
|
||||
Promise.resolve({ success: true, data: [] }), // bedrock 占位
|
||||
Promise.resolve({ success: true, data: [] }), // gemini 占位
|
||||
Promise.resolve({ success: true, data: [] }), // openai 占位
|
||||
apiClient.get('/admin/azure-openai-accounts', { params })
|
||||
)
|
||||
break
|
||||
}
|
||||
@@ -978,8 +1115,8 @@ const loadAccounts = async (forceReload = false) => {
|
||||
// 使用缓存机制加载 API Keys 和分组数据
|
||||
await Promise.all([loadApiKeys(forceReload), loadAccountGroups(forceReload)])
|
||||
|
||||
// 加载分组成员关系(需要在分组数据加载完成后)
|
||||
await loadGroupMembers(forceReload)
|
||||
// 后端账户API已经包含分组信息,不需要单独加载分组成员关系
|
||||
// await loadGroupMembers(forceReload)
|
||||
|
||||
const [claudeData, claudeConsoleData, bedrockData, geminiData, openaiData, azureOpenaiData] =
|
||||
await Promise.all(requests)
|
||||
@@ -992,9 +1129,8 @@ const loadAccounts = async (forceReload = false) => {
|
||||
const boundApiKeysCount = apiKeys.value.filter(
|
||||
(key) => key.claudeAccountId === acc.id
|
||||
).length
|
||||
// 检查是否属于某个分组
|
||||
const groupInfo = accountGroupMap.value.get(acc.id) || null
|
||||
return { ...acc, platform: 'claude', boundApiKeysCount, groupInfo }
|
||||
// 后端已经包含了groupInfos,直接使用
|
||||
return { ...acc, platform: 'claude', boundApiKeysCount }
|
||||
})
|
||||
allAccounts.push(...claudeAccounts)
|
||||
}
|
||||
@@ -1002,8 +1138,8 @@ const loadAccounts = async (forceReload = false) => {
|
||||
if (claudeConsoleData.success) {
|
||||
const claudeConsoleAccounts = (claudeConsoleData.data || []).map((acc) => {
|
||||
// Claude Console账户暂时不支持直接绑定
|
||||
const groupInfo = accountGroupMap.value.get(acc.id) || null
|
||||
return { ...acc, platform: 'claude-console', boundApiKeysCount: 0, groupInfo }
|
||||
// 后端已经包含了groupInfos,直接使用
|
||||
return { ...acc, platform: 'claude-console', boundApiKeysCount: 0 }
|
||||
})
|
||||
allAccounts.push(...claudeConsoleAccounts)
|
||||
}
|
||||
@@ -1011,8 +1147,8 @@ const loadAccounts = async (forceReload = false) => {
|
||||
if (bedrockData.success) {
|
||||
const bedrockAccounts = (bedrockData.data || []).map((acc) => {
|
||||
// Bedrock账户暂时不支持直接绑定
|
||||
const groupInfo = accountGroupMap.value.get(acc.id) || null
|
||||
return { ...acc, platform: 'bedrock', boundApiKeysCount: 0, groupInfo }
|
||||
// 后端已经包含了groupInfos,直接使用
|
||||
return { ...acc, platform: 'bedrock', boundApiKeysCount: 0 }
|
||||
})
|
||||
allAccounts.push(...bedrockAccounts)
|
||||
}
|
||||
@@ -1023,8 +1159,8 @@ const loadAccounts = async (forceReload = false) => {
|
||||
const boundApiKeysCount = apiKeys.value.filter(
|
||||
(key) => key.geminiAccountId === acc.id
|
||||
).length
|
||||
const groupInfo = accountGroupMap.value.get(acc.id) || null
|
||||
return { ...acc, platform: 'gemini', boundApiKeysCount, groupInfo }
|
||||
// 后端已经包含了groupInfos,直接使用
|
||||
return { ...acc, platform: 'gemini', boundApiKeysCount }
|
||||
})
|
||||
allAccounts.push(...geminiAccounts)
|
||||
}
|
||||
@@ -1034,8 +1170,8 @@ const loadAccounts = async (forceReload = false) => {
|
||||
const boundApiKeysCount = apiKeys.value.filter(
|
||||
(key) => key.openaiAccountId === acc.id
|
||||
).length
|
||||
const groupInfo = accountGroupMap.value.get(acc.id) || null
|
||||
return { ...acc, platform: 'openai', boundApiKeysCount, groupInfo }
|
||||
// 后端已经包含了groupInfos,直接使用
|
||||
return { ...acc, platform: 'openai', boundApiKeysCount }
|
||||
})
|
||||
allAccounts.push(...openaiAccounts)
|
||||
}
|
||||
@@ -1076,9 +1212,11 @@ const formatNumber = (num) => {
|
||||
if (num === null || num === undefined) return '0'
|
||||
const number = Number(num)
|
||||
if (number >= 1000000) {
|
||||
return Math.floor(number / 1000000).toLocaleString() + 'M'
|
||||
return (number / 1000000).toFixed(2)
|
||||
} else if (number >= 1000) {
|
||||
return (number / 1000000).toFixed(4)
|
||||
}
|
||||
return number.toLocaleString()
|
||||
return (number / 1000000).toFixed(6)
|
||||
}
|
||||
|
||||
// 格式化最后使用时间
|
||||
@@ -1131,36 +1269,6 @@ const loadAccountGroups = async (forceReload = false) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载分组成员关系(缓存版本)
|
||||
const loadGroupMembers = async (forceReload = false) => {
|
||||
if (!forceReload && groupMembersLoaded.value) {
|
||||
return // 使用缓存数据
|
||||
}
|
||||
|
||||
try {
|
||||
// 重置映射
|
||||
accountGroupMap.value.clear()
|
||||
|
||||
// 获取所有分组的成员信息
|
||||
for (const group of accountGroups.value) {
|
||||
try {
|
||||
const membersResponse = await apiClient.get(`/admin/account-groups/${group.id}/members`)
|
||||
if (membersResponse.success) {
|
||||
const members = membersResponse.data || []
|
||||
members.forEach((member) => {
|
||||
accountGroupMap.value.set(member.id, group)
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load members for group ${group.id}:`, error)
|
||||
}
|
||||
}
|
||||
groupMembersLoaded.value = true
|
||||
} catch (error) {
|
||||
console.error('Failed to load group members:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 清空缓存的函数
|
||||
const clearCache = () => {
|
||||
apiKeysLoaded.value = false
|
||||
@@ -1452,6 +1560,55 @@ const getClaudeAccountType = (account) => {
|
||||
return 'Claude'
|
||||
}
|
||||
|
||||
// 获取停止调度的原因
|
||||
const getSchedulableReason = (account) => {
|
||||
if (account.schedulable !== false) return null
|
||||
|
||||
// Claude Console 账户的错误状态
|
||||
if (account.platform === 'claude-console') {
|
||||
if (account.status === 'unauthorized') {
|
||||
return 'API Key无效或已过期(401错误)'
|
||||
}
|
||||
if (account.overloadStatus === 'overloaded') {
|
||||
return '服务过载(529错误)'
|
||||
}
|
||||
if (account.rateLimitStatus === 'limited') {
|
||||
return '触发限流(429错误)'
|
||||
}
|
||||
if (account.status === 'blocked' && account.errorMessage) {
|
||||
return account.errorMessage
|
||||
}
|
||||
}
|
||||
|
||||
// Claude 官方账户的错误状态
|
||||
if (account.platform === 'claude') {
|
||||
if (account.status === 'unauthorized') {
|
||||
return '认证失败(401错误)'
|
||||
}
|
||||
if (account.status === 'error' && account.errorMessage) {
|
||||
return account.errorMessage
|
||||
}
|
||||
if (account.isRateLimited) {
|
||||
return '触发限流(429错误)'
|
||||
}
|
||||
// 自动停止调度的原因
|
||||
if (account.stoppedReason) {
|
||||
return account.stoppedReason
|
||||
}
|
||||
}
|
||||
|
||||
// 通用原因
|
||||
if (account.stoppedReason) {
|
||||
return account.stoppedReason
|
||||
}
|
||||
if (account.errorMessage) {
|
||||
return account.errorMessage
|
||||
}
|
||||
|
||||
// 默认为手动停止
|
||||
return '手动停止调度'
|
||||
}
|
||||
|
||||
// 获取账户状态文本
|
||||
const getAccountStatusText = (account) => {
|
||||
// 检查是否被封锁
|
||||
@@ -1537,6 +1694,54 @@ const formatRelativeTime = (dateString) => {
|
||||
return formatLastUsed(dateString)
|
||||
}
|
||||
|
||||
// 获取会话窗口进度条的样式类
|
||||
const getSessionProgressBarClass = (status) => {
|
||||
// 根据状态返回不同的颜色类,包含防御性检查
|
||||
if (!status) {
|
||||
// 无状态信息时默认为蓝色
|
||||
return 'bg-gradient-to-r from-blue-500 to-indigo-600'
|
||||
}
|
||||
|
||||
// 转换为小写进行比较,避免大小写问题
|
||||
const normalizedStatus = String(status).toLowerCase()
|
||||
|
||||
if (normalizedStatus === 'rejected') {
|
||||
// 被拒绝 - 红色
|
||||
return 'bg-gradient-to-r from-red-500 to-red-600'
|
||||
} else if (normalizedStatus === 'allowed_warning') {
|
||||
// 警告状态 - 橙色/黄色
|
||||
return 'bg-gradient-to-r from-yellow-500 to-orange-500'
|
||||
} else {
|
||||
// 正常状态(allowed 或其他) - 蓝色
|
||||
return 'bg-gradient-to-r from-blue-500 to-indigo-600'
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化费用显示
|
||||
const formatCost = (cost) => {
|
||||
if (!cost || cost === 0) return '0.0000'
|
||||
if (cost < 0.0001) return cost.toExponential(2)
|
||||
if (cost < 0.01) return cost.toFixed(6)
|
||||
if (cost < 1) return cost.toFixed(4)
|
||||
return cost.toFixed(2)
|
||||
}
|
||||
|
||||
// 计算每日费用(估算,基于平均模型价格)
|
||||
const calculateDailyCost = (account) => {
|
||||
if (!account.usage || !account.usage.daily) return '0.0000'
|
||||
|
||||
const dailyTokens = account.usage.daily.allTokens || 0
|
||||
if (dailyTokens === 0) return '0.0000'
|
||||
|
||||
// 使用平均价格估算(基于Claude 3.5 Sonnet的价格)
|
||||
// 输入: $3/1M tokens, 输出: $15/1M tokens
|
||||
// 假设平均比例为 输入:输出 = 3:1
|
||||
const avgPricePerMillion = 3 * 0.75 + 15 * 0.25 // 加权平均价格
|
||||
const cost = (dailyTokens / 1000000) * avgPricePerMillion
|
||||
|
||||
return formatCost(cost)
|
||||
}
|
||||
|
||||
// 切换调度状态
|
||||
// const toggleDispatch = async (account) => {
|
||||
// await toggleSchedulable(account)
|
||||
|
||||
@@ -466,7 +466,7 @@
|
||||
<!-- 每日费用限制进度条 -->
|
||||
<div v-if="key.dailyCostLimit > 0" class="space-y-1">
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-500 dark:text-gray-400">费用限额</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">每日费用</span>
|
||||
<span class="text-gray-700 dark:text-gray-300">
|
||||
${{ (key.dailyCost || 0).toFixed(2) }} / ${{
|
||||
key.dailyCostLimit.toFixed(2)
|
||||
@@ -482,9 +482,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Opus 周费用限制进度条 -->
|
||||
<div v-if="key.weeklyOpusCostLimit > 0" class="space-y-1">
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-500 dark:text-gray-400">Opus周费用</span>
|
||||
<span class="text-gray-700 dark:text-gray-300">
|
||||
${{ (key.weeklyOpusCost || 0).toFixed(2) }} / ${{
|
||||
key.weeklyOpusCostLimit.toFixed(2)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="h-1.5 w-full rounded-full bg-gray-200">
|
||||
<div
|
||||
class="h-1.5 rounded-full transition-all duration-300"
|
||||
:class="getWeeklyOpusCostProgressColor(key)"
|
||||
:style="{ width: getWeeklyOpusCostProgress(key) + '%' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间窗口限制进度条 -->
|
||||
<WindowCountdown
|
||||
v-if="key.rateLimitWindow > 0"
|
||||
:cost-limit="key.rateLimitCost"
|
||||
:current-cost="key.currentWindowCost"
|
||||
:current-requests="key.currentWindowRequests"
|
||||
:current-tokens="key.currentWindowTokens"
|
||||
:rate-limit-window="key.rateLimitWindow"
|
||||
@@ -1059,8 +1080,11 @@
|
||||
<!-- 移动端时间窗口限制 -->
|
||||
<WindowCountdown
|
||||
v-if="
|
||||
key.rateLimitWindow > 0 && (key.rateLimitRequests > 0 || key.tokenLimit > 0)
|
||||
key.rateLimitWindow > 0 &&
|
||||
(key.rateLimitRequests > 0 || key.tokenLimit > 0 || key.rateLimitCost > 0)
|
||||
"
|
||||
:cost-limit="key.rateLimitCost"
|
||||
:current-cost="key.currentWindowCost"
|
||||
:current-requests="key.currentWindowRequests"
|
||||
:current-tokens="key.currentWindowTokens"
|
||||
:rate-limit-window="key.rateLimitWindow"
|
||||
@@ -2480,6 +2504,21 @@ const getDailyCostProgressColor = (key) => {
|
||||
return 'bg-green-500'
|
||||
}
|
||||
|
||||
// 获取 Opus 周费用进度
|
||||
const getWeeklyOpusCostProgress = (key) => {
|
||||
if (!key.weeklyOpusCostLimit || key.weeklyOpusCostLimit === 0) return 0
|
||||
const percentage = ((key.weeklyOpusCost || 0) / key.weeklyOpusCostLimit) * 100
|
||||
return Math.min(percentage, 100)
|
||||
}
|
||||
|
||||
// 获取 Opus 周费用进度条颜色
|
||||
const getWeeklyOpusCostProgressColor = (key) => {
|
||||
const progress = getWeeklyOpusCostProgress(key)
|
||||
if (progress >= 100) return 'bg-red-500'
|
||||
if (progress >= 80) return 'bg-yellow-500'
|
||||
return 'bg-green-500'
|
||||
}
|
||||
|
||||
// 显示使用详情
|
||||
const showUsageDetails = (apiKey) => {
|
||||
selectedApiKeyForDetail.value = apiKey
|
||||
|
||||
Reference in New Issue
Block a user