feat: 实现账户分组管理功能和优化响应式设计

主要更新:
- 实现账户分组管理功能,支持创建、编辑、删除分组
- 支持将账户添加到分组进行统一调度
- 优化 API Keys 页面响应式设计,解决操作栏被隐藏的问题
- 优化账户管理页面布局,合并平台/类型列,改进操作按钮布局
- 修复代理信息显示溢出问题
- 改进表格列宽分配,充分利用屏幕空间

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-08-03 21:37:28 +08:00
parent 329904ba72
commit 9c9afe1528
20 changed files with 3588 additions and 717 deletions

View File

@@ -67,6 +67,24 @@ class Application {
const claudeAccountService = require('./services/claudeAccountService');
await claudeAccountService.initializeSessionWindows();
// 超早期拦截 /admin-next/ 请求 - 在所有中间件之前
this.app.use((req, res, next) => {
if (req.path === '/admin-next/' && req.method === 'GET') {
logger.warn(`🚨 INTERCEPTING /admin-next/ request at the very beginning!`);
const adminSpaPath = path.join(__dirname, '..', 'web', 'admin-spa', 'dist');
const indexPath = path.join(adminSpaPath, 'index.html');
if (fs.existsSync(indexPath)) {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
return res.sendFile(indexPath);
} else {
logger.error('❌ index.html not found at:', indexPath);
return res.status(404).send('index.html not found');
}
}
next();
});
// 🛡️ 安全中间件
this.app.use(helmet({
contentSecurityPolicy: false, // 允许内联样式和脚本
@@ -121,6 +139,14 @@ class Application {
this.app.set('trust proxy', 1);
}
// 调试中间件 - 拦截所有 /admin-next 请求
this.app.use((req, res, next) => {
if (req.path.startsWith('/admin-next')) {
logger.info(`🔍 DEBUG: Incoming request - method: ${req.method}, path: ${req.path}, originalUrl: ${req.originalUrl}`);
}
next();
});
// 🎨 新版管理界面静态文件服务(必须在其他路由之前)
const adminSpaPath = path.join(__dirname, '..', 'web', 'admin-spa', 'dist');
if (fs.existsSync(adminSpaPath)) {
@@ -129,40 +155,54 @@ class Application {
res.redirect(301, '/admin-next/');
});
// 安全的静态文件服务配置
this.app.use('/admin-next/', express.static(adminSpaPath, {
maxAge: '1d', // 缓存静态资源1天
etag: true,
lastModified: true,
index: 'index.html',
// 安全选项:禁止目录遍历
dotfiles: 'deny', // 拒绝访问点文件
redirect: false, // 禁止目录重定向
// 自定义错误处理
setHeaders: (res, path) => {
// 为不同类型的文件设置适当的缓存策略
if (path.endsWith('.js') || path.endsWith('.css')) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); // 1年缓存
} else if (path.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
}
// 使用 all 方法确保捕获所有 HTTP 方法
this.app.all('/admin-next/', (req, res) => {
logger.info('🎯 HIT: /admin-next/ route handler triggered!');
logger.info(`Method: ${req.method}, Path: ${req.path}, URL: ${req.url}`);
if (req.method !== 'GET' && req.method !== 'HEAD') {
return res.status(405).send('Method Not Allowed');
}
}));
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.sendFile(path.join(adminSpaPath, 'index.html'));
});
// 处理SPA路由所有未匹配的admin-next路径都返回index.html
this.app.get('/admin-next/*', (req, res, next) => {
// 安全检查:防止路径遍历攻击
// 处理所有其他 /admin-next/* 路径(但排除根路径)
this.app.get('/admin-next/*', (req, res) => {
// 如果是根路径,跳过(应该由上面的路由处理)
if (req.path === '/admin-next/') {
logger.error('❌ ERROR: /admin-next/ should not reach here!');
return res.status(500).send('Route configuration error');
}
const requestPath = req.path.replace('/admin-next/', '');
// 安全检查
if (requestPath.includes('..') || requestPath.includes('//') || requestPath.includes('\\')) {
return res.status(400).json({ error: 'Invalid path' });
}
// 如果是静态资源请求但文件不存在返回404
// 检查是否为静态资源
const filePath = path.join(adminSpaPath, requestPath);
// 如果文件存在且是静态资源
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
// 设置缓存头
if (filePath.endsWith('.js') || filePath.endsWith('.css')) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
} else if (filePath.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
}
return res.sendFile(filePath);
}
// 如果是静态资源但文件不存在
if (requestPath.match(/\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf)$/i)) {
return res.status(404).send('Not found');
}
// 其他路径返回index.htmlSPA路由处理
// 其他所有路径返回 index.htmlSPA 路由)
res.sendFile(path.join(adminSpaPath, 'index.html'));
});

View File

@@ -3,6 +3,7 @@ const apiKeyService = require('../services/apiKeyService');
const claudeAccountService = require('../services/claudeAccountService');
const claudeConsoleAccountService = require('../services/claudeConsoleAccountService');
const geminiAccountService = require('../services/geminiAccountService');
const accountGroupService = require('../services/accountGroupService');
const redis = require('../models/redis');
const { authenticateAdmin } = require('../middleware/auth');
const logger = require('../utils/logger');
@@ -712,6 +713,118 @@ router.delete('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
}
});
// 👥 账户分组管理
// 创建账户分组
router.post('/account-groups', authenticateAdmin, async (req, res) => {
try {
const { name, platform, description } = req.body;
const group = await accountGroupService.createGroup({
name,
platform,
description
});
res.json({ success: true, data: group });
} catch (error) {
logger.error('❌ Failed to create account group:', error);
res.status(400).json({ error: error.message });
}
});
// 获取所有分组
router.get('/account-groups', authenticateAdmin, async (req, res) => {
try {
const { platform } = req.query;
const groups = await accountGroupService.getAllGroups(platform);
res.json({ success: true, data: groups });
} catch (error) {
logger.error('❌ Failed to get account groups:', error);
res.status(500).json({ error: error.message });
}
});
// 获取分组详情
router.get('/account-groups/:groupId', authenticateAdmin, async (req, res) => {
try {
const { groupId } = req.params;
const group = await accountGroupService.getGroup(groupId);
if (!group) {
return res.status(404).json({ error: '分组不存在' });
}
res.json({ success: true, data: group });
} catch (error) {
logger.error('❌ Failed to get account group:', error);
res.status(500).json({ error: error.message });
}
});
// 更新分组
router.put('/account-groups/:groupId', authenticateAdmin, async (req, res) => {
try {
const { groupId } = req.params;
const updates = req.body;
const updatedGroup = await accountGroupService.updateGroup(groupId, updates);
res.json({ success: true, data: updatedGroup });
} catch (error) {
logger.error('❌ Failed to update account group:', error);
res.status(400).json({ error: error.message });
}
});
// 删除分组
router.delete('/account-groups/:groupId', authenticateAdmin, async (req, res) => {
try {
const { groupId } = req.params;
await accountGroupService.deleteGroup(groupId);
res.json({ success: true, message: '分组删除成功' });
} catch (error) {
logger.error('❌ Failed to delete account group:', error);
res.status(400).json({ error: error.message });
}
});
// 获取分组成员
router.get('/account-groups/:groupId/members', authenticateAdmin, async (req, res) => {
try {
const { groupId } = req.params;
const memberIds = await accountGroupService.getGroupMembers(groupId);
// 获取成员详细信息
const members = [];
for (const memberId of memberIds) {
// 尝试从不同的服务获取账户信息
let account = null;
// 先尝试Claude OAuth账户
account = await claudeAccountService.getAccount(memberId);
// 如果找不到尝试Claude Console账户
if (!account) {
account = await claudeConsoleAccountService.getAccount(memberId);
}
// 如果还找不到尝试Gemini账户
if (!account) {
account = await geminiAccountService.getAccount(memberId);
}
if (account) {
members.push(account);
}
}
res.json({ success: true, data: members });
} catch (error) {
logger.error('❌ Failed to get group members:', error);
res.status(500).json({ error: error.message });
}
});
// 🏢 Claude 账户管理
// 生成OAuth授权URL
@@ -863,7 +976,8 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
claudeAiOauth,
proxy,
accountType,
priority
priority,
groupId
} = req.body;
if (!name) {
@@ -871,8 +985,13 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
}
// 验证accountType的有效性
if (accountType && !['shared', 'dedicated'].includes(accountType)) {
return res.status(400).json({ error: 'Invalid account type. Must be "shared" or "dedicated"' });
if (accountType && !['shared', 'dedicated', 'group'].includes(accountType)) {
return res.status(400).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' });
}
// 验证priority的有效性
@@ -892,6 +1011,11 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
priority: priority || 50 // 默认优先级为50
});
// 如果是分组类型,将账户添加到分组
if (accountType === 'group' && groupId) {
await accountGroupService.addAccountToGroup(newAccount.id, groupId, newAccount.platform);
}
logger.success(`🏢 Admin created new Claude account: ${name} (${accountType || 'shared'})`);
res.json({ success: true, data: newAccount });
} catch (error) {
@@ -911,6 +1035,39 @@ router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) =>
return res.status(400).json({ error: 'Priority must be a number between 1 and 100' });
}
// 验证accountType的有效性
if (updates.accountType && !['shared', 'dedicated', 'group'].includes(updates.accountType)) {
return res.status(400).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' });
}
// 获取账户当前信息以处理分组变更
const currentAccount = await claudeAccountService.getAccount(accountId);
if (!currentAccount) {
return res.status(404).json({ error: 'Account not found' });
}
// 处理分组的变更
if (updates.accountType !== undefined) {
// 如果之前是分组类型,需要从原分组中移除
if (currentAccount.accountType === 'group') {
const oldGroup = await accountGroupService.getAccountGroup(accountId);
if (oldGroup) {
await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id);
}
}
// 如果新类型是分组,添加到新分组
if (updates.accountType === 'group' && updates.groupId) {
// 从路由知道这是 Claude OAuth 账户,平台为 'claude'
await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'claude');
}
}
await claudeAccountService.updateAccount(accountId, updates);
logger.success(`📝 Admin updated Claude account: ${accountId}`);
@@ -926,6 +1083,15 @@ router.delete('/claude-accounts/:accountId', authenticateAdmin, async (req, res)
try {
const { accountId } = req.params;
// 获取账户信息以检查是否在分组中
const account = await claudeAccountService.getAccount(accountId);
if (account && account.accountType === 'group') {
const group = await accountGroupService.getAccountGroup(accountId);
if (group) {
await accountGroupService.removeAccountFromGroup(accountId, group.id);
}
}
await claudeAccountService.deleteAccount(accountId);
logger.success(`🗑️ Admin deleted Claude account: ${accountId}`);
@@ -1026,7 +1192,8 @@ router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => {
userAgent,
rateLimitDuration,
proxy,
accountType
accountType,
groupId
} = req.body;
if (!name || !apiUrl || !apiKey) {
@@ -1039,8 +1206,13 @@ router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => {
}
// 验证accountType的有效性
if (accountType && !['shared', 'dedicated'].includes(accountType)) {
return res.status(400).json({ error: 'Invalid account type. Must be "shared" or "dedicated"' });
if (accountType && !['shared', 'dedicated', 'group'].includes(accountType)) {
return res.status(400).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' });
}
const newAccount = await claudeConsoleAccountService.createAccount({
@@ -1056,6 +1228,11 @@ router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => {
accountType: accountType || 'shared'
});
// 如果是分组类型,将账户添加到分组
if (accountType === 'group' && groupId) {
await accountGroupService.addAccountToGroup(newAccount.id, groupId, 'claude');
}
logger.success(`🎮 Admin created Claude Console account: ${name}`);
res.json({ success: true, data: newAccount });
} catch (error) {
@@ -1263,8 +1440,23 @@ router.post('/gemini-accounts', authenticateAdmin, async (req, res) => {
return res.status(400).json({ error: 'Account name is required' });
}
// 验证accountType的有效性
if (accountData.accountType && !['shared', 'dedicated', 'group'].includes(accountData.accountType)) {
return res.status(400).json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' });
}
// 如果是分组类型验证groupId
if (accountData.accountType === 'group' && !accountData.groupId) {
return res.status(400).json({ error: 'Group ID is required for group type accounts' });
}
const newAccount = await geminiAccountService.createAccount(accountData);
// 如果是分组类型,将账户添加到分组
if (accountData.accountType === 'group' && accountData.groupId) {
await accountGroupService.addAccountToGroup(newAccount.id, accountData.groupId, 'gemini');
}
logger.success(`🏢 Admin created new Gemini account: ${accountData.name}`);
res.json({ success: true, data: newAccount });
} catch (error) {

View File

@@ -0,0 +1,351 @@
const { v4: uuidv4 } = require('uuid');
const logger = require('../utils/logger');
const redis = require('../models/redis');
class AccountGroupService {
constructor() {
this.GROUPS_KEY = 'account_groups';
this.GROUP_PREFIX = 'account_group:';
this.GROUP_MEMBERS_PREFIX = 'account_group_members:';
}
/**
* 创建账户分组
* @param {Object} groupData - 分组数据
* @param {string} groupData.name - 分组名称
* @param {string} groupData.platform - 平台类型 (claude/gemini)
* @param {string} groupData.description - 分组描述
* @returns {Object} 创建的分组
*/
async createGroup(groupData) {
try {
const { name, platform, description = '' } = groupData;
// 验证必填字段
if (!name || !platform) {
throw new Error('分组名称和平台类型为必填项');
}
// 验证平台类型
if (!['claude', 'gemini'].includes(platform)) {
throw new Error('平台类型必须是 claude 或 gemini');
}
const client = redis.getClientSafe();
const groupId = uuidv4();
const now = new Date().toISOString();
const group = {
id: groupId,
name,
platform,
description,
createdAt: now,
updatedAt: now
};
// 保存分组数据
await client.hmset(`${this.GROUP_PREFIX}${groupId}`, group);
// 添加到分组集合
await client.sadd(this.GROUPS_KEY, groupId);
logger.success(`✅ 创建账户分组成功: ${name} (${platform})`);
return group;
} catch (error) {
logger.error('❌ 创建账户分组失败:', error);
throw error;
}
}
/**
* 更新分组信息
* @param {string} groupId - 分组ID
* @param {Object} updates - 更新的字段
* @returns {Object} 更新后的分组
*/
async updateGroup(groupId, updates) {
try {
const client = redis.getClientSafe();
const groupKey = `${this.GROUP_PREFIX}${groupId}`;
// 检查分组是否存在
const exists = await client.exists(groupKey);
if (!exists) {
throw new Error('分组不存在');
}
// 获取现有分组数据
const existingGroup = await client.hgetall(groupKey);
// 不允许修改平台类型
if (updates.platform && updates.platform !== existingGroup.platform) {
throw new Error('不能修改分组的平台类型');
}
// 准备更新数据
const updateData = {
...updates,
updatedAt: new Date().toISOString()
};
// 移除不允许修改的字段
delete updateData.id;
delete updateData.platform;
delete updateData.createdAt;
// 更新分组
await client.hmset(groupKey, updateData);
// 返回更新后的完整数据
const updatedGroup = await client.hgetall(groupKey);
logger.success(`✅ 更新账户分组成功: ${updatedGroup.name}`);
return updatedGroup;
} catch (error) {
logger.error('❌ 更新账户分组失败:', error);
throw error;
}
}
/**
* 删除分组
* @param {string} groupId - 分组ID
*/
async deleteGroup(groupId) {
try {
const client = redis.getClientSafe();
// 检查分组是否存在
const group = await this.getGroup(groupId);
if (!group) {
throw new Error('分组不存在');
}
// 检查分组是否为空
const members = await this.getGroupMembers(groupId);
if (members.length > 0) {
throw new Error('分组内还有账户,无法删除');
}
// 检查是否有API Key绑定此分组
const boundApiKeys = await this.getApiKeysUsingGroup(groupId);
if (boundApiKeys.length > 0) {
throw new Error('还有API Key使用此分组无法删除');
}
// 删除分组数据
await client.del(`${this.GROUP_PREFIX}${groupId}`);
await client.del(`${this.GROUP_MEMBERS_PREFIX}${groupId}`);
// 从分组集合中移除
await client.srem(this.GROUPS_KEY, groupId);
logger.success(`✅ 删除账户分组成功: ${group.name}`);
} catch (error) {
logger.error('❌ 删除账户分组失败:', error);
throw error;
}
}
/**
* 获取分组详情
* @param {string} groupId - 分组ID
* @returns {Object|null} 分组信息
*/
async getGroup(groupId) {
try {
const client = redis.getClientSafe();
const groupData = await client.hgetall(`${this.GROUP_PREFIX}${groupId}`);
if (!groupData || Object.keys(groupData).length === 0) {
return null;
}
// 获取成员数量
const memberCount = await client.scard(`${this.GROUP_MEMBERS_PREFIX}${groupId}`);
return {
...groupData,
memberCount: memberCount || 0
};
} catch (error) {
logger.error('❌ 获取分组详情失败:', error);
throw error;
}
}
/**
* 获取所有分组
* @param {string} platform - 平台筛选 (可选)
* @returns {Array} 分组列表
*/
async getAllGroups(platform = null) {
try {
const client = redis.getClientSafe();
const groupIds = await client.smembers(this.GROUPS_KEY);
const groups = [];
for (const groupId of groupIds) {
const group = await this.getGroup(groupId);
if (group) {
// 如果指定了平台,进行筛选
if (!platform || group.platform === platform) {
groups.push(group);
}
}
}
// 按创建时间倒序排序
groups.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
return groups;
} catch (error) {
logger.error('❌ 获取分组列表失败:', error);
throw error;
}
}
/**
* 添加账户到分组
* @param {string} accountId - 账户ID
* @param {string} groupId - 分组ID
* @param {string} accountPlatform - 账户平台
*/
async addAccountToGroup(accountId, groupId, accountPlatform) {
try {
const client = redis.getClientSafe();
// 获取分组信息
const group = await this.getGroup(groupId);
if (!group) {
throw new Error('分组不存在');
}
// 验证平台一致性 (Claude和Claude Console视为同一平台)
const normalizedAccountPlatform = accountPlatform === 'claude-console' ? 'claude' : accountPlatform;
if (normalizedAccountPlatform !== group.platform) {
throw new Error('账户平台与分组平台不匹配');
}
// 添加到分组成员集合
await client.sadd(`${this.GROUP_MEMBERS_PREFIX}${groupId}`, accountId);
logger.success(`✅ 添加账户到分组成功: ${accountId} -> ${group.name}`);
} catch (error) {
logger.error('❌ 添加账户到分组失败:', error);
throw error;
}
}
/**
* 从分组移除账户
* @param {string} accountId - 账户ID
* @param {string} groupId - 分组ID
*/
async removeAccountFromGroup(accountId, groupId) {
try {
const client = redis.getClientSafe();
// 从分组成员集合中移除
await client.srem(`${this.GROUP_MEMBERS_PREFIX}${groupId}`, accountId);
logger.success(`✅ 从分组移除账户成功: ${accountId}`);
} catch (error) {
logger.error('❌ 从分组移除账户失败:', error);
throw error;
}
}
/**
* 获取分组成员
* @param {string} groupId - 分组ID
* @returns {Array} 成员ID列表
*/
async getGroupMembers(groupId) {
try {
const client = redis.getClientSafe();
const members = await client.smembers(`${this.GROUP_MEMBERS_PREFIX}${groupId}`);
return members || [];
} catch (error) {
logger.error('❌ 获取分组成员失败:', error);
throw error;
}
}
/**
* 检查分组是否为空
* @param {string} groupId - 分组ID
* @returns {boolean} 是否为空
*/
async isGroupEmpty(groupId) {
try {
const members = await this.getGroupMembers(groupId);
return members.length === 0;
} catch (error) {
logger.error('❌ 检查分组是否为空失败:', error);
throw error;
}
}
/**
* 获取使用指定分组的API Key列表
* @param {string} groupId - 分组ID
* @returns {Array} API Key列表
*/
async getApiKeysUsingGroup(groupId) {
try {
const client = redis.getClientSafe();
const groupKey = `group:${groupId}`;
// 获取所有API Key
const apiKeyIds = await client.smembers('api_keys');
const boundApiKeys = [];
for (const keyId of apiKeyIds) {
const keyData = await client.hgetall(`api_key:${keyId}`);
if (keyData &&
(keyData.claudeAccountId === groupKey ||
keyData.geminiAccountId === groupKey)) {
boundApiKeys.push({
id: keyId,
name: keyData.name
});
}
}
return boundApiKeys;
} catch (error) {
logger.error('❌ 获取使用分组的API Key失败:', error);
throw error;
}
}
/**
* 根据账户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;
}
}
}
module.exports = new AccountGroupService();

View File

@@ -68,7 +68,7 @@ class ClaudeAccountService {
lastRefreshAt: '',
status: 'active', // 有OAuth数据的账户直接设为active
errorMessage: '',
schedulable: schedulable.toString() // 是否可被调度
schedulable: schedulable.toString(), // 是否可被调度
};
} else {
// 兼容旧格式
@@ -91,7 +91,7 @@ class ClaudeAccountService {
lastRefreshAt: '',
status: 'created', // created, active, expired, error
errorMessage: '',
schedulable: schedulable.toString() // 是否可被调度
schedulable: schedulable.toString(), // 是否可被调度
};
}
@@ -233,6 +233,23 @@ class ClaudeAccountService {
}
}
// 🔍 获取账户信息
async getAccount(accountId) {
try {
const accountData = await redis.getClaudeAccount(accountId);
if (!accountData || Object.keys(accountData).length === 0) {
return null;
}
return accountData;
} catch (error) {
logger.error('❌ Failed to get Claude account:', error);
return null;
}
}
// 🎯 获取有效的访问token
async getValidAccessToken(accountId) {
try {

View File

@@ -1,5 +1,6 @@
const claudeAccountService = require('./claudeAccountService');
const claudeConsoleAccountService = require('./claudeConsoleAccountService');
const accountGroupService = require('./accountGroupService');
const redis = require('../models/redis');
const logger = require('../utils/logger');
@@ -11,9 +12,16 @@ class UnifiedClaudeScheduler {
// 🎯 统一调度Claude账号官方和Console
async selectAccountForApiKey(apiKeyData, sessionHash = null, requestedModel = null) {
try {
// 如果API Key绑定了专属账户优先使用
// 1. 检查Claude OAuth账户绑定
// 如果API Key绑定了专属账户或分组,优先使用
if (apiKeyData.claudeAccountId) {
// 检查是否是分组
if (apiKeyData.claudeAccountId.startsWith('group:')) {
const groupId = apiKeyData.claudeAccountId.replace('group:', '');
logger.info(`🎯 API key ${apiKeyData.name} is bound to group ${groupId}, selecting from group`);
return await this.selectAccountFromGroup(groupId, sessionHash, requestedModel, apiKeyData);
}
// 普通专属账户
const boundAccount = await redis.getClaudeAccount(apiKeyData.claudeAccountId);
if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error') {
logger.info(`🎯 Using bound dedicated Claude OAuth account: ${boundAccount.name} (${apiKeyData.claudeAccountId}) for API key ${apiKeyData.name}`);
@@ -360,6 +368,132 @@ class UnifiedClaudeScheduler {
throw error;
}
}
// 👥 从分组中选择账户
async selectAccountFromGroup(groupId, sessionHash = null, requestedModel = null, apiKeyData = null) {
try {
// 获取分组信息
const group = await accountGroupService.getGroup(groupId);
if (!group) {
throw new Error(`Group ${groupId} not found`);
}
logger.info(`👥 Selecting account from group: ${group.name} (${group.platform})`);
// 如果有会话哈希,检查是否有已映射的账户
if (sessionHash) {
const mappedAccount = await this._getSessionMapping(sessionHash);
if (mappedAccount) {
// 验证映射的账户是否属于这个分组
const memberIds = await accountGroupService.getGroupMembers(groupId);
if (memberIds.includes(mappedAccount.accountId)) {
const isAvailable = await this._isAccountAvailable(mappedAccount.accountId, mappedAccount.accountType);
if (isAvailable) {
logger.info(`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`);
return mappedAccount;
}
}
// 如果映射的账户不可用或不在分组中,删除映射
await this._deleteSessionMapping(sessionHash);
}
}
// 获取分组内的所有账户
const memberIds = await accountGroupService.getGroupMembers(groupId);
if (memberIds.length === 0) {
throw new Error(`Group ${group.name} has no members`);
}
const availableAccounts = [];
// 获取所有成员账户的详细信息
for (const memberId of memberIds) {
let account = null;
let accountType = null;
// 根据平台类型获取账户
if (group.platform === 'claude') {
// 先尝试官方账户
account = await redis.getClaudeAccount(memberId);
if (account) {
accountType = 'claude-official';
} else {
// 尝试Console账户
account = await claudeConsoleAccountService.getAccount(memberId);
if (account) {
accountType = 'claude-console';
}
}
} else if (group.platform === 'gemini') {
// Gemini暂时不支持预留接口
logger.warn(`⚠️ Gemini group scheduling not yet implemented`);
continue;
}
if (!account) {
logger.warn(`⚠️ Account ${memberId} not found in group ${group.name}`);
continue;
}
// 检查账户是否可用
const isActive = accountType === 'claude-official'
? account.isActive === 'true'
: account.isActive === true;
const status = accountType === 'claude-official'
? account.status !== 'error' && account.status !== 'blocked'
: account.status === 'active';
if (isActive && status && account.schedulable !== false) {
// 检查模型支持Console账户
if (accountType === 'claude-console' && requestedModel && account.supportedModels && account.supportedModels.length > 0) {
if (!account.supportedModels.includes(requestedModel)) {
logger.info(`🚫 Account ${account.name} in group does not support model ${requestedModel}`);
continue;
}
}
// 检查是否被限流
const isRateLimited = await this.isAccountRateLimited(account.id, accountType);
if (!isRateLimited) {
availableAccounts.push({
...account,
accountId: account.id,
accountType: accountType,
priority: parseInt(account.priority) || 50,
lastUsedAt: account.lastUsedAt || '0'
});
}
}
}
if (availableAccounts.length === 0) {
throw new Error(`No available accounts in group ${group.name}`);
}
// 使用现有的优先级排序逻辑
const sortedAccounts = this._sortAccountsByPriority(availableAccounts);
// 选择第一个账户
const selectedAccount = sortedAccounts[0];
// 如果有会话哈希,建立新的映射
if (sessionHash) {
await this._setSessionMapping(sessionHash, selectedAccount.accountId, selectedAccount.accountType);
logger.info(`🎯 Created new sticky session mapping in group: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) for session ${sessionHash}`);
}
logger.info(`🎯 Selected account from group ${group.name}: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) with priority ${selectedAccount.priority}`);
return {
accountId: selectedAccount.accountId,
accountType: selectedAccount.accountType
};
} catch (error) {
logger.error(`❌ Failed to select account from group ${groupId}:`, error);
throw error;
}
}
}
module.exports = new UnifiedClaudeScheduler();