mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
Merge pull request #760 from IanShaw027/upstream-pr-account-full [skip ci]
feat: 增强账户管理功能
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -26,6 +26,7 @@ redis_data/
|
|||||||
|
|
||||||
# Logs directory
|
# Logs directory
|
||||||
logs/
|
logs/
|
||||||
|
logs1/
|
||||||
*.log
|
*.log
|
||||||
startup.log
|
startup.log
|
||||||
app.log
|
app.log
|
||||||
|
|||||||
@@ -247,9 +247,11 @@ const handleResponses = async (req, res) => {
|
|||||||
|
|
||||||
// 从请求体中提取模型和流式标志
|
// 从请求体中提取模型和流式标志
|
||||||
let requestedModel = req.body?.model || null
|
let requestedModel = req.body?.model || null
|
||||||
|
const isCodexModel =
|
||||||
|
typeof requestedModel === 'string' && requestedModel.toLowerCase().includes('codex')
|
||||||
|
|
||||||
// 如果模型是 gpt-5 开头且后面还有内容(如 gpt-5-2025-08-07),则覆盖为 gpt-5
|
// 如果模型是 gpt-5 开头且后面还有内容(如 gpt-5-2025-08-07),并且不是 Codex 系列,则覆盖为 gpt-5
|
||||||
if (requestedModel && requestedModel.startsWith('gpt-5-') && requestedModel !== 'gpt-5-codex') {
|
if (requestedModel && requestedModel.startsWith('gpt-5-') && !isCodexModel) {
|
||||||
logger.info(`📝 Model ${requestedModel} detected, normalizing to gpt-5 for Codex API`)
|
logger.info(`📝 Model ${requestedModel} detected, normalizing to gpt-5 for Codex API`)
|
||||||
requestedModel = 'gpt-5'
|
requestedModel = 'gpt-5'
|
||||||
req.body.model = 'gpt-5' // 同时更新请求体中的模型
|
req.body.model = 'gpt-5' // 同时更新请求体中的模型
|
||||||
|
|||||||
@@ -41,19 +41,25 @@
|
|||||||
<div
|
<div
|
||||||
v-for="option in options"
|
v-for="option in options"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
class="flex cursor-pointer items-center gap-2 whitespace-nowrap px-3 py-2 text-sm transition-colors duration-150"
|
class="flex cursor-pointer items-center gap-2 whitespace-nowrap py-2 text-sm transition-colors duration-150"
|
||||||
:class="[
|
:class="[
|
||||||
option.value === modelValue
|
option.value === modelValue
|
||||||
? 'bg-blue-50 font-medium text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
? 'bg-blue-50 font-medium text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
||||||
: 'text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700'
|
: option.isGroup
|
||||||
|
? 'bg-gray-50 font-semibold text-gray-800 dark:bg-gray-700/50 dark:text-gray-200'
|
||||||
|
: 'text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700'
|
||||||
]"
|
]"
|
||||||
|
:style="{
|
||||||
|
paddingLeft: option.indent ? `${12 + option.indent * 16}px` : '12px',
|
||||||
|
paddingRight: '12px'
|
||||||
|
}"
|
||||||
@click="selectOption(option)"
|
@click="selectOption(option)"
|
||||||
>
|
>
|
||||||
<i v-if="option.icon" :class="['fas', option.icon, 'text-xs']"></i>
|
<i v-if="option.icon" :class="['fas', option.icon, 'text-xs']"></i>
|
||||||
<span>{{ option.label }}</span>
|
<span>{{ option.label }}</span>
|
||||||
<i
|
<i
|
||||||
v-if="option.value === modelValue"
|
v-if="option.value === modelValue"
|
||||||
class="fas fa-check ml-auto pl-3 text-xs text-blue-600"
|
class="fas fa-check ml-auto pl-3 text-xs text-blue-600 dark:text-blue-400"
|
||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,6 +58,20 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 状态筛选器 -->
|
||||||
|
<div class="group relative min-w-[120px]">
|
||||||
|
<div
|
||||||
|
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-green-500 to-emerald-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
|
||||||
|
></div>
|
||||||
|
<CustomDropdown
|
||||||
|
v-model="statusFilter"
|
||||||
|
icon="fa-check-circle"
|
||||||
|
icon-color="text-green-500"
|
||||||
|
:options="statusOptions"
|
||||||
|
placeholder="选择状态"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
<div class="group relative min-w-[200px]">
|
<div class="group relative min-w-[200px]">
|
||||||
<div
|
<div
|
||||||
@@ -83,6 +97,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex w-full flex-col gap-3 sm:w-auto sm:flex-row sm:items-center sm:gap-3">
|
<div class="flex w-full flex-col gap-3 sm:w-auto sm:flex-row sm:items-center sm:gap-3">
|
||||||
|
<!-- 账户统计按钮 -->
|
||||||
|
<div class="relative">
|
||||||
|
<el-tooltip content="查看账户统计汇总" effect="dark" placement="bottom">
|
||||||
|
<button
|
||||||
|
class="group relative flex items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-all duration-200 hover:border-gray-300 hover:shadow-md dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-gray-500 sm:w-auto"
|
||||||
|
@click="showAccountStatsModal = true"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-violet-500 to-purple-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
|
||||||
|
></div>
|
||||||
|
<i class="fas fa-chart-bar relative text-violet-500" />
|
||||||
|
<span class="relative">统计</span>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 刷新按钮 -->
|
<!-- 刷新按钮 -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
@@ -1849,6 +1879,146 @@
|
|||||||
:show="showAccountTestModal"
|
:show="showAccountTestModal"
|
||||||
@close="closeAccountTestModal"
|
@close="closeAccountTestModal"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 账户统计弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="showAccountStatsModal"
|
||||||
|
:style="{ maxWidth: '1200px' }"
|
||||||
|
title="账户统计汇总"
|
||||||
|
width="90%"
|
||||||
|
>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full border-collapse text-sm" style="min-width: 1000px">
|
||||||
|
<thead class="bg-gray-100 dark:bg-gray-700">
|
||||||
|
<tr>
|
||||||
|
<th class="border border-gray-300 px-4 py-2 text-left dark:border-gray-600">
|
||||||
|
平台类型
|
||||||
|
</th>
|
||||||
|
<th class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
正常
|
||||||
|
</th>
|
||||||
|
<th class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
不可调度
|
||||||
|
</th>
|
||||||
|
<th class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
限流0-1h
|
||||||
|
</th>
|
||||||
|
<th class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
限流1-5h
|
||||||
|
</th>
|
||||||
|
<th class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
限流5-12h
|
||||||
|
</th>
|
||||||
|
<th class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
限流12-24h
|
||||||
|
</th>
|
||||||
|
<th class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
限流>24h
|
||||||
|
</th>
|
||||||
|
<th class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
其他
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="border border-gray-300 bg-blue-50 px-4 py-2 text-center font-bold dark:border-gray-600 dark:bg-blue-900/30"
|
||||||
|
>
|
||||||
|
合计
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="stat in accountStats" :key="stat.platform">
|
||||||
|
<td class="border border-gray-300 px-4 py-2 font-medium dark:border-gray-600">
|
||||||
|
{{ stat.platformLabel }}
|
||||||
|
</td>
|
||||||
|
<td class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
<span class="text-green-600 dark:text-green-400">{{ stat.normal }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
<span class="text-yellow-600 dark:text-yellow-400">{{ stat.unschedulable }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
<span class="text-orange-600 dark:text-orange-400">{{ stat.rateLimit0_1h }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
<span class="text-orange-600 dark:text-orange-400">{{ stat.rateLimit1_5h }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
<span class="text-orange-600 dark:text-orange-400">{{
|
||||||
|
stat.rateLimit5_12h
|
||||||
|
}}</span>
|
||||||
|
</td>
|
||||||
|
<td class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
<span class="text-orange-600 dark:text-orange-400">{{
|
||||||
|
stat.rateLimit12_24h
|
||||||
|
}}</span>
|
||||||
|
</td>
|
||||||
|
<td class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
<span class="text-orange-600 dark:text-orange-400">{{
|
||||||
|
stat.rateLimitOver24h
|
||||||
|
}}</span>
|
||||||
|
</td>
|
||||||
|
<td class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
<span class="text-red-600 dark:text-red-400">{{ stat.other }}</span>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="border border-gray-300 bg-blue-50 px-4 py-2 text-center font-bold dark:border-gray-600 dark:bg-blue-900/30"
|
||||||
|
>
|
||||||
|
{{ stat.total }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="bg-blue-50 font-bold dark:bg-blue-900/30">
|
||||||
|
<td class="border border-gray-300 px-4 py-2 dark:border-gray-600">合计</td>
|
||||||
|
<td class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
<span class="text-green-600 dark:text-green-400">{{
|
||||||
|
accountStatsTotal.normal
|
||||||
|
}}</span>
|
||||||
|
</td>
|
||||||
|
<td class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
<span class="text-yellow-600 dark:text-yellow-400">{{
|
||||||
|
accountStatsTotal.unschedulable
|
||||||
|
}}</span>
|
||||||
|
</td>
|
||||||
|
<td class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
<span class="text-orange-600 dark:text-orange-400">{{
|
||||||
|
accountStatsTotal.rateLimit0_1h
|
||||||
|
}}</span>
|
||||||
|
</td>
|
||||||
|
<td class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
<span class="text-orange-600 dark:text-orange-400">{{
|
||||||
|
accountStatsTotal.rateLimit1_5h
|
||||||
|
}}</span>
|
||||||
|
</td>
|
||||||
|
<td class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
<span class="text-orange-600 dark:text-orange-400">{{
|
||||||
|
accountStatsTotal.rateLimit5_12h
|
||||||
|
}}</span>
|
||||||
|
</td>
|
||||||
|
<td class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
<span class="text-orange-600 dark:text-orange-400">{{
|
||||||
|
accountStatsTotal.rateLimit12_24h
|
||||||
|
}}</span>
|
||||||
|
</td>
|
||||||
|
<td class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
<span class="text-orange-600 dark:text-orange-400">{{
|
||||||
|
accountStatsTotal.rateLimitOver24h
|
||||||
|
}}</span>
|
||||||
|
</td>
|
||||||
|
<td class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
<span class="text-red-600 dark:text-red-400">{{ accountStatsTotal.other }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="border border-gray-300 px-4 py-2 text-center dark:border-gray-600">
|
||||||
|
{{ accountStatsTotal.total }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
注:限流时间列表示剩余限流时间在指定范围内的账户数量
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -1879,6 +2049,7 @@ const bindingCounts = ref({}) // 轻量级绑定计数,用于显示"绑定: X
|
|||||||
const accountGroups = ref([])
|
const accountGroups = ref([])
|
||||||
const groupFilter = ref('all')
|
const groupFilter = ref('all')
|
||||||
const platformFilter = ref('all')
|
const platformFilter = ref('all')
|
||||||
|
const statusFilter = ref('normal') // 状态过滤 (normal/rateLimited/other/all)
|
||||||
const searchKeyword = ref('')
|
const searchKeyword = ref('')
|
||||||
const PAGE_SIZE_STORAGE_KEY = 'accountsPageSize'
|
const PAGE_SIZE_STORAGE_KEY = 'accountsPageSize'
|
||||||
const getInitialPageSize = () => {
|
const getInitialPageSize = () => {
|
||||||
@@ -1928,6 +2099,9 @@ const expiryEditModalRef = ref(null)
|
|||||||
const showAccountTestModal = ref(false)
|
const showAccountTestModal = ref(false)
|
||||||
const testingAccount = ref(null)
|
const testingAccount = ref(null)
|
||||||
|
|
||||||
|
// 账户统计弹窗状态
|
||||||
|
const showAccountStatsModal = ref(false)
|
||||||
|
|
||||||
// 表格横向滚动检测
|
// 表格横向滚动检测
|
||||||
const tableContainerRef = ref(null)
|
const tableContainerRef = ref(null)
|
||||||
const needsHorizontalScroll = ref(false)
|
const needsHorizontalScroll = ref(false)
|
||||||
@@ -1945,21 +2119,102 @@ const sortOptions = ref([
|
|||||||
{ value: 'dailyTokens', label: '按今日Token排序', icon: 'fa-coins' },
|
{ value: 'dailyTokens', label: '按今日Token排序', icon: 'fa-coins' },
|
||||||
{ value: 'dailyRequests', label: '按今日请求数排序', icon: 'fa-chart-line' },
|
{ value: 'dailyRequests', label: '按今日请求数排序', icon: 'fa-chart-line' },
|
||||||
{ value: 'totalTokens', label: '按总Token排序', icon: 'fa-database' },
|
{ value: 'totalTokens', label: '按总Token排序', icon: 'fa-database' },
|
||||||
{ value: 'lastUsed', label: '按最后使用排序', icon: 'fa-clock' }
|
{ value: 'lastUsed', label: '按最后使用排序', icon: 'fa-clock' },
|
||||||
|
{ value: 'rateLimitTime', label: '按限流时间排序', icon: 'fa-hourglass' }
|
||||||
])
|
])
|
||||||
|
|
||||||
const platformOptions = ref([
|
// 平台层级结构定义
|
||||||
{ value: 'all', label: '所有平台', icon: 'fa-globe' },
|
const platformHierarchy = [
|
||||||
{ value: 'claude', label: 'Claude', icon: 'fa-brain' },
|
{
|
||||||
{ value: 'claude-console', label: 'Claude Console', icon: 'fa-terminal' },
|
value: 'group-claude',
|
||||||
{ value: 'gemini', label: 'Gemini', icon: 'fab fa-google' },
|
label: 'Claude(全部)',
|
||||||
{ value: 'gemini-api', label: 'Gemini API', icon: 'fa-key' },
|
icon: 'fa-brain',
|
||||||
{ value: 'openai', label: 'OpenAi', icon: 'fa-openai' },
|
children: [
|
||||||
{ value: 'azure_openai', label: 'Azure OpenAI', icon: 'fab fa-microsoft' },
|
{ value: 'claude', label: 'Claude 官方/OAuth', icon: 'fa-brain' },
|
||||||
{ value: 'bedrock', label: 'Bedrock', icon: 'fab fa-aws' },
|
{ value: 'claude-console', label: 'Claude Console', icon: 'fa-terminal' },
|
||||||
{ value: 'openai-responses', label: 'OpenAI-Responses', icon: 'fa-server' },
|
{ value: 'bedrock', label: 'Bedrock', icon: 'fab fa-aws' },
|
||||||
{ value: 'ccr', label: 'CCR', icon: 'fa-code-branch' },
|
{ value: 'ccr', label: 'CCR Relay', icon: 'fa-code-branch' }
|
||||||
{ value: 'droid', label: 'Droid', icon: 'fa-robot' }
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'group-openai',
|
||||||
|
label: 'Codex / OpenAI(全部)',
|
||||||
|
icon: 'fa-openai',
|
||||||
|
children: [
|
||||||
|
{ value: 'openai', label: 'OpenAI 官方', icon: 'fa-openai' },
|
||||||
|
{ value: 'openai-responses', label: 'OpenAI-Responses (Codex)', icon: 'fa-server' },
|
||||||
|
{ value: 'azure_openai', label: 'Azure OpenAI', icon: 'fab fa-microsoft' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'group-gemini',
|
||||||
|
label: 'Gemini(全部)',
|
||||||
|
icon: 'fab fa-google',
|
||||||
|
children: [
|
||||||
|
{ value: 'gemini', label: 'Gemini OAuth', icon: 'fab fa-google' },
|
||||||
|
{ value: 'gemini-api', label: 'Gemini API', icon: 'fa-key' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'group-droid',
|
||||||
|
label: 'Droid(全部)',
|
||||||
|
icon: 'fa-robot',
|
||||||
|
children: [{ value: 'droid', label: 'Droid', icon: 'fa-robot' }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 平台分组映射
|
||||||
|
const platformGroupMap = {
|
||||||
|
'group-claude': ['claude', 'claude-console', 'bedrock', 'ccr'],
|
||||||
|
'group-openai': ['openai', 'openai-responses', 'azure_openai'],
|
||||||
|
'group-gemini': ['gemini', 'gemini-api'],
|
||||||
|
'group-droid': ['droid']
|
||||||
|
}
|
||||||
|
|
||||||
|
// 平台请求处理器
|
||||||
|
const platformRequestHandlers = {
|
||||||
|
claude: (params) => apiClient.get('/admin/claude-accounts', { params }),
|
||||||
|
'claude-console': (params) => apiClient.get('/admin/claude-console-accounts', { params }),
|
||||||
|
bedrock: (params) => apiClient.get('/admin/bedrock-accounts', { params }),
|
||||||
|
gemini: (params) => apiClient.get('/admin/gemini-accounts', { params }),
|
||||||
|
openai: (params) => apiClient.get('/admin/openai-accounts', { params }),
|
||||||
|
azure_openai: (params) => apiClient.get('/admin/azure-openai-accounts', { params }),
|
||||||
|
'openai-responses': (params) => apiClient.get('/admin/openai-responses-accounts', { params }),
|
||||||
|
ccr: (params) => apiClient.get('/admin/ccr-accounts', { params }),
|
||||||
|
droid: (params) => apiClient.get('/admin/droid-accounts', { params }),
|
||||||
|
'gemini-api': (params) => apiClient.get('/admin/gemini-api-accounts', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
const allPlatformKeys = Object.keys(platformRequestHandlers)
|
||||||
|
|
||||||
|
// 根据过滤器获取需要加载的平台列表
|
||||||
|
const getPlatformsForFilter = (filter) => {
|
||||||
|
if (filter === 'all') return allPlatformKeys
|
||||||
|
if (platformGroupMap[filter]) return platformGroupMap[filter]
|
||||||
|
if (allPlatformKeys.includes(filter)) return [filter]
|
||||||
|
return allPlatformKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
// 平台选项(两级结构)
|
||||||
|
const platformOptions = computed(() => {
|
||||||
|
const options = [{ value: 'all', label: '所有平台', icon: 'fa-globe', indent: 0 }]
|
||||||
|
|
||||||
|
platformHierarchy.forEach((group) => {
|
||||||
|
options.push({ ...group, indent: 0, isGroup: true })
|
||||||
|
group.children?.forEach((child) => {
|
||||||
|
options.push({ ...child, indent: 1, parent: group.value })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return options
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusOptions = ref([
|
||||||
|
{ value: 'normal', label: '正常', icon: 'fa-check-circle' },
|
||||||
|
{ value: 'unschedulable', label: '不可调度', icon: 'fa-ban' },
|
||||||
|
{ value: 'rateLimited', label: '限流', icon: 'fa-hourglass-half' },
|
||||||
|
{ value: 'other', label: '其他', icon: 'fa-exclamation-triangle' },
|
||||||
|
{ value: 'all', label: '全部状态', icon: 'fa-list' }
|
||||||
])
|
])
|
||||||
|
|
||||||
const groupOptions = computed(() => {
|
const groupOptions = computed(() => {
|
||||||
@@ -2198,6 +2453,33 @@ const sortedAccounts = computed(() => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 状态过滤 (normal/unschedulable/rateLimited/other/all)
|
||||||
|
// 限流: isActive && rate-limited (最高优先级)
|
||||||
|
// 正常: isActive && !rate-limited && !blocked && schedulable
|
||||||
|
// 不可调度: isActive && !rate-limited && !blocked && schedulable === false
|
||||||
|
// 其他: 非限流的(未激活 || 被阻止)
|
||||||
|
if (statusFilter.value !== 'all') {
|
||||||
|
sourceAccounts = sourceAccounts.filter((account) => {
|
||||||
|
const isRateLimited = isAccountRateLimited(account)
|
||||||
|
const isBlocked = account.status === 'blocked' || account.status === 'unauthorized'
|
||||||
|
|
||||||
|
if (statusFilter.value === 'rateLimited') {
|
||||||
|
// 限流: 激活且限流中(优先判断)
|
||||||
|
return account.isActive && isRateLimited
|
||||||
|
} else if (statusFilter.value === 'normal') {
|
||||||
|
// 正常: 激活且非限流且非阻止且可调度
|
||||||
|
return account.isActive && !isRateLimited && !isBlocked && account.schedulable !== false
|
||||||
|
} else if (statusFilter.value === 'unschedulable') {
|
||||||
|
// 不可调度: 激活且非限流且非阻止但不可调度
|
||||||
|
return account.isActive && !isRateLimited && !isBlocked && account.schedulable === false
|
||||||
|
} else if (statusFilter.value === 'other') {
|
||||||
|
// 其他: 非限流的异常账户(未激活或被阻止)
|
||||||
|
return !isRateLimited && (!account.isActive || isBlocked)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (!accountsSortBy.value) return sourceAccounts
|
if (!accountsSortBy.value) return sourceAccounts
|
||||||
|
|
||||||
const sorted = [...sourceAccounts].sort((a, b) => {
|
const sorted = [...sourceAccounts].sort((a, b) => {
|
||||||
@@ -2228,6 +2510,23 @@ const sortedAccounts = computed(() => {
|
|||||||
bVal = b.isActive ? 1 : 0
|
bVal = b.isActive ? 1 : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理限流时间排序: 未限流优先,然后按剩余时间从小到大
|
||||||
|
if (accountsSortBy.value === 'rateLimitTime') {
|
||||||
|
const aIsRateLimited = isAccountRateLimited(a)
|
||||||
|
const bIsRateLimited = isAccountRateLimited(b)
|
||||||
|
const aMinutes = aIsRateLimited ? getRateLimitRemainingMinutes(a) : 0
|
||||||
|
const bMinutes = bIsRateLimited ? getRateLimitRemainingMinutes(b) : 0
|
||||||
|
|
||||||
|
// 未限流的排在前面
|
||||||
|
if (!aIsRateLimited && bIsRateLimited) return -1
|
||||||
|
if (aIsRateLimited && !bIsRateLimited) return 1
|
||||||
|
|
||||||
|
// 都未限流或都限流时,按剩余时间升序
|
||||||
|
if (aMinutes < bMinutes) return -1
|
||||||
|
if (aMinutes > bMinutes) return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
if (aVal < bVal) return accountsSortOrder.value === 'asc' ? -1 : 1
|
if (aVal < bVal) return accountsSortOrder.value === 'asc' ? -1 : 1
|
||||||
if (aVal > bVal) return accountsSortOrder.value === 'asc' ? 1 : -1
|
if (aVal > bVal) return accountsSortOrder.value === 'asc' ? 1 : -1
|
||||||
return 0
|
return 0
|
||||||
@@ -2241,6 +2540,120 @@ const totalPages = computed(() => {
|
|||||||
return Math.ceil(total / pageSize.value) || 0
|
return Math.ceil(total / pageSize.value) || 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 账户统计数据(按平台和状态分类)
|
||||||
|
const accountStats = computed(() => {
|
||||||
|
const platforms = [
|
||||||
|
{ value: 'claude', label: 'Claude' },
|
||||||
|
{ value: 'claude-console', label: 'Claude Console' },
|
||||||
|
{ value: 'gemini', label: 'Gemini' },
|
||||||
|
{ value: 'gemini-api', label: 'Gemini API' },
|
||||||
|
{ value: 'openai', label: 'OpenAI' },
|
||||||
|
{ value: 'azure_openai', label: 'Azure OpenAI' },
|
||||||
|
{ value: 'bedrock', label: 'Bedrock' },
|
||||||
|
{ value: 'openai-responses', label: 'OpenAI-Responses' },
|
||||||
|
{ value: 'ccr', label: 'CCR' },
|
||||||
|
{ value: 'droid', label: 'Droid' }
|
||||||
|
]
|
||||||
|
|
||||||
|
return platforms
|
||||||
|
.map((p) => {
|
||||||
|
const platformAccounts = accounts.value.filter((acc) => acc.platform === p.value)
|
||||||
|
|
||||||
|
// 先筛选限流账户(优先级最高)
|
||||||
|
const rateLimitedAccounts = platformAccounts.filter((acc) => isAccountRateLimited(acc))
|
||||||
|
|
||||||
|
// 正常: 非限流 && 激活 && 非阻止 && 可调度
|
||||||
|
const normal = platformAccounts.filter((acc) => {
|
||||||
|
const isRateLimited = isAccountRateLimited(acc)
|
||||||
|
const isBlocked = acc.status === 'blocked' || acc.status === 'unauthorized'
|
||||||
|
return !isRateLimited && acc.isActive && !isBlocked && acc.schedulable !== false
|
||||||
|
}).length
|
||||||
|
|
||||||
|
// 不可调度: 非限流 && 激活 && 非阻止 && 不可调度
|
||||||
|
const unschedulable = platformAccounts.filter((acc) => {
|
||||||
|
const isRateLimited = isAccountRateLimited(acc)
|
||||||
|
const isBlocked = acc.status === 'blocked' || acc.status === 'unauthorized'
|
||||||
|
return !isRateLimited && acc.isActive && !isBlocked && acc.schedulable === false
|
||||||
|
}).length
|
||||||
|
|
||||||
|
// 其他: 非限流的异常账户(未激活或被阻止)
|
||||||
|
const other = platformAccounts.filter((acc) => {
|
||||||
|
const isRateLimited = isAccountRateLimited(acc)
|
||||||
|
const isBlocked = acc.status === 'blocked' || acc.status === 'unauthorized'
|
||||||
|
return !isRateLimited && (!acc.isActive || isBlocked)
|
||||||
|
}).length
|
||||||
|
|
||||||
|
const rateLimit0_1h = rateLimitedAccounts.filter((acc) => {
|
||||||
|
const minutes = getRateLimitRemainingMinutes(acc)
|
||||||
|
return minutes > 0 && minutes <= 60
|
||||||
|
}).length
|
||||||
|
|
||||||
|
const rateLimit1_5h = rateLimitedAccounts.filter((acc) => {
|
||||||
|
const minutes = getRateLimitRemainingMinutes(acc)
|
||||||
|
return minutes > 60 && minutes <= 300
|
||||||
|
}).length
|
||||||
|
|
||||||
|
const rateLimit5_12h = rateLimitedAccounts.filter((acc) => {
|
||||||
|
const minutes = getRateLimitRemainingMinutes(acc)
|
||||||
|
return minutes > 300 && minutes <= 720
|
||||||
|
}).length
|
||||||
|
|
||||||
|
const rateLimit12_24h = rateLimitedAccounts.filter((acc) => {
|
||||||
|
const minutes = getRateLimitRemainingMinutes(acc)
|
||||||
|
return minutes > 720 && minutes <= 1440
|
||||||
|
}).length
|
||||||
|
|
||||||
|
const rateLimitOver24h = rateLimitedAccounts.filter((acc) => {
|
||||||
|
const minutes = getRateLimitRemainingMinutes(acc)
|
||||||
|
return minutes > 1440
|
||||||
|
}).length
|
||||||
|
|
||||||
|
return {
|
||||||
|
platform: p.value,
|
||||||
|
platformLabel: p.label,
|
||||||
|
normal,
|
||||||
|
unschedulable,
|
||||||
|
rateLimit0_1h,
|
||||||
|
rateLimit1_5h,
|
||||||
|
rateLimit5_12h,
|
||||||
|
rateLimit12_24h,
|
||||||
|
rateLimitOver24h,
|
||||||
|
other,
|
||||||
|
total: platformAccounts.length
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((stat) => stat.total > 0) // 只显示有账户的平台
|
||||||
|
})
|
||||||
|
|
||||||
|
// 账户统计合计
|
||||||
|
const accountStatsTotal = computed(() => {
|
||||||
|
return accountStats.value.reduce(
|
||||||
|
(total, stat) => {
|
||||||
|
total.normal += stat.normal
|
||||||
|
total.unschedulable += stat.unschedulable
|
||||||
|
total.rateLimit0_1h += stat.rateLimit0_1h
|
||||||
|
total.rateLimit1_5h += stat.rateLimit1_5h
|
||||||
|
total.rateLimit5_12h += stat.rateLimit5_12h
|
||||||
|
total.rateLimit12_24h += stat.rateLimit12_24h
|
||||||
|
total.rateLimitOver24h += stat.rateLimitOver24h
|
||||||
|
total.other += stat.other
|
||||||
|
total.total += stat.total
|
||||||
|
return total
|
||||||
|
},
|
||||||
|
{
|
||||||
|
normal: 0,
|
||||||
|
unschedulable: 0,
|
||||||
|
rateLimit0_1h: 0,
|
||||||
|
rateLimit1_5h: 0,
|
||||||
|
rateLimit5_12h: 0,
|
||||||
|
rateLimit12_24h: 0,
|
||||||
|
rateLimitOver24h: 0,
|
||||||
|
other: 0,
|
||||||
|
total: 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const pageNumbers = computed(() => {
|
const pageNumbers = computed(() => {
|
||||||
const total = totalPages.value
|
const total = totalPages.value
|
||||||
const current = currentPage.value
|
const current = currentPage.value
|
||||||
@@ -2354,190 +2767,14 @@ const loadAccounts = async (forceReload = false) => {
|
|||||||
try {
|
try {
|
||||||
// 构建查询参数(用于其他筛选情况)
|
// 构建查询参数(用于其他筛选情况)
|
||||||
const params = {}
|
const params = {}
|
||||||
if (platformFilter.value !== 'all') {
|
if (platformFilter.value !== 'all' && !platformGroupMap[platformFilter.value]) {
|
||||||
params.platform = platformFilter.value
|
params.platform = platformFilter.value
|
||||||
}
|
}
|
||||||
if (groupFilter.value !== 'all') {
|
if (groupFilter.value !== 'all') {
|
||||||
params.groupId = groupFilter.value
|
params.groupId = groupFilter.value
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据平台筛选决定需要请求哪些接口
|
const platformsToFetch = getPlatformsForFilter(platformFilter.value)
|
||||||
const requests = []
|
|
||||||
|
|
||||||
if (platformFilter.value === 'all') {
|
|
||||||
// 请求所有平台
|
|
||||||
requests.push(
|
|
||||||
apiClient.get('/admin/claude-accounts', { params }),
|
|
||||||
apiClient.get('/admin/claude-console-accounts', { params }),
|
|
||||||
apiClient.get('/admin/bedrock-accounts', { params }),
|
|
||||||
apiClient.get('/admin/gemini-accounts', { params }),
|
|
||||||
apiClient.get('/admin/openai-accounts', { params }),
|
|
||||||
apiClient.get('/admin/azure-openai-accounts', { params }),
|
|
||||||
apiClient.get('/admin/openai-responses-accounts', { params }),
|
|
||||||
apiClient.get('/admin/ccr-accounts', { params }),
|
|
||||||
apiClient.get('/admin/droid-accounts', { params }),
|
|
||||||
apiClient.get('/admin/gemini-api-accounts', { params })
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// 只请求指定平台,其他平台设为null占位
|
|
||||||
switch (platformFilter.value) {
|
|
||||||
case 'claude':
|
|
||||||
requests.push(
|
|
||||||
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: [] }), // openai 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // azure-openai 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // openai-responses 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // ccr 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // droid 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }) // gemini-api 占位
|
|
||||||
)
|
|
||||||
break
|
|
||||||
case 'claude-console':
|
|
||||||
requests.push(
|
|
||||||
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: [] }), // openai 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // azure-openai 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // openai-responses 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // ccr 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // droid 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }) // gemini-api 占位
|
|
||||||
)
|
|
||||||
break
|
|
||||||
case 'bedrock':
|
|
||||||
requests.push(
|
|
||||||
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: [] }), // openai 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // azure-openai 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // openai-responses 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // ccr 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // droid 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }) // gemini-api 占位
|
|
||||||
)
|
|
||||||
break
|
|
||||||
case 'gemini':
|
|
||||||
requests.push(
|
|
||||||
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 }),
|
|
||||||
Promise.resolve({ success: true, data: [] }), // openai 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // azure-openai 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // openai-responses 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // ccr 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // droid 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }) // gemini-api 占位
|
|
||||||
)
|
|
||||||
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 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // openai-responses 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // ccr 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // droid 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }) // gemini-api 占位
|
|
||||||
)
|
|
||||||
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 }),
|
|
||||||
Promise.resolve({ success: true, data: [] }), // openai-responses 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // ccr 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // droid 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }) // gemini-api 占位
|
|
||||||
)
|
|
||||||
break
|
|
||||||
case 'openai-responses':
|
|
||||||
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 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // azure-openai 占位
|
|
||||||
apiClient.get('/admin/openai-responses-accounts', { params }),
|
|
||||||
Promise.resolve({ success: true, data: [] }), // ccr 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // droid 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }) // gemini-api 占位
|
|
||||||
)
|
|
||||||
break
|
|
||||||
case 'ccr':
|
|
||||||
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 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // azure 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // openai-responses 占位
|
|
||||||
apiClient.get('/admin/ccr-accounts', { params }),
|
|
||||||
Promise.resolve({ success: true, data: [] }), // droid 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }) // gemini-api 占位
|
|
||||||
)
|
|
||||||
break
|
|
||||||
case 'droid':
|
|
||||||
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 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // azure 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // openai-responses 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // ccr 占位
|
|
||||||
apiClient.get('/admin/droid-accounts', { params }),
|
|
||||||
Promise.resolve({ success: true, data: [] }) // gemini-api 占位
|
|
||||||
)
|
|
||||||
break
|
|
||||||
case 'gemini-api':
|
|
||||||
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 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // azure-openai 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // openai-responses 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // ccr 占位
|
|
||||||
Promise.resolve({ success: true, data: [] }), // droid 占位
|
|
||||||
apiClient.get('/admin/gemini-api-accounts', { params })
|
|
||||||
)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
// 默认情况下返回空数组
|
|
||||||
requests.push(
|
|
||||||
Promise.resolve({ success: true, data: [] }),
|
|
||||||
Promise.resolve({ success: true, data: [] }),
|
|
||||||
Promise.resolve({ success: true, data: [] }),
|
|
||||||
Promise.resolve({ success: true, data: [] }),
|
|
||||||
Promise.resolve({ success: true, data: [] }),
|
|
||||||
Promise.resolve({ success: true, data: [] }),
|
|
||||||
Promise.resolve({ success: true, data: [] }),
|
|
||||||
Promise.resolve({ success: true, data: [] }),
|
|
||||||
Promise.resolve({ success: true, data: [] }),
|
|
||||||
Promise.resolve({ success: true, data: [] })
|
|
||||||
)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用缓存机制加载绑定计数和分组数据(不再加载完整的 API Keys 数据)
|
// 使用缓存机制加载绑定计数和分组数据(不再加载完整的 API Keys 数据)
|
||||||
await Promise.all([loadBindingCounts(forceReload), loadAccountGroups(forceReload)])
|
await Promise.all([loadBindingCounts(forceReload), loadAccountGroups(forceReload)])
|
||||||
@@ -2545,125 +2782,137 @@ const loadAccounts = async (forceReload = false) => {
|
|||||||
// 后端账户API已经包含分组信息,不需要单独加载分组成员关系
|
// 后端账户API已经包含分组信息,不需要单独加载分组成员关系
|
||||||
// await loadGroupMembers(forceReload)
|
// await loadGroupMembers(forceReload)
|
||||||
|
|
||||||
const [
|
const platformResults = await Promise.all(
|
||||||
claudeData,
|
platformsToFetch.map(async (platform) => {
|
||||||
claudeConsoleData,
|
const handler = platformRequestHandlers[platform]
|
||||||
bedrockData,
|
if (!handler) {
|
||||||
geminiData,
|
return { platform, success: true, data: [] }
|
||||||
openaiData,
|
}
|
||||||
azureOpenaiData,
|
|
||||||
openaiResponsesData,
|
|
||||||
ccrData,
|
|
||||||
droidData,
|
|
||||||
geminiApiData
|
|
||||||
] = await Promise.all(requests)
|
|
||||||
|
|
||||||
const allAccounts = []
|
try {
|
||||||
|
const res = await handler(params)
|
||||||
// 获取绑定计数数据
|
return { platform, success: res?.success, data: res?.data }
|
||||||
const counts = bindingCounts.value
|
} catch (error) {
|
||||||
|
console.debug(`Failed to load ${platform} accounts:`, error)
|
||||||
if (claudeData.success) {
|
return { platform, success: false, data: [] }
|
||||||
const claudeAccounts = (claudeData.data || []).map((acc) => {
|
|
||||||
// 从绑定计数缓存获取数量
|
|
||||||
const boundApiKeysCount = counts.claudeAccountId?.[acc.id] || 0
|
|
||||||
// 后端已经包含了groupInfos,直接使用
|
|
||||||
return { ...acc, platform: 'claude', boundApiKeysCount }
|
|
||||||
})
|
|
||||||
allAccounts.push(...claudeAccounts)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (claudeConsoleData.success) {
|
|
||||||
const claudeConsoleAccounts = (claudeConsoleData.data || []).map((acc) => {
|
|
||||||
// 从绑定计数缓存获取数量
|
|
||||||
const boundApiKeysCount = counts.claudeConsoleAccountId?.[acc.id] || 0
|
|
||||||
// 后端已经包含了groupInfos,直接使用
|
|
||||||
return { ...acc, platform: 'claude-console', boundApiKeysCount }
|
|
||||||
})
|
|
||||||
allAccounts.push(...claudeConsoleAccounts)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bedrockData.success) {
|
|
||||||
const bedrockAccounts = (bedrockData.data || []).map((acc) => {
|
|
||||||
// Bedrock账户暂时不支持直接绑定
|
|
||||||
// 后端已经包含了groupInfos,直接使用
|
|
||||||
return { ...acc, platform: 'bedrock', boundApiKeysCount: 0 }
|
|
||||||
})
|
|
||||||
allAccounts.push(...bedrockAccounts)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (geminiData.success) {
|
|
||||||
const geminiAccounts = (geminiData.data || []).map((acc) => {
|
|
||||||
// 从绑定计数缓存获取数量
|
|
||||||
const boundApiKeysCount = counts.geminiAccountId?.[acc.id] || 0
|
|
||||||
// 后端已经包含了groupInfos,直接使用
|
|
||||||
return { ...acc, platform: 'gemini', boundApiKeysCount }
|
|
||||||
})
|
|
||||||
allAccounts.push(...geminiAccounts)
|
|
||||||
}
|
|
||||||
if (openaiData.success) {
|
|
||||||
const openaiAccounts = (openaiData.data || []).map((acc) => {
|
|
||||||
// 从绑定计数缓存获取数量
|
|
||||||
const boundApiKeysCount = counts.openaiAccountId?.[acc.id] || 0
|
|
||||||
// 后端已经包含了groupInfos,直接使用
|
|
||||||
return { ...acc, platform: 'openai', boundApiKeysCount }
|
|
||||||
})
|
|
||||||
allAccounts.push(...openaiAccounts)
|
|
||||||
}
|
|
||||||
if (azureOpenaiData && azureOpenaiData.success) {
|
|
||||||
const azureOpenaiAccounts = (azureOpenaiData.data || []).map((acc) => {
|
|
||||||
// 从绑定计数缓存获取数量
|
|
||||||
const boundApiKeysCount = counts.azureOpenaiAccountId?.[acc.id] || 0
|
|
||||||
// 后端已经包含了groupInfos,直接使用
|
|
||||||
return { ...acc, platform: 'azure_openai', boundApiKeysCount }
|
|
||||||
})
|
|
||||||
allAccounts.push(...azureOpenaiAccounts)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (openaiResponsesData && openaiResponsesData.success) {
|
|
||||||
const openaiResponsesAccounts = (openaiResponsesData.data || []).map((acc) => {
|
|
||||||
// 从绑定计数缓存获取数量
|
|
||||||
// OpenAI-Responses账户使用 responses: 前缀
|
|
||||||
const boundApiKeysCount = counts.openaiAccountId?.[`responses:${acc.id}`] || 0
|
|
||||||
// 后端已经包含了groupInfos,直接使用
|
|
||||||
return { ...acc, platform: 'openai-responses', boundApiKeysCount }
|
|
||||||
})
|
|
||||||
allAccounts.push(...openaiResponsesAccounts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CCR 账户
|
|
||||||
if (ccrData && ccrData.success) {
|
|
||||||
const ccrAccounts = (ccrData.data || []).map((acc) => {
|
|
||||||
// CCR 不支持 API Key 绑定,固定为 0
|
|
||||||
return { ...acc, platform: 'ccr', boundApiKeysCount: 0 }
|
|
||||||
})
|
|
||||||
allAccounts.push(...ccrAccounts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Droid 账户
|
|
||||||
if (droidData && droidData.success) {
|
|
||||||
const droidAccounts = (droidData.data || []).map((acc) => {
|
|
||||||
// 从绑定计数缓存获取数量
|
|
||||||
const boundApiKeysCount = counts.droidAccountId?.[acc.id] || acc.boundApiKeysCount || 0
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
platform: 'droid',
|
|
||||||
boundApiKeysCount
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
allAccounts.push(...droidAccounts)
|
)
|
||||||
|
|
||||||
|
const allAccounts = []
|
||||||
|
const counts = bindingCounts.value || {}
|
||||||
|
let openaiResponsesRaw = []
|
||||||
|
|
||||||
|
const appendAccounts = (platform, data) => {
|
||||||
|
if (!data || data.length === 0) return
|
||||||
|
|
||||||
|
switch (platform) {
|
||||||
|
case 'claude': {
|
||||||
|
const items = data.map((acc) => {
|
||||||
|
const boundApiKeysCount = counts.claudeAccountId?.[acc.id] || 0
|
||||||
|
return { ...acc, platform: 'claude', boundApiKeysCount }
|
||||||
|
})
|
||||||
|
allAccounts.push(...items)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'claude-console': {
|
||||||
|
const items = data.map((acc) => {
|
||||||
|
const boundApiKeysCount = counts.claudeConsoleAccountId?.[acc.id] || 0
|
||||||
|
return { ...acc, platform: 'claude-console', boundApiKeysCount }
|
||||||
|
})
|
||||||
|
allAccounts.push(...items)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'bedrock': {
|
||||||
|
const items = data.map((acc) => ({ ...acc, platform: 'bedrock', boundApiKeysCount: 0 }))
|
||||||
|
allAccounts.push(...items)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'gemini': {
|
||||||
|
const items = data.map((acc) => {
|
||||||
|
const boundApiKeysCount = counts.geminiAccountId?.[acc.id] || 0
|
||||||
|
return { ...acc, platform: 'gemini', boundApiKeysCount }
|
||||||
|
})
|
||||||
|
allAccounts.push(...items)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'openai': {
|
||||||
|
const items = data.map((acc) => {
|
||||||
|
const boundApiKeysCount = counts.openaiAccountId?.[acc.id] || 0
|
||||||
|
return { ...acc, platform: 'openai', boundApiKeysCount }
|
||||||
|
})
|
||||||
|
allAccounts.push(...items)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'azure_openai': {
|
||||||
|
const items = data.map((acc) => {
|
||||||
|
const boundApiKeysCount = counts.azureOpenaiAccountId?.[acc.id] || 0
|
||||||
|
return { ...acc, platform: 'azure_openai', boundApiKeysCount }
|
||||||
|
})
|
||||||
|
allAccounts.push(...items)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'openai-responses': {
|
||||||
|
openaiResponsesRaw = data
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'ccr': {
|
||||||
|
const items = data.map((acc) => ({ ...acc, platform: 'ccr', boundApiKeysCount: 0 }))
|
||||||
|
allAccounts.push(...items)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'droid': {
|
||||||
|
const items = data.map((acc) => {
|
||||||
|
const boundApiKeysCount = counts.droidAccountId?.[acc.id] || acc.boundApiKeysCount || 0
|
||||||
|
return { ...acc, platform: 'droid', boundApiKeysCount }
|
||||||
|
})
|
||||||
|
allAccounts.push(...items)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'gemini-api': {
|
||||||
|
const items = data.map((acc) => {
|
||||||
|
const boundApiKeysCount = counts.geminiAccountId?.[`api:${acc.id}`] || 0
|
||||||
|
return { ...acc, platform: 'gemini-api', boundApiKeysCount }
|
||||||
|
})
|
||||||
|
allAccounts.push(...items)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gemini API 账户
|
platformResults.forEach(({ platform, success, data }) => {
|
||||||
if (geminiApiData && geminiApiData.success) {
|
if (success) {
|
||||||
const geminiApiAccounts = (geminiApiData.data || []).map((acc) => {
|
appendAccounts(platform, data || [])
|
||||||
// 从绑定计数缓存获取数量
|
}
|
||||||
// Gemini-API账户使用 api: 前缀
|
})
|
||||||
const boundApiKeysCount = counts.geminiAccountId?.[`api:${acc.id}`] || 0
|
|
||||||
// 后端已经包含了groupInfos,直接使用
|
if (openaiResponsesRaw.length > 0) {
|
||||||
return { ...acc, platform: 'gemini-api', boundApiKeysCount }
|
let autoRecoveryConfigMap = {}
|
||||||
|
try {
|
||||||
|
const configsRes = await apiClient.get(
|
||||||
|
'/admin/openai-responses-accounts/auto-recovery-configs'
|
||||||
|
)
|
||||||
|
if (configsRes.success && Array.isArray(configsRes.data)) {
|
||||||
|
autoRecoveryConfigMap = configsRes.data.reduce((map, config) => {
|
||||||
|
if (config?.accountId) {
|
||||||
|
map[config.accountId] = config
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.debug('Failed to load auto-recovery configs:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const responsesAccounts = openaiResponsesRaw.map((acc) => {
|
||||||
|
const boundApiKeysCount = counts.openaiAccountId?.[`responses:${acc.id}`] || 0
|
||||||
|
const autoRecoveryConfig = autoRecoveryConfigMap[acc.id] || acc.autoRecoveryConfig || null
|
||||||
|
return { ...acc, platform: 'openai-responses', boundApiKeysCount, autoRecoveryConfig }
|
||||||
})
|
})
|
||||||
allAccounts.push(...geminiApiAccounts)
|
|
||||||
|
allAccounts.push(...responsesAccounts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据分组筛选器过滤账户
|
// 根据分组筛选器过滤账户
|
||||||
@@ -3013,6 +3262,58 @@ const formatRateLimitTime = (minutes) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查账户是否被限流
|
||||||
|
const isAccountRateLimited = (account) => {
|
||||||
|
if (!account) return false
|
||||||
|
|
||||||
|
// 检查 rateLimitStatus
|
||||||
|
if (account.rateLimitStatus) {
|
||||||
|
if (typeof account.rateLimitStatus === 'string' && account.rateLimitStatus === 'limited') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof account.rateLimitStatus === 'object' &&
|
||||||
|
account.rateLimitStatus.isRateLimited === true
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取限流剩余时间(分钟)
|
||||||
|
const getRateLimitRemainingMinutes = (account) => {
|
||||||
|
if (!account || !account.rateLimitStatus) return 0
|
||||||
|
|
||||||
|
if (typeof account.rateLimitStatus === 'object') {
|
||||||
|
const status = account.rateLimitStatus
|
||||||
|
if (Number.isFinite(status.minutesRemaining)) {
|
||||||
|
return Math.max(0, Math.ceil(status.minutesRemaining))
|
||||||
|
}
|
||||||
|
if (Number.isFinite(status.remainingMinutes)) {
|
||||||
|
return Math.max(0, Math.ceil(status.remainingMinutes))
|
||||||
|
}
|
||||||
|
if (Number.isFinite(status.remainingSeconds)) {
|
||||||
|
return Math.max(0, Math.ceil(status.remainingSeconds / 60))
|
||||||
|
}
|
||||||
|
if (status.rateLimitResetAt) {
|
||||||
|
const diffMs = new Date(status.rateLimitResetAt).getTime() - Date.now()
|
||||||
|
return diffMs > 0 ? Math.ceil(diffMs / 60000) : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有 rateLimitUntil 字段,计算剩余时间
|
||||||
|
if (account.rateLimitUntil) {
|
||||||
|
const now = new Date().getTime()
|
||||||
|
const untilTime = new Date(account.rateLimitUntil).getTime()
|
||||||
|
const diff = untilTime - now
|
||||||
|
return diff > 0 ? Math.ceil(diff / 60000) : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// 打开创建账户模态框
|
// 打开创建账户模态框
|
||||||
const openCreateAccountModal = () => {
|
const openCreateAccountModal = () => {
|
||||||
newAccountPlatform.value = null // 重置选择的平台
|
newAccountPlatform.value = null // 重置选择的平台
|
||||||
|
|||||||
Reference in New Issue
Block a user