Merge pull request #227 from mouyong/dev-local

增强账户管理页面的平台筛选和缓存优化功能
This commit is contained in:
Wesley Liddick
2025-08-10 13:25:02 +08:00
committed by GitHub
3 changed files with 375 additions and 89 deletions

View File

@@ -1146,7 +1146,27 @@ router.post('/claude-accounts/exchange-setup-token-code', authenticateAdmin, asy
// 获取所有Claude账户 // 获取所有Claude账户
router.get('/claude-accounts', authenticateAdmin, async (req, res) => { router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
try { try {
const accounts = await claudeAccountService.getAllAccounts() const { platform, groupId } = req.query
let accounts = await claudeAccountService.getAllAccounts()
// 根据查询参数进行筛选
if (platform && platform !== 'all' && platform !== 'claude') {
// 如果指定了其他平台,返回空数组
accounts = []
}
// 如果指定了分组筛选
if (groupId && groupId !== 'all') {
if (groupId === 'ungrouped') {
// 筛选未分组账户
accounts = accounts.filter((account) => !account.groupInfo)
} else {
// 筛选特定分组的账户
accounts = accounts.filter(
(account) => account.groupInfo && account.groupInfo.id === groupId
)
}
}
// 为每个账户添加使用统计信息 // 为每个账户添加使用统计信息
const accountsWithStats = await Promise.all( const accountsWithStats = await Promise.all(
@@ -1403,7 +1423,27 @@ router.put(
// 获取所有Claude Console账户 // 获取所有Claude Console账户
router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => { router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => {
try { try {
const accounts = await claudeConsoleAccountService.getAllAccounts() const { platform, groupId } = req.query
let accounts = await claudeConsoleAccountService.getAllAccounts()
// 根据查询参数进行筛选
if (platform && platform !== 'all' && platform !== 'claude-console') {
// 如果指定了其他平台,返回空数组
accounts = []
}
// 如果指定了分组筛选
if (groupId && groupId !== 'all') {
if (groupId === 'ungrouped') {
// 筛选未分组账户
accounts = accounts.filter((account) => !account.groupInfo)
} else {
// 筛选特定分组的账户
accounts = accounts.filter(
(account) => account.groupInfo && account.groupInfo.id === groupId
)
}
}
// 为每个账户添加使用统计信息 // 为每个账户添加使用统计信息
const accountsWithStats = await Promise.all( const accountsWithStats = await Promise.all(
@@ -1652,6 +1692,7 @@ router.put(
// 获取所有Bedrock账户 // 获取所有Bedrock账户
router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => { router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
try { try {
const { platform, groupId } = req.query
const result = await bedrockAccountService.getAllAccounts() const result = await bedrockAccountService.getAllAccounts()
if (!result.success) { if (!result.success) {
return res return res
@@ -1659,9 +1700,30 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
.json({ error: 'Failed to get Bedrock accounts', message: result.error }) .json({ error: 'Failed to get Bedrock accounts', message: result.error })
} }
let accounts = result.data
// 根据查询参数进行筛选
if (platform && platform !== 'all' && platform !== 'bedrock') {
// 如果指定了其他平台,返回空数组
accounts = []
}
// 如果指定了分组筛选
if (groupId && groupId !== 'all') {
if (groupId === 'ungrouped') {
// 筛选未分组账户
accounts = accounts.filter((account) => !account.groupInfo)
} else {
// 筛选特定分组的账户
accounts = accounts.filter(
(account) => account.groupInfo && account.groupInfo.id === groupId
)
}
}
// 为每个账户添加使用统计信息 // 为每个账户添加使用统计信息
const accountsWithStats = await Promise.all( const accountsWithStats = await Promise.all(
result.data.map(async (account) => { accounts.map(async (account) => {
try { try {
const usageStats = await redis.getAccountUsageStats(account.id) const usageStats = await redis.getAccountUsageStats(account.id)
return { return {
@@ -2027,7 +2089,27 @@ router.post('/gemini-accounts/exchange-code', authenticateAdmin, async (req, res
// 获取所有 Gemini 账户 // 获取所有 Gemini 账户
router.get('/gemini-accounts', authenticateAdmin, async (req, res) => { router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
try { try {
const accounts = await geminiAccountService.getAllAccounts() const { platform, groupId } = req.query
let accounts = await geminiAccountService.getAllAccounts()
// 根据查询参数进行筛选
if (platform && platform !== 'all' && platform !== 'gemini') {
// 如果指定了其他平台,返回空数组
accounts = []
}
// 如果指定了分组筛选
if (groupId && groupId !== 'all') {
if (groupId === 'ungrouped') {
// 筛选未分组账户
accounts = accounts.filter((account) => !account.groupInfo)
} else {
// 筛选特定分组的账户
accounts = accounts.filter(
(account) => account.groupInfo && account.groupInfo.id === groupId
)
}
}
// 为每个账户添加使用统计信息与Claude账户相同的逻辑 // 为每个账户添加使用统计信息与Claude账户相同的逻辑
const accountsWithStats = await Promise.all( const accountsWithStats = await Promise.all(
@@ -2368,7 +2450,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
acc.isActive && acc.isActive &&
acc.status !== 'blocked' && acc.status !== 'blocked' &&
acc.status !== 'unauthorized' && acc.status !== 'unauthorized' &&
acc.schedulable !== false acc.schedulable !== false &&
!(acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited)
).length ).length
const abnormalClaudeAccounts = claudeAccounts.filter( const abnormalClaudeAccounts = claudeAccounts.filter(
(acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized' (acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized'
@@ -2390,7 +2473,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
acc.isActive && acc.isActive &&
acc.status !== 'blocked' && acc.status !== 'blocked' &&
acc.status !== 'unauthorized' && acc.status !== 'unauthorized' &&
acc.schedulable !== false acc.schedulable !== false &&
!(acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited)
).length ).length
const abnormalClaudeConsoleAccounts = claudeConsoleAccounts.filter( const abnormalClaudeConsoleAccounts = claudeConsoleAccounts.filter(
(acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized' (acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized'
@@ -2412,7 +2496,11 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
acc.isActive && acc.isActive &&
acc.status !== 'blocked' && acc.status !== 'blocked' &&
acc.status !== 'unauthorized' && acc.status !== 'unauthorized' &&
acc.schedulable !== false acc.schedulable !== false &&
!(
acc.rateLimitStatus === 'limited' ||
(acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited)
)
).length ).length
const abnormalGeminiAccounts = geminiAccounts.filter( const abnormalGeminiAccounts = geminiAccounts.filter(
(acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized' (acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized'
@@ -2425,7 +2513,9 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
acc.status !== 'unauthorized' acc.status !== 'unauthorized'
).length ).length
const rateLimitedGeminiAccounts = geminiAccounts.filter( const rateLimitedGeminiAccounts = geminiAccounts.filter(
(acc) => acc.rateLimitStatus === 'limited' (acc) =>
acc.rateLimitStatus === 'limited' ||
(acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited)
).length ).length
// Bedrock账户统计 // Bedrock账户统计
@@ -2434,7 +2524,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
acc.isActive && acc.isActive &&
acc.status !== 'blocked' && acc.status !== 'blocked' &&
acc.status !== 'unauthorized' && acc.status !== 'unauthorized' &&
acc.schedulable !== false acc.schedulable !== false &&
!(acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited)
).length ).length
const abnormalBedrockAccounts = bedrockAccounts.filter( const abnormalBedrockAccounts = bedrockAccounts.filter(
(acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized' (acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized'

View File

@@ -504,6 +504,9 @@ async function getAllAccounts() {
for (const key of keys) { for (const key of keys) {
const accountData = await client.hgetall(key) const accountData = await client.hgetall(key)
if (accountData && Object.keys(accountData).length > 0) { if (accountData && Object.keys(accountData).length > 0) {
// 获取限流状态信息
const rateLimitInfo = await getAccountRateLimitInfo(accountData.id)
// 解析代理配置 // 解析代理配置
if (accountData.proxy) { if (accountData.proxy) {
try { try {
@@ -519,7 +522,19 @@ async function getAllAccounts() {
...accountData, ...accountData,
geminiOauth: accountData.geminiOauth ? '[ENCRYPTED]' : '', geminiOauth: accountData.geminiOauth ? '[ENCRYPTED]' : '',
accessToken: accountData.accessToken ? '[ENCRYPTED]' : '', accessToken: accountData.accessToken ? '[ENCRYPTED]' : '',
refreshToken: accountData.refreshToken ? '[ENCRYPTED]' : '' refreshToken: accountData.refreshToken ? '[ENCRYPTED]' : '',
// 添加限流状态信息(统一格式)
rateLimitStatus: rateLimitInfo
? {
isRateLimited: rateLimitInfo.isRateLimited,
rateLimitedAt: rateLimitInfo.rateLimitedAt,
minutesRemaining: rateLimitInfo.minutesRemaining
}
: {
isRateLimited: false,
rateLimitedAt: null,
minutesRemaining: 0
}
}) })
} }
} }
@@ -774,6 +789,45 @@ async function setAccountRateLimited(accountId, isLimited = true) {
await updateAccount(accountId, updates) await updateAccount(accountId, updates)
} }
// 获取账户的限流信息(参考 claudeAccountService 的实现)
async function getAccountRateLimitInfo(accountId) {
try {
const account = await getAccount(accountId)
if (!account) {
return null
}
if (account.rateLimitStatus === 'limited' && account.rateLimitedAt) {
const rateLimitedAt = new Date(account.rateLimitedAt)
const now = new Date()
const minutesSinceRateLimit = Math.floor((now - rateLimitedAt) / (1000 * 60))
// Gemini 限流持续时间为 1 小时
const minutesRemaining = Math.max(0, 60 - minutesSinceRateLimit)
const rateLimitEndAt = new Date(rateLimitedAt.getTime() + 60 * 60 * 1000).toISOString()
return {
isRateLimited: minutesRemaining > 0,
rateLimitedAt: account.rateLimitedAt,
minutesSinceRateLimit,
minutesRemaining,
rateLimitEndAt
}
}
return {
isRateLimited: false,
rateLimitedAt: null,
minutesSinceRateLimit: 0,
minutesRemaining: 0,
rateLimitEndAt: null
}
} catch (error) {
logger.error(`❌ Failed to get rate limit info for Gemini account: ${accountId}`, error)
return null
}
}
// 获取配置的OAuth客户端 - 参考GeminiCliSimulator的getOauthClient方法 // 获取配置的OAuth客户端 - 参考GeminiCliSimulator的getOauthClient方法
async function getOauthClient(accessToken, refreshToken) { async function getOauthClient(accessToken, refreshToken) {
const client = new OAuth2Client({ const client = new OAuth2Client({
@@ -1137,6 +1191,7 @@ module.exports = {
refreshAccountToken, refreshAccountToken,
markAccountUsed, markAccountUsed,
setAccountRateLimited, setAccountRateLimited,
getAccountRateLimitInfo,
isTokenExpired, isTokenExpired,
getOauthClient, getOauthClient,
loadCodeAssist, loadCodeAssist,

View File

@@ -24,6 +24,21 @@
/> />
</div> </div>
<!-- 平台筛选器 -->
<div class="group relative min-w-[140px]">
<div
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-blue-500 to-indigo-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
></div>
<CustomDropdown
v-model="platformFilter"
icon="fa-server"
icon-color="text-blue-500"
:options="platformOptions"
placeholder="选择平台"
@change="filterByPlatform"
/>
</div>
<!-- 分组筛选器 --> <!-- 分组筛选器 -->
<div class="group relative min-w-[160px]"> <div class="group relative min-w-[160px]">
<div <div
@@ -40,22 +55,32 @@
</div> </div>
<!-- 刷新按钮 --> <!-- 刷新按钮 -->
<button <div class="relative">
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 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto" <el-tooltip
:disabled="accountsLoading" content="刷新数据 (Ctrl/⌘+点击强制刷新所有缓存)"
@click="loadAccounts()" effect="dark"
> placement="bottom"
<div >
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-green-500 to-teal-500 opacity-0 blur transition duration-300 group-hover:opacity-20" <button
></div> 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 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
<i :disabled="accountsLoading"
:class="[ @click.ctrl.exact="loadAccounts(true)"
'fas relative text-green-500', @click.exact="loadAccounts(false)"
accountsLoading ? 'fa-spinner fa-spin' : 'fa-sync-alt' @click.meta.exact="loadAccounts(true)"
]" >
/> <div
<span class="relative">刷新</span> class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-green-500 to-teal-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
</button> ></div>
<i
:class="[
'fas relative text-green-500',
accountsLoading ? 'fa-spinner fa-spin' : 'fa-sync-alt'
]"
/>
<span class="relative">刷新</span>
</button>
</el-tooltip>
</div>
</div> </div>
<!-- 添加账户按钮 --> <!-- 添加账户按钮 -->
@@ -307,11 +332,22 @@
}} }}
</span> </span>
<span <span
v-if="account.rateLimitStatus && account.rateLimitStatus.isRateLimited" v-if="
(account.rateLimitStatus && account.rateLimitStatus.isRateLimited) ||
account.rateLimitStatus === 'limited'
"
class="inline-flex items-center rounded-full bg-yellow-100 px-3 py-1 text-xs font-semibold text-yellow-800" class="inline-flex items-center rounded-full bg-yellow-100 px-3 py-1 text-xs font-semibold text-yellow-800"
> >
<i class="fas fa-exclamation-triangle mr-1" /> <i class="fas fa-exclamation-triangle mr-1" />
限流中 ({{ account.rateLimitStatus.minutesRemaining }}分钟) 限流中
<span
v-if="
account.rateLimitStatus &&
typeof account.rateLimitStatus === 'object' &&
account.rateLimitStatus.minutesRemaining > 0
"
>({{ account.rateLimitStatus.minutesRemaining }}分钟)</span
>
</span> </span>
<span <span
v-if="account.schedulable === false" v-if="account.schedulable === false"
@@ -458,6 +494,7 @@
(account.status === 'unauthorized' || (account.status === 'unauthorized' ||
account.status !== 'active' || account.status !== 'active' ||
account.rateLimitStatus?.isRateLimited || account.rateLimitStatus?.isRateLimited ||
account.rateLimitStatus === 'limited' ||
!account.isActive) !account.isActive)
" "
:class="[ :class="[
@@ -754,7 +791,13 @@ const apiKeys = ref([])
const refreshingTokens = ref({}) const refreshingTokens = ref({})
const accountGroups = ref([]) const accountGroups = ref([])
const groupFilter = ref('all') const groupFilter = ref('all')
const filteredAccounts = ref([]) const platformFilter = ref('all')
// 缓存状态标志
const apiKeysLoaded = ref(false)
const groupsLoaded = ref(false)
const groupMembersLoaded = ref(false)
const accountGroupMap = ref(new Map())
// 下拉选项数据 // 下拉选项数据
const sortOptions = ref([ const sortOptions = ref([
@@ -765,6 +808,14 @@ const sortOptions = ref([
{ value: 'lastUsed', label: '按最后使用排序', icon: 'fa-clock' } { value: 'lastUsed', label: '按最后使用排序', icon: 'fa-clock' }
]) ])
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: 'fa-robot' },
{ value: 'bedrock', label: 'Bedrock', icon: 'fab fa-aws' }
])
const groupOptions = computed(() => { const groupOptions = computed(() => {
const options = [ const options = [
{ value: 'all', label: '所有账户', icon: 'fa-globe' }, { value: 'all', label: '所有账户', icon: 'fa-globe' },
@@ -787,7 +838,7 @@ const editingAccount = ref(null)
// 计算排序后的账户列表 // 计算排序后的账户列表
const sortedAccounts = computed(() => { const sortedAccounts = computed(() => {
const sourceAccounts = filteredAccounts.value.length > 0 ? filteredAccounts.value : accounts.value const sourceAccounts = accounts.value
if (!accountsSortBy.value) return sourceAccounts if (!accountsSortBy.value) return sourceAccounts
const sorted = [...sourceAccounts].sort((a, b) => { const sorted = [...sourceAccounts].sort((a, b) => {
@@ -827,49 +878,75 @@ const sortedAccounts = computed(() => {
}) })
// 加载账户列表 // 加载账户列表
const loadAccounts = async () => { const loadAccounts = async (forceReload = false) => {
accountsLoading.value = true accountsLoading.value = true
try { try {
const [claudeData, claudeConsoleData, bedrockData, geminiData, apiKeysData, groupsData] = // 构建查询参数
await Promise.all([ const params = {}
apiClient.get('/admin/claude-accounts'), if (platformFilter.value !== 'all') {
apiClient.get('/admin/claude-console-accounts'), params.platform = platformFilter.value
apiClient.get('/admin/bedrock-accounts'), }
apiClient.get('/admin/gemini-accounts'), if (groupFilter.value !== 'all') {
apiClient.get('/admin/api-keys'), params.groupId = groupFilter.value
apiClient.get('/admin/account-groups')
])
// 更新API Keys列表
if (apiKeysData.success) {
apiKeys.value = apiKeysData.data || []
} }
// 更新分组列表 // 根据平台筛选决定需要请求哪些接口
if (groupsData.success) { const requests = []
accountGroups.value = groupsData.data || []
}
// 创建分组ID到分组信息的映射 if (platformFilter.value === 'all') {
const groupMap = new Map() // 请求所有平台
const accountGroupMap = new Map() requests.push(
apiClient.get('/admin/claude-accounts', { params }),
// 获取所有分组的成员信息 apiClient.get('/admin/claude-console-accounts', { params }),
for (const group of accountGroups.value) { apiClient.get('/admin/bedrock-accounts', { params }),
groupMap.set(group.id, group) apiClient.get('/admin/gemini-accounts', { params })
try { )
const membersResponse = await apiClient.get(`/admin/account-groups/${group.id}/members`) } else {
if (membersResponse.success) { // 只请求指定平台其他平台设为null占位
const members = membersResponse.data || [] switch (platformFilter.value) {
members.forEach((member) => { case 'claude':
accountGroupMap.set(member.id, group) requests.push(
}) apiClient.get('/admin/claude-accounts', { params }),
} Promise.resolve({ success: true, data: [] }), // claude-console 占位
} catch (error) { Promise.resolve({ success: true, data: [] }), // bedrock 占位
console.error(`Failed to load members for group ${group.id}:`, error) Promise.resolve({ success: true, data: [] }) // gemini 占位
)
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 占位
)
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 占位
)
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 })
)
break
} }
} }
// 使用缓存机制加载 API Keys 和分组数据
await Promise.all([loadApiKeys(forceReload), loadAccountGroups(forceReload)])
// 加载分组成员关系(需要在分组数据加载完成后)
await loadGroupMembers(forceReload)
const [claudeData, claudeConsoleData, bedrockData, geminiData] = await Promise.all(requests)
const allAccounts = [] const allAccounts = []
if (claudeData.success) { if (claudeData.success) {
@@ -879,7 +956,7 @@ const loadAccounts = async () => {
(key) => key.claudeAccountId === acc.id (key) => key.claudeAccountId === acc.id
).length ).length
// 检查是否属于某个分组 // 检查是否属于某个分组
const groupInfo = accountGroupMap.get(acc.id) || null const groupInfo = accountGroupMap.value.get(acc.id) || null
return { ...acc, platform: 'claude', boundApiKeysCount, groupInfo } return { ...acc, platform: 'claude', boundApiKeysCount, groupInfo }
}) })
allAccounts.push(...claudeAccounts) allAccounts.push(...claudeAccounts)
@@ -888,7 +965,7 @@ const loadAccounts = async () => {
if (claudeConsoleData.success) { if (claudeConsoleData.success) {
const claudeConsoleAccounts = (claudeConsoleData.data || []).map((acc) => { const claudeConsoleAccounts = (claudeConsoleData.data || []).map((acc) => {
// Claude Console账户暂时不支持直接绑定 // Claude Console账户暂时不支持直接绑定
const groupInfo = accountGroupMap.get(acc.id) || null const groupInfo = accountGroupMap.value.get(acc.id) || null
return { ...acc, platform: 'claude-console', boundApiKeysCount: 0, groupInfo } return { ...acc, platform: 'claude-console', boundApiKeysCount: 0, groupInfo }
}) })
allAccounts.push(...claudeConsoleAccounts) allAccounts.push(...claudeConsoleAccounts)
@@ -897,7 +974,7 @@ const loadAccounts = async () => {
if (bedrockData.success) { if (bedrockData.success) {
const bedrockAccounts = (bedrockData.data || []).map((acc) => { const bedrockAccounts = (bedrockData.data || []).map((acc) => {
// Bedrock账户暂时不支持直接绑定 // Bedrock账户暂时不支持直接绑定
const groupInfo = accountGroupMap.get(acc.id) || null const groupInfo = accountGroupMap.value.get(acc.id) || null
return { ...acc, platform: 'bedrock', boundApiKeysCount: 0, groupInfo } return { ...acc, platform: 'bedrock', boundApiKeysCount: 0, groupInfo }
}) })
allAccounts.push(...bedrockAccounts) allAccounts.push(...bedrockAccounts)
@@ -909,15 +986,13 @@ const loadAccounts = async () => {
const boundApiKeysCount = apiKeys.value.filter( const boundApiKeysCount = apiKeys.value.filter(
(key) => key.geminiAccountId === acc.id (key) => key.geminiAccountId === acc.id
).length ).length
const groupInfo = accountGroupMap.get(acc.id) || null const groupInfo = accountGroupMap.value.get(acc.id) || null
return { ...acc, platform: 'gemini', boundApiKeysCount, groupInfo } return { ...acc, platform: 'gemini', boundApiKeysCount, groupInfo }
}) })
allAccounts.push(...geminiAccounts) allAccounts.push(...geminiAccounts)
} }
accounts.value = allAccounts accounts.value = allAccounts
// 初始化过滤后的账户列表
filterByGroup()
} catch (error) { } catch (error) {
showToast('加载账户失败', 'error') showToast('加载账户失败', 'error')
} finally { } finally {
@@ -963,30 +1038,86 @@ const formatLastUsed = (dateString) => {
return date.toLocaleDateString('zh-CN') return date.toLocaleDateString('zh-CN')
} }
// 加载API Keys列表 // 加载API Keys列表(缓存版本)
const loadApiKeys = async () => { const loadApiKeys = async (forceReload = false) => {
if (!forceReload && apiKeysLoaded.value) {
return // 使用缓存数据
}
try { try {
const response = await apiClient.get('/admin/api-keys') const response = await apiClient.get('/admin/api-keys')
if (response.success) { if (response.success) {
apiKeys.value = response.data apiKeys.value = response.data || []
apiKeysLoaded.value = true
} }
} catch (error) { } catch (error) {
console.error('Failed to load API keys:', error) console.error('Failed to load API keys:', error)
} }
} }
// 加载账户分组列表(缓存版本)
const loadAccountGroups = async (forceReload = false) => {
if (!forceReload && groupsLoaded.value) {
return // 使用缓存数据
}
try {
const response = await apiClient.get('/admin/account-groups')
if (response.success) {
accountGroups.value = response.data || []
groupsLoaded.value = true
}
} catch (error) {
console.error('Failed to load account groups:', error)
}
}
// 加载分组成员关系(缓存版本)
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
groupsLoaded.value = false
groupMembersLoaded.value = false
accountGroupMap.value.clear()
}
// 按平台筛选账户
const filterByPlatform = () => {
loadAccounts()
}
// 按分组筛选账户 // 按分组筛选账户
const filterByGroup = () => { const filterByGroup = () => {
if (groupFilter.value === 'all') { loadAccounts()
filteredAccounts.value = accounts.value
} else if (groupFilter.value === 'ungrouped') {
filteredAccounts.value = accounts.value.filter((acc) => !acc.groupInfo)
} else {
// 按特定分组筛选
filteredAccounts.value = accounts.value.filter(
(acc) => acc.groupInfo && acc.groupInfo.id === groupFilter.value
)
}
} }
// 格式化代理信息显示 // 格式化代理信息显示
@@ -1091,6 +1222,8 @@ const deleteAccount = async (account) => {
if (data.success) { if (data.success) {
showToast('账户已删除', 'success') showToast('账户已删除', 'success')
// 清空分组成员缓存因为账户可能从分组中移除
groupMembersLoaded.value = false
loadAccounts() loadAccounts()
} else { } else {
showToast(data.message || '删除失败', 'error') showToast(data.message || '删除失败', 'error')
@@ -1196,6 +1329,8 @@ const toggleSchedulable = async (account) => {
const handleCreateSuccess = () => { const handleCreateSuccess = () => {
showCreateAccountModal.value = false showCreateAccountModal.value = false
showToast('账户创建成功', 'success') showToast('账户创建成功', 'success')
// 清空缓存,因为可能涉及分组关系变化
clearCache()
loadAccounts() loadAccounts()
} }
@@ -1203,6 +1338,8 @@ const handleCreateSuccess = () => {
const handleEditSuccess = () => { const handleEditSuccess = () => {
showEditAccountModal.value = false showEditAccountModal.value = false
showToast('账户更新成功', 'success') showToast('账户更新成功', 'success')
// 清空分组成员缓存,因为账户类型和分组可能发生变化
groupMembersLoaded.value = false
loadAccounts() loadAccounts()
} }
@@ -1216,7 +1353,8 @@ const getAccountStatusText = (account) => {
if ( if (
account.isRateLimited || account.isRateLimited ||
account.status === 'rate_limited' || account.status === 'rate_limited' ||
(account.rateLimitStatus && account.rateLimitStatus.isRateLimited) (account.rateLimitStatus && account.rateLimitStatus.isRateLimited) ||
account.rateLimitStatus === 'limited'
) )
return '限流中' return '限流中'
// 检查是否错误 // 检查是否错误
@@ -1238,7 +1376,8 @@ const getAccountStatusClass = (account) => {
if ( if (
account.isRateLimited || account.isRateLimited ||
account.status === 'rate_limited' || account.status === 'rate_limited' ||
(account.rateLimitStatus && account.rateLimitStatus.isRateLimited) (account.rateLimitStatus && account.rateLimitStatus.isRateLimited) ||
account.rateLimitStatus === 'limited'
) { ) {
return 'bg-orange-100 text-orange-800' return 'bg-orange-100 text-orange-800'
} }
@@ -1262,7 +1401,8 @@ const getAccountStatusDotClass = (account) => {
if ( if (
account.isRateLimited || account.isRateLimited ||
account.status === 'rate_limited' || account.status === 'rate_limited' ||
(account.rateLimitStatus && account.rateLimitStatus.isRateLimited) (account.rateLimitStatus && account.rateLimitStatus.isRateLimited) ||
account.rateLimitStatus === 'limited'
) { ) {
return 'bg-orange-500' return 'bg-orange-500'
} }
@@ -1330,8 +1470,8 @@ watch(accountSortBy, (newVal) => {
}) })
onMounted(() => { onMounted(() => {
loadAccounts() // 首次加载时强制刷新所有数据
loadApiKeys() loadAccounts(true)
}) })
</script> </script>