feat: 增强账户管理页面的平台筛选和缓存优化功能

- 添加平台筛选功能到账户管理页面
  * 后端:在所有账户接口中支持platform和groupId查询参数
  * 前端:添加平台筛选下拉框,支持条件性API请求

- 使用智能缓存机制优化数据加载
  * 缓存API Keys、账户分组和分组成员数据
  * 通过Ctrl/⌘+点击刷新按钮实现强制重新加载
  * 在数据变更时自动清除相关缓存(创建/编辑/删除)

- 改进Gemini账户限流状态显示
  * 在geminiAccountService中添加限流信息支持
  * 统一所有平台的限流状态格式
  * 修复仪表板统计,排除被限流的账户

- 提升用户界面体验
  * 将原生title提示替换为Element Plus的el-tooltip组件
  * 支持跨平台键盘快捷键(Ctrl/⌘+点击)
  * ESLint规范合规和代码格式化改进

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
mouyong
2025-08-10 12:38:17 +08:00
parent 112ed7a289
commit 4bcd2878f2
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账户
router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
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(
@@ -1403,7 +1423,27 @@ router.put(
// 获取所有Claude Console账户
router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => {
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(
@@ -1652,6 +1692,7 @@ router.put(
// 获取所有Bedrock账户
router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
try {
const { platform, groupId } = req.query
const result = await bedrockAccountService.getAllAccounts()
if (!result.success) {
return res
@@ -1659,9 +1700,30 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
.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(
result.data.map(async (account) => {
accounts.map(async (account) => {
try {
const usageStats = await redis.getAccountUsageStats(account.id)
return {
@@ -2027,7 +2089,27 @@ router.post('/gemini-accounts/exchange-code', authenticateAdmin, async (req, res
// 获取所有 Gemini 账户
router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
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账户相同的逻辑
const accountsWithStats = await Promise.all(
@@ -2368,7 +2450,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
acc.isActive &&
acc.status !== 'blocked' &&
acc.status !== 'unauthorized' &&
acc.schedulable !== false
acc.schedulable !== false &&
!(acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited)
).length
const abnormalClaudeAccounts = claudeAccounts.filter(
(acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized'
@@ -2390,7 +2473,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
acc.isActive &&
acc.status !== 'blocked' &&
acc.status !== 'unauthorized' &&
acc.schedulable !== false
acc.schedulable !== false &&
!(acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited)
).length
const abnormalClaudeConsoleAccounts = claudeConsoleAccounts.filter(
(acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized'
@@ -2412,7 +2496,11 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
acc.isActive &&
acc.status !== 'blocked' &&
acc.status !== 'unauthorized' &&
acc.schedulable !== false
acc.schedulable !== false &&
!(
acc.rateLimitStatus === 'limited' ||
(acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited)
)
).length
const abnormalGeminiAccounts = geminiAccounts.filter(
(acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized'
@@ -2425,7 +2513,9 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
acc.status !== 'unauthorized'
).length
const rateLimitedGeminiAccounts = geminiAccounts.filter(
(acc) => acc.rateLimitStatus === 'limited'
(acc) =>
acc.rateLimitStatus === 'limited' ||
(acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited)
).length
// Bedrock账户统计
@@ -2434,7 +2524,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
acc.isActive &&
acc.status !== 'blocked' &&
acc.status !== 'unauthorized' &&
acc.schedulable !== false
acc.schedulable !== false &&
!(acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited)
).length
const abnormalBedrockAccounts = bedrockAccounts.filter(
(acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized'

View File

@@ -504,6 +504,9 @@ async function getAllAccounts() {
for (const key of keys) {
const accountData = await client.hgetall(key)
if (accountData && Object.keys(accountData).length > 0) {
// 获取限流状态信息
const rateLimitInfo = await getAccountRateLimitInfo(accountData.id)
// 解析代理配置
if (accountData.proxy) {
try {
@@ -519,7 +522,19 @@ async function getAllAccounts() {
...accountData,
geminiOauth: accountData.geminiOauth ? '[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)
}
// 获取账户的限流信息(参考 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方法
async function getOauthClient(accessToken, refreshToken) {
const client = new OAuth2Client({
@@ -1137,6 +1191,7 @@ module.exports = {
refreshAccountToken,
markAccountUsed,
setAccountRateLimited,
getAccountRateLimitInfo,
isTokenExpired,
getOauthClient,
loadCodeAssist,