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管理 +

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 + + + + + + +
+ + + + +
+ +
+
+
+

仪表盘

+

系统概览和实时数据

+
+
+
+ + +
+ +
+
+
+
+ +
+ 0 +
+

API Keys

+

活跃密钥数量

+
+ +
+
+
+ +
+ 0 +
+

Tokens

+

账户令牌数量

+
+ +
+
+
+ +
+ 0 +
+

今日请求

+

API 调用次数

+
+ +
+
+
+ +
+ 100% +
+

成功率

+

请求成功比例

+
+
+ + +
+

+ 最近活动 +

+
+
+ 暂无活动记录 +
+
+
+
+ + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + 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 = ` +
    +
    + ${avatarText} +
    +
    +
    + ${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 }; }