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'));
});