diff --git a/.env.exa b/.env.exa
index c358052..8db8cbf 100644
--- a/.env.exa
+++ b/.env.exa
@@ -6,3 +6,13 @@ TOKEN_FILE=./token.json
# 模型列表文件路径
MODELS_FILE=./models.json
+
+# HTTP 代理(可选,用于 token 刷新)
+# HTTP_PROXY=http://127.0.0.1:7890
+# HTTPS_PROXY=http://127.0.0.1:7890
+
+# 负载均衡策略(可选)
+# round-robin: 轮询(默认)
+# random: 随机选择
+# least-used: 最少使用(选择请求次数最少的账号)
+LOAD_BALANCE_STRATEGY=round-robin
diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md
new file mode 100644
index 0000000..c065481
--- /dev/null
+++ b/DEPLOYMENT.md
@@ -0,0 +1,371 @@
+# GPT2API Node - 部署文档
+
+## 🎉 系统功能
+
+### 核心功能
+- ✅ OpenAI Codex 反向代理服务
+- ✅ 完整的 Web 管理后台
+- ✅ 多账号管理和批量操作
+- ✅ 自动 Token 刷新机制
+- ✅ 负载均衡(轮询/随机/最少使用)
+- ✅ API Key 管理和认证
+- ✅ 请求统计和数据分析
+- ✅ 实时活动记录
+
+### 管理后台功能
+
+#### 1. 仪表盘
+- 系统概览和实时统计
+- API Keys 数量
+- Token 账号数量
+- 今日请求数和成功率
+- 最近活动记录(API请求、账号添加等)
+
+#### 2. API Keys 管理
+- 创建和管理 API Keys
+- 查看使用统计
+- 启用/禁用 API Key
+- 删除 API Key
+
+#### 3. 账号管理
+- **批量导入 Token**(支持 JSON 文件和多文件)
+- **批量删除账号**(支持多选)
+- 手动添加账号
+- 查看账号额度和使用情况
+- 刷新账号额度(单个/全部)
+- 负载均衡策略配置
+- 账号总数实时显示
+
+#### 4. 数据分析
+- **请求量趋势图表**(基于真实数据)
+- 模型使用分布
+- 账号详细统计(带滚动条)
+- API 请求日志(带滚动条)
+- 支持时间范围筛选(24小时/7天/30天)
+
+#### 5. 系统设置
+- 修改管理员密码
+- 负载均衡策略设置
+- GitHub 项目链接
+
+## 🚀 快速部署
+
+### 1. 环境要求
+- Node.js 16+
+- npm 或 yarn
+
+### 2. 安装步骤
+
+```bash
+# 克隆项目
+git clone https://github.com/lulistart/gpt2api-node.git
+cd gpt2api-node
+
+# 安装依赖
+npm install
+
+# 初始化数据库
+npm run init-db
+
+# 启动服务
+npm start
+```
+
+### 3. 访问管理后台
+
+打开浏览器访问:`http://localhost:3000/admin`
+
+默认管理员账户:
+- 用户名:`admin`
+- 密码:`admin123`
+
+**重要**:首次登录后请立即修改密码!
+
+## 📁 项目结构
+
+```
+gpt2api-node/
+├── src/
+│ ├── config/
+│ │ └── database.js # 数据库配置和初始化
+│ ├── middleware/
+│ │ └── auth.js # 认证中间件
+│ ├── models/
+│ │ └── index.js # 数据模型(User、ApiKey、Token、ApiLog)
+│ ├── routes/
+│ │ ├── auth.js # 认证路由(登录、登出、修改密码)
+│ │ ├── apiKeys.js # API Keys 管理路由
+│ │ ├── tokens.js # Tokens 管理路由(含批量删除)
+│ │ ├── stats.js # 统计路由(含最近活动)
+│ │ └── settings.js # 设置路由
+│ ├── scripts/
+│ │ └── initDatabase.js # 数据库初始化脚本
+│ ├── index.js # 主入口文件
+│ ├── tokenManager.js # Token 管理模块
+│ └── proxyHandler.js # 代理处理模块
+├── public/
+│ └── admin/
+│ ├── login.html # 登录页面
+│ ├── index.html # 管理后台主页
+│ └── js/
+│ └── admin.js # 管理后台脚本
+├── database/
+│ └── app.db # SQLite 数据库
+├── models.json # 模型配置
+├── package.json
+├── README.md
+└── DEPLOYMENT.md
+```
+
+## 🔧 配置说明
+
+### 环境变量
+
+创建 `.env` 文件:
+
+```env
+# 服务端口
+PORT=3000
+
+# Session 密钥(生产环境必须修改)
+SESSION_SECRET=your-random-secret-key-change-in-production
+
+# 负载均衡策略:round-robin(轮询)、random(随机)、least-used(最少使用)
+LOAD_BALANCE_STRATEGY=round-robin
+
+# 模型配置文件
+MODELS_FILE=./models.json
+
+# 数据库路径
+DATABASE_PATH=./database/app.db
+```
+
+### 负载均衡策略
+
+支持三种策略:
+
+1. **round-robin(轮询)**:按顺序依次使用每个账号,默认策略
+2. **random(随机)**:随机选择一个可用账号
+3. **least-used(最少使用)**:选择请求次数最少的账号
+
+可通过环境变量或管理后台配置。
+
+## 📊 数据库结构
+
+### users 表
+- 管理员账户信息
+- 字段:id, username, password_hash, created_at
+
+### api_keys 表
+- API 密钥管理
+- 字段:id, name, key, is_active, usage_count, last_used_at, created_at
+
+### tokens 表
+- OpenAI Token 账户
+- 字段:id, name, email, account_id, access_token, refresh_token, id_token, expired_at, last_refresh_at, is_active, total_requests, success_requests, failed_requests, quota_total, quota_used, quota_remaining, created_at
+
+### api_logs 表
+- API 请求日志
+- 字段:id, api_key_id, token_id, model, endpoint, status_code, error_message, created_at
+
+## 🔐 安全建议
+
+### 生产环境配置
+
+1. **修改默认密码**
+ - 首次登录后立即修改管理员密码
+ - 使用强密码(至少8位,包含大小写字母、数字、特殊字符)
+
+2. **设置环境变量**
+ ```bash
+ SESSION_SECRET=$(openssl rand -base64 32)
+ ```
+
+3. **启用 HTTPS**
+ - 使用 Nginx 或 Caddy 作为反向代理
+ - 配置 SSL 证书
+ - 设置 `cookie.secure = true`
+
+4. **防火墙配置**
+ - 只开放必要的端口
+ - 限制管理后台访问 IP
+
+5. **定期备份**
+ - 备份 `database/app.db` 数据库文件
+ - 备份环境变量配置
+
+### Nginx 反向代理示例
+
+```nginx
+server {
+ listen 80;
+ server_name your-domain.com;
+ return 301 https://$server_name$request_uri;
+}
+
+server {
+ listen 443 ssl http2;
+ server_name your-domain.com;
+
+ ssl_certificate /path/to/cert.pem;
+ ssl_certificate_key /path/to/key.pem;
+
+ location / {
+ proxy_pass http://localhost:3000;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host $host;
+ proxy_cache_bypass $http_upgrade;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+}
+```
+
+## 🎯 使用指南
+
+### 1. 创建 API Key
+
+1. 登录管理后台
+2. 进入 **API Keys** 页面
+3. 点击 **创建 API Key**
+4. 输入名称(可选)
+5. 复制生成的 API Key(只显示一次)
+
+### 2. 导入 Token 账号
+
+#### 方式一:批量导入 JSON
+
+1. 准备 JSON 文件:
+```json
+[
+ {
+ "access_token": "your_access_token",
+ "refresh_token": "your_refresh_token",
+ "id_token": "your_id_token",
+ "account_id": "account_id",
+ "email": "email@example.com",
+ "name": "账号名称"
+ }
+]
+```
+
+2. 进入 **账号管理** 页面
+3. 点击 **导入 JSON**
+4. 选择文件或粘贴 JSON 内容
+5. 点击 **预览导入**
+6. 确认后点击 **确认导入**
+
+#### 方式二:手动添加
+
+1. 进入 **账号管理** 页面
+2. 点击 **手动添加**
+3. 填写 Access Token 和 Refresh Token
+4. 点击 **添加**
+
+### 3. 批量删除账号
+
+1. 进入 **账号管理** 页面
+2. 勾选要删除的账号
+3. 点击 **删除选中** 按钮
+4. 确认删除
+
+### 4. 使用 API
+
+```bash
+curl http://localhost:3000/v1/chat/completions \
+ -H "Authorization: Bearer YOUR_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "model": "gpt-5.3-codex",
+ "messages": [
+ {"role": "user", "content": "Hello!"}
+ ]
+ }'
+```
+
+## 🐛 故障排除
+
+### 无法访问管理后台
+
+1. 检查服务是否启动:`npm start`
+2. 检查端口是否被占用:`netstat -ano | findstr :3000`
+3. 检查防火墙设置
+
+### 数据库初始化失败
+
+```bash
+# 删除旧数据库
+rm database/app.db
+
+# 重新初始化
+npm run init-db
+```
+
+### Token 刷新失败
+
+1. 检查网络连接
+2. 确认 refresh_token 是否有效
+3. 重新导入新的 token
+
+### API 请求失败
+
+1. 检查 API Key 是否正确
+2. 确保有可用的 Token 账号
+3. 查看管理后台的请求日志
+4. 检查账号是否被禁用
+
+### 请求趋势图表显示异常
+
+- 图表数据基于 `api_logs` 表的真实请求记录
+- 如果没有请求记录,图表会显示为空
+- 发送几次 API 请求后刷新页面查看
+
+## 📝 维护建议
+
+1. **定期备份数据库**
+ ```bash
+ cp database/app.db database/app.db.backup.$(date +%Y%m%d)
+ ```
+
+2. **监控日志**
+ - 查看终端输出
+ - 检查请求日志
+
+3. **更新依赖**
+ ```bash
+ npm update
+ ```
+
+4. **清理旧日志**
+ - 定期清理 `api_logs` 表中的旧记录
+
+## 🔄 更新日志
+
+### v2.0.0 (2026-02-17)
+- ✅ 添加批量删除账号功能
+- ✅ 添加仪表盘最近活动记录
+- ✅ 添加 GitHub 项目链接
+- ✅ 移除前台页面,根路径重定向到管理后台
+- ✅ 修复模型列表(删除不存在的 gpt-5.3-codex-spark)
+- ✅ 优化终端日志输出
+- ✅ 账号管理页面显示账号总数
+- ✅ 账号详细统计和请求日志添加滚动条
+- ✅ 修复请求趋势图表,使用真实数据
+
+### v1.0.0
+- ✅ 基础管理系统
+- ✅ API Keys 管理
+- ✅ Tokens 管理
+- ✅ 数据统计
+
+## 📞 支持
+
+- GitHub: https://github.com/lulistart/gpt2api-node
+- Issues: https://github.com/lulistart/gpt2api-node/issues
+
+## 📄 许可证
+
+MIT License
diff --git a/README.md b/README.md
index 9d3ef71..e1c75e6 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,55 @@
# GPT2API Node
-基于 Node.js + Express 的 OpenAI Codex 反向代理服务,支持 JSON 文件导入 token,自动刷新 token,提供 OpenAI 兼容的 API 接口。
+基于 Node.js + Express 的 OpenAI Codex 反向代理服务,支持多账号管理、自动刷新 token、负载均衡,提供 OpenAI 兼容的 API 接口和完整的管理后台。
+
+## 界面预览
+
+
+
+
+
+ 管理员登录
+ |
+
+
+ 仪表盘
+ |
+
+
+
+
+ API Keys 管理
+ |
+
+
+ 账号管理
+ |
+
+
+
+
+ 数据分析
+ |
+
+
+ 系统设置
+ |
+
+
## 功能特性
- ✅ OpenAI Codex 反向代理
+- ✅ 完整的 Web 管理后台
+- ✅ 多账号管理和批量导入
- ✅ 自动 Token 刷新机制
+- ✅ 负载均衡(轮询/随机/最少使用)
+- ✅ API Key 管理和认证
+- ✅ 请求统计和数据分析
- ✅ 支持流式和非流式响应
- ✅ OpenAI API 兼容接口
-- ✅ JSON 文件导入 Token
-- ✅ 简单易用的配置
+- ✅ 批量删除账号功能
+- ✅ 实时活动记录
## 快速开始
@@ -20,37 +60,17 @@ cd gpt2api-node
npm install
```
-### 2. 配置 Token
-
-从 CLIProxyAPI 或其他来源获取 token 文件,复制到项目根目录并命名为 `token.json`:
-
-```json
-{
- "id_token": "your_id_token_here",
- "access_token": "your_access_token_here",
- "refresh_token": "your_refresh_token_here",
- "account_id": "your_account_id",
- "email": "your_email@example.com",
- "type": "codex",
- "expired": "2026-12-31T23:59:59.000Z",
- "last_refresh": "2026-01-01T00:00:00.000Z"
-}
-```
-
-### 3. 配置环境变量(可选)
-
-复制 `.env.example` 为 `.env` 并修改配置:
+### 2. 初始化数据库
```bash
-cp .env.example .env
+npm run init-db
```
-```env
-PORT=3000
-TOKEN_FILE=./token.json
-```
+默认管理员账户:
+- 用户名:`admin`
+- 密码:`admin123`
-### 4. 启动服务
+### 3. 启动服务
```bash
npm start
@@ -62,16 +82,71 @@ npm start
npm run dev
```
+### 4. 访问管理后台
+
+打开浏览器访问:`http://localhost:3000/admin`
+
+使用默认账户登录后,请立即修改密码。
+
+## 管理后台功能
+
+### 仪表盘
+- 系统概览和实时统计
+- API Keys 数量
+- Token 账号数量
+- 今日请求数和成功率
+- 最近活动记录
+
+### API Keys 管理
+- 创建和管理 API Keys
+- 查看使用统计
+- 启用/禁用 API Key
+
+### 账号管理
+- 批量导入 Token(支持 JSON 文件)
+- 手动添加账号
+- 批量删除账号
+- 查看账号额度和使用情况
+- 刷新账号额度
+- 负载均衡策略配置
+
+### 数据分析
+- 请求量趋势图表
+- 模型使用分布
+- 账号详细统计
+- API 请求日志
+
+### 系统设置
+- 修改管理员密码
+- 负载均衡策略设置
+
+## 负载均衡策略
+
+支持三种负载均衡策略:
+
+1. **轮询(round-robin)**:按顺序依次使用每个账号
+2. **随机(random)**:随机选择一个可用账号
+3. **最少使用(least-used)**:选择请求次数最少的账号
+
+可在管理后台的账号管理页面或通过环境变量配置。
+
## API 接口
### 聊天完成接口
**端点**: `POST /v1/chat/completions`
+**请求头**:
+```
+Authorization: Bearer YOUR_API_KEY
+Content-Type: application/json
+```
+
**请求示例**:
```bash
curl http://localhost:3000/v1/chat/completions \
+ -H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.3-codex",
@@ -86,6 +161,7 @@ curl http://localhost:3000/v1/chat/completions \
```bash
curl http://localhost:3000/v1/chat/completions \
+ -H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.3-codex",
@@ -115,7 +191,6 @@ curl http://localhost:3000/health
## 支持的模型
- `gpt-5.3-codex` - GPT 5.3 Codex(最新)
-- `gpt-5.3-codex-spark` - GPT 5.3 Codex Spark(超快速编码模型)
- `gpt-5.2` - GPT 5.2
- `gpt-5.2-codex` - GPT 5.2 Codex
- `gpt-5.1` - GPT 5.1
@@ -130,12 +205,12 @@ curl http://localhost:3000/health
Cherry Studio 是一个支持多种 AI 服务的桌面客户端。配置步骤:
-### 1. 启动代理服务
+### 1. 创建 API Key
-```bash
-cd gpt2api-node
-npm start
-```
+1. 访问管理后台:`http://localhost:3000/admin`
+2. 进入 **API Keys** 页面
+3. 点击 **创建 API Key**
+4. 复制生成的 API Key(只显示一次)
### 2. 在 Cherry Studio 中配置
@@ -145,22 +220,13 @@ npm start
4. 填写配置:
- **名称**: GPT2API Node(或自定义名称)
- **API 地址**: `http://localhost:3000/v1`
- - **API Key**: 随意填写(如 `dummy`),不会被验证
+ - **API Key**: 粘贴刚才创建的 API Key
- **模型**: 选择或手动输入模型名称(如 `gpt-5.3-codex`)
### 3. 开始使用
配置完成后,在 Cherry Studio 中选择刚才添加的提供商和模型,即可开始对话。
-### 可用模型列表
-
-在 Cherry Studio 中可以使用以下任意模型:
-- `gpt-5.3-codex` - 推荐,最新版本
-- `gpt-5.3-codex-spark` - 超快速编码
-- `gpt-5.2-codex` - 稳定版本
-- `gpt-5.1-codex` - 较旧版本
-- 其他 GPT-5 系列模型
-
## 使用示例
### Python
@@ -170,7 +236,7 @@ import openai
client = openai.OpenAI(
base_url="http://localhost:3000/v1",
- api_key="dummy" # 不需要真实的 API key
+ api_key="YOUR_API_KEY"
)
response = client.chat.completions.create(
@@ -190,7 +256,7 @@ import OpenAI from 'openai';
const client = new OpenAI({
baseURL: 'http://localhost:3000/v1',
- apiKey: 'dummy'
+ apiKey: 'YOUR_API_KEY'
});
const response = await client.chat.completions.create({
@@ -203,77 +269,115 @@ const response = await client.chat.completions.create({
console.log(response.choices[0].message.content);
```
-### cURL
+## Token 管理
-```bash
-curl http://localhost:3000/v1/chat/completions \
- -H "Content-Type: application/json" \
- -d '{
- "model": "gpt-5.3-codex",
- "messages": [
- {"role": "system", "content": "You are a helpful assistant."},
- {"role": "user", "content": "What is the capital of France?"}
- ]
- }'
+### 批量导入
+
+1. 准备 JSON 文件,格式如下:
+
+```json
+[
+ {
+ "access_token": "your_access_token",
+ "refresh_token": "your_refresh_token",
+ "id_token": "your_id_token",
+ "account_id": "account_id",
+ "email": "email@example.com",
+ "name": "账号名称"
+ }
+]
```
-## Token 管理
+2. 在管理后台的账号管理页面点击 **导入 JSON**
+3. 选择文件或粘贴 JSON 内容
+4. 预览后确认导入
+
+### 手动添加
+
+在管理后台的账号管理页面点击 **手动添加**,填写必要信息。
### 自动刷新
-服务会自动检测 token 是否过期(提前 5 分钟),并在需要时自动刷新。刷新后的 token 会自动保存到文件中。
+服务会自动检测 token 是否过期,并在需要时自动刷新。
-### 手动导入
+## 环境变量配置
-如果你有从 CLIProxyAPI 导出的 token 文件,直接复制为 `token.json` 即可使用。
+创建 `.env` 文件:
-### Token 文件格式
-
-Token 文件必须包含以下字段:
-
-- `access_token`: 访问令牌
-- `refresh_token`: 刷新令牌
-- `id_token`: ID 令牌(可选)
-- `account_id`: 账户 ID(可选)
-- `email`: 邮箱(可选)
-- `expired`: 过期时间(ISO 8601 格式)
-- `type`: 类型(固定为 "codex")
+```env
+PORT=3000
+SESSION_SECRET=your-secret-key-change-in-production
+LOAD_BALANCE_STRATEGY=round-robin
+MODELS_FILE=./models.json
+```
## 项目结构
```
gpt2api-node/
├── src/
-│ ├── index.js # 主服务器文件
-│ ├── tokenManager.js # Token 管理模块
-│ └── proxyHandler.js # 代理处理模块
+│ ├── index.js # 主服务器文件
+│ ├── tokenManager.js # Token 管理模块
+│ ├── proxyHandler.js # 代理处理模块
+│ ├── config/
+│ │ └── database.js # 数据库配置
+│ ├── models/
+│ │ └── index.js # 数据模型
+│ ├── routes/
+│ │ ├── auth.js # 认证路由
+│ │ ├── apiKeys.js # API Keys 路由
+│ │ ├── tokens.js # Tokens 路由
+│ │ ├── stats.js # 统计路由
+│ │ └── settings.js # 设置路由
+│ ├── middleware/
+│ │ └── auth.js # 认证中间件
+│ └── scripts/
+│ └── initDatabase.js # 数据库初始化脚本
+├── public/
+│ └── admin/ # 管理后台前端
+│ ├── index.html
+│ ├── login.html
+│ └── js/
+│ └── admin.js
+├── database/
+│ └── app.db # SQLite 数据库
+├── models.json # 模型配置
├── package.json
-├── .env.example
-├── token.example.json
-├── .gitignore
└── README.md
```
## 注意事项
-1. **Token 安全**: 请妥善保管 `token.json` 文件,不要提交到版本控制系统
+1. **安全性**:
+ - 首次登录后请立即修改管理员密码
+ - 妥善保管 API Keys
+ - 生产环境请使用 HTTPS
+
2. **网络要求**: 需要能够访问 `chatgpt.com` 和 `auth.openai.com`
+
3. **Token 有效期**: Token 会自动刷新,但如果 refresh_token 失效,需要重新获取
+
4. **并发限制**: 根据 OpenAI 账户限制,注意控制并发请求数量
## 故障排除
-### Token 加载失败
+### 无法访问管理后台
-确保 `token.json` 文件存在且格式正确,参考 `token.example.json`。
+确保服务已启动,访问 `http://localhost:3000/admin`
+
+### 数据库初始化失败
+
+删除 `database/app.db` 文件,重新运行 `npm run init-db`
### Token 刷新失败
-可能是 refresh_token 已过期,需要重新从 CLIProxyAPI 获取新的 token。
+可能是 refresh_token 已过期,需要重新导入新的 token
-### 代理请求失败
+### API 请求失败
-检查网络连接,确保能够访问 OpenAI 服务。
+1. 检查 API Key 是否正确
+2. 确保有可用的 Token 账号
+3. 查看管理后台的请求日志
## 许可证
diff --git a/UI.md b/UI.md
new file mode 100644
index 0000000..dc24ad5
--- /dev/null
+++ b/UI.md
@@ -0,0 +1,99 @@
+🎨 UI 设计风格分析
+kiro-unified-manager 的 UI 特点:
+
+现代简约风格 - 使用 Tailwind CSS,干净的卡片布局
+专业的管理后台风格 - 类似企业级 SaaS 产品
+优雅的动画效果 - 淡入淡出、滑动过渡
+响应式设计 - 适配不同屏幕尺寸
+直观的状态指示 - 颜色编码的状态标签
+丰富的交互反馈 - 悬停效果、加载状态
+📝 UI 提示词模板
+基础风格提示词
+请设计一个现代化的管理后台界面,具有以下特点:
+
+**整体风格:**
+- 使用简约现代的设计语言,类似 Tailwind CSS 风格
+- 采用卡片式布局,白色背景配合浅灰色边框
+- 整体配色以蓝色系为主色调(#3b82f6),搭配灰色系辅助色
+- 界面干净整洁,留白充足,视觉层次清晰
+
+**布局结构:**
+- 顶部导航栏:白色背景,包含 Logo、导航标签和用户信息
+- 主内容区域:浅灰色背景(#f9fafb),居中布局,最大宽度限制
+- 卡片容器:白色背景,圆角边框,轻微阴影效果
+
+**交互元素:**
+- 按钮:圆角设计,主要按钮使用蓝色,次要按钮使用灰色
+- 表格:斑马纹效果,悬停高亮
+- 表单:简洁的输入框,聚焦时蓝色边框
+- 状态标签:彩色圆角标签,不同状态使用不同颜色
+
+**动画效果:**
+- 页面切换使用淡入淡出效果
+- 按钮悬停有颜色过渡
+- 模态框弹出有缩放动画
+- 通知消息从右上角滑入
+
+**响应式设计:**
+- 移动端友好,自适应布局
+- 表格在小屏幕上可横向滚动
+- 导航在移动端可折叠
+具体组件提示词
+**导航栏设计:**
+创建一个顶部导航栏,白色背景,高度64px,包含:
+- 左侧:品牌 Logo + 图标,使用蓝色主题
+- 中间:水平导航标签,激活状态有蓝色下边框
+- 右侧:用户头像 + 下拉菜单
+
+**数据表格设计:**
+设计一个现代化数据表格:
+- 表头使用浅灰色背景
+- 行间隔使用斑马纹效果
+- 悬停行高亮显示
+- 状态列使用彩色圆角标签
+- 操作列包含彩色文字链接
+
+**模态框设计:**
+创建优雅的模态框:
+- 半透明黑色遮罩背景
+- 白色圆角内容区域
+- 顶部标题栏带关闭按钮
+- 底部操作按钮区域
+- 弹出时有缩放动画效果
+
+**通知消息设计:**
+设计右上角通知组件:
+- 固定定位在右上角
+- 彩色背景(成功绿色、错误红色、信息蓝色)
+- 白色文字,包含图标
+- 自动消失,滑入滑出动画
+
+**按钮系统:**
+- 主要按钮:蓝色背景,白色文字,悬停变深蓝
+- 次要按钮:灰色边框,灰色文字,悬停背景变浅灰
+- 危险按钮:红色背景,用于删除等操作
+- 禁用状态:50% 透明度,不可点击
+颜色系统提示词
+**配色方案:**
+- 主色调:蓝色系 (#3b82f6, #2563eb, #1d4ed8)
+- 成功色:绿色系 (#10b981, #059669)
+- 警告色:黄色系 (#f59e0b, #d97706)
+- 错误色:红色系 (#ef4444, #dc2626)
+- 中性色:灰色系 (#6b7280, #4b5563, #374151)
+- 背景色:浅灰色 (#f9fafb, #f3f4f6)
+- 文字色:深灰色 (#111827, #374151, #6b7280)
+动画效果提示词
+**动画系统:**
+- 页面过渡:300ms 淡入淡出效果
+- 按钮悬停:200ms 颜色过渡
+- 模态框:弹出时 0.3s 缩放动画,从 0.95 到 1.0
+- 通知消息:从右侧滑入,3秒后自动滑出
+- 加载状态:旋转动画的 spinner 图标
+- 表格行悬停:背景色平滑过渡
+🚀 使用建议
+当你要创建类似风格的网站时,可以这样使用提示词:
+
+基础搭建:先使用"基础风格提示词"建立整体框架
+组件细化:根据需要使用"具体组件提示词"
+样式调整:参考"颜色系统"和"动画效果"进行微调
+技术栈:建议使用 Tailwind CSS + Vue.js 或 React
\ No newline at end of file
diff --git a/database/app.db b/database/app.db
new file mode 100644
index 0000000..bd03e64
Binary files /dev/null and b/database/app.db differ
diff --git a/models.json b/models.json
index 65e143d..0061b66 100644
--- a/models.json
+++ b/models.json
@@ -5,12 +5,6 @@
"created": 1770307200,
"owned_by": "openai"
},
- {
- "id": "gpt-5.3-codex-spark",
- "object": "model",
- "created": 1770912000,
- "owned_by": "openai"
- },
{
"id": "gpt-5.2",
"object": "model",
diff --git a/package-lock.json b/package-lock.json
index ed22194..b75769c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,10 +10,91 @@
"license": "MIT",
"dependencies": {
"axios": "^1.6.0",
+ "bcrypt": "^5.1.1",
+ "better-sqlite3": "^9.2.2",
+ "cookie-parser": "^1.4.6",
"dotenv": "^16.3.1",
- "express": "^4.18.2"
+ "express": "^4.18.2",
+ "express-session": "^1.17.3",
+ "https-proxy-agent": "^7.0.2",
+ "jsonwebtoken": "^9.0.2",
+ "multer": "^1.4.5-lts.1"
}
},
+ "node_modules/@mapbox/node-pre-gyp": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmmirror.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
+ "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "https-proxy-agent": "^5.0.0",
+ "make-dir": "^3.1.0",
+ "node-fetch": "^2.6.7",
+ "nopt": "^5.0.0",
+ "npmlog": "^5.0.1",
+ "rimraf": "^3.0.2",
+ "semver": "^7.3.5",
+ "tar": "^6.1.11"
+ },
+ "bin": {
+ "node-pre-gyp": "bin/node-pre-gyp"
+ }
+ },
+ "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/@mapbox/node-pre-gyp/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/@mapbox/node-pre-gyp/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+ "license": "ISC"
+ },
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
@@ -27,6 +108,64 @@
"node": ">= 0.6"
}
},
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/append-field": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/append-field/-/append-field-1.0.0.tgz",
+ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
+ "license": "MIT"
+ },
+ "node_modules/aproba": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/aproba/-/aproba-2.1.0.tgz",
+ "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
+ "license": "ISC"
+ },
+ "node_modules/are-we-there-yet": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
+ "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
+ "deprecated": "This package is no longer supported.",
+ "license": "ISC",
+ "dependencies": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/are-we-there-yet/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -50,6 +189,91 @@
"proxy-from-env": "^1.1.0"
}
},
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/bcrypt": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmmirror.com/bcrypt/-/bcrypt-5.1.1.tgz",
+ "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "@mapbox/node-pre-gyp": "^1.0.11",
+ "node-addon-api": "^5.0.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/better-sqlite3": {
+ "version": "9.6.0",
+ "resolved": "https://registry.npmmirror.com/better-sqlite3/-/better-sqlite3-9.6.0.tgz",
+ "integrity": "sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "bindings": "^1.5.0",
+ "prebuild-install": "^7.1.1"
+ }
+ },
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmmirror.com/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/bl/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/body-parser": {
"version": "1.20.4",
"resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.4.tgz",
@@ -74,6 +298,63 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "license": "MIT"
+ },
+ "node_modules/busboy": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz",
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+ "dependencies": {
+ "streamsearch": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.16.0"
+ }
+ },
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
@@ -112,6 +393,24 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/color-support": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmmirror.com/color-support/-/color-support-1.1.3.tgz",
+ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
+ "license": "ISC",
+ "bin": {
+ "color-support": "bin.js"
+ }
+ },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -124,6 +423,33 @@
"node": ">= 0.8"
}
},
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "license": "MIT"
+ },
+ "node_modules/concat-stream": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmmirror.com/concat-stream/-/concat-stream-1.6.2.tgz",
+ "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+ "engines": [
+ "node >= 0.8"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "node_modules/console-control-strings": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/console-control-strings/-/console-control-strings-1.1.0.tgz",
+ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
+ "license": "ISC"
+ },
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -154,12 +480,37 @@
"node": ">= 0.6"
}
},
+ "node_modules/cookie-parser": {
+ "version": "1.4.7",
+ "resolved": "https://registry.npmmirror.com/cookie-parser/-/cookie-parser-1.4.7.tgz",
+ "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "0.7.2",
+ "cookie-signature": "1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/cookie-parser/node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "license": "MIT"
+ },
"node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "license": "MIT"
+ },
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz",
@@ -169,6 +520,30 @@
"ms": "2.0.0"
}
},
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmmirror.com/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -178,6 +553,12 @@
"node": ">=0.4.0"
}
},
+ "node_modules/delegates": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
+ "license": "MIT"
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
@@ -197,6 +578,15 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz",
@@ -223,12 +613,27 @@
"node": ">= 0.4"
}
},
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
@@ -238,6 +643,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -298,6 +712,15 @@
"node": ">= 0.6"
}
},
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "license": "(MIT OR WTFPL)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/express": {
"version": "4.22.1",
"resolved": "https://registry.npmmirror.com/express/-/express-4.22.1.tgz",
@@ -344,6 +767,35 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/express-session": {
+ "version": "1.19.0",
+ "resolved": "https://registry.npmmirror.com/express-session/-/express-session-1.19.0.tgz",
+ "integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "~0.7.2",
+ "cookie-signature": "~1.0.7",
+ "debug": "~2.6.9",
+ "depd": "~2.0.0",
+ "on-headers": "~1.1.0",
+ "parseurl": "~1.3.3",
+ "safe-buffer": "~5.2.1",
+ "uid-safe": "~2.1.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "license": "MIT"
+ },
"node_modules/finalhandler": {
"version": "1.3.2",
"resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.2.tgz",
@@ -416,6 +868,42 @@
"node": ">= 0.6"
}
},
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "license": "MIT"
+ },
+ "node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/fs-minipass/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "license": "ISC"
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
@@ -425,6 +913,27 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/gauge": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmmirror.com/gauge/-/gauge-3.0.2.tgz",
+ "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
+ "deprecated": "This package is no longer supported.",
+ "license": "ISC",
+ "dependencies": {
+ "aproba": "^1.0.3 || ^2.0.0",
+ "color-support": "^1.1.2",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.1",
+ "object-assign": "^4.1.1",
+ "signal-exit": "^3.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1",
+ "wide-align": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -462,6 +971,33 @@
"node": ">= 0.4"
}
},
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmmirror.com/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+ "license": "MIT"
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
@@ -501,6 +1037,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/has-unicode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/has-unicode/-/has-unicode-2.0.1.tgz",
+ "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
+ "license": "ISC"
+ },
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
@@ -533,6 +1075,42 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/https-proxy-agent/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -545,12 +1123,49 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -560,6 +1175,136 @@
"node": ">= 0.10"
}
},
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
+ "license": "MIT",
+ "dependencies": {
+ "jws": "^4.0.1",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jsonwebtoken/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz",
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+ "license": "MIT"
+ },
+ "node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -629,12 +1374,122 @@
"node": ">= 0.6"
}
},
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/minipass/-/minipass-5.0.0.tgz",
+ "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmmirror.com/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minizlib/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmmirror.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
+ "node_modules/multer": {
+ "version": "1.4.5-lts.2",
+ "resolved": "https://registry.npmmirror.com/multer/-/multer-1.4.5-lts.2.tgz",
+ "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
+ "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.",
+ "license": "MIT",
+ "dependencies": {
+ "append-field": "^1.0.0",
+ "busboy": "^1.0.0",
+ "concat-stream": "^1.5.2",
+ "mkdirp": "^0.5.4",
+ "object-assign": "^4.1.1",
+ "type-is": "^1.6.4",
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/napi-build-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
+ "license": "MIT"
+ },
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz",
@@ -644,6 +1499,81 @@
"node": ">= 0.6"
}
},
+ "node_modules/node-abi": {
+ "version": "3.87.0",
+ "resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.87.0.tgz",
+ "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-5.1.0.tgz",
+ "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
+ "license": "MIT"
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/nopt": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/nopt/-/nopt-5.0.0.tgz",
+ "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "1"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/npmlog": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmmirror.com/npmlog/-/npmlog-5.0.1.tgz",
+ "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
+ "deprecated": "This package is no longer supported.",
+ "license": "ISC",
+ "dependencies": {
+ "are-we-there-yet": "^2.0.0",
+ "console-control-strings": "^1.1.0",
+ "gauge": "^3.0.0",
+ "set-blocking": "^2.0.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -668,6 +1598,24 @@
"node": ">= 0.8"
}
},
+ "node_modules/on-headers": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/on-headers/-/on-headers-1.1.0.tgz",
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
@@ -677,12 +1625,53 @@
"node": ">= 0.8"
}
},
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
+ "node_modules/prebuild-install": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmmirror.com/prebuild-install/-/prebuild-install-7.1.3.tgz",
+ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^2.0.0",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "license": "MIT"
+ },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -702,6 +1691,16 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
+ "node_modules/pump": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.3.tgz",
+ "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
"node_modules/qs": {
"version": "6.14.2",
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.2.tgz",
@@ -717,6 +1716,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/random-bytes": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/random-bytes/-/random-bytes-1.0.0.tgz",
+ "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
@@ -741,6 +1749,58 @@
"node": ">= 0.8"
}
},
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmmirror.com/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/readable-stream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -767,6 +1827,18 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/send": {
"version": "0.19.2",
"resolved": "https://registry.npmmirror.com/send/-/send-0.19.2.tgz",
@@ -812,6 +1884,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "license": "ISC"
+ },
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -890,6 +1968,57 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "license": "ISC"
+ },
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmmirror.com/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz",
@@ -899,6 +2028,141 @@
"node": ">= 0.8"
}
},
+ "node_modules/streamsearch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz",
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/string_decoder/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tar": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz",
+ "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
+ "license": "ISC",
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^5.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tar-fs": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmmirror.com/tar-fs/-/tar-fs-2.1.4.tgz",
+ "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/tar-fs/node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmmirror.com/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "license": "ISC"
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar-stream/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/tar/node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -908,6 +2172,24 @@
"node": ">=0.6"
}
},
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz",
@@ -921,6 +2203,24 @@
"node": ">= 0.6"
}
},
+ "node_modules/typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmmirror.com/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
+ "license": "MIT"
+ },
+ "node_modules/uid-safe": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmmirror.com/uid-safe/-/uid-safe-2.1.5.tgz",
+ "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
+ "license": "MIT",
+ "dependencies": {
+ "random-bytes": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
@@ -930,6 +2230,12 @@
"node": ">= 0.8"
}
},
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -947,6 +2253,52 @@
"engines": {
"node": ">= 0.8"
}
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/wide-align": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmmirror.com/wide-align/-/wide-align-1.1.5.tgz",
+ "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^1.0.2 || 2 || 3 || 4"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "license": "ISC"
}
}
}
diff --git a/package.json b/package.json
index 6878fb2..8ed44af 100644
--- a/package.json
+++ b/package.json
@@ -1,19 +1,27 @@
{
"name": "gpt2api-node",
"version": "1.0.0",
- "description": "OpenAI Codex reverse proxy with token management",
+ "description": "OpenAI Codex reverse proxy with admin management system",
"main": "src/index.js",
"type": "module",
"scripts": {
"start": "node src/index.js",
- "dev": "node --watch src/index.js"
+ "dev": "node --watch src/index.js",
+ "init-db": "node src/scripts/initDatabase.js"
},
- "keywords": ["openai", "codex", "proxy", "api"],
+ "keywords": ["openai", "codex", "proxy", "api", "admin"],
"author": "",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"axios": "^1.6.0",
- "dotenv": "^16.3.1"
+ "dotenv": "^16.3.1",
+ "better-sqlite3": "^9.2.2",
+ "bcrypt": "^5.1.1",
+ "jsonwebtoken": "^9.0.2",
+ "express-session": "^1.17.3",
+ "multer": "^1.4.5-lts.1",
+ "cookie-parser": "^1.4.6",
+ "https-proxy-agent": "^7.0.2"
}
}
diff --git a/plans/admin-system-architecture.md b/plans/admin-system-architecture.md
new file mode 100644
index 0000000..b9ce7ff
--- /dev/null
+++ b/plans/admin-system-architecture.md
@@ -0,0 +1,306 @@
+# GPT2API Node - API 网关后台管理系统架构设计
+
+## 1. 系统概述
+
+构建一个专业的 API 网关后台管理系统,用于管理 OpenAI Codex 代理服务的用户、API Keys 和 Token 账户。
+
+## 2. 核心功能模块
+
+### 2.1 用户认证模块
+- 管理员登录/登出
+- 密码修改
+- Session 管理
+- JWT Token 认证
+
+### 2.2 API Key 管理模块
+- 创建 API Key(自动生成)
+- 删除 API Key
+- 列表展示(包含创建时间、最后使用时间、使用次数)
+- API Key 权限控制(可选:限流、配额)
+
+### 2.3 Token 账户管理模块
+- JSON 文件导入(支持 CLIProxyAPI 格式)
+- 账户列表展示
+- 账户状态监控(Token 过期时间、刷新状态)
+- 账户删除
+- 自动 Token 刷新
+
+### 2.4 统计监控模块
+- API 调用统计
+- 使用量统计
+- 错误日志
+- 实时状态监控
+
+## 3. 技术架构
+
+### 3.1 后端技术栈
+```
+- Node.js + Express
+- SQLite(轻量级数据库)
+- bcrypt(密码加密)
+- jsonwebtoken(JWT 认证)
+- multer(文件上传)
+- express-session(会话管理)
+```
+
+### 3.2 前端技术栈
+```
+- HTML5 + TailwindCSS + DaisyUI
+- Vanilla JavaScript(无框架,保持轻量)
+- Fetch API(HTTP 请求)
+```
+
+### 3.3 数据库设计
+
+#### 表结构
+
+**users 表**(管理员用户)
+```sql
+CREATE TABLE users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ username TEXT UNIQUE NOT NULL,
+ password TEXT NOT NULL, -- bcrypt 加密
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+**api_keys 表**(API 密钥)
+```sql
+CREATE TABLE api_keys (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ key TEXT UNIQUE NOT NULL, -- sk-xxx 格式
+ name TEXT, -- 密钥名称/备注
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ last_used_at DATETIME,
+ usage_count INTEGER DEFAULT 0,
+ is_active BOOLEAN DEFAULT 1
+);
+```
+
+**tokens 表**(OpenAI Token 账户)
+```sql
+CREATE TABLE 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,
+ is_active BOOLEAN DEFAULT 1,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+**api_logs 表**(API 调用日志)
+```sql
+CREATE TABLE 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)
+);
+```
+
+## 4. API 接口设计
+
+### 4.1 认证接口
+```
+POST /admin/login # 管理员登录
+POST /admin/logout # 管理员登出
+POST /admin/change-password # 修改密码
+GET /admin/profile # 获取当前用户信息
+```
+
+### 4.2 API Key 管理接口
+```
+GET /admin/api-keys # 获取 API Key 列表
+POST /admin/api-keys # 创建新的 API Key
+DELETE /admin/api-keys/:id # 删除 API Key
+PATCH /admin/api-keys/:id # 更新 API Key(启用/禁用)
+```
+
+### 4.3 Token 管理接口
+```
+GET /admin/tokens # 获取 Token 列表
+POST /admin/tokens/import # 导入 Token JSON 文件
+DELETE /admin/tokens/:id # 删除 Token
+POST /admin/tokens/:id/refresh # 手动刷新 Token
+```
+
+### 4.4 统计接口
+```
+GET /admin/stats/overview # 总览统计
+GET /admin/stats/usage # 使用量统计
+GET /admin/logs # 获取日志
+```
+
+### 4.5 代理接口(需要 API Key 认证)
+```
+POST /v1/chat/completions # OpenAI 兼容接口
+GET /v1/models # 模型列表
+```
+
+## 5. 前端界面设计
+
+### 5.1 布局结构
+```
+┌─────────────────────────────────────────┐
+│ 顶部导航栏(Logo、用户信息、登出) │
+├──────────┬──────────────────────────────┤
+│ │ │
+│ 左侧 │ │
+│ 导航 │ 主内容区域 │
+│ 菜单 │ │
+│ │ │
+│ - 仪表盘│ │
+│ - API Keys │
+│ - Tokens│ │
+│ - 日志 │ │
+│ - 设置 │ │
+│ │ │
+└──────────┴──────────────────────────────┘
+```
+
+### 5.2 页面列表
+1. **登录页面** - 管理员登录
+2. **仪表盘** - 总览统计、快速操作
+3. **API Keys 管理** - 列表、创建、删除
+4. **Tokens 管理** - 列表、导入、删除、刷新
+5. **日志查看** - API 调用日志、错误日志
+6. **设置页面** - 密码修改、系统配置
+
+## 6. 安全设计
+
+### 6.1 认证机制
+- 管理后台使用 JWT Token 认证
+- API 代理使用 API Key 认证
+- 密码使用 bcrypt 加密存储
+
+### 6.2 权限控制
+- 所有 `/admin/*` 接口需要登录认证
+- API Key 验证中间件
+- CORS 配置
+
+### 6.3 安全措施
+- 密码强度验证
+- 登录失败次数限制
+- API Key 格式:`sk-` + 32位随机字符
+- Token 自动刷新机制
+
+## 7. 部署方案
+
+### 7.1 目录结构
+```
+gpt2api-node/
+├── src/
+│ ├── index.js # 主入口
+│ ├── config/
+│ │ └── database.js # 数据库配置
+│ ├── middleware/
+│ │ ├── auth.js # 认证中间件
+│ │ └── apiKey.js # API Key 验证
+│ ├── models/
+│ │ ├── User.js
+│ │ ├── ApiKey.js
+│ │ └── Token.js
+│ ├── routes/
+│ │ ├── admin.js # 管理接口
+│ │ ├── apiKeys.js
+│ │ ├── tokens.js
+│ │ └── proxy.js # 代理接口
+│ ├── services/
+│ │ ├── tokenManager.js # Token 管理服务
+│ │ └── proxyHandler.js # 代理处理服务
+│ └── utils/
+│ ├── crypto.js # 加密工具
+│ └── logger.js # 日志工具
+├── public/
+│ ├── admin/
+│ │ ├── index.html # 管理后台
+│ │ ├── login.html # 登录页
+│ │ ├── css/
+│ │ └── js/
+│ └── assets/
+├── database/
+│ └── app.db # SQLite 数据库
+├── package.json
+└── README.md
+```
+
+### 7.2 环境变量
+```env
+PORT=3000
+JWT_SECRET=your-secret-key
+ADMIN_USERNAME=admin
+ADMIN_PASSWORD=admin123
+DATABASE_PATH=./database/app.db
+```
+
+## 8. 实施计划
+
+### 阶段 1:数据库和认证(核心)
+1. 创建数据库模型
+2. 实现用户认证系统
+3. 创建初始管理员账户
+
+### 阶段 2:API Key 管理
+1. API Key 生成和存储
+2. API Key 验证中间件
+3. API Key 管理接口
+
+### 阶段 3:Token 管理
+1. Token 导入功能
+2. Token 自动刷新
+3. Token 管理接口
+
+### 阶段 4:前端界面
+1. 登录页面
+2. 管理后台布局
+3. 各功能页面实现
+
+### 阶段 5:统计和日志
+1. API 调用日志记录
+2. 统计数据展示
+3. 日志查询功能
+
+## 9. 技术难点和解决方案
+
+### 9.1 多 Token 负载均衡
+**问题**:多个 Token 账户如何分配请求?
+**方案**:
+- 轮询策略
+- 根据 Token 状态(过期时间、使用次数)智能选择
+- 失败自动切换
+
+### 9.2 Token 自动刷新
+**问题**:Token 过期前自动刷新
+**方案**:
+- 定时任务检查即将过期的 Token
+- 请求失败时触发刷新
+- 刷新失败通知管理员
+
+### 9.3 并发请求处理
+**问题**:高并发下的性能
+**方案**:
+- 连接池管理
+- 请求队列
+- 缓存机制
+
+## 10. 后续扩展
+
+- 多用户支持(不同权限级别)
+- API Key 配额限制
+- Webhook 通知
+- 更详细的统计报表
+- Docker 部署支持
+- 集群部署支持
diff --git a/public/admin/index.html b/public/admin/index.html
new file mode 100644
index 0000000..cfef0cb
--- /dev/null
+++ b/public/admin/index.html
@@ -0,0 +1,609 @@
+
+
+
+
+
+ 管理后台 - GPT2API Node
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
API Keys 管理
+
+
+
+
+
+
+
+ | 名称 |
+ Key |
+ 使用次数 |
+ 最后使用 |
+ 状态 |
+ 操作 |
+
+
+
+
+ |
+ 加载中...
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
账号管理 (Tokens)
+
+ 账号总数: 0
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 账号详细统计
+
+
+
+
+
+ | 账号名称 |
+ 请求次数 |
+ 成功率 |
+ 平均响应时间 |
+ 最后使用 |
+
+
+
+
+ |
+ 加载中...
+ |
+
+
+
+
+
+
+
+
+
+ 最近请求日志
+
+
+
+
+
+ | 时间 |
+ API Key |
+ 模型 |
+ 状态 |
+ 响应时间 |
+
+
+
+
+ |
+ 加载中...
+ |
+
+
+
+
+
+
+
+
+
+
+
系统设置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
导入 Token (JSON 文件)
+
+
+
+
+
支持的 JSON 格式:
+
[
+ {
+ "name": "账户名称",
+ "access_token": "sess-xxx",
+ "refresh_token": "xxx",
+ "email": "user@example.com",
+ "account_id": "user-xxx"
+ }
+]
+
+
+
+
+
+
+
+
支持选择多个文件批量导入,或者直接粘贴 JSON 内容到下方文本框
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
修改密码
+
+
+
+
+
+
+
+
diff --git a/public/admin/js/admin.js b/public/admin/js/admin.js
new file mode 100644
index 0000000..cd7eb55
--- /dev/null
+++ b/public/admin/js/admin.js
@@ -0,0 +1,1053 @@
+// 全局变量
+let currentPage = 'dashboard';
+
+// 页面加载时初始化
+document.addEventListener('DOMContentLoaded', async () => {
+ await checkAuth();
+ await loadStats();
+ await loadApiKeys();
+ await loadRecentActivity();
+});
+
+// 检查认证状态
+async function checkAuth() {
+ try {
+ const response = await fetch('/admin/auth/check');
+ if (!response.ok) {
+ window.location.href = '/admin/login.html';
+ }
+ } catch (error) {
+ console.error('认证检查失败:', error);
+ window.location.href = '/admin/login.html';
+ }
+}
+
+// 加载统计数据
+async function loadStats() {
+ try {
+ const response = await fetch('/admin/stats');
+ const data = await response.json();
+
+ document.getElementById('apiKeysCount').textContent = data.apiKeys || 0;
+ document.getElementById('tokensCount').textContent = data.tokens || 0;
+ document.getElementById('todayRequests').textContent = data.todayRequests || 0;
+ document.getElementById('successRate').textContent = (data.successRate || 100) + '%';
+ } catch (error) {
+ console.error('加载统计数据失败:', error);
+ }
+}
+
+// 加载最近活动记录
+async function loadRecentActivity() {
+ try {
+ const response = await fetch('/admin/stats/recent-activity?limit=10');
+ const activities = await response.json();
+
+ const container = document.getElementById('recentActivity');
+
+ if (activities.length === 0) {
+ container.innerHTML = '暂无活动记录
';
+ return;
+ }
+
+ container.innerHTML = activities.map(activity => {
+ const timeAgo = getTimeAgo(activity.time);
+ return `
+
+
+
+
${escapeHtml(activity.title)}
+
${escapeHtml(activity.description)}
+
+
+ ${timeAgo}
+
+
+ `;
+ }).join('');
+ } catch (error) {
+ console.error('加载最近活动失败:', error);
+ }
+}
+
+// 计算时间差
+function getTimeAgo(timestamp) {
+ if (!timestamp) return '未知';
+
+ const now = new Date();
+ const time = new Date(timestamp);
+ const diff = Math.floor((now - time) / 1000); // 秒
+
+ if (diff < 60) return '刚刚';
+ if (diff < 3600) return `${Math.floor(diff / 60)} 分钟前`;
+ if (diff < 86400) return `${Math.floor(diff / 3600)} 小时前`;
+ if (diff < 604800) return `${Math.floor(diff / 86400)} 天前`;
+ return time.toLocaleDateString('zh-CN');
+}
+
+// 切换页面
+function switchPage(event, page) {
+ event.preventDefault();
+ currentPage = page;
+
+ // 更新导航样式
+ document.querySelectorAll('.nav-item').forEach(item => {
+ item.classList.remove('active', 'text-white');
+ item.classList.add('text-gray-700');
+ });
+ event.currentTarget.classList.add('active');
+ event.currentTarget.classList.remove('text-gray-700');
+
+ // 隐藏所有页面
+ document.getElementById('dashboardPage').classList.add('hidden');
+ document.getElementById('apikeysPage').classList.add('hidden');
+ document.getElementById('accountsPage').classList.add('hidden');
+ document.getElementById('analyticsPage').classList.add('hidden');
+ document.getElementById('settingsPage').classList.add('hidden');
+
+ // 更新页面标题
+ const titles = {
+ dashboard: { title: '仪表盘', desc: '系统概览和实时数据' },
+ apikeys: { title: 'API Keys', desc: 'API 密钥管理' },
+ accounts: { title: '账号管理', desc: 'Tokens 账户管理' },
+ analytics: { title: '数据分析', desc: 'API 请求统计和分析' },
+ settings: { title: '系统设置', desc: '系统配置和偏好设置' }
+ };
+
+ document.getElementById('pageTitle').textContent = titles[page].title;
+ document.getElementById('pageDesc').textContent = titles[page].desc;
+
+ // 显示对应页面
+ if (page === 'dashboard') {
+ document.getElementById('dashboardPage').classList.remove('hidden');
+ } else if (page === 'apikeys') {
+ document.getElementById('apikeysPage').classList.remove('hidden');
+ loadApiKeys();
+ } else if (page === 'accounts') {
+ document.getElementById('accountsPage').classList.remove('hidden');
+ loadTokens();
+ loadLoadBalanceStrategy();
+ } else if (page === 'analytics') {
+ document.getElementById('analyticsPage').classList.remove('hidden');
+ loadAnalytics();
+ } else if (page === 'settings') {
+ document.getElementById('settingsPage').classList.remove('hidden');
+ }
+}
+
+// 切换账号管理标签
+// 已移除,不再需要
+
+// ==================== API Keys 管理 ====================
+
+async function loadApiKeys() {
+ try {
+ const response = await fetch('/admin/api-keys');
+ const data = await response.json();
+
+ const tbody = document.getElementById('apiKeysTable');
+
+ if (data.length === 0) {
+ tbody.innerHTML = '| 暂无 API Key |
';
+ return;
+ }
+
+ tbody.innerHTML = data.map(key => `
+
+ | ${escapeHtml(key.name || '-')} |
+
+ ${escapeHtml(key.key.substring(0, 20))}...
+
+ |
+ ${key.usage_count || 0} |
+ ${key.last_used_at ? new Date(key.last_used_at).toLocaleString('zh-CN') : '-'} |
+
+
+ ${key.is_active ? '启用' : '禁用'}
+
+ |
+
+
+
+ |
+
+ `).join('');
+ } catch (error) {
+ console.error('加载 API Keys 失败:', error);
+ }
+}
+
+function showCreateApiKeyModal() {
+ document.getElementById('createApiKeyModal').classList.remove('hidden');
+}
+
+async function handleCreateApiKey(event) {
+ event.preventDefault();
+
+ const name = document.getElementById('apiKeyName').value;
+
+ try {
+ const response = await fetch('/admin/api-keys', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name })
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ document.getElementById('createApiKeyModal').classList.add('hidden');
+ document.getElementById('apiKeyName').value = '';
+ alert('API Key 创建成功!\n\n' + data.key + '\n\n请妥善保存,此 Key 不会再次显示!');
+ await loadApiKeys();
+ await loadStats();
+ } else {
+ alert('创建失败: ' + (data.error || '未知错误'));
+ }
+ } catch (error) {
+ alert('创建失败: ' + error.message);
+ }
+}
+
+async function toggleApiKey(id, currentStatus) {
+ try {
+ const response = await fetch(`/admin/api-keys/${id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ is_active: !currentStatus })
+ });
+
+ if (response.ok) {
+ await loadApiKeys();
+ await loadStats();
+ }
+ } catch (error) {
+ alert('操作失败: ' + error.message);
+ }
+}
+
+async function deleteApiKey(id) {
+ if (!confirm('确定要删除此 API Key 吗?')) return;
+
+ try {
+ const response = await fetch(`/admin/api-keys/${id}`, { method: 'DELETE' });
+ if (response.ok) {
+ await loadApiKeys();
+ await loadStats();
+ }
+ } catch (error) {
+ alert('删除失败: ' + error.message);
+ }
+}
+
+// ==================== Tokens 管理 ====================
+
+let currentTokenPage = 1;
+let tokenPageSize = 20;
+let totalTokens = 0;
+let selectedTokens = new Set();
+
+async function loadTokens(page = 1) {
+ try {
+ currentTokenPage = page;
+ selectedTokens.clear();
+ updateBatchDeleteButton();
+
+ const response = await fetch(`/admin/tokens?page=${page}&limit=${tokenPageSize}`);
+ const result = await response.json();
+
+ const data = result.data || [];
+ const pagination = result.pagination || {};
+ totalTokens = pagination.total || 0;
+
+ // 更新账号总数显示
+ const totalCountEl = document.getElementById('totalTokensCount');
+ if (totalCountEl) {
+ totalCountEl.textContent = totalTokens;
+ }
+
+ const tbody = document.getElementById('tokensTable');
+
+ if (data.length === 0) {
+ tbody.innerHTML = '| 暂无 Token |
';
+ updateTokenPagination(0, 0);
+ return;
+ }
+
+ tbody.innerHTML = data.map(token => {
+ // 计算额度百分比
+ const quotaTotal = token.quota_total || 0;
+ const quotaUsed = token.quota_used || 0;
+ const quotaRemaining = token.quota_remaining || 0;
+ const quotaPercent = quotaTotal > 0 ? Math.round((quotaUsed / quotaTotal) * 100) : 0;
+
+ // 额度显示颜色
+ let quotaColor = 'text-green-600';
+ if (quotaPercent > 80) quotaColor = 'text-red-600';
+ else if (quotaPercent > 50) quotaColor = 'text-yellow-600';
+
+ // 额度显示文本
+ let quotaText = '-';
+ if (quotaTotal > 0) {
+ quotaText = `
+
${quotaRemaining.toLocaleString()} / ${quotaTotal.toLocaleString()}
+
${quotaPercent}% 已用
+
`;
+ }
+
+ return `
+
+ |
+
+ |
+ ${escapeHtml(token.name || '-')} |
+ ${quotaText} |
+ ${token.total_requests || 0} |
+ ${token.success_requests || 0} |
+ ${token.failed_requests || 0} |
+ ${token.expired_at ? new Date(token.expired_at).toLocaleString('zh-CN') : '-'} |
+
+
+ ${token.is_active ? '启用' : '禁用'}
+
+ |
+
+
+
+
+ |
+
+ `;
+ }).join('');
+
+ updateTokenPagination(pagination.page, pagination.totalPages);
+ } catch (error) {
+ console.error('加载 Tokens 失败:', error);
+ }
+}
+
+function updateTokenPagination(currentPage, totalPages) {
+ const paginationEl = document.getElementById('tokenPagination');
+ if (!paginationEl) return;
+
+ if (totalPages <= 1) {
+ paginationEl.innerHTML = '';
+ return;
+ }
+
+ let html = '';
+ html += `
共 ${totalTokens} 个账号,第 ${currentPage}/${totalPages} 页
`;
+ html += '
';
+
+ // 上一页
+ if (currentPage > 1) {
+ html += ``;
+ } else {
+ html += ``;
+ }
+
+ // 页码
+ const maxPages = 5;
+ let startPage = Math.max(1, currentPage - Math.floor(maxPages / 2));
+ let endPage = Math.min(totalPages, startPage + maxPages - 1);
+
+ if (endPage - startPage < maxPages - 1) {
+ startPage = Math.max(1, endPage - maxPages + 1);
+ }
+
+ if (startPage > 1) {
+ html += ``;
+ if (startPage > 2) {
+ html += `...`;
+ }
+ }
+
+ for (let i = startPage; i <= endPage; i++) {
+ if (i === currentPage) {
+ html += ``;
+ } else {
+ html += ``;
+ }
+ }
+
+ if (endPage < totalPages) {
+ if (endPage < totalPages - 1) {
+ html += `...`;
+ }
+ html += ``;
+ }
+
+ // 下一页
+ if (currentPage < totalPages) {
+ html += ``;
+ } else {
+ html += ``;
+ }
+
+ html += '
';
+ paginationEl.innerHTML = html;
+}
+
+function showCreateTokenModal() {
+ document.getElementById('createTokenModal').classList.remove('hidden');
+}
+
+function showImportTokenModal() {
+ document.getElementById('importTokenModal').classList.remove('hidden');
+
+ // 监听文件选择
+ document.getElementById('tokenFileInput').addEventListener('change', handleFileSelect);
+}
+
+function closeImportModal() {
+ document.getElementById('importTokenModal').classList.add('hidden');
+ document.getElementById('tokenFileInput').value = '';
+ document.getElementById('tokenJsonContent').value = '';
+ document.getElementById('importPreview').classList.add('hidden');
+}
+
+function handleFileSelect(event) {
+ const files = event.target.files;
+ if (!files || files.length === 0) return;
+
+ // 如果只有一个文件,直接读取
+ if (files.length === 1) {
+ const reader = new FileReader();
+ reader.onload = function(e) {
+ document.getElementById('tokenJsonContent').value = e.target.result;
+ };
+ reader.onerror = function(e) {
+ alert('文件读取失败: ' + e.target.error);
+ };
+ reader.readAsText(files[0]);
+ return;
+ }
+
+ // 多个文件,合并成数组
+ let allTokens = [];
+ let filesRead = 0;
+ const totalFiles = files.length;
+
+ console.log(`开始读取 ${totalFiles} 个文件...`);
+
+ Array.from(files).forEach((file, index) => {
+ const reader = new FileReader();
+ reader.onload = function(e) {
+ try {
+ console.log(`读取文件 ${index + 1}/${totalFiles}: ${file.name}`);
+ const data = JSON.parse(e.target.result);
+ // 如果是数组,展开;如果是对象,作为单个元素
+ if (Array.isArray(data)) {
+ allTokens = allTokens.concat(data);
+ console.log(`文件 ${file.name} 包含 ${data.length} 个 token`);
+ } else {
+ allTokens.push(data);
+ console.log(`文件 ${file.name} 包含 1 个 token`);
+ }
+ } catch (error) {
+ console.error(`文件 ${file.name} 解析失败:`, error);
+ alert(`文件 ${file.name} 解析失败: ${error.message}`);
+ }
+
+ filesRead++;
+ // 所有文件都读取完成后,更新文本框
+ if (filesRead === totalFiles) {
+ console.log(`所有文件读取完成,共 ${allTokens.length} 个 token`);
+ document.getElementById('tokenJsonContent').value = JSON.stringify(allTokens, null, 2);
+ }
+ };
+ reader.onerror = function(e) {
+ console.error(`文件 ${file.name} 读取失败:`, e.target.error);
+ alert(`文件 ${file.name} 读取失败`);
+ filesRead++;
+ if (filesRead === totalFiles && allTokens.length > 0) {
+ document.getElementById('tokenJsonContent').value = JSON.stringify(allTokens, null, 2);
+ }
+ };
+ reader.readAsText(file);
+ });
+}
+
+let importData = null;
+
+function previewImport() {
+ const jsonContent = document.getElementById('tokenJsonContent').value.trim();
+
+ if (!jsonContent) {
+ alert('请先选择文件或粘贴 JSON 内容');
+ return;
+ }
+
+ try {
+ importData = JSON.parse(jsonContent);
+
+ if (!Array.isArray(importData)) {
+ importData = [importData];
+ }
+
+ // 验证数据格式
+ const validTokens = importData.filter(token => {
+ return token.access_token && token.refresh_token;
+ });
+
+ if (validTokens.length === 0) {
+ alert('JSON 格式错误:未找到有效的 token 数据\n\n每个 token 必须包含 access_token 和 refresh_token 字段');
+ return;
+ }
+
+ // 显示预览
+ document.getElementById('importCount').textContent = validTokens.length;
+ const listEl = document.getElementById('importList');
+ listEl.innerHTML = validTokens.map((token, index) => `
+
+
+ ${index + 1}. ${escapeHtml(token.name || token.email || token.account_id || 'Token ' + (index + 1))}
+
+ `).join('');
+
+ document.getElementById('importPreview').classList.remove('hidden');
+ importData = validTokens;
+
+ } catch (error) {
+ alert('JSON 解析失败:' + error.message);
+ }
+}
+
+async function handleImportTokens() {
+ if (!importData || importData.length === 0) {
+ alert('请先预览导入数据');
+ return;
+ }
+
+ if (!confirm(`确定要导入 ${importData.length} 个账户吗?`)) {
+ return;
+ }
+
+ try {
+ const response = await fetch('/admin/tokens/import', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ tokens: importData })
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ alert(`导入成功!\n成功:${data.success || 0} 个\n失败:${data.failed || 0} 个`);
+ closeImportModal();
+ await loadTokens();
+ await loadStats();
+ } else {
+ alert('导入失败: ' + (data.error || '未知错误'));
+ }
+ } catch (error) {
+ alert('导入失败: ' + error.message);
+ }
+}
+
+async function handleCreateToken(event) {
+ event.preventDefault();
+
+ const name = document.getElementById('tokenName').value;
+ const access_token = document.getElementById('accessToken').value;
+ const refresh_token = document.getElementById('refreshToken').value;
+
+ try {
+ const response = await fetch('/admin/tokens', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name, access_token, refresh_token })
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ document.getElementById('createTokenModal').classList.add('hidden');
+ document.getElementById('tokenName').value = '';
+ document.getElementById('accessToken').value = '';
+ document.getElementById('refreshToken').value = '';
+ alert('Token 添加成功!');
+ await loadTokens();
+ await loadStats();
+ } else {
+ alert('添加失败: ' + (data.error || '未知错误'));
+ }
+ } catch (error) {
+ alert('添加失败: ' + error.message);
+ }
+}
+
+async function toggleToken(id, currentStatus) {
+ try {
+ const response = await fetch(`/admin/tokens/${id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ is_active: !currentStatus })
+ });
+
+ if (response.ok) {
+ await loadTokens();
+ await loadStats();
+ }
+ } catch (error) {
+ alert('操作失败: ' + error.message);
+ }
+}
+
+async function deleteToken(id) {
+ if (!confirm('确定要删除此 Token 吗?')) return;
+
+ try {
+ const response = await fetch(`/admin/tokens/${id}`, { method: 'DELETE' });
+ if (response.ok) {
+ await loadTokens(currentTokenPage);
+ await loadStats();
+ }
+ } catch (error) {
+ alert('删除失败: ' + error.message);
+ }
+}
+
+async function refreshTokenQuota(id) {
+ try {
+ const response = await fetch(`/admin/tokens/${id}/quota`, { method: 'POST' });
+ const data = await response.json();
+
+ if (response.ok) {
+ await loadTokens(currentTokenPage);
+ if (data.quota) {
+ alert(`额度已更新\n总额度: ${data.quota.total.toLocaleString()}\n已使用: ${data.quota.used.toLocaleString()}\n剩余: ${data.quota.remaining.toLocaleString()}`);
+ }
+ } else {
+ alert('刷新额度失败: ' + (data.error || '未知错误'));
+ }
+ } catch (error) {
+ alert('刷新额度失败: ' + error.message);
+ }
+}
+
+async function refreshAllQuotas() {
+ if (!confirm('确定要刷新所有账号的额度吗?这可能需要一些时间。')) {
+ return;
+ }
+
+ try {
+ const response = await fetch('/admin/tokens/quota/refresh-all', { method: 'POST' });
+ const data = await response.json();
+
+ if (response.ok) {
+ await loadTokens(currentTokenPage);
+ alert(`批量刷新完成\n成功: ${data.success || 0} 个\n失败: ${data.failed || 0} 个`);
+ } else {
+ alert('批量刷新失败: ' + (data.error || '未知错误'));
+ }
+ } catch (error) {
+ alert('批量刷新失败: ' + error.message);
+ }
+}
+
+// ==================== 批量删除功能 ====================
+
+function toggleTokenSelection(id) {
+ if (selectedTokens.has(id)) {
+ selectedTokens.delete(id);
+ } else {
+ selectedTokens.add(id);
+ }
+ updateBatchDeleteButton();
+ updateSelectAllCheckbox();
+}
+
+function toggleSelectAll() {
+ const checkbox = document.getElementById('selectAllTokens');
+ const checkboxes = document.querySelectorAll('.token-checkbox');
+
+ if (checkbox.checked) {
+ checkboxes.forEach(cb => {
+ const id = parseInt(cb.value);
+ selectedTokens.add(id);
+ cb.checked = true;
+ });
+ } else {
+ selectedTokens.clear();
+ checkboxes.forEach(cb => {
+ cb.checked = false;
+ });
+ }
+
+ updateBatchDeleteButton();
+}
+
+function updateSelectAllCheckbox() {
+ const checkbox = document.getElementById('selectAllTokens');
+ const checkboxes = document.querySelectorAll('.token-checkbox');
+
+ if (checkboxes.length === 0) {
+ checkbox.checked = false;
+ return;
+ }
+
+ const allChecked = Array.from(checkboxes).every(cb => cb.checked);
+ checkbox.checked = allChecked;
+}
+
+function updateBatchDeleteButton() {
+ const btn = document.getElementById('batchDeleteBtn');
+ const countSpan = document.getElementById('selectedCount');
+
+ if (selectedTokens.size > 0) {
+ btn.classList.remove('hidden');
+ countSpan.textContent = selectedTokens.size;
+ } else {
+ btn.classList.add('hidden');
+ }
+}
+
+async function batchDeleteTokens() {
+ if (selectedTokens.size === 0) {
+ alert('请先选择要删除的账号');
+ return;
+ }
+
+ if (!confirm(`确定要删除选中的 ${selectedTokens.size} 个账号吗?此操作不可恢复!`)) {
+ return;
+ }
+
+ try {
+ const ids = Array.from(selectedTokens);
+ const response = await fetch('/admin/tokens/batch-delete', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ ids })
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ alert(`批量删除完成\n成功: ${data.success || 0} 个\n失败: ${data.failed || 0} 个`);
+ selectedTokens.clear();
+ await loadTokens(currentTokenPage);
+ await loadStats();
+ } else {
+ alert('批量删除失败: ' + (data.error || '未知错误'));
+ }
+ } catch (error) {
+ alert('批量删除失败: ' + error.message);
+ }
+}
+
+// ==================== 日志管理 ====================
+
+async function loadAnalytics() {
+ // 加载统计数据
+ await loadAnalyticsStats();
+ // 加载图表
+ await loadCharts();
+ // 加载模型统计
+ await loadModelStats();
+ // 加载日志
+ await loadLogs();
+}
+
+let currentTimeRange = '24h';
+
+function changeTimeRange(range) {
+ currentTimeRange = range;
+
+ // 更新按钮样式
+ document.querySelectorAll('.time-range-btn').forEach(btn => {
+ btn.classList.remove('bg-blue-500', 'text-white');
+ btn.classList.add('text-gray-700', 'hover:bg-gray-100');
+ });
+ event.target.classList.add('bg-blue-500', 'text-white');
+ event.target.classList.remove('text-gray-700', 'hover:bg-gray-100');
+
+ // 重新加载数据
+ loadAnalytics();
+}
+
+async function loadAnalyticsStats() {
+ try {
+ const response = await fetch(`/admin/stats/analytics?range=${currentTimeRange}`);
+ const data = await response.json();
+
+ document.getElementById('totalRequests').textContent = data.totalRequests || 0;
+ document.getElementById('successRequests').textContent = data.successRequests || 0;
+ document.getElementById('failedRequests').textContent = data.failedRequests || 0;
+ document.getElementById('avgResponseTime').textContent = (data.avgResponseTime || 0) + 'ms';
+ } catch (error) {
+ console.error('加载统计数据失败:', error);
+ }
+}
+
+let requestTrendChart = null;
+let modelDistributionChart = null;
+
+async function loadCharts() {
+ try {
+ const response = await fetch(`/admin/stats/charts?range=${currentTimeRange}`);
+ const data = await response.json();
+
+ // 请求量趋势图
+ const trendCtx = document.getElementById('requestTrendChart').getContext('2d');
+ if (requestTrendChart) {
+ requestTrendChart.destroy();
+ }
+ requestTrendChart = new Chart(trendCtx, {
+ type: 'line',
+ data: {
+ labels: data.trendLabels || [],
+ datasets: [{
+ label: '请求数',
+ data: data.trendData || [],
+ borderColor: '#3b82f6',
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
+ tension: 0.4
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ display: false
+ }
+ },
+ scales: {
+ y: {
+ beginAtZero: true
+ }
+ }
+ }
+ });
+
+ // 模型使用分布饼图
+ const distCtx = document.getElementById('modelDistributionChart').getContext('2d');
+ if (modelDistributionChart) {
+ modelDistributionChart.destroy();
+ }
+ modelDistributionChart = new Chart(distCtx, {
+ type: 'pie',
+ data: {
+ labels: data.modelLabels || [],
+ datasets: [{
+ data: data.modelData || [],
+ backgroundColor: [
+ '#3b82f6',
+ '#10b981',
+ '#f59e0b',
+ '#ef4444',
+ '#8b5cf6',
+ '#ec4899'
+ ]
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ position: 'right'
+ }
+ }
+ }
+ });
+ } catch (error) {
+ console.error('加载图表失败:', error);
+ }
+}
+
+async function loadModelStats() {
+ try {
+ const response = await fetch(`/admin/stats/accounts?range=${currentTimeRange}`);
+ const data = await response.json();
+
+ const tbody = document.getElementById('accountStatsTable');
+
+ if (data.length === 0) {
+ tbody.innerHTML = '| 暂无数据 |
';
+ return;
+ }
+
+ tbody.innerHTML = data.map(account => `
+
+ | ${escapeHtml(account.name)} |
+ ${account.requests} |
+
+
+ ${account.successRate}%
+
+ |
+ ${account.avgResponseTime}ms |
+ ${account.lastUsed ? new Date(account.lastUsed).toLocaleString('zh-CN') : '-'} |
+
+ `).join('');
+ } catch (error) {
+ console.error('加载账号统计失败:', error);
+ }
+}
+
+async function loadLogs() {
+ try {
+ const response = await fetch(`/admin/stats/logs?limit=50&range=${currentTimeRange}`);
+ const data = await response.json();
+
+ const tbody = document.getElementById('logsTable');
+
+ if (data.length === 0) {
+ tbody.innerHTML = '| 暂无日志 |
';
+ return;
+ }
+
+ tbody.innerHTML = data.map(log => `
+
+ | ${new Date(log.created_at).toLocaleString('zh-CN')} |
+ ${log.api_key_name || log.api_key_id || '-'} |
+ ${escapeHtml(log.model || '-')} |
+
+
+ ${log.status_code}
+
+ |
+ ${log.response_time || '-'}ms |
+
+ `).join('');
+ } catch (error) {
+ console.error('加载日志失败:', error);
+ }
+}
+
+// ==================== 工具函数 ====================
+
+async function handleLogout() {
+ if (!confirm('确定要退出登录吗?')) return;
+
+ try {
+ await fetch('/admin/auth/logout', { method: 'POST' });
+ window.location.href = '/admin/login.html';
+ } catch (error) {
+ window.location.href = '/admin/login.html';
+ }
+}
+
+function copyToClipboard(text) {
+ navigator.clipboard.writeText(text).then(() => {
+ alert('已复制到剪贴板!');
+ }).catch(() => {
+ alert('复制失败,请手动复制');
+ });
+}
+
+// ==================== 负载均衡策略管理 ====================
+
+async function loadLoadBalanceStrategy() {
+ try {
+ const response = await fetch('/admin/settings/load-balance-strategy');
+ const data = await response.json();
+
+ const select = document.getElementById('loadBalanceStrategy');
+ if (select && data.strategy) {
+ select.value = data.strategy;
+ }
+ } catch (error) {
+ console.error('加载负载均衡策略失败:', error);
+ }
+}
+
+async function changeLoadBalanceStrategy() {
+ const select = document.getElementById('loadBalanceStrategy');
+ const strategy = select.value;
+
+ try {
+ const response = await fetch('/admin/settings/load-balance-strategy', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ strategy })
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ alert('负载均衡策略已更新为:' + (strategy === 'round-robin' ? '轮询' : strategy === 'random' ? '随机' : '最少使用'));
+ } else {
+ alert('更新失败: ' + (data.error || '未知错误'));
+ }
+ } catch (error) {
+ alert('更新失败: ' + error.message);
+ }
+}
+
+// ==================== 修改密码 ====================
+
+function showChangePasswordModal() {
+ document.getElementById('changePasswordModal').classList.remove('hidden');
+}
+
+function closeChangePasswordModal() {
+ document.getElementById('changePasswordModal').classList.add('hidden');
+ document.getElementById('currentPassword').value = '';
+ document.getElementById('newPassword').value = '';
+ document.getElementById('confirmPassword').value = '';
+}
+
+async function handleChangePassword(event) {
+ event.preventDefault();
+
+ const currentPassword = document.getElementById('currentPassword').value;
+ const newPassword = document.getElementById('newPassword').value;
+ const confirmPassword = document.getElementById('confirmPassword').value;
+
+ if (newPassword !== confirmPassword) {
+ alert('两次输入的新密码不一致');
+ return;
+ }
+
+ if (newPassword.length < 6) {
+ alert('密码长度至少 6 位');
+ return;
+ }
+
+ try {
+ const response = await fetch('/admin/auth/change-password', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ oldPassword: currentPassword, newPassword })
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ alert('密码修改成功,请重新登录');
+ closeChangePasswordModal();
+ window.location.href = '/admin/login.html';
+ } else {
+ alert('修改失败: ' + (data.error || '未知错误'));
+ }
+ } catch (error) {
+ alert('修改失败: ' + error.message);
+ }
+}
+
+// ==================== 工具函数 ====================
+
+function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
diff --git a/public/admin/login.html b/public/admin/login.html
new file mode 100644
index 0000000..8b62921
--- /dev/null
+++ b/public/admin/login.html
@@ -0,0 +1,155 @@
+
+
+
+
+
+ 管理员登录 - GPT2API Node
+
+
+
+
+
+
+
+
+
+
+
+
管理员登录
+
GPT2API Node 管理系统
+
+
+
+
+
+
+
+
+
+
+ 默认账户: admin / admin123
+
+
+ 首次登录后请立即修改密码
+
+
+
+
+
+
+
+
diff --git a/public/js/app.js b/public/js/app.js
new file mode 100644
index 0000000..9fc334f
--- /dev/null
+++ b/public/js/app.js
@@ -0,0 +1,238 @@
+// 全局变量
+let messages = [];
+let currentModel = 'gpt-5.3-codex';
+
+// 页面加载时初始化
+document.addEventListener('DOMContentLoaded', async () => {
+ await loadStatus();
+ await loadModels();
+});
+
+// 加载服务状态
+async function loadStatus() {
+ try {
+ const response = await fetch('/health');
+ const data = await response.json();
+
+ if (data.status === 'ok') {
+ document.getElementById('serviceStatus').textContent = '运行中';
+ document.getElementById('accountEmail').textContent = data.token.email || data.token.account_id || '未知';
+
+ if (data.token.expired) {
+ const expireDate = new Date(data.token.expired);
+ document.getElementById('tokenExpire').textContent = expireDate.toLocaleString('zh-CN');
+ }
+ }
+ } catch (error) {
+ console.error('加载状态失败:', error);
+ document.getElementById('serviceStatus').textContent = '离线';
+ document.getElementById('serviceStatus').classList.remove('text-primary');
+ document.getElementById('serviceStatus').classList.add('text-error');
+ }
+}
+
+// 加载模型列表
+async function loadModels() {
+ try {
+ const response = await fetch('/v1/models');
+ const data = await response.json();
+
+ const select = document.getElementById('modelSelect');
+ select.innerHTML = '';
+
+ data.data.forEach(model => {
+ const option = document.createElement('option');
+ option.value = model.id;
+ option.textContent = model.id;
+ select.appendChild(option);
+ });
+
+ if (data.data.length > 0) {
+ currentModel = data.data[0].id;
+ select.value = currentModel;
+ }
+
+ select.addEventListener('change', (e) => {
+ currentModel = e.target.value;
+ });
+ } catch (error) {
+ console.error('加载模型失败:', error);
+ }
+}
+
+// 发送消息
+async function sendMessage() {
+ const input = document.getElementById('messageInput');
+ const message = input.value.trim();
+
+ if (!message) return;
+
+ // 添加用户消息
+ messages.push({ role: 'user', content: message });
+ appendMessage('user', message);
+ input.value = '';
+
+ // 显示加载状态
+ const loadingId = appendMessage('assistant', '思考中...', true);
+
+ try {
+ const response = await fetch('/v1/chat/completions', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ model: currentModel,
+ messages: messages,
+ stream: true
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ // 移除加载消息
+ document.getElementById(loadingId).remove();
+
+ // 处理流式响应
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let assistantMessage = '';
+ let messageId = null;
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ const chunk = decoder.decode(value);
+ const lines = chunk.split('\n');
+
+ for (const line of lines) {
+ if (line.startsWith('data: ')) {
+ const data = line.slice(6);
+ if (data === '[DONE]') continue;
+
+ try {
+ const json = JSON.parse(data);
+ const content = json.choices[0]?.delta?.content;
+
+ if (content) {
+ assistantMessage += content;
+
+ if (!messageId) {
+ messageId = appendMessage('assistant', assistantMessage);
+ } else {
+ updateMessage(messageId, assistantMessage);
+ }
+ }
+ } catch (e) {
+ // 忽略解析错误
+ }
+ }
+ }
+ }
+
+ // 保存助手消息
+ if (assistantMessage) {
+ messages.push({ role: 'assistant', content: assistantMessage });
+ }
+
+ } catch (error) {
+ console.error('发送消息失败:', error);
+ document.getElementById(loadingId).remove();
+ appendMessage('system', '错误: ' + error.message);
+ }
+}
+
+// 添加消息到聊天区域
+function appendMessage(role, content, isLoading = false) {
+ const container = document.getElementById('chatMessages');
+
+ // 首次添加消息时清除欢迎文本
+ if (container.children.length === 1 && container.children[0].classList.contains('text-center')) {
+ container.innerHTML = '';
+ }
+
+ const messageId = 'msg-' + Date.now() + '-' + Math.random();
+ const messageDiv = document.createElement('div');
+ messageDiv.id = messageId;
+ messageDiv.className = 'chat chat-message ' + (role === 'user' ? 'chat-end' : 'chat-start');
+
+ let avatarClass = 'bg-primary';
+ let avatarText = 'U';
+
+ if (role === 'assistant') {
+ avatarClass = 'bg-secondary';
+ avatarText = 'AI';
+ } else if (role === 'system') {
+ avatarClass = 'bg-error';
+ avatarText = '!';
+ }
+
+ messageDiv.innerHTML = `
+
+
+ ${isLoading ? '' : escapeHtml(content)}
+
+ `;
+
+ container.appendChild(messageDiv);
+ container.scrollTop = container.scrollHeight;
+
+ return messageId;
+}
+
+// 更新消息内容
+function updateMessage(messageId, content) {
+ const messageDiv = document.getElementById(messageId);
+ if (messageDiv) {
+ const bubble = messageDiv.querySelector('.chat-bubble');
+ bubble.textContent = content;
+ }
+
+ const container = document.getElementById('chatMessages');
+ container.scrollTop = container.scrollHeight;
+}
+
+// 清空聊天
+function clearChat() {
+ messages = [];
+ const container = document.getElementById('chatMessages');
+ container.innerHTML = '开始对话吧!
';
+}
+
+// HTML 转义
+function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
+// 显示设置
+function showSettings() {
+ alert('设置功能开发中...');
+}
+
+// 显示状态
+async function showStatus() {
+ await loadStatus();
+ alert('状态已刷新!');
+}
+
+// 显示模型列表
+async function showModels() {
+ try {
+ const response = await fetch('/v1/models');
+ const data = await response.json();
+
+ const modelList = data.data.map(m => m.id).join('\n');
+ alert('可用模型:\n\n' + modelList);
+ } catch (error) {
+ alert('获取模型列表失败: ' + error.message);
+ }
+}
diff --git a/screenshots/API keys.png b/screenshots/API keys.png
new file mode 100644
index 0000000..1c29e47
Binary files /dev/null and b/screenshots/API keys.png differ
diff --git a/screenshots/仪表盘.png b/screenshots/仪表盘.png
new file mode 100644
index 0000000..0c6a35a
Binary files /dev/null and b/screenshots/仪表盘.png differ
diff --git a/screenshots/数据分析.png b/screenshots/数据分析.png
new file mode 100644
index 0000000..bc6978d
Binary files /dev/null and b/screenshots/数据分析.png differ
diff --git a/screenshots/管理员登录.png b/screenshots/管理员登录.png
new file mode 100644
index 0000000..e49d0ff
Binary files /dev/null and b/screenshots/管理员登录.png differ
diff --git a/screenshots/系统设置.png b/screenshots/系统设置.png
new file mode 100644
index 0000000..0cc2b0a
Binary files /dev/null and b/screenshots/系统设置.png differ
diff --git a/screenshots/账号管理.png b/screenshots/账号管理.png
new file mode 100644
index 0000000..56b0055
Binary files /dev/null and b/screenshots/账号管理.png differ
diff --git a/src/config/database.js b/src/config/database.js
new file mode 100644
index 0000000..e6871bd
--- /dev/null
+++ b/src/config/database.js
@@ -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;
diff --git a/src/index.js b/src/index.js
index 6ce9f84..8c1acfc 100644
--- a/src/index.js
+++ b/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`);
});
diff --git a/src/middleware/auth.js b/src/middleware/auth.js
new file mode 100644
index 0000000..6f1c8a8
--- /dev/null
+++ b/src/middleware/auth.js
@@ -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;
+}
diff --git a/src/models/index.js b/src/models/index.js
new file mode 100644
index 0000000..88b50e7
--- /dev/null
+++ b/src/models/index.js
@@ -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
+ };
+ }
+}
diff --git a/src/proxyHandler.js b/src/proxyHandler.js
index 27a003e..668c6a4 100644
--- a/src/proxyHandler.js
+++ b/src/proxyHandler.js
@@ -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();
diff --git a/src/routes/apiKeys.js b/src/routes/apiKeys.js
new file mode 100644
index 0000000..92831cd
--- /dev/null
+++ b/src/routes/apiKeys.js
@@ -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;
diff --git a/src/routes/auth.js b/src/routes/auth.js
new file mode 100644
index 0000000..57ff602
--- /dev/null
+++ b/src/routes/auth.js
@@ -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;
diff --git a/src/routes/settings.js b/src/routes/settings.js
new file mode 100644
index 0000000..aebfcb3
--- /dev/null
+++ b/src/routes/settings.js
@@ -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;
diff --git a/src/routes/stats.js b/src/routes/stats.js
new file mode 100644
index 0000000..27fa3b3
--- /dev/null
+++ b/src/routes/stats.js
@@ -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;
diff --git a/src/routes/tokens.js b/src/routes/tokens.js
new file mode 100644
index 0000000..bb9fa0f
--- /dev/null
+++ b/src/routes/tokens.js
@@ -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;
diff --git a/src/scripts/initDatabase.js b/src/scripts/initDatabase.js
new file mode 100644
index 0000000..e7bb51f
--- /dev/null
+++ b/src/scripts/initDatabase.js
@@ -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);
+}
diff --git a/src/tokenManager.js b/src/tokenManager.js
index e40efbd..e6b5cdf 100644
--- a/src/tokenManager.js
+++ b/src/tokenManager.js
@@ -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
};
}