feat: 添加批量删除、最近活动、界面优化等功能
This commit is contained in:
131
src/config/database.js
Normal file
131
src/config/database.js
Normal 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;
|
||||
226
src/index.js
226
src/index.js
@@ -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
69
src/middleware/auth.js
Normal 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
181
src/models/index.js
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
68
src/routes/apiKeys.js
Normal 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
111
src/routes/auth.js
Normal 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
75
src/routes/settings.js
Normal 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
254
src/routes/stats.js
Normal 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
357
src/routes/tokens.js
Normal 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;
|
||||
39
src/scripts/initDatabase.js
Normal file
39
src/scripts/initDatabase.js
Normal 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);
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user