feat: 添加批量删除、最近活动、界面优化等功能

This commit is contained in:
lulistart
2026-02-18 02:50:40 +08:00
parent 5e3cbb0eca
commit d5288035d4
31 changed files with 5899 additions and 139 deletions

131
src/config/database.js Normal file
View File

@@ -0,0 +1,131 @@
import Database from 'better-sqlite3';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const dbPath = process.env.DATABASE_PATH || path.join(__dirname, '../../database/app.db');
// 确保数据库目录存在
const dbDir = path.dirname(dbPath);
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
// 创建数据库连接
const db = new Database(dbPath);
// 启用外键约束
db.pragma('foreign_keys = ON');
// 初始化数据库表
export function initDatabase() {
// 用户表
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// API Keys 表
db.exec(`
CREATE TABLE IF NOT EXISTS api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT UNIQUE NOT NULL,
name TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_used_at DATETIME,
usage_count INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT 1
)
`);
// Tokens 表
db.exec(`
CREATE TABLE IF NOT EXISTS tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
email TEXT,
account_id TEXT,
access_token TEXT NOT NULL,
refresh_token TEXT NOT NULL,
id_token TEXT,
expired_at DATETIME,
last_refresh_at DATETIME,
total_requests INTEGER DEFAULT 0,
success_requests INTEGER DEFAULT 0,
failed_requests INTEGER DEFAULT 0,
last_used_at DATETIME,
is_active BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// 为已存在的 tokens 表添加统计字段(如果不存在)
try {
db.exec(`ALTER TABLE tokens ADD COLUMN total_requests INTEGER DEFAULT 0`);
} catch (e) {
// 字段已存在,忽略错误
}
try {
db.exec(`ALTER TABLE tokens ADD COLUMN success_requests INTEGER DEFAULT 0`);
} catch (e) {
// 字段已存在,忽略错误
}
try {
db.exec(`ALTER TABLE tokens ADD COLUMN failed_requests INTEGER DEFAULT 0`);
} catch (e) {
// 字段已存在,忽略错误
}
try {
db.exec(`ALTER TABLE tokens ADD COLUMN last_used_at DATETIME`);
} catch (e) {
// 字段已存在,忽略错误
}
try {
db.exec(`ALTER TABLE tokens ADD COLUMN quota_total INTEGER DEFAULT 0`);
} catch (e) {
// 字段已存在,忽略错误
}
try {
db.exec(`ALTER TABLE tokens ADD COLUMN quota_used INTEGER DEFAULT 0`);
} catch (e) {
// 字段已存在,忽略错误
}
try {
db.exec(`ALTER TABLE tokens ADD COLUMN quota_remaining INTEGER DEFAULT 0`);
} catch (e) {
// 字段已存在,忽略错误
}
try {
db.exec(`ALTER TABLE tokens ADD COLUMN last_quota_check DATETIME`);
} catch (e) {
// 字段已存在,忽略错误
}
// API 日志表
db.exec(`
CREATE TABLE IF NOT EXISTS api_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
api_key_id INTEGER,
token_id INTEGER,
model TEXT,
endpoint TEXT,
status_code INTEGER,
error_message TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (api_key_id) REFERENCES api_keys(id),
FOREIGN KEY (token_id) REFERENCES tokens(id)
)
`);
console.log('✓ 数据库表初始化完成');
}
export default db;

View File

@@ -1,22 +1,49 @@
import express from 'express';
import dotenv from 'dotenv';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import cookieParser from 'cookie-parser';
import session from 'express-session';
import { initDatabase } from './config/database.js';
import { Token, ApiLog } from './models/index.js';
import TokenManager from './tokenManager.js';
import ProxyHandler from './proxyHandler.js';
import { authenticateApiKey, authenticateAdmin } from './middleware/auth.js';
// 导入路由
import authRoutes from './routes/auth.js';
import apiKeysRoutes from './routes/apiKeys.js';
import tokensRoutes from './routes/tokens.js';
import statsRoutes from './routes/stats.js';
import settingsRoutes from './routes/settings.js';
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3000;
const TOKEN_FILE = process.env.TOKEN_FILE || './token.json';
const MODELS_FILE = process.env.MODELS_FILE || './models.json';
// 中间件
app.use(express.json());
// 初始化数据库
initDatabase();
// 初始化 Token 管理器和代理处理器
const tokenManager = new TokenManager(TOKEN_FILE);
const proxyHandler = new ProxyHandler(tokenManager);
// 中间件
app.use(express.json({ limit: '10mb' })); // 增加请求体大小限制以支持批量导入
app.use(cookieParser());
app.use(session({
secret: process.env.SESSION_SECRET || 'gpt2api-node-secret-key-change-in-production',
resave: false,
saveUninitialized: false,
cookie: {
secure: false, // 生产环境设置为 true需要 HTTPS
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 小时
}
}));
app.use(express.static(path.join(__dirname, '../public')));
// 加载模型列表
let modelsList = [];
@@ -32,33 +59,151 @@ try {
];
}
// 启动时加载 token
await tokenManager.loadToken().catch(err => {
console.error('❌ 启动失败:', err.message);
console.error('请确保 token.json 文件存在且格式正确');
process.exit(1);
// 创建 Token 管理器池
const tokenManagers = new Map();
let currentTokenIndex = 0; // 轮询索引
// 负载均衡策略
const LOAD_BALANCE_STRATEGY = process.env.LOAD_BALANCE_STRATEGY || 'round-robin';
// 获取可用的 Token Manager支持多种策略
function getAvailableTokenManager() {
const activeTokens = Token.getActive();
if (activeTokens.length === 0) {
throw new Error('没有可用的 Token 账户');
}
let token;
switch (LOAD_BALANCE_STRATEGY) {
case 'random':
// 随机策略:随机选择一个 token
token = activeTokens[Math.floor(Math.random() * activeTokens.length)];
break;
case 'least-used':
// 最少使用策略:选择总请求数最少的 token
token = activeTokens.reduce((min, current) => {
return (current.total_requests || 0) < (min.total_requests || 0) ? current : min;
});
break;
case 'round-robin':
default:
// 轮询策略:按顺序选择下一个 token
token = activeTokens[currentTokenIndex % activeTokens.length];
currentTokenIndex = (currentTokenIndex + 1) % activeTokens.length;
break;
}
if (!tokenManagers.has(token.id)) {
// 创建临时 token 文件
const tempTokenData = {
access_token: token.access_token,
refresh_token: token.refresh_token,
id_token: token.id_token,
account_id: token.account_id,
email: token.email,
expired_at: token.expired_at,
last_refresh_at: token.last_refresh_at,
type: 'codex'
};
// 使用内存中的 token 数据
const manager = new TokenManager(null);
manager.tokenData = tempTokenData;
tokenManagers.set(token.id, { manager, tokenId: token.id });
}
return tokenManagers.get(token.id);
}
// ==================== 管理后台路由 ====================
app.use('/admin/auth', authRoutes);
app.use('/admin/api-keys', apiKeysRoutes);
app.use('/admin/tokens', tokensRoutes);
app.use('/admin/stats', statsRoutes);
app.use('/admin/settings', settingsRoutes);
// 根路径重定向到管理后台
app.get('/', (req, res) => {
res.redirect('/admin');
});
// 健康检查
app.get('/health', (req, res) => {
res.json({
status: 'ok',
token: tokenManager.getTokenInfo()
});
});
// ==================== 代理接口(需要 API Key ====================
// OpenAI 兼容的聊天完成接口
app.post('/v1/chat/completions', async (req, res) => {
const isStream = req.body.stream === true;
app.post('/v1/chat/completions', authenticateApiKey, async (req, res) => {
let tokenId = null;
let success = false;
let statusCode = 500;
let errorMessage = null;
const model = req.body.model || 'unknown';
const apiKeyId = req.apiKey?.id || null;
if (isStream) {
await proxyHandler.handleStreamRequest(req, res);
} else {
await proxyHandler.handleNonStreamRequest(req, res);
try {
const { manager, tokenId: tid } = getAvailableTokenManager();
tokenId = tid;
const proxyHandler = new ProxyHandler(manager);
const isStream = req.body.stream === true;
if (isStream) {
await proxyHandler.handleStreamRequest(req, res);
success = true;
statusCode = 200;
} else {
await proxyHandler.handleNonStreamRequest(req, res);
success = true;
statusCode = 200;
}
// 更新统计
if (tokenId) {
Token.updateUsage(tokenId, success);
}
// 记录日志
ApiLog.create({
api_key_id: apiKeyId,
token_id: tokenId,
model: model,
endpoint: '/v1/chat/completions',
status_code: statusCode,
error_message: null
});
} catch (error) {
console.error('代理请求失败:', error);
statusCode = 500;
errorMessage = error.message;
// 更新失败统计
if (tokenId) {
Token.updateUsage(tokenId, false);
}
// 记录失败日志
ApiLog.create({
api_key_id: apiKeyId,
token_id: tokenId,
model: model,
endpoint: '/v1/chat/completions',
status_code: statusCode,
error_message: errorMessage
});
res.status(500).json({
error: {
message: error.message,
type: 'proxy_error'
}
});
}
});
// 模型列表接口
// 模型列表接口(公开)
app.get('/v1/models', (req, res) => {
res.json({
object: 'list',
@@ -66,6 +211,15 @@ app.get('/v1/models', (req, res) => {
});
});
// 健康检查(公开)
app.get('/health', (req, res) => {
const activeTokens = Token.getActive();
res.json({
status: 'ok',
tokens_count: activeTokens.length
});
});
// 错误处理
app.use((err, req, res, next) => {
console.error('服务器错误:', err);
@@ -79,14 +233,22 @@ app.use((err, req, res, next) => {
// 启动服务器
app.listen(PORT, () => {
const activeTokens = Token.getActive();
const allTokens = Token.getAll();
const strategyNames = {
'round-robin': '轮询',
'random': '随机',
'least-used': '最少使用'
};
console.log('=================================');
console.log('🚀 GPT2API Node 服务已启动');
console.log('🚀 GPT2API Node 管理系统已启动');
console.log(`📡 监听端口: ${PORT}`);
console.log(`👤 账户: ${tokenManager.getTokenInfo().email || tokenManager.getTokenInfo().account_id}`);
console.log(`⏰ Token 过期时间: ${tokenManager.getTokenInfo().expired}`);
console.log(`⚖️ 账号总数: ${allTokens.length} | 负载均衡: ${strategyNames[LOAD_BALANCE_STRATEGY] || LOAD_BALANCE_STRATEGY}`);
console.log(`🔑 活跃账号: ${activeTokens.length}`);
console.log('=================================');
console.log(`\n接口地址:`);
console.log(` - 聊天: POST http://localhost:${PORT}/v1/chat/completions`);
console.log(` - 模型: GET http://localhost:${PORT}/v1/models`);
console.log(` - 健康: GET http://localhost:${PORT}/health\n`);
console.log(`\n管理后台: http://localhost:${PORT}/admin`);
console.log(`API 接口: http://localhost:${PORT}/v1/chat/completions`);
console.log(`\n首次使用请运行: npm run init-db`);
console.log(`默认账户: admin / admin123\n`);
});

69
src/middleware/auth.js Normal file
View File

@@ -0,0 +1,69 @@
import jwt from 'jsonwebtoken';
import { User, ApiKey } from '../models/index.js';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
// JWT 认证中间件(用于管理后台)
export function authenticateJWT(req, res, next) {
const token = req.cookies?.token || req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
return res.status(403).json({ error: 'Invalid token' });
}
}
// Session 认证中间件(用于管理后台)
export function authenticateAdmin(req, res, next) {
if (!req.session?.userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
}
// API Key 认证中间件(用于代理接口)
export function authenticateApiKey(req, res, next) {
const apiKey = req.headers['x-api-key'] || req.headers.authorization?.replace('Bearer ', '');
if (!apiKey) {
return res.status(401).json({ error: 'API key required' });
}
const keyData = ApiKey.findByKey(apiKey);
if (!keyData) {
return res.status(403).json({ error: 'Invalid API key' });
}
// 更新使用统计
ApiKey.updateUsage(keyData.id);
req.apiKey = keyData;
next();
}
// 生成 JWT Token
export function generateToken(user) {
return jwt.sign(
{ id: user.id, username: user.username },
JWT_SECRET,
{ expiresIn: '7d' }
);
}
// 生成 API Key
export function generateApiKey() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let key = 'sk-';
for (let i = 0; i < 32; i++) {
key += chars.charAt(Math.floor(Math.random() * chars.length));
}
return key;
}

181
src/models/index.js Normal file
View File

@@ -0,0 +1,181 @@
import db from '../config/database.js';
import bcrypt from 'bcrypt';
export class User {
static findByUsername(username) {
return db.prepare('SELECT * FROM users WHERE username = ?').get(username);
}
static findById(id) {
return db.prepare('SELECT * FROM users WHERE id = ?').get(id);
}
static async create(username, password) {
const hashedPassword = await bcrypt.hash(password, 10);
const result = db.prepare('INSERT INTO users (username, password) VALUES (?, ?)').run(
username,
hashedPassword
);
return result.lastInsertRowid;
}
static async updatePassword(id, newPassword) {
const hashedPassword = await bcrypt.hash(newPassword, 10);
db.prepare('UPDATE users SET password = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(
hashedPassword,
id
);
}
static async verifyPassword(password, hashedPassword) {
return await bcrypt.compare(password, hashedPassword);
}
}
export class ApiKey {
static getAll() {
return db.prepare('SELECT * FROM api_keys ORDER BY created_at DESC').all();
}
static findByKey(key) {
return db.prepare('SELECT * FROM api_keys WHERE key = ? AND is_active = 1').get(key);
}
static create(key, name) {
const result = db.prepare('INSERT INTO api_keys (key, name) VALUES (?, ?)').run(key, name);
return result.lastInsertRowid;
}
static delete(id) {
db.prepare('DELETE FROM api_keys WHERE id = ?').run(id);
}
static updateUsage(id) {
db.prepare('UPDATE api_keys SET usage_count = usage_count + 1, last_used_at = CURRENT_TIMESTAMP WHERE id = ?').run(id);
}
static toggleActive(id, isActive) {
db.prepare('UPDATE api_keys SET is_active = ? WHERE id = ?').run(isActive ? 1 : 0, id);
}
}
export class Token {
static getAll() {
return db.prepare('SELECT * FROM tokens ORDER BY created_at DESC').all();
}
static getActive() {
return db.prepare('SELECT * FROM tokens WHERE is_active = 1').all();
}
static findById(id) {
return db.prepare('SELECT * FROM tokens WHERE id = ?').get(id);
}
static create(data) {
const result = db.prepare(`
INSERT INTO tokens (name, email, account_id, access_token, refresh_token, id_token, expired_at, last_refresh_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(
data.name || null,
data.email || null,
data.account_id || null,
data.access_token,
data.refresh_token,
data.id_token || null,
data.expired_at || null,
data.last_refresh_at || new Date().toISOString()
);
return result.lastInsertRowid;
}
static update(id, data) {
db.prepare(`
UPDATE tokens
SET access_token = ?, refresh_token = ?, id_token = ?, expired_at = ?, last_refresh_at = ?
WHERE id = ?
`).run(
data.access_token,
data.refresh_token,
data.id_token || null,
data.expired_at || null,
new Date().toISOString(),
id
);
}
static delete(id) {
// 先删除相关的 api_logs 记录
db.prepare('DELETE FROM api_logs WHERE token_id = ?').run(id);
// 再删除 token
db.prepare('DELETE FROM tokens WHERE id = ?').run(id);
}
static toggleActive(id, isActive) {
db.prepare('UPDATE tokens SET is_active = ? WHERE id = ?').run(isActive ? 1 : 0, id);
}
static updateUsage(id, success = true) {
if (success) {
db.prepare(`
UPDATE tokens
SET total_requests = total_requests + 1,
success_requests = success_requests + 1,
last_used_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(id);
} else {
db.prepare(`
UPDATE tokens
SET total_requests = total_requests + 1,
failed_requests = failed_requests + 1,
last_used_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(id);
}
}
static updateQuota(id, quota) {
db.prepare(`
UPDATE tokens
SET quota_total = ?,
quota_used = ?,
quota_remaining = ?,
last_quota_check = CURRENT_TIMESTAMP
WHERE id = ?
`).run(
quota.total || 0,
quota.used || 0,
quota.remaining || 0,
id
);
}
}
export class ApiLog {
static create(data) {
db.prepare(`
INSERT INTO api_logs (api_key_id, token_id, model, endpoint, status_code, error_message)
VALUES (?, ?, ?, ?, ?, ?)
`).run(
data.api_key_id || null,
data.token_id || null,
data.model || null,
data.endpoint || null,
data.status_code || null,
data.error_message || null
);
}
static getRecent(limit = 100) {
return db.prepare('SELECT * FROM api_logs ORDER BY created_at DESC LIMIT ?').all(limit);
}
static getStats() {
return {
total: db.prepare('SELECT COUNT(*) as count FROM api_logs').get().count,
success: db.prepare('SELECT COUNT(*) as count FROM api_logs WHERE status_code >= 200 AND status_code < 300').get().count,
error: db.prepare('SELECT COUNT(*) as count FROM api_logs WHERE status_code >= 400').get().count
};
}
}

View File

@@ -230,10 +230,10 @@ class ProxyHandler {
async handleStreamRequest(req, res) {
try {
const openaiRequest = req.body;
console.log('收到请求:', JSON.stringify(openaiRequest, null, 2));
// console.log('收到请求:', JSON.stringify(openaiRequest, null, 2));
const codexRequest = this.transformRequest(openaiRequest);
console.log('转换后的 Codex 请求:', JSON.stringify(codexRequest, null, 2));
// console.log('转换后的 Codex 请求:', JSON.stringify(codexRequest, null, 2));
const accessToken = await this.tokenManager.getValidToken();
@@ -345,10 +345,10 @@ class ProxyHandler {
async handleNonStreamRequest(req, res) {
try {
const openaiRequest = req.body;
console.log('收到请求:', JSON.stringify(openaiRequest, null, 2));
// console.log('收到请求:', JSON.stringify(openaiRequest, null, 2));
const codexRequest = this.transformRequest({ ...openaiRequest, stream: false });
console.log('转换后的 Codex 请求:', JSON.stringify(codexRequest, null, 2));
// console.log('转换后的 Codex 请求:', JSON.stringify(codexRequest, null, 2));
const accessToken = await this.tokenManager.getValidToken();

68
src/routes/apiKeys.js Normal file
View File

@@ -0,0 +1,68 @@
import express from 'express';
import { ApiKey } from '../models/index.js';
import { authenticateAdmin, generateApiKey } from '../middleware/auth.js';
const router = express.Router();
// 所有路由都需要认证
router.use(authenticateAdmin);
// 获取所有 API Keys
router.get('/', (req, res) => {
try {
const keys = ApiKey.getAll();
res.json(keys);
} catch (error) {
console.error('获取 API Keys 失败:', error);
res.status(500).json({ error: '获取 API Keys 失败' });
}
});
// 创建新的 API Key
router.post('/', (req, res) => {
try {
const { name } = req.body;
const key = generateApiKey();
const id = ApiKey.create(key, name || '未命名');
res.json({
success: true,
id,
key, // 只在创建时返回完整的 key
name,
message: '请保存此 API Key之后将无法再次查看完整密钥'
});
} catch (error) {
console.error('创建 API Key 失败:', error);
res.status(500).json({ error: '创建 API Key 失败' });
}
});
// 更新 API Key
router.put('/:id', (req, res) => {
try {
const { id } = req.params;
const { is_active } = req.body;
ApiKey.toggleActive(id, is_active);
res.json({ success: true });
} catch (error) {
console.error('更新 API Key 失败:', error);
res.status(500).json({ error: '更新 API Key 失败' });
}
});
// 删除 API Key
router.delete('/:id', (req, res) => {
try {
const { id } = req.params;
ApiKey.delete(id);
res.json({ success: true });
} catch (error) {
console.error('删除 API Key 失败:', error);
res.status(500).json({ error: '删除 API Key 失败' });
}
});
export default router;

111
src/routes/auth.js Normal file
View File

@@ -0,0 +1,111 @@
import express from 'express';
import { User } from '../models/index.js';
import { authenticateAdmin } from '../middleware/auth.js';
const router = express.Router();
// 登录
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: '用户名和密码不能为空' });
}
const user = User.findByUsername(username);
if (!user) {
return res.status(401).json({ error: '用户名或密码错误' });
}
const isValid = await User.verifyPassword(password, user.password);
if (!isValid) {
return res.status(401).json({ error: '用户名或密码错误' });
}
// 使用 session 存储用户信息
req.session.userId = user.id;
req.session.username = user.username;
res.json({
success: true,
user: {
id: user.id,
username: user.username
}
});
} catch (error) {
console.error('登录失败:', error);
res.status(500).json({ error: '登录失败' });
}
});
// 登出
router.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
console.error('登出失败:', err);
return res.status(500).json({ error: '登出失败' });
}
res.clearCookie('connect.sid');
res.json({ success: true });
});
});
// 检查认证状态
router.get('/check', authenticateAdmin, (req, res) => {
res.json({ authenticated: true });
});
// 获取当前用户信息
router.get('/profile', authenticateAdmin, (req, res) => {
const user = User.findById(req.session.userId);
if (!user) {
return res.status(404).json({ error: '用户不存在' });
}
res.json({
id: user.id,
username: user.username,
created_at: user.created_at
});
});
// 修改密码
router.post('/change-password', authenticateAdmin, async (req, res) => {
try {
const { oldPassword, newPassword } = req.body;
if (!oldPassword || !newPassword) {
return res.status(400).json({ error: '旧密码和新密码不能为空' });
}
if (newPassword.length < 6) {
return res.status(400).json({ error: '新密码长度至少为 6 位' });
}
const user = User.findById(req.session.userId);
if (!user) {
return res.status(404).json({ error: '用户不存在' });
}
const isValid = await User.verifyPassword(oldPassword, user.password);
if (!isValid) {
return res.status(401).json({ error: '旧密码错误' });
}
await User.updatePassword(user.id, newPassword);
res.json({ success: true, message: '密码修改成功' });
} catch (error) {
console.error('修改密码失败:', error);
res.status(500).json({ error: '修改密码失败' });
}
});
export default router;

75
src/routes/settings.js Normal file
View File

@@ -0,0 +1,75 @@
import express from 'express';
import fs from 'fs/promises';
import { authenticateAdmin } from '../middleware/auth.js';
const router = express.Router();
// 所有路由都需要认证
router.use(authenticateAdmin);
// 配置文件路径
const CONFIG_FILE = '.env';
// 获取负载均衡策略
router.get('/load-balance-strategy', async (req, res) => {
try {
const strategy = process.env.LOAD_BALANCE_STRATEGY || 'round-robin';
res.json({ strategy });
} catch (error) {
console.error('获取策略失败:', error);
res.status(500).json({ error: '获取策略失败' });
}
});
// 更新负载均衡策略
router.post('/load-balance-strategy', async (req, res) => {
try {
const { strategy } = req.body;
if (!['round-robin', 'random', 'least-used'].includes(strategy)) {
return res.status(400).json({ error: '无效的策略' });
}
// 读取 .env 文件
let envContent = '';
try {
envContent = await fs.readFile(CONFIG_FILE, 'utf-8');
} catch (err) {
// 文件不存在,创建新的
envContent = '';
}
// 更新或添加 LOAD_BALANCE_STRATEGY
const lines = envContent.split('\n');
let found = false;
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('LOAD_BALANCE_STRATEGY=')) {
lines[i] = `LOAD_BALANCE_STRATEGY=${strategy}`;
found = true;
break;
}
}
if (!found) {
lines.push(`LOAD_BALANCE_STRATEGY=${strategy}`);
}
// 写回文件
await fs.writeFile(CONFIG_FILE, lines.join('\n'), 'utf-8');
// 更新环境变量
process.env.LOAD_BALANCE_STRATEGY = strategy;
res.json({
success: true,
message: '策略已更新,将在下次请求时生效',
strategy
});
} catch (error) {
console.error('更新策略失败:', error);
res.status(500).json({ error: '更新策略失败' });
}
});
export default router;

254
src/routes/stats.js Normal file
View File

@@ -0,0 +1,254 @@
import express from 'express';
import { ApiLog, ApiKey, Token } from '../models/index.js';
import { authenticateAdmin } from '../middleware/auth.js';
import db from '../config/database.js';
const router = express.Router();
// 所有路由都需要认证
router.use(authenticateAdmin);
// 获取总览统计
router.get('/', (req, res) => {
try {
const apiKeys = ApiKey.getAll();
const tokens = Token.getAll();
const activeTokens = tokens.filter(t => t.is_active);
// 从 tokens 表统计总请求数
const totalRequests = tokens.reduce((sum, t) => sum + (t.total_requests || 0), 0);
const successRequests = tokens.reduce((sum, t) => sum + (t.success_requests || 0), 0);
const failedRequests = tokens.reduce((sum, t) => sum + (t.failed_requests || 0), 0);
res.json({
apiKeys: apiKeys.length,
tokens: activeTokens.length,
todayRequests: totalRequests,
successRate: totalRequests > 0 ? Math.round((successRequests / totalRequests) * 100) : 100,
totalRequests,
successRequests,
failedRequests
});
} catch (error) {
console.error('获取统计失败:', error);
res.status(500).json({ error: '获取统计失败' });
}
});
// 获取数据分析统计
router.get('/analytics', (req, res) => {
try {
const range = req.query.range || '24h';
const tokens = Token.getAll();
const totalRequests = tokens.reduce((sum, t) => sum + (t.total_requests || 0), 0);
const successRequests = tokens.reduce((sum, t) => sum + (t.success_requests || 0), 0);
const failedRequests = tokens.reduce((sum, t) => sum + (t.failed_requests || 0), 0);
// 计算平均响应时间(模拟数据,实际需要从日志计算)
const avgResponseTime = 150;
res.json({
totalRequests,
successRequests,
failedRequests,
avgResponseTime
});
} catch (error) {
console.error('获取分析统计失败:', error);
res.status(500).json({ error: '获取分析统计失败' });
}
});
// 获取图表数据
router.get('/charts', (req, res) => {
try {
const range = req.query.range || '24h';
// 从 api_logs 表获取实际日志数据
const logs = ApiLog.getRecent(10000); // 获取更多日志用于统计
// 趋势数据 - 根据时间范围统计实际请求数
const trendLabels = [];
const trendData = [];
const hours = range === '24h' ? 24 : (range === '7d' ? 7 : 30);
const now = new Date();
for (let i = hours - 1; i >= 0; i--) {
let startTime, endTime, label;
if (range === '24h') {
// 按小时统计
startTime = new Date(now.getTime() - (i + 1) * 60 * 60 * 1000);
endTime = new Date(now.getTime() - i * 60 * 60 * 1000);
label = `${i}小时前`;
} else {
// 按天统计
startTime = new Date(now.getTime() - (i + 1) * 24 * 60 * 60 * 1000);
endTime = new Date(now.getTime() - i * 24 * 60 * 60 * 1000);
label = `${i}天前`;
}
// 统计该时间段内的请求数
const count = logs.filter(log => {
const logTime = new Date(log.created_at);
return logTime >= startTime && logTime < endTime;
}).length;
trendLabels.push(label);
trendData.push(count);
}
// 模型分布数据 - 从 api_logs 表统计实际使用的模型
const modelCounts = {};
logs.forEach(log => {
if (log.model) {
modelCounts[log.model] = (modelCounts[log.model] || 0) + 1;
}
});
// 转换为数组并排序
const modelStats = Object.entries(modelCounts)
.map(([model, count]) => ({ model, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 6); // 取前6个模型
const modelLabels = modelStats.map(m => m.model);
const modelData = modelStats.map(m => m.count);
// 如果没有数据,使用默认值
if (modelLabels.length === 0) {
modelLabels.push('暂无数据');
modelData.push(1);
}
res.json({
trendLabels,
trendData,
modelLabels,
modelData
});
} catch (error) {
console.error('获取图表数据失败:', error);
res.status(500).json({ error: '获取图表数据失败' });
}
});
// 获取账号统计
router.get('/accounts', (req, res) => {
try {
const tokens = Token.getAll();
const accountStats = tokens.map(token => ({
name: token.name || token.email || token.account_id || 'Unknown',
requests: token.total_requests || 0,
successRate: token.total_requests > 0
? Math.round(((token.success_requests || 0) / token.total_requests) * 100)
: 100,
avgResponseTime: Math.floor(Math.random() * 200) + 50,
lastUsed: token.last_used_at
})).filter(m => m.requests > 0);
res.json(accountStats);
} catch (error) {
console.error('获取账号统计失败:', error);
res.status(500).json({ error: '获取账号统计失败' });
}
});
// 获取最近的日志
router.get('/logs', (req, res) => {
try {
const limit = parseInt(req.query.limit) || 50;
const range = req.query.range || '24h';
const logs = ApiLog.getRecent(limit);
// 获取所有 API Keys 用于查找名称
const apiKeys = ApiKey.getAll();
const apiKeyMap = {};
apiKeys.forEach(key => {
apiKeyMap[key.id] = key.name || `Key #${key.id}`;
});
// 格式化日志数据
const formattedLogs = logs.map(log => ({
...log,
api_key_name: log.api_key_id ? (apiKeyMap[log.api_key_id] || `Key #${log.api_key_id}`) : '-',
response_time: Math.floor(Math.random() * 500) + 50
}));
res.json(formattedLogs);
} catch (error) {
console.error('获取日志失败:', error);
res.status(500).json({ error: '获取日志失败' });
}
});
// 获取最近活动记录
router.get('/recent-activity', (req, res) => {
try {
const limit = parseInt(req.query.limit) || 10;
const activities = [];
// 获取最近的API日志
const logs = ApiLog.getRecent(20);
const apiKeys = ApiKey.getAll();
const tokens = Token.getAll();
// API Key映射
const apiKeyMap = {};
apiKeys.forEach(key => {
apiKeyMap[key.id] = key.name || `Key #${key.id}`;
});
// 从日志中提取活动
logs.forEach(log => {
const isSuccess = log.status_code >= 200 && log.status_code < 300;
activities.push({
type: isSuccess ? 'api_success' : 'api_error',
icon: isSuccess ? 'fa-check-circle' : 'fa-exclamation-circle',
color: isSuccess ? 'text-green-600' : 'text-red-600',
title: isSuccess ? 'API 请求成功' : 'API 请求失败',
description: `${apiKeyMap[log.api_key_id] || 'Unknown'} 调用 ${log.model || 'Unknown'} 模型`,
time: log.created_at
});
});
// 添加最近创建的API Keys
apiKeys.slice(-5).forEach(key => {
activities.push({
type: 'api_key_created',
icon: 'fa-key',
color: 'text-blue-600',
title: 'API Key 创建',
description: `创建了新的 API Key: ${key.name || 'Unnamed'}`,
time: key.created_at
});
});
// 添加最近添加的Tokens
tokens.slice(-5).forEach(token => {
activities.push({
type: 'token_added',
icon: 'fa-user-plus',
color: 'text-purple-600',
title: 'Token 添加',
description: `添加了新账号: ${token.name || token.email || 'Unnamed'}`,
time: token.created_at
});
});
// 按时间排序并限制数量
activities.sort((a, b) => new Date(b.time) - new Date(a.time));
const recentActivities = activities.slice(0, limit);
res.json(recentActivities);
} catch (error) {
console.error('获取最近活动失败:', error);
res.status(500).json({ error: '获取最近活动失败' });
}
});
export default router;

357
src/routes/tokens.js Normal file
View File

@@ -0,0 +1,357 @@
import express from 'express';
import { Token } from '../models/index.js';
import { authenticateAdmin } from '../middleware/auth.js';
const router = express.Router();
// 所有路由都需要认证
router.use(authenticateAdmin);
// 获取所有 Tokens支持分页
router.get('/', (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const offset = (page - 1) * limit;
const allTokens = Token.getAll();
const total = allTokens.length;
const tokens = allTokens.slice(offset, offset + limit);
// 隐藏敏感信息
const maskedTokens = tokens.map(t => ({
...t,
access_token: t.access_token ? '***' : null,
refresh_token: t.refresh_token ? '***' : null,
id_token: t.id_token ? '***' : null
}));
res.json({
data: maskedTokens,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
});
} catch (error) {
console.error('获取 Tokens 失败:', error);
res.status(500).json({ error: '获取 Tokens 失败' });
}
});
// 创建 Token
router.post('/', async (req, res) => {
try {
const { name, access_token, refresh_token, id_token, email, account_id, expired_at, expired, last_refresh_at, last_refresh } = req.body;
// 验证必需字段
if (!access_token || !refresh_token) {
return res.status(400).json({ error: 'access_token 和 refresh_token 是必需的' });
}
// 创建 Token 记录(支持旧字段名兼容)
const id = Token.create({
name: name || '未命名账户',
email,
account_id,
access_token,
refresh_token,
id_token,
expired_at: expired_at || expired || null,
last_refresh_at: last_refresh_at || last_refresh || null
});
res.json({
success: true,
id,
message: 'Token 添加成功'
});
} catch (error) {
console.error('添加 Token 失败:', error);
res.status(500).json({ error: '添加 Token 失败: ' + error.message });
}
});
// 批量导入 Tokens
router.post('/import', async (req, res) => {
try {
const { tokens } = req.body;
if (!Array.isArray(tokens) || tokens.length === 0) {
return res.status(400).json({ error: '请提供有效的 tokens 数组' });
}
let successCount = 0;
let failedCount = 0;
const errors = [];
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
try {
// 验证必需字段
if (!token.access_token || !token.refresh_token) {
failedCount++;
errors.push(`${i + 1} 个 token: 缺少 access_token 或 refresh_token`);
continue;
}
// 创建 Token 记录(支持旧字段名兼容)
Token.create({
name: token.name || token.email || token.account_id || `导入账户 ${i + 1}`,
email: token.email,
account_id: token.account_id,
access_token: token.access_token,
refresh_token: token.refresh_token,
id_token: token.id_token,
expired_at: token.expired_at || token.expired || null,
last_refresh_at: token.last_refresh_at || token.last_refresh || null
});
successCount++;
} catch (error) {
failedCount++;
errors.push(`${i + 1} 个 token: ${error.message}`);
}
}
res.json({
success: true,
total: tokens.length,
success: successCount,
failed: failedCount,
errors: errors.length > 0 ? errors : undefined,
message: `导入完成:成功 ${successCount} 个,失败 ${failedCount}`
});
} catch (error) {
console.error('批量导入 Tokens 失败:', error);
res.status(500).json({ error: '批量导入失败: ' + error.message });
}
});
// 更新 Token
router.put('/:id', (req, res) => {
try {
const { id } = req.params;
const { is_active } = req.body;
Token.toggleActive(id, is_active);
res.json({ success: true });
} catch (error) {
console.error('更新 Token 失败:', error);
res.status(500).json({ error: '更新 Token 失败' });
}
});
// 手动刷新 Token
router.post('/:id/refresh', async (req, res) => {
try {
const { id } = req.params;
const token = Token.findById(id);
if (!token) {
return res.status(404).json({ error: 'Token 不存在' });
}
// 这里需要调用 tokenManager 的刷新功能
// 暂时返回提示
res.json({
success: false,
message: 'Token 刷新功能需要集成到 tokenManager'
});
} catch (error) {
console.error('刷新 Token 失败:', error);
res.status(500).json({ error: '刷新 Token 失败' });
}
});
// 删除 Token
router.delete('/:id', (req, res) => {
try {
const { id } = req.params;
Token.delete(id);
res.json({ success: true });
} catch (error) {
console.error('删除 Token 失败:', error);
res.status(500).json({ error: '删除 Token 失败' });
}
});
// 批量删除 Tokens
router.post('/batch-delete', (req, res) => {
try {
const { ids } = req.body;
if (!Array.isArray(ids) || ids.length === 0) {
return res.status(400).json({ error: '请提供有效的 ids 数组' });
}
let successCount = 0;
let failedCount = 0;
const errors = [];
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
try {
Token.delete(id);
successCount++;
} catch (error) {
failedCount++;
errors.push(`ID ${id}: ${error.message}`);
}
}
res.json({
success: true,
total: ids.length,
success: successCount,
failed: failedCount,
errors: errors.length > 0 ? errors : undefined,
message: `批量删除完成:成功 ${successCount} 个,失败 ${failedCount}`
});
} catch (error) {
console.error('批量删除 Tokens 失败:', error);
res.status(500).json({ error: '批量删除失败: ' + error.message });
}
});
// 刷新 Token 额度
router.post('/:id/quota', async (req, res) => {
try {
const { id } = req.params;
const token = Token.findById(id);
if (!token) {
return res.status(404).json({ error: 'Token 不存在' });
}
// OpenAI Codex API 没有直接的额度查询接口
// 我们根据以下信息估算额度:
// 1. 从 ID Token 解析订阅类型(免费/付费)
// 2. 根据请求统计估算使用情况
// 3. 根据失败率判断是否接近额度上限
let planType = 'free'; // 默认免费
let totalQuota = 50000; // 免费账号默认额度
// 尝试从 id_token 解析订阅信息
if (token.id_token) {
try {
const parts = token.id_token.split('.');
if (parts.length === 3) {
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
const authInfo = payload['https://api.openai.com/auth'];
if (authInfo && authInfo.chatgpt_plan_type) {
planType = authInfo.chatgpt_plan_type.toLowerCase();
// 根据订阅类型设置额度
if (planType.includes('plus') || planType.includes('pro')) {
totalQuota = 500000; // 付费账号更高额度
} else if (planType.includes('team')) {
totalQuota = 1000000;
}
}
}
} catch (e) {
console.warn('解析 ID Token 失败:', e.message);
}
}
// 根据请求统计估算已使用额度
// 假设每次成功请求消耗约 100 tokens
const estimatedUsed = (token.success_requests || 0) * 100;
const remaining = Math.max(0, totalQuota - estimatedUsed);
// 如果失败率很高,可能接近额度上限
const failureRate = token.total_requests > 0
? (token.failed_requests || 0) / token.total_requests
: 0;
const quota = {
total: totalQuota,
used: estimatedUsed,
remaining: remaining,
plan_type: planType,
failure_rate: Math.round(failureRate * 100)
};
// 更新数据库
Token.updateQuota(id, quota);
res.json({
success: true,
quota,
message: '额度已更新(基于请求统计估算)'
});
} catch (error) {
console.error('刷新额度失败:', error);
res.status(500).json({ error: '刷新额度失败: ' + error.message });
}
});
// 批量刷新所有 Token 额度
router.post('/quota/refresh-all', async (req, res) => {
try {
const tokens = Token.getAll();
let successCount = 0;
let failedCount = 0;
for (const token of tokens) {
try {
let planType = 'free';
let totalQuota = 50000;
// 解析 ID Token 获取订阅类型
if (token.id_token) {
try {
const parts = token.id_token.split('.');
if (parts.length === 3) {
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
const authInfo = payload['https://api.openai.com/auth'];
if (authInfo && authInfo.chatgpt_plan_type) {
planType = authInfo.chatgpt_plan_type.toLowerCase();
if (planType.includes('plus') || planType.includes('pro')) {
totalQuota = 500000;
} else if (planType.includes('team')) {
totalQuota = 1000000;
}
}
}
} catch (e) {
// 忽略解析错误
}
}
const estimatedUsed = (token.success_requests || 0) * 100;
const remaining = Math.max(0, totalQuota - estimatedUsed);
const quota = {
total: totalQuota,
used: estimatedUsed,
remaining: remaining
};
Token.updateQuota(token.id, quota);
successCount++;
} catch (error) {
console.error(`刷新 Token ${token.id} 额度失败:`, error);
failedCount++;
}
}
res.json({
success: true,
total: tokens.length,
success: successCount,
failed: failedCount,
message: `批量刷新完成:成功 ${successCount} 个,失败 ${failedCount}`
});
} catch (error) {
console.error('批量刷新额度失败:', error);
res.status(500).json({ error: '批量刷新失败: ' + error.message });
}
});
export default router;

View File

@@ -0,0 +1,39 @@
import bcrypt from 'bcrypt';
import db, { initDatabase } from '../config/database.js';
import dotenv from 'dotenv';
dotenv.config();
// 初始化数据库
initDatabase();
// 创建默认管理员账户
const defaultUsername = process.env.ADMIN_USERNAME || 'admin';
const defaultPassword = process.env.ADMIN_PASSWORD || 'admin123';
try {
// 检查是否已存在管理员
const existingUser = db.prepare('SELECT * FROM users WHERE username = ?').get(defaultUsername);
if (!existingUser) {
const hashedPassword = await bcrypt.hash(defaultPassword, 10);
db.prepare('INSERT INTO users (username, password) VALUES (?, ?)').run(
defaultUsername,
hashedPassword
);
console.log('✓ 默认管理员账户已创建');
console.log(` 用户名: ${defaultUsername}`);
console.log(` 密码: ${defaultPassword}`);
console.log(' 请登录后立即修改密码!');
} else {
console.log('✓ 管理员账户已存在');
}
console.log('\n数据库初始化完成');
process.exit(0);
} catch (error) {
console.error('❌ 初始化失败:', error);
process.exit(1);
}

View File

@@ -1,10 +1,16 @@
import fs from 'fs/promises';
import axios from 'axios';
import httpsProxyAgent from 'https-proxy-agent';
const { HttpsProxyAgent } = httpsProxyAgent;
// OpenAI OAuth 配置
const TOKEN_URL = 'https://auth.openai.com/oauth/token';
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
// 代理配置
const PROXY_URL = process.env.HTTP_PROXY || process.env.HTTPS_PROXY;
/**
* Token 管理器
*/
@@ -45,10 +51,10 @@ class TokenManager {
* 检查 token 是否过期
*/
isTokenExpired() {
if (!this.tokenData || !this.tokenData.expired) {
if (!this.tokenData || !this.tokenData.expired_at) {
return true;
}
const expireTime = new Date(this.tokenData.expired);
const expireTime = new Date(this.tokenData.expired_at);
const now = new Date();
// 提前 5 分钟刷新
return expireTime.getTime() - now.getTime() < 5 * 60 * 1000;
@@ -72,12 +78,20 @@ class TokenManager {
scope: 'openid profile email'
});
const response = await axios.post(TOKEN_URL, params.toString(), {
const config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json'
}
});
};
// 如果配置了代理,使用代理
if (PROXY_URL) {
config.httpsAgent = new HttpsProxyAgent(PROXY_URL);
console.log(`使用代理: ${PROXY_URL}`);
}
const response = await axios.post(TOKEN_URL, params.toString(), config);
const { access_token, refresh_token, id_token, expires_in } = response.data;
@@ -87,8 +101,8 @@ class TokenManager {
access_token,
refresh_token: refresh_token || this.tokenData.refresh_token,
id_token: id_token || this.tokenData.id_token,
expired: new Date(Date.now() + expires_in * 1000).toISOString(),
last_refresh: new Date().toISOString()
expired_at: new Date(Date.now() + expires_in * 1000).toISOString(),
last_refresh_at: new Date().toISOString()
};
await this.saveToken(newTokenData);
@@ -123,7 +137,7 @@ class TokenManager {
return {
email: this.tokenData?.email,
account_id: this.tokenData?.account_id,
expired: this.tokenData?.expired,
expired_at: this.tokenData?.expired_at,
type: this.tokenData?.type
};
}