mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 完善账户多分组功能和Azure OpenAI支持
主要功能: - 实现账户多分组调度功能完整支持 - 修复Azure OpenAI账户优先级显示问题(前端条件判断缺失) - 修复未分组筛选功能失效(API参数处理) - 修复Azure OpenAI账户创建错误调用Gemini API的问题 - 完善各平台分组信息支持和使用统计显示 - 统一删除账户时的分组清理逻辑 - 添加前端请求参数处理支持 技术改进: - 前端支持多平台账户请求构造 - 后端统一groupInfos返回格式 - API客户端完善查询参数处理 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,379 +0,0 @@
|
|||||||
/**
|
|
||||||
* 多分组功能测试脚本
|
|
||||||
* 测试一个账户可以属于多个分组的功能
|
|
||||||
*/
|
|
||||||
|
|
||||||
require('dotenv').config()
|
|
||||||
const redis = require('../src/models/redis')
|
|
||||||
const accountGroupService = require('../src/services/accountGroupService')
|
|
||||||
const claudeAccountService = require('../src/services/claudeAccountService')
|
|
||||||
|
|
||||||
// 测试配置
|
|
||||||
const TEST_PREFIX = 'multi_group_test_'
|
|
||||||
const CLEANUP_ON_FINISH = true
|
|
||||||
|
|
||||||
// 测试数据存储
|
|
||||||
const testData = {
|
|
||||||
groups: [],
|
|
||||||
accounts: []
|
|
||||||
}
|
|
||||||
|
|
||||||
// 颜色输出
|
|
||||||
const colors = {
|
|
||||||
green: '\x1b[32m',
|
|
||||||
red: '\x1b[31m',
|
|
||||||
yellow: '\x1b[33m',
|
|
||||||
blue: '\x1b[34m',
|
|
||||||
reset: '\x1b[0m'
|
|
||||||
}
|
|
||||||
|
|
||||||
function log(message, type = 'info') {
|
|
||||||
const color =
|
|
||||||
{
|
|
||||||
success: colors.green,
|
|
||||||
error: colors.red,
|
|
||||||
warning: colors.yellow,
|
|
||||||
info: colors.blue
|
|
||||||
}[type] || colors.reset
|
|
||||||
|
|
||||||
console.log(`${color}${message}${colors.reset}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sleep(ms) {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理测试数据
|
|
||||||
async function cleanup() {
|
|
||||||
log('\n🧹 清理测试数据...', 'info')
|
|
||||||
|
|
||||||
// 删除测试账户
|
|
||||||
for (const account of testData.accounts) {
|
|
||||||
try {
|
|
||||||
await claudeAccountService.deleteAccount(account.id)
|
|
||||||
log(`✅ 删除测试账户: ${account.name}`, 'success')
|
|
||||||
} catch (error) {
|
|
||||||
log(`❌ 删除账户失败: ${error.message}`, 'error')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除测试分组
|
|
||||||
for (const group of testData.groups) {
|
|
||||||
try {
|
|
||||||
// 先移除所有成员
|
|
||||||
const members = await accountGroupService.getGroupMembers(group.id)
|
|
||||||
for (const memberId of members) {
|
|
||||||
await accountGroupService.removeAccountFromGroup(memberId, group.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
await accountGroupService.deleteGroup(group.id)
|
|
||||||
log(`✅ 删除测试分组: ${group.name}`, 'success')
|
|
||||||
} catch (error) {
|
|
||||||
log(`❌ 删除分组失败: ${error.message}`, 'error')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试1: 创建测试数据
|
|
||||||
async function test1_createTestData() {
|
|
||||||
log('\n📝 测试1: 创建测试数据', 'info')
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建3个测试分组
|
|
||||||
const group1 = await accountGroupService.createGroup({
|
|
||||||
name: `${TEST_PREFIX}高优先级组`,
|
|
||||||
platform: 'claude',
|
|
||||||
description: '高优先级账户分组'
|
|
||||||
})
|
|
||||||
testData.groups.push(group1)
|
|
||||||
log(`✅ 创建分组1: ${group1.name}`, 'success')
|
|
||||||
|
|
||||||
const group2 = await accountGroupService.createGroup({
|
|
||||||
name: `${TEST_PREFIX}备用组`,
|
|
||||||
platform: 'claude',
|
|
||||||
description: '备用账户分组'
|
|
||||||
})
|
|
||||||
testData.groups.push(group2)
|
|
||||||
log(`✅ 创建分组2: ${group2.name}`, 'success')
|
|
||||||
|
|
||||||
const group3 = await accountGroupService.createGroup({
|
|
||||||
name: `${TEST_PREFIX}专用组`,
|
|
||||||
platform: 'claude',
|
|
||||||
description: '专用账户分组'
|
|
||||||
})
|
|
||||||
testData.groups.push(group3)
|
|
||||||
log(`✅ 创建分组3: ${group3.name}`, 'success')
|
|
||||||
|
|
||||||
// 创建测试账户
|
|
||||||
const account1 = await claudeAccountService.createAccount({
|
|
||||||
name: `${TEST_PREFIX}测试账户1`,
|
|
||||||
email: 'test1@example.com',
|
|
||||||
refreshToken: 'test_refresh_token_1',
|
|
||||||
accountType: 'group'
|
|
||||||
})
|
|
||||||
testData.accounts.push(account1)
|
|
||||||
log(`✅ 创建测试账户1: ${account1.name}`, 'success')
|
|
||||||
|
|
||||||
const account2 = await claudeAccountService.createAccount({
|
|
||||||
name: `${TEST_PREFIX}测试账户2`,
|
|
||||||
email: 'test2@example.com',
|
|
||||||
refreshToken: 'test_refresh_token_2',
|
|
||||||
accountType: 'group'
|
|
||||||
})
|
|
||||||
testData.accounts.push(account2)
|
|
||||||
log(`✅ 创建测试账户2: ${account2.name}`, 'success')
|
|
||||||
|
|
||||||
log(`✅ 测试数据创建完成: 3个分组, 2个账户`, 'success')
|
|
||||||
} catch (error) {
|
|
||||||
log(`❌ 测试1失败: ${error.message}`, 'error')
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试2: 账户加入多个分组
|
|
||||||
async function test2_addAccountToMultipleGroups() {
|
|
||||||
log('\n📝 测试2: 账户加入多个分组', 'info')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [group1, group2, group3] = testData.groups
|
|
||||||
const [account1, account2] = testData.accounts
|
|
||||||
|
|
||||||
// 账户1加入分组1和分组2
|
|
||||||
await accountGroupService.addAccountToGroup(account1.id, group1.id, 'claude')
|
|
||||||
log(`✅ 账户1加入分组1: ${group1.name}`, 'success')
|
|
||||||
|
|
||||||
await accountGroupService.addAccountToGroup(account1.id, group2.id, 'claude')
|
|
||||||
log(`✅ 账户1加入分组2: ${group2.name}`, 'success')
|
|
||||||
|
|
||||||
// 账户2加入分组2和分组3
|
|
||||||
await accountGroupService.addAccountToGroup(account2.id, group2.id, 'claude')
|
|
||||||
log(`✅ 账户2加入分组2: ${group2.name}`, 'success')
|
|
||||||
|
|
||||||
await accountGroupService.addAccountToGroup(account2.id, group3.id, 'claude')
|
|
||||||
log(`✅ 账户2加入分组3: ${group3.name}`, 'success')
|
|
||||||
|
|
||||||
log(`✅ 多分组关系建立完成`, 'success')
|
|
||||||
} catch (error) {
|
|
||||||
log(`❌ 测试2失败: ${error.message}`, 'error')
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试3: 验证多分组关系
|
|
||||||
async function test3_verifyMultiGroupRelationships() {
|
|
||||||
log('\n📝 测试3: 验证多分组关系', 'info')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [group1, group2, group3] = testData.groups
|
|
||||||
const [account1, account2] = testData.accounts
|
|
||||||
|
|
||||||
// 验证账户1的分组关系
|
|
||||||
const account1Groups = await accountGroupService.getAccountGroup(account1.id)
|
|
||||||
log(`📊 账户1所属分组数量: ${account1Groups.length}`, 'info')
|
|
||||||
|
|
||||||
const account1GroupNames = account1Groups.map((g) => g.name).sort()
|
|
||||||
const expectedAccount1Groups = [group1.name, group2.name].sort()
|
|
||||||
|
|
||||||
if (JSON.stringify(account1GroupNames) === JSON.stringify(expectedAccount1Groups)) {
|
|
||||||
log(`✅ 账户1分组关系正确: [${account1GroupNames.join(', ')}]`, 'success')
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`账户1分组关系错误,期望: [${expectedAccount1Groups.join(', ')}], 实际: [${account1GroupNames.join(', ')}]`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证账户2的分组关系
|
|
||||||
const account2Groups = await accountGroupService.getAccountGroup(account2.id)
|
|
||||||
log(`📊 账户2所属分组数量: ${account2Groups.length}`, 'info')
|
|
||||||
|
|
||||||
const account2GroupNames = account2Groups.map((g) => g.name).sort()
|
|
||||||
const expectedAccount2Groups = [group2.name, group3.name].sort()
|
|
||||||
|
|
||||||
if (JSON.stringify(account2GroupNames) === JSON.stringify(expectedAccount2Groups)) {
|
|
||||||
log(`✅ 账户2分组关系正确: [${account2GroupNames.join(', ')}]`, 'success')
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`账户2分组关系错误,期望: [${expectedAccount2Groups.join(', ')}], 实际: [${account2GroupNames.join(', ')}]`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
log(`✅ 多分组关系验证通过`, 'success')
|
|
||||||
} catch (error) {
|
|
||||||
log(`❌ 测试3失败: ${error.message}`, 'error')
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试4: 验证分组成员关系
|
|
||||||
async function test4_verifyGroupMemberships() {
|
|
||||||
log('\n📝 测试4: 验证分组成员关系', 'info')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [group1, group2, group3] = testData.groups
|
|
||||||
const [account1, account2] = testData.accounts
|
|
||||||
|
|
||||||
// 验证分组1的成员
|
|
||||||
const group1Members = await accountGroupService.getGroupMembers(group1.id)
|
|
||||||
if (group1Members.includes(account1.id) && group1Members.length === 1) {
|
|
||||||
log(`✅ 分组1成员正确: [${account1.name}]`, 'success')
|
|
||||||
} else {
|
|
||||||
throw new Error(`分组1成员错误,期望: [${account1.id}], 实际: [${group1Members.join(', ')}]`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证分组2的成员(应该包含两个账户)
|
|
||||||
const group2Members = await accountGroupService.getGroupMembers(group2.id)
|
|
||||||
const expectedGroup2Members = [account1.id, account2.id].sort()
|
|
||||||
const actualGroup2Members = group2Members.sort()
|
|
||||||
|
|
||||||
if (JSON.stringify(actualGroup2Members) === JSON.stringify(expectedGroup2Members)) {
|
|
||||||
log(`✅ 分组2成员正确: [${account1.name}, ${account2.name}]`, 'success')
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`分组2成员错误,期望: [${expectedGroup2Members.join(', ')}], 实际: [${actualGroup2Members.join(', ')}]`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证分组3的成员
|
|
||||||
const group3Members = await accountGroupService.getGroupMembers(group3.id)
|
|
||||||
if (group3Members.includes(account2.id) && group3Members.length === 1) {
|
|
||||||
log(`✅ 分组3成员正确: [${account2.name}]`, 'success')
|
|
||||||
} else {
|
|
||||||
throw new Error(`分组3成员错误,期望: [${account2.id}], 实际: [${group3Members.join(', ')}]`)
|
|
||||||
}
|
|
||||||
|
|
||||||
log(`✅ 分组成员关系验证通过`, 'success')
|
|
||||||
} catch (error) {
|
|
||||||
log(`❌ 测试4失败: ${error.message}`, 'error')
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试5: 从部分分组中移除账户
|
|
||||||
async function test5_removeFromPartialGroups() {
|
|
||||||
log('\n📝 测试5: 从部分分组中移除账户', 'info')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [group1, group2] = testData.groups
|
|
||||||
const [account1] = testData.accounts
|
|
||||||
|
|
||||||
// 将账户1从分组1中移除(但仍在分组2中)
|
|
||||||
await accountGroupService.removeAccountFromGroup(account1.id, group1.id)
|
|
||||||
log(`✅ 从分组1中移除账户1`, 'success')
|
|
||||||
|
|
||||||
// 验证账户1现在只属于分组2
|
|
||||||
const account1Groups = await accountGroupService.getAccountGroup(account1.id)
|
|
||||||
if (account1Groups.length === 1 && account1Groups[0].id === group2.id) {
|
|
||||||
log(`✅ 账户1现在只属于分组2: ${account1Groups[0].name}`, 'success')
|
|
||||||
} else {
|
|
||||||
const groupNames = account1Groups.map((g) => g.name)
|
|
||||||
throw new Error(`账户1分组状态错误,期望只在分组2中,实际: [${groupNames.join(', ')}]`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证分组1现在为空
|
|
||||||
const group1Members = await accountGroupService.getGroupMembers(group1.id)
|
|
||||||
if (group1Members.length === 0) {
|
|
||||||
log(`✅ 分组1现在为空`, 'success')
|
|
||||||
} else {
|
|
||||||
throw new Error(`分组1应该为空,但还有成员: [${group1Members.join(', ')}]`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证分组2仍有两个成员
|
|
||||||
const group2Members = await accountGroupService.getGroupMembers(group2.id)
|
|
||||||
if (group2Members.length === 2) {
|
|
||||||
log(`✅ 分组2仍有两个成员`, 'success')
|
|
||||||
} else {
|
|
||||||
throw new Error(`分组2应该有2个成员,实际: ${group2Members.length}个`)
|
|
||||||
}
|
|
||||||
|
|
||||||
log(`✅ 部分移除测试通过`, 'success')
|
|
||||||
} catch (error) {
|
|
||||||
log(`❌ 测试5失败: ${error.message}`, 'error')
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试6: 账户完全移除时的分组清理
|
|
||||||
async function test6_accountDeletionGroupCleanup() {
|
|
||||||
log('\n📝 测试6: 账户删除时的分组清理', 'info')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [, group2, group3] = testData.groups // 跳过第一个元素
|
|
||||||
const [account1, account2] = testData.accounts
|
|
||||||
|
|
||||||
// 记录删除前的状态
|
|
||||||
const beforeGroup2Members = await accountGroupService.getGroupMembers(group2.id)
|
|
||||||
const beforeGroup3Members = await accountGroupService.getGroupMembers(group3.id)
|
|
||||||
|
|
||||||
log(`📊 删除前分组2成员数: ${beforeGroup2Members.length}`, 'info')
|
|
||||||
log(`📊 删除前分组3成员数: ${beforeGroup3Members.length}`, 'info')
|
|
||||||
|
|
||||||
// 删除账户2(这应该会触发从所有分组中移除的逻辑)
|
|
||||||
await claudeAccountService.deleteAccount(account2.id)
|
|
||||||
log(`✅ 删除账户2: ${account2.name}`, 'success')
|
|
||||||
|
|
||||||
// 从测试数据中移除,避免cleanup时重复删除
|
|
||||||
testData.accounts = testData.accounts.filter((acc) => acc.id !== account2.id)
|
|
||||||
|
|
||||||
// 等待一下确保删除操作完成
|
|
||||||
await sleep(500)
|
|
||||||
|
|
||||||
// 验证分组2现在只有账户1
|
|
||||||
const afterGroup2Members = await accountGroupService.getGroupMembers(group2.id)
|
|
||||||
if (afterGroup2Members.length === 1 && afterGroup2Members[0] === account1.id) {
|
|
||||||
log(`✅ 分组2现在只有账户1`, 'success')
|
|
||||||
} else {
|
|
||||||
throw new Error(`分组2成员状态错误,期望只有账户1,实际: [${afterGroup2Members.join(', ')}]`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证分组3现在为空
|
|
||||||
const afterGroup3Members = await accountGroupService.getGroupMembers(group3.id)
|
|
||||||
if (afterGroup3Members.length === 0) {
|
|
||||||
log(`✅ 分组3现在为空`, 'success')
|
|
||||||
} else {
|
|
||||||
throw new Error(`分组3应该为空,但还有成员: [${afterGroup3Members.join(', ')}]`)
|
|
||||||
}
|
|
||||||
|
|
||||||
log(`✅ 账户删除的分组清理测试通过`, 'success')
|
|
||||||
} catch (error) {
|
|
||||||
log(`❌ 测试6失败: ${error.message}`, 'error')
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 主测试函数
|
|
||||||
async function runTests() {
|
|
||||||
log('\n🚀 开始多分组功能测试\n', 'info')
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 连接Redis
|
|
||||||
await redis.connect()
|
|
||||||
log('✅ Redis连接成功', 'success')
|
|
||||||
|
|
||||||
// 执行测试
|
|
||||||
await test1_createTestData()
|
|
||||||
await test2_addAccountToMultipleGroups()
|
|
||||||
await test3_verifyMultiGroupRelationships()
|
|
||||||
await test4_verifyGroupMemberships()
|
|
||||||
await test5_removeFromPartialGroups()
|
|
||||||
await test6_accountDeletionGroupCleanup()
|
|
||||||
|
|
||||||
log('\n🎉 所有测试通过!多分组功能工作正常', 'success')
|
|
||||||
} catch (error) {
|
|
||||||
log(`\n❌ 测试失败: ${error.message}`, 'error')
|
|
||||||
console.error(error)
|
|
||||||
} finally {
|
|
||||||
// 清理测试数据
|
|
||||||
if (CLEANUP_ON_FINISH) {
|
|
||||||
await cleanup()
|
|
||||||
} else {
|
|
||||||
log('\n⚠️ 测试数据未清理,请手动清理', 'warning')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭Redis连接
|
|
||||||
await redis.disconnect()
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 运行测试
|
|
||||||
runTests()
|
|
||||||
@@ -1183,7 +1183,34 @@ router.get('/account-groups/:groupId/members', authenticateAdmin, async (req, re
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (account) {
|
if (account) {
|
||||||
members.push(account)
|
try {
|
||||||
|
// 添加使用统计信息
|
||||||
|
const usageStats = await redis.getAccountUsageStats(account.id)
|
||||||
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
|
|
||||||
|
members.push({
|
||||||
|
...account,
|
||||||
|
groupInfos,
|
||||||
|
usage: {
|
||||||
|
daily: usageStats.daily,
|
||||||
|
total: usageStats.total,
|
||||||
|
averages: usageStats.averages
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (statsError) {
|
||||||
|
logger.warn(`⚠️ Failed to get usage stats for account ${account.id}:`, statsError.message)
|
||||||
|
// 如果获取统计失败,返回空统计
|
||||||
|
const groupInfos = await accountGroupService.getAccountGroups(account.id).catch(() => [])
|
||||||
|
members.push({
|
||||||
|
...account,
|
||||||
|
groupInfos,
|
||||||
|
usage: {
|
||||||
|
daily: { tokens: 0, requests: 0 },
|
||||||
|
total: { tokens: 0, requests: 0 },
|
||||||
|
averages: { tokensPerRequest: 0 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1449,15 +1476,18 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
if (groupId && groupId !== 'all') {
|
if (groupId && groupId !== 'all') {
|
||||||
if (groupId === 'ungrouped') {
|
if (groupId === 'ungrouped') {
|
||||||
// 筛选未分组账户
|
// 筛选未分组账户
|
||||||
accounts = accounts.filter(
|
const filteredAccounts = []
|
||||||
(account) => !account.groupInfos || account.groupInfos.length === 0
|
for (const account of accounts) {
|
||||||
)
|
const groups = await accountGroupService.getAccountGroups(account.id)
|
||||||
|
if (!groups || groups.length === 0) {
|
||||||
|
filteredAccounts.push(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accounts = filteredAccounts
|
||||||
} else {
|
} else {
|
||||||
// 筛选特定分组的账户
|
// 筛选特定分组的账户
|
||||||
accounts = accounts.filter(
|
const groupMembers = await accountGroupService.getGroupMembers(groupId)
|
||||||
(account) =>
|
accounts = accounts.filter((account) => groupMembers.includes(account.id))
|
||||||
account.groupInfos && account.groupInfos.some((group) => group.id === groupId)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1466,7 +1496,7 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
accounts.map(async (account) => {
|
accounts.map(async (account) => {
|
||||||
try {
|
try {
|
||||||
const usageStats = await redis.getAccountUsageStats(account.id)
|
const usageStats = await redis.getAccountUsageStats(account.id)
|
||||||
const groupInfos = await accountGroupService.getAccountGroup(account.id)
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
@@ -1481,7 +1511,7 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
logger.warn(`⚠️ Failed to get usage stats for account ${account.id}:`, statsError.message)
|
logger.warn(`⚠️ Failed to get usage stats for account ${account.id}:`, statsError.message)
|
||||||
// 如果获取统计失败,返回空统计
|
// 如果获取统计失败,返回空统计
|
||||||
try {
|
try {
|
||||||
const groupInfos = await accountGroupService.getAccountGroup(account.id)
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
groupInfos,
|
groupInfos,
|
||||||
@@ -1531,7 +1561,8 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
accountType,
|
accountType,
|
||||||
platform = 'claude',
|
platform = 'claude',
|
||||||
priority,
|
priority,
|
||||||
groupId
|
groupId,
|
||||||
|
groupIds
|
||||||
} = req.body
|
} = req.body
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@@ -1545,9 +1576,11 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
.json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
|
.json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是分组类型,验证groupId
|
// 如果是分组类型,验证groupId或groupIds
|
||||||
if (accountType === 'group' && !groupId) {
|
if (accountType === 'group' && !groupId && (!groupIds || groupIds.length === 0)) {
|
||||||
return res.status(400).json({ error: 'Group ID is required for group type accounts' })
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: 'Group ID or Group IDs are required for group type accounts' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证priority的有效性
|
// 验证priority的有效性
|
||||||
@@ -1572,8 +1605,14 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 如果是分组类型,将账户添加到分组
|
// 如果是分组类型,将账户添加到分组
|
||||||
if (accountType === 'group' && groupId) {
|
if (accountType === 'group') {
|
||||||
await accountGroupService.addAccountToGroup(newAccount.id, groupId, newAccount.platform)
|
if (groupIds && groupIds.length > 0) {
|
||||||
|
// 使用多分组设置
|
||||||
|
await accountGroupService.setAccountGroups(newAccount.id, groupIds, newAccount.platform)
|
||||||
|
} else if (groupId) {
|
||||||
|
// 兼容单分组模式
|
||||||
|
await accountGroupService.addAccountToGroup(newAccount.id, groupId, newAccount.platform)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.success(`🏢 Admin created new Claude account: ${name} (${accountType || 'shared'})`)
|
logger.success(`🏢 Admin created new Claude account: ${name} (${accountType || 'shared'})`)
|
||||||
@@ -1607,9 +1646,15 @@ router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) =>
|
|||||||
.json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
|
.json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果更新为分组类型,验证groupId
|
// 如果更新为分组类型,验证groupId或groupIds
|
||||||
if (updates.accountType === 'group' && !updates.groupId) {
|
if (
|
||||||
return res.status(400).json({ error: 'Group ID is required for group type accounts' })
|
updates.accountType === 'group' &&
|
||||||
|
!updates.groupId &&
|
||||||
|
(!updates.groupIds || updates.groupIds.length === 0)
|
||||||
|
) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: 'Group ID or Group IDs are required for group type accounts' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取账户当前信息以处理分组变更
|
// 获取账户当前信息以处理分组变更
|
||||||
@@ -1622,16 +1667,24 @@ router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) =>
|
|||||||
if (updates.accountType !== undefined) {
|
if (updates.accountType !== undefined) {
|
||||||
// 如果之前是分组类型,需要从所有分组中移除
|
// 如果之前是分组类型,需要从所有分组中移除
|
||||||
if (currentAccount.accountType === 'group') {
|
if (currentAccount.accountType === 'group') {
|
||||||
const oldGroups = await accountGroupService.getAccountGroup(accountId)
|
await accountGroupService.removeAccountFromAllGroups(accountId)
|
||||||
for (const oldGroup of oldGroups) {
|
|
||||||
await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果新类型是分组,添加到新分组
|
// 如果新类型是分组,添加到新分组
|
||||||
if (updates.accountType === 'group' && updates.groupId) {
|
if (updates.accountType === 'group') {
|
||||||
// 从路由知道这是 Claude OAuth 账户,平台为 'claude'
|
// 处理多分组/单分组的兼容性
|
||||||
await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'claude')
|
if (Object.prototype.hasOwnProperty.call(updates, 'groupIds')) {
|
||||||
|
if (updates.groupIds && updates.groupIds.length > 0) {
|
||||||
|
// 使用多分组设置
|
||||||
|
await accountGroupService.setAccountGroups(accountId, updates.groupIds, 'claude')
|
||||||
|
} else {
|
||||||
|
// groupIds 为空数组,从所有分组中移除
|
||||||
|
await accountGroupService.removeAccountFromAllGroups(accountId)
|
||||||
|
}
|
||||||
|
} else if (updates.groupId) {
|
||||||
|
// 兼容单分组模式
|
||||||
|
await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'claude')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1805,15 +1858,18 @@ router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
if (groupId && groupId !== 'all') {
|
if (groupId && groupId !== 'all') {
|
||||||
if (groupId === 'ungrouped') {
|
if (groupId === 'ungrouped') {
|
||||||
// 筛选未分组账户
|
// 筛选未分组账户
|
||||||
accounts = accounts.filter(
|
const filteredAccounts = []
|
||||||
(account) => !account.groupInfos || account.groupInfos.length === 0
|
for (const account of accounts) {
|
||||||
)
|
const groups = await accountGroupService.getAccountGroups(account.id)
|
||||||
|
if (!groups || groups.length === 0) {
|
||||||
|
filteredAccounts.push(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accounts = filteredAccounts
|
||||||
} else {
|
} else {
|
||||||
// 筛选特定分组的账户
|
// 筛选特定分组的账户
|
||||||
accounts = accounts.filter(
|
const groupMembers = await accountGroupService.getGroupMembers(groupId)
|
||||||
(account) =>
|
accounts = accounts.filter((account) => groupMembers.includes(account.id))
|
||||||
account.groupInfos && account.groupInfos.some((group) => group.id === groupId)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1822,7 +1878,7 @@ router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
accounts.map(async (account) => {
|
accounts.map(async (account) => {
|
||||||
try {
|
try {
|
||||||
const usageStats = await redis.getAccountUsageStats(account.id)
|
const usageStats = await redis.getAccountUsageStats(account.id)
|
||||||
const groupInfos = await accountGroupService.getAccountGroup(account.id)
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
@@ -1839,7 +1895,7 @@ router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
statsError.message
|
statsError.message
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
const groupInfos = await accountGroupService.getAccountGroup(account.id)
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
groupInfos,
|
groupInfos,
|
||||||
@@ -1931,7 +1987,7 @@ router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
|
|
||||||
// 如果是分组类型,将账户添加到分组
|
// 如果是分组类型,将账户添加到分组
|
||||||
if (accountType === 'group' && groupId) {
|
if (accountType === 'group' && groupId) {
|
||||||
await accountGroupService.addAccountToGroup(newAccount.id, groupId, 'claude')
|
await accountGroupService.addAccountToGroup(newAccount.id, groupId)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.success(`🎮 Admin created Claude Console account: ${name}`)
|
logger.success(`🎮 Admin created Claude Console account: ${name}`)
|
||||||
@@ -1977,15 +2033,26 @@ router.put('/claude-console-accounts/:accountId', authenticateAdmin, async (req,
|
|||||||
if (updates.accountType !== undefined) {
|
if (updates.accountType !== undefined) {
|
||||||
// 如果之前是分组类型,需要从所有分组中移除
|
// 如果之前是分组类型,需要从所有分组中移除
|
||||||
if (currentAccount.accountType === 'group') {
|
if (currentAccount.accountType === 'group') {
|
||||||
const oldGroups = await accountGroupService.getAccountGroup(accountId)
|
const oldGroups = await accountGroupService.getAccountGroups(accountId)
|
||||||
for (const oldGroup of oldGroups) {
|
for (const oldGroup of oldGroups) {
|
||||||
await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id)
|
await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果新类型是分组,添加到新分组
|
// 如果新类型是分组,处理多分组支持
|
||||||
if (updates.accountType === 'group' && updates.groupId) {
|
if (updates.accountType === 'group') {
|
||||||
// Claude Console 账户在分组中被视为 'claude' 平台
|
if (Object.prototype.hasOwnProperty.call(updates, 'groupIds')) {
|
||||||
await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'claude')
|
// 如果明确提供了 groupIds 参数(包括空数组)
|
||||||
|
if (updates.groupIds && updates.groupIds.length > 0) {
|
||||||
|
// 设置新的多分组
|
||||||
|
await accountGroupService.setAccountGroups(accountId, updates.groupIds, 'claude')
|
||||||
|
} else {
|
||||||
|
// groupIds 为空数组,从所有分组中移除
|
||||||
|
await accountGroupService.removeAccountFromAllGroups(accountId)
|
||||||
|
}
|
||||||
|
} else if (updates.groupId) {
|
||||||
|
// 向后兼容:仅当没有 groupIds 但有 groupId 时使用单分组逻辑
|
||||||
|
await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'claude')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2119,15 +2186,18 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
if (groupId && groupId !== 'all') {
|
if (groupId && groupId !== 'all') {
|
||||||
if (groupId === 'ungrouped') {
|
if (groupId === 'ungrouped') {
|
||||||
// 筛选未分组账户
|
// 筛选未分组账户
|
||||||
accounts = accounts.filter(
|
const filteredAccounts = []
|
||||||
(account) => !account.groupInfos || account.groupInfos.length === 0
|
for (const account of accounts) {
|
||||||
)
|
const groups = await accountGroupService.getAccountGroups(account.id)
|
||||||
|
if (!groups || groups.length === 0) {
|
||||||
|
filteredAccounts.push(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accounts = filteredAccounts
|
||||||
} else {
|
} else {
|
||||||
// 筛选特定分组的账户
|
// 筛选特定分组的账户
|
||||||
accounts = accounts.filter(
|
const groupMembers = await accountGroupService.getGroupMembers(groupId)
|
||||||
(account) =>
|
accounts = accounts.filter((account) => groupMembers.includes(account.id))
|
||||||
account.groupInfos && account.groupInfos.some((group) => group.id === groupId)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2136,7 +2206,7 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
accounts.map(async (account) => {
|
accounts.map(async (account) => {
|
||||||
try {
|
try {
|
||||||
const usageStats = await redis.getAccountUsageStats(account.id)
|
const usageStats = await redis.getAccountUsageStats(account.id)
|
||||||
const groupInfos = await accountGroupService.getAccountGroup(account.id)
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
@@ -2153,7 +2223,7 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
statsError.message
|
statsError.message
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
const groupInfos = await accountGroupService.getAccountGroup(account.id)
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
groupInfos,
|
groupInfos,
|
||||||
@@ -2566,15 +2636,18 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
if (groupId && groupId !== 'all') {
|
if (groupId && groupId !== 'all') {
|
||||||
if (groupId === 'ungrouped') {
|
if (groupId === 'ungrouped') {
|
||||||
// 筛选未分组账户
|
// 筛选未分组账户
|
||||||
accounts = accounts.filter(
|
const filteredAccounts = []
|
||||||
(account) => !account.groupInfos || account.groupInfos.length === 0
|
for (const account of accounts) {
|
||||||
)
|
const groups = await accountGroupService.getAccountGroups(account.id)
|
||||||
|
if (!groups || groups.length === 0) {
|
||||||
|
filteredAccounts.push(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accounts = filteredAccounts
|
||||||
} else {
|
} else {
|
||||||
// 筛选特定分组的账户
|
// 筛选特定分组的账户
|
||||||
accounts = accounts.filter(
|
const groupMembers = await accountGroupService.getGroupMembers(groupId)
|
||||||
(account) =>
|
accounts = accounts.filter((account) => groupMembers.includes(account.id))
|
||||||
account.groupInfos && account.groupInfos.some((group) => group.id === groupId)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2583,7 +2656,7 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
accounts.map(async (account) => {
|
accounts.map(async (account) => {
|
||||||
try {
|
try {
|
||||||
const usageStats = await redis.getAccountUsageStats(account.id)
|
const usageStats = await redis.getAccountUsageStats(account.id)
|
||||||
const groupInfos = await accountGroupService.getAccountGroup(account.id)
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
@@ -2601,7 +2674,7 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
)
|
)
|
||||||
// 如果获取统计失败,返回空统计
|
// 如果获取统计失败,返回空统计
|
||||||
try {
|
try {
|
||||||
const groupInfos = await accountGroupService.getAccountGroup(account.id)
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
groupInfos,
|
groupInfos,
|
||||||
@@ -2705,14 +2778,26 @@ router.put('/gemini-accounts/:accountId', authenticateAdmin, async (req, res) =>
|
|||||||
if (updates.accountType !== undefined) {
|
if (updates.accountType !== undefined) {
|
||||||
// 如果之前是分组类型,需要从所有分组中移除
|
// 如果之前是分组类型,需要从所有分组中移除
|
||||||
if (currentAccount.accountType === 'group') {
|
if (currentAccount.accountType === 'group') {
|
||||||
const oldGroups = await accountGroupService.getAccountGroup(accountId)
|
const oldGroups = await accountGroupService.getAccountGroups(accountId)
|
||||||
for (const oldGroup of oldGroups) {
|
for (const oldGroup of oldGroups) {
|
||||||
await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id)
|
await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果新类型是分组,添加到新分组
|
// 如果新类型是分组,处理多分组支持
|
||||||
if (updates.accountType === 'group' && updates.groupId) {
|
if (updates.accountType === 'group') {
|
||||||
await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'gemini')
|
if (Object.prototype.hasOwnProperty.call(updates, 'groupIds')) {
|
||||||
|
// 如果明确提供了 groupIds 参数(包括空数组)
|
||||||
|
if (updates.groupIds && updates.groupIds.length > 0) {
|
||||||
|
// 设置新的多分组
|
||||||
|
await accountGroupService.setAccountGroups(accountId, updates.groupIds, 'gemini')
|
||||||
|
} else {
|
||||||
|
// groupIds 为空数组,从所有分组中移除
|
||||||
|
await accountGroupService.removeAccountFromAllGroups(accountId)
|
||||||
|
}
|
||||||
|
} else if (updates.groupId) {
|
||||||
|
// 向后兼容:仅当没有 groupIds 但有 groupId 时使用单分组逻辑
|
||||||
|
await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'gemini')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5073,25 +5158,30 @@ router.get('/openai-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
if (groupId && groupId !== 'all') {
|
if (groupId && groupId !== 'all') {
|
||||||
if (groupId === 'ungrouped') {
|
if (groupId === 'ungrouped') {
|
||||||
// 筛选未分组账户
|
// 筛选未分组账户
|
||||||
accounts = accounts.filter(
|
const filteredAccounts = []
|
||||||
(account) => !account.groupInfos || account.groupInfos.length === 0
|
for (const account of accounts) {
|
||||||
)
|
const groups = await accountGroupService.getAccountGroups(account.id)
|
||||||
|
if (!groups || groups.length === 0) {
|
||||||
|
filteredAccounts.push(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accounts = filteredAccounts
|
||||||
} else {
|
} else {
|
||||||
// 筛选特定分组的账户
|
// 筛选特定分组的账户
|
||||||
accounts = accounts.filter(
|
const groupMembers = await accountGroupService.getGroupMembers(groupId)
|
||||||
(account) =>
|
accounts = accounts.filter((account) => groupMembers.includes(account.id))
|
||||||
account.groupInfos && account.groupInfos.some((group) => group.id === groupId)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 为每个账户添加使用统计信息
|
// 为每个账户添加使用统计信息和分组信息
|
||||||
const accountsWithStats = await Promise.all(
|
const accountsWithStats = await Promise.all(
|
||||||
accounts.map(async (account) => {
|
accounts.map(async (account) => {
|
||||||
try {
|
try {
|
||||||
const usageStats = await redis.getAccountUsageStats(account.id)
|
const usageStats = await redis.getAccountUsageStats(account.id)
|
||||||
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
|
groupInfos,
|
||||||
usage: {
|
usage: {
|
||||||
daily: usageStats.daily,
|
daily: usageStats.daily,
|
||||||
total: usageStats.total,
|
total: usageStats.total,
|
||||||
@@ -5100,12 +5190,27 @@ router.get('/openai-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug(`Failed to get usage stats for OpenAI account ${account.id}:`, error)
|
logger.debug(`Failed to get usage stats for OpenAI account ${account.id}:`, error)
|
||||||
return {
|
try {
|
||||||
...account,
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
usage: {
|
return {
|
||||||
daily: { requests: 0, tokens: 0, allTokens: 0 },
|
...account,
|
||||||
total: { requests: 0, tokens: 0, allTokens: 0 },
|
groupInfos,
|
||||||
monthly: { requests: 0, tokens: 0, allTokens: 0 }
|
usage: {
|
||||||
|
daily: { requests: 0, tokens: 0, allTokens: 0 },
|
||||||
|
total: { requests: 0, tokens: 0, allTokens: 0 },
|
||||||
|
averages: { rpm: 0, tpm: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (groupError) {
|
||||||
|
logger.debug(`Failed to get group info for account ${account.id}:`, groupError)
|
||||||
|
return {
|
||||||
|
...account,
|
||||||
|
groupInfos: [],
|
||||||
|
usage: {
|
||||||
|
daily: { requests: 0, tokens: 0, allTokens: 0 },
|
||||||
|
total: { requests: 0, tokens: 0, allTokens: 0 },
|
||||||
|
averages: { rpm: 0, tpm: 0 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5402,10 +5507,81 @@ router.put(
|
|||||||
// 获取所有 Azure OpenAI 账户
|
// 获取所有 Azure OpenAI 账户
|
||||||
router.get('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
|
router.get('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const accounts = await azureOpenaiAccountService.getAllAccounts()
|
const { platform, groupId } = req.query
|
||||||
|
let accounts = await azureOpenaiAccountService.getAllAccounts()
|
||||||
|
|
||||||
|
// 根据查询参数进行筛选
|
||||||
|
if (platform && platform !== 'all' && platform !== 'azure_openai') {
|
||||||
|
// 如果指定了其他平台,返回空数组
|
||||||
|
accounts = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果指定了分组筛选
|
||||||
|
if (groupId && groupId !== 'all') {
|
||||||
|
if (groupId === 'ungrouped') {
|
||||||
|
// 筛选未分组账户
|
||||||
|
const filteredAccounts = []
|
||||||
|
for (const account of accounts) {
|
||||||
|
const groups = await accountGroupService.getAccountGroups(account.id)
|
||||||
|
if (!groups || groups.length === 0) {
|
||||||
|
filteredAccounts.push(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accounts = filteredAccounts
|
||||||
|
} else {
|
||||||
|
// 筛选特定分组的账户
|
||||||
|
const groupMembers = await accountGroupService.getGroupMembers(groupId)
|
||||||
|
accounts = accounts.filter((account) => groupMembers.includes(account.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为每个账户添加使用统计信息和分组信息
|
||||||
|
const accountsWithStats = await Promise.all(
|
||||||
|
accounts.map(async (account) => {
|
||||||
|
try {
|
||||||
|
const usageStats = await redis.getAccountUsageStats(account.id)
|
||||||
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
|
return {
|
||||||
|
...account,
|
||||||
|
groupInfos,
|
||||||
|
usage: {
|
||||||
|
daily: usageStats.daily,
|
||||||
|
total: usageStats.total,
|
||||||
|
averages: usageStats.averages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug(`Failed to get usage stats for Azure OpenAI account ${account.id}:`, error)
|
||||||
|
try {
|
||||||
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
|
return {
|
||||||
|
...account,
|
||||||
|
groupInfos,
|
||||||
|
usage: {
|
||||||
|
daily: { requests: 0, tokens: 0, allTokens: 0 },
|
||||||
|
total: { requests: 0, tokens: 0, allTokens: 0 },
|
||||||
|
averages: { rpm: 0, tpm: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (groupError) {
|
||||||
|
logger.debug(`Failed to get group info for account ${account.id}:`, groupError)
|
||||||
|
return {
|
||||||
|
...account,
|
||||||
|
groupInfos: [],
|
||||||
|
usage: {
|
||||||
|
daily: { requests: 0, tokens: 0, allTokens: 0 },
|
||||||
|
total: { requests: 0, tokens: 0, allTokens: 0 },
|
||||||
|
averages: { rpm: 0, tpm: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: accounts
|
data: accountsWithStats
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to fetch Azure OpenAI accounts:', error)
|
logger.error('Failed to fetch Azure OpenAI accounts:', error)
|
||||||
@@ -5431,6 +5607,7 @@ router.post('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
supportedModels,
|
supportedModels,
|
||||||
proxy,
|
proxy,
|
||||||
groupId,
|
groupId,
|
||||||
|
groupIds,
|
||||||
priority,
|
priority,
|
||||||
isActive,
|
isActive,
|
||||||
schedulable
|
schedulable
|
||||||
@@ -5510,6 +5687,17 @@ router.post('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
schedulable: schedulable !== false
|
schedulable: schedulable !== false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 如果是分组类型,将账户添加到分组
|
||||||
|
if (accountType === 'group') {
|
||||||
|
if (groupIds && groupIds.length > 0) {
|
||||||
|
// 使用多分组设置
|
||||||
|
await accountGroupService.setAccountGroups(account.id, groupIds, 'azure_openai')
|
||||||
|
} else if (groupId) {
|
||||||
|
// 兼容单分组模式
|
||||||
|
await accountGroupService.addAccountToGroup(account.id, groupId, 'azure_openai')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: account,
|
data: account,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class AccountGroupService {
|
|||||||
* 创建账户分组
|
* 创建账户分组
|
||||||
* @param {Object} groupData - 分组数据
|
* @param {Object} groupData - 分组数据
|
||||||
* @param {string} groupData.name - 分组名称
|
* @param {string} groupData.name - 分组名称
|
||||||
* @param {string} groupData.platform - 平台类型 (claude/gemini)
|
* @param {string} groupData.platform - 平台类型 (claude/gemini/openai)
|
||||||
* @param {string} groupData.description - 分组描述
|
* @param {string} groupData.description - 分组描述
|
||||||
* @returns {Object} 创建的分组
|
* @returns {Object} 创建的分组
|
||||||
*/
|
*/
|
||||||
@@ -327,12 +327,36 @@ class AccountGroupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据账户ID获取其所属的分组(兼容性方法,返回单个分组)
|
||||||
|
* @param {string} accountId - 账户ID
|
||||||
|
* @returns {Object|null} 分组信息
|
||||||
|
*/
|
||||||
|
async getAccountGroup(accountId) {
|
||||||
|
try {
|
||||||
|
const client = redis.getClientSafe()
|
||||||
|
const allGroupIds = await client.smembers(this.GROUPS_KEY)
|
||||||
|
|
||||||
|
for (const groupId of allGroupIds) {
|
||||||
|
const isMember = await client.sismember(`${this.GROUP_MEMBERS_PREFIX}${groupId}`, accountId)
|
||||||
|
if (isMember) {
|
||||||
|
return await this.getGroup(groupId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ 获取账户所属分组失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据账户ID获取其所属的所有分组
|
* 根据账户ID获取其所属的所有分组
|
||||||
* @param {string} accountId - 账户ID
|
* @param {string} accountId - 账户ID
|
||||||
* @returns {Array} 分组信息数组
|
* @returns {Array} 分组信息数组
|
||||||
*/
|
*/
|
||||||
async getAccountGroup(accountId) {
|
async getAccountGroups(accountId) {
|
||||||
try {
|
try {
|
||||||
const client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
const allGroupIds = await client.smembers(this.GROUPS_KEY)
|
const allGroupIds = await client.smembers(this.GROUPS_KEY)
|
||||||
@@ -357,6 +381,49 @@ class AccountGroupService {
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量设置账户的分组
|
||||||
|
* @param {string} accountId - 账户ID
|
||||||
|
* @param {Array} groupIds - 分组ID数组
|
||||||
|
* @param {string} accountPlatform - 账户平台
|
||||||
|
*/
|
||||||
|
async setAccountGroups(accountId, groupIds, accountPlatform) {
|
||||||
|
try {
|
||||||
|
// 首先移除账户的所有现有分组
|
||||||
|
await this.removeAccountFromAllGroups(accountId)
|
||||||
|
|
||||||
|
// 然后添加到新的分组中
|
||||||
|
for (const groupId of groupIds) {
|
||||||
|
await this.addAccountToGroup(accountId, groupId, accountPlatform)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.success(`✅ 批量设置账户分组成功: ${accountId} -> [${groupIds.join(', ')}]`)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ 批量设置账户分组失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从所有分组中移除账户
|
||||||
|
* @param {string} accountId - 账户ID
|
||||||
|
*/
|
||||||
|
async removeAccountFromAllGroups(accountId) {
|
||||||
|
try {
|
||||||
|
const client = redis.getClientSafe()
|
||||||
|
const allGroupIds = await client.smembers(this.GROUPS_KEY)
|
||||||
|
|
||||||
|
for (const groupId of allGroupIds) {
|
||||||
|
await client.srem(`${this.GROUP_MEMBERS_PREFIX}${groupId}`, accountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.success(`✅ 从所有分组移除账户成功: ${accountId}`)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ 从所有分组移除账户失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new AccountGroupService()
|
module.exports = new AccountGroupService()
|
||||||
|
|||||||
@@ -249,6 +249,10 @@ async function updateAccount(accountId, updates) {
|
|||||||
|
|
||||||
// 删除账户
|
// 删除账户
|
||||||
async function deleteAccount(accountId) {
|
async function deleteAccount(accountId) {
|
||||||
|
// 首先从所有分组中移除此账户
|
||||||
|
const accountGroupService = require('./accountGroupService')
|
||||||
|
await accountGroupService.removeAccountFromAllGroups(accountId)
|
||||||
|
|
||||||
const client = redisClient.getClientSafe()
|
const client = redisClient.getClientSafe()
|
||||||
const accountKey = `${AZURE_OPENAI_ACCOUNT_KEY_PREFIX}${accountId}`
|
const accountKey = `${AZURE_OPENAI_ACCOUNT_KEY_PREFIX}${accountId}`
|
||||||
|
|
||||||
|
|||||||
@@ -611,10 +611,7 @@ class ClaudeAccountService {
|
|||||||
try {
|
try {
|
||||||
// 首先从所有分组中移除此账户
|
// 首先从所有分组中移除此账户
|
||||||
const accountGroupService = require('./accountGroupService')
|
const accountGroupService = require('./accountGroupService')
|
||||||
const groups = await accountGroupService.getAccountGroup(accountId)
|
await accountGroupService.removeAccountFromAllGroups(accountId)
|
||||||
for (const group of groups) {
|
|
||||||
await accountGroupService.removeAccountFromGroup(accountId, group.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await redis.deleteClaudeAccount(accountId)
|
const result = await redis.deleteClaudeAccount(accountId)
|
||||||
|
|
||||||
|
|||||||
@@ -585,6 +585,10 @@ async function deleteAccount(accountId) {
|
|||||||
throw new Error('Account not found')
|
throw new Error('Account not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 首先从所有分组中移除此账户
|
||||||
|
const accountGroupService = require('./accountGroupService')
|
||||||
|
await accountGroupService.removeAccountFromAllGroups(accountId)
|
||||||
|
|
||||||
// 从 Redis 删除
|
// 从 Redis 删除
|
||||||
const client = redisClient.getClientSafe()
|
const client = redisClient.getClientSafe()
|
||||||
await client.del(`${GEMINI_ACCOUNT_KEY_PREFIX}${accountId}`)
|
await client.del(`${GEMINI_ACCOUNT_KEY_PREFIX}${accountId}`)
|
||||||
|
|||||||
@@ -440,6 +440,10 @@ async function deleteAccount(accountId) {
|
|||||||
throw new Error('Account not found')
|
throw new Error('Account not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 首先从所有分组中移除此账户
|
||||||
|
const accountGroupService = require('./accountGroupService')
|
||||||
|
await accountGroupService.removeAccountFromAllGroups(accountId)
|
||||||
|
|
||||||
// 从 Redis 删除
|
// 从 Redis 删除
|
||||||
const client = redisClient.getClientSafe()
|
const client = redisClient.getClientSafe()
|
||||||
await client.del(`${OPENAI_ACCOUNT_KEY_PREFIX}${accountId}`)
|
await client.del(`${OPENAI_ACCOUNT_KEY_PREFIX}${accountId}`)
|
||||||
|
|||||||
@@ -244,19 +244,47 @@
|
|||||||
>选择分组 *</label
|
>选择分组 *</label
|
||||||
>
|
>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<select
|
<div class="flex-1">
|
||||||
v-model="form.groupId"
|
<!-- 多选分组界面 -->
|
||||||
class="form-input flex-1 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
<div
|
||||||
required
|
class="max-h-48 space-y-2 overflow-y-auto rounded-md border p-3 dark:border-gray-600 dark:bg-gray-700"
|
||||||
>
|
>
|
||||||
<option value="">请选择分组</option>
|
<div
|
||||||
<option v-for="group in filteredGroups" :key="group.id" :value="group.id">
|
v-if="filteredGroups.length === 0"
|
||||||
{{ group.name }} ({{ group.memberCount || 0 }} 个成员)
|
class="text-sm text-gray-500 dark:text-gray-400"
|
||||||
</option>
|
>
|
||||||
<option value="__new__">+ 新建分组</option>
|
暂无可用分组
|
||||||
</select>
|
</div>
|
||||||
|
<label
|
||||||
|
v-for="group in filteredGroups"
|
||||||
|
:key="group.id"
|
||||||
|
class="flex cursor-pointer items-center gap-2 rounded-md p-2 hover:bg-gray-50 dark:hover:bg-gray-600"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="form.groupIds"
|
||||||
|
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
|
type="checkbox"
|
||||||
|
:value="group.id"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-200">
|
||||||
|
{{ group.name }} ({{ group.memberCount || 0 }} 个成员)
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<!-- 新建分组选项 -->
|
||||||
|
<div class="border-t pt-2 dark:border-gray-600">
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-2 text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
|
type="button"
|
||||||
|
@click="handleNewGroup"
|
||||||
|
>
|
||||||
|
<i class="fas fa-plus" />
|
||||||
|
新建分组
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
class="rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
class="rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
|
||||||
type="button"
|
type="button"
|
||||||
@click="refreshGroups"
|
@click="refreshGroups"
|
||||||
>
|
>
|
||||||
@@ -1240,19 +1268,47 @@
|
|||||||
>选择分组 *</label
|
>选择分组 *</label
|
||||||
>
|
>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<select
|
<div class="flex-1">
|
||||||
v-model="form.groupId"
|
<!-- 多选分组界面 -->
|
||||||
class="form-input flex-1 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
<div
|
||||||
required
|
class="max-h-48 space-y-2 overflow-y-auto rounded-md border p-3 dark:border-gray-600 dark:bg-gray-700"
|
||||||
>
|
>
|
||||||
<option value="">请选择分组</option>
|
<div
|
||||||
<option v-for="group in filteredGroups" :key="group.id" :value="group.id">
|
v-if="filteredGroups.length === 0"
|
||||||
{{ group.name }} ({{ group.memberCount || 0 }} 个成员)
|
class="text-sm text-gray-500 dark:text-gray-400"
|
||||||
</option>
|
>
|
||||||
<option value="__new__">+ 新建分组</option>
|
暂无可用分组
|
||||||
</select>
|
</div>
|
||||||
|
<label
|
||||||
|
v-for="group in filteredGroups"
|
||||||
|
:key="group.id"
|
||||||
|
class="flex cursor-pointer items-center gap-2 rounded-md p-2 hover:bg-gray-50 dark:hover:bg-gray-600"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="form.groupIds"
|
||||||
|
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
|
type="checkbox"
|
||||||
|
:value="group.id"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-200">
|
||||||
|
{{ group.name }} ({{ group.memberCount || 0 }} 个成员)
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<!-- 新建分组选项 -->
|
||||||
|
<div class="border-t pt-2 dark:border-gray-600">
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-2 text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
|
type="button"
|
||||||
|
@click="handleNewGroup"
|
||||||
|
>
|
||||||
|
<i class="fas fa-plus" />
|
||||||
|
新建分组
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
class="rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
class="rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
|
||||||
type="button"
|
type="button"
|
||||||
@click="refreshGroups"
|
@click="refreshGroups"
|
||||||
>
|
>
|
||||||
@@ -1771,6 +1827,7 @@ const form = ref({
|
|||||||
accountType: props.account?.accountType || 'shared',
|
accountType: props.account?.accountType || 'shared',
|
||||||
subscriptionType: 'claude_max', // 默认为 Claude Max,兼容旧数据
|
subscriptionType: 'claude_max', // 默认为 Claude Max,兼容旧数据
|
||||||
groupId: '',
|
groupId: '',
|
||||||
|
groupIds: [],
|
||||||
projectId: props.account?.projectId || '',
|
projectId: props.account?.projectId || '',
|
||||||
idToken: '',
|
idToken: '',
|
||||||
accessToken: '',
|
accessToken: '',
|
||||||
@@ -1872,15 +1929,24 @@ const nextStep = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分组类型验证
|
// 分组类型验证 - OAuth流程修复
|
||||||
if (
|
if (
|
||||||
form.value.accountType === 'group' &&
|
form.value.accountType === 'group' &&
|
||||||
(!form.value.groupId || form.value.groupId.trim() === '')
|
(!form.value.groupIds || form.value.groupIds.length === 0)
|
||||||
) {
|
) {
|
||||||
showToast('请选择一个分组', 'error')
|
showToast('请选择一个分组', 'error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 数据同步:确保 groupId 和 groupIds 保持一致 - OAuth流程
|
||||||
|
if (form.value.accountType === 'group') {
|
||||||
|
if (form.value.groupIds && form.value.groupIds.length > 0) {
|
||||||
|
form.value.groupId = form.value.groupIds[0]
|
||||||
|
} else {
|
||||||
|
form.value.groupId = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 对于Gemini账户,检查项目 ID
|
// 对于Gemini账户,检查项目 ID
|
||||||
if (form.value.platform === 'gemini' && oauthStep.value === 1 && form.value.addType === 'oauth') {
|
if (form.value.platform === 'gemini' && oauthStep.value === 1 && form.value.addType === 'oauth') {
|
||||||
if (!form.value.projectId || form.value.projectId.trim() === '') {
|
if (!form.value.projectId || form.value.projectId.trim() === '') {
|
||||||
@@ -2014,6 +2080,7 @@ const handleOAuthSuccess = async (tokenInfo) => {
|
|||||||
description: form.value.description,
|
description: form.value.description,
|
||||||
accountType: form.value.accountType,
|
accountType: form.value.accountType,
|
||||||
groupId: form.value.accountType === 'group' ? form.value.groupId : undefined,
|
groupId: form.value.accountType === 'group' ? form.value.groupId : undefined,
|
||||||
|
groupIds: form.value.accountType === 'group' ? form.value.groupIds : undefined,
|
||||||
proxy: form.value.proxy.enabled
|
proxy: form.value.proxy.enabled
|
||||||
? {
|
? {
|
||||||
type: form.value.proxy.type,
|
type: form.value.proxy.type,
|
||||||
@@ -2106,6 +2173,20 @@ const createAccount = async () => {
|
|||||||
errors.value.region = '请选择 AWS 区域'
|
errors.value.region = '请选择 AWS 区域'
|
||||||
hasError = true
|
hasError = true
|
||||||
}
|
}
|
||||||
|
} else if (form.value.platform === 'azure_openai') {
|
||||||
|
// Azure OpenAI 验证
|
||||||
|
if (!form.value.azureEndpoint || form.value.azureEndpoint.trim() === '') {
|
||||||
|
errors.value.azureEndpoint = 'Azure Endpoint 是必填项'
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
if (!form.value.deploymentName || form.value.deploymentName.trim() === '') {
|
||||||
|
errors.value.deploymentName = 'Deployment Name 是必填项'
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
if (!form.value.apiKey || form.value.apiKey.trim() === '') {
|
||||||
|
errors.value.apiKey = 'API Key 是必填项'
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
} else if (form.value.addType === 'manual') {
|
} else if (form.value.addType === 'manual') {
|
||||||
// 手动模式验证
|
// 手动模式验证
|
||||||
if (!form.value.accessToken || form.value.accessToken.trim() === '') {
|
if (!form.value.accessToken || form.value.accessToken.trim() === '') {
|
||||||
@@ -2122,15 +2203,24 @@ const createAccount = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分组类型验证
|
// 分组类型验证 - 创建账户流程修复
|
||||||
if (
|
if (
|
||||||
form.value.accountType === 'group' &&
|
form.value.accountType === 'group' &&
|
||||||
(!form.value.groupId || form.value.groupId.trim() === '')
|
(!form.value.groupIds || form.value.groupIds.length === 0)
|
||||||
) {
|
) {
|
||||||
showToast('请选择一个分组', 'error')
|
showToast('请选择一个分组', 'error')
|
||||||
hasError = true
|
hasError = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 数据同步:确保 groupId 和 groupIds 保持一致 - 创建流程
|
||||||
|
if (form.value.accountType === 'group') {
|
||||||
|
if (form.value.groupIds && form.value.groupIds.length > 0) {
|
||||||
|
form.value.groupId = form.value.groupIds[0]
|
||||||
|
} else {
|
||||||
|
form.value.groupId = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -2142,6 +2232,7 @@ const createAccount = async () => {
|
|||||||
description: form.value.description,
|
description: form.value.description,
|
||||||
accountType: form.value.accountType,
|
accountType: form.value.accountType,
|
||||||
groupId: form.value.accountType === 'group' ? form.value.groupId : undefined,
|
groupId: form.value.accountType === 'group' ? form.value.groupId : undefined,
|
||||||
|
groupIds: form.value.accountType === 'group' ? form.value.groupIds : undefined,
|
||||||
proxy: form.value.proxy.enabled
|
proxy: form.value.proxy.enabled
|
||||||
? {
|
? {
|
||||||
type: form.value.proxy.type,
|
type: form.value.proxy.type,
|
||||||
@@ -2266,6 +2357,16 @@ const createAccount = async () => {
|
|||||||
data.priority = form.value.priority || 50
|
data.priority = form.value.priority || 50
|
||||||
// 如果不启用限流,传递 0 表示不限流
|
// 如果不启用限流,传递 0 表示不限流
|
||||||
data.rateLimitDuration = form.value.enableRateLimit ? form.value.rateLimitDuration || 60 : 0
|
data.rateLimitDuration = form.value.enableRateLimit ? form.value.rateLimitDuration || 60 : 0
|
||||||
|
} else if (form.value.platform === 'azure_openai') {
|
||||||
|
// Azure OpenAI 账户特定数据
|
||||||
|
data.azureEndpoint = form.value.azureEndpoint
|
||||||
|
data.apiVersion = form.value.apiVersion || '2024-02-01'
|
||||||
|
data.deploymentName = form.value.deploymentName
|
||||||
|
data.apiKey = form.value.apiKey
|
||||||
|
data.supportedModels = form.value.supportedModels || []
|
||||||
|
data.priority = form.value.priority || 50
|
||||||
|
data.isActive = form.value.isActive !== false
|
||||||
|
data.schedulable = form.value.schedulable !== false
|
||||||
}
|
}
|
||||||
|
|
||||||
let result
|
let result
|
||||||
@@ -2277,8 +2378,12 @@ const createAccount = async () => {
|
|||||||
result = await accountsStore.createBedrockAccount(data)
|
result = await accountsStore.createBedrockAccount(data)
|
||||||
} else if (form.value.platform === 'openai') {
|
} else if (form.value.platform === 'openai') {
|
||||||
result = await accountsStore.createOpenAIAccount(data)
|
result = await accountsStore.createOpenAIAccount(data)
|
||||||
} else {
|
} else if (form.value.platform === 'azure_openai') {
|
||||||
|
result = await accountsStore.createAzureOpenAIAccount(data)
|
||||||
|
} else if (form.value.platform === 'gemini') {
|
||||||
result = await accountsStore.createGeminiAccount(data)
|
result = await accountsStore.createGeminiAccount(data)
|
||||||
|
} else {
|
||||||
|
throw new Error(`不支持的平台: ${form.value.platform}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('success', result)
|
emit('success', result)
|
||||||
@@ -2300,15 +2405,24 @@ const updateAccount = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分组类型验证
|
// 分组类型验证 - 更新账户流程修复
|
||||||
if (
|
if (
|
||||||
form.value.accountType === 'group' &&
|
form.value.accountType === 'group' &&
|
||||||
(!form.value.groupId || form.value.groupId.trim() === '')
|
(!form.value.groupIds || form.value.groupIds.length === 0)
|
||||||
) {
|
) {
|
||||||
showToast('请选择一个分组', 'error')
|
showToast('请选择一个分组', 'error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 数据同步:确保 groupId 和 groupIds 保持一致 - 更新流程
|
||||||
|
if (form.value.accountType === 'group') {
|
||||||
|
if (form.value.groupIds && form.value.groupIds.length > 0) {
|
||||||
|
form.value.groupId = form.value.groupIds[0]
|
||||||
|
} else {
|
||||||
|
form.value.groupId = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 对于Gemini账户,检查项目 ID
|
// 对于Gemini账户,检查项目 ID
|
||||||
if (form.value.platform === 'gemini') {
|
if (form.value.platform === 'gemini') {
|
||||||
if (!form.value.projectId || form.value.projectId.trim() === '') {
|
if (!form.value.projectId || form.value.projectId.trim() === '') {
|
||||||
@@ -2332,6 +2446,7 @@ const updateAccount = async () => {
|
|||||||
description: form.value.description,
|
description: form.value.description,
|
||||||
accountType: form.value.accountType,
|
accountType: form.value.accountType,
|
||||||
groupId: form.value.accountType === 'group' ? form.value.groupId : undefined,
|
groupId: form.value.accountType === 'group' ? form.value.groupId : undefined,
|
||||||
|
groupIds: form.value.accountType === 'group' ? form.value.groupIds : undefined,
|
||||||
proxy: form.value.proxy.enabled
|
proxy: form.value.proxy.enabled
|
||||||
? {
|
? {
|
||||||
type: form.value.proxy.type,
|
type: form.value.proxy.type,
|
||||||
@@ -2458,8 +2573,12 @@ const updateAccount = async () => {
|
|||||||
await accountsStore.updateBedrockAccount(props.account.id, data)
|
await accountsStore.updateBedrockAccount(props.account.id, data)
|
||||||
} else if (props.account.platform === 'openai') {
|
} else if (props.account.platform === 'openai') {
|
||||||
await accountsStore.updateOpenAIAccount(props.account.id, data)
|
await accountsStore.updateOpenAIAccount(props.account.id, data)
|
||||||
} else {
|
} else if (props.account.platform === 'azure_openai') {
|
||||||
|
await accountsStore.updateAzureOpenAIAccount(props.account.id, data)
|
||||||
|
} else if (props.account.platform === 'gemini') {
|
||||||
await accountsStore.updateGeminiAccount(props.account.id, data)
|
await accountsStore.updateGeminiAccount(props.account.id, data)
|
||||||
|
} else {
|
||||||
|
throw new Error(`不支持的平台: ${props.account.platform}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('success')
|
emit('success')
|
||||||
@@ -2541,6 +2660,11 @@ const refreshGroups = async () => {
|
|||||||
showToast('分组列表已刷新', 'success')
|
showToast('分组列表已刷新', 'success')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理新建分组
|
||||||
|
const handleNewGroup = () => {
|
||||||
|
showGroupManagement.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// 处理分组管理模态框刷新
|
// 处理分组管理模态框刷新
|
||||||
const handleGroupRefresh = async () => {
|
const handleGroupRefresh = async () => {
|
||||||
await loadGroups()
|
await loadGroups()
|
||||||
@@ -2567,10 +2691,28 @@ watch(
|
|||||||
// 平台变化时,清空分组选择
|
// 平台变化时,清空分组选择
|
||||||
if (form.value.accountType === 'group') {
|
if (form.value.accountType === 'group') {
|
||||||
form.value.groupId = ''
|
form.value.groupId = ''
|
||||||
|
form.value.groupIds = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 监听分组选择变化,保持 groupId 和 groupIds 同步
|
||||||
|
watch(
|
||||||
|
() => form.value.groupIds,
|
||||||
|
(newGroupIds) => {
|
||||||
|
if (form.value.accountType === 'group') {
|
||||||
|
if (newGroupIds && newGroupIds.length > 0) {
|
||||||
|
// 如果有选中的分组,使用第一个作为主分组
|
||||||
|
form.value.groupId = newGroupIds[0]
|
||||||
|
} else {
|
||||||
|
// 如果没有选中分组,清空主分组
|
||||||
|
form.value.groupId = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
// 监听Setup Token授权码输入,自动提取URL中的code参数
|
// 监听Setup Token授权码输入,自动提取URL中的code参数
|
||||||
watch(setupTokenAuthCode, (newValue) => {
|
watch(setupTokenAuthCode, (newValue) => {
|
||||||
if (!newValue || typeof newValue !== 'string') return
|
if (!newValue || typeof newValue !== 'string') return
|
||||||
@@ -2731,6 +2873,7 @@ watch(
|
|||||||
accountType: newAccount.accountType || 'shared',
|
accountType: newAccount.accountType || 'shared',
|
||||||
subscriptionType: subscriptionType,
|
subscriptionType: subscriptionType,
|
||||||
groupId: groupId,
|
groupId: groupId,
|
||||||
|
groupIds: [],
|
||||||
projectId: newAccount.projectId || '',
|
projectId: newAccount.projectId || '',
|
||||||
accessToken: '',
|
accessToken: '',
|
||||||
refreshToken: '',
|
refreshToken: '',
|
||||||
@@ -2768,24 +2911,35 @@ watch(
|
|||||||
// 如果是分组类型,加载分组ID
|
// 如果是分组类型,加载分组ID
|
||||||
if (newAccount.accountType === 'group') {
|
if (newAccount.accountType === 'group') {
|
||||||
// 先加载分组列表
|
// 先加载分组列表
|
||||||
loadGroups().then(() => {
|
loadGroups().then(async () => {
|
||||||
|
const foundGroupIds = []
|
||||||
|
|
||||||
// 如果账户有 groupInfo,直接使用它的 groupId
|
// 如果账户有 groupInfo,直接使用它的 groupId
|
||||||
if (newAccount.groupInfo && newAccount.groupInfo.id) {
|
if (newAccount.groupInfo && newAccount.groupInfo.id) {
|
||||||
form.value.groupId = newAccount.groupInfo.id
|
form.value.groupId = newAccount.groupInfo.id
|
||||||
|
foundGroupIds.push(newAccount.groupInfo.id)
|
||||||
} else {
|
} else {
|
||||||
// 否则查找账户所属的分组
|
// 否则查找账户所属的分组
|
||||||
groups.value.forEach((group) => {
|
const checkPromises = groups.value.map(async (group) => {
|
||||||
apiClient
|
try {
|
||||||
.get(`/admin/account-groups/${group.id}/members`)
|
const response = await apiClient.get(`/admin/account-groups/${group.id}/members`)
|
||||||
.then((response) => {
|
const members = response.data || []
|
||||||
const members = response.data || []
|
if (members.some((m) => m.id === newAccount.id)) {
|
||||||
if (members.some((m) => m.id === newAccount.id)) {
|
foundGroupIds.push(group.id)
|
||||||
form.value.groupId = group.id
|
if (!form.value.groupId) {
|
||||||
|
form.value.groupId = group.id // 设置第一个找到的分组作为主分组
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.catch(() => {})
|
} catch (error) {
|
||||||
|
// 忽略错误
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await Promise.all(checkPromises)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置多选分组
|
||||||
|
form.value.groupIds = foundGroupIds
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,9 +98,18 @@ class ApiClient {
|
|||||||
|
|
||||||
// GET 请求
|
// GET 请求
|
||||||
async get(url, options = {}) {
|
async get(url, options = {}) {
|
||||||
const fullUrl = createApiUrl(url)
|
// 处理查询参数
|
||||||
|
let fullUrl = createApiUrl(url)
|
||||||
|
if (options.params) {
|
||||||
|
const params = new URLSearchParams(options.params)
|
||||||
|
fullUrl += '?' + params.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除 params 避免传递给 fetch
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const { params, ...configOptions } = options
|
||||||
const config = this.buildConfig({
|
const config = this.buildConfig({
|
||||||
...options,
|
...configOptions,
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -418,7 +418,8 @@
|
|||||||
account.platform === 'claude-console' ||
|
account.platform === 'claude-console' ||
|
||||||
account.platform === 'bedrock' ||
|
account.platform === 'bedrock' ||
|
||||||
account.platform === 'gemini' ||
|
account.platform === 'gemini' ||
|
||||||
account.platform === 'openai'
|
account.platform === 'openai' ||
|
||||||
|
account.platform === 'azure_openai'
|
||||||
"
|
"
|
||||||
class="flex items-center gap-2"
|
class="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
@@ -917,12 +918,24 @@ const sortedAccounts = computed(() => {
|
|||||||
const loadAccounts = async (forceReload = false) => {
|
const loadAccounts = async (forceReload = false) => {
|
||||||
accountsLoading.value = true
|
accountsLoading.value = true
|
||||||
try {
|
try {
|
||||||
// 构建查询参数
|
// 检查是否选择了特定分组
|
||||||
|
if (groupFilter.value && groupFilter.value !== 'all' && groupFilter.value !== 'ungrouped') {
|
||||||
|
// 直接调用分组成员接口
|
||||||
|
const response = await apiClient.get(`/admin/account-groups/${groupFilter.value}/members`)
|
||||||
|
if (response.success) {
|
||||||
|
// 分组成员接口已经包含了完整的账户信息,直接使用
|
||||||
|
accounts.value = response.data
|
||||||
|
accountsLoading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建查询参数(用于其他筛选情况)
|
||||||
const params = {}
|
const params = {}
|
||||||
if (platformFilter.value !== 'all') {
|
if (platformFilter.value !== 'all') {
|
||||||
params.platform = platformFilter.value
|
params.platform = platformFilter.value
|
||||||
}
|
}
|
||||||
if (groupFilter.value !== 'all') {
|
if (groupFilter.value === 'ungrouped') {
|
||||||
params.groupId = groupFilter.value
|
params.groupId = groupFilter.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -947,7 +960,9 @@ const loadAccounts = async (forceReload = false) => {
|
|||||||
apiClient.get('/admin/claude-accounts', { params }),
|
apiClient.get('/admin/claude-accounts', { params }),
|
||||||
Promise.resolve({ success: true, data: [] }), // claude-console 占位
|
Promise.resolve({ success: true, data: [] }), // claude-console 占位
|
||||||
Promise.resolve({ success: true, data: [] }), // bedrock 占位
|
Promise.resolve({ success: true, data: [] }), // bedrock 占位
|
||||||
Promise.resolve({ success: true, data: [] }) // gemini 占位
|
Promise.resolve({ success: true, data: [] }), // gemini 占位
|
||||||
|
Promise.resolve({ success: true, data: [] }), // openai 占位
|
||||||
|
Promise.resolve({ success: true, data: [] }) // azure-openai 占位
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
case 'claude-console':
|
case 'claude-console':
|
||||||
@@ -955,7 +970,9 @@ const loadAccounts = async (forceReload = false) => {
|
|||||||
Promise.resolve({ success: true, data: [] }), // claude 占位
|
Promise.resolve({ success: true, data: [] }), // claude 占位
|
||||||
apiClient.get('/admin/claude-console-accounts', { params }),
|
apiClient.get('/admin/claude-console-accounts', { params }),
|
||||||
Promise.resolve({ success: true, data: [] }), // bedrock 占位
|
Promise.resolve({ success: true, data: [] }), // bedrock 占位
|
||||||
Promise.resolve({ success: true, data: [] }) // gemini 占位
|
Promise.resolve({ success: true, data: [] }), // gemini 占位
|
||||||
|
Promise.resolve({ success: true, data: [] }), // openai 占位
|
||||||
|
Promise.resolve({ success: true, data: [] }) // azure-openai 占位
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
case 'bedrock':
|
case 'bedrock':
|
||||||
@@ -963,7 +980,9 @@ const loadAccounts = async (forceReload = false) => {
|
|||||||
Promise.resolve({ success: true, data: [] }), // claude 占位
|
Promise.resolve({ success: true, data: [] }), // claude 占位
|
||||||
Promise.resolve({ success: true, data: [] }), // claude-console 占位
|
Promise.resolve({ success: true, data: [] }), // claude-console 占位
|
||||||
apiClient.get('/admin/bedrock-accounts', { params }),
|
apiClient.get('/admin/bedrock-accounts', { params }),
|
||||||
Promise.resolve({ success: true, data: [] }) // gemini 占位
|
Promise.resolve({ success: true, data: [] }), // gemini 占位
|
||||||
|
Promise.resolve({ success: true, data: [] }), // openai 占位
|
||||||
|
Promise.resolve({ success: true, data: [] }) // azure-openai 占位
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
case 'gemini':
|
case 'gemini':
|
||||||
@@ -971,7 +990,40 @@ const loadAccounts = async (forceReload = false) => {
|
|||||||
Promise.resolve({ success: true, data: [] }), // claude 占位
|
Promise.resolve({ success: true, data: [] }), // claude 占位
|
||||||
Promise.resolve({ success: true, data: [] }), // claude-console 占位
|
Promise.resolve({ success: true, data: [] }), // claude-console 占位
|
||||||
Promise.resolve({ success: true, data: [] }), // bedrock 占位
|
Promise.resolve({ success: true, data: [] }), // bedrock 占位
|
||||||
apiClient.get('/admin/gemini-accounts', { params })
|
apiClient.get('/admin/gemini-accounts', { params }),
|
||||||
|
Promise.resolve({ success: true, data: [] }), // openai 占位
|
||||||
|
Promise.resolve({ success: true, data: [] }) // azure-openai 占位
|
||||||
|
)
|
||||||
|
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 占位
|
||||||
|
)
|
||||||
|
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 })
|
||||||
|
)
|
||||||
|
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: [] })
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -1046,8 +1098,8 @@ const loadAccounts = async (forceReload = false) => {
|
|||||||
const boundApiKeysCount = apiKeys.value.filter(
|
const boundApiKeysCount = apiKeys.value.filter(
|
||||||
(key) => key.azureOpenaiAccountId === acc.id
|
(key) => key.azureOpenaiAccountId === acc.id
|
||||||
).length
|
).length
|
||||||
const groupInfo = accountGroupMap.value.get(acc.id) || null
|
// 后端已经包含了groupInfos,直接使用
|
||||||
return { ...acc, platform: 'azure_openai', boundApiKeysCount, groupInfo }
|
return { ...acc, platform: 'azure_openai', boundApiKeysCount }
|
||||||
})
|
})
|
||||||
allAccounts.push(...azureOpenaiAccounts)
|
allAccounts.push(...azureOpenaiAccounts)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user