mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
@@ -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'
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user