diff --git a/scripts/test-multi-group.js b/scripts/test-multi-group.js deleted file mode 100644 index 484bc714..00000000 --- a/scripts/test-multi-group.js +++ /dev/null @@ -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() diff --git a/src/routes/admin.js b/src/routes/admin.js index 9eac7135..49d43fb2 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -1510,15 +1510,18 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => { if (groupId && groupId !== 'all') { if (groupId === 'ungrouped') { // 筛选未分组账户 - accounts = accounts.filter( - (account) => !account.groupInfos || account.groupInfos.length === 0 - ) + 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 { // 筛选特定分组的账户 - accounts = accounts.filter( - (account) => - account.groupInfos && account.groupInfos.some((group) => group.id === groupId) - ) + const groupMembers = await accountGroupService.getGroupMembers(groupId) + accounts = accounts.filter((account) => groupMembers.includes(account.id)) } } @@ -1527,7 +1530,7 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => { accounts.map(async (account) => { try { const usageStats = await redis.getAccountUsageStats(account.id) - const groupInfos = await accountGroupService.getAccountGroup(account.id) + const groupInfos = await accountGroupService.getAccountGroups(account.id) // 获取会话窗口使用统计(仅对有活跃窗口的账户) let sessionWindowUsage = null @@ -1585,7 +1588,7 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => { logger.warn(`⚠️ Failed to get usage stats for account ${account.id}:`, statsError.message) // 如果获取统计失败,返回空统计 try { - const groupInfos = await accountGroupService.getAccountGroup(account.id) + const groupInfos = await accountGroupService.getAccountGroups(account.id) return { ...account, groupInfos, @@ -1638,6 +1641,7 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => { platform = 'claude', priority, groupId, + groupIds, autoStopOnWarning } = req.body @@ -1652,9 +1656,11 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => { .json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' }) } - // 如果是分组类型,验证groupId - if (accountType === 'group' && !groupId) { - return res.status(400).json({ error: 'Group ID is required for group type accounts' }) + // 如果是分组类型,验证groupId或groupIds + if (accountType === 'group' && !groupId && (!groupIds || groupIds.length === 0)) { + return res + .status(400) + .json({ error: 'Group ID or Group IDs are required for group type accounts' }) } // 验证priority的有效性 @@ -1680,8 +1686,14 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => { }) // 如果是分组类型,将账户添加到分组 - if (accountType === 'group' && groupId) { - await accountGroupService.addAccountToGroup(newAccount.id, groupId, newAccount.platform) + if (accountType === 'group') { + 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'})`) @@ -1715,9 +1727,15 @@ router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) => .json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' }) } - // 如果更新为分组类型,验证groupId - if (updates.accountType === 'group' && !updates.groupId) { - return res.status(400).json({ error: 'Group ID is required for group type accounts' }) + // 如果更新为分组类型,验证groupId或groupIds + if ( + 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' }) } // 获取账户当前信息以处理分组变更 @@ -1730,16 +1748,24 @@ router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) => if (updates.accountType !== undefined) { // 如果之前是分组类型,需要从所有分组中移除 if (currentAccount.accountType === 'group') { - const oldGroups = await accountGroupService.getAccountGroup(accountId) - for (const oldGroup of oldGroups) { - await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id) - } + await accountGroupService.removeAccountFromAllGroups(accountId) } // 如果新类型是分组,添加到新分组 - if (updates.accountType === 'group' && updates.groupId) { - // 从路由知道这是 Claude OAuth 账户,平台为 'claude' - await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'claude') + if (updates.accountType === 'group') { + // 处理多分组/单分组的兼容性 + 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') + } } } @@ -1913,15 +1939,18 @@ router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => { if (groupId && groupId !== 'all') { if (groupId === 'ungrouped') { // 筛选未分组账户 - accounts = accounts.filter( - (account) => !account.groupInfos || account.groupInfos.length === 0 - ) + 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 { // 筛选特定分组的账户 - accounts = accounts.filter( - (account) => - account.groupInfos && account.groupInfos.some((group) => group.id === groupId) - ) + const groupMembers = await accountGroupService.getGroupMembers(groupId) + accounts = accounts.filter((account) => groupMembers.includes(account.id)) } } @@ -1930,7 +1959,7 @@ router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => { accounts.map(async (account) => { try { const usageStats = await redis.getAccountUsageStats(account.id) - const groupInfos = await accountGroupService.getAccountGroup(account.id) + const groupInfos = await accountGroupService.getAccountGroups(account.id) return { ...account, @@ -1949,7 +1978,7 @@ router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => { statsError.message ) try { - const groupInfos = await accountGroupService.getAccountGroup(account.id) + const groupInfos = await accountGroupService.getAccountGroups(account.id) return { ...account, // 转换schedulable为布尔值 @@ -2043,7 +2072,7 @@ router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => { // 如果是分组类型,将账户添加到分组 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}`) @@ -2089,15 +2118,26 @@ router.put('/claude-console-accounts/:accountId', authenticateAdmin, async (req, if (updates.accountType !== undefined) { // 如果之前是分组类型,需要从所有分组中移除 if (currentAccount.accountType === 'group') { - const oldGroups = await accountGroupService.getAccountGroup(accountId) + const oldGroups = await accountGroupService.getAccountGroups(accountId) for (const oldGroup of oldGroups) { await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id) } } - // 如果新类型是分组,添加到新分组 - if (updates.accountType === 'group' && updates.groupId) { - // Claude Console 账户在分组中被视为 'claude' 平台 - await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'claude') + // 如果新类型是分组,处理多分组支持 + if (updates.accountType === 'group') { + if (Object.prototype.hasOwnProperty.call(updates, 'groupIds')) { + // 如果明确提供了 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') + } } } @@ -2231,15 +2271,18 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => { if (groupId && groupId !== 'all') { if (groupId === 'ungrouped') { // 筛选未分组账户 - accounts = accounts.filter( - (account) => !account.groupInfos || account.groupInfos.length === 0 - ) + 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 { // 筛选特定分组的账户 - accounts = accounts.filter( - (account) => - account.groupInfos && account.groupInfos.some((group) => group.id === groupId) - ) + const groupMembers = await accountGroupService.getGroupMembers(groupId) + accounts = accounts.filter((account) => groupMembers.includes(account.id)) } } @@ -2248,7 +2291,7 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => { accounts.map(async (account) => { try { const usageStats = await redis.getAccountUsageStats(account.id) - const groupInfos = await accountGroupService.getAccountGroup(account.id) + const groupInfos = await accountGroupService.getAccountGroups(account.id) return { ...account, @@ -2265,7 +2308,7 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => { statsError.message ) try { - const groupInfos = await accountGroupService.getAccountGroup(account.id) + const groupInfos = await accountGroupService.getAccountGroups(account.id) return { ...account, groupInfos, @@ -2678,15 +2721,18 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => { if (groupId && groupId !== 'all') { if (groupId === 'ungrouped') { // 筛选未分组账户 - accounts = accounts.filter( - (account) => !account.groupInfos || account.groupInfos.length === 0 - ) + 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 { // 筛选特定分组的账户 - accounts = accounts.filter( - (account) => - account.groupInfos && account.groupInfos.some((group) => group.id === groupId) - ) + const groupMembers = await accountGroupService.getGroupMembers(groupId) + accounts = accounts.filter((account) => groupMembers.includes(account.id)) } } @@ -2695,7 +2741,7 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => { accounts.map(async (account) => { try { const usageStats = await redis.getAccountUsageStats(account.id) - const groupInfos = await accountGroupService.getAccountGroup(account.id) + const groupInfos = await accountGroupService.getAccountGroups(account.id) return { ...account, @@ -2713,7 +2759,7 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => { ) // 如果获取统计失败,返回空统计 try { - const groupInfos = await accountGroupService.getAccountGroup(account.id) + const groupInfos = await accountGroupService.getAccountGroups(account.id) return { ...account, groupInfos, @@ -2817,14 +2863,26 @@ router.put('/gemini-accounts/:accountId', authenticateAdmin, async (req, res) => if (updates.accountType !== undefined) { // 如果之前是分组类型,需要从所有分组中移除 if (currentAccount.accountType === 'group') { - const oldGroups = await accountGroupService.getAccountGroup(accountId) + const oldGroups = await accountGroupService.getAccountGroups(accountId) for (const oldGroup of oldGroups) { await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id) } } - // 如果新类型是分组,添加到新分组 - if (updates.accountType === 'group' && updates.groupId) { - await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'gemini') + // 如果新类型是分组,处理多分组支持 + if (updates.accountType === 'group') { + 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') + } } } @@ -5189,15 +5247,18 @@ router.get('/openai-accounts', authenticateAdmin, async (req, res) => { if (groupId && groupId !== 'all') { if (groupId === 'ungrouped') { // 筛选未分组账户 - accounts = accounts.filter( - (account) => !account.groupInfos || account.groupInfos.length === 0 - ) + 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 { // 筛选特定分组的账户 - accounts = accounts.filter( - (account) => - account.groupInfos && account.groupInfos.some((group) => group.id === groupId) - ) + const groupMembers = await accountGroupService.getGroupMembers(groupId) + accounts = accounts.filter((account) => groupMembers.includes(account.id)) } } @@ -5518,10 +5579,81 @@ router.put( // 获取所有 Azure OpenAI 账户 router.get('/azure-openai-accounts', authenticateAdmin, async (req, res) => { 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({ success: true, - data: accounts + data: accountsWithStats }) } catch (error) { logger.error('Failed to fetch Azure OpenAI accounts:', error) @@ -5547,6 +5679,7 @@ router.post('/azure-openai-accounts', authenticateAdmin, async (req, res) => { supportedModels, proxy, groupId, + groupIds, priority, isActive, schedulable @@ -5626,6 +5759,17 @@ router.post('/azure-openai-accounts', authenticateAdmin, async (req, res) => { 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({ success: true, data: account, diff --git a/src/services/accountGroupService.js b/src/services/accountGroupService.js index d2061266..7268dad5 100644 --- a/src/services/accountGroupService.js +++ b/src/services/accountGroupService.js @@ -13,7 +13,7 @@ class AccountGroupService { * 创建账户分组 * @param {Object} groupData - 分组数据 * @param {string} groupData.name - 分组名称 - * @param {string} groupData.platform - 平台类型 (claude/gemini) + * @param {string} groupData.platform - 平台类型 (claude/gemini/openai) * @param {string} groupData.description - 分组描述 * @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获取其所属的所有分组 * @param {string} accountId - 账户ID * @returns {Array} 分组信息数组 */ - async getAccountGroup(accountId) { + async getAccountGroups(accountId) { try { const client = redis.getClientSafe() const allGroupIds = await client.smembers(this.GROUPS_KEY) @@ -357,6 +381,49 @@ class AccountGroupService { 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() diff --git a/src/services/azureOpenaiAccountService.js b/src/services/azureOpenaiAccountService.js index 8ff86a99..e7d5754d 100644 --- a/src/services/azureOpenaiAccountService.js +++ b/src/services/azureOpenaiAccountService.js @@ -249,6 +249,10 @@ async function updateAccount(accountId, updates) { // 删除账户 async function deleteAccount(accountId) { + // 首先从所有分组中移除此账户 + const accountGroupService = require('./accountGroupService') + await accountGroupService.removeAccountFromAllGroups(accountId) + const client = redisClient.getClientSafe() const accountKey = `${AZURE_OPENAI_ACCOUNT_KEY_PREFIX}${accountId}` diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index e236d626..1540b74b 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -621,10 +621,7 @@ class ClaudeAccountService { try { // 首先从所有分组中移除此账户 const accountGroupService = require('./accountGroupService') - const groups = await accountGroupService.getAccountGroup(accountId) - for (const group of groups) { - await accountGroupService.removeAccountFromGroup(accountId, group.id) - } + await accountGroupService.removeAccountFromAllGroups(accountId) const result = await redis.deleteClaudeAccount(accountId) diff --git a/web/admin-spa/src/components/accounts/AccountForm.vue b/web/admin-spa/src/components/accounts/AccountForm.vue index 35e3f776..b7055c1e 100644 --- a/web/admin-spa/src/components/accounts/AccountForm.vue +++ b/web/admin-spa/src/components/accounts/AccountForm.vue @@ -244,19 +244,47 @@ >选择分组 *