diff --git a/web/admin-spa/src/views/AccountsView.vue b/web/admin-spa/src/views/AccountsView.vue index 7a868b26..01734bc8 100644 --- a/web/admin-spa/src/views/AccountsView.vue +++ b/web/admin-spa/src/views/AccountsView.vue @@ -2124,19 +2124,91 @@ const sortOptions = ref([ { value: 'rateLimitTime', label: '按限流时间排序', icon: 'fa-hourglass' } ]) -const platformOptions = ref([ - { value: 'all', label: '所有平台', icon: 'fa-globe' }, - { value: 'claude', label: 'Claude', icon: 'fa-brain' }, - { value: 'claude-console', label: 'Claude Console', icon: 'fa-terminal' }, - { value: 'gemini', label: 'Gemini', icon: 'fab fa-google' }, - { value: 'gemini-api', label: 'Gemini API', icon: 'fa-key' }, - { value: 'openai', label: 'OpenAi', icon: 'fa-openai' }, - { value: 'azure_openai', label: 'Azure OpenAI', icon: 'fab fa-microsoft' }, - { value: 'bedrock', label: 'Bedrock', icon: 'fab fa-aws' }, - { value: 'openai-responses', label: 'OpenAI-Responses', icon: 'fa-server' }, - { value: 'ccr', label: 'CCR', icon: 'fa-code-branch' }, - { value: 'droid', label: 'Droid', icon: 'fa-robot' } -]) +// 平台层级结构定义 +const platformHierarchy = [ + { + value: 'group-claude', + label: 'Claude(全部)', + icon: 'fa-brain', + children: [ + { value: 'claude', label: 'Claude 官方/OAuth', icon: 'fa-brain' }, + { value: 'claude-console', label: 'Claude Console', icon: 'fa-terminal' }, + { value: 'bedrock', label: 'Bedrock', icon: 'fab fa-aws' }, + { value: 'ccr', label: 'CCR Relay', icon: 'fa-code-branch' } + ] + }, + { + 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' }, @@ -2696,190 +2768,14 @@ const loadAccounts = async (forceReload = false) => { try { // 构建查询参数(用于其他筛选情况) const params = {} - if (platformFilter.value !== 'all') { + if (platformFilter.value !== 'all' && !platformGroupMap[platformFilter.value]) { params.platform = platformFilter.value } if (groupFilter.value !== 'all') { params.groupId = groupFilter.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 - } - } + const platformsToFetch = getPlatformsForFilter(platformFilter.value) // 使用缓存机制加载绑定计数和分组数据(不再加载完整的 API Keys 数据) await Promise.all([loadBindingCounts(forceReload), loadAccountGroups(forceReload)]) @@ -2887,125 +2783,137 @@ const loadAccounts = async (forceReload = false) => { // 后端账户API已经包含分组信息,不需要单独加载分组成员关系 // await loadGroupMembers(forceReload) - const [ - claudeData, - claudeConsoleData, - bedrockData, - geminiData, - openaiData, - azureOpenaiData, - openaiResponsesData, - ccrData, - droidData, - geminiApiData - ] = await Promise.all(requests) + const platformResults = await Promise.all( + platformsToFetch.map(async (platform) => { + const handler = platformRequestHandlers[platform] + if (!handler) { + return { platform, success: true, data: [] } + } - const allAccounts = [] - - // 获取绑定计数数据 - const counts = bindingCounts.value - - if (claudeData.success) { - 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 + try { + const res = await handler(params) + return { platform, success: res?.success, data: res?.data } + } catch (error) { + console.debug(`Failed to load ${platform} accounts:`, error) + return { platform, success: false, data: [] } } }) - 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 账户 - if (geminiApiData && geminiApiData.success) { - const geminiApiAccounts = (geminiApiData.data || []).map((acc) => { - // 从绑定计数缓存获取数量 - // Gemini-API账户使用 api: 前缀 - const boundApiKeysCount = counts.geminiAccountId?.[`api:${acc.id}`] || 0 - // 后端已经包含了groupInfos,直接使用 - return { ...acc, platform: 'gemini-api', boundApiKeysCount } + platformResults.forEach(({ platform, success, data }) => { + if (success) { + appendAccounts(platform, data || []) + } + }) + + if (openaiResponsesRaw.length > 0) { + 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) } // 根据分组筛选器过滤账户